diff --git a/lib/providers/MusicPlayer.dart b/lib/providers/MusicPlayer.dart index d5df1b6..bc68e6c 100644 --- a/lib/providers/MusicPlayer.dart +++ b/lib/providers/MusicPlayer.dart @@ -1,5 +1,8 @@ -import 'dart:developer'; - +import 'dart:async'; +// import 'dart:developer'; +// _player.sequenceState?.effectiveSequence +// .map((e) => e.tag.extras['track'] as Track) +// .toList() import 'package:flutter/cupertino.dart'; import 'package:just_audio/just_audio.dart'; import 'package:just_audio_background/just_audio_background.dart'; @@ -18,8 +21,10 @@ class MusicPlayer extends ChangeNotifier { int _index = 0; bool _initialised = false; bool _isPlaying = false; - List _songs = []; - MiniplayerController _miniplayerController = MiniplayerController(); + + Track? tempSong; + final MiniplayerController _miniplayerController = MiniplayerController(); + StreamController _cancelController = StreamController(); PaletteGenerator? _color; ConcatenatingAudioSource? playlist = ConcatenatingAudioSource( @@ -40,6 +45,10 @@ class MusicPlayer extends ChangeNotifier { _index = event ?? _index; notifyListeners(); }); + _player.sequenceStream.listen((event) { + notifyListeners(); + }); + _player.loopModeStream.listen((event) { notifyListeners(); }); @@ -52,32 +61,34 @@ class MusicPlayer extends ChangeNotifier { get player => _player; get isInitialized => _initialised; + get miniplayerController => _miniplayerController; - get song { - if (_songs.isEmpty) { - return null; - } else if (_player.currentIndex == null) { - return _songs[0]; - } else if (_player.currentIndex! >= _songs.length) { - return _songs[0]; - } else { - return _songs[_player.currentIndex!]; - } - } + Track? get song => _player.sequenceState?.currentSource?.tag.extras == null + ? tempSong + : Track.fromMap(_player.sequenceState?.currentSource?.tag.extras); + + get songs => + _player.sequence?.map((e) => Track.fromMap(e.tag.extras)).toList() ?? []; - get songs => _songs; get color => _color; get isPlaying => _isPlaying; addToQUeue(Track newSong) async { + bool exists = _player.sequence + ?.where((element) => element.tag.id == newSong.videoId) + .isNotEmpty ?? + false; + if (exists) { + return; + } PaletteGenerator? color = await generateColor(newSong.thumbnails.last.url); newSong.colorPalette = ColorPalette( darkMutedColor: color?.darkMutedColor?.color, lightMutedColor: color?.lightMutedColor?.color); - _songs.add(newSong); _initialised = true; notifyListeners(); - playlist?.add( + playlist + ?.add( AudioSource.uri( await getAudioUri(newSong.videoId), tag: MediaItem( @@ -85,38 +96,54 @@ class MusicPlayer extends ChangeNotifier { title: newSong.title, artUri: Uri.parse(newSong.thumbnails.last.url), artist: newSong.artists.first.name, + extras: newSong.toMap(), ), ), - ); + ) + .then((value) { + tempSong = null; + notifyListeners(); + }); } - setPlayer() async { + Future setPlayer() async { await _player.stop(); playlist!.clear(); - _songs.clear(); + playlist = null; + playlist = ConcatenatingAudioSource( + useLazyPreparation: true, + shuffleOrder: DefaultShuffleOrder(), + children: [], + ); + // _songs.clear(); - _player.setAudioSource(playlist!, + await _player.setAudioSource(playlist!, initialIndex: 0, initialPosition: Duration.zero, preload: false); } addNew(Track newSong) async { + if (newSong.videoId == _player.sequenceState?.currentSource?.tag.id) { + _miniplayerController.animateToHeight(state: PanelState.MAX); + return; + } + _cancelController.sink.add(true); + _cancelController = StreamController(); try { PaletteGenerator? color = await generateColor(newSong.thumbnails.last.url); newSong.colorPalette = ColorPalette( darkMutedColor: color?.darkMutedColor?.color, lightMutedColor: color?.lightMutedColor?.color); - + tempSong = newSong; await setPlayer(); - _songs = [newSong]; - _initialised = true; - notifyListeners(); - _miniplayerController.animateToHeight(state: PanelState.MAX); - await playlist?.add(AudioSource.uri( + _player.play(); + + await playlist + ?.add(AudioSource.uri( await getAudioUri(newSong.videoId), tag: MediaItem( id: newSong.videoId, @@ -124,26 +151,39 @@ class MusicPlayer extends ChangeNotifier { artUri: Uri.parse(newSong.thumbnails.last.url), artist: newSong.artists.first.name, album: newSong.albums?.name, + extras: newSong.toMap(), ), - )); + )) + .then((value) { + tempSong = null; + _miniplayerController.animateToHeight(state: PanelState.MAX); + notifyListeners(); + }); - _player.play(); await HomeApi.getWatchPlaylist(newSong.videoId, 10) .then((List value) async { List tracks = value; - _songs[0].thumbnails.last.url = tracks[0]['thumbnail'].last['url']; + // _songs[0].thumbnails.last.url = tracks[0]['thumbnail'].last['url']; tracks.removeAt(0); - for (var track in tracks) { - if (playlist == null || playlist!.length >= 10) { - break; + + bool breakIt = false; + _cancelController.stream.first.then((cancelled) async { + if (cancelled) { + breakIt = true; } - try { - track['thumbnails'] = track['thumbnail']; - Track tr = Track.fromMap(track); - await addToQUeue(tr); - } catch (e) { - log(e.toString()); + }); + for (Map track in tracks) { + // If the loop has been cancelled, exit + + if (breakIt) { + break; } + + // Add the item to the otherSongs list + + track['thumbnails'] = track['thumbnail']; + Track tr = Track.fromMap(track); + await addToQUeue(tr); } }); return true; @@ -153,31 +193,54 @@ class MusicPlayer extends ChangeNotifier { } Future addPlayList(List newSongs) async { + _cancelController.sink.add(true); + _cancelController = StreamController(); + tempSong = Track.fromMap(newSongs[0]); + Track newSong = Track.fromMap(newSongs[0]); + + PaletteGenerator? color = await generateColor(newSong.thumbnails.last.url); + newSong.colorPalette = ColorPalette( + darkMutedColor: color?.darkMutedColor?.color, + lightMutedColor: color?.lightMutedColor?.color); + tempSong = newSong; + await setPlayer(); + _initialised = true; - setPlayer(); - int i = 0; - for (Map nSong in newSongs) { - if (playlist != null) { - Track newSong = Track.fromMap(nSong); - await addToQUeue(newSong); - if (i == 0) { - _player.play(); - _miniplayerController.animateToHeight(state: PanelState.MAX); - } + notifyListeners(); + _miniplayerController.animateToHeight(state: PanelState.MAX); + + _player.play(); + + bool breakIt = false; + _cancelController.stream.first.then((cancelled) async { + if (cancelled) { + breakIt = true; + } + }); - i += 1; - } else { + for (Map nSong in newSongs) { + // If the loop has been cancelled, exit + if (breakIt) { break; } + + // Add the item to the otherSongs list + Track newSong = Track.fromMap(nSong); + await addToQUeue(newSong); } } /// Change the songe position in playlist from one index to another. - moveTo(index, newIndex) { - Track song = _songs.removeAt(index); - _songs.insertAll(newIndex, [song]); - playlist?.move(index, newIndex); + moveTo(index, newIndex) async { + // Track song = _songs.removeAt(index); + // _songs.insertAll(newIndex, [song]); + await playlist?.move(index, newIndex); + notifyListeners(); + } + + removeAt(index) async { + await playlist?.removeAt(index); notifyListeners(); } @@ -203,7 +266,7 @@ class MusicPlayer extends ChangeNotifier { if (!_player.playing) _player.play(); } - /// Play the previous song in the playlist. If the current song is first one, nothing will happen. + /// Play the previous item, or does nothing if there is no previous item. previous() { _player.seekToPrevious(); if (!_player.playing) _player.play(); @@ -226,8 +289,7 @@ class MusicPlayer extends ChangeNotifier { : (audios.length / 2).floor()); audioUrl = audios[audioNumber].url.toString(); - // log(audios[audioNumber].size.totalMegaBytes.toString()); - // log(audioQuality); + return Uri.parse(audioUrl); } catch (e) { return Uri(); diff --git a/lib/screens/FavouriteScreen.dart b/lib/screens/FavouriteScreen.dart index 776df3e..30aaa42 100644 --- a/lib/screens/FavouriteScreen.dart +++ b/lib/screens/FavouriteScreen.dart @@ -37,7 +37,28 @@ class FavouriteScreen extends StatelessWidget { return ListView( children: favourites.map((track) { Map newMap = jsonDecode(jsonEncode(track)); - return TrackTile(track: newMap); + + return Dismissible( + direction: DismissDirection.endToStart, + key: Key("$newMap['videoId']"), + onDismissed: (direction) { + box.delete(newMap['videoId']); + }, + background: Container( + color: Colors.red, + child: Center( + child: Text( + "Remove from Favourites", + style: Theme.of(context) + .primaryTextTheme + .bodyLarge + ?.copyWith( + color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + ), + child: TrackTile(track: newMap), + ); }).toList(), ); }, diff --git a/lib/screens/HomeScreen.dart b/lib/screens/HomeScreen.dart index c2fbe25..be56ecc 100644 --- a/lib/screens/HomeScreen.dart +++ b/lib/screens/HomeScreen.dart @@ -287,7 +287,11 @@ class _HomeScreenState extends State crossAxisAlignment: CrossAxisAlignment.start, children: content - .sublist(0, 5) + .sublist( + 0, + content.length > 5 + ? 5 + : content.length) .map((track) { return TrackTile(track: track); }).toList()), diff --git a/lib/screens/MainScreen.dart b/lib/screens/MainScreen.dart index 88fe9a7..f751768 100644 --- a/lib/screens/MainScreen.dart +++ b/lib/screens/MainScreen.dart @@ -45,6 +45,7 @@ class _MainScreenState extends State { Widget build(BuildContext context) { bool isDarkTheme = context.watch().themeMode == ThemeMode.dark; + return Scaffold( body: LayoutBuilder(builder: (context, constraints) { return Column( @@ -109,7 +110,9 @@ class _MainScreenState extends State { ), Opacity( opacity: 1 - (percentage), - child: const PanelHeader()), + child: PanelHeader( + song: context.watch().song!, + )), ], ); }), diff --git a/lib/screens/PlayerScreen.dart b/lib/screens/PlayerScreen.dart index 8e8fb51..bd8ce27 100644 --- a/lib/screens/PlayerScreen.dart +++ b/lib/screens/PlayerScreen.dart @@ -116,6 +116,7 @@ class _PlayerScreenState extends State { builder: (context, Box box, child) { Map? favourite = box.get(song.videoId); return MaterialButton( + padding: EdgeInsets.all(16), elevation: 0, color: favourite != null ? Theme.of(context) @@ -317,7 +318,9 @@ class _PlayerScreenState extends State { showModalBottomSheet( context: context, builder: (context) { - return QueueScreen(song: song, songs: songs); + return QueueScreen( + song: song, /*songs: songs*/ + ); }); }), title: Icon( @@ -340,15 +343,16 @@ class QueueScreen extends StatelessWidget { const QueueScreen({ Key? key, required this.song, - required this.songs, + // required this.songs, }) : super(key: key); final Track song; - final List songs; + // final List songs; @override Widget build(BuildContext context) { AudioPlayer player = context.watch().player; + List songs = context.watch().songs ?? []; bool isdarkTheme = context.watch().themeMode == ThemeMode.dark; return Directionality( @@ -368,54 +372,78 @@ class QueueScreen extends StatelessWidget { return Material( color: Colors.transparent, key: Key("$index"), - child: ListTile( - onTap: () { - if (player.currentIndex != index) { - player.play(); + child: Dismissible( + key: Key(song.videoId), + direction: DismissDirection.endToStart, + onDismissed: (direction) async { + await context.read().removeAt(index); + if (songs.isEmpty) { + Navigator.pop(context); } }, - key: Key("$index"), - leading: ClipRRect( - borderRadius: BorderRadius.circular(5), - child: Stack( - children: [ - Image.network( - 'https://vibeapi-sheikh-haziq.vercel.app/thumb/hd?id=${song.videoId}', - width: 50, - height: 50, - ), - if (player.currentIndex == index) - Container( - color: Colors.black.withOpacity(0.7), - height: 50, - width: 50, - child: const Center( - child: Icon( - Icons.music_note, + background: Container( + color: Colors.red, + child: Center( + child: Text( + "Remove from queue", + style: Theme.of(context) + .primaryTextTheme + .bodyLarge + ?.copyWith( color: Colors.white, + fontWeight: FontWeight.bold), + ), + ), + ), + child: ListTile( + onTap: () { + if (player.currentIndex != index) { + player.play(); + } + }, + key: Key("$index"), + leading: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Stack( + children: [ + Image.network( + 'https://vibeapi-sheikh-haziq.vercel.app/thumb/hd?id=${song.videoId}', + width: 50, + height: 50, + ), + if (player.currentIndex == index) + Container( + color: Colors.black.withOpacity(0.7), + height: 50, + width: 50, + child: const Center( + child: Icon( + Icons.music_note, + color: Colors.white, + ), ), ), - ), - ], + ], + ), ), - ), - title: Text( - song.title, - style: Theme.of(context) - .primaryTextTheme - .titleMedium - ?.copyWith(overflow: TextOverflow.ellipsis), - ), - subtitle: Text( - song.artists.first.name, - style: - const TextStyle(color: Color.fromARGB(255, 93, 92, 92)), - ), - trailing: ReorderableDragStartListener( - index: index, - child: Icon( - Icons.drag_handle, - color: isdarkTheme ? Colors.white : Colors.black, + title: Text( + song.title, + style: Theme.of(context) + .primaryTextTheme + .titleMedium + ?.copyWith(overflow: TextOverflow.ellipsis), + ), + subtitle: Text( + song.artists.first.name, + style: const TextStyle( + color: Color.fromARGB(255, 93, 92, 92)), + ), + trailing: ReorderableDragStartListener( + index: index, + child: Icon( + Icons.drag_handle, + color: isdarkTheme ? Colors.white : Colors.black, + ), ), ), ), diff --git a/lib/widgets/PanelHeader.dart b/lib/widgets/PanelHeader.dart index d17f2a3..7df27cd 100644 --- a/lib/widgets/PanelHeader.dart +++ b/lib/widgets/PanelHeader.dart @@ -6,18 +6,19 @@ import 'package:vibe_music/Models/Track.dart'; import 'package:vibe_music/providers/LanguageProvider.dart'; import 'package:vibe_music/providers/MusicPlayer.dart'; import 'package:vibe_music/providers/ThemeProvider.dart'; -import 'package:vibe_music/utils/colors.dart'; class PanelHeader extends StatelessWidget { const PanelHeader({ Key? key, + required this.song, }) : super(key: key); + final Track song; @override Widget build(BuildContext context) { MusicPlayer musicPlayer = context.watch(); AudioPlayer player = musicPlayer.player; - Track song = musicPlayer.song; + // Track? song = musicPlayer.song; return Directionality( textDirection: context.watch().textDirection, child: Material( @@ -72,94 +73,98 @@ class PanelHeader extends StatelessWidget { ), ), ), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 7), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(2), - child: CachedNetworkImage( - imageUrl: - 'https://vibeapi-sheikh-haziq.vercel.app/thumb/hd?id=${song.videoId}', - width: 54, - height: 54, - fit: BoxFit.fill, - errorWidget: (context, error, stackTrace) { - return Image.network( - song.thumbnails.last.url, - width: double.infinity, - fit: BoxFit.fill, - ); - }, + if (song != null) + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 7), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(2), + child: CachedNetworkImage( + imageUrl: + 'https://vibeapi-sheikh-haziq.vercel.app/thumb/hd?id=${song.videoId}', + width: 54, + height: 54, + fit: BoxFit.fill, + errorWidget: (context, error, stackTrace) { + return Image.network( + song.thumbnails.last.url, + width: double.infinity, + fit: BoxFit.fill, + ); + }, + ), ), - ), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - song.title, - style: Theme.of(context) - .primaryTextTheme - .titleMedium - ?.copyWith( - fontSize: 14, - fontWeight: FontWeight.bold, - overflow: TextOverflow.ellipsis, - ), - ), - Text( - song.artists.first.name, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Color.fromARGB(255, 93, 92, 92), - overflow: TextOverflow.ellipsis, + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + song.title, + style: Theme.of(context) + .primaryTextTheme + .titleMedium + ?.copyWith( + fontSize: 14, + fontWeight: FontWeight.bold, + overflow: TextOverflow.ellipsis, + ), + ), + Text( + song.artists.first.name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Color.fromARGB(255, 93, 92, 92), + overflow: TextOverflow.ellipsis, + ), ), - ), - ], + ], + ), ), - ), - IconButton( - onPressed: () { - context.read().togglePlay(); - }, - icon: StreamBuilder( - stream: player.playerStateStream, - builder: (context, snapshot) { - PlayerState? state = snapshot.data; - if (state == null) { - return const CircularProgressIndicator(); - } - switch (state.processingState) { - case ProcessingState.buffering: - case ProcessingState.loading: + IconButton( + onPressed: () { + context.read().togglePlay(); + }, + icon: StreamBuilder( + stream: player.playerStateStream, + builder: (context, snapshot) { + PlayerState? state = snapshot.data; + if (state == null) { return const CircularProgressIndicator(); - case ProcessingState.completed: - return const Icon(Icons.play_arrow); - case ProcessingState.idle: - return const Icon(Icons.play_arrow); - case ProcessingState.ready: - return Icon( - player.playing - ? Icons.pause_rounded - : Icons.play_arrow_rounded, - color: context - .watch() - .themeMode == - ThemeMode.dark - ? Colors.white - : Colors.black, - ); - } - }), - ), - ], + } + switch (state.processingState) { + case ProcessingState.buffering: + case ProcessingState.loading: + case ProcessingState.idle: + return const CircularProgressIndicator(); + case ProcessingState.completed: + return const Icon(Icons.play_arrow); + // case ProcessingState.idle: + // return const Icon(Icons.play_arrow); + case ProcessingState.ready: + return Icon( + player.playing + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + color: context + .watch() + .themeMode == + ThemeMode.dark + ? Colors.white + : Colors.black, + ); + default: + return const CircularProgressIndicator(); + } + }), + ), + ], + ), ), - ), ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 20bf98c..cf5b2d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: vibe_music description: A new Flutter project. publish_to: 'none' -version: 0.4.2+4 +version: 0.5.1+5 environment: sdk: '>=2.18.5 <3.0.0'