0.6.6 - standalone track naming, artist separator
This commit is contained in:
parent
ef9ae6e2ad
commit
babd12bae2
|
@ -75,8 +75,8 @@ 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 files('libs/jaudiotagger-2.2.3.jar')
|
||||||
|
implementation files('libs/extension-flac.aar')
|
||||||
implementation group: 'org.nanohttpd', name: 'nanohttpd', version: '2.3.1'
|
implementation group: 'org.nanohttpd', name: 'nanohttpd', version: '2.3.1'
|
||||||
compile files('libs/extension-flac.aar')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
|
|
|
@ -262,7 +262,7 @@ public class Deezer {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Tag track with data from API
|
//Tag track with data from API
|
||||||
public void tagTrack(String path, JSONObject publicTrack, JSONObject publicAlbum, String cover, JSONObject lyricsData, JSONObject privateJson) throws Exception {
|
public void tagTrack(String path, JSONObject publicTrack, JSONObject publicAlbum, String cover, JSONObject lyricsData, JSONObject privateJson, DownloadService.DownloadSettings settings) throws Exception {
|
||||||
TagOptionSingleton.getInstance().setAndroid(true);
|
TagOptionSingleton.getInstance().setAndroid(true);
|
||||||
//Load file
|
//Load file
|
||||||
AudioFile f = AudioFileIO.read(new File(path));
|
AudioFile f = AudioFileIO.read(new File(path));
|
||||||
|
@ -280,9 +280,9 @@ public class Deezer {
|
||||||
for (int i=0; i<publicTrack.getJSONArray("contributors").length(); i++) {
|
for (int i=0; i<publicTrack.getJSONArray("contributors").length(); i++) {
|
||||||
String artist = publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name");
|
String artist = publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name");
|
||||||
if (!artists.contains(artist))
|
if (!artists.contains(artist))
|
||||||
artists += ", " + artist;
|
artists += settings.artistSeparator + artist;
|
||||||
}
|
}
|
||||||
tag.addField(FieldKey.ARTIST, artists.substring(2));
|
tag.addField(FieldKey.ARTIST, artists.substring(settings.artistSeparator.length()));
|
||||||
tag.setField(FieldKey.TRACK, String.format("%02d", publicTrack.getInt("track_position")));
|
tag.setField(FieldKey.TRACK, String.format("%02d", publicTrack.getInt("track_position")));
|
||||||
tag.setField(FieldKey.DISC_NO, Integer.toString(publicTrack.getInt("disk_number")));
|
tag.setField(FieldKey.DISC_NO, Integer.toString(publicTrack.getInt("disk_number")));
|
||||||
tag.setField(FieldKey.ALBUM_ARTIST, publicAlbum.getJSONObject("artist").getString("name"));
|
tag.setField(FieldKey.ALBUM_ARTIST, publicAlbum.getJSONObject("artist").getString("name"));
|
||||||
|
@ -326,36 +326,36 @@ public class Deezer {
|
||||||
JSONArray composers = contrib.getJSONArray("composer");
|
JSONArray composers = contrib.getJSONArray("composer");
|
||||||
String composer = "";
|
String composer = "";
|
||||||
for (int i = 0; i < composers.length(); i++)
|
for (int i = 0; i < composers.length(); i++)
|
||||||
composer += ", " + composers.getString(i);
|
composer += settings.artistSeparator + composers.getString(i);
|
||||||
if (composer.length() > 2)
|
if (composer.length() > 2)
|
||||||
tag.setField(FieldKey.COMPOSER, composer.substring(2));
|
tag.setField(FieldKey.COMPOSER, composer.substring(settings.artistSeparator.length()));
|
||||||
}
|
}
|
||||||
//Engineer
|
//Engineer
|
||||||
if (contrib.has("engineer")) {
|
if (contrib.has("engineer")) {
|
||||||
JSONArray engineers = contrib.getJSONArray("engineer");
|
JSONArray engineers = contrib.getJSONArray("engineer");
|
||||||
String engineer = "";
|
String engineer = "";
|
||||||
for (int i = 0; i < engineers.length(); i++)
|
for (int i = 0; i < engineers.length(); i++)
|
||||||
engineer += ", " + engineers.getString(i);
|
engineer += settings.artistSeparator + engineers.getString(i);
|
||||||
if (engineer.length() > 2)
|
if (engineer.length() > 2)
|
||||||
tag.setField(FieldKey.ENGINEER, engineer.substring(2));
|
tag.setField(FieldKey.ENGINEER, engineer.substring(settings.artistSeparator.length()));
|
||||||
}
|
}
|
||||||
//Mixer
|
//Mixer
|
||||||
if (contrib.has("mixer")) {
|
if (contrib.has("mixer")) {
|
||||||
JSONArray mixers = contrib.getJSONArray("mixer");
|
JSONArray mixers = contrib.getJSONArray("mixer");
|
||||||
String mixer = "";
|
String mixer = "";
|
||||||
for (int i = 0; i < mixers.length(); i++)
|
for (int i = 0; i < mixers.length(); i++)
|
||||||
mixer += ", " + mixers.getString(i);
|
mixer += settings.artistSeparator + mixers.getString(i);
|
||||||
if (mixer.length() > 2)
|
if (mixer.length() > 2)
|
||||||
tag.setField(FieldKey.MIXER, mixer.substring(2));
|
tag.setField(FieldKey.MIXER, mixer.substring(settings.artistSeparator.length()));
|
||||||
}
|
}
|
||||||
//Producer
|
//Producer
|
||||||
if (contrib.has("producer")) {
|
if (contrib.has("producer")) {
|
||||||
JSONArray producers = contrib.getJSONArray("producer");
|
JSONArray producers = contrib.getJSONArray("producer");
|
||||||
String producer = "";
|
String producer = "";
|
||||||
for (int i = 0; i < producers.length(); i++)
|
for (int i = 0; i < producers.length(); i++)
|
||||||
producer += ", " + producers.getString(i);
|
producer += settings.artistSeparator + producers.getString(i);
|
||||||
if (producer.length() > 2)
|
if (producer.length() > 2)
|
||||||
tag.setField(FieldKey.MIXER, producer.substring(2));
|
tag.setField(FieldKey.MIXER, producer.substring(settings.artistSeparator.length()));
|
||||||
}
|
}
|
||||||
|
|
||||||
//FLAC Only
|
//FLAC Only
|
||||||
|
@ -365,18 +365,18 @@ public class Deezer {
|
||||||
JSONArray authors = contrib.getJSONArray("author");
|
JSONArray authors = contrib.getJSONArray("author");
|
||||||
String author = "";
|
String author = "";
|
||||||
for (int i = 0; i < authors.length(); i++)
|
for (int i = 0; i < authors.length(); i++)
|
||||||
author += ", " + authors.getString(i);
|
author += settings.artistSeparator + authors.getString(i);
|
||||||
if (author.length() > 2)
|
if (author.length() > 2)
|
||||||
((FlacTag) tag).setField("AUTHOR", author.substring(2));
|
((FlacTag) tag).setField("AUTHOR", author.substring(settings.artistSeparator.length()));
|
||||||
}
|
}
|
||||||
//Writer
|
//Writer
|
||||||
if (contrib.has("writer")) {
|
if (contrib.has("writer")) {
|
||||||
JSONArray writers = contrib.getJSONArray("writer");
|
JSONArray writers = contrib.getJSONArray("writer");
|
||||||
String writer = "";
|
String writer = "";
|
||||||
for (int i = 0; i < writers.length(); i++)
|
for (int i = 0; i < writers.length(); i++)
|
||||||
writer += ", " + writers.getString(i);
|
writer += settings.artistSeparator + writers.getString(i);
|
||||||
if (writer.length() > 2)
|
if (writer.length() > 2)
|
||||||
((FlacTag) tag).setField("WRITER", writer.substring(2));
|
((FlacTag) tag).setField("WRITER", writer.substring(settings.artistSeparator.length()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -534,7 +534,7 @@ public class DownloadService extends Service {
|
||||||
|
|
||||||
//Tag
|
//Tag
|
||||||
try {
|
try {
|
||||||
deezer.tagTrack(outFile.getPath(), trackJson, albumJson, coverFile.getPath(), lyricsData, privateJson);
|
deezer.tagTrack(outFile.getPath(), trackJson, albumJson, coverFile.getPath(), lyricsData, privateJson, settings);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e("ERR", "Tagging error!");
|
Log.e("ERR", "Tagging error!");
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
@ -807,8 +807,9 @@ public class DownloadService extends Service {
|
||||||
String arl;
|
String arl;
|
||||||
boolean albumCover;
|
boolean albumCover;
|
||||||
boolean nomediaFiles;
|
boolean nomediaFiles;
|
||||||
|
String artistSeparator;
|
||||||
|
|
||||||
private DownloadSettings(int downloadThreads, boolean overwriteDownload, boolean downloadLyrics, boolean trackCover, String arl, boolean albumCover, boolean nomediaFiles) {
|
private DownloadSettings(int downloadThreads, boolean overwriteDownload, boolean downloadLyrics, boolean trackCover, String arl, boolean albumCover, boolean nomediaFiles, String artistSeparator) {
|
||||||
this.downloadThreads = downloadThreads;
|
this.downloadThreads = downloadThreads;
|
||||||
this.overwriteDownload = overwriteDownload;
|
this.overwriteDownload = overwriteDownload;
|
||||||
this.downloadLyrics = downloadLyrics;
|
this.downloadLyrics = downloadLyrics;
|
||||||
|
@ -816,6 +817,7 @@ public class DownloadService extends Service {
|
||||||
this.arl = arl;
|
this.arl = arl;
|
||||||
this.albumCover = albumCover;
|
this.albumCover = albumCover;
|
||||||
this.nomediaFiles = nomediaFiles;
|
this.nomediaFiles = nomediaFiles;
|
||||||
|
this.artistSeparator = artistSeparator;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Parse settings from bundle sent from UI
|
//Parse settings from bundle sent from UI
|
||||||
|
@ -830,7 +832,8 @@ public class DownloadService extends Service {
|
||||||
json.getBoolean("trackCover"),
|
json.getBoolean("trackCover"),
|
||||||
json.getString("arl"),
|
json.getString("arl"),
|
||||||
json.getBoolean("albumCover"),
|
json.getBoolean("albumCover"),
|
||||||
json.getBoolean("nomediaFiles")
|
json.getBoolean("nomediaFiles"),
|
||||||
|
json.getString("artistSeparator")
|
||||||
);
|
);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
//Shouldn't happen
|
//Shouldn't happen
|
||||||
|
|
|
@ -212,6 +212,14 @@ public class MainActivity extends FlutterActivity {
|
||||||
result.success(null);
|
result.success(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
//Stop services
|
||||||
|
if (call.method.equals("kill")) {
|
||||||
|
Intent intent = new Intent(this, DownloadService.class);
|
||||||
|
stopService(intent);
|
||||||
|
if (streamServer != null)
|
||||||
|
streamServer.stop();
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
result.error("0", "Not implemented!", "Not implemented!");
|
result.error("0", "Not implemented!", "Not implemented!");
|
||||||
})));
|
})));
|
||||||
|
|
|
@ -200,7 +200,7 @@ public class StreamServer {
|
||||||
URL url = new URL(sURL);
|
URL url = new URL(sURL);
|
||||||
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
||||||
//Set headers
|
//Set headers
|
||||||
connection.setConnectTimeout(30000);
|
connection.setConnectTimeout(10000);
|
||||||
connection.setRequestMethod("GET");
|
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("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-Language", "*");
|
||||||
|
|
|
@ -82,6 +82,10 @@ class Cache {
|
||||||
return p.join((await getApplicationDocumentsDirectory()).path, 'metacache.json');
|
return p.join((await getApplicationDocumentsDirectory()).path, 'metacache.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future wipe() async {
|
||||||
|
await File(await getPath()).delete();
|
||||||
|
}
|
||||||
|
|
||||||
static Future<Cache> load() async {
|
static Future<Cache> load() async {
|
||||||
File file = File(await Cache.getPath());
|
File file = File(await Cache.getPath());
|
||||||
//Doesn't exist, create new
|
//Doesn't exist, create new
|
||||||
|
|
|
@ -156,9 +156,9 @@ class Track {
|
||||||
'trackNumber': trackNumber,
|
'trackNumber': trackNumber,
|
||||||
'offline': off?1:0,
|
'offline': off?1:0,
|
||||||
'lyrics': jsonEncode(lyrics.toJson()),
|
'lyrics': jsonEncode(lyrics.toJson()),
|
||||||
'favorite': (favorite??0)?1:0,
|
'favorite': (favorite??false) ? 1 : 0,
|
||||||
'diskNumber': diskNumber,
|
'diskNumber': diskNumber,
|
||||||
'explicit': explicit?1:0,
|
'explicit': (explicit??false) ? 1 : 0,
|
||||||
//'favoriteDate': favoriteDate
|
//'favoriteDate': favoriteDate
|
||||||
};
|
};
|
||||||
factory Track.fromSQL(Map<String, dynamic> data) => Track(
|
factory Track.fromSQL(Map<String, dynamic> data) => Track(
|
||||||
|
@ -232,9 +232,9 @@ class Album {
|
||||||
Map<String, dynamic> toSQL({off = false}) => {
|
Map<String, dynamic> toSQL({off = false}) => {
|
||||||
'id': id,
|
'id': id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'artists': artists.map<String>((dynamic a) => a.id).join(','),
|
'artists': (artists??[]).map<String>((dynamic a) => a.id).join(','),
|
||||||
'tracks': tracks.map<String>((dynamic t) => t.id).join(','),
|
'tracks': (tracks??[]).map<String>((dynamic t) => t.id).join(','),
|
||||||
'art': art.full,
|
'art': art?.full??'',
|
||||||
'fans': fans,
|
'fans': fans,
|
||||||
'offline': off?1:0,
|
'offline': off?1:0,
|
||||||
'library': (library??false)?1:0,
|
'library': (library??false)?1:0,
|
||||||
|
@ -255,7 +255,7 @@ class Album {
|
||||||
fans: data['fans'],
|
fans: data['fans'],
|
||||||
offline: (data['offline'] == 1) ? true:false,
|
offline: (data['offline'] == 1) ? true:false,
|
||||||
library: (data['library'] == 1) ? true:false,
|
library: (data['library'] == 1) ? true:false,
|
||||||
type: AlbumType.values[data['type']],
|
type: AlbumType.values[(data['type'] == -1) ? 0 : data['type']],
|
||||||
releaseDate: data['releaseDate'],
|
releaseDate: data['releaseDate'],
|
||||||
//favoriteDate: data['favoriteDate']
|
//favoriteDate: data['favoriteDate']
|
||||||
);
|
);
|
||||||
|
@ -619,6 +619,9 @@ class HomePage {
|
||||||
Map data = jsonDecode(await File(path).readAsString());
|
Map data = jsonDecode(await File(path).readAsString());
|
||||||
return HomePage.fromJson(data);
|
return HomePage.fromJson(data);
|
||||||
}
|
}
|
||||||
|
Future wipe() async {
|
||||||
|
await File(await _getPath()).delete();
|
||||||
|
}
|
||||||
|
|
||||||
//JSON
|
//JSON
|
||||||
factory HomePage.fromPrivateJson(Map<dynamic, dynamic> json) {
|
factory HomePage.fromPrivateJson(Map<dynamic, dynamic> json) {
|
||||||
|
|
|
@ -157,7 +157,7 @@ class DownloadManager {
|
||||||
return quality;
|
return quality;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> addOfflineTrack(Track track, {private = true, BuildContext context}) async {
|
Future<bool> addOfflineTrack(Track track, {private = true, BuildContext context, isSingleton = false}) async {
|
||||||
//Permission
|
//Permission
|
||||||
if (!private && !(await checkPermission())) return false;
|
if (!private && !(await checkPermission())) return false;
|
||||||
|
|
||||||
|
@ -168,6 +168,10 @@ class DownloadManager {
|
||||||
if (quality == null) return false;
|
if (quality == null) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Fetch track if missing meta
|
||||||
|
if (track.artists == null || track.artists.length == 0 || track.album == null)
|
||||||
|
track = await deezerAPI.track(track.id);
|
||||||
|
|
||||||
//Add to DB
|
//Add to DB
|
||||||
if (private) {
|
if (private) {
|
||||||
Batch b = db.batch();
|
Batch b = db.batch();
|
||||||
|
@ -180,9 +184,10 @@ class DownloadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Get path
|
//Get path
|
||||||
String path = _generatePath(track, private);
|
String path = _generatePath(track, private, isSingleton: isSingleton);
|
||||||
await platform.invokeMethod('addDownloads', [await Download.jsonFromTrack(track, path, private: private, quality: quality)]);
|
await platform.invokeMethod('addDownloads', [await Download.jsonFromTrack(track, path, private: private, quality: quality)]);
|
||||||
await start();
|
await start();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future addOfflineAlbum(Album album, {private = true, BuildContext context}) async {
|
Future addOfflineAlbum(Album album, {private = true, BuildContext context}) async {
|
||||||
|
@ -478,7 +483,7 @@ class DownloadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Generate track download path
|
//Generate track download path
|
||||||
String _generatePath(Track track, bool private, {String playlistName, int playlistTrackNumber}) {
|
String _generatePath(Track track, bool private, {String playlistName, int playlistTrackNumber, bool isSingleton = false}) {
|
||||||
String path;
|
String path;
|
||||||
if (private) {
|
if (private) {
|
||||||
path = p.join(offlinePath, track.id);
|
path = p.join(offlinePath, track.id);
|
||||||
|
@ -501,7 +506,7 @@ class DownloadManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Final path
|
//Final path
|
||||||
path = p.join(path, settings.downloadFilename);
|
path = p.join(path, isSingleton ? settings.singletonFilename : settings.downloadFilename);
|
||||||
//Playlist track number variable (not accessible in service)
|
//Playlist track number variable (not accessible in service)
|
||||||
if (playlistTrackNumber != null) {
|
if (playlistTrackNumber != null) {
|
||||||
path = path.replaceAll('%playlistTrackNumber%', playlistTrackNumber.toString());
|
path = path.replaceAll('%playlistTrackNumber%', playlistTrackNumber.toString());
|
||||||
|
|
|
@ -31,6 +31,20 @@ class SpotifyAPI {
|
||||||
//Get spotify embed url from uri
|
//Get spotify embed url from uri
|
||||||
String getEmbedUrl(String uri) => 'https://embed.spotify.com/?uri=$uri';
|
String getEmbedUrl(String uri) => 'https://embed.spotify.com/?uri=$uri';
|
||||||
|
|
||||||
|
//https://link.tospotify.com/ or https://spotify.app.link/
|
||||||
|
Future resolveLinkUrl(String url) async {
|
||||||
|
http.Response response = await http.get(Uri.parse(url));
|
||||||
|
Match match = RegExp(r'window\.top\.location = validate\("(.+)"\);').firstMatch(response.body);
|
||||||
|
return match.group(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future resolveUrl(String url) async {
|
||||||
|
if (url.contains("link.tospotify") || url.contains("spotify.app.link")) {
|
||||||
|
return parseUrl(await resolveLinkUrl(url));
|
||||||
|
}
|
||||||
|
return parseUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
//Extract JSON data form spotify embed page
|
//Extract JSON data form spotify embed page
|
||||||
Future<Map> getEmbedData(String url) async {
|
Future<Map> getEmbedData(String url) async {
|
||||||
//Fetch
|
//Fetch
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -292,6 +292,11 @@ const language_en_us = {
|
||||||
"Share show": "Share show",
|
"Share show": "Share show",
|
||||||
"Date added": "Date added",
|
"Date added": "Date added",
|
||||||
"Discord": "Discord",
|
"Discord": "Discord",
|
||||||
"Official Discord server": "Official Discord server"
|
"Official Discord server": "Official Discord server",
|
||||||
|
|
||||||
|
//0.6.6
|
||||||
|
"Restart of app is required to properly log out!": "Restart of app is required to properly log out!",
|
||||||
|
"Artist separator": "Artist separator",
|
||||||
|
"Singleton naming": "Standalone tracks filename"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -142,10 +142,11 @@ class _LoginMainWrapperState extends State<LoginMainWrapper> {
|
||||||
Future _logOut() async {
|
Future _logOut() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
settings.arl = null;
|
settings.arl = null;
|
||||||
settings.offlineMode = true;
|
settings.offlineMode = false;
|
||||||
deezerAPI = new DeezerAPI();
|
deezerAPI = new DeezerAPI();
|
||||||
});
|
});
|
||||||
await settings.save();
|
await settings.save();
|
||||||
|
await Cache.wipe();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -45,7 +45,7 @@ class Settings {
|
||||||
//Download options
|
//Download options
|
||||||
String downloadPath;
|
String downloadPath;
|
||||||
|
|
||||||
@JsonKey(defaultValue: "%artists% - %title%")
|
@JsonKey(defaultValue: "%artist% - %title%")
|
||||||
String downloadFilename;
|
String downloadFilename;
|
||||||
@JsonKey(defaultValue: true)
|
@JsonKey(defaultValue: true)
|
||||||
bool albumFolder;
|
bool albumFolder;
|
||||||
|
@ -67,6 +67,10 @@ class Settings {
|
||||||
bool albumCover;
|
bool albumCover;
|
||||||
@JsonKey(defaultValue: false)
|
@JsonKey(defaultValue: false)
|
||||||
bool nomediaFiles;
|
bool nomediaFiles;
|
||||||
|
@JsonKey(defaultValue: ", ")
|
||||||
|
String artistSeparator;
|
||||||
|
@JsonKey(defaultValue: "%artist% - %title%")
|
||||||
|
String singletonFilename;
|
||||||
|
|
||||||
//Appearance
|
//Appearance
|
||||||
@JsonKey(defaultValue: Themes.Dark)
|
@JsonKey(defaultValue: Themes.Dark)
|
||||||
|
|
|
@ -26,7 +26,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
|
||||||
_$enumDecodeNullable(_$AudioQualityEnumMap, json['downloadQuality']) ??
|
_$enumDecodeNullable(_$AudioQualityEnumMap, json['downloadQuality']) ??
|
||||||
AudioQuality.FLAC
|
AudioQuality.FLAC
|
||||||
..downloadFilename =
|
..downloadFilename =
|
||||||
json['downloadFilename'] as String ?? '%artists% - %title%'
|
json['downloadFilename'] as String ?? '%artist% - %title%'
|
||||||
..albumFolder = json['albumFolder'] as bool ?? true
|
..albumFolder = json['albumFolder'] as bool ?? true
|
||||||
..artistFolder = json['artistFolder'] as bool ?? true
|
..artistFolder = json['artistFolder'] as bool ?? true
|
||||||
..albumDiscFolder = json['albumDiscFolder'] as bool ?? false
|
..albumDiscFolder = json['albumDiscFolder'] as bool ?? false
|
||||||
|
@ -37,6 +37,9 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
|
||||||
..trackCover = json['trackCover'] as bool ?? false
|
..trackCover = json['trackCover'] as bool ?? false
|
||||||
..albumCover = json['albumCover'] as bool ?? true
|
..albumCover = json['albumCover'] as bool ?? true
|
||||||
..nomediaFiles = json['nomediaFiles'] as bool ?? false
|
..nomediaFiles = json['nomediaFiles'] as bool ?? false
|
||||||
|
..artistSeparator = json['artistSeparator'] as String ?? ', '
|
||||||
|
..singletonFilename =
|
||||||
|
json['singletonFilename'] as String ?? '%artist% - %title%'
|
||||||
..theme =
|
..theme =
|
||||||
_$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Dark
|
_$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Dark
|
||||||
..useSystemTheme = json['useSystemTheme'] as bool ?? false
|
..useSystemTheme = json['useSystemTheme'] as bool ?? false
|
||||||
|
@ -71,6 +74,8 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
|
||||||
'trackCover': instance.trackCover,
|
'trackCover': instance.trackCover,
|
||||||
'albumCover': instance.albumCover,
|
'albumCover': instance.albumCover,
|
||||||
'nomediaFiles': instance.nomediaFiles,
|
'nomediaFiles': instance.nomediaFiles,
|
||||||
|
'artistSeparator': instance.artistSeparator,
|
||||||
|
'singletonFilename': instance.singletonFilename,
|
||||||
'theme': _$ThemesEnumMap[instance.theme],
|
'theme': _$ThemesEnumMap[instance.theme],
|
||||||
'useSystemTheme': instance.useSystemTheme,
|
'useSystemTheme': instance.useSystemTheme,
|
||||||
'colorGradientBackground': instance.colorGradientBackground,
|
'colorGradientBackground': instance.colorGradientBackground,
|
||||||
|
|
|
@ -27,6 +27,7 @@ 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('vi', 'VI'),
|
||||||
const Locale('fil', 'PH'),
|
const Locale('fil', 'PH'),
|
||||||
const Locale('uwu', 'UWU')
|
const Locale('uwu', 'UWU')
|
||||||
];
|
];
|
||||||
|
|
|
@ -159,7 +159,10 @@ class HomepageSectionWidget extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
contentPadding: EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0),
|
||||||
|
title: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 6.0),
|
||||||
|
child: Text(
|
||||||
section.title??'',
|
section.title??'',
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
|
@ -169,6 +172,7 @@ class HomepageSectionWidget extends StatelessWidget {
|
||||||
fontWeight: FontWeight.w900
|
fontWeight: FontWeight.w900
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
subtitle: SingleChildScrollView(
|
subtitle: SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
|
@ -4,7 +4,6 @@ import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:freezer/api/definitions.dart';
|
import 'package:freezer/api/definitions.dart';
|
||||||
import 'package:freezer/api/download.dart';
|
import 'package:freezer/api/download.dart';
|
||||||
import 'package:freezer/api/spotify.dart';
|
import 'package:freezer/api/spotify.dart';
|
||||||
import 'package:freezer/main.dart';
|
|
||||||
import 'package:freezer/settings.dart';
|
import 'package:freezer/settings.dart';
|
||||||
import 'package:freezer/ui/elements.dart';
|
import 'package:freezer/ui/elements.dart';
|
||||||
import 'package:freezer/ui/menu.dart';
|
import 'package:freezer/ui/menu.dart';
|
||||||
|
@ -28,7 +27,7 @@ class _ImporterScreenState extends State<ImporterScreen> {
|
||||||
_loading = true;
|
_loading = true;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
String uri = spotify.parseUrl(_url);
|
String uri = await spotify.resolveUrl(_url);
|
||||||
|
|
||||||
//Error/NonPlaylist
|
//Error/NonPlaylist
|
||||||
if (uri == null || uri.split(':')[1] != 'playlist') {
|
if (uri == null || uri.split(':')[1] != 'playlist') {
|
||||||
|
|
|
@ -133,6 +133,7 @@ class MenuSheet {
|
||||||
(cache.checkTrackFavorite(track))?removeFavoriteTrack(track, onUpdate: onRemove):addTrackFavorite(track),
|
(cache.checkTrackFavorite(track))?removeFavoriteTrack(track, onUpdate: onRemove):addTrackFavorite(track),
|
||||||
addToPlaylist(track),
|
addToPlaylist(track),
|
||||||
downloadTrack(track),
|
downloadTrack(track),
|
||||||
|
offlineTrack(track),
|
||||||
shareTile('track', track.id),
|
shareTile('track', track.id),
|
||||||
playMix(track),
|
playMix(track),
|
||||||
showAlbum(track.album),
|
showAlbum(track.album),
|
||||||
|
@ -191,7 +192,7 @@ class MenuSheet {
|
||||||
title: Text('Download'.i18n),
|
title: Text('Download'.i18n),
|
||||||
leading: Icon(Icons.file_download),
|
leading: Icon(Icons.file_download),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (await downloadManager.addOfflineTrack(t, private: false, context: context) != false)
|
if (await downloadManager.addOfflineTrack(t, private: false, context: context, isSingleton: true) != false)
|
||||||
showDownloadStartedToast();
|
showDownloadStartedToast();
|
||||||
_close();
|
_close();
|
||||||
},
|
},
|
||||||
|
@ -301,6 +302,15 @@ class MenuSheet {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Widget offlineTrack(Track track) => ListTile(
|
||||||
|
title: Text('Offline'.i18n),
|
||||||
|
leading: Icon(Icons.offline_pin),
|
||||||
|
onTap: () async {
|
||||||
|
await downloadManager.addOfflineTrack(track, private: true, context: context);
|
||||||
|
_close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
//===================
|
//===================
|
||||||
// ALBUM
|
// ALBUM
|
||||||
//===================
|
//===================
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
|
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
|
||||||
import 'package:fluttericon/font_awesome5_icons.dart';
|
import 'package:fluttericon/font_awesome5_icons.dart';
|
||||||
|
import 'package:fluttericon/web_symbols_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';
|
||||||
|
@ -580,6 +581,79 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FilenameTemplateDialog extends StatefulWidget {
|
||||||
|
|
||||||
|
String initial;
|
||||||
|
Function onSave;
|
||||||
|
FilenameTemplateDialog(this.initial, this.onSave, {Key key}): super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_FilenameTemplateDialogState createState() => _FilenameTemplateDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FilenameTemplateDialogState extends State<FilenameTemplateDialog> {
|
||||||
|
|
||||||
|
TextEditingController _controller;
|
||||||
|
String _new;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_controller = TextEditingController(text: widget.initial);
|
||||||
|
_new = _controller.value.text;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
//Dialog with filename format
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('Downloaded tracks filename'.i18n),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _controller,
|
||||||
|
onChanged: (String s) => _new = s,
|
||||||
|
),
|
||||||
|
Container(height: 8.0),
|
||||||
|
Text(
|
||||||
|
'Valid variables are'.i18n + ': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%, %playlistTrackNumber%, %0playlistTrackNumber%, %year%, %date%\n\n' +
|
||||||
|
"If you want to use custom directory naming - use '/' as directory separator.".i18n,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Cancel'.i18n),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Reset'.i18n),
|
||||||
|
onPressed: () {
|
||||||
|
_controller.value = _controller.value.copyWith(text: '%artist% - %title%');
|
||||||
|
_new = '%artist% - %title%';
|
||||||
|
},
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Clear'.i18n),
|
||||||
|
onPressed: () => _controller.clear(),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Save'.i18n),
|
||||||
|
onPressed: () async {
|
||||||
|
widget.onSave(_new);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DownloadsSettings extends StatefulWidget {
|
class DownloadsSettings extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
_DownloadsSettingsState createState() => _DownloadsSettingsState();
|
_DownloadsSettingsState createState() => _DownloadsSettingsState();
|
||||||
|
@ -588,6 +662,7 @@ class DownloadsSettings extends StatefulWidget {
|
||||||
class _DownloadsSettingsState extends State<DownloadsSettings> {
|
class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||||||
|
|
||||||
double _downloadThreads = settings.downloadThreads.toDouble();
|
double _downloadThreads = settings.downloadThreads.toDouble();
|
||||||
|
TextEditingController _artistSeparatorController = TextEditingController(text: settings.artistSeparator);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -619,62 +694,26 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
|
return FilenameTemplateDialog(settings.downloadFilename, (f) async {
|
||||||
TextEditingController _controller = TextEditingController();
|
setState(() => settings.downloadFilename = f);
|
||||||
String filename = settings.downloadFilename;
|
|
||||||
_controller.value = _controller.value.copyWith(text: filename);
|
|
||||||
String _new = _controller.value.text;
|
|
||||||
|
|
||||||
//Dialog with filename format
|
|
||||||
return AlertDialog(
|
|
||||||
title: Text('Downloaded tracks filename'.i18n),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
TextField(
|
|
||||||
controller: _controller,
|
|
||||||
onChanged: (String s) => _new = s,
|
|
||||||
),
|
|
||||||
Container(height: 8.0),
|
|
||||||
Text(
|
|
||||||
'Valid variables are'.i18n + ': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%, %playlistTrackNumber%, %0playlistTrackNumber%, %year%, %date%\n\n' +
|
|
||||||
"If you want to use custom directory naming - use '/' as directory separator.".i18n,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12.0,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
FlatButton(
|
|
||||||
child: Text('Cancel'.i18n),
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
),
|
|
||||||
FlatButton(
|
|
||||||
child: Text('Reset'.i18n),
|
|
||||||
onPressed: () {
|
|
||||||
_controller.value = _controller.value.copyWith(
|
|
||||||
text: '%artists% - %title%'
|
|
||||||
);
|
|
||||||
_new = '%artists% - %title%';
|
|
||||||
},
|
|
||||||
),
|
|
||||||
FlatButton(
|
|
||||||
child: Text('Clear'.i18n),
|
|
||||||
onPressed: () => _controller.clear(),
|
|
||||||
),
|
|
||||||
FlatButton(
|
|
||||||
child: Text('Save'.i18n),
|
|
||||||
onPressed: () async {
|
|
||||||
setState(() {
|
|
||||||
settings.downloadFilename = _new;
|
|
||||||
});
|
|
||||||
await settings.save();
|
await settings.save();
|
||||||
Navigator.of(context).pop();
|
});
|
||||||
},
|
}
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('Singleton naming'.i18n),
|
||||||
|
subtitle: Text('Currently'.i18n + ': ${settings.singletonFilename}'),
|
||||||
|
leading: Icon(Icons.text_format),
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return FilenameTemplateDialog(settings.singletonFilename, (f) async {
|
||||||
|
setState(() => settings.singletonFilename = f);
|
||||||
|
await settings.save();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -829,6 +868,20 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||||||
),
|
),
|
||||||
leading: Icon(Icons.insert_drive_file)
|
leading: Icon(Icons.insert_drive_file)
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('Artist separator'.i18n),
|
||||||
|
leading: Icon(WebSymbols.tag),
|
||||||
|
trailing: Container(
|
||||||
|
width: 100.0,
|
||||||
|
child: TextField(
|
||||||
|
controller: _artistSeparatorController,
|
||||||
|
onChanged: (s) async {
|
||||||
|
settings.artistSeparator = s;
|
||||||
|
await settings.save();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
FreezerDivider(),
|
FreezerDivider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Download Log'.i18n),
|
title: Text('Download Log'.i18n),
|
||||||
|
@ -943,24 +996,26 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text('Log out'.i18n),
|
title: Text('Log out'.i18n),
|
||||||
content: Text('Due to plugin incompatibility, login using browser is unavailable without restart.'.i18n),
|
// content: Text('Due to plugin incompatibility, login using browser is unavailable without restart.'.i18n),
|
||||||
|
content: Text('Restart of app is required to properly log out!'.i18n),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
FlatButton(
|
FlatButton(
|
||||||
child: Text('Cancel'.i18n),
|
child: Text('Cancel'.i18n),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
FlatButton(
|
// FlatButton(
|
||||||
child: Text('(ARL ONLY) Continue'.i18n),
|
// child: Text('(ARL ONLY) Continue'.i18n),
|
||||||
onPressed: () async {
|
// onPressed: () async {
|
||||||
await logOut();
|
// await logOut();
|
||||||
Navigator.of(context).pop();
|
// Navigator.of(context).pop();
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
FlatButton(
|
FlatButton(
|
||||||
child: Text('Log out & Exit'.i18n),
|
child: Text('Log out & Exit'.i18n),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
try {AudioService.stop();} catch (e) {}
|
try {AudioService.stop();} catch (e) {}
|
||||||
await logOut();
|
await logOut();
|
||||||
|
await DownloadManager.platform.invokeMethod("kill");
|
||||||
SystemNavigator.pop();
|
SystemNavigator.pop();
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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.5+1
|
version: 0.6.6+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.8.0 <3.0.0"
|
sdk: ">=2.8.0 <3.0.0"
|
||||||
|
|
Loading…
Reference in New Issue