0.6.0 - Redesign, downloads, tagging fixes, download quality selector...

This commit is contained in:
exttex 2020-10-19 21:28:45 +02:00
parent bcf709e56d
commit 1384aedb35
28 changed files with 1201 additions and 878 deletions

View File

@ -8,11 +8,13 @@ import org.jaudiotagger.audio.AudioFileIO;
import org.jaudiotagger.tag.FieldKey;
import org.jaudiotagger.tag.Tag;
import org.jaudiotagger.tag.TagOptionSingleton;
import org.jaudiotagger.tag.datatype.Artwork;
import org.jaudiotagger.tag.flac.FlacTag;
import org.jaudiotagger.tag.id3.ID3v23Tag;
import org.jaudiotagger.tag.id3.valuepair.ImageFormats;
import org.jaudiotagger.tag.images.Artwork;
import org.jaudiotagger.tag.images.ArtworkFactory;
import org.jaudiotagger.tag.reference.PictureTypes;
import org.jaudiotagger.tag.vorbiscomment.VorbisCommentFieldKey;
import org.json.JSONArray;
import org.json.JSONObject;
@ -119,6 +121,12 @@ public class Deezer {
data += scanner.nextLine();
}
//End
try {
connection.disconnect();
scanner.close();
} catch (Exception e) {}
//Parse JSON
JSONObject out = new JSONObject(data);
@ -156,6 +164,12 @@ public class Deezer {
data += scanner.nextLine();
}
//Close
try {
connection.disconnect();
scanner.close();
} catch (Exception e) {}
//Parse JSON
JSONObject out = new JSONObject(data);
return out;
@ -252,9 +266,11 @@ public class Deezer {
String artists = "";
String feats = "";
for (int i=0; i<publicTrack.getJSONArray("contributors").length(); i++) {
artists += ", " + publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name");
if (i > 0)
feats += ", " + publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name");
String artist = publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name");
if (!artists.contains(artist))
artists += ", " + artist;
if (i > 0 && !artists.contains(artist) && !feats.contains(artist))
feats += ", " + artist;
}
original = original.replaceAll("%artists%", sanitize(artists).substring(2));
if (feats.length() >= 2)
@ -267,6 +283,9 @@ public class Deezer {
original = original.replaceAll("%year%", publicTrack.getString("release_date").substring(0, 4));
original = original.replaceAll("%date%", publicTrack.getString("release_date"));
//Remove leading dots
original = original.replaceAll("/\\.+", "/");
if (newQuality == 9) return original + ".flac";
return original + ".mp3";
}
@ -286,7 +305,7 @@ public class Deezer {
}
//Tag track with data from API
public static void tagTrack(String path, JSONObject publicTrack, JSONObject publicAlbum, String cover, JSONObject lyricsData, JSONObject privateJson) throws Exception {
public void tagTrack(String path, JSONObject publicTrack, JSONObject publicAlbum, String cover, JSONObject lyricsData, JSONObject privateJson) throws Exception {
TagOptionSingleton.getInstance().setAndroid(true);
//Load file
AudioFile f = AudioFileIO.read(new File(path));
@ -302,7 +321,9 @@ public class Deezer {
//Artist
String artists = "";
for (int i=0; i<publicTrack.getJSONArray("contributors").length(); i++) {
artists += ", " + publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name");
String artist = publicTrack.getJSONArray("contributors").getJSONObject(i).getString("name");
if (!artists.contains(artist))
artists += ", " + artist;
}
tag.addField(FieldKey.ARTIST, artists.substring(2));
tag.setField(FieldKey.TRACK, Integer.toString(publicTrack.getInt("track_position")));
@ -337,66 +358,70 @@ public class Deezer {
tag.setField(FieldKey.GENRE, genres.substring(2));
//Additional tags from private api
if (privateJson != null && privateJson.has("SNG_CONTRIBUTORS")) {
JSONObject contrib = privateJson.getJSONObject("SNG_CONTRIBUTORS");
//Composer
if (contrib.has("composer")) {
JSONArray composers = contrib.getJSONArray("composer");
String composer = "";
for (int i=0; i<composers.length(); i++)
composer += ", " + composers.getString(i);
if (composer.length() > 2)
tag.setField(FieldKey.COMPOSER, composer.substring(2));
}
//Engineer
if (contrib.has("engineer")) {
JSONArray engineers = contrib.getJSONArray("engineer");
String engineer = "";
for (int i=0; i<engineers.length(); i++)
engineer += ", " + engineers.getString(i);
if (engineer.length() > 2)
tag.setField(FieldKey.ENGINEER, engineer.substring(2));
}
//Mixer
if (contrib.has("mixer")) {
JSONArray mixers = contrib.getJSONArray("mixer");
String mixer = "";
for (int i=0; i<mixers.length(); i++)
mixer += ", " + mixers.getString(i);
if (mixer.length() > 2)
tag.setField(FieldKey.MIXER, mixer.substring(2));
}
//Producer
if (contrib.has("producer")) {
JSONArray producers = contrib.getJSONArray("producer");
String producer = "";
for (int i=0; i<producers.length(); i++)
producer += ", " + producers.getString(i);
if (producer.length() > 2)
tag.setField(FieldKey.MIXER, producer.substring(2));
}
try {
if (privateJson != null && privateJson.has("SNG_CONTRIBUTORS")) {
JSONObject contrib = privateJson.getJSONObject("SNG_CONTRIBUTORS");
//Composer
if (contrib.has("composer")) {
JSONArray composers = contrib.getJSONArray("composer");
String composer = "";
for (int i = 0; i < composers.length(); i++)
composer += ", " + composers.getString(i);
if (composer.length() > 2)
tag.setField(FieldKey.COMPOSER, composer.substring(2));
}
//Engineer
if (contrib.has("engineer")) {
JSONArray engineers = contrib.getJSONArray("engineer");
String engineer = "";
for (int i = 0; i < engineers.length(); i++)
engineer += ", " + engineers.getString(i);
if (engineer.length() > 2)
tag.setField(FieldKey.ENGINEER, engineer.substring(2));
}
//Mixer
if (contrib.has("mixer")) {
JSONArray mixers = contrib.getJSONArray("mixer");
String mixer = "";
for (int i = 0; i < mixers.length(); i++)
mixer += ", " + mixers.getString(i);
if (mixer.length() > 2)
tag.setField(FieldKey.MIXER, mixer.substring(2));
}
//Producer
if (contrib.has("producer")) {
JSONArray producers = contrib.getJSONArray("producer");
String producer = "";
for (int i = 0; i < producers.length(); i++)
producer += ", " + producers.getString(i);
if (producer.length() > 2)
tag.setField(FieldKey.MIXER, producer.substring(2));
}
//FLAC Only
if (isFlac) {
//Author
if (contrib.has("author")) {
JSONArray authors = contrib.getJSONArray("author");
String author = "";
for (int i=0; i<authors.length(); i++)
author += ", " + authors.getString(i);
if (author.length() > 2)
((FlacTag)tag).setField("AUTHOR", author.substring(2));
}
//Writer
if (contrib.has("writer")) {
JSONArray writers = contrib.getJSONArray("writer");
String writer = "";
for (int i=0; i<writers.length(); i++)
writer += ", " + writers.getString(i);
if (writer.length() > 2)
((FlacTag)tag).setField("WRITER", writer.substring(2));
//FLAC Only
if (isFlac) {
//Author
if (contrib.has("author")) {
JSONArray authors = contrib.getJSONArray("author");
String author = "";
for (int i = 0; i < authors.length(); i++)
author += ", " + authors.getString(i);
if (author.length() > 2)
((FlacTag) tag).setField("AUTHOR", author.substring(2));
}
//Writer
if (contrib.has("writer")) {
JSONArray writers = contrib.getJSONArray("writer");
String writer = "";
for (int i = 0; i < writers.length(); i++)
writer += ", " + writers.getString(i);
if (writer.length() > 2)
((FlacTag) tag).setField("WRITER", writer.substring(2));
}
}
}
} catch (Exception e) {
logger.warn("Error writing contributors data: " + e.toString());
}
File coverFile = new File(cover);
@ -423,7 +448,8 @@ public class Deezer {
}
} else {
if (addCover) {
Artwork art = Artwork.createArtworkFromFile(coverFile);
Artwork art = ArtworkFactory.createArtworkFromFile(coverFile);
//Artwork art = Artwork.createArtworkFromFile(coverFile);
tag.addField(art);
}
}

View File

@ -358,7 +358,7 @@ public class DownloadService extends Service {
return;
}
} catch (Exception e) {
logger.error("ISRC Fallback failed, track unavailable! " + e.toString());
logger.error("ISRC Fallback failed, track unavailable! " + e.toString(), download);
download.state = Download.DownloadState.DEEZER_ERROR;
exit();
return;
@ -572,7 +572,7 @@ public class DownloadService extends Service {
//Tag
try {
Deezer.tagTrack(outFile.getPath(), trackJson, albumJson, coverFile.getPath(), lyricsData, privateJson);
deezer.tagTrack(outFile.getPath(), trackJson, albumJson, coverFile.getPath(), lyricsData, privateJson);
} catch (Exception e) {
Log.e("ERR", "Tagging error!");
e.printStackTrace();
@ -600,7 +600,7 @@ public class DownloadService extends Service {
File coverFile = new File(parentDir, "cover.jpg");
if (coverFile.exists()) return;
//Don't download if doesn't have album
if (!download.path.matches(".*/%album%.*/.*")) return;
if (!download.path.matches(".*/.*%album%.*/.*")) return;
try {
//Create to lock

View File

@ -1,91 +1,67 @@
import 'dart:async';
import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:freezer/api/cache.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/settings.dart';
import 'package:http/http.dart' as http;
import 'dart:io';
import 'dart:convert';
import '../settings.dart';
import 'definitions.dart';
import 'dart:async';
DeezerAPI deezerAPI = DeezerAPI();
class DeezerAPI {
String arl;
DeezerAPI({this.arl});
String arl;
String token;
String userId;
String userName;
String favoritesPlaylistId;
String privateUrl = 'http://www.deezer.com/ajax/gw-light.php';
Map<String, String> headers = {
String sid;
Future _authorizing;
//Get headers
Map<String, String> get headers => {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Content-Language": '${settings.deezerLanguage??"en"}-${settings.deezerCountry??'US'}',
"Cache-Control": "max-age=0",
"Accept": "*/*",
"Accept-Charset": "utf-8,ISO-8859-1;q=0.7,*;q=0.3",
"Accept-Language": "${settings.deezerLanguage??"en"}-${settings.deezerCountry??'US'},${settings.deezerLanguage??"en"};q=0.9,en-US;q=0.8,en;q=0.7",
"Connection": "keep-alive"
"Connection": "keep-alive",
"Cookie": "arl=${arl}" + ((sid == null) ? '' : '; sid=${sid}')
};
Future _authorizing;
Dio dio = Dio();
CookieJar _cookieJar = new CookieJar();
//Call private api
//Call private API
Future<Map<dynamic, dynamic>> callApi(String method, {Map<dynamic, dynamic> params, String gatewayInput}) async {
//Add headers
dio.interceptors.add(InterceptorsWrapper(
onRequest: (RequestOptions options) {
options.headers = this.headers;
return options;
//Generate URL
Uri uri = Uri.https('www.deezer.com', '/ajax/gw-light.php', {
'api_version': '1.0',
'api_token': this.token,
'input': '3',
'method': method,
//Used for homepage
if (gatewayInput != null)
'gateway_input': gatewayInput
});
//Post
http.Response res = await http.post(uri, headers: headers, body: jsonEncode(params));
//Grab SID
if (method == 'deezer.getUserData') {
for (String cookieHeader in res.headers['set-cookie'].split(';')) {
if (cookieHeader.startsWith('sid=')) {
sid = cookieHeader.split('=')[1];
}
}
));
//Proxy
if (settings.proxyAddress != null && settings.proxyAddress != '' && settings.proxyAddress.length > 9) {
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client.findProxy = (uri) => "PROXY ${settings.proxyAddress}";
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
};
}
//Add cookies
List<Cookie> cookies = [Cookie('arl', this.arl)];
_cookieJar.saveFromResponse(Uri.parse(this.privateUrl), cookies);
dio.interceptors.add(CookieManager(_cookieJar));
//Make request
Response<dynamic> response = await dio.post(
this.privateUrl,
queryParameters: {
'api_version': '1.0',
'api_token': this.token,
'input': '3',
'method': method,
//Used for homepage
if (gatewayInput != null)
'gateway_input': gatewayInput
},
data: jsonEncode(params??{}),
options: Options(responseType: ResponseType.json, sendTimeout: 10000, receiveTimeout: 10000)
);
return response.data;
return jsonDecode(res.body);
}
Future<Map> callPublicApi(String path) async {
Dio dio = Dio();
Response response = await dio.get(
'https://api.deezer.com/' + path,
options: Options(responseType: ResponseType.json, sendTimeout: 10000, receiveTimeout: 10000)
);
return response.data;
Future<Map<dynamic, dynamic>> callPublicApi(String path) async {
http.Response res = await http.get('https://api.deezer.com/' + path);
return jsonDecode(res.body);
}
//Wrapper so it can be globally awaited
@ -128,11 +104,11 @@ class DeezerAPI {
}
//Share URL
if (uri.host == 'deezer.page.link' || uri.host == 'www.deezer.page.link') {
Dio dio = Dio();
Response res = await dio.head(url, options: RequestOptions(
followRedirects: true
));
return parseLink('http://deezer.com' + res.realUri.toString());
http.BaseRequest request = http.Request('HEAD', Uri.parse(url));
request.followRedirects = false;
http.StreamedResponse response = await request.send();
String newUrl = response.headers['location'];
return parseLink(newUrl);
}
}
@ -445,5 +421,14 @@ class DeezerAPI {
'songs': []
});
}
//Get shuffled library
Future<List<Track>> libraryShuffle({int start=0}) async {
Map data = await callApi('tracklist.getShuffledCollection', params: {
'nb': 50,
'start': start
});
return data['results']['data'].map<Track>((t) => Track.fromPrivateJson(t)).toList();
}
}

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:disk_space/disk_space.dart';
import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:fluttertoast/fluttertoast.dart';
@ -110,9 +111,63 @@ class DownloadManager {
return batch;
}
Future addOfflineTrack(Track track, {private = true}) async {
//Quality selector for custom quality
Future qualitySelect(BuildContext context) async {
AudioQuality quality;
await showModalBottomSheet(
context: context,
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.fromLTRB(0, 12, 0, 2),
child: Text(
'Quality'.i18n,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20.0
),
),
),
ListTile(
title: Text('MP3 128kbps'),
onTap: () {
quality = AudioQuality.MP3_128;
Navigator.of(context).pop();
},
),
ListTile(
title: Text('MP3 320kbps'),
onTap: () {
quality = AudioQuality.MP3_320;
Navigator.of(context).pop();
},
),
ListTile(
title: Text('FLAC'),
onTap: () {
quality = AudioQuality.FLAC;
Navigator.of(context).pop();
},
)
],
);
}
);
return quality;
}
Future<bool> addOfflineTrack(Track track, {private = true, BuildContext context}) async {
//Permission
if (!private && !(await checkPermission())) return;
if (!private && !(await checkPermission())) return false;
//Ask for quality
AudioQuality quality;
if (!private && settings.downloadQuality == AudioQuality.ASK) {
quality = await qualitySelect(context);
if (quality == null) return false;
}
//Add to DB
if (private) {
@ -127,14 +182,21 @@ class DownloadManager {
//Get path
String path = _generatePath(track, private);
await platform.invokeMethod('addDownloads', [await Download.jsonFromTrack(track, path, private: private)]);
await platform.invokeMethod('addDownloads', [await Download.jsonFromTrack(track, path, private: private, quality: quality)]);
await start();
}
Future addOfflineAlbum(Album album, {private = true}) async {
Future addOfflineAlbum(Album album, {private = true, BuildContext context}) async {
//Permission
if (!private && !(await checkPermission())) return;
//Ask for quality
AudioQuality quality;
if (!private && settings.downloadQuality == AudioQuality.ASK) {
quality = await qualitySelect(context);
if (quality == null) return false;
}
//Get from API if no tracks
if (album.tracks == null || album.tracks.length == 0) {
album = await deezerAPI.album(album.id);
@ -157,16 +219,22 @@ class DownloadManager {
//Create downloads
List<Map> out = [];
for (Track t in album.tracks) {
out.add(await Download.jsonFromTrack(t, _generatePath(t, private), private: private));
out.add(await Download.jsonFromTrack(t, _generatePath(t, private), private: private, quality: quality));
}
await platform.invokeMethod('addDownloads', out);
await start();
}
Future addOfflinePlaylist(Playlist playlist, {private = true}) async {
Future addOfflinePlaylist(Playlist playlist, {private = true, BuildContext context, AudioQuality quality}) async {
//Permission
if (!private && !(await checkPermission())) return;
//Ask for quality
if (!private && settings.downloadQuality == AudioQuality.ASK && quality == null) {
quality = await qualitySelect(context);
if (quality == null) return false;
}
//Get tracks if missing
if (playlist.tracks == null || playlist.tracks.length < playlist.trackCount) {
playlist = await deezerAPI.fullPlaylist(playlist.id);
@ -193,8 +261,8 @@ class DownloadManager {
t,
private,
playlistName: playlist.title,
playlistTrackNumber: i
), private: private));
playlistTrackNumber: i,
), private: private, quality: quality));
}
await platform.invokeMethod('addDownloads', out);
await start();
@ -375,7 +443,7 @@ class DownloadManager {
return true;
}
//Playlist
if (playlist != null) {
if (playlist != null && playlist.id != null) {
List res = await db.query('Playlists', where: 'id == ?', whereArgs: [playlist.id]);
if (res.length == 0) return false;
return true;
@ -553,7 +621,7 @@ class Download {
}
//Track to download JSON for service
static Future<Map> jsonFromTrack(Track t, String path, {private = true}) async {
static Future<Map> jsonFromTrack(Track t, String path, {private = true, AudioQuality quality}) async {
//Get download info
if (t.playbackDetails == null || t.playbackDetails == []) {
t = await deezerAPI.track(t.id);
@ -565,7 +633,7 @@ class Download {
"mediaVersion": t.playbackDetails[1],
"quality": private
? settings.getQualityInt(settings.offlineQuality)
: settings.getQualityInt(settings.downloadQuality),
: settings.getQualityInt((quality??settings.downloadQuality)),
"title": t.title,
"path": path,
"image": t.albumArt.thumb

View File

@ -104,6 +104,7 @@ class PlayerHelper {
androidNotificationChannelDescription: 'Freezer',
androidNotificationChannelName: 'Freezer',
androidNotificationIcon: 'drawable/ic_logo',
params: {'ignoreInterruptions': settings.ignoreInterruptions}
);
}
@ -138,14 +139,17 @@ class PlayerHelper {
await startService();
await settings.updateAudioServiceQuality();
await AudioService.updateQueue(queue);
await AudioService.skipToQueueItem(trackId);
if (queue[0].id != trackId)
await AudioService.skipToQueueItem(trackId);
if (!AudioService.playbackState.playing)
AudioService.play();
}
//Called when queue ends to load more tracks
Future onQueueEnd() async {
//Flow
if (queueSource == null) return;
print('test');
if (queueSource.id == 'flow') {
List<Track> tracks = await deezerAPI.flow();
List<MediaItem> mi = tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
@ -163,6 +167,15 @@ class PlayerHelper {
return;
}
//Library shuffle
if (queueSource.source == 'libraryshuffle') {
List<Track> tracks = await deezerAPI.libraryShuffle(start: AudioService.queue.length);
List<MediaItem> mi = tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
await AudioService.addQueueItems(mi);
AudioService.skipToNext();
return;
}
print(queueSource.toJson());
}
@ -245,7 +258,7 @@ void backgroundTaskEntrypoint() async {
}
class AudioPlayerTask extends BackgroundAudioTask {
AudioPlayer _player = AudioPlayer();
AudioPlayer _player;
//Queue
List<MediaItem> _queue = <MediaItem>[];
@ -274,6 +287,13 @@ class AudioPlayerTask extends BackgroundAudioTask {
final session = await AudioSession.instance;
session.configure(AudioSessionConfiguration.music());
if (params['ignoreInterruptions'] == true) {
_player = AudioPlayer(handleInterruptions: false);
session.interruptionEventStream.listen((_) {});
session.becomingNoisyEventStream.listen((_) {});
} else
_player = AudioPlayer();
//Update track index
_player.currentIndexStream.listen((index) {
if (index != null) {
@ -365,7 +385,6 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override
Future<void> onSkipToNext() async {
print('skipping');
if (_queueIndex == _queue.length-1) return;
//Update buffering state
_skipState = AudioProcessingState.skippingToNext;
@ -428,10 +447,10 @@ class AudioPlayerTask extends BackgroundAudioTask {
MediaControl.skipToNext,
//Stop
MediaControl(
androidIcon: 'drawable/ic_action_stop',
label: 'stop',
action: MediaAction.stop
)
androidIcon: 'drawable/ic_action_stop',
label: 'stop',
action: MediaAction.stop
),
],
systemActions: [
MediaAction.seekTo,

View File

@ -1,9 +1,11 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/download.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/settings.dart';
import 'package:html/parser.dart';
import 'package:html/dom.dart';
import 'package:html/dom.dart' as dom;
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:async';
@ -32,11 +34,10 @@ class SpotifyAPI {
//Extract JSON data form spotify embed page
Future<Map> getEmbedData(String url) async {
//Fetch
Dio dio = Dio();
Response response = await dio.get(url);
http.Response response = await http.get(url);
//Parse
Document document = parse(response.data);
Element element = document.getElementById('resource');
dom.Document document = parse(response.body);
dom.Element element = document.getElementById('resource');
return jsonDecode(element.innerHtml);
}
@ -50,7 +51,7 @@ class SpotifyAPI {
}
Future convertPlaylist(SpotifyPlaylist playlist, {bool downloadOnly = false}) async {
Future convertPlaylist(SpotifyPlaylist playlist, {bool downloadOnly = false, BuildContext context, AudioQuality quality}) async {
doneImporting = false;
importingSpotifyPlaylist = playlist;
@ -60,6 +61,7 @@ class SpotifyAPI {
playlistId = await deezerAPI.createPlaylist(playlist.name, description: playlist.description);
//Search for tracks
List<Track> downloadTracks = [];
for (SpotifyTrack track in playlist.tracks) {
Map deezer;
try {
@ -71,12 +73,21 @@ class SpotifyAPI {
if (!downloadOnly)
await deezerAPI.addToPlaylist(id, playlistId);
if (downloadOnly)
await downloadManager.addOfflineTrack(Track(id: id), private: false);
downloadTracks.add(Track(id: id));
track.state = TrackImportState.OK;
} catch (e) {
//On error
track.state = TrackImportState.ERROR;
}
//Download
if (downloadOnly)
await downloadManager.addOfflinePlaylist(
Playlist(trackCount: downloadTracks.length, tracks: downloadTracks, title: playlist.name),
private: false,
quality: quality
);
//Add playlist id to stream, stream is for updating ui only
importingStream.add(playlistId);
importingSpotifyPlaylist = playlist;

File diff suppressed because one or more lines are too long

View File

@ -248,6 +248,13 @@ const language_en_us = {
"Current timer ends at": "Current timer ends at",
//0.5.8 Strings:
"Smart track list": "Smart track list"
"Smart track list": "Smart track list",
//0.6.0 Strings:
"Shuffle": "Shuffle",
"Library shuffle": "Library shuffle",
"Ignore interruptions": "Ignore interruptions",
"Requires app restart to apply!": "Requires app restart to apply!",
"Ask before downloading": "Ask before downloading"
}
};

View File

@ -67,6 +67,7 @@ class _FreezerAppState extends State<FreezerApp> {
});
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: settings.themeData.bottomAppBarColor,
systemNavigationBarIconBrightness: (settings.theme == Themes.Light)?Brightness.dark:Brightness.light
));
}
@ -231,45 +232,45 @@ class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateM
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
PlayerBar(),
BottomNavigationBar(
backgroundColor: Theme.of(context).bottomAppBarColor,
currentIndex: _selected,
onTap: (int s) async {
//Pop all routes until home screen
while (navigatorKey.currentState.canPop()) {
await navigatorKey.currentState.maybePop();
}
bottomNavigationBar: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
PlayerBar(),
BottomNavigationBar(
backgroundColor: Theme.of(context).bottomAppBarColor,
currentIndex: _selected,
onTap: (int s) async {
//Pop all routes until home screen
while (navigatorKey.currentState.canPop()) {
await navigatorKey.currentState.maybePop();
setState(() {
_selected = s;
});
},
selectedItemColor: Theme.of(context).primaryColor,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text('Home'.i18n)),
BottomNavigationBarItem(
icon: Icon(Icons.search),
title: Text('Search'.i18n),
),
BottomNavigationBarItem(
icon: Icon(Icons.library_music), title: Text('Library'.i18n))
],
)
],
}
await navigatorKey.currentState.maybePop();
setState(() {
_selected = s;
});
},
selectedItemColor: Theme.of(context).primaryColor,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text('Home'.i18n)),
BottomNavigationBarItem(
icon: Icon(Icons.search),
title: Text('Search'.i18n),
),
BottomNavigationBarItem(
icon: Icon(Icons.library_music), title: Text('Library'.i18n))
],
)
],
),
body: AudioServiceWidget(
child: CustomNavigator(
navigatorKey: navigatorKey,
home: _screens[_selected],
pageRoute: PageRoutes.materialPageRoute,
),
body: AudioServiceWidget(
child: CustomNavigator(
navigatorKey: navigatorKey,
home: _screens[_selected],
pageRoute: PageRoutes.materialPageRoute,
),
));
));
}
}

View File

@ -24,6 +24,9 @@ class Settings {
@JsonKey(defaultValue: null)
String language;
@JsonKey(defaultValue: false)
bool ignoreInterruptions;
//Account
String arl;
@JsonKey(ignore: true)
@ -185,6 +188,7 @@ class Settings {
}
static const deezerBg = Color(0xFF1F1A16);
static const deezerBottom = Color(0xFF1b1714);
static const font = 'MabryPro';
Map<Themes, ThemeData> get _themeData => {
Themes.Light: ThemeData(
@ -193,7 +197,7 @@ class Settings {
accentColor: primaryColor,
sliderTheme: _sliderTheme,
toggleableActiveColor: primaryColor,
bottomAppBarColor: Color(0xfff7f7f7)
bottomAppBarColor: Color(0xfff5f5f5),
),
Themes.Dark: ThemeData(
fontFamily: font,
@ -212,10 +216,10 @@ class Settings {
toggleableActiveColor: primaryColor,
backgroundColor: deezerBg,
scaffoldBackgroundColor: deezerBg,
bottomAppBarColor: deezerBg,
dialogBackgroundColor: deezerBg,
bottomAppBarColor: deezerBottom,
dialogBackgroundColor: deezerBottom,
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: deezerBg
backgroundColor: deezerBottom
),
cardColor: deezerBg
),
@ -245,7 +249,8 @@ class Settings {
enum AudioQuality {
MP3_128,
MP3_320,
FLAC
FLAC,
ASK
}
enum Themes {

View File

@ -12,6 +12,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
arl: json['arl'] as String,
)
..language = json['language'] as String
..ignoreInterruptions = json['ignoreInterruptions'] as bool ?? false
..wifiQuality =
_$enumDecodeNullable(_$AudioQualityEnumMap, json['wifiQuality']) ??
AudioQuality.MP3_320
@ -49,6 +50,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
'language': instance.language,
'ignoreInterruptions': instance.ignoreInterruptions,
'arl': instance.arl,
'wifiQuality': _$AudioQualityEnumMap[instance.wifiQuality],
'mobileQuality': _$AudioQualityEnumMap[instance.mobileQuality],
@ -112,6 +114,7 @@ const _$AudioQualityEnumMap = {
AudioQuality.MP3_128: 'MP3_128',
AudioQuality.MP3_320: 'MP3_320',
AudioQuality.FLAC: 'FLAC',
AudioQuality.ASK: 'ASK',
};
const _$ThemesEnumMap = {

View File

@ -22,6 +22,7 @@ const supportedLocales = [
const Locale('fa', 'IR'),
const Locale('pl', 'PL'),
const Locale('uk', 'UA'),
const Locale('hu', 'HU'),
const Locale('fil', 'PH')
];

View File

@ -38,8 +38,9 @@ class CachedImage extends StatefulWidget {
final double height;
final bool circular;
final bool fullThumb;
final bool rounded;
const CachedImage({Key key, this.url, this.height, this.width, this.circular = false, this.fullThumb = false}): super(key: key);
const CachedImage({Key key, this.url, this.height, this.width, this.circular = false, this.fullThumb = false, this.rounded = false}): super(key: key);
@override
_CachedImageState createState() => _CachedImageState();
@ -49,8 +50,13 @@ class _CachedImageState extends State<CachedImage> {
@override
Widget build(BuildContext context) {
if (widget.rounded) return ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: CachedImage(url: widget.url, height: widget.height, width: widget.width, circular: false, rounded: false, fullThumb: widget.fullThumb),
);
if (widget.circular) return ClipOval(
child: CachedImage(url: widget.url, height: widget.height, width: widget.width, circular: false)
child: CachedImage(url: widget.url, height: widget.height, width: widget.width, circular: false, rounded: false, fullThumb: widget.fullThumb,)
);
if (!widget.url.startsWith('http'))

View File

@ -7,6 +7,7 @@ import 'package:freezer/api/cache.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/download.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/search.dart';
import 'package:freezer/translations.i18n.dart';
@ -53,14 +54,17 @@ class AlbumDetails extends StatelessWidget {
return ListView(
children: <Widget>[
//Album art, title, artists
Card(
Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(height: 8.0,),
CachedImage(
url: album.art.full,
width: MediaQuery.of(context).size.width / 2
width: MediaQuery.of(context).size.width / 2,
fullThumb: true,
rounded: true,
),
Container(height: 8,),
Text(
@ -97,8 +101,9 @@ class AlbumDetails extends StatelessWidget {
],
),
),
FreezerDivider(),
//Details
Card(
Container(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
@ -136,8 +141,9 @@ class AlbumDetails extends StatelessWidget {
],
),
),
FreezerDivider(),
//Options (offline, download...)
Card(
Container(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
@ -168,20 +174,28 @@ class AlbumDetails extends StatelessWidget {
Text('Download'.i18n)
],
),
onPressed: () {
downloadManager.addOfflineAlbum(album, private: false);
onPressed: () async {
if (await downloadManager.addOfflineAlbum(album, private: false, context: context) != false)
MenuSheet(context).showDownloadStartedToast();
},
)
],
),
),
FreezerDivider(),
...List.generate(cdCount, (cdi) {
List<Track> tracks = album.tracks.where((t) => (t.diskNumber??1) == cdi + 1).toList();
return Column(
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 4.0),
child: Text('Disk'.i18n + ' ${cdi + 1}'),
child: Text(
'Disk'.i18n.toUpperCase() + ' ${cdi + 1}',
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.w300
),
),
),
...List.generate(tracks.length, (i) => TrackTile(
tracks[i],
@ -237,6 +251,7 @@ class _MakeAlbumOfflineState extends State<MakeAlbumOffline> {
//Add to offline
await deezerAPI.addFavoriteAlbum(widget.album.id);
downloadManager.addOfflineAlbum(widget.album, private: true);
MenuSheet(context).showDownloadStartedToast();
setState(() {
_offline = true;
});
@ -283,13 +298,16 @@ class ArtistDetails extends StatelessWidget {
return ListView(
children: <Widget>[
Card(
Container(height: 4.0),
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
CachedImage(
url: artist.picture.full,
width: MediaQuery.of(context).size.width / 2 - 8,
rounded: true,
fullThumb: true,
),
Container(
width: MediaQuery.of(context).size.width / 2 - 8,
@ -347,8 +365,9 @@ class ArtistDetails extends StatelessWidget {
],
),
),
Container(height: 4.0,),
Card(
Container(height: 4.0),
FreezerDivider(),
Container(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
@ -391,14 +410,18 @@ class ArtistDetails extends StatelessWidget {
],
),
),
Container(height: 16.0,),
FreezerDivider(),
Container(height: 12.0,),
//Top tracks
Text(
'Top Tracks'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22.0
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 2.0),
child: Text(
'Top Tracks'.i18n,
textAlign: TextAlign.left,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20.0
),
),
),
Container(height: 4.0),
@ -432,14 +455,17 @@ class ArtistDetails extends StatelessWidget {
);
}
),
Divider(),
FreezerDivider(),
//Albums
Text(
'Top Albums'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22.0
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Text(
'Top Albums'.i18n,
textAlign: TextAlign.left,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20.0
),
),
),
...List.generate(artist.albums.length > 10 ? 11 : artist.albums.length + 1, (i) {
@ -582,8 +608,8 @@ class _DiscographyScreenState extends State<DiscographyScreen> {
});
return Scaffold(
appBar: AppBar(
title: Text('Discography'.i18n),
appBar: FreezerAppBar(
'Discography'.i18n,
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.album)),
@ -591,6 +617,7 @@ class _DiscographyScreenState extends State<DiscographyScreen> {
Tab(icon: Icon(Icons.recent_actors))
],
),
height: 100.0,
),
body: TabBarView(
children: [
@ -748,7 +775,8 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
controller: _scrollController,
children: <Widget>[
Container(height: 4.0,),
Card(
Padding(
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisSize: MainAxisSize.max,
@ -756,6 +784,8 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
CachedImage(
url: playlist.image.full,
height: MediaQuery.of(context).size.width / 2 - 8,
rounded: true,
fullThumb: true,
),
Container(
width: MediaQuery.of(context).size.width / 2 - 8,
@ -767,12 +797,13 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
playlist.title,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
maxLines: 2,
maxLines: 3,
style: TextStyle(
fontSize: 24.0,
fontSize: 20.0,
fontWeight: FontWeight.bold
),
),
Container(height: 4.0),
Text(
playlist.user.name,
overflow: TextOverflow.ellipsis,
@ -780,12 +811,10 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontSize: 18.0
fontSize: 17.0
),
),
Container(
height: 8.0,
),
Container(height: 10.0),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
@ -814,22 +843,25 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
],
),
),
Container(height: 4.0,),
Card(
child: Padding(
padding: EdgeInsets.all(4.0),
child: Text(
playlist.description ?? '',
maxLines: 4,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0
if (playlist.description != null && playlist.description.length > 0)
FreezerDivider(),
if (playlist.description != null && playlist.description.length > 0)
Container(
child: Padding(
padding: EdgeInsets.all(6.0),
child: Text(
playlist.description ?? '',
maxLines: 4,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0
),
),
),
)
),
Card(
)
),
FreezerDivider(),
Container(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
@ -848,12 +880,14 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
),
IconButton(
icon: Icon(Icons.file_download, size: 32.0,),
onPressed: () {
downloadManager.addOfflinePlaylist(playlist, private: false);
onPressed: () async {
if (await downloadManager.addOfflinePlaylist(playlist, private: false, context: context) != false)
MenuSheet(context).showDownloadStartedToast();
},
),
PopupMenuButton(
child: Icon(Icons.sort, size: 32.0),
color: Theme.of(context).scaffoldBackgroundColor,
onSelected: (SortType s) async {
if (playlist.tracks.length < playlist.trackCount) {
//Preload whole playlist
@ -868,19 +902,19 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
PopupMenuItem(
value: SortType.DEFAULT,
child: Text('Default'.i18n),
child: Text('Default'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: SortType.REVERSE,
child: Text('Reverse'.i18n),
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: SortType.ALPHABETIC,
child: Text('Alphabetic'.i18n),
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: SortType.ARTIST,
child: Text('Artist'.i18n),
child: Text('Artist'.i18n, style: popupMenuTextStyle()),
),
],
),
@ -888,6 +922,7 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
],
),
),
FreezerDivider(),
...List.generate(playlist.tracks.length, (i) {
Track t = sorted[i];
return TrackTile(
@ -909,11 +944,14 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
);
}),
if (_loading)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator()
],
Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator()
],
),
),
if (_error)
ErrorScreen()
@ -955,6 +993,7 @@ class _MakePlaylistOfflineState extends State<MakePlaylistOffline> {
if (widget.playlist.user != null && widget.playlist.user.id != deezerAPI.userId)
await deezerAPI.addPlaylist(widget.playlist.id);
downloadManager.addOfflinePlaylist(widget.playlist, private: true);
MenuSheet(context).showDownloadStartedToast();
setState(() {
_offline = true;
});

View File

@ -4,6 +4,7 @@ import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart';
import 'package:freezer/api/download.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:freezer/ui/elements.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'cached_image.dart';
@ -69,8 +70,8 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Downloads'.i18n),
appBar: FreezerAppBar(
'Downloads'.i18n,
actions: [
IconButton(
icon: Icon(Icons.delete_sweep),
@ -348,9 +349,7 @@ class _DownloadLogViewerState extends State<DownloadLogViewer> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Download Log'.i18n),
),
appBar: FreezerAppBar('Download Log'.i18n),
body: ListView.builder(
itemCount: data.length,
itemBuilder: (context, i) {

83
lib/ui/elements.dart Normal file
View File

@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:freezer/settings.dart';
class LeadingIcon extends StatelessWidget {
final IconData icon;
final Color color;
LeadingIcon(this.icon, {this.color});
@override
Widget build(BuildContext context) {
return Container(
width: 42.0,
height: 42.0,
decoration: BoxDecoration(
color: (color??Theme.of(context).primaryColor).withOpacity(1.0),
shape: BoxShape.circle
),
child: Icon(
icon,
color: Colors.white,
),
);
}
}
//Container with set size to match LeadingIcon
class EmptyLeading extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(width: 42.0, height: 42.0);
}
}
class FreezerAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final List<Widget> actions;
final Widget bottom;
//Should be specified if bottom is specified
final double height;
FreezerAppBar(this.title, {this.actions = const [], this.bottom, this.height = 56.0});
Size get preferredSize => Size.fromHeight(this.height);
@override
Widget build(BuildContext context) {
return Theme(
data: ThemeData(primaryColor: (Theme.of(context).brightness == Brightness.light)?Colors.white:Colors.black),
child: AppBar(
elevation: 0.0,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
title: Text(
title,
style: TextStyle(
fontWeight: FontWeight.w900,
),
),
actions: actions,
bottom: bottom,
),
);
}
}
class FreezerDivider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Divider(
thickness: 1.5,
indent: 16.0,
endIndent: 16.0,
);
}
}
TextStyle popupMenuTextStyle() {
return TextStyle(
color: (settings.theme == Themes.Light)?Colors.black:Colors.white
);
}

View File

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/main.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/menu.dart';
import 'package:freezer/translations.i18n.dart';
@ -16,9 +18,7 @@ class HomeScreen extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SafeArea(
child: FreezerTitle(),
),
SafeArea(child: Container()),
Flexible(child: HomePageScreen(),)
],
),
@ -161,10 +161,10 @@ class _HomePageScreenState extends State<HomePageScreen> {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold
fontWeight: FontWeight.w900
),
),
padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0)
padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0)
),
SingleChildScrollView(
@ -184,9 +184,7 @@ class _HomePageScreenState extends State<HomePageScreen> {
),
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(
title: Text(section.title),
),
appBar: FreezerAppBar(section.title),
body: SingleChildScrollView(
child: HomePageScreen(
channel: DeezerChannel(target: section.pagePath)
@ -205,6 +203,7 @@ class _HomePageScreenState extends State<HomePageScreen> {
}),
),
),
Container(height: 8.0),
],
);
},

View File

@ -2,7 +2,11 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
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';
import 'package:freezer/translations.i18n.dart';
@ -49,9 +53,7 @@ class _ImporterScreenState extends State<ImporterScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Importer'.i18n),
),
appBar: FreezerAppBar('Importer'.i18n),
body: ListView(
children: <Widget>[
ListTile(
@ -62,7 +64,7 @@ class _ImporterScreenState extends State<ImporterScreen> {
color: Colors.deepOrangeAccent,
),
),
Divider(),
FreezerDivider(),
Container(height: 16.0,),
Text(
'Enter your playlist link below'.i18n,
@ -130,7 +132,7 @@ class _ImporterWidgetState extends State<ImporterWidget> {
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Divider(),
FreezerDivider(),
ListTile(
title: Text(widget.playlist.name),
subtitle: Text(widget.playlist.description),
@ -153,8 +155,15 @@ class _ImporterWidgetState extends State<ImporterWidget> {
RaisedButton(
child: Text('Download only'.i18n),
color: Theme.of(context).primaryColor,
onPressed: () {
spotify.convertPlaylist(widget.playlist, downloadOnly: true);
onPressed: () async {
//Ask for quality
AudioQuality quality;
if (settings.downloadQuality == AudioQuality.ASK) {
quality = await downloadManager.qualitySelect(context);
if (quality == null) return;
}
spotify.convertPlaylist(widget.playlist, downloadOnly: true, context: context, quality: quality);
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => CurrentlyImportingScreen()
));
@ -199,7 +208,7 @@ class CurrentlyImportingScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Importing...'.i18n),),
appBar: FreezerAppBar('Importing...'.i18n),
body: StreamBuilder(
stream: spotify.importingStream.stream,
builder: (context, snapshot) {
@ -225,7 +234,7 @@ class CurrentlyImportingScreen extends StatelessWidget {
],
),
),
Card(
Container(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
@ -267,6 +276,8 @@ class CurrentlyImportingScreen extends StatelessWidget {
],
),
),
Container(height: 8.0),
FreezerDivider(),
...List.generate(spotify.importingSpotifyPlaylist.tracks.length, (i) {
SpotifyTrack t = spotify.importingSpotifyPlaylist.tracks[i];
return ListTile(

View File

@ -8,6 +8,7 @@ import 'package:freezer/api/player.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/ui/details_screens.dart';
import 'package:freezer/ui/downloads_screen.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/importer_screen.dart';
import 'package:freezer/ui/tiles.dart';
@ -25,8 +26,8 @@ class LibraryAppBar extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
return AppBar(
title: Text('Library'.i18n),
return FreezerAppBar(
'Library'.i18n,
actions: <Widget>[
IconButton(
icon: Icon(Icons.file_download),
@ -61,7 +62,7 @@ class LibraryScreen extends StatelessWidget {
if (!downloadManager.running && downloadManager.queueSize > 0)
ListTile(
title: Text('Downloads'.i18n),
leading: Icon(Icons.file_download),
leading: LeadingIcon(Icons.file_download, color: Colors.grey),
subtitle: Text('Downloading is currently stopped, click here to resume.'.i18n),
onTap: () {
downloadManager.start();
@ -70,13 +71,22 @@ class LibraryScreen extends StatelessWidget {
));
},
),
//Dirty if to not use columns
if (!downloadManager.running && downloadManager.queueSize > 0)
Divider(),
ListTile(
title: Text('Shuffle'.i18n),
leading: LeadingIcon(Icons.shuffle, color: Color(0xffeca704)),
onTap: () async {
List<Track> tracks = await deezerAPI.libraryShuffle();
playerHelper.playFromTrackList(tracks, tracks[0].id, QueueSource(
id: 'libraryshuffle',
source: 'libraryshuffle',
text: 'Library shuffle'.i18n
));
},
),
FreezerDivider(),
ListTile(
title: Text('Tracks'.i18n),
leading: Icon(Icons.audiotrack),
leading: LeadingIcon(Icons.audiotrack, color: Color(0xffbe3266)),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => LibraryTracks())
@ -85,7 +95,7 @@ class LibraryScreen extends StatelessWidget {
),
ListTile(
title: Text('Albums'.i18n),
leading: Icon(Icons.album),
leading: LeadingIcon(Icons.album, color: Color(0xff4b2e7e)),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => LibraryAlbums())
@ -94,7 +104,7 @@ class LibraryScreen extends StatelessWidget {
),
ListTile(
title: Text('Artists'.i18n),
leading: Icon(Icons.recent_actors),
leading: LeadingIcon(Icons.recent_actors, color: Color(0xff384697)),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => LibraryArtists())
@ -103,26 +113,27 @@ class LibraryScreen extends StatelessWidget {
),
ListTile(
title: Text('Playlists'.i18n),
leading: Icon(Icons.playlist_play),
leading: LeadingIcon(Icons.playlist_play, color: Color(0xff0880b5)),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => LibraryPlaylists())
);
},
),
FreezerDivider(),
ListTile(
title: Text('History'.i18n),
leading: Icon(Icons.history),
leading: LeadingIcon(Icons.history, color: Color(0xff009a85)),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => HistoryScreen())
);
},
),
Divider(),
FreezerDivider(),
ListTile(
title: Text('Import'.i18n),
leading: Icon(Icons.import_export),
leading: LeadingIcon(Icons.import_export, color: Color(0xff2ba766)),
subtitle: Text('Import playlists from Spotify'.i18n),
onTap: () {
if (spotify.doneImporting != null) {
@ -140,7 +151,7 @@ class LibraryScreen extends StatelessWidget {
),
ExpansionTile(
title: Text('Statistics'.i18n),
leading: Icon(Icons.insert_chart),
leading: LeadingIcon(Icons.insert_chart, color: Colors.grey),
children: <Widget>[
FutureBuilder(
future: downloadManager.getStats(),
@ -350,11 +361,12 @@ class _LibraryTracksState extends State<LibraryTracks> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Tracks'.i18n),
appBar: FreezerAppBar(
'Tracks'.i18n,
actions: [
PopupMenuButton(
child: Icon(Icons.sort, size: 32.0),
color: Theme.of(context).scaffoldBackgroundColor,
onSelected: (SortType s) async {
//Preload for sorting
if (tracks.length < (trackCount??0))
@ -367,19 +379,19 @@ class _LibraryTracksState extends State<LibraryTracks> {
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
PopupMenuItem(
value: SortType.DEFAULT,
child: Text('Default'.i18n),
child: Text('Default'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: SortType.REVERSE,
child: Text('Reverse'.i18n),
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: SortType.ALPHABETIC,
child: Text('Alphabetic'.i18n),
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: SortType.ARTIST,
child: Text('Artist'.i18n),
child: Text('Artist'.i18n, style: popupMenuTextStyle()),
),
],
),
@ -389,40 +401,29 @@ class _LibraryTracksState extends State<LibraryTracks> {
body: ListView(
controller: _scrollController,
children: <Widget>[
Card(
child: Column(
mainAxisSize: MainAxisSize.min,
Container(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Container(height: 8.0,),
Text(
'Loved tracks'.i18n,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 24
MakePlaylistOffline(_playlist),
FlatButton(
child: Row(
children: <Widget>[
Icon(Icons.file_download, size: 32.0,),
Container(width: 4,),
Text('Download'.i18n)
],
),
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
MakePlaylistOffline(_playlist),
FlatButton(
child: Row(
children: <Widget>[
Icon(Icons.file_download, size: 32.0,),
Container(width: 4,),
Text('Download'.i18n)
],
),
onPressed: () {
downloadManager.addOfflinePlaylist(_playlist, private: false);
},
)
],
onPressed: () async {
if (await downloadManager.addOfflinePlaylist(_playlist, private: false, context: context) != false)
MenuSheet(context).showDownloadStartedToast();
},
)
],
),
)
),
FreezerDivider(),
//Loved tracks
...List.generate(tracks.length, (i) {
Track t = (tracks.length == (trackCount??0))?_sorted[i]:tracks[i];
@ -458,7 +459,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
)
],
),
Divider(),
FreezerDivider(),
Text(
'All offline tracks'.i18n,
textAlign: TextAlign.center,
@ -545,10 +546,11 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Albums'.i18n),
appBar: FreezerAppBar(
'Albums'.i18n,
actions: [
PopupMenuButton(
color: Theme.of(context).scaffoldBackgroundColor,
child: Icon(Icons.sort, size: 32.0),
onSelected: (AlbumSortType s) async {
setState(() => _sort = s);
@ -558,19 +560,19 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
itemBuilder: (context) => <PopupMenuEntry<AlbumSortType>>[
PopupMenuItem(
value: AlbumSortType.DEFAULT,
child: Text('Default'.i18n),
child: Text('Default'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: AlbumSortType.REVERSE,
child: Text('Reverse'.i18n),
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: AlbumSortType.ALPHABETIC,
child: Text('Alphabetic'.i18n),
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: AlbumSortType.ARTIST,
child: Text('Artist'.i18n),
child: Text('Artist'.i18n, style: popupMenuTextStyle()),
),
],
),
@ -615,7 +617,7 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
List<Album> albums = snapshot.data;
return Column(
children: <Widget>[
Divider(),
FreezerDivider(),
Text(
'Offline albums'.i18n,
textAlign: TextAlign.center,
@ -719,11 +721,12 @@ class _LibraryArtistsState extends State<LibraryArtists> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Artists'.i18n),
appBar: FreezerAppBar(
'Artists'.i18n,
actions: [
PopupMenuButton(
child: Icon(Icons.sort, size: 32.0),
color: Theme.of(context).scaffoldBackgroundColor,
onSelected: (ArtistSortType s) async {
setState(() => _sort = s);
cache.artistSort = s;
@ -732,19 +735,19 @@ class _LibraryArtistsState extends State<LibraryArtists> {
itemBuilder: (context) => <PopupMenuEntry<ArtistSortType>>[
PopupMenuItem(
value: ArtistSortType.DEFAULT,
child: Text('Default'.i18n),
child: Text('Default'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: ArtistSortType.REVERSE,
child: Text('Reverse'.i18n),
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: ArtistSortType.ALPHABETIC,
child: Text('Alphabetic'.i18n),
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: ArtistSortType.POPULARITY,
child: Text('Popularity'.i18n),
child: Text('Popularity'.i18n, style: popupMenuTextStyle()),
),
],
),
@ -859,11 +862,12 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Playlists'.i18n),
appBar: FreezerAppBar(
'Playlists'.i18n,
actions: [
PopupMenuButton(
child: Icon(Icons.sort, size: 32.0),
color: Theme.of(context).scaffoldBackgroundColor,
onSelected: (PlaylistSortType s) async {
setState(() => _sort = s);
cache.libraryPlaylistSort = s;
@ -872,23 +876,23 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
itemBuilder: (context) => <PopupMenuEntry<PlaylistSortType>>[
PopupMenuItem(
value: PlaylistSortType.DEFAULT,
child: Text('Default'.i18n),
child: Text('Default'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: PlaylistSortType.REVERSE,
child: Text('Reverse'.i18n),
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: PlaylistSortType.USER,
child: Text('User'.i18n),
child: Text('User'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: PlaylistSortType.TRACK_COUNT,
child: Text('Track count'.i18n),
child: Text('Track count'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: PlaylistSortType.ALPHABETIC,
child: Text('Alphabetic'.i18n),
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
),
],
),
@ -899,7 +903,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
children: <Widget>[
ListTile(
title: Text('Create new playlist'.i18n),
leading: Icon(Icons.playlist_add),
leading: LeadingIcon(Icons.playlist_add, color: Color(0xff009a85)),
onTap: () async {
if (settings.offlineMode) {
Fluttertoast.showToast(
@ -913,7 +917,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
await _load();
},
),
Divider(),
FreezerDivider(),
if (!settings.offlineMode && _playlists == null)
Row(
@ -965,7 +969,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
List<Playlist> playlists = snapshot.data;
return Column(
children: <Widget>[
Divider(),
FreezerDivider(),
Text(
'Offline playlists'.i18n,
textAlign: TextAlign.center,
@ -1012,8 +1016,8 @@ class _HistoryScreenState extends State<HistoryScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('History'.i18n),
appBar: FreezerAppBar(
'History'.i18n,
actions: [
IconButton(
icon: Icon(Icons.delete_sweep),

View File

@ -188,9 +188,9 @@ class MenuSheet {
title: Text('Download'.i18n),
leading: Icon(Icons.file_download),
onTap: () async {
await downloadManager.addOfflineTrack(t, private: false);
if (await downloadManager.addOfflineTrack(t, private: false, context: context) != false)
showDownloadStartedToast();
_close();
showDownloadStartedToast();
},
);
@ -314,8 +314,8 @@ class MenuSheet {
leading: Icon(Icons.file_download),
onTap: () async {
_close();
await downloadManager.addOfflineAlbum(a, private: false);
showDownloadStartedToast();
if (await downloadManager.addOfflineAlbum(a, private: false, context: context) != false)
showDownloadStartedToast();
}
);
@ -471,9 +471,9 @@ class MenuSheet {
title: Text('Download playlist'.i18n),
leading: Icon(Icons.file_download),
onTap: () async {
downloadManager.addOfflinePlaylist(p, private: false);
_close();
showDownloadStartedToast();
if (await downloadManager.addOfflinePlaylist(p, private: false, context: context) != false)
showDownloadStartedToast();
},
);

View File

@ -15,7 +15,7 @@ class PlayerBar extends StatelessWidget {
return AudioService.playbackState.currentPosition.inSeconds / AudioService.currentMediaItem.duration.inSeconds;
}
double iconSize = 32;
double iconSize = 28;
bool _gestureRegistered = false;
@override
@ -40,42 +40,48 @@ class PlayerBar extends StatelessWidget {
child: StreamBuilder(
stream: Stream.periodic(Duration(milliseconds: 250)),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (AudioService.currentMediaItem == null) return Container(width: 0, height: 0,);
if (AudioService.currentMediaItem == null)
return Container(width: 0, height: 0,);
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
color: Theme.of(context).bottomAppBarColor,
child: ListTile(
dense: true,
contentPadding: EdgeInsets.symmetric(horizontal: 8.0),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) => PlayerScreen()));
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: settings.themeData.scaffoldBackgroundColor,
));
},
leading: CachedImage(
width: 50,
height: 50,
url: AudioService.currentMediaItem.extras['thumb'] ?? AudioService.currentMediaItem.artUri,
),
title: Text(
AudioService.currentMediaItem.displayTitle,
overflow: TextOverflow.clip,
maxLines: 1,
),
subtitle: Text(
AudioService.currentMediaItem.displaySubtitle,
overflow: TextOverflow.clip,
maxLines: 1,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
PrevNextButton(iconSize, prev: true, hidePrev: true,),
PlayPauseButton(iconSize),
PrevNextButton(iconSize)
],
)
Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) => PlayerScreen()));
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: settings.themeData
.scaffoldBackgroundColor,
));
},
leading: CachedImage(
width: 50,
height: 50,
url: AudioService.currentMediaItem.extras['thumb'] ??
AudioService.currentMediaItem.artUri,
),
title: Text(
AudioService.currentMediaItem.displayTitle,
overflow: TextOverflow.clip,
maxLines: 1,
),
subtitle: Text(
AudioService.currentMediaItem.displaySubtitle,
overflow: TextOverflow.clip,
maxLines: 1,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
PrevNextButton(iconSize, prev: true, hidePrev: true,),
PlayPauseButton(iconSize),
PrevNextButton(iconSize)
],
)
),
),
Container(
@ -87,7 +93,7 @@ class PlayerBar extends StatelessWidget {
)
],
);
},
}
),
);
}

View File

@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
import 'package:audio_service/audio_service.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/screenutil.dart';
import 'package:freezer/api/cache.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/menu.dart';
import 'package:freezer/ui/settings_screen.dart';
import 'package:freezer/ui/tiles.dart';
@ -78,7 +80,6 @@ class PlayerScreenHorizontal extends StatefulWidget {
class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
double iconSize = ScreenUtil().setWidth(64);
bool _lyrics = false;
@override
@ -115,9 +116,9 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
padding: EdgeInsets.fromLTRB(8, 16, 8, 0),
child: Container(
child: PlayerScreenTopRow(
textSize: ScreenUtil().setSp(26),
iconSize: ScreenUtil().setSp(32),
textWidth: ScreenUtil().setWidth(256),
textSize: ScreenUtil().setSp(24),
iconSize: ScreenUtil().setSp(36),
textWidth: ScreenUtil().setWidth(350),
short: true
),
)
@ -166,17 +167,7 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: SeekBar(),
),
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
PrevNextButton(iconSize, prev: true,),
PlayPauseButton(iconSize),
PrevNextButton(iconSize)
],
),
),
PlaybackControls(ScreenUtil().setSp(60)),
Padding(
padding: EdgeInsets.fromLTRB(8, 0, 8, 16),
child: Container(
@ -230,7 +221,6 @@ class PlayerScreenVertical extends StatefulWidget {
}
class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
double iconSize = ScreenUtil().setWidth(100);
bool _lyrics = false;
@override
@ -240,26 +230,27 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(28, 10, 28, 0),
child: PlayerScreenTopRow()
padding: EdgeInsets.fromLTRB(30, 4, 16, 0),
child: PlayerScreenTopRow()
),
Padding(
padding: EdgeInsets.fromLTRB(16, 0, 16, 0),
child: Container(
height: ScreenUtil().setHeight(1050),
child: Stack(
children: <Widget>[
BigAlbumArt(),
if (_lyrics) LyricsWidget(
artUri: AudioService.currentMediaItem.extras['thumb'],
trackId: AudioService.currentMediaItem.id,
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
height: ScreenUtil().setHeight(1050),
),
],
),
padding: EdgeInsets.fromLTRB(16, 0, 16, 0),
child: Container(
height: ScreenUtil().setHeight(1000),
child: Stack(
children: <Widget>[
BigAlbumArt(),
if (_lyrics) LyricsWidget(
artUri: AudioService.currentMediaItem.extras['thumb'],
trackId: AudioService.currentMediaItem.id,
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
height: ScreenUtil().setHeight(1000),
),
],
),
),
),
Container(height: 4.0),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
@ -301,16 +292,7 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
],
),
SeekBar(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
PrevNextButton(iconSize, prev: true,),
PlayPauseButton(iconSize),
PrevNextButton(iconSize)
],
),
//Container(height: 8.0,),
PlaybackControls(ScreenUtil().setWidth(100)),
Padding(
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 16.0),
child: Row(
@ -351,6 +333,90 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
}
}
class PlaybackControls extends StatefulWidget {
final double iconSize;
PlaybackControls(this.iconSize, {Key key}): super(key: key);
@override
_PlaybackControlsState createState() => _PlaybackControlsState();
}
class _PlaybackControlsState extends State<PlaybackControls> {
Icon get repeatIcon {
switch (playerHelper.repeatType) {
case LoopMode.off:
return Icon(
Icons.repeat,
size: widget.iconSize * 0.64
);
case LoopMode.all:
return Icon(
Icons.repeat,
color: Theme.of(context).primaryColor,
size: widget.iconSize * 0.64
);
case LoopMode.one:
return Icon(
Icons.repeat_one,
color: Theme.of(context).primaryColor,
size: widget.iconSize * 0.64,
);
}
}
Icon get libraryIcon {
if (cache.checkTrackFavorite(Track.fromMediaItem(AudioService.currentMediaItem))) {
return Icon(Icons.favorite, size: widget.iconSize * 0.64);
}
return Icon(Icons.favorite_border, size: widget.iconSize * 0.64);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
IconButton(
icon: repeatIcon,
onPressed: () async {
await playerHelper.changeRepeat();
setState(() {});
},
),
PrevNextButton(widget.iconSize, prev: true),
PlayPauseButton(widget.iconSize * 1.25),
PrevNextButton(widget.iconSize),
IconButton(
icon: libraryIcon,
onPressed: () async {
if (cache.libraryTracks == null)
cache.libraryTracks = [];
if (cache.checkTrackFavorite(Track.fromMediaItem(AudioService.currentMediaItem))) {
//Remove from library
setState(() => cache.libraryTracks.remove(AudioService.currentMediaItem.id));
await deezerAPI.removeFavorite(AudioService.currentMediaItem.id);
await cache.save();
} else {
//Add
setState(() => cache.libraryTracks.add(AudioService.currentMediaItem.id));
await deezerAPI.addFavoriteTrack(AudioService.currentMediaItem.id);
await cache.save();
}
},
)
],
),
);
}
}
class BigAlbumArt extends StatefulWidget {
@override
_BigAlbumArtState createState() => _BigAlbumArtState();
@ -383,16 +449,23 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
@override
Widget build(BuildContext context) {
return PageView(
controller: _pageController,
onPageChanged: (int index) {
if (_animationLock) return;
AudioService.skipToQueueItem(AudioService.queue[index].id);
return GestureDetector(
onVerticalDragUpdate: (DragUpdateDetails details) {
if (details.delta.dy > 16) {
Navigator.of(context).pop();
}
},
children: List.generate(AudioService.queue.length, (i) => CachedImage(
url: AudioService.queue[i].artUri,
fullThumb: true,
)),
child: PageView(
controller: _pageController,
onPageChanged: (int index) {
if (_animationLock) return;
AudioService.skipToQueueItem(AudioService.queue[index].id);
},
children: List.generate(AudioService.queue.length, (i) => CachedImage(
url: AudioService.queue[i].artUri,
fullThumb: true,
)),
),
);
}
}
@ -569,51 +642,28 @@ class PlayerScreenTopRow extends StatelessWidget {
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Row(
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(0, 0, 8, 0),
child: InkWell(
child: Container(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.keyboard_arrow_down, size: this.iconSize??ScreenUtil().setWidth(46)),
),
onTap: () {
Navigator.of(context).pop();
},
),
),
Container(
width: this.textWidth??ScreenUtil().setWidth(550),
child: Text(
(short??false)?(playerHelper.queueSource.text??''):'Playing from:'.i18n + ' ' + (playerHelper.queueSource.text??''),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.left,
style: TextStyle(fontSize: this.textSize??ScreenUtil().setSp(34)),
),
)
],
Container(
width: this.textWidth??ScreenUtil().setWidth(800),
child: Text(
(short??false)?(playerHelper.queueSource.text??''):'Playing from:'.i18n + ' ' + (playerHelper.queueSource?.text??''),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.left,
style: TextStyle(fontSize: this.textSize??ScreenUtil().setSp(38)),
),
),
IconButton(
icon: Icon(Icons.menu),
iconSize: this.iconSize??ScreenUtil().setSp(52),
splashRadius: this.iconSize??ScreenUtil().setWidth(52),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => QueueScreen()
));
},
),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
RepeatButton(size: this.iconSize),
Container(width: 16.0,),
InkWell(
child: Container(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.menu, size: this.iconSize??ScreenUtil().setWidth(46)),
),
onTap: (){
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => QueueScreen()
));
},
),
],
)
],
);
}
@ -621,60 +671,6 @@ class PlayerScreenTopRow extends StatelessWidget {
class RepeatButton extends StatefulWidget {
double size;
RepeatButton({this.size, Key key}): super(key: key);
@override
_RepeatButtonState createState() => _RepeatButtonState();
}
class _RepeatButtonState extends State<RepeatButton> {
double _size = ScreenUtil().setWidth(46);
Icon get icon {
switch (playerHelper.repeatType) {
case LoopMode.off:
return Icon(Icons.repeat, size: widget.size??_size);
case LoopMode.all:
return Icon(
Icons.repeat,
color: Theme.of(context).primaryColor,
size: widget.size??_size
);
case LoopMode.one:
return Icon(
Icons.repeat_one,
color: Theme.of(context).primaryColor,
size: widget.size??_size
);
}
}
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () async {
await playerHelper.changeRepeat();
setState(() {});
},
child: Container(
padding: EdgeInsets.all(8.0),
child: icon,
),
);
}
}
class SeekBar extends StatefulWidget {
@override
_SeekBarState createState() => _SeekBarState();
@ -789,8 +785,8 @@ class _QueueScreenState extends State<QueueScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Queue'.i18n),
appBar: FreezerAppBar(
'Queue'.i18n,
actions: <Widget>[
IconButton(
icon: Icon(

View File

@ -4,6 +4,7 @@ import 'package:freezer/api/cache.dart';
import 'package:freezer/api/download.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/ui/details_screens.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/menu.dart';
import 'package:freezer/translations.i18n.dart';
@ -50,6 +51,7 @@ class _SearchScreenState extends State<SearchScreen> {
bool _loading = false;
TextEditingController _controller = new TextEditingController();
List _suggestions = [];
bool _cancel = false;
void _submit(BuildContext context, {String query}) async {
if (query != null) _query = query;
@ -78,7 +80,7 @@ class _SearchScreenState extends State<SearchScreen> {
@override
void initState() {
print(cache.searchHistory);
_cancel = true;
//Check for connectivity and enable offline mode
Connectivity().checkConnectivity().then((res) {
if (res == ConnectivityResult.none) setState(() {
@ -102,24 +104,30 @@ class _SearchScreenState extends State<SearchScreen> {
sugg = await deezerAPI.searchSuggestions(_query);
} catch (e) {}
if (sugg != null)
if (sugg != null && !_cancel)
setState(() => _suggestions = sugg);
}
@override
void dispose() {
_cancel = true;
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Search'.i18n),),
appBar: FreezerAppBar('Search'.i18n),
body: ListView(
children: <Widget>[
Container(height: 16.0),
Container(height: 4.0),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: <Widget>[
Expanded(
child: Stack(
alignment: Alignment(1.0, 1.0),
alignment: Alignment(1.0, 0.0),
children: [
TextField(
onChanged: (String s) {
@ -127,37 +135,49 @@ class _SearchScreenState extends State<SearchScreen> {
_loadSuggestions();
},
decoration: InputDecoration(
labelText: 'Search or paste URL'.i18n
labelText: 'Search or paste URL'.i18n,
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey)
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey)
),
),
controller: _controller,
onSubmitted: (String s) => _submit(context, query: s),
),
IconButton(
icon: Icon(Icons.clear),
onPressed: () {
setState(() {
_suggestions = [];
_query = '';
});
_controller.clear();
},
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40.0,
child: IconButton(
splashRadius: 20.0,
icon: Icon(Icons.clear),
onPressed: () {
setState(() {
_suggestions = [];
_query = '';
});
_controller.clear();
},
),
),
],
)
],
)
),
Padding(
padding: EdgeInsets.fromLTRB(0, 8, 0, 0),
child: IconButton(
icon: Icon(Icons.search),
onPressed: () => _submit(context),
),
)
],
),
),
Container(height: 8.0),
ListTile(
title: Text('Offline search'.i18n),
leading: Switch(
leading: Icon(Icons.offline_pin),
trailing: Switch(
value: _offline,
onChanged: (v) {
setState(() => _offline = !_offline);
@ -166,7 +186,7 @@ class _SearchScreenState extends State<SearchScreen> {
),
if (_loading)
LinearProgressIndicator(),
Divider(),
FreezerDivider(),
//History
if (cache.searchHistory != null && cache.searchHistory.length > 0 && (_query??'').length == 0)
@ -213,9 +233,7 @@ class SearchResultsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Search Results'.i18n),
),
appBar: FreezerAppBar('Search Results'.i18n),
body: FutureBuilder(
future: _search(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
@ -243,12 +261,15 @@ class SearchResultsScreen extends StatelessWidget {
List<Widget> tracks = [];
if (results.tracks != null && results.tracks.length != 0) {
tracks = [
Text(
'Tracks'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26.0,
fontWeight: FontWeight.bold
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
child: Text(
'Tracks'.i18n,
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold
),
),
),
...List.generate(3, (i) {
@ -280,7 +301,8 @@ class SearchResultsScreen extends StatelessWidget {
)))
);
},
)
),
FreezerDivider()
];
}
@ -288,12 +310,15 @@ class SearchResultsScreen extends StatelessWidget {
List<Widget> albums = [];
if (results.albums != null && results.albums.length != 0) {
albums = [
Text(
'Albums'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26.0,
fontWeight: FontWeight.bold
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
child: Text(
'Albums'.i18n,
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold
),
),
),
...List.generate(3, (i) {
@ -319,7 +344,8 @@ class SearchResultsScreen extends StatelessWidget {
MaterialPageRoute(builder: (context) => AlbumListScreen(results.albums))
);
},
)
),
FreezerDivider()
];
}
@ -327,12 +353,15 @@ class SearchResultsScreen extends StatelessWidget {
List<Widget> artists = [];
if (results.artists != null && results.artists.length != 0) {
artists = [
Text(
'Artists'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26.0,
fontWeight: FontWeight.bold
Padding(
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
child: Text(
'Artists'.i18n,
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold
),
),
),
Container(height: 4),
@ -355,7 +384,8 @@ class SearchResultsScreen extends StatelessWidget {
);
}),
)
)
),
FreezerDivider()
];
}
@ -363,12 +393,15 @@ class SearchResultsScreen extends StatelessWidget {
List<Widget> playlists = [];
if (results.playlists != null && results.playlists.length != 0) {
playlists = [
Text(
'Playlists'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26.0,
fontWeight: FontWeight.bold
Padding(
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
child: Text(
'Playlists'.i18n,
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold
),
),
),
...List.generate(3, (i) {
@ -427,7 +460,7 @@ class TrackListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Tracks'.i18n),),
appBar: FreezerAppBar('Tracks'.i18n),
body: ListView.builder(
itemCount: tracks.length,
itemBuilder: (BuildContext context, int i) {
@ -457,7 +490,7 @@ class AlbumListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Albums'.i18n),),
appBar: FreezerAppBar('Albums'.i18n),
body: ListView.builder(
itemCount: albums.length,
itemBuilder: (context, i) {
@ -488,7 +521,7 @@ class SearchResultPlaylists extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Playlists'.i18n),),
appBar: FreezerAppBar('Playlists'.i18n),
body: ListView.builder(
itemCount: playlists.length,
itemBuilder: (context, i) {

View File

@ -12,6 +12,7 @@ import 'package:freezer/api/cache.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/download.dart';
import 'package:freezer/ui/downloads_screen.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/home_screen.dart';
import 'package:i18n_extension/i18n_widget.dart';
@ -56,26 +57,26 @@ class _SettingsScreenState extends State<SettingsScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Settings'.i18n),),
appBar: FreezerAppBar('Settings'.i18n),
body: ListView(
children: <Widget>[
ListTile(
title: Text('General'.i18n),
leading: Icon(Icons.settings),
leading: LeadingIcon(Icons.settings, color: Color(0xffeca704)),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => GeneralSettings()
)),
),
ListTile(
title: Text('Download Settings'.i18n),
leading: Icon(Icons.cloud_download),
leading: LeadingIcon(Icons.cloud_download, color: Color(0xffbe3266)),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => DownloadsSettings()
)),
),
ListTile(
title: Text('Appearance'.i18n),
leading: Icon(Icons.color_lens),
leading: LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => AppearanceSettings())
@ -83,7 +84,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
ListTile(
title: Text('Quality'.i18n),
leading: Icon(Icons.high_quality),
leading: LeadingIcon(Icons.high_quality, color: Color(0xff384697)),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => QualitySettings())
@ -91,7 +92,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
ListTile(
title: Text('Deezer'.i18n),
leading: Icon(Icons.equalizer),
leading: LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)),
onTap: () => Navigator.push(context, MaterialPageRoute(
builder: (context) => DeezerSettings()
)),
@ -99,7 +100,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
//Language select
ListTile(
title: Text('Language'.i18n),
leading: Icon(Icons.language),
leading: LeadingIcon(Icons.language, color: Color(0xff009a85)),
onTap: () {
showDialog(
context: context,
@ -140,7 +141,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
ListTile(
title: Text('About'.i18n),
leading: Icon(Icons.info),
leading: LeadingIcon(Icons.info, color: Color(0xff2ba766)),
onTap: () => Navigator.push(context, MaterialPageRoute(
builder: (context) => CreditsScreen()
)),
@ -164,7 +165,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Appearance'.i18n),),
appBar: FreezerAppBar('Appearance'.i18n),
body: ListView(
children: <Widget>[
ListTile(
@ -222,19 +223,17 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
),
ListTile(
title: Text('Use system theme'.i18n),
leading: Container(
width: 30.0,
child: Checkbox(
value: settings.useSystemTheme,
onChanged: (bool v) async {
setState(() {
settings.useSystemTheme = v;
});
updateTheme();
await settings.save();
},
),
trailing: Switch(
value: settings.useSystemTheme,
onChanged: (bool v) async {
setState(() {
settings.useSystemTheme = v;
});
updateTheme();
await settings.save();
},
),
leading: Icon(Icons.android)
),
ListTile(
title: Text('Primary color'.i18n),
@ -285,12 +284,10 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
ListTile(
title: Text('Use album art primary color'.i18n),
subtitle: Text('Warning: might be buggy'.i18n),
leading: Container(
width: 30.0,
child: Checkbox(
value: settings.useArtColor,
onChanged: (v) => setState(() => settings.updateUseArtColor(v)),
),
leading: Icon(Icons.invert_colors),
trailing: Switch(
value: settings.useArtColor,
onChanged: (v) => setState(() => settings.updateUseArtColor(v)),
),
)
],
@ -309,30 +306,30 @@ class _QualitySettingsState extends State<QualitySettings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Quality'.i18n),),
appBar: FreezerAppBar('Quality'.i18n),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Mobile streaming'.i18n),
leading: Icon(Icons.network_cell),
leading: LeadingIcon(Icons.network_cell, color: Color(0xff384697)),
),
QualityPicker('mobile'),
Divider(),
FreezerDivider(),
ListTile(
title: Text('Wifi streaming'.i18n),
leading: Icon(Icons.network_wifi),
leading: LeadingIcon(Icons.network_wifi, color: Color(0xff0880b5)),
),
QualityPicker('wifi'),
Divider(),
FreezerDivider(),
ListTile(
title: Text('Offline'.i18n),
leading: Icon(Icons.offline_pin),
leading: LeadingIcon(Icons.offline_pin, color: Color(0xff009a85)),
),
QualityPicker('offline'),
Divider(),
FreezerDivider(),
ListTile(
title: Text('External downloads'.i18n),
leading: Icon(Icons.file_download),
leading: LeadingIcon(Icons.file_download, color: Color(0xff2ba766)),
),
QualityPicker('download'),
],
@ -425,6 +422,15 @@ class _QualityPickerState extends State<QualityPicker> {
onChanged: (q) => _updateQuality(q),
),
),
if (widget.field == 'download')
ListTile(
title: Text('Ask before downloading'),
leading: Radio(
groupValue: _quality,
value: AudioQuality.ASK,
onChanged: (q) => _updateQuality(q),
)
)
],
);
}
@ -439,7 +445,7 @@ class _DeezerSettingsState extends State<DeezerSettings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Deezer'.i18n),),
appBar: FreezerAppBar('Deezer'.i18n),
body: ListView(
children: <Widget>[
ListTile(
@ -485,65 +491,64 @@ class _DeezerSettingsState extends State<DeezerSettings> {
ListTile(
title: Text('Log tracks'.i18n),
subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'.i18n),
leading: Container(
width: 30,
child: Checkbox(
value: settings.logListen,
onChanged: (bool v) {
setState(() => settings.logListen = v);
settings.save();
},
),
trailing: Switch(
value: settings.logListen,
onChanged: (bool v) {
setState(() => settings.logListen = v);
settings.save();
},
),
leading: Icon(Icons.history_toggle_off),
),
ListTile(
title: Text('Proxy'.i18n),
leading: Icon(Icons.vpn_key),
subtitle: Text(settings.proxyAddress??'Not set'.i18n),
onTap: () {
String _new;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Proxy'.i18n),
content: TextField(
onChanged: (String v) => _new = v,
decoration: InputDecoration(
hintText: 'IP:PORT'
),
),
actions: [
FlatButton(
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('Reset'.i18n),
onPressed: () async {
setState(() {
settings.proxyAddress = null;
});
await settings.save();
Navigator.of(context).pop();
},
),
FlatButton(
child: Text('Save'.i18n),
onPressed: () async {
setState(() {
settings.proxyAddress = _new;
});
await settings.save();
Navigator.of(context).pop();
},
)
],
);
}
);
},
)
//TODO: Reimplement proxy
// ListTile(
// title: Text('Proxy'.i18n),
// leading: Icon(Icons.vpn_key),
// subtitle: Text(settings.proxyAddress??'Not set'.i18n),
// onTap: () {
// String _new;
// showDialog(
// context: context,
// builder: (BuildContext context) {
// return AlertDialog(
// title: Text('Proxy'.i18n),
// content: TextField(
// onChanged: (String v) => _new = v,
// decoration: InputDecoration(
// hintText: 'IP:PORT'
// ),
// ),
// actions: [
// FlatButton(
// child: Text('Cancel'.i18n),
// onPressed: () => Navigator.of(context).pop(),
// ),
// FlatButton(
// child: Text('Reset'.i18n),
// onPressed: () async {
// setState(() {
// settings.proxyAddress = null;
// });
// await settings.save();
// Navigator.of(context).pop();
// },
// ),
// FlatButton(
// child: Text('Save'.i18n),
// onPressed: () async {
// setState(() {
// settings.proxyAddress = _new;
// });
// await settings.save();
// Navigator.of(context).pop();
// },
// )
// ],
// );
// }
// );
// },
// )
],
),
);
@ -562,7 +567,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Download Settings'.i18n),),
appBar: FreezerAppBar('Download Settings'.i18n),
body: ListView(
children: [
ListTile(
@ -696,128 +701,110 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
}
}
),
Divider(),
FreezerDivider(),
ListTile(
title: Text('Create folders for artist'.i18n),
leading: Container(
width: 30.0,
child: Checkbox(
value: settings.artistFolder,
onChanged: (v) {
setState(() => settings.artistFolder = v);
settings.save();
},
),
trailing: Switch(
value: settings.artistFolder,
onChanged: (v) {
setState(() => settings.artistFolder = v);
settings.save();
},
),
leading: Icon(Icons.folder),
),
ListTile(
title: Text('Create folders for albums'.i18n),
leading: Container(
width: 30.0,
child: Checkbox(
value: settings.albumFolder,
onChanged: (v) {
setState(() => settings.albumFolder = v);
settings.save();
},
),
trailing: Switch(
value: settings.albumFolder,
onChanged: (v) {
setState(() => settings.albumFolder = v);
settings.save();
},
),
leading: Icon(Icons.folder)
),
ListTile(
title: Text('Create folder for playlist'.i18n),
leading: Container(
width: 30.0,
child: Checkbox(
value: settings.playlistFolder,
onChanged: (v) {
setState(() => settings.playlistFolder = v);
settings.save();
},
),
trailing: Switch(
value: settings.playlistFolder,
onChanged: (v) {
setState(() => settings.playlistFolder = v);
settings.save();
},
),
leading: Icon(Icons.folder)
),
Divider(),
FreezerDivider(),
ListTile(
title: Text('Separate albums by discs'.i18n),
leading: Container(
width: 30.0,
child: Checkbox(
value: settings.albumDiscFolder,
onChanged: (v) {
setState(() => settings.albumDiscFolder = v);
settings.save();
},
),
trailing: Switch(
value: settings.albumDiscFolder,
onChanged: (v) {
setState(() => settings.albumDiscFolder = v);
settings.save();
},
),
leading: Icon(Icons.album)
),
ListTile(
title: Text('Overwrite already downloaded files'.i18n),
leading: Container(
width: 30.0,
child: Checkbox(
value: settings.overwriteDownload,
onChanged: (v) {
setState(() => settings.overwriteDownload = v);
settings.save();
},
),
trailing: Switch(
value: settings.overwriteDownload,
onChanged: (v) {
setState(() => settings.overwriteDownload = v);
settings.save();
},
),
leading: Icon(Icons.delete)
),
ListTile(
title: Text('Download .LRC lyrics'.i18n),
leading: Container(
width: 30.0,
child: Checkbox(
value: settings.downloadLyrics,
onChanged: (v) {
setState(() => settings.downloadLyrics = v);
settings.save();
},
),
trailing: Switch(
value: settings.downloadLyrics,
onChanged: (v) {
setState(() => settings.downloadLyrics = v);
settings.save();
},
),
leading: Icon(Icons.subtitles)
),
Divider(),
FreezerDivider(),
ListTile(
title: Text('Save cover file for every track'.i18n),
leading: Container(
width: 30.0,
child: Checkbox(
value: settings.trackCover,
onChanged: (v) {
setState(() => settings.trackCover = v);
settings.save();
},
),
trailing: Switch(
value: settings.trackCover,
onChanged: (v) {
setState(() => settings.trackCover = v);
settings.save();
},
),
leading: Icon(Icons.image)
),
ListTile(
title: Text('Save album cover'.i18n),
leading: Container(
width: 30.0,
child: Checkbox(
value: settings.albumCover,
onChanged: (v) {
setState(() => settings.albumCover = v);
settings.save();
},
),
trailing: Switch(
value: settings.albumCover,
onChanged: (v) {
setState(() => settings.albumCover = v);
settings.save();
},
),
leading: Icon(Icons.image)
),
ListTile(
title: Text('Create .nomedia files'.i18n),
subtitle: Text('To prevent gallery being filled with album art'.i18n),
leading: Container(
width: 30.0,
child: Checkbox(
value: settings.nomediaFiles,
onChanged: (v) {
setState(() => settings.nomediaFiles = v);
settings.save();
},
),
trailing: Switch(
value: settings.nomediaFiles,
onChanged: (v) {
setState(() => settings.nomediaFiles = v);
settings.save();
},
),
leading: Icon(Icons.insert_drive_file)
),
Divider(),
FreezerDivider(),
ListTile(
title: Text('Download Log'.i18n),
leading: Icon(Icons.sticky_note_2),
@ -841,51 +828,49 @@ class _GeneralSettingsState extends State<GeneralSettings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('General'.i18n),),
appBar: FreezerAppBar('General'.i18n),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Offline mode'.i18n),
subtitle: Text('Will be overwritten on start.'.i18n),
leading: Container(
width: 30.0,
child: Checkbox(
value: settings.offlineMode,
onChanged: (bool v) {
if (v) {
setState(() => settings.offlineMode = true);
return;
}
showDialog(
context: context,
builder: (context) {
deezerAPI.authorize().then((v) {
if (v) {
setState(() => settings.offlineMode = false);
} else {
Fluttertoast.showToast(
msg: 'Error logging in, check your internet connections.'.i18n,
gravity: ToastGravity.BOTTOM,
toastLength: Toast.LENGTH_SHORT
);
}
Navigator.of(context).pop();
});
return AlertDialog(
title: Text('Logging in...'.i18n),
content: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator()
],
)
);
}
);
},
),
trailing: Switch(
value: settings.offlineMode,
onChanged: (bool v) {
if (v) {
setState(() => settings.offlineMode = true);
return;
}
showDialog(
context: context,
builder: (context) {
deezerAPI.authorize().then((v) {
if (v) {
setState(() => settings.offlineMode = false);
} else {
Fluttertoast.showToast(
msg: 'Error logging in, check your internet connections.'.i18n,
gravity: ToastGravity.BOTTOM,
toastLength: Toast.LENGTH_SHORT
);
}
Navigator.of(context).pop();
});
return AlertDialog(
title: Text('Logging in...'.i18n),
content: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator()
],
)
);
}
);
},
),
leading: Icon(Icons.lock),
),
ListTile(
title: Text('Copy ARL'.i18n),
@ -933,6 +918,18 @@ class _GeneralSettingsState extends State<GeneralSettings> {
}
);
}
),
ListTile(
title: Text('Ignore interruptions'.i18n),
subtitle: Text('Requires app restart to apply!'.i18n),
leading: Icon(Icons.not_interested),
trailing: Switch(
value: settings.ignoreInterruptions,
onChanged: (bool v) async {
setState(() => settings.ignoreInterruptions = v);
await settings.save();
},
),
)
],
),
@ -965,8 +962,8 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Pick-a-Path'.i18n),
appBar: FreezerAppBar(
'Pick-a-Path'.i18n,
actions: <Widget>[
IconButton(
icon: Icon(Icons.sd_card),
@ -1110,7 +1107,8 @@ class _CreditsScreenState extends State<CreditsScreen> {
['kobyrevah', 'Hebrew'],
['HoScHaKaL', 'Turkish'],
['MicroMihai', 'Romanian'],
['LenteraMalam', 'Indonesian']
['LenteraMalam', 'Indonesian'],
['RTWO2', 'Persian']
];
@override
@ -1126,9 +1124,6 @@ class _CreditsScreenState extends State<CreditsScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('About'.i18n),
),
body: ListView(
children: [
FreezerTitle(),
@ -1139,7 +1134,7 @@ class _CreditsScreenState extends State<CreditsScreen> {
fontStyle: FontStyle.italic
),
),
Divider(),
FreezerDivider(),
ListTile(
title: Text('Telegram Channel'.i18n),
subtitle: Text('To get latest releases'.i18n),
@ -1164,7 +1159,7 @@ class _CreditsScreenState extends State<CreditsScreen> {
launch('https://notabug.org/exttex/freezer');
},
),
Divider(),
FreezerDivider(),
ListTile(
title: Text('exttex'),
subtitle: Text('Developer'),
@ -1203,7 +1198,7 @@ class _CreditsScreenState extends State<CreditsScreen> {
title: Text('Annexhack'),
subtitle: Text('Android Auto help'),
),
Divider(),
FreezerDivider(),
...List.generate(translators.length, (i) => ListTile(
title: Text(translators[i][0]),
subtitle: Text(translators[i][1]),

View File

@ -1,11 +1,14 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/translations.i18n.dart';
import '../api/definitions.dart';
import 'cached_image.dart';
import 'dart:async';
class TrackTile extends StatefulWidget {
final Track track;
@ -132,6 +135,8 @@ class ArtistTile extends StatelessWidget {
return SizedBox(
width: 150,
child: Card(
color: Theme.of(context).scaffoldBackgroundColor,
elevation: 0.0,
child: InkWell(
onTap: onTap,
onLongPress: onHold,
@ -144,7 +149,7 @@ class ArtistTile extends StatelessWidget {
circular: true,
width: 100,
),
Container(height: 4,),
Container(height: 8,),
Text(
artist.name,
maxLines: 1,
@ -172,6 +177,13 @@ class PlaylistTile extends StatelessWidget {
PlaylistTile(this.playlist, {this.onHold, this.onTap, this.trailing});
String get subtitle {
if (playlist.user == null || playlist.user.name == null || playlist.user.name == '' || playlist.user.id == deezerAPI.userId) {
return '${playlist.trackCount} ' + 'Tracks'.i18n;
}
return playlist.user.name;
}
@override
Widget build(BuildContext context) {
return ListTile(
@ -180,7 +192,7 @@ class PlaylistTile extends StatelessWidget {
maxLines: 1,
),
subtitle: Text(
playlist.user.name,
subtitle,
maxLines: 1,
),
leading: CachedImage(
@ -234,6 +246,8 @@ class PlaylistCardTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).scaffoldBackgroundColor,
elevation: 0.0,
child: InkWell(
onTap: onTap,
onLongPress: onHold,
@ -245,8 +259,10 @@ class PlaylistCardTile extends StatelessWidget {
url: playlist.image.thumb,
width: 128,
height: 128,
rounded: true,
),
),
Container(height: 2.0),
Container(
width: 144,
child: Text(
@ -257,7 +273,7 @@ class PlaylistCardTile extends StatelessWidget {
style: TextStyle(fontSize: 14.0),
),
),
Container(height: 8.0,)
Container(height: 4.0,)
],
),
)
@ -276,6 +292,8 @@ class SmartTrackListTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
elevation: 0,
color: Theme.of(context).scaffoldBackgroundColor,
child: InkWell(
onTap: onTap,
onLongPress: onHold,
@ -287,6 +305,7 @@ class SmartTrackListTile extends StatelessWidget {
width: 128,
height: 128,
url: smartTrackList.cover.thumb,
rounded: true,
),
),
Container(
@ -320,6 +339,8 @@ class AlbumCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).scaffoldBackgroundColor,
elevation: 0.0,
child: InkWell(
onTap: onTap,
onLongPress: onHold,
@ -331,6 +352,7 @@ class AlbumCard extends StatelessWidget {
width: 128.0,
height: 128.0,
url: album.art.thumb,
rounded: true
),
),
Container(
@ -345,6 +367,20 @@ class AlbumCard extends StatelessWidget {
),
),
),
Container(height: 4.0),
Container(
width: 144.0,
child: Text(
album.artistString,
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12.0,
color: (Theme.of(context).brightness == Brightness.light) ? Colors.grey[800] : Colors.white70
),
),
),
Container(height: 8.0,)
],
),
@ -366,28 +402,31 @@ class ChannelTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
color: channel.backgroundColor,
child: InkWell(
onTap: this.onTap,
child: Container(
width: 150,
height: 75,
child: Center(
child: Text(
channel.title,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
color: _textColor()
return Padding(
padding: EdgeInsets.symmetric(horizontal: 4.0),
child: Card(
color: channel.backgroundColor,
child: InkWell(
onTap: this.onTap,
child: Container(
width: 150,
height: 75,
child: Center(
child: Text(
channel.title,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
color: _textColor()
),
),
),
),
),
)
)
),
);
}
}

View File

@ -246,20 +246,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.7"
dio:
dependency: "direct main"
description:
name: dio
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.10"
dio_cookie_manager:
dependency: "direct main"
description:
name: dio_cookie_manager
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
disk_space:
dependency: "direct main"
description:
@ -428,7 +414,7 @@ packages:
source: hosted
version: "0.14.0+4"
http:
dependency: transitive
dependency: "direct main"
description:
name: http
url: "https://pub.dartlang.org"

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.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.5.10+1
version: 0.6.0+1
environment:
sdk: ">=2.8.0 <3.0.0"
@ -27,8 +27,7 @@ dependencies:
flutter_localizations:
sdk: flutter
dio: ^3.0.10
dio_cookie_manager: ^1.0.0
http: ^0.12.2
cookie_jar: ^1.0.1
json_annotation: ^3.0.1
path_provider: 1.6.10

View File

@ -18,7 +18,9 @@ lang_crowdin = {
'ro': 'ro_ro',
'ru': 'ru_ru',
'tr': 'tr_tr',
'pl': 'pl_pl'
'pl': 'pl_pl',
'uk': 'uk_ua',
'hu': 'hu_hu'
}
def generate_dart():
@ -30,7 +32,7 @@ def generate_dart():
lang = file.split('/')[0]
out[lang_crowdin[lang]] = json.loads(data)
with open('crowdin.dart', 'w') as f:
with open('../lib/languages/crowdin.dart', 'w') as f:
data = json.dumps(out, ensure_ascii=False).replace('$', '\\$')
out = f'const crowdin = {data};'
f.write(out)