add build script for linux
fix audio service stop on android getTrack backend improvements get new track token when expired move shuffle button into LibraryPlaylists as FAB move favoriteButton next to track title move lyrics button on top of album art search: fix chips, and remove checkbox when selected
This commit is contained in:
parent
bb4448731e
commit
87c9733f51
|
@ -0,0 +1,7 @@
|
|||
echo "Target?"
|
||||
read target
|
||||
|
||||
set -x
|
||||
flutter pub get
|
||||
flutter pub run build_runner build
|
||||
flutter build $target
|
|
@ -103,8 +103,11 @@ class DeezerAudioSource extends StreamAudioSource {
|
|||
_md5origin = track.playbackDetails![0];
|
||||
}
|
||||
try {
|
||||
_downloadUrl =
|
||||
final res =
|
||||
await _deezerAudio.getUrl(_trackToken!, _trackTokenExpiration!);
|
||||
_downloadUrl = res!.$1;
|
||||
_trackToken = res.$2;
|
||||
_trackTokenExpiration = res.$3;
|
||||
} catch (e) {
|
||||
_logger.warning('get_url API failed with error: $e');
|
||||
_logger.warning('falling back to old url generation!');
|
||||
|
|
|
@ -599,8 +599,8 @@ class DeezerAPI {
|
|||
await callApi('log.listen', params: {
|
||||
'params': {
|
||||
'timestamp':
|
||||
timestamp ?? (DateTime.now().millisecondsSinceEpoch) ~/ 1000,
|
||||
'ts_listen': DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
timestamp ?? (DateTime.timestamp().millisecondsSinceEpoch) ~/ 1000,
|
||||
'ts_listen': DateTime.timestamp().millisecondsSinceEpoch ~/ 1000,
|
||||
'type': 1,
|
||||
'stat': {
|
||||
'seek': seek, // amount of times seeked
|
||||
|
|
|
@ -281,30 +281,41 @@ class DeezerAudio {
|
|||
}
|
||||
|
||||
static bool isTokenExpired(int trackTokenExpiration) =>
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000 > trackTokenExpiration;
|
||||
DateTime.timestamp().millisecondsSinceEpoch ~/ 1000 >
|
||||
trackTokenExpiration;
|
||||
|
||||
Future<Uri?> getUrl(String trackToken, int expiration) =>
|
||||
Future<(Uri, String trackToken, int tokenExpiration)?> getUrl(
|
||||
String trackToken, int expiration) =>
|
||||
getTrackUrl(deezerAPI, trackId, trackToken, expiration, quality: quality);
|
||||
|
||||
static Future<Uri?> getTrackUrl(
|
||||
static Future<(Uri, String trackToken, int tokenExpiration)?> getTrackUrl(
|
||||
DeezerAPI deezerAPI,
|
||||
String trackId,
|
||||
String trackToken,
|
||||
int expiration, {
|
||||
required AudioQuality quality,
|
||||
}) async {
|
||||
final String actualTrackToken;
|
||||
_logger.fine(
|
||||
'token expiration: $expiration/${DateTime.timestamp().millisecondsSinceEpoch ~/ 1000}');
|
||||
if (isTokenExpired(expiration)) {
|
||||
// get new token via pipe API
|
||||
_logger.fine('token is expired, getting new token.');
|
||||
final newTrack = await deezerAPI.track(trackId);
|
||||
actualTrackToken = newTrack.trackToken!;
|
||||
} else {
|
||||
actualTrackToken = trackToken;
|
||||
trackToken = newTrack.trackToken!;
|
||||
expiration = newTrack.trackTokenExpiration!;
|
||||
}
|
||||
|
||||
final res = await deezerAPI.getTrackUrl(
|
||||
actualTrackToken, quality.toDeezerQualityString());
|
||||
trackToken, quality.toDeezerQualityString());
|
||||
if (res.error != null) {
|
||||
try {
|
||||
final json = jsonDecode(res.error!);
|
||||
if (json['code'] == 2001) {
|
||||
// token expired.
|
||||
return getTrackUrl(deezerAPI, trackId, trackToken, 0,
|
||||
quality: quality);
|
||||
}
|
||||
} catch (e) {}
|
||||
_logger.warning('Error while getting track url: ${res.error!}');
|
||||
return null;
|
||||
}
|
||||
|
@ -313,6 +324,6 @@ class DeezerAudio {
|
|||
return null;
|
||||
}
|
||||
|
||||
return Uri.parse(res.sources![0].url);
|
||||
return (Uri.parse(res.sources![0].url), trackToken, expiration);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -162,8 +162,9 @@ class Track extends DeezerMediaItem {
|
|||
albumArt: DeezerImageDetails.fromUrl(mi.artUri.toString()),
|
||||
duration: mi.duration!,
|
||||
playbackDetails: playbackDetails,
|
||||
lyrics:
|
||||
Lyrics.fromJson(jsonDecode(((mi.extras ?? {})['lyrics']) ?? "{}")));
|
||||
lyrics: mi.extras?['lyrics'] == null
|
||||
? null
|
||||
: Lyrics.fromJson(jsonDecode(mi.extras!['lyrics'])));
|
||||
}
|
||||
|
||||
//JSON
|
||||
|
|
|
@ -18,10 +18,11 @@ class PipeAPI {
|
|||
|
||||
Dio get dio => deezerAPI.dio;
|
||||
|
||||
Future<void> authorize() async {
|
||||
Future<void> authorize({bool force = false}) async {
|
||||
// authorize on pipe.deezer.com
|
||||
|
||||
if (DateTime.now().millisecondsSinceEpoch ~/ 1000 < _jwtExpiration) {
|
||||
if (!force &&
|
||||
DateTime.timestamp().millisecondsSinceEpoch ~/ 1000 < _jwtExpiration) {
|
||||
// only continue if JWT expired!
|
||||
return;
|
||||
}
|
||||
|
@ -124,25 +125,36 @@ fragment LyricsSynchronizedLines on LyricsSynchronizedLine {
|
|||
{'trackId': trackId},
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
final lyrics = data['data']['track']['lyrics'] as Map?;
|
||||
if (lyrics == null) {
|
||||
if (data['errors'] != null && data['errors'].isNotEmpty) {
|
||||
for (final Map error in data['errors']) {
|
||||
if (error['type'] == 'JwtTokenExpiredError') {
|
||||
await authorize(force: true);
|
||||
return lyrics(trackId, cancelToken: cancelToken);
|
||||
}
|
||||
}
|
||||
|
||||
throw Exception(data['errors']);
|
||||
}
|
||||
|
||||
final lrc = data['data']['track']['lyrics'] as Map?;
|
||||
if (lrc == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lyrics['synchronizedLines'] != null) {
|
||||
if (lrc['synchronizedLines'] != null) {
|
||||
return Lyrics(
|
||||
id: lyrics['id'],
|
||||
writers: lyrics['writers'],
|
||||
id: lrc['id'],
|
||||
writers: lrc['writers'],
|
||||
sync: true,
|
||||
lyrics: (lyrics['synchronizedLines'] as List)
|
||||
lyrics: (lrc['synchronizedLines'] as List)
|
||||
.map<Lyric>((lrc) => Lyric.fromPrivateJson(lrc as Map))
|
||||
.toList(growable: false));
|
||||
}
|
||||
|
||||
return Lyrics(
|
||||
id: lyrics['id'],
|
||||
writers: lyrics['writers'],
|
||||
id: lrc['id'],
|
||||
writers: lrc['writers'],
|
||||
sync: false,
|
||||
lyrics: [Lyric(text: lyrics['text'])]);
|
||||
lyrics: [Lyric(text: lrc['text'])]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
|
||||
PlayerHelper playerHelper = PlayerHelper();
|
||||
late AudioHandler audioHandler;
|
||||
late AudioPlayerTask audioHandler;
|
||||
bool failsafe = false;
|
||||
|
||||
class AudioPlayerTaskInitArguments {
|
||||
|
@ -71,8 +71,6 @@ class AudioPlayerTaskInitArguments {
|
|||
class AudioPlayerTask extends BaseAudioHandler {
|
||||
final _logger = Logger('AudioPlayerTask');
|
||||
|
||||
bool _disposed = false;
|
||||
|
||||
late AudioPlayer _player;
|
||||
late ConcatenatingAudioSource _audioSource;
|
||||
late DeezerAPI _deezerAPI;
|
||||
|
@ -276,13 +274,6 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _maybeResume() {
|
||||
if (!_disposed) return Future.value();
|
||||
return Future.value();
|
||||
_logger.fine('resuming audioHandler.');
|
||||
return _init(shouldLoadQueue: true);
|
||||
}
|
||||
|
||||
/// Determine the [AudioQuality] to use according to current connection
|
||||
///
|
||||
/// Returns whether the [Connectivity] plugin is available on this system or not
|
||||
|
@ -326,7 +317,6 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
|
||||
@override
|
||||
Future skipToQueueItem(int index) async {
|
||||
await _maybeResume();
|
||||
_lastPosition = null;
|
||||
_lastQueueIndex = null;
|
||||
// next or prev track?
|
||||
|
@ -344,7 +334,6 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
|
||||
@override
|
||||
Future play() async {
|
||||
await _maybeResume();
|
||||
_logger.fine('playing...');
|
||||
await _player.play();
|
||||
//Restore position and queue index on play
|
||||
|
@ -363,14 +352,12 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
|
||||
@override
|
||||
Future<void> seek(Duration? position) async {
|
||||
await _maybeResume();
|
||||
_amountSeeked++;
|
||||
return _player.seek(position);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> fastForward() async {
|
||||
await _maybeResume();
|
||||
print('fast forward called');
|
||||
if (currentMediaItemIsShow) {
|
||||
return _seekRelative(const Duration(seconds: 30));
|
||||
|
@ -383,7 +370,6 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
|
||||
@override
|
||||
Future<void> rewind() async {
|
||||
await _maybeResume();
|
||||
print('rewind called');
|
||||
if (currentMediaItemIsShow) {
|
||||
return _seekRelative(-const Duration(seconds: 30));
|
||||
|
@ -431,7 +417,6 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
|
||||
@override
|
||||
Future<void> skipToNext() async {
|
||||
await _maybeResume();
|
||||
_lastPosition = null;
|
||||
if (_queueIndex == queue.value.length - 1) return;
|
||||
//Update buffering state
|
||||
|
@ -443,7 +428,6 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
|
||||
@override
|
||||
Future<void> skipToPrevious() async {
|
||||
await _maybeResume();
|
||||
if (_queueIndex == 0) return;
|
||||
//Update buffering state
|
||||
//_skipState = AudioProcessingState.skippingToPrevious;
|
||||
|
@ -778,16 +762,21 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
@override
|
||||
Future<void> stop() async {
|
||||
await _saveQueue();
|
||||
_disposed = true;
|
||||
// save state
|
||||
_lastPosition = _player.position;
|
||||
_lastQueueIndex = _queueIndex;
|
||||
await _player.stop();
|
||||
// await _player.dispose();
|
||||
// for (final subscription in _subscriptions) {
|
||||
// await subscription.cancel();
|
||||
// }
|
||||
await super.stop();
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _saveQueue();
|
||||
await _player.dispose();
|
||||
for (final subscription in _subscriptions) {
|
||||
await subscription.cancel();
|
||||
}
|
||||
await super.stop();
|
||||
}
|
||||
|
||||
//Export queue to -JSON- hive box
|
||||
|
|
|
@ -68,7 +68,7 @@ class PlayerHelper {
|
|||
final initArgs = AudioPlayerTaskInitArguments.from(
|
||||
settings: settings, deezerAPI: deezerAPI);
|
||||
// initialize our audiohandler instance
|
||||
audioHandler = await AudioService.init(
|
||||
audioHandler = await AudioService.init<AudioPlayerTask>(
|
||||
builder: () => AudioPlayerTask(initArgs),
|
||||
config: AudioServiceConfig(
|
||||
notificationColor: settings.primaryColor,
|
||||
|
|
|
@ -105,6 +105,6 @@ class SysTray with TrayListener, WindowListener {
|
|||
@override
|
||||
void onWindowClose() {
|
||||
// release resources before closing
|
||||
audioHandler.stop();
|
||||
audioHandler.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -170,7 +170,6 @@ class _FreezerAppState extends State<FreezerApp> with WidgetsBindingObserver {
|
|||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
playerHelper.stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
@ -70,21 +70,21 @@ class LibraryScreen extends StatelessWidget {
|
|||
Navigator.pushNamed(context, '/downloads');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Shuffle'.i18n),
|
||||
leading: const LeadingIcon(Icons.shuffle, color: Color(0xffeca704)),
|
||||
onTap: () async {
|
||||
List<Track> tracks = (await deezerAPI.libraryShuffle())!;
|
||||
playerHelper.playFromTrackList(
|
||||
tracks,
|
||||
tracks[0].id,
|
||||
QueueSource(
|
||||
id: 'libraryshuffle',
|
||||
source: 'libraryshuffle',
|
||||
text: 'Library shuffle'.i18n));
|
||||
},
|
||||
),
|
||||
const FreezerDivider(),
|
||||
// ListTile(
|
||||
// title: Text('Shuffle'.i18n),
|
||||
// leading: const LeadingIcon(Icons.shuffle, color: Color(0xffeca704)),
|
||||
// onTap: () async {
|
||||
// List<Track> tracks = (await deezerAPI.libraryShuffle())!;
|
||||
// playerHelper.playFromTrackList(
|
||||
// tracks,
|
||||
// tracks[0].id,
|
||||
// QueueSource(
|
||||
// id: 'libraryshuffle',
|
||||
// source: 'libraryshuffle',
|
||||
// text: 'Library shuffle'.i18n));
|
||||
// },
|
||||
// ),
|
||||
// const FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text('Tracks'.i18n),
|
||||
leading:
|
||||
|
@ -1021,179 +1021,236 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Playlists'.i18n),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_sort!.reverse!
|
||||
? FreezerIcons.sort_alpha_up
|
||||
: FreezerIcons.sort_alpha_down,
|
||||
semanticLabel: _sort!.reverse!
|
||||
? "Sort descending".i18n
|
||||
: "Sort ascending".i18n,
|
||||
),
|
||||
onPressed: () => _reverse(),
|
||||
appBar: AppBar(
|
||||
title: Text('Playlists'.i18n),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_sort!.reverse!
|
||||
? FreezerIcons.sort_alpha_up
|
||||
: FreezerIcons.sort_alpha_down,
|
||||
semanticLabel: _sort!.reverse!
|
||||
? "Sort descending".i18n
|
||||
: "Sort ascending".i18n,
|
||||
),
|
||||
PopupMenuButton(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
onSelected: (SortType s) async {
|
||||
setState(() => _sort!.type = s);
|
||||
//Save to cache
|
||||
int? index = Sorting.index(SortSourceTypes.PLAYLISTS);
|
||||
if (index == null) {
|
||||
cache.sorts.add(_sort);
|
||||
} else {
|
||||
cache.sorts[index] = _sort;
|
||||
onPressed: () => _reverse(),
|
||||
),
|
||||
PopupMenuButton(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
onSelected: (SortType s) async {
|
||||
setState(() => _sort!.type = s);
|
||||
//Save to cache
|
||||
int? index = Sorting.index(SortSourceTypes.PLAYLISTS);
|
||||
if (index == null) {
|
||||
cache.sorts.add(_sort);
|
||||
} else {
|
||||
cache.sorts[index] = _sort;
|
||||
}
|
||||
|
||||
await cache.save();
|
||||
},
|
||||
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
|
||||
PopupMenuItem(
|
||||
value: SortType.DEFAULT,
|
||||
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortType.USER,
|
||||
child: Text('User'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortType.TRACK_COUNT,
|
||||
child: Text('Track count'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortType.ALPHABETIC,
|
||||
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
],
|
||||
child: const Icon(Icons.sort, size: 32.0),
|
||||
),
|
||||
Container(width: 8.0),
|
||||
],
|
||||
),
|
||||
body: Scrollbar(
|
||||
interactive: true,
|
||||
controller: _scrollController,
|
||||
thickness: 8.0,
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
children: <Widget>[
|
||||
//Search
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: TextField(
|
||||
onChanged: (String s) => setState(() => _filter = s),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Search'.i18n,
|
||||
prefixIcon: Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(30.0)),
|
||||
)),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Create new playlist'.i18n),
|
||||
leading: const LeadingIcon(Icons.playlist_add,
|
||||
color: Color(0xff009a85)),
|
||||
onTap: () async {
|
||||
if (settings.offlineMode) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.snack('Cannot create playlists in offline mode'.i18n);
|
||||
return;
|
||||
}
|
||||
MenuSheet m = MenuSheet(context);
|
||||
await m.createPlaylist();
|
||||
await _load();
|
||||
},
|
||||
),
|
||||
const FreezerDivider(),
|
||||
|
||||
if (!settings.offlineMode && _playlists == null)
|
||||
const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator(),
|
||||
],
|
||||
),
|
||||
|
||||
//Favorites playlist
|
||||
PlaylistTile(
|
||||
favoritesPlaylist,
|
||||
onTap: () async {
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => PlaylistDetails(favoritesPlaylist));
|
||||
},
|
||||
onSecondary: (details) {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
favoritesPlaylist.library = true;
|
||||
m.defaultPlaylistMenu(favoritesPlaylist, details: details);
|
||||
},
|
||||
),
|
||||
|
||||
if (_playlists != null)
|
||||
...List.generate(_sorted.length, (int i) {
|
||||
Playlist p = _sorted[i];
|
||||
return PlaylistTile(
|
||||
p,
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushRoute(builder: (context) => PlaylistDetails(p)),
|
||||
onSecondary: (details) {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultPlaylistMenu(p, details: details, onRemove: () {
|
||||
setState(() => _playlists!.remove(p));
|
||||
}, onUpdate: () {
|
||||
_load();
|
||||
});
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
||||
FutureBuilder(
|
||||
future: downloadManager.getOfflinePlaylists(),
|
||||
builder: (context, AsyncSnapshot<List<Playlist>> snapshot) {
|
||||
if (snapshot.hasError ||
|
||||
!snapshot.hasData ||
|
||||
snapshot.data!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
await cache.save();
|
||||
List<Playlist> playlists = snapshot.data!;
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
const FreezerDivider(),
|
||||
Text(
|
||||
'Offline playlists'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 24.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
...List.generate(playlists.length, (i) {
|
||||
Playlist p = playlists[i];
|
||||
return PlaylistTile(
|
||||
p,
|
||||
onTap: () => Navigator.of(context).pushRoute(
|
||||
builder: (context) => PlaylistDetails(p)),
|
||||
onSecondary: (details) {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultPlaylistMenu(p, details: details,
|
||||
onRemove: () {
|
||||
setState(() {
|
||||
playlists.remove(p);
|
||||
_playlists!.remove(p);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
})
|
||||
],
|
||||
);
|
||||
},
|
||||
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
|
||||
PopupMenuItem(
|
||||
value: SortType.DEFAULT,
|
||||
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortType.USER,
|
||||
child: Text('User'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortType.TRACK_COUNT,
|
||||
child: Text('Track count'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortType.ALPHABETIC,
|
||||
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
||||
),
|
||||
],
|
||||
child: const Icon(Icons.sort, size: 32.0),
|
||||
),
|
||||
Container(width: 8.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Scrollbar(
|
||||
interactive: true,
|
||||
controller: _scrollController,
|
||||
thickness: 8.0,
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
children: <Widget>[
|
||||
//Search
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: TextField(
|
||||
onChanged: (String s) => setState(() => _filter = s),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Search'.i18n,
|
||||
filled: true,
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.grey)),
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.grey)),
|
||||
)),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Create new playlist'.i18n),
|
||||
leading: const LeadingIcon(Icons.playlist_add,
|
||||
color: Color(0xff009a85)),
|
||||
onTap: () async {
|
||||
if (settings.offlineMode) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.snack('Cannot create playlists in offline mode'.i18n);
|
||||
return;
|
||||
}
|
||||
MenuSheet m = MenuSheet(context);
|
||||
await m.createPlaylist();
|
||||
await _load();
|
||||
},
|
||||
),
|
||||
const FreezerDivider(),
|
||||
),
|
||||
floatingActionButton: AwaitingFloatingActionButton(
|
||||
onPressed: () async {
|
||||
await Future.delayed(Durations.extralong4);
|
||||
List<Track> tracks = (await deezerAPI.libraryShuffle())!;
|
||||
playerHelper.playFromTrackList(
|
||||
tracks,
|
||||
tracks[0].id,
|
||||
QueueSource(
|
||||
id: 'libraryshuffle',
|
||||
source: 'libraryshuffle',
|
||||
text: 'Library shuffle'.i18n));
|
||||
},
|
||||
child: const Icon(Icons.shuffle)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!settings.offlineMode && _playlists == null)
|
||||
const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator(),
|
||||
],
|
||||
),
|
||||
class AwaitingFloatingActionButton extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Future<void> Function() onPressed;
|
||||
final double size;
|
||||
const AwaitingFloatingActionButton(
|
||||
{required this.onPressed,
|
||||
required this.child,
|
||||
this.size = 24.0,
|
||||
super.key});
|
||||
|
||||
//Favorites playlist
|
||||
PlaylistTile(
|
||||
favoritesPlaylist,
|
||||
onTap: () async {
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => PlaylistDetails(favoritesPlaylist));
|
||||
},
|
||||
onSecondary: (details) {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
favoritesPlaylist.library = true;
|
||||
m.defaultPlaylistMenu(favoritesPlaylist, details: details);
|
||||
},
|
||||
),
|
||||
@override
|
||||
State<AwaitingFloatingActionButton> createState() =>
|
||||
_AwaitingFloatingActionButtonState();
|
||||
}
|
||||
|
||||
if (_playlists != null)
|
||||
...List.generate(_sorted.length, (int i) {
|
||||
Playlist p = _sorted[i];
|
||||
return PlaylistTile(
|
||||
p,
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushRoute(builder: (context) => PlaylistDetails(p)),
|
||||
onSecondary: (details) {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultPlaylistMenu(p, details: details, onRemove: () {
|
||||
setState(() => _playlists!.remove(p));
|
||||
}, onUpdate: () {
|
||||
_load();
|
||||
});
|
||||
},
|
||||
);
|
||||
}),
|
||||
class _AwaitingFloatingActionButtonState
|
||||
extends State<AwaitingFloatingActionButton> {
|
||||
bool _loading = false;
|
||||
void _onPressed() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
});
|
||||
|
||||
FutureBuilder(
|
||||
future: downloadManager.getOfflinePlaylists(),
|
||||
builder: (context, AsyncSnapshot<List<Playlist>> snapshot) {
|
||||
if (snapshot.hasError ||
|
||||
!snapshot.hasData ||
|
||||
snapshot.data!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
await widget.onPressed();
|
||||
if (!mounted) return;
|
||||
|
||||
List<Playlist> playlists = snapshot.data!;
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
const FreezerDivider(),
|
||||
Text(
|
||||
'Offline playlists'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 24.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
...List.generate(playlists.length, (i) {
|
||||
Playlist p = playlists[i];
|
||||
return PlaylistTile(
|
||||
p,
|
||||
onTap: () => Navigator.of(context).pushRoute(
|
||||
builder: (context) => PlaylistDetails(p)),
|
||||
onSecondary: (details) {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultPlaylistMenu(p, details: details,
|
||||
onRemove: () {
|
||||
setState(() {
|
||||
playlists.remove(p);
|
||||
_playlists!.remove(p);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
})
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
));
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FloatingActionButton(
|
||||
onPressed: _onPressed,
|
||||
child: SizedBox.square(
|
||||
dimension: widget.size,
|
||||
child: _loading
|
||||
? const CircularProgressIndicator(
|
||||
color: Colors.white, strokeWidth: 2.5)
|
||||
: widget.child),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -182,9 +182,9 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
}
|
||||
return KeyEventResult.handled;
|
||||
});
|
||||
if (settings.arl == null) {
|
||||
return Scaffold(
|
||||
body: Padding(
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0),
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
|
@ -197,18 +197,22 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
// style: ButtonStyle(
|
||||
// foregroundColor:
|
||||
// MaterialStateProperty.all(Colors.white)))),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 700.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 700.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight:
|
||||
MediaQuery.of(context).size.height - 250.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FreezerTitle(),
|
||||
const SizedBox(height: 16.0),
|
||||
|
@ -286,41 +290,48 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
});
|
||||
},
|
||||
),
|
||||
]))),
|
||||
const SizedBox(height: 16.0),
|
||||
Text(
|
||||
"If you don't have account, you can register on deezer.com for free."
|
||||
.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 16.0),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: ElevatedButton(
|
||||
child: Text('Open in browser'.i18n),
|
||||
onPressed: () {
|
||||
launchUrlString('https://deezer.com/register');
|
||||
},
|
||||
])),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
"By using this app, you don't agree with the Deezer ToS"
|
||||
.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 14.0),
|
||||
)
|
||||
],
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 16.0),
|
||||
Text(
|
||||
"If you don't have account, you can register on deezer.com for free."
|
||||
.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 16.0),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: ElevatedButton(
|
||||
child: Text('Open in browser'.i18n),
|
||||
onPressed: () {
|
||||
launchUrlString('https://deezer.com/register');
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
"By using this app, you don't agree with the Deezer ToS"
|
||||
.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 14.0),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
return const SizedBox();
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
import 'dart:async';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
|
@ -481,54 +482,63 @@ class PlayerTextSubtext extends StatelessWidget {
|
|||
return const SizedBox();
|
||||
}
|
||||
final currentMediaItem = snapshot.data!;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: 1.5 * textSize,
|
||||
child: FitOrScrollText(
|
||||
key: Key(currentMediaItem.displayTitle!),
|
||||
text: currentMediaItem.displayTitle!,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: textSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
overflow: TextOverflow.ellipsis)),
|
||||
),
|
||||
// child: currentMediaItem.displayTitle!.length >= 26
|
||||
// ? Marquee(
|
||||
// key: Key(currentMediaItem.displayTitle!),
|
||||
// text: currentMediaItem.displayTitle!,
|
||||
// style: TextStyle(
|
||||
// fontSize: textSize, fontWeight: FontWeight.bold),
|
||||
// blankSpace: 32.0,
|
||||
// startPadding: 0.0,
|
||||
// accelerationDuration: const Duration(seconds: 1),
|
||||
// pauseAfterRound: const Duration(seconds: 2),
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// fadingEdgeEndFraction: 0.05,
|
||||
// fadingEdgeStartFraction: 0.05,
|
||||
// )
|
||||
// : Text(
|
||||
// currentMediaItem.displayTitle!,
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// textAlign: TextAlign.start,
|
||||
// style: TextStyle(
|
||||
// fontSize: textSize, fontWeight: FontWeight.bold),
|
||||
// )),
|
||||
Text(
|
||||
currentMediaItem.displaySubtitle ?? '',
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(
|
||||
fontSize: textSize * 0.8, // 20% smaller
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
]);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: 1.5 * textSize,
|
||||
child: FitOrScrollText(
|
||||
key: Key(currentMediaItem.displayTitle!),
|
||||
text: currentMediaItem.displayTitle!,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: textSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
overflow: TextOverflow.ellipsis)),
|
||||
),
|
||||
// child: currentMediaItem.displayTitle!.length >= 26
|
||||
// ? Marquee(
|
||||
// key: Key(currentMediaItem.displayTitle!),
|
||||
// text: currentMediaItem.displayTitle!,
|
||||
// style: TextStyle(
|
||||
// fontSize: textSize, fontWeight: FontWeight.bold),
|
||||
// blankSpace: 32.0,
|
||||
// startPadding: 0.0,
|
||||
// accelerationDuration: const Duration(seconds: 1),
|
||||
// pauseAfterRound: const Duration(seconds: 2),
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// fadingEdgeEndFraction: 0.05,
|
||||
// fadingEdgeStartFraction: 0.05,
|
||||
// )
|
||||
// : Text(
|
||||
// currentMediaItem.displayTitle!,
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// textAlign: TextAlign.start,
|
||||
// style: TextStyle(
|
||||
// fontSize: textSize, fontWeight: FontWeight.bold),
|
||||
// )),
|
||||
Text(
|
||||
currentMediaItem.displaySubtitle ?? '',
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: textSize * 0.8, // 20% smaller
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
FavoriteButton(size: textSize),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -551,22 +561,23 @@ class QualityInfoWidget extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<String>(
|
||||
stream: playerHelper.streamInfo.map<String>(_getQualityStringFromInfo),
|
||||
builder: (context, snapshot) {
|
||||
return TextButton(
|
||||
// style: ButtonStyle(
|
||||
// elevation: MaterialStatePropertyAll(0.5),
|
||||
// padding: MaterialStatePropertyAll(
|
||||
// EdgeInsets.symmetric(horizontal: 16, vertical: 4)),
|
||||
// foregroundColor: MaterialStatePropertyAll(
|
||||
// Theme.of(context).colorScheme.onSurface)),
|
||||
child: Text(snapshot.data ?? '',
|
||||
style: textSize == null ? null : TextStyle(fontSize: textSize)),
|
||||
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => const QualitySettings())),
|
||||
);
|
||||
});
|
||||
return TextButton(
|
||||
// style: ButtonStyle(
|
||||
// elevation: MaterialStatePropertyAll(0.5),
|
||||
// padding: MaterialStatePropertyAll(
|
||||
// EdgeInsets.symmetric(horizontal: 16, vertical: 4)),
|
||||
// foregroundColor: MaterialStatePropertyAll(
|
||||
// Theme.of(context).colorScheme.onSurface)),
|
||||
child: StreamBuilder<String>(
|
||||
stream:
|
||||
playerHelper.streamInfo.map<String>(_getQualityStringFromInfo),
|
||||
builder: (context, snapshot) => Text(snapshot.data ?? '',
|
||||
style: textSize == null
|
||||
? null
|
||||
: TextStyle(fontSize: textSize! * 0.9))),
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const QualitySettings())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -898,6 +909,13 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
void _pushLyrics() {
|
||||
builder(ctx) => ChangeNotifierProvider<BackgroundProvider>.value(
|
||||
value: Provider.of<BackgroundProvider>(context),
|
||||
child: const LyricsScreen());
|
||||
Navigator.of(context).pushRoute(builder: builder);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final child = GestureDetector(
|
||||
|
@ -912,42 +930,91 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
|
|||
imageUrl: mediaItem.artUri.toString(), heroKey: mediaItem.id);
|
||||
},
|
||||
)),
|
||||
child: StreamBuilder<List<MediaItem>>(
|
||||
stream: audioHandler.queue,
|
||||
initialData: audioHandler.queue.valueOrNull,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final queue = snapshot.data!;
|
||||
return PageView.builder(
|
||||
controller: _pageController,
|
||||
onPageChanged: (int index) {
|
||||
// ignore if not initiated by user.
|
||||
if (!_userScroll) return;
|
||||
Logger('BigAlbumArt')
|
||||
.fine('page changed, skipping to media item');
|
||||
if (queue[index].id == audioHandler.mediaItem.value?.id) {
|
||||
return;
|
||||
}
|
||||
child: Stack(
|
||||
children: [
|
||||
StreamBuilder<List<MediaItem>>(
|
||||
stream: audioHandler.queue,
|
||||
initialData: audioHandler.queue.valueOrNull,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final queue = snapshot.data!;
|
||||
return PageView.builder(
|
||||
controller: _pageController,
|
||||
onPageChanged: (int index) {
|
||||
// ignore if not initiated by user.
|
||||
if (!_userScroll) return;
|
||||
Logger('BigAlbumArt')
|
||||
.fine('page changed, skipping to media item');
|
||||
if (queue[index].id == audioHandler.mediaItem.value?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
audioHandler.skipToQueueItem(index);
|
||||
},
|
||||
itemCount: queue.length,
|
||||
itemBuilder: (context, i) => Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Hero(
|
||||
tag: queue[i].id,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
child: CachedImage(
|
||||
url: queue[i].artUri.toString(),
|
||||
fullThumb: true,
|
||||
audioHandler.skipToQueueItem(index);
|
||||
},
|
||||
itemCount: queue.length,
|
||||
itemBuilder: (context, i) => Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Hero(
|
||||
tag: queue[i].id,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
child: CachedImage(
|
||||
url: queue[i].artUri.toString(),
|
||||
fullThumb: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
}),
|
||||
));
|
||||
}),
|
||||
StreamBuilder<MediaItem?>(
|
||||
initialData: audioHandler.mediaItem.valueOrNull,
|
||||
stream: audioHandler.mediaItem,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data == null) return const SizedBox.shrink();
|
||||
|
||||
print(snapshot.data!.extras);
|
||||
final l = snapshot.data!.extras?['lyrics'] == null
|
||||
? null
|
||||
: Lyrics.fromJson(
|
||||
jsonDecode(snapshot.data!.extras!['lyrics']));
|
||||
|
||||
if (l == null || l.id == null || l.id == '0') {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Positioned(
|
||||
key: const ValueKey('lyrics_button'),
|
||||
bottom: 16.0,
|
||||
right: 16.0,
|
||||
child: Consumer<BackgroundProvider>(
|
||||
builder: (context, provider, child) => Material(
|
||||
color: Color.lerp(
|
||||
Theme.of(context).colorScheme.background,
|
||||
provider.dominantColor,
|
||||
0.25),
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: child),
|
||||
child: InkWell(
|
||||
onTap: _pushLyrics,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0, vertical: 4.0),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.subtitles, size: 18.0),
|
||||
const SizedBox(width: 8.0),
|
||||
Text('Lyrics'.i18n),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return AspectRatio(
|
||||
|
@ -1178,6 +1245,7 @@ class BottomBarControls extends StatelessWidget {
|
|||
QualityInfoWidget(
|
||||
textSize: size * 0.75,
|
||||
),
|
||||
const Expanded(child: SizedBox()),
|
||||
PlayerMenuButton(size: size),
|
||||
],
|
||||
);
|
||||
|
@ -1189,14 +1257,7 @@ class BottomBarControls extends StatelessWidget {
|
|||
children: <Widget>[
|
||||
QualityInfoWidget(textSize: size * 0.75),
|
||||
const Expanded(child: SizedBox()),
|
||||
if (!desktopMode)
|
||||
IconButton(
|
||||
iconSize: size,
|
||||
icon: Icon(
|
||||
Icons.subtitles,
|
||||
semanticLabel: "Lyrics".i18n,
|
||||
),
|
||||
onPressed: () => _pushLyrics(context)),
|
||||
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.sentiment_very_dissatisfied,
|
||||
|
@ -1228,18 +1289,10 @@ class BottomBarControls extends StatelessWidget {
|
|||
// toastLength: Toast.LENGTH_SHORT);
|
||||
// },
|
||||
// ),
|
||||
FavoriteButton(size: iconSize),
|
||||
desktopMode
|
||||
? PlayerMenuButtonDesktop(size: iconSize)
|
||||
: PlayerMenuButton(size: iconSize)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _pushLyrics(BuildContext context) {
|
||||
builder(ctx) => ChangeNotifierProvider<BackgroundProvider>.value(
|
||||
value: Provider.of<BackgroundProvider>(context),
|
||||
child: const LyricsScreen());
|
||||
Navigator.of(context).pushRoute(builder: builder);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -543,73 +543,83 @@ class _SearchResultsScreenState extends State<SearchResultsScreen> {
|
|||
preferredSize: Size.fromHeight(_results == null ? 0.0 : 50.0),
|
||||
child: _results == null
|
||||
? const SizedBox.shrink()
|
||||
: Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0, vertical: 8.0),
|
||||
child:
|
||||
ListView(scrollDirection: Axis.horizontal, children: [
|
||||
if (_results!.tracks != null &&
|
||||
_results!.tracks!.isNotEmpty) ...[
|
||||
FilterChip(
|
||||
elevation: 1.0,
|
||||
label: Text('Tracks'.i18n),
|
||||
selected: _page == DeezerMediaType.track,
|
||||
onSelected: (selected) => setState(() => _page =
|
||||
selected ? DeezerMediaType.track : null)),
|
||||
const SizedBox(width: 8.0),
|
||||
],
|
||||
if (_results!.albums != null &&
|
||||
_results!.albums!.isNotEmpty) ...[
|
||||
FilterChip(
|
||||
elevation: 1.0,
|
||||
label: Text('Albums'.i18n),
|
||||
selected: _page == DeezerMediaType.album,
|
||||
onSelected: (selected) => setState(() => _page =
|
||||
selected ? DeezerMediaType.album : null)),
|
||||
const SizedBox(width: 8.0),
|
||||
],
|
||||
// if (_results!.artists != null &&
|
||||
// _results!.artists!.isNotEmpty) ...[
|
||||
// FilterChip(
|
||||
// elevation: 1.0,
|
||||
// label: Text('Artists'.i18n),
|
||||
// selected: _page == DeezerMediaType.artist,
|
||||
// onSelected: (selected) => setState(() => _page =
|
||||
// selected ? DeezerMediaType.artist : null)),
|
||||
// const SizedBox(width: 8.0),
|
||||
// ],
|
||||
if (_results!.playlists != null &&
|
||||
_results!.playlists!.isNotEmpty) ...[
|
||||
FilterChip(
|
||||
elevation: 1.0,
|
||||
label: Text('Playlists'.i18n),
|
||||
selected: _page == DeezerMediaType.playlist,
|
||||
onSelected: (selected) => setState(() => _page =
|
||||
selected ? DeezerMediaType.playlist : null)),
|
||||
const SizedBox(width: 8.0),
|
||||
],
|
||||
if (_results!.shows != null &&
|
||||
_results!.shows!.isNotEmpty) ...[
|
||||
FilterChip(
|
||||
elevation: 1.0,
|
||||
label: Text('Shows'.i18n),
|
||||
selected: _page == DeezerMediaType.show,
|
||||
onSelected: (selected) => setState(() => _page =
|
||||
selected ? DeezerMediaType.show : null)),
|
||||
const SizedBox(width: 8.0),
|
||||
],
|
||||
if (_results!.episodes != null &&
|
||||
_results!.episodes!.isNotEmpty) ...[
|
||||
FilterChip(
|
||||
elevation: 1.0,
|
||||
label: Text('Episodes'.i18n),
|
||||
selected: _page == DeezerMediaType.episode,
|
||||
onSelected: (selected) => setState(() => _page =
|
||||
selected ? DeezerMediaType.episode : null)),
|
||||
const SizedBox(width: 8.0),
|
||||
],
|
||||
]),
|
||||
: ChipTheme(
|
||||
data: const ChipThemeData(
|
||||
elevation: 1.0, showCheckmark: false),
|
||||
child: Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0, vertical: 8.0),
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
if (_results!.tracks != null &&
|
||||
_results!.tracks!.isNotEmpty) ...[
|
||||
FilterChip(
|
||||
label: Text('Tracks'.i18n),
|
||||
selected: _page == DeezerMediaType.track,
|
||||
onSelected: (selected) => setState(() =>
|
||||
_page = selected
|
||||
? DeezerMediaType.track
|
||||
: null)),
|
||||
const SizedBox(width: 8.0),
|
||||
],
|
||||
if (_results!.albums != null &&
|
||||
_results!.albums!.isNotEmpty) ...[
|
||||
FilterChip(
|
||||
label: Text('Albums'.i18n),
|
||||
selected: _page == DeezerMediaType.album,
|
||||
onSelected: (selected) => setState(() =>
|
||||
_page = selected
|
||||
? DeezerMediaType.album
|
||||
: null)),
|
||||
const SizedBox(width: 8.0),
|
||||
],
|
||||
// if (_results!.artists != null &&
|
||||
// _results!.artists!.isNotEmpty) ...[
|
||||
// FilterChip(
|
||||
// elevation: 1.0,
|
||||
// label: Text('Artists'.i18n),
|
||||
// selected: _page == DeezerMediaType.artist,
|
||||
// onSelected: (selected) => setState(() => _page =
|
||||
// selected ? DeezerMediaType.artist : null)),
|
||||
// const SizedBox(width: 8.0),
|
||||
// ],
|
||||
if (_results!.playlists != null &&
|
||||
_results!.playlists!.isNotEmpty) ...[
|
||||
FilterChip(
|
||||
label: Text('Playlists'.i18n),
|
||||
selected: _page == DeezerMediaType.playlist,
|
||||
onSelected: (selected) => setState(() =>
|
||||
_page = selected
|
||||
? DeezerMediaType.playlist
|
||||
: null)),
|
||||
const SizedBox(width: 8.0),
|
||||
],
|
||||
if (_results!.shows != null &&
|
||||
_results!.shows!.isNotEmpty) ...[
|
||||
FilterChip(
|
||||
label: Text('Shows'.i18n),
|
||||
selected: _page == DeezerMediaType.show,
|
||||
onSelected: (selected) => setState(() =>
|
||||
_page = selected
|
||||
? DeezerMediaType.show
|
||||
: null)),
|
||||
const SizedBox(width: 8.0),
|
||||
],
|
||||
if (_results!.episodes != null &&
|
||||
_results!.episodes!.isNotEmpty) ...[
|
||||
FilterChip(
|
||||
label: Text('Episodes'.i18n),
|
||||
selected: _page == DeezerMediaType.episode,
|
||||
onSelected: (selected) => setState(() =>
|
||||
_page = selected
|
||||
? DeezerMediaType.episode
|
||||
: null)),
|
||||
const SizedBox(width: 8.0),
|
||||
],
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'package:freezer/api/definitions.dart';
|
|||
import 'package:freezer/api/player/systray.dart';
|
||||
import 'package:freezer/icons.dart';
|
||||
import 'package:freezer/ui/login_on_other_device.dart';
|
||||
import 'package:freezer/ui/login_screen.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:scrobblenaut/scrobblenaut.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
@ -1528,9 +1529,13 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||
ScaffoldMessenger.of(context).snack('Copied'.i18n);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('DEBUG: stop audioHandler'),
|
||||
onTap: () => audioHandler.stop()),
|
||||
// ListTile(
|
||||
// title: const Text('DEBUG: stop audioHandler'),
|
||||
// onTap: () => audioHandler.stop()),
|
||||
// ListTile(
|
||||
// title: const Text('DEBUG: show login screen'),
|
||||
// onTap: () => Navigator.of(context, rootNavigator: true)
|
||||
// .pushRoute(builder: (ctx) => LoginWidget())),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -370,17 +370,41 @@ class PlaylistCardTile extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class PlayItemButton extends StatefulWidget {
|
||||
class PlayItemButton extends StatelessWidget {
|
||||
final FutureOr<void> Function() onTap;
|
||||
final double size;
|
||||
const PlayItemButton({required this.onTap, this.size = 32.0, Key? key})
|
||||
: super(key: key);
|
||||
const PlayItemButton({required this.onTap, this.size = 32.0, super.key});
|
||||
|
||||
@override
|
||||
State<PlayItemButton> createState() => _PlayItemButtonState();
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.square(
|
||||
dimension: size,
|
||||
child: DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle, color: Colors.white),
|
||||
child: Center(
|
||||
child: AwaitingButton(
|
||||
onTap: onTap,
|
||||
child: Icon(
|
||||
Icons.play_arrow,
|
||||
color: Colors.black,
|
||||
size: size / 1.5,
|
||||
)))));
|
||||
}
|
||||
}
|
||||
|
||||
class _PlayItemButtonState extends State<PlayItemButton> {
|
||||
class AwaitingButton extends StatefulWidget {
|
||||
final FutureOr<void> Function() onTap;
|
||||
final double size;
|
||||
final Widget child;
|
||||
const AwaitingButton(
|
||||
{required this.onTap, required this.child, this.size = 32.0, super.key});
|
||||
|
||||
@override
|
||||
State<AwaitingButton> createState() => _AwaitingButtonState();
|
||||
}
|
||||
|
||||
class _AwaitingButtonState extends State<AwaitingButton> {
|
||||
final _isLoading = ValueNotifier(false);
|
||||
void _onTap() {
|
||||
final ret = widget.onTap();
|
||||
|
@ -398,34 +422,21 @@ class _PlayItemButtonState extends State<PlayItemButton> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.square(
|
||||
dimension: widget.size,
|
||||
child: DecoratedBox(
|
||||
decoration:
|
||||
const BoxDecoration(shape: BoxShape.circle, color: Colors.white),
|
||||
child: Center(
|
||||
child: ValueListenableBuilder<bool>(
|
||||
valueListenable: _isLoading,
|
||||
child: InkWell(
|
||||
onTap: _onTap,
|
||||
child: Icon(
|
||||
Icons.play_arrow,
|
||||
color: Colors.black,
|
||||
size: widget.size / 1.5,
|
||||
),
|
||||
),
|
||||
builder: (context, isLoading, child) => isLoading
|
||||
? SizedBox.square(
|
||||
dimension: widget.size / 2,
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
color: Colors.black,
|
||||
),
|
||||
)
|
||||
: child!),
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: _isLoading,
|
||||
child: InkWell(
|
||||
onTap: _onTap,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
builder: (context, isLoading, child) => isLoading
|
||||
? SizedBox.square(
|
||||
dimension: widget.size / 2,
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
color: Colors.black,
|
||||
),
|
||||
)
|
||||
: child!);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ packages:
|
|||
source: hosted
|
||||
version: "61.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562
|
||||
|
@ -789,7 +789,7 @@ packages:
|
|||
path: "../just_audio_media_kit"
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.0.0"
|
||||
version: "2.0.1"
|
||||
just_audio_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -113,6 +113,7 @@ dependencies:
|
|||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
analyzer: ^5.13.0
|
||||
json_serializable: ^6.0.1
|
||||
build_runner: ^2.4.6
|
||||
hive_generator: ^2.0.0
|
||||
|
|
Loading…
Reference in New Issue