diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2a15298..eaa058d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -40,6 +40,8 @@ PODS: - DKImagePickerController/PhotoGallery - Flutter - Flutter (1.0.0) + - image_picker_ios (0.0.1): + - Flutter - metadata_god (0.0.1) - network_info_plus (0.0.1): - Flutter @@ -69,6 +71,7 @@ DEPENDENCIES: - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - metadata_god (from `.symlinks/plugins/metadata_god/ios`) - network_info_plus (from `.symlinks/plugins/network_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) @@ -96,6 +99,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" metadata_god: :path: ".symlinks/plugins/metadata_god/ios" network_info_plus: @@ -121,6 +126,7 @@ SPEC CHECKSUMS: DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 metadata_god: eceae399d0020475069a5cebc35943ce8562b5d7 network_info_plus: 9d930145451916919786087c4173226363616071 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 diff --git a/lib/audio/nplayer.dart b/lib/audio/nplayer.dart index f592df7..9cbcc0a 100644 --- a/lib/audio/nplayer.dart +++ b/lib/audio/nplayer.dart @@ -114,6 +114,7 @@ class NPlayer extends ChangeNotifier { List get playlists => PlaylistManager.playlistNames; + NServer? _server; NServer? get server => _server; @@ -521,6 +522,16 @@ class NPlayer extends ChangeNotifier { } // SECTION: Song Loading and Management + + Future setPlaylistImage(String playlistName, File imageFile) async { + await PlaylistManager.setPlaylistImage(playlistName, imageFile); + notifyListeners(); + } + + String? getPlaylistImagePath(String playlistName) { + return PlaylistManager.getPlaylistImagePath(playlistName); + } + Future reloadSongs() async { _log("Reloading songs"); _allSongs.clear(); diff --git a/lib/audio/nplayer_widget.dart b/lib/audio/nplayer_widget.dart index ee64d2e..767fa27 100644 --- a/lib/audio/nplayer_widget.dart +++ b/lib/audio/nplayer_widget.dart @@ -15,6 +15,7 @@ class NPlayerWidget extends StatefulWidget { class _NPlayerWidgetState extends State { bool _isPlayerExpanded = false; + double _swipeOffset = 0.0; Widget _buildMarqueeText(String text) { return LayoutBuilder( @@ -253,56 +254,79 @@ IconButton( return const SizedBox.shrink(); } - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, player, child) { - final currentSong = player.getCurrentSong(); - if (currentSong == null) { - return const SizedBox.shrink(); - } +@override +Widget build(BuildContext context) { + return Consumer( + builder: (context, player, child) { + final currentSong = player.getCurrentSong(); + if (currentSong == null) { + return const SizedBox.shrink(); + } - return GestureDetector( - onVerticalDragEnd: (details) { - if (details.primaryVelocity! < 0) { - // Swipe up - setState(() => _isPlayerExpanded = true); - } else if (details.primaryVelocity! > 0) { - // Swipe down - setState(() => _isPlayerExpanded = false); + return GestureDetector( + onVerticalDragEnd: (details) { + if (details.primaryVelocity! < 0) { + // Swipe up + setState(() => _isPlayerExpanded = true); + } else if (details.primaryVelocity! > 0) { + // Swipe down + setState(() => _isPlayerExpanded = false); + } + }, + onHorizontalDragUpdate: (details) { + setState(() { + _swipeOffset += details.delta.dx; + _swipeOffset = _swipeOffset.clamp(-100.0, 100.0); + }); + }, + onHorizontalDragEnd: (details) { + if (_swipeOffset.abs() > 50) { + if (_swipeOffset > 0) { + player.previousSong(); + } else { + player.nextSong(); } - }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 250), - height: _isPlayerExpanded ? MediaQuery.of(context).size.height * 0.55 : 70, - margin: const EdgeInsets.symmetric(horizontal: 16.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16.0), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 10, - offset: const Offset(0, 5), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(16.0), - child: Stack( - children: [ - _buildBackgroundImage(player), - _isPlayerExpanded - ? _buildExpandedView(player) - : Container( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: _buildSongInfo(player), - ), - ], + } + setState(() { + _swipeOffset = 0; + }); + }, + onLongPress: () { + player.togglePlayPause(); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOutCubic, + transform: Matrix4.translationValues(_swipeOffset, 0, 0), + height: _isPlayerExpanded ? MediaQuery.of(context).size.height * 0.55 : 70, + margin: const EdgeInsets.symmetric(horizontal: 16.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16.0), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 5), ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16.0), + child: Stack( + children: [ + _buildBackgroundImage(player), + _isPlayerExpanded + ? _buildExpandedView(player) + : Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: _buildSongInfo(player), + ), + ], ), ), - ); - }, - ); - } + ), + ); + }, + ); +} } \ No newline at end of file diff --git a/lib/audio/nplaylist.dart b/lib/audio/nplaylist.dart index 7ef30bd..ad4c5b8 100644 --- a/lib/audio/nplaylist.dart +++ b/lib/audio/nplaylist.dart @@ -5,16 +5,21 @@ import 'package:blossom/tools/settings.dart'; class PlaylistManager { static const String _playlistFileName = 'playlists.json'; - static Map> _playlists = {}; + static Map> _playlists = {}; + static late String _playlistArtDir; static Future load() async { final songDir = await Settings.getSongDir(); final file = File(path.join(songDir, _playlistFileName)); + _playlistArtDir = path.join(songDir, 'playlistArt'); + + // Create playlistArt directory if it doesn't exist + await Directory(_playlistArtDir).create(recursive: true); if (await file.exists()) { final content = await file.readAsString(); final json = jsonDecode(content) as Map; - _playlists = json.map((key, value) => MapEntry(key, List.from(value))); + _playlists = json.map((key, value) => MapEntry(key, Map.from(value))); } } @@ -27,17 +32,25 @@ class PlaylistManager { static List get playlistNames => _playlists.keys.toList(); static List getPlaylistSongs(String playlistName) { - return _playlists[playlistName] ?? []; + return List.from(_playlists[playlistName]?['songs'] ?? []); + } + + static String? getPlaylistImagePath(String playlistName) { + return _playlists[playlistName]?['imagePath']; } - static Future createPlaylist(String name) async { + static Future createPlaylist(String name, {String? imagePath}) async { if (!_playlists.containsKey(name)) { - _playlists[name] = []; + _playlists[name] = {'songs': [], 'imagePath': imagePath}; await save(); } } static Future deletePlaylist(String name) async { + String? imagePath = _playlists[name]?['imagePath']; + if (imagePath != null) { + await File(imagePath).delete(); + } _playlists.remove(name); await save(); } @@ -46,16 +59,23 @@ class PlaylistManager { if (!_playlists.containsKey(playlistName)) { await createPlaylist(playlistName); } - if (!_playlists[playlistName]!.contains(songName)) { - _playlists[playlistName]!.add(songName); + if (!_playlists[playlistName]!['songs'].contains(songName)) { + _playlists[playlistName]!['songs'].add(songName); await save(); } } static Future removeSongFromPlaylist(String playlistName, String songName) async { if (_playlists.containsKey(playlistName)) { - _playlists[playlistName]!.remove(songName); + _playlists[playlistName]!['songs'].remove(songName); await save(); } } + + static Future setPlaylistImage(String playlistName, File imageFile) async { + String newImagePath = path.join(_playlistArtDir, '$playlistName${path.extension(imageFile.path)}'); + await imageFile.copy(newImagePath); + _playlists[playlistName]!['imagePath'] = newImagePath; + await save(); + } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 14b7944..b33d500 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -127,6 +127,7 @@ class _MainStructureState extends State with SingleTickerProviderStateMixin { int _currentIndex = 0; late PageController _pageController; + final bool enableTesting = false; @override void initState() { @@ -144,45 +145,48 @@ class _MainStructureState extends State setState(() => _currentIndex = index); } - List _getPages() { - List pages = [ - const SongLibrary(), - const PlaylistPage(), - const SongAlbums(), - const ArtistsPage(), - const ServerPage(), // Add the new page - ]; +List _getPages() { + List pages = [ + const SongLibrary(), + const PlaylistPage(), + const SongAlbums(), + const ArtistsPage(), + ]; - if (!kIsWeb && - (Platform.isWindows || Platform.isMacOS || Platform.isLinux)) { - pages.add(const Downloader()); - } + if (enableTesting) { + pages.add(const ServerPage()); + } - return pages; + if (!kIsWeb && + (Platform.isWindows || Platform.isMacOS || Platform.isLinux)) { + pages.add(const Downloader()); } - String _getAppBarTitle() { - switch (_currentIndex) { - case 0: - return 'Song Library'; - case 1: - return 'Playlists'; - case 2: - return 'Albums'; - case 3: - return 'Artists'; - case 4: - return 'Stream'; - case 5: - return 'Downloader'; - default: - return 'Blossom'; - } + return pages; +} + +String _getAppBarTitle() { + switch (_currentIndex) { + case 0: + return 'Song Library'; + case 1: + return 'Playlists'; + case 2: + return 'Albums'; + case 3: + return 'Artists'; + case 4: + return enableTesting ? 'Stream' : 'Downloader'; + case 5: + return 'Downloader'; + default: + return 'Blossom'; } +} double _getPlayerBottomPosition() { if (Platform.isAndroid || Platform.isIOS) { - return 45; + return 10; } else { return 0; } @@ -193,7 +197,6 @@ class _MainStructureState extends State final pages = _getPages(); final isDesktop = !kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux); - final enableTesting = false; return Scaffold( appBar: !Platform.isIOS && !Platform.isAndroid @@ -233,33 +236,34 @@ class _MainStructureState extends State setState(() => _currentIndex = index); _pageController.jumpToPage(index); }, - items: [ - const BottomNavigationBarItem( - icon: Icon(Icons.library_music), - label: 'Library', - ), - const BottomNavigationBarItem( - icon: Icon(Icons.playlist_play), - label: 'Playlists', - ), - const BottomNavigationBarItem( - icon: Icon(Icons.album), - label: 'Albums', - ), - const BottomNavigationBarItem( - icon: Icon(Icons.person), - label: 'Artists', - ), - const BottomNavigationBarItem( - icon: Icon(Icons.wifi), - label: 'Server Scan', - ), - if (isDesktop) - const BottomNavigationBarItem( - icon: Icon(Icons.download), - label: 'Downloader', - ), - ], + items: [ + const BottomNavigationBarItem( + icon: Icon(Icons.library_music), + label: 'Library', + ), + const BottomNavigationBarItem( + icon: Icon(Icons.playlist_play), + label: 'Playlists', + ), + const BottomNavigationBarItem( + icon: Icon(Icons.album), + label: 'Albums', + ), + const BottomNavigationBarItem( + icon: Icon(Icons.person), + label: 'Artists', + ), + if (enableTesting) + const BottomNavigationBarItem( + icon: Icon(Icons.wifi), + label: 'Server Scan', + ), + if (isDesktop) + const BottomNavigationBarItem( + icon: Icon(Icons.download), + label: 'Downloader', + ), + ], type: BottomNavigationBarType.fixed, backgroundColor: Theme.of(context).scaffoldBackgroundColor, selectedItemColor: Theme.of(context).colorScheme.secondary, diff --git a/lib/pages/albums_page.dart b/lib/pages/albums_page.dart index a7bd896..b11a4d8 100644 --- a/lib/pages/albums_page.dart +++ b/lib/pages/albums_page.dart @@ -82,13 +82,6 @@ class _SongAlbumsState extends State { }, ), actions: [ - IconButton( - icon: const Icon(Icons.refresh_rounded), - tooltip: 'Refresh Library', - onPressed: () { - _initializeAlbumList(); - }, - ), PopupMenuButton( icon: const Icon(Icons.sort), tooltip: 'Sort by', diff --git a/lib/pages/artists_page.dart b/lib/pages/artists_page.dart index 95fe701..728083b 100644 --- a/lib/pages/artists_page.dart +++ b/lib/pages/artists_page.dart @@ -79,13 +79,6 @@ class _ArtistsPageState extends State { }, ), actions: [ - IconButton( - icon: const Icon(Icons.refresh_rounded), - tooltip: 'Refresh Library', - onPressed: () { - _initializeArtistList(); - }, - ), PopupMenuButton( icon: const Icon(Icons.sort), tooltip: 'Sort by', diff --git a/lib/pages/library_page.dart b/lib/pages/library_page.dart index c2c95c8..f2271cc 100644 --- a/lib/pages/library_page.dart +++ b/lib/pages/library_page.dart @@ -81,15 +81,9 @@ class _SongLibraryState extends State { padding: EdgeInsets.symmetric(horizontal: 10), child: GestureDetector( behavior: HitTestBehavior.translucent, - child: Scrollbar( - thumbVisibility: true, - interactive: true, - thickness: 8, - radius: const Radius.circular(4), - child: SongListBuilder( + child: SongListBuilder( songs: player.sortedSongs, ), - ), ), ), ); diff --git a/lib/pages/playlist_page.dart b/lib/pages/playlist_page.dart index f230b8c..58f5bd8 100644 --- a/lib/pages/playlist_page.dart +++ b/lib/pages/playlist_page.dart @@ -1,4 +1,7 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:provider/provider.dart'; import 'package:blossom/audio/nplayer.dart'; import 'package:blossom/sheets/bottom_sheet.dart'; @@ -13,6 +16,17 @@ class PlaylistPage extends StatefulWidget { class _PlaylistPageState extends State { String _searchQuery = ''; + + Future _selectPlaylistImage(BuildContext context, NPlayer player, String playlist) async { + final ImagePicker _picker = ImagePicker(); + final XFile? image = await _picker.pickImage(source: ImageSource.gallery); + + if (image != null) { + File imageFile = File(image.path); + await player.setPlaylistImage(playlist, imageFile); + } + } + @override Widget build(BuildContext context) { return Consumer( @@ -56,19 +70,22 @@ class _PlaylistPageState extends State { interactive: true, thickness: 8, radius: const Radius.circular(4), - child: ListView.builder( + child: ListView.builder( itemCount: filteredPlaylists.length, itemBuilder: (context, index) { String playlist = filteredPlaylists[index]; List playlistSongs = player.getPlaylistSongs(playlist); + String? imagePath = player.getPlaylistImagePath(playlist); return _PlaylistListTile( playlist: playlist, songCount: playlistSongs.length, + imagePath: imagePath, onTap: () { _showPlaylistBottomSheet(context, player, playlist, playlistSongs); }, onPlay: () => player.playPlaylistFromIndex(playlistSongs, 0), onDelete: () => _showDeletePlaylistDialog(context, player, playlist), + onImageTap: () => _selectPlaylistImage(context, player, playlist), ); }, ), @@ -86,6 +103,7 @@ void _showPlaylistBottomSheet(BuildContext context, NPlayer player, String playl isScrollControlled: true, backgroundColor: Colors.transparent, builder: (BuildContext context) { + String? playlistImagePath = player.getPlaylistImagePath(playlistName); return MusicBottomSheet( title: playlistName, subtitle: '${songs.length} songs', @@ -96,9 +114,11 @@ void _showPlaylistBottomSheet(BuildContext context, NPlayer player, String playl player.playPlaylistFromIndex(songs, index); Navigator.pop(context); }, - image: songs.isNotEmpty && songs.first.picture != null + image: playlistImagePath != null + ? Image.file(File(playlistImagePath), fit: BoxFit.cover) + : (songs.isNotEmpty && songs.first.picture != null ? Image.memory(songs.first.picture!, fit: BoxFit.cover) - : null, + : null), isPlaylist: true, ); }, @@ -170,17 +190,21 @@ void _showPlaylistBottomSheet(BuildContext context, NPlayer player, String playl class _PlaylistListTile extends StatelessWidget { final String playlist; final int songCount; + final String? imagePath; final VoidCallback onTap; final VoidCallback onPlay; final VoidCallback onDelete; + final VoidCallback onImageTap; const _PlaylistListTile({ Key? key, required this.playlist, required this.songCount, + this.imagePath, required this.onTap, required this.onPlay, required this.onDelete, + required this.onImageTap, }) : super(key: key); @override @@ -193,14 +217,19 @@ class _PlaylistListTile extends StatelessWidget { ), margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: ListTile( - leading: ClipRRect( - borderRadius: BorderRadius.circular(4), - child: SizedBox( - width: 48, - height: 48, - child: Container( - color: Colors.grey[800], - child: Icon(Icons.playlist_play, color: Colors.grey[600]), + leading: GestureDetector( + onTap: onImageTap, + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: SizedBox( + width: 48, + height: 48, + child: imagePath != null + ? Image.file(File(imagePath!), fit: BoxFit.cover) + : Container( + color: Colors.grey[800], + child: Icon(Icons.playlist_play, color: Colors.grey[600]), + ), ), ), ), diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 33e8050..332d1f0 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -41,7 +41,6 @@ class SettingsPage extends StatelessWidget { ); } } - @override Widget build(BuildContext context) { return Scaffold( @@ -56,7 +55,7 @@ class SettingsPage extends StatelessWidget { _buildSection( 'Music Library', [ - _buildInfoTile('Add songs to your library', 'Put files into the Blossom folder'), + _buildInfoTile('To add songs to your library', 'Put files into the Blossom folder in files'), _buildButton( 'Copy Files to Blossom Folder', () => _copyFilesToBlossomFolder(context), @@ -90,38 +89,6 @@ class SettingsPage extends StatelessWidget { ), ], ), - _buildSection( - 'Sorting Preferences', - [ -_buildDropdownTile( - 'Album Sort By', - Settings.albumSortBy, - ['name', 'year', 'artist'], - (String value) => Settings.setAlbumSort(value, Settings.albumSortAscending, Settings.albumOrganizeByFolder), -), - _buildSwitchTile( - 'Artist Sort Order', - Settings.artistSortAscending, - (bool value) => Settings.setArtistSort(Settings.artistSortBy, value), - ), - _buildDropdownTile( - 'Album Sort By', - Settings.albumSortBy, - ['name', 'year', 'artist'], - (String value) => Settings.setAlbumSort(value, Settings.albumSortAscending, Settings.albumOrganizeByFolder), - ), - _buildSwitchTile( - 'Album Sort Order', - Settings.albumSortAscending, - (bool value) => Settings.setAlbumSort(Settings.albumSortBy, value, Settings.albumOrganizeByFolder), - ), - _buildSwitchTile( - 'Organize Albums by Folder', - Settings.albumOrganizeByFolder, - (bool value) => Settings.setAlbumSort(Settings.albumSortBy, Settings.albumSortAscending, value), - ), - ], - ), ], ); }, @@ -129,6 +96,7 @@ _buildDropdownTile( ); } + Widget _buildSection(String title, List children) { return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/sheets/playing_sheet.dart b/lib/sheets/playing_sheet.dart index 7f426b3..8d1c697 100644 --- a/lib/sheets/playing_sheet.dart +++ b/lib/sheets/playing_sheet.dart @@ -1,11 +1,40 @@ +import 'package:blossom/sheets/bottom_sheet.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../audio/nplayer.dart'; import '../custom/custom_song_list_builder.dart'; -class PlayingSongsSheet extends StatelessWidget { +class PlayingSongsSheet extends StatefulWidget { const PlayingSongsSheet({Key? key}) : super(key: key); + @override + _PlayingSongsSheetState createState() => _PlayingSongsSheetState(); +} + +class _PlayingSongsSheetState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _animation = CurvedAnimation( + parent: _controller, + curve: Curves.easeOut, + ); + _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Consumer( @@ -20,39 +49,74 @@ class PlayingSongsSheet extends StatelessWidget { (total, song) => total + Duration(milliseconds: song.duration), ); - return Container( - height: MediaQuery.of(context).size.height * 0.6, - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.5), - blurRadius: 10, - spreadRadius: 5, + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Transform.translate( + offset: Offset(0, (1 - _animation.value) * 100), + child: Opacity( + opacity: _animation.value, + child: child, ), - ], - ), - child: Column( - children: [ - _buildHeader(context, player, currentSong), - _buildStats(context, player, totalDuration), - const SizedBox(height: 16), - Expanded( - child: SongListBuilder( - songs: player.playingSongs, - isPlayingList: true, - onTap: (song) => player.playSpecificSong(song), - isPlaylist: false, - ), + ); + }, + child: Container( + height: MediaQuery.of(context).size.height * 0.8, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).scaffoldBackgroundColor, + Theme.of(context).scaffoldBackgroundColor.withOpacity(0.8), + ], ), - ], + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.5), + blurRadius: 10, + spreadRadius: 5, + ), + ], + ), + child: Column( + children: [ + Container( + width: 40, + height: 5, + margin: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: Colors.grey[400], + borderRadius: BorderRadius.circular(2.5), + ), + ), + _buildHeader(context, player, currentSong), + _buildStats(context, player, totalDuration), + const SizedBox(height: 16), + Expanded( + child: ScrollConfiguration( + behavior: DesktopScrollBehavior(), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SongListBuilder( + songs: player.playingSongs, + isPlayingList: true, + onTap: (song) => player.playSpecificSong(song), + isPlaylist: false, + ), + ), + ), + ), + ], + ), ), ); }, ); } + Widget _buildHeader(BuildContext context, NPlayer player, Music currentSong) { return Padding( padding: const EdgeInsets.all(16.0), @@ -92,16 +156,17 @@ class PlayingSongsSheet extends StatelessWidget { ), ), IconButton( - icon: const Icon(Icons.play_circle_fill_rounded), - onPressed: () => player.togglePlayPause(), - color: Theme.of(context).colorScheme.secondary, - iconSize: 48, + icon: const Icon(Icons.shuffle_rounded), + onPressed: () => player.shuffle(), + iconSize: 32, ), + SizedBox(width: 16), ], ), ); } + Widget _buildStats(BuildContext context, NPlayer player, Duration totalDuration) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 794e81f..b6bb4c2 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -15,6 +16,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar); + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); g_autoptr(FlPluginRegistrar) screen_retriever_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 535d4e0..9293fa0 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_linux + file_selector_linux screen_retriever url_launcher_linux window_manager diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b4c6fa3..a8f02da 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import audio_service import audio_session import audioplayers_darwin +import file_selector_macos import network_info_plus import path_provider_foundation import screen_retriever @@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 7488a1b..2ad8e48 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -265,6 +265,38 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.2" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" flutter: dependency: "direct main" description: flutter @@ -352,6 +384,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.0" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: c0a6763d50b354793d0192afd0a12560b823147d3ded7c6b77daf658fa05cc85 + url: "https://pub.dev" + source: hosted + version: "0.8.12+13" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" + url: "https://pub.dev" + source: hosted + version: "0.8.12" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" intl: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 6d49fb6..6b3dc78 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,7 @@ dependencies: ticker_text: ^2.1.0 flutter_svg: ^2.0.10+1 network_info_plus: ^6.0.1 + image_picker: ^1.1.2 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index af112e4..a9de7dd 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -15,6 +16,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { AudioplayersWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); ScreenRetrieverPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 7e2c06c..bac1134 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_windows + file_selector_windows permission_handler_windows screen_retriever url_launcher_windows