0.6.2 - Spotify albums/tracks, album art gradient, languages, minor fixes
This commit is contained in:
parent
f877aa9d7b
commit
e9d97986b5
|
@ -25,7 +25,7 @@
|
||||||
android:name=".DownloadService"
|
android:name=".DownloadService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:process=':downloads'></service>
|
android:process="f.f.freezer.DownloadService" ></service>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
|
|
@ -352,7 +352,10 @@ public class Deezer {
|
||||||
//Genres
|
//Genres
|
||||||
String genres = "";
|
String genres = "";
|
||||||
for (int i=0; i<publicAlbum.getJSONObject("genres").getJSONArray("data").length(); i++) {
|
for (int i=0; i<publicAlbum.getJSONObject("genres").getJSONArray("data").length(); i++) {
|
||||||
genres += ", " + publicAlbum.getJSONObject("genres").getJSONArray("data").getJSONObject(0).getString("name");
|
String genre = publicAlbum.getJSONObject("genres").getJSONArray("data").getJSONObject(0).getString("name");
|
||||||
|
if (!genres.contains(genre)) {
|
||||||
|
genres += ", " + genre;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (genres.length() > 2)
|
if (genres.length() > 2)
|
||||||
tag.setField(FieldKey.GENRE, genres.substring(2));
|
tag.setField(FieldKey.GENRE, genres.substring(2));
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:freezer/api/definitions.dart';
|
import 'package:freezer/api/definitions.dart';
|
||||||
|
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;
|
||||||
|
|
||||||
|
@ -114,6 +115,23 @@ class DeezerAPI {
|
||||||
String newUrl = response.headers['location'];
|
String newUrl = response.headers['location'];
|
||||||
return parseLink(newUrl);
|
return parseLink(newUrl);
|
||||||
}
|
}
|
||||||
|
//Spotify
|
||||||
|
if (uri.host == 'open.spotify.com') {
|
||||||
|
if (uri.pathSegments.length < 2) return null;
|
||||||
|
String spotifyUri = 'spotify:' + uri.pathSegments.sublist(0, 2).join(':');
|
||||||
|
try {
|
||||||
|
//Tracks
|
||||||
|
if (uri.pathSegments[0] == 'track') {
|
||||||
|
String id = await spotify.convertTrack(spotifyUri);
|
||||||
|
return DeezerLinkResponse(type: DeezerLinkType.TRACK, id: id);
|
||||||
|
}
|
||||||
|
//Albums
|
||||||
|
if (uri.pathSegments[0] == 'album') {
|
||||||
|
String id = await spotify.convertAlbum(spotifyUri);
|
||||||
|
return DeezerLinkResponse(type: DeezerLinkType.ALBUM, id: id);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Search
|
//Search
|
||||||
|
|
|
@ -49,8 +49,8 @@ Map<String, dynamic> _$TrackToJson(Track instance) => <String, dynamic>{
|
||||||
'favorite': instance.favorite,
|
'favorite': instance.favorite,
|
||||||
'diskNumber': instance.diskNumber,
|
'diskNumber': instance.diskNumber,
|
||||||
'explicit': instance.explicit,
|
'explicit': instance.explicit,
|
||||||
'playbackDetails': instance.playbackDetails,
|
|
||||||
'favoriteDate': instance.favoriteDate,
|
'favoriteDate': instance.favoriteDate,
|
||||||
|
'playbackDetails': instance.playbackDetails,
|
||||||
};
|
};
|
||||||
|
|
||||||
Album _$AlbumFromJson(Map<String, dynamic> json) {
|
Album _$AlbumFromJson(Map<String, dynamic> json) {
|
||||||
|
@ -73,7 +73,7 @@ Album _$AlbumFromJson(Map<String, dynamic> json) {
|
||||||
library: json['library'] as bool,
|
library: json['library'] as bool,
|
||||||
type: _$enumDecodeNullable(_$AlbumTypeEnumMap, json['type']),
|
type: _$enumDecodeNullable(_$AlbumTypeEnumMap, json['type']),
|
||||||
releaseDate: json['releaseDate'] as String,
|
releaseDate: json['releaseDate'] as String,
|
||||||
favoriteDate: json['favoriteDate'] as String
|
favoriteDate: json['favoriteDate'] as String,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ Map<String, dynamic> _$AlbumToJson(Album instance) => <String, dynamic>{
|
||||||
'library': instance.library,
|
'library': instance.library,
|
||||||
'type': _$AlbumTypeEnumMap[instance.type],
|
'type': _$AlbumTypeEnumMap[instance.type],
|
||||||
'releaseDate': instance.releaseDate,
|
'releaseDate': instance.releaseDate,
|
||||||
'favoriteDate': instance.favoriteDate
|
'favoriteDate': instance.favoriteDate,
|
||||||
};
|
};
|
||||||
|
|
||||||
T _$enumDecode<T>(
|
T _$enumDecode<T>(
|
||||||
|
@ -149,6 +149,7 @@ Artist _$ArtistFromJson(Map<String, dynamic> json) {
|
||||||
offline: json['offline'] as bool,
|
offline: json['offline'] as bool,
|
||||||
library: json['library'] as bool,
|
library: json['library'] as bool,
|
||||||
radio: json['radio'] as bool,
|
radio: json['radio'] as bool,
|
||||||
|
favoriteDate: json['favoriteDate'] as String,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,6 +164,7 @@ Map<String, dynamic> _$ArtistToJson(Artist instance) => <String, dynamic>{
|
||||||
'offline': instance.offline,
|
'offline': instance.offline,
|
||||||
'library': instance.library,
|
'library': instance.library,
|
||||||
'radio': instance.radio,
|
'radio': instance.radio,
|
||||||
|
'favoriteDate': instance.favoriteDate,
|
||||||
};
|
};
|
||||||
|
|
||||||
Playlist _$PlaylistFromJson(Map<String, dynamic> json) {
|
Playlist _$PlaylistFromJson(Map<String, dynamic> json) {
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:audio_session/audio_session.dart';
|
import 'package:audio_session/audio_session.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
@ -11,6 +9,7 @@ import 'package:connectivity/connectivity.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
|
import 'package:scrobblenaut/scrobblenaut.dart';
|
||||||
|
|
||||||
import 'definitions.dart';
|
import 'definitions.dart';
|
||||||
import '../settings.dart';
|
import '../settings.dart';
|
||||||
|
@ -28,6 +27,8 @@ class PlayerHelper {
|
||||||
StreamSubscription _playbackStateStreamSubscription;
|
StreamSubscription _playbackStateStreamSubscription;
|
||||||
QueueSource queueSource;
|
QueueSource queueSource;
|
||||||
LoopMode repeatType = LoopMode.off;
|
LoopMode repeatType = LoopMode.off;
|
||||||
|
Timer _timer;
|
||||||
|
Scrobblenaut scrobblenaut;
|
||||||
//Find queue index by id
|
//Find queue index by id
|
||||||
int get queueIndex => AudioService.queue == null ? 0 : AudioService.queue.indexWhere((mi) => mi.id == AudioService.currentMediaItem?.id??'Random string so it returns -1');
|
int get queueIndex => AudioService.queue == null ? 0 : AudioService.queue.indexWhere((mi) => mi.id == AudioService.currentMediaItem?.id??'Random string so it returns -1');
|
||||||
|
|
||||||
|
@ -63,18 +64,6 @@ class PlayerHelper {
|
||||||
await androidAuto.playItem(event['id']);
|
await androidAuto.playItem(event['id']);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_playbackStateStreamSubscription = AudioService.playbackStateStream.listen((event) {
|
|
||||||
//Log song (if allowed)
|
|
||||||
if (event == null) return;
|
|
||||||
if (event.processingState == AudioProcessingState.ready && event.playing) {
|
|
||||||
if (settings.logListen) {
|
|
||||||
//Check if duplicate
|
|
||||||
if (cache.loggedTrackId == AudioService.currentMediaItem.id) return;
|
|
||||||
cache.loggedTrackId = AudioService.currentMediaItem.id;
|
|
||||||
deezerAPI.logListen(AudioService.currentMediaItem.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
_mediaItemSubscription = AudioService.currentMediaItemStream.listen((event) {
|
_mediaItemSubscription = AudioService.currentMediaItemStream.listen((event) {
|
||||||
if (event == null) return;
|
if (event == null) return;
|
||||||
//Save queue
|
//Save queue
|
||||||
|
@ -86,6 +75,31 @@ class PlayerHelper {
|
||||||
cache.save();
|
cache.save();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Logging listen timer
|
||||||
|
_timer = Timer.periodic(Duration(seconds: 2), (timer) async {
|
||||||
|
if (AudioService.currentMediaItem == null || !AudioService.playbackState.playing) return;
|
||||||
|
if (AudioService.playbackState.currentPosition.inSeconds > (AudioService.currentMediaItem.duration.inSeconds * 0.75)) {
|
||||||
|
if (cache.loggedTrackId == AudioService.currentMediaItem.id) return;
|
||||||
|
cache.loggedTrackId = AudioService.currentMediaItem.id;
|
||||||
|
await cache.save();
|
||||||
|
|
||||||
|
//Log to Deezer
|
||||||
|
if (settings.logListen) {
|
||||||
|
deezerAPI.logListen(AudioService.currentMediaItem.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
//LastFM
|
||||||
|
if (scrobblenaut != null) {
|
||||||
|
await scrobblenaut.track.scrobble(
|
||||||
|
track: AudioService.currentMediaItem.title,
|
||||||
|
artist: AudioService.currentMediaItem.artist,
|
||||||
|
album: AudioService.currentMediaItem.album,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
//Start audio_service
|
//Start audio_service
|
||||||
await startService();
|
await startService();
|
||||||
}
|
}
|
||||||
|
@ -108,6 +122,19 @@ class PlayerHelper {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future authorizeLastFM() async {
|
||||||
|
if (settings.lastFMUsername == null || settings.lastFMPassword == null) return;
|
||||||
|
try {
|
||||||
|
LastFM lastFM = await LastFM.authenticateWithPasswordHash(
|
||||||
|
apiKey: 'b6ab5ae967bcd8b10b23f68f42493829',
|
||||||
|
apiSecret: '861b0dff9a8a574bec747f9dab8b82bf',
|
||||||
|
username: settings.lastFMUsername,
|
||||||
|
passwordHash: settings.lastFMPassword
|
||||||
|
);
|
||||||
|
scrobblenaut = Scrobblenaut(lastFM: lastFM);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
Future toggleShuffle() async {
|
Future toggleShuffle() async {
|
||||||
await AudioService.customAction('shuffle');
|
await AudioService.customAction('shuffle');
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,13 @@ class SpotifyAPI {
|
||||||
//Parse
|
//Parse
|
||||||
dom.Document document = parse(response.body);
|
dom.Document document = parse(response.body);
|
||||||
dom.Element element = document.getElementById('resource');
|
dom.Element element = document.getElementById('resource');
|
||||||
|
|
||||||
|
//Some are URL encoded
|
||||||
|
try {
|
||||||
return jsonDecode(element.innerHtml);
|
return jsonDecode(element.innerHtml);
|
||||||
|
} catch (e) {
|
||||||
|
return jsonDecode(Uri.decodeComponent(element.innerHtml));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SpotifyPlaylist> playlist(String uri) async {
|
Future<SpotifyPlaylist> playlist(String uri) async {
|
||||||
|
@ -50,6 +56,21 @@ class SpotifyAPI {
|
||||||
return playlist;
|
return playlist;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Get Deezer track ID from Spotify URI
|
||||||
|
Future<String> convertTrack(String uri) async {
|
||||||
|
Map data = await getEmbedData(getEmbedUrl(uri));
|
||||||
|
SpotifyTrack track = SpotifyTrack.fromJson(data);
|
||||||
|
Map deezer = await deezerAPI.callPublicApi('track/isrc:' + track.isrc);
|
||||||
|
return deezer['id'].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get Deezer album ID by UPC
|
||||||
|
Future<String> convertAlbum(String uri) async {
|
||||||
|
Map data = await getEmbedData(getEmbedUrl(uri));
|
||||||
|
SpotifyAlbum album = SpotifyAlbum.fromJson(data);
|
||||||
|
Map deezer = await deezerAPI.callPublicApi('album/upc:' + album.upc);
|
||||||
|
return deezer['id'].toString();
|
||||||
|
}
|
||||||
|
|
||||||
Future convertPlaylist(SpotifyPlaylist playlist, {bool downloadOnly = false, BuildContext context, AudioQuality quality}) async {
|
Future convertPlaylist(SpotifyPlaylist playlist, {bool downloadOnly = false, BuildContext context, AudioQuality quality}) async {
|
||||||
doneImporting = false;
|
doneImporting = false;
|
||||||
|
@ -132,6 +153,17 @@ class SpotifyPlaylist {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SpotifyAlbum {
|
||||||
|
String upc;
|
||||||
|
|
||||||
|
SpotifyAlbum({this.upc});
|
||||||
|
|
||||||
|
//JSON
|
||||||
|
factory SpotifyAlbum.fromJson(Map json) => SpotifyAlbum(
|
||||||
|
upc: json['external_ids']['upc']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
enum TrackImportState {
|
enum TrackImportState {
|
||||||
NONE,
|
NONE,
|
||||||
ERROR,
|
ERROR,
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -259,6 +259,18 @@ const language_en_us = {
|
||||||
|
|
||||||
//0.6.1 Strings:
|
//0.6.1 Strings:
|
||||||
"Search history": "Search history",
|
"Search history": "Search history",
|
||||||
"Clear search history": "Clear search history"
|
"Clear search history": "Clear search history",
|
||||||
|
|
||||||
|
//0.6.2 Strings:
|
||||||
|
"LastFM": "LastFM",
|
||||||
|
"Login to enable scrobbling.": "Login to enable scrobbling.",
|
||||||
|
"Login to LastFM": "Login to LastFM",
|
||||||
|
"Username": "Username",
|
||||||
|
"Password": "Password",
|
||||||
|
"Login": "Login",
|
||||||
|
"Authorization error!": "Authorization error!",
|
||||||
|
"Logged out!": "Logged out!",
|
||||||
|
"Lyrics": "Lyrics",
|
||||||
|
"Player gradient background": "Player gradient background"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -38,6 +38,9 @@ void main() async {
|
||||||
await downloadManager.init();
|
await downloadManager.init();
|
||||||
cache = await Cache.load();
|
cache = await Cache.load();
|
||||||
|
|
||||||
|
//Do on BG
|
||||||
|
playerHelper.authorizeLastFM();
|
||||||
|
|
||||||
runApp(FreezerApp());
|
runApp(FreezerApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -73,6 +73,8 @@ class Settings {
|
||||||
Themes theme;
|
Themes theme;
|
||||||
@JsonKey(defaultValue: false)
|
@JsonKey(defaultValue: false)
|
||||||
bool useSystemTheme;
|
bool useSystemTheme;
|
||||||
|
@JsonKey(defaultValue: true)
|
||||||
|
bool colorGradientBackground;
|
||||||
|
|
||||||
//Colors
|
//Colors
|
||||||
@JsonKey(toJson: _colorToJson, fromJson: _colorFromJson)
|
@JsonKey(toJson: _colorToJson, fromJson: _colorFromJson)
|
||||||
|
@ -95,6 +97,13 @@ class Settings {
|
||||||
@JsonKey(defaultValue: null)
|
@JsonKey(defaultValue: null)
|
||||||
String proxyAddress;
|
String proxyAddress;
|
||||||
|
|
||||||
|
//LastFM
|
||||||
|
@JsonKey(defaultValue: null)
|
||||||
|
String lastFMUsername;
|
||||||
|
@JsonKey(defaultValue: null)
|
||||||
|
String lastFMPassword;
|
||||||
|
|
||||||
|
|
||||||
Settings({this.downloadPath, this.arl});
|
Settings({this.downloadPath, this.arl});
|
||||||
|
|
||||||
ThemeData get themeData {
|
ThemeData get themeData {
|
||||||
|
|
|
@ -40,12 +40,15 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
|
||||||
..theme =
|
..theme =
|
||||||
_$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Dark
|
_$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Dark
|
||||||
..useSystemTheme = json['useSystemTheme'] as bool ?? false
|
..useSystemTheme = json['useSystemTheme'] as bool ?? false
|
||||||
|
..colorGradientBackground = json['colorGradientBackground'] as bool ?? true
|
||||||
..primaryColor = Settings._colorFromJson(json['primaryColor'] as int)
|
..primaryColor = Settings._colorFromJson(json['primaryColor'] as int)
|
||||||
..useArtColor = json['useArtColor'] as bool ?? false
|
..useArtColor = json['useArtColor'] as bool ?? false
|
||||||
..deezerLanguage = json['deezerLanguage'] as String ?? 'en'
|
..deezerLanguage = json['deezerLanguage'] as String ?? 'en'
|
||||||
..deezerCountry = json['deezerCountry'] as String ?? 'US'
|
..deezerCountry = json['deezerCountry'] as String ?? 'US'
|
||||||
..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
|
||||||
|
..lastFMPassword = json['lastFMPassword'] as String;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
|
Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
|
||||||
|
@ -70,12 +73,15 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
|
||||||
'nomediaFiles': instance.nomediaFiles,
|
'nomediaFiles': instance.nomediaFiles,
|
||||||
'theme': _$ThemesEnumMap[instance.theme],
|
'theme': _$ThemesEnumMap[instance.theme],
|
||||||
'useSystemTheme': instance.useSystemTheme,
|
'useSystemTheme': instance.useSystemTheme,
|
||||||
|
'colorGradientBackground': instance.colorGradientBackground,
|
||||||
'primaryColor': Settings._colorToJson(instance.primaryColor),
|
'primaryColor': Settings._colorToJson(instance.primaryColor),
|
||||||
'useArtColor': instance.useArtColor,
|
'useArtColor': instance.useArtColor,
|
||||||
'deezerLanguage': instance.deezerLanguage,
|
'deezerLanguage': instance.deezerLanguage,
|
||||||
'deezerCountry': instance.deezerCountry,
|
'deezerCountry': instance.deezerCountry,
|
||||||
'logListen': instance.logListen,
|
'logListen': instance.logListen,
|
||||||
'proxyAddress': instance.proxyAddress,
|
'proxyAddress': instance.proxyAddress,
|
||||||
|
'lastFMUsername': instance.lastFMUsername,
|
||||||
|
'lastFMPassword': instance.lastFMPassword,
|
||||||
};
|
};
|
||||||
|
|
||||||
T _$enumDecode<T>(
|
T _$enumDecode<T>(
|
||||||
|
|
|
@ -25,6 +25,7 @@ const supportedLocales = [
|
||||||
const Locale('hu', 'HU'),
|
const Locale('hu', 'HU'),
|
||||||
const Locale('ur', 'PK'),
|
const Locale('ur', 'PK'),
|
||||||
const Locale('hi', 'IN'),
|
const Locale('hi', 'IN'),
|
||||||
|
const Locale('sk', 'SK'),
|
||||||
const Locale('fil', 'PH')
|
const Locale('fil', 'PH')
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:audio_service/audio_service.dart';
|
||||||
|
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/ui/elements.dart';
|
||||||
|
import 'package:freezer/translations.i18n.dart';
|
||||||
|
import 'package:freezer/ui/error.dart';
|
||||||
|
|
||||||
|
class LyricsScreen extends StatefulWidget {
|
||||||
|
|
||||||
|
final Lyrics lyrics;
|
||||||
|
final String trackId;
|
||||||
|
|
||||||
|
LyricsScreen({this.lyrics, this.trackId, Key key}): super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_LyricsScreenState createState() => _LyricsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LyricsScreenState extends State<LyricsScreen> {
|
||||||
|
|
||||||
|
Lyrics lyrics;
|
||||||
|
bool _loading = true;
|
||||||
|
bool _error = false;
|
||||||
|
int _currentIndex = 0;
|
||||||
|
int _prevIndex = 0;
|
||||||
|
Timer _timer;
|
||||||
|
ScrollController _controller = ScrollController();
|
||||||
|
StreamSubscription _mediaItemSub;
|
||||||
|
final double height = 90;
|
||||||
|
|
||||||
|
Future _load() async {
|
||||||
|
//Already available
|
||||||
|
if (this.lyrics != null) return;
|
||||||
|
if (widget.lyrics != null && widget.lyrics.lyrics != null && widget.lyrics.lyrics.length > 0) {
|
||||||
|
setState(() {
|
||||||
|
lyrics = widget.lyrics;
|
||||||
|
_loading = false;
|
||||||
|
_error = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Fetch
|
||||||
|
try {
|
||||||
|
Lyrics l = await deezerAPI.lyrics(widget.trackId);
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
lyrics = l;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_error = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_load();
|
||||||
|
|
||||||
|
Timer.periodic(Duration(milliseconds: 350), (timer) {
|
||||||
|
_timer = timer;
|
||||||
|
if (_loading) return;
|
||||||
|
|
||||||
|
//Update current lyric index
|
||||||
|
setState(() => _currentIndex = lyrics.lyrics.lastIndexWhere((l) => l.offset <= AudioService.playbackState.currentPosition));
|
||||||
|
|
||||||
|
//Scroll to current lyric
|
||||||
|
if (_currentIndex <= 0) return;
|
||||||
|
if (_prevIndex == _currentIndex) return;
|
||||||
|
_prevIndex = _currentIndex;
|
||||||
|
_controller.animateTo(
|
||||||
|
//Lyric height, screen height, appbar height
|
||||||
|
(height * _currentIndex) - (MediaQuery.of(context).size.height / 2) + (height / 2) + 56,
|
||||||
|
duration: Duration(milliseconds: 250),
|
||||||
|
curve: Curves.ease
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
//Track change = exit lyrics
|
||||||
|
AudioService.currentMediaItemStream.listen((event) {
|
||||||
|
if (event.id != widget.trackId)
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
});
|
||||||
|
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (_timer != null)
|
||||||
|
_timer.cancel();
|
||||||
|
if (_mediaItemSub != null)
|
||||||
|
_mediaItemSub.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: FreezerAppBar('Lyrics'.i18n),
|
||||||
|
body: ListView(
|
||||||
|
controller: _controller,
|
||||||
|
children: [
|
||||||
|
//Shouldn't really happen, empty lyrics have own text
|
||||||
|
if (_error)
|
||||||
|
ErrorScreen(),
|
||||||
|
|
||||||
|
//Loading
|
||||||
|
if (_loading)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (lyrics != null)
|
||||||
|
...List.generate(lyrics.lyrics.length, (i) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
color: (_currentIndex == i) ? Colors.grey.withOpacity(0.25) : Colors.transparent,
|
||||||
|
),
|
||||||
|
height: height,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
lyrics.lyrics[i].text,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 26.0,
|
||||||
|
fontWeight: (_currentIndex == i) ? FontWeight.bold : FontWeight.normal
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
@ -8,12 +9,14 @@ import 'package:freezer/api/player.dart';
|
||||||
import 'package:freezer/settings.dart';
|
import 'package:freezer/settings.dart';
|
||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
import 'package:freezer/ui/elements.dart';
|
import 'package:freezer/ui/elements.dart';
|
||||||
|
import 'package:freezer/ui/lyrics.dart';
|
||||||
import 'package:freezer/ui/menu.dart';
|
import 'package:freezer/ui/menu.dart';
|
||||||
import 'package:freezer/ui/settings_screen.dart';
|
import 'package:freezer/ui/settings_screen.dart';
|
||||||
import 'package:freezer/ui/tiles.dart';
|
import 'package:freezer/ui/tiles.dart';
|
||||||
import 'package:async/async.dart';
|
import 'package:async/async.dart';
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:just_audio/just_audio.dart';
|
||||||
import 'package:marquee/marquee.dart';
|
import 'package:marquee/marquee.dart';
|
||||||
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
|
|
||||||
import 'cached_image.dart';
|
import 'cached_image.dart';
|
||||||
import '../api/definitions.dart';
|
import '../api/definitions.dart';
|
||||||
|
@ -29,8 +32,39 @@ class PlayerScreen extends StatefulWidget {
|
||||||
|
|
||||||
class _PlayerScreenState extends State<PlayerScreen> {
|
class _PlayerScreenState extends State<PlayerScreen> {
|
||||||
|
|
||||||
|
LinearGradient _bgGradient;
|
||||||
|
StreamSubscription _mediaItemSub;
|
||||||
|
|
||||||
|
//Calculate background color
|
||||||
|
Future _calculateColor() async {
|
||||||
|
if (!settings.colorGradientBackground)
|
||||||
|
return;
|
||||||
|
PaletteGenerator palette = await PaletteGenerator.fromImageProvider(CachedNetworkImageProvider(AudioService.currentMediaItem.extras['thumb'] ?? AudioService.currentMediaItem.artUri));
|
||||||
|
setState(() => _bgGradient = LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [palette.dominantColor.color.withOpacity(0.5), Theme.of(context).bottomAppBarColor],
|
||||||
|
stops: [
|
||||||
|
0.0,
|
||||||
|
0.4
|
||||||
|
]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_calculateColor();
|
||||||
|
_mediaItemSub = AudioService.currentMediaItemStream.listen((event) {
|
||||||
|
_calculateColor();
|
||||||
|
});
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
if (_mediaItemSub != null)
|
||||||
|
_mediaItemSub.cancel();
|
||||||
|
//Fix bottom buttons
|
||||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||||
systemNavigationBarColor: settings.themeData.bottomAppBarColor,
|
systemNavigationBarColor: settings.themeData.bottomAppBarColor,
|
||||||
));
|
));
|
||||||
|
@ -44,6 +78,10 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: _bgGradient
|
||||||
|
),
|
||||||
child: StreamBuilder(
|
child: StreamBuilder(
|
||||||
stream: StreamZip([AudioService.playbackStateStream, AudioService.currentMediaItemStream]),
|
stream: StreamZip([AudioService.playbackStateStream, AudioService.currentMediaItemStream]),
|
||||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||||
|
@ -68,6 +106,7 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,9 +118,6 @@ class PlayerScreenHorizontal extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
||||||
|
|
||||||
bool _lyrics = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
|
@ -95,12 +131,6 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
BigAlbumArt(),
|
BigAlbumArt(),
|
||||||
if (_lyrics) LyricsWidget(
|
|
||||||
artUri: AudioService.currentMediaItem.extras['thumb'],
|
|
||||||
trackId: AudioService.currentMediaItem.id,
|
|
||||||
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
|
|
||||||
height: ScreenUtil().setWidth(500),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -179,7 +209,9 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.subtitles, size: ScreenUtil().setWidth(32)),
|
icon: Icon(Icons.subtitles, size: ScreenUtil().setWidth(32)),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() => _lyrics = !_lyrics);
|
Navigator.of(context).push(MaterialPageRoute(
|
||||||
|
builder: (context) => LyricsScreen(trackId: AudioService.currentMediaItem.id)
|
||||||
|
));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (AudioService.currentMediaItem.extras['qualityString'] != null)
|
if (AudioService.currentMediaItem.extras['qualityString'] != null)
|
||||||
|
@ -223,8 +255,6 @@ class PlayerScreenVertical extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
||||||
bool _lyrics = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
|
@ -242,12 +272,6 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
BigAlbumArt(),
|
BigAlbumArt(),
|
||||||
if (_lyrics) LyricsWidget(
|
|
||||||
artUri: AudioService.currentMediaItem.extras['thumb'],
|
|
||||||
trackId: AudioService.currentMediaItem.id,
|
|
||||||
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
|
|
||||||
height: ScreenUtil().setHeight(1000),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -304,7 +328,9 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.subtitles, size: ScreenUtil().setWidth(46)),
|
icon: Icon(Icons.subtitles, size: ScreenUtil().setWidth(46)),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() => _lyrics = !_lyrics);
|
Navigator.of(context).push(MaterialPageRoute(
|
||||||
|
builder: (context) => LyricsScreen(trackId: AudioService.currentMediaItem.id)
|
||||||
|
));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (AudioService.currentMediaItem.extras['qualityString'] != null)
|
if (AudioService.currentMediaItem.extras['qualityString'] != null)
|
||||||
|
@ -499,164 +525,6 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class LyricsWidget extends StatefulWidget {
|
|
||||||
|
|
||||||
final Lyrics lyrics;
|
|
||||||
final String trackId;
|
|
||||||
final String artUri;
|
|
||||||
final double height;
|
|
||||||
LyricsWidget({this.artUri, this.lyrics, this.trackId, this.height, Key key}): super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_LyricsWidgetState createState() => _LyricsWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LyricsWidgetState extends State<LyricsWidget> {
|
|
||||||
|
|
||||||
bool _loading = true;
|
|
||||||
Lyrics _l;
|
|
||||||
Color _textColor = Colors.black;
|
|
||||||
ScrollController _scrollController = ScrollController();
|
|
||||||
Timer _timer;
|
|
||||||
int _currentIndex;
|
|
||||||
double _boxHeight;
|
|
||||||
double _lyricHeight = 128;
|
|
||||||
String _trackId;
|
|
||||||
|
|
||||||
Future _load() async {
|
|
||||||
_trackId = widget.trackId;
|
|
||||||
|
|
||||||
//Get text color by album art (black or white)
|
|
||||||
if (widget.artUri != null) {
|
|
||||||
bool bw = await imagesDatabase.isDark(widget.artUri);
|
|
||||||
if (bw != null) setState(() => _textColor = bw?Colors.white:Colors.black);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.lyrics.lyrics == null || widget.lyrics.lyrics.length == 0) {
|
|
||||||
//Load from api
|
|
||||||
try {
|
|
||||||
_l = await deezerAPI.lyrics(_trackId);
|
|
||||||
setState(() => _loading = false);
|
|
||||||
} catch (e) {
|
|
||||||
print(e);
|
|
||||||
//Error Lyrics
|
|
||||||
setState(() => _l = Lyrics.error());
|
|
||||||
}
|
|
||||||
|
|
||||||
//Empty lyrics
|
|
||||||
if (_l.lyrics.length == 0) {
|
|
||||||
setState(() {
|
|
||||||
_l = Lyrics.error();
|
|
||||||
_loading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
//Use provided lyrics
|
|
||||||
_l = widget.lyrics;
|
|
||||||
setState(() => _loading = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
this._boxHeight = widget.height??400.0;
|
|
||||||
_load();
|
|
||||||
|
|
||||||
Timer.periodic(Duration(milliseconds: 500), (timer) {
|
|
||||||
_timer = timer;
|
|
||||||
if (_loading) return;
|
|
||||||
//Update index of current lyric
|
|
||||||
setState(() {
|
|
||||||
_currentIndex = _l.lyrics.lastIndexWhere((l) => l.offset <= AudioService.playbackState.currentPosition);
|
|
||||||
});
|
|
||||||
//Scroll to current lyric
|
|
||||||
if (_currentIndex <= 0) return;
|
|
||||||
_scrollController.animateTo(
|
|
||||||
(_lyricHeight * _currentIndex) + (_lyricHeight / 2) - (_boxHeight / 2),
|
|
||||||
duration: Duration(milliseconds: 250),
|
|
||||||
curve: Curves.ease
|
|
||||||
);
|
|
||||||
|
|
||||||
});
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
if (_timer != null) _timer.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(LyricsWidget oldWidget) {
|
|
||||||
if (this._trackId != widget.trackId) {
|
|
||||||
setState(() {
|
|
||||||
_loading = true;
|
|
||||||
this._trackId = widget.trackId;
|
|
||||||
});
|
|
||||||
_load();
|
|
||||||
}
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
height: _boxHeight,
|
|
||||||
width: _boxHeight,
|
|
||||||
child: BackdropFilter(
|
|
||||||
filter: ImageFilter.blur(
|
|
||||||
sigmaX: 7.0,
|
|
||||||
sigmaY: 7.0
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
child: _loading?
|
|
||||||
Center(child: CircularProgressIndicator(),) :
|
|
||||||
SingleChildScrollView(
|
|
||||||
controller: _scrollController,
|
|
||||||
child: Column(
|
|
||||||
children: List.generate(_l.lyrics.length, (i) {
|
|
||||||
return Container(
|
|
||||||
height: _lyricHeight,
|
|
||||||
child: Center(
|
|
||||||
child: Stack(
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
_l.lyrics[i].text,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 28.0,
|
|
||||||
fontWeight: (_currentIndex == i)?FontWeight.bold:FontWeight.normal,
|
|
||||||
foreground: Paint()
|
|
||||||
..strokeWidth = 6
|
|
||||||
..style = PaintingStyle.stroke
|
|
||||||
..color = (_textColor==Colors.black)?Colors.white:Colors.black,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
_l.lyrics[i].text,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
color: _textColor,
|
|
||||||
fontSize: 28.0,
|
|
||||||
fontWeight: (_currentIndex == i)?FontWeight.bold:FontWeight.normal
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Top row containing QueueSource, queue...
|
//Top row containing QueueSource, queue...
|
||||||
class PlayerScreenTopRow extends StatelessWidget {
|
class PlayerScreenTopRow extends StatelessWidget {
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,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/download.dart';
|
import 'package:freezer/api/download.dart';
|
||||||
|
import 'package:freezer/api/player.dart';
|
||||||
import 'package:freezer/ui/downloads_screen.dart';
|
import 'package:freezer/ui/downloads_screen.dart';
|
||||||
import 'package:freezer/ui/elements.dart';
|
import 'package:freezer/ui/elements.dart';
|
||||||
import 'package:freezer/ui/error.dart';
|
import 'package:freezer/ui/error.dart';
|
||||||
|
@ -23,6 +24,7 @@ import 'package:path_provider_ex/path_provider_ex.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
import 'package:clipboard/clipboard.dart';
|
import 'package:clipboard/clipboard.dart';
|
||||||
|
import 'package:scrobblenaut/scrobblenaut.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../settings.dart';
|
import '../settings.dart';
|
||||||
|
@ -235,6 +237,17 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||||
),
|
),
|
||||||
leading: Icon(Icons.android)
|
leading: Icon(Icons.android)
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('Player gradient background'.i18n),
|
||||||
|
leading: Icon(Icons.colorize),
|
||||||
|
trailing: Switch(
|
||||||
|
value: settings.colorGradientBackground,
|
||||||
|
onChanged: (bool v) async {
|
||||||
|
setState(() => settings.colorGradientBackground = v);
|
||||||
|
await settings.save();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Primary color'.i18n),
|
title: Text('Primary color'.i18n),
|
||||||
leading: Icon(Icons.format_paint),
|
leading: Icon(Icons.format_paint),
|
||||||
|
@ -883,6 +896,32 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('LastFM'.i18n),
|
||||||
|
subtitle: Text(
|
||||||
|
(settings.lastFMPassword != null && settings.lastFMUsername != null)
|
||||||
|
? 'Log out'.i18n
|
||||||
|
: 'Login to enable scrobbling.'.i18n
|
||||||
|
),
|
||||||
|
leading: Icon(FontAwesome5.lastfm),
|
||||||
|
onTap: () async {
|
||||||
|
//Log out
|
||||||
|
if (settings.lastFMPassword != null && settings.lastFMUsername != null) {
|
||||||
|
settings.lastFMUsername = null;
|
||||||
|
settings.lastFMPassword = null;
|
||||||
|
playerHelper.scrobblenaut = null;
|
||||||
|
await settings.save();
|
||||||
|
setState(() {});
|
||||||
|
Fluttertoast.showToast(msg: 'Logged out!'.i18n);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => LastFMLogin()
|
||||||
|
);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Log out'.i18n, style: TextStyle(color: Colors.red),),
|
title: Text('Log out'.i18n, style: TextStyle(color: Colors.red),),
|
||||||
leading: Icon(Icons.exit_to_app),
|
leading: Icon(Icons.exit_to_app),
|
||||||
|
@ -937,6 +976,73 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LastFMLogin extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_LastFMLoginState createState() => _LastFMLoginState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LastFMLoginState extends State<LastFMLogin> {
|
||||||
|
|
||||||
|
String _username = '';
|
||||||
|
String _password = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('Login to LastFM'.i18n),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Username'.i18n
|
||||||
|
),
|
||||||
|
onChanged: (v) => _username = v,
|
||||||
|
),
|
||||||
|
Container(height: 8.0),
|
||||||
|
TextField(
|
||||||
|
obscureText: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Password'.i18n
|
||||||
|
),
|
||||||
|
onChanged: (v) => _password = v,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Cancel'.i18n),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
child: Text('Login'.i18n),
|
||||||
|
onPressed: () async {
|
||||||
|
LastFM last;
|
||||||
|
try {
|
||||||
|
last = await LastFM.authenticate(
|
||||||
|
apiKey: 'b6ab5ae967bcd8b10b23f68f42493829',
|
||||||
|
apiSecret: '861b0dff9a8a574bec747f9dab8b82bf',
|
||||||
|
username: _username,
|
||||||
|
password: _password
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Fluttertoast.showToast(msg: 'Authorization error!'.i18n);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//Save
|
||||||
|
settings.lastFMUsername = last.username;
|
||||||
|
settings.lastFMPassword = last.passwordHash;
|
||||||
|
await settings.save();
|
||||||
|
playerHelper.scrobblenaut = Scrobblenaut(lastFM: last);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DirectoryPicker extends StatefulWidget {
|
class DirectoryPicker extends StatefulWidget {
|
||||||
|
|
||||||
final String initialPath;
|
final String initialPath;
|
||||||
|
|
35
pubspec.lock
35
pubspec.lock
|
@ -246,6 +246,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.7"
|
version: "1.3.7"
|
||||||
|
dio:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dio
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.10"
|
||||||
disk_space:
|
disk_space:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -253,6 +260,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.3"
|
version: "0.0.3"
|
||||||
|
draggable_scrollbar:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: draggable_scrollbar
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.4"
|
||||||
ext_storage:
|
ext_storage:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -651,6 +665,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
photo_view:
|
photo_view:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -735,6 +756,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.24.1"
|
version: "0.24.1"
|
||||||
|
scrobblenaut:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: scrobblenaut
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.4"
|
||||||
share:
|
share:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -943,6 +971,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.2"
|
version: "0.1.2"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.1"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -70,7 +70,8 @@ dependencies:
|
||||||
numberpicker: ^1.2.1
|
numberpicker: ^1.2.1
|
||||||
quick_actions: ^0.4.0+10
|
quick_actions: ^0.4.0+10
|
||||||
photo_view: ^0.10.2
|
photo_view: ^0.10.2
|
||||||
draggable_scrollbar: 0.0.4
|
draggable_scrollbar: ^0.0.4
|
||||||
|
scrobblenaut: ^2.0.4
|
||||||
|
|
||||||
audio_session: ^0.0.9
|
audio_session: ^0.0.9
|
||||||
audio_service:
|
audio_service:
|
||||||
|
|
Loading…
Reference in New Issue