reworked CurrentlyPlayingPage; fix #47

This commit is contained in:
Moritz Weber 2023-01-27 21:36:26 +01:00
parent 3e49f2bdc6
commit 104b27c379
14 changed files with 414 additions and 376 deletions

View file

@ -8,11 +8,9 @@ import '../state/audio_store.dart';
import '../theming.dart';
import '../widgets/album_art_swipe.dart';
import '../widgets/album_background.dart';
import '../widgets/currently_playing_control.dart';
import '../widgets/currently_playing_header.dart';
import '../widgets/playback_control.dart';
import '../widgets/song_bottom_sheet.dart';
import '../widgets/song_customization_buttons.dart';
import '../widgets/time_progress_indicator.dart';
import 'queue_page.dart';
class CurrentlyPlayingPage extends StatelessWidget {
@ -26,116 +24,84 @@ class CurrentlyPlayingPage extends StatelessWidget {
final AudioStore audioStore = GetIt.I<AudioStore>();
return Scaffold(
body: SafeArea(
child: GestureDetector(
onVerticalDragEnd: (dragEndDetails) {
if (dragEndDetails.primaryVelocity! < 0) {
_openQueue(context);
} else if (dragEndDetails.primaryVelocity! > 0) {
Navigator.pop(context);
}
},
child: Stack(
children: [
const AlbumBackground(),
Material(
color: Colors.transparent,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 12.0, right: 12.0),
child: CurrentlyPlayingHeader(
onTap: _openQueue,
onMoreTap: _openMoreMenu,
),
),
const Spacer(
flex: 10,
),
const Expanded(
flex: 720,
child: Center(
child: AlbumArtSwipe(),
),
),
const Spacer(
flex: 50,
),
Observer(
builder: (BuildContext context) {
final Song? song = audioStore.currentSongStream.value;
if (song == null) return Container();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0 + 12.0),
child: SizedBox(
width: double.infinity,
height: 74.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
song.title,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 1,
style: TEXT_BIG,
),
Text(
'${song.artist}${song.album}',
style: TextStyle(
color: Colors.grey[300],
fontSize: 18.0,
fontWeight: FontWeight.w300,
),
maxLines: 2,
),
],
body: Stack(
children: [
GestureDetector(
onVerticalDragEnd: (dragEndDetails) {
if (dragEndDetails.primaryVelocity! < 0) {
_openQueue(context);
} else if (dragEndDetails.primaryVelocity! > 0) {
Navigator.pop(context);
}
},
child: const AlbumBackground(),
),
SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: CurrentlyPlayingHeader(
onTap: _openQueue,
onMoreTap: _openMoreMenu,
),
),
const Spacer(
flex: 10,
),
const Expanded(
flex: 720,
child: Center(
child: AlbumArtSwipe(),
),
),
const Spacer(
flex: 50,
),
Observer(
builder: (BuildContext context) {
final Song? song = audioStore.currentSongStream.value;
if (song == null) return Container();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0 + 12.0),
child: SizedBox(
width: double.infinity,
height: 74.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
song.title,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 1,
style: TEXT_BIG,
),
),
);
},
),
const Spacer(
flex: 50,
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0 + 6.0),
child: SongCustomizationButtons(),
),
const Spacer(
flex: 20,
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0 + 2.0),
child: PlaybackControl(),
),
const Spacer(
flex: 30,
),
const Padding(
padding: EdgeInsets.only(left: 12.0 + 16.0, right: 12.0 + 16.0, top: 10.0),
child: TimeProgressIndicator(),
),
const Spacer(
flex: 60,
),
const Center(
child: Padding(
padding: EdgeInsets.only(bottom: 8.0),
child: Icon(
Icons.expand_less_rounded,
color: Colors.white70,
Text(
'${song.artist}${song.album}',
style: TextStyle(
color: Colors.grey[300],
fontSize: 18.0,
fontWeight: FontWeight.w300,
),
maxLines: 2,
),
],
),
),
),
],
);
},
),
),
],
const Spacer(
flex: 10,
),
const CurrentlyPlayingControl(),
],
),
),
),
],
),
);
}

View file

