freezer/lib/ui/player_screen.dart

943 lines
31 KiB
Dart
Raw Normal View History

2021-11-01 16:41:25 +00:00
import 'dart:ui';
import 'dart:convert';
import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart';
2020-11-28 21:32:17 +00:00
import 'package:flutter/foundation.dart';
2020-06-23 19:23:12 +00:00
import 'package:flutter/material.dart';
import 'package:audio_service/audio_service.dart';
2021-09-02 20:45:14 +00:00
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
2021-08-29 22:25:18 +00:00
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:freezer/api/cache.dart';
2020-06-23 19:23:12 +00:00
import 'package:freezer/api/deezer.dart';
2021-11-01 16:41:25 +00:00
import 'package:freezer/api/definitions.dart';
2020-11-28 21:32:17 +00:00
import 'package:freezer/api/download.dart';
2020-06-23 19:23:12 +00:00
import 'package:freezer/api/player.dart';
2021-11-01 16:41:25 +00:00
import 'package:freezer/page_routes/fade.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/translations.i18n.dart';
2021-11-01 16:41:25 +00:00
import 'package:freezer/ui/cached_image.dart';
import 'package:freezer/ui/lyrics_screen.dart';
2020-06-23 19:23:12 +00:00
import 'package:freezer/ui/menu.dart';
2021-11-01 16:41:25 +00:00
import 'package:freezer/ui/player_bar.dart';
import 'package:freezer/ui/queue_screen.dart';
import 'package:freezer/ui/settings_screen.dart';
2020-07-19 12:41:05 +00:00
import 'package:marquee/marquee.dart';
import 'package:palette_generator/palette_generator.dart';
2021-09-02 20:45:14 +00:00
import 'package:photo_view/photo_view.dart';
2021-11-01 16:41:25 +00:00
import 'package:provider/provider.dart';
2021-02-09 20:14:14 +00:00
//Changing item in queue view and pressing back causes the pageView to skip song
bool pageViewLock = false;
2020-11-28 21:32:17 +00:00
2021-11-01 16:41:25 +00:00
const _blurStrength = 90.0;
/// A simple [ChangeNotifier] that listens to the [AudioHandler.mediaItem] stream and
/// notifies its listeners when background changes
class BackgroundProvider extends ChangeNotifier {
Color _dominantColor;
ImageProvider? _imageProvider;
StreamSubscription? _mediaItemSub;
BackgroundProvider(this._dominantColor);
/// Calculate background color from [mediaItem]
///
/// Warning: this function is expensive to call, and should only be called when songs change!
Future _updateColor(MediaItem mediaItem) async {
if (!settings.colorGradientBackground && !settings.blurPlayerBackground)
return;
2021-08-29 22:25:18 +00:00
final imageProvider = CachedNetworkImageProvider(
2021-11-01 16:41:25 +00:00
mediaItem.extras!['thumb'] ?? mediaItem.artUri as String);
//Run in isolate
PaletteGenerator palette =
await PaletteGenerator.fromImageProvider(imageProvider);
_dominantColor = palette.dominantColor!.color;
_imageProvider = settings.blurPlayerBackground ? imageProvider : null;
notifyListeners();
2021-08-29 22:25:18 +00:00
}
@override
2021-11-01 16:41:25 +00:00
void addListener(VoidCallback listener) {
_mediaItemSub ??= audioHandler.mediaItem.listen((mediaItem) {
if (mediaItem == null) return;
_updateColor(mediaItem);
});
2021-11-01 16:41:25 +00:00
super.addListener(listener);
}
2021-03-16 19:35:50 +00:00
2021-11-01 16:41:25 +00:00
@override
void removeListener(VoidCallback listener) {
super.removeListener(listener);
if (!hasListeners && _mediaItemSub != null) {
_mediaItemSub!.cancel();
_mediaItemSub = null;
}
}
@override
void dispose() {
2021-11-01 16:41:25 +00:00
_mediaItemSub?.cancel();
super.dispose();
}
2020-06-23 19:23:12 +00:00
2021-11-01 16:41:25 +00:00
Color get dominantColor => _dominantColor;
ImageProvider<Object>? get imageProvider => _imageProvider;
}
class PlayerScreen extends StatelessWidget {
const PlayerScreen({Key? key}) : super(key: key);
2020-06-23 19:23:12 +00:00
@override
Widget build(BuildContext context) {
2021-11-01 16:41:25 +00:00
final defaultColor = Theme.of(context).cardColor;
return ChangeNotifierProvider(
create: (context) => BackgroundProvider(defaultColor),
child: PlayerScreenBackground(
child: OrientationBuilder(
builder: (context, orientation) =>
orientation == Orientation.landscape
? PlayerScreenHorizontal()
: PlayerScreenVertical())),
);
}
}
/// Will change the background based on [BackgroundProvider],
/// it will wrap the [child] in a [Scaffold] and [SafeArea] widget
class PlayerScreenBackground extends StatelessWidget {
final Widget child;
final bool enabled;
final PreferredSizeWidget? appBar;
const PlayerScreenBackground({
required this.child,
this.enabled = true,
this.appBar,
});
Widget _buildChild(
BuildContext context, BackgroundProvider provider, Widget child) {
return Stack(children: [
if (provider.imageProvider != null || settings.colorGradientBackground)
Positioned.fill(
child: provider.imageProvider != null
? ImageFiltered(
imageFilter: ImageFilter.blur(
sigmaX: _blurStrength,
sigmaY: _blurStrength,
),
child: DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: provider.imageProvider!,
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
Colors.white
.withOpacity(settings.isDark ? 0.5 : 0.8),
BlendMode.dstATop),
)),
),
)
: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
provider.dominantColor,
Theme.of(context).scaffoldBackgroundColor,
],
stops: [0.0, 0.6],
)),
)),
child,
]);
}
static SystemUiOverlayStyle getSystemUiOverlayStyle(BuildContext context,
{bool enabled = true}) {
final hasBackground = enabled &&
(settings.blurPlayerBackground || settings.colorGradientBackground);
2021-08-29 22:25:18 +00:00
final color = hasBackground
? Colors.transparent
: Theme.of(context).scaffoldBackgroundColor;
2021-11-01 16:41:25 +00:00
final brightness = hasBackground
? Brightness.light
: (ThemeData.estimateBrightnessForColor(color) == Brightness.light
? Brightness.dark
: Brightness.light);
return SystemUiOverlayStyle(
statusBarColor: color,
statusBarBrightness: brightness,
statusBarIconBrightness: brightness,
systemNavigationBarIconBrightness: brightness,
systemNavigationBarColor: color,
systemNavigationBarDividerColor: color,
2021-08-29 22:25:18 +00:00
);
}
2021-11-01 16:41:25 +00:00
@override
Widget build(BuildContext context) {
final hasBackground = enabled &&
(settings.blurPlayerBackground || settings.colorGradientBackground);
final color = hasBackground
? Colors.transparent
: Theme.of(context).scaffoldBackgroundColor;
Widget widgetChild = Scaffold(
appBar: appBar,
backgroundColor: color,
body: SafeArea(child: child),
);
if (enabled)
widgetChild = Consumer<BackgroundProvider>(
builder: (context, provider, child) {
return _buildChild(context, provider, child!);
},
child: widgetChild,
);
if (appBar == null)
widgetChild = AnnotatedRegion<SystemUiOverlayStyle>(
value: getSystemUiOverlayStyle(context, enabled: enabled),
child: widgetChild,
);
return widgetChild;
}
}
//Landscape
class PlayerScreenHorizontal extends StatefulWidget {
@override
_PlayerScreenHorizontalState createState() => _PlayerScreenHorizontalState();
}
class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
@override
Widget build(BuildContext context) {
2021-09-02 20:45:14 +00:00
return Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
flex: 5,
child: BigAlbumArt(),
),
//Right side
Expanded(
flex: 5,
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
2021-04-05 20:27:54 +00:00
children: <Widget>[
2021-09-02 20:45:14 +00:00
Padding(
padding: EdgeInsets.fromLTRB(8, 0, 8, 0),
child: Container(
child: PlayerScreenTopRow(
textSize: ScreenUtil().setSp(24),
iconSize: ScreenUtil().setSp(36),
textWidth: ScreenUtil().setWidth(350),
short: true),
)),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
height: ScreenUtil().setSp(50),
child: audioHandler
.mediaItem.value!.displayTitle!.length >=
22
? Marquee(
text:
audioHandler.mediaItem.value!.displayTitle!,
style: TextStyle(
fontSize: 40.sp,
fontWeight: FontWeight.bold),
blankSpace: 32.0,
startPadding: 10.0,
accelerationDuration: Duration(seconds: 1),
pauseAfterRound: Duration(seconds: 2),
)
: Text(
audioHandler.mediaItem.value!.displayTitle!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 40.sp,
fontWeight: FontWeight.bold),
)),
const SizedBox(height: 4.0),
Text(
audioHandler.mediaItem.value!.displaySubtitle ?? '',
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.clip,
style: TextStyle(
fontSize: 32.sp,
color: Theme.of(context).primaryColor,
),
),
],
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: SeekBar(),
),
PlaybackControls(56.sp),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: BottomBarControls(size: 42.sp),
)
2021-04-05 20:27:54 +00:00
],
),
2021-09-02 20:45:14 +00:00
)
],
),
);
}
}
2020-06-23 19:23:12 +00:00
//Portrait
class PlayerScreenVertical extends StatefulWidget {
@override
_PlayerScreenVerticalState createState() => _PlayerScreenVerticalState();
}
class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
@override
Widget build(BuildContext context) {
2021-09-02 20:45:14 +00:00
return Padding(
padding: EdgeInsets.fromLTRB(30, 4, 16, 0),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
2021-09-02 20:45:14 +00:00
PlayerScreenTopRow(),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
child: BigAlbumArt(),
width: 1000.w,
height: 1000.w,
),
),
2021-11-01 16:41:25 +00:00
PlayerTextSubtext(textSize: 64.sp),
2021-09-02 20:45:14 +00:00
const SizedBox(height: 4.0),
SeekBar(),
PlaybackControls(86.sp),
Padding(
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 16.0),
child: BottomBarControls(size: 56.sp),
)
],
));
2020-06-23 19:23:12 +00:00
}
}
2021-11-01 16:41:25 +00:00
class PlayerTextSubtext extends StatelessWidget {
final double textSize;
const PlayerTextSubtext({Key? key, required this.textSize}) : super(key: key);
@override
Widget build(BuildContext context) {
return StreamBuilder<MediaItem?>(
stream: audioHandler.mediaItem,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox();
}
final currentMediaItem = snapshot.data!;
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox(
height: textSize * 1.5,
child: currentMediaItem.displayTitle!.length >= 26
? Marquee(
text: currentMediaItem.displayTitle!,
style: TextStyle(
fontSize: textSize, fontWeight: FontWeight.bold),
blankSpace: 32.0,
startPadding: 10.0,
accelerationDuration: Duration(seconds: 1),
pauseAfterRound: Duration(seconds: 2),
)
: Text(
currentMediaItem.displayTitle!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: textSize, fontWeight: FontWeight.bold),
)),
const SizedBox(height: 4),
Text(
currentMediaItem.displaySubtitle ?? '',
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.clip,
style: TextStyle(
fontSize: textSize * 0.8, // 20% smaller
color: Theme.of(context).primaryColor,
),
),
],
);
});
}
}
2020-11-28 21:32:17 +00:00
class QualityInfoWidget extends StatefulWidget {
@override
_QualityInfoWidgetState createState() => _QualityInfoWidgetState();
}
class _QualityInfoWidgetState extends State<QualityInfoWidget> {
String value = '';
2021-09-01 12:38:32 +00:00
late StreamSubscription streamSubscription;
2020-11-28 21:32:17 +00:00
//Load data from native
void _load() async {
2021-09-01 12:38:32 +00:00
if (audioHandler.mediaItem.value == null) return;
Map? data = await DownloadManager.platform.invokeMethod(
"getStreamInfo", {"id": audioHandler.mediaItem.value!.id});
2020-11-28 21:32:17 +00:00
//N/A
if (data == null) {
2021-09-02 20:45:14 +00:00
if (mounted) setState(() => value = '');
2020-11-28 21:32:17 +00:00
//If not show, try again later
2021-09-01 12:38:32 +00:00
if (audioHandler.mediaItem.value!.extras!['show'] == null)
2020-11-28 21:32:17 +00:00
Future.delayed(Duration(milliseconds: 200), _load);
return;
}
//Update
StreamQualityInfo info = StreamQualityInfo.fromJson(data);
setState(() {
2021-04-05 20:27:54 +00:00
value =
2021-09-01 12:38:32 +00:00
'${info.format} ${info.bitrate(audioHandler.mediaItem.value!.duration)}kbps';
2020-11-28 21:32:17 +00:00
});
}
@override
void initState() {
2021-09-02 20:45:14 +00:00
SchedulerBinding.instance!.addPostFrameCallback((_) => _load());
streamSubscription = audioHandler.mediaItem.listen((_) => _load());
2020-11-28 21:32:17 +00:00
super.initState();
}
@override
void dispose() {
2021-09-01 12:38:32 +00:00
streamSubscription.cancel();
2020-11-28 21:32:17 +00:00
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextButton(
2020-11-28 21:32:17 +00:00
child: Text(value),
onPressed: () {
2021-04-05 20:27:54 +00:00
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => QualitySettings()));
2020-11-28 21:32:17 +00:00
},
);
}
}
class PlayerMenuButton extends StatelessWidget {
2021-09-02 20:45:14 +00:00
final double size;
const PlayerMenuButton({required this.size});
2020-11-28 21:32:17 +00:00
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(
Icons.more_vert,
2021-09-02 20:45:14 +00:00
size: size,
semanticLabel: "Options".i18n,
),
2020-11-28 21:32:17 +00:00
onPressed: () {
2021-09-01 12:38:32 +00:00
final currentMediaItem = audioHandler.mediaItem.value!;
Track t = Track.fromMediaItem(currentMediaItem);
2020-12-27 18:33:59 +00:00
MenuSheet m = MenuSheet(context, navigateCallback: () {
Navigator.of(context).pop();
});
2021-09-01 12:38:32 +00:00
if (currentMediaItem.extras!['show'] == null)
m.defaultTrackMenu(t, options: [m.sleepTimer(), m.wakelock()]);
2020-11-28 21:32:17 +00:00
else
m.defaultShowEpisodeMenu(
2021-09-01 12:38:32 +00:00
Show.fromJson(jsonDecode(currentMediaItem.extras!['show'])),
ShowEpisode.fromMediaItem(currentMediaItem),
2021-04-05 20:27:54 +00:00
options: [m.sleepTimer(), m.wakelock()]);
2020-11-28 21:32:17 +00:00
},
);
}
}
2020-11-01 19:23:24 +00:00
class RepeatButton extends StatefulWidget {
final double iconSize;
2021-09-01 12:38:32 +00:00
RepeatButton(this.iconSize, {Key? key}) : super(key: key);
@override
2020-11-01 19:23:24 +00:00
_RepeatButtonState createState() => _RepeatButtonState();
}
2020-11-01 19:23:24 +00:00
class _RepeatButtonState extends State<RepeatButton> {
2021-08-29 22:25:18 +00:00
// ignore: missing_return
Icon get repeatIcon {
switch (playerHelper.repeatType) {
2021-11-01 16:41:25 +00:00
case AudioServiceRepeatMode.none:
return Icon(
Icons.repeat,
semanticLabel: "Repeat off".i18n,
);
2021-11-01 16:41:25 +00:00
case AudioServiceRepeatMode.one:
return Icon(
Icons.repeat_one,
2021-07-02 16:28:59 +00:00
semanticLabel: "Repeat one".i18n,
);
2021-11-01 16:41:25 +00:00
case AudioServiceRepeatMode.group:
case AudioServiceRepeatMode.all:
return Icon(
Icons.repeat,
semanticLabel: "Repeat".i18n,
);
}
}
2020-11-01 19:23:24 +00:00
@override
Widget build(BuildContext context) {
return IconButton(
2021-11-01 16:41:25 +00:00
color: playerHelper.repeatType == AudioServiceRepeatMode.none
? null
: Theme.of(context).primaryColor,
iconSize: widget.iconSize,
2020-11-01 19:23:24 +00:00
icon: repeatIcon,
onPressed: () async {
await playerHelper.changeRepeat();
setState(() {});
},
);
}
}
2021-11-01 16:41:25 +00:00
class ShuffleButton extends StatefulWidget {
2020-11-01 19:23:24 +00:00
final double iconSize;
2021-11-01 16:41:25 +00:00
const ShuffleButton({Key? key, required this.iconSize}) : super(key: key);
2020-11-01 19:23:24 +00:00
@override
2021-11-01 16:41:25 +00:00
_ShuffleButtonState createState() => _ShuffleButtonState();
2020-11-01 19:23:24 +00:00
}
2021-11-01 16:41:25 +00:00
class _ShuffleButtonState extends State<ShuffleButton> {
@override
Widget build(BuildContext context) => IconButton(
icon: Icon(Icons.shuffle),
iconSize: widget.iconSize,
color:
playerHelper.shuffleEnabled ? Theme.of(context).primaryColor : null,
onPressed: _toggleShuffle,
);
void _toggleShuffle() {
playerHelper.toggleShuffle().then((_) => setState(() => null));
}
}
class FavoriteButton extends StatefulWidget {
final double size;
const FavoriteButton({Key? key, required this.size}) : super(key: key);
@override
_FavoriteButtonState createState() => _FavoriteButtonState();
}
class _FavoriteButtonState extends State<FavoriteButton> {
Icon get libraryIcon {
2021-04-05 20:27:54 +00:00
if (cache.checkTrackFavorite(
2021-09-01 12:38:32 +00:00
Track.fromMediaItem(audioHandler.mediaItem.value!))) {
return Icon(
Icons.favorite,
semanticLabel: "Unlove".i18n,
);
}
return Icon(
Icons.favorite_border,
semanticLabel: "Love".i18n,
);
}
2021-11-01 16:41:25 +00:00
@override
Widget build(BuildContext context) => IconButton(
icon: libraryIcon,
iconSize: widget.size,
onPressed: () async {
if (cache.libraryTracks == null) cache.libraryTracks = [];
if (cache.checkTrackFavorite(
Track.fromMediaItem(audioHandler.mediaItem.value!))) {
//Remove from library
setState(() =>
cache.libraryTracks!.remove(audioHandler.mediaItem.value!.id));
await deezerAPI.removeFavorite(audioHandler.mediaItem.value!.id);
await cache.save();
} else {
//Add
setState(() =>
cache.libraryTracks!.add(audioHandler.mediaItem.value!.id));
await deezerAPI.addFavoriteTrack(audioHandler.mediaItem.value!.id);
await cache.save();
}
},
);
}
class PlaybackControls extends StatelessWidget {
final double size;
PlaybackControls(this.size, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
2021-11-01 16:41:25 +00:00
ShuffleButton(iconSize: size * 0.75),
PrevNextButton(size, prev: true),
if (settings.enableFilledPlayButton)
Consumer<BackgroundProvider>(builder: (context, provider, _) {
final color = Theme.of(context).brightness == Brightness.light
? provider.dominantColor
: darken(provider.dominantColor);
return PlayPauseButton(size * 2.25,
filled: true,
color: color,
iconColor: Color.lerp(
(ThemeData.estimateBrightnessForColor(color) ==
Brightness.light
? Colors.black
: Colors.white),
color,
0.25));
})
else
PlayPauseButton(size * 1.25),
PrevNextButton(size),
RepeatButton(size * 0.75),
],
),
);
}
}
class BigAlbumArt extends StatefulWidget {
@override
_BigAlbumArtState createState() => _BigAlbumArtState();
}
class _BigAlbumArtState extends State<BigAlbumArt> {
2021-11-01 16:41:25 +00:00
final _pageController = PageController(
initialPage: playerHelper.queueIndex,
2021-09-02 20:45:14 +00:00
viewportFraction: 1.0,
);
2021-09-01 12:38:32 +00:00
StreamSubscription? _currentItemSub;
2021-11-01 16:41:25 +00:00
bool _animationLock = false;
bool _initiatedByUser = false;
@override
void initState() {
2021-09-01 12:38:32 +00:00
_currentItemSub = audioHandler.mediaItem.listen((event) async {
2021-11-01 16:41:25 +00:00
if (_initiatedByUser) {
_initiatedByUser = false;
return;
}
if (!_pageController.hasClients) return;
print('animating controller to page');
_animationLock = true;
2021-04-05 20:27:54 +00:00
await _pageController.animateToPage(playerHelper.queueIndex,
duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
_animationLock = false;
});
super.initState();
}
@override
void dispose() {
2021-09-01 12:38:32 +00:00
_currentItemSub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onVerticalDragUpdate: (DragUpdateDetails details) {
if (details.delta.dy > 16) {
Navigator.of(context).pop();
}
},
2021-09-02 20:45:14 +00:00
onTap: () => Navigator.push(
context,
PageRouteBuilder(
opaque: false, // transparent background
barrierDismissible: true,
2021-11-01 16:41:25 +00:00
pageBuilder: (context, animation, __) {
return FadeTransition(
opacity: animation,
child: PhotoView(
imageProvider: CachedNetworkImageProvider(
audioHandler.mediaItem.value!.artUri.toString()),
maxScale: 8.0,
minScale: 0.2,
heroAttributes: PhotoViewHeroAttributes(
tag: audioHandler.mediaItem.value!.id),
backgroundDecoration:
BoxDecoration(color: Color.fromARGB(0x90, 0, 0, 0))),
);
2021-09-02 20:45:14 +00:00
})),
2021-11-01 16:41:25 +00:00
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(
controller: _pageController,
onPageChanged: (int index) {
if (pageViewLock || _animationLock) return;
_initiatedByUser = true;
audioHandler.skipToQueueItem(index);
},
children: List.generate(
queue.length,
(i) => Padding(
padding: const EdgeInsets.all(8.0),
child: Hero(
tag: queue[i].id,
child: ClipRRect(
borderRadius: BorderRadius.circular(5.0),
child: CachedImage(
url: queue[i].artUri.toString(),
),
),
),
)),
);
}),
);
}
}
2020-06-23 19:23:12 +00:00
//Top row containing QueueSource, queue...
class PlayerScreenTopRow extends StatelessWidget {
2021-09-01 12:38:32 +00:00
final double? textSize;
final double? iconSize;
final double? textWidth;
final bool? short;
2021-04-05 20:27:54 +00:00
PlayerScreenTopRow(
{this.textSize, this.iconSize, this.textWidth, this.short});
2020-06-23 19:23:12 +00:00
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
2020-06-23 19:23:12 +00:00
children: <Widget>[
Container(
2021-04-05 20:27:54 +00:00
width: this.textWidth ?? ScreenUtil().setWidth(800),
child: Text(
2021-04-05 20:27:54 +00:00
(short ?? false)
2021-09-01 12:38:32 +00:00
? (playerHelper.queueSource!.text ?? '')
2021-04-05 20:27:54 +00:00
: 'Playing from:'.i18n +
' ' +
(playerHelper.queueSource?.text ?? ''),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.left,
2021-04-05 20:27:54 +00:00
style: TextStyle(fontSize: this.textSize ?? ScreenUtil().setSp(38)),
),
),
IconButton(
icon: Icon(
Icons.menu,
semanticLabel: "Queue".i18n,
),
2021-04-05 20:27:54 +00:00
iconSize: this.iconSize ?? ScreenUtil().setSp(52),
splashRadius: this.iconSize ?? ScreenUtil().setWidth(52),
2021-08-29 22:25:18 +00:00
onPressed: () => Navigator.of(context)
2021-11-01 16:41:25 +00:00
.pushRoute(builder: (context) => QueueScreen()),
2020-06-23 19:23:12 +00:00
),
],
);
}
}
class SeekBar extends StatefulWidget {
2021-09-02 20:45:14 +00:00
const SeekBar();
2020-06-23 19:23:12 +00:00
@override
_SeekBarState createState() => _SeekBarState();
}
class _SeekBarState extends State<SeekBar> {
2021-09-01 12:38:32 +00:00
bool _seeking = false;
late StreamSubscription _subscription;
final position = ValueNotifier<Duration>(Duration.zero);
@override
void initState() {
_subscription = AudioService.position.listen((position) {
if (_seeking) return; // user is seeking
this.position.value = position;
});
super.initState();
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
double parseDuration(Duration position) {
if (position > duration) return duration.inMilliseconds.toDouble();
return position.inMilliseconds.toDouble();
2020-06-23 19:23:12 +00:00
}
//Duration to mm:ss
2021-09-01 12:38:32 +00:00
String _timeString(Duration d) {
2020-06-23 19:23:12 +00:00
return "${d.inMinutes}:${d.inSeconds.remainder(60).toString().padLeft(2, '0')}";
}
2021-09-01 12:38:32 +00:00
Duration get duration {
if (audioHandler.mediaItem.value == null) return Duration.zero;
return audioHandler.mediaItem.value!.duration!;
2020-06-23 19:23:12 +00:00
}
@override
Widget build(BuildContext context) {
2021-09-01 12:38:32 +00:00
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ValueListenableBuilder<Duration>(
valueListenable: position,
builder: (context, value, _) => Slider(
focusNode: FocusNode(
canRequestFocus: false,
skipTraversal:
true), // Don't focus on Slider - it doesn't work (and not needed)
value: parseDuration(value),
max: duration.inMilliseconds.toDouble(),
onChangeStart: (double d) {
_seeking = true;
position.value = Duration(milliseconds: d.toInt());
},
onChanged: (double d) {
position.value = Duration(milliseconds: d.toInt());
},
onChangeEnd: (double d) {
_seeking = false;
audioHandler.seek(Duration(milliseconds: d.toInt()));
},
)),
2021-09-02 20:45:14 +00:00
Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
ValueListenableBuilder<Duration>(
valueListenable: position,
builder: (context, value, _) => Text(
_timeString(value),
style: TextStyle(
fontSize: ScreenUtil().setSp(35),
color: Theme.of(context)
.textTheme
.bodyText1!
.color!
.withOpacity(.75)),
)),
StreamBuilder<MediaItem?>(
stream: audioHandler.mediaItem,
builder: (context, snapshot) => Text(
_timeString(snapshot.data?.duration ?? Duration.zero),
style: TextStyle(
fontSize: ScreenUtil().setSp(35),
color: Theme.of(context)
.textTheme
.bodyText1!
.color!
.withOpacity(.75)),
)),
],
),
),
2021-09-01 12:38:32 +00:00
],
2020-06-23 19:23:12 +00:00
);
}
}
2021-09-02 20:45:14 +00:00
class BottomBarControls extends StatelessWidget {
final double size;
const BottomBarControls({Key? key, required this.size}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(
2021-11-01 16:41:25 +00:00
icon: Icon(
Icons.subtitles,
size: size,
semanticLabel: "Lyrics".i18n,
),
onPressed: () => _pushLyrics(context)),
2021-09-02 20:45:14 +00:00
IconButton(
2021-11-01 16:41:25 +00:00
icon: Icon(
Icons.sentiment_very_dissatisfied,
semanticLabel: "Dislike".i18n,
),
iconSize: size * 0.85,
onPressed: () async {
await deezerAPI.dislikeTrack(audioHandler.mediaItem.value!.id);
if (playerHelper.queueIndex <
audioHandler.queue.value.length - 1) {
audioHandler.skipToNext();
}
}),
// IconButton(
// iconSize: size,
// icon: Icon(
// Icons.file_download,
// semanticLabel: "Download".i18n,
// ),
// onPressed: () async {
// Track t = Track.fromMediaItem(audioHandler.mediaItem.value!);
// if (await downloadManager.addOfflineTrack(t,
// private: false, context: context, isSingleton: true) !=
// false)
// Fluttertoast.showToast(
// msg: 'Downloads added!'.i18n,
// gravity: ToastGravity.BOTTOM,
// toastLength: Toast.LENGTH_SHORT);
// },
// ),
2021-09-02 20:45:14 +00:00
QualityInfoWidget(),
2021-11-01 16:41:25 +00:00
FavoriteButton(size: size * 0.85),
2021-09-02 20:45:14 +00:00
PlayerMenuButton(size: size)
],
);
2020-06-23 19:23:12 +00:00
}
2021-11-01 16:41:25 +00:00
void _pushLyrics(BuildContext context) {
final builder = (ctx) => ChangeNotifierProvider<BackgroundProvider>.value(
value: Provider.of<BackgroundProvider>(context), child: LyricsScreen());
if (settings.playerBackgroundOnLyrics) {
Navigator.of(context).push(FadePageRoute(builder: builder));
return;
}
Navigator.of(context).pushRoute(builder: builder);
}
2021-04-05 20:27:54 +00:00
}