0.6.0 - Redesign, downloads, tagging fixes, download quality selector...

This commit is contained in:
exttex 2020-10-19 21:28:45 +02:00
parent bcf709e56d
commit 1384aedb35
28 changed files with 1201 additions and 878 deletions

View File

@ -8,11 +8,13 @@ import org.jaudiotagger.audio.AudioFileIO;
import org.jaudiotagger.tag.FieldKey; import org.jaudiotagger.tag.FieldKey;
import org.jaudiotagger.tag.Tag; import org.jaudiotagger.tag.Tag;
import org.jaudiotagger.tag.TagOptionSingleton; import org.jaudiotagger.tag.TagOptionSingleton;
import org.jaudiotagger.tag.datatype.Artwork;
import org.jaudiotagger.tag.flac.FlacTag; import org.jaudiotagger.tag.flac.FlacTag;
import org.jaudiotagger.tag.id3.ID3v23Tag; import org.jaudiotagger.tag.id3.ID3v23Tag;
import org.jaudiotagger.tag.id3.valuepair.ImageFormats; import org.jaudiotagger.tag.id3.valuepair.ImageFormats;
import org.jaudiotagger.tag.images.Artwork;
import org.jaudiotagger.tag.images.ArtworkFactory;
import org.jaudiotagger.tag.reference.PictureTypes; import org.jaudiotagger.tag.reference.PictureTypes;
import org.jaudiotagger.tag.vorbiscomment.VorbisCommentFieldKey;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
@ -119,6 +121,12 @@ public class Deezer {
data += scanner.nextLine(); data += scanner.nextLine();
} }
//End
try {
connection.disconnect();
scanner.close();
} catch (Exception e) {}
//Parse JSON //Parse JSON
JSONObject out = new JSONObject(data); JSONObject out = new JSONObject(data);
@ -156,6 +164,12 @@ public class Deezer {
data += scanner.nextLine(); data += scanner.nextLine();
} }
//Close
try {
connection.disconnect();
scanner.close();
} catch (Exception e) {}
//Parse JSON //Parse JSON
JSONObject out = new JSONObject(data); JSONObject out = new JSONObject(data);
return out; return out;
@ -252,9 +266,11 @@ public class Deezer {
String artists = ""; String artists = "";
String feats = ""; String feats = "";
for (int i=0; i<publicTrack.getJSONArray("contributors").length(); i++) { for (int i=0; i<publicTrack.getJSONArray("contributors").length(); i++) {
artists += ", " + publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name"); String artist = publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name");
if (i > 0) if (!artists.contains(artist))
feats += ", " + publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name"); artists += ", " + artist;
if (i > 0 && !artists.contains(artist) && !feats.contains(artist))
feats += ", " + artist;
} }
original = original.replaceAll("%artists%", sanitize(artists).substring(2)); original = original.replaceAll("%artists%", sanitize(artists).substring(2));
if (feats.length() >= 2) if (feats.length() >= 2)
@ -267,6 +283,9 @@ public class Deezer {
original = original.replaceAll("%year%", publicTrack.getString("release_date").substring(0, 4)); original = original.replaceAll("%year%", publicTrack.getString("release_date").substring(0, 4));
original = original.replaceAll("%date%", publicTrack.getString("release_date")); original = original.replaceAll("%date%", publicTrack.getString("release_date"));
//Remove leading dots
original = original.replaceAll("/\\.+", "/");
if (newQuality == 9) return original + ".flac"; if (newQuality == 9) return original + ".flac";
return original + ".mp3"; return original + ".mp3";
} }
@ -286,7 +305,7 @@ public class Deezer {
} }
//Tag track with data from API //Tag track with data from API
public static void tagTrack(String path, JSONObject publicTrack, JSONObject publicAlbum, String cover, JSONObject lyricsData, JSONObject privateJson) throws Exception { public void tagTrack(String path, JSONObject publicTrack, JSONObject publicAlbum, String cover, JSONObject lyricsData, JSONObject privateJson) throws Exception {
TagOptionSingleton.getInstance().setAndroid(true); TagOptionSingleton.getInstance().setAndroid(true);
//Load file //Load file
AudioFile f = AudioFileIO.read(new File(path)); AudioFile f = AudioFileIO.read(new File(path));
@ -302,7 +321,9 @@ public class Deezer {
//Artist //Artist
String artists = ""; String artists = "";
for (int i=0; i<publicTrack.getJSONArray("contributors").length(); i++) { for (int i=0; i<publicTrack.getJSONArray("contributors").length(); i++) {
artists += ", " + publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name"); String artist = publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name");
if (!artists.contains(artist))
artists += ", " + artist;
} }
tag.addField(FieldKey.ARTIST, artists.substring(2)); tag.addField(FieldKey.ARTIST, artists.substring(2));
tag.setField(FieldKey.TRACK, Integer.toString(publicTrack.getInt("track_position"))); tag.setField(FieldKey.TRACK, Integer.toString(publicTrack.getInt("track_position")));
@ -337,66 +358,70 @@ public class Deezer {
tag.setField(FieldKey.GENRE, genres.substring(2)); tag.setField(FieldKey.GENRE, genres.substring(2));
//Additional tags from private api //Additional tags from private api
if (privateJson != null && privateJson.has("SNG_CONTRIBUTORS")) { try {
JSONObject contrib = privateJson.getJSONObject("SNG_CONTRIBUTORS"); if (privateJson != null && privateJson.has("SNG_CONTRIBUTORS")) {
//Composer JSONObject contrib = privateJson.getJSONObject("SNG_CONTRIBUTORS");
if (contrib.has("composer")) { //Composer
JSONArray composers = contrib.getJSONArray("composer"); if (contrib.has("composer")) {
String composer = ""; JSONArray composers = contrib.getJSONArray("composer");
for (int i=0; i<composers.length(); i++) String composer = "";
composer += ", " + composers.getString(i); for (int i = 0; i < composers.length(); i++)
if (composer.length() > 2) composer += ", " + composers.getString(i);
tag.setField(FieldKey.COMPOSER, composer.substring(2)); if (composer.length() > 2)
} tag.setField(FieldKey.COMPOSER, composer.substring(2));
//Engineer }
if (contrib.has("engineer")) { //Engineer
JSONArray engineers = contrib.getJSONArray("engineer"); if (contrib.has("engineer")) {
String engineer = ""; JSONArray engineers = contrib.getJSONArray("engineer");
for (int i=0; i<engineers.length(); i++) String engineer = "";
engineer += ", " + engineers.getString(i); for (int i = 0; i < engineers.length(); i++)
if (engineer.length() > 2) engineer += ", " + engineers.getString(i);
tag.setField(FieldKey.ENGINEER, engineer.substring(2)); if (engineer.length() > 2)
} tag.setField(FieldKey.ENGINEER, engineer.substring(2));
//Mixer }
if (contrib.has("mixer")) { //Mixer
JSONArray mixers = contrib.getJSONArray("mixer"); if (contrib.has("mixer")) {
String mixer = ""; JSONArray mixers = contrib.getJSONArray("mixer");
for (int i=0; i<mixers.length(); i++) String mixer = "";
mixer += ", " + mixers.getString(i); for (int i = 0; i < mixers.length(); i++)
if (mixer.length() > 2) mixer += ", " + mixers.getString(i);
tag.setField(FieldKey.MIXER, mixer.substring(2)); if (mixer.length() > 2)
} tag.setField(FieldKey.MIXER, mixer.substring(2));
//Producer }
if (contrib.has("producer")) { //Producer
JSONArray producers = contrib.getJSONArray("producer"); if (contrib.has("producer")) {
String producer = ""; JSONArray producers = contrib.getJSONArray("producer");
for (int i=0; i<producers.length(); i++) String producer = "";
producer += ", " + producers.getString(i); for (int i = 0; i < producers.length(); i++)
if (producer.length() > 2) producer += ", " + producers.getString(i);
tag.setField(FieldKey.MIXER, producer.substring(2)); if (producer.length() > 2)
} tag.setField(FieldKey.MIXER, producer.substring(2));
}
//FLAC Only //FLAC Only
if (isFlac) { if (isFlac) {
//Author //Author
if (contrib.has("author")) { if (contrib.has("author")) {
JSONArray authors = contrib.getJSONArray("author"); JSONArray authors = contrib.getJSONArray("author");
String author = ""; String author = "";
for (int i=0; i<authors.length(); i++) for (int i = 0; i < authors.length(); i++)
author += ", " + authors.getString(i); author += ", " + authors.getString(i);
if (author.length() > 2) if (author.length() > 2)
((FlacTag)tag).setField("AUTHOR", author.substring(2)); ((FlacTag) tag).setField("AUTHOR", author.substring(2));
} }
//Writer //Writer
if (contrib.has("writer")) { if (contrib.has("writer")) {
JSONArray writers = contrib.getJSONArray("writer"); JSONArray writers = contrib.getJSONArray("writer");
String writer = ""; String writer = "";
for (int i=0; i<writers.length(); i++) for (int i = 0; i < writers.length(); i++)
writer += ", " + writers.getString(i); writer += ", " + writers.getString(i);
if (writer.length() > 2) if (writer.length() > 2)
((FlacTag)tag).setField("WRITER", writer.substring(2)); ((FlacTag) tag).setField("WRITER", writer.substring(2));
}
} }
} }
} catch (Exception e) {
logger.warn("Error writing contributors data: " + e.toString());
} }
File coverFile = new File(cover); File coverFile = new File(cover);
@ -423,7 +448,8 @@ public class Deezer {
} }
} else { } else {
if (addCover) { if (addCover) {
Artwork art = Artwork.createArtworkFromFile(coverFile); Artwork art = ArtworkFactory.createArtworkFromFile(coverFile);
//Artwork art = Artwork.createArtworkFromFile(coverFile);
tag.addField(art); tag.addField(art);
} }
} }

View File

@ -358,7 +358,7 @@ public class DownloadService extends Service {
return; return;
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("ISRC Fallback failed, track unavailable! " + e.toString()); logger.error("ISRC Fallback failed, track unavailable! " + e.toString(), download);
download.state = Download.DownloadState.DEEZER_ERROR; download.state = Download.DownloadState.DEEZER_ERROR;
exit(); exit();
return; return;
@ -572,7 +572,7 @@ public class DownloadService extends Service {
//Tag //Tag
try { try {
Deezer.tagTrack(outFile.getPath(), trackJson, albumJson, coverFile.getPath(), lyricsData, privateJson); deezer.tagTrack(outFile.getPath(), trackJson, albumJson, coverFile.getPath(), lyricsData, privateJson);
} catch (Exception e) { } catch (Exception e) {
Log.e("ERR", "Tagging error!"); Log.e("ERR", "Tagging error!");
e.printStackTrace(); e.printStackTrace();
@ -600,7 +600,7 @@ public class DownloadService extends Service {
File coverFile = new File(parentDir, "cover.jpg"); File coverFile = new File(parentDir, "cover.jpg");
if (coverFile.exists()) return; if (coverFile.exists()) return;
//Don't download if doesn't have album //Don't download if doesn't have album
if (!download.path.matches(".*/%album%.*/.*")) return; if (!download.path.matches(".*/.*%album%.*/.*")) return;
try { try {
//Create to lock //Create to lock

View File

@ -1,91 +1,67 @@
import 'dart:async'; import 'package:freezer/api/definitions.dart';
import 'package:freezer/settings.dart';
import 'package:dio/adapter.dart'; import 'package:http/http.dart' as http;
import 'package:dio/dio.dart';
import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:freezer/api/cache.dart';
import 'dart:io'; import 'dart:io';
import 'dart:convert'; import 'dart:convert';
import 'dart:async';
import '../settings.dart';
import 'definitions.dart';
DeezerAPI deezerAPI = DeezerAPI(); DeezerAPI deezerAPI = DeezerAPI();
class DeezerAPI { class DeezerAPI {
String arl;
DeezerAPI({this.arl}); DeezerAPI({this.arl});
String arl;
String token; String token;
String userId; String userId;
String userName; String userName;
String favoritesPlaylistId; String favoritesPlaylistId;
String privateUrl = 'http://www.deezer.com/ajax/gw-light.php'; String sid;
Map<String, String> headers = {
Future _authorizing;
//Get headers
Map<String, String> get headers => {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Content-Language": '${settings.deezerLanguage??"en"}-${settings.deezerCountry??'US'}', "Content-Language": '${settings.deezerLanguage??"en"}-${settings.deezerCountry??'US'}',
"Cache-Control": "max-age=0", "Cache-Control": "max-age=0",
"Accept": "*/*", "Accept": "*/*",
"Accept-Charset": "utf-8,ISO-8859-1;q=0.7,*;q=0.3", "Accept-Charset": "utf-8,ISO-8859-1;q=0.7,*;q=0.3",
"Accept-Language": "${settings.deezerLanguage??"en"}-${settings.deezerCountry??'US'},${settings.deezerLanguage??"en"};q=0.9,en-US;q=0.8,en;q=0.7", "Accept-Language": "${settings.deezerLanguage??"en"}-${settings.deezerCountry??'US'},${settings.deezerLanguage??"en"};q=0.9,en-US;q=0.8,en;q=0.7",
"Connection": "keep-alive" "Connection": "keep-alive",
"Cookie": "arl=${arl}" + ((sid == null) ? '' : '; sid=${sid}')
}; };
Future _authorizing;
Dio dio = Dio();
CookieJar _cookieJar = new CookieJar();
//Call private api //Call private API
Future<Map<dynamic, dynamic>> callApi(String method, {Map<dynamic, dynamic> params, String gatewayInput}) async { Future<Map<dynamic, dynamic>> callApi(String method, {Map<dynamic, dynamic> params, String gatewayInput}) async {
//Generate URL
//Add headers Uri uri = Uri.https('www.deezer.com', '/ajax/gw-light.php', {
dio.interceptors.add(InterceptorsWrapper( 'api_version': '1.0',
onRequest: (RequestOptions options) { 'api_token': this.token,
options.headers = this.headers; 'input': '3',
return options; 'method': method,
//Used for homepage
if (gatewayInput != null)
'gateway_input': gatewayInput
});
//Post
http.Response res = await http.post(uri, headers: headers, body: jsonEncode(params));
//Grab SID
if (method == 'deezer.getUserData') {
for (String cookieHeader in res.headers['set-cookie'].split(';')) {
if (cookieHeader.startsWith('sid=')) {
sid = cookieHeader.split('=')[1];
}
} }
));
//Proxy
if (settings.proxyAddress != null && settings.proxyAddress != '' && settings.proxyAddress.length > 9) {
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client.findProxy = (uri) => "PROXY ${settings.proxyAddress}";
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
};
} }
//Add cookies return jsonDecode(res.body);
List<Cookie> cookies = [Cookie('arl', this.arl)];
_cookieJar.saveFromResponse(Uri.parse(this.privateUrl), cookies);
dio.interceptors.add(CookieManager(_cookieJar));
//Make request
Response<dynamic> response = await dio.post(
this.privateUrl,
queryParameters: {
'api_version': '1.0',
'api_token': this.token,
'input': '3',
'method': method,
//Used for homepage
if (gatewayInput != null)
'gateway_input': gatewayInput
},
data: jsonEncode(params??{}),
options: Options(responseType: ResponseType.json, sendTimeout: 10000, receiveTimeout: 10000)
);
return response.data;
} }
Future<Map> callPublicApi(String path) async { Future<Map<dynamic, dynamic>> callPublicApi(String path) async {
Dio dio = Dio(); http.Response res = await http.get('https://api.deezer.com/' + path);
Response response = await dio.get( return jsonDecode(res.body);
'https://api.deezer.com/' + path,
options: Options(responseType: ResponseType.json, sendTimeout: 10000, receiveTimeout: 10000)
);
return response.data;
} }
//Wrapper so it can be globally awaited //Wrapper so it can be globally awaited
@ -128,11 +104,11 @@ class DeezerAPI {
} }
//Share URL //Share URL
if (uri.host == 'deezer.page.link' || uri.host == 'www.deezer.page.link') { if (uri.host == 'deezer.page.link' || uri.host == 'www.deezer.page.link') {
Dio dio = Dio(); http.BaseRequest request = http.Request('HEAD', Uri.parse(url));
Response res = await dio.head(url, options: RequestOptions( request.followRedirects = false;
followRedirects: true http.StreamedResponse response = await request.send();
)); String newUrl = response.headers['location'];
return parseLink('http://deezer.com' + res.realUri.toString()); return parseLink(newUrl);
} }
} }
@ -445,5 +421,14 @@ class DeezerAPI {
'songs': [] 'songs': []
}); });
} }
//Get shuffled library
Future<List<Track>> libraryShuffle({int start=0}) async {
Map data = await callApi('tracklist.getShuffledCollection', params: {
'nb': 50,
'start': start
});
return data['results']['data'].map<Track>((t) => Track.fromPrivateJson(t)).toList();
}
} }

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:disk_space/disk_space.dart'; import 'package:disk_space/disk_space.dart';
import 'package:filesize/filesize.dart'; import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
@ -110,9 +111,63 @@ class DownloadManager {
return batch; return batch;
} }
Future addOfflineTrack(Track track, {private = true}) async { //Quality selector for custom quality
Future qualitySelect(BuildContext context) async {
AudioQuality quality;
await showModalBottomSheet(
context: context,
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.fromLTRB(0, 12, 0, 2),
child: Text(
'Quality'.i18n,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20.0
),
),
),
ListTile(
title: Text('MP3 128kbps'),
onTap: () {
quality = AudioQuality.MP3_128;
Navigator.of(context).pop();
},
),
ListTile(
title: Text('MP3 320kbps'),
onTap: () {
quality = AudioQuality.MP3_320;
Navigator.of(context).pop();
},
),
ListTile(
title: Text('FLAC'),
onTap: () {
quality = AudioQuality.FLAC;
Navigator.of(context).pop();
},
)
],
);
}
);
return quality;
}
Future<bool> addOfflineTrack(Track track, {private = true, BuildContext context}) async {
//Permission //Permission
if (!private && !(await checkPermission())) return; if (!private && !(await checkPermission())) return false;
//Ask for quality
AudioQuality quality;
if (!private && settings.downloadQuality == AudioQuality.ASK) {
quality = await qualitySelect(context);
if (quality == null) return false;
}
//Add to DB //Add to DB
if (private) { if (private) {
@ -127,14 +182,21 @@ class DownloadManager {
//Get path //Get path
String path = _generatePath(track, private); String path = _generatePath(track, private);
await platform.invokeMethod('addDownloads', [await Download.jsonFromTrack(track, path, private: private)]); await platform.invokeMethod('addDownloads', [await Download.jsonFromTrack(track, path, private: private, quality: quality)]);
await start(); await start();
} }
Future addOfflineAlbum(Album album, {private = true}) async { Future addOfflineAlbum(Album album, {private = true, BuildContext context}) async {
//Permission //Permission
if (!private && !(await checkPermission())) return; if (!private && !(await checkPermission())) return;
//Ask for quality
AudioQuality quality;
if (!private && settings.downloadQuality == AudioQuality.ASK) {
quality = await qualitySelect(context);
if (quality == null) return false;
}
//Get from API if no tracks //Get from API if no tracks
if (album.tracks == null || album.tracks.length == 0) { if (album.tracks == null || album.tracks.length == 0) {
album = await deezerAPI.album(album.id); album = await deezerAPI.album(album.id);
@ -157,16 +219,22 @@ class DownloadManager {
//Create downloads //Create downloads
List<Map> out = []; List<Map> out = [];
for (Track t in album.tracks) { for (Track t in album.tracks) {
out.add(await Download.jsonFromTrack(t, _generatePath(t, private), private: private)); out.add(await Download.jsonFromTrack(t, _generatePath(t, private), private: private, quality: quality));
} }
await platform.invokeMethod('addDownloads', out); await platform.invokeMethod('addDownloads', out);
await start(); await start();
} }
Future addOfflinePlaylist(Playlist playlist, {private = true}) async { Future addOfflinePlaylist(Playlist playlist, {private = true, BuildContext context, AudioQuality quality}) async {
//Permission //Permission
if (!private && !(await checkPermission())) return; if (!private && !(await checkPermission())) return;
//Ask for quality
if (!private && settings.downloadQuality == AudioQuality.ASK && quality == null) {
quality = await qualitySelect(context);
if (quality == null) return false;
}
//Get tracks if missing //Get tracks if missing
if (playlist.tracks == null || playlist.tracks.length < playlist.trackCount) { if (playlist.tracks == null || playlist.tracks.length < playlist.trackCount) {
playlist = await deezerAPI.fullPlaylist(playlist.id); playlist = await deezerAPI.fullPlaylist(playlist.id);
@ -193,8 +261,8 @@ class DownloadManager {
t, t,
private, private,
playlistName: playlist.title, playlistName: playlist.title,
playlistTrackNumber: i playlistTrackNumber: i,
), private: private)); ), private: private, quality: quality));
} }
await platform.invokeMethod('addDownloads', out); await platform.invokeMethod('addDownloads', out);
await start(); await start();
@ -375,7 +443,7 @@ class DownloadManager {
return true; return true;
} }
//Playlist //Playlist
if (playlist != null) { if (playlist != null && playlist.id != null) {
List res = await db.query('Playlists', where: 'id == ?', whereArgs: [playlist.id]); List res = await db.query('Playlists', where: 'id == ?', whereArgs: [playlist.id]);
if (res.length == 0) return false; if (res.length == 0) return false;
return true; return true;
@ -553,7 +621,7 @@ class Download {
} }
//Track to download JSON for service //Track to download JSON for service
static Future<Map> jsonFromTrack(Track t, String path, {private = true}) async { static Future<Map> jsonFromTrack(Track t, String path, {private = true, AudioQuality quality}) async {
//Get download info //Get download info
if (t.playbackDetails == null || t.playbackDetails == []) { if (t.playbackDetails == null || t.playbackDetails == []) {
t = await deezerAPI.track(t.id); t = await deezerAPI.track(t.id);
@ -565,7 +633,7 @@ class Download {
"mediaVersion": t.playbackDetails[1], "mediaVersion": t.playbackDetails[1],
"quality": private "quality": private
? settings.getQualityInt(settings.offlineQuality) ? settings.getQualityInt(settings.offlineQuality)
: settings.getQualityInt(settings.downloadQuality), : settings.getQualityInt((quality??settings.downloadQuality)),
"title": t.title, "title": t.title,
"path": path, "path": path,
"image": t.albumArt.thumb "image": t.albumArt.thumb

View File

@ -104,6 +104,7 @@ class PlayerHelper {
androidNotificationChannelDescription: 'Freezer', androidNotificationChannelDescription: 'Freezer',
androidNotificationChannelName: 'Freezer', androidNotificationChannelName: 'Freezer',
androidNotificationIcon: 'drawable/ic_logo', androidNotificationIcon: 'drawable/ic_logo',
params: {'ignoreInterruptions': settings.ignoreInterruptions}
); );
} }
@ -138,14 +139,17 @@ class PlayerHelper {
await startService(); await startService();
await settings.updateAudioServiceQuality(); await settings.updateAudioServiceQuality();
await AudioService.updateQueue(queue); await AudioService.updateQueue(queue);
await AudioService.skipToQueueItem(trackId); if (queue[0].id != trackId)
await AudioService.skipToQueueItem(trackId);
if (!AudioService.playbackState.playing)
AudioService.play();
} }
//Called when queue ends to load more tracks //Called when queue ends to load more tracks
Future onQueueEnd() async { Future onQueueEnd() async {
//Flow //Flow
if (queueSource == null) return; if (queueSource == null) return;
print('test');
if (queueSource.id == 'flow') { if (queueSource.id == 'flow') {
List<Track> tracks = await deezerAPI.flow(); List<Track> tracks = await deezerAPI.flow();
List<MediaItem> mi = tracks.map<MediaItem>((t) => t.toMediaItem()).toList(); List<MediaItem> mi = tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
@ -163,6 +167,15 @@ class PlayerHelper {
return; return;
} }
//Library shuffle
if (queueSource.source == 'libraryshuffle') {
List<Track> tracks = await deezerAPI.libraryShuffle(start: AudioService.queue.length);
List<MediaItem> mi = tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
await AudioService.addQueueItems(mi);
AudioService.skipToNext();
return;
}
print(queueSource.toJson()); print(queueSource.toJson());
} }
@ -245,7 +258,7 @@ void backgroundTaskEntrypoint() async {
} }
class AudioPlayerTask extends BackgroundAudioTask { class AudioPlayerTask extends BackgroundAudioTask {
AudioPlayer _player = AudioPlayer(); AudioPlayer _player;
//Queue //Queue
List<MediaItem> _queue = <MediaItem>[]; List<MediaItem> _queue = <MediaItem>[];
@ -274,6 +287,13 @@ class AudioPlayerTask extends BackgroundAudioTask {
final session = await AudioSession.instance; final session = await AudioSession.instance;
session.configure(AudioSessionConfiguration.music()); session.configure(AudioSessionConfiguration.music());
if (params['ignoreInterruptions'] == true) {
_player = AudioPlayer(handleInterruptions: false);
session.interruptionEventStream.listen((_) {});
session.becomingNoisyEventStream.listen((_) {});
} else
_player = AudioPlayer();
//Update track index //Update track index
_player.currentIndexStream.listen((index) { _player.currentIndexStream.listen((index) {
if (index != null) { if (index != null) {
@ -365,7 +385,6 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override @override
Future<void> onSkipToNext() async { Future<void> onSkipToNext() async {
print('skipping');
if (_queueIndex == _queue.length-1) return; if (_queueIndex == _queue.length-1) return;
//Update buffering state //Update buffering state
_skipState = AudioProcessingState.skippingToNext; _skipState = AudioProcessingState.skippingToNext;
@ -428,10 +447,10 @@ class AudioPlayerTask extends BackgroundAudioTask {
MediaControl.skipToNext, MediaControl.skipToNext,
//Stop //Stop
MediaControl( MediaControl(
androidIcon: 'drawable/ic_action_stop', androidIcon: 'drawable/ic_action_stop',
label: 'stop', label: 'stop',
action: MediaAction.stop action: MediaAction.stop
) ),
], ],
systemActions: [ systemActions: [
MediaAction.seekTo, MediaAction.seekTo,

View File

@ -1,9 +1,11 @@
import 'package:dio/dio.dart'; import 'package:flutter/material.dart';
import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/download.dart'; import 'package:freezer/api/download.dart';
import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/definitions.dart';
import 'package:freezer/settings.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:html/dom.dart'; import 'package:html/dom.dart' as dom;
import 'package:http/http.dart' as http;
import 'dart:convert'; import 'dart:convert';
import 'dart:async'; import 'dart:async';
@ -32,11 +34,10 @@ class SpotifyAPI {
//Extract JSON data form spotify embed page //Extract JSON data form spotify embed page
Future<Map> getEmbedData(String url) async { Future<Map> getEmbedData(String url) async {
//Fetch //Fetch
Dio dio = Dio(); http.Response response = await http.get(url);
Response response = await dio.get(url);
//Parse //Parse
Document document = parse(response.data); dom.Document document = parse(response.body);
Element element = document.getElementById('resource'); dom.Element element = document.getElementById('resource');
return jsonDecode(element.innerHtml); return jsonDecode(element.innerHtml);
} }
@ -50,7 +51,7 @@ class SpotifyAPI {
} }
Future convertPlaylist(SpotifyPlaylist playlist, {bool downloadOnly = false}) async { Future convertPlaylist(SpotifyPlaylist playlist, {bool downloadOnly = false, BuildContext context, AudioQuality quality}) async {
doneImporting = false; doneImporting = false;
importingSpotifyPlaylist = playlist; importingSpotifyPlaylist = playlist;
@ -60,6 +61,7 @@ class SpotifyAPI {
playlistId = await deezerAPI.createPlaylist(playlist.name, description: playlist.description); playlistId = await deezerAPI.createPlaylist(playlist.name, description: playlist.description);
//Search for tracks //Search for tracks
List<Track> downloadTracks = [];
for (SpotifyTrack track in playlist.tracks) { for (SpotifyTrack track in playlist.tracks) {
Map deezer; Map deezer;
try { try {
@ -71,12 +73,21 @@ class SpotifyAPI {
if (!downloadOnly) if (!downloadOnly)
await deezerAPI.addToPlaylist(id, playlistId); await deezerAPI.addToPlaylist(id, playlistId);
if (downloadOnly) if (downloadOnly)
await downloadManager.addOfflineTrack(Track(id: id), private: false); downloadTracks.add(Track(id: id));
track.state = TrackImportState.OK; track.state = TrackImportState.OK;
} catch (e) { } catch (e) {
//On error //On error
track.state = TrackImportState.ERROR; track.state = TrackImportState.ERROR;
} }
//Download
if (downloadOnly)
await downloadManager.addOfflinePlaylist(
Playlist(trackCount: downloadTracks.length, tracks: downloadTracks, title: playlist.name),
private: false,
quality: quality
);
//Add playlist id to stream, stream is for updating ui only //Add playlist id to stream, stream is for updating ui only
importingStream.add(playlistId); importingStream.add(playlistId);
importingSpotifyPlaylist = playlist; importingSpotifyPlaylist = playlist;

File diff suppressed because one or more lines are too long

View File

@ -248,6 +248,13 @@ const language_en_us = {
"Current timer ends at": "Current timer ends at", "Current timer ends at": "Current timer ends at",
//0.5.8 Strings: //0.5.8 Strings:
"Smart track list": "Smart track list" "Smart track list": "Smart track list",
//0.6.0 Strings:
"Shuffle": "Shuffle",
"Library shuffle": "Library shuffle",
"Ignore interruptions": "Ignore interruptions",
"Requires app restart to apply!": "Requires app restart to apply!",
"Ask before downloading": "Ask before downloading"
} }
}; };

View File

@ -67,6 +67,7 @@ class _FreezerAppState extends State<FreezerApp> {
}); });
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: settings.themeData.bottomAppBarColor, systemNavigationBarColor: settings.themeData.bottomAppBarColor,
systemNavigationBarIconBrightness: (settings.theme == Themes.Light)?Brightness.dark:Brightness.light
)); ));
} }
@ -231,45 +232,45 @@ class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateM
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
bottomNavigationBar: Column( bottomNavigationBar: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
PlayerBar(), PlayerBar(),
BottomNavigationBar( BottomNavigationBar(
backgroundColor: Theme.of(context).bottomAppBarColor, backgroundColor: Theme.of(context).bottomAppBarColor,
currentIndex: _selected, currentIndex: _selected,
onTap: (int s) async { onTap: (int s) async {
//Pop all routes until home screen
while (navigatorKey.currentState.canPop()) {
await navigatorKey.currentState.maybePop();
}
//Pop all routes until home screen
while (navigatorKey.currentState.canPop()) {
await navigatorKey.currentState.maybePop(); await navigatorKey.currentState.maybePop();
setState(() { }
_selected = s;
}); await navigatorKey.currentState.maybePop();
}, setState(() {
selectedItemColor: Theme.of(context).primaryColor, _selected = s;
items: <BottomNavigationBarItem>[ });
BottomNavigationBarItem( },
icon: Icon(Icons.home), title: Text('Home'.i18n)), selectedItemColor: Theme.of(context).primaryColor,
BottomNavigationBarItem( items: <BottomNavigationBarItem>[
icon: Icon(Icons.search), BottomNavigationBarItem(
title: Text('Search'.i18n), icon: Icon(Icons.home), title: Text('Home'.i18n)),
), BottomNavigationBarItem(
BottomNavigationBarItem( icon: Icon(Icons.search),
icon: Icon(Icons.library_music), title: Text('Library'.i18n)) title: Text('Search'.i18n),
], ),
) BottomNavigationBarItem(
], icon: Icon(Icons.library_music), title: Text('Library'.i18n))
],
)
],
),
body: AudioServiceWidget(
child: CustomNavigator(
navigatorKey: navigatorKey,
home: _screens[_selected],
pageRoute: PageRoutes.materialPageRoute,
), ),
body: AudioServiceWidget( ));
child: CustomNavigator(
navigatorKey: navigatorKey,
home: _screens[_selected],
pageRoute: PageRoutes.materialPageRoute,
),
));
} }
} }

