405 lines
19 KiB
Dart
405 lines
19 KiB
Dart
import 'package:auto_size_text/auto_size_text.dart';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_xlider/flutter_xlider.dart';
|
|
import 'package:ocarina/api/audio/audioplayer_service.dart';
|
|
import 'package:ocarina/api/subsonic/song.dart';
|
|
import 'package:ocarina/main.dart';
|
|
import 'package:ocarina/util/util.dart';
|
|
import 'package:responsive_sizer/responsive_sizer.dart';
|
|
import 'package:shimmer/shimmer.dart';
|
|
import 'package:text_scroll/text_scroll.dart';
|
|
|
|
/// The player control widget
|
|
///
|
|
/// Showcases the playing song's details and features playback controls
|
|
class Player extends StatefulWidget {
|
|
/// The player control widget
|
|
///
|
|
/// Showcases the playing song's details and features playback controls
|
|
const Player({super.key});
|
|
|
|
@override
|
|
State<Player> createState() => PlayerState();
|
|
}
|
|
|
|
/// State of [Player]
|
|
class PlayerState extends State<Player> {
|
|
void update() {
|
|
logger.d(AudioPlayerService().song?.coverArtUrl);
|
|
setState(() {});
|
|
}
|
|
|
|
var _showFullControls = false;
|
|
final _sheetController = DraggableScrollableController();
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return NotificationListener<DraggableScrollableNotification>(
|
|
onNotification: (n) {
|
|
if (n.extent > 0.3) {
|
|
_showFullControls = true;
|
|
} else {
|
|
_showFullControls = false;
|
|
}
|
|
setState(() {});
|
|
return false;
|
|
},
|
|
child: DraggableScrollableSheet(
|
|
controller: _sheetController,
|
|
initialChildSize: 0.1,
|
|
snap: true,
|
|
snapSizes: const [0.1, 1],
|
|
minChildSize: 0.1,
|
|
builder: (c, s) => SingleChildScrollView(
|
|
controller: s,
|
|
child: Column(
|
|
children: [
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
_sheetController.animateTo(
|
|
(_sheetController.size == 1) ? 0.1 : 1,
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.easeIn,
|
|
);
|
|
},
|
|
child: Container(
|
|
color: Theme.of(context).colorScheme.primaryContainer,
|
|
height: 10.h,
|
|
width: 100.w,
|
|
child: AnimatedOpacity(
|
|
opacity: _showFullControls ? 0 : 1,
|
|
duration: const Duration(milliseconds: 300),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: ValueListenableBuilder<Song?>(
|
|
valueListenable: songNotifier,
|
|
builder: (c, t, w) {
|
|
return Row(
|
|
children: [
|
|
SizedBox(
|
|
height: 10.h,
|
|
width: 10.h,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: ClipRRect(
|
|
child: (t == null)
|
|
? ColoredBox(
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.primaryContainer,
|
|
child: Center(
|
|
child: Icon(
|
|
Icons.music_note,
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.onPrimaryContainer,
|
|
),
|
|
),
|
|
)
|
|
: CachedNetworkImage(
|
|
cacheKey: t.coverArtId,
|
|
imageUrl: t.coverArtUrl,
|
|
placeholder: (c, d) =>
|
|
Shimmer.fromColors(
|
|
baseColor: Colors.grey.shade300,
|
|
highlightColor:
|
|
Colors.grey.shade100,
|
|
child: Container(
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
errorWidget: (c, _, __) {
|
|
logger
|
|
..e(_)
|
|
..e(__);
|
|
return ColoredBox(
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.primaryContainer,
|
|
child: Center(
|
|
child: Icon(
|
|
Icons.music_note,
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.onPrimaryContainer,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(
|
|
width: 5,
|
|
),
|
|
Expanded(
|
|
child: AutoSizeText(
|
|
AudioPlayerService().song == null
|
|
? "Nothing"
|
|
: AudioPlayerService().song!.title,
|
|
style: TextStyle(
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.onPrimaryContainer,
|
|
fontSize: 14,
|
|
decoration: TextDecoration.none,
|
|
),
|
|
overflowReplacement: TextScroll(
|
|
AudioPlayerService().song == null
|
|
? "Nothing"
|
|
: AudioPlayerService().song!.title,
|
|
),
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 35.w,
|
|
child: Row(
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
IconButton(
|
|
onPressed: () {
|
|
AudioPlayerService().previous();
|
|
},
|
|
icon: const Icon(Icons.skip_previous),
|
|
),
|
|
IconButton(
|
|
onPressed: () async {
|
|
if (AudioPlayerService().song ==
|
|
null) {
|
|
return;
|
|
}
|
|
if (AudioPlayerService().isPlaying) {
|
|
await AudioPlayerService().pause();
|
|
} else {
|
|
AudioPlayerService().resume();
|
|
}
|
|
setState(() {});
|
|
},
|
|
icon: AnimatedCrossFade(
|
|
firstChild:
|
|
const Icon(Icons.play_arrow),
|
|
secondChild: const Icon(Icons.pause),
|
|
crossFadeState:
|
|
(AudioPlayerService().isPlaying)
|
|
? CrossFadeState.showSecond
|
|
: CrossFadeState.showFirst,
|
|
duration:
|
|
const Duration(milliseconds: 300),
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () {
|
|
AudioPlayerService().next();
|
|
},
|
|
icon: const Icon(Icons.skip_next),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
// Full player controls here
|
|
height: 90.h,
|
|
color: Theme.of(context).colorScheme.primaryContainer,
|
|
width: 100.w,
|
|
child: Center(
|
|
child: Column(
|
|
children: [
|
|
const SizedBox(
|
|
height: 30,
|
|
),
|
|
SizedBox(
|
|
width: 80.w,
|
|
height: 80.w,
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(16),
|
|
child: (AudioPlayerService().song == null)
|
|
? ColoredBox(
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.secondaryContainer,
|
|
child: Center(
|
|
child: Icon(
|
|
Icons.music_note,
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.onSecondaryContainer,
|
|
),
|
|
),
|
|
)
|
|
: (CachedNetworkImage(
|
|
cacheKey:
|
|
AudioPlayerService().song!.coverArtId,
|
|
imageUrl:
|
|
AudioPlayerService().song!.coverArtUrl,
|
|
placeholder: (c, d) => Shimmer.fromColors(
|
|
baseColor: Colors.grey.shade300,
|
|
highlightColor: Colors.grey.shade100,
|
|
child: Container(
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
errorWidget: (c, _, __) {
|
|
logger
|
|
..e(_)
|
|
..e(__);
|
|
return ColoredBox(
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.secondaryContainer,
|
|
child: Center(
|
|
child: Icon(
|
|
Icons.music_note,
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.onSecondaryContainer,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
)),
|
|
),
|
|
),
|
|
const SizedBox(
|
|
height: 10,
|
|
),
|
|
SizedBox(
|
|
width: 70.w,
|
|
height: 40,
|
|
child: AutoSizeText(
|
|
AudioPlayerService().song?.title ?? "Nothing",
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 20.sp,
|
|
),
|
|
overflowReplacement: TextScroll(
|
|
AudioPlayerService().song?.title ?? "Nothing",
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 20.sp,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 70.w,
|
|
height: 30,
|
|
child: AutoSizeText(
|
|
AudioPlayerService().song?.artistName ?? "Nobody",
|
|
style: TextStyle(fontSize: 16.sp),
|
|
textAlign: TextAlign.center,
|
|
overflowReplacement: TextScroll(
|
|
AudioPlayerService().song?.artistName ?? "Nobody",
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 60.w,
|
|
height: 40,
|
|
child: Overlay(
|
|
initialEntries: [
|
|
OverlayEntry(
|
|
builder: (c) => ValueListenableBuilder<List<int>>(
|
|
valueListenable: progressNotifier,
|
|
builder: (c, v, _) {
|
|
return FlutterSlider(
|
|
values: [v[0].toDouble()],
|
|
max: v[1].toDouble(),
|
|
min: 0,
|
|
onDragCompleted:
|
|
(handlerIndex, lowerValue, upperValue) {
|
|
if (AudioPlayerService().song == null) {
|
|
return;
|
|
}
|
|
logger.d(lowerValue);
|
|
AudioPlayerService()
|
|
.seek(lowerValue as double);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(
|
|
height: 10,
|
|
),
|
|
SizedBox(
|
|
width: 60.w,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
IconButton(
|
|
onPressed: () {
|
|
AudioPlayerService().previous();
|
|
},
|
|
icon: const Icon(
|
|
Icons.skip_previous,
|
|
size: 36,
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () async {
|
|
if (AudioPlayerService().song == null) {
|
|
return;
|
|
}
|
|
if (AudioPlayerService().isPlaying) {
|
|
await AudioPlayerService().pause();
|
|
} else {
|
|
AudioPlayerService().resume();
|
|
}
|
|
setState(() {});
|
|
},
|
|
icon: AnimatedCrossFade(
|
|
firstChild: const Icon(
|
|
Icons.play_arrow,
|
|
size: 36, // TODO: adapt to display size
|
|
),
|
|
secondChild: const Icon(Icons.pause, size: 36),
|
|
crossFadeState: (AudioPlayerService().isPlaying)
|
|
? CrossFadeState.showSecond
|
|
: CrossFadeState.showFirst,
|
|
duration: const Duration(milliseconds: 300),
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () {
|
|
AudioPlayerService().next();
|
|
},
|
|
icon: const Icon(
|
|
Icons.skip_next,
|
|
size: 36,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|