diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..116bc22 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1 @@ +-keep class androidx.lifecycle.DefaultLifecycleObserver \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3ad5a68..5ee36ce 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ xmlns:tools="http://schemas.android.com/tools"> + + + diff --git a/lib/blocs/library/cubit/library_items_cubit.dart b/lib/blocs/library/cubit/library_items_cubit.dart index 2432cad..6f5828b 100644 --- a/lib/blocs/library/cubit/library_items_cubit.dart +++ b/lib/blocs/library/cubit/library_items_cubit.dart @@ -91,4 +91,22 @@ class LibraryItemsCubit extends Cubit { "Removed ${mediaItem.title} from ${mediaPlaylistDB.playlistName}"); } } + + Future?> getPlaylist(String playlistName) async { + try { + final playlistDB = mediaPlaylistsDB + .firstWhere((element) => element.playlistName == playlistName); + final _playlist = await BloomeeDBService.getPlaylistItems(playlistDB); + + if (_playlist != null) { + final mediaItems = + _playlist.map((e) => MediaItemDB2MediaItem(e)).toList(); + return mediaItems; + } + } catch (e) { + log("Error in getting playlist: $e", name: "libItemCubit"); + return null; + } + return null; + } } diff --git a/lib/routes_and_consts/routes.dart b/lib/routes_and_consts/routes.dart index 3773e78..b046ad5 100644 --- a/lib/routes_and_consts/routes.dart +++ b/lib/routes_and_consts/routes.dart @@ -50,9 +50,10 @@ class GlobalRoutes { GoRoute( name: GlobalStrConsts.playlistView, // parentNavigatorKey: globalRouterKey, - path: '/PlaylistView/:playlistName', - builder: (context, state) => PlaylistView( - playListName: state.pathParameters['playlistName'] ?? "none"), + path: '/PlaylistView', + builder: (context, state) { + return PlaylistView(); + }, ), GoRoute( path: '/AddToPlaylist', diff --git a/lib/screens/screen/audioPlayer_screen.dart b/lib/screens/screen/audioPlayer_screen.dart index 7679fee..22d419a 100644 --- a/lib/screens/screen/audioPlayer_screen.dart +++ b/lib/screens/screen/audioPlayer_screen.dart @@ -290,17 +290,29 @@ class _AudioPlayerViewState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - InkWell( - onTap: () => musicPlayer.rewind(), - child: const Icon( + IconButton( + padding: const EdgeInsets.all(5), + constraints: const BoxConstraints(), + style: const ButtonStyle( + tapTargetSize: MaterialTapTargetSize + .shrinkWrap, // the '2023' part + ), + onPressed: () => musicPlayer.rewind(), + icon: const Icon( MingCute.refresh_4_line, color: Default_Theme.primaryColor1, size: 40, ), ), - InkWell( - onTap: () => musicPlayer.skipToPrevious(), - child: const Icon( + IconButton( + padding: const EdgeInsets.all(5), + constraints: const BoxConstraints(), + style: const ButtonStyle( + tapTargetSize: MaterialTapTargetSize + .shrinkWrap, // the '2023' part + ), + onPressed: () => musicPlayer.skipToPrevious(), + icon: const Icon( MingCute.skip_previous_fill, color: Default_Theme.primaryColor1, size: 40, @@ -354,21 +366,33 @@ class _AudioPlayerViewState extends State { ); }); }), - InkWell( - onTap: () => musicPlayer.skipToNext(), - child: const Icon( + IconButton( + padding: const EdgeInsets.all(5), + constraints: const BoxConstraints(), + style: const ButtonStyle( + tapTargetSize: MaterialTapTargetSize + .shrinkWrap, // the '2023' part + ), + onPressed: () => musicPlayer.skipToNext(), + icon: const Icon( MingCute.skip_forward_fill, color: Default_Theme.primaryColor1, size: 40, ), ), - InkWell( - child: const Icon( + IconButton( + padding: const EdgeInsets.all(5), + constraints: const BoxConstraints(), + style: const ButtonStyle( + tapTargetSize: MaterialTapTargetSize + .shrinkWrap, // the '2023' part + ), + icon: const Icon( MingCute.external_link_line, color: Default_Theme.primaryColor1, size: 40, ), - onTap: () { + onPressed: () { launchUrlString(context .read() .bloomeePlayer diff --git a/lib/screens/screen/chart/chart_view.dart b/lib/screens/screen/chart/chart_view.dart index 3b0cf41..0ba781d 100644 --- a/lib/screens/screen/chart/chart_view.dart +++ b/lib/screens/screen/chart/chart_view.dart @@ -1,11 +1,9 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first -import 'package:Bloomee/blocs/explore/cubit/explore_cubits.dart'; import 'package:Bloomee/model/chart_model.dart'; import 'package:Bloomee/services/db/bloomee_db_service.dart'; import 'package:flutter/material.dart'; import 'package:Bloomee/screens/widgets/chart_list_tile.dart'; import 'package:Bloomee/theme_data/default.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; class ChartScreen extends StatefulWidget { // ChartCubit? chartCubit; diff --git a/lib/screens/screen/explore_screen.dart b/lib/screens/screen/explore_screen.dart index d5e8035..792e7e8 100644 --- a/lib/screens/screen/explore_screen.dart +++ b/lib/screens/screen/explore_screen.dart @@ -1,6 +1,5 @@ import 'package:Bloomee/blocs/explore/cubit/explore_cubits.dart'; import 'package:Bloomee/blocs/mediaPlayer/bloomee_player_cubit.dart'; -import 'package:Bloomee/model/MediaPlaylistModel.dart'; import 'package:Bloomee/routes_and_consts/global_str_consts.dart'; import 'package:Bloomee/screens/widgets/chart_list_tile.dart'; import 'package:Bloomee/screens/widgets/more_bottom_sheet.dart'; @@ -162,41 +161,50 @@ class _ExploreScreenState extends State { style: Default_Theme.primaryTextStyle.merge(const TextStyle( fontSize: 34, color: Default_Theme.primaryColor1))), const Spacer(), - Padding( - padding: const EdgeInsets.only(right: 10), - child: InkWell( - splashColor: Colors.transparent, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const NotificationView())); - }, - child: const Icon(MingCute.notification_line, - color: Default_Theme.primaryColor1, size: 30.0), + IconButton( + padding: const EdgeInsets.all(5), + constraints: const BoxConstraints(), + style: const ButtonStyle( + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, // the '2023' part ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const NotificationView())); + }, + icon: const Icon(MingCute.notification_line, + color: Default_Theme.primaryColor1, size: 30.0), ), - Padding( - padding: const EdgeInsets.only(right: 10), - child: InkWell( - splashColor: Colors.transparent, - onTap: () { - Navigator.push(context, - MaterialPageRoute(builder: (context) => const TimerView())); - }, - child: const Icon(MingCute.stopwatch_line, - color: Default_Theme.primaryColor1, size: 30.0), + IconButton( + padding: EdgeInsets.all(5), + constraints: const BoxConstraints(), + style: const ButtonStyle( + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, // the '2023' part ), + onPressed: () { + Navigator.push(context, + MaterialPageRoute(builder: (context) => const TimerView())); + }, + icon: const Icon(MingCute.stopwatch_line, + color: Default_Theme.primaryColor1, size: 30.0), ), - InkWell( - splashColor: Colors.transparent, - onTap: () { + IconButton( + padding: EdgeInsets.all(5), + constraints: const BoxConstraints(), + style: const ButtonStyle( + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, // the '2023' part + ), + onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => const SettingsView())); }, - child: const Icon(MingCute.settings_3_line, + icon: const Icon(MingCute.settings_3_line, color: Default_Theme.primaryColor1, size: 30.0)), ], ), diff --git a/lib/screens/screen/library_screen.dart b/lib/screens/screen/library_screen.dart index 045e24d..46e131a 100644 --- a/lib/screens/screen/library_screen.dart +++ b/lib/screens/screen/library_screen.dart @@ -1,3 +1,5 @@ +import 'package:Bloomee/screens/screen/library_views/cubit/current_playlist_cubit.dart'; +import 'package:Bloomee/screens/screen/library_views/more_opts_sheet.dart'; import 'package:Bloomee/screens/widgets/sign_board_widget.dart'; import 'package:Bloomee/utils/load_Image.dart'; import 'package:flutter/material.dart'; @@ -7,7 +9,6 @@ import 'package:Bloomee/blocs/library/cubit/library_items_cubit.dart'; import 'package:Bloomee/routes_and_consts/global_str_consts.dart'; import 'package:Bloomee/screens/widgets/createPlaylist_bottomsheet.dart'; import 'package:Bloomee/screens/widgets/smallPlaylistCard_widget.dart'; -import 'package:Bloomee/services/db/GlobalDB.dart'; import 'package:Bloomee/theme_data/default.dart'; import 'package:icons_plus/icons_plus.dart'; @@ -65,22 +66,34 @@ class LibraryScreen extends StatelessWidget { style: Default_Theme.primaryTextStyle.merge(const TextStyle( fontSize: 34, color: Default_Theme.primaryColor1))), const Spacer(), - InkWell( - onTap: () { - createPlaylistBottomSheet(context); - }, - child: const Icon(MingCute.add_fill, - size: 25, color: Default_Theme.primaryColor1), - ), - InkWell( - onTap: () { - context.pushNamed(GlobalStrConsts.ImportMediaFromPlatforms); - }, - child: const Padding( - padding: EdgeInsets.only(left: 10), - child: Icon(FontAwesome.file_import_solid, - size: 25, color: Default_Theme.primaryColor1), - ), + ButtonBar( + buttonPadding: const EdgeInsets.all(0), + children: [ + IconButton( + padding: const EdgeInsets.all(5), + constraints: const BoxConstraints(), + style: const ButtonStyle( + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, // the '2023' part + ), + onPressed: () { + createPlaylistBottomSheet(context); + }, + icon: const Icon(MingCute.add_fill, + size: 25, color: Default_Theme.primaryColor1)), + IconButton( + padding: const EdgeInsets.all(5), + constraints: const BoxConstraints(), + style: const ButtonStyle( + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, // the '2023' part + ), + onPressed: () { + context.pushNamed(GlobalStrConsts.ImportMediaFromPlatforms); + }, + icon: const Icon(FontAwesome.file_import_solid, + size: 25, color: Default_Theme.primaryColor1)) + ], ), ], ), @@ -89,8 +102,8 @@ class LibraryScreen extends StatelessWidget { } class ListOfPlaylists extends StatefulWidget { - LibraryItemsState state; - ListOfPlaylists({super.key, required this.state}); + final LibraryItemsState state; + const ListOfPlaylists({super.key, required this.state}); @override State createState() => _ListOfPlaylistsState(); @@ -109,47 +122,31 @@ class _ListOfPlaylistsState extends State { return const SizedBox(); } else { return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Dismissible( - key: ValueKey(widget.state.playlists[index].playlistName), - background: Container( - color: Colors.red, - child: const Row( - children: [ - Padding( - padding: EdgeInsets.only(left: 20), - child: Icon( - MingCute.delete_3_line, - color: Colors.white, - size: 30, - ), - ), - Spacer(), - ], - ), - ), - direction: DismissDirection.startToEnd, - onDismissed: (DismissDirection direction) { - context.read().removePlaylist( - MediaPlaylistDB( - playlistName: - widget.state.playlists[index].playlistName)); - setState(() { - widget.state.playlists.removeAt(index); - }); + padding: const EdgeInsets.only( + bottom: 8, + ), + child: InkWell( + splashColor: Default_Theme.accentColor1.withOpacity(0.2), + hoverColor: Default_Theme.accentColor2.withOpacity(0.1), + highlightColor: Default_Theme.accentColor2.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + onLongPress: () { + showPlaylistOptsSheet( + context, widget.state.playlists[index].playlistName); }, - child: InkWell( - onTap: () => context - .pushNamed(GlobalStrConsts.playlistView, pathParameters: { - "playlistName": widget.state.playlists[index].playlistName - }), - child: SmallPlaylistCard( - playListTitle: widget.state.playlists[index].playlistName, - coverArt: loadImageCached( - widget.state.playlists[index].coverImgUrl.toString()), - playListsubTitle: - widget.state.playlists[index].subTitle ?? "Unknown"), - ), + onTap: () { + context.read().setupPlaylist( + widget.state.playlists[index].playlistName); + context.pushNamed( + GlobalStrConsts.playlistView, + ); + }, + child: SmallPlaylistCard( + playListTitle: widget.state.playlists[index].playlistName, + coverArt: loadImageCached( + widget.state.playlists[index].coverImgUrl.toString()), + playListsubTitle: + widget.state.playlists[index].subTitle ?? "Unknown"), ), ); } diff --git a/lib/screens/screen/library_views/cubit/current_playlist_cubit.dart b/lib/screens/screen/library_views/cubit/current_playlist_cubit.dart index 9c9f70c..2e6dd86 100644 --- a/lib/screens/screen/library_views/cubit/current_playlist_cubit.dart +++ b/lib/screens/screen/library_views/cubit/current_playlist_cubit.dart @@ -1,19 +1,14 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:Bloomee/model/MediaPlaylistModel.dart'; import 'package:Bloomee/model/songModel.dart'; import 'package:Bloomee/services/db/GlobalDB.dart'; import 'package:Bloomee/services/db/cubit/bloomee_db_cubit.dart'; import 'package:Bloomee/utils/pallete_generator.dart'; - part 'current_playlist_state.dart'; -// load current playlist -// return if data is loaded or not -// provide fucntion to return length of playlist -// provide album art - class CurrentPlaylistCubit extends Cubit { MediaPlaylist? mediaPlaylist; PaletteGenerator? paletteGenerator; @@ -32,7 +27,7 @@ class CurrentPlaylistCubit extends Cubit { emit(state.copyWith( albumName: mediaPlaylist?.albumName, isFetched: true, - mediaItem: mediaPlaylist?.mediaItems)); + mediaItem: List.from(mediaPlaylist!.mediaItems))); } } @@ -49,7 +44,7 @@ class CurrentPlaylistCubit extends Cubit { emit(state.copyWith( albumName: mediaPlaylist?.albumName, isFetched: true, - mediaItem: mediaPlaylist?.mediaItems)); + mediaItem: List.from(mediaPlaylist!.mediaItems))); } int getPlaylistLength() { diff --git a/lib/screens/screen/library_views/cubit/current_playlist_state.dart b/lib/screens/screen/library_views/cubit/current_playlist_state.dart index a12cafc..aec4c68 100644 --- a/lib/screens/screen/library_views/cubit/current_playlist_state.dart +++ b/lib/screens/screen/library_views/cubit/current_playlist_state.dart @@ -1,15 +1,15 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first part of 'current_playlist_cubit.dart'; -class CurrentPlaylistState extends MediaPlaylist { +class CurrentPlaylistState extends Equatable { bool isFetched; - late List mediaItem; - late String albumName; + List mediaItems; + String albumName; CurrentPlaylistState({ required this.isFetched, - required this.mediaItem, + required this.mediaItems, required this.albumName, - }) : super(albumName: albumName, mediaItems: mediaItem); + }); CurrentPlaylistState copyWith({ bool? isFetched, @@ -18,18 +18,21 @@ class CurrentPlaylistState extends MediaPlaylist { }) { return CurrentPlaylistState( isFetched: isFetched ?? this.isFetched, - mediaItem: mediaItem ?? this.mediaItem, + mediaItems: mediaItem ?? mediaItems, albumName: albumName ?? this.albumName, ); } + + @override + List get props => [isFetched, mediaItems, albumName]; } final class CurrentPlaylistInitial extends CurrentPlaylistState { CurrentPlaylistInitial() - : super(albumName: "", isFetched: false, mediaItem: []); + : super(albumName: "", isFetched: false, mediaItems: []); } final class CurrentPlaylistLoading extends CurrentPlaylistState { CurrentPlaylistLoading() - : super(albumName: "Loading", isFetched: false, mediaItem: []); + : super(albumName: "Loading", isFetched: false, mediaItems: []); } diff --git a/lib/screens/screen/library_views/import_media_view.dart b/lib/screens/screen/library_views/import_media_view.dart index 1789705..205bd69 100644 --- a/lib/screens/screen/library_views/import_media_view.dart +++ b/lib/screens/screen/library_views/import_media_view.dart @@ -1,15 +1,14 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:developer'; +import 'package:Bloomee/screens/widgets/snackbar.dart'; import 'package:Bloomee/utils/external_list_importer.dart'; +import 'package:Bloomee/utils/file_manager.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:icons_plus/icons_plus.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; -import 'package:Bloomee/repository/Saavn/cubit/saavn_repository_cubit.dart'; -import 'package:Bloomee/screens/screen/library_views/cubit/import_playlist_cubit.dart'; import 'package:Bloomee/screens/widgets/import_playlist.dart'; -import 'package:Bloomee/services/db/cubit/bloomee_db_cubit.dart'; import 'package:Bloomee/theme_data/default.dart'; class ImportMediaFromPlatformsView extends StatelessWidget { @@ -61,6 +60,26 @@ class ImportMediaFromPlatformsView extends StatelessWidget { onClickFunc: () { log("music from youtube"); }), + ImportFromBtn( + btnName: "Import Playlist from Storage", + btnIcon: FontAwesome.file, + onClickFunc: () { + FilePicker.platform.pickFiles().then((value) { + if (value != null) { + log(value.files[0].path.toString(), name: "Import File"); + if (value.files[0].path != null) { + if (value.files[0].path!.endsWith('.blm')) { + BloomeeFileManager.importPlaylist(value.files[0].path!); + SnackbarService.showMessage( + "Started Importing Playlist"); + } else { + log("Invalid File Format", name: "Import File"); + SnackbarService.showMessage("Invalid File Format"); + } + } + } + }); + }), ], ), ); diff --git a/lib/screens/screen/library_views/more_opts_sheet.dart b/lib/screens/screen/library_views/more_opts_sheet.dart new file mode 100644 index 0000000..674585c --- /dev/null +++ b/lib/screens/screen/library_views/more_opts_sheet.dart @@ -0,0 +1,186 @@ +import 'package:Bloomee/blocs/library/cubit/library_items_cubit.dart'; +import 'package:Bloomee/blocs/mediaPlayer/bloomee_player_cubit.dart'; +import 'package:Bloomee/model/MediaPlaylistModel.dart'; +import 'package:Bloomee/screens/widgets/snackbar.dart'; +import 'package:Bloomee/services/db/GlobalDB.dart'; +import 'package:Bloomee/theme_data/default.dart'; +import 'package:Bloomee/utils/file_manager.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:icons_plus/icons_plus.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:share_plus/share_plus.dart'; + +void showPlaylistOptsSheet(BuildContext context, String playlistName) { + showFloatingModalBottomSheet( + context: context, + builder: (context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [ + Color.fromARGB(255, 7, 17, 50), + Color.fromARGB(255, 5, 0, 24), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: [0.0, 0.5]), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), topRight: Radius.circular(20)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ListTile( + leading: const Icon( + MingCute.play_circle_fill, + color: Default_Theme.primaryColor1, + size: 28, + ), + title: const Text( + "Play", + style: TextStyle( + color: Default_Theme.primaryColor1, + fontFamily: "Unageo", + fontSize: 17, + fontWeight: FontWeight.w400), + ), + onTap: () async { + Navigator.pop(context); + final _list = await context + .read() + .getPlaylist(playlistName); + if (_list != null && _list.isNotEmpty) { + context + .read() + .bloomeePlayer + .loadPlaylist( + MediaPlaylist( + mediaItems: _list, albumName: playlistName), + doPlay: true); + SnackbarService.showMessage("Playing $playlistName"); + } + }, + ), + ListTile( + leading: const Icon( + MingCute.playlist_2_line, + color: Default_Theme.primaryColor1, + size: 28, + ), + title: const Text( + "Add Playlist to Queue", + style: TextStyle( + color: Default_Theme.primaryColor1, + fontFamily: "Unageo", + fontSize: 17, + fontWeight: FontWeight.w400), + ), + onTap: () async { + Navigator.pop(context); + final _list = await context + .read() + .getPlaylist(playlistName); + if (_list != null && _list.isNotEmpty) { + context + .read() + .bloomeePlayer + .addQueueItems(_list); + SnackbarService.showMessage( + "Added $playlistName to Queue"); + } + }, + ), + ListTile( + leading: const Icon( + MingCute.share_2_fill, + color: Default_Theme.primaryColor1, + size: 28, + ), + title: const Text( + "Share Playlist", + style: TextStyle( + color: Default_Theme.primaryColor1, + fontFamily: "Unageo", + fontSize: 17, + fontWeight: FontWeight.w400), + ), + onTap: () async { + Navigator.pop(context); + final _tmpPath = + await BloomeeFileManager.exportPlaylist(playlistName); + _tmpPath != null + ? Share.shareXFiles([XFile(_tmpPath)]) + : null; + }, + ), + ListTile( + leading: const Icon( + MingCute.delete_2_fill, + color: Default_Theme.primaryColor1, + size: 28, + ), + title: const Text( + "Delete Playlist", + style: TextStyle( + color: Default_Theme.primaryColor1, + fontFamily: "Unageo", + fontSize: 17, + fontWeight: FontWeight.w400), + ), + onTap: () { + Navigator.pop(context); + context.read().removePlaylist( + MediaPlaylistDB(playlistName: playlistName)); + }, + ), + ], + ), + ), + ], + ); + }, + ); +} + +Future showFloatingModalBottomSheet({ + required BuildContext context, + required WidgetBuilder builder, + Color? backgroundColor, +}) async { + final result = await showCustomModalBottomSheet( + context: context, + builder: builder, + containerWidget: (_, animation, child) => FloatingModal( + child: child, + ), + expand: false); + + return result; +} + +class FloatingModal extends StatelessWidget { + final Widget child; + final Color? backgroundColor; + + const FloatingModal({super.key, required this.child, this.backgroundColor}); + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Material( + color: backgroundColor, + clipBehavior: Clip.antiAlias, + borderRadius: BorderRadius.circular(12), + child: child, + ), + ), + ); + } +} diff --git a/lib/screens/screen/library_views/playlist_screen.dart b/lib/screens/screen/library_views/playlist_screen.dart index 3a61143..770bd4a 100644 --- a/lib/screens/screen/library_views/playlist_screen.dart +++ b/lib/screens/screen/library_views/playlist_screen.dart @@ -1,4 +1,4 @@ -import 'dart:developer'; +import 'package:Bloomee/model/MediaPlaylistModel.dart'; import 'package:Bloomee/screens/widgets/more_bottom_sheet.dart'; import 'package:Bloomee/screens/widgets/sign_board_widget.dart'; import 'package:Bloomee/screens/widgets/song_card_widget.dart'; @@ -20,21 +20,13 @@ import '../../../blocs/mediaPlayer/bloomee_player_cubit.dart'; import 'dart:ui'; class PlaylistView extends StatelessWidget { - String playListName; - PlaylistView({ Key? key, - required this.playListName, - }) : super(key: key) { - log("Showing playlist: $playListName", name: "PlaylistView"); - } - Future setUpPlaylist(BuildContext context) async { - context.read().loadPlaylist(playListName); - } + }) : super(key: key); @override Widget build(BuildContext context) { - setUpPlaylist(context); + // setUpPlaylist(context); return Scaffold( extendBodyBehindAppBar: true, backgroundColor: Default_Theme.themeColor, @@ -47,6 +39,7 @@ class PlaylistView extends StatelessWidget { builder: (context, state) { if (state is! CurrentPlaylistInitial && state.mediaItems.isNotEmpty) { return Column( + mainAxisSize: MainAxisSize.min, children: [ SizedBox( height: 380, @@ -59,7 +52,7 @@ class PlaylistView extends StatelessWidget { height: 260, child: Container( decoration: BoxDecoration( - color: Colors.blueAccent.shade400, + color: Colors.transparent, boxShadow: [ BoxShadow( color: context @@ -80,9 +73,11 @@ class PlaylistView extends StatelessWidget { height: 260, child: Container( color: Colors.blueAccent.shade400, - child: loadImageCached(context - .read() - .getPlaylistCoverArt()), + child: loadImageCached( + context + .read() + .getPlaylistCoverArt(), + fit: BoxFit.cover), ), ), Positioned( @@ -95,7 +90,7 @@ class PlaylistView extends StatelessWidget { .queueTitle, builder: (context, snapshot) { if (snapshot.hasData && - snapshot.data == playListName) { + snapshot.data == state.albumName) { return StreamBuilder( stream: context .read() @@ -143,7 +138,9 @@ class PlaylistView extends StatelessWidget { context .read() .bloomeePlayer - .loadPlaylist(state); + .loadPlaylist(MediaPlaylist( + mediaItems: state.mediaItems, + albumName: state.albumName)); context .read() .bloomeePlayer @@ -162,7 +159,7 @@ class PlaylistView extends StatelessWidget { SizedBox( width: 300, child: Text( - playListName, + state.albumName, maxLines: 2, overflow: TextOverflow.fade, style: Default_Theme.secondoryTextStyle.merge( @@ -184,7 +181,7 @@ class PlaylistView extends StatelessWidget { Padding( padding: const EdgeInsets.only(top: 20), child: Text( - "${state.mediaItem.length} Songs", + "${state.mediaItems.length} Songs", style: Default_Theme.secondoryTextStyle .merge(TextStyle( color: Default_Theme.primaryColor1 @@ -215,12 +212,8 @@ class PlaylistView extends StatelessWidget { ), ); } else { - return const Center( - child: SignBoardWidget( - message: "No Songs in Playlist", - icon: MingCute.music_2_line, - ), - ); + return const SignBoardWidget( + message: "No Songs Yet", icon: MingCute.playlist_line); } }, ), @@ -241,6 +234,7 @@ class _PlaylistState extends State { Widget build(BuildContext context) { final _state = widget.state; return ReorderableListView.builder( + physics: const BouncingScrollPhysics(), proxyDecorator: proxyDecorator, itemBuilder: (context, index) { return Dismissible( @@ -262,7 +256,7 @@ class _PlaylistState extends State { ), onDismissed: (direction) { context.read().removeMediaFromPlaylist( - _state.mediaItem[index], + _state.mediaItems[index], MediaPlaylistDB(playlistName: _state.albumName)); setState(() { _state.mediaItems.removeAt(index); @@ -278,10 +272,12 @@ class _PlaylistState extends State { .bloomeePlayer .currentPlaylist, _state.mediaItems)) { - context - .read() - .bloomeePlayer - .loadPlaylist(_state, idx: index, doPlay: true); + context.read().bloomeePlayer.loadPlaylist( + MediaPlaylist( + mediaItems: _state.mediaItems, + albumName: _state.albumName), + idx: index, + doPlay: true); // context.read().bloomeePlayer.play(); } else if (context .read() @@ -302,7 +298,7 @@ class _PlaylistState extends State { ), ); }, - itemCount: _state.mediaItem.length, + itemCount: _state.mediaItems.length, onReorder: (oldIndex, newIndex) { setState(() { if (oldIndex < newIndex) { @@ -314,7 +310,7 @@ class _PlaylistState extends State { .read() .reorderPositionOfItemInDB(_state.albumName, oldIndex, newIndex); }); - print(_state.mediaItem.toList().toString()); + print(_state.mediaItems.toList().toString()); }, ); } diff --git a/lib/screens/widgets/more_bottom_sheet.dart b/lib/screens/widgets/more_bottom_sheet.dart index e8f7ce4..b52d5c3 100644 --- a/lib/screens/widgets/more_bottom_sheet.dart +++ b/lib/screens/widgets/more_bottom_sheet.dart @@ -10,7 +10,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:icons_plus/icons_plus.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -215,41 +214,3 @@ void showMoreBottomSheet( ); }); } - -class FloatingModal extends StatelessWidget { - final Widget child; - final Color? backgroundColor; - - const FloatingModal({super.key, required this.child, this.backgroundColor}); - - @override - Widget build(BuildContext context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Material( - color: backgroundColor, - clipBehavior: Clip.antiAlias, - borderRadius: BorderRadius.circular(12), - child: child, - ), - ), - ); - } -} - -Future showFloatingModalBottomSheet({ - required BuildContext context, - required WidgetBuilder builder, - Color? backgroundColor, -}) async { - final result = await showCustomModalBottomSheet( - context: context, - builder: builder, - containerWidget: (_, animation, child) => FloatingModal( - child: child, - ), - expand: false); - - return result; -} diff --git a/lib/screens/widgets/smallPlaylistCard_widget.dart b/lib/screens/widgets/smallPlaylistCard_widget.dart index 2db4613..a055e9c 100644 --- a/lib/screens/widgets/smallPlaylistCard_widget.dart +++ b/lib/screens/widgets/smallPlaylistCard_widget.dart @@ -19,43 +19,45 @@ class SmallPlaylistCard extends StatelessWidget { return SizedBox( height: 80, child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox( - width: 80, - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: coverArt, + Padding( + padding: const EdgeInsets.only(left: 2, right: 10), + child: SizedBox( + width: 80, + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: coverArt, + ), ), ), - Padding( - padding: const EdgeInsets.only(left: 15), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: MediaQuery.of(context).size.width * 0.7, - child: Text( - playListTitle, - overflow: TextOverflow.fade, - maxLines: 2, - style: Default_Theme.secondoryTextStyle.merge( - const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w900, - color: Default_Theme.primaryColor1)), - ), - ), - Text( - playListsubTitle, + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.7, + child: Text( + playListTitle, + overflow: TextOverflow.fade, + maxLines: 2, style: Default_Theme.secondoryTextStyle.merge(const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, + fontSize: 18, + fontWeight: FontWeight.w900, color: Default_Theme.primaryColor1)), ), - ], - ), + ), + Text( + playListsubTitle, + style: Default_Theme.secondoryTextStyle.merge(const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Default_Theme.primaryColor1)), + ), + ], ) ], ), diff --git a/lib/services/bloomeePlayer.dart b/lib/services/bloomeePlayer.dart index ef161ae..65b3389 100644 --- a/lib/services/bloomeePlayer.dart +++ b/lib/services/bloomeePlayer.dart @@ -247,8 +247,12 @@ class BloomeeMusicPlayer extends BaseAudioHandler @override Future addQueueItems(List mediaItems, {String queueName = "Queue"}) async { - queue.add(mediaItems); - queueTitle.add(queueName); + for (var mediaItem in mediaItems) { + await addQueueItem( + mediaItem, + atLast: true, + ); + } } @override diff --git a/lib/services/db/GlobalDB.dart b/lib/services/db/GlobalDB.dart index 2585201..83cf035 100644 --- a/lib/services/db/GlobalDB.dart +++ b/lib/services/db/GlobalDB.dart @@ -1,6 +1,7 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first // import 'dart:js_util'; +import 'dart:convert'; import 'package:isar/isar.dart'; part 'GlobalDB.g.dart'; @@ -96,6 +97,47 @@ class MediaItemDB { permaURL.hashCode ^ language.hashCode; } + + Map toMap() { + return { + 'id': null, + 'title': title, + 'album': album, + 'artist': artist, + 'artURL': artURL, + 'genre': genre, + 'duration': duration, + 'mediaID': mediaID, + 'streamingURL': streamingURL, + 'source': source, + 'permaURL': permaURL, + 'language': language, + 'isLiked': isLiked, + }; + } + + factory MediaItemDB.fromMap(Map map) { + return MediaItemDB( + id: null, + title: map['title'] as String, + album: map['album'] as String, + artist: map['artist'] as String, + artURL: map['artURL'] as String, + genre: map['genre'] as String, + duration: map['duration'] != null ? map['duration'] as int : null, + mediaID: map['mediaID'] as String, + streamingURL: map['streamingURL'] as String, + source: map['source'] != null ? map['source'] as String : null, + permaURL: map['permaURL'] as String, + language: map['language'] as String, + isLiked: map['isLiked'] as bool, + ); + } + + String toJson() => json.encode(toMap()); + + factory MediaItemDB.fromJson(String source) => + MediaItemDB.fromMap(json.decode(source) as Map); } int fastHash(String string) { diff --git a/lib/services/db/bloomee_db_service.dart b/lib/services/db/bloomee_db_service.dart index 5086105..33e74e2 100644 --- a/lib/services/db/bloomee_db_service.dart +++ b/lib/services/db/bloomee_db_service.dart @@ -28,12 +28,17 @@ class BloomeeDBService { log(_mediaPlaylistDB.toString(), name: "DB"); if (_mediaPlaylistDB == null) { - addPlaylist(mediaPlaylistDB); + final tmpId = await addPlaylist(mediaPlaylistDB); + _mediaPlaylistDB = isarDB.mediaPlaylistDBs + .filter() + .isarIdEqualTo(mediaPlaylistDB.isarId) + .findFirstSync(); + log("${_mediaPlaylistDB.toString()} ID: $tmpId", name: "DB"); } if (_mediaitem != null) { // log("1", name: "DB"); - _mediaitem.mediaInPlaylistsDB.add(mediaPlaylistDB); + _mediaitem.mediaInPlaylistsDB.add(_mediaPlaylistDB!); _id = _mediaitem.id; // log("2", name: "DB"); isarDB.writeTxnSync(() => isarDB.mediaItemDBs.putSync(_mediaitem!)); @@ -113,19 +118,30 @@ class BloomeeDBService { // isarDB.writeTxnSync(() => isarDB.mediaItemDBs.putSync(mediaItemDB)); } - static Future addPlaylist(MediaPlaylistDB mediaPlaylistDB) async { + static Future addPlaylist(MediaPlaylistDB mediaPlaylistDB) async { Isar isarDB = await db; + int? id; MediaPlaylistDB? _mediaPlaylist = isarDB.mediaPlaylistDBs .filter() .isarIdEqualTo(mediaPlaylistDB.isarId) .findFirstSync(); if (_mediaPlaylist == null) { - isarDB - .writeTxnSync(() => isarDB.mediaPlaylistDBs.putSync(mediaPlaylistDB)); + isarDB.writeTxnSync( + () => id = isarDB.mediaPlaylistDBs.putSync(mediaPlaylistDB)); } else { log("Already created", name: "DB"); + id = _mediaPlaylist.isarId; } + return id; + } + + static Future getPlaylist(String playlistName) async { + Isar isarDB = await db; + return isarDB.mediaPlaylistDBs + .filter() + .playlistNameEqualTo(playlistName) + .findFirstSync(); } static Future likeMediaItem(MediaItemDB mediaItemDB, @@ -406,7 +422,9 @@ class BloomeeDBService { isarDB.recentlyPlayedDBs.where().sortByLastPlayedDesc().findAllSync(); List _mediaItems = []; for (var element in _recentlyPlayed) { - _mediaItems.add(MediaItemDB2MediaItem(element.mediaItem.value!)); + if (element.mediaItem.value != null) { + _mediaItems.add(MediaItemDB2MediaItem(element.mediaItem.value!)); + } } return MediaPlaylist(mediaItems: _mediaItems, albumName: "Recently Played"); } diff --git a/lib/utils/file_manager.dart b/lib/utils/file_manager.dart new file mode 100644 index 0000000..85b13d4 --- /dev/null +++ b/lib/utils/file_manager.dart @@ -0,0 +1,150 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; +import 'package:Bloomee/services/db/GlobalDB.dart'; +import 'package:Bloomee/services/db/bloomee_db_service.dart'; +import 'package:path_provider/path_provider.dart'; + +class BloomeeFileManager { + static Future isPlaylistExists(String playlistName) async { + // check if playlist exists + final _list = await BloomeeDBService.getPlaylists4Library(); + for (final playlist in _list) { + if (playlist.playlistName == playlistName) { + return true; + } + } + return false; + } + + static Future exportPlaylist(String playlistName) async { + // export playlist to json file + final mediaPlaylistDB = await BloomeeDBService.getPlaylist(playlistName); + if (mediaPlaylistDB != null) { + try { + List? playlistItems = + await BloomeeDBService.getPlaylistItems(mediaPlaylistDB); + if (playlistItems != null) { + final Map playlistMap = { + 'playlistName': mediaPlaylistDB.playlistName, + 'mediaRanks': mediaPlaylistDB.mediaRanks, + 'mediaItems': playlistItems.map((e) => e.toMap()).toList(), + }; + final path = await writeToJSON( + '${mediaPlaylistDB.playlistName}_BloomeePlaylist.blm', + playlistMap); + log("Playlist exported successfully", name: "FileManager"); + return path; + } + } catch (e) { + log("Error exporting playlist: $e"); + return null; + } + } else { + log("Playlist not found", name: "FileManager"); + } + return null; + } + + static Future exportMediaItem(MediaItemDB mediaItemDB) async { + // export media item to json file + try { + final Map mediaItemMap = mediaItemDB.toMap(); + await writeToJSON('${mediaItemDB.title}_BloomeeSong.blm', mediaItemMap); + log("Media item exported successfully", name: "FileManager"); + return '${mediaItemDB.title}_BloomeeSong.blm'; + } catch (e) { + log("Error exporting media item: $e", name: "FileManager"); + return null; + } + } + + static Future importPlaylist(String filePath) async { + //check if file is json or not + if (!filePath.endsWith('.blm')) { + log("Invalid file format", name: "FileManager"); + return; + } + // import playlist from json file + await readFromJSON(filePath).then((playlistMap) async { + log("Playlist map: $playlistMap", name: "FileManager"); + if (playlistMap != null && playlistMap.isNotEmpty) { + try { + bool playlistExists = + await isPlaylistExists(playlistMap['playlistName']); + int i = 1; + String playlistName = playlistMap['playlistName']; + while (playlistExists) { + playlistName = playlistMap['playlistName'] + "_$i"; + playlistExists = await isPlaylistExists(playlistName); + i++; + } + log("Playlist name: $playlistName", name: "FileManager"); + + final mediaPlaylistDB = MediaPlaylistDB( + playlistName: playlistName, + ); + + for (final mediaItemMap in playlistMap['mediaItems']) { + final mediaItemDB = MediaItemDB.fromMap(mediaItemMap); + await BloomeeDBService.addMediaItem(mediaItemDB, mediaPlaylistDB); + log("Media item imported successfully - ${mediaItemDB.title}", + name: "FileManager"); + } + + log("Playlist imported successfully"); + } catch (e) { + log("Error importing playlist: $e"); + } + } + }); + } + + static void importMediaItem(String filePath) async { + if (!filePath.endsWith('.blm')) { + log("Invalid file format", name: "FileManager"); + return; + } + // import media item from json file + await readFromJSON(filePath).then((mediaItemMap) { + if (mediaItemMap != null && mediaItemMap.isNotEmpty) { + try { + final mediaItemDB = MediaItemDB.fromMap(mediaItemMap); + BloomeeDBService.addMediaItem( + mediaItemDB, MediaPlaylistDB(playlistName: "Imported")); + log("Media item imported successfully"); + } catch (e) { + log("Error importing media item: $e"); + } + } + }); + } + + static Future writeToJSON( + String fileName, Map data) async { + // write data to file + try { + final filePath = (await getApplicationCacheDirectory()).path; + final file = File('$filePath/$fileName'); + await file.writeAsString(jsonEncode(data)); + log("Data written to file: $filePath/$fileName", name: "FileManager"); + return '$filePath/$fileName'; + } catch (e) { + log("Error writing file:", error: e, name: "FileManager"); + return null; + } + } + + static Future?> readFromJSON(String filePath) async { + // read data from file + try { + final file = File(filePath); + final data = await file.readAsString(); + log("Data read from file: $filePath", name: "FileManager"); + return jsonDecode(data); + } catch (e) { + log("Error reading file:", error: e); + return null; + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 0a77e3f..613c68a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,6 +63,7 @@ dependencies: connectivity_plus: ^5.0.2 share_plus: ^7.2.2 receive_sharing_intent: ^1.6.7 + file_picker: ^8.0.0+1