reworked CurrentlyPlayingPage; fix #47
This commit is contained in:
parent
3e49f2bdc6
commit
104b27c379
14 changed files with 414 additions and 376 deletions
|
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
42
src/lib/presentation/widgets/currently_playing_control.dart
Normal file
42
src/lib/presentation/widgets/currently_playing_control.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue