Pato05
f126ffef46
use get_url api by default, and fall back to old generation if get_url failed start to write a better cachemanager to implement in all systems write in more appropriate directories on windows and linux improve check for Connectivity by adding a fallback (needed for example on linux systems without NetworkManager) allow to dynamically change track quality without rebuilding the object
300 lines
10 KiB
Dart
300 lines
10 KiB
Dart
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 'package:freezer/ui/fancy_scaffold.dart';
|
|
import 'package:rxdart/rxdart.dart';
|
|
|
|
import '../api/player/audio_handler.dart';
|
|
import 'cached_image.dart';
|
|
|
|
class PlayerBar extends StatelessWidget {
|
|
final VoidCallback? onTap;
|
|
final bool shouldHaveHero;
|
|
final Color? backgroundColor;
|
|
final FocusNode? focusNode;
|
|
const PlayerBar({
|
|
Key? key,
|
|
this.onTap,
|
|
this.shouldHaveHero = true,
|
|
this.backgroundColor,
|
|
this.focusNode,
|
|
}) : super(key: key);
|
|
|
|
final double iconSize = 28;
|
|
|
|
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;
|
|
}
|
|
|
|
Color? get _backgroundColor => backgroundColor;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox(
|
|
height: 68.0,
|
|
child: Column(mainAxisSize: MainAxisSize.max, children: <Widget>[
|
|
Expanded(
|
|
child: StreamBuilder<MediaItem?>(
|
|
stream: audioHandler.mediaItem,
|
|
initialData: audioHandler.mediaItem.valueOrNull,
|
|
builder: (context, snapshot) {
|
|
if (snapshot.data == null) {
|
|
return Material(
|
|
child: ListTile(
|
|
dense: true,
|
|
visualDensity: VisualDensity.standard,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16.0, vertical: 6.0),
|
|
leading: Image.asset(
|
|
'assets/cover_thumb.jpg',
|
|
width: 48.0,
|
|
height: 48.0,
|
|
),
|
|
title: Text('Nothing is currently playing'.i18n),
|
|
),
|
|
);
|
|
}
|
|
final currentMediaItem = snapshot.data!;
|
|
final image = CachedImage(
|
|
width: 50,
|
|
height: 50,
|
|
url: currentMediaItem.extras!['thumb'] ??
|
|
currentMediaItem.artUri.toString(),
|
|
);
|
|
final leadingWidget = shouldHaveHero
|
|
? Hero(tag: currentMediaItem.id, child: image)
|
|
: image;
|
|
return Material(
|
|
child: ListTile(
|
|
dense: true,
|
|
tileColor: _backgroundColor,
|
|
focusNode: focusNode,
|
|
visualDensity: VisualDensity.standard,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16.0, vertical: 0.0),
|
|
onTap: onTap,
|
|
leading: AnimatedSwitcher(
|
|
key: const ValueKey('player_bar_art_switcher'),
|
|
duration: const Duration(milliseconds: 250),
|
|
child: leadingWidget),
|
|
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: <Widget>[
|
|
PrevNextButton(
|
|
iconSize,
|
|
prev: true,
|
|
),
|
|
PlayPauseButton(iconSize),
|
|
PrevNextButton(iconSize)
|
|
],
|
|
),
|
|
)));
|
|
}),
|
|
),
|
|
SizedBox(
|
|
height: 3.0,
|
|
child: StreamBuilder<Duration>(
|
|
stream: AudioService.position,
|
|
builder: (context, snapshot) {
|
|
return LinearProgressIndicator(
|
|
value: parsePosition(snapshot.data ?? Duration.zero),
|
|
);
|
|
}),
|
|
),
|
|
]),
|
|
);
|
|
}
|
|
}
|
|
|
|
class PrevNextButton extends StatelessWidget {
|
|
final double size;
|
|
final bool prev;
|
|
final bool hidePrev;
|
|
const PrevNextButton(this.size,
|
|
{super.key, this.prev = false, this.hidePrev = false});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Listens to both mediaItem updates (a.k.a. when the song changes)
|
|
// and queue updates (so for example, in SmartTrackLists, when the songs are fetched, it's updated with the new items)
|
|
return StreamBuilder<bool>(
|
|
stream: Rx.combineLatest2<MediaItem?, List<MediaItem>, bool>(
|
|
audioHandler.mediaItem,
|
|
audioHandler.queue,
|
|
(_, queue) => playerHelper.queueIndex < queue.length - 1),
|
|
builder: (context, snapshot) {
|
|
if (!prev) {
|
|
return IconButton(
|
|
splashRadius: size * 2.0,
|
|
icon: Icon(
|
|
Icons.skip_next,
|
|
semanticLabel: "Play next".i18n,
|
|
),
|
|
iconSize: size,
|
|
onPressed:
|
|
snapshot.data == true ? () => audioHandler.skipToNext() : null,
|
|
);
|
|
}
|
|
final canGoPrev = playerHelper.queueIndex > 0;
|
|
|
|
if (!canGoPrev && hidePrev) return const SizedBox.shrink();
|
|
|
|
return IconButton(
|
|
splashRadius: size * 2.0,
|
|
icon: Icon(
|
|
Icons.skip_previous,
|
|
semanticLabel: "Play previous".i18n,
|
|
),
|
|
iconSize: size,
|
|
onPressed: canGoPrev ? () => audioHandler.skipToPrevious() : null,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class PlayPauseButton extends StatefulWidget {
|
|
final double size;
|
|
final bool filled;
|
|
final Color? iconColor;
|
|
|
|
/// The color of the card if [filled] is true
|
|
final Color? color;
|
|
const PlayPauseButton(
|
|
this.size, {
|
|
Key? key,
|
|
this.filled = false,
|
|
this.color,
|
|
this.iconColor,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<PlayPauseButton> createState() => _PlayPauseButtonState();
|
|
}
|
|
|
|
class _PlayPauseButtonState extends State<PlayPauseButton>
|
|
with SingleTickerProviderStateMixin {
|
|
late final AnimationController _controller;
|
|
late final Animation<double> _animation;
|
|
late StreamSubscription _subscription;
|
|
late bool _canPlay = audioHandler.playbackState.value.processingState ==
|
|
AudioProcessingState.ready ||
|
|
audioHandler.playbackState.value.processingState ==
|
|
AudioProcessingState.idle;
|
|
|
|
@override
|
|
void initState() {
|
|
_controller = AnimationController(
|
|
vsync: this, duration: const Duration(milliseconds: 200));
|
|
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
|
|
|
|
_subscription = audioHandler.playbackState.listen((playbackState) {
|
|
if (playbackState.processingState == AudioProcessingState.ready ||
|
|
audioHandler.playbackState.value.processingState ==
|
|
AudioProcessingState.idle) {
|
|
if (playbackState.playing) {
|
|
_controller.forward();
|
|
} else {
|
|
_controller.reverse();
|
|
}
|
|
if (!_canPlay) setState(() => _canPlay = true);
|
|
return;
|
|
}
|
|
setState(() => _canPlay = false);
|
|
});
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_subscription.cancel();
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _playPause() {
|
|
if (audioHandler.playbackState.value.playing) {
|
|
audioHandler.pause();
|
|
} else {
|
|
audioHandler.play();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final Widget? child;
|
|
if (_canPlay) {
|
|
final icon = AnimatedIcon(
|
|
icon: AnimatedIcons.play_pause,
|
|
progress: _animation,
|
|
semanticLabel: audioHandler.playbackState.value.playing
|
|
? 'Pause'.i18n
|
|
: 'Play'.i18n,
|
|
);
|
|
if (!widget.filled) {
|
|
return IconButton(
|
|
color: widget.iconColor,
|
|
icon: icon,
|
|
iconSize: widget.size,
|
|
onPressed: _playPause);
|
|
} else {
|
|
child = Material(
|
|
type: MaterialType.transparency,
|
|
child: InkWell(
|
|
customBorder: const CircleBorder(),
|
|
onTap: _playPause,
|
|
child: IconTheme.merge(
|
|
child: Center(child: icon),
|
|
data: IconThemeData(
|
|
size: widget.size / 2, color: widget.iconColor))),
|
|
);
|
|
}
|
|
} else {
|
|
switch (audioHandler.playbackState.value.processingState) {
|
|
//Stopped/Error
|
|
case AudioProcessingState.error:
|
|
child = null;
|
|
break;
|
|
//Loading, connecting, rewinding...
|
|
default:
|
|
child = const Center(child: CircularProgressIndicator());
|
|
break;
|
|
}
|
|
}
|
|
if (widget.filled) {
|
|
return SizedBox.square(
|
|
dimension: widget.size,
|
|
child: AnimatedContainer(
|
|
duration: const Duration(seconds: 1),
|
|
decoration:
|
|
BoxDecoration(shape: BoxShape.circle, color: widget.color),
|
|
child: child));
|
|
} else {
|
|
return SizedBox.square(dimension: widget.size, child: child);
|
|
}
|
|
}
|
|
}
|