@ -19,6 +19,7 @@ import '../../domain/usecases/play_songs.dart';
import '../../domain/usecases/seek_to_next.dart';
import '../../domain/usecases/shuffle_all.dart';
import '../../domain/utils.dart';
import '../utils.dart' as utils;
part 'audio_store.g.dart';
@ -59,8 +60,7 @@ abstract class _AudioStore with Store {
this._playPlayable,
) {
_audioPlayerRepository.managedQueueInfo.queueItemsStream.listen(_setQueue);
_audioPlayerRepository.managedQueueInfo.availableSongsStream
.listen((_) => _setAvSongs());
_audioPlayerRepository.managedQueueInfo.availableSongsStream.listen((_) => _setAvSongs());
_audioPlayerRepository.shuffleModeStream.listen((_) => _setAvSongs());
_audioPlayerRepository.playableStream.listen((_) => _setAvSongs());
}
@ -81,13 +81,15 @@ abstract class _AudioStore with Store {
_audioPlayerRepository.currentSongStream.asObservable();
@observable
late ObservableStream<bool> playingStream =
_audioPlayerRepository.playingStream.asObservable();
late ObservableStream<bool> playingStream = _audioPlayerRepository.playingStream.asObservable();
@observable
late ObservableStream<Duration> currentPositionStream = _audioPlayerRepository
.positionStream
.asObservable(initialValue: const Duration(seconds: 0));
late ObservableStream<Duration> currentPositionStream =
_audioPlayerRepository.positionStream.asObservable(initialValue: const Duration(seconds: 0));
@computed
String get positionString =>
utils.msToTimeString(currentPositionStream.value ?? const Duration(seconds: 0));
@readonly
late List<QueueItem> _queue = [];
@ -135,8 +137,7 @@ abstract class _AudioStore with Store {
@computed
bool get hasNext =>
(queueIndexStream.value != null &&
queueIndexStream.value! < _queue.length - 1) ||
(queueIndexStream.value != null && queueIndexStream.value! < _queue.length - 1) ||
(loopModeStream.value ?? LoopMode.off) != LoopMode.off;
@computed
@ -144,13 +145,8 @@ abstract class _AudioStore with Store {
(queueIndexStream.value != null && queueIndexStream.value! > 0) ||
(loopModeStream.value ?? LoopMode.off) != LoopMode.off;
Future<void> playSong(
int index, List<Song> songList, Playable playable) async {
_playSongs(
songs: songList,
initialIndex: index,
playable: playable,
keepInitialIndex: true);
Future<void> playSong(int index, List<Song> songList, Playable playable) async {
_playSongs(songs: songList, initialIndex: index, playable: playable, keepInitialIndex: true);
}
Future<void> play() async => _audioPlayerRepository.play();
@ -159,11 +155,9 @@ abstract class _AudioStore with Store {
Future<void> skipToNext() async => _seekToNext();
Future<void> skipToPrevious() async =>
_audioPlayerRepository.seekToPrevious();
Future<void> skipToPrevious() async => _audioPlayerRepository.seekToPrevious();
Future<void> seekToIndex(int index) async =>
_audioPlayerRepository.seekToIndex(index);
Future<void> seekToIndex(int index) async => _audioPlayerRepository.seekToIndex(index);
Future<void> seekToPosition(double position) async =>
_audioPlayerRepository.seekToPosition(position);
@ -171,20 +165,15 @@ abstract class _AudioStore with Store {
Future<void> setShuffleMode(ShuffleMode shuffleMode) async =>
_audioPlayerRepository.setShuffleMode(shuffleMode);
Future<void> setLoopMode(LoopMode loopMode) async =>
_audioPlayerRepository.setLoopMode(loopMode);
Future<void> setLoopMode(LoopMode loopMode) async => _audioPlayerRepository.setLoopMode(loopMode);
Future<void> shuffleAll(ShuffleMode shuffleMode) async =>
_shuffleAll(shuffleMode);
Future<void> shuffleAll(ShuffleMode shuffleMode) async => _shuffleAll(shuffleMode);
Future<void> addToQueue(List<Song> songs) async =>
_audioPlayerRepository.addToQueue(songs);
Future<void> addToQueue(List<Song> songs) async => _audioPlayerRepository.addToQueue(songs);
Future<void> playNext(List<Song> songs) async =>
_audioPlayerRepository.playNext(songs);
Future<void> playNext(List<Song> songs) async => _audioPlayerRepository.playNext(songs);
Future<void> appendToNext(List<Song> songs) async =>
_audioPlayerRepository.addToNext(songs);
Future<void> appendToNext(List<Song> songs) async => _audioPlayerRepository.addToNext(songs);
Future<void> moveQueueItem(int oldIndex, int newIndex) async =>
_audioPlayerRepository.moveQueueItem(oldIndex, newIndex);
@ -194,15 +183,13 @@ abstract class _AudioStore with Store {
Future<void> playAlbum(Album album) async => _playAlbum(album);
Future<void> playSmartList(SmartList smartList) async =>
_playSmartList(smartList);
Future<void> playSmartList(SmartList smartList) async => _playSmartList(smartList);
Future<void> playPlaylist(Playlist playlist) async => _playPlaylist(playlist);
Future<void> playArtist(Artist artist, ShuffleMode? shuffleMode) async =>
_playArtist(artist, shuffleMode);
Future<void> playPlayable(
Playable playable, ShuffleMode? shuffleMode) async =>
Future<void> playPlayable(Playable playable, ShuffleMode? shuffleMode) async =>
_playPlayable(playable, shuffleMode);
}

View file

@ -9,6 +9,13 @@ part of 'audio_store.dart';
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
mixin _$AudioStore on _AudioStore, Store {
Computed<String>? _$positionStringComputed;
@override
String get positionString =>
(_$positionStringComputed ??= Computed<String>(() => super.positionString,
name: '_AudioStore.positionString'))
.value;
Computed<int>? _$queueLengthComputed;
@override
@ -29,6 +36,13 @@ mixin _$AudioStore on _AudioStore, Store {
bool get hasNext => (_$hasNextComputed ??=
Computed<bool>(() => super.hasNext, name: '_AudioStore.hasNext'))
.value;
Computed<bool>? _$hasPreviousComputed;
@override
bool get hasPrevious =>
(_$hasPreviousComputed ??= Computed<bool>(() => super.hasPrevious,
name: '_AudioStore.hasPrevious'))
.value;
late final _$currentSongStreamAtom =
Atom(name: '_AudioStore.currentSongStream', context: context);
@ -211,9 +225,11 @@ playableStream: ${playableStream},
queueIndexStream: ${queueIndexStream},
shuffleModeStream: ${shuffleModeStream},
loopModeStream: ${loopModeStream},
positionString: ${positionString},
queueLength: ${queueLength},
numAvailableSongs: ${numAvailableSongs},
hasNext: ${hasNext}
hasNext: ${hasNext},
hasPrevious: ${hasPrevious}
''';
}
}

View file

@ -13,24 +13,14 @@ class AlbumArt extends StatelessWidget {
return AspectRatio(
aspectRatio: 1.0,
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2.0),
boxShadow: const [
decoration: const BoxDecoration(
boxShadow: [
BoxShadow(color: Colors.black26, blurRadius: 8, offset: Offset(0, 1)),
],
),
child: Stack(
children: [
Container(
width: double.infinity,
height: double.infinity,
child: Image(
image: getAlbumImage(song.albumArtPath),
fit: BoxFit.cover,
),
),
],
child: Image(
image: getAlbumImage(song.albumArtPath),
fit: BoxFit.cover,
),
),
);

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:fimber/fimber.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@ -25,26 +27,36 @@ class _AlbumArtSwipeState extends State<AlbumArtSwipe> {
int seekingCount = 0;
bool get isSeekActive => seekingCount <= 0;
late StreamSubscription _streamSubscription;
@override
void initState() {
super.initState();
controller = PageController(initialPage: audioStore.queueIndexStream.value!);
audioStore.queueIndexStream.distinct().listen((value) {
_streamSubscription = audioStore.queueIndexStream.listen((value) {
AlbumArtSwipe._log.d('index: $value');
if (value == null) return;
AlbumArtSwipe._log.d('queue item: ${audioStore.queue[value]}');
// only animate if not already on the same page (rounded)
if (controller.positions.isNotEmpty && value != null && value != controller.page?.round()) {
AlbumArtSwipe._log.v('Animate to page: $value | Currently at page: ${controller.page}');
seekingCount++;
controller
.animateToPage(
value,
duration: const Duration(milliseconds: 600),
curve: Curves.easeInOut,
)
.then((_) {
AlbumArtSwipe._log.v('Animation done ($value) -> enable seek');
if (controller.positions.isNotEmpty && value != controller.page?.round()) {
if ((value - (controller.page ?? value)).abs() > 1.6) {
AlbumArtSwipe._log.d('jump to: $value');
seekingCount++;
controller.jumpToPage(value);
seekingCount--;
});
} else {
AlbumArtSwipe._log.d('seek to: $value');
seekingCount++;
controller
.animateToPage(
value,
duration: const Duration(milliseconds: 600),
curve: Curves.easeInOut,
)
.then((_) => seekingCount--);
}
}
});
}
@ -52,15 +64,17 @@ class _AlbumArtSwipeState extends State<AlbumArtSwipe> {
@override
void dispose() {
controller.dispose();
_streamSubscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
const Key key = ValueKey('ALBUM_ART_SLIDE');
const Key key = ValueKey('ALBUM_ART_SWIPE');
return Observer(builder: (context) {
AlbumArtSwipe._log.v('Build PageView');
AlbumArtSwipe._log.d('Build PageView');
final queue = audioStore.queue;
return PageView.builder(
key: key,
controller: controller,
@ -69,7 +83,7 @@ class _AlbumArtSwipeState extends State<AlbumArtSwipe> {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 0.0),
child: AlbumArt(song: audioStore.queue[index].song),
child: AlbumArt(song: queue[index].song),
),
);
},
@ -79,9 +93,7 @@ class _AlbumArtSwipeState extends State<AlbumArtSwipe> {
}
void _conditionalSeek(int index) {
AlbumArtSwipe._log.v('Seek triggered to: $index | Current page: ${controller.page}');
if (isSeekActive && index != audioStore.queueIndexStream.value) {
AlbumArtSwipe._log.v('Seeking to: $index');
audioStore.seekToIndex(index);
}
}

View file

@ -1,8 +1,8 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:palette_generator/palette_generator.dart';
import '../../domain/entities/song.dart';
import '../state/audio_store.dart';
@ -18,22 +18,26 @@ class AlbumBackground extends StatefulWidget {
class _AlbumBackgroundState extends State<AlbumBackground> {
final AudioStore audioStore = GetIt.I<AudioStore>();
late Widget _backgroundWidget;
Widget _backgroundWidget = Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [DARK3, DARK1],
stops: [0.0, 1.0],
)),
);
late StreamSubscription<Song?> _streamSub;
@override
void initState() {
super.initState();
setState(() {
_backgroundWidget = _getBackgroundWidget(audioStore.currentSongStream.value);
});
_setBackgroundWidget(audioStore.currentSongStream.value);
_streamSub = audioStore.currentSongStream.listen((value) {
setState(() {
_backgroundWidget = _getBackgroundWidget(value);
});
});
_streamSub = audioStore.currentSongStream.listen(_setBackgroundWidget);
}
@override
@ -44,49 +48,46 @@ class _AlbumBackgroundState extends State<AlbumBackground> {
@override
Widget build(BuildContext context) {
return ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 96.0, sigmaY: 96.0),
child: ShaderMask(
shaderCallback: (Rect bounds) => LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Theme.of(context).scaffoldBackgroundColor,
DARK3.withOpacity(0.2),
DARK3.withOpacity(0.2),
Theme.of(context).scaffoldBackgroundColor,
Theme.of(context).scaffoldBackgroundColor,
],
stops: const [
0.0,
0.2,
0.6,
0.75,
1.0,
],
).createShader(bounds),
blendMode: BlendMode.srcATop,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 600),
child: _backgroundWidget,
),
),
return AnimatedSwitcher(
duration: const Duration(milliseconds: 600),
child: _backgroundWidget,
);
}
Widget _getBackgroundWidget(Song? song) {
if (song == null) return Container(color: DARK3);
Future<void> _setBackgroundWidget(Song? song) async {
if (song == null) return;
return Container(
key: ValueKey(song.albumArtPath),
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
image: DecorationImage(
image: getAlbumImage(song.albumArtPath),
fit: BoxFit.cover,
),
),
);
PaletteGenerator.fromImageProvider(
getAlbumImage(song.albumArtPath),
targets: PaletteTarget.baseTargets,
).then((paletteGenerator) {
final colors = <Color?>[
paletteGenerator.vibrantColor?.color,
paletteGenerator.lightVibrantColor?.color,
paletteGenerator.mutedColor?.color,
paletteGenerator.darkVibrantColor?.color,
paletteGenerator.lightMutedColor?.color,
paletteGenerator.dominantColor?.color,
DARK3,
];
final Color? color = colors.firstWhere((c) => c != null);
setState(() {
_backgroundWidget = Container(
key: ValueKey(song.albumArtPath),
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color.lerp(DARK3, color, 0.4) ?? DARK3, DARK1],
stops: const [0.0, 1.0],
),
),
);
});
});
}
}

View file

@ -17,84 +17,82 @@ class CurrentlyPlayingBar extends StatelessWidget {
Widget build(BuildContext context) {
final AudioStore audioStore = GetIt.I<AudioStore>();
return Observer(
builder: (BuildContext context) {
final Song? song = audioStore.currentSongStream.value;
final Duration position =
audioStore.currentPositionStream.value ?? const Duration(seconds: 0);
if (song != null) {
return Column(
verticalDirection: VerticalDirection.up,
children: <Widget>[
GestureDetector(
onTap: () => _onTap(context),
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.only(bottom: 0.0, top: 8.0),
child: Material(
color: Colors.transparent,
child: Row(
return Column(
verticalDirection: VerticalDirection.up,
children: <Widget>[
GestureDetector(
onTap: () => _onTap(context),
child: Material(
color: DARK1,
child: Observer(builder: (context) {
final Song? song = audioStore.currentSongStream.value;
if (song == null) return Container();
return Padding(
padding: const EdgeInsets.only(
bottom: 0.0,
top: 8.0,
left: 4.0,
right: 4.0,
),
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: 56.0,
child: Image(
image: getAlbumImage(song.albumArtPath),
height: 56.0,
),
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SizedBox(width: 4.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
border: Border.all(color: DARK3, width: 0.4),
borderRadius: BorderRadius.circular(2.0),
),
width: 56.0,
child: Image(
image: getAlbumImage(song.albumArtPath),
height: 56.0,
),
),
Text(
song.title,
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
song.title,
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
Text(
song.artist,
style: TEXT_SMALL_SUBTITLE.copyWith(color: Colors.white70),
)
],
),
),
const PlayPauseButton(
key: ValueKey('CURRENTLY_PLAYING_BAR_PLAY_PAUSE'),
circle: false,
),
const NextButton(
key: ValueKey('CURRENTLY_PLAYING_BAR_NEXT'),
),
const SizedBox(width: 4.0),
Text(
song.artist,
style: TEXT_SMALL_SUBTITLE.copyWith(color: Colors.white70),
)
],
),
),
),
const PlayPauseButton(
key: ValueKey('CURRENTLY_PLAYING_BAR_PLAY_PAUSE'),
circle: false,
),
const NextButton(
key: ValueKey('CURRENTLY_PLAYING_BAR_NEXT'),
),
],
),
),
Container(
child: LinearProgressIndicator(
value: position.inMilliseconds / song.duration.inMilliseconds,
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
backgroundColor: Colors.white10,
),
height: 2,
),
],
);
}
return Container();
},
);
}),
),
),
Container(
child: Observer(
builder: (context) {
final Song? song = audioStore.currentSongStream.value;
if (song == null) return Container();
final Duration position =
audioStore.currentPositionStream.value ?? const Duration(seconds: 0);
return LinearProgressIndicator(
value: position.inMilliseconds / song.duration.inMilliseconds,
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
backgroundColor: Colors.white10,
);
},
),
height: 2,
),
],
);
}

View file

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'playback_control.dart';
import 'song_customization_buttons.dart';
import 'time_progress_indicator.dart';
class CurrentlyPlayingControl extends StatelessWidget {
const CurrentlyPlayingControl({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: const [
SizedBox(height: 12.0),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0 + 2.0),
child: SongCustomizationButtons(),
),
SizedBox(height: 12.0),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0 + 2.0),
child: PlaybackControl(),
),
SizedBox(height: 12.0),
Padding(
padding: EdgeInsets.only(left: 12.0 - 4.0, right: 12.0 - 4.0),
child: TimeProgressIndicator(),
),
SizedBox(height: 8.0),
Center(
child: Padding(
padding: EdgeInsets.only(bottom: 8.0),
child: Icon(
Icons.expand_less_rounded,
color: Colors.white70,
),
),
),
],
);
}
}

View file

@ -11,19 +11,21 @@ class LikeButton extends StatelessWidget {
Key? key,
required this.song,
this.iconSize = 20.0,
this.visualDensity = VisualDensity.compact,
}) : super(key: key);
final double iconSize;
final Song song;
final VisualDensity visualDensity;
@override
Widget build(BuildContext context) {
final MusicDataStore musicDataStore = GetIt.I<MusicDataStore>();
return IconButton(
iconSize: iconSize,
icon: Icon(
likeCountIcon(song.likeCount),
size: iconSize,
color: likeCountColor(song.likeCount),
),
onPressed: () {
@ -33,7 +35,7 @@ class LikeButton extends StatelessWidget {
musicDataStore.setLikeCount([song], 0);
}
},
visualDensity: VisualDensity.compact,
visualDensity: visualDensity,
);
}
}

View file

@ -13,15 +13,15 @@ class PlaybackControl extends StatelessWidget {
Widget build(BuildContext context) {
return Row(
children: const [
LoopButton(iconSize: 20.0),
PreviousButton(iconSize: 32.0),
LoopButton(iconSize: 24.0),
PreviousButton(iconSize: 48.0),
PlayPauseButton(
circle: true,
iconSize: 52.0,
iconSize: 60.0,
),
NextButton(iconSize: 32.0),
NextButton(iconSize: 48.0),
ShuffleButton(
iconSize: 20.0,
iconSize: 24.0,
),
],
mainAxisAlignment: MainAxisAlignment.spaceBetween,

View file

@ -33,22 +33,21 @@ class SongCustomizationButtons extends StatelessWidget {
song.next || song.previous ? Icons.link_rounded : Icons.link_off_rounded,
color: linkColor(song),
),
iconSize: 20.0,
iconSize: 24.0,
onPressed: () => _editLinks(context),
visualDensity: VisualDensity.compact,
),
LikeButton(
iconSize: 20.0,
iconSize: 28.0,
song: song,
visualDensity: VisualDensity.standard,
),
IconButton(
icon: Icon(
blockLevelIcon(song.blockLevel),
size: 20.0,
size: 24.0,
color: song.blockLevel == 0 ? Colors.white24 : Colors.white,
),
onPressed: () => _editBlockLevel(context),
visualDensity: VisualDensity.compact,
),
],
mainAxisAlignment: MainAxisAlignment.spaceBetween,

View file

@ -7,77 +7,94 @@ import 'package:get_it/get_it.dart';
import '../state/audio_store.dart';
import '../utils.dart';
class TimeProgressIndicator extends StatefulWidget {
class TimeProgressIndicator extends StatelessWidget {
const TimeProgressIndicator({Key? key}) : super(key: key);
@override
State<TimeProgressIndicator> createState() => _TimeProgressIndicatorState();
}
class _TimeProgressIndicatorState extends State<TimeProgressIndicator> {
bool useLocalPosition = false;
double localPosition = 0.0;
@override
Widget build(BuildContext context) {
final AudioStore audioStore = GetIt.I<AudioStore>();
return Observer(
builder: (BuildContext context) {
final duration = audioStore.currentSongStream.value?.duration ?? const Duration(minutes: 1);
final sliderWidth = useLocalPosition
? localPosition
: _position(audioStore.currentPositionStream.value?.inMilliseconds ?? 0,
duration.inMilliseconds);
return Row(
children: [
Container(
width: 44,
child: Text(
msToTimeString(
audioStore.currentPositionStream.value ?? const Duration(seconds: 0),
),
),
),
Expanded(
child: Slider(
value: sliderWidth,
activeColor: Colors.white,
inactiveColor: Colors.white10,
onChanged: (value) {
setState(() {
localPosition = value;
});
},
onChangeStart: (value) {
setState(() {
localPosition = value;
useLocalPosition = true;
});
},
onChangeEnd: (value) {
setState(() {
useLocalPosition = false;
});
audioStore.seekToPosition(value);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const CustomTimeIndicator(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Row(
children: [
Observer(builder: (context) {
return Text(audioStore.positionString);
}),
Observer(
builder: (context) {
final duration =
audioStore.currentSongStream.value?.duration ?? const Duration(minutes: 1);
return Text(msToTimeString(duration));
},
),
),
Container(
width: 44,
alignment: Alignment.centerRight,
child: Text(msToTimeString(duration)),
),
],
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
);
},
],
mainAxisAlignment: MainAxisAlignment.spaceBetween,
),
),
],
);
}
}
class CustomTimeIndicator extends StatefulWidget {
const CustomTimeIndicator({Key? key}) : super(key: key);
@override
_CustomTimeIndicatorState createState() => _CustomTimeIndicatorState();
}
class _CustomTimeIndicatorState extends State<CustomTimeIndicator> {
bool useLocalPosition = false;
double localPosition = 0.0;
final AudioStore audioStore = GetIt.I<AudioStore>();
@override
Widget build(BuildContext context) {
return SliderTheme(
data: const SliderThemeData(
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 6.0, elevation: 0.0),
),
child: Observer(
builder: (context) {
final duration =
audioStore.currentSongStream.value?.duration ?? const Duration(minutes: 1);
final sliderWidth = useLocalPosition
? localPosition
: _position(audioStore.currentPositionStream.value, duration);
return Slider(
value: sliderWidth,
activeColor: Colors.white,
inactiveColor: Colors.white10,
onChanged: (value) {
setState(() {
localPosition = value;
});
},
onChangeStart: (value) {
setState(() {
localPosition = value;
useLocalPosition = true;
});
},
onChangeEnd: (value) {
setState(() {
useLocalPosition = false;
});
audioStore.seekToPosition(value);
},
);
},
),
);
}
double _position(int position, int duration) {
final res = position / duration;
double _position(Duration? position, Duration duration) {
if (position == null) return 0;
final res = position.inMilliseconds / duration.inMilliseconds;
return min(1.0, max(0.0, res));
}
}

View file

@ -42,7 +42,7 @@ packages:
name: audio_service
url: "https://pub.dartlang.org"
source: hosted
version: "0.18.7"
version: "0.18.9"
audio_service_platform_interface:
dependency: transitive
description:
@ -63,7 +63,7 @@ packages:
name: audio_session
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.11"
version: "0.1.13"
boolean_selector:
dependency: transitive
description:
@ -407,7 +407,7 @@ packages:
name: just_audio
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.30"
version: "0.9.31"
just_audio_platform_interface:
dependency: transitive
description:
@ -506,6 +506,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
palette_generator:
dependency: "direct main"
description:
name: palette_generator
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3+2"
path:
dependency: "direct main"
description:
@ -692,14 +699,14 @@ packages:
name: sqflite
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
version: "2.2.3"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0+2"
version: "2.4.1"
sqlite3:
dependency: transitive
description:

View file

@ -26,6 +26,7 @@ dependencies:
just_audio: ^0.9.18 # MIT
mobx: ^2.0.1 # MIT
on_audio_query: ^2.6.1 # BSD 3
palette_generator: ^0.3.3+2 # BSD 3
path: ^1.8.0 # BSD 3
path_provider: ^2.0.2 # BSD 3
permission_handler: ^8.3.0 # MIT