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

@ -82,6 +82,10 @@ class Cache {
return p.join((await getApplicationDocumentsDirectory()).path, 'metacache.json');
}
static Future wipe() async {
await File(await getPath()).delete();
}
static Future<Cache> load() async {
File file = File(await Cache.getPath());
//Doesn't exist, create new

View file

@ -156,9 +156,9 @@ class Track {
'trackNumber': trackNumber,
'offline': off?1:0,
'lyrics': jsonEncode(lyrics.toJson()),
'favorite': (favorite??0)?1:0,
'favorite': (favorite??false) ? 1 : 0,
'diskNumber': diskNumber,
'explicit': explicit?1:0,
'explicit': (explicit??false) ? 1 : 0,
//'favoriteDate': favoriteDate
};
factory Track.fromSQL(Map<String, dynamic> data) => Track(
@ -232,9 +232,9 @@ class Album {
Map<String, dynamic> toSQL({off = false}) => {
'id': id,
'title': title,
'artists': artists.map<String>((dynamic a) => a.id).join(','),
'tracks': tracks.map<String>((dynamic t) => t.id).join(','),
'art': art.full,
'artists': (artists??[]).map<String>((dynamic a) => a.id).join(','),
'tracks': (tracks??[]).map<String>((dynamic t) => t.id).join(','),
'art': art?.full??'',
'fans': fans,
'offline': off?1:0,
'library': (library??false)?1:0,
@ -255,7 +255,7 @@ class Album {
fans: data['fans'],
offline: (data['offline'] == 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'],
//favoriteDate: data['favoriteDate']
);
@ -619,6 +619,9 @@ class HomePage {
Map data = jsonDecode(await File(path).readAsString());
return HomePage.fromJson(data);
}
Future wipe() async {
await File(await _getPath()).delete();
}
//JSON
factory HomePage.fromPrivateJson(Map<dynamic, dynamic> json) {

View file

@ -157,7 +157,7 @@ class DownloadManager {
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
if (!private && !(await checkPermission())) return false;
@ -168,6 +168,10 @@ class DownloadManager {
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
if (private) {
Batch b = db.batch();
@ -180,9 +184,10 @@ class DownloadManager {
}
//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 start();
return true;
}
Future addOfflineAlbum(Album album, {private = true, BuildContext context}) async {
@ -478,7 +483,7 @@ class DownloadManager {
}
//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;
if (private) {
path = p.join(offlinePath, track.id);
@ -501,7 +506,7 @@ class DownloadManager {
}
}
//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)
if (playlistTrackNumber != null) {
path = path.replaceAll('%playlistTrackNumber%', playlistTrackNumber.toString());

View file

@ -31,6 +31,20 @@ class SpotifyAPI {
//Get spotify embed url from 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
Future<Map> getEmbedData(String url) async {
//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",
"Date added": "Date added",
"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 {
setState(() {
settings.arl = null;
settings.offlineMode = true;
settings.offlineMode = false;
deezerAPI = new DeezerAPI();
});
await settings.save();
await Cache.wipe();
}
@override

View file

@ -45,7 +45,7 @@ class Settings {
//Download options
String downloadPath;
@JsonKey(defaultValue: "%artists% - %title%")
@JsonKey(defaultValue: "%artist% - %title%")
String downloadFilename;
@JsonKey(defaultValue: true)
bool albumFolder;
@ -67,6 +67,10 @@ class Settings {
bool albumCover;
@JsonKey(defaultValue: false)
bool nomediaFiles;
@JsonKey(defaultValue: ", ")
String artistSeparator;
@JsonKey(defaultValue: "%artist% - %title%")
String singletonFilename;
//Appearance
@JsonKey(defaultValue: Themes.Dark)

View file

@ -26,7 +26,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
_$enumDecodeNullable(_$AudioQualityEnumMap, json['downloadQuality']) ??
AudioQuality.FLAC
..downloadFilename =
json['downloadFilename'] as String ?? '%artists% - %title%'
json['downloadFilename'] as String ?? '%artist% - %title%'
..albumFolder = json['albumFolder'] as bool ?? true
..artistFolder = json['artistFolder'] as bool ?? true
..albumDiscFolder = json['albumDiscFolder'] as bool ?? false
@ -37,6 +37,9 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
..trackCover = json['trackCover'] as bool ?? false
..albumCover = json['albumCover'] as bool ?? true
..nomediaFiles = json['nomediaFiles'] as bool ?? false
..artistSeparator = json['artistSeparator'] as String ?? ', '
..singletonFilename =
json['singletonFilename'] as String ?? '%artist% - %title%'
..theme =
_$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Dark
..useSystemTheme = json['useSystemTheme'] as bool ?? false
@ -71,6 +74,8 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
'trackCover': instance.trackCover,
'albumCover': instance.albumCover,
'nomediaFiles': instance.nomediaFiles,
'artistSeparator': instance.artistSeparator,
'singletonFilename': instance.singletonFilename,
'theme': _$ThemesEnumMap[instance.theme],
'useSystemTheme': instance.useSystemTheme,
'colorGradientBackground': instance.colorGradientBackground,

View file

@ -27,6 +27,7 @@ const supportedLocales = [
const Locale('hi', 'IN'),
const Locale('sk', 'SK'),
const Locale('cs', 'CZ'),
const Locale('vi', 'VI'),
const Locale('fil', 'PH'),
const Locale('uwu', 'UWU')
];

View file

@ -158,54 +158,58 @@ class HomepageSectionWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(
section.title??'',
textAlign: TextAlign.left,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.w900
),
),
subtitle: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(section.items.length + 1, (j) {
//Has more items
if (j == section.items.length) {
if (section.hasMore ?? false) {
return FlatButton(
child: Text(
'Show more'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.0
),
),
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => Scaffold(
appBar: FreezerAppBar(section.title),
body: SingleChildScrollView(
child: HomePageScreen(
channel: DeezerChannel(target: section.pagePath)
)
),
),
)),
);
}
return Container(height: 0, width: 0);
}
return ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0),
title: Padding(
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 6.0),
child: Text(
section.title??'',
textAlign: TextAlign.left,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.w900
),
),
),
subtitle: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(section.items.length + 1, (j) {
//Has more items
if (j == section.items.length) {
if (section.hasMore ?? false) {
return FlatButton(
child: Text(
'Show more'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.0
),
),
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => Scaffold(
appBar: FreezerAppBar(section.title),
body: SingleChildScrollView(
child: HomePageScreen(
channel: DeezerChannel(target: section.pagePath)
)
),
),
)),
);
}
return Container(height: 0, width: 0);
}
//Show item
HomePageItem item = section.items[j];
return HomePageItemWidget(item);
}),
),
)
);
//Show item
HomePageItem item = section.items[j];
return HomePageItemWidget(item);
}),
),
)
);
}
}

