0.6.5 - Local streaming http server
This commit is contained in:
parent
28c2de55fb
commit
21e7f55017
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,6 +2,8 @@
|
|||||||
freezerkey.jsk
|
freezerkey.jsk
|
||||||
android/key.properties
|
android/key.properties
|
||||||
|
|
||||||
|
just_audio/
|
||||||
|
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
*.class
|
*.class
|
||||||
*.log
|
*.log
|
||||||
|
@ -32,7 +32,7 @@ apply plugin: 'com.android.application'
|
|||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 29
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
disable 'InvalidPackage'
|
disable 'InvalidPackage'
|
||||||
@ -42,7 +42,7 @@ android {
|
|||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "f.f.freezer"
|
applicationId "f.f.freezer"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 28
|
targetSdkVersion 29
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
}
|
}
|
||||||
@ -73,7 +73,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation group: 'org', name: 'jaudiotagger', version: '2.0.3'
|
//implementation group: 'org', name: 'jaudiotagger', version: '2.0.3'
|
||||||
|
implementation files('libs/jaudiotagger-2.2.3.jar')
|
||||||
|
implementation group: 'org.nanohttpd', name: 'nanohttpd', version: '2.3.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
|
BIN
android/app/libs/jaudiotagger-2.2.3.jar
Normal file
BIN
android/app/libs/jaudiotagger-2.2.3.jar
Normal file
Binary file not shown.
@ -26,7 +26,8 @@
|
|||||||
android:name=".DownloadService"
|
android:name=".DownloadService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:process="f.f.freezer.DownloadService" ></service>
|
android:stopWithTask="false"
|
||||||
|
android:process=":FreezerDownloadService" ></service>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package f.f.freezer;
|
package f.f.freezer;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.jaudiotagger.audio.AudioFile;
|
import org.jaudiotagger.audio.AudioFile;
|
||||||
@ -14,24 +13,17 @@ import org.jaudiotagger.tag.id3.valuepair.ImageFormats;
|
|||||||
import org.jaudiotagger.tag.images.Artwork;
|
import org.jaudiotagger.tag.images.Artwork;
|
||||||
import org.jaudiotagger.tag.images.ArtworkFactory;
|
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;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.RandomAccessFile;
|
import java.io.RandomAccessFile;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Scanner;
|
import java.util.Scanner;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
@ -74,21 +66,6 @@ public class Deezer {
|
|||||||
|
|
||||||
public native void decryptFile(String trackId, String inputFilename, String outputFilename);
|
public native void decryptFile(String trackId, String inputFilename, String outputFilename);
|
||||||
|
|
||||||
//Get guest SID cookie from deezer.com
|
|
||||||
public static String getSidCookie() throws Exception {
|
|
||||||
URL url = new URL("https://deezer.com/");
|
|
||||||
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
|
||||||
connection.setConnectTimeout(20000);
|
|
||||||
connection.setRequestMethod("HEAD");
|
|
||||||
String sid = "";
|
|
||||||
for (String cookie : connection.getHeaderFields().get("Set-Cookie")) {
|
|
||||||
if (cookie.startsWith("sid=")) {
|
|
||||||
sid = cookie.split(";")[0].split("=")[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JSONObject callGWAPI(String method, String params) throws Exception {
|
public JSONObject callGWAPI(String method, String params) throws Exception {
|
||||||
//Get token
|
//Get token
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
@ -175,26 +152,6 @@ public class Deezer {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int qualityFallback(String trackId, String md5origin, String mediaVersion, int originalQuality) throws Exception {
|
|
||||||
//Create HEAD requests to check if exists
|
|
||||||
URL url = new URL(getTrackUrl(trackId, md5origin, mediaVersion, originalQuality));
|
|
||||||
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
|
||||||
connection.setRequestMethod("HEAD");
|
|
||||||
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36");
|
|
||||||
connection.setRequestProperty("Accept-Language", "*");
|
|
||||||
connection.setRequestProperty("Accept", "*/*");
|
|
||||||
int rc = connection.getResponseCode();
|
|
||||||
//Track not available
|
|
||||||
if (rc > 400) {
|
|
||||||
logger.warn("Quality fallback, response code: " + Integer.toString(rc) + ", current: " + Integer.toString(originalQuality));
|
|
||||||
//Returns -1 if no quality available
|
|
||||||
if (originalQuality == 1) return -1;
|
|
||||||
if (originalQuality == 3) return qualityFallback(trackId, md5origin, mediaVersion, 1);
|
|
||||||
if (originalQuality == 9) return qualityFallback(trackId, md5origin, mediaVersion, 3);
|
|
||||||
}
|
|
||||||
return originalQuality;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Generate track download URL
|
//Generate track download URL
|
||||||
public static String getTrackUrl(String trackId, String md5origin, String mediaVersion, int quality) {
|
public static String getTrackUrl(String trackId, String md5origin, String mediaVersion, int quality) {
|
||||||
try {
|
try {
|
||||||
@ -490,4 +447,138 @@ public class Deezer {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Track decryption key
|
||||||
|
static byte[] getKey(String id) {
|
||||||
|
String secret = "g4el58wc0zvf9na1";
|
||||||
|
try {
|
||||||
|
MessageDigest md5 = MessageDigest.getInstance("MD5");
|
||||||
|
md5.update(id.getBytes());
|
||||||
|
byte[] md5id = md5.digest();
|
||||||
|
String idmd5 = bytesToHex(md5id).toLowerCase();
|
||||||
|
String key = "";
|
||||||
|
for(int i=0; i<16; i++) {
|
||||||
|
int s0 = idmd5.charAt(i);
|
||||||
|
int s1 = idmd5.charAt(i+16);
|
||||||
|
int s2 = secret.charAt(i);
|
||||||
|
key += (char)(s0^s1^s2);
|
||||||
|
}
|
||||||
|
return key.getBytes();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("E", e.toString());
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Decrypt 2048b of data
|
||||||
|
static byte[] decryptChunk(byte[] key, byte[] data) {
|
||||||
|
try {
|
||||||
|
byte[] IV = {00, 01, 02, 03, 04, 05, 06, 07};
|
||||||
|
SecretKeySpec Skey = new SecretKeySpec(key, "Blowfish");
|
||||||
|
Cipher cipher = Cipher.getInstance("Blowfish/CBC/NoPadding");
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, Skey, new javax.crypto.spec.IvParameterSpec(IV));
|
||||||
|
return cipher.doFinal(data);
|
||||||
|
}catch (Exception e) {
|
||||||
|
Log.e("D", e.toString());
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class QualityInfo {
|
||||||
|
int quality;
|
||||||
|
String md5origin;
|
||||||
|
String mediaVersion;
|
||||||
|
String trackId;
|
||||||
|
int initialQuality;
|
||||||
|
DownloadLog logger;
|
||||||
|
|
||||||
|
QualityInfo(int quality, String trackId, String md5origin, String mediaVersion, DownloadLog logger) {
|
||||||
|
this.quality = quality;
|
||||||
|
this.initialQuality = quality;
|
||||||
|
this.trackId = trackId;
|
||||||
|
this.mediaVersion = mediaVersion;
|
||||||
|
this.md5origin = md5origin;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean fallback(Deezer deezer) {
|
||||||
|
//Quality fallback
|
||||||
|
try {
|
||||||
|
qualityFallback();
|
||||||
|
//No quality
|
||||||
|
if (quality == -1)
|
||||||
|
throw new Exception("No quality to fallback to!");
|
||||||
|
|
||||||
|
//Success
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Quality fallback failed! ID: " + trackId + " " + e.toString());
|
||||||
|
quality = initialQuality;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Track ID Fallback
|
||||||
|
JSONObject privateJson = null;
|
||||||
|
try {
|
||||||
|
//Fetch meta
|
||||||
|
JSONObject privateRaw = deezer.callGWAPI("deezer.pageTrack", "{\"sng_id\": \"" + trackId + "\"}");
|
||||||
|
privateJson = privateRaw.getJSONObject("results").getJSONObject("DATA");
|
||||||
|
if (privateJson.has("FALLBACK")) {
|
||||||
|
//Fetch new track
|
||||||
|
String fallbackId = privateJson.getJSONObject("FALLBACK").getString("SNG_ID");
|
||||||
|
if (!fallbackId.equals(trackId)) {
|
||||||
|
JSONObject newPrivate = deezer.callGWAPI("song.getListData", "{\"sng_ids\": [" + fallbackId + "]}");
|
||||||
|
JSONObject trackData = newPrivate.getJSONObject("results").getJSONArray("data").getJSONObject(0);
|
||||||
|
trackId = trackData.getString("SNG_ID");
|
||||||
|
md5origin = trackData.getString("MD5_ORIGIN");
|
||||||
|
mediaVersion = trackData.getString("MEDIA_VERSION");
|
||||||
|
return fallback(deezer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("ID fallback failed! ID: " + trackId + " " + e.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
//ISRC Fallback
|
||||||
|
try {
|
||||||
|
JSONObject newTrackJson = Deezer.callPublicAPI("track", "isrc:" + privateJson.getString("ISRC"));
|
||||||
|
//Same track check
|
||||||
|
if (newTrackJson.getInt("id") == Integer.parseInt(trackId)) throw new Exception("No more to ISRC fallback!");
|
||||||
|
//Get private data
|
||||||
|
privateJson = deezer.callGWAPI("song.getListData", "{\"sng_ids\": [" + newTrackJson.getInt("id") + "]}");
|
||||||
|
JSONObject trackData = privateJson.getJSONObject("results").getJSONArray("data").getJSONObject(0);
|
||||||
|
trackId = trackData.getString("SNG_ID");
|
||||||
|
md5origin = trackData.getString("MD5_ORIGIN");
|
||||||
|
mediaVersion = trackData.getString("MEDIA_VERSION");
|
||||||
|
return fallback(deezer);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("ISRC Fallback failed, track unavailable! ID: " + trackId + " " + e.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void qualityFallback() throws Exception {
|
||||||
|
//Create HEAD requests to check if exists
|
||||||
|
URL url = new URL(getTrackUrl(trackId, md5origin, mediaVersion, quality));
|
||||||
|
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
||||||
|
connection.setRequestMethod("HEAD");
|
||||||
|
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36");
|
||||||
|
connection.setRequestProperty("Accept-Language", "*");
|
||||||
|
connection.setRequestProperty("Accept", "*/*");
|
||||||
|
int rc = connection.getResponseCode();
|
||||||
|
//Track not available
|
||||||
|
if (rc > 400) {
|
||||||
|
logger.warn("Quality fallback, response code: " + Integer.toString(rc) + ", current: " + Integer.toString(quality));
|
||||||
|
//-1 if no quality available
|
||||||
|
if (quality == 1) {
|
||||||
|
quality = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (quality == 3) quality = 1;
|
||||||
|
if (quality == 9) quality = 3;
|
||||||
|
qualityFallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ public class DownloadLog {
|
|||||||
BufferedWriter writer;
|
BufferedWriter writer;
|
||||||
|
|
||||||
//Open/Create file
|
//Open/Create file
|
||||||
public void open(Context context) {
|
void open(Context context) {
|
||||||
File file = new File(context.getExternalFilesDir(""), "download.log");
|
File file = new File(context.getExternalFilesDir(""), "download.log");
|
||||||
try {
|
try {
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
@ -27,7 +27,7 @@ public class DownloadLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Close log
|
//Close log
|
||||||
public void close() {
|
void close() {
|
||||||
try {
|
try {
|
||||||
writer.close();
|
writer.close();
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
@ -35,13 +35,13 @@ public class DownloadLog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String time() {
|
String time() {
|
||||||
SimpleDateFormat format = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss");
|
SimpleDateFormat format = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss");
|
||||||
return format.format(Calendar.getInstance().getTime());
|
return format.format(Calendar.getInstance().getTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
//Write error to log
|
//Write error to log
|
||||||
public void error(String info) {
|
void error(String info) {
|
||||||
if (writer == null) return;
|
if (writer == null) return;
|
||||||
String data = "E:" + time() + ": " + info;
|
String data = "E:" + time() + ": " + info;
|
||||||
try {
|
try {
|
||||||
@ -55,7 +55,7 @@ public class DownloadLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Write error to log with download info
|
//Write error to log with download info
|
||||||
public void error(String info, Download download) {
|
void error(String info, Download download) {
|
||||||
if (writer == null) return;
|
if (writer == null) return;
|
||||||
String data = "E:" + time() + " (TrackID: " + download.trackId + ", ID: " + Integer.toString(download.id) + "): " +info;
|
String data = "E:" + time() + " (TrackID: " + download.trackId + ", ID: " + Integer.toString(download.id) + "): " +info;
|
||||||
try {
|
try {
|
||||||
@ -69,7 +69,7 @@ public class DownloadLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Write warning to log
|
//Write warning to log
|
||||||
public void warn(String info) {
|
void warn(String info) {
|
||||||
if (writer == null) return;
|
if (writer == null) return;
|
||||||
String data = "W:" + time() + ": " + info;
|
String data = "W:" + time() + ": " + info;
|
||||||
try {
|
try {
|
||||||
@ -83,7 +83,7 @@ public class DownloadLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Write warning to log with download info
|
//Write warning to log with download info
|
||||||
public void warn(String info, Download download) {
|
void warn(String info, Download download) {
|
||||||
if (writer == null) return;
|
if (writer == null) return;
|
||||||
String data = "W:" + time() + " (TrackID: " + download.trackId + ", ID: " + Integer.toString(download.id) + "): " +info;
|
String data = "W:" + time() + " (TrackID: " + download.trackId + ", ID: " + Integer.toString(download.id) + "): " +info;
|
||||||
try {
|
try {
|
||||||
|
@ -119,7 +119,9 @@ public class DownloadService extends Service {
|
|||||||
if (intent != null)
|
if (intent != null)
|
||||||
activityMessenger = intent.getParcelableExtra("activityMessenger");
|
activityMessenger = intent.getParcelableExtra("activityMessenger");
|
||||||
|
|
||||||
return super.onStartCommand(intent, flags, startId);
|
//return super.onStartCommand(intent, flags, startId);
|
||||||
|
//Prevent battery savers I guess
|
||||||
|
return START_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Android O+ Notifications
|
//Android O+ Notifications
|
||||||
@ -313,73 +315,33 @@ public class DownloadService extends Service {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Quality fallback
|
//Fallback
|
||||||
int newQuality;
|
Deezer.QualityInfo qualityInfo = new Deezer.QualityInfo(this.download.quality, this.download.trackId, this.download.md5origin, this.download.mediaVersion, logger);
|
||||||
try {
|
if (!download.isUserUploaded()) {
|
||||||
newQuality = deezer.qualityFallback(download.trackId, download.md5origin, download.mediaVersion, download.quality);
|
try {
|
||||||
} catch (Exception e) {
|
boolean res = qualityInfo.fallback(deezer);
|
||||||
logger.error("Quality fallback failed: " + e.toString(), download);
|
if (!res)
|
||||||
download.state = Download.DownloadState.ERROR;
|
throw new Exception("No more to fallback!");
|
||||||
exit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//TrackID Fallback
|
download.quality = qualityInfo.quality;
|
||||||
try {
|
} catch (Exception e) {
|
||||||
if (newQuality == -1 && !download.isUserUploaded() && privateJson.has("FALLBACK")) {
|
logger.error("Fallback failed " + e.toString());
|
||||||
logger.warn("TrackID Fallback!", download);
|
download.state = Download.DownloadState.DEEZER_ERROR;
|
||||||
String fallbackId = privateJson.getJSONObject("FALLBACK").getString("SNG_ID");
|
exit();
|
||||||
JSONObject newPrivate = deezer.callGWAPI("song.getListData", "{\"sng_ids\": [" + fallbackId + "]}");
|
|
||||||
JSONObject trackData = newPrivate.getJSONObject("results").getJSONArray("data").getJSONObject(0);
|
|
||||||
download.trackId = trackData.getString("SNG_ID");
|
|
||||||
download.md5origin = trackData.getString("MD5_ORIGIN");
|
|
||||||
download.mediaVersion = trackData.getString("MEDIA_VERSION");
|
|
||||||
run();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} else {
|
||||||
logger.error("ID fallback failed: " + e.toString(), download);
|
//User uploaded MP3
|
||||||
|
qualityInfo.quality = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
//ISRC Fallback
|
|
||||||
try {
|
|
||||||
if (newQuality == -1 && !download.isUserUploaded()) {
|
|
||||||
logger.warn("ISRC Fallback!", download);
|
|
||||||
JSONObject newTrackJson = Deezer.callPublicAPI("track", "isrc:" + trackJson.getString("isrc"));
|
|
||||||
//Same track check
|
|
||||||
if (newTrackJson.getInt("id") == trackJson.getInt("id")) throw new Exception("No more to fallback!");
|
|
||||||
//Get private data
|
|
||||||
JSONObject privateJson = deezer.callGWAPI("song.getListData", "{\"sng_ids\": [" + newTrackJson.getInt("id") + "]}");
|
|
||||||
JSONObject trackData = privateJson.getJSONObject("results").getJSONArray("data").getJSONObject(0);
|
|
||||||
download.trackId = trackData.getString("SNG_ID");
|
|
||||||
download.md5origin = trackData.getString("MD5_ORIGIN");
|
|
||||||
download.mediaVersion = trackData.getString("MEDIA_VERSION");
|
|
||||||
run();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("ISRC Fallback failed, track unavailable! " + e.toString(), download);
|
|
||||||
download.state = Download.DownloadState.DEEZER_ERROR;
|
|
||||||
exit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//No quality available
|
|
||||||
if (newQuality == -1) {
|
|
||||||
logger.error("No available quality!", download);
|
|
||||||
download.state = Download.DownloadState.DEEZER_ERROR;
|
|
||||||
exit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
download.quality = newQuality;
|
|
||||||
|
|
||||||
if (!download.priv) {
|
if (!download.priv) {
|
||||||
//Check file
|
//Check file
|
||||||
try {
|
try {
|
||||||
if (download.isUserUploaded()) {
|
if (download.isUserUploaded()) {
|
||||||
outFile = new File(Deezer.generateUserUploadedMP3Filename(download.path, privateJson));
|
outFile = new File(Deezer.generateUserUploadedMP3Filename(download.path, privateJson));
|
||||||
} else {
|
} else {
|
||||||
outFile = new File(Deezer.generateFilename(download.path, trackJson, albumJson, newQuality));
|
outFile = new File(Deezer.generateFilename(download.path, trackJson, albumJson, qualityInfo.quality));
|
||||||
}
|
}
|
||||||
parentDir = new File(outFile.getParent());
|
parentDir = new File(outFile.getParent());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -415,7 +377,7 @@ public class DownloadService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Download
|
//Download
|
||||||
String sURL = Deezer.getTrackUrl(download.trackId, download.md5origin, download.mediaVersion, newQuality);
|
String sURL = Deezer.getTrackUrl(qualityInfo.trackId, qualityInfo.md5origin, qualityInfo.mediaVersion, qualityInfo.quality);
|
||||||
try {
|
try {
|
||||||
URL url = new URL(sURL);
|
URL url = new URL(sURL);
|
||||||
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
||||||
@ -858,7 +820,23 @@ public class DownloadService extends Service {
|
|||||||
|
|
||||||
//Parse settings from bundle sent from UI
|
//Parse settings from bundle sent from UI
|
||||||
static DownloadSettings fromBundle(Bundle b) {
|
static DownloadSettings fromBundle(Bundle b) {
|
||||||
return new DownloadSettings(b.getInt("downloadThreads"), b.getBoolean("overwriteDownload"), b.getBoolean("downloadLyrics"), b.getBoolean("trackCover"), b.getString("arl"), b.getBoolean("albumCover"), b.getBoolean("nomediaFiles"));
|
JSONObject json;
|
||||||
|
try {
|
||||||
|
json = new JSONObject(b.getString("json"));
|
||||||
|
return new DownloadSettings(
|
||||||
|
json.getInt("downloadThreads"),
|
||||||
|
json.getBoolean("overwriteDownload"),
|
||||||
|
json.getBoolean("downloadLyrics"),
|
||||||
|
json.getBoolean("trackCover"),
|
||||||
|
json.getString("arl"),
|
||||||
|
json.getBoolean("albumCover"),
|
||||||
|
json.getBoolean("nomediaFiles")
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
//Shouldn't happen
|
||||||
|
Log.e("ERR", "Error loading settings!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,13 +23,20 @@ import java.io.ByteArrayOutputStream;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity;
|
import io.flutter.embedding.android.FlutterActivity;
|
||||||
import io.flutter.embedding.engine.FlutterEngine;
|
import io.flutter.embedding.engine.FlutterEngine;
|
||||||
@ -48,6 +55,7 @@ public class MainActivity extends FlutterActivity {
|
|||||||
Messenger serviceMessenger;
|
Messenger serviceMessenger;
|
||||||
Messenger activityMessenger;
|
Messenger activityMessenger;
|
||||||
SQLiteDatabase db;
|
SQLiteDatabase db;
|
||||||
|
StreamServer streamServer;
|
||||||
|
|
||||||
//Data if started from intent
|
//Data if started from intent
|
||||||
String intentPreload;
|
String intentPreload;
|
||||||
@ -122,13 +130,7 @@ public class MainActivity extends FlutterActivity {
|
|||||||
//Update settings from UI
|
//Update settings from UI
|
||||||
if (call.method.equals("updateSettings")) {
|
if (call.method.equals("updateSettings")) {
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putInt("downloadThreads", (int)call.argument("downloadThreads"));
|
bundle.putString("json", call.argument("json").toString());
|
||||||
bundle.putBoolean("overwriteDownload", (boolean)call.argument("overwriteDownload"));
|
|
||||||
bundle.putBoolean("downloadLyrics", (boolean)call.argument("downloadLyrics"));
|
|
||||||
bundle.putBoolean("trackCover", (boolean)call.argument("trackCover"));
|
|
||||||
bundle.putString("arl", (String)call.argument("arl"));
|
|
||||||
bundle.putBoolean("albumCover", (boolean)call.argument("albumCover"));
|
|
||||||
bundle.putBoolean("nomediaFiles", (boolean)call.argument("nomediaFiles"));
|
|
||||||
sendMessage(DownloadService.SERVICE_SETTINGS_UPDATE, bundle);
|
sendMessage(DownloadService.SERVICE_SETTINGS_UPDATE, bundle);
|
||||||
|
|
||||||
result.success(null);
|
result.success(null);
|
||||||
@ -185,6 +187,31 @@ public class MainActivity extends FlutterActivity {
|
|||||||
result.success(System.getProperty("os.arch"));
|
result.success(System.getProperty("os.arch"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
//Start streaming server
|
||||||
|
if (call.method.equals("startServer")) {
|
||||||
|
if (streamServer == null) {
|
||||||
|
//Get offline path
|
||||||
|
String offlinePath = getExternalFilesDir("offline").getAbsolutePath();
|
||||||
|
//Start server
|
||||||
|
streamServer = new StreamServer(call.argument("arl"), offlinePath);
|
||||||
|
streamServer.start();
|
||||||
|
}
|
||||||
|
result.success(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//Get quality info from stream
|
||||||
|
if (call.method.equals("getStreamInfo")) {
|
||||||
|
if (streamServer == null) {
|
||||||
|
result.success(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StreamServer.StreamInfo info = streamServer.streams.get(call.argument("id").toString());
|
||||||
|
if (info != null)
|
||||||
|
result.success(info.toJSON());
|
||||||
|
else
|
||||||
|
result.success(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
result.error("0", "Not implemented!", "Not implemented!");
|
result.error("0", "Not implemented!", "Not implemented!");
|
||||||
})));
|
})));
|
||||||
@ -208,14 +235,38 @@ public class MainActivity extends FlutterActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onStart() {
|
protected void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
||||||
//Bind downloader service
|
//Bind downloader service
|
||||||
activityMessenger = new Messenger(new IncomingHandler(this));
|
activityMessenger = new Messenger(new IncomingHandler(this));
|
||||||
Intent intent = new Intent(this, DownloadService.class);
|
Intent intent = new Intent(this, DownloadService.class);
|
||||||
intent.putExtra("activityMessenger", activityMessenger);
|
intent.putExtra("activityMessenger", activityMessenger);
|
||||||
bindService(intent, connection, Context.BIND_AUTO_CREATE);
|
startService(intent);
|
||||||
|
bindService(intent, connection, 0);
|
||||||
//Get DB
|
//Get DB
|
||||||
DownloadsDatabase dbHelper = new DownloadsDatabase(getApplicationContext());
|
DownloadsDatabase dbHelper = new DownloadsDatabase(getApplicationContext());
|
||||||
db = dbHelper.getWritableDatabase();
|
db = dbHelper.getWritableDatabase();
|
||||||
|
|
||||||
|
//Trust all SSL Certs - Credits to Kilowatt36
|
||||||
|
TrustManager[] trustAllCerts = new TrustManager[]{
|
||||||
|
new X509TrustManager() {
|
||||||
|
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
public void checkClientTrusted(X509Certificate[] certs, String authType) {
|
||||||
|
}
|
||||||
|
public void checkServerTrusted(X509Certificate[] certs, String authType) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
SSLContext sc;
|
||||||
|
try {
|
||||||
|
sc = SSLContext.getInstance("SSL");
|
||||||
|
sc.init(null, trustAllCerts, new java.security.SecureRandom());
|
||||||
|
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
|
||||||
|
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
||||||
|
Log.e(this.getLocalClassName(), e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -229,6 +280,14 @@ public class MainActivity extends FlutterActivity {
|
|||||||
db.close();
|
db.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
//Stop server
|
||||||
|
if (streamServer != null)
|
||||||
|
streamServer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
//Connection to download service
|
//Connection to download service
|
||||||
private ServiceConnection connection = new ServiceConnection() {
|
private ServiceConnection connection = new ServiceConnection() {
|
||||||
@Override
|
@Override
|
||||||
|
285
android/app/src/main/java/f/f/freezer/StreamServer.java
Normal file
285
android/app/src/main/java/f/f/freezer/StreamServer.java
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
package f.f.freezer;
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
|
import fi.iki.elonen.NanoHTTPD;
|
||||||
|
|
||||||
|
public class StreamServer {
|
||||||
|
|
||||||
|
public HashMap<String, StreamInfo> streams = new HashMap<>();
|
||||||
|
|
||||||
|
private WebServer server;
|
||||||
|
private String host = "127.0.0.1";
|
||||||
|
private int port = 36958;
|
||||||
|
private String offlinePath;
|
||||||
|
|
||||||
|
//Shared log & API
|
||||||
|
private DownloadLog logger;
|
||||||
|
private Deezer deezer;
|
||||||
|
|
||||||
|
StreamServer(String arl, String offlinePath) {
|
||||||
|
//Initialize shared variables
|
||||||
|
logger = new DownloadLog();
|
||||||
|
deezer = new Deezer();
|
||||||
|
deezer.init(logger, arl);
|
||||||
|
this.offlinePath = offlinePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Create server
|
||||||
|
void start() {
|
||||||
|
try {
|
||||||
|
server = new WebServer(host, port);
|
||||||
|
server.start();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
if (server != null)
|
||||||
|
server.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Information about streamed audio - for showing in UI
|
||||||
|
public class StreamInfo {
|
||||||
|
String format;
|
||||||
|
long size;
|
||||||
|
//"Stream" or "Offline"
|
||||||
|
String source;
|
||||||
|
|
||||||
|
StreamInfo(String format, long size, String source) {
|
||||||
|
this.format = format;
|
||||||
|
this.size = size;
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
//For passing into UI
|
||||||
|
public HashMap<String, Object> toJSON() {
|
||||||
|
HashMap<String, Object> out = new HashMap<>();
|
||||||
|
out.put("format", format);
|
||||||
|
out.put("size", size);
|
||||||
|
out.put("source", source);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class WebServer extends NanoHTTPD {
|
||||||
|
public WebServer(String hostname, int port) {
|
||||||
|
super(hostname, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response serve(IHTTPSession session) {
|
||||||
|
//Must be only GET
|
||||||
|
if (session.getMethod() != Method.GET)
|
||||||
|
return newFixedLengthResponse(Response.Status.METHOD_NOT_ALLOWED, MIME_PLAINTEXT, "Only GET request supported!");
|
||||||
|
|
||||||
|
//Parse range header
|
||||||
|
String rangeHeader = session.getHeaders().get("range");
|
||||||
|
int startBytes = 0;
|
||||||
|
boolean isRanged = false;
|
||||||
|
int end = -1;
|
||||||
|
if (rangeHeader != null && rangeHeader.startsWith("bytes")) {
|
||||||
|
isRanged = true;
|
||||||
|
String[] ranges = rangeHeader.split("=")[1].split("-");
|
||||||
|
startBytes = Integer.parseInt(ranges[0]);
|
||||||
|
if (ranges.length > 1 && !ranges[1].equals(" ")) {
|
||||||
|
end = Integer.parseInt(ranges[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check query parameters
|
||||||
|
if (session.getParameters().keySet().size() < 4) {
|
||||||
|
//Play offline
|
||||||
|
if (session.getParameters().get("id") != null) {
|
||||||
|
return offlineStream(session, startBytes, end, isRanged);
|
||||||
|
}
|
||||||
|
//Missing QP
|
||||||
|
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "Invalid / Missing QP");
|
||||||
|
}
|
||||||
|
|
||||||
|
//Stream
|
||||||
|
return deezerStream(session, startBytes, end, isRanged);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response offlineStream(IHTTPSession session, int startBytes, int end, boolean isRanged) {
|
||||||
|
//Get path
|
||||||
|
String trackId = session.getParameters().get("id").get(0);
|
||||||
|
File file = new File(offlinePath, trackId);
|
||||||
|
long size = file.length();
|
||||||
|
//Read header
|
||||||
|
boolean isFlac = false;
|
||||||
|
try {
|
||||||
|
InputStream inputStream = new FileInputStream(file);
|
||||||
|
byte[] buffer = new byte[4];
|
||||||
|
inputStream.read(buffer, 0, 4);
|
||||||
|
inputStream.close();
|
||||||
|
if (new String(buffer).equals("fLaC"))
|
||||||
|
isFlac = true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "Invalid file!");
|
||||||
|
}
|
||||||
|
//Open file
|
||||||
|
RandomAccessFile randomAccessFile;
|
||||||
|
try {
|
||||||
|
randomAccessFile = new RandomAccessFile(file, "r");
|
||||||
|
randomAccessFile.seek(startBytes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "Failed getting data!");
|
||||||
|
}
|
||||||
|
|
||||||
|
//Generate response
|
||||||
|
Response response = newFixedLengthResponse(
|
||||||
|
isRanged ? Response.Status.PARTIAL_CONTENT : Response.Status.OK,
|
||||||
|
isFlac ? "audio/flac" : "audio/mpeg",
|
||||||
|
new InputStream() {
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
//Pass thru
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b, int off, int len) throws IOException {
|
||||||
|
return randomAccessFile.read(b, off, len);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
((end == -1) ? size : end) - startBytes
|
||||||
|
);
|
||||||
|
//Ranged header
|
||||||
|
if (isRanged) {
|
||||||
|
String range = "bytes " + Integer.toString(startBytes) + "-" + Long.toString((end == -1) ? size - 1 : end);
|
||||||
|
range += "/" + Long.toString(size);
|
||||||
|
response.addHeader("Content-Range", range);
|
||||||
|
}
|
||||||
|
response.addHeader("Accept-Ranges", "bytes");
|
||||||
|
|
||||||
|
//Save stream info
|
||||||
|
streams.put(trackId, new StreamInfo((isFlac ? "FLAC" : "MP3"), size, "Offline"));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response deezerStream(IHTTPSession session, int startBytes, int end, boolean isRanged) {
|
||||||
|
//Get QP into Quality Info
|
||||||
|
Deezer.QualityInfo qualityInfo = new Deezer.QualityInfo(
|
||||||
|
Integer.parseInt(session.getParameters().get("q").get(0)),
|
||||||
|
session.getParameters().get("id").get(0),
|
||||||
|
session.getParameters().get("md5origin").get(0),
|
||||||
|
session.getParameters().get("mv").get(0),
|
||||||
|
logger
|
||||||
|
);
|
||||||
|
//Fallback
|
||||||
|
try {
|
||||||
|
boolean res = qualityInfo.fallback(deezer);
|
||||||
|
if (!res)
|
||||||
|
throw new Exception("No more to fallback!");
|
||||||
|
} catch (Exception e) {
|
||||||
|
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Fallback failed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
//Calculate Deezer offsets
|
||||||
|
int deezerStart = startBytes - (startBytes % 2048);
|
||||||
|
int dropBytes = startBytes % 2048;
|
||||||
|
//Start download
|
||||||
|
String sURL = Deezer.getTrackUrl(qualityInfo.trackId, qualityInfo.md5origin, qualityInfo.mediaVersion, qualityInfo.quality);
|
||||||
|
try {
|
||||||
|
URL url = new URL(sURL);
|
||||||
|
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
||||||
|
//Set headers
|
||||||
|
connection.setConnectTimeout(30000);
|
||||||
|
connection.setRequestMethod("GET");
|
||||||
|
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36");
|
||||||
|
connection.setRequestProperty("Accept-Language", "*");
|
||||||
|
connection.setRequestProperty("Accept", "*/*");
|
||||||
|
connection.setRequestProperty("Range", "bytes=" + Integer.toString(deezerStart) + "-" + ((end == -1) ? "" : Integer.toString(end)));
|
||||||
|
connection.connect();
|
||||||
|
|
||||||
|
//Get decryption key
|
||||||
|
final byte[] key = Deezer.getKey(qualityInfo.trackId);
|
||||||
|
|
||||||
|
//Write response headers
|
||||||
|
Response outResponse = newFixedLengthResponse(
|
||||||
|
isRanged ? Response.Status.PARTIAL_CONTENT : Response.Status.OK,
|
||||||
|
(qualityInfo.quality == 9) ? "audio/flac" : "audio/mpeg",
|
||||||
|
new BufferedInputStream(new FilterInputStream(connection.getInputStream()) {
|
||||||
|
|
||||||
|
int counter = deezerStart / 2048;
|
||||||
|
int drop = dropBytes;
|
||||||
|
|
||||||
|
//Decryption stream
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b, int off, int len) throws IOException {
|
||||||
|
//Read 2048b or EOF
|
||||||
|
byte[] buffer = new byte[2048];
|
||||||
|
int read = 0;
|
||||||
|
int totalRead = 0;
|
||||||
|
while (read != -1 && totalRead != 2048) {
|
||||||
|
read = in.read(buffer, totalRead, 2048 - totalRead);
|
||||||
|
if (read != -1)
|
||||||
|
totalRead += read;
|
||||||
|
}
|
||||||
|
if (totalRead == 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
//Not full chunk return unencrypted
|
||||||
|
if (totalRead != 2048) {
|
||||||
|
System.arraycopy(buffer, 0, b, off, totalRead);
|
||||||
|
return totalRead;
|
||||||
|
}
|
||||||
|
//Decrypt
|
||||||
|
if ((counter % 3) == 0) {
|
||||||
|
buffer = Deezer.decryptChunk(key, buffer);
|
||||||
|
}
|
||||||
|
//Drop bytes from rounding to 2048
|
||||||
|
if (drop > 0) {
|
||||||
|
int output = 2048 - drop;
|
||||||
|
System.arraycopy(buffer, drop, b, off, output);
|
||||||
|
drop = 0;
|
||||||
|
counter++;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
//Copy
|
||||||
|
System.arraycopy(buffer, 0, b, off, 2048);
|
||||||
|
counter++;
|
||||||
|
return 2048;
|
||||||
|
}
|
||||||
|
}, 2048),
|
||||||
|
connection.getContentLength() - dropBytes
|
||||||
|
);
|
||||||
|
//Ranged header
|
||||||
|
if (isRanged) {
|
||||||
|
String range = "bytes " + Integer.toString(startBytes) + "-" + Integer.toString((end == -1) ? (connection.getContentLength() + deezerStart) - 1 : end);
|
||||||
|
range += "/" + Integer.toString(connection.getContentLength() + deezerStart);
|
||||||
|
outResponse.addHeader("Content-Range", range);
|
||||||
|
}
|
||||||
|
outResponse.addHeader("Accept-Ranges", "bytes");
|
||||||
|
|
||||||
|
//Save stream info, use original track id
|
||||||
|
streams.put(session.getParameters().get("id").get(0), new StreamInfo(
|
||||||
|
((qualityInfo.quality == 9) ? "FLAC" : "MP3"),
|
||||||
|
deezerStart + connection.getContentLength(),
|
||||||
|
"Stream"
|
||||||
|
));
|
||||||
|
|
||||||
|
return outResponse;
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "Failed getting data!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -29,19 +29,9 @@ class Cache {
|
|||||||
@JsonKey(defaultValue: [])
|
@JsonKey(defaultValue: [])
|
||||||
List<Track> history = [];
|
List<Track> history = [];
|
||||||
|
|
||||||
//Cache playlist sort type {id: sort}
|
//All sorting cached
|
||||||
@JsonKey(defaultValue: {})
|
@JsonKey(defaultValue: [])
|
||||||
Map<String, SortType> playlistSort;
|
List<Sorting> sorts = [];
|
||||||
|
|
||||||
//Sort
|
|
||||||
@JsonKey(defaultValue: AlbumSortType.DEFAULT)
|
|
||||||
AlbumSortType albumSort;
|
|
||||||
@JsonKey(defaultValue: ArtistSortType.DEFAULT)
|
|
||||||
ArtistSortType artistSort;
|
|
||||||
@JsonKey(defaultValue: PlaylistSortType.DEFAULT)
|
|
||||||
PlaylistSortType libraryPlaylistSort;
|
|
||||||
@JsonKey(defaultValue: SortType.DEFAULT)
|
|
||||||
SortType trackSort;
|
|
||||||
|
|
||||||
//Sleep timer
|
//Sleep timer
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
|
@ -16,21 +16,11 @@ Cache _$CacheFromJson(Map<String, dynamic> json) {
|
|||||||
e == null ? null : Track.fromJson(e as Map<String, dynamic>))
|
e == null ? null : Track.fromJson(e as Map<String, dynamic>))
|
||||||
?.toList() ??
|
?.toList() ??
|
||||||
[]
|
[]
|
||||||
..playlistSort = (json['playlistSort'] as Map<String, dynamic>)?.map(
|
..sorts = (json['sorts'] as List)
|
||||||
(k, e) => MapEntry(k, _$enumDecodeNullable(_$SortTypeEnumMap, e)),
|
?.map((e) =>
|
||||||
) ??
|
e == null ? null : Sorting.fromJson(e as Map<String, dynamic>))
|
||||||
{}
|
?.toList() ??
|
||||||
..albumSort =
|
[]
|
||||||
_$enumDecodeNullable(_$AlbumSortTypeEnumMap, json['albumSort']) ??
|
|
||||||
AlbumSortType.DEFAULT
|
|
||||||
..artistSort =
|
|
||||||
_$enumDecodeNullable(_$ArtistSortTypeEnumMap, json['artistSort']) ??
|
|
||||||
ArtistSortType.DEFAULT
|
|
||||||
..libraryPlaylistSort = _$enumDecodeNullable(
|
|
||||||
_$PlaylistSortTypeEnumMap, json['libraryPlaylistSort']) ??
|
|
||||||
PlaylistSortType.DEFAULT
|
|
||||||
..trackSort = _$enumDecodeNullable(_$SortTypeEnumMap, json['trackSort']) ??
|
|
||||||
SortType.DEFAULT
|
|
||||||
..searchHistory =
|
..searchHistory =
|
||||||
Cache._searchHistoryFromJson(json['searchHistory2'] as List)
|
Cache._searchHistoryFromJson(json['searchHistory2'] as List)
|
||||||
..threadsWarning = json['threadsWarning'] as bool ?? false
|
..threadsWarning = json['threadsWarning'] as bool ?? false
|
||||||
@ -40,18 +30,25 @@ Cache _$CacheFromJson(Map<String, dynamic> json) {
|
|||||||
Map<String, dynamic> _$CacheToJson(Cache instance) => <String, dynamic>{
|
Map<String, dynamic> _$CacheToJson(Cache instance) => <String, dynamic>{
|
||||||
'libraryTracks': instance.libraryTracks,
|
'libraryTracks': instance.libraryTracks,
|
||||||
'history': instance.history,
|
'history': instance.history,
|
||||||
'playlistSort': instance.playlistSort
|
'sorts': instance.sorts,
|
||||||
?.map((k, e) => MapEntry(k, _$SortTypeEnumMap[e])),
|
|
||||||
'albumSort': _$AlbumSortTypeEnumMap[instance.albumSort],
|
|
||||||
'artistSort': _$ArtistSortTypeEnumMap[instance.artistSort],
|
|
||||||
'libraryPlaylistSort':
|
|
||||||
_$PlaylistSortTypeEnumMap[instance.libraryPlaylistSort],
|
|
||||||
'trackSort': _$SortTypeEnumMap[instance.trackSort],
|
|
||||||
'searchHistory2': Cache._searchHistoryToJson(instance.searchHistory),
|
'searchHistory2': Cache._searchHistoryToJson(instance.searchHistory),
|
||||||
'threadsWarning': instance.threadsWarning,
|
'threadsWarning': instance.threadsWarning,
|
||||||
'lastUpdateCheck': instance.lastUpdateCheck,
|
'lastUpdateCheck': instance.lastUpdateCheck,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
SearchHistoryItem _$SearchHistoryItemFromJson(Map<String, dynamic> json) {
|
||||||
|
return SearchHistoryItem(
|
||||||
|
json['data'],
|
||||||
|
_$enumDecodeNullable(_$SearchHistoryItemTypeEnumMap, json['type']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SearchHistoryItemToJson(SearchHistoryItem instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'data': instance.data,
|
||||||
|
'type': _$SearchHistoryItemTypeEnumMap[instance.type],
|
||||||
|
};
|
||||||
|
|
||||||
T _$enumDecode<T>(
|
T _$enumDecode<T>(
|
||||||
Map<T, dynamic> enumValues,
|
Map<T, dynamic> enumValues,
|
||||||
dynamic source, {
|
dynamic source, {
|
||||||
@ -84,49 +81,6 @@ T _$enumDecodeNullable<T>(
|
|||||||
return _$enumDecode<T>(enumValues, source, unknownValue: unknownValue);
|
return _$enumDecode<T>(enumValues, source, unknownValue: unknownValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
const _$SortTypeEnumMap = {
|
|
||||||
SortType.DEFAULT: 'DEFAULT',
|
|
||||||
SortType.REVERSE: 'REVERSE',
|
|
||||||
SortType.ALPHABETIC: 'ALPHABETIC',
|
|
||||||
SortType.ARTIST: 'ARTIST',
|
|
||||||
};
|
|
||||||
|
|
||||||
const _$AlbumSortTypeEnumMap = {
|
|
||||||
AlbumSortType.DEFAULT: 'DEFAULT',
|
|
||||||
AlbumSortType.REVERSE: 'REVERSE',
|
|
||||||
AlbumSortType.ALPHABETIC: 'ALPHABETIC',
|
|
||||||
AlbumSortType.ARTIST: 'ARTIST',
|
|
||||||
AlbumSortType.DATE: 'DATE',
|
|
||||||
};
|
|
||||||
|
|
||||||
const _$ArtistSortTypeEnumMap = {
|
|
||||||
ArtistSortType.DEFAULT: 'DEFAULT',
|
|
||||||
ArtistSortType.REVERSE: 'REVERSE',
|
|
||||||
ArtistSortType.POPULARITY: 'POPULARITY',
|
|
||||||
ArtistSortType.ALPHABETIC: 'ALPHABETIC',
|
|
||||||
};
|
|
||||||
|
|
||||||
const _$PlaylistSortTypeEnumMap = {
|
|
||||||
PlaylistSortType.DEFAULT: 'DEFAULT',
|
|
||||||
PlaylistSortType.REVERSE: 'REVERSE',
|
|
||||||
PlaylistSortType.ALPHABETIC: 'ALPHABETIC',
|
|
||||||
PlaylistSortType.USER: 'USER',
|
|
||||||
PlaylistSortType.TRACK_COUNT: 'TRACK_COUNT',
|
|
||||||
};
|
|
||||||
|
|
||||||
SearchHistoryItem _$SearchHistoryItemFromJson(Map<String, dynamic> json) {
|
|
||||||
return SearchHistoryItem(
|
|
||||||
json['data'],
|
|
||||||
_$enumDecodeNullable(_$SearchHistoryItemTypeEnumMap, json['type']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> _$SearchHistoryItemToJson(SearchHistoryItem instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'data': instance.data,
|
|
||||||
'type': _$SearchHistoryItemTypeEnumMap[instance.type],
|
|
||||||
};
|
|
||||||
|
|
||||||
const _$SearchHistoryItemTypeEnumMap = {
|
const _$SearchHistoryItemTypeEnumMap = {
|
||||||
SearchHistoryItemType.TRACK: 'TRACK',
|
SearchHistoryItemType.TRACK: 'TRACK',
|
||||||
SearchHistoryItemType.ALBUM: 'ALBUM',
|
SearchHistoryItemType.ALBUM: 'ALBUM',
|
||||||
|
@ -459,12 +459,24 @@ class DeezerAPI {
|
|||||||
return data['results']['data'].map<Track>((t) => Track.fromPrivateJson(t)).toList();
|
return data['results']['data'].map<Track>((t) => Track.fromPrivateJson(t)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get similar tracks for track with id [trackId]
|
//Get similar tracks for track with id [trackId]
|
||||||
Future<List<Track>> playMix(String trackId) async {
|
Future<List<Track>> playMix(String trackId) async {
|
||||||
Map data = await callApi('song.getContextualTrackMix', params: {
|
Map data = await callApi('song.getContextualTrackMix', params: {
|
||||||
'sng_ids': [trackId]
|
'sng_ids': [trackId]
|
||||||
});
|
});
|
||||||
return data['results']['data'].map<Track>((t) => Track.fromPrivateJson(t)).toList();
|
return data['results']['data'].map<Track>((t) => Track.fromPrivateJson(t)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<ShowEpisode>> allShowEpisodes(String showId) async {
|
||||||
|
Map data = await callApi('deezer.pageShow', params: {
|
||||||
|
'country': settings.deezerCountry,
|
||||||
|
'lang': settings.deezerLanguage,
|
||||||
|
'nb': 1000,
|
||||||
|
'show_id': showId,
|
||||||
|
'start': 0,
|
||||||
|
'user_id': int.parse(deezerAPI.userId)
|
||||||
|
});
|
||||||
|
return data['results']['EPISODES']['data'].map<ShowEpisode>((e) => ShowEpisode.fromPrivateJson(e)).toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
|
import 'package:freezer/api/cache.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@ -33,12 +34,13 @@ class Track {
|
|||||||
bool favorite;
|
bool favorite;
|
||||||
int diskNumber;
|
int diskNumber;
|
||||||
bool explicit;
|
bool explicit;
|
||||||
int favoriteDate;
|
//Date added to playlist / favorites
|
||||||
|
int addedDate;
|
||||||
|
|
||||||
List<dynamic> playbackDetails;
|
List<dynamic> playbackDetails;
|
||||||
|
|
||||||
Track({this.id, this.title, this.duration, this.album, this.playbackDetails, this.albumArt,
|
Track({this.id, this.title, this.duration, this.album, this.playbackDetails, this.albumArt,
|
||||||
this.artists, this.trackNumber, this.offline, this.lyrics, this.favorite, this.diskNumber, this.explicit, this.favoriteDate});
|
this.artists, this.trackNumber, this.offline, this.lyrics, this.favorite, this.diskNumber, this.explicit, this.addedDate});
|
||||||
|
|
||||||
String get artistString => artists.map<String>((art) => art.name).join(', ');
|
String get artistString => artists.map<String>((art) => art.name).join(', ');
|
||||||
String get durationString => "${duration.inMinutes}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
String get durationString => "${duration.inMinutes}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||||
@ -141,7 +143,7 @@ class Track {
|
|||||||
favorite: favorite,
|
favorite: favorite,
|
||||||
diskNumber: int.parse(json['DISK_NUMBER']??'1'),
|
diskNumber: int.parse(json['DISK_NUMBER']??'1'),
|
||||||
explicit: (json['EXPLICIT_LYRICS'].toString() == '1') ? true:false,
|
explicit: (json['EXPLICIT_LYRICS'].toString() == '1') ? true:false,
|
||||||
favoriteDate: json['DATE_ADD']
|
addedDate: json['DATE_ADD']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Map<String, dynamic> toSQL({off = false}) => {
|
Map<String, dynamic> toSQL({off = false}) => {
|
||||||
@ -157,7 +159,7 @@ class Track {
|
|||||||
'favorite': (favorite??0)?1:0,
|
'favorite': (favorite??0)?1:0,
|
||||||
'diskNumber': diskNumber,
|
'diskNumber': diskNumber,
|
||||||
'explicit': explicit?1:0,
|
'explicit': explicit?1:0,
|
||||||
// 'favoriteDate': favoriteDate
|
//'favoriteDate': favoriteDate
|
||||||
};
|
};
|
||||||
factory Track.fromSQL(Map<String, dynamic> data) => Track(
|
factory Track.fromSQL(Map<String, dynamic> data) => Track(
|
||||||
id: data['trackId']??data['id'], //If loading from downloads table
|
id: data['trackId']??data['id'], //If loading from downloads table
|
||||||
@ -174,7 +176,7 @@ class Track {
|
|||||||
favorite: (data['favorite'] == 1) ? true:false,
|
favorite: (data['favorite'] == 1) ? true:false,
|
||||||
diskNumber: data['diskNumber'],
|
diskNumber: data['diskNumber'],
|
||||||
explicit: (data['explicit'] == 1) ? true:false,
|
explicit: (data['explicit'] == 1) ? true:false,
|
||||||
// favoriteDate: data['favoriteDate']
|
//favoriteDate: data['favoriteDate']
|
||||||
);
|
);
|
||||||
|
|
||||||
factory Track.fromJson(Map<String, dynamic> json) => _$TrackFromJson(json);
|
factory Track.fromJson(Map<String, dynamic> json) => _$TrackFromJson(json);
|
||||||
@ -238,7 +240,7 @@ class Album {
|
|||||||
'library': (library??false)?1:0,
|
'library': (library??false)?1:0,
|
||||||
'type': AlbumType.values.indexOf(type),
|
'type': AlbumType.values.indexOf(type),
|
||||||
'releaseDate': releaseDate,
|
'releaseDate': releaseDate,
|
||||||
// 'favoriteDate': favoriteDate
|
//'favoriteDate': favoriteDate
|
||||||
};
|
};
|
||||||
factory Album.fromSQL(Map<String, dynamic> data) => Album(
|
factory Album.fromSQL(Map<String, dynamic> data) => Album(
|
||||||
id: data['id'],
|
id: data['id'],
|
||||||
@ -255,7 +257,7 @@ class Album {
|
|||||||
library: (data['library'] == 1) ? true:false,
|
library: (data['library'] == 1) ? true:false,
|
||||||
type: AlbumType.values[data['type']],
|
type: AlbumType.values[data['type']],
|
||||||
releaseDate: data['releaseDate'],
|
releaseDate: data['releaseDate'],
|
||||||
// favoriteDate: data['favoriteDate']
|
//favoriteDate: data['favoriteDate']
|
||||||
);
|
);
|
||||||
|
|
||||||
factory Album.fromJson(Map<String, dynamic> json) => _$AlbumFromJson(json);
|
factory Album.fromJson(Map<String, dynamic> json) => _$AlbumFromJson(json);
|
||||||
@ -344,7 +346,7 @@ class Artist {
|
|||||||
'offline': off?1:0,
|
'offline': off?1:0,
|
||||||
'library': (library??false)?1:0,
|
'library': (library??false)?1:0,
|
||||||
'radio': radio?1:0,
|
'radio': radio?1:0,
|
||||||
// 'favoriteDate': favoriteDate
|
//'favoriteDate': favoriteDate
|
||||||
};
|
};
|
||||||
factory Artist.fromSQL(Map<String, dynamic> data) => Artist(
|
factory Artist.fromSQL(Map<String, dynamic> data) => Artist(
|
||||||
id: data['id'],
|
id: data['id'],
|
||||||
@ -361,7 +363,7 @@ class Artist {
|
|||||||
offline: (data['offline'] == 1)?true:false,
|
offline: (data['offline'] == 1)?true:false,
|
||||||
library: (data['library'] == 1)?true:false,
|
library: (data['library'] == 1)?true:false,
|
||||||
radio: (data['radio'] == 1)?true:false,
|
radio: (data['radio'] == 1)?true:false,
|
||||||
// favoriteDate: data['favoriteDate']
|
//favoriteDate: data['favoriteDate']
|
||||||
);
|
);
|
||||||
|
|
||||||
factory Artist.fromJson(Map<String, dynamic> json) => _$ArtistFromJson(json);
|
factory Artist.fromJson(Map<String, dynamic> json) => _$ArtistFromJson(json);
|
||||||
@ -702,6 +704,8 @@ class HomePageItem {
|
|||||||
return HomePageItem(type: HomePageItemType.CHANNEL, value: DeezerChannel.fromPrivateJson(json));
|
return HomePageItem(type: HomePageItemType.CHANNEL, value: DeezerChannel.fromPrivateJson(json));
|
||||||
case 'album':
|
case 'album':
|
||||||
return HomePageItem(type: HomePageItemType.ALBUM, value: Album.fromPrivateJson(json['data']));
|
return HomePageItem(type: HomePageItemType.ALBUM, value: Album.fromPrivateJson(json['data']));
|
||||||
|
case 'show':
|
||||||
|
return HomePageItem(type: HomePageItemType.SHOW, value: Show.fromPrivateJson(json['data']));
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -720,6 +724,8 @@ class HomePageItem {
|
|||||||
return HomePageItem(type: HomePageItemType.CHANNEL, value: DeezerChannel.fromJson(json['value']));
|
return HomePageItem(type: HomePageItemType.CHANNEL, value: DeezerChannel.fromJson(json['value']));
|
||||||
case 'ALBUM':
|
case 'ALBUM':
|
||||||
return HomePageItem(type: HomePageItemType.ALBUM, value: Album.fromJson(json['value']));
|
return HomePageItem(type: HomePageItemType.ALBUM, value: Album.fromJson(json['value']));
|
||||||
|
case 'SHOW':
|
||||||
|
return HomePageItem(type: HomePageItemType.SHOW, value: Show.fromPrivateJson(json['value']));
|
||||||
default:
|
default:
|
||||||
return HomePageItem();
|
return HomePageItem();
|
||||||
}
|
}
|
||||||
@ -762,7 +768,8 @@ enum HomePageItemType {
|
|||||||
PLAYLIST,
|
PLAYLIST,
|
||||||
ARTIST,
|
ARTIST,
|
||||||
CHANNEL,
|
CHANNEL,
|
||||||
ALBUM
|
ALBUM,
|
||||||
|
SHOW
|
||||||
}
|
}
|
||||||
|
|
||||||
enum HomePageSectionLayout {
|
enum HomePageSectionLayout {
|
||||||
@ -797,4 +804,164 @@ class DeezerLinkResponse {
|
|||||||
if (t == 'track') return DeezerLinkType.TRACK;
|
if (t == 'track') return DeezerLinkType.TRACK;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Sorting
|
||||||
|
enum SortType {
|
||||||
|
DEFAULT,
|
||||||
|
ALPHABETIC,
|
||||||
|
ARTIST,
|
||||||
|
ALBUM,
|
||||||
|
RELEASE_DATE,
|
||||||
|
POPULARITY,
|
||||||
|
USER,
|
||||||
|
TRACK_COUNT,
|
||||||
|
DATE_ADDED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SortSourceTypes {
|
||||||
|
//Library
|
||||||
|
TRACKS,
|
||||||
|
PLAYLISTS,
|
||||||
|
ALBUMS,
|
||||||
|
ARTISTS,
|
||||||
|
|
||||||
|
PLAYLIST
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Sorting {
|
||||||
|
SortType type;
|
||||||
|
bool reverse;
|
||||||
|
|
||||||
|
//For preserving sorting
|
||||||
|
String id;
|
||||||
|
SortSourceTypes sourceType;
|
||||||
|
|
||||||
|
Sorting({this.type = SortType.DEFAULT, this.reverse = false, this.id, this.sourceType});
|
||||||
|
|
||||||
|
//Find index of sorting from cache
|
||||||
|
static int index(SortSourceTypes type, {String id}) {
|
||||||
|
//Empty cache
|
||||||
|
if (cache.sorts == null) {
|
||||||
|
cache.sorts = [];
|
||||||
|
cache.save();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
//Find index
|
||||||
|
int index;
|
||||||
|
if (id != null)
|
||||||
|
index = cache.sorts.indexWhere((s) => s.sourceType == type && s.id == id);
|
||||||
|
else
|
||||||
|
index = cache.sorts.indexWhere((s) => s.sourceType == type);
|
||||||
|
if (index == -1)
|
||||||
|
return null;
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Sorting.fromJson(Map<String, dynamic> json) => _$SortingFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$SortingToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Show {
|
||||||
|
|
||||||
|
String name;
|
||||||
|
String description;
|
||||||
|
ImageDetails art;
|
||||||
|
String id;
|
||||||
|
|
||||||
|
Show({this.name, this.description, this.art, this.id});
|
||||||
|
|
||||||
|
//JSON
|
||||||
|
factory Show.fromPrivateJson(Map<dynamic, dynamic> json) => Show(
|
||||||
|
id: json['SHOW_ID'],
|
||||||
|
name: json['SHOW_NAME'],
|
||||||
|
art: ImageDetails.fromPrivateString(json['SHOW_ART_MD5'], type: 'talk'),
|
||||||
|
description: json['SHOW_DESCRIPTION']
|
||||||
|
);
|
||||||
|
|
||||||
|
factory Show.fromJson(Map<String, dynamic> json) => _$ShowFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$ShowToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class ShowEpisode {
|
||||||
|
|
||||||
|
String id;
|
||||||
|
String title;
|
||||||
|
String description;
|
||||||
|
String url;
|
||||||
|
Duration duration;
|
||||||
|
String publishedDate;
|
||||||
|
|
||||||
|
ShowEpisode({this.id, this.title, this.description, this.url, this.duration, this.publishedDate});
|
||||||
|
|
||||||
|
String get durationString => "${duration.inMinutes}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||||
|
|
||||||
|
//Generate MediaItem for playback
|
||||||
|
MediaItem toMediaItem(Show show) {
|
||||||
|
return MediaItem(
|
||||||
|
title: title,
|
||||||
|
displayTitle: title,
|
||||||
|
displaySubtitle: show.name,
|
||||||
|
album: show.name,
|
||||||
|
id: id,
|
||||||
|
extras: {
|
||||||
|
'showUrl': url,
|
||||||
|
'show': jsonEncode(show.toJson()),
|
||||||
|
'thumb': show.art.thumb
|
||||||
|
},
|
||||||
|
displayDescription: description,
|
||||||
|
duration: duration,
|
||||||
|
artUri: show.art.full
|
||||||
|
);
|
||||||
|
}
|
||||||
|
factory ShowEpisode.fromMediaItem(MediaItem mi) {
|
||||||
|
return ShowEpisode(
|
||||||
|
id: mi.id,
|
||||||
|
title: mi.title,
|
||||||
|
description: mi.displayDescription,
|
||||||
|
url: mi.extras['showUrl'],
|
||||||
|
duration: mi.duration,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//JSON
|
||||||
|
factory ShowEpisode.fromPrivateJson(Map<dynamic, dynamic> json) => ShowEpisode(
|
||||||
|
id: json['EPISODE_ID'],
|
||||||
|
title: json['EPISODE_TITLE'],
|
||||||
|
description: json['EPISODE_DESCRIPTION'],
|
||||||
|
url: json['EPISODE_DIRECT_STREAM_URL'],
|
||||||
|
duration: Duration(seconds: int.parse(json['DURATION'].toString())),
|
||||||
|
publishedDate: json['EPISODE_PUBLISHED_TIMESTAMP']
|
||||||
|
);
|
||||||
|
|
||||||
|
factory ShowEpisode.fromJson(Map<String, dynamic> json) => _$ShowEpisodeFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$ShowEpisodeToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
class StreamQualityInfo {
|
||||||
|
String format;
|
||||||
|
int size;
|
||||||
|
String source;
|
||||||
|
|
||||||
|
StreamQualityInfo({this.format, this.size, this.source});
|
||||||
|
|
||||||
|
factory StreamQualityInfo.fromJson(Map json) => StreamQualityInfo(
|
||||||
|
format: json['format'],
|
||||||
|
size: json['size'],
|
||||||
|
source: json['source']
|
||||||
|
);
|
||||||
|
|
||||||
|
int bitrate(Duration duration) {
|
||||||
|
if (size == null || size == 0) return 0;
|
||||||
|
int bitrate = (((size * 8) / 1000) / duration.inSeconds).round();
|
||||||
|
//Round to known values
|
||||||
|
if (bitrate > 122 && bitrate < 134)
|
||||||
|
return 128;
|
||||||
|
if (bitrate > 315 && bitrate < 325)
|
||||||
|
return 320;
|
||||||
|
return bitrate;
|
||||||
|
}
|
||||||
}
|
}
|
@ -32,7 +32,7 @@ Track _$TrackFromJson(Map<String, dynamic> json) {
|
|||||||
favorite: json['favorite'] as bool,
|
favorite: json['favorite'] as bool,
|
||||||
diskNumber: json['diskNumber'] as int,
|
diskNumber: json['diskNumber'] as int,
|
||||||
explicit: json['explicit'] as bool,
|
explicit: json['explicit'] as bool,
|
||||||
favoriteDate: json['favoriteDate'] as int,
|
addedDate: json['addedDate'] as int,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ Map<String, dynamic> _$TrackToJson(Track instance) => <String, dynamic>{
|
|||||||
'favorite': instance.favorite,
|
'favorite': instance.favorite,
|
||||||
'diskNumber': instance.diskNumber,
|
'diskNumber': instance.diskNumber,
|
||||||
'explicit': instance.explicit,
|
'explicit': instance.explicit,
|
||||||
'favoriteDate': instance.favoriteDate,
|
'addedDate': instance.addedDate,
|
||||||
'playbackDetails': instance.playbackDetails,
|
'playbackDetails': instance.playbackDetails,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -387,3 +387,81 @@ Map<String, dynamic> _$DeezerChannelToJson(DeezerChannel instance) =>
|
|||||||
'title': instance.title,
|
'title': instance.title,
|
||||||
'backgroundColor': DeezerChannel._colorToJson(instance.backgroundColor),
|
'backgroundColor': DeezerChannel._colorToJson(instance.backgroundColor),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Sorting _$SortingFromJson(Map<String, dynamic> json) {
|
||||||
|
return Sorting(
|
||||||
|
type: _$enumDecodeNullable(_$SortTypeEnumMap, json['type']),
|
||||||
|
reverse: json['reverse'] as bool,
|
||||||
|
id: json['id'] as String,
|
||||||
|
sourceType:
|
||||||
|
_$enumDecodeNullable(_$SortSourceTypesEnumMap, json['sourceType']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SortingToJson(Sorting instance) => <String, dynamic>{
|
||||||
|
'type': _$SortTypeEnumMap[instance.type],
|
||||||
|
'reverse': instance.reverse,
|
||||||
|
'id': instance.id,
|
||||||
|
'sourceType': _$SortSourceTypesEnumMap[instance.sourceType],
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$SortTypeEnumMap = {
|
||||||
|
SortType.DEFAULT: 'DEFAULT',
|
||||||
|
SortType.ALPHABETIC: 'ALPHABETIC',
|
||||||
|
SortType.ARTIST: 'ARTIST',
|
||||||
|
SortType.ALBUM: 'ALBUM',
|
||||||
|
SortType.RELEASE_DATE: 'RELEASE_DATE',
|
||||||
|
SortType.POPULARITY: 'POPULARITY',
|
||||||
|
SortType.USER: 'USER',
|
||||||
|
SortType.TRACK_COUNT: 'TRACK_COUNT',
|
||||||
|
SortType.DATE_ADDED: 'DATE_ADDED',
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$SortSourceTypesEnumMap = {
|
||||||
|
SortSourceTypes.TRACKS: 'TRACKS',
|
||||||
|
SortSourceTypes.PLAYLISTS: 'PLAYLISTS',
|
||||||
|
SortSourceTypes.ALBUMS: 'ALBUMS',
|
||||||
|
SortSourceTypes.ARTISTS: 'ARTISTS',
|
||||||
|
SortSourceTypes.PLAYLIST: 'PLAYLIST',
|
||||||
|
};
|
||||||
|
|
||||||
|
Show _$ShowFromJson(Map<String, dynamic> json) {
|
||||||
|
return Show(
|
||||||
|
name: json['name'] as String,
|
||||||
|
description: json['description'] as String,
|
||||||
|
art: json['art'] == null
|
||||||
|
? null
|
||||||
|
: ImageDetails.fromJson(json['art'] as Map<String, dynamic>),
|
||||||
|
id: json['id'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ShowToJson(Show instance) => <String, dynamic>{
|
||||||
|
'name': instance.name,
|
||||||
|
'description': instance.description,
|
||||||
|
'art': instance.art,
|
||||||
|
'id': instance.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
ShowEpisode _$ShowEpisodeFromJson(Map<String, dynamic> json) {
|
||||||
|
return ShowEpisode(
|
||||||
|
id: json['id'] as String,
|
||||||
|
title: json['title'] as String,
|
||||||
|
description: json['description'] as String,
|
||||||
|
url: json['url'] as String,
|
||||||
|
duration: json['duration'] == null
|
||||||
|
? null
|
||||||
|
: Duration(microseconds: json['duration'] as int),
|
||||||
|
publishedDate: json['publishedDate'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ShowEpisodeToJson(ShowEpisode instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'title': instance.title,
|
||||||
|
'description': instance.description,
|
||||||
|
'url': instance.url,
|
||||||
|
'duration': instance.duration?.inMicroseconds,
|
||||||
|
'publishedDate': instance.publishedDate,
|
||||||
|
};
|
||||||
|
@ -165,9 +165,10 @@ class PlayerHelper {
|
|||||||
Future _loadQueuePlay(List<MediaItem> queue, String trackId) async {
|
Future _loadQueuePlay(List<MediaItem> queue, String trackId) async {
|
||||||
await startService();
|
await startService();
|
||||||
await settings.updateAudioServiceQuality();
|
await settings.updateAudioServiceQuality();
|
||||||
|
await AudioService.customAction('setIndex', queue.indexWhere((m) => m.id == trackId));
|
||||||
await AudioService.updateQueue(queue);
|
await AudioService.updateQueue(queue);
|
||||||
if (queue[0].id != trackId)
|
// if (queue[0].id != trackId)
|
||||||
await AudioService.skipToQueueItem(trackId);
|
// await AudioService.skipToQueueItem(trackId);
|
||||||
if (!AudioService.playbackState.playing)
|
if (!AudioService.playbackState.playing)
|
||||||
AudioService.play();
|
AudioService.play();
|
||||||
}
|
}
|
||||||
@ -236,6 +237,27 @@ class PlayerHelper {
|
|||||||
source: 'playlist'
|
source: 'playlist'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Play episode from show, load whole show as queue
|
||||||
|
Future playShowEpisode(Show show, List<ShowEpisode> episodes, {int index = 0}) async {
|
||||||
|
QueueSource queueSource = QueueSource(
|
||||||
|
id: show.id,
|
||||||
|
text: show.name,
|
||||||
|
source: 'show'
|
||||||
|
);
|
||||||
|
//Generate media items
|
||||||
|
List<MediaItem> queue = episodes.map<MediaItem>((e) => e.toMediaItem(show)).toList();
|
||||||
|
|
||||||
|
//Load and play
|
||||||
|
await startService();
|
||||||
|
await settings.updateAudioServiceQuality();
|
||||||
|
await setQueueSource(queueSource);
|
||||||
|
await AudioService.customAction('setIndex', index);
|
||||||
|
await AudioService.updateQueue(queue);
|
||||||
|
if (!AudioService.playbackState.playing)
|
||||||
|
AudioService.play();
|
||||||
|
}
|
||||||
|
|
||||||
//Load tracks as queue, play track id, set queue source
|
//Load tracks as queue, play track id, set queue source
|
||||||
Future playFromTrackList(List<Track> tracks, String trackId, QueueSource queueSource) async {
|
Future playFromTrackList(List<Track> tracks, String trackId, QueueSource queueSource) async {
|
||||||
await startService();
|
await startService();
|
||||||
@ -340,7 +362,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||||||
//Quality string
|
//Quality string
|
||||||
if (_queueIndex != -1 && _queueIndex < _queue.length) {
|
if (_queueIndex != -1 && _queueIndex < _queue.length) {
|
||||||
Map extras = mediaItem.extras;
|
Map extras = mediaItem.extras;
|
||||||
extras['qualityString'] = event.qualityString??'';
|
extras['qualityString'] = '';
|
||||||
_queue[_queueIndex] = mediaItem.copyWith(extras: extras);
|
_queue[_queueIndex] = mediaItem.copyWith(extras: extras);
|
||||||
}
|
}
|
||||||
//Update
|
//Update
|
||||||
@ -530,7 +552,6 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||||||
this._queue = q;
|
this._queue = q;
|
||||||
AudioServiceBackground.setQueue(_queue);
|
AudioServiceBackground.setQueue(_queue);
|
||||||
//Load
|
//Load
|
||||||
_queueIndex = 0;
|
|
||||||
await _loadQueue();
|
await _loadQueue();
|
||||||
//await _player.seek(Duration.zero, index: 0);
|
//await _player.seek(Duration.zero, index: 0);
|
||||||
}
|
}
|
||||||
@ -550,8 +571,8 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||||||
_audioSource = ConcatenatingAudioSource(children: sources);
|
_audioSource = ConcatenatingAudioSource(children: sources);
|
||||||
//Load in just_audio
|
//Load in just_audio
|
||||||
try {
|
try {
|
||||||
await _player.load(_audioSource);
|
await _player.load(_audioSource, initialPosition: Duration.zero, initialIndex: qi);
|
||||||
await _player.seek(Duration.zero, index: qi);
|
// await _player.seek(Duration.zero, index: qi);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//Error loading tracks
|
//Error loading tracks
|
||||||
}
|
}
|
||||||
@ -571,9 +592,15 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||||||
String _offlinePath = p.join((await getExternalStorageDirectory()).path, 'offline/');
|
String _offlinePath = p.join((await getExternalStorageDirectory()).path, 'offline/');
|
||||||
File f = File(p.join(_offlinePath, mediaItem.id));
|
File f = File(p.join(_offlinePath, mediaItem.id));
|
||||||
if (await f.exists()) {
|
if (await f.exists()) {
|
||||||
return f.path;
|
//return f.path;
|
||||||
|
//Stream server URL
|
||||||
|
return 'http://localhost:36958/?id=${mediaItem.id}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Show episode direct link
|
||||||
|
if (mediaItem.extras['showUrl'] != null)
|
||||||
|
return mediaItem.extras['showUrl'];
|
||||||
|
|
||||||
//Due to current limitations of just_audio, quality fallback moved to DeezerDataSource in ExoPlayer
|
//Due to current limitations of just_audio, quality fallback moved to DeezerDataSource in ExoPlayer
|
||||||
//This just returns fake url that contains metadata
|
//This just returns fake url that contains metadata
|
||||||
List playbackDetails = jsonDecode(mediaItem.extras['playbackDetails']);
|
List playbackDetails = jsonDecode(mediaItem.extras['playbackDetails']);
|
||||||
@ -583,7 +610,8 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||||||
if (conn == ConnectivityResult.wifi) quality = wifiQuality;
|
if (conn == ConnectivityResult.wifi) quality = wifiQuality;
|
||||||
|
|
||||||
if ((playbackDetails??[]).length < 2) return null;
|
if ((playbackDetails??[]).length < 2) return null;
|
||||||
String url = 'https://dzcdn.net/?md5=${playbackDetails[0]}&mv=${playbackDetails[1]}&q=${quality.toString()}#${mediaItem.id}';
|
//String url = 'https://dzcdn.net/?md5=${playbackDetails[0]}&mv=${playbackDetails[1]}&q=${quality.toString()}#${mediaItem.id}';
|
||||||
|
String url = 'http://localhost:36958/?q=$quality&mv=${playbackDetails[1]}&md5origin=${playbackDetails[0]}&id=${mediaItem.id}';
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -632,6 +660,10 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||||||
AudioServiceBackground.setQueue(_queue);
|
AudioServiceBackground.setQueue(_queue);
|
||||||
_broadcastState();
|
_broadcastState();
|
||||||
}
|
}
|
||||||
|
//Set index without affecting playback for loading
|
||||||
|
if (name == 'setIndex') {
|
||||||
|
this._queueIndex = args;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
@ -281,6 +281,17 @@ const language_en_us = {
|
|||||||
"Unsupported platform!": "Unsupported platform!",
|
"Unsupported platform!": "Unsupported platform!",
|
||||||
"Freezer Updates": "Freezer Updates",
|
"Freezer Updates": "Freezer Updates",
|
||||||
"Update to latest version in the settings.": "Update to latest version in the settings.",
|
"Update to latest version in the settings.": "Update to latest version in the settings.",
|
||||||
"Release date": "Release date"
|
"Release date": "Release date",
|
||||||
|
|
||||||
|
//0.6.4 Strings:
|
||||||
|
"Shows": "Shows",
|
||||||
|
"Charts": "Charts",
|
||||||
|
"Browse": "Browse",
|
||||||
|
"Quick access": "Quick access",
|
||||||
|
"Play mix": "Play mix",
|
||||||
|
"Share show": "Share show",
|
||||||
|
"Date added": "Date added",
|
||||||
|
"Discord": "Discord",
|
||||||
|
"Official Discord server": "Official Discord server"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -71,7 +71,7 @@ class _FreezerAppState extends State<FreezerApp> {
|
|||||||
});
|
});
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||||
systemNavigationBarColor: settings.themeData.bottomAppBarColor,
|
systemNavigationBarColor: settings.themeData.bottomAppBarColor,
|
||||||
systemNavigationBarIconBrightness: settings.isDark? Brightness.light : Brightness.dark
|
systemNavigationBarIconBrightness: settings.isDark ? Brightness.light : Brightness.dark,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,6 +168,8 @@ class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateM
|
|||||||
void initState() {
|
void initState() {
|
||||||
navigatorKey = GlobalKey<NavigatorState>();
|
navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
_startStreamingServer();
|
||||||
|
|
||||||
//Start with parameters
|
//Start with parameters
|
||||||
_setupUniLinks();
|
_setupUniLinks();
|
||||||
_loadPreloadInfo();
|
_loadPreloadInfo();
|
||||||
@ -181,6 +183,10 @@ class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateM
|
|||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _startStreamingServer() async {
|
||||||
|
await DownloadManager.platform.invokeMethod("startServer", {"arl": settings.arl});
|
||||||
|
}
|
||||||
|
|
||||||
void _prepareQuickActions() {
|
void _prepareQuickActions() {
|
||||||
final QuickActions quickActions = QuickActions();
|
final QuickActions quickActions = QuickActions();
|
||||||
quickActions.initialize((type) {
|
quickActions.initialize((type) {
|
||||||
|
@ -122,15 +122,7 @@ class Settings {
|
|||||||
|
|
||||||
//JSON to forward into download service
|
//JSON to forward into download service
|
||||||
Map getServiceSettings() {
|
Map getServiceSettings() {
|
||||||
return {
|
return {"json": jsonEncode(this.toJson())};
|
||||||
"downloadThreads": downloadThreads,
|
|
||||||
"overwriteDownload": overwriteDownload,
|
|
||||||
"downloadLyrics": downloadLyrics,
|
|
||||||
"trackCover": trackCover,
|
|
||||||
"arl": arl,
|
|
||||||
"albumCover": albumCover,
|
|
||||||
"nomediaFiles": nomediaFiles
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateUseArtColor(bool v) {
|
void updateUseArtColor(bool v) {
|
||||||
|
@ -27,7 +27,8 @@ const supportedLocales = [
|
|||||||
const Locale('hi', 'IN'),
|
const Locale('hi', 'IN'),
|
||||||
const Locale('sk', 'SK'),
|
const Locale('sk', 'SK'),
|
||||||
const Locale('cs', 'CZ'),
|
const Locale('cs', 'CZ'),
|
||||||
const Locale('fil', 'PH')
|
const Locale('fil', 'PH'),
|
||||||
|
const Locale('uwu', 'UWU')
|
||||||
];
|
];
|
||||||
|
|
||||||
extension Localization on String {
|
extension Localization on String {
|
||||||
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttericon/font_awesome5_icons.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:freezer/api/cache.dart';
|
import 'package:freezer/api/cache.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
@ -683,13 +684,6 @@ class _DiscographyScreenState extends State<DiscographyScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SortType {
|
|
||||||
DEFAULT,
|
|
||||||
REVERSE,
|
|
||||||
ALPHABETIC,
|
|
||||||
ARTIST
|
|
||||||
}
|
|
||||||
|
|
||||||
class PlaylistDetails extends StatefulWidget {
|
class PlaylistDetails extends StatefulWidget {
|
||||||
|
|
||||||
Playlist playlist;
|
Playlist playlist;
|
||||||
@ -704,25 +698,30 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||||||
Playlist playlist;
|
Playlist playlist;
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
bool _error = false;
|
bool _error = false;
|
||||||
SortType _sort = SortType.DEFAULT;
|
Sorting _sort;
|
||||||
ScrollController _scrollController = ScrollController();
|
ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
//Get sorted playlist
|
//Get sorted playlist
|
||||||
List<Track> get sorted {
|
List<Track> get sorted {
|
||||||
List<Track> tracks = new List.from(playlist.tracks??[]);
|
List<Track> tracks = new List.from(playlist.tracks??[]);
|
||||||
switch (_sort) {
|
switch (_sort.type) {
|
||||||
case SortType.ALPHABETIC:
|
case SortType.ALPHABETIC:
|
||||||
tracks.sort((a, b) => a.title.compareTo(b.title));
|
tracks.sort((a, b) => a.title.compareTo(b.title));
|
||||||
return tracks;
|
break;
|
||||||
case SortType.ARTIST:
|
case SortType.ARTIST:
|
||||||
tracks.sort((a, b) => a.artists[0].name.toLowerCase().compareTo(b.artists[0].name.toLowerCase()));
|
tracks.sort((a, b) => a.artists[0].name.toLowerCase().compareTo(b.artists[0].name.toLowerCase()));
|
||||||
return tracks;
|
break;
|
||||||
case SortType.REVERSE:
|
case SortType.DATE_ADDED:
|
||||||
return tracks.reversed.toList();
|
tracks.sort((a, b) => (a.addedDate??0) - (b.addedDate??0));
|
||||||
|
break;
|
||||||
case SortType.DEFAULT:
|
case SortType.DEFAULT:
|
||||||
default:
|
default:
|
||||||
return tracks;
|
break;
|
||||||
}
|
}
|
||||||
|
//Reverse
|
||||||
|
if (_sort.reverse)
|
||||||
|
return tracks.reversed.toList();
|
||||||
|
return tracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Load tracks from api
|
//Load tracks from api
|
||||||
@ -748,23 +747,40 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||||||
|
|
||||||
//Load cached playlist sorting
|
//Load cached playlist sorting
|
||||||
void _restoreSort() async {
|
void _restoreSort() async {
|
||||||
if (cache.playlistSort == null) {
|
//Find index
|
||||||
cache.playlistSort = {};
|
int index = Sorting.index(SortSourceTypes.PLAYLIST, id: playlist.id);
|
||||||
await cache.save();
|
if (index == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
//Preload tracks
|
||||||
|
if (playlist.tracks.length < playlist.trackCount) {
|
||||||
|
playlist = await deezerAPI.fullPlaylist(playlist.id);
|
||||||
}
|
}
|
||||||
if (cache.playlistSort[playlist.id] != null) {
|
setState(() => _sort = cache.sorts[index]);
|
||||||
//Preload tracks
|
}
|
||||||
if (playlist.tracks.length < playlist.trackCount) {
|
|
||||||
playlist = await deezerAPI.fullPlaylist(playlist.id);
|
|
||||||
}
|
Future _reverse() async {
|
||||||
setState(() => _sort = cache.playlistSort[playlist.id]);
|
setState(() => _sort.reverse = !_sort.reverse);
|
||||||
|
//Save sorting in cache
|
||||||
|
int index = Sorting.index(SortSourceTypes.TRACKS);
|
||||||
|
if (index != null) {
|
||||||
|
cache.sorts[index] = _sort;
|
||||||
|
} else {
|
||||||
|
cache.sorts.add(_sort);
|
||||||
|
}
|
||||||
|
await cache.save();
|
||||||
|
|
||||||
|
//Preload for sorting
|
||||||
|
if (playlist.tracks.length < playlist.trackCount) {
|
||||||
|
playlist = await deezerAPI.fullPlaylist(playlist.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
playlist = widget.playlist;
|
playlist = widget.playlist;
|
||||||
|
_sort = Sorting(sourceType: SortSourceTypes.PLAYLIST, id: playlist.id);
|
||||||
//If scrolled past 90% load next tracks
|
//If scrolled past 90% load next tracks
|
||||||
_scrollController.addListener(() {
|
_scrollController.addListener(() {
|
||||||
double off = _scrollController.position.maxScrollExtent * 0.90;
|
double off = _scrollController.position.maxScrollExtent * 0.90;
|
||||||
@ -918,21 +934,22 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||||||
//Preload whole playlist
|
//Preload whole playlist
|
||||||
playlist = await deezerAPI.fullPlaylist(playlist.id);
|
playlist = await deezerAPI.fullPlaylist(playlist.id);
|
||||||
}
|
}
|
||||||
setState(() => _sort = s);
|
setState(() => _sort.type = s);
|
||||||
|
|
||||||
//Save sort type to cache
|
//Save sort type to cache
|
||||||
cache.playlistSort[playlist.id] = s;
|
int index = Sorting.index(SortSourceTypes.PLAYLIST, id: playlist.id);
|
||||||
cache.save();
|
if (index == null) {
|
||||||
|
cache.sorts.add(_sort);
|
||||||
|
} else {
|
||||||
|
cache.sorts[index] = _sort;
|
||||||
|
}
|
||||||
|
await cache.save();
|
||||||
},
|
},
|
||||||
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
|
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: SortType.DEFAULT,
|
value: SortType.DEFAULT,
|
||||||
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
|
||||||
value: SortType.REVERSE,
|
|
||||||
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: SortType.ALPHABETIC,
|
value: SortType.ALPHABETIC,
|
||||||
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
||||||
@ -941,8 +958,16 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||||||
value: SortType.ARTIST,
|
value: SortType.ARTIST,
|
||||||
child: Text('Artist'.i18n, style: popupMenuTextStyle()),
|
child: Text('Artist'.i18n, style: popupMenuTextStyle()),
|
||||||
),
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: SortType.DATE_ADDED,
|
||||||
|
child: Text('Date added'.i18n, style: popupMenuTextStyle()),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(_sort.reverse ? FontAwesome5.sort_alpha_up : FontAwesome5.sort_alpha_down),
|
||||||
|
onPressed: () => _reverse(),
|
||||||
|
),
|
||||||
Container(width: 4.0)
|
Container(width: 4.0)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -1039,3 +1064,136 @@ class _MakePlaylistOfflineState extends State<MakePlaylistOffline> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ShowScreen extends StatefulWidget {
|
||||||
|
|
||||||
|
Show show;
|
||||||
|
ShowScreen(this.show, {Key key}): super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ShowScreenState createState() => _ShowScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShowScreenState extends State<ShowScreen> {
|
||||||
|
|
||||||
|
Show _show;
|
||||||
|
bool _loading = true;
|
||||||
|
bool _error = false;
|
||||||
|
List<ShowEpisode> _episodes;
|
||||||
|
|
||||||
|
Future _load() async {
|
||||||
|
//Fetch
|
||||||
|
List<ShowEpisode> e;
|
||||||
|
try {
|
||||||
|
e = await deezerAPI.allShowEpisodes(_show.id);
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
_error = true;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_episodes = e;
|
||||||
|
_loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_show = widget.show;
|
||||||
|
_load();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: FreezerAppBar(_show.name),
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
CachedImage(
|
||||||
|
url: _show.art.full,
|
||||||
|
rounded: true,
|
||||||
|
width: MediaQuery.of(context).size.width / 2 - 16,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: MediaQuery.of(context).size.width / 2 - 16,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_show.name,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20.0,
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Container(height: 8.0),
|
||||||
|
Text(
|
||||||
|
_show.description,
|
||||||
|
maxLines: 6,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16.0
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(height: 4.0),
|
||||||
|
FreezerDivider(),
|
||||||
|
|
||||||
|
//Error
|
||||||
|
if (_error)
|
||||||
|
ErrorScreen(),
|
||||||
|
|
||||||
|
//Loading
|
||||||
|
if (_loading)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
//Data
|
||||||
|
if (!_loading && !_error)
|
||||||
|
...List.generate(_episodes.length, (i) {
|
||||||
|
ShowEpisode e = _episodes[i];
|
||||||
|
return ShowEpisodeTile(
|
||||||
|
e,
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: Icon(Icons.more_vert),
|
||||||
|
onPressed: () {
|
||||||
|
MenuSheet m = MenuSheet(context);
|
||||||
|
m.defaultShowEpisodeMenu(_show, e);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
await playerHelper.playShowEpisode(_show, _episodes, index: i);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -291,6 +291,15 @@ class HomePageItemWidget extends StatelessWidget {
|
|||||||
));
|
));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
case HomePageItemType.SHOW:
|
||||||
|
return ShowCard(
|
||||||
|
item.value,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).push(MaterialPageRoute(
|
||||||
|
builder: (context) => ShowScreen(item.value)
|
||||||
|
));
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return Container(height: 0, width: 0);
|
return Container(height: 0, width: 0);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:connectivity/connectivity.dart';
|
import 'package:connectivity/connectivity.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttericon/font_awesome5_icons.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:freezer/api/cache.dart';
|
import 'package:freezer/api/cache.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
@ -220,26 +221,44 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||||||
List<Track> tracks = [];
|
List<Track> tracks = [];
|
||||||
List<Track> allTracks = [];
|
List<Track> allTracks = [];
|
||||||
int trackCount;
|
int trackCount;
|
||||||
SortType _sort = SortType.DEFAULT;
|
Sorting _sort = Sorting(sourceType: SortSourceTypes.TRACKS);
|
||||||
|
|
||||||
Playlist get _playlist => Playlist(id: deezerAPI.favoritesPlaylistId);
|
Playlist get _playlist => Playlist(id: deezerAPI.favoritesPlaylistId);
|
||||||
|
|
||||||
List<Track> get _sorted {
|
List<Track> get _sorted {
|
||||||
List<Track> tcopy = List.from(tracks);
|
List<Track> tcopy = List.from(tracks);
|
||||||
tcopy.sort((a, b) => a.favoriteDate.compareTo(b.favoriteDate));
|
tcopy.sort((a, b) => a.addedDate.compareTo(b.addedDate));
|
||||||
switch (_sort) {
|
switch (_sort.type) {
|
||||||
case SortType.ALPHABETIC:
|
case SortType.ALPHABETIC:
|
||||||
tcopy.sort((a, b) => a.title.compareTo(b.title));
|
tcopy.sort((a, b) => a.title.compareTo(b.title));
|
||||||
return tcopy;
|
break;
|
||||||
case SortType.ARTIST:
|
case SortType.ARTIST:
|
||||||
tcopy.sort((a, b) => a.artists[0].name.toLowerCase().compareTo(b.artists[0].name.toLowerCase()));
|
tcopy.sort((a, b) => a.artists[0].name.toLowerCase().compareTo(b.artists[0].name.toLowerCase()));
|
||||||
return tcopy;
|
break;
|
||||||
case SortType.REVERSE:
|
|
||||||
return tcopy.reversed.toList();
|
|
||||||
case SortType.DEFAULT:
|
case SortType.DEFAULT:
|
||||||
default:
|
default:
|
||||||
return tcopy;
|
break;
|
||||||
}
|
}
|
||||||
|
//Reverse
|
||||||
|
if (_sort.reverse)
|
||||||
|
return tcopy.reversed.toList();
|
||||||
|
return tcopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _reverse() async {
|
||||||
|
setState(() => _sort.reverse = !_sort.reverse);
|
||||||
|
//Save sorting in cache
|
||||||
|
int index = Sorting.index(SortSourceTypes.TRACKS);
|
||||||
|
if (index != null) {
|
||||||
|
cache.sorts[index] = _sort;
|
||||||
|
} else {
|
||||||
|
cache.sorts.add(_sort);
|
||||||
|
}
|
||||||
|
await cache.save();
|
||||||
|
|
||||||
|
//Preload for sorting
|
||||||
|
if (tracks.length < (trackCount??0))
|
||||||
|
_loadFull();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _load() async {
|
Future _load() async {
|
||||||
@ -274,7 +293,8 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||||||
//Update
|
//Update
|
||||||
setState(() {
|
setState(() {
|
||||||
trackCount = favPlaylist.trackCount;
|
trackCount = favPlaylist.trackCount;
|
||||||
tracks = favPlaylist.tracks;
|
if (tracks.length == 0)
|
||||||
|
tracks = favPlaylist.tracks;
|
||||||
_makeFavorite();
|
_makeFavorite();
|
||||||
_loading = false;
|
_loading = false;
|
||||||
});
|
});
|
||||||
@ -306,7 +326,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||||||
|
|
||||||
//Load all tracks
|
//Load all tracks
|
||||||
Future _loadFull() async {
|
Future _loadFull() async {
|
||||||
if (tracks.length < (trackCount??0)) {
|
if (tracks.length == 0 || tracks.length < (trackCount??0)) {
|
||||||
Playlist p;
|
Playlist p;
|
||||||
try {
|
try {
|
||||||
p = await deezerAPI.fullPlaylist(deezerAPI.favoritesPlaylistId);
|
p = await deezerAPI.fullPlaylist(deezerAPI.favoritesPlaylistId);
|
||||||
@ -315,6 +335,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
tracks = p.tracks;
|
tracks = p.tracks;
|
||||||
trackCount = p.trackCount;
|
trackCount = p.trackCount;
|
||||||
|
_sort = _sort;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -348,13 +369,16 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||||||
if (_scrollController.position.pixels > off) _load();
|
if (_scrollController.position.pixels > off) _load();
|
||||||
});
|
});
|
||||||
|
|
||||||
_sort = cache.trackSort??SortType.DEFAULT;
|
|
||||||
|
|
||||||
_load();
|
_load();
|
||||||
//Load all offline tracks
|
//Load all offline tracks
|
||||||
_loadAllOffline();
|
_loadAllOffline();
|
||||||
|
|
||||||
if (_sort != SortType.DEFAULT)
|
//Load sorting
|
||||||
|
int index = Sorting.index(SortSourceTypes.TRACKS);
|
||||||
|
if (index != null)
|
||||||
|
setState(() => _sort = cache.sorts[index]);
|
||||||
|
|
||||||
|
if (_sort.type != SortType.DEFAULT || _sort.reverse)
|
||||||
_loadFull();
|
_loadFull();
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -366,6 +390,12 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||||||
appBar: FreezerAppBar(
|
appBar: FreezerAppBar(
|
||||||
'Tracks'.i18n,
|
'Tracks'.i18n,
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(_sort.reverse ? FontAwesome5.sort_alpha_up : FontAwesome5.sort_alpha_down),
|
||||||
|
onPressed: () async {
|
||||||
|
await _reverse();
|
||||||
|
}
|
||||||
|
),
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
child: Icon(Icons.sort, size: 32.0),
|
child: Icon(Icons.sort, size: 32.0),
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
@ -374,8 +404,14 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||||||
if (tracks.length < (trackCount??0))
|
if (tracks.length < (trackCount??0))
|
||||||
await _loadFull();
|
await _loadFull();
|
||||||
|
|
||||||
setState(() => _sort = s);
|
setState(() => _sort.type = s);
|
||||||
cache.trackSort = s;
|
//Save sorting in cache
|
||||||
|
int index = Sorting.index(SortSourceTypes.TRACKS);
|
||||||
|
if (index != null) {
|
||||||
|
cache.sorts[index] = _sort;
|
||||||
|
} else {
|
||||||
|
cache.sorts.add(_sort);
|
||||||
|
}
|
||||||
await cache.save();
|
await cache.save();
|
||||||
},
|
},
|
||||||
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
|
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
|
||||||
@ -383,10 +419,6 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||||||
value: SortType.DEFAULT,
|
value: SortType.DEFAULT,
|
||||||
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
|
||||||
value: SortType.REVERSE,
|
|
||||||
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: SortType.ALPHABETIC,
|
value: SortType.ALPHABETIC,
|
||||||
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
||||||
@ -498,14 +530,6 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enum AlbumSortType {
|
|
||||||
DEFAULT,
|
|
||||||
REVERSE,
|
|
||||||
ALPHABETIC,
|
|
||||||
ARTIST,
|
|
||||||
DATE
|
|
||||||
}
|
|
||||||
|
|
||||||
class LibraryAlbums extends StatefulWidget {
|
class LibraryAlbums extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
_LibraryAlbumsState createState() => _LibraryAlbumsState();
|
_LibraryAlbumsState createState() => _LibraryAlbumsState();
|
||||||
@ -514,27 +538,28 @@ class LibraryAlbums extends StatefulWidget {
|
|||||||
class _LibraryAlbumsState extends State<LibraryAlbums> {
|
class _LibraryAlbumsState extends State<LibraryAlbums> {
|
||||||
|
|
||||||
List<Album> _albums;
|
List<Album> _albums;
|
||||||
AlbumSortType _sort = AlbumSortType.DEFAULT;
|
Sorting _sort = Sorting(sourceType: SortSourceTypes.ALBUMS);
|
||||||
ScrollController _scrollController = ScrollController();
|
ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
List<Album> get _sorted {
|
List<Album> get _sorted {
|
||||||
List<Album> albums = List.from(_albums);
|
List<Album> albums = List.from(_albums);
|
||||||
albums.sort((a, b) => a.favoriteDate.compareTo(b.favoriteDate));
|
albums.sort((a, b) => a.favoriteDate.compareTo(b.favoriteDate));
|
||||||
switch (_sort) {
|
switch (_sort.type) {
|
||||||
case AlbumSortType.DEFAULT:
|
case SortType.DEFAULT:
|
||||||
return albums;
|
break;
|
||||||
case AlbumSortType.REVERSE:
|
case SortType.ALPHABETIC:
|
||||||
return albums.reversed.toList();
|
|
||||||
case AlbumSortType.ALPHABETIC:
|
|
||||||
albums.sort((a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase()));
|
albums.sort((a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase()));
|
||||||
return albums;
|
break;
|
||||||
case AlbumSortType.ARTIST:
|
case SortType.ARTIST:
|
||||||
albums.sort((a, b) => a.artists[0].name.toLowerCase().compareTo(b.artists[0].name.toLowerCase()));
|
albums.sort((a, b) => a.artists[0].name.toLowerCase().compareTo(b.artists[0].name.toLowerCase()));
|
||||||
return albums;
|
break;
|
||||||
case AlbumSortType.DATE:
|
case SortType.RELEASE_DATE:
|
||||||
albums.sort((a, b) => DateTime.parse(a.releaseDate).compareTo(DateTime.parse(b.releaseDate)));
|
albums.sort((a, b) => DateTime.parse(a.releaseDate).compareTo(DateTime.parse(b.releaseDate)));
|
||||||
return albums;
|
break;
|
||||||
}
|
}
|
||||||
|
//Reverse
|
||||||
|
if (_sort.reverse)
|
||||||
|
return albums.reversed.toList();
|
||||||
return albums;
|
return albums;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -550,43 +575,65 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_load();
|
_load();
|
||||||
_sort = cache.albumSort??AlbumSortType.DEFAULT;
|
//Load sorting
|
||||||
|
int index = Sorting.index(SortSourceTypes.ALBUMS);
|
||||||
|
if (index != null)
|
||||||
|
_sort = cache.sorts[index];
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future _reverse() async {
|
||||||
|
setState(() => _sort.reverse = !_sort.reverse);
|
||||||
|
//Save sorting in cache
|
||||||
|
int index = Sorting.index(SortSourceTypes.ALBUMS);
|
||||||
|
if (index != null) {
|
||||||
|
cache.sorts[index] = _sort;
|
||||||
|
} else {
|
||||||
|
cache.sorts.add(_sort);
|
||||||
|
}
|
||||||
|
await cache.save();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: FreezerAppBar(
|
appBar: FreezerAppBar(
|
||||||
'Albums'.i18n,
|
'Albums'.i18n,
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(_sort.reverse ? FontAwesome5.sort_alpha_up : FontAwesome5.sort_alpha_down),
|
||||||
|
onPressed: () => _reverse(),
|
||||||
|
),
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
child: Icon(Icons.sort, size: 32.0),
|
child: Icon(Icons.sort, size: 32.0),
|
||||||
onSelected: (AlbumSortType s) async {
|
onSelected: (SortType s) async {
|
||||||
setState(() => _sort = s);
|
setState(() => _sort.type = s);
|
||||||
cache.albumSort = s;
|
//Save to cache
|
||||||
|
int index = Sorting.index(SortSourceTypes.ALBUMS);
|
||||||
|
if (index == null) {
|
||||||
|
cache.sorts.add(_sort);
|
||||||
|
} else {
|
||||||
|
cache.sorts[index] = _sort;
|
||||||
|
}
|
||||||
await cache.save();
|
await cache.save();
|
||||||
},
|
},
|
||||||
itemBuilder: (context) => <PopupMenuEntry<AlbumSortType>>[
|
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: AlbumSortType.DEFAULT,
|
value: SortType.DEFAULT,
|
||||||
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: AlbumSortType.REVERSE,
|
value: SortType.ALPHABETIC,
|
||||||
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
value: AlbumSortType.ALPHABETIC,
|
|
||||||
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: AlbumSortType.ARTIST,
|
value: SortType.ARTIST,
|
||||||
child: Text('Artist'.i18n, style: popupMenuTextStyle()),
|
child: Text('Artist'.i18n, style: popupMenuTextStyle()),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: AlbumSortType.DATE,
|
value: SortType.RELEASE_DATE,
|
||||||
child: Text('Release date'.i18n, style: popupMenuTextStyle()),
|
child: Text('Release date'.i18n, style: popupMenuTextStyle()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -675,12 +722,6 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ArtistSortType {
|
|
||||||
DEFAULT,
|
|
||||||
REVERSE,
|
|
||||||
POPULARITY,
|
|
||||||
ALPHABETIC
|
|
||||||
}
|
|
||||||
|
|
||||||
class LibraryArtists extends StatefulWidget {
|
class LibraryArtists extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -690,7 +731,7 @@ class LibraryArtists extends StatefulWidget {
|
|||||||
class _LibraryArtistsState extends State<LibraryArtists> {
|
class _LibraryArtistsState extends State<LibraryArtists> {
|
||||||
|
|
||||||
List<Artist> _artists;
|
List<Artist> _artists;
|
||||||
ArtistSortType _sort = ArtistSortType.DEFAULT;
|
Sorting _sort = Sorting(sourceType: SortSourceTypes.ARTISTS);
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
bool _error = false;
|
bool _error = false;
|
||||||
ScrollController _scrollController = ScrollController();
|
ScrollController _scrollController = ScrollController();
|
||||||
@ -698,18 +739,19 @@ class _LibraryArtistsState extends State<LibraryArtists> {
|
|||||||
List<Artist> get _sorted {
|
List<Artist> get _sorted {
|
||||||
List<Artist> artists = List.from(_artists);
|
List<Artist> artists = List.from(_artists);
|
||||||
artists.sort((a, b) => a.favoriteDate.compareTo(b.favoriteDate));
|
artists.sort((a, b) => a.favoriteDate.compareTo(b.favoriteDate));
|
||||||
switch (_sort) {
|
switch (_sort.type) {
|
||||||
case ArtistSortType.DEFAULT:
|
case SortType.DEFAULT:
|
||||||
return artists;
|
break;
|
||||||
case ArtistSortType.REVERSE:
|
case SortType.POPULARITY:
|
||||||
return artists.reversed.toList();
|
|
||||||
case ArtistSortType.POPULARITY:
|
|
||||||
artists.sort((a, b) => b.fans - a.fans);
|
artists.sort((a, b) => b.fans - a.fans);
|
||||||
return artists;
|
break;
|
||||||
case ArtistSortType.ALPHABETIC:
|
case SortType.ALPHABETIC:
|
||||||
artists.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
artists.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||||
return artists;
|
break;
|
||||||
}
|
}
|
||||||
|
//Reverse
|
||||||
|
if (_sort.reverse)
|
||||||
|
return artists.reversed.toList();
|
||||||
return artists;
|
return artists;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -732,9 +774,26 @@ class _LibraryArtistsState extends State<LibraryArtists> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future _reverse() async {
|
||||||
|
setState(() => _sort.reverse = !_sort.reverse);
|
||||||
|
//Save sorting in cache
|
||||||
|
int index = Sorting.index(SortSourceTypes.ARTISTS);
|
||||||
|
if (index != null) {
|
||||||
|
cache.sorts[index] = _sort;
|
||||||
|
} else {
|
||||||
|
cache.sorts.add(_sort);
|
||||||
|
}
|
||||||
|
await cache.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_sort = cache.artistSort;
|
//Restore sort
|
||||||
|
int index = Sorting.index(SortSourceTypes.ARTISTS);
|
||||||
|
if (index != null)
|
||||||
|
_sort = cache.sorts[index];
|
||||||
|
|
||||||
_load();
|
_load();
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
@ -745,29 +804,35 @@ class _LibraryArtistsState extends State<LibraryArtists> {
|
|||||||
appBar: FreezerAppBar(
|
appBar: FreezerAppBar(
|
||||||
'Artists'.i18n,
|
'Artists'.i18n,
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(_sort.reverse ? FontAwesome5.sort_alpha_up : FontAwesome5.sort_alpha_down),
|
||||||
|
onPressed: () => _reverse(),
|
||||||
|
),
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
child: Icon(Icons.sort, size: 32.0),
|
child: Icon(Icons.sort, size: 32.0),
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
onSelected: (ArtistSortType s) async {
|
onSelected: (SortType s) async {
|
||||||
setState(() => _sort = s);
|
setState(() => _sort.type = s);
|
||||||
cache.artistSort = s;
|
//Save
|
||||||
|
int index = Sorting.index(SortSourceTypes.ARTISTS);
|
||||||
|
if (index == null) {
|
||||||
|
cache.sorts.add(_sort);
|
||||||
|
} else {
|
||||||
|
cache.sorts[index] = _sort;
|
||||||
|
}
|
||||||
await cache.save();
|
await cache.save();
|
||||||
},
|
},
|
||||||
itemBuilder: (context) => <PopupMenuEntry<ArtistSortType>>[
|
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: ArtistSortType.DEFAULT,
|
value: SortType.DEFAULT,
|
||||||
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: ArtistSortType.REVERSE,
|
value: SortType.ALPHABETIC,
|
||||||
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
value: ArtistSortType.ALPHABETIC,
|
|
||||||
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: ArtistSortType.POPULARITY,
|
value: SortType.POPULARITY,
|
||||||
child: Text('Popularity'.i18n, style: popupMenuTextStyle()),
|
child: Text('Popularity'.i18n, style: popupMenuTextStyle()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -819,14 +884,6 @@ class _LibraryArtistsState extends State<LibraryArtists> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PlaylistSortType {
|
|
||||||
DEFAULT,
|
|
||||||
REVERSE,
|
|
||||||
ALPHABETIC,
|
|
||||||
USER,
|
|
||||||
TRACK_COUNT
|
|
||||||
}
|
|
||||||
|
|
||||||
class LibraryPlaylists extends StatefulWidget {
|
class LibraryPlaylists extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
_LibraryPlaylistsState createState() => _LibraryPlaylistsState();
|
_LibraryPlaylistsState createState() => _LibraryPlaylistsState();
|
||||||
@ -835,27 +892,27 @@ class LibraryPlaylists extends StatefulWidget {
|
|||||||
class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
||||||
|
|
||||||
List<Playlist> _playlists;
|
List<Playlist> _playlists;
|
||||||
PlaylistSortType _sort = PlaylistSortType.DEFAULT;
|
Sorting _sort = Sorting(sourceType: SortSourceTypes.PLAYLISTS);
|
||||||
ScrollController _scrollController = ScrollController();
|
ScrollController _scrollController = ScrollController();
|
||||||
String _filter = '';
|
String _filter = '';
|
||||||
|
|
||||||
List<Playlist> get _sorted {
|
List<Playlist> get _sorted {
|
||||||
List<Playlist> playlists = List.from(_playlists.where((p) => p.title.toLowerCase().contains(_filter.toLowerCase())));
|
List<Playlist> playlists = List.from(_playlists.where((p) => p.title.toLowerCase().contains(_filter.toLowerCase())));
|
||||||
switch (_sort) {
|
switch (_sort.type) {
|
||||||
case PlaylistSortType.DEFAULT:
|
case SortType.DEFAULT:
|
||||||
return playlists;
|
break;
|
||||||
case PlaylistSortType.REVERSE:
|
case SortType.USER:
|
||||||
return playlists.reversed.toList();
|
|
||||||
case PlaylistSortType.USER:
|
|
||||||
playlists.sort((a, b) => (a.user.name??deezerAPI.userName).toLowerCase().compareTo((b.user.name??deezerAPI.userName).toLowerCase()));
|
playlists.sort((a, b) => (a.user.name??deezerAPI.userName).toLowerCase().compareTo((b.user.name??deezerAPI.userName).toLowerCase()));
|
||||||
return playlists;
|
break;
|
||||||
case PlaylistSortType.TRACK_COUNT:
|
case SortType.TRACK_COUNT:
|
||||||
playlists.sort((a, b) => b.trackCount - a.trackCount);
|
playlists.sort((a, b) => b.trackCount - a.trackCount);
|
||||||
return playlists;
|
break;
|
||||||
case PlaylistSortType.ALPHABETIC:
|
case SortType.ALPHABETIC:
|
||||||
playlists.sort((a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase()));
|
playlists.sort((a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase()));
|
||||||
return playlists;
|
break;
|
||||||
}
|
}
|
||||||
|
if (_sort.reverse)
|
||||||
|
return playlists.reversed.toList();
|
||||||
return playlists;
|
return playlists;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -868,9 +925,25 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future _reverse() async {
|
||||||
|
setState(() => _sort.reverse = !_sort.reverse);
|
||||||
|
//Save sorting in cache
|
||||||
|
int index = Sorting.index(SortSourceTypes.PLAYLISTS);
|
||||||
|
if (index != null) {
|
||||||
|
cache.sorts[index] = _sort;
|
||||||
|
} else {
|
||||||
|
cache.sorts.add(_sort);
|
||||||
|
}
|
||||||
|
await cache.save();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_sort = cache.libraryPlaylistSort;
|
//Restore sort
|
||||||
|
int index = Sorting.index(SortSourceTypes.PLAYLISTS);
|
||||||
|
if (index != null)
|
||||||
|
_sort = cache.sorts[index];
|
||||||
|
|
||||||
_load();
|
_load();
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
@ -892,33 +965,39 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||||||
appBar: FreezerAppBar(
|
appBar: FreezerAppBar(
|
||||||
'Playlists'.i18n,
|
'Playlists'.i18n,
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(_sort.reverse ? FontAwesome5.sort_alpha_up : FontAwesome5.sort_alpha_down),
|
||||||
|
onPressed: () => _reverse(),
|
||||||
|
),
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
child: Icon(Icons.sort, size: 32.0),
|
child: Icon(Icons.sort, size: 32.0),
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
onSelected: (PlaylistSortType s) async {
|
onSelected: (SortType s) async {
|
||||||
setState(() => _sort = s);
|
setState(() => _sort.type = s);
|
||||||
cache.libraryPlaylistSort = s;
|
//Save to cache
|
||||||
|
int index = Sorting.index(SortSourceTypes.PLAYLISTS);
|
||||||
|
if (index == null)
|
||||||
|
cache.sorts.add(_sort);
|
||||||
|
else
|
||||||
|
cache.sorts[index] = _sort;
|
||||||
|
|
||||||
await cache.save();
|
await cache.save();
|
||||||
},
|
},
|
||||||
itemBuilder: (context) => <PopupMenuEntry<PlaylistSortType>>[
|
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: PlaylistSortType.DEFAULT,
|
value: SortType.DEFAULT,
|
||||||
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: PlaylistSortType.REVERSE,
|
value: SortType.USER,
|
||||||
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
value: PlaylistSortType.USER,
|
|
||||||
child: Text('User'.i18n, style: popupMenuTextStyle()),
|
child: Text('User'.i18n, style: popupMenuTextStyle()),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: PlaylistSortType.TRACK_COUNT,
|
value: SortType.TRACK_COUNT,
|
||||||
child: Text('Track count'.i18n, style: popupMenuTextStyle()),
|
child: Text('Track count'.i18n, style: popupMenuTextStyle()),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: PlaylistSortType.ALPHABETIC,
|
value: SortType.ALPHABETIC,
|
||||||
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -13,6 +13,7 @@ import 'package:freezer/ui/error.dart';
|
|||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
import 'package:numberpicker/numberpicker.dart';
|
import 'package:numberpicker/numberpicker.dart';
|
||||||
import 'package:share/share.dart';
|
import 'package:share/share.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../api/definitions.dart';
|
import '../api/definitions.dart';
|
||||||
import 'cached_image.dart';
|
import 'cached_image.dart';
|
||||||
@ -501,6 +502,35 @@ class MenuSheet {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//===================
|
||||||
|
// SHOW/EPISODE
|
||||||
|
//===================
|
||||||
|
|
||||||
|
defaultShowEpisodeMenu(Show s, ShowEpisode e, {List<Widget> options = const []}) {
|
||||||
|
show([
|
||||||
|
shareTile('episode', e.id),
|
||||||
|
shareShow(s.id),
|
||||||
|
downloadExternalEpisode(e),
|
||||||
|
...options
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget shareShow(String id) => ListTile(
|
||||||
|
title: Text('Share show'.i18n),
|
||||||
|
leading: Icon(Icons.share),
|
||||||
|
onTap: () async {
|
||||||
|
Share.share('https://deezer.com/show/$id');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
//Open direct download link in browser
|
||||||
|
Widget downloadExternalEpisode(ShowEpisode e) => ListTile(
|
||||||
|
title: Text('Download externally'.i18n),
|
||||||
|
leading: Icon(Icons.file_download),
|
||||||
|
onTap: () async {
|
||||||
|
launch(e.url);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
//===================
|
//===================
|
||||||
// OTHER
|
// OTHER
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:isolate';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
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/cache.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
|
import 'package:freezer/api/download.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';
|
||||||
@ -25,6 +30,7 @@ import 'player_bar.dart';
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
|
||||||
class PlayerScreen extends StatefulWidget {
|
class PlayerScreen extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
_PlayerScreenState createState() => _PlayerScreenState();
|
_PlayerScreenState createState() => _PlayerScreenState();
|
||||||
@ -36,26 +42,34 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||||||
StreamSubscription _mediaItemSub;
|
StreamSubscription _mediaItemSub;
|
||||||
|
|
||||||
//Calculate background color
|
//Calculate background color
|
||||||
Future _calculateColor() async {
|
Future _updateColor() async {
|
||||||
if (!settings.colorGradientBackground)
|
if (!settings.colorGradientBackground)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
//Run in isolate
|
||||||
PaletteGenerator palette = await PaletteGenerator.fromImageProvider(CachedNetworkImageProvider(AudioService.currentMediaItem.extras['thumb'] ?? AudioService.currentMediaItem.artUri));
|
PaletteGenerator palette = await PaletteGenerator.fromImageProvider(CachedNetworkImageProvider(AudioService.currentMediaItem.extras['thumb'] ?? AudioService.currentMediaItem.artUri));
|
||||||
|
|
||||||
|
//Update notification
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||||
|
statusBarColor: palette.dominantColor.color.withOpacity(0.5)
|
||||||
|
));
|
||||||
|
|
||||||
setState(() => _bgGradient = LinearGradient(
|
setState(() => _bgGradient = LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [palette.dominantColor.color.withOpacity(0.5), Theme.of(context).bottomAppBarColor],
|
colors: [palette.dominantColor.color.withOpacity(0.5), Theme.of(context).bottomAppBarColor],
|
||||||
stops: [
|
stops: [
|
||||||
0.0,
|
0.0,
|
||||||
0.4
|
0.4
|
||||||
]
|
]
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_calculateColor();
|
Future.delayed(Duration(milliseconds: 1000), _updateColor);
|
||||||
_mediaItemSub = AudioService.currentMediaItemStream.listen((event) {
|
_mediaItemSub = AudioService.currentMediaItemStream.listen((event) {
|
||||||
_calculateColor();
|
_updateColor();
|
||||||
});
|
});
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
@ -67,6 +81,7 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||||||
//Fix bottom buttons
|
//Fix bottom buttons
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||||
systemNavigationBarColor: settings.themeData.bottomAppBarColor,
|
systemNavigationBarColor: settings.themeData.bottomAppBarColor,
|
||||||
|
statusBarColor: Colors.transparent
|
||||||
));
|
));
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@ -214,26 +229,9 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
|||||||
));
|
));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (AudioService.currentMediaItem.extras['qualityString'] != null)
|
QualityInfoWidget(),
|
||||||
FlatButton(
|
|
||||||
onPressed: () => Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(builder: (context) => QualitySettings())
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
AudioService.currentMediaItem.extras['qualityString'] ?? '',
|
|
||||||
style: TextStyle(fontSize: ScreenUtil().setSp(24)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
RepeatButton(ScreenUtil().setWidth(32)),
|
RepeatButton(ScreenUtil().setWidth(32)),
|
||||||
IconButton(
|
PlayerMenuButton()
|
||||||
icon: Icon(Icons.more_vert, size: ScreenUtil().setWidth(32)),
|
|
||||||
onPressed: () {
|
|
||||||
Track t = Track.fromMediaItem(AudioService.currentMediaItem);
|
|
||||||
MenuSheet m = MenuSheet(context);
|
|
||||||
m.defaultTrackMenu(t, options: [m.sleepTimer()]);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -333,28 +331,9 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||||||
));
|
));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (AudioService.currentMediaItem.extras['qualityString'] != null)
|
QualityInfoWidget(),
|
||||||
FlatButton(
|
|
||||||
onPressed: () => Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(builder: (context) => QualitySettings())
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
AudioService.currentMediaItem.extras['qualityString'] ?? '',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: ScreenUtil().setSp(32),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
RepeatButton(ScreenUtil().setWidth(46)),
|
RepeatButton(ScreenUtil().setWidth(46)),
|
||||||
IconButton(
|
PlayerMenuButton()
|
||||||
icon: Icon(Icons.more_vert, size: ScreenUtil().setWidth(46)),
|
|
||||||
onPressed: () {
|
|
||||||
Track t = Track.fromMediaItem(AudioService.currentMediaItem);
|
|
||||||
MenuSheet m = MenuSheet(context);
|
|
||||||
m.defaultTrackMenu(t, options: [m.sleepTimer()]);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -363,6 +342,86 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class QualityInfoWidget extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_QualityInfoWidgetState createState() => _QualityInfoWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QualityInfoWidgetState extends State<QualityInfoWidget> {
|
||||||
|
|
||||||
|
String value = '';
|
||||||
|
StreamSubscription streamSubscription;
|
||||||
|
|
||||||
|
//Load data from native
|
||||||
|
void _load() async {
|
||||||
|
if (AudioService.currentMediaItem == null) return;
|
||||||
|
Map data = await DownloadManager.platform.invokeMethod("getStreamInfo", {"id": AudioService.currentMediaItem.id});
|
||||||
|
//N/A
|
||||||
|
if (data == null) {
|
||||||
|
setState(() => value = '');
|
||||||
|
//If not show, try again later
|
||||||
|
if (AudioService.currentMediaItem.extras['show'] == null)
|
||||||
|
Future.delayed(Duration(milliseconds: 200), _load);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//Update
|
||||||
|
StreamQualityInfo info = StreamQualityInfo.fromJson(data);
|
||||||
|
setState(() {
|
||||||
|
value = '${info.format} ${info.bitrate(AudioService.currentMediaItem.duration)}kbps';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_load();
|
||||||
|
if (streamSubscription == null)
|
||||||
|
streamSubscription = AudioService.currentMediaItemStream.listen((event) async {
|
||||||
|
await _load();
|
||||||
|
});
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (streamSubscription != null)
|
||||||
|
streamSubscription.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FlatButton(
|
||||||
|
child: Text(value),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).push(MaterialPageRoute(builder: (context) => QualitySettings()));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerMenuButton extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IconButton(
|
||||||
|
icon: Icon(Icons.more_vert, size: ScreenUtil().setWidth(46)),
|
||||||
|
onPressed: () {
|
||||||
|
Track t = Track.fromMediaItem(AudioService.currentMediaItem);
|
||||||
|
MenuSheet m = MenuSheet(context);
|
||||||
|
if (AudioService.currentMediaItem.extras['show'] == null)
|
||||||
|
m.defaultTrackMenu(t, options: [m.sleepTimer()]);
|
||||||
|
else
|
||||||
|
m.defaultShowEpisodeMenu(
|
||||||
|
Show.fromJson(jsonDecode(AudioService.currentMediaItem.extras['show'])),
|
||||||
|
ShowEpisode.fromMediaItem(AudioService.currentMediaItem),
|
||||||
|
options: [m.sleepTimer()]
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class RepeatButton extends StatefulWidget {
|
class RepeatButton extends StatefulWidget {
|
||||||
|
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import 'package:connectivity/connectivity.dart';
|
import 'package:connectivity/connectivity.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:fluttericon/font_awesome5_icons.dart';
|
||||||
|
import 'package:fluttericon/typicons_icons.dart';
|
||||||
import 'package:freezer/api/cache.dart';
|
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/elements.dart';
|
||||||
|
import 'package:freezer/ui/home_screen.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';
|
||||||
|
|
||||||
@ -52,6 +56,8 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
TextEditingController _controller = new TextEditingController();
|
TextEditingController _controller = new TextEditingController();
|
||||||
List _suggestions = [];
|
List _suggestions = [];
|
||||||
bool _cancel = false;
|
bool _cancel = false;
|
||||||
|
bool _showCards = true;
|
||||||
|
FocusNode _focus = FocusNode();
|
||||||
|
|
||||||
void _submit(BuildContext context, {String query}) async {
|
void _submit(BuildContext context, {String query}) async {
|
||||||
if (query != null) _query = query;
|
if (query != null) _query = query;
|
||||||
@ -139,6 +145,10 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
setState(() => _query = s);
|
setState(() => _query = s);
|
||||||
_loadSuggestions();
|
_loadSuggestions();
|
||||||
},
|
},
|
||||||
|
onTap: () {
|
||||||
|
setState(() => _showCards = false);
|
||||||
|
},
|
||||||
|
focusNode: _focus,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Search or paste URL'.i18n,
|
labelText: 'Search or paste URL'.i18n,
|
||||||
fillColor: Theme.of(context).bottomAppBarColor,
|
fillColor: Theme.of(context).bottomAppBarColor,
|
||||||
@ -195,8 +205,87 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
LinearProgressIndicator(),
|
LinearProgressIndicator(),
|
||||||
FreezerDivider(),
|
FreezerDivider(),
|
||||||
|
|
||||||
|
//"Browse" Cards
|
||||||
|
if (_showCards)
|
||||||
|
...[
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||||
|
child: Text(
|
||||||
|
'Quick access',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20.0,
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
SearchBrowseCard(
|
||||||
|
color: Color(0xff11b192),
|
||||||
|
text: 'Flow'.i18n,
|
||||||
|
icon: Icon(Typicons.waves),
|
||||||
|
onTap: () async {
|
||||||
|
await playerHelper.playFromSmartTrackList(SmartTrackList(id: 'flow'));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SearchBrowseCard(
|
||||||
|
color: Color(0xff7c42bb),
|
||||||
|
text: 'Shows'.i18n,
|
||||||
|
icon: Icon(FontAwesome5.podcast),
|
||||||
|
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||||
|
builder: (context) => Scaffold(
|
||||||
|
appBar: FreezerAppBar('Shows'.i18n),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: HomePageScreen(
|
||||||
|
channel: DeezerChannel(target: 'shows')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Container(height: 4.0),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
SearchBrowseCard(
|
||||||
|
color: Color(0xffff555d),
|
||||||
|
icon: Icon(FontAwesome5.chart_line),
|
||||||
|
text: 'Charts'.i18n,
|
||||||
|
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||||
|
builder: (context) => Scaffold(
|
||||||
|
appBar: FreezerAppBar('Charts'.i18n),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: HomePageScreen(
|
||||||
|
channel: DeezerChannel(target: 'channels/charts')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
SearchBrowseCard(
|
||||||
|
color: Color(0xff2c4ea7),
|
||||||
|
text: 'Browse'.i18n,
|
||||||
|
icon: Image.asset('assets/browse_icon.png', width: 26.0),
|
||||||
|
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||||
|
builder: (context) => Scaffold(
|
||||||
|
appBar: FreezerAppBar('Browse'.i18n),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: HomePageScreen(
|
||||||
|
channel: DeezerChannel(target: 'channels/explore')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
|
||||||
//History
|
//History
|
||||||
if (cache.searchHistory != null && cache.searchHistory.length > 0 && (_query??'').length < 2)
|
if (!_showCards && cache.searchHistory != null && cache.searchHistory.length > 0 && (_query??'').length < 2)
|
||||||
...List.generate(cache.searchHistory.length > 10 ? 10 : cache.searchHistory.length, (int i) {
|
...List.generate(cache.searchHistory.length > 10 ? 10 : cache.searchHistory.length, (int i) {
|
||||||
dynamic data = cache.searchHistory[i].data;
|
dynamic data = cache.searchHistory[i].data;
|
||||||
switch (cache.searchHistory[i].type) {
|
switch (cache.searchHistory[i].type) {
|
||||||
@ -290,6 +379,50 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SearchBrowseCard extends StatelessWidget {
|
||||||
|
|
||||||
|
final Color color;
|
||||||
|
final Widget icon;
|
||||||
|
final Function onTap;
|
||||||
|
final String text;
|
||||||
|
SearchBrowseCard({@required this.color, @required this.onTap, @required this.text, this.icon});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
color: color,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: this.onTap,
|
||||||
|
child: Container(
|
||||||
|
width: MediaQuery.of(context).size.width / 2 - 32,
|
||||||
|
height: 75,
|
||||||
|
child: Center(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (icon != null)
|
||||||
|
icon,
|
||||||
|
if (icon != null)
|
||||||
|
Container(width: 8.0),
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18.0,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: (color.computeLuminance() > 0.5) ? Colors.black:Colors.white
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SearchResultsScreen extends StatelessWidget {
|
class SearchResultsScreen extends StatelessWidget {
|
||||||
|
@ -46,6 +46,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
'name': 'Filipino',
|
'name': 'Filipino',
|
||||||
'isoCode': 'fil'
|
'isoCode': 'fil'
|
||||||
});
|
});
|
||||||
|
defaultLanguagesList.add({
|
||||||
|
'name': 'Furry',
|
||||||
|
'isoCode': 'uwu'
|
||||||
|
});
|
||||||
List<Map<String, String>> _l = supportedLocales.map<Map<String, String>>((l) {
|
List<Map<String, String>> _l = supportedLocales.map<Map<String, String>>((l) {
|
||||||
Map _lang = defaultLanguagesList.firstWhere((lang) => lang['isoCode'] == l.languageCode);
|
Map _lang = defaultLanguagesList.firstWhere((lang) => lang['isoCode'] == l.languageCode);
|
||||||
return {
|
return {
|
||||||
@ -445,7 +449,7 @@ class _QualityPickerState extends State<QualityPicker> {
|
|||||||
),
|
),
|
||||||
if (widget.field == 'download')
|
if (widget.field == 'download')
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Ask before downloading'),
|
title: Text('Ask before downloading'.i18n),
|
||||||
leading: Radio(
|
leading: Radio(
|
||||||
groupValue: _quality,
|
groupValue: _quality,
|
||||||
value: AudioQuality.ASK,
|
value: AudioQuality.ASK,
|
||||||
@ -947,8 +951,8 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||||||
),
|
),
|
||||||
FlatButton(
|
FlatButton(
|
||||||
child: Text('(ARL ONLY) Continue'.i18n),
|
child: Text('(ARL ONLY) Continue'.i18n),
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
logOut();
|
await logOut();
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -1073,6 +1077,11 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future _resetPath() async {
|
||||||
|
StorageInfo si = (await PathProviderEx.getStorageInfo())[0];
|
||||||
|
setState(() => _path = si.appFilesDir);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -1147,7 +1156,13 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
|
|||||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||||
|
|
||||||
//On error go to last good path
|
//On error go to last good path
|
||||||
if (snapshot.hasError) Future.delayed(Duration(milliseconds: 50), () => setState(() => _path = _previous));
|
if (snapshot.hasError) Future.delayed(Duration(milliseconds: 50), () {
|
||||||
|
if (_previous == null) {
|
||||||
|
_resetPath();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _path = _previous);
|
||||||
|
});
|
||||||
if (!snapshot.hasData) return Center(child: CircularProgressIndicator(),);
|
if (!snapshot.hasData) return Center(child: CircularProgressIndicator(),);
|
||||||
|
|
||||||
List<FileSystemEntity> data = snapshot.data;
|
List<FileSystemEntity> data = snapshot.data;
|
||||||
@ -1266,12 +1281,20 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
|||||||
launch('https://t.me/freezerandroid');
|
launch('https://t.me/freezerandroid');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('Discord'.i18n),
|
||||||
|
subtitle: Text('Official Discord server'.i18n),
|
||||||
|
leading: Icon(FontAwesome5.discord, color: Color(0xff7289da), size: 36.0),
|
||||||
|
onTap: () {
|
||||||
|
launch('https://discord.gg/7ap654Tp3z');
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Repository'.i18n),
|
title: Text('Repository'.i18n),
|
||||||
subtitle: Text('Source code, report issues there.'.i18n),
|
subtitle: Text('Source code, report issues there.'.i18n),
|
||||||
leading: Icon(Icons.code, color: Colors.green, size: 36.0),
|
leading: Icon(Icons.code, color: Colors.green, size: 36.0),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launch('https://notabug.org/exttex/freezer');
|
launch('https://git.rip/freezer/');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
FreezerDivider(),
|
FreezerDivider(),
|
||||||
|
@ -321,11 +321,12 @@ class SmartTrackListTile extends StatelessWidget {
|
|||||||
fontSize: 18.0,
|
fontSize: 18.0,
|
||||||
shadows: [
|
shadows: [
|
||||||
Shadow(
|
Shadow(
|
||||||
offset: Offset(1, 1),
|
offset: Offset(1, 1),
|
||||||
blurRadius: 2,
|
blurRadius: 2,
|
||||||
color: Colors.black
|
color: Colors.black
|
||||||
)
|
)
|
||||||
]
|
],
|
||||||
|
color: Colors.white
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -454,3 +455,104 @@ class ChannelTile extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ShowCard extends StatelessWidget {
|
||||||
|
|
||||||
|
final Show show;
|
||||||
|
final Function onTap;
|
||||||
|
final Function onHold;
|
||||||
|
|
||||||
|
ShowCard(this.show, {this.onTap, this.onHold});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
onLongPress: onHold,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: CachedImage(
|
||||||
|
url: show.art.thumb,
|
||||||
|
width: 128.0,
|
||||||
|
height: 128.0,
|
||||||
|
rounded: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 144.0,
|
||||||
|
child: Text(
|
||||||
|
show.name,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14.0
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShowEpisodeTile extends StatelessWidget {
|
||||||
|
|
||||||
|
final ShowEpisode episode;
|
||||||
|
final Function onTap;
|
||||||
|
final Function onHold;
|
||||||
|
final Widget trailing;
|
||||||
|
|
||||||
|
ShowEpisodeTile(this.episode, {this.onTap, this.onHold, this.trailing});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
onLongPress: onHold,
|
||||||
|
onTap: onTap,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text(episode.title, maxLines: 2),
|
||||||
|
trailing: trailing,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Text(
|
||||||
|
episode.description,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).textTheme.subtitle1.color.withOpacity(0.9)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 8.0, 0, 0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${episode.publishedDate} | ${episode.durationString}',
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.0,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).textTheme.subtitle1.color.withOpacity(0.6)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Divider(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -15,6 +15,8 @@ import 'package:path_provider/path_provider.dart';
|
|||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:version/version.dart';
|
||||||
|
|
||||||
|
|
||||||
class UpdaterScreen extends StatefulWidget {
|
class UpdaterScreen extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -109,12 +111,13 @@ class _UpdaterScreenState extends State<UpdaterScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
if (!_error && !_loading && _versions.latest == _current)
|
if (!_error && !_loading && Version.parse(_versions.latest) <= Version.parse(_current))
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(8.0),
|
padding: EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'You are running latest version!'.i18n,
|
'You are running latest version!'.i18n,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 26.0
|
fontSize: 26.0
|
||||||
)
|
)
|
||||||
@ -122,17 +125,20 @@ class _UpdaterScreenState extends State<UpdaterScreen> {
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
if (!_error && !_loading && _versions.latest != _current)
|
if (!_error && !_loading && Version.parse(_versions.latest) > Version.parse(_current))
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Padding(
|
||||||
'New update available!'.i18n + ' ' + _versions.latest,
|
padding: EdgeInsets.all(8.0),
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
fontSize: 20.0,
|
'New update available!'.i18n + ' ' + _versions.latest,
|
||||||
fontWeight: FontWeight.bold
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20.0,
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(height: 8.0),
|
|
||||||
Text(
|
Text(
|
||||||
'Current version: ' + _current,
|
'Current version: ' + _current,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@ -204,6 +210,7 @@ class FreezerVersions {
|
|||||||
//Fetch from website API
|
//Fetch from website API
|
||||||
static Future<FreezerVersions> fetch() async {
|
static Future<FreezerVersions> fetch() async {
|
||||||
http.Response response = await http.get('https://freezer.life/api/versions');
|
http.Response response = await http.get('https://freezer.life/api/versions');
|
||||||
|
// http.Response response = await http.get('https://cum.freezerapp.workers.dev/api/versions');
|
||||||
return FreezerVersions.fromJson(jsonDecode(response.body));
|
return FreezerVersions.fromJson(jsonDecode(response.body));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,7 +225,7 @@ class FreezerVersions {
|
|||||||
|
|
||||||
//Load current version
|
//Load current version
|
||||||
PackageInfo info = await PackageInfo.fromPlatform();
|
PackageInfo info = await PackageInfo.fromPlatform();
|
||||||
if (info.version == versions.latest) return;
|
if (Version.parse(versions.latest) <= Version.parse(info.version)) return;
|
||||||
|
|
||||||
//Get architecture
|
//Get architecture
|
||||||
String _arch = await DownloadManager.platform.invokeMethod("arch");
|
String _arch = await DownloadManager.platform.invokeMethod("arch");
|
||||||
|
45
pubspec.lock
45
pubspec.lock
@ -56,14 +56,14 @@ packages:
|
|||||||
name: build
|
name: build
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "1.5.1"
|
||||||
build_config:
|
build_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build_config
|
name: build_config
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.2"
|
version: "0.4.3"
|
||||||
build_daemon:
|
build_daemon:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -84,14 +84,14 @@ packages:
|
|||||||
name: build_runner
|
name: build_runner
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.4"
|
version: "1.10.6"
|
||||||
build_runner_core:
|
build_runner_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build_runner_core
|
name: build_runner_core
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.3"
|
version: "6.1.1"
|
||||||
built_collection:
|
built_collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -245,7 +245,7 @@ packages:
|
|||||||
name: dart_style
|
name: dart_style
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.9"
|
version: "1.3.10"
|
||||||
dio:
|
dio:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -496,7 +496,7 @@ packages:
|
|||||||
name: json_annotation
|
name: json_annotation
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.1"
|
||||||
json_serializable:
|
json_serializable:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -507,10 +507,24 @@ packages:
|
|||||||
just_audio:
|
just_audio:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: just_audio
|
name: just_audio
|
||||||
relative: true
|
url: "https://pub.dartlang.org"
|
||||||
source: path
|
source: hosted
|
||||||
version: "0.4.4"
|
version: "0.5.6"
|
||||||
|
just_audio_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: just_audio_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
just_audio_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: just_audio_web
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.1"
|
||||||
language_pickers:
|
language_pickers:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -580,7 +594,7 @@ packages:
|
|||||||
name: numberpicker
|
name: numberpicker
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.3.0"
|
||||||
octo_image:
|
octo_image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -692,7 +706,7 @@ packages:
|
|||||||
name: photo_view
|
name: photo_view
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.10.2"
|
version: "0.10.3"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -964,6 +978,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0-nullsafety.3"
|
version: "2.1.0-nullsafety.3"
|
||||||
|
version:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: version
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -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.6.3+1
|
version: 0.6.5+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.8.0 <3.0.0"
|
sdk: ">=2.8.0 <3.0.0"
|
||||||
@ -73,12 +73,13 @@ dependencies:
|
|||||||
draggable_scrollbar: ^0.0.4
|
draggable_scrollbar: ^0.0.4
|
||||||
scrobblenaut: ^2.0.4
|
scrobblenaut: ^2.0.4
|
||||||
open_file: ^3.0.3
|
open_file: ^3.0.3
|
||||||
|
version: ^1.2.0
|
||||||
|
|
||||||
audio_session: ^0.0.9
|
audio_session: ^0.0.9
|
||||||
audio_service:
|
audio_service:
|
||||||
path: ./audio_service
|
path: ./audio_service
|
||||||
just_audio:
|
just_audio: ^0.5.6
|
||||||
path: ./just_audio
|
# path: ./just_audio
|
||||||
|
|
||||||
# cupertino_icons: ^0.1.3
|
# cupertino_icons: ^0.1.3
|
||||||
|
|
||||||
@ -110,6 +111,7 @@ flutter:
|
|||||||
- assets/cover_thumb.jpg
|
- assets/cover_thumb.jpg
|
||||||
- assets/icon.png
|
- assets/icon.png
|
||||||
- assets/favorites_thumb.jpg
|
- assets/favorites_thumb.jpg
|
||||||
|
- assets/browse_icon.png
|
||||||
|
|
||||||
fonts:
|
fonts:
|
||||||
# - family: Montserrat
|
# - family: Montserrat
|
||||||
|
Loading…
Reference in New Issue
Block a user