import 'dart:async'; import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; import 'package:freezer/settings.dart'; import 'package:freezer/translations.i18n.dart'; import '../api/player.dart'; import 'cached_image.dart'; import 'player_screen.dart'; class PlayerBar extends StatefulWidget { final bool shouldHandleClicks; const PlayerBar({Key? key, this.shouldHandleClicks = true}) : super(key: key); @override _PlayerBarState createState() => _PlayerBarState(); } class _PlayerBarState extends State { final double iconSize = 28; late StreamSubscription mediaItemSub; late bool _isNothingPlaying = audioHandler.mediaItem.value == null; double parsePosition(Duration position) { if (audioHandler.mediaItem.value == null) return 0.0; if (audioHandler.mediaItem.value!.duration!.inSeconds == 0) return 0.0; //Division by 0 return position.inSeconds / audioHandler.mediaItem.value!.duration!.inSeconds; } @override void initState() { mediaItemSub = audioHandler.mediaItem.listen((playingItem) { if ((playingItem == null && !_isNothingPlaying) || (playingItem != null && _isNothingPlaying)) setState(() => _isNothingPlaying = playingItem == null); }); super.initState(); } @override void dispose() { mediaItemSub.cancel(); super.dispose(); } bool _gestureRegistered = false; @override Widget build(BuildContext context) { var focusNode = FocusNode(); return _isNothingPlaying ? const SizedBox() : GestureDetector( onHorizontalDragUpdate: (details) async { if (_gestureRegistered) return; final double sensitivity = 12.69; //Right swipe _gestureRegistered = true; if (details.delta.dx > sensitivity) { await audioHandler.skipToPrevious(); } //Left if (details.delta.dx < -sensitivity) { await audioHandler.skipToNext(); } _gestureRegistered = false; return; }, child: Column(mainAxisSize: MainAxisSize.min, children: [ StreamBuilder( stream: audioHandler.mediaItem, builder: (context, snapshot) { if (!snapshot.hasData) return const SizedBox(); final currentMediaItem = snapshot.data!; return DecoratedBox( // For Android TV: indicate focus by grey decoration: BoxDecoration( color: focusNode.hasFocus ? Colors.black26 : Theme.of(context).bottomAppBarColor), child: ListTile( dense: true, focusNode: focusNode, contentPadding: EdgeInsets.symmetric(horizontal: 8.0), onTap: widget.shouldHandleClicks ? () { Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) => PlayerScreen())); } : null, leading: CachedImage( width: 50, height: 50, url: currentMediaItem.extras!['thumb'] ?? audioHandler.mediaItem.value!.artUri as String?, ), title: Text( currentMediaItem.displayTitle!, overflow: TextOverflow.clip, maxLines: 1, ), subtitle: Text( 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: [ PrevNextButton( iconSize, prev: true, ), PlayPauseButton(iconSize), PrevNextButton(iconSize) ], ), ))); }), SizedBox( height: 3.0, child: StreamBuilder( stream: AudioService.position, builder: (context, snapshot) { return LinearProgressIndicator( backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1), value: parsePosition(snapshot.data ?? Duration.zero), ); }), ), ]), ); } } class PrevNextButton extends StatelessWidget { final double size; final bool prev; final bool hidePrev; PrevNextButton(this.size, {this.prev = false, this.hidePrev = false}); @override Widget build(BuildContext context) { return StreamBuilder>( stream: audioHandler.queue, builder: (context, snapshot) { if (!prev) { return IconButton( icon: Icon( Icons.skip_next, semanticLabel: "Play next".i18n, ), iconSize: size, onPressed: playerHelper.queueIndex == (snapshot.data ?? []).length - 1 ? null : () => audioHandler.skipToNext(), ); } final canGoPrev = playerHelper.queueIndex > 0; if (!canGoPrev && hidePrev) return const SizedBox(width: 0.0, height: 0.0); return IconButton( icon: Icon( Icons.skip_previous, semanticLabel: "Play previous".i18n, ), iconSize: size, onPressed: canGoPrev ? () => audioHandler.skipToPrevious() : null, ); }, ); } } class PlayPauseButton extends StatefulWidget { final double size; PlayPauseButton(this.size, {Key? key}) : super(key: key); @override _PlayPauseButtonState createState() => _PlayPauseButtonState(); } class _PlayPauseButtonState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @override void initState() { _controller = AnimationController(vsync: this, duration: Duration(milliseconds: 200)); _animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut); super.initState(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return StreamBuilder( stream: audioHandler.playbackState, builder: (context, snapshot) { //Animated icon by pato05 bool _playing = audioHandler.playbackState.value.playing; if (_playing || audioHandler.playbackState.value.processingState == AudioProcessingState.ready) { if (_playing) _controller.forward(); else _controller.reverse(); return IconButton( icon: AnimatedIcon( icon: AnimatedIcons.play_pause, progress: _animation, semanticLabel: _playing ? "Pause".i18n : "Play".i18n, ), iconSize: widget.size, onPressed: _playing ? () => audioHandler.pause() : () => audioHandler.play()); } switch (audioHandler.playbackState.value.processingState) { //Stopped/Error case AudioProcessingState.error: case AudioProcessingState.idle: return SizedBox(width: widget.size, height: widget.size); //Loading, connecting, rewinding... default: return SizedBox( width: widget.size, height: widget.size, child: const CircularProgressIndicator(), ); } }, ); } }