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