freezer/lib/ui/player_bar.dart
Pato05 2862c9ec05
remove browser login for desktop
restore translations functionality
make scrollViews handle mouse pointers like touch, so that pull to refresh functionality is available
exit app if opening cache or settings fails (another instance running)
remove draggable_scrollbar and use builtin widget instead
fix email login
better way to manage lyrics (less updates and lookups in the lyrics List)
fix player_screen on mobile (too big -> just average :))
right click: use TapUp events instead
desktop: show context menu on triple dots button also
avoid showing connection error if the homepage is cached and available offline
i'm probably forgetting something idk
2023-10-25 00:32:28 +02:00

299 lines
9.9 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: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);
}
}
}