0.6.2 - Spotify albums/tracks, album art gradient, languages, minor fixes

This commit is contained in:
exttex 2020-11-09 22:05:47 +01:00
parent f877aa9d7b
commit e9d97986b5
17 changed files with 497 additions and 221 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

153
lib/ui/lyrics.dart Normal file
View File

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

View File

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

View File

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

View File

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

View File

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