first commit
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
import '../../services/music_player_background_task.dart';
|
||||
import '../../services/progress_state_stream.dart';
|
||||
import '../print_duration.dart';
|
||||
|
||||
class ProgressSlider extends StatefulWidget {
|
||||
const ProgressSlider({
|
||||
Key? key,
|
||||
this.allowSeeking = true,
|
||||
this.showBuffer = true,
|
||||
this.showDuration = true,
|
||||
this.showPlaceholder = true,
|
||||
}) : super(key: key);
|
||||
|
||||
final bool allowSeeking;
|
||||
final bool showBuffer;
|
||||
final bool showDuration;
|
||||
final bool showPlaceholder;
|
||||
|
||||
@override
|
||||
State<ProgressSlider> createState() => _ProgressSliderState();
|
||||
}
|
||||
|
||||
class _ProgressSliderState extends State<ProgressSlider> {
|
||||
/// Value used to hold the slider's value when dragging.
|
||||
double? _dragValue;
|
||||
|
||||
late SliderThemeData _sliderThemeData;
|
||||
|
||||
final _audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
_sliderThemeData = SliderTheme.of(context).copyWith(
|
||||
trackHeight: 4.0,
|
||||
inactiveTrackColor:
|
||||
Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// The slider always needs to be LTR, so we use Directionality to save
|
||||
// putting TextDirection.ltr all over the place
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
// The slider can refresh up to 60 times per second, so we wrap it in a
|
||||
// RepaintBoundary to avoid more areas being repainted than necessary
|
||||
child: RepaintBoundary(
|
||||
child: StreamBuilder<ProgressState>(
|
||||
stream: progressStateStream,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data?.mediaItem == null) {
|
||||
// If nothing is playing or the AudioService isn't connected, return a
|
||||
// greyed out slider with some fake numbers. We also do this if
|
||||
// currentPosition is null, which sometimes happens when the app is
|
||||
// closed and reopened.
|
||||
return widget.showPlaceholder
|
||||
? Column(
|
||||
children: [
|
||||
SliderTheme(
|
||||
data: _sliderThemeData.copyWith(
|
||||
trackShape: CustomTrackShape(),
|
||||
),
|
||||
child: const Slider(
|
||||
value: 0,
|
||||
max: 1,
|
||||
onChanged: null,
|
||||
),
|
||||
),
|
||||
if (widget.showDuration)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"00:00",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.color),
|
||||
),
|
||||
Text(
|
||||
"00:00",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.color),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
} else if (snapshot.hasData) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Slider displaying playback and buffering progress.
|
||||
SliderTheme(
|
||||
data: widget.allowSeeking
|
||||
? _sliderThemeData.copyWith(
|
||||
trackShape: CustomTrackShape(),
|
||||
)
|
||||
: _sliderThemeData.copyWith(
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 0),
|
||||
// gets rid of both horizontal and vertical padding
|
||||
overlayShape:
|
||||
const RoundSliderOverlayShape(overlayRadius: 0),
|
||||
trackShape: const RectangularSliderTrackShape(),
|
||||
),
|
||||
child: Slider(
|
||||
min: 0.0,
|
||||
max: snapshot.data!.mediaItem?.duration == null
|
||||
? snapshot.data!.playbackState.bufferedPosition
|
||||
.inMicroseconds
|
||||
.toDouble()
|
||||
: snapshot.data!.mediaItem!.duration!.inMicroseconds
|
||||
.toDouble(),
|
||||
value: (_dragValue ??
|
||||
snapshot.data!.position.inMicroseconds)
|
||||
.clamp(
|
||||
0,
|
||||
snapshot.data!.mediaItem!.duration!.inMicroseconds
|
||||
.toDouble())
|
||||
.toDouble(),
|
||||
secondaryTrackValue: widget.showBuffer &&
|
||||
snapshot.data!.mediaItem
|
||||
?.extras?["downloadedSongJson"] ==
|
||||
null
|
||||
? snapshot.data!.playbackState.bufferedPosition
|
||||
.inMicroseconds
|
||||
.clamp(
|
||||
0.0,
|
||||
snapshot.data!.mediaItem!.duration == null
|
||||
? snapshot.data!.playbackState
|
||||
.bufferedPosition.inMicroseconds
|
||||
: snapshot.data!.mediaItem!.duration!
|
||||
.inMicroseconds,
|
||||
)
|
||||
.toDouble()
|
||||
: 0,
|
||||
onChanged: widget.allowSeeking
|
||||
? (newValue) async {
|
||||
// We don't actually tell audio_service to seek here
|
||||
// because it would get flooded with seek requests
|
||||
setState(() {
|
||||
_dragValue = newValue;
|
||||
});
|
||||
}
|
||||
: (_) {},
|
||||
onChangeStart: widget.allowSeeking
|
||||
? (value) {
|
||||
setState(() {
|
||||
_dragValue = value;
|
||||
});
|
||||
}
|
||||
: (_) {},
|
||||
onChangeEnd: widget.allowSeeking
|
||||
? (newValue) async {
|
||||
// Seek to the new position
|
||||
await _audioHandler.seek(
|
||||
Duration(microseconds: newValue.toInt()));
|
||||
|
||||
// Clear drag value so that the slider uses the play
|
||||
// duration again.
|
||||
setState(() {
|
||||
_dragValue = null;
|
||||
});
|
||||
}
|
||||
: (_) {},
|
||||
),
|
||||
),
|
||||
if (widget.showDuration)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
printDuration(
|
||||
Duration(
|
||||
microseconds: _dragValue?.toInt() ??
|
||||
snapshot.data!.position.inMicroseconds),
|
||||
),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.color),
|
||||
),
|
||||
Text(
|
||||
printDuration(snapshot.data!.mediaItem?.duration),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.color),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const Text(
|
||||
"Snapshot doesn't have data and MediaItem isn't null and AudioService is connected?");
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PositionData {
|
||||
final Duration position;
|
||||
final Duration bufferedPosition;
|
||||
|
||||
PositionData(this.position, this.bufferedPosition);
|
||||
}
|
||||
|
||||
/// Track shape used to remove horizontal padding.
|
||||
/// https://github.com/flutter/flutter/issues/37057
|
||||
class CustomTrackShape extends RoundedRectSliderTrackShape {
|
||||
@override
|
||||
Rect getPreferredRect({
|
||||
required RenderBox parentBox,
|
||||
Offset offset = Offset.zero,
|
||||
required SliderThemeData sliderTheme,
|
||||
bool isEnabled = false,
|
||||
bool isDiscrete = false,
|
||||
}) {
|
||||
final double trackHeight = sliderTheme.trackHeight!;
|
||||
final double trackLeft = offset.dx;
|
||||
final double trackTop =
|
||||
offset.dy + (parentBox.size.height - trackHeight) / 2;
|
||||
final double trackWidth = parentBox.size.width;
|
||||
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
|
||||
}
|
||||
|
||||
/// Disable additionalActiveTrackHeight
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
Offset offset, {
|
||||
required RenderBox parentBox,
|
||||
required SliderThemeData sliderTheme,
|
||||
required Animation<double> enableAnimation,
|
||||
required TextDirection textDirection,
|
||||
required Offset thumbCenter,
|
||||
Offset? secondaryOffset,
|
||||
bool isDiscrete = false,
|
||||
bool isEnabled = false,
|
||||
double additionalActiveTrackHeight = 0,
|
||||
}) {
|
||||
super.paint(
|
||||
context,
|
||||
offset,
|
||||
parentBox: parentBox,
|
||||
sliderTheme: sliderTheme,
|
||||
enableAnimation: enableAnimation,
|
||||
textDirection: textDirection,
|
||||
thumbCenter: thumbCenter,
|
||||
secondaryOffset: secondaryOffset,
|
||||
isDiscrete: isDiscrete,
|
||||
isEnabled: isEnabled,
|
||||
additionalActiveTrackHeight: additionalActiveTrackHeight,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user