2021-04-04 22:59:57 +00:00
|
|
|
import 'dart:async';
|
|
|
|
|
2020-06-23 19:23:12 +00:00
|
|
|
import 'package:audio_service/audio_service.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
2020-10-16 18:54:04 +00:00
|
|
|
import 'package:flutter/services.dart';
|
2020-06-23 19:23:12 +00:00
|
|
|
import 'package:freezer/settings.dart';
|
2021-07-02 16:28:59 +00:00
|
|
|
import 'package:freezer/translations.i18n.dart';
|
2020-06-23 19:23:12 +00:00
|
|
|
|
|
|
|
import '../api/player.dart';
|
|
|
|
import 'cached_image.dart';
|
|
|
|
import 'player_screen.dart';
|
|
|
|
|
|
|
|
class PlayerBar extends StatelessWidget {
|
|
|
|
double get progress {
|
|
|
|
if (AudioService.playbackState == null) return 0.0;
|
|
|
|
if (AudioService.currentMediaItem == null) return 0.0;
|
2021-04-04 22:59:57 +00:00
|
|
|
if (AudioService.currentMediaItem.duration.inSeconds == 0)
|
|
|
|
return 0.0; //Division by 0
|
|
|
|
return AudioService.playbackState.currentPosition.inSeconds /
|
|
|
|
AudioService.currentMediaItem.duration.inSeconds;
|
2020-06-23 19:23:12 +00:00
|
|
|
}
|
|
|
|
|
2020-10-19 19:28:45 +00:00
|
|
|
double iconSize = 28;
|
2020-10-14 19:09:16 +00:00
|
|
|
bool _gestureRegistered = false;
|
2020-06-23 19:23:12 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2020-10-31 20:52:23 +00:00
|
|
|
var focusNode = FocusNode();
|
2020-10-14 19:09:16 +00:00
|
|
|
return GestureDetector(
|
|
|
|
onHorizontalDragUpdate: (details) async {
|
|
|
|
if (_gestureRegistered) return;
|
|
|
|
final double sensitivity = 12.69;
|
|
|
|
//Right swipe
|
|
|
|
_gestureRegistered = true;
|
|
|
|
if (details.delta.dx > sensitivity) {
|
|
|
|
await AudioService.skipToPrevious();
|
|
|
|
}
|
|
|
|
//Left
|
|
|
|
if (details.delta.dx < -sensitivity) {
|
|
|
|
await AudioService.skipToNext();
|
|
|
|
}
|
|
|
|
_gestureRegistered = false;
|
|
|
|
return;
|
2020-06-23 19:23:12 +00:00
|
|
|
},
|
2020-10-14 19:09:16 +00:00
|
|
|
child: StreamBuilder(
|
2021-04-04 22:59:57 +00:00
|
|
|
stream: Stream.periodic(Duration(milliseconds: 250)),
|
|
|
|
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
|
|
|
if (AudioService.currentMediaItem == null)
|
|
|
|
return Container(
|
|
|
|
width: 0,
|
|
|
|
height: 0,
|
|
|
|
);
|
|
|
|
return Column(
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
children: <Widget>[
|
|
|
|
Container(
|
2021-07-23 17:14:21 +00:00
|
|
|
// For Android TV: indicate focus by grey
|
|
|
|
color: focusNode.hasFocus
|
|
|
|
? Colors.black26
|
|
|
|
: Theme.of(context).bottomAppBarColor,
|
|
|
|
child: ListTile(
|
|
|
|
dense: true,
|
|
|
|
focusNode: focusNode,
|
|
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 8.0),
|
|
|
|
onTap: () {
|
|
|
|
Navigator.of(context).push(MaterialPageRoute(
|
|
|
|
builder: (BuildContext context) =>
|
|
|
|
PlayerScreen()));
|
|
|
|
SystemChrome.setSystemUIOverlayStyle(
|
|
|
|
SystemUiOverlayStyle(
|
|
|
|
systemNavigationBarColor:
|
|
|
|
settings.themeData.scaffoldBackgroundColor,
|
|
|
|
));
|
|
|
|
},
|
|
|
|
leading: CachedImage(
|
|
|
|
width: 50,
|
|
|
|
height: 50,
|
|
|
|
url: AudioService.currentMediaItem.extras['thumb'] ??
|
|
|
|
AudioService.currentMediaItem.artUri,
|
|
|
|
),
|
|
|
|
title: Text(
|
|
|
|
AudioService.currentMediaItem.displayTitle,
|
|
|
|
overflow: TextOverflow.clip,
|
|
|
|
maxLines: 1,
|
|
|
|
),
|
|
|
|
subtitle: Text(
|
|
|
|
AudioService.currentMediaItem.displaySubtitle ?? '',
|
|
|
|
overflow: TextOverflow.clip,
|
|
|
|
maxLines: 1,
|
|
|
|
),
|
|
|
|
trailing: IconTheme(
|
|
|
|
data: IconThemeData(
|
|
|
|
color: settings.isDark
|
|
|
|
? Colors.white
|
|
|
|
: Colors.grey[600]),
|
|
|
|
child: Row(
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
children: <Widget>[
|
|
|
|
PrevNextButton(
|
|
|
|
iconSize,
|
|
|
|
prev: true,
|
|
|
|
hidePrev: true,
|
|
|
|
),
|
|
|
|
PlayPauseButton(iconSize),
|
|
|
|
PrevNextButton(iconSize)
|
|
|
|
],
|
2021-04-04 22:59:57 +00:00
|
|
|
),
|
2021-07-23 17:14:21 +00:00
|
|
|
))),
|
2021-04-04 22:59:57 +00:00
|
|
|
Container(
|
|
|
|
height: 3.0,
|
|
|
|
child: LinearProgressIndicator(
|
|
|
|
backgroundColor:
|
|
|
|
Theme.of(context).primaryColor.withOpacity(0.1),
|
|
|
|
value: progress,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}),
|
2020-06-23 19:23:12 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class PrevNextButton extends StatelessWidget {
|
|
|
|
final double size;
|
|
|
|
final bool prev;
|
|
|
|
final bool hidePrev;
|
|
|
|
int i;
|
|
|
|
PrevNextButton(this.size, {this.prev = false, this.hidePrev = false});
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2021-02-09 20:14:14 +00:00
|
|
|
return StreamBuilder(
|
|
|
|
stream: AudioService.queueStream,
|
|
|
|
builder: (context, _snapshot) {
|
|
|
|
if (!prev) {
|
2021-04-04 22:59:57 +00:00
|
|
|
if (playerHelper.queueIndex ==
|
|
|
|
(AudioService.queue ?? []).length - 1) {
|
2021-02-09 20:14:14 +00:00
|
|
|
return IconButton(
|
2021-07-23 17:14:21 +00:00
|
|
|
icon: Icon(
|
|
|
|
Icons.skip_next,
|
|
|
|
semanticLabel: "Play next".i18n,
|
|
|
|
),
|
2021-02-09 20:14:14 +00:00
|
|
|
iconSize: size,
|
|
|
|
onPressed: null,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return IconButton(
|
2021-07-23 17:14:21 +00:00
|
|
|
icon: Icon(
|
|
|
|
Icons.skip_next,
|
|
|
|
semanticLabel: "Play next".i18n,
|
|
|
|
),
|
2021-02-09 20:14:14 +00:00
|
|
|
iconSize: size,
|
|
|
|
onPressed: () => AudioService.skipToNext(),
|
|
|
|
);
|
2020-06-23 19:23:12 +00:00
|
|
|
}
|
2021-02-09 20:14:14 +00:00
|
|
|
if (prev) {
|
|
|
|
if (i == 0) {
|
|
|
|
if (hidePrev) {
|
2021-04-04 22:59:57 +00:00
|
|
|
return Container(
|
|
|
|
height: 0,
|
|
|
|
width: 0,
|
|
|
|
);
|
2021-02-09 20:14:14 +00:00
|
|
|
}
|
|
|
|
return IconButton(
|
2021-07-23 17:14:21 +00:00
|
|
|
icon: Icon(
|
|
|
|
Icons.skip_previous,
|
|
|
|
semanticLabel: "Play previous".i18n,
|
|
|
|
),
|
2021-02-09 20:14:14 +00:00
|
|
|
iconSize: size,
|
|
|
|
onPressed: null,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return IconButton(
|
2021-07-23 17:14:21 +00:00
|
|
|
icon: Icon(
|
|
|
|
Icons.skip_previous,
|
|
|
|
semanticLabel: "Play previous".i18n,
|
|
|
|
),
|
2021-02-09 20:14:14 +00:00
|
|
|
iconSize: size,
|
|
|
|
onPressed: () => AudioService.skipToPrevious(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return Container();
|
|
|
|
},
|
|
|
|
);
|
2020-06-23 19:23:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-04 22:59:57 +00:00
|
|
|
class PlayPauseButton extends StatefulWidget {
|
|
|
|
final double size;
|
2021-04-05 21:27:15 +00:00
|
|
|
PlayPauseButton(this.size, {Key key}) : super(key: key);
|
2020-06-23 19:23:12 +00:00
|
|
|
|
2021-04-04 22:59:57 +00:00
|
|
|
@override
|
|
|
|
_PlayPauseButtonState createState() => _PlayPauseButtonState();
|
|
|
|
}
|
2020-06-23 19:23:12 +00:00
|
|
|
|
2021-04-04 22:59:57 +00:00
|
|
|
class _PlayPauseButtonState extends State<PlayPauseButton>
|
|
|
|
with SingleTickerProviderStateMixin {
|
|
|
|
AnimationController _controller;
|
|
|
|
Animation<double> _animation;
|
2020-06-23 19:23:12 +00:00
|
|
|
|
2021-04-04 22:59:57 +00:00
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
_controller =
|
2021-04-05 21:27:15 +00:00
|
|
|
AnimationController(vsync: this, duration: Duration(milliseconds: 200));
|
2021-04-04 22:59:57 +00:00
|
|
|
_animation = Tween<double>(begin: 0, end: 1)
|
|
|
|
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
|
|
|
super.initState();
|
|
|
|
}
|
2020-06-23 19:23:12 +00:00
|
|
|
|
|
|
|
@override
|
2021-04-04 22:59:57 +00:00
|
|
|
void dispose() {
|
|
|
|
_controller.dispose();
|
|
|
|
super.dispose();
|
|
|
|
}
|
2020-06-23 19:23:12 +00:00
|
|
|
|
2021-04-04 22:59:57 +00:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2020-07-18 21:45:48 +00:00
|
|
|
return StreamBuilder(
|
|
|
|
stream: AudioService.playbackStateStream,
|
|
|
|
builder: (context, snapshot) {
|
2021-04-05 20:22:32 +00:00
|
|
|
//Animated icon by pato05
|
2021-04-04 22:59:57 +00:00
|
|
|
bool _playing = AudioService.playbackState?.playing ?? false;
|
|
|
|
if (_playing ||
|
|
|
|
AudioService.playbackState?.processingState ==
|
|
|
|
AudioProcessingState.ready ||
|
|
|
|
AudioService.playbackState?.processingState ==
|
|
|
|
AudioProcessingState.none) {
|
|
|
|
if (_playing)
|
|
|
|
_controller.forward();
|
|
|
|
else
|
|
|
|
_controller.reverse();
|
2020-06-23 19:23:12 +00:00
|
|
|
|
2020-07-18 21:45:48 +00:00
|
|
|
return IconButton(
|
2021-04-05 21:27:15 +00:00
|
|
|
splashRadius: widget.size,
|
2021-04-04 22:59:57 +00:00
|
|
|
icon: AnimatedIcon(
|
|
|
|
icon: AnimatedIcons.play_pause,
|
|
|
|
progress: _animation,
|
2021-07-23 17:14:21 +00:00
|
|
|
semanticLabel: _playing ? "Pause".i18n : "Play".i18n,
|
2021-04-04 22:59:57 +00:00
|
|
|
),
|
|
|
|
iconSize: widget.size,
|
|
|
|
onPressed: _playing
|
|
|
|
? () => AudioService.pause()
|
|
|
|
: () => AudioService.play());
|
2020-07-18 21:45:48 +00:00
|
|
|
}
|
2020-06-23 19:23:12 +00:00
|
|
|
|
2020-07-18 21:45:48 +00:00
|
|
|
switch (AudioService.playbackState.processingState) {
|
2021-04-04 22:59:57 +00:00
|
|
|
//Stopped/Error
|
2020-07-18 21:45:48 +00:00
|
|
|
case AudioProcessingState.error:
|
|
|
|
case AudioProcessingState.none:
|
|
|
|
case AudioProcessingState.stopped:
|
2021-04-04 22:59:57 +00:00
|
|
|
return Container(width: widget.size, height: widget.size);
|
|
|
|
//Loading, connecting, rewinding...
|
2020-07-18 21:45:48 +00:00
|
|
|
default:
|
|
|
|
return Container(
|
2021-04-04 22:59:57 +00:00
|
|
|
width: widget.size,
|
|
|
|
height: widget.size,
|
2020-07-18 21:45:48 +00:00
|
|
|
child: CircularProgressIndicator(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
2020-06-23 19:23:12 +00:00
|
|
|
}
|
|
|
|
}
|