View File

@ -24,6 +24,9 @@ class Settings {
@JsonKey(defaultValue: null) @JsonKey(defaultValue: null)
String language; String language;
@JsonKey(defaultValue: false)
bool ignoreInterruptions;
//Account //Account
String arl; String arl;
@JsonKey(ignore: true) @JsonKey(ignore: true)
@ -185,6 +188,7 @@ class Settings {
} }
static const deezerBg = Color(0xFF1F1A16); static const deezerBg = Color(0xFF1F1A16);
static const deezerBottom = Color(0xFF1b1714);
static const font = 'MabryPro'; static const font = 'MabryPro';
Map<Themes, ThemeData> get _themeData => { Map<Themes, ThemeData> get _themeData => {
Themes.Light: ThemeData( Themes.Light: ThemeData(
@ -193,7 +197,7 @@ class Settings {
accentColor: primaryColor, accentColor: primaryColor,
sliderTheme: _sliderTheme, sliderTheme: _sliderTheme,
toggleableActiveColor: primaryColor, toggleableActiveColor: primaryColor,
bottomAppBarColor: Color(0xfff7f7f7) bottomAppBarColor: Color(0xfff5f5f5),
), ),
Themes.Dark: ThemeData( Themes.Dark: ThemeData(
fontFamily: font, fontFamily: font,
@ -212,10 +216,10 @@ class Settings {
toggleableActiveColor: primaryColor, toggleableActiveColor: primaryColor,
backgroundColor: deezerBg, backgroundColor: deezerBg,
scaffoldBackgroundColor: deezerBg, scaffoldBackgroundColor: deezerBg,
bottomAppBarColor: deezerBg, bottomAppBarColor: deezerBottom,
dialogBackgroundColor: deezerBg, dialogBackgroundColor: deezerBottom,
bottomSheetTheme: BottomSheetThemeData( bottomSheetTheme: BottomSheetThemeData(
backgroundColor: deezerBg backgroundColor: deezerBottom
), ),
cardColor: deezerBg cardColor: deezerBg
), ),
@ -245,7 +249,8 @@ class Settings {
enum AudioQuality { enum AudioQuality {
MP3_128, MP3_128,
MP3_320, MP3_320,
FLAC FLAC,
ASK
} }
enum Themes { enum Themes {

View File

@ -12,6 +12,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
arl: json['arl'] as String, arl: json['arl'] as String,
) )
..language = json['language'] as String ..language = json['language'] as String
..ignoreInterruptions = json['ignoreInterruptions'] as bool ?? false
..wifiQuality = ..wifiQuality =
_$enumDecodeNullable(_$AudioQualityEnumMap, json['wifiQuality']) ?? _$enumDecodeNullable(_$AudioQualityEnumMap, json['wifiQuality']) ??
AudioQuality.MP3_320 AudioQuality.MP3_320
@ -49,6 +50,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
'language': instance.language, 'language': instance.language,
'ignoreInterruptions': instance.ignoreInterruptions,
'arl': instance.arl, 'arl': instance.arl,
'wifiQuality': _$AudioQualityEnumMap[instance.wifiQuality], 'wifiQuality': _$AudioQualityEnumMap[instance.wifiQuality],
'mobileQuality': _$AudioQualityEnumMap[instance.mobileQuality], 'mobileQuality': _$AudioQualityEnumMap[instance.mobileQuality],
@ -112,6 +114,7 @@ const _$AudioQualityEnumMap = {
AudioQuality.MP3_128: 'MP3_128', AudioQuality.MP3_128: 'MP3_128',
AudioQuality.MP3_320: 'MP3_320', AudioQuality.MP3_320: 'MP3_320',
AudioQuality.FLAC: 'FLAC', AudioQuality.FLAC: 'FLAC',
AudioQuality.ASK: 'ASK',
}; };
const _$ThemesEnumMap = { const _$ThemesEnumMap = {

View File

@ -22,6 +22,7 @@ const supportedLocales = [
const Locale('fa', 'IR'), const Locale('fa', 'IR'),
const Locale('pl', 'PL'), const Locale('pl', 'PL'),
const Locale('uk', 'UA'), const Locale('uk', 'UA'),
const Locale('hu', 'HU'),
const Locale('fil', 'PH') const Locale('fil', 'PH')
]; ];

View File

@ -38,8 +38,9 @@ class CachedImage extends StatefulWidget {
final double height; final double height;
final bool circular; final bool circular;
final bool fullThumb; final bool fullThumb;
final bool rounded;
const CachedImage({Key key, this.url, this.height, this.width, this.circular = false, this.fullThumb = false}): super(key: key); const CachedImage({Key key, this.url, this.height, this.width, this.circular = false, this.fullThumb = false, this.rounded = false}): super(key: key);
@override @override
_CachedImageState createState() => _CachedImageState(); _CachedImageState createState() => _CachedImageState();
@ -49,8 +50,13 @@ class _CachedImageState extends State<CachedImage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.rounded) return ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: CachedImage(url: widget.url, height: widget.height, width: widget.width, circular: false, rounded: false, fullThumb: widget.fullThumb),
);
if (widget.circular) return ClipOval( if (widget.circular) return ClipOval(
child: CachedImage(url: widget.url, height: widget.height, width: widget.width, circular: false) child: CachedImage(url: widget.url, height: widget.height, width: widget.width, circular: false, rounded: false, fullThumb: widget.fullThumb,)
); );
if (!widget.url.startsWith('http')) if (!widget.url.startsWith('http'))

View File

@ -7,6 +7,7 @@ import 'package:freezer/api/cache.dart';
import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/download.dart'; import 'package:freezer/api/download.dart';
import 'package:freezer/api/player.dart'; import 'package:freezer/api/player.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/error.dart'; import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/search.dart'; import 'package:freezer/ui/search.dart';
import 'package:freezer/translations.i18n.dart'; import 'package:freezer/translations.i18n.dart';
@ -53,14 +54,17 @@ class AlbumDetails extends StatelessWidget {
return ListView( return ListView(
children: <Widget>[ children: <Widget>[
//Album art, title, artists //Album art, title, artists
Card( Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Container(height: 8.0,), Container(height: 8.0,),
CachedImage( CachedImage(
url: album.art.full, url: album.art.full,
width: MediaQuery.of(context).size.width / 2 width: MediaQuery.of(context).size.width / 2,
fullThumb: true,
rounded: true,
), ),
Container(height: 8,), Container(height: 8,),
Text( Text(
@ -97,8 +101,9 @@ class AlbumDetails extends StatelessWidget {
], ],
), ),
), ),
FreezerDivider(),
//Details //Details
Card( Container(
child: Row( child: Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
@ -136,8 +141,9 @@ class AlbumDetails extends StatelessWidget {
], ],
), ),
), ),
FreezerDivider(),
//Options (offline, download...) //Options (offline, download...)
Card( Container(
child: Row( child: Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
@ -168,20 +174,28 @@ class AlbumDetails extends StatelessWidget {
Text('Download'.i18n) Text('Download'.i18n)
], ],
), ),
onPressed: () { onPressed: () async {
downloadManager.addOfflineAlbum(album, private: false); if (await downloadManager.addOfflineAlbum(album, private: false, context: context) != false)
MenuSheet(context).showDownloadStartedToast();
}, },
) )
], ],
), ),
), ),
FreezerDivider(),
...List.generate(cdCount, (cdi) { ...List.generate(cdCount, (cdi) {
List<Track> tracks = album.tracks.where((t) => (t.diskNumber??1) == cdi + 1).toList(); List<Track> tracks = album.tracks.where((t) => (t.diskNumber??1) == cdi + 1).toList();
return Column( return Column(
children: [ children: [
Padding( Padding(
padding: EdgeInsets.symmetric(vertical: 4.0), padding: EdgeInsets.symmetric(vertical: 4.0),
child: Text('Disk'.i18n + ' ${cdi + 1}'), child: Text(
'Disk'.i18n.toUpperCase() + ' ${cdi + 1}',
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.w300
),
),
), ),
...List.generate(tracks.length, (i) => TrackTile( ...List.generate(tracks.length, (i) => TrackTile(
tracks[i], tracks[i],
@ -237,6 +251,7 @@ class _MakeAlbumOfflineState extends State<MakeAlbumOffline> {
//Add to offline //Add to offline
await deezerAPI.addFavoriteAlbum(widget.album.id); await deezerAPI.addFavoriteAlbum(widget.album.id);
downloadManager.addOfflineAlbum(widget.album, private: true); downloadManager.addOfflineAlbum(widget.album, private: true);
MenuSheet(context).showDownloadStartedToast();
setState(() { setState(() {
_offline = true; _offline = true;
}); });
@ -283,13 +298,16 @@ class ArtistDetails extends StatelessWidget {
return ListView( return ListView(
children: <Widget>[ children: <Widget>[
Card( Container(height: 4.0),
Container(
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[ children: <Widget>[
CachedImage( CachedImage(
url: artist.picture.full, url: artist.picture.full,
width: MediaQuery.of(context).size.width / 2 - 8, width: MediaQuery.of(context).size.width / 2 - 8,
rounded: true,
fullThumb: true,
), ),
Container( Container(
width: MediaQuery.of(context).size.width / 2 - 8, width: MediaQuery.of(context).size.width / 2 - 8,
@ -347,8 +365,9 @@ class ArtistDetails extends StatelessWidget {
], ],
), ),
), ),
Container(height: 4.0,), Container(height: 4.0),
Card( FreezerDivider(),
Container(
child: Row( child: Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
@ -391,14 +410,18 @@ class ArtistDetails extends StatelessWidget {
], ],
), ),
), ),
Container(height: 16.0,), FreezerDivider(),
Container(height: 12.0,),
//Top tracks //Top tracks
Text( Padding(
'Top Tracks'.i18n, padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 2.0),
textAlign: TextAlign.center, child: Text(
style: TextStyle( 'Top Tracks'.i18n,
fontWeight: FontWeight.bold, textAlign: TextAlign.left,
fontSize: 22.0 style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20.0
),
), ),
), ),
Container(height: 4.0), Container(height: 4.0),
@ -432,14 +455,17 @@ class ArtistDetails extends StatelessWidget {
); );
} }
), ),
Divider(), FreezerDivider(),
//Albums //Albums
Text( Padding(
'Top Albums'.i18n, padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
textAlign: TextAlign.center, child: Text(
style: TextStyle( 'Top Albums'.i18n,
fontWeight: FontWeight.bold, textAlign: TextAlign.left,
fontSize: 22.0 style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20.0
),
), ),
), ),
...List.generate(artist.albums.length > 10 ? 11 : artist.albums.length + 1, (i) { ...List.generate(artist.albums.length > 10 ? 11 : artist.albums.length + 1, (i) {
@ -582,8 +608,8 @@ class _DiscographyScreenState extends State<DiscographyScreen> {
}); });
return Scaffold( return Scaffold(
appBar: AppBar( appBar: FreezerAppBar(
title: Text('Discography'.i18n), 'Discography'.i18n,
bottom: TabBar( bottom: TabBar(
tabs: [ tabs: [
Tab(icon: Icon(Icons.album)), Tab(icon: Icon(Icons.album)),
@ -591,6 +617,7 @@ class _DiscographyScreenState extends State<DiscographyScreen> {
Tab(icon: Icon(Icons.recent_actors)) Tab(icon: Icon(Icons.recent_actors))
], ],
), ),
height: 100.0,
), ),
body: TabBarView( body: TabBarView(
children: [ children: [
@ -748,7 +775,8 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
controller: _scrollController, controller: _scrollController,
children: <Widget>[ children: <Widget>[
Container(height: 4.0,), Container(height: 4.0,),
Card( Padding(
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
@ -756,6 +784,8 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
CachedImage( CachedImage(
url: playlist.image.full, url: playlist.image.full,
height: MediaQuery.of(context).size.width / 2 - 8, height: MediaQuery.of(context).size.width / 2 - 8,
rounded: true,
fullThumb: true,
), ),
Container( Container(
width: MediaQuery.of(context).size.width / 2 - 8, width: MediaQuery.of(context).size.width / 2 - 8,
@ -767,12 +797,13 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
playlist.title, playlist.title,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center, textAlign: TextAlign.center,
maxLines: 2, maxLines: 3,
style: TextStyle( style: TextStyle(
fontSize: 24.0, fontSize: 20.0,
fontWeight: FontWeight.bold fontWeight: FontWeight.bold
), ),
), ),
Container(height: 4.0),
Text( Text(
playlist.user.name, playlist.user.name,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@ -780,12 +811,10 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
fontSize: 18.0 fontSize: 17.0
), ),
), ),
Container( Container(height: 10.0),
height: 8.0,
),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
@ -814,22 +843,25 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
], ],
), ),
), ),
Container(height: 4.0,), if (playlist.description != null && playlist.description.length > 0)
Card( FreezerDivider(),
child: Padding( if (playlist.description != null && playlist.description.length > 0)
padding: EdgeInsets.all(4.0), Container(
child: Text( child: Padding(
playlist.description ?? '', padding: EdgeInsets.all(6.0),
maxLines: 4, child: Text(
overflow: TextOverflow.ellipsis, playlist.description ?? '',
textAlign: TextAlign.center, maxLines: 4,
style: TextStyle( overflow: TextOverflow.ellipsis,
fontSize: 16.0 textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0
),
), ),
), )
) ),
), FreezerDivider(),
Card( Container(
child: Row( child: Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
@ -848,12 +880,14 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
), ),
IconButton( IconButton(
icon: Icon(Icons.file_download, size: 32.0,), icon: Icon(Icons.file_download, size: 32.0,),
onPressed: () { onPressed: () async {
downloadManager.addOfflinePlaylist(playlist, private: false); if (await downloadManager.addOfflinePlaylist(playlist, private: false, context: context) != false)
MenuSheet(context).showDownloadStartedToast();
}, },
), ),
PopupMenuButton( PopupMenuButton(
child: Icon(Icons.sort, size: 32.0), child: Icon(Icons.sort, size: 32.0),
color: Theme.of(context).scaffoldBackgroundColor,
onSelected: (SortType s) async { onSelected: (SortType s) async {
if (playlist.tracks.length < playlist.trackCount) { if (playlist.tracks.length < playlist.trackCount) {
//Preload whole playlist //Preload whole playlist
@ -868,19 +902,19 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
itemBuilder: (context) => <PopupMenuEntry<SortType>>[ itemBuilder: (context) => <PopupMenuEntry<SortType>>[
PopupMenuItem( PopupMenuItem(
value: SortType.DEFAULT, value: SortType.DEFAULT,
child: Text('Default'.i18n), child: Text('Default'.i18n, style: popupMenuTextStyle()),
), ),
PopupMenuItem( PopupMenuItem(
value: SortType.REVERSE, value: SortType.REVERSE,
child: Text('Reverse'.i18n), child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
), ),
PopupMenuItem( PopupMenuItem(
value: SortType.ALPHABETIC, value: SortType.ALPHABETIC,
child: Text('Alphabetic'.i18n), child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
), ),
PopupMenuItem( PopupMenuItem(
value: SortType.ARTIST, value: SortType.ARTIST,
child: Text('Artist'.i18n), child: Text('Artist'.i18n, style: popupMenuTextStyle()),
), ),
], ],
), ),
@ -888,6 +922,7 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
], ],
), ),
), ),
FreezerDivider(),
...List.generate(playlist.tracks.length, (i) { ...List.generate(playlist.tracks.length, (i) {
Track t = sorted[i]; Track t = sorted[i];
return TrackTile( return TrackTile(
@ -909,11 +944,14 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
); );
}), }),
if (_loading) if (_loading)
Row( Padding(
mainAxisAlignment: MainAxisAlignment.center, padding: EdgeInsets.symmetric(vertical: 8.0),
children: <Widget>[ child: Row(
CircularProgressIndicator() mainAxisAlignment: MainAxisAlignment.center,
], children: <Widget>[
CircularProgressIndicator()
],
),
), ),
if (_error) if (_error)
ErrorScreen() ErrorScreen()
@ -955,6 +993,7 @@ class _MakePlaylistOfflineState extends State<MakePlaylistOffline> {
if (widget.playlist.user != null && widget.playlist.user.id != deezerAPI.userId) if (widget.playlist.user != null && widget.playlist.user.id != deezerAPI.userId)
await deezerAPI.addPlaylist(widget.playlist.id); await deezerAPI.addPlaylist(widget.playlist.id);
downloadManager.addOfflinePlaylist(widget.playlist, private: true); downloadManager.addOfflinePlaylist(widget.playlist, private: true);
MenuSheet(context).showDownloadStartedToast();
setState(() { setState(() {
_offline = true; _offline = true;
}); });

View File

@ -4,6 +4,7 @@ import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:freezer/api/download.dart'; import 'package:freezer/api/download.dart';
import 'package:freezer/translations.i18n.dart'; import 'package:freezer/translations.i18n.dart';
import 'package:freezer/ui/elements.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'cached_image.dart'; import 'cached_image.dart';
@ -69,8 +70,8 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: FreezerAppBar(
title: Text('Downloads'.i18n), 'Downloads'.i18n,
actions: [ actions: [
IconButton( IconButton(
icon: Icon(Icons.delete_sweep), icon: Icon(Icons.delete_sweep),
@ -348,9 +349,7 @@ class _DownloadLogViewerState extends State<DownloadLogViewer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: FreezerAppBar('Download Log'.i18n),
title: Text('Download Log'.i18n),
),
body: ListView.builder( body: ListView.builder(
itemCount: data.length, itemCount: data.length,
itemBuilder: (context, i) { itemBuilder: (context, i) {

83
lib/ui/elements.dart Normal file
View File

@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:freezer/settings.dart';
class LeadingIcon extends StatelessWidget {
final IconData icon;
final Color color;
LeadingIcon(this.icon, {this.color});
@override
Widget build(BuildContext context) {
return Container(
width: 42.0,
height: 42.0,
decoration: BoxDecoration(
color: (color??Theme.of(context).primaryColor).withOpacity(1.0),
shape: BoxShape.circle
),
child: Icon(
icon,
color: Colors.white,
),
);
}
}
//Container with set size to match LeadingIcon
class EmptyLeading extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(width: 42.0, height: 42.0);
}
}
class FreezerAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final List<Widget> actions;
final Widget bottom;
//Should be specified if bottom is specified
final double height;
FreezerAppBar(this.title, {this.actions = const [], this.bottom, this.height = 56.0});
Size get preferredSize => Size.fromHeight(this.height);
@override
Widget build(BuildContext context) {
return Theme(
data: ThemeData(primaryColor: (Theme.of(context).brightness == Brightness.light)?Colors.white:Colors.black),
child: AppBar(
elevation: 0.0,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
title: Text(
title,
style: TextStyle(
fontWeight: FontWeight.w900,
),
),
actions: actions,
bottom: bottom,
),
);
}
}
class FreezerDivider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Divider(
thickness: 1.5,
indent: 16.0,
endIndent: 16.0,
);
}
}
TextStyle popupMenuTextStyle() {
return TextStyle(
color: (settings.theme == Themes.Light)?Colors.black:Colors.white
);
}

View File

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/player.dart'; import 'package:freezer/api/player.dart';
import 'package:freezer/main.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/error.dart'; import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/menu.dart'; import 'package:freezer/ui/menu.dart';
import 'package:freezer/translations.i18n.dart'; import 'package:freezer/translations.i18n.dart';
@ -16,9 +18,7 @@ class HomeScreen extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
SafeArea( SafeArea(child: Container()),
child: FreezerTitle(),
),
Flexible(child: HomePageScreen(),) Flexible(child: HomePageScreen(),)
], ],
), ),
@ -161,10 +161,10 @@ class _HomePageScreenState extends State<HomePageScreen> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 20.0, fontSize: 20.0,
fontWeight: FontWeight.bold fontWeight: FontWeight.w900
), ),
), ),
padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0) padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0)
), ),
SingleChildScrollView( SingleChildScrollView(
@ -184,9 +184,7 @@ class _HomePageScreenState extends State<HomePageScreen> {
), ),
onPressed: () => Navigator.of(context).push(MaterialPageRoute( onPressed: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => Scaffold( builder: (context) => Scaffold(
appBar: AppBar( appBar: FreezerAppBar(section.title),
title: Text(section.title),
),
body: SingleChildScrollView( body: SingleChildScrollView(
child: HomePageScreen( child: HomePageScreen(
channel: DeezerChannel(target: section.pagePath) channel: DeezerChannel(target: section.pagePath)
@ -205,6 +203,7 @@ class _HomePageScreenState extends State<HomePageScreen> {
}), }),
), ),
), ),
Container(height: 8.0),
], ],
); );
}, },

