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:
Pato05 2024-02-19 00:49:32 +01:00
parent bb4448731e
commit 87c9733f51
No known key found for this signature in database
GPG key ID: ED4C6F9C3D574FB6
18 changed files with 654 additions and 484 deletions

7
build.sh Normal file
View file

@ -0,0 +1,7 @@
echo "Target?"
read target
set -x
flutter pub get
flutter pub run build_runner build
flutter build $target

View file

@ -103,8 +103,11 @@ class DeezerAudioSource extends StreamAudioSource {
_md5origin = track.playbackDetails![0]; _md5origin = track.playbackDetails![0];
} }
try { try {
_downloadUrl = final res =
await _deezerAudio.getUrl(_trackToken!, _trackTokenExpiration!); await _deezerAudio.getUrl(_trackToken!, _trackTokenExpiration!);
_downloadUrl = res!.$1;
_trackToken = res.$2;
_trackTokenExpiration = res.$3;
} catch (e) { } catch (e) {
_logger.warning('get_url API failed with error: $e'); _logger.warning('get_url API failed with error: $e');
_logger.warning('falling back to old url generation!'); _logger.warning('falling back to old url generation!');

View file

@ -599,8 +599,8 @@ class DeezerAPI {
await callApi('log.listen', params: { await callApi('log.listen', params: {
'params': { 'params': {
'timestamp': 'timestamp':
timestamp ?? (DateTime.now().millisecondsSinceEpoch) ~/ 1000, timestamp ?? (DateTime.timestamp().millisecondsSinceEpoch) ~/ 1000,
'ts_listen': DateTime.now().millisecondsSinceEpoch ~/ 1000, 'ts_listen': DateTime.timestamp().millisecondsSinceEpoch ~/ 1000,
'type': 1, 'type': 1,
'stat': { 'stat': {
'seek': seek, // amount of times seeked 'seek': seek, // amount of times seeked

View file

@ -281,30 +281,41 @@ class DeezerAudio {
} }
static bool isTokenExpired(int trackTokenExpiration) => 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); getTrackUrl(deezerAPI, trackId, trackToken, expiration, quality: quality);
static Future<Uri?> getTrackUrl( static Future<(Uri, String trackToken, int tokenExpiration)?> getTrackUrl(
DeezerAPI deezerAPI, DeezerAPI deezerAPI,
String trackId, String trackId,
String trackToken, String trackToken,
int expiration, { int expiration, {
required AudioQuality quality, required AudioQuality quality,
}) async { }) async {
final String actualTrackToken; _logger.fine(
'token expiration: $expiration/${DateTime.timestamp().millisecondsSinceEpoch ~/ 1000}');
if (isTokenExpired(expiration)) { if (isTokenExpired(expiration)) {
// get new token via pipe API // get new token via pipe API
_logger.fine('token is expired, getting new token.');
final newTrack = await deezerAPI.track(trackId); final newTrack = await deezerAPI.track(trackId);
actualTrackToken = newTrack.trackToken!; trackToken = newTrack.trackToken!;
} else { expiration = newTrack.trackTokenExpiration!;
actualTrackToken = trackToken;
} }
final res = await deezerAPI.getTrackUrl( final res = await deezerAPI.getTrackUrl(
actualTrackToken, quality.toDeezerQualityString()); trackToken, quality.toDeezerQualityString());
if (res.error != null) { 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!}'); _logger.warning('Error while getting track url: ${res.error!}');
return null; return null;
} }
@ -313,6 +324,6 @@ class DeezerAudio {
return null; return null;
} }
return Uri.parse(res.sources![0].url); return (Uri.parse(res.sources![0].url), trackToken, expiration);
} }
} }

View file

@ -162,8 +162,9 @@ class Track extends DeezerMediaItem {
albumArt: DeezerImageDetails.fromUrl(mi.artUri.toString()), albumArt: DeezerImageDetails.fromUrl(mi.artUri.toString()),
duration: mi.duration!, duration: mi.duration!,
playbackDetails: playbackDetails, playbackDetails: playbackDetails,
lyrics: lyrics: mi.extras?['lyrics'] == null
Lyrics.fromJson(jsonDecode(((mi.extras ?? {})['lyrics']) ?? "{}"))); ? null
: Lyrics.fromJson(jsonDecode(mi.extras!['lyrics'])));
} }
//JSON //JSON

View file

@ -18,10 +18,11 @@ class PipeAPI {
Dio get dio => deezerAPI.dio; Dio get dio => deezerAPI.dio;
Future<void> authorize() async { Future<void> authorize({bool force = false}) async {
// authorize on pipe.deezer.com // authorize on pipe.deezer.com
if (DateTime.now().millisecondsSinceEpoch ~/ 1000 < _jwtExpiration) { if (!force &&
DateTime.timestamp().millisecondsSinceEpoch ~/ 1000 < _jwtExpiration) {
// only continue if JWT expired! // only continue if JWT expired!
return; return;
} }
@ -124,25 +125,36 @@ fragment LyricsSynchronizedLines on LyricsSynchronizedLine {
{'trackId': trackId}, {'trackId': trackId},
cancelToken: cancelToken, cancelToken: cancelToken,
); );
final lyrics = data['data']['track']['lyrics'] as Map?; if (data['errors'] != null && data['errors'].isNotEmpty) {
if (lyrics == null) { 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; return null;
} }
if (lyrics['synchronizedLines'] != null) { if (lrc['synchronizedLines'] != null) {
return Lyrics( return Lyrics(
id: lyrics['id'], id: lrc['id'],
writers: lyrics['writers'], writers: lrc['writers'],
sync: true, sync: true,
lyrics: (lyrics['synchronizedLines'] as List) lyrics: (lrc['synchronizedLines'] as List)
.map<Lyric>((lrc) => Lyric.fromPrivateJson(lrc as Map)) .map<Lyric>((lrc) => Lyric.fromPrivateJson(lrc as Map))
.toList(growable: false)); .toList(growable: false));
} }
return Lyrics( return Lyrics(
id: lyrics['id'], id: lrc['id'],
writers: lyrics['writers'], writers: lrc['writers'],
sync: false, sync: false,
lyrics: [Lyric(text: lyrics['text'])]); lyrics: [Lyric(text: lrc['text'])]);
} }
} }

