0.6.10 - Spotify is ass
This commit is contained in:
parent
3105ed6c1d
commit
676d5d45cc
|
@ -4,7 +4,6 @@ import 'package:freezer/api/spotify.dart';
|
||||||
import 'package:freezer/settings.dart';
|
import 'package:freezer/settings.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
@ -149,12 +148,12 @@ class DeezerAPI {
|
||||||
try {
|
try {
|
||||||
//Tracks
|
//Tracks
|
||||||
if (uri.pathSegments[0] == 'track') {
|
if (uri.pathSegments[0] == 'track') {
|
||||||
String id = await spotify.convertTrack(spotifyUri);
|
String id = await SpotifyScrapper.convertTrack(spotifyUri);
|
||||||
return DeezerLinkResponse(type: DeezerLinkType.TRACK, id: id);
|
return DeezerLinkResponse(type: DeezerLinkType.TRACK, id: id);
|
||||||
}
|
}
|
||||||
//Albums
|
//Albums
|
||||||
if (uri.pathSegments[0] == 'album') {
|
if (uri.pathSegments[0] == 'album') {
|
||||||
String id = await spotify.convertAlbum(spotifyUri);
|
String id = await SpotifyScrapper.convertAlbum(spotifyUri);
|
||||||
return DeezerLinkResponse(type: DeezerLinkType.ALBUM, id: id);
|
return DeezerLinkResponse(type: DeezerLinkType.ALBUM, id: id);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
|
@ -0,0 +1,171 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:freezer/api/deezer.dart';
|
||||||
|
import 'package:freezer/api/definitions.dart';
|
||||||
|
import 'package:freezer/api/download.dart';
|
||||||
|
|
||||||
|
Importer importer = Importer();
|
||||||
|
|
||||||
|
class Importer {
|
||||||
|
//Options
|
||||||
|
bool download = false;
|
||||||
|
|
||||||
|
//Preserve context
|
||||||
|
BuildContext context;
|
||||||
|
String title;
|
||||||
|
String description;
|
||||||
|
List<ImporterTrack> tracks;
|
||||||
|
String playlistId;
|
||||||
|
Playlist playlist;
|
||||||
|
|
||||||
|
bool done = false;
|
||||||
|
bool busy = false;
|
||||||
|
Future _future;
|
||||||
|
StreamController _streamController;
|
||||||
|
|
||||||
|
Stream get updateStream => _streamController.stream;
|
||||||
|
int get ok => tracks.fold(0, (v, t) => (t.state == TrackImportState.OK) ? v+1 : v);
|
||||||
|
int get error => tracks.fold(0, (v, t) => (t.state == TrackImportState.ERROR) ? v+1 : v);
|
||||||
|
|
||||||
|
Importer();
|
||||||
|
|
||||||
|
//Start importing wrapper
|
||||||
|
Future<void> start(BuildContext context, String title, String description, List<ImporterTrack> tracks) async {
|
||||||
|
//Save variables
|
||||||
|
this.playlist = null;
|
||||||
|
this.context = context;
|
||||||
|
this.title = title;
|
||||||
|
this.description = description??'';
|
||||||
|
this.tracks = tracks.map((t) {t.state = TrackImportState.NONE; return t;}).toList();
|
||||||
|
|
||||||
|
//Create playlist
|
||||||
|
playlistId = await deezerAPI.createPlaylist(title, description: description);
|
||||||
|
|
||||||
|
busy = true;
|
||||||
|
done = false;
|
||||||
|
_streamController = StreamController.broadcast();
|
||||||
|
_future = _start();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Start importer
|
||||||
|
Future _start() async {
|
||||||
|
for (int i=0; i<tracks.length; i++) {
|
||||||
|
try {
|
||||||
|
String id = await _searchTrack(tracks[i]);
|
||||||
|
//Not found
|
||||||
|
if (id == null) {
|
||||||
|
tracks[i].state = TrackImportState.ERROR;
|
||||||
|
_streamController.add(tracks[i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
//Add to playlist
|
||||||
|
await deezerAPI.addToPlaylist(id, playlistId);
|
||||||
|
tracks[i].state = TrackImportState.OK;
|
||||||
|
} catch (_) {
|
||||||
|
//Error occurred, mark as error
|
||||||
|
tracks[i].state = TrackImportState.ERROR;
|
||||||
|
}
|
||||||
|
_streamController.add(tracks[i]);
|
||||||
|
}
|
||||||
|
//Get full playlist
|
||||||
|
playlist = await deezerAPI.playlist(playlistId, nb: 10000);
|
||||||
|
playlist.library = true;
|
||||||
|
|
||||||
|
//Download
|
||||||
|
if (download) {
|
||||||
|
await downloadManager.addOfflinePlaylist(playlist, private: false, context: context);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Mark as done
|
||||||
|
done = true;
|
||||||
|
busy = false;
|
||||||
|
//To update UI
|
||||||
|
_streamController.add(null);
|
||||||
|
_streamController.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Find track on Deezer servers
|
||||||
|
Future<String> _searchTrack(ImporterTrack track) async {
|
||||||
|
//Try by ISRC
|
||||||
|
if (track.isrc != null && track.isrc.length == 12) {
|
||||||
|
Map deezer = await deezerAPI.callPublicApi('track/isrc:' + track.isrc);
|
||||||
|
if (deezer["id"] != null) {
|
||||||
|
return deezer["id"].toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Search
|
||||||
|
String cleanedTitle = track.title.trim().toLowerCase().replaceAll("-", "").replaceAll("&", "").replaceAll("+", "");
|
||||||
|
SearchResults results = await deezerAPI.search("${track.artists[0]} $cleanedTitle");
|
||||||
|
for (Track t in results.tracks) {
|
||||||
|
//Match title
|
||||||
|
if (_cleanMatching(t.title) == _cleanMatching(track.title)) {
|
||||||
|
//Match artist
|
||||||
|
if (_matchArtists(track.artists, t.artists.map((a) => a.name))) {
|
||||||
|
return t.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Clean title for matching
|
||||||
|
String _cleanMatching(String t) {
|
||||||
|
return t.toLowerCase()
|
||||||
|
.replaceAll(",", "")
|
||||||
|
.replaceAll("-", "")
|
||||||
|
.replaceAll(" ", "")
|
||||||
|
.replaceAll("&", "")
|
||||||
|
.replaceAll("+", "")
|
||||||
|
.replaceAll("/", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
String _cleanArtist(String a) {
|
||||||
|
return a.toLowerCase()
|
||||||
|
.replaceAll(" ", "")
|
||||||
|
.replaceAll(",", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
//Match at least 1 artist
|
||||||
|
bool _matchArtists(List<String> a, List<String> b) {
|
||||||
|
//Clean
|
||||||
|
List<String> _a = a.map(_cleanArtist).toList();
|
||||||
|
List<String> _b = b.map(_cleanArtist).toList();
|
||||||
|
|
||||||
|
for (String artist in _a) {
|
||||||
|
if (_b.contains(artist)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImporterTrack {
|
||||||
|
String title;
|
||||||
|
List<String> artists;
|
||||||
|
String isrc;
|
||||||
|
TrackImportState state;
|
||||||
|
|
||||||
|
ImporterTrack(this.title, this.artists, {this.isrc, this.state = TrackImportState.NONE});
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TrackImportState {
|
||||||
|
NONE,
|
||||||
|
ERROR,
|
||||||
|
OK
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TrackImportStateExtension on TrackImportState {
|
||||||
|
Widget get icon {
|
||||||
|
switch (this) {
|
||||||
|
case TrackImportState.ERROR:
|
||||||
|
return Icon(Icons.error, color: Colors.red,);
|
||||||
|
case TrackImportState.OK:
|
||||||
|
return Icon(Icons.done, color: Colors.green);
|
||||||
|
default:
|
||||||
|
return Container(width: 0, height: 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,26 +1,18 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:freezer/api/download.dart';
|
import 'package:freezer/api/importer.dart';
|
||||||
import 'package:freezer/api/definitions.dart';
|
|
||||||
import 'package:freezer/settings.dart';
|
|
||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:html/dom.dart' as dom;
|
import 'package:html/dom.dart' as dom;
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:async';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
SpotifyAPI spotify = SpotifyAPI();
|
class SpotifyScrapper {
|
||||||
|
|
||||||
class SpotifyAPI {
|
|
||||||
|
|
||||||
SpotifyPlaylist importingSpotifyPlaylist;
|
|
||||||
StreamController importingStream = StreamController.broadcast();
|
|
||||||
bool doneImporting;
|
|
||||||
|
|
||||||
//Parse spotify URL to URI (spotify:track:1234)
|
//Parse spotify URL to URI (spotify:track:1234)
|
||||||
String parseUrl(String url) {
|
static String parseUrl(String url) {
|
||||||
Uri uri = Uri.parse(url);
|
Uri uri = Uri.parse(url);
|
||||||
if (uri.pathSegments.length > 3) return null; //Invalid URL
|
if (uri.pathSegments.length > 3) return null; //Invalid URL
|
||||||
if (uri.pathSegments.length == 3) return 'spotify:${uri.pathSegments[1]}:${uri.pathSegments[2]}';
|
if (uri.pathSegments.length == 3) return 'spotify:${uri.pathSegments[1]}:${uri.pathSegments[2]}';
|
||||||
|
@ -29,16 +21,16 @@ class SpotifyAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Get spotify embed url from uri
|
//Get spotify embed url from uri
|
||||||
String getEmbedUrl(String uri) => 'https://embed.spotify.com/?uri=$uri';
|
static String getEmbedUrl(String uri) => 'https://embed.spotify.com/?uri=$uri';
|
||||||
|
|
||||||
//https://link.tospotify.com/ or https://spotify.app.link/
|
//https://link.tospotify.com/ or https://spotify.app.link/
|
||||||
Future resolveLinkUrl(String url) async {
|
static Future resolveLinkUrl(String url) async {
|
||||||
http.Response response = await http.get(Uri.parse(url));
|
http.Response response = await http.get(Uri.parse(url));
|
||||||
Match match = RegExp(r'window\.top\.location = validate\("(.+)"\);').firstMatch(response.body);
|
Match match = RegExp(r'window\.top\.location = validate\("(.+)"\);').firstMatch(response.body);
|
||||||
return match.group(1);
|
return match.group(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future resolveUrl(String url) async {
|
static Future resolveUrl(String url) async {
|
||||||
if (url.contains("link.tospotify") || url.contains("spotify.app.link")) {
|
if (url.contains("link.tospotify") || url.contains("spotify.app.link")) {
|
||||||
return parseUrl(await resolveLinkUrl(url));
|
return parseUrl(await resolveLinkUrl(url));
|
||||||
}
|
}
|
||||||
|
@ -46,7 +38,7 @@ class SpotifyAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Extract JSON data form spotify embed page
|
//Extract JSON data form spotify embed page
|
||||||
Future<Map> getEmbedData(String url) async {
|
static Future<Map> getEmbedData(String url) async {
|
||||||
//Fetch
|
//Fetch
|
||||||
http.Response response = await http.get(url);
|
http.Response response = await http.get(url);
|
||||||
//Parse
|
//Parse
|
||||||
|
@ -61,7 +53,7 @@ class SpotifyAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SpotifyPlaylist> playlist(String uri) async {
|
static Future<SpotifyPlaylist> playlist(String uri) async {
|
||||||
//Load data
|
//Load data
|
||||||
String url = getEmbedUrl(uri);
|
String url = getEmbedUrl(uri);
|
||||||
Map data = await getEmbedData(url);
|
Map data = await getEmbedData(url);
|
||||||
|
@ -71,7 +63,7 @@ class SpotifyAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Get Deezer track ID from Spotify URI
|
//Get Deezer track ID from Spotify URI
|
||||||
Future<String> convertTrack(String uri) async {
|
static Future<String> convertTrack(String uri) async {
|
||||||
Map data = await getEmbedData(getEmbedUrl(uri));
|
Map data = await getEmbedData(getEmbedUrl(uri));
|
||||||
SpotifyTrack track = SpotifyTrack.fromJson(data);
|
SpotifyTrack track = SpotifyTrack.fromJson(data);
|
||||||
Map deezer = await deezerAPI.callPublicApi('track/isrc:' + track.isrc);
|
Map deezer = await deezerAPI.callPublicApi('track/isrc:' + track.isrc);
|
||||||
|
@ -79,75 +71,34 @@ class SpotifyAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Get Deezer album ID by UPC
|
//Get Deezer album ID by UPC
|
||||||
Future<String> convertAlbum(String uri) async {
|
static Future<String> convertAlbum(String uri) async {
|
||||||
Map data = await getEmbedData(getEmbedUrl(uri));
|
Map data = await getEmbedData(getEmbedUrl(uri));
|
||||||
SpotifyAlbum album = SpotifyAlbum.fromJson(data);
|
SpotifyAlbum album = SpotifyAlbum.fromJson(data);
|
||||||
Map deezer = await deezerAPI.callPublicApi('album/upc:' + album.upc);
|
Map deezer = await deezerAPI.callPublicApi('album/upc:' + album.upc);
|
||||||
return deezer['id'].toString();
|
return deezer['id'].toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future convertPlaylist(SpotifyPlaylist playlist, {bool downloadOnly = false, BuildContext context, AudioQuality quality}) async {
|
|
||||||
doneImporting = false;
|
|
||||||
importingSpotifyPlaylist = playlist;
|
|
||||||
|
|
||||||
//Create Deezer playlist
|
|
||||||
String playlistId;
|
|
||||||
if (!downloadOnly)
|
|
||||||
playlistId = await deezerAPI.createPlaylist(playlist.name, description: playlist.description);
|
|
||||||
|
|
||||||
//Search for tracks
|
|
||||||
List<Track> downloadTracks = [];
|
|
||||||
for (SpotifyTrack track in playlist.tracks) {
|
|
||||||
Map deezer;
|
|
||||||
try {
|
|
||||||
//Search
|
|
||||||
deezer = await deezerAPI.callPublicApi('track/isrc:' + track.isrc);
|
|
||||||
if (deezer.containsKey('error')) throw Exception();
|
|
||||||
String id = deezer['id'].toString();
|
|
||||||
//Add
|
|
||||||
if (!downloadOnly)
|
|
||||||
await deezerAPI.addToPlaylist(id, playlistId);
|
|
||||||
if (downloadOnly)
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
doneImporting = true;
|
|
||||||
//Return DEEZER playlist id
|
|
||||||
return playlistId;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SpotifyTrack {
|
class SpotifyTrack {
|
||||||
String title;
|
String title;
|
||||||
String artists;
|
List<String> artists;
|
||||||
String isrc;
|
String isrc;
|
||||||
TrackImportState state = TrackImportState.NONE;
|
|
||||||
|
|
||||||
SpotifyTrack({this.title, this.artists, this.isrc});
|
SpotifyTrack({this.title, this.artists, this.isrc});
|
||||||
|
|
||||||
//JSON
|
//JSON
|
||||||
factory SpotifyTrack.fromJson(Map json) => SpotifyTrack(
|
factory SpotifyTrack.fromJson(Map json) => SpotifyTrack(
|
||||||
title: json['name'],
|
title: json['name'],
|
||||||
artists: json['artists'].map((j) => j['name']).toList().join(', '),
|
artists: json['artists'].map<String>((a) => a["name"].toString()).toList(),
|
||||||
isrc: json['external_ids']['isrc']
|
isrc: json['external_ids']['isrc']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//Convert track to importer track
|
||||||
|
ImporterTrack toImporter() {
|
||||||
|
return ImporterTrack(title, artists, isrc: isrc);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SpotifyPlaylist {
|
class SpotifyPlaylist {
|
||||||
|
@ -165,6 +116,11 @@ class SpotifyPlaylist {
|
||||||
image: (json['images'].length > 0) ? json['images'][0]['url'] : null,
|
image: (json['images'].length > 0) ? json['images'][0]['url'] : null,
|
||||||
tracks: json['tracks']['items'].map<SpotifyTrack>((j) => SpotifyTrack.fromJson(j['track'])).toList()
|
tracks: json['tracks']['items'].map<SpotifyTrack>((j) => SpotifyTrack.fromJson(j['track'])).toList()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//Convert to importer tracks
|
||||||
|
List<ImporterTrack> toImporter() {
|
||||||
|
return tracks.map((t) => t.toImporter()).toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SpotifyAlbum {
|
class SpotifyAlbum {
|
||||||
|
@ -178,8 +134,50 @@ class SpotifyAlbum {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TrackImportState {
|
|
||||||
NONE,
|
class SpotifyAPIWrapper {
|
||||||
ERROR,
|
|
||||||
OK
|
HttpServer _server;
|
||||||
|
SpotifyApi spotify;
|
||||||
|
User me;
|
||||||
|
|
||||||
|
Future authorize(String clientId, String clientSecret) async {
|
||||||
|
//Spotify
|
||||||
|
SpotifyApiCredentials credentials = SpotifyApiCredentials(clientId, clientSecret);
|
||||||
|
spotify = SpotifyApi(credentials);
|
||||||
|
//Create server
|
||||||
|
_server = await HttpServer.bind(InternetAddress.loopbackIPv4, 42069);
|
||||||
|
String responseUri;
|
||||||
|
//Get URL
|
||||||
|
final grant = SpotifyApi.authorizationCodeGrant(credentials);
|
||||||
|
final redirectUri = "http://localhost:42069";
|
||||||
|
final scopes = ['user-read-private', 'playlist-read-private', 'playlist-read-collaborative', 'user-library-read'];
|
||||||
|
final authUri = grant.getAuthorizationUrl(Uri.parse(redirectUri), scopes: scopes);
|
||||||
|
launch(authUri.toString());
|
||||||
|
//Wait for code
|
||||||
|
await for (HttpRequest request in _server) {
|
||||||
|
//Exit window
|
||||||
|
request.response.headers.set("Content-Type", "text/html; charset=UTF-8");
|
||||||
|
request.response.write("<body><h1>You can close this page and go back to Freezer.</h1></body><script>window.close();</script>");
|
||||||
|
request.response.close();
|
||||||
|
//Get token
|
||||||
|
if (request.uri.queryParameters["code"] != null) {
|
||||||
|
_server.close();
|
||||||
|
_server = null;
|
||||||
|
responseUri = request.uri.toString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Create spotify
|
||||||
|
spotify = SpotifyApi.fromAuthCodeGrant(grant, responseUri);
|
||||||
|
me = await spotify.me.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Cancel authorization
|
||||||
|
void cancelAuthorize() {
|
||||||
|
if (_server != null) {
|
||||||
|
_server.close(force: true);
|
||||||
|
_server = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
File diff suppressed because one or more lines are too long
|
@ -351,6 +351,28 @@ const language_en_us = {
|
||||||
"Error logging in!": "Error logging in!",
|
"Error logging in!": "Error logging in!",
|
||||||
"Change display mode": "Change display mode",
|
"Change display mode": "Change display mode",
|
||||||
"Enable high refresh rates": "Enable high refresh rates",
|
"Enable high refresh rates": "Enable high refresh rates",
|
||||||
"Display mode": "Display mode"
|
"Display mode": "Display mode",
|
||||||
|
"Spotify v1": "Spotify v1",
|
||||||
|
"Import Spotify playlists up to 100 tracks without any login.": "Import Spotify playlists up to 100 tracks without any login.",
|
||||||
|
"Download imported tracks": "Download imported tracks",
|
||||||
|
"Start import": "Start import",
|
||||||
|
"Spotify v2": "Spotify v2",
|
||||||
|
"Import any Spotify playlist, import from own Spotify library. Requires free account.": "Import any Spotify playlist, import from own Spotify library. Requires free account.",
|
||||||
|
"Spotify Importer v2": "Spotify Importer v2",
|
||||||
|
"This importer requires Spotify Client ID and Client Secret. To obtain them:": "This importer requires Spotify Client ID and Client Secret. To obtain them:",
|
||||||
|
"1. Go to: developer.spotify.com/dashboard and create an app.": "1. Go to: developer.spotify.com/dashboard and create an app.",
|
||||||
|
"Open in Browser": "Open in Browser",
|
||||||
|
"2. In the app you just created go to settings, and set the Redirect URL to: ": "2. In the app you just created go to settings, and set the Redirect URL to: ",
|
||||||
|
"Copy the Redirect URL": "Copy the Redirect URL",
|
||||||
|
"Client ID": "Client ID",
|
||||||
|
"Client Secret": "Client Secret",
|
||||||
|
"Authorize": "Authorize",
|
||||||
|
"Logged in as: ": "Logged in as: ",
|
||||||
|
"Import playlists by URL": "Import playlists by URL",
|
||||||
|
"URL": "URL",
|
||||||
|
"Options": "Options",
|
||||||
|
"Invalid/Unsupported URL": "Invalid/Unsupported URL",
|
||||||
|
"Please wait...": "Please wait...",
|
||||||
|
"Login using email": "Login using email"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -177,8 +177,9 @@ class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateM
|
||||||
navigatorKey = GlobalKey<NavigatorState>();
|
navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
//Set display mode
|
//Set display mode
|
||||||
if (settings.displayMode != null) {
|
if (settings.displayMode != null && settings.displayMode >= 0) {
|
||||||
FlutterDisplayMode.supported.then((modes) async {
|
FlutterDisplayMode.supported.then((modes) async {
|
||||||
|
if (modes.length - 1 >= settings.displayMode)
|
||||||
FlutterDisplayMode.setMode(modes[settings.displayMode]);
|
FlutterDisplayMode.setMode(modes[settings.displayMode]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -368,19 +369,25 @@ class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateM
|
||||||
setState(() {
|
setState(() {
|
||||||
_selected = s;
|
_selected = s;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Fix statusbar
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||||
|
statusBarColor: Colors.transparent
|
||||||
|
));
|
||||||
},
|
},
|
||||||
selectedItemColor: Theme.of(context).primaryColor,
|
selectedItemColor: Theme.of(context).primaryColor,
|
||||||
items: <BottomNavigationBarItem>[
|
items: <BottomNavigationBarItem>[
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.home),
|
icon: Icon(Icons.home),
|
||||||
title: Text('Home'.i18n)),
|
label: 'Home'.i18n),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.search),
|
icon: Icon(Icons.search),
|
||||||
title: Text('Search'.i18n),
|
label: 'Search'.i18n,
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.library_music),
|
icon: Icon(Icons.library_music),
|
||||||
title: Text('Library'.i18n))
|
label: 'Library'.i18n
|
||||||
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
|
@ -127,6 +127,12 @@ class Settings {
|
||||||
@JsonKey(defaultValue: null)
|
@JsonKey(defaultValue: null)
|
||||||
String lastFMPassword;
|
String lastFMPassword;
|
||||||
|
|
||||||
|
//Spotify
|
||||||
|
@JsonKey(defaultValue: null)
|
||||||
|
String spotifyClientId;
|
||||||
|
@JsonKey(defaultValue: null)
|
||||||
|
String spotifyClientSecret;
|
||||||
|
|
||||||
|
|
||||||
Settings({this.downloadPath, this.arl});
|
Settings({this.downloadPath, this.arl});
|
||||||
|
|
||||||
|
@ -213,8 +219,9 @@ class Settings {
|
||||||
case AudioQuality.MP3_128: return 1;
|
case AudioQuality.MP3_128: return 1;
|
||||||
case AudioQuality.MP3_320: return 3;
|
case AudioQuality.MP3_320: return 3;
|
||||||
case AudioQuality.FLAC: return 9;
|
case AudioQuality.FLAC: return 9;
|
||||||
|
//Deezer default
|
||||||
|
default: return 8;
|
||||||
}
|
}
|
||||||
return 8; //default
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Check if is dark, can't use theme directly, because of system themes, and Theme.of(context).brightness broke
|
//Check if is dark, can't use theme directly, because of system themes, and Theme.of(context).brightness broke
|
||||||
|
@ -229,9 +236,23 @@ class Settings {
|
||||||
|
|
||||||
static const deezerBg = Color(0xFF1F1A16);
|
static const deezerBg = Color(0xFF1F1A16);
|
||||||
static const deezerBottom = Color(0xFF1b1714);
|
static const deezerBottom = Color(0xFF1b1714);
|
||||||
TextTheme get _textTheme => (font == 'Deezer') ? null : GoogleFonts.getTextTheme(font);
|
TextTheme get _textTheme => (font == 'Deezer')
|
||||||
|
? null
|
||||||
|
: GoogleFonts.getTextTheme(font, this.isDark ? ThemeData.dark().textTheme : ThemeData.light().textTheme);
|
||||||
String get _fontFamily => (font == 'Deezer') ? 'MabryPro' : null;
|
String get _fontFamily => (font == 'Deezer') ? 'MabryPro' : null;
|
||||||
|
|
||||||
|
//Overrides for the non-deprecated buttons to look like the old ones
|
||||||
|
static final outlinedButtonTheme = OutlinedButtonThemeData(
|
||||||
|
style: ButtonStyle(
|
||||||
|
foregroundColor: MaterialStateProperty.all(Colors.white),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
static final textButtonTheme = TextButtonThemeData(
|
||||||
|
style: ButtonStyle(
|
||||||
|
foregroundColor: MaterialStateProperty.all(Colors.white),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
Map<Themes, ThemeData> get _themeData => {
|
Map<Themes, ThemeData> get _themeData => {
|
||||||
Themes.Light: ThemeData(
|
Themes.Light: ThemeData(
|
||||||
textTheme: _textTheme,
|
textTheme: _textTheme,
|
||||||
|
@ -242,6 +263,8 @@ class Settings {
|
||||||
sliderTheme: _sliderTheme,
|
sliderTheme: _sliderTheme,
|
||||||
toggleableActiveColor: primaryColor,
|
toggleableActiveColor: primaryColor,
|
||||||
bottomAppBarColor: Color(0xfff5f5f5),
|
bottomAppBarColor: Color(0xfff5f5f5),
|
||||||
|
outlinedButtonTheme: outlinedButtonTheme,
|
||||||
|
textButtonTheme: textButtonTheme
|
||||||
),
|
),
|
||||||
Themes.Dark: ThemeData(
|
Themes.Dark: ThemeData(
|
||||||
textTheme: _textTheme,
|
textTheme: _textTheme,
|
||||||
|
@ -251,6 +274,8 @@ class Settings {
|
||||||
accentColor: primaryColor,
|
accentColor: primaryColor,
|
||||||
sliderTheme: _sliderTheme,
|
sliderTheme: _sliderTheme,
|
||||||
toggleableActiveColor: primaryColor,
|
toggleableActiveColor: primaryColor,
|
||||||
|
outlinedButtonTheme: outlinedButtonTheme,
|
||||||
|
textButtonTheme: textButtonTheme
|
||||||
),
|
),
|
||||||
Themes.Deezer: ThemeData(
|
Themes.Deezer: ThemeData(
|
||||||
textTheme: _textTheme,
|
textTheme: _textTheme,
|
||||||
|
@ -267,7 +292,9 @@ class Settings {
|
||||||
bottomSheetTheme: BottomSheetThemeData(
|
bottomSheetTheme: BottomSheetThemeData(
|
||||||
backgroundColor: deezerBottom
|
backgroundColor: deezerBottom
|
||||||
),
|
),
|
||||||
cardColor: deezerBg
|
cardColor: deezerBg,
|
||||||
|
outlinedButtonTheme: outlinedButtonTheme,
|
||||||
|
textButtonTheme: textButtonTheme
|
||||||
),
|
),
|
||||||
Themes.Black: ThemeData(
|
Themes.Black: ThemeData(
|
||||||
textTheme: _textTheme,
|
textTheme: _textTheme,
|
||||||
|
@ -283,7 +310,9 @@ class Settings {
|
||||||
toggleableActiveColor: primaryColor,
|
toggleableActiveColor: primaryColor,
|
||||||
bottomSheetTheme: BottomSheetThemeData(
|
bottomSheetTheme: BottomSheetThemeData(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
)
|
),
|
||||||
|
outlinedButtonTheme: outlinedButtonTheme,
|
||||||
|
textButtonTheme: textButtonTheme
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,9 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
|
||||||
..logListen = json['logListen'] as bool ?? false
|
..logListen = json['logListen'] as bool ?? false
|
||||||
..proxyAddress = json['proxyAddress'] as String
|
..proxyAddress = json['proxyAddress'] as String
|
||||||
..lastFMUsername = json['lastFMUsername'] as String
|
..lastFMUsername = json['lastFMUsername'] as String
|
||||||
..lastFMPassword = json['lastFMPassword'] as String;
|
..lastFMPassword = json['lastFMPassword'] as String
|
||||||
|
..spotifyClientId = json['spotifyClientId'] as String
|
||||||
|
..spotifyClientSecret = json['spotifyClientSecret'] as String;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
|
Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
|
||||||
|
@ -119,6 +121,8 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
|
||||||
'proxyAddress': instance.proxyAddress,
|
'proxyAddress': instance.proxyAddress,
|
||||||
'lastFMUsername': instance.lastFMUsername,
|
'lastFMUsername': instance.lastFMUsername,
|
||||||
'lastFMPassword': instance.lastFMPassword,
|
'lastFMPassword': instance.lastFMPassword,
|
||||||
|
'spotifyClientId': instance.spotifyClientId,
|
||||||
|
'spotifyClientSecret': instance.spotifyClientSecret,
|
||||||
};
|
};
|
||||||
|
|
||||||
T _$enumDecode<T>(
|
T _$enumDecode<T>(
|
||||||
|
|
|
@ -114,7 +114,7 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
ctx = context;
|
ctx = context;
|
||||||
return FlatButton(
|
return TextButton(
|
||||||
child: CachedImage(
|
child: CachedImage(
|
||||||
url: widget.url,
|
url: widget.url,
|
||||||
rounded: widget.rounded,
|
rounded: widget.rounded,
|
||||||
|
|
|
@ -21,7 +21,7 @@ import 'menu.dart';
|
||||||
|
|
||||||
class AlbumDetails extends StatefulWidget {
|
class AlbumDetails extends StatefulWidget {
|
||||||
|
|
||||||
Album album;
|
final Album album;
|
||||||
AlbumDetails(this.album, {Key key}): super(key: key);
|
AlbumDetails(this.album, {Key key}): super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -165,7 +165,7 @@ class _AlbumDetailsState extends State<AlbumDetails> {
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon((album.library??false)? Icons.favorite : Icons.favorite_border, size: 32),
|
Icon((album.library??false)? Icons.favorite : Icons.favorite_border, size: 32),
|
||||||
|
@ -196,7 +196,7 @@ class _AlbumDetailsState extends State<AlbumDetails> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
MakeAlbumOffline(album: album),
|
MakeAlbumOffline(album: album),
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(Icons.file_download, size: 32.0,),
|
Icon(Icons.file_download, size: 32.0,),
|
||||||
|
@ -248,7 +248,7 @@ class _AlbumDetailsState extends State<AlbumDetails> {
|
||||||
|
|
||||||
class MakeAlbumOffline extends StatefulWidget {
|
class MakeAlbumOffline extends StatefulWidget {
|
||||||
|
|
||||||
Album album;
|
final Album album;
|
||||||
MakeAlbumOffline({Key key, this.album}): super(key: key);
|
MakeAlbumOffline({Key key, this.album}): super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -399,7 +399,7 @@ class ArtistDetails extends StatelessWidget {
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(Icons.favorite, size: 32),
|
Icon(Icons.favorite, size: 32),
|
||||||
|
@ -417,7 +417,7 @@ class ArtistDetails extends StatelessWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if ((artist.radio??false))
|
if ((artist.radio??false))
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(Icons.radio, size: 32),
|
Icon(Icons.radio, size: 32),
|
||||||
|
@ -714,7 +714,7 @@ class _DiscographyScreenState extends State<DiscographyScreen> {
|
||||||
|
|
||||||
class PlaylistDetails extends StatefulWidget {
|
class PlaylistDetails extends StatefulWidget {
|
||||||
|
|
||||||
Playlist playlist;
|
final Playlist playlist;
|
||||||
PlaylistDetails(this.playlist, {Key key}): super(key: key);
|
PlaylistDetails(this.playlist, {Key key}): super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -239,11 +239,11 @@ class DownloadTile extends StatelessWidget {
|
||||||
title: Text('Delete'.i18n),
|
title: Text('Delete'.i18n),
|
||||||
content: Text('Are you sure you want to delete this download?'.i18n),
|
content: Text('Are you sure you want to delete this download?'.i18n),
|
||||||
actions: [
|
actions: [
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('Cancel'.i18n),
|
child: Text('Cancel'.i18n),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('Delete'.i18n),
|
child: Text('Delete'.i18n),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await downloadManager.removeDownload(download.id);
|
await downloadManager.removeDownload(download.id);
|
||||||
|
|
|
@ -50,6 +50,7 @@ class FreezerAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
return Theme(
|
return Theme(
|
||||||
data: ThemeData(primaryColor: (Theme.of(context).brightness == Brightness.light)?Colors.white:Colors.black),
|
data: ThemeData(primaryColor: (Theme.of(context).brightness == Brightness.light)?Colors.white:Colors.black),
|
||||||
child: AppBar(
|
child: AppBar(
|
||||||
|
brightness: Theme.of(context).brightness,
|
||||||
elevation: 0.0,
|
elevation: 0.0,
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
title: Text(
|
title: Text(
|
||||||
|
|
|
@ -180,7 +180,7 @@ class HomepageSectionWidget extends StatelessWidget {
|
||||||
//Has more items
|
//Has more items
|
||||||
if (j == section.items.length) {
|
if (j == section.items.length) {
|
||||||
if (section.hasMore ?? false) {
|
if (section.hasMore ?? false) {
|
||||||
return FlatButton(
|
return TextButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Show more'.i18n,
|
'Show more'.i18n,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
|
|
@ -1,40 +1,44 @@
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:freezer/api/definitions.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:freezer/api/download.dart';
|
|
||||||
import 'package:freezer/api/spotify.dart';
|
|
||||||
import 'package:freezer/settings.dart';
|
import 'package:freezer/settings.dart';
|
||||||
import 'package:freezer/ui/elements.dart';
|
|
||||||
import 'package:freezer/ui/menu.dart';
|
import 'package:freezer/ui/menu.dart';
|
||||||
|
import 'package:freezer/api/importer.dart';
|
||||||
|
import 'package:freezer/api/spotify.dart';
|
||||||
|
import 'package:freezer/ui/elements.dart';
|
||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
|
import 'package:spotify/spotify.dart' as spotify;
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class ImporterScreen extends StatefulWidget {
|
import 'dart:async';
|
||||||
|
|
||||||
|
class SpotifyImporterV1 extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
_ImporterScreenState createState() => _ImporterScreenState();
|
_SpotifyImporterV1State createState() => _SpotifyImporterV1State();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ImporterScreenState extends State<ImporterScreen> {
|
class _SpotifyImporterV1State extends State<SpotifyImporterV1> {
|
||||||
|
|
||||||
String _url;
|
String _url;
|
||||||
bool _error = false;
|
bool _error = false;
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
SpotifyPlaylist _data;
|
SpotifyPlaylist _data;
|
||||||
|
|
||||||
|
//Load URL
|
||||||
Future _load() async {
|
Future _load() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_error = false;
|
_error = false;
|
||||||
_loading = true;
|
_loading = true;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
String uri = await spotify.resolveUrl(_url);
|
String uri = await SpotifyScrapper.resolveUrl(_url);
|
||||||
|
|
||||||
//Error/NonPlaylist
|
//Error/NonPlaylist
|
||||||
if (uri == null || uri.split(':')[1] != 'playlist') {
|
if (uri == null || uri.split(':')[1] != 'playlist') {
|
||||||
throw Exception();
|
throw Exception();
|
||||||
}
|
}
|
||||||
//Load
|
//Load
|
||||||
SpotifyPlaylist data = await spotify.playlist(uri);
|
SpotifyPlaylist data = await SpotifyScrapper.playlist(uri);
|
||||||
setState(() => _data = data);
|
setState(() => _data = data);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -46,7 +50,12 @@ class _ImporterScreenState extends State<ImporterScreen> {
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Start importing
|
||||||
|
Future _start() async {
|
||||||
|
List<ImporterTrack> tracks = _data.toImporter();
|
||||||
|
await importer.start(context, _data.name, _data.description, tracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -109,98 +118,96 @@ class _ImporterScreenState extends State<ImporterScreen> {
|
||||||
title: Text('Error loading URL!'.i18n),
|
title: Text('Error loading URL!'.i18n),
|
||||||
leading: Icon(Icons.error, color: Colors.red,),
|
leading: Icon(Icons.error, color: Colors.red,),
|
||||||
),
|
),
|
||||||
|
//Playlist
|
||||||
if (_data != null)
|
if (_data != null)
|
||||||
ImporterWidget(_data)
|
...[
|
||||||
|
FreezerDivider(),
|
||||||
|
ListTile(
|
||||||
|
title: Text(_data.name),
|
||||||
|
subtitle: Text((_data.description ?? '') == '' ? '${_data.tracks.length} tracks' : _data.description),
|
||||||
|
leading: Image.network(_data.image??'http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg')
|
||||||
|
),
|
||||||
|
ImporterSettings(),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
|
||||||
|
child: ElevatedButton(
|
||||||
|
child: Text('Start import'.i18n),
|
||||||
|
onPressed: () async {
|
||||||
|
await _start();
|
||||||
|
Navigator.of(context).pushReplacement(MaterialPageRoute(
|
||||||
|
builder: (context) => ImporterStatusScreen()
|
||||||
|
));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImporterWidget extends StatefulWidget {
|
class ImporterSettings extends StatefulWidget {
|
||||||
|
|
||||||
final SpotifyPlaylist playlist;
|
|
||||||
ImporterWidget(this.playlist, {Key key}): super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_ImporterWidgetState createState() => _ImporterWidgetState();
|
_ImporterSettingsState createState() => _ImporterSettingsState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ImporterWidgetState extends State<ImporterWidget> {
|
class _ImporterSettingsState extends State<ImporterSettings> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
children: <Widget>[
|
mainAxisSize: MainAxisSize.min,
|
||||||
FreezerDivider(),
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(widget.playlist.name),
|
title: Text('Download imported tracks'.i18n),
|
||||||
subtitle: Text(widget.playlist.description),
|
leading: Switch(
|
||||||
//Default image
|
value: importer.download,
|
||||||
leading: Image.network(widget.playlist.image??'http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg'),
|
onChanged: (v) => setState(() => importer.download = v),
|
||||||
),
|
),
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
children: <Widget>[
|
|
||||||
RaisedButton(
|
|
||||||
child: Text('Convert'.i18n),
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
onPressed: () {
|
|
||||||
spotify.convertPlaylist(widget.playlist);
|
|
||||||
Navigator.of(context).pushReplacement(MaterialPageRoute(
|
|
||||||
builder: (context) => CurrentlyImportingScreen()
|
|
||||||
));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
RaisedButton(
|
|
||||||
child: Text('Download only'.i18n),
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
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()
|
|
||||||
));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
...List.generate(widget.playlist.tracks.length, (i) {
|
|
||||||
SpotifyTrack t = widget.playlist.tracks[i];
|
|
||||||
return ListTile(
|
|
||||||
title: Text(
|
|
||||||
t.title,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
t.artists,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CurrentlyImportingScreen extends StatelessWidget {
|
class ImporterStatusScreen extends StatefulWidget {
|
||||||
|
@override
|
||||||
Widget _stateIcon(TrackImportState s) {
|
_ImporterStatusScreenState createState() => _ImporterStatusScreenState();
|
||||||
switch (s) {
|
|
||||||
case TrackImportState.ERROR:
|
|
||||||
return Icon(Icons.error, color: Colors.red,);
|
|
||||||
case TrackImportState.OK:
|
|
||||||
return Icon(Icons.done, color: Colors.green);
|
|
||||||
default:
|
|
||||||
return Container(width: 0, height: 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ImporterStatusScreenState extends State<ImporterStatusScreen> {
|
||||||
|
|
||||||
|
bool _done = false;
|
||||||
|
StreamSubscription _subscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
//If import done mark as not done, to prevent double routing
|
||||||
|
if (importer.done) {
|
||||||
|
_done = true;
|
||||||
|
importer.done = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update
|
||||||
|
_subscription = importer.updateStream.listen((event) {
|
||||||
|
setState(() {
|
||||||
|
//Unset done so this page doesn't reopen
|
||||||
|
if (importer.done) {
|
||||||
|
_done = true;
|
||||||
|
importer.done = false;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (_subscription != null)
|
||||||
|
_subscription.cancel();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -208,23 +215,11 @@ class CurrentlyImportingScreen extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: FreezerAppBar('Importing...'.i18n),
|
appBar: FreezerAppBar('Importing...'.i18n),
|
||||||
body: StreamBuilder(
|
body: ListView(
|
||||||
stream: spotify.importingStream.stream,
|
children: [
|
||||||
builder: (context, snapshot) {
|
// Spinner
|
||||||
|
if (!_done)
|
||||||
//If not in progress
|
Padding(
|
||||||
if (spotify.importingSpotifyPlaylist == null || spotify.importingSpotifyPlaylist.tracks == null || spotify.doneImporting == null)
|
|
||||||
return Center(child: CircularProgressIndicator(),);
|
|
||||||
if (spotify.doneImporting) spotify.doneImporting = null;
|
|
||||||
|
|
||||||
//Cont OK, error, total
|
|
||||||
int ok = spotify.importingSpotifyPlaylist.tracks.where((t) => t.state == TrackImportState.OK).length;
|
|
||||||
int err = spotify.importingSpotifyPlaylist.tracks.where((t) => t.state == TrackImportState.ERROR).length;
|
|
||||||
int count = spotify.importingSpotifyPlaylist.tracks.length;
|
|
||||||
|
|
||||||
return ListView(
|
|
||||||
children: <Widget>[
|
|
||||||
if (!(spotify.doneImporting??true)) Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
@ -233,6 +228,8 @@ class CurrentlyImportingScreen extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Progress indicator
|
||||||
Container(
|
Container(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
@ -243,7 +240,7 @@ class CurrentlyImportingScreen extends StatelessWidget {
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(Icons.import_export, size: 24.0,),
|
Icon(Icons.import_export, size: 24.0,),
|
||||||
Container(width: 4.0,),
|
Container(width: 4.0,),
|
||||||
Text('${ok+err}/$count', style: TextStyle(fontSize: 24.0),)
|
Text('${importer.ok+importer.error}/${importer.tracks.length}', style: TextStyle(fontSize: 24.0),)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
|
@ -251,7 +248,7 @@ class CurrentlyImportingScreen extends StatelessWidget {
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(Icons.done, size: 24.0,),
|
Icon(Icons.done, size: 24.0,),
|
||||||
Container(width: 4.0,),
|
Container(width: 4.0,),
|
||||||
Text('$ok', style: TextStyle(fontSize: 24.0),)
|
Text('${importer.ok}', style: TextStyle(fontSize: 24.0),)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
|
@ -259,17 +256,17 @@ class CurrentlyImportingScreen extends StatelessWidget {
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(Icons.error, size: 24.0,),
|
Icon(Icons.error, size: 24.0,),
|
||||||
Container(width: 4.0,),
|
Container(width: 4.0,),
|
||||||
Text('$err', style: TextStyle(fontSize: 24.0),),
|
Text('${importer.error}', style: TextStyle(fontSize: 24.0),),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (snapshot.data != null)
|
|
||||||
FlatButton(
|
//When Done
|
||||||
|
if (_done)
|
||||||
|
TextButton(
|
||||||
child: Text('Playlist menu'.i18n),
|
child: Text('Playlist menu'.i18n),
|
||||||
onPressed: () async {
|
onPressed: () {
|
||||||
Playlist p = await deezerAPI.playlist(snapshot.data);
|
|
||||||
p.library = true;
|
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultPlaylistMenu(p);
|
m.defaultPlaylistMenu(importer.playlist);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -277,18 +274,391 @@ class CurrentlyImportingScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
Container(height: 8.0),
|
Container(height: 8.0),
|
||||||
FreezerDivider(),
|
FreezerDivider(),
|
||||||
...List.generate(spotify.importingSpotifyPlaylist.tracks.length, (i) {
|
|
||||||
SpotifyTrack t = spotify.importingSpotifyPlaylist.tracks[i];
|
//Tracks
|
||||||
|
...List.generate(importer.tracks.length, (i) {
|
||||||
|
ImporterTrack t = importer.tracks[i];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
|
leading: t.state.icon,
|
||||||
title: Text(t.title),
|
title: Text(t.title),
|
||||||
subtitle: Text(t.artists),
|
subtitle: Text(
|
||||||
leading: _stateIcon(t.state),
|
t.artists.join(", "),
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
||||||
],
|
],
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SpotifyImporterV2 extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_SpotifyImporterV2State createState() => _SpotifyImporterV2State();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpotifyImporterV2State extends State<SpotifyImporterV2> {
|
||||||
|
|
||||||
|
bool _authorizing = false;
|
||||||
|
String _clientId;
|
||||||
|
String _clientSecret;
|
||||||
|
SpotifyAPIWrapper spotify;
|
||||||
|
|
||||||
|
//Spotify authorization flow
|
||||||
|
Future _authorize() async {
|
||||||
|
setState(() => _authorizing = true);
|
||||||
|
spotify = SpotifyAPIWrapper();
|
||||||
|
await spotify.authorize(_clientId, _clientSecret);
|
||||||
|
//Save credentials
|
||||||
|
settings.spotifyClientId = _clientId;
|
||||||
|
settings.spotifyClientSecret = _clientSecret;
|
||||||
|
await settings.save();
|
||||||
|
setState(() => _authorizing = false);
|
||||||
|
//Redirect
|
||||||
|
Navigator.of(context).pushReplacement(MaterialPageRoute(
|
||||||
|
builder: (context) => SpotifyImporterV2Main(spotify)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_clientId = settings.spotifyClientId;
|
||||||
|
_clientSecret = settings.spotifyClientSecret;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
//Stop server
|
||||||
|
if (spotify != null) {
|
||||||
|
spotify.cancelAuthorize();
|
||||||
|
}
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: FreezerAppBar("Spotify Importer v2".i18n),
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0),
|
||||||
|
child: Text(
|
||||||
|
"This importer requires Spotify Client ID and Client Secret. To obtain them:".i18n,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Text(
|
||||||
|
"1. Go to: developer.spotify.com/dashboard and create an app.".i18n,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
|
||||||
|
child: ElevatedButton(
|
||||||
|
child: Text("Open in Browser".i18n),
|
||||||
|
onPressed: () {
|
||||||
|
launch("https://developer.spotify.com/dashboard");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(height: 16.0),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Text(
|
||||||
|
"2. In the app you just created go to settings, and set the Redirect URL to: ".i18n + "http://localhost:42069",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
|
||||||
|
child: ElevatedButton(
|
||||||
|
child: Text("Copy the Redirect URL".i18n),
|
||||||
|
onPressed: () async {
|
||||||
|
await Clipboard.setData(new ClipboardData(text: "http://localhost:42069"));
|
||||||
|
Fluttertoast.showToast(msg: "Copied".i18n, gravity: ToastGravity.BOTTOM, toastLength: Toast.LENGTH_SHORT);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: TextField(
|
||||||
|
controller: TextEditingController(text: _clientId),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Client ID".i18n
|
||||||
|
),
|
||||||
|
onChanged: (v) => setState(() => _clientId = v),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(width: 16.0),
|
||||||
|
Flexible(
|
||||||
|
child: TextField(
|
||||||
|
controller: TextEditingController(text: _clientSecret),
|
||||||
|
obscureText: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Client Secret".i18n
|
||||||
|
),
|
||||||
|
onChanged: (v) => setState(() => _clientSecret = v),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
|
||||||
|
child: ElevatedButton(
|
||||||
|
child: Text("Authorize".i18n),
|
||||||
|
onPressed: (_clientId != null && _clientSecret != null && !_authorizing)
|
||||||
|
? () => _authorize()
|
||||||
|
: null
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_authorizing)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpotifyImporterV2Main extends StatefulWidget {
|
||||||
|
|
||||||
|
SpotifyAPIWrapper spotify;
|
||||||
|
SpotifyImporterV2Main(this.spotify, {Key key}): super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SpotifyImporterV2MainState createState() => _SpotifyImporterV2MainState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpotifyImporterV2MainState extends State<SpotifyImporterV2Main> {
|
||||||
|
|
||||||
|
String _url;
|
||||||
|
bool _urlLoading = false;
|
||||||
|
spotify.Playlist _urlPlaylist;
|
||||||
|
bool _playlistsLoading = true;
|
||||||
|
List<spotify.PlaylistSimple> _playlists;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_loadPlaylists();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Load playlists
|
||||||
|
Future _loadPlaylists() async {
|
||||||
|
var pages = widget.spotify.spotify.users.playlists(widget.spotify.me.id);
|
||||||
|
_playlists = List.from(await pages.all());
|
||||||
|
setState(() => _playlistsLoading = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _loadUrl() async {
|
||||||
|
setState(() => _urlLoading = true);
|
||||||
|
//Resolve URL
|
||||||
|
try {
|
||||||
|
String uri = await SpotifyScrapper.resolveUrl(_url);
|
||||||
|
//Error/NonPlaylist
|
||||||
|
if (uri == null || uri.split(':')[1] != 'playlist') {
|
||||||
|
throw Exception();
|
||||||
|
}
|
||||||
|
//Get playlist
|
||||||
|
spotify.Playlist playlist = await widget.spotify.spotify.playlists.get(uri.split(":")[2]);
|
||||||
|
setState(() {
|
||||||
|
_urlLoading = false;
|
||||||
|
_urlPlaylist = playlist;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Fluttertoast.showToast(msg: "Invalid/Unsupported URL".i18n, gravity: ToastGravity.BOTTOM, toastLength: Toast.LENGTH_SHORT);
|
||||||
|
setState(() => _urlLoading = false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Future _startImport(String title, String description, String id) async {
|
||||||
|
//Show loading dialog
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => WillPopScope(
|
||||||
|
onWillPop: () => Future.value(false),
|
||||||
|
child: AlertDialog(
|
||||||
|
title: Text("Please wait...".i18n),
|
||||||
|
content: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [CircularProgressIndicator()],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
//Fetch entire playlist
|
||||||
|
var pages = widget.spotify.spotify.playlists.getTracksByPlaylistId(id);
|
||||||
|
var all = await pages.all();
|
||||||
|
//Map to importer track
|
||||||
|
List<ImporterTrack> tracks = all.map((t) => ImporterTrack(t.name, t.artists.map((a) => a.name).toList(), isrc: t.externalIds.isrc)).toList();
|
||||||
|
await importer.start(context, title, description, tracks);
|
||||||
|
//Route
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Navigator.of(context).pushReplacement(MaterialPageRoute(
|
||||||
|
builder: (context) => ImporterStatusScreen()
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
Fluttertoast.showToast(msg: e.toString(), gravity: ToastGravity.BOTTOM, toastLength: Toast.LENGTH_SHORT);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: FreezerAppBar("Spotify Importer v2".i18n),
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||||
|
child: Text(
|
||||||
|
'Logged in as: '.i18n + widget.spotify.me.displayName,
|
||||||
|
maxLines: 1,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18.0,
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FreezerDivider(),
|
||||||
|
Container(height: 4.0),
|
||||||
|
Text(
|
||||||
|
"Options".i18n,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18.0,
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ImporterSettings(),
|
||||||
|
FreezerDivider(),
|
||||||
|
Container(height: 4.0),
|
||||||
|
Text(
|
||||||
|
"Import playlists by URL".i18n,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18.0,
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "URL".i18n
|
||||||
|
),
|
||||||
|
onChanged: (v) => setState(() => _url = v)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.search),
|
||||||
|
onPressed: () => _loadUrl(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
if (_urlLoading)
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 4.0),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_urlPlaylist != null)
|
||||||
|
ListTile(
|
||||||
|
title: Text(_urlPlaylist.name),
|
||||||
|
subtitle: Text(_urlPlaylist.description ?? ''),
|
||||||
|
leading: Image.network(_urlPlaylist.images.first?.url??"http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg")
|
||||||
|
),
|
||||||
|
if (_urlPlaylist != null)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: ElevatedButton(
|
||||||
|
child: Text("Import".i18n),
|
||||||
|
onPressed: () {
|
||||||
|
_startImport(_urlPlaylist.name, _urlPlaylist.description, _urlPlaylist.id);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Playlists
|
||||||
|
FreezerDivider(),
|
||||||
|
Container(height: 4.0),
|
||||||
|
Text(
|
||||||
|
"Playlists".i18n,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18.0,
|
||||||
|
fontWeight: FontWeight.bold
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Container(height: 4.0),
|
||||||
|
if (_playlistsLoading)
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 4.0),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (!_playlistsLoading && _playlists != null)
|
||||||
|
...List.generate(_playlists.length, (i) {
|
||||||
|
spotify.PlaylistSimple p = _playlists[i];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(p.name, maxLines: 1),
|
||||||
|
subtitle: Text(p.owner.displayName, maxLines: 1),
|
||||||
|
leading: Image.network(p.images.first?.url??"http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg"),
|
||||||
|
onTap: () {
|
||||||
|
_startImport(p.name, "", p.id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:freezer/api/cache.dart';
|
import 'package:freezer/api/cache.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:freezer/api/definitions.dart';
|
import 'package:freezer/api/definitions.dart';
|
||||||
|
import 'package:freezer/api/importer.dart';
|
||||||
import 'package:freezer/api/player.dart';
|
import 'package:freezer/api/player.dart';
|
||||||
import 'package:freezer/settings.dart';
|
import 'package:freezer/settings.dart';
|
||||||
import 'package:freezer/ui/details_screens.dart';
|
import 'package:freezer/ui/details_screens.dart';
|
||||||
|
@ -138,16 +139,44 @@ class LibraryScreen extends StatelessWidget {
|
||||||
leading: LeadingIcon(Icons.import_export, color: Color(0xff2ba766)),
|
leading: LeadingIcon(Icons.import_export, color: Color(0xff2ba766)),
|
||||||
subtitle: Text('Import playlists from Spotify'.i18n),
|
subtitle: Text('Import playlists from Spotify'.i18n),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (spotify.doneImporting != null) {
|
//Show progress
|
||||||
Navigator.of(context).push(
|
if (importer.done || importer.busy) {
|
||||||
MaterialPageRoute(builder: (context) => CurrentlyImportingScreen())
|
Navigator.of(context).push(MaterialPageRoute(
|
||||||
);
|
builder: (context) => ImporterStatusScreen()
|
||||||
if (spotify.doneImporting) spotify.doneImporting = null;
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Navigator.of(context).push(
|
//Pick importer dialog
|
||||||
MaterialPageRoute(builder: (context) => ImporterScreen())
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => SimpleDialog(
|
||||||
|
title: Text('Importer'.i18n),
|
||||||
|
children: [
|
||||||
|
ListTile (
|
||||||
|
leading: Icon(FontAwesome5.spotify),
|
||||||
|
title: Text('Spotify v1'.i18n),
|
||||||
|
subtitle: Text('Import Spotify playlists up to 100 tracks without any login.'.i18n),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Navigator.of(context).push(MaterialPageRoute(
|
||||||
|
builder: (context) => SpotifyImporterV1()
|
||||||
|
));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile (
|
||||||
|
leading: Icon(FontAwesome5.spotify),
|
||||||
|
title: Text('Spotify v2'.i18n),
|
||||||
|
subtitle: Text('Import any Spotify playlist, import from own Spotify library. Requires free account.'.i18n),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Navigator.of(context).push(MaterialPageRoute(
|
||||||
|
builder: (context) => SpotifyImporterV2()
|
||||||
|
));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -444,7 +473,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
MakePlaylistOffline(_playlist),
|
MakePlaylistOffline(_playlist),
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(Icons.file_download, size: 32.0,),
|
Icon(Icons.file_download, size: 32.0,),
|
||||||
|
|
|
@ -13,7 +13,7 @@ import 'home_screen.dart';
|
||||||
|
|
||||||
class LoginWidget extends StatefulWidget {
|
class LoginWidget extends StatefulWidget {
|
||||||
|
|
||||||
Function callback;
|
final Function callback;
|
||||||
LoginWidget({this.callback, Key key}): super(key: key);
|
LoginWidget({this.callback, Key key}): super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -95,7 +95,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('Dismiss'.i18n),
|
child: Text('Dismiss'.i18n),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
@ -169,14 +169,6 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(height: 16.0,),
|
|
||||||
Text(
|
|
||||||
'Welcome to'.i18n,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16.0
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FreezerTitle(),
|
FreezerTitle(),
|
||||||
Container(height: 8.0,),
|
Container(height: 8.0,),
|
||||||
Text(
|
Text(
|
||||||
|
@ -190,7 +182,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||||
//Email login dialog
|
//Email login dialog
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 32.0),
|
padding: EdgeInsets.symmetric(horizontal: 32.0),
|
||||||
child: OutlineButton(
|
child: OutlinedButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Login using email'.i18n,
|
'Login using email'.i18n,
|
||||||
),
|
),
|
||||||
|
@ -204,7 +196,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 32.0),
|
padding: EdgeInsets.symmetric(horizontal: 32.0),
|
||||||
child: OutlineButton(
|
child: OutlinedButton(
|
||||||
child: Text('Login using browser'.i18n),
|
child: Text('Login using browser'.i18n),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
|
@ -215,7 +207,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 32.0),
|
padding: EdgeInsets.symmetric(horizontal: 32.0),
|
||||||
child: OutlineButton(
|
child: OutlinedButton(
|
||||||
child: Text('Login using token'.i18n),
|
child: Text('Login using token'.i18n),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
|
@ -238,7 +230,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('Save'.i18n),
|
child: Text('Save'.i18n),
|
||||||
onPressed: () => goARL(null, _controller),
|
onPressed: () => goARL(null, _controller),
|
||||||
)
|
)
|
||||||
|
@ -259,7 +251,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 32.0),
|
padding: EdgeInsets.symmetric(horizontal: 32.0),
|
||||||
child: OutlineButton(
|
child: OutlinedButton(
|
||||||
child: Text('Open in browser'.i18n),
|
child: Text('Open in browser'.i18n),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
InAppBrowser.openWithSystemBrowser(url: 'https://deezer.com/register');
|
InAppBrowser.openWithSystemBrowser(url: 'https://deezer.com/register');
|
||||||
|
@ -287,7 +279,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||||
|
|
||||||
class LoginBrowser extends StatelessWidget {
|
class LoginBrowser extends StatelessWidget {
|
||||||
|
|
||||||
Function updateParent;
|
final Function updateParent;
|
||||||
LoginBrowser(this.updateParent);
|
LoginBrowser(this.updateParent);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -331,7 +323,7 @@ class LoginBrowser extends StatelessWidget {
|
||||||
|
|
||||||
class EmailLogin extends StatefulWidget {
|
class EmailLogin extends StatefulWidget {
|
||||||
|
|
||||||
Function callback;
|
final Function callback;
|
||||||
EmailLogin(this.callback, {Key key}): super(key: key);
|
EmailLogin(this.callback, {Key key}): super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -697,14 +697,14 @@ class _SleepTimerDialogState extends State<SleepTimerDialog> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('Dismiss'.i18n),
|
child: Text('Dismiss'.i18n),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (cache.sleepTimer != null)
|
if (cache.sleepTimer != null)
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('Cancel current timer'.i18n),
|
child: Text('Cancel current timer'.i18n),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
cache.sleepTimer.cancel();
|
cache.sleepTimer.cancel();
|
||||||
|
@ -714,7 +714,7 @@ class _SleepTimerDialogState extends State<SleepTimerDialog> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('Save'.i18n),
|
child: Text('Save'.i18n),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Duration duration = Duration(hours: hours, minutes: minutes);
|
Duration duration = Duration(hours: hours, minutes: minutes);
|
||||||
|
@ -891,11 +891,11 @@ class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('Cancel'.i18n),
|
child: Text('Cancel'.i18n),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text(edit ? 'Update'.i18n : 'Create'.i18n),
|
child: Text(edit ? 'Update'.i18n : 'Create'.i18n),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (edit) {
|
if (edit) {
|
||||||
|
|
|
@ -152,36 +152,57 @@ class PrevNextButton extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PlayPauseButton extends StatefulWidget {
|
||||||
class PlayPauseButton extends StatelessWidget {
|
|
||||||
|
|
||||||
final double size;
|
final double size;
|
||||||
PlayPauseButton(this.size);
|
PlayPauseButton(this.size, {Key key}): super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_PlayPauseButtonState createState() => _PlayPauseButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlayPauseButtonState extends State<PlayPauseButton> with SingleTickerProviderStateMixin {
|
||||||
|
|
||||||
|
AnimationController _controller;
|
||||||
|
Animation<double> _animation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_controller = AnimationController(vsync: this, duration: Duration(milliseconds: 200));
|
||||||
|
_animation = Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
return StreamBuilder(
|
return StreamBuilder(
|
||||||
stream: AudioService.playbackStateStream,
|
stream: AudioService.playbackStateStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
//Playing
|
//Animated icon by pato05
|
||||||
if (AudioService.playbackState?.playing??false) {
|
bool _playing = AudioService.playbackState?.playing ?? false;
|
||||||
return IconButton(
|
if (_playing || AudioService.playbackState?.processingState == AudioProcessingState.ready ||
|
||||||
iconSize: this.size,
|
AudioService.playbackState?.processingState == AudioProcessingState.none) {
|
||||||
icon: Icon(Icons.pause),
|
if (_playing)
|
||||||
onPressed: () => AudioService.pause()
|
_controller.forward();
|
||||||
);
|
else
|
||||||
}
|
_controller.reverse();
|
||||||
|
|
||||||
//Paused
|
|
||||||
if ((!(AudioService.playbackState?.playing??false) &&
|
|
||||||
AudioService.playbackState.processingState == AudioProcessingState.ready) ||
|
|
||||||
//None state (stopped)
|
|
||||||
AudioService.playbackState.processingState == AudioProcessingState.none) {
|
|
||||||
return IconButton(
|
return IconButton(
|
||||||
iconSize: this.size,
|
splashRadius: widget.size,
|
||||||
icon: Icon(Icons.play_arrow),
|
icon: AnimatedIcon(
|
||||||
onPressed: () => AudioService.play()
|
icon: AnimatedIcons.play_pause,
|
||||||
|
progress: _animation,
|
||||||
|
),
|
||||||
|
iconSize: widget.size,
|
||||||
|
onPressed: _playing
|
||||||
|
? () => AudioService.pause()
|
||||||
|
: () => AudioService.play()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,12 +211,12 @@ class PlayPauseButton extends StatelessWidget {
|
||||||
case AudioProcessingState.error:
|
case AudioProcessingState.error:
|
||||||
case AudioProcessingState.none:
|
case AudioProcessingState.none:
|
||||||
case AudioProcessingState.stopped:
|
case AudioProcessingState.stopped:
|
||||||
return Container(width: this.size, height: this.size);
|
return Container(width: widget.size, height: widget.size);
|
||||||
//Loading, connecting, rewinding...
|
//Loading, connecting, rewinding...
|
||||||
default:
|
default:
|
||||||
return Container(
|
return Container(
|
||||||
width: this.size,
|
width: widget.size,
|
||||||
height: this.size,
|
height: widget.size,
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -203,3 +224,6 @@ class PlayPauseButton extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -439,7 +439,7 @@ class _QualityInfoWidgetState extends State<QualityInfoWidget> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlatButton(
|
return TextButton(
|
||||||
child: Text(value),
|
child: Text(value),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) => QualitySettings()));
|
Navigator.of(context).push(MaterialPageRoute(builder: (context) => QualitySettings()));
|
||||||
|
|
|
@ -106,7 +106,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
title: Text('Language'.i18n),
|
title: Text('Language'.i18n),
|
||||||
content: Text('Language changed, please restart Freezer to apply!'.i18n),
|
content: Text('Language changed, please restart Freezer to apply!'.i18n),
|
||||||
actions: [
|
actions: [
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('OK'),
|
child: Text('OK'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
@ -387,7 +387,7 @@ class _FontSelectorState extends State<FontSelector> {
|
||||||
title: Text('Warning'.i18n),
|
title: Text('Warning'.i18n),
|
||||||
content: Text("This app isn't made for supporting many fonts, it can break layouts and overflow. Use at your own risk!".i18n),
|
content: Text("This app isn't made for supporting many fonts, it can break layouts and overflow. Use at your own risk!".i18n),
|
||||||
actions: [
|
actions: [
|
||||||
FlatButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
setState(() => settings.font = font);
|
setState(() => settings.font = font);
|
||||||
await settings.save();
|
await settings.save();
|
||||||
|
@ -398,7 +398,7 @@ class _FontSelectorState extends State<FontSelector> {
|
||||||
},
|
},
|
||||||
child: Text('Apply'.i18n),
|
child: Text('Apply'.i18n),
|
||||||
),
|
),
|
||||||
FlatButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
widget.callback();
|
widget.callback();
|
||||||
|
@ -701,11 +701,11 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
||||||
// ),
|
// ),
|
||||||
// ),
|
// ),
|
||||||
// actions: [
|
// actions: [
|
||||||
// FlatButton(
|
// TextButton(
|
||||||
// child: Text('Cancel'.i18n),
|
// child: Text('Cancel'.i18n),
|
||||||
// onPressed: () => Navigator.of(context).pop(),
|
// onPressed: () => Navigator.of(context).pop(),
|
||||||
// ),
|
// ),
|
||||||
// FlatButton(
|
// TextButton(
|
||||||
// child: Text('Reset'.i18n),
|
// child: Text('Reset'.i18n),
|
||||||
// onPressed: () async {
|
// onPressed: () async {
|
||||||
// setState(() {
|
// setState(() {
|
||||||
|
@ -715,7 +715,7 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
||||||
// Navigator.of(context).pop();
|
// Navigator.of(context).pop();
|
||||||
// },
|
// },
|
||||||
// ),
|
// ),
|
||||||
// FlatButton(
|
// TextButton(
|
||||||
// child: Text('Save'.i18n),
|
// child: Text('Save'.i18n),
|
||||||
// onPressed: () async {
|
// onPressed: () async {
|
||||||
// setState(() {
|
// setState(() {
|
||||||
|
@ -739,8 +739,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
||||||
|
|
||||||
class FilenameTemplateDialog extends StatefulWidget {
|
class FilenameTemplateDialog extends StatefulWidget {
|
||||||
|
|
||||||
String initial;
|
final String initial;
|
||||||
Function onSave;
|
final Function onSave;
|
||||||
FilenameTemplateDialog(this.initial, this.onSave, {Key key}): super(key: key);
|
FilenameTemplateDialog(this.initial, this.onSave, {Key key}): super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -782,22 +782,22 @@ class _FilenameTemplateDialogState extends State<FilenameTemplateDialog> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('Cancel'.i18n),
|
child: Text('Cancel'.i18n),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('Reset'.i18n),
|
child: Text('Reset'.i18n),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_controller.value = _controller.value.copyWith(text: '%artist% - %title%');
|
_controller.value = _controller.value.copyWith(text: '%artist% - %title%');
|
||||||
_new = '%artist% - %title%';
|
_new = '%artist% - %title%';
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('Clear'.i18n),
|
child: Text('Clear'.i18n),
|
||||||
onPressed: () => _controller.clear(),
|
onPressed: () => _controller.clear(),
|
||||||
),
|
),
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('Save'.i18n),
|
child: Text('Save'.i18n),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
widget.onSave(_new);
|
widget.onSave(_new);
|
||||||
|
@ -907,7 +907,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||||||
title: Text('Warning'.i18n),
|
title: Text('Warning'.i18n),
|
||||||
content: Text('Using too many concurrent downloads on older/weaker devices might cause crashes!'.i18n),
|
content: Text('Using too many concurrent downloads on older/weaker devices might cause crashes!'.i18n),
|
||||||
actions: [
|
actions: [
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('Dismiss'.i18n),
|
child: Text('Dismiss'.i18n),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
)
|
)
|
||||||
|
@ -1251,18 +1251,18 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
||||||
// content: Text('Due to plugin incompatibility, login using browser is unavailable without restart.'.i18n),
|
// content: Text('Due to plugin incompatibility, login using browser is unavailable without restart.'.i18n),
|
||||||
content: Text('Restart of app is required to properly log out!'.i18n),
|
content: Text('Restart of app is required to properly log out!'.i18n),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('Cancel'.i18n),
|
child: Text('Cancel'.i18n),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
// FlatButton(
|
// TextButton(
|
||||||
// child: Text('(ARL ONLY) Continue'.i18n),
|
// child: Text('(ARL ONLY) Continue'.i18n),
|
||||||
// onPressed: () async {
|
// onPressed: () async {
|
||||||
// await logOut();
|
// await logOut();
|
||||||
// Navigator.of(context).pop();
|
// Navigator.of(context).pop();
|
||||||
// },
|
// },
|
||||||
// ),
|
// ),
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('Log out & Exit'.i18n),
|
child: Text('Log out & Exit'.i18n),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
try {AudioService.stop();} catch (e) {}
|
try {AudioService.stop();} catch (e) {}
|
||||||
|
@ -1329,11 +1329,11 @@ class _LastFMLoginState extends State<LastFMLogin> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('Cancel'.i18n),
|
child: Text('Cancel'.i18n),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
FlatButton(
|
TextButton(
|
||||||
child: Text('Login'.i18n),
|
child: Text('Login'.i18n),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
LastFM last;
|
LastFM last;
|
||||||
|
@ -1577,7 +1577,7 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
||||||
subtitle: Text('To get latest releases'.i18n),
|
subtitle: Text('To get latest releases'.i18n),
|
||||||
leading: Icon(FontAwesome5.telegram, color: Color(0xFF27A2DF), size: 36.0),
|
leading: Icon(FontAwesome5.telegram, color: Color(0xFF27A2DF), size: 36.0),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launch('https://t.me/freezereleases');
|
launch('https://t.me/joinchat/Se4zLEBvjS1NCiY9');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
@ -1601,7 +1601,7 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
||||||
subtitle: Text('Source code, report issues there.'.i18n),
|
subtitle: Text('Source code, report issues there.'.i18n),
|
||||||
leading: Icon(Icons.code, color: Colors.green, size: 36.0),
|
leading: Icon(Icons.code, color: Colors.green, size: 36.0),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launch('https://git.rip/freezer/');
|
launch('https://git.freezer.life/exttex/freezer');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|
|
@ -168,7 +168,7 @@ class _UpdaterScreenState extends State<UpdaterScreen> {
|
||||||
//Available download
|
//Available download
|
||||||
if (_versionDownload != null)
|
if (_versionDownload != null)
|
||||||
Column(children: [
|
Column(children: [
|
||||||
RaisedButton(
|
ElevatedButton(
|
||||||
child: Text('Download'.i18n + ' (${_versionDownload.version})'),
|
child: Text('Download'.i18n + ' (${_versionDownload.version})'),
|
||||||
onPressed: _buttonEnabled ? () {
|
onPressed: _buttonEnabled ? () {
|
||||||
setState(() => _buttonEnabled = false);
|
setState(() => _buttonEnabled = false);
|
||||||
|
|
14
pubspec.lock
14
pubspec.lock
|
@ -625,6 +625,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
oauth2:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: oauth2
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.3"
|
||||||
octo_image:
|
octo_image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -854,6 +861,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.0"
|
version: "1.8.0"
|
||||||
|
spotify:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: spotify
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.1"
|
||||||
sprintf:
|
sprintf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -27,6 +27,7 @@ dependencies:
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
spotify: ^0.5.1
|
||||||
flutter_displaymode: ^0.1.1
|
flutter_displaymode: ^0.1.1
|
||||||
crypto: ^2.1.5
|
crypto: ^2.1.5
|
||||||
http: ^0.12.2
|
http: ^0.12.2
|
||||||
|
|
Loading…
Reference in New Issue