0.6.2 - Spotify albums/tracks, album art gradient, languages, minor fixes
This commit is contained in:
parent
f877aa9d7b
commit
e9d97986b5
17 changed files with 497 additions and 221 deletions
153
lib/ui/lyrics.dart
Normal file
153
lib/ui/lyrics.dart
Normal 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
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
}),
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
@ -8,12 +9,14 @@ import 'package:freezer/api/player.dart';
|
|||
import 'package:freezer/settings.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
import 'package:freezer/ui/elements.dart';
|
||||
import 'package:freezer/ui/lyrics.dart';
|
||||
import 'package:freezer/ui/menu.dart';
|
||||
import 'package:freezer/ui/settings_screen.dart';
|
||||
import 'package:freezer/ui/tiles.dart';
|
||||
import 'package:async/async.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:marquee/marquee.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
|
||||
import 'cached_image.dart';
|
||||
import '../api/definitions.dart';
|
||||
|
@ -29,8 +32,39 @@ class PlayerScreen extends StatefulWidget {
|
|||
|
||||
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
|
||||
void dispose() {
|
||||
if (_mediaItemSub != null)
|
||||
_mediaItemSub.cancel();
|
||||
//Fix bottom buttons
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor: settings.themeData.bottomAppBarColor,
|
||||
));
|
||||
|
@ -44,29 +78,34 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: StreamBuilder(
|
||||
stream: StreamZip([AudioService.playbackStateStream, AudioService.currentMediaItemStream]),
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: _bgGradient
|
||||
),
|
||||
child: StreamBuilder(
|
||||
stream: StreamZip([AudioService.playbackStateStream, AudioService.currentMediaItemStream]),
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
|
||||
//When disconnected
|
||||
if (AudioService.currentMediaItem == null) {
|
||||
playerHelper.startService();
|
||||
return Center(child: CircularProgressIndicator(),);
|
||||
}
|
||||
//When disconnected
|
||||
if (AudioService.currentMediaItem == null) {
|
||||
playerHelper.startService();
|
||||
return Center(child: CircularProgressIndicator(),);
|
||||
}
|
||||
|
||||
return OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
//Landscape
|
||||
if (orientation == Orientation.landscape) {
|
||||
return PlayerScreenHorizontal();
|
||||
}
|
||||
//Portrait
|
||||
return PlayerScreenVertical();
|
||||
},
|
||||
);
|
||||
return OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
//Landscape
|
||||
if (orientation == Orientation.landscape) {
|
||||
return PlayerScreenHorizontal();
|
||||
}
|
||||
//Portrait
|
||||
return PlayerScreenVertical();
|
||||
},
|
||||
);
|
||||
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -79,9 +118,6 @@ class PlayerScreenHorizontal extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
||||
|
||||
bool _lyrics = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
|
@ -95,12 +131,6 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
|||
child: Stack(
|
||||
children: <Widget>[
|
||||
BigAlbumArt(),
|
||||
if (_lyrics) LyricsWidget(
|
||||
artUri: AudioService.currentMediaItem.extras['thumb'],
|
||||
trackId: AudioService.currentMediaItem.id,
|
||||
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
|
||||
height: ScreenUtil().setWidth(500),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -179,7 +209,9 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
|||
IconButton(
|
||||
icon: Icon(Icons.subtitles, size: ScreenUtil().setWidth(32)),
|
||||
onPressed: () {
|
||||
setState(() => _lyrics = !_lyrics);
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => LyricsScreen(trackId: AudioService.currentMediaItem.id)
|
||||
));
|
||||
},
|
||||
),
|
||||
if (AudioService.currentMediaItem.extras['qualityString'] != null)
|
||||
|
@ -223,8 +255,6 @@ class PlayerScreenVertical extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
||||
bool _lyrics = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
|
@ -242,12 +272,6 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||
child: Stack(
|
||||
children: <Widget>[
|
||||
BigAlbumArt(),
|
||||
if (_lyrics) LyricsWidget(
|
||||
artUri: AudioService.currentMediaItem.extras['thumb'],
|
||||
trackId: AudioService.currentMediaItem.id,
|
||||
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
|
||||
height: ScreenUtil().setHeight(1000),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -304,7 +328,9 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||
IconButton(
|
||||
icon: Icon(Icons.subtitles, size: ScreenUtil().setWidth(46)),
|
||||
onPressed: () {
|
||||
setState(() => _lyrics = !_lyrics);
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => LyricsScreen(trackId: AudioService.currentMediaItem.id)
|
||||
));
|
||||
},
|
||||
),
|
||||
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...
|
||||
class PlayerScreenTopRow extends StatelessWidget {
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:fluttertoast/fluttertoast.dart';
|
|||
import 'package:freezer/api/cache.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
import 'package:freezer/ui/downloads_screen.dart';
|
||||
import 'package:freezer/ui/elements.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:freezer/translations.i18n.dart';
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:scrobblenaut/scrobblenaut.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../settings.dart';
|
||||
|
@ -235,6 +237,17 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
),
|
||||
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(
|
||||
title: Text('Primary color'.i18n),
|
||||
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(
|
||||
title: Text('Log out'.i18n, style: TextStyle(color: Colors.red),),
|
||||
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 {
|
||||
|
||||
final String initialPath;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue