mirror of
https://github.com/gokadzev/Musify.git
synced 2025-08-06 10:59:47 +08:00
944 lines
29 KiB
Dart
944 lines
29 KiB
Dart
/*
|
|
* Copyright (C) 2025 Valeri Gokadze
|
|
*
|
|
* Musify is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Musify is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
*
|
|
* For more information about Musify, including how to contribute,
|
|
* please visit: https://github.com/gokadzev/Musify
|
|
*/
|
|
|
|
import 'package:audio_service/audio_service.dart';
|
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_flip_card/flutter_flip_card.dart';
|
|
import 'package:musify/API/musify.dart';
|
|
import 'package:musify/extensions/l10n.dart';
|
|
import 'package:musify/main.dart';
|
|
import 'package:musify/models/position_data.dart';
|
|
import 'package:musify/services/settings_manager.dart';
|
|
import 'package:musify/utilities/common_variables.dart';
|
|
import 'package:musify/utilities/flutter_bottom_sheet.dart';
|
|
import 'package:musify/utilities/flutter_toast.dart';
|
|
import 'package:musify/utilities/formatter.dart';
|
|
import 'package:musify/utilities/mediaitem.dart';
|
|
import 'package:musify/utilities/utils.dart';
|
|
import 'package:musify/widgets/marque.dart';
|
|
import 'package:musify/widgets/playback_icon_button.dart';
|
|
import 'package:musify/widgets/song_artwork.dart';
|
|
import 'package:musify/widgets/song_bar.dart';
|
|
import 'package:musify/widgets/spinner.dart';
|
|
|
|
final _lyricsController = FlipCardController();
|
|
|
|
class NowPlayingPage extends StatelessWidget {
|
|
const NowPlayingPage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final size = MediaQuery.sizeOf(context);
|
|
final isLargeScreen = size.width > 800;
|
|
const adjustedIconSize = 43.0;
|
|
const adjustedMiniIconSize = 20.0;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.transparent,
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_downward),
|
|
splashColor: Colors.transparent,
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
},
|
|
),
|
|
),
|
|
body: StreamBuilder<MediaItem?>(
|
|
stream: audioHandler.mediaItem.distinct((prev, curr) {
|
|
if (prev == null || curr == null) return false;
|
|
return prev.id == curr.id &&
|
|
prev.title == curr.title &&
|
|
prev.artist == curr.artist &&
|
|
prev.artUri == curr.artUri;
|
|
}),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.data == null || !snapshot.hasData) {
|
|
return const SizedBox.shrink();
|
|
} else {
|
|
final metadata = snapshot.data!;
|
|
return isLargeScreen
|
|
? _DesktopLayout(
|
|
metadata: metadata,
|
|
size: size,
|
|
adjustedIconSize: adjustedIconSize,
|
|
adjustedMiniIconSize: adjustedMiniIconSize,
|
|
)
|
|
: _MobileLayout(
|
|
metadata: metadata,
|
|
size: size,
|
|
adjustedIconSize: adjustedIconSize,
|
|
adjustedMiniIconSize: adjustedMiniIconSize,
|
|
isLargeScreen: isLargeScreen,
|
|
);
|
|
}
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DesktopLayout extends StatelessWidget {
|
|
const _DesktopLayout({
|
|
required this.metadata,
|
|
required this.size,
|
|
required this.adjustedIconSize,
|
|
required this.adjustedMiniIconSize,
|
|
});
|
|
final MediaItem metadata;
|
|
final Size size;
|
|
final double adjustedIconSize;
|
|
final double adjustedMiniIconSize;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
children: [
|
|
const SizedBox(height: 5),
|
|
NowPlayingArtwork(size: size, metadata: metadata),
|
|
const SizedBox(height: 5),
|
|
if (!(metadata.extras?['isLive'] ?? false))
|
|
NowPlayingControls(
|
|
context: context,
|
|
size: size,
|
|
audioId: metadata.extras?['ytid'],
|
|
adjustedIconSize: adjustedIconSize,
|
|
adjustedMiniIconSize: adjustedMiniIconSize,
|
|
metadata: metadata,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const VerticalDivider(width: 1),
|
|
const Expanded(child: QueueListView()),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _MobileLayout extends StatelessWidget {
|
|
const _MobileLayout({
|
|
required this.metadata,
|
|
required this.size,
|
|
required this.adjustedIconSize,
|
|
required this.adjustedMiniIconSize,
|
|
required this.isLargeScreen,
|
|
});
|
|
final MediaItem metadata;
|
|
final Size size;
|
|
final double adjustedIconSize;
|
|
final double adjustedMiniIconSize;
|
|
final bool isLargeScreen;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
const SizedBox(height: 10),
|
|
NowPlayingArtwork(size: size, metadata: metadata),
|
|
const SizedBox(height: 10),
|
|
if (!(metadata.extras?['isLive'] ?? false))
|
|
NowPlayingControls(
|
|
context: context,
|
|
size: size,
|
|
audioId: metadata.extras?['ytid'],
|
|
adjustedIconSize: adjustedIconSize,
|
|
adjustedMiniIconSize: adjustedMiniIconSize,
|
|
metadata: metadata,
|
|
),
|
|
if (!isLargeScreen) ...[
|
|
const SizedBox(height: 10),
|
|
BottomActionsRow(
|
|
context: context,
|
|
audioId: metadata.extras?['ytid'],
|
|
metadata: metadata,
|
|
iconSize: adjustedMiniIconSize,
|
|
isLargeScreen: isLargeScreen,
|
|
),
|
|
const SizedBox(height: 35),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class NowPlayingArtwork extends StatelessWidget {
|
|
const NowPlayingArtwork({
|
|
super.key,
|
|
required this.size,
|
|
required this.metadata,
|
|
});
|
|
final Size size;
|
|
final MediaItem metadata;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
const _padding = 50;
|
|
const _radius = 17.0;
|
|
final screenWidth = size.width;
|
|
final screenHeight = size.height;
|
|
final isLandscape = screenWidth > screenHeight;
|
|
final imageSize =
|
|
isLandscape
|
|
? screenHeight * 0.40
|
|
: (screenWidth + screenHeight) / 3.35 - _padding;
|
|
const lyricsTextStyle = TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.w500,
|
|
);
|
|
|
|
return FlipCard(
|
|
rotateSide: RotateSide.right,
|
|
onTapFlipping: !offlineMode.value,
|
|
controller: _lyricsController,
|
|
frontWidget: SongArtworkWidget(
|
|
metadata: metadata,
|
|
size: imageSize,
|
|
errorWidgetIconSize: size.width / 8,
|
|
borderRadius: _radius,
|
|
),
|
|
backWidget: Container(
|
|
width: imageSize,
|
|
height: imageSize,
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
|
borderRadius: BorderRadius.circular(_radius),
|
|
),
|
|
child: FutureBuilder<String?>(
|
|
future: getSongLyrics(metadata.artist, metadata.title),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return const Spinner();
|
|
} else if (snapshot.hasError || snapshot.data == null) {
|
|
return Center(
|
|
child: Text(
|
|
context.l10n!.lyricsNotAvailable,
|
|
style: lyricsTextStyle.copyWith(
|
|
color: Theme.of(context).colorScheme.secondary,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
);
|
|
} else {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Center(
|
|
child: Text(
|
|
snapshot.data ?? context.l10n!.lyricsNotAvailable,
|
|
style: lyricsTextStyle.copyWith(
|
|
color: Theme.of(context).colorScheme.secondary,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class QueueListView extends StatelessWidget {
|
|
const QueueListView({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final _textColor = Theme.of(context).colorScheme.secondary;
|
|
return Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(8),
|
|
child: Text(
|
|
context.l10n!.playlist,
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.headlineSmall?.copyWith(color: _textColor),
|
|
),
|
|
),
|
|
Expanded(
|
|
child:
|
|
activePlaylist['list'].isEmpty
|
|
? Center(
|
|
child: Text(
|
|
context.l10n!.noSongsInQueue,
|
|
style: TextStyle(color: _textColor),
|
|
),
|
|
)
|
|
: ListView.builder(
|
|
itemCount: activePlaylist['list'].length,
|
|
itemBuilder: (context, index) {
|
|
final borderRadius = getItemBorderRadius(
|
|
index,
|
|
activePlaylist['list'].length,
|
|
);
|
|
return SongBar(
|
|
activePlaylist['list'][index],
|
|
false,
|
|
onPlay: () {
|
|
audioHandler.playPlaylistSong(songIndex: index);
|
|
},
|
|
backgroundColor:
|
|
Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
borderRadius: borderRadius,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class MarqueeTextWidget extends StatelessWidget {
|
|
const MarqueeTextWidget({
|
|
super.key,
|
|
required this.text,
|
|
required this.fontColor,
|
|
required this.fontSize,
|
|
required this.fontWeight,
|
|
});
|
|
final String text;
|
|
final Color fontColor;
|
|
final double fontSize;
|
|
final FontWeight fontWeight;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MarqueeWidget(
|
|
backDuration: const Duration(seconds: 1),
|
|
child: Text(
|
|
text,
|
|
style: TextStyle(
|
|
fontSize: fontSize,
|
|
fontWeight: fontWeight,
|
|
color: fontColor,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class NowPlayingControls extends StatelessWidget {
|
|
const NowPlayingControls({
|
|
super.key,
|
|
required this.context,
|
|
required this.size,
|
|
required this.audioId,
|
|
required this.adjustedIconSize,
|
|
required this.adjustedMiniIconSize,
|
|
required this.metadata,
|
|
});
|
|
final BuildContext context;
|
|
final Size size;
|
|
final dynamic audioId;
|
|
final double adjustedIconSize;
|
|
final double adjustedMiniIconSize;
|
|
final MediaItem metadata;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final screenWidth = size.width;
|
|
final screenHeight = size.height;
|
|
|
|
return Expanded(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Spacer(),
|
|
SizedBox(
|
|
width: screenWidth * 0.85,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
MarqueeTextWidget(
|
|
text: metadata.title,
|
|
fontColor: Theme.of(context).colorScheme.primary,
|
|
fontSize: screenHeight * 0.028,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
const SizedBox(height: 10),
|
|
if (metadata.artist != null)
|
|
MarqueeTextWidget(
|
|
text: metadata.artist!,
|
|
fontColor: Theme.of(context).colorScheme.secondary,
|
|
fontSize: screenHeight * 0.017,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Spacer(),
|
|
const PositionSlider(),
|
|
const Spacer(),
|
|
PlayerControlButtons(
|
|
context: context,
|
|
metadata: metadata,
|
|
iconSize: adjustedIconSize,
|
|
miniIconSize: adjustedMiniIconSize,
|
|
),
|
|
const Spacer(flex: 2),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class PositionSlider extends StatelessWidget {
|
|
const PositionSlider({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final primaryColor = Theme.of(context).colorScheme.primary;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
|
child: StreamBuilder<PositionData>(
|
|
stream: audioHandler.positionDataStream.distinct(),
|
|
builder: (context, snapshot) {
|
|
final hasData = snapshot.hasData && snapshot.data != null;
|
|
final positionData =
|
|
hasData
|
|
? snapshot.data!
|
|
: PositionData(Duration.zero, Duration.zero, Duration.zero);
|
|
|
|
return Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Slider(
|
|
value: positionData.position.inSeconds.toDouble(),
|
|
onChanged:
|
|
hasData
|
|
? (value) {
|
|
audioHandler.seek(Duration(seconds: value.toInt()));
|
|
}
|
|
: null,
|
|
max:
|
|
positionData.duration.inSeconds > 0
|
|
? positionData.duration.inSeconds.toDouble()
|
|
: 1.0,
|
|
),
|
|
_buildPositionRow(context, primaryColor, positionData),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPositionRow(
|
|
BuildContext context,
|
|
Color fontColor,
|
|
PositionData positionData,
|
|
) {
|
|
final positionText = formatDuration(positionData.position.inSeconds);
|
|
final durationText = formatDuration(positionData.duration.inSeconds);
|
|
final textStyle = TextStyle(fontSize: 15, color: fontColor);
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 22),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(positionText, style: textStyle),
|
|
Text(durationText, style: textStyle),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class PlayerControlButtons extends StatelessWidget {
|
|
const PlayerControlButtons({
|
|
super.key,
|
|
required this.context,
|
|
required this.metadata,
|
|
required this.iconSize,
|
|
required this.miniIconSize,
|
|
});
|
|
final BuildContext context;
|
|
final MediaItem metadata;
|
|
final double iconSize;
|
|
final double miniIconSize;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final _primaryColor = theme.colorScheme.primary;
|
|
final _secondaryColor = theme.colorScheme.secondaryContainer;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 22),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: <Widget>[
|
|
_buildShuffleButton(_primaryColor, _secondaryColor, miniIconSize),
|
|
Row(
|
|
children: [
|
|
_buildPreviousButton(_primaryColor, _secondaryColor, iconSize),
|
|
const SizedBox(width: 10),
|
|
_buildPlayPauseButton(_primaryColor, _secondaryColor, iconSize),
|
|
const SizedBox(width: 10),
|
|
_buildNextButton(_primaryColor, _secondaryColor, iconSize),
|
|
],
|
|
),
|
|
_buildRepeatButton(_primaryColor, _secondaryColor, miniIconSize),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildShuffleButton(
|
|
Color primaryColor,
|
|
Color secondaryColor,
|
|
double iconSize,
|
|
) {
|
|
return ValueListenableBuilder<bool>(
|
|
valueListenable: shuffleNotifier,
|
|
builder: (_, value, __) {
|
|
return value
|
|
? IconButton.filled(
|
|
icon: Icon(
|
|
FluentIcons.arrow_shuffle_24_filled,
|
|
color: secondaryColor,
|
|
),
|
|
iconSize: iconSize,
|
|
onPressed: () {
|
|
audioHandler.setShuffleMode(AudioServiceShuffleMode.none);
|
|
},
|
|
)
|
|
: IconButton.filledTonal(
|
|
icon: Icon(
|
|
FluentIcons.arrow_shuffle_off_24_filled,
|
|
color: primaryColor,
|
|
),
|
|
iconSize: iconSize,
|
|
onPressed: () {
|
|
audioHandler.setShuffleMode(AudioServiceShuffleMode.all);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildPreviousButton(
|
|
Color primaryColor,
|
|
Color secondaryColor,
|
|
double iconSize,
|
|
) {
|
|
return ValueListenableBuilder<AudioServiceRepeatMode>(
|
|
valueListenable: repeatNotifier,
|
|
builder: (_, repeatMode, __) {
|
|
return IconButton(
|
|
icon: Icon(
|
|
FluentIcons.previous_24_filled,
|
|
color: audioHandler.hasPrevious ? primaryColor : secondaryColor,
|
|
),
|
|
iconSize: iconSize / 1.7,
|
|
onPressed:
|
|
() =>
|
|
repeatNotifier.value == AudioServiceRepeatMode.one
|
|
? audioHandler.playAgain()
|
|
: audioHandler.skipToPrevious(),
|
|
splashColor: Colors.transparent,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildPlayPauseButton(
|
|
Color primaryColor,
|
|
Color secondaryColor,
|
|
double iconSize,
|
|
) {
|
|
return StreamBuilder<PlaybackState>(
|
|
stream: audioHandler.playbackState.distinct((previous, current) {
|
|
// Only rebuild if playing state or processing state changes
|
|
return previous.playing == current.playing &&
|
|
previous.processingState == current.processingState;
|
|
}),
|
|
builder: (context, snapshot) {
|
|
return buildPlaybackIconButton(
|
|
snapshot.data,
|
|
iconSize,
|
|
primaryColor,
|
|
secondaryColor,
|
|
elevation: 0,
|
|
padding: EdgeInsets.all(iconSize * 0.40),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildNextButton(
|
|
Color primaryColor,
|
|
Color secondaryColor,
|
|
double iconSize,
|
|
) {
|
|
return ValueListenableBuilder<AudioServiceRepeatMode>(
|
|
valueListenable: repeatNotifier,
|
|
builder: (_, repeatMode, __) {
|
|
return IconButton(
|
|
icon: Icon(
|
|
FluentIcons.next_24_filled,
|
|
color: audioHandler.hasNext ? primaryColor : secondaryColor,
|
|
),
|
|
iconSize: iconSize / 1.7,
|
|
onPressed:
|
|
() =>
|
|
repeatNotifier.value == AudioServiceRepeatMode.one
|
|
? audioHandler.playAgain()
|
|
: audioHandler.skipToNext(),
|
|
splashColor: Colors.transparent,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildRepeatButton(
|
|
Color primaryColor,
|
|
Color secondaryColor,
|
|
double iconSize,
|
|
) {
|
|
return ValueListenableBuilder<AudioServiceRepeatMode>(
|
|
valueListenable: repeatNotifier,
|
|
builder: (_, repeatMode, __) {
|
|
return repeatMode != AudioServiceRepeatMode.none
|
|
? IconButton.filled(
|
|
icon: Icon(
|
|
repeatMode == AudioServiceRepeatMode.all
|
|
? FluentIcons.arrow_repeat_all_24_filled
|
|
: FluentIcons.arrow_repeat_1_24_filled,
|
|
color: secondaryColor,
|
|
),
|
|
iconSize: iconSize,
|
|
onPressed: () {
|
|
final newRepeatMode =
|
|
repeatMode == AudioServiceRepeatMode.all
|
|
? AudioServiceRepeatMode.one
|
|
: AudioServiceRepeatMode.none;
|
|
|
|
repeatNotifier.value = newRepeatMode;
|
|
|
|
audioHandler.setRepeatMode(newRepeatMode);
|
|
},
|
|
)
|
|
: IconButton.filledTonal(
|
|
icon: Icon(
|
|
FluentIcons.arrow_repeat_all_off_24_filled,
|
|
color: primaryColor,
|
|
),
|
|
iconSize: iconSize,
|
|
onPressed: () {
|
|
final _isSingleSongPlaying = activePlaylist['list'].isEmpty;
|
|
final newRepeatMode =
|
|
_isSingleSongPlaying
|
|
? AudioServiceRepeatMode.one
|
|
: AudioServiceRepeatMode.all;
|
|
|
|
repeatNotifier.value = newRepeatMode;
|
|
|
|
if (repeatNotifier.value == AudioServiceRepeatMode.one)
|
|
audioHandler.setRepeatMode(newRepeatMode);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class BottomActionsRow extends StatelessWidget {
|
|
const BottomActionsRow({
|
|
super.key,
|
|
required this.context,
|
|
required this.audioId,
|
|
required this.metadata,
|
|
required this.iconSize,
|
|
required this.isLargeScreen,
|
|
});
|
|
final BuildContext context;
|
|
final dynamic audioId;
|
|
final MediaItem metadata;
|
|
final double iconSize;
|
|
final bool isLargeScreen;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final songLikeStatus = ValueNotifier<bool>(isSongAlreadyLiked(audioId));
|
|
final songOfflineStatus = ValueNotifier<bool>(
|
|
isSongAlreadyOffline(audioId),
|
|
);
|
|
final _primaryColor = Theme.of(context).colorScheme.primary;
|
|
|
|
return Wrap(
|
|
alignment: WrapAlignment.center,
|
|
spacing: 8,
|
|
children: [
|
|
_buildOfflineButton(songOfflineStatus, _primaryColor),
|
|
if (!offlineMode.value) _buildAddToPlaylistButton(_primaryColor),
|
|
if (activePlaylist['list'].isNotEmpty && !isLargeScreen)
|
|
_buildQueueButton(context, _primaryColor),
|
|
if (!offlineMode.value) ...[
|
|
_buildLyricsButton(_primaryColor),
|
|
_buildSleepTimerButton(context, _primaryColor),
|
|
_buildLikeButton(songLikeStatus, _primaryColor),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildOfflineButton(ValueNotifier<bool> status, Color primaryColor) {
|
|
return ValueListenableBuilder<bool>(
|
|
valueListenable: status,
|
|
builder: (_, value, __) {
|
|
return IconButton.filledTonal(
|
|
icon: Icon(
|
|
value
|
|
? FluentIcons.cellular_off_24_regular
|
|
: FluentIcons.cellular_data_1_24_regular,
|
|
color: primaryColor,
|
|
),
|
|
iconSize: iconSize,
|
|
onPressed: () {
|
|
if (value) {
|
|
removeSongFromOffline(audioId);
|
|
} else {
|
|
makeSongOffline(mediaItemToMap(metadata));
|
|
}
|
|
status.value = !status.value;
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildAddToPlaylistButton(Color primaryColor) {
|
|
return IconButton.filledTonal(
|
|
icon: Icon(Icons.add, color: primaryColor),
|
|
iconSize: iconSize,
|
|
onPressed: () {
|
|
showAddToPlaylistDialog(context, mediaItemToMap(metadata));
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildQueueButton(BuildContext context, Color primaryColor) {
|
|
return IconButton.filledTonal(
|
|
icon: Icon(FluentIcons.apps_list_24_filled, color: primaryColor),
|
|
iconSize: iconSize,
|
|
onPressed: () {
|
|
showCustomBottomSheet(
|
|
context,
|
|
ListView.builder(
|
|
shrinkWrap: true,
|
|
physics: const BouncingScrollPhysics(),
|
|
padding: commonListViewBottmomPadding,
|
|
itemCount: activePlaylist['list'].length,
|
|
itemBuilder: (BuildContext context, int index) {
|
|
final borderRadius = getItemBorderRadius(
|
|
index,
|
|
activePlaylist['list'].length,
|
|
);
|
|
return SongBar(
|
|
activePlaylist['list'][index],
|
|
false,
|
|
onPlay: () {
|
|
audioHandler.playPlaylistSong(songIndex: index);
|
|
},
|
|
backgroundColor:
|
|
Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
borderRadius: borderRadius,
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildLyricsButton(Color primaryColor) {
|
|
return IconButton.filledTonal(
|
|
icon: Icon(FluentIcons.text_32_filled, color: primaryColor),
|
|
iconSize: iconSize,
|
|
onPressed: _lyricsController.flipcard,
|
|
);
|
|
}
|
|
|
|
Widget _buildSleepTimerButton(BuildContext context, Color primaryColor) {
|
|
return ValueListenableBuilder<Duration?>(
|
|
valueListenable: sleepTimerNotifier,
|
|
builder: (_, value, __) {
|
|
return IconButton.filledTonal(
|
|
icon: Icon(
|
|
value != null
|
|
? FluentIcons.timer_24_filled
|
|
: FluentIcons.timer_24_regular,
|
|
color: primaryColor,
|
|
),
|
|
iconSize: iconSize,
|
|
onPressed: () {
|
|
if (value != null) {
|
|
audioHandler.cancelSleepTimer();
|
|
sleepTimerNotifier.value = null;
|
|
showToast(
|
|
context,
|
|
context.l10n!.sleepTimerCancelled,
|
|
duration: const Duration(seconds: 1, milliseconds: 500),
|
|
);
|
|
} else {
|
|
_showSleepTimerDialog(context);
|
|
}
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildLikeButton(ValueNotifier<bool> status, Color primaryColor) {
|
|
return ValueListenableBuilder<bool>(
|
|
valueListenable: status,
|
|
builder: (_, value, __) {
|
|
final icon =
|
|
value ? FluentIcons.heart_24_filled : FluentIcons.heart_24_regular;
|
|
|
|
return IconButton.filledTonal(
|
|
icon: Icon(icon, color: primaryColor),
|
|
iconSize: iconSize,
|
|
onPressed: () {
|
|
updateSongLikeStatus(audioId, !status.value);
|
|
status.value = !status.value;
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _showSleepTimerDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
final duration = sleepTimerNotifier.value ?? Duration.zero;
|
|
var hours = duration.inMinutes ~/ 60;
|
|
var minutes = duration.inMinutes % 60;
|
|
return StatefulBuilder(
|
|
builder: (context, setState) {
|
|
return AlertDialog(
|
|
title: Text(context.l10n!.setSleepTimer),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(context.l10n!.selectDuration),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(context.l10n!.hours),
|
|
Row(
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(Icons.remove),
|
|
onPressed: () {
|
|
if (hours > 0) {
|
|
setState(() {
|
|
hours--;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
Text('$hours'),
|
|
IconButton(
|
|
icon: const Icon(Icons.add),
|
|
onPressed: () {
|
|
setState(() {
|
|
hours++;
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(context.l10n!.minutes),
|
|
Row(
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(Icons.remove),
|
|
onPressed: () {
|
|
if (minutes > 0) {
|
|
setState(() {
|
|
minutes--;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
Text('$minutes'),
|
|
IconButton(
|
|
icon: const Icon(Icons.add),
|
|
onPressed: () {
|
|
setState(() {
|
|
minutes++;
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: Text(context.l10n!.cancel),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
final duration = Duration(hours: hours, minutes: minutes);
|
|
if (duration.inSeconds > 0) {
|
|
audioHandler.setSleepTimer(duration);
|
|
showToast(
|
|
context,
|
|
context.l10n!.sleepTimerSet,
|
|
duration: const Duration(seconds: 1, milliseconds: 500),
|
|
);
|
|
}
|
|
Navigator.pop(context);
|
|
},
|
|
child: Text(context.l10n!.setTimer),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|