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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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