View file

@ -27,7 +27,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
PlayerHelper playerHelper = PlayerHelper(); PlayerHelper playerHelper = PlayerHelper();
late AudioHandler audioHandler; late AudioPlayerTask audioHandler;
bool failsafe = false; bool failsafe = false;
class AudioPlayerTaskInitArguments { class AudioPlayerTaskInitArguments {
@ -71,8 +71,6 @@ class AudioPlayerTaskInitArguments {
class AudioPlayerTask extends BaseAudioHandler { class AudioPlayerTask extends BaseAudioHandler {
final _logger = Logger('AudioPlayerTask'); final _logger = Logger('AudioPlayerTask');
bool _disposed = false;
late AudioPlayer _player; late AudioPlayer _player;
late ConcatenatingAudioSource _audioSource; late ConcatenatingAudioSource _audioSource;
late DeezerAPI _deezerAPI; 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 /// Determine the [AudioQuality] to use according to current connection
/// ///
/// Returns whether the [Connectivity] plugin is available on this system or not /// Returns whether the [Connectivity] plugin is available on this system or not
@ -326,7 +317,6 @@ class AudioPlayerTask extends BaseAudioHandler {
@override @override
Future skipToQueueItem(int index) async { Future skipToQueueItem(int index) async {
await _maybeResume();
_lastPosition = null; _lastPosition = null;
_lastQueueIndex = null; _lastQueueIndex = null;
// next or prev track? // next or prev track?
@ -344,7 +334,6 @@ class AudioPlayerTask extends BaseAudioHandler {
@override @override
Future play() async { Future play() async {
await _maybeResume();
_logger.fine('playing...'); _logger.fine('playing...');
await _player.play(); await _player.play();
//Restore position and queue index on play //Restore position and queue index on play
@ -363,14 +352,12 @@ class AudioPlayerTask extends BaseAudioHandler {
@override @override
Future<void> seek(Duration? position) async { Future<void> seek(Duration? position) async {
await _maybeResume();
_amountSeeked++; _amountSeeked++;
return _player.seek(position); return _player.seek(position);
} }
@override @override
Future<void> fastForward() async { Future<void> fastForward() async {
await _maybeResume();
print('fast forward called'); print('fast forward called');
if (currentMediaItemIsShow) { if (currentMediaItemIsShow) {
return _seekRelative(const Duration(seconds: 30)); return _seekRelative(const Duration(seconds: 30));
@ -383,7 +370,6 @@ class AudioPlayerTask extends BaseAudioHandler {
@override @override
Future<void> rewind() async { Future<void> rewind() async {
await _maybeResume();
print('rewind called'); print('rewind called');
if (currentMediaItemIsShow) { if (currentMediaItemIsShow) {
return _seekRelative(-const Duration(seconds: 30)); return _seekRelative(-const Duration(seconds: 30));
@ -431,7 +417,6 @@ class AudioPlayerTask extends BaseAudioHandler {
@override @override
Future<void> skipToNext() async { Future<void> skipToNext() async {
await _maybeResume();
_lastPosition = null; _lastPosition = null;
if (_queueIndex == queue.value.length - 1) return; if (_queueIndex == queue.value.length - 1) return;
//Update buffering state //Update buffering state
@ -443,7 +428,6 @@ class AudioPlayerTask extends BaseAudioHandler {
@override @override
Future<void> skipToPrevious() async { Future<void> skipToPrevious() async {
await _maybeResume();
if (_queueIndex == 0) return; if (_queueIndex == 0) return;
//Update buffering state //Update buffering state
//_skipState = AudioProcessingState.skippingToPrevious; //_skipState = AudioProcessingState.skippingToPrevious;
@ -778,16 +762,21 @@ class AudioPlayerTask extends BaseAudioHandler {
@override @override
Future<void> stop() async { Future<void> stop() async {
await _saveQueue(); await _saveQueue();
_disposed = true;
// save state // save state
_lastPosition = _player.position;
_lastQueueIndex = _queueIndex;
await _player.stop(); await _player.stop();
// await _player.dispose(); // 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) { for (final subscription in _subscriptions) {
await subscription.cancel(); await subscription.cancel();
} }
await super.stop();
} }
//Export queue to -JSON- hive box //Export queue to -JSON- hive box

View file

@ -68,7 +68,7 @@ class PlayerHelper {
final initArgs = AudioPlayerTaskInitArguments.from( final initArgs = AudioPlayerTaskInitArguments.from(
settings: settings, deezerAPI: deezerAPI); settings: settings, deezerAPI: deezerAPI);
// initialize our audiohandler instance // initialize our audiohandler instance
audioHandler = await AudioService.init( audioHandler = await AudioService.init<AudioPlayerTask>(
builder: () => AudioPlayerTask(initArgs), builder: () => AudioPlayerTask(initArgs),
config: AudioServiceConfig( config: AudioServiceConfig(
notificationColor: settings.primaryColor, notificationColor: settings.primaryColor,

View file

@ -105,6 +105,6 @@ class SysTray with TrayListener, WindowListener {
@override @override
void onWindowClose() { void onWindowClose() {
// release resources before closing // release resources before closing
audioHandler.stop(); audioHandler.dispose();
} }
} }

View file

@ -170,7 +170,6 @@ class _FreezerAppState extends State<FreezerApp> with WidgetsBindingObserver {
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
playerHelper.stop();
super.dispose(); super.dispose();
} }

View file

@ -70,21 +70,21 @@ class LibraryScreen extends StatelessWidget {
Navigator.pushNamed(context, '/downloads'); Navigator.pushNamed(context, '/downloads');
}, },
), ),
ListTile( // ListTile(
title: Text('Shuffle'.i18n), // title: Text('Shuffle'.i18n),
leading: const LeadingIcon(Icons.shuffle, color: Color(0xffeca704)), // leading: const LeadingIcon(Icons.shuffle, color: Color(0xffeca704)),
onTap: () async { // onTap: () async {
List<Track> tracks = (await deezerAPI.libraryShuffle())!; // List<Track> tracks = (await deezerAPI.libraryShuffle())!;
playerHelper.playFromTrackList( // playerHelper.playFromTrackList(
tracks, // tracks,
tracks[0].id, // tracks[0].id,
QueueSource( // QueueSource(
id: 'libraryshuffle', // id: 'libraryshuffle',
source: 'libraryshuffle', // source: 'libraryshuffle',
text: 'Library shuffle'.i18n)); // text: 'Library shuffle'.i18n));
}, // },
), // ),
const FreezerDivider(), // const FreezerDivider(),
ListTile( ListTile(
title: Text('Tracks'.i18n), title: Text('Tracks'.i18n),
leading: leading:
@ -1086,11 +1086,9 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
onChanged: (String s) => setState(() => _filter = s), onChanged: (String s) => setState(() => _filter = s),
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Search'.i18n, labelText: 'Search'.i18n,
filled: true, prefixIcon: Icon(Icons.search),
focusedBorder: const OutlineInputBorder( border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey)), borderRadius: BorderRadius.circular(30.0)),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey)),
)), )),
), ),
ListTile( ListTile(
@ -1193,7 +1191,66 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
) )
], ],
), ),
)); ),
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)),
);
}
}
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});
@override
State<AwaitingFloatingActionButton> createState() =>
_AwaitingFloatingActionButtonState();
}
class _AwaitingFloatingActionButtonState
extends State<AwaitingFloatingActionButton> {
bool _loading = false;
void _onPressed() async {
setState(() {
_loading = true;
});
await widget.onPressed();
if (!mounted) return;
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),
);
} }
} }

View file

@ -182,9 +182,9 @@ class _LoginWidgetState extends State<LoginWidget> {
} }
return KeyEventResult.handled; return KeyEventResult.handled;
}); });
if (settings.arl == null) {
return Scaffold( return Scaffold(
body: Padding( body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0),
child: Theme( child: Theme(
data: Theme.of(context).copyWith( data: Theme.of(context).copyWith(
@ -197,18 +197,22 @@ class _LoginWidgetState extends State<LoginWidget> {
// style: ButtonStyle( // style: ButtonStyle(
// foregroundColor: // foregroundColor:
// MaterialStateProperty.all(Colors.white)))), // MaterialStateProperty.all(Colors.white)))),
child: SingleChildScrollView(
child: Center( child: Center(
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 700.0), constraints: const BoxConstraints(maxWidth: 700.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
Expanded( ConstrainedBox(
constraints: BoxConstraints(
minHeight:
MediaQuery.of(context).size.height - 250.0),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0), padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [ children: [
const FreezerTitle(), const FreezerTitle(),
const SizedBox(height: 16.0), const SizedBox(height: 16.0),
@ -286,7 +290,12 @@ class _LoginWidgetState extends State<LoginWidget> {
}); });
}, },
), ),
]))), ])),
),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 16.0), const SizedBox(height: 16.0),
Text( Text(
"If you don't have account, you can register on deezer.com for free." "If you don't have account, you can register on deezer.com for free."
@ -314,14 +323,16 @@ class _LoginWidgetState extends State<LoginWidget> {
style: const TextStyle(fontSize: 14.0), style: const TextStyle(fontSize: 14.0),
) )
], ],
)
],
),
),
), ),
), ),
), ),
), ),
)); ));
} }
return const SizedBox();
}
} }
class LoadingWindowWait extends StatelessWidget { class LoadingWindowWait extends StatelessWidget {

View file

@ -1,5 +1,6 @@
// ignore_for_file: unused_import // ignore_for_file: unused_import
import 'dart:convert';
import 'dart:ui'; import 'dart:ui';
import 'dart:async'; import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
@ -481,7 +482,11 @@ class PlayerTextSubtext extends StatelessWidget {
return const SizedBox(); return const SizedBox();
} }
final currentMediaItem = snapshot.data!; final currentMediaItem = snapshot.data!;
return Column( return Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
@ -522,13 +527,18 @@ class PlayerTextSubtext extends StatelessWidget {
currentMediaItem.displaySubtitle ?? '', currentMediaItem.displaySubtitle ?? '',
maxLines: 1, maxLines: 1,
textAlign: TextAlign.start, textAlign: TextAlign.start,
overflow: TextOverflow.clip, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: textSize * 0.8, // 20% smaller fontSize: textSize * 0.8, // 20% smaller
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
), ),
]); ]),
),
const SizedBox(width: 8.0),
FavoriteButton(size: textSize),
],
);
}); });
} }
} }
@ -551,9 +561,6 @@ class QualityInfoWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StreamBuilder<String>(
stream: playerHelper.streamInfo.map<String>(_getQualityStringFromInfo),
builder: (context, snapshot) {
return TextButton( return TextButton(
// style: ButtonStyle( // style: ButtonStyle(
// elevation: MaterialStatePropertyAll(0.5), // elevation: MaterialStatePropertyAll(0.5),
@ -561,12 +568,16 @@ class QualityInfoWidget extends StatelessWidget {
// EdgeInsets.symmetric(horizontal: 16, vertical: 4)), // EdgeInsets.symmetric(horizontal: 16, vertical: 4)),
// foregroundColor: MaterialStatePropertyAll( // foregroundColor: MaterialStatePropertyAll(
// Theme.of(context).colorScheme.onSurface)), // Theme.of(context).colorScheme.onSurface)),
child: Text(snapshot.data ?? '', child: StreamBuilder<String>(
style: textSize == null ? null : TextStyle(fontSize: textSize)), stream:
onPressed: () => Navigator.of(context).push(MaterialPageRoute( playerHelper.streamInfo.map<String>(_getQualityStringFromInfo),
builder: (context) => const QualitySettings())), 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(); super.dispose();
} }
void _pushLyrics() {
builder(ctx) => ChangeNotifierProvider<BackgroundProvider>.value(
value: Provider.of<BackgroundProvider>(context),
child: const LyricsScreen());
Navigator.of(context).pushRoute(builder: builder);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final child = GestureDetector( final child = GestureDetector(
@ -912,7 +930,9 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
imageUrl: mediaItem.artUri.toString(), heroKey: mediaItem.id); imageUrl: mediaItem.artUri.toString(), heroKey: mediaItem.id);
}, },
)), )),
child: StreamBuilder<List<MediaItem>>( child: Stack(
children: [
StreamBuilder<List<MediaItem>>(
stream: audioHandler.queue, stream: audioHandler.queue,
initialData: audioHandler.queue.valueOrNull, initialData: audioHandler.queue.valueOrNull,
builder: (context, snapshot) { builder: (context, snapshot) {
@ -948,6 +968,53 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
), ),
)); ));
}), }),
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( return AspectRatio(
@ -1178,6 +1245,7 @@ class BottomBarControls extends StatelessWidget {
QualityInfoWidget( QualityInfoWidget(
textSize: size * 0.75, textSize: size * 0.75,
), ),
const Expanded(child: SizedBox()),
PlayerMenuButton(size: size), PlayerMenuButton(size: size),
], ],
); );
@ -1189,14 +1257,7 @@ class BottomBarControls extends StatelessWidget {
children: <Widget>[ children: <Widget>[
QualityInfoWidget(textSize: size * 0.75), QualityInfoWidget(textSize: size * 0.75),
const Expanded(child: SizedBox()), const Expanded(child: SizedBox()),
if (!desktopMode)
IconButton(
iconSize: size,
icon: Icon(
Icons.subtitles,
semanticLabel: "Lyrics".i18n,
),
onPressed: () => _pushLyrics(context)),
IconButton( IconButton(
icon: Icon( icon: Icon(
Icons.sentiment_very_dissatisfied, Icons.sentiment_very_dissatisfied,
@ -1228,18 +1289,10 @@ class BottomBarControls extends StatelessWidget {
// toastLength: Toast.LENGTH_SHORT); // toastLength: Toast.LENGTH_SHORT);
// }, // },
// ), // ),
FavoriteButton(size: iconSize),
desktopMode desktopMode
? PlayerMenuButtonDesktop(size: iconSize) ? PlayerMenuButtonDesktop(size: iconSize)
: PlayerMenuButton(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);
}
} }

View file

@ -543,30 +543,36 @@ class _SearchResultsScreenState extends State<SearchResultsScreen> {
preferredSize: Size.fromHeight(_results == null ? 0.0 : 50.0), preferredSize: Size.fromHeight(_results == null ? 0.0 : 50.0),
child: _results == null child: _results == null
? const SizedBox.shrink() ? const SizedBox.shrink()
: Expanded( : ChipTheme(
data: const ChipThemeData(
elevation: 1.0, showCheckmark: false),
child: Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 8.0), horizontal: 16.0, vertical: 8.0),
child: child: ListView(
ListView(scrollDirection: Axis.horizontal, children: [ scrollDirection: Axis.horizontal,
children: [
if (_results!.tracks != null && if (_results!.tracks != null &&
_results!.tracks!.isNotEmpty) ...[ _results!.tracks!.isNotEmpty) ...[
FilterChip( FilterChip(
elevation: 1.0,
label: Text('Tracks'.i18n), label: Text('Tracks'.i18n),
selected: _page == DeezerMediaType.track, selected: _page == DeezerMediaType.track,
onSelected: (selected) => setState(() => _page = onSelected: (selected) => setState(() =>
selected ? DeezerMediaType.track : null)), _page = selected
? DeezerMediaType.track
: null)),
const SizedBox(width: 8.0), const SizedBox(width: 8.0),
], ],
if (_results!.albums != null && if (_results!.albums != null &&
_results!.albums!.isNotEmpty) ...[ _results!.albums!.isNotEmpty) ...[
FilterChip( FilterChip(
elevation: 1.0,
label: Text('Albums'.i18n), label: Text('Albums'.i18n),
selected: _page == DeezerMediaType.album, selected: _page == DeezerMediaType.album,
onSelected: (selected) => setState(() => _page = onSelected: (selected) => setState(() =>
selected ? DeezerMediaType.album : null)), _page = selected
? DeezerMediaType.album
: null)),
const SizedBox(width: 8.0), const SizedBox(width: 8.0),
], ],
// if (_results!.artists != null && // if (_results!.artists != null &&
@ -582,31 +588,34 @@ class _SearchResultsScreenState extends State<SearchResultsScreen> {
if (_results!.playlists != null && if (_results!.playlists != null &&
_results!.playlists!.isNotEmpty) ...[ _results!.playlists!.isNotEmpty) ...[
FilterChip( FilterChip(
elevation: 1.0,
label: Text('Playlists'.i18n), label: Text('Playlists'.i18n),
selected: _page == DeezerMediaType.playlist, selected: _page == DeezerMediaType.playlist,
onSelected: (selected) => setState(() => _page = onSelected: (selected) => setState(() =>
selected ? DeezerMediaType.playlist : null)), _page = selected
? DeezerMediaType.playlist
: null)),
const SizedBox(width: 8.0), const SizedBox(width: 8.0),
], ],
if (_results!.shows != null && if (_results!.shows != null &&
_results!.shows!.isNotEmpty) ...[ _results!.shows!.isNotEmpty) ...[
FilterChip( FilterChip(
elevation: 1.0,
label: Text('Shows'.i18n), label: Text('Shows'.i18n),
selected: _page == DeezerMediaType.show, selected: _page == DeezerMediaType.show,
onSelected: (selected) => setState(() => _page = onSelected: (selected) => setState(() =>
selected ? DeezerMediaType.show : null)), _page = selected
? DeezerMediaType.show
: null)),
const SizedBox(width: 8.0), const SizedBox(width: 8.0),
], ],
if (_results!.episodes != null && if (_results!.episodes != null &&
_results!.episodes!.isNotEmpty) ...[ _results!.episodes!.isNotEmpty) ...[
FilterChip( FilterChip(
elevation: 1.0,
label: Text('Episodes'.i18n), label: Text('Episodes'.i18n),
selected: _page == DeezerMediaType.episode, selected: _page == DeezerMediaType.episode,
onSelected: (selected) => setState(() => _page = onSelected: (selected) => setState(() =>
selected ? DeezerMediaType.episode : null)), _page = selected
? DeezerMediaType.episode
: null)),
const SizedBox(width: 8.0), const SizedBox(width: 8.0),
], ],
]), ]),
@ -614,6 +623,7 @@ class _SearchResultsScreenState extends State<SearchResultsScreen> {
), ),
), ),
), ),
),
body: _error != null body: _error != null
? ErrorScreen(message: _error.toString()) ? ErrorScreen(message: _error.toString())
: _results == null : _results == null

View file

@ -14,6 +14,7 @@ import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/player/systray.dart'; import 'package:freezer/api/player/systray.dart';
import 'package:freezer/icons.dart'; import 'package:freezer/icons.dart';
import 'package:freezer/ui/login_on_other_device.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:package_info_plus/package_info_plus.dart';
import 'package:scrobblenaut/scrobblenaut.dart'; import 'package:scrobblenaut/scrobblenaut.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -1528,9 +1529,13 @@ class _GeneralSettingsState extends State<GeneralSettings> {
ScaffoldMessenger.of(context).snack('Copied'.i18n); ScaffoldMessenger.of(context).snack('Copied'.i18n);
}, },
), ),
ListTile( // ListTile(
title: const Text('DEBUG: stop audioHandler'), // title: const Text('DEBUG: stop audioHandler'),
onTap: () => audioHandler.stop()), // onTap: () => audioHandler.stop()),
// ListTile(
// title: const Text('DEBUG: show login screen'),
// onTap: () => Navigator.of(context, rootNavigator: true)
// .pushRoute(builder: (ctx) => LoginWidget())),
], ],
), ),
); );

