Implementation for a MAUI audio player app with song management, favorites, and playlist features.
1. Set Up the Project
First, create a new .NET MAUI project and add these NuGet packages:
Plugin.Maui.Audio (for audio playback)
CommunityToolkit.Mvvm (for MVVM helpers)
2. Model Classes
// Models/Song.cs public class Song { public string Id { get; set; } = Guid.NewGuid().ToString(); public string Title { get; set; } public string Artist { get; set; } public string Album { get; set; } public string FilePath { get; set; } public TimeSpan Duration { get; set; } public bool IsFavorite { get; set; } public DateTime DateAdded { get; set; } = DateTime.Now; } // Models/Playlist.cs public class Playlist { public string Id { get; set; } = Guid.NewGuid().ToString(); public string Name { get; set; } public List<string> SongIds { get; set; } = new(); public DateTime CreatedDate { get; set; } = DateTime.Now; }
3. ViewModel
// ViewModels/PlayerViewModel.cs public partial class PlayerViewModel : ObservableObject { private readonly IAudioManager audioManager; private IAudioPlayer player; [ObservableProperty] private Song currentSong; [ObservableProperty] private bool isPlaying; [ObservableProperty] private TimeSpan currentPosition; [ObservableProperty] private double playbackProgress; [ObservableProperty] private ObservableCollection<Song> songs = new(); [ObservableProperty] private ObservableCollection<Playlist> playlists = new(); [ObservableProperty] private ObservableCollection<Song> favorites = new(); public PlayerViewModel(IAudioManager audioManager) { this.audioManager = audioManager; // Load data from preferences LoadSongs(); LoadPlaylists(); LoadFavorites(); // Timer to update progress Device.StartTimer(TimeSpan.FromMilliseconds(500), () => { if (player != null && IsPlaying) { CurrentPosition = player.CurrentPosition; PlaybackProgress = player.CurrentPosition.TotalMilliseconds / player.Duration.TotalMilliseconds; } return true; }); } [RelayCommand] private async Task AddSongs() { try { var options = new PickOptions { PickerTitle = "Please select audio files", FileTypes = new FilePickerFileType( new Dictionary<DevicePlatform, IEnumerable<string>> { { DevicePlatform.WinUI, new[] { ".mp3", ".wav", ".m4a" } }, { DevicePlatform.Android, new[] { "audio/*" } }, { DevicePlatform.iOS, new[] { "public.audio" } }, { DevicePlatform.MacCatalyst, new[] { "public.audio" } } }) }; var results = await FilePicker.Default.PickMultipleAsync(options); if (results != null) { foreach (var result in results) { var song = new Song { Title = result.FileName, FilePath = result.FullPath, // You could use a library like TagLib# to extract metadata Artist = "Unknown Artist", Album = "Unknown Album" }; // Create player to get duration var tempPlayer = audioManager.CreatePlayer(await result.OpenReadAsync()); song.Duration = tempPlayer.Duration; tempPlayer.Dispose(); Songs.Add(song); } SaveSongs(); } } catch (Exception ex) { await Shell.Current.DisplayAlert("Error", ex.Message, "OK"); } } [RelayCommand] private async Task PlaySong(Song song) { if (song == null) return; if (player != null) { player.Dispose(); } try { var file = await FileSystem.OpenAppPackageFileAsync(song.FilePath); player = audioManager.CreatePlayer(file); CurrentSong = song; player.Play(); IsPlaying = true; player.PlaybackEnded += (s, e) => { IsPlaying = false; PlaybackProgress = 0; CurrentPosition = TimeSpan.Zero; }; } catch (Exception ex) { await Shell.Current.DisplayAlert("Error", ex.Message, "OK"); } } [RelayCommand] private void TogglePlayPause() { if (player == null) return; if (IsPlaying) { player.Pause(); } else { player.Play(); } IsPlaying = !IsPlaying; } [RelayCommand] private void Seek(double value) { if (player != null) { var newPosition = value * player.Duration.TotalMilliseconds; player.Seek(TimeSpan.FromMilliseconds(newPosition)); } } [RelayCommand] private void ToggleFavorite(Song song) { song.IsFavorite = !song.IsFavorite; if (song.IsFavorite && !Favorites.Contains(song)) { Favorites.Add(song); } else if (!song.IsFavorite && Favorites.Contains(song)) { Favorites.Remove(song); } SaveFavorites(); SaveSongs(); } [RelayCommand] private async Task CreatePlaylist() { var name = await Shell.Current.DisplayPromptAsync("New Playlist", "Enter playlist name:"); if (!string.IsNullOrWhiteSpace(name)) { var playlist = new Playlist { Name = name }; Playlists.Add(playlist); SavePlaylists(); } } [RelayCommand] private async Task AddToPlaylist(Playlist playlist) { if (CurrentSong == null) return; if (!playlist.SongIds.Contains(CurrentSong.Id)) { playlist.SongIds.Add(CurrentSong.Id); SavePlaylists(); await Shell.Current.DisplayAlert("Success", $"Added to {playlist.Name}", "OK"); } else { await Shell.Current.DisplayAlert("Info", "Song already in playlist", "OK"); } } [RelayCommand] private async Task ViewPlaylistSongs(Playlist playlist) { var songsInPlaylist = Songs.Where(s => playlist.SongIds.Contains(s.Id)).ToList(); await Shell.Current.DisplayAlert(playlist.Name, string.Join("\n", songsInPlaylist.Select(s => s.Title)), "OK"); } private void LoadSongs() { var songsJson = Preferences.Get("UserSongs", string.Empty); if (!string.IsNullOrEmpty(songsJson)) { var songs = JsonSerializer.Deserialize<List<Song>>(songsJson); Songs = new ObservableCollection<Song>(songs); } } private void SaveSongs() { var songsJson = JsonSerializer.Serialize(Songs.ToList()); Preferences.Set("UserSongs", songsJson); } private void LoadPlaylists() { var playlistsJson = Preferences.Get("UserPlaylists", string.Empty); if (!string.IsNullOrEmpty(playlistsJson)) { var playlists = JsonSerializer.Deserialize<List<Playlist>>(playlistsJson); Playlists = new ObservableCollection<Playlist>(playlists); } } private void SavePlaylists() { var playlistsJson = JsonSerializer.Serialize(Playlists.ToList()); Preferences.Set("UserPlaylists", playlistsJson); } private void LoadFavorites() { var favoritesJson = Preferences.Get("UserFavorites", string.Empty); if (!string.IsNullOrEmpty(favoritesJson)) { var favorites = JsonSerializer.Deserialize<List<Song>>(favoritesJson); Favorites = new ObservableCollection<Song>(favorites); } } private void SaveFavorites() { var favoritesJson = JsonSerializer.Serialize(Favorites.ToList()); Preferences.Set("UserFavorites", favoritesJson); } }
4. Main Page (UI)
<!-- MainPage.xaml --> <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:vm="clr-namespace:YourNamespace.ViewModels" x:Class="YourNamespace.MainPage" Title="MAUI Music Player"> <ContentPage.BindingContext> <vm:PlayerViewModel /> </ContentPage.BindingContext> <Grid RowDefinitions="*,Auto,Auto" Padding="10"> <!-- Song List --> <RefreshView Grid.Row="0" Command="{Binding LoadSongsCommand}" IsRefreshing="{Binding IsBusy, Mode=TwoWay}"> <CollectionView ItemsSource="{Binding Songs}" SelectionMode="Single" SelectedItem="{Binding CurrentSong}"> <CollectionView.ItemTemplate> <DataTemplate> <Grid Padding="10" ColumnDefinitions="Auto,*,Auto"> <Image Grid.Column="0" Source="{OnPlatform Default='music_note', iOS='music_note_ios'}" WidthRequest="40" HeightRequest="40" /> <VerticalStackLayout Grid.Column="1" Padding="10,0"> <Label Text="{Binding Title}" FontSize="16" /> <Label Text="{Binding Artist}" FontSize="14" TextColor="Gray" /> </VerticalStackLayout> <Button Grid.Column="2" Text="{Binding IsFavorite, Converter={StaticResource FavoriteConverter}}" Command="{Binding Source={RelativeSource AncestorType={x:Type vm:PlayerViewModel}}, Path=ToggleFavoriteCommand}" CommandParameter="{Binding .}" BackgroundColor="Transparent" FontSize="20" /> </Grid> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> </RefreshView> <!-- Player Controls --> <Grid Grid.Row="1" Padding="10" IsVisible="{Binding CurrentSong, Converter={StaticResource NullToBoolConverter}}"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Label Grid.Row="0" Text="{Binding CurrentSong.Title}" FontSize="18" FontAttributes="Bold" /> <Label Grid.Row="0" HorizontalOptions="End" Text="{Binding CurrentSong.Artist}" /> <Slider Grid.Row="1" Minimum="0" Maximum="1" Value="{Binding PlaybackProgress}" ValueChanged="Slider_ValueChanged" DragCompletedCommand="{Binding SeekCommand}" DragCompletedCommandParameter="{Binding Source={RelativeSource Self}, Path=Value}" /> <Grid Grid.Row="2" ColumnDefinitions="*,Auto,Auto,Auto,*"> <Label Grid.Column="1" Text="{Binding CurrentPosition, StringFormat='{0:mm\\:ss}'}" /> <Label Grid.Column="3" Text="{Binding CurrentSong.Duration, StringFormat='{0:mm\\:ss}'}" /> <Button Grid.Column="2" Text="{Binding IsPlaying, Converter={StaticResource PlayPauseConverter}}" Command="{Binding TogglePlayPauseCommand}" FontSize="24" /> </Grid> </Grid> <!-- Bottom Menu --> <Grid Grid.Row="2" ColumnDefinitions="*,*,*,*" Padding="10"> <Button Grid.Column="0" Text="Add Songs" Command="{Binding AddSongsCommand}" /> <Button Grid.Column="1" Text="Favorites" Command="{Binding ViewFavoritesCommand}" /> <Button Grid.Column="2" Text="Playlists" Command="{Binding ViewPlaylistsCommand}" /> <Button Grid.Column="3" Text="Add to Playlist" Command="{Binding AddToPlaylistCommand}" /> </Grid> </Grid> </ContentPage>
5. Converters and Helpers
// Converters/FavoriteConverter.cs public class FavoriteConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return (bool)value ? "❤️" : "♡"; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } // Converters/PlayPauseConverter.cs public class PlayPauseConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return (bool)value ? "⏸" : "▶"; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } // Converters/NullToBoolConverter.cs public class NullToBoolConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return value != null; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
6. Register Services
In MauiProgram.cs:
builder.Services.AddSingleton(AudioManager.Current); builder.Services.AddSingleton<PlayerViewModel>(); builder.Services.AddSingleton<MainPage>();
7. Additional Features to Consider
Metadata Extraction: Use TagLib# to read ID3 tags from MP3 files for better song information.
Background Playback: Implement background service for continuous playback when app is minimized.
Visualizations: Add audio visualizations using SkiaSharp.
Dark/Light Theme: Support for theme switching.
Search Functionality: Add search bar to filter songs.
Shuffle/Repeat: Implement playback modes.
Cloud Sync: Add support for syncing with cloud storage.
8. Platform-Specific Considerations
Android: Request runtime permissions for file access
iOS: Handle audio session interruptions
Windows: Implement system media transport controls
This implementation provides a complete foundation for a MAUI audio player with all the requested features. You can extend it further based on your specific requirements.
No comments:
Post a Comment