0.6.6 - standalone track naming, artist separator

This commit is contained in:
exttex 2020-12-04 18:02:50 +01:00
parent ef9ae6e2ad
commit babd12bae2
20 changed files with 271 additions and 150 deletions

View File

@ -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 {

View File

@ -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()));
} }
} }
} }

View File

@ -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

View File

@ -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!");
}))); })));

View File

@ -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", "*");

View File

@ -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

View File

@ -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) {

View File

@ -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());

View File

@ -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

View File

@ -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"
} }
}; };

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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')
]; ];

View File

@ -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(

View File

@ -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') {

View File

@ -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
//=================== //===================

View File

@ -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();
}, },
) )

View File

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.6.5+1 version: 0.6.6+1
environment: environment:
sdk: ">=2.8.0 <3.0.0" sdk: ">=2.8.0 <3.0.0"