View file

@ -4,7 +4,6 @@ import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/download.dart';
import 'package:freezer/api/spotify.dart';
import 'package:freezer/main.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/menu.dart';
@ -28,7 +27,7 @@ class _ImporterScreenState extends State<ImporterScreen> {
_loading = true;
});
try {
String uri = spotify.parseUrl(_url);
String uri = await spotify.resolveUrl(_url);
//Error/NonPlaylist
if (uri == null || uri.split(':')[1] != 'playlist') {

View file

@ -133,6 +133,7 @@ class MenuSheet {
(cache.checkTrackFavorite(track))?removeFavoriteTrack(track, onUpdate: onRemove):addTrackFavorite(track),
addToPlaylist(track),
downloadTrack(track),
offlineTrack(track),
shareTile('track', track.id),
playMix(track),
showAlbum(track.album),
@ -191,7 +192,7 @@ class MenuSheet {
title: Text('Download'.i18n),
leading: Icon(Icons.file_download),
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();
_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
//===================

View file

@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
import 'package:fluttericon/font_awesome5_icons.dart';
import 'package:fluttericon/web_symbols_icons.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:freezer/api/cache.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 {
@override
_DownloadsSettingsState createState() => _DownloadsSettingsState();
@ -588,6 +662,7 @@ class DownloadsSettings extends StatefulWidget {
class _DownloadsSettingsState extends State<DownloadsSettings> {
double _downloadThreads = settings.downloadThreads.toDouble();
TextEditingController _artistSeparatorController = TextEditingController(text: settings.artistSeparator);
@override
Widget build(BuildContext context) {
@ -619,62 +694,26 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
showDialog(
context: context,
builder: (context) {
TextEditingController _controller = TextEditingController();
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();
Navigator.of(context).pop();
},
)
],
);
return FilenameTemplateDialog(settings.downloadFilename, (f) async {
setState(() => settings.downloadFilename = f);
await settings.save();
});
}
);
},
),
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)
),
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(),
ListTile(
title: Text('Download Log'.i18n),
@ -943,24 +996,26 @@ class _GeneralSettingsState extends State<GeneralSettings> {
builder: (context) {
return AlertDialog(
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>[
FlatButton(
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('(ARL ONLY) Continue'.i18n),
onPressed: () async {
await logOut();
Navigator.of(context).pop();
},
),
// FlatButton(
// child: Text('(ARL ONLY) Continue'.i18n),
// onPressed: () async {
// await logOut();
// Navigator.of(context).pop();
// },
// ),
FlatButton(
child: Text('Log out & Exit'.i18n),
onPressed: () async {
try {AudioService.stop();} catch (e) {}
await logOut();
await DownloadManager.platform.invokeMethod("kill");
SystemNavigator.pop();
},
)