View file

@ -370,17 +370,41 @@ class PlaylistCardTile extends StatelessWidget {
} }
} }
class PlayItemButton extends StatefulWidget { class PlayItemButton extends StatelessWidget {
final FutureOr<void> Function() onTap; final FutureOr<void> Function() onTap;
final double size; final double size;
const PlayItemButton({required this.onTap, this.size = 32.0, Key? key}) const PlayItemButton({required this.onTap, this.size = 32.0, super.key});
: super(key: key);
@override @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); final _isLoading = ValueNotifier(false);
void _onTap() { void _onTap() {
final ret = widget.onTap(); final ret = widget.onTap();
@ -398,21 +422,11 @@ class _PlayItemButtonState extends State<PlayItemButton> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox.square( return ValueListenableBuilder<bool>(
dimension: widget.size,
child: DecoratedBox(
decoration:
const BoxDecoration(shape: BoxShape.circle, color: Colors.white),
child: Center(
child: ValueListenableBuilder<bool>(
valueListenable: _isLoading, valueListenable: _isLoading,
child: InkWell( child: InkWell(
onTap: _onTap, onTap: _onTap,
child: Icon( child: widget.child,
Icons.play_arrow,
color: Colors.black,
size: widget.size / 1.5,
),
), ),
builder: (context, isLoading, child) => isLoading builder: (context, isLoading, child) => isLoading
? SizedBox.square( ? SizedBox.square(
@ -422,10 +436,7 @@ class _PlayItemButtonState extends State<PlayItemButton> {
color: Colors.black, color: Colors.black,
), ),
) )
: child!), : child!);
),
),
);
} }
} }

View file

@ -10,7 +10,7 @@ packages:
source: hosted source: hosted
version: "61.0.0" version: "61.0.0"
analyzer: analyzer:
dependency: transitive dependency: "direct dev"
description: description:
name: analyzer name: analyzer
sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562
@ -789,7 +789,7 @@ packages:
path: "../just_audio_media_kit" path: "../just_audio_media_kit"
relative: true relative: true
source: path source: path
version: "2.0.0" version: "2.0.1"
just_audio_platform_interface: just_audio_platform_interface:
dependency: transitive dependency: transitive
description: description:

View file

@ -113,6 +113,7 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
analyzer: ^5.13.0
json_serializable: ^6.0.1 json_serializable: ^6.0.1
build_runner: ^2.4.6 build_runner: ^2.4.6
hive_generator: ^2.0.0 hive_generator: ^2.0.0