View File

@ -2,7 +2,11 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/download.dart';
import 'package:freezer/api/spotify.dart'; import 'package:freezer/api/spotify.dart';
import 'package:freezer/main.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/menu.dart'; import 'package:freezer/ui/menu.dart';
import 'package:freezer/translations.i18n.dart'; import 'package:freezer/translations.i18n.dart';
@ -49,9 +53,7 @@ class _ImporterScreenState extends State<ImporterScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: FreezerAppBar('Importer'.i18n),
title: Text('Importer'.i18n),
),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
@ -62,7 +64,7 @@ class _ImporterScreenState extends State<ImporterScreen> {
color: Colors.deepOrangeAccent, color: Colors.deepOrangeAccent,
), ),
), ),
Divider(), FreezerDivider(),
Container(height: 16.0,), Container(height: 16.0,),
Text( Text(
'Enter your playlist link below'.i18n, 'Enter your playlist link below'.i18n,
@ -130,7 +132,7 @@ class _ImporterWidgetState extends State<ImporterWidget> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: <Widget>[ children: <Widget>[
Divider(), FreezerDivider(),
ListTile( ListTile(
title: Text(widget.playlist.name), title: Text(widget.playlist.name),
subtitle: Text(widget.playlist.description), subtitle: Text(widget.playlist.description),
@ -153,8 +155,15 @@ class _ImporterWidgetState extends State<ImporterWidget> {
RaisedButton( RaisedButton(
child: Text('Download only'.i18n), child: Text('Download only'.i18n),
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
onPressed: () { onPressed: () async {
spotify.convertPlaylist(widget.playlist, downloadOnly: true); //Ask for quality
AudioQuality quality;
if (settings.downloadQuality == AudioQuality.ASK) {
quality = await downloadManager.qualitySelect(context);
if (quality == null) return;
}
spotify.convertPlaylist(widget.playlist, downloadOnly: true, context: context, quality: quality);
Navigator.of(context).pushReplacement(MaterialPageRoute( Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => CurrentlyImportingScreen() builder: (context) => CurrentlyImportingScreen()
)); ));
@ -199,7 +208,7 @@ class CurrentlyImportingScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Importing...'.i18n),), appBar: FreezerAppBar('Importing...'.i18n),
body: StreamBuilder( body: StreamBuilder(
stream: spotify.importingStream.stream, stream: spotify.importingStream.stream,
builder: (context, snapshot) { builder: (context, snapshot) {
@ -225,7 +234,7 @@ class CurrentlyImportingScreen extends StatelessWidget {
], ],
), ),
), ),
Card( Container(
child: Row( child: Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
@ -267,6 +276,8 @@ class CurrentlyImportingScreen extends StatelessWidget {
], ],
), ),
), ),
Container(height: 8.0),
FreezerDivider(),
...List.generate(spotify.importingSpotifyPlaylist.tracks.length, (i) { ...List.generate(spotify.importingSpotifyPlaylist.tracks.length, (i) {
SpotifyTrack t = spotify.importingSpotifyPlaylist.tracks[i]; SpotifyTrack t = spotify.importingSpotifyPlaylist.tracks[i];
return ListTile( return ListTile(

View File

@ -8,6 +8,7 @@ import 'package:freezer/api/player.dart';
import 'package:freezer/settings.dart'; import 'package:freezer/settings.dart';
import 'package:freezer/ui/details_screens.dart'; import 'package:freezer/ui/details_screens.dart';
import 'package:freezer/ui/downloads_screen.dart'; import 'package:freezer/ui/downloads_screen.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/error.dart'; import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/importer_screen.dart'; import 'package:freezer/ui/importer_screen.dart';
import 'package:freezer/ui/tiles.dart'; import 'package:freezer/ui/tiles.dart';
@ -25,8 +26,8 @@ class LibraryAppBar extends StatelessWidget implements PreferredSizeWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppBar( return FreezerAppBar(
title: Text('Library'.i18n), 'Library'.i18n,
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
icon: Icon(Icons.file_download), icon: Icon(Icons.file_download),
@ -61,7 +62,7 @@ class LibraryScreen extends StatelessWidget {
if (!downloadManager.running && downloadManager.queueSize > 0) if (!downloadManager.running && downloadManager.queueSize > 0)
ListTile( ListTile(
title: Text('Downloads'.i18n), title: Text('Downloads'.i18n),
leading: Icon(Icons.file_download), leading: LeadingIcon(Icons.file_download, color: Colors.grey),
subtitle: Text('Downloading is currently stopped, click here to resume.'.i18n), subtitle: Text('Downloading is currently stopped, click here to resume.'.i18n),
onTap: () { onTap: () {
downloadManager.start(); downloadManager.start();
@ -70,13 +71,22 @@ class LibraryScreen extends StatelessWidget {
)); ));
}, },
), ),
//Dirty if to not use columns ListTile(
if (!downloadManager.running && downloadManager.queueSize > 0) title: Text('Shuffle'.i18n),
Divider(), leading: LeadingIcon(Icons.shuffle, color: Color(0xffeca704)),
onTap: () async {
List<Track> tracks = await deezerAPI.libraryShuffle();
playerHelper.playFromTrackList(tracks, tracks[0].id, QueueSource(
id: 'libraryshuffle',
source: 'libraryshuffle',
text: 'Library shuffle'.i18n
));
},
),
FreezerDivider(),
ListTile( ListTile(
title: Text('Tracks'.i18n), title: Text('Tracks'.i18n),
leading: Icon(Icons.audiotrack), leading: LeadingIcon(Icons.audiotrack, color: Color(0xffbe3266)),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute(builder: (context) => LibraryTracks()) MaterialPageRoute(builder: (context) => LibraryTracks())
@ -85,7 +95,7 @@ class LibraryScreen extends StatelessWidget {
), ),
ListTile( ListTile(
title: Text('Albums'.i18n), title: Text('Albums'.i18n),
leading: Icon(Icons.album), leading: LeadingIcon(Icons.album, color: Color(0xff4b2e7e)),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute(builder: (context) => LibraryAlbums()) MaterialPageRoute(builder: (context) => LibraryAlbums())
@ -94,7 +104,7 @@ class LibraryScreen extends StatelessWidget {
), ),
ListTile( ListTile(
title: Text('Artists'.i18n), title: Text('Artists'.i18n),
leading: Icon(Icons.recent_actors), leading: LeadingIcon(Icons.recent_actors, color: Color(0xff384697)),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute(builder: (context) => LibraryArtists()) MaterialPageRoute(builder: (context) => LibraryArtists())
@ -103,26 +113,27 @@ class LibraryScreen extends StatelessWidget {
), ),
ListTile( ListTile(
title: Text('Playlists'.i18n), title: Text('Playlists'.i18n),
leading: Icon(Icons.playlist_play), leading: LeadingIcon(Icons.playlist_play, color: Color(0xff0880b5)),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute(builder: (context) => LibraryPlaylists()) MaterialPageRoute(builder: (context) => LibraryPlaylists())
); );
}, },
), ),
FreezerDivider(),
ListTile( ListTile(
title: Text('History'.i18n), title: Text('History'.i18n),
leading: Icon(Icons.history), leading: LeadingIcon(Icons.history, color: Color(0xff009a85)),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute(builder: (context) => HistoryScreen()) MaterialPageRoute(builder: (context) => HistoryScreen())
); );
}, },
), ),
Divider(), FreezerDivider(),
ListTile( ListTile(
title: Text('Import'.i18n), title: Text('Import'.i18n),
leading: Icon(Icons.import_export), leading: LeadingIcon(Icons.import_export, color: Color(0xff2ba766)),
subtitle: Text('Import playlists from Spotify'.i18n), subtitle: Text('Import playlists from Spotify'.i18n),
onTap: () { onTap: () {
if (spotify.doneImporting != null) { if (spotify.doneImporting != null) {
@ -140,7 +151,7 @@ class LibraryScreen extends StatelessWidget {
), ),
ExpansionTile( ExpansionTile(
title: Text('Statistics'.i18n), title: Text('Statistics'.i18n),
leading: Icon(Icons.insert_chart), leading: LeadingIcon(Icons.insert_chart, color: Colors.grey),
children: <Widget>[ children: <Widget>[
FutureBuilder( FutureBuilder(
future: downloadManager.getStats(), future: downloadManager.getStats(),
@ -350,11 +361,12 @@ class _LibraryTracksState extends State<LibraryTracks> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: FreezerAppBar(
title: Text('Tracks'.i18n), 'Tracks'.i18n,
actions: [ actions: [
PopupMenuButton( PopupMenuButton(
child: Icon(Icons.sort, size: 32.0), child: Icon(Icons.sort, size: 32.0),
color: Theme.of(context).scaffoldBackgroundColor,
onSelected: (SortType s) async { onSelected: (SortType s) async {
//Preload for sorting //Preload for sorting
if (tracks.length < (trackCount??0)) if (tracks.length < (trackCount??0))
@ -367,19 +379,19 @@ class _LibraryTracksState extends State<LibraryTracks> {
itemBuilder: (context) => <PopupMenuEntry<SortType>>[ itemBuilder: (context) => <PopupMenuEntry<SortType>>[
PopupMenuItem( PopupMenuItem(
value: SortType.DEFAULT, value: SortType.DEFAULT,
child: Text('Default'.i18n), child: Text('Default'.i18n, style: popupMenuTextStyle()),
), ),
PopupMenuItem( PopupMenuItem(
value: SortType.REVERSE, value: SortType.REVERSE,
child: Text('Reverse'.i18n), child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
), ),
PopupMenuItem( PopupMenuItem(
value: SortType.ALPHABETIC, value: SortType.ALPHABETIC,
child: Text('Alphabetic'.i18n), child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
), ),
PopupMenuItem( PopupMenuItem(
value: SortType.ARTIST, value: SortType.ARTIST,
child: Text('Artist'.i18n), child: Text('Artist'.i18n, style: popupMenuTextStyle()),
), ),
], ],
), ),
@ -389,40 +401,29 @@ class _LibraryTracksState extends State<LibraryTracks> {
body: ListView( body: ListView(
controller: _scrollController, controller: _scrollController,
children: <Widget>[ children: <Widget>[
Card( Container(
child: Column( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[ children: <Widget>[
Container(height: 8.0,), MakePlaylistOffline(_playlist),
Text( FlatButton(
'Loved tracks'.i18n, child: Row(
style: TextStyle( children: <Widget>[
fontWeight: FontWeight.bold, Icon(Icons.file_download, size: 32.0,),
fontSize: 24 Container(width: 4,),
Text('Download'.i18n)
],
), ),
), onPressed: () async {
Row( if (await downloadManager.addOfflinePlaylist(_playlist, private: false, context: context) != false)
mainAxisSize: MainAxisSize.max, MenuSheet(context).showDownloadStartedToast();
mainAxisAlignment: MainAxisAlignment.spaceAround, },
children: <Widget>[
MakePlaylistOffline(_playlist),
FlatButton(
child: Row(
children: <Widget>[
Icon(Icons.file_download, size: 32.0,),
Container(width: 4,),
Text('Download'.i18n)
],
),
onPressed: () {
downloadManager.addOfflinePlaylist(_playlist, private: false);
},
)
],
) )
], ],
), )
), ),
FreezerDivider(),
//Loved tracks //Loved tracks
...List.generate(tracks.length, (i) { ...List.generate(tracks.length, (i) {
Track t = (tracks.length == (trackCount??0))?_sorted[i]:tracks[i]; Track t = (tracks.length == (trackCount??0))?_sorted[i]:tracks[i];
@ -458,7 +459,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
) )
], ],
), ),
Divider(), FreezerDivider(),
Text( Text(
'All offline tracks'.i18n, 'All offline tracks'.i18n,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -545,10 +546,11 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: FreezerAppBar(
title: Text('Albums'.i18n), 'Albums'.i18n,
actions: [ actions: [
PopupMenuButton( PopupMenuButton(
color: Theme.of(context).scaffoldBackgroundColor,
child: Icon(Icons.sort, size: 32.0), child: Icon(Icons.sort, size: 32.0),
onSelected: (AlbumSortType s) async { onSelected: (AlbumSortType s) async {
setState(() => _sort = s); setState(() => _sort = s);
@ -558,19 +560,19 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
itemBuilder: (context) => <PopupMenuEntry<AlbumSortType>>[ itemBuilder: (context) => <PopupMenuEntry<AlbumSortType>>[
PopupMenuItem( PopupMenuItem(
value: AlbumSortType.DEFAULT, value: AlbumSortType.DEFAULT,
child: Text('Default'.i18n), child: Text('Default'.i18n, style: popupMenuTextStyle()),
), ),
PopupMenuItem( PopupMenuItem(
value: AlbumSortType.REVERSE, value: AlbumSortType.REVERSE,
child: Text('Reverse'.i18n), child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
), ),
PopupMenuItem( PopupMenuItem(
value: AlbumSortType.ALPHABETIC, value: AlbumSortType.ALPHABETIC,
child: Text('Alphabetic'.i18n), child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
), ),
PopupMenuItem( PopupMenuItem(
value: AlbumSortType.ARTIST, value: AlbumSortType.ARTIST,
child: Text('Artist'.i18n), child: Text('Artist'.i18n, style: popupMenuTextStyle()),
), ),
], ],
), ),
@ -615,7 +617,7 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
List<Album> albums = snapshot.data; List<Album> albums = snapshot.data;
return Column( return Column(
children: <Widget>[ children: <Widget>[
Divider(), FreezerDivider(),
Text( Text(
'Offline albums'.i18n, 'Offline albums'.i18n,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -719,11 +721,12 @@ class _LibraryArtistsState extends State<LibraryArtists> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: FreezerAppBar(
title: Text('Artists'.i18n), 'Artists'.i18n,
actions: [ actions: [
PopupMenuButton( PopupMenuButton(
child: Icon(Icons.sort, size: 32.0), child: Icon(Icons.sort, size: 32.0),
color: Theme.of(context).scaffoldBackgroundColor,
onSelected: (ArtistSortType s) async { onSelected: (ArtistSortType s) async {
setState(() => _sort = s); setState(() => _sort = s);
cache.artistSort = s; cache.artistSort = s;
@ -732,19 +735,19 @@ class _LibraryArtistsState extends State<LibraryArtists> {
itemBuilder: (context) => <PopupMenuEntry<ArtistSortType>>[ itemBuilder: (context) => <PopupMenuEntry<ArtistSortType>>[
PopupMenuItem( PopupMenuItem(
value: ArtistSortType.DEFAULT, value: ArtistSortType.DEFAULT,
child: Text('Default'.i18n), child: Text('Default'.i18n, style: popupMenuTextStyle()),
), ),
PopupMenuItem( PopupMenuItem(
value: ArtistSortType.REVERSE, value: ArtistSortType.REVERSE,
child: Text('Reverse'.i18n), child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
), ),
PopupMenuItem( PopupMenuItem(
value: ArtistSortType.ALPHABETIC, value: ArtistSortType.ALPHABETIC,
child: Text('Alphabetic'.i18n), child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
), ),
PopupMenuItem( PopupMenuItem(
value: ArtistSortType.POPULARITY, value: ArtistSortType.POPULARITY,
child: Text('Popularity'.i18n), child: Text('Popularity'.i18n, style: popupMenuTextStyle()),
), ),
], ],
), ),
@ -859,11 +862,12 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: FreezerAppBar(
title: Text('Playlists'.i18n), 'Playlists'.i18n,
actions: [ actions: [
PopupMenuButton( PopupMenuButton(
child: Icon(Icons.sort, size: 32.0), child: Icon(Icons.sort, size: 32.0),
color: Theme.of(context).scaffoldBackgroundColor,
onSelected: (PlaylistSortType s) async { onSelected: (PlaylistSortType s) async {
setState(() => _sort = s); setState(() => _sort = s);
cache.libraryPlaylistSort = s; cache.libraryPlaylistSort = s;
@ -872,23 +876,23 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
itemBuilder: (context) => <PopupMenuEntry<PlaylistSortType>>[ itemBuilder: (context) => <PopupMenuEntry<PlaylistSortType>>[
PopupMenuItem( PopupMenuItem(
value: PlaylistSortType.DEFAULT, value: PlaylistSortType.DEFAULT,
child: Text('Default'.i18n), child: Text('Default'.i18n, style: popupMenuTextStyle()),
), ),
PopupMenuItem( PopupMenuItem(
value: PlaylistSortType.REVERSE, value: PlaylistSortType.REVERSE,
child: Text('Reverse'.i18n), child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
), ),
PopupMenuItem( PopupMenuItem(
value: PlaylistSortType.USER, value: PlaylistSortType.USER,
child: Text('User'.i18n), child: Text('User'.i18n, style: popupMenuTextStyle()),
), ),
PopupMenuItem( PopupMenuItem(
value: PlaylistSortType.TRACK_COUNT, value: PlaylistSortType.TRACK_COUNT,
child: Text('Track count'.i18n), child: Text('Track count'.i18n, style: popupMenuTextStyle()),
), ),
PopupMenuItem( PopupMenuItem(
value: PlaylistSortType.ALPHABETIC, value: PlaylistSortType.ALPHABETIC,
child: Text('Alphabetic'.i18n), child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
), ),
], ],
), ),
@ -899,7 +903,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
title: Text('Create new playlist'.i18n), title: Text('Create new playlist'.i18n),
leading: Icon(Icons.playlist_add), leading: LeadingIcon(Icons.playlist_add, color: Color(0xff009a85)),
onTap: () async { onTap: () async {
if (settings.offlineMode) { if (settings.offlineMode) {
Fluttertoast.showToast( Fluttertoast.showToast(
@ -913,7 +917,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
await _load(); await _load();
}, },
), ),
Divider(), FreezerDivider(),
if (!settings.offlineMode && _playlists == null) if (!settings.offlineMode && _playlists == null)
Row( Row(
@ -965,7 +969,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
List<Playlist> playlists = snapshot.data; List<Playlist> playlists = snapshot.data;
return Column( return Column(
children: <Widget>[ children: <Widget>[
Divider(), FreezerDivider(),
Text( Text(
'Offline playlists'.i18n, 'Offline playlists'.i18n,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -1012,8 +1016,8 @@ class _HistoryScreenState extends State<HistoryScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: FreezerAppBar(
title: Text('History'.i18n), 'History'.i18n,
actions: [ actions: [
IconButton( IconButton(
icon: Icon(Icons.delete_sweep), icon: Icon(Icons.delete_sweep),

View File

@ -188,9 +188,9 @@ class MenuSheet {
title: Text('Download'.i18n), title: Text('Download'.i18n),
leading: Icon(Icons.file_download), leading: Icon(Icons.file_download),
onTap: () async { onTap: () async {
await downloadManager.addOfflineTrack(t, private: false); if (await downloadManager.addOfflineTrack(t, private: false, context: context) != false)
showDownloadStartedToast();
_close(); _close();
showDownloadStartedToast();
}, },
); );
@ -314,8 +314,8 @@ class MenuSheet {
leading: Icon(Icons.file_download), leading: Icon(Icons.file_download),
onTap: () async { onTap: () async {
_close(); _close();
await downloadManager.addOfflineAlbum(a, private: false); if (await downloadManager.addOfflineAlbum(a, private: false, context: context) != false)
showDownloadStartedToast(); showDownloadStartedToast();
} }
); );
@ -471,9 +471,9 @@ class MenuSheet {
title: Text('Download playlist'.i18n), title: Text('Download playlist'.i18n),
leading: Icon(Icons.file_download), leading: Icon(Icons.file_download),
onTap: () async { onTap: () async {
downloadManager.addOfflinePlaylist(p, private: false);
_close(); _close();
showDownloadStartedToast(); if (await downloadManager.addOfflinePlaylist(p, private: false, context: context) != false)
showDownloadStartedToast();
}, },
); );

View File

@ -15,7 +15,7 @@ class PlayerBar extends StatelessWidget {
return AudioService.playbackState.currentPosition.inSeconds / AudioService.currentMediaItem.duration.inSeconds; return AudioService.playbackState.currentPosition.inSeconds / AudioService.currentMediaItem.duration.inSeconds;
} }
double iconSize = 32; double iconSize = 28;
bool _gestureRegistered = false; bool _gestureRegistered = false;
@override @override
@ -40,42 +40,48 @@ class PlayerBar extends StatelessWidget {
child: StreamBuilder( child: StreamBuilder(
stream: Stream.periodic(Duration(milliseconds: 250)), stream: Stream.periodic(Duration(milliseconds: 250)),
builder: (BuildContext context, AsyncSnapshot snapshot) { builder: (BuildContext context, AsyncSnapshot snapshot) {
if (AudioService.currentMediaItem == null) return Container(width: 0, height: 0,); if (AudioService.currentMediaItem == null)
return Container(width: 0, height: 0,);
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Container( Container(
color: Theme.of(context).bottomAppBarColor, color: Theme.of(context).bottomAppBarColor,
child: ListTile( child: ListTile(
dense: true,
contentPadding: EdgeInsets.symmetric(horizontal: 8.0),
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) => PlayerScreen())); Navigator.of(context).push(MaterialPageRoute(
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( builder: (BuildContext context) => PlayerScreen()));
systemNavigationBarColor: settings.themeData.scaffoldBackgroundColor, SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
)); systemNavigationBarColor: settings.themeData
}, .scaffoldBackgroundColor,
leading: CachedImage( ));
width: 50, },
height: 50, leading: CachedImage(
url: AudioService.currentMediaItem.extras['thumb'] ?? AudioService.currentMediaItem.artUri, width: 50,
), height: 50,
title: Text( url: AudioService.currentMediaItem.extras['thumb'] ??
AudioService.currentMediaItem.displayTitle, AudioService.currentMediaItem.artUri,
overflow: TextOverflow.clip, ),
maxLines: 1, title: Text(
), AudioService.currentMediaItem.displayTitle,
subtitle: Text( overflow: TextOverflow.clip,
AudioService.currentMediaItem.displaySubtitle, maxLines: 1,
overflow: TextOverflow.clip, ),
maxLines: 1, subtitle: Text(
), AudioService.currentMediaItem.displaySubtitle,
trailing: Row( overflow: TextOverflow.clip,
mainAxisSize: MainAxisSize.min, maxLines: 1,
children: <Widget>[ ),
PrevNextButton(iconSize, prev: true, hidePrev: true,), trailing: Row(
PlayPauseButton(iconSize), mainAxisSize: MainAxisSize.min,
PrevNextButton(iconSize) children: <Widget>[
], PrevNextButton(iconSize, prev: true, hidePrev: true,),
) PlayPauseButton(iconSize),
PrevNextButton(iconSize)
],
)
), ),
), ),
Container( Container(
@ -87,7 +93,7 @@ class PlayerBar extends StatelessWidget {
) )
], ],
); );
}, }
), ),
); );
} }

View File

@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_screenutil/screenutil.dart'; import 'package:flutter_screenutil/screenutil.dart';
import 'package:freezer/api/cache.dart';
import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/player.dart'; import 'package:freezer/api/player.dart';
import 'package:freezer/settings.dart'; import 'package:freezer/settings.dart';
import 'package:freezer/translations.i18n.dart'; import 'package:freezer/translations.i18n.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/menu.dart'; import 'package:freezer/ui/menu.dart';
import 'package:freezer/ui/settings_screen.dart'; import 'package:freezer/ui/settings_screen.dart';
import 'package:freezer/ui/tiles.dart'; import 'package:freezer/ui/tiles.dart';
@ -78,7 +80,6 @@ class PlayerScreenHorizontal extends StatefulWidget {
class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> { class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
double iconSize = ScreenUtil().setWidth(64);
bool _lyrics = false; bool _lyrics = false;
@override @override
@ -115,9 +116,9 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
padding: EdgeInsets.fromLTRB(8, 16, 8, 0), padding: EdgeInsets.fromLTRB(8, 16, 8, 0),
child: Container( child: Container(
child: PlayerScreenTopRow( child: PlayerScreenTopRow(
textSize: ScreenUtil().setSp(26), textSize: ScreenUtil().setSp(24),
iconSize: ScreenUtil().setSp(32), iconSize: ScreenUtil().setSp(36),
textWidth: ScreenUtil().setWidth(256), textWidth: ScreenUtil().setWidth(350),
short: true short: true
), ),
) )
@ -166,17 +167,7 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
padding: EdgeInsets.symmetric(horizontal: 16.0), padding: EdgeInsets.symmetric(horizontal: 16.0),
child: SeekBar(), child: SeekBar(),
), ),
Container( PlaybackControls(ScreenUtil().setSp(60)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
PrevNextButton(iconSize, prev: true,),
PlayPauseButton(iconSize),
PrevNextButton(iconSize)
],
),
),
Padding( Padding(
padding: EdgeInsets.fromLTRB(8, 0, 8, 16), padding: EdgeInsets.fromLTRB(8, 0, 8, 16),
child: Container( child: Container(
@ -230,7 +221,6 @@ class PlayerScreenVertical extends StatefulWidget {
} }
class _PlayerScreenVerticalState extends State<PlayerScreenVertical> { class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
double iconSize = ScreenUtil().setWidth(100);
bool _lyrics = false; bool _lyrics = false;
@override @override
@ -240,26 +230,27 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: EdgeInsets.fromLTRB(28, 10, 28, 0), padding: EdgeInsets.fromLTRB(30, 4, 16, 0),
child: PlayerScreenTopRow() child: PlayerScreenTopRow()
), ),
Padding( Padding(
padding: EdgeInsets.fromLTRB(16, 0, 16, 0), padding: EdgeInsets.fromLTRB(16, 0, 16, 0),
child: Container( child: Container(
height: ScreenUtil().setHeight(1050), height: ScreenUtil().setHeight(1000),
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
BigAlbumArt(), BigAlbumArt(),
if (_lyrics) LyricsWidget( if (_lyrics) LyricsWidget(
artUri: AudioService.currentMediaItem.extras['thumb'], artUri: AudioService.currentMediaItem.extras['thumb'],
trackId: AudioService.currentMediaItem.id, trackId: AudioService.currentMediaItem.id,
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics, lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
height: ScreenUtil().setHeight(1050), height: ScreenUtil().setHeight(1000),
), ),
], ],
),
), ),
),
), ),
Container(height: 4.0),
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
@ -301,16 +292,7 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
], ],
), ),
SeekBar(), SeekBar(),
Row( PlaybackControls(ScreenUtil().setWidth(100)),
mainAxisAlignment: MainAxisAlignment.spaceAround,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
PrevNextButton(iconSize, prev: true,),
PlayPauseButton(iconSize),
PrevNextButton(iconSize)
],
),
//Container(height: 8.0,),
Padding( Padding(
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 16.0), padding: EdgeInsets.symmetric(vertical: 0, horizontal: 16.0),
child: Row( child: Row(
@ -351,6 +333,90 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
} }
} }
class PlaybackControls extends StatefulWidget {
final double iconSize;
PlaybackControls(this.iconSize, {Key key}): super(key: key);
@override
_PlaybackControlsState createState() => _PlaybackControlsState();
}
class _PlaybackControlsState extends State<PlaybackControls> {
Icon get repeatIcon {
switch (playerHelper.repeatType) {
case LoopMode.off:
return Icon(
Icons.repeat,
size: widget.iconSize * 0.64
);
case LoopMode.all:
return Icon(
Icons.repeat,
color: Theme.of(context).primaryColor,
size: widget.iconSize * 0.64
);
case LoopMode.one:
return Icon(
Icons.repeat_one,
color: Theme.of(context).primaryColor,
size: widget.iconSize * 0.64,
);
}
}
Icon get libraryIcon {
if (cache.checkTrackFavorite(Track.fromMediaItem(AudioService.currentMediaItem))) {
return Icon(Icons.favorite, size: widget.iconSize * 0.64);
}
return Icon(Icons.favorite_border, size: widget.iconSize * 0.64);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
IconButton(
icon: repeatIcon,
onPressed: () async {
await playerHelper.changeRepeat();
setState(() {});
},
),
PrevNextButton(widget.iconSize, prev: true),
PlayPauseButton(widget.iconSize * 1.25),
PrevNextButton(widget.iconSize),
IconButton(
icon: libraryIcon,
onPressed: () async {
if (cache.libraryTracks == null)
cache.libraryTracks = [];
if (cache.checkTrackFavorite(Track.fromMediaItem(AudioService.currentMediaItem))) {
//Remove from library
setState(() => cache.libraryTracks.remove(AudioService.currentMediaItem.id));
await deezerAPI.removeFavorite(AudioService.currentMediaItem.id);
await cache.save();
} else {
//Add
setState(() => cache.libraryTracks.add(AudioService.currentMediaItem.id));
await deezerAPI.addFavoriteTrack(AudioService.currentMediaItem.id);
await cache.save();
}
},
)
],
),
);
}
}
class BigAlbumArt extends StatefulWidget { class BigAlbumArt extends StatefulWidget {
@override @override
_BigAlbumArtState createState() => _BigAlbumArtState(); _BigAlbumArtState createState() => _BigAlbumArtState();
@ -383,16 +449,23 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PageView( return GestureDetector(
controller: _pageController, onVerticalDragUpdate: (DragUpdateDetails details) {
onPageChanged: (int index) { if (details.delta.dy > 16) {
if (_animationLock) return; Navigator.of(context).pop();
AudioService.skipToQueueItem(AudioService.queue[index].id); }
}, },
children: List.generate(AudioService.queue.length, (i) => CachedImage( child: PageView(
url: AudioService.queue[i].artUri, controller: _pageController,
fullThumb: true, onPageChanged: (int index) {
)), if (_animationLock) return;
AudioService.skipToQueueItem(AudioService.queue[index].id);
},
children: List.generate(AudioService.queue.length, (i) => CachedImage(
url: AudioService.queue[i].artUri,
fullThumb: true,
)),
),
); );
} }
} }
@ -569,51 +642,28 @@ class PlayerScreenTopRow extends StatelessWidget {
return Row( return Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Row( Container(
children: <Widget>[ width: this.textWidth??ScreenUtil().setWidth(800),
Padding( child: Text(
padding: EdgeInsets.fromLTRB(0, 0, 8, 0), (short??false)?(playerHelper.queueSource.text??''):'Playing from:'.i18n + ' ' + (playerHelper.queueSource?.text??''),
child: InkWell( maxLines: 1,
child: Container( overflow: TextOverflow.ellipsis,
padding: EdgeInsets.all(8.0), textAlign: TextAlign.left,
child: Icon(Icons.keyboard_arrow_down, size: this.iconSize??ScreenUtil().setWidth(46)), style: TextStyle(fontSize: this.textSize??ScreenUtil().setSp(38)),
), ),
onTap: () { ),
Navigator.of(context).pop(); IconButton(
}, icon: Icon(Icons.menu),
), iconSize: this.iconSize??ScreenUtil().setSp(52),
), splashRadius: this.iconSize??ScreenUtil().setWidth(52),
Container( onPressed: () {
width: this.textWidth??ScreenUtil().setWidth(550), Navigator.of(context).push(MaterialPageRoute(
child: Text( builder: (context) => QueueScreen()
(short??false)?(playerHelper.queueSource.text??''):'Playing from:'.i18n + ' ' + (playerHelper.queueSource.text??''), ));
maxLines: 1, },
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.left,
style: TextStyle(fontSize: this.textSize??ScreenUtil().setSp(34)),
),
)
],
), ),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
RepeatButton(size: this.iconSize),
Container(width: 16.0,),
InkWell(
child: Container(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.menu, size: this.iconSize??ScreenUtil().setWidth(46)),
),
onTap: (){
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => QueueScreen()
));
},
),
],
)
], ],
); );
} }
@ -621,60 +671,6 @@ class PlayerScreenTopRow extends StatelessWidget {
class RepeatButton extends StatefulWidget {
double size;
RepeatButton({this.size, Key key}): super(key: key);
@override
_RepeatButtonState createState() => _RepeatButtonState();
}
class _RepeatButtonState extends State<RepeatButton> {
double _size = ScreenUtil().setWidth(46);
Icon get icon {
switch (playerHelper.repeatType) {
case LoopMode.off:
return Icon(Icons.repeat, size: widget.size??_size);
case LoopMode.all:
return Icon(
Icons.repeat,
color: Theme.of(context).primaryColor,
size: widget.size??_size
);
case LoopMode.one:
return Icon(
Icons.repeat_one,
color: Theme.of(context).primaryColor,
size: widget.size??_size
);
}
}
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () async {
await playerHelper.changeRepeat();
setState(() {});
},
child: Container(
padding: EdgeInsets.all(8.0),
child: icon,
),
);
}
}
class SeekBar extends StatefulWidget { class SeekBar extends StatefulWidget {
@override @override
_SeekBarState createState() => _SeekBarState(); _SeekBarState createState() => _SeekBarState();
@ -789,8 +785,8 @@ class _QueueScreenState extends State<QueueScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: FreezerAppBar(
title: Text('Queue'.i18n), 'Queue'.i18n,
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
icon: Icon( icon: Icon(

View File

@ -4,6 +4,7 @@ import 'package:freezer/api/cache.dart';
import 'package:freezer/api/download.dart'; import 'package:freezer/api/download.dart';
import 'package:freezer/api/player.dart'; import 'package:freezer/api/player.dart';
import 'package:freezer/ui/details_screens.dart'; import 'package:freezer/ui/details_screens.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/menu.dart'; import 'package:freezer/ui/menu.dart';
import 'package:freezer/translations.i18n.dart'; import 'package:freezer/translations.i18n.dart';
@ -50,6 +51,7 @@ class _SearchScreenState extends State<SearchScreen> {
bool _loading = false; bool _loading = false;
TextEditingController _controller = new TextEditingController(); TextEditingController _controller = new TextEditingController();
List _suggestions = []; List _suggestions = [];
bool _cancel = false;
void _submit(BuildContext context, {String query}) async { void _submit(BuildContext context, {String query}) async {
if (query != null) _query = query; if (query != null) _query = query;
@ -78,7 +80,7 @@ class _SearchScreenState extends State<SearchScreen> {
@override @override
void initState() { void initState() {
print(cache.searchHistory); _cancel = true;
//Check for connectivity and enable offline mode //Check for connectivity and enable offline mode
Connectivity().checkConnectivity().then((res) { Connectivity().checkConnectivity().then((res) {
if (res == ConnectivityResult.none) setState(() { if (res == ConnectivityResult.none) setState(() {
@ -102,24 +104,30 @@ class _SearchScreenState extends State<SearchScreen> {
sugg = await deezerAPI.searchSuggestions(_query); sugg = await deezerAPI.searchSuggestions(_query);
} catch (e) {} } catch (e) {}
if (sugg != null) if (sugg != null && !_cancel)
setState(() => _suggestions = sugg); setState(() => _suggestions = sugg);
} }
@override
void dispose() {
_cancel = true;
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Search'.i18n),), appBar: FreezerAppBar('Search'.i18n),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
Container(height: 16.0), Container(height: 4.0),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0), padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: Stack( child: Stack(
alignment: Alignment(1.0, 1.0), alignment: Alignment(1.0, 0.0),
children: [ children: [
TextField( TextField(
onChanged: (String s) { onChanged: (String s) {
@ -127,37 +135,49 @@ class _SearchScreenState extends State<SearchScreen> {
_loadSuggestions(); _loadSuggestions();
}, },
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Search or paste URL'.i18n labelText: 'Search or paste URL'.i18n,
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey)
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey)
),
), ),
controller: _controller, controller: _controller,
onSubmitted: (String s) => _submit(context, query: s), onSubmitted: (String s) => _submit(context, query: s),
), ),
IconButton( Row(
icon: Icon(Icons.clear), mainAxisSize: MainAxisSize.min,
onPressed: () { children: [
setState(() { Container(
_suggestions = []; width: 40.0,
_query = ''; child: IconButton(
}); splashRadius: 20.0,
_controller.clear(); icon: Icon(Icons.clear),
}, onPressed: () {
), setState(() {
_suggestions = [];
_query = '';
});
_controller.clear();
},
),
),
],
)
], ],
) )
), ),
Padding(
padding: EdgeInsets.fromLTRB(0, 8, 0, 0),
child: IconButton(
icon: Icon(Icons.search),
onPressed: () => _submit(context),
),
)
], ],
), ),
), ),
Container(height: 8.0),
ListTile( ListTile(
title: Text('Offline search'.i18n), title: Text('Offline search'.i18n),
leading: Switch( leading: Icon(Icons.offline_pin),
trailing: Switch(
value: _offline, value: _offline,
onChanged: (v) { onChanged: (v) {
setState(() => _offline = !_offline); setState(() => _offline = !_offline);
@ -166,7 +186,7 @@ class _SearchScreenState extends State<SearchScreen> {
), ),
if (_loading) if (_loading)
LinearProgressIndicator(), LinearProgressIndicator(),
Divider(), FreezerDivider(),
//History //History
if (cache.searchHistory != null && cache.searchHistory.length > 0 && (_query??'').length == 0) if (cache.searchHistory != null && cache.searchHistory.length > 0 && (_query??'').length == 0)
@ -213,9 +233,7 @@ class SearchResultsScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: FreezerAppBar('Search Results'.i18n),
title: Text('Search Results'.i18n),
),
body: FutureBuilder( body: FutureBuilder(
future: _search(), future: _search(),
builder: (BuildContext context, AsyncSnapshot snapshot) { builder: (BuildContext context, AsyncSnapshot snapshot) {
@ -243,12 +261,15 @@ class SearchResultsScreen extends StatelessWidget {
List<Widget> tracks = []; List<Widget> tracks = [];
if (results.tracks != null && results.tracks.length != 0) { if (results.tracks != null && results.tracks.length != 0) {
tracks = [ tracks = [
Text( Padding(
'Tracks'.i18n, padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
textAlign: TextAlign.center, child: Text(
style: TextStyle( 'Tracks'.i18n,
fontSize: 26.0, textAlign: TextAlign.left,
fontWeight: FontWeight.bold style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold
),
), ),
), ),
...List.generate(3, (i) { ...List.generate(3, (i) {
@ -280,7 +301,8 @@ class SearchResultsScreen extends StatelessWidget {
))) )))
); );
}, },
) ),
FreezerDivider()
]; ];
} }
@ -288,12 +310,15 @@ class SearchResultsScreen extends StatelessWidget {
List<Widget> albums = []; List<Widget> albums = [];
if (results.albums != null && results.albums.length != 0) { if (results.albums != null && results.albums.length != 0) {
albums = [ albums = [
Text( Padding(
'Albums'.i18n, padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
textAlign: TextAlign.center, child: Text(
style: TextStyle( 'Albums'.i18n,
fontSize: 26.0, textAlign: TextAlign.left,
fontWeight: FontWeight.bold style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold
),
), ),
), ),
...List.generate(3, (i) { ...List.generate(3, (i) {
@ -319,7 +344,8 @@ class SearchResultsScreen extends StatelessWidget {
MaterialPageRoute(builder: (context) => AlbumListScreen(results.albums)) MaterialPageRoute(builder: (context) => AlbumListScreen(results.albums))
); );
}, },
) ),
FreezerDivider()
]; ];
} }
@ -327,12 +353,15 @@ class SearchResultsScreen extends StatelessWidget {
List<Widget> artists = []; List<Widget> artists = [];
if (results.artists != null && results.artists.length != 0) { if (results.artists != null && results.artists.length != 0) {
artists = [ artists = [
Text( Padding(
'Artists'.i18n, padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
textAlign: TextAlign.center, child: Text(
style: TextStyle( 'Artists'.i18n,
fontSize: 26.0, textAlign: TextAlign.left,
fontWeight: FontWeight.bold style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold
),
), ),
), ),
Container(height: 4), Container(height: 4),
@ -355,7 +384,8 @@ class SearchResultsScreen extends StatelessWidget {
); );
}), }),
) )
) ),
FreezerDivider()
]; ];
} }
@ -363,12 +393,15 @@ class SearchResultsScreen extends StatelessWidget {
List<Widget> playlists = []; List<Widget> playlists = [];
if (results.playlists != null && results.playlists.length != 0) { if (results.playlists != null && results.playlists.length != 0) {
playlists = [ playlists = [
Text( Padding(
'Playlists'.i18n, padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
textAlign: TextAlign.center, child: Text(
style: TextStyle( 'Playlists'.i18n,
fontSize: 26.0, textAlign: TextAlign.left,
fontWeight: FontWeight.bold style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold
),
), ),
), ),
...List.generate(3, (i) { ...List.generate(3, (i) {
@ -427,7 +460,7 @@ class TrackListScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Tracks'.i18n),), appBar: FreezerAppBar('Tracks'.i18n),
body: ListView.builder( body: ListView.builder(
itemCount: tracks.length, itemCount: tracks.length,
itemBuilder: (BuildContext context, int i) { itemBuilder: (BuildContext context, int i) {
@ -457,7 +490,7 @@ class AlbumListScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Albums'.i18n),), appBar: FreezerAppBar('Albums'.i18n),
body: ListView.builder( body: ListView.builder(
itemCount: albums.length, itemCount: albums.length,
itemBuilder: (context, i) { itemBuilder: (context, i) {
@ -488,7 +521,7 @@ class SearchResultPlaylists extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Playlists'.i18n),), appBar: FreezerAppBar('Playlists'.i18n),
body: ListView.builder( body: ListView.builder(
itemCount: playlists.length, itemCount: playlists.length,
itemBuilder: (context, i) { itemBuilder: (context, i) {

View File

@ -12,6 +12,7 @@ import 'package:freezer/api/cache.dart';
import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/download.dart'; import 'package:freezer/api/download.dart';
import 'package:freezer/ui/downloads_screen.dart'; import 'package:freezer/ui/downloads_screen.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/error.dart'; import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/home_screen.dart'; import 'package:freezer/ui/home_screen.dart';
import 'package:i18n_extension/i18n_widget.dart'; import 'package:i18n_extension/i18n_widget.dart';
@ -56,26 +57,26 @@ class _SettingsScreenState extends State<SettingsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Settings'.i18n),), appBar: FreezerAppBar('Settings'.i18n),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
title: Text('General'.i18n), title: Text('General'.i18n),
leading: Icon(Icons.settings), leading: LeadingIcon(Icons.settings, color: Color(0xffeca704)),
onTap: () => Navigator.of(context).push(MaterialPageRoute( onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => GeneralSettings() builder: (context) => GeneralSettings()
)), )),
), ),
ListTile( ListTile(
title: Text('Download Settings'.i18n), title: Text('Download Settings'.i18n),
leading: Icon(Icons.cloud_download), leading: LeadingIcon(Icons.cloud_download, color: Color(0xffbe3266)),
onTap: () => Navigator.of(context).push(MaterialPageRoute( onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => DownloadsSettings() builder: (context) => DownloadsSettings()
)), )),
), ),
ListTile( ListTile(
title: Text('Appearance'.i18n), title: Text('Appearance'.i18n),
leading: Icon(Icons.color_lens), leading: LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)),
onTap: () => Navigator.push( onTap: () => Navigator.push(
context, context,
MaterialPageRoute(builder: (context) => AppearanceSettings()) MaterialPageRoute(builder: (context) => AppearanceSettings())
@ -83,7 +84,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
ListTile( ListTile(
title: Text('Quality'.i18n), title: Text('Quality'.i18n),
leading: Icon(Icons.high_quality), leading: LeadingIcon(Icons.high_quality, color: Color(0xff384697)),
onTap: () => Navigator.push( onTap: () => Navigator.push(
context, context,
MaterialPageRoute(builder: (context) => QualitySettings()) MaterialPageRoute(builder: (context) => QualitySettings())
@ -91,7 +92,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
ListTile( ListTile(
title: Text('Deezer'.i18n), title: Text('Deezer'.i18n),
leading: Icon(Icons.equalizer), leading: LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)),
onTap: () => Navigator.push(context, MaterialPageRoute( onTap: () => Navigator.push(context, MaterialPageRoute(
builder: (context) => DeezerSettings() builder: (context) => DeezerSettings()
)), )),
@ -99,7 +100,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
//Language select //Language select
ListTile( ListTile(
title: Text('Language'.i18n), title: Text('Language'.i18n),
leading: Icon(Icons.language), leading: LeadingIcon(Icons.language, color: Color(0xff009a85)),
onTap: () { onTap: () {
showDialog( showDialog(
context: context, context: context,
@ -140,7 +141,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
ListTile( ListTile(
title: Text('About'.i18n), title: Text('About'.i18n),
leading: Icon(Icons.info), leading: LeadingIcon(Icons.info, color: Color(0xff2ba766)),
onTap: () => Navigator.push(context, MaterialPageRoute( onTap: () => Navigator.push(context, MaterialPageRoute(
builder: (context) => CreditsScreen() builder: (context) => CreditsScreen()
)), )),
@ -164,7 +165,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Appearance'.i18n),), appBar: FreezerAppBar('Appearance'.i18n),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
@ -222,19 +223,17 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
), ),
ListTile( ListTile(
title: Text('Use system theme'.i18n), title: Text('Use system theme'.i18n),
leading: Container( trailing: Switch(
width: 30.0, value: settings.useSystemTheme,
child: Checkbox( onChanged: (bool v) async {
value: settings.useSystemTheme, setState(() {
onChanged: (bool v) async { settings.useSystemTheme = v;
setState(() { });
settings.useSystemTheme = v; updateTheme();
}); await settings.save();
updateTheme(); },
await settings.save();
},
),
), ),
leading: Icon(Icons.android)
), ),
ListTile( ListTile(
title: Text('Primary color'.i18n), title: Text('Primary color'.i18n),
@ -285,12 +284,10 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
ListTile( ListTile(
title: Text('Use album art primary color'.i18n), title: Text('Use album art primary color'.i18n),
subtitle: Text('Warning: might be buggy'.i18n), subtitle: Text('Warning: might be buggy'.i18n),
leading: Container( leading: Icon(Icons.invert_colors),
width: 30.0, trailing: Switch(
child: Checkbox( value: settings.useArtColor,
value: settings.useArtColor, onChanged: (v) => setState(() => settings.updateUseArtColor(v)),
onChanged: (v) => setState(() => settings.updateUseArtColor(v)),
),
), ),
) )
], ],
@ -309,30 +306,30 @@ class _QualitySettingsState extends State<QualitySettings> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Quality'.i18n),), appBar: FreezerAppBar('Quality'.i18n),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
title: Text('Mobile streaming'.i18n), title: Text('Mobile streaming'.i18n),
leading: Icon(Icons.network_cell), leading: LeadingIcon(Icons.network_cell, color: Color(0xff384697)),
), ),
QualityPicker('mobile'), QualityPicker('mobile'),
Divider(), FreezerDivider(),
ListTile( ListTile(
title: Text('Wifi streaming'.i18n), title: Text('Wifi streaming'.i18n),
leading: Icon(Icons.network_wifi), leading: LeadingIcon(Icons.network_wifi, color: Color(0xff0880b5)),
), ),
QualityPicker('wifi'), QualityPicker('wifi'),
Divider(), FreezerDivider(),
ListTile( ListTile(
title: Text('Offline'.i18n), title: Text('Offline'.i18n),
leading: Icon(Icons.offline_pin), leading: LeadingIcon(Icons.offline_pin, color: Color(0xff009a85)),
), ),
QualityPicker('offline'), QualityPicker('offline'),
Divider(), FreezerDivider(),
ListTile( ListTile(
title: Text('External downloads'.i18n), title: Text('External downloads'.i18n),
leading: Icon(Icons.file_download), leading: LeadingIcon(Icons.file_download, color: Color(0xff2ba766)),
), ),
QualityPicker('download'), QualityPicker('download'),
], ],
@ -425,6 +422,15 @@ class _QualityPickerState extends State<QualityPicker> {
onChanged: (q) => _updateQuality(q), onChanged: (q) => _updateQuality(q),
), ),
), ),
if (widget.field == 'download')
ListTile(
title: Text('Ask before downloading'),
leading: Radio(
groupValue: _quality,
value: AudioQuality.ASK,
onChanged: (q) => _updateQuality(q),
)
)
], ],
); );
} }
@ -439,7 +445,7 @@ class _DeezerSettingsState extends State<DeezerSettings> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Deezer'.i18n),), appBar: FreezerAppBar('Deezer'.i18n),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
@ -485,65 +491,64 @@ class _DeezerSettingsState extends State<DeezerSettings> {
ListTile( ListTile(
title: Text('Log tracks'.i18n), title: Text('Log tracks'.i18n),
subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'.i18n), subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'.i18n),
leading: Container( trailing: Switch(
width: 30, value: settings.logListen,
child: Checkbox( onChanged: (bool v) {
value: settings.logListen, setState(() => settings.logListen = v);
onChanged: (bool v) { settings.save();
setState(() => settings.logListen = v); },
settings.save();
},
),
), ),
leading: Icon(Icons.history_toggle_off),
), ),
ListTile( //TODO: Reimplement proxy
title: Text('Proxy'.i18n), // ListTile(
leading: Icon(Icons.vpn_key), // title: Text('Proxy'.i18n),
subtitle: Text(settings.proxyAddress??'Not set'.i18n), // leading: Icon(Icons.vpn_key),
onTap: () { // subtitle: Text(settings.proxyAddress??'Not set'.i18n),
String _new; // onTap: () {
showDialog( // String _new;
context: context, // showDialog(
builder: (BuildContext context) { // context: context,
return AlertDialog( // builder: (BuildContext context) {
title: Text('Proxy'.i18n), // return AlertDialog(
content: TextField( // title: Text('Proxy'.i18n),
onChanged: (String v) => _new = v, // content: TextField(
decoration: InputDecoration( // onChanged: (String v) => _new = v,
hintText: 'IP:PORT' // decoration: InputDecoration(
), // hintText: 'IP:PORT'
), // ),
actions: [ // ),
FlatButton( // actions: [
child: Text('Cancel'.i18n), // FlatButton(
onPressed: () => Navigator.of(context).pop(), // child: Text('Cancel'.i18n),
), // onPressed: () => Navigator.of(context).pop(),
FlatButton( // ),
child: Text('Reset'.i18n), // FlatButton(
onPressed: () async { // child: Text('Reset'.i18n),
setState(() { // onPressed: () async {
settings.proxyAddress = null; // setState(() {
}); // settings.proxyAddress = null;
await settings.save(); // });
Navigator.of(context).pop(); // await settings.save();
}, // Navigator.of(context).pop();
), // },
FlatButton( // ),
child: Text('Save'.i18n), // FlatButton(
onPressed: () async { // child: Text('Save'.i18n),
setState(() { // onPressed: () async {
settings.proxyAddress = _new; // setState(() {
}); // settings.proxyAddress = _new;
await settings.save(); // });
Navigator.of(context).pop(); // await settings.save();
}, // Navigator.of(context).pop();
) // },
], // )
); // ],
} // );
); // }
}, // );
) // },
// )
], ],
), ),
); );
@ -562,7 +567,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Download Settings'.i18n),), appBar: FreezerAppBar('Download Settings'.i18n),
body: ListView( body: ListView(
children: [ children: [
ListTile( ListTile(
@ -696,128 +701,110 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
} }
} }
), ),
Divider(), FreezerDivider(),
ListTile( ListTile(
title: Text('Create folders for artist'.i18n), title: Text('Create folders for artist'.i18n),
leading: Container( trailing: Switch(
width: 30.0, value: settings.artistFolder,
child: Checkbox( onChanged: (v) {
value: settings.artistFolder, setState(() => settings.artistFolder = v);
onChanged: (v) { settings.save();
setState(() => settings.artistFolder = v); },
settings.save();
},
),
), ),
leading: Icon(Icons.folder),
), ),
ListTile( ListTile(
title: Text('Create folders for albums'.i18n), title: Text('Create folders for albums'.i18n),
leading: Container( trailing: Switch(
width: 30.0, value: settings.albumFolder,
child: Checkbox( onChanged: (v) {
value: settings.albumFolder, setState(() => settings.albumFolder = v);
onChanged: (v) { settings.save();
setState(() => settings.albumFolder = v); },
settings.save();
},
),
), ),
leading: Icon(Icons.folder)
), ),
ListTile( ListTile(
title: Text('Create folder for playlist'.i18n), title: Text('Create folder for playlist'.i18n),
leading: Container( trailing: Switch(
width: 30.0, value: settings.playlistFolder,
child: Checkbox( onChanged: (v) {
value: settings.playlistFolder, setState(() => settings.playlistFolder = v);
onChanged: (v) { settings.save();
setState(() => settings.playlistFolder = v); },
settings.save();
},
),
), ),
leading: Icon(Icons.folder)
), ),
Divider(), FreezerDivider(),
ListTile( ListTile(
title: Text('Separate albums by discs'.i18n), title: Text('Separate albums by discs'.i18n),
leading: Container( trailing: Switch(
width: 30.0, value: settings.albumDiscFolder,
child: Checkbox( onChanged: (v) {
value: settings.albumDiscFolder, setState(() => settings.albumDiscFolder = v);
onChanged: (v) { settings.save();
setState(() => settings.albumDiscFolder = v); },
settings.save();
},
),
), ),
leading: Icon(Icons.album)
), ),
ListTile( ListTile(
title: Text('Overwrite already downloaded files'.i18n), title: Text('Overwrite already downloaded files'.i18n),
leading: Container( trailing: Switch(
width: 30.0, value: settings.overwriteDownload,
child: Checkbox( onChanged: (v) {
value: settings.overwriteDownload, setState(() => settings.overwriteDownload = v);
onChanged: (v) { settings.save();
setState(() => settings.overwriteDownload = v); },
settings.save();
},
),
), ),
leading: Icon(Icons.delete)
), ),
ListTile( ListTile(
title: Text('Download .LRC lyrics'.i18n), title: Text('Download .LRC lyrics'.i18n),
leading: Container( trailing: Switch(
width: 30.0, value: settings.downloadLyrics,
child: Checkbox( onChanged: (v) {
value: settings.downloadLyrics, setState(() => settings.downloadLyrics = v);
onChanged: (v) { settings.save();
setState(() => settings.downloadLyrics = v); },
settings.save();
},
),
), ),
leading: Icon(Icons.subtitles)
), ),
Divider(), FreezerDivider(),
ListTile( ListTile(
title: Text('Save cover file for every track'.i18n), title: Text('Save cover file for every track'.i18n),
leading: Container( trailing: Switch(
width: 30.0, value: settings.trackCover,
child: Checkbox( onChanged: (v) {
value: settings.trackCover, setState(() => settings.trackCover = v);
onChanged: (v) { settings.save();
setState(() => settings.trackCover = v); },
settings.save();
},
),
), ),
leading: Icon(Icons.image)
), ),
ListTile( ListTile(
title: Text('Save album cover'.i18n), title: Text('Save album cover'.i18n),
leading: Container( trailing: Switch(
width: 30.0, value: settings.albumCover,
child: Checkbox( onChanged: (v) {
value: settings.albumCover, setState(() => settings.albumCover = v);
onChanged: (v) { settings.save();
setState(() => settings.albumCover = v); },
settings.save();
},
),
), ),
leading: Icon(Icons.image)
), ),
ListTile( ListTile(
title: Text('Create .nomedia files'.i18n), title: Text('Create .nomedia files'.i18n),
subtitle: Text('To prevent gallery being filled with album art'.i18n), subtitle: Text('To prevent gallery being filled with album art'.i18n),
leading: Container( trailing: Switch(
width: 30.0, value: settings.nomediaFiles,
child: Checkbox( onChanged: (v) {
value: settings.nomediaFiles, setState(() => settings.nomediaFiles = v);
onChanged: (v) { settings.save();
setState(() => settings.nomediaFiles = v); },
settings.save();
},
),
), ),
leading: Icon(Icons.insert_drive_file)
), ),
Divider(), FreezerDivider(),
ListTile( ListTile(
title: Text('Download Log'.i18n), title: Text('Download Log'.i18n),
leading: Icon(Icons.sticky_note_2), leading: Icon(Icons.sticky_note_2),
@ -841,51 +828,49 @@ class _GeneralSettingsState extends State<GeneralSettings> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('General'.i18n),), appBar: FreezerAppBar('General'.i18n),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
title: Text('Offline mode'.i18n), title: Text('Offline mode'.i18n),
subtitle: Text('Will be overwritten on start.'.i18n), subtitle: Text('Will be overwritten on start.'.i18n),
leading: Container( trailing: Switch(
width: 30.0, value: settings.offlineMode,
child: Checkbox( onChanged: (bool v) {
value: settings.offlineMode, if (v) {
onChanged: (bool v) { setState(() => settings.offlineMode = true);
if (v) { return;
setState(() => settings.offlineMode = true); }
return; showDialog(
} context: context,
showDialog( builder: (context) {
context: context, deezerAPI.authorize().then((v) {
builder: (context) { if (v) {
deezerAPI.authorize().then((v) { setState(() => settings.offlineMode = false);
if (v) { } else {
setState(() => settings.offlineMode = false); Fluttertoast.showToast(
} else { msg: 'Error logging in, check your internet connections.'.i18n,
Fluttertoast.showToast( gravity: ToastGravity.BOTTOM,
msg: 'Error logging in, check your internet connections.'.i18n, toastLength: Toast.LENGTH_SHORT
gravity: ToastGravity.BOTTOM, );
toastLength: Toast.LENGTH_SHORT }
); Navigator.of(context).pop();
} });
Navigator.of(context).pop(); return AlertDialog(
}); title: Text('Logging in...'.i18n),
return AlertDialog( content: Row(
title: Text('Logging in...'.i18n), mainAxisSize: MainAxisSize.max,
content: Row( mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max, children: <Widget>[
mainAxisAlignment: MainAxisAlignment.center, CircularProgressIndicator()
children: <Widget>[ ],
CircularProgressIndicator() )
], );
) }
); );
} },
);
},
),
), ),
leading: Icon(Icons.lock),
), ),
ListTile( ListTile(
title: Text('Copy ARL'.i18n), title: Text('Copy ARL'.i18n),
@ -933,6 +918,18 @@ class _GeneralSettingsState extends State<GeneralSettings> {
} }
); );
} }
),
ListTile(
title: Text('Ignore interruptions'.i18n),
subtitle: Text('Requires app restart to apply!'.i18n),
leading: Icon(Icons.not_interested),
trailing: Switch(
value: settings.ignoreInterruptions,
onChanged: (bool v) async {
setState(() => settings.ignoreInterruptions = v);
await settings.save();
},
),
) )
], ],
), ),
@ -965,8 +962,8 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: FreezerAppBar(
title: Text('Pick-a-Path'.i18n), 'Pick-a-Path'.i18n,
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
icon: Icon(Icons.sd_card), icon: Icon(Icons.sd_card),
@ -1110,7 +1107,8 @@ class _CreditsScreenState extends State<CreditsScreen> {
['kobyrevah', 'Hebrew'], ['kobyrevah', 'Hebrew'],
['HoScHaKaL', 'Turkish'], ['HoScHaKaL', 'Turkish'],
['MicroMihai', 'Romanian'], ['MicroMihai', 'Romanian'],
['LenteraMalam', 'Indonesian'] ['LenteraMalam', 'Indonesian'],
['RTWO2', 'Persian']
]; ];
@override @override
@ -1126,9 +1124,6 @@ class _CreditsScreenState extends State<CreditsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(
title: Text('About'.i18n),
),
body: ListView( body: ListView(
children: [ children: [
FreezerTitle(), FreezerTitle(),
@ -1139,7 +1134,7 @@ class _CreditsScreenState extends State<CreditsScreen> {
fontStyle: FontStyle.italic fontStyle: FontStyle.italic
), ),
), ),
Divider(), FreezerDivider(),
ListTile( ListTile(
title: Text('Telegram Channel'.i18n), title: Text('Telegram Channel'.i18n),
subtitle: Text('To get latest releases'.i18n), subtitle: Text('To get latest releases'.i18n),
@ -1164,7 +1159,7 @@ class _CreditsScreenState extends State<CreditsScreen> {
launch('https://notabug.org/exttex/freezer'); launch('https://notabug.org/exttex/freezer');
}, },
), ),
Divider(), FreezerDivider(),
ListTile( ListTile(
title: Text('exttex'), title: Text('exttex'),
subtitle: Text('Developer'), subtitle: Text('Developer'),
@ -1203,7 +1198,7 @@ class _CreditsScreenState extends State<CreditsScreen> {
title: Text('Annexhack'), title: Text('Annexhack'),
subtitle: Text('Android Auto help'), subtitle: Text('Android Auto help'),
), ),
Divider(), FreezerDivider(),
...List.generate(translators.length, (i) => ListTile( ...List.generate(translators.length, (i) => ListTile(
title: Text(translators[i][0]), title: Text(translators[i][0]),
subtitle: Text(translators[i][1]), subtitle: Text(translators[i][1]),

View File

@ -1,11 +1,14 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/translations.i18n.dart';
import '../api/definitions.dart'; import '../api/definitions.dart';
import 'cached_image.dart'; import 'cached_image.dart';
import 'dart:async';
class TrackTile extends StatefulWidget { class TrackTile extends StatefulWidget {
final Track track; final Track track;
@ -132,6 +135,8 @@ class ArtistTile extends StatelessWidget {
return SizedBox( return SizedBox(
width: 150, width: 150,
child: Card( child: Card(
color: Theme.of(context).scaffoldBackgroundColor,
elevation: 0.0,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
onLongPress: onHold, onLongPress: onHold,
@ -144,7 +149,7 @@ class ArtistTile extends StatelessWidget {
circular: true, circular: true,
width: 100, width: 100,
), ),
Container(height: 4,), Container(height: 8,),
Text( Text(
artist.name, artist.name,
maxLines: 1, maxLines: 1,
@ -172,6 +177,13 @@ class PlaylistTile extends StatelessWidget {
PlaylistTile(this.playlist, {this.onHold, this.onTap, this.trailing}); PlaylistTile(this.playlist, {this.onHold, this.onTap, this.trailing});
String get subtitle {
if (playlist.user == null || playlist.user.name == null || playlist.user.name == '' || playlist.user.id == deezerAPI.userId) {
return '${playlist.trackCount} ' + 'Tracks'.i18n;
}
return playlist.user.name;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
@ -180,7 +192,7 @@ class PlaylistTile extends StatelessWidget {
maxLines: 1, maxLines: 1,
), ),
subtitle: Text( subtitle: Text(
playlist.user.name, subtitle,
maxLines: 1, maxLines: 1,
), ),
leading: CachedImage( leading: CachedImage(
@ -234,6 +246,8 @@ class PlaylistCardTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
color: Theme.of(context).scaffoldBackgroundColor,
elevation: 0.0,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
onLongPress: onHold, onLongPress: onHold,
@ -245,8 +259,10 @@ class PlaylistCardTile extends StatelessWidget {
url: playlist.image.thumb, url: playlist.image.thumb,
width: 128, width: 128,
height: 128, height: 128,
rounded: true,
), ),
), ),
Container(height: 2.0),
Container( Container(
width: 144, width: 144,
child: Text( child: Text(
@ -257,7 +273,7 @@ class PlaylistCardTile extends StatelessWidget {
style: TextStyle(fontSize: 14.0), style: TextStyle(fontSize: 14.0),
), ),
), ),
Container(height: 8.0,) Container(height: 4.0,)
], ],
), ),
) )
@ -276,6 +292,8 @@ class SmartTrackListTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
elevation: 0,
color: Theme.of(context).scaffoldBackgroundColor,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
onLongPress: onHold, onLongPress: onHold,
@ -287,6 +305,7 @@ class SmartTrackListTile extends StatelessWidget {
width: 128, width: 128,
height: 128, height: 128,
url: smartTrackList.cover.thumb, url: smartTrackList.cover.thumb,
rounded: true,
), ),
), ),
Container( Container(
@ -320,6 +339,8 @@ class AlbumCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
color: Theme.of(context).scaffoldBackgroundColor,
elevation: 0.0,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
onLongPress: onHold, onLongPress: onHold,
@ -331,6 +352,7 @@ class AlbumCard extends StatelessWidget {
width: 128.0, width: 128.0,
height: 128.0, height: 128.0,
url: album.art.thumb, url: album.art.thumb,
rounded: true
), ),
), ),
Container( Container(
@ -345,6 +367,20 @@ class AlbumCard extends StatelessWidget {
), ),
), ),
), ),
Container(height: 4.0),
Container(
width: 144.0,
child: Text(
album.artistString,
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12.0,
color: (Theme.of(context).brightness == Brightness.light) ? Colors.grey[800] : Colors.white70
),
),
),
Container(height: 8.0,) Container(height: 8.0,)
], ],
), ),
@ -366,28 +402,31 @@ class ChannelTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Padding(
color: channel.backgroundColor, padding: EdgeInsets.symmetric(horizontal: 4.0),
child: InkWell( child: Card(
onTap: this.onTap, color: channel.backgroundColor,
child: Container( child: InkWell(
width: 150, onTap: this.onTap,
height: 75, child: Container(
child: Center( width: 150,
child: Text( height: 75,
channel.title, child: Center(
textAlign: TextAlign.center, child: Text(
maxLines: 2, channel.title,
overflow: TextOverflow.ellipsis, textAlign: TextAlign.center,
style: TextStyle( maxLines: 2,
fontSize: 18.0, overflow: TextOverflow.ellipsis,
fontWeight: FontWeight.bold, style: TextStyle(
color: _textColor() fontSize: 18.0,
fontWeight: FontWeight.bold,
color: _textColor()
),
), ),
), ),
), ),
), )
) ),
); );
} }
} }

View File

@ -246,20 +246,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.7" version: "1.3.7"
dio:
dependency: "direct main"
description:
name: dio
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.10"
dio_cookie_manager:
dependency: "direct main"
description:
name: dio_cookie_manager
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
disk_space: disk_space:
dependency: "direct main" dependency: "direct main"
description: description:
@ -428,7 +414,7 @@ packages:
source: hosted source: hosted
version: "0.14.0+4" version: "0.14.0+4"
http: http:
dependency: transitive dependency: "direct main"
description: description:
name: http name: http
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"

View File

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.5.10+1 version: 0.6.0+1
environment: environment:
sdk: ">=2.8.0 <3.0.0" sdk: ">=2.8.0 <3.0.0"
@ -27,8 +27,7 @@ dependencies:
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
dio: ^3.0.10 http: ^0.12.2
dio_cookie_manager: ^1.0.0
cookie_jar: ^1.0.1 cookie_jar: ^1.0.1
json_annotation: ^3.0.1 json_annotation: ^3.0.1
path_provider: 1.6.10 path_provider: 1.6.10

View File

@ -18,7 +18,9 @@ lang_crowdin = {
'ro': 'ro_ro', 'ro': 'ro_ro',
'ru': 'ru_ru', 'ru': 'ru_ru',
'tr': 'tr_tr', 'tr': 'tr_tr',
'pl': 'pl_pl' 'pl': 'pl_pl',
'uk': 'uk_ua',
'hu': 'hu_hu'
} }
def generate_dart(): def generate_dart():
@ -30,7 +32,7 @@ def generate_dart():
lang = file.split('/')[0] lang = file.split('/')[0]
out[lang_crowdin[lang]] = json.loads(data) out[lang_crowdin[lang]] = json.loads(data)
with open('crowdin.dart', 'w') as f: with open('../lib/languages/crowdin.dart', 'w') as f:
data = json.dumps(out, ensure_ascii=False).replace('$', '\\$') data = json.dumps(out, ensure_ascii=False).replace('$', '\\$')
out = f'const crowdin = {data};' out = f'const crowdin = {data};'
f.write(out) f.write(out)