0.6.0 - Redesign, downloads, tagging fixes, download quality selector...
This commit is contained in:
parent
bcf709e56d
commit
1384aedb35
|
@ -8,11 +8,13 @@ import org.jaudiotagger.audio.AudioFileIO;
|
|||
import org.jaudiotagger.tag.FieldKey;
|
||||
import org.jaudiotagger.tag.Tag;
|
||||
import org.jaudiotagger.tag.TagOptionSingleton;
|
||||
import org.jaudiotagger.tag.datatype.Artwork;
|
||||
import org.jaudiotagger.tag.flac.FlacTag;
|
||||
import org.jaudiotagger.tag.id3.ID3v23Tag;
|
||||
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.vorbiscomment.VorbisCommentFieldKey;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
@ -119,6 +121,12 @@ public class Deezer {
|
|||
data += scanner.nextLine();
|
||||
}
|
||||
|
||||
//End
|
||||
try {
|
||||
connection.disconnect();
|
||||
scanner.close();
|
||||
} catch (Exception e) {}
|
||||
|
||||
//Parse JSON
|
||||
JSONObject out = new JSONObject(data);
|
||||
|
||||
|
@ -156,6 +164,12 @@ public class Deezer {
|
|||
data += scanner.nextLine();
|
||||
}
|
||||
|
||||
//Close
|
||||
try {
|
||||
connection.disconnect();
|
||||
scanner.close();
|
||||
} catch (Exception e) {}
|
||||
|
||||
//Parse JSON
|
||||
JSONObject out = new JSONObject(data);
|
||||
return out;
|
||||
|
@ -252,9 +266,11 @@ public class Deezer {
|
|||
String artists = "";
|
||||
String feats = "";
|
||||
for (int i=0; i<publicTrack.getJSONArray("contributors").length(); i++) {
|
||||
artists += ", " + publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name");
|
||||
if (i > 0)
|
||||
feats += ", " + publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name");
|
||||
String artist = publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name");
|
||||
if (!artists.contains(artist))
|
||||
artists += ", " + artist;
|
||||
if (i > 0 && !artists.contains(artist) && !feats.contains(artist))
|
||||
feats += ", " + artist;
|
||||
}
|
||||
original = original.replaceAll("%artists%", sanitize(artists).substring(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("%date%", publicTrack.getString("release_date"));
|
||||
|
||||
//Remove leading dots
|
||||
original = original.replaceAll("/\\.+", "/");
|
||||
|
||||
if (newQuality == 9) return original + ".flac";
|
||||
return original + ".mp3";
|
||||
}
|
||||
|
@ -286,7 +305,7 @@ public class Deezer {
|
|||
}
|
||||
|
||||
//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);
|
||||
//Load file
|
||||
AudioFile f = AudioFileIO.read(new File(path));
|
||||
|
@ -302,7 +321,9 @@ public class Deezer {
|
|||
//Artist
|
||||
String artists = "";
|
||||
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.setField(FieldKey.TRACK, Integer.toString(publicTrack.getInt("track_position")));
|
||||
|
@ -337,66 +358,70 @@ public class Deezer {
|
|||
tag.setField(FieldKey.GENRE, genres.substring(2));
|
||||
|
||||
//Additional tags from private api
|
||||
if (privateJson != null && privateJson.has("SNG_CONTRIBUTORS")) {
|
||||
JSONObject contrib = privateJson.getJSONObject("SNG_CONTRIBUTORS");
|
||||
//Composer
|
||||
if (contrib.has("composer")) {
|
||||
JSONArray composers = contrib.getJSONArray("composer");
|
||||
String composer = "";
|
||||
for (int i=0; i<composers.length(); i++)
|
||||
composer += ", " + composers.getString(i);
|
||||
if (composer.length() > 2)
|
||||
tag.setField(FieldKey.COMPOSER, composer.substring(2));
|
||||
}
|
||||
//Engineer
|
||||
if (contrib.has("engineer")) {
|
||||
JSONArray engineers = contrib.getJSONArray("engineer");
|
||||
String engineer = "";
|
||||
for (int i=0; i<engineers.length(); i++)
|
||||
engineer += ", " + engineers.getString(i);
|
||||
if (engineer.length() > 2)
|
||||
tag.setField(FieldKey.ENGINEER, engineer.substring(2));
|
||||
}
|
||||
//Mixer
|
||||
if (contrib.has("mixer")) {
|
||||
JSONArray mixers = contrib.getJSONArray("mixer");
|
||||
String mixer = "";
|
||||
for (int i=0; i<mixers.length(); i++)
|
||||
mixer += ", " + mixers.getString(i);
|
||||
if (mixer.length() > 2)
|
||||
tag.setField(FieldKey.MIXER, mixer.substring(2));
|
||||
}
|
||||
//Producer
|
||||
if (contrib.has("producer")) {
|
||||
JSONArray producers = contrib.getJSONArray("producer");
|
||||
String producer = "";
|
||||
for (int i=0; i<producers.length(); i++)
|
||||
producer += ", " + producers.getString(i);
|
||||
if (producer.length() > 2)
|
||||
tag.setField(FieldKey.MIXER, producer.substring(2));
|
||||
}
|
||||
try {
|
||||
if (privateJson != null && privateJson.has("SNG_CONTRIBUTORS")) {
|
||||
JSONObject contrib = privateJson.getJSONObject("SNG_CONTRIBUTORS");
|
||||
//Composer
|
||||
if (contrib.has("composer")) {
|
||||
JSONArray composers = contrib.getJSONArray("composer");
|
||||
String composer = "";
|
||||
for (int i = 0; i < composers.length(); i++)
|
||||
composer += ", " + composers.getString(i);
|
||||
if (composer.length() > 2)
|
||||
tag.setField(FieldKey.COMPOSER, composer.substring(2));
|
||||
}
|
||||
//Engineer
|
||||
if (contrib.has("engineer")) {
|
||||
JSONArray engineers = contrib.getJSONArray("engineer");
|
||||
String engineer = "";
|
||||
for (int i = 0; i < engineers.length(); i++)
|
||||
engineer += ", " + engineers.getString(i);
|
||||
if (engineer.length() > 2)
|
||||
tag.setField(FieldKey.ENGINEER, engineer.substring(2));
|
||||
}
|
||||
//Mixer
|
||||
if (contrib.has("mixer")) {
|
||||
JSONArray mixers = contrib.getJSONArray("mixer");
|
||||
String mixer = "";
|
||||
for (int i = 0; i < mixers.length(); i++)
|
||||
mixer += ", " + mixers.getString(i);
|
||||
if (mixer.length() > 2)
|
||||
tag.setField(FieldKey.MIXER, mixer.substring(2));
|
||||
}
|
||||
//Producer
|
||||
if (contrib.has("producer")) {
|
||||
JSONArray producers = contrib.getJSONArray("producer");
|
||||
String producer = "";
|
||||
for (int i = 0; i < producers.length(); i++)
|
||||
producer += ", " + producers.getString(i);
|
||||
if (producer.length() > 2)
|
||||
tag.setField(FieldKey.MIXER, producer.substring(2));
|
||||
}
|
||||
|
||||
//FLAC Only
|
||||
if (isFlac) {
|
||||
//Author
|
||||
if (contrib.has("author")) {
|
||||
JSONArray authors = contrib.getJSONArray("author");
|
||||
String author = "";
|
||||
for (int i=0; i<authors.length(); i++)
|
||||
author += ", " + authors.getString(i);
|
||||
if (author.length() > 2)
|
||||
((FlacTag)tag).setField("AUTHOR", author.substring(2));
|
||||
}
|
||||
//Writer
|
||||
if (contrib.has("writer")) {
|
||||
JSONArray writers = contrib.getJSONArray("writer");
|
||||
String writer = "";
|
||||
for (int i=0; i<writers.length(); i++)
|
||||
writer += ", " + writers.getString(i);
|
||||
if (writer.length() > 2)
|
||||
((FlacTag)tag).setField("WRITER", writer.substring(2));
|
||||
//FLAC Only
|
||||
if (isFlac) {
|
||||
//Author
|
||||
if (contrib.has("author")) {
|
||||
JSONArray authors = contrib.getJSONArray("author");
|
||||
String author = "";
|
||||
for (int i = 0; i < authors.length(); i++)
|
||||
author += ", " + authors.getString(i);
|
||||
if (author.length() > 2)
|
||||
((FlacTag) tag).setField("AUTHOR", author.substring(2));
|
||||
}
|
||||
//Writer
|
||||
if (contrib.has("writer")) {
|
||||
JSONArray writers = contrib.getJSONArray("writer");
|
||||
String writer = "";
|
||||
for (int i = 0; i < writers.length(); i++)
|
||||
writer += ", " + writers.getString(i);
|
||||
if (writer.length() > 2)
|
||||
((FlacTag) tag).setField("WRITER", writer.substring(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Error writing contributors data: " + e.toString());
|
||||
}
|
||||
|
||||
File coverFile = new File(cover);
|
||||
|
@ -423,7 +448,8 @@ public class Deezer {
|
|||
}
|
||||
} else {
|
||||
if (addCover) {
|
||||
Artwork art = Artwork.createArtworkFromFile(coverFile);
|
||||
Artwork art = ArtworkFactory.createArtworkFromFile(coverFile);
|
||||
//Artwork art = Artwork.createArtworkFromFile(coverFile);
|
||||
tag.addField(art);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -358,7 +358,7 @@ public class DownloadService extends Service {
|
|||
return;
|
||||
}
|
||||
} 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;
|
||||
exit();
|
||||
return;
|
||||
|
@ -572,7 +572,7 @@ public class DownloadService extends Service {
|
|||
|
||||
//Tag
|
||||
try {
|
||||
Deezer.tagTrack(outFile.getPath(), trackJson, albumJson, coverFile.getPath(), lyricsData, privateJson);
|
||||
deezer.tagTrack(outFile.getPath(), trackJson, albumJson, coverFile.getPath(), lyricsData, privateJson);
|
||||
} catch (Exception e) {
|
||||
Log.e("ERR", "Tagging error!");
|
||||
e.printStackTrace();
|
||||
|
@ -600,7 +600,7 @@ public class DownloadService extends Service {
|
|||
File coverFile = new File(parentDir, "cover.jpg");
|
||||
if (coverFile.exists()) return;
|
||||
//Don't download if doesn't have album
|
||||
if (!download.path.matches(".*/%album%.*/.*")) return;
|
||||
if (!download.path.matches(".*/.*%album%.*/.*")) return;
|
||||
|
||||
try {
|
||||
//Create to lock
|
||||
|
|
|
@ -1,91 +1,67 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:dio/adapter.dart';
|
||||
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 'package:freezer/api/definitions.dart';
|
||||
import 'package:freezer/settings.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
|
||||
import '../settings.dart';
|
||||
import 'definitions.dart';
|
||||
import 'dart:async';
|
||||
|
||||
DeezerAPI deezerAPI = DeezerAPI();
|
||||
|
||||
class DeezerAPI {
|
||||
|
||||
String arl;
|
||||
|
||||
DeezerAPI({this.arl});
|
||||
|
||||
String arl;
|
||||
String token;
|
||||
String userId;
|
||||
String userName;
|
||||
String favoritesPlaylistId;
|
||||
String privateUrl = 'http://www.deezer.com/ajax/gw-light.php';
|
||||
Map<String, String> headers = {
|
||||
String sid;
|
||||
|
||||
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",
|
||||
"Content-Language": '${settings.deezerLanguage??"en"}-${settings.deezerCountry??'US'}',
|
||||
"Cache-Control": "max-age=0",
|
||||
"Accept": "*/*",
|
||||
"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",
|
||||
"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 {
|
||||
|
||||
//Add headers
|
||||
dio.interceptors.add(InterceptorsWrapper(
|
||||
onRequest: (RequestOptions options) {
|
||||
options.headers = this.headers;
|
||||
return options;
|
||||
//Generate URL
|
||||
Uri uri = Uri.https('www.deezer.com', '/ajax/gw-light.php', {
|
||||
'api_version': '1.0',
|
||||
'api_token': this.token,
|
||||
'input': '3',
|
||||
'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
|
||||
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;
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
Future<Map> callPublicApi(String path) async {
|
||||
Dio dio = Dio();
|
||||
Response response = await dio.get(
|
||||
'https://api.deezer.com/' + path,
|
||||
options: Options(responseType: ResponseType.json, sendTimeout: 10000, receiveTimeout: 10000)
|
||||
);
|
||||
return response.data;
|
||||
Future<Map<dynamic, dynamic>> callPublicApi(String path) async {
|
||||
http.Response res = await http.get('https://api.deezer.com/' + path);
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
//Wrapper so it can be globally awaited
|
||||
|
@ -128,11 +104,11 @@ class DeezerAPI {
|
|||
}
|
||||
//Share URL
|
||||
if (uri.host == 'deezer.page.link' || uri.host == 'www.deezer.page.link') {
|
||||
Dio dio = Dio();
|
||||
Response res = await dio.head(url, options: RequestOptions(
|
||||
followRedirects: true
|
||||
));
|
||||
return parseLink('http://deezer.com' + res.realUri.toString());
|
||||
http.BaseRequest request = http.Request('HEAD', Uri.parse(url));
|
||||
request.followRedirects = false;
|
||||
http.StreamedResponse response = await request.send();
|
||||
String newUrl = response.headers['location'];
|
||||
return parseLink(newUrl);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -445,5 +421,14 @@ class DeezerAPI {
|
|||
'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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:disk_space/disk_space.dart';
|
||||
import 'package:filesize/filesize.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
|
@ -110,9 +111,63 @@ class DownloadManager {
|
|||
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
|
||||
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
|
||||
if (private) {
|
||||
|
@ -127,14 +182,21 @@ class DownloadManager {
|
|||
|
||||
//Get path
|
||||
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();
|
||||
}
|
||||
|
||||
Future addOfflineAlbum(Album album, {private = true}) async {
|
||||
Future addOfflineAlbum(Album album, {private = true, BuildContext context}) async {
|
||||
//Permission
|
||||
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
|
||||
if (album.tracks == null || album.tracks.length == 0) {
|
||||
album = await deezerAPI.album(album.id);
|
||||
|
@ -157,16 +219,22 @@ class DownloadManager {
|
|||
//Create downloads
|
||||
List<Map> out = [];
|
||||
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 start();
|
||||
}
|
||||
|
||||
Future addOfflinePlaylist(Playlist playlist, {private = true}) async {
|
||||
Future addOfflinePlaylist(Playlist playlist, {private = true, BuildContext context, AudioQuality quality}) async {
|
||||
//Permission
|
||||
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
|
||||
if (playlist.tracks == null || playlist.tracks.length < playlist.trackCount) {
|
||||
playlist = await deezerAPI.fullPlaylist(playlist.id);
|
||||
|
@ -193,8 +261,8 @@ class DownloadManager {
|
|||
t,
|
||||
private,
|
||||
playlistName: playlist.title,
|
||||
playlistTrackNumber: i
|
||||
), private: private));
|
||||
playlistTrackNumber: i,
|
||||
), private: private, quality: quality));
|
||||
}
|
||||
await platform.invokeMethod('addDownloads', out);
|
||||
await start();
|
||||
|
@ -375,7 +443,7 @@ class DownloadManager {
|
|||
return true;
|
||||
}
|
||||
//Playlist
|
||||
if (playlist != null) {
|
||||
if (playlist != null && playlist.id != null) {
|
||||
List res = await db.query('Playlists', where: 'id == ?', whereArgs: [playlist.id]);
|
||||
if (res.length == 0) return false;
|
||||
return true;
|
||||
|
@ -553,7 +621,7 @@ class Download {
|
|||
}
|
||||
|
||||
//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
|
||||
if (t.playbackDetails == null || t.playbackDetails == []) {
|
||||
t = await deezerAPI.track(t.id);
|
||||
|
@ -565,7 +633,7 @@ class Download {
|
|||
"mediaVersion": t.playbackDetails[1],
|
||||
"quality": private
|
||||
? settings.getQualityInt(settings.offlineQuality)
|
||||
: settings.getQualityInt(settings.downloadQuality),
|
||||
: settings.getQualityInt((quality??settings.downloadQuality)),
|
||||
"title": t.title,
|
||||
"path": path,
|
||||
"image": t.albumArt.thumb
|
||||
|
|
|
@ -104,6 +104,7 @@ class PlayerHelper {
|
|||
androidNotificationChannelDescription: 'Freezer',
|
||||
androidNotificationChannelName: 'Freezer',
|
||||
androidNotificationIcon: 'drawable/ic_logo',
|
||||
params: {'ignoreInterruptions': settings.ignoreInterruptions}
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -138,14 +139,17 @@ class PlayerHelper {
|
|||
await startService();
|
||||
await settings.updateAudioServiceQuality();
|
||||
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
|
||||
Future onQueueEnd() async {
|
||||
//Flow
|
||||
if (queueSource == null) return;
|
||||
print('test');
|
||||
|
||||
if (queueSource.id == 'flow') {
|
||||
List<Track> tracks = await deezerAPI.flow();
|
||||
List<MediaItem> mi = tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
|
||||
|
@ -163,6 +167,15 @@ class PlayerHelper {
|
|||
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());
|
||||
}
|
||||
|
||||
|
@ -245,7 +258,7 @@ void backgroundTaskEntrypoint() async {
|
|||
}
|
||||
|
||||
class AudioPlayerTask extends BackgroundAudioTask {
|
||||
AudioPlayer _player = AudioPlayer();
|
||||
AudioPlayer _player;
|
||||
|
||||
//Queue
|
||||
List<MediaItem> _queue = <MediaItem>[];
|
||||
|
@ -274,6 +287,13 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
final session = await AudioSession.instance;
|
||||
session.configure(AudioSessionConfiguration.music());
|
||||
|
||||
if (params['ignoreInterruptions'] == true) {
|
||||
_player = AudioPlayer(handleInterruptions: false);
|
||||
session.interruptionEventStream.listen((_) {});
|
||||
session.becomingNoisyEventStream.listen((_) {});
|
||||
} else
|
||||
_player = AudioPlayer();
|
||||
|
||||
//Update track index
|
||||
_player.currentIndexStream.listen((index) {
|
||||
if (index != null) {
|
||||
|
@ -365,7 +385,6 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
|
||||
@override
|
||||
Future<void> onSkipToNext() async {
|
||||
print('skipping');
|
||||
if (_queueIndex == _queue.length-1) return;
|
||||
//Update buffering state
|
||||
_skipState = AudioProcessingState.skippingToNext;
|
||||
|
@ -428,10 +447,10 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
MediaControl.skipToNext,
|
||||
//Stop
|
||||
MediaControl(
|
||||
androidIcon: 'drawable/ic_action_stop',
|
||||
label: 'stop',
|
||||
action: MediaAction.stop
|
||||
)
|
||||
androidIcon: 'drawable/ic_action_stop',
|
||||
label: 'stop',
|
||||
action: MediaAction.stop
|
||||
),
|
||||
],
|
||||
systemActions: [
|
||||
MediaAction.seekTo,
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:freezer/settings.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:async';
|
||||
|
@ -32,11 +34,10 @@ class SpotifyAPI {
|
|||
//Extract JSON data form spotify embed page
|
||||
Future<Map> getEmbedData(String url) async {
|
||||
//Fetch
|
||||
Dio dio = Dio();
|
||||
Response response = await dio.get(url);
|
||||
http.Response response = await http.get(url);
|
||||
//Parse
|
||||
Document document = parse(response.data);
|
||||
Element element = document.getElementById('resource');
|
||||
dom.Document document = parse(response.body);
|
||||
dom.Element element = document.getElementById('resource');
|
||||
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;
|
||||
importingSpotifyPlaylist = playlist;
|
||||
|
||||
|
@ -60,6 +61,7 @@ class SpotifyAPI {
|
|||
playlistId = await deezerAPI.createPlaylist(playlist.name, description: playlist.description);
|
||||
|
||||
//Search for tracks
|
||||
List<Track> downloadTracks = [];
|
||||
for (SpotifyTrack track in playlist.tracks) {
|
||||
Map deezer;
|
||||
try {
|
||||
|
@ -71,12 +73,21 @@ class SpotifyAPI {
|
|||
if (!downloadOnly)
|
||||
await deezerAPI.addToPlaylist(id, playlistId);
|
||||
if (downloadOnly)
|
||||
await downloadManager.addOfflineTrack(Track(id: id), private: false);
|
||||
downloadTracks.add(Track(id: id));
|
||||
track.state = TrackImportState.OK;
|
||||
} catch (e) {
|
||||
//On 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
|
||||
importingStream.add(playlistId);
|
||||
importingSpotifyPlaylist = playlist;
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -248,6 +248,13 @@ const language_en_us = {
|
|||
"Current timer ends at": "Current timer ends at",
|
||||
|
||||
//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"
|
||||
}
|
||||
};
|
||||
|
|
|
@ -67,6 +67,7 @@ class _FreezerAppState extends State<FreezerApp> {
|
|||
});
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
bottomNavigationBar: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
PlayerBar(),
|
||||
BottomNavigationBar(
|
||||
backgroundColor: Theme.of(context).bottomAppBarColor,
|
||||
currentIndex: _selected,
|
||||
onTap: (int s) async {
|
||||
|
||||
//Pop all routes until home screen
|
||||
while (navigatorKey.currentState.canPop()) {
|
||||
await navigatorKey.currentState.maybePop();
|
||||
}
|
||||
bottomNavigationBar: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
PlayerBar(),
|
||||
BottomNavigationBar(
|
||||
backgroundColor: Theme.of(context).bottomAppBarColor,
|
||||
currentIndex: _selected,
|
||||
onTap: (int s) async {
|
||||
|
||||
//Pop all routes until home screen
|
||||
while (navigatorKey.currentState.canPop()) {
|
||||
await navigatorKey.currentState.maybePop();
|
||||
setState(() {
|
||||
_selected = s;
|
||||
});
|
||||
},
|
||||
selectedItemColor: Theme.of(context).primaryColor,
|
||||
items: <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home), title: Text('Home'.i18n)),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.search),
|
||||
title: Text('Search'.i18n),
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.library_music), title: Text('Library'.i18n))
|
||||
],
|
||||
)
|
||||
],
|
||||
}
|
||||
|
||||
await navigatorKey.currentState.maybePop();
|
||||
setState(() {
|
||||
_selected = s;
|
||||
});
|
||||
},
|
||||
selectedItemColor: Theme.of(context).primaryColor,
|
||||
items: <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home), title: Text('Home'.i18n)),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.search),
|
||||
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,
|
||||
),
|
||||
));
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,9 @@ class Settings {
|
|||
@JsonKey(defaultValue: null)
|
||||
String language;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool ignoreInterruptions;
|
||||
|
||||
//Account
|
||||
String arl;
|
||||
@JsonKey(ignore: true)
|
||||
|
@ -185,6 +188,7 @@ class Settings {
|
|||
}
|
||||
|
||||
static const deezerBg = Color(0xFF1F1A16);
|
||||
static const deezerBottom = Color(0xFF1b1714);
|
||||
static const font = 'MabryPro';
|
||||
Map<Themes, ThemeData> get _themeData => {
|
||||
Themes.Light: ThemeData(
|
||||
|
@ -193,7 +197,7 @@ class Settings {
|
|||
accentColor: primaryColor,
|
||||
sliderTheme: _sliderTheme,
|
||||
toggleableActiveColor: primaryColor,
|
||||
bottomAppBarColor: Color(0xfff7f7f7)
|
||||
bottomAppBarColor: Color(0xfff5f5f5),
|
||||
),
|
||||
Themes.Dark: ThemeData(
|
||||
fontFamily: font,
|
||||
|
@ -212,10 +216,10 @@ class Settings {
|
|||
toggleableActiveColor: primaryColor,
|
||||
backgroundColor: deezerBg,
|
||||
scaffoldBackgroundColor: deezerBg,
|
||||
bottomAppBarColor: deezerBg,
|
||||
dialogBackgroundColor: deezerBg,
|
||||
bottomAppBarColor: deezerBottom,
|
||||
dialogBackgroundColor: deezerBottom,
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
backgroundColor: deezerBg
|
||||
backgroundColor: deezerBottom
|
||||
),
|
||||
cardColor: deezerBg
|
||||
),
|
||||
|
@ -245,7 +249,8 @@ class Settings {
|
|||
enum AudioQuality {
|
||||
MP3_128,
|
||||
MP3_320,
|
||||
FLAC
|
||||
FLAC,
|
||||
ASK
|
||||
}
|
||||
|
||||
enum Themes {
|
||||
|
|
|
@ -12,6 +12,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
|
|||
arl: json['arl'] as String,
|
||||
)
|
||||
..language = json['language'] as String
|
||||
..ignoreInterruptions = json['ignoreInterruptions'] as bool ?? false
|
||||
..wifiQuality =
|
||||
_$enumDecodeNullable(_$AudioQualityEnumMap, json['wifiQuality']) ??
|
||||
AudioQuality.MP3_320
|
||||
|
@ -49,6 +50,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
|
|||
|
||||
Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
|
||||
'language': instance.language,
|
||||
'ignoreInterruptions': instance.ignoreInterruptions,
|
||||
'arl': instance.arl,
|
||||
'wifiQuality': _$AudioQualityEnumMap[instance.wifiQuality],
|
||||
'mobileQuality': _$AudioQualityEnumMap[instance.mobileQuality],
|
||||
|
@ -112,6 +114,7 @@ const _$AudioQualityEnumMap = {
|
|||
AudioQuality.MP3_128: 'MP3_128',
|
||||
AudioQuality.MP3_320: 'MP3_320',
|
||||
AudioQuality.FLAC: 'FLAC',
|
||||
AudioQuality.ASK: 'ASK',
|
||||
};
|
||||
|
||||
const _$ThemesEnumMap = {
|
||||
|
|
|
@ -22,6 +22,7 @@ const supportedLocales = [
|
|||
const Locale('fa', 'IR'),
|
||||
const Locale('pl', 'PL'),
|
||||
const Locale('uk', 'UA'),
|
||||
const Locale('hu', 'HU'),
|
||||
const Locale('fil', 'PH')
|
||||
];
|
||||
|
||||
|
|
|
@ -38,8 +38,9 @@ class CachedImage extends StatefulWidget {
|
|||
final double height;
|
||||
final bool circular;
|
||||
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
|
||||
_CachedImageState createState() => _CachedImageState();
|
||||
|
@ -49,8 +50,13 @@ class _CachedImageState extends State<CachedImage> {
|
|||
@override
|
||||
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(
|
||||
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'))
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:freezer/api/cache.dart';
|
|||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
import 'package:freezer/ui/elements.dart';
|
||||
import 'package:freezer/ui/error.dart';
|
||||
import 'package:freezer/ui/search.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
|
@ -53,14 +54,17 @@ class AlbumDetails extends StatelessWidget {
|
|||
return ListView(
|
||||
children: <Widget>[
|
||||
//Album art, title, artists
|
||||
Card(
|
||||
Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(height: 8.0,),
|
||||
CachedImage(
|
||||
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,),
|
||||
Text(
|
||||
|
@ -97,8 +101,9 @@ class AlbumDetails extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
FreezerDivider(),
|
||||
//Details
|
||||
Card(
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
|
@ -136,8 +141,9 @@ class AlbumDetails extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
FreezerDivider(),
|
||||
//Options (offline, download...)
|
||||
Card(
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
|
@ -168,20 +174,28 @@ class AlbumDetails extends StatelessWidget {
|
|||
Text('Download'.i18n)
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
downloadManager.addOfflineAlbum(album, private: false);
|
||||
onPressed: () async {
|
||||
if (await downloadManager.addOfflineAlbum(album, private: false, context: context) != false)
|
||||
MenuSheet(context).showDownloadStartedToast();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
FreezerDivider(),
|
||||
...List.generate(cdCount, (cdi) {
|
||||
List<Track> tracks = album.tracks.where((t) => (t.diskNumber??1) == cdi + 1).toList();
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
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(
|
||||
tracks[i],
|
||||
|
@ -237,6 +251,7 @@ class _MakeAlbumOfflineState extends State<MakeAlbumOffline> {
|
|||
//Add to offline
|
||||
await deezerAPI.addFavoriteAlbum(widget.album.id);
|
||||
downloadManager.addOfflineAlbum(widget.album, private: true);
|
||||
MenuSheet(context).showDownloadStartedToast();
|
||||
setState(() {
|
||||
_offline = true;
|
||||
});
|
||||
|
@ -283,13 +298,16 @@ class ArtistDetails extends StatelessWidget {
|
|||
|
||||
return ListView(
|
||||
children: <Widget>[
|
||||
Card(
|
||||
Container(height: 4.0),
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
CachedImage(
|
||||
url: artist.picture.full,
|
||||
width: MediaQuery.of(context).size.width / 2 - 8,
|
||||
rounded: true,
|
||||
fullThumb: true,
|
||||
),
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width / 2 - 8,
|
||||
|
@ -347,8 +365,9 @@ class ArtistDetails extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
Container(height: 4.0,),
|
||||
Card(
|
||||
Container(height: 4.0),
|
||||
FreezerDivider(),
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
|
@ -391,14 +410,18 @@ class ArtistDetails extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
Container(height: 16.0,),
|
||||
FreezerDivider(),
|
||||
Container(height: 12.0,),
|
||||
//Top tracks
|
||||
Text(
|
||||
'Top Tracks'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 22.0
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 2.0),
|
||||
child: Text(
|
||||
'Top Tracks'.i18n,
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20.0
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(height: 4.0),
|
||||
|
@ -432,14 +455,17 @@ class ArtistDetails extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
),
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
//Albums
|
||||
Text(
|
||||
'Top Albums'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 22.0
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Text(
|
||||
'Top Albums'.i18n,
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20.0
|
||||
),
|
||||
),
|
||||
),
|
||||
...List.generate(artist.albums.length > 10 ? 11 : artist.albums.length + 1, (i) {
|
||||
|
@ -582,8 +608,8 @@ class _DiscographyScreenState extends State<DiscographyScreen> {
|
|||
});
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Discography'.i18n),
|
||||
appBar: FreezerAppBar(
|
||||
'Discography'.i18n,
|
||||
bottom: TabBar(
|
||||
tabs: [
|
||||
Tab(icon: Icon(Icons.album)),
|
||||
|
@ -591,6 +617,7 @@ class _DiscographyScreenState extends State<DiscographyScreen> {
|
|||
Tab(icon: Icon(Icons.recent_actors))
|
||||
],
|
||||
),
|
||||
height: 100.0,
|
||||
),
|
||||
body: TabBarView(
|
||||
children: [
|
||||
|
@ -748,7 +775,8 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||
controller: _scrollController,
|
||||
children: <Widget>[
|
||||
Container(height: 4.0,),
|
||||
Card(
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
|
@ -756,6 +784,8 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||
CachedImage(
|
||||
url: playlist.image.full,
|
||||
height: MediaQuery.of(context).size.width / 2 - 8,
|
||||
rounded: true,
|
||||
fullThumb: true,
|
||||
),
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width / 2 - 8,
|
||||
|
@ -767,12 +797,13 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||
playlist.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
maxLines: 3,
|
||||
style: TextStyle(
|
||||
fontSize: 24.0,
|
||||
fontSize: 20.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
Container(height: 4.0),
|
||||
Text(
|
||||
playlist.user.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
@ -780,12 +811,10 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 18.0
|
||||
fontSize: 17.0
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 8.0,
|
||||
),
|
||||
Container(height: 10.0),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
|
@ -814,22 +843,25 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||
],
|
||||
),
|
||||
),
|
||||
Container(height: 4.0,),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(4.0),
|
||||
child: Text(
|
||||
playlist.description ?? '',
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0
|
||||
if (playlist.description != null && playlist.description.length > 0)
|
||||
FreezerDivider(),
|
||||
if (playlist.description != null && playlist.description.length > 0)
|
||||
Container(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(6.0),
|
||||
child: Text(
|
||||
playlist.description ?? '',
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
Card(
|
||||
)
|
||||
),
|
||||
FreezerDivider(),
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
|
@ -848,12 +880,14 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.file_download, size: 32.0,),
|
||||
onPressed: () {
|
||||
downloadManager.addOfflinePlaylist(playlist, private: false);
|
||||
onPressed: () async {
|
||||
if (await downloadManager.addOfflinePlaylist(playlist, private: false, context: context) != false)
|
||||
MenuSheet(context).showDownloadStartedToast();
|
||||
},
|
||||
),
|
||||
PopupMenuButton(
|
||||
child: Icon(Icons.sort, size: 32.0),
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
onSelected: (SortType s) async {
|
||||
if (playlist.tracks.length < playlist.trackCount) {
|
||||
//Preload whole playlist
|
||||
|
@ -868,19 +902,19 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
|
||||
PopupMenuItem(
|
||||
value: SortType.DEFAULT,
|
||||
child: Text('Default'.i18n),
|
||||
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortType.REVERSE,
|
||||
child: Text('Reverse'.i18n),
|
||||
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortType.ALPHABETIC,
|
||||
child: Text('Alphabetic'.i18n),
|
||||
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
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) {
|
||||
Track t = sorted[i];
|
||||
return TrackTile(
|
||||
|
@ -909,11 +944,14 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||
);
|
||||
}),
|
||||
if (_loading)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator()
|
||||
],
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator()
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_error)
|
||||
ErrorScreen()
|
||||
|
@ -955,6 +993,7 @@ class _MakePlaylistOfflineState extends State<MakePlaylistOffline> {
|
|||
if (widget.playlist.user != null && widget.playlist.user.id != deezerAPI.userId)
|
||||
await deezerAPI.addPlaylist(widget.playlist.id);
|
||||
downloadManager.addOfflinePlaylist(widget.playlist, private: true);
|
||||
MenuSheet(context).showDownloadStartedToast();
|
||||
setState(() {
|
||||
_offline = true;
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:filesize/filesize.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
import 'package:freezer/ui/elements.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'cached_image.dart';
|
||||
|
@ -69,8 +70,8 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Downloads'.i18n),
|
||||
appBar: FreezerAppBar(
|
||||
'Downloads'.i18n,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete_sweep),
|
||||
|
@ -348,9 +349,7 @@ class _DownloadLogViewerState extends State<DownloadLogViewer> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Download Log'.i18n),
|
||||
),
|
||||
appBar: FreezerAppBar('Download Log'.i18n),
|
||||
body: ListView.builder(
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, i) {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
|||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
import 'package:freezer/main.dart';
|
||||
import 'package:freezer/ui/elements.dart';
|
||||
import 'package:freezer/ui/error.dart';
|
||||
import 'package:freezer/ui/menu.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
|
@ -16,9 +18,7 @@ class HomeScreen extends StatelessWidget {
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
SafeArea(
|
||||
child: FreezerTitle(),
|
||||
),
|
||||
SafeArea(child: Container()),
|
||||
Flexible(child: HomePageScreen(),)
|
||||
],
|
||||
),
|
||||
|
@ -161,10 +161,10 @@ class _HomePageScreenState extends State<HomePageScreen> {
|
|||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
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(
|
||||
|
@ -184,9 +184,7 @@ class _HomePageScreenState extends State<HomePageScreen> {
|
|||
),
|
||||
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(section.title),
|
||||
),
|
||||
appBar: FreezerAppBar(section.title),
|
||||
body: SingleChildScrollView(
|
||||
child: HomePageScreen(
|
||||
channel: DeezerChannel(target: section.pagePath)
|
||||
|
@ -205,6 +203,7 @@ class _HomePageScreenState extends State<HomePageScreen> {
|
|||
}),
|
||||
),
|
||||
),
|
||||
Container(height: 8.0),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
|
|
@ -2,7 +2,11 @@ import 'package:flutter/cupertino.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:freezer/api/download.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/translations.i18n.dart';
|
||||
|
||||
|
@ -49,9 +53,7 @@ class _ImporterScreenState extends State<ImporterScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Importer'.i18n),
|
||||
),
|
||||
appBar: FreezerAppBar('Importer'.i18n),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
|
@ -62,7 +64,7 @@ class _ImporterScreenState extends State<ImporterScreen> {
|
|||
color: Colors.deepOrangeAccent,
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
Container(height: 16.0,),
|
||||
Text(
|
||||
'Enter your playlist link below'.i18n,
|
||||
|
@ -130,7 +132,7 @@ class _ImporterWidgetState extends State<ImporterWidget> {
|
|||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text(widget.playlist.name),
|
||||
subtitle: Text(widget.playlist.description),
|
||||
|
@ -153,8 +155,15 @@ class _ImporterWidgetState extends State<ImporterWidget> {
|
|||
RaisedButton(
|
||||
child: Text('Download only'.i18n),
|
||||
color: Theme.of(context).primaryColor,
|
||||
onPressed: () {
|
||||
spotify.convertPlaylist(widget.playlist, downloadOnly: true);
|
||||
onPressed: () async {
|
||||
//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(
|
||||
builder: (context) => CurrentlyImportingScreen()
|
||||
));
|
||||
|
@ -199,7 +208,7 @@ class CurrentlyImportingScreen extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Importing...'.i18n),),
|
||||
appBar: FreezerAppBar('Importing...'.i18n),
|
||||
body: StreamBuilder(
|
||||
stream: spotify.importingStream.stream,
|
||||
builder: (context, snapshot) {
|
||||
|
@ -225,7 +234,7 @@ class CurrentlyImportingScreen extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
Card(
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
|
@ -267,6 +276,8 @@ class CurrentlyImportingScreen extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
Container(height: 8.0),
|
||||
FreezerDivider(),
|
||||
...List.generate(spotify.importingSpotifyPlaylist.tracks.length, (i) {
|
||||
SpotifyTrack t = spotify.importingSpotifyPlaylist.tracks[i];
|
||||
return ListTile(
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:freezer/api/player.dart';
|
|||
import 'package:freezer/settings.dart';
|
||||
import 'package:freezer/ui/details_screens.dart';
|
||||
import 'package:freezer/ui/downloads_screen.dart';
|
||||
import 'package:freezer/ui/elements.dart';
|
||||
import 'package:freezer/ui/error.dart';
|
||||
import 'package:freezer/ui/importer_screen.dart';
|
||||
import 'package:freezer/ui/tiles.dart';
|
||||
|
@ -25,8 +26,8 @@ class LibraryAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
title: Text('Library'.i18n),
|
||||
return FreezerAppBar(
|
||||
'Library'.i18n,
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.file_download),
|
||||
|
@ -61,7 +62,7 @@ class LibraryScreen extends StatelessWidget {
|
|||
if (!downloadManager.running && downloadManager.queueSize > 0)
|
||||
ListTile(
|
||||
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),
|
||||
onTap: () {
|
||||
downloadManager.start();
|
||||
|
@ -70,13 +71,22 @@ class LibraryScreen extends StatelessWidget {
|
|||
));
|
||||
},
|
||||
),
|
||||
//Dirty if to not use columns
|
||||
if (!downloadManager.running && downloadManager.queueSize > 0)
|
||||
Divider(),
|
||||
|
||||
ListTile(
|
||||
title: Text('Shuffle'.i18n),
|
||||
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(
|
||||
title: Text('Tracks'.i18n),
|
||||
leading: Icon(Icons.audiotrack),
|
||||
leading: LeadingIcon(Icons.audiotrack, color: Color(0xffbe3266)),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => LibraryTracks())
|
||||
|
@ -85,7 +95,7 @@ class LibraryScreen extends StatelessWidget {
|
|||
),
|
||||
ListTile(
|
||||
title: Text('Albums'.i18n),
|
||||
leading: Icon(Icons.album),
|
||||
leading: LeadingIcon(Icons.album, color: Color(0xff4b2e7e)),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => LibraryAlbums())
|
||||
|
@ -94,7 +104,7 @@ class LibraryScreen extends StatelessWidget {
|
|||
),
|
||||
ListTile(
|
||||
title: Text('Artists'.i18n),
|
||||
leading: Icon(Icons.recent_actors),
|
||||
leading: LeadingIcon(Icons.recent_actors, color: Color(0xff384697)),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => LibraryArtists())
|
||||
|
@ -103,26 +113,27 @@ class LibraryScreen extends StatelessWidget {
|
|||
),
|
||||
ListTile(
|
||||
title: Text('Playlists'.i18n),
|
||||
leading: Icon(Icons.playlist_play),
|
||||
leading: LeadingIcon(Icons.playlist_play, color: Color(0xff0880b5)),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => LibraryPlaylists())
|
||||
);
|
||||
},
|
||||
),
|
||||
FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text('History'.i18n),
|
||||
leading: Icon(Icons.history),
|
||||
leading: LeadingIcon(Icons.history, color: Color(0xff009a85)),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => HistoryScreen())
|
||||
);
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text('Import'.i18n),
|
||||
leading: Icon(Icons.import_export),
|
||||
leading: LeadingIcon(Icons.import_export, color: Color(0xff2ba766)),
|
||||
subtitle: Text('Import playlists from Spotify'.i18n),
|
||||
onTap: () {
|
||||
if (spotify.doneImporting != null) {
|
||||
|
@ -140,7 +151,7 @@ class LibraryScreen extends StatelessWidget {
|
|||
),
|
||||
ExpansionTile(
|
||||
title: Text('Statistics'.i18n),
|
||||
leading: Icon(Icons.insert_chart),
|
||||
leading: LeadingIcon(Icons.insert_chart, color: Colors.grey),
|
||||
children: <Widget>[
|
||||
FutureBuilder(
|
||||
future: downloadManager.getStats(),
|
||||
|
@ -350,11 +361,12 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Tracks'.i18n),
|
||||
appBar: FreezerAppBar(
|
||||
'Tracks'.i18n,
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
child: Icon(Icons.sort, size: 32.0),
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
onSelected: (SortType s) async {
|
||||
//Preload for sorting
|
||||
if (tracks.length < (trackCount??0))
|
||||
|
@ -367,19 +379,19 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
|
||||
PopupMenuItem(
|
||||
value: SortType.DEFAULT,
|
||||
child: Text('Default'.i18n),
|
||||
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortType.REVERSE,
|
||||
child: Text('Reverse'.i18n),
|
||||
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortType.ALPHABETIC,
|
||||
child: Text('Alphabetic'.i18n),
|
||||
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortType.ARTIST,
|
||||
child: Text('Artist'.i18n),
|
||||
child: Text('Artist'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -389,40 +401,29 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
body: ListView(
|
||||
controller: _scrollController,
|
||||
children: <Widget>[
|
||||
Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
Container(height: 8.0,),
|
||||
Text(
|
||||
'Loved tracks'.i18n,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24
|
||||
MakePlaylistOffline(_playlist),
|
||||
FlatButton(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.file_download, size: 32.0,),
|
||||
Container(width: 4,),
|
||||
Text('Download'.i18n)
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
MakePlaylistOffline(_playlist),
|
||||
FlatButton(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.file_download, size: 32.0,),
|
||||
Container(width: 4,),
|
||||
Text('Download'.i18n)
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
downloadManager.addOfflinePlaylist(_playlist, private: false);
|
||||
},
|
||||
)
|
||||
],
|
||||
onPressed: () async {
|
||||
if (await downloadManager.addOfflinePlaylist(_playlist, private: false, context: context) != false)
|
||||
MenuSheet(context).showDownloadStartedToast();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
FreezerDivider(),
|
||||
//Loved tracks
|
||||
...List.generate(tracks.length, (i) {
|
||||
Track t = (tracks.length == (trackCount??0))?_sorted[i]:tracks[i];
|
||||
|
@ -458,7 +459,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
)
|
||||
],
|
||||
),
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
Text(
|
||||
'All offline tracks'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
|
@ -545,10 +546,11 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Albums'.i18n),
|
||||
appBar: FreezerAppBar(
|
||||
'Albums'.i18n,
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Icon(Icons.sort, size: 32.0),
|
||||
onSelected: (AlbumSortType s) async {
|
||||
setState(() => _sort = s);
|
||||
|
@ -558,19 +560,19 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
|
|||
itemBuilder: (context) => <PopupMenuEntry<AlbumSortType>>[
|
||||
PopupMenuItem(
|
||||
value: AlbumSortType.DEFAULT,
|
||||
child: Text('Default'.i18n),
|
||||
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: AlbumSortType.REVERSE,
|
||||
child: Text('Reverse'.i18n),
|
||||
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: AlbumSortType.ALPHABETIC,
|
||||
child: Text('Alphabetic'.i18n),
|
||||
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
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;
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
Text(
|
||||
'Offline albums'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
|
@ -719,11 +721,12 @@ class _LibraryArtistsState extends State<LibraryArtists> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Artists'.i18n),
|
||||
appBar: FreezerAppBar(
|
||||
'Artists'.i18n,
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
child: Icon(Icons.sort, size: 32.0),
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
onSelected: (ArtistSortType s) async {
|
||||
setState(() => _sort = s);
|
||||
cache.artistSort = s;
|
||||
|
@ -732,19 +735,19 @@ class _LibraryArtistsState extends State<LibraryArtists> {
|
|||
itemBuilder: (context) => <PopupMenuEntry<ArtistSortType>>[
|
||||
PopupMenuItem(
|
||||
value: ArtistSortType.DEFAULT,
|
||||
child: Text('Default'.i18n),
|
||||
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: ArtistSortType.REVERSE,
|
||||
child: Text('Reverse'.i18n),
|
||||
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: ArtistSortType.ALPHABETIC,
|
||||
child: Text('Alphabetic'.i18n),
|
||||
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: ArtistSortType.POPULARITY,
|
||||
child: Text('Popularity'.i18n),
|
||||
child: Text('Popularity'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -859,11 +862,12 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Playlists'.i18n),
|
||||
appBar: FreezerAppBar(
|
||||
'Playlists'.i18n,
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
child: Icon(Icons.sort, size: 32.0),
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
onSelected: (PlaylistSortType s) async {
|
||||
setState(() => _sort = s);
|
||||
cache.libraryPlaylistSort = s;
|
||||
|
@ -872,23 +876,23 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
itemBuilder: (context) => <PopupMenuEntry<PlaylistSortType>>[
|
||||
PopupMenuItem(
|
||||
value: PlaylistSortType.DEFAULT,
|
||||
child: Text('Default'.i18n),
|
||||
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: PlaylistSortType.REVERSE,
|
||||
child: Text('Reverse'.i18n),
|
||||
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: PlaylistSortType.USER,
|
||||
child: Text('User'.i18n),
|
||||
child: Text('User'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: PlaylistSortType.TRACK_COUNT,
|
||||
child: Text('Track count'.i18n),
|
||||
child: Text('Track count'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: PlaylistSortType.ALPHABETIC,
|
||||
child: Text('Alphabetic'.i18n),
|
||||
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -899,7 +903,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('Create new playlist'.i18n),
|
||||
leading: Icon(Icons.playlist_add),
|
||||
leading: LeadingIcon(Icons.playlist_add, color: Color(0xff009a85)),
|
||||
onTap: () async {
|
||||
if (settings.offlineMode) {
|
||||
Fluttertoast.showToast(
|
||||
|
@ -913,7 +917,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
await _load();
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
|
||||
if (!settings.offlineMode && _playlists == null)
|
||||
Row(
|
||||
|
@ -965,7 +969,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
List<Playlist> playlists = snapshot.data;
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
Text(
|
||||
'Offline playlists'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
|
@ -1012,8 +1016,8 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('History'.i18n),
|
||||
appBar: FreezerAppBar(
|
||||
'History'.i18n,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete_sweep),
|
||||
|
|
|
@ -188,9 +188,9 @@ class MenuSheet {
|
|||
title: Text('Download'.i18n),
|
||||
leading: Icon(Icons.file_download),
|
||||
onTap: () async {
|
||||
await downloadManager.addOfflineTrack(t, private: false);
|
||||
if (await downloadManager.addOfflineTrack(t, private: false, context: context) != false)
|
||||
showDownloadStartedToast();
|
||||
_close();
|
||||
showDownloadStartedToast();
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -314,8 +314,8 @@ class MenuSheet {
|
|||
leading: Icon(Icons.file_download),
|
||||
onTap: () async {
|
||||
_close();
|
||||
await downloadManager.addOfflineAlbum(a, private: false);
|
||||
showDownloadStartedToast();
|
||||
if (await downloadManager.addOfflineAlbum(a, private: false, context: context) != false)
|
||||
showDownloadStartedToast();
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -471,9 +471,9 @@ class MenuSheet {
|
|||
title: Text('Download playlist'.i18n),
|
||||
leading: Icon(Icons.file_download),
|
||||
onTap: () async {
|
||||
downloadManager.addOfflinePlaylist(p, private: false);
|
||||
_close();
|
||||
showDownloadStartedToast();
|
||||
if (await downloadManager.addOfflinePlaylist(p, private: false, context: context) != false)
|
||||
showDownloadStartedToast();
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ class PlayerBar extends StatelessWidget {
|
|||
return AudioService.playbackState.currentPosition.inSeconds / AudioService.currentMediaItem.duration.inSeconds;
|
||||
}
|
||||
|
||||
double iconSize = 32;
|
||||
double iconSize = 28;
|
||||
bool _gestureRegistered = false;
|
||||
|
||||
@override
|
||||
|
@ -40,42 +40,48 @@ class PlayerBar extends StatelessWidget {
|
|||
child: StreamBuilder(
|
||||
stream: Stream.periodic(Duration(milliseconds: 250)),
|
||||
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(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
color: Theme.of(context).bottomAppBarColor,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) => PlayerScreen()));
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor: settings.themeData.scaffoldBackgroundColor,
|
||||
));
|
||||
},
|
||||
leading: CachedImage(
|
||||
width: 50,
|
||||
height: 50,
|
||||
url: AudioService.currentMediaItem.extras['thumb'] ?? AudioService.currentMediaItem.artUri,
|
||||
),
|
||||
title: Text(
|
||||
AudioService.currentMediaItem.displayTitle,
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: Text(
|
||||
AudioService.currentMediaItem.displaySubtitle,
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
PrevNextButton(iconSize, prev: true, hidePrev: true,),
|
||||
PlayPauseButton(iconSize),
|
||||
PrevNextButton(iconSize)
|
||||
],
|
||||
)
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (BuildContext context) => PlayerScreen()));
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor: settings.themeData
|
||||
.scaffoldBackgroundColor,
|
||||
));
|
||||
},
|
||||
leading: CachedImage(
|
||||
width: 50,
|
||||
height: 50,
|
||||
url: AudioService.currentMediaItem.extras['thumb'] ??
|
||||
AudioService.currentMediaItem.artUri,
|
||||
),
|
||||
title: Text(
|
||||
AudioService.currentMediaItem.displayTitle,
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: Text(
|
||||
AudioService.currentMediaItem.displaySubtitle,
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
PrevNextButton(iconSize, prev: true, hidePrev: true,),
|
||||
PlayPauseButton(iconSize),
|
||||
PrevNextButton(iconSize)
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
Container(
|
||||
|
@ -87,7 +93,7 @@ class PlayerBar extends StatelessWidget {
|
|||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/screenutil.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
import 'package:freezer/settings.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
import 'package:freezer/ui/elements.dart';
|
||||
import 'package:freezer/ui/menu.dart';
|
||||
import 'package:freezer/ui/settings_screen.dart';
|
||||
import 'package:freezer/ui/tiles.dart';
|
||||
|
@ -78,7 +80,6 @@ class PlayerScreenHorizontal extends StatefulWidget {
|
|||
|
||||
class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
||||
|
||||
double iconSize = ScreenUtil().setWidth(64);
|
||||
bool _lyrics = false;
|
||||
|
||||
@override
|
||||
|
@ -115,9 +116,9 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
|||
padding: EdgeInsets.fromLTRB(8, 16, 8, 0),
|
||||
child: Container(
|
||||
child: PlayerScreenTopRow(
|
||||
textSize: ScreenUtil().setSp(26),
|
||||
iconSize: ScreenUtil().setSp(32),
|
||||
textWidth: ScreenUtil().setWidth(256),
|
||||
textSize: ScreenUtil().setSp(24),
|
||||
iconSize: ScreenUtil().setSp(36),
|
||||
textWidth: ScreenUtil().setWidth(350),
|
||||
short: true
|
||||
),
|
||||
)
|
||||
|
@ -166,17 +167,7 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
|||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: SeekBar(),
|
||||
),
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
PrevNextButton(iconSize, prev: true,),
|
||||
PlayPauseButton(iconSize),
|
||||
PrevNextButton(iconSize)
|
||||
],
|
||||
),
|
||||
),
|
||||
PlaybackControls(ScreenUtil().setSp(60)),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(8, 0, 8, 16),
|
||||
child: Container(
|
||||
|
@ -230,7 +221,6 @@ class PlayerScreenVertical extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
||||
double iconSize = ScreenUtil().setWidth(100);
|
||||
bool _lyrics = false;
|
||||
|
||||
@override
|
||||
|
@ -240,26 +230,27 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(28, 10, 28, 0),
|
||||
child: PlayerScreenTopRow()
|
||||
padding: EdgeInsets.fromLTRB(30, 4, 16, 0),
|
||||
child: PlayerScreenTopRow()
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 0, 16, 0),
|
||||
child: Container(
|
||||
height: ScreenUtil().setHeight(1050),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
BigAlbumArt(),
|
||||
if (_lyrics) LyricsWidget(
|
||||
artUri: AudioService.currentMediaItem.extras['thumb'],
|
||||
trackId: AudioService.currentMediaItem.id,
|
||||
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
|
||||
height: ScreenUtil().setHeight(1050),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(16, 0, 16, 0),
|
||||
child: Container(
|
||||
height: ScreenUtil().setHeight(1000),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
BigAlbumArt(),
|
||||
if (_lyrics) LyricsWidget(
|
||||
artUri: AudioService.currentMediaItem.extras['thumb'],
|
||||
trackId: AudioService.currentMediaItem.id,
|
||||
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
|
||||
height: ScreenUtil().setHeight(1000),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(height: 4.0),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
|
@ -301,16 +292,7 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||
],
|
||||
),
|
||||
SeekBar(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
PrevNextButton(iconSize, prev: true,),
|
||||
PlayPauseButton(iconSize),
|
||||
PrevNextButton(iconSize)
|
||||
],
|
||||
),
|
||||
//Container(height: 8.0,),
|
||||
PlaybackControls(ScreenUtil().setWidth(100)),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 16.0),
|
||||
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 {
|
||||
@override
|
||||
_BigAlbumArtState createState() => _BigAlbumArtState();
|
||||
|
@ -383,16 +449,23 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PageView(
|
||||
controller: _pageController,
|
||||
onPageChanged: (int index) {
|
||||
if (_animationLock) return;
|
||||
AudioService.skipToQueueItem(AudioService.queue[index].id);
|
||||
return GestureDetector(
|
||||
onVerticalDragUpdate: (DragUpdateDetails details) {
|
||||
if (details.delta.dy > 16) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
children: List.generate(AudioService.queue.length, (i) => CachedImage(
|
||||
url: AudioService.queue[i].artUri,
|
||||
fullThumb: true,
|
||||
)),
|
||||
child: PageView(
|
||||
controller: _pageController,
|
||||
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(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0, 0, 8, 0),
|
||||
child: InkWell(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.keyboard_arrow_down, size: this.iconSize??ScreenUtil().setWidth(46)),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: this.textWidth??ScreenUtil().setWidth(550),
|
||||
child: Text(
|
||||
(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)),
|
||||
),
|
||||
)
|
||||
],
|
||||
Container(
|
||||
width: this.textWidth??ScreenUtil().setWidth(800),
|
||||
child: Text(
|
||||
(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(38)),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.menu),
|
||||
iconSize: this.iconSize??ScreenUtil().setSp(52),
|
||||
splashRadius: this.iconSize??ScreenUtil().setWidth(52),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => QueueScreen()
|
||||
));
|
||||
},
|
||||
),
|
||||
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 {
|
||||
@override
|
||||
_SeekBarState createState() => _SeekBarState();
|
||||
|
@ -789,8 +785,8 @@ class _QueueScreenState extends State<QueueScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Queue'.i18n),
|
||||
appBar: FreezerAppBar(
|
||||
'Queue'.i18n,
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:freezer/api/cache.dart';
|
|||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
import 'package:freezer/ui/details_screens.dart';
|
||||
import 'package:freezer/ui/elements.dart';
|
||||
import 'package:freezer/ui/menu.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
|
||||
|
@ -50,6 +51,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
bool _loading = false;
|
||||
TextEditingController _controller = new TextEditingController();
|
||||
List _suggestions = [];
|
||||
bool _cancel = false;
|
||||
|
||||
void _submit(BuildContext context, {String query}) async {
|
||||
if (query != null) _query = query;
|
||||
|
@ -78,7 +80,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
print(cache.searchHistory);
|
||||
_cancel = true;
|
||||
//Check for connectivity and enable offline mode
|
||||
Connectivity().checkConnectivity().then((res) {
|
||||
if (res == ConnectivityResult.none) setState(() {
|
||||
|
@ -102,24 +104,30 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
sugg = await deezerAPI.searchSuggestions(_query);
|
||||
} catch (e) {}
|
||||
|
||||
if (sugg != null)
|
||||
if (sugg != null && !_cancel)
|
||||
setState(() => _suggestions = sugg);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cancel = true;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Search'.i18n),),
|
||||
appBar: FreezerAppBar('Search'.i18n),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
Container(height: 16.0),
|
||||
Container(height: 4.0),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Stack(
|
||||
alignment: Alignment(1.0, 1.0),
|
||||
alignment: Alignment(1.0, 0.0),
|
||||
children: [
|
||||
TextField(
|
||||
onChanged: (String s) {
|
||||
|
@ -127,37 +135,49 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
_loadSuggestions();
|
||||
},
|
||||
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,
|
||||
onSubmitted: (String s) => _submit(context, query: s),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_suggestions = [];
|
||||
_query = '';
|
||||
});
|
||||
_controller.clear();
|
||||
},
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 40.0,
|
||||
child: IconButton(
|
||||
splashRadius: 20.0,
|
||||
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(
|
||||
title: Text('Offline search'.i18n),
|
||||
leading: Switch(
|
||||
leading: Icon(Icons.offline_pin),
|
||||
trailing: Switch(
|
||||
value: _offline,
|
||||
onChanged: (v) {
|
||||
setState(() => _offline = !_offline);
|
||||
|
@ -166,7 +186,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
),
|
||||
if (_loading)
|
||||
LinearProgressIndicator(),
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
|
||||
//History
|
||||
if (cache.searchHistory != null && cache.searchHistory.length > 0 && (_query??'').length == 0)
|
||||
|
@ -213,9 +233,7 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Search Results'.i18n),
|
||||
),
|
||||
appBar: FreezerAppBar('Search Results'.i18n),
|
||||
body: FutureBuilder(
|
||||
future: _search(),
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
|
@ -243,12 +261,15 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
List<Widget> tracks = [];
|
||||
if (results.tracks != null && results.tracks.length != 0) {
|
||||
tracks = [
|
||||
Text(
|
||||
'Tracks'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26.0,
|
||||
fontWeight: FontWeight.bold
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
|
||||
child: Text(
|
||||
'Tracks'.i18n,
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
),
|
||||
...List.generate(3, (i) {
|
||||
|
@ -280,7 +301,8 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
)))
|
||||
);
|
||||
},
|
||||
)
|
||||
),
|
||||
FreezerDivider()
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -288,12 +310,15 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
List<Widget> albums = [];
|
||||
if (results.albums != null && results.albums.length != 0) {
|
||||
albums = [
|
||||
Text(
|
||||
'Albums'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26.0,
|
||||
fontWeight: FontWeight.bold
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
|
||||
child: Text(
|
||||
'Albums'.i18n,
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
),
|
||||
...List.generate(3, (i) {
|
||||
|
@ -319,7 +344,8 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
MaterialPageRoute(builder: (context) => AlbumListScreen(results.albums))
|
||||
);
|
||||
},
|
||||
)
|
||||
),
|
||||
FreezerDivider()
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -327,12 +353,15 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
List<Widget> artists = [];
|
||||
if (results.artists != null && results.artists.length != 0) {
|
||||
artists = [
|
||||
Text(
|
||||
'Artists'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26.0,
|
||||
fontWeight: FontWeight.bold
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
|
||||
child: Text(
|
||||
'Artists'.i18n,
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(height: 4),
|
||||
|
@ -355,7 +384,8 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
);
|
||||
}),
|
||||
)
|
||||
)
|
||||
),
|
||||
FreezerDivider()
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -363,12 +393,15 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
List<Widget> playlists = [];
|
||||
if (results.playlists != null && results.playlists.length != 0) {
|
||||
playlists = [
|
||||
Text(
|
||||
'Playlists'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26.0,
|
||||
fontWeight: FontWeight.bold
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
|
||||
child: Text(
|
||||
'Playlists'.i18n,
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
),
|
||||
...List.generate(3, (i) {
|
||||
|
@ -427,7 +460,7 @@ class TrackListScreen extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Tracks'.i18n),),
|
||||
appBar: FreezerAppBar('Tracks'.i18n),
|
||||
body: ListView.builder(
|
||||
itemCount: tracks.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
|
@ -457,7 +490,7 @@ class AlbumListScreen extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Albums'.i18n),),
|
||||
appBar: FreezerAppBar('Albums'.i18n),
|
||||
body: ListView.builder(
|
||||
itemCount: albums.length,
|
||||
itemBuilder: (context, i) {
|
||||
|
@ -488,7 +521,7 @@ class SearchResultPlaylists extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Playlists'.i18n),),
|
||||
appBar: FreezerAppBar('Playlists'.i18n),
|
||||
body: ListView.builder(
|
||||
itemCount: playlists.length,
|
||||
itemBuilder: (context, i) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:freezer/api/cache.dart';
|
|||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/ui/downloads_screen.dart';
|
||||
import 'package:freezer/ui/elements.dart';
|
||||
import 'package:freezer/ui/error.dart';
|
||||
import 'package:freezer/ui/home_screen.dart';
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
|
@ -56,26 +57,26 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Settings'.i18n),),
|
||||
appBar: FreezerAppBar('Settings'.i18n),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('General'.i18n),
|
||||
leading: Icon(Icons.settings),
|
||||
leading: LeadingIcon(Icons.settings, color: Color(0xffeca704)),
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => GeneralSettings()
|
||||
)),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Download Settings'.i18n),
|
||||
leading: Icon(Icons.cloud_download),
|
||||
leading: LeadingIcon(Icons.cloud_download, color: Color(0xffbe3266)),
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => DownloadsSettings()
|
||||
)),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Appearance'.i18n),
|
||||
leading: Icon(Icons.color_lens),
|
||||
leading: LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => AppearanceSettings())
|
||||
|
@ -83,7 +84,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
),
|
||||
ListTile(
|
||||
title: Text('Quality'.i18n),
|
||||
leading: Icon(Icons.high_quality),
|
||||
leading: LeadingIcon(Icons.high_quality, color: Color(0xff384697)),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => QualitySettings())
|
||||
|
@ -91,7 +92,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
),
|
||||
ListTile(
|
||||
title: Text('Deezer'.i18n),
|
||||
leading: Icon(Icons.equalizer),
|
||||
leading: LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)),
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) => DeezerSettings()
|
||||
)),
|
||||
|
@ -99,7 +100,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
//Language select
|
||||
ListTile(
|
||||
title: Text('Language'.i18n),
|
||||
leading: Icon(Icons.language),
|
||||
leading: LeadingIcon(Icons.language, color: Color(0xff009a85)),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
|
@ -140,7 +141,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
),
|
||||
ListTile(
|
||||
title: Text('About'.i18n),
|
||||
leading: Icon(Icons.info),
|
||||
leading: LeadingIcon(Icons.info, color: Color(0xff2ba766)),
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) => CreditsScreen()
|
||||
)),
|
||||
|
@ -164,7 +165,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Appearance'.i18n),),
|
||||
appBar: FreezerAppBar('Appearance'.i18n),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
|
@ -222,19 +223,17 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
),
|
||||
ListTile(
|
||||
title: Text('Use system theme'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.useSystemTheme,
|
||||
onChanged: (bool v) async {
|
||||
setState(() {
|
||||
settings.useSystemTheme = v;
|
||||
});
|
||||
updateTheme();
|
||||
await settings.save();
|
||||
},
|
||||
),
|
||||
trailing: Switch(
|
||||
value: settings.useSystemTheme,
|
||||
onChanged: (bool v) async {
|
||||
setState(() {
|
||||
settings.useSystemTheme = v;
|
||||
});
|
||||
updateTheme();
|
||||
await settings.save();
|
||||
},
|
||||
),
|
||||
leading: Icon(Icons.android)
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Primary color'.i18n),
|
||||
|
@ -285,12 +284,10 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
ListTile(
|
||||
title: Text('Use album art primary color'.i18n),
|
||||
subtitle: Text('Warning: might be buggy'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.useArtColor,
|
||||
onChanged: (v) => setState(() => settings.updateUseArtColor(v)),
|
||||
),
|
||||
leading: Icon(Icons.invert_colors),
|
||||
trailing: Switch(
|
||||
value: settings.useArtColor,
|
||||
onChanged: (v) => setState(() => settings.updateUseArtColor(v)),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
@ -309,30 +306,30 @@ class _QualitySettingsState extends State<QualitySettings> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Quality'.i18n),),
|
||||
appBar: FreezerAppBar('Quality'.i18n),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('Mobile streaming'.i18n),
|
||||
leading: Icon(Icons.network_cell),
|
||||
leading: LeadingIcon(Icons.network_cell, color: Color(0xff384697)),
|
||||
),
|
||||
QualityPicker('mobile'),
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text('Wifi streaming'.i18n),
|
||||
leading: Icon(Icons.network_wifi),
|
||||
leading: LeadingIcon(Icons.network_wifi, color: Color(0xff0880b5)),
|
||||
),
|
||||
QualityPicker('wifi'),
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text('Offline'.i18n),
|
||||
leading: Icon(Icons.offline_pin),
|
||||
leading: LeadingIcon(Icons.offline_pin, color: Color(0xff009a85)),
|
||||
),
|
||||
QualityPicker('offline'),
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text('External downloads'.i18n),
|
||||
leading: Icon(Icons.file_download),
|
||||
leading: LeadingIcon(Icons.file_download, color: Color(0xff2ba766)),
|
||||
),
|
||||
QualityPicker('download'),
|
||||
],
|
||||
|
@ -425,6 +422,15 @@ class _QualityPickerState extends State<QualityPicker> {
|
|||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Deezer'.i18n),),
|
||||
appBar: FreezerAppBar('Deezer'.i18n),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
|
@ -485,65 +491,64 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
|||
ListTile(
|
||||
title: Text('Log tracks'.i18n),
|
||||
subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'.i18n),
|
||||
leading: Container(
|
||||
width: 30,
|
||||
child: Checkbox(
|
||||
value: settings.logListen,
|
||||
onChanged: (bool v) {
|
||||
setState(() => settings.logListen = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
trailing: Switch(
|
||||
value: settings.logListen,
|
||||
onChanged: (bool v) {
|
||||
setState(() => settings.logListen = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
leading: Icon(Icons.history_toggle_off),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Proxy'.i18n),
|
||||
leading: Icon(Icons.vpn_key),
|
||||
subtitle: Text(settings.proxyAddress??'Not set'.i18n),
|
||||
onTap: () {
|
||||
String _new;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Proxy'.i18n),
|
||||
content: TextField(
|
||||
onChanged: (String v) => _new = v,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'IP:PORT'
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('Cancel'.i18n),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Reset'.i18n),
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
settings.proxyAddress = null;
|
||||
});
|
||||
await settings.save();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Save'.i18n),
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
settings.proxyAddress = _new;
|
||||
});
|
||||
await settings.save();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
)
|
||||
//TODO: Reimplement proxy
|
||||
// ListTile(
|
||||
// title: Text('Proxy'.i18n),
|
||||
// leading: Icon(Icons.vpn_key),
|
||||
// subtitle: Text(settings.proxyAddress??'Not set'.i18n),
|
||||
// onTap: () {
|
||||
// String _new;
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// builder: (BuildContext context) {
|
||||
// return AlertDialog(
|
||||
// title: Text('Proxy'.i18n),
|
||||
// content: TextField(
|
||||
// onChanged: (String v) => _new = v,
|
||||
// decoration: InputDecoration(
|
||||
// hintText: 'IP:PORT'
|
||||
// ),
|
||||
// ),
|
||||
// actions: [
|
||||
// FlatButton(
|
||||
// child: Text('Cancel'.i18n),
|
||||
// onPressed: () => Navigator.of(context).pop(),
|
||||
// ),
|
||||
// FlatButton(
|
||||
// child: Text('Reset'.i18n),
|
||||
// onPressed: () async {
|
||||
// setState(() {
|
||||
// settings.proxyAddress = null;
|
||||
// });
|
||||
// await settings.save();
|
||||
// Navigator.of(context).pop();
|
||||
// },
|
||||
// ),
|
||||
// FlatButton(
|
||||
// child: Text('Save'.i18n),
|
||||
// onPressed: () async {
|
||||
// setState(() {
|
||||
// settings.proxyAddress = _new;
|
||||
// });
|
||||
// await settings.save();
|
||||
// Navigator.of(context).pop();
|
||||
// },
|
||||
// )
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
// );
|
||||
// },
|
||||
// )
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -562,7 +567,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Download Settings'.i18n),),
|
||||
appBar: FreezerAppBar('Download Settings'.i18n),
|
||||
body: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
|
@ -696,128 +701,110 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
}
|
||||
}
|
||||
),
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text('Create folders for artist'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.artistFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.artistFolder = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
trailing: Switch(
|
||||
value: settings.artistFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.artistFolder = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
leading: Icon(Icons.folder),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Create folders for albums'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.albumFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.albumFolder = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
trailing: Switch(
|
||||
value: settings.albumFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.albumFolder = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
leading: Icon(Icons.folder)
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Create folder for playlist'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.playlistFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.playlistFolder = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
trailing: Switch(
|
||||
value: settings.playlistFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.playlistFolder = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
leading: Icon(Icons.folder)
|
||||
),
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text('Separate albums by discs'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.albumDiscFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.albumDiscFolder = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
trailing: Switch(
|
||||
value: settings.albumDiscFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.albumDiscFolder = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
leading: Icon(Icons.album)
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Overwrite already downloaded files'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.overwriteDownload,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.overwriteDownload = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
trailing: Switch(
|
||||
value: settings.overwriteDownload,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.overwriteDownload = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
leading: Icon(Icons.delete)
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Download .LRC lyrics'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.downloadLyrics,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.downloadLyrics = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
trailing: Switch(
|
||||
value: settings.downloadLyrics,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.downloadLyrics = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
leading: Icon(Icons.subtitles)
|
||||
),
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text('Save cover file for every track'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.trackCover,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.trackCover = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
trailing: Switch(
|
||||
value: settings.trackCover,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.trackCover = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
leading: Icon(Icons.image)
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Save album cover'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.albumCover,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.albumCover = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
trailing: Switch(
|
||||
value: settings.albumCover,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.albumCover = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
leading: Icon(Icons.image)
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Create .nomedia files'.i18n),
|
||||
subtitle: Text('To prevent gallery being filled with album art'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.nomediaFiles,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.nomediaFiles = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
trailing: Switch(
|
||||
value: settings.nomediaFiles,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.nomediaFiles = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
leading: Icon(Icons.insert_drive_file)
|
||||
),
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text('Download Log'.i18n),
|
||||
leading: Icon(Icons.sticky_note_2),
|
||||
|
@ -841,51 +828,49 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('General'.i18n),),
|
||||
appBar: FreezerAppBar('General'.i18n),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('Offline mode'.i18n),
|
||||
subtitle: Text('Will be overwritten on start.'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.offlineMode,
|
||||
onChanged: (bool v) {
|
||||
if (v) {
|
||||
setState(() => settings.offlineMode = true);
|
||||
return;
|
||||
}
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
deezerAPI.authorize().then((v) {
|
||||
if (v) {
|
||||
setState(() => settings.offlineMode = false);
|
||||
} else {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Error logging in, check your internet connections.'.i18n,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastLength: Toast.LENGTH_SHORT
|
||||
);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
return AlertDialog(
|
||||
title: Text('Logging in...'.i18n),
|
||||
content: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator()
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
),
|
||||
trailing: Switch(
|
||||
value: settings.offlineMode,
|
||||
onChanged: (bool v) {
|
||||
if (v) {
|
||||
setState(() => settings.offlineMode = true);
|
||||
return;
|
||||
}
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
deezerAPI.authorize().then((v) {
|
||||
if (v) {
|
||||
setState(() => settings.offlineMode = false);
|
||||
} else {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Error logging in, check your internet connections.'.i18n,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastLength: Toast.LENGTH_SHORT
|
||||
);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
return AlertDialog(
|
||||
title: Text('Logging in...'.i18n),
|
||||
content: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator()
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
),
|
||||
leading: Icon(Icons.lock),
|
||||
),
|
||||
ListTile(
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Pick-a-Path'.i18n),
|
||||
appBar: FreezerAppBar(
|
||||
'Pick-a-Path'.i18n,
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.sd_card),
|
||||
|
@ -1110,7 +1107,8 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
|||
['kobyrevah', 'Hebrew'],
|
||||
['HoScHaKaL', 'Turkish'],
|
||||
['MicroMihai', 'Romanian'],
|
||||
['LenteraMalam', 'Indonesian']
|
||||
['LenteraMalam', 'Indonesian'],
|
||||
['RTWO2', 'Persian']
|
||||
];
|
||||
|
||||
@override
|
||||
|
@ -1126,9 +1124,6 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('About'.i18n),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
FreezerTitle(),
|
||||
|
@ -1139,7 +1134,7 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
|||
fontStyle: FontStyle.italic
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text('Telegram Channel'.i18n),
|
||||
subtitle: Text('To get latest releases'.i18n),
|
||||
|
@ -1164,7 +1159,7 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
|||
launch('https://notabug.org/exttex/freezer');
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text('exttex'),
|
||||
subtitle: Text('Developer'),
|
||||
|
@ -1203,7 +1198,7 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
|||
title: Text('Annexhack'),
|
||||
subtitle: Text('Android Auto help'),
|
||||
),
|
||||
Divider(),
|
||||
FreezerDivider(),
|
||||
...List.generate(translators.length, (i) => ListTile(
|
||||
title: Text(translators[i][0]),
|
||||
subtitle: Text(translators[i][1]),
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
|
||||
import '../api/definitions.dart';
|
||||
import 'cached_image.dart';
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
|
||||
class TrackTile extends StatefulWidget {
|
||||
|
||||
final Track track;
|
||||
|
@ -132,6 +135,8 @@ class ArtistTile extends StatelessWidget {
|
|||
return SizedBox(
|
||||
width: 150,
|
||||
child: Card(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
elevation: 0.0,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
|
@ -144,7 +149,7 @@ class ArtistTile extends StatelessWidget {
|
|||
circular: true,
|
||||
width: 100,
|
||||
),
|
||||
Container(height: 4,),
|
||||
Container(height: 8,),
|
||||
Text(
|
||||
artist.name,
|
||||
maxLines: 1,
|
||||
|
@ -172,6 +177,13 @@ class PlaylistTile extends StatelessWidget {
|
|||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
|
@ -180,7 +192,7 @@ class PlaylistTile extends StatelessWidget {
|
|||
maxLines: 1,
|
||||
),
|
||||
subtitle: Text(
|
||||
playlist.user.name,
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
),
|
||||
leading: CachedImage(
|
||||
|
@ -234,6 +246,8 @@ class PlaylistCardTile extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
elevation: 0.0,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
|
@ -245,8 +259,10 @@ class PlaylistCardTile extends StatelessWidget {
|
|||
url: playlist.image.thumb,
|
||||
width: 128,
|
||||
height: 128,
|
||||
rounded: true,
|
||||
),
|
||||
),
|
||||
Container(height: 2.0),
|
||||
Container(
|
||||
width: 144,
|
||||
child: Text(
|
||||
|
@ -257,7 +273,7 @@ class PlaylistCardTile extends StatelessWidget {
|
|||
style: TextStyle(fontSize: 14.0),
|
||||
),
|
||||
),
|
||||
Container(height: 8.0,)
|
||||
Container(height: 4.0,)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
@ -276,6 +292,8 @@ class SmartTrackListTile extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
|
@ -287,6 +305,7 @@ class SmartTrackListTile extends StatelessWidget {
|
|||
width: 128,
|
||||
height: 128,
|
||||
url: smartTrackList.cover.thumb,
|
||||
rounded: true,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
|
@ -320,6 +339,8 @@ class AlbumCard extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
elevation: 0.0,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
|
@ -331,6 +352,7 @@ class AlbumCard extends StatelessWidget {
|
|||
width: 128.0,
|
||||
height: 128.0,
|
||||
url: album.art.thumb,
|
||||
rounded: true
|
||||
),
|
||||
),
|
||||
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,)
|
||||
],
|
||||
),
|
||||
|
@ -366,28 +402,31 @@ class ChannelTile extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: channel.backgroundColor,
|
||||
child: InkWell(
|
||||
onTap: this.onTap,
|
||||
child: Container(
|
||||
width: 150,
|
||||
height: 75,
|
||||
child: Center(
|
||||
child: Text(
|
||||
channel.title,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _textColor()
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: Card(
|
||||
color: channel.backgroundColor,
|
||||
child: InkWell(
|
||||
onTap: this.onTap,
|
||||
child: Container(
|
||||
width: 150,
|
||||
height: 75,
|
||||
child: Center(
|
||||
child: Text(
|
||||
channel.title,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _textColor()
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
16
pubspec.lock
16
pubspec.lock
|
@ -246,20 +246,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -428,7 +414,7 @@ packages:
|
|||
source: hosted
|
||||
version: "0.14.0+4"
|
||||
http:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
url: "https://pub.dartlang.org"
|
||||
|
|
|
@ -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.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 0.5.10+1
|
||||
version: 0.6.0+1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.8.0 <3.0.0"
|
||||
|
@ -27,8 +27,7 @@ dependencies:
|
|||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
||||
dio: ^3.0.10
|
||||
dio_cookie_manager: ^1.0.0
|
||||
http: ^0.12.2
|
||||
cookie_jar: ^1.0.1
|
||||
json_annotation: ^3.0.1
|
||||
path_provider: 1.6.10
|
||||
|
|
|
@ -18,7 +18,9 @@ lang_crowdin = {
|
|||
'ro': 'ro_ro',
|
||||
'ru': 'ru_ru',
|
||||
'tr': 'tr_tr',
|
||||
'pl': 'pl_pl'
|
||||
'pl': 'pl_pl',
|
||||
'uk': 'uk_ua',
|
||||
'hu': 'hu_hu'
|
||||
}
|
||||
|
||||
def generate_dart():
|
||||
|
@ -30,7 +32,7 @@ def generate_dart():
|
|||
lang = file.split('/')[0]
|
||||
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('$', '\\$')
|
||||
out = f'const crowdin = {data};'
|
||||
f.write(out)
|
||||
|
|
Loading…
Reference in New Issue