ui improvements in lyrics screen animated bars when track is playing fix back button when player screen is open instantly pop when track is changed in queue list
1390 lines
47 KiB
Dart
1390 lines
47 KiB
Dart
// ignore_for_file: unused_import
|
|
|
|
import 'dart:convert';
|
|
import 'dart:ui';
|
|
import 'dart:async';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:dynamic_color/dynamic_color.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:audio_service/audio_service.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:freezer/api/cache.dart';
|
|
import 'package:freezer/api/deezer.dart';
|
|
import 'package:freezer/api/definitions.dart';
|
|
import 'package:freezer/api/player/audio_handler.dart';
|
|
import 'package:freezer/api/player/player_helper.dart';
|
|
import 'package:freezer/main.dart';
|
|
import 'package:freezer/page_routes/fade.dart';
|
|
import 'package:freezer/settings.dart';
|
|
import 'package:freezer/translations.i18n.dart';
|
|
import 'package:freezer/ui/cached_image.dart';
|
|
import 'package:freezer/ui/fancy_scaffold.dart';
|
|
import 'package:freezer/ui/lyrics_screen.dart';
|
|
import 'package:freezer/ui/menu.dart';
|
|
import 'package:freezer/ui/player_bar.dart';
|
|
import 'package:freezer/ui/queue_screen.dart';
|
|
import 'package:freezer/ui/settings_screen.dart';
|
|
import 'package:logging/logging.dart';
|
|
import 'package:marquee/marquee.dart';
|
|
import 'package:palette_generator/palette_generator.dart';
|
|
import 'package:photo_view/photo_view.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
|
|
const _blurStrength = 90.0;
|
|
|
|
/// A simple [ChangeNotifier] that listens to the [AudioHandler.mediaItem] stream and
|
|
/// notifies its listeners when background changes
|
|
class BackgroundProvider extends ChangeNotifier {
|
|
PaletteGenerator? _palette;
|
|
Color? _dominantColor;
|
|
ImageProvider? _imageProvider;
|
|
StreamSubscription? _mediaItemSub;
|
|
bool _isDisposed = false;
|
|
BackgroundProvider();
|
|
|
|
/// Calculate background color from [mediaItem]
|
|
///
|
|
/// Warning: this function is expensive to call, and should only be called when songs change!
|
|
Future<void> _updateColor(MediaItem mediaItem) async {
|
|
if (!settings.colorGradientBackground &&
|
|
!settings.blurPlayerBackground &&
|
|
!settings.enableFilledPlayButton &&
|
|
!settings.playerAlbumArtDropShadow) return;
|
|
final imageProvider = CachedNetworkImageProvider(
|
|
mediaItem.extras!['thumb'] ?? mediaItem.artUri.toString(),
|
|
cacheManager: cacheManager);
|
|
//Run in isolate
|
|
_palette = await PaletteGenerator.fromImageProvider(imageProvider);
|
|
_dominantColor = _palette!.dominantColor!.color;
|
|
_imageProvider = settings.blurPlayerBackground ? imageProvider : null;
|
|
if (!_isDisposed) notifyListeners();
|
|
}
|
|
|
|
@override
|
|
void addListener(VoidCallback listener) {
|
|
print('[PROVIDER] listener added $hasListeners');
|
|
_mediaItemSub ??= audioHandler.mediaItem.listen((mediaItem) {
|
|
if (mediaItem == null) return;
|
|
_updateColor(mediaItem);
|
|
});
|
|
super.addListener(listener);
|
|
}
|
|
|
|
@override
|
|
void removeListener(VoidCallback listener) {
|
|
super.removeListener(listener);
|
|
print('[PROVIDER] listener removed! hasListeners? $hasListeners');
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
print('[PROVIDER] DISPOSED');
|
|
_isDisposed = true;
|
|
_mediaItemSub?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
Color? get dominantColor => _dominantColor;
|
|
PaletteGenerator? get palette => _palette;
|
|
ImageProvider<Object>? get imageProvider => _imageProvider;
|
|
}
|
|
|
|
class PlayerScreen extends StatelessWidget {
|
|
const PlayerScreen({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ChangeNotifierProvider(
|
|
lazy: false,
|
|
create: (context) => BackgroundProvider(),
|
|
child: PlayerArtColorScheme(
|
|
child: PlayerScreenBackground(
|
|
child: MainScreen.of(context).isDesktop
|
|
? const PlayerScreenDesktop()
|
|
: OrientationBuilder(
|
|
builder: (context, orientation) =>
|
|
orientation == Orientation.landscape
|
|
? const PlayerScreenHorizontal()
|
|
: const PlayerScreenVertical())),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Will inject a [Theme] containing a [ColorScheme] generated
|
|
/// from [BackgroundProvider.dominantColor]
|
|
class PlayerArtColorScheme extends StatelessWidget {
|
|
final Widget child;
|
|
final bool enabled;
|
|
final bool forceDark;
|
|
const PlayerArtColorScheme({
|
|
super.key,
|
|
required this.child,
|
|
this.enabled = true,
|
|
this.forceDark = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final backgroundProvider = context.watch<BackgroundProvider>();
|
|
if (backgroundProvider.dominantColor == null) {
|
|
// do nothing
|
|
return child;
|
|
}
|
|
|
|
final brightness =
|
|
forceDark ? Brightness.dark : Theme.of(context).colorScheme.brightness;
|
|
|
|
return Theme(
|
|
data: Theme.of(context).copyWith(
|
|
colorScheme: ColorScheme.fromSeed(
|
|
seedColor: backgroundProvider.dominantColor!,
|
|
brightness: brightness,
|
|
)),
|
|
child: child);
|
|
}
|
|
}
|
|
|
|
/// Will change the background based on [BackgroundProvider],
|
|
/// it will wrap the [child] in a [Scaffold] and [SafeArea] widget
|
|
class PlayerScreenBackground extends StatelessWidget {
|
|
final Widget child;
|
|
final bool enabled;
|
|
final PreferredSizeWidget? appBar;
|
|
const PlayerScreenBackground({
|
|
super.key,
|
|
required this.child,
|
|
this.enabled = true,
|
|
this.appBar,
|
|
});
|
|
|
|
Widget _buildChild(
|
|
BuildContext context, BackgroundProvider provider, Widget child) {
|
|
final isLightMode = Theme.of(context).brightness == Brightness.light;
|
|
return Stack(children: [
|
|
if (provider.imageProvider != null || settings.colorGradientBackground)
|
|
Positioned.fill(
|
|
child: provider.imageProvider != null
|
|
? DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
color: Color.lerp(provider.dominantColor,
|
|
isLightMode ? Colors.white : Colors.black, 0.75)),
|
|
child: ImageFiltered(
|
|
imageFilter: ImageFilter.blur(
|
|
tileMode: TileMode.decal,
|
|
sigmaX: _blurStrength,
|
|
sigmaY: _blurStrength,
|
|
),
|
|
child: DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
image: DecorationImage(
|
|
image: provider.imageProvider!,
|
|
fit: BoxFit.cover,
|
|
opacity: 0.35,
|
|
)),
|
|
),
|
|
),
|
|
)
|
|
: DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
if (provider.dominantColor != null)
|
|
provider.dominantColor!,
|
|
Theme.of(context).scaffoldBackgroundColor,
|
|
],
|
|
stops: const [0.0, 0.6],
|
|
)),
|
|
)),
|
|
child,
|
|
]);
|
|
}
|
|
|
|
static SystemUiOverlayStyle? getSystemUiOverlayStyle(BuildContext context,
|
|
{bool enabled = true}) {
|
|
final hasBackground = enabled &&
|
|
(settings.blurPlayerBackground || settings.colorGradientBackground);
|
|
if (!hasBackground) return null;
|
|
const color = Colors.transparent;
|
|
final brightness = hasBackground
|
|
? Brightness.light
|
|
: (ThemeData.estimateBrightnessForColor(color) == Brightness.light
|
|
? Brightness.dark
|
|
: Brightness.light);
|
|
return SystemUiOverlayStyle(
|
|
statusBarColor: color,
|
|
statusBarBrightness: brightness,
|
|
statusBarIconBrightness: brightness,
|
|
systemNavigationBarIconBrightness: brightness,
|
|
systemNavigationBarColor: color,
|
|
systemNavigationBarDividerColor: color,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final hasBackground = enabled &&
|
|
(settings.blurPlayerBackground || settings.colorGradientBackground);
|
|
final color = hasBackground
|
|
? Colors.transparent
|
|
: Theme.of(context).colorScheme.background;
|
|
Widget widgetChild = Scaffold(
|
|
appBar: appBar,
|
|
backgroundColor: color,
|
|
body: SafeArea(child: child),
|
|
);
|
|
if (enabled) {
|
|
widgetChild = Consumer<BackgroundProvider>(
|
|
builder: (context, provider, child) {
|
|
return _buildChild(context, provider, child!);
|
|
},
|
|
child: widgetChild,
|
|
);
|
|
}
|
|
final suios = getSystemUiOverlayStyle(context, enabled: enabled);
|
|
if (appBar == null && suios != null) {
|
|
widgetChild = AnnotatedRegion<SystemUiOverlayStyle>(
|
|
value: suios,
|
|
child: widgetChild,
|
|
);
|
|
}
|
|
return widgetChild;
|
|
}
|
|
}
|
|
|
|
//Landscape
|
|
class PlayerScreenHorizontal extends StatelessWidget {
|
|
const PlayerScreenHorizontal({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: <Widget>[
|
|
const Expanded(
|
|
flex: 4,
|
|
child: Padding(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: BigAlbumArt(),
|
|
),
|
|
),
|
|
const SizedBox(width: 56.0),
|
|
//Right side
|
|
Expanded(
|
|
flex: 5,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: <Widget>[
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(0, 0, 8, 0),
|
|
child: PlayerScreenTopRow(
|
|
textSize: 24.sp,
|
|
iconSize: 36.sp,
|
|
textWidth: 350.w,
|
|
short: true),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
|
child: PlayerTextSubtext(textSize: 35.sp),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
child: SeekBar(textSize: 24.sp),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
|
child: PlaybackControls(46.sp),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
|
child: BottomBarControls(size: 30.sp),
|
|
)
|
|
],
|
|
),
|
|
)
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
//Portrait
|
|
class PlayerScreenVertical extends StatelessWidget {
|
|
const PlayerScreenVertical({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: <Widget>[
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
|
child: PlayerScreenTopRow(
|
|
textSize: 14.spMax,
|
|
iconSize: 20.spMax,
|
|
),
|
|
),
|
|
const Flexible(child: BigAlbumArt()),
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 24.0, right: 16.0),
|
|
child: PlayerTextSubtext(textSize: 24.spMax),
|
|
),
|
|
SeekBar(textSize: 14.spMax),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
child: PlaybackControls(32.spMax),
|
|
),
|
|
Padding(
|
|
padding:
|
|
const EdgeInsets.symmetric(vertical: 0, horizontal: 16.0),
|
|
child: BottomBarControls(size: 22.spMax),
|
|
)
|
|
],
|
|
));
|
|
}
|
|
}
|
|
|
|
class PlayerScreenDesktop extends StatelessWidget {
|
|
const PlayerScreenDesktop({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(children: [
|
|
AspectRatio(
|
|
aspectRatio: 9 / 16,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
|
child: PlayerScreenTopRow(
|
|
textSize: 12.h, iconSize: 21.h, desktopMode: true),
|
|
),
|
|
Flexible(
|
|
child: ConstrainedBox(
|
|
constraints: BoxConstraints.loose(const Size.square(500)),
|
|
child: const BigAlbumArt(showLyricsButton: false)),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 24.0, right: 16.0),
|
|
child: PlayerTextSubtext(textSize: 22.h),
|
|
),
|
|
SeekBar(textSize: 16.h),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
child: PlaybackControls(28.h),
|
|
),
|
|
Padding(
|
|
padding:
|
|
const EdgeInsets.symmetric(vertical: 0, horizontal: 16.0),
|
|
child: BottomBarControls(
|
|
size: 20.h,
|
|
desktopMode: true,
|
|
),
|
|
)
|
|
]),
|
|
),
|
|
),
|
|
const Expanded(
|
|
flex: 2,
|
|
child: Padding(
|
|
padding: EdgeInsets.only(left: 16.0, right: 16.0, top: 16.0),
|
|
child: _DesktopTabView(),
|
|
)),
|
|
]);
|
|
}
|
|
}
|
|
|
|
class _DesktopTabView extends StatelessWidget {
|
|
const _DesktopTabView({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return DefaultTabController(
|
|
length: 2,
|
|
child: Column(children: [
|
|
TabBar(
|
|
tabs: [
|
|
Tab(
|
|
text: 'Queue'.i18n,
|
|
height: 48.0,
|
|
),
|
|
Tab(
|
|
text: 'Lyrics'.i18n,
|
|
),
|
|
],
|
|
labelStyle: Theme.of(context)
|
|
.textTheme
|
|
.labelLarge!
|
|
.copyWith(fontSize: 18.0)),
|
|
Expanded(
|
|
child: SizedBox.expand(
|
|
child: Material(
|
|
type: MaterialType.transparency,
|
|
child: TabBarView(children: [
|
|
QueueListWidget(
|
|
closePlayer: FancyScaffold.of(context)!.closePanel,
|
|
isInsidePlayer: true,
|
|
),
|
|
const LyricsWidget(),
|
|
]),
|
|
),
|
|
)),
|
|
]),
|
|
);
|
|
}
|
|
}
|
|
|
|
class FitOrScrollText extends StatefulWidget {
|
|
final String text;
|
|
final TextStyle style;
|
|
final TextAlign? textAlign;
|
|
final TextDirection? textDirection;
|
|
final int? maxLines;
|
|
const FitOrScrollText({
|
|
required this.text,
|
|
required this.style,
|
|
this.textAlign,
|
|
this.textDirection,
|
|
this.maxLines,
|
|
super.key,
|
|
});
|
|
|
|
@override
|
|
State<FitOrScrollText> createState() => _FitOrScrollTextState();
|
|
}
|
|
|
|
class _FitOrScrollTextState extends State<FitOrScrollText> {
|
|
bool _checkTextFits(String text, BoxConstraints constraints) {
|
|
final textPainter = TextPainter(
|
|
text: TextSpan(text: text, style: widget.style),
|
|
textAlign: widget.textAlign ?? TextAlign.left,
|
|
textDirection: widget.textDirection ?? TextDirection.ltr,
|
|
maxLines: widget.maxLines,
|
|
);
|
|
|
|
textPainter.layout(maxWidth: constraints.maxWidth);
|
|
|
|
return !(textPainter.didExceedMaxLines ||
|
|
textPainter.height > constraints.maxHeight ||
|
|
textPainter.width > constraints.maxWidth);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(builder: (context, constraints) {
|
|
return _checkTextFits(widget.text, constraints)
|
|
? Text(
|
|
widget.text,
|
|
maxLines: widget.maxLines,
|
|
style: widget.style,
|
|
)
|
|
: Marquee(
|
|
text: widget.text,
|
|
style: widget.style,
|
|
blankSpace: 32.0,
|
|
startPadding: 0.0,
|
|
numberOfRounds: 2,
|
|
accelerationDuration: const Duration(seconds: 1),
|
|
pauseAfterRound: const Duration(seconds: 2),
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
fadingEdgeEndFraction: 0.05,
|
|
fadingEdgeStartFraction: 0.05,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
class PlayerTextSubtext extends StatelessWidget {
|
|
final double textSize;
|
|
const PlayerTextSubtext({super.key, required this.textSize});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return StreamBuilder<MediaItem?>(
|
|
stream: audioHandler.mediaItem,
|
|
builder: (context, snapshot) {
|
|
if (!snapshot.hasData) {
|
|
return const SizedBox();
|
|
}
|
|
final currentMediaItem = snapshot.data!;
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: <Widget>[
|
|
SizedBox(
|
|
height: 1.5 * textSize,
|
|
child: FitOrScrollText(
|
|
key: Key(currentMediaItem.displayTitle!),
|
|
text: currentMediaItem.displayTitle!,
|
|
maxLines: 1,
|
|
style: TextStyle(
|
|
fontSize: textSize,
|
|
fontWeight: FontWeight.bold,
|
|
overflow: TextOverflow.ellipsis)),
|
|
),
|
|
// child: currentMediaItem.displayTitle!.length >= 26
|
|
// ? Marquee(
|
|
// key: Key(currentMediaItem.displayTitle!),
|
|
// text: currentMediaItem.displayTitle!,
|
|
// style: TextStyle(
|
|
// fontSize: textSize, fontWeight: FontWeight.bold),
|
|
// blankSpace: 32.0,
|
|
// startPadding: 0.0,
|
|
// accelerationDuration: const Duration(seconds: 1),
|
|
// pauseAfterRound: const Duration(seconds: 2),
|
|
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
// fadingEdgeEndFraction: 0.05,
|
|
// fadingEdgeStartFraction: 0.05,
|
|
// )
|
|
// : Text(
|
|
// currentMediaItem.displayTitle!,
|
|
// maxLines: 1,
|
|
// overflow: TextOverflow.ellipsis,
|
|
// textAlign: TextAlign.start,
|
|
// style: TextStyle(
|
|
// fontSize: textSize, fontWeight: FontWeight.bold),
|
|
// )),
|
|
Text(
|
|
currentMediaItem.displaySubtitle ?? '',
|
|
maxLines: 1,
|
|
textAlign: TextAlign.start,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: textSize * 0.8, // 20% smaller
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
),
|
|
]),
|
|
),
|
|
const SizedBox(width: 8.0),
|
|
FavoriteButton(size: textSize),
|
|
],
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
class QualityInfoWidget extends StatelessWidget {
|
|
final double? textSize;
|
|
const QualityInfoWidget({Key? key, this.textSize}) : super(key: key);
|
|
|
|
String _getQualityStringFromInfo(StreamQualityInfo info) {
|
|
if (audioHandler.mediaItem.value == null) return '';
|
|
|
|
int bitrate = info.quality == AudioQuality.MP3_128
|
|
? 128
|
|
: info.quality == AudioQuality.MP3_320
|
|
? 320
|
|
: info.calculateBitrate(audioHandler.mediaItem.value!.duration!);
|
|
|
|
return '${info.format.name} ${bitrate}kbps';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return TextButton(
|
|
// style: ButtonStyle(
|
|
// elevation: MaterialStatePropertyAll(0.5),
|
|
// padding: MaterialStatePropertyAll(
|
|
// EdgeInsets.symmetric(horizontal: 16, vertical: 4)),
|
|
// foregroundColor: MaterialStatePropertyAll(
|
|
// Theme.of(context).colorScheme.onSurface)),
|
|
child: StreamBuilder<String>(
|
|
stream:
|
|
playerHelper.streamInfo.map<String>(_getQualityStringFromInfo),
|
|
builder: (context, snapshot) => Text(snapshot.data ?? '',
|
|
style: textSize == null
|
|
? null
|
|
: TextStyle(fontSize: textSize! * 0.9))),
|
|
onPressed: () => Navigator.of(context).push(
|
|
MaterialPageRoute(builder: (context) => const QualitySettings())),
|
|
);
|
|
}
|
|
}
|
|
|
|
void Function([TapUpDetails?]) _onMenuPressedCallback(BuildContext context) {
|
|
return ([details]) {
|
|
final currentMediaItem = audioHandler.mediaItem.value!;
|
|
Track t = Track.fromMediaItem(currentMediaItem);
|
|
MenuSheet m = MenuSheet(context, navigateCallback: () {
|
|
// close player
|
|
FancyScaffold.of(context)?.closePanel();
|
|
});
|
|
if (currentMediaItem.extras!['show'] == null) {
|
|
m.defaultTrackMenu(t,
|
|
options: [m.sleepTimer(), m.wakelock()], details: details);
|
|
} else {
|
|
m.defaultShowEpisodeMenu(Show.fromJson(currentMediaItem.extras!['show']),
|
|
ShowEpisode.fromMediaItem(currentMediaItem),
|
|
options: [m.sleepTimer(), m.wakelock()], details: details);
|
|
}
|
|
};
|
|
}
|
|
|
|
class PlayerMenuButtonDesktop extends StatelessWidget {
|
|
final double size;
|
|
|
|
const PlayerMenuButtonDesktop({super.key, required this.size});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
customBorder: const CircleBorder(),
|
|
onTapUp: _onMenuPressedCallback(context),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Icon(
|
|
Icons.more_vert,
|
|
semanticLabel: "Options".i18n,
|
|
size: size,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class PlayerMenuButton extends StatelessWidget {
|
|
final double size;
|
|
const PlayerMenuButton({super.key, required this.size});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return IconButton(
|
|
iconSize: size,
|
|
icon: Icon(
|
|
Icons.more_vert,
|
|
semanticLabel: "Options".i18n,
|
|
),
|
|
onPressed: _onMenuPressedCallback(context));
|
|
}
|
|
}
|
|
|
|
class RepeatButton extends StatefulWidget {
|
|
final double iconSize;
|
|
const RepeatButton(this.iconSize, {Key? key}) : super(key: key);
|
|
|
|
@override
|
|
State<RepeatButton> createState() => _RepeatButtonState();
|
|
}
|
|
|
|
class _RepeatButtonState extends State<RepeatButton> {
|
|
// ignore: missing_return
|
|
Icon get repeatIcon {
|
|
switch (playerHelper.repeatType) {
|
|
case AudioServiceRepeatMode.none:
|
|
return Icon(
|
|
Icons.repeat,
|
|
semanticLabel: "Repeat off".i18n,
|
|
);
|
|
case AudioServiceRepeatMode.one:
|
|
return Icon(
|
|
Icons.repeat_one,
|
|
semanticLabel: "Repeat one".i18n,
|
|
);
|
|
case AudioServiceRepeatMode.group:
|
|
case AudioServiceRepeatMode.all:
|
|
return Icon(
|
|
Icons.repeat,
|
|
semanticLabel: "Repeat".i18n,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return IconButton(
|
|
color: playerHelper.repeatType == AudioServiceRepeatMode.none
|
|
? null
|
|
: Theme.of(context).colorScheme.primary,
|
|
iconSize: widget.iconSize,
|
|
icon: repeatIcon,
|
|
onPressed: () async {
|
|
await playerHelper.changeRepeat();
|
|
setState(() {});
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class ShuffleButton extends StatefulWidget {
|
|
final double iconSize;
|
|
const ShuffleButton({Key? key, required this.iconSize}) : super(key: key);
|
|
|
|
@override
|
|
State<ShuffleButton> createState() => _ShuffleButtonState();
|
|
}
|
|
|
|
class _ShuffleButtonState extends State<ShuffleButton> {
|
|
@override
|
|
Widget build(BuildContext context) => IconButton(
|
|
icon: const Icon(Icons.shuffle),
|
|
iconSize: widget.iconSize,
|
|
color: playerHelper.shuffleEnabled
|
|
? Theme.of(context).colorScheme.primary
|
|
: null,
|
|
onPressed: _toggleShuffle,
|
|
);
|
|
|
|
void _toggleShuffle() {
|
|
playerHelper.toggleShuffle().then((_) => setState(() {}));
|
|
}
|
|
}
|
|
|
|
class FavoriteButton extends StatefulWidget {
|
|
final double size;
|
|
const FavoriteButton({Key? key, required this.size}) : super(key: key);
|
|
|
|
@override
|
|
State<FavoriteButton> createState() => _FavoriteButtonState();
|
|
}
|
|
|
|
class _FavoriteButtonState extends State<FavoriteButton> {
|
|
Icon get libraryIcon {
|
|
if (cache.checkTrackFavorite(
|
|
Track.fromMediaItem(audioHandler.mediaItem.value!))) {
|
|
return Icon(
|
|
Icons.favorite,
|
|
semanticLabel: "Unlove".i18n,
|
|
);
|
|
}
|
|
return Icon(
|
|
Icons.favorite_border,
|
|
semanticLabel: "Love".i18n,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) => StreamBuilder<MediaItem?>(
|
|
stream: audioHandler.mediaItem,
|
|
builder: (context, snapshot) {
|
|
if (!snapshot.hasData || snapshot.data == null) {
|
|
return IconButton(
|
|
onPressed: () {}, icon: const Icon(Icons.favorite_border));
|
|
}
|
|
|
|
final mediaItem = snapshot.data!;
|
|
|
|
return IconButton(
|
|
icon: libraryIcon,
|
|
iconSize: widget.size,
|
|
onPressed: () async {
|
|
if (cache.checkTrackFavorite(Track.fromMediaItem(mediaItem))) {
|
|
//Remove from library
|
|
setState(() => cache.libraryTracks.remove(mediaItem.id));
|
|
await DeezerAPI.instance.removeFavorite(mediaItem.id);
|
|
await cache.save();
|
|
} else {
|
|
//Add
|
|
setState(() => cache.libraryTracks.add(mediaItem.id));
|
|
await DeezerAPI.instance.addFavoriteTrack(mediaItem.id);
|
|
await cache.save();
|
|
}
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
class ForwardReplay30Button extends StatelessWidget {
|
|
final bool forward;
|
|
const ForwardReplay30Button({super.key, required this.forward});
|
|
|
|
void _seek(Duration position) {
|
|
// validate position
|
|
if (position.isNegative) return;
|
|
if (position > audioHandler.mediaItem.value!.duration!) return;
|
|
|
|
audioHandler.seek(position);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (forward) {
|
|
return IconButton(
|
|
onPressed: () => _seek(audioHandler.playbackState.value.position +
|
|
const Duration(seconds: 30)),
|
|
icon: const Icon(Icons.forward_30));
|
|
}
|
|
|
|
return IconButton(
|
|
onPressed: () => _seek(audioHandler.playbackState.value.position -
|
|
const Duration(seconds: 30)),
|
|
icon: const Icon(Icons.replay_30));
|
|
}
|
|
}
|
|
|
|
class PlaybackControls extends StatelessWidget {
|
|
final double size;
|
|
const PlaybackControls(this.size, {Key? key}) : super(key: key);
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
child: StreamBuilder<QueueSource>(
|
|
stream: playerHelper.queueSource,
|
|
builder: (context, snapshot) {
|
|
final queueSource = snapshot.data;
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
mainAxisSize: MainAxisSize.max,
|
|
children: [
|
|
queueSource?.source != 'show'
|
|
? ShuffleButton(iconSize: size * 0.75)
|
|
: const ForwardReplay30Button(forward: false),
|
|
PrevNextButton(size, prev: true),
|
|
if (settings.enableFilledPlayButton)
|
|
Consumer<BackgroundProvider>(builder: (context, provider, _) {
|
|
final color = provider.dominantColor == null
|
|
? Colors.transparent
|
|
: Theme.of(context).brightness == Brightness.light
|
|
? provider.dominantColor!
|
|
: darken(provider.dominantColor!);
|
|
return PlayPauseButton(size * 2.25,
|
|
filled: true,
|
|
material3: settings.enableMaterial3PlayButton,
|
|
color: color,
|
|
iconColor: Color.lerp(
|
|
(ThemeData.estimateBrightnessForColor(color) ==
|
|
Brightness.light
|
|
? Colors.black
|
|
: Colors.white),
|
|
color,
|
|
0.25));
|
|
})
|
|
else
|
|
PlayPauseButton(size * 1.25),
|
|
PrevNextButton(size),
|
|
queueSource?.source != 'show'
|
|
? RepeatButton(size * 0.75)
|
|
: const ForwardReplay30Button(forward: true),
|
|
],
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
class BigAlbumArt extends StatefulWidget {
|
|
final bool showLyricsButton;
|
|
const BigAlbumArt({super.key, this.showLyricsButton = true});
|
|
|
|
@override
|
|
State<BigAlbumArt> createState() => _BigAlbumArtState();
|
|
}
|
|
|
|
class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
|
|
final _pageController = PageController(
|
|
initialPage: playerHelper.queueIndex,
|
|
keepPage: false,
|
|
viewportFraction: 1.0,
|
|
);
|
|
StreamSubscription? _currentItemSub;
|
|
|
|
/// is true on pointer down event
|
|
/// used to distinguish between [PageController.animateToPage] and user gesture
|
|
bool _userScroll = true;
|
|
|
|
/// whether the user has already scrolled the [PageView],
|
|
/// so to avoid calling [PageController.animateToPage] again.
|
|
bool _initiatedByUser = false;
|
|
|
|
void _listenForMediaItemChanges() {
|
|
if (_currentItemSub != null) return;
|
|
|
|
if (audioHandler.mediaItem.hasValue && _pageController.hasClients) {
|
|
_pageController.jumpToPage(playerHelper.queueIndex);
|
|
}
|
|
|
|
_currentItemSub = audioHandler.mediaItem.listen((event) async {
|
|
if (_initiatedByUser) {
|
|
_initiatedByUser = false;
|
|
return;
|
|
}
|
|
if (!_pageController.hasClients) return;
|
|
if (_pageController.page?.toInt() == playerHelper.queueIndex) return;
|
|
print('animating controller to page');
|
|
|
|
_userScroll = false;
|
|
await _pageController.animateToPage(playerHelper.queueIndex,
|
|
duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
|
|
_userScroll = true;
|
|
});
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
_listenForMediaItemChanges();
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
switch (state) {
|
|
case AppLifecycleState.paused:
|
|
_currentItemSub?.cancel();
|
|
case AppLifecycleState.resumed:
|
|
_listenForMediaItemChanges();
|
|
default:
|
|
break;
|
|
}
|
|
super.didChangeAppLifecycleState(state);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_currentItemSub?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
void _pushLyrics() {
|
|
// enable wakelock if not already enabled
|
|
// ideally we would use WakelockPlus.enabled
|
|
final wakelockChanged = !cache.wakelock;
|
|
if (wakelockChanged) {
|
|
WakelockPlus.enable();
|
|
}
|
|
builder(ctx) => ChangeNotifierProvider<BackgroundProvider>.value(
|
|
value: Provider.of<BackgroundProvider>(context),
|
|
child: const LyricsScreen());
|
|
final pushed = Navigator.of(context).pushRoute(builder: builder);
|
|
if (wakelockChanged) {
|
|
pushed.then((_) => WakelockPlus.disable());
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final child = GestureDetector(
|
|
onTap: () => Navigator.push(
|
|
context,
|
|
FadePageRoute(
|
|
barrierDismissible: true,
|
|
opaque: false,
|
|
builder: (context) {
|
|
final mediaItem = audioHandler.mediaItem.value!;
|
|
return ZoomableImageRoute(
|
|
imageUrl: mediaItem.artUri.toString(), heroKey: mediaItem.id);
|
|
},
|
|
)),
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) => Stack(
|
|
children: [
|
|
StreamBuilder<List<MediaItem>>(
|
|
stream: audioHandler.queue,
|
|
initialData: audioHandler.queue.valueOrNull,
|
|
builder: (context, snapshot) {
|
|
if (!snapshot.hasData) {
|
|
return const Center(
|
|
child: CircularProgressIndicator());
|
|
}
|
|
final queue = snapshot.data!;
|
|
return PageView.builder(
|
|
controller: _pageController,
|
|
onPageChanged: (int index) {
|
|
// ignore if not initiated by user.
|
|
if (!_userScroll) return;
|
|
Logger('BigAlbumArt')
|
|
.fine('page changed, skipping to media item');
|
|
if (queue[index].id ==
|
|
audioHandler.mediaItem.value?.id) {
|
|
return;
|
|
}
|
|
|
|
audioHandler.skipToQueueItem(index);
|
|
},
|
|
itemCount: queue.length,
|
|
itemBuilder: (context, i) => Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Hero(
|
|
tag: queue[i].id,
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(8.0),
|
|
child: CachedImage(
|
|
url: queue[i].artUri.toString(),
|
|
fullThumb: true,
|
|
),
|
|
),
|
|
),
|
|
));
|
|
}),
|
|
if (widget.showLyricsButton)
|
|
StreamBuilder<MediaItem?>(
|
|
initialData: audioHandler.mediaItem.valueOrNull,
|
|
stream: audioHandler.mediaItem,
|
|
builder: (context, snapshot) {
|
|
if (snapshot.data == null) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
final l = snapshot.data!.extras?['lyrics'] == null
|
|
? null
|
|
: Lyrics.fromJson(
|
|
jsonDecode(snapshot.data!.extras!['lyrics']));
|
|
|
|
if (l == null || l.id == null || l.id == '0') {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
return Positioned(
|
|
key: const ValueKey('lyrics_button'),
|
|
bottom: 16.0,
|
|
right: 16.0,
|
|
child: LyricsButton(
|
|
onTap: _pushLyrics,
|
|
size: 20.spMax,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
)),
|
|
);
|
|
|
|
return AspectRatio(
|
|
aspectRatio: 1.0,
|
|
child: Center(
|
|
child: settings.playerAlbumArtDropShadow
|
|
? Consumer<BackgroundProvider>(
|
|
builder: (context, background, child) => AnimatedContainer(
|
|
duration: const Duration(seconds: 1),
|
|
decoration: BoxDecoration(boxShadow: [
|
|
BoxShadow(
|
|
color:
|
|
(background.dominantColor ?? Colors.transparent)
|
|
.withOpacity(0.5),
|
|
spreadRadius: 20.0,
|
|
blurRadius: 150.0)
|
|
]),
|
|
child: child),
|
|
child: child,
|
|
)
|
|
: child,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class LyricsButton extends StatelessWidget {
|
|
final VoidCallback onTap;
|
|
final double size;
|
|
const LyricsButton({super.key, required this.onTap, this.size = 24.0});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Consumer<BackgroundProvider>(
|
|
builder: (context, provider, child) => Material(
|
|
color: Color.lerp(Theme.of(context).colorScheme.background,
|
|
provider.dominantColor, 0.25),
|
|
borderRadius: BorderRadius.circular(size * 2 / 3),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: child),
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
child: Padding(
|
|
padding:
|
|
EdgeInsets.symmetric(horizontal: size / 1.25, vertical: size / 6),
|
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
|
Icon(Icons.subtitles, size: size),
|
|
SizedBox(width: size / 3),
|
|
Text(
|
|
'Lyrics'.i18n,
|
|
style: TextStyle(fontSize: size * 0.8),
|
|
),
|
|
]),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
//Top row containing QueueSource, queue...
|
|
class PlayerScreenTopRow extends StatelessWidget {
|
|
final double? textSize;
|
|
final double? iconSize;
|
|
final double? textWidth;
|
|
final bool short;
|
|
final bool desktopMode;
|
|
const PlayerScreenTopRow({
|
|
super.key,
|
|
this.textSize,
|
|
this.iconSize,
|
|
this.textWidth,
|
|
this.short = false,
|
|
this.desktopMode = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final size = iconSize ?? 52.sp;
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: <Widget>[
|
|
IconButton(
|
|
onPressed: FancyScaffold.of(context)!.closePanel,
|
|
icon: Icon(
|
|
Icons.keyboard_arrow_down,
|
|
semanticLabel: 'Close'.i18n,
|
|
),
|
|
iconSize: size,
|
|
splashRadius: size * 1.5,
|
|
),
|
|
Expanded(
|
|
child: StreamBuilder<QueueSource>(
|
|
stream: playerHelper.queueSource,
|
|
builder: (context, snapshot) {
|
|
final queueSource = snapshot.data;
|
|
if (queueSource == null) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
return RichText(
|
|
textAlign: TextAlign.center,
|
|
maxLines: 2,
|
|
text: TextSpan(
|
|
children: [
|
|
if (!short)
|
|
TextSpan(
|
|
text: '${'PLAYING FROM'.i18n}\n',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
letterSpacing: 1.5,
|
|
fontSize: (textSize ?? 38.sp) * 0.85)),
|
|
TextSpan(text: queueSource.text ?? '')
|
|
],
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.bodySmall!
|
|
.copyWith(fontSize: textSize ?? 38.sp)));
|
|
}),
|
|
),
|
|
desktopMode
|
|
? PlayerMenuButtonDesktop(size: size)
|
|
: PlayerMenuButton(size: size)
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class SeekBar extends StatefulWidget {
|
|
final double textSize;
|
|
|
|
const SeekBar({Key? key, this.textSize = 16.0}) : super(key: key);
|
|
|
|
@override
|
|
State<SeekBar> createState() => _SeekBarState();
|
|
}
|
|
|
|
class _SeekBarState extends State<SeekBar> {
|
|
bool _seeking = false;
|
|
late StreamSubscription _subscription;
|
|
final position = ValueNotifier<Duration>(Duration.zero);
|
|
|
|
@override
|
|
void initState() {
|
|
_subscription = AudioService.position.listen((position) {
|
|
if (_seeking) return; // user is seeking
|
|
this.position.value = position;
|
|
});
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_subscription.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
double parseDuration(Duration position) {
|
|
if (position > duration) return duration.inMilliseconds.toDouble();
|
|
return position.inMilliseconds.toDouble();
|
|
}
|
|
|
|
//Duration to mm:ss
|
|
String _timeString(Duration d) {
|
|
return "${d.inMinutes}:${d.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
|
}
|
|
|
|
Duration get duration {
|
|
if (audioHandler.mediaItem.value == null) return Duration.zero;
|
|
return audioHandler.mediaItem.value!.duration!;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
ValueListenableBuilder<Duration>(
|
|
valueListenable: position,
|
|
builder: (context, value, _) => StreamBuilder<Duration>(
|
|
stream: playerHelper.bufferPosition,
|
|
builder: (context, snapshot) {
|
|
return Slider(
|
|
secondaryTrackValue:
|
|
parseDuration(snapshot.data ?? Duration.zero),
|
|
focusNode: FocusNode(
|
|
canRequestFocus: false,
|
|
skipTraversal:
|
|
true), // Don't focus on Slider - it doesn't work (and not needed)
|
|
value: parseDuration(value),
|
|
max: duration.inMilliseconds.toDouble(),
|
|
onChangeStart: (double d) {
|
|
_seeking = true;
|
|
position.value = Duration(milliseconds: d.toInt());
|
|
},
|
|
onChanged: (double d) {
|
|
position.value = Duration(milliseconds: d.toInt());
|
|
},
|
|
onChangeEnd: (double d) {
|
|
_seeking = false;
|
|
audioHandler.seek(Duration(milliseconds: d.toInt()));
|
|
},
|
|
);
|
|
})),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: <Widget>[
|
|
ValueListenableBuilder<Duration>(
|
|
valueListenable: position,
|
|
builder: (context, value, _) => Text(
|
|
_timeString(value),
|
|
style: TextStyle(
|
|
fontSize: widget.textSize,
|
|
color: Theme.of(context)
|
|
.textTheme
|
|
.bodyMedium!
|
|
.color!
|
|
.withOpacity(.75)),
|
|
)),
|
|
StreamBuilder<MediaItem?>(
|
|
stream: audioHandler.mediaItem,
|
|
builder: (context, snapshot) => Text(
|
|
_timeString(snapshot.data?.duration ?? Duration.zero),
|
|
style: TextStyle(
|
|
fontSize: widget.textSize,
|
|
color: Theme.of(context)
|
|
.textTheme
|
|
.bodyMedium!
|
|
.color!
|
|
.withOpacity(.75)),
|
|
)),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class BottomBarControls extends StatelessWidget {
|
|
final double size;
|
|
final bool
|
|
desktopMode; // removed in desktop mode, because there's a tabbed view which includes it
|
|
const BottomBarControls({
|
|
super.key,
|
|
required this.size,
|
|
this.desktopMode = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final iconSize = size * 0.9;
|
|
if (playerHelper.queueSource.valueOrNull?.source == 'show') {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
QualityInfoWidget(
|
|
textSize: size * 0.75,
|
|
),
|
|
const Expanded(child: SizedBox()),
|
|
if (!desktopMode)
|
|
IconButton(
|
|
icon: Icon(
|
|
Icons.playlist_play,
|
|
semanticLabel: "Queue".i18n,
|
|
),
|
|
iconSize: size,
|
|
splashRadius: size * 1.5,
|
|
onPressed: () => Navigator.of(context).pushRoute(
|
|
builder: (ctx) => QueueScreen(
|
|
closePlayer: FancyScaffold.of(context)!.closePanel,
|
|
)),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: <Widget>[
|
|
QualityInfoWidget(textSize: size * 0.75),
|
|
const Expanded(child: SizedBox()),
|
|
IconButton(
|
|
icon: Icon(
|
|
Icons.sentiment_very_dissatisfied,
|
|
semanticLabel: "Dislike".i18n,
|
|
),
|
|
iconSize: iconSize,
|
|
onPressed: () async {
|
|
unawaited(DeezerAPI.instance
|
|
.dislikeTrack(audioHandler.mediaItem.value!.id));
|
|
if (playerHelper.queueIndex <
|
|
audioHandler.queue.value.length - 1) {
|
|
audioHandler.skipToNext();
|
|
}
|
|
}),
|
|
// IconButton(
|
|
// iconSize: size,
|
|
// icon: Icon(
|
|
// Icons.file_download,
|
|
// semanticLabel: "Download".i18n,
|
|
// ),
|
|
// onPressed: () async {
|
|
// Track t = Track.fromMediaItem(audioHandler.mediaItem.value!);
|
|
// if (await downloadManager.addOfflineTrack(t,
|
|
// private: false, context: context, isSingleton: true) !=
|
|
// false)
|
|
// Fluttertoast.showToast(
|
|
// msg: 'Downloads added!'.i18n,
|
|
// gravity: ToastGravity.BOTTOM,
|
|
// toastLength: Toast.LENGTH_SHORT);
|
|
// },
|
|
// ),
|
|
if (!desktopMode)
|
|
IconButton(
|
|
icon: Icon(
|
|
Icons.playlist_play,
|
|
semanticLabel: "Queue".i18n,
|
|
),
|
|
iconSize: size,
|
|
splashRadius: size * 1.5,
|
|
onPressed: () => Navigator.of(context).pushRoute(
|
|
builder: (ctx) => QueueScreen(
|
|
closePlayer: FancyScaffold.of(context)!.closePanel,
|
|
)),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|