diff --git a/Harmonia.Core/Player/AudioPlayer.cs b/Harmonia.Core/Player/AudioPlayer.cs index 0643310..bf8e3b8 100644 --- a/Harmonia.Core/Player/AudioPlayer.cs +++ b/Harmonia.Core/Player/AudioPlayer.cs @@ -173,7 +173,7 @@ public class AudioPlayer : IAudioPlayer return; } - int currentIndex = Playlist.Songs.IndexOf(CurrentPlaylistSong); + int currentIndex = Playlist.IndexOf(CurrentPlaylistSong); int nextIndex = currentIndex + 1; if (nextIndex > Playlist.Songs.Count - 1) @@ -200,7 +200,7 @@ public class AudioPlayer : IAudioPlayer return; } - int currentIndex = Playlist.Songs.IndexOf(CurrentPlaylistSong); + int currentIndex = Playlist.IndexOf(CurrentPlaylistSong); int nextIndex = currentIndex - 1; if (nextIndex < 0) @@ -233,7 +233,7 @@ public class AudioPlayer : IAudioPlayer { if (Playlist == null || Playlist.Songs.Contains(song) == false) { - Playlist? newPlaylist = _playlistManager.GetPlaylist(song); + Playlist? newPlaylist = _playlistManager.FindPlaylistContaining(song); if (newPlaylist == null) return false; diff --git a/Harmonia.Core/Playlists/IPlaylistManager.cs b/Harmonia.Core/Playlists/IPlaylistManager.cs index 5b46de3..74cef68 100644 --- a/Harmonia.Core/Playlists/IPlaylistManager.cs +++ b/Harmonia.Core/Playlists/IPlaylistManager.cs @@ -8,7 +8,7 @@ public interface IPlaylistManager Task InitializeAsync(); Task AddPlaylistAsync(); void RemovePlaylist(Playlist playlist); - Playlist? GetPlaylist(PlaylistSong playlistSong); + Playlist? FindPlaylistContaining(PlaylistSong playlistSong); event EventHandler? CurrentPlaylistChanged; event EventHandler PlaylistAdded; diff --git a/Harmonia.Core/Playlists/IPlaylistRepository.cs b/Harmonia.Core/Playlists/IPlaylistRepository.cs index 724881f..8d930c0 100644 --- a/Harmonia.Core/Playlists/IPlaylistRepository.cs +++ b/Harmonia.Core/Playlists/IPlaylistRepository.cs @@ -4,10 +4,5 @@ namespace Harmonia.Core.Playlists; public interface IPlaylistRepository : IRepository { - Playlist? GetPlaylist(PlaylistSong playlistSong); - //void AddPlaylist(); - //void RemovePlaylist(Playlist playlist); - //event EventHandler PlaylistAdded; - //event EventHandler PlaylistRemoved; } \ No newline at end of file diff --git a/Harmonia.Core/Playlists/Playlist.cs b/Harmonia.Core/Playlists/Playlist.cs index 45aaa32..2f44fc5 100644 --- a/Harmonia.Core/Playlists/Playlist.cs +++ b/Harmonia.Core/Playlists/Playlist.cs @@ -5,15 +5,48 @@ namespace Harmonia.Core.Playlists; public class Playlist { + private readonly List _songs = []; + private readonly List _groupOptions = []; + private readonly List _sortOptions = []; + public string UID { get; init; } = Guid.NewGuid().ToString(); - public string? Name { get; set; } - public List Songs { get; init; } = []; // TODO: Change to "private init" once deserialization is fixed - public List GroupOptions { get; set; } = []; - public List SortOptions { get; set; } = []; - public bool IsLocked { get; set; } + public string? Name { get; private set; } + public IReadOnlyList Songs => _songs; + public IReadOnlyList GroupOptions => _groupOptions; + public IReadOnlyList SortOptions => _sortOptions; + public bool IsLocked { get; private set; } public event EventHandler? PlaylistUpdated; + public Playlist() + { + } + + public Playlist(string? name) + { + Name = name; + } + + internal static Playlist Restore(string uid, string? name, IEnumerable songs, IEnumerable groupOptions, IEnumerable sortOptions, bool isLocked) + { + Playlist playlist = new(name) + { + UID = uid, + IsLocked = isLocked + }; + + playlist._songs.AddRange(songs); + playlist._groupOptions.AddRange(groupOptions); + playlist._sortOptions.AddRange(sortOptions); + + return playlist; + } + + public int IndexOf(PlaylistSong playlistSong) + { + return _songs.IndexOf(playlistSong); + } + public void SetName(string name) { if (string.Equals(Name, name, StringComparison.Ordinal)) @@ -88,9 +121,9 @@ public class Playlist if (playlistSongs.Length == 0) return; - int insertIndex = index ?? Songs.Count; + int insertIndex = index ?? _songs.Count; - Songs.InsertRange(insertIndex, playlistSongs); + _songs.InsertRange(insertIndex, playlistSongs); PlaylistUpdatedEventArgs eventArgs = new() { @@ -105,7 +138,7 @@ public class Playlist public void MoveSong(PlaylistSong playlistSong, int newIndex) { - int currentIndex = Songs.IndexOf(playlistSong); + int currentIndex = _songs.IndexOf(playlistSong); MoveSong(currentIndex, newIndex); } @@ -118,10 +151,10 @@ public class Playlist if (oldIndex == newIndex) return; - PlaylistSong playlistSong = Songs[oldIndex]; + PlaylistSong playlistSong = _songs[oldIndex]; - Songs.Remove(playlistSong); - Songs.Insert(newIndex, playlistSong); + _songs.Remove(playlistSong); + _songs.Insert(newIndex, playlistSong); PlaylistUpdatedEventArgs eventArgs = new() { @@ -141,8 +174,8 @@ public class Playlist return; Dictionary oldPlaylistSongs = playlistSongs - .OrderBy(Songs.IndexOf) - .ToDictionary(Songs.IndexOf, playlistSong => playlistSong); + .OrderBy(_songs.IndexOf) + .ToDictionary(_songs.IndexOf, playlistSong => playlistSong); Song[] songs = [.. playlistSongs.Select(playlistSong => playlistSong.Song)]; Song[] sortedSongs = [.. songs.SortBy(sortOptions)]; @@ -158,8 +191,8 @@ public class Playlist if (newPlaylistSong == playlistSong) continue; - Songs.RemoveAt(index); - Songs.Insert(index, newPlaylistSong); + _songs.RemoveAt(index); + _songs.Insert(index, newPlaylistSong); } PlaylistUpdatedEventArgs eventArgs = new() @@ -178,11 +211,11 @@ public class Playlist if (IsLocked) return; - int[] originalIndexes = [.. playlistSongs.Select(playlistSong => Songs.IndexOf(playlistSong))]; + int[] originalIndexes = [.. playlistSongs.Select(playlistSong => _songs.IndexOf(playlistSong))]; PlaylistSong[] shuffledSongs = [.. playlistSongs.Shuffle()]; for (int i = 0; i < originalIndexes.Length; i++) - Songs[originalIndexes[i]] = shuffledSongs[i]; + _songs[originalIndexes[i]] = shuffledSongs[i]; PlaylistUpdatedEventArgs eventArgs = new() { @@ -200,11 +233,11 @@ public class Playlist if (IsLocked) return; - int[] originalIndexes = [.. playlistSongs.Select(playlistSong => Songs.IndexOf(playlistSong))]; - PlaylistSong[] reversedSongs = [.. originalIndexes.Select(i => Songs[i]).Reverse()]; + int[] originalIndexes = [.. playlistSongs.Select(playlistSong => _songs.IndexOf(playlistSong))]; + PlaylistSong[] reversedSongs = [.. originalIndexes.Select(i => _songs[i]).Reverse()]; for (int i = 0; i < originalIndexes.Length; i++) - Songs[originalIndexes[i]] = reversedSongs[i]; + _songs[originalIndexes[i]] = reversedSongs[i]; PlaylistUpdatedEventArgs eventArgs = new() { @@ -224,7 +257,7 @@ public class Playlist public void RemoveSongs(int index, int count) { - PlaylistSong[] playlistSongs = [.. Songs.GetRange(index, count)]; + PlaylistSong[] playlistSongs = [.. _songs.GetRange(index, count)]; RemoveSongs(playlistSongs); } @@ -243,7 +276,7 @@ public class Playlist foreach (PlaylistSong playlistSong in playlistSongs) { - if (Songs.Remove(playlistSong)) + if (_songs.Remove(playlistSong)) { removedSongs.Add(playlistSong); } diff --git a/Harmonia.Core/Playlists/PlaylistDto.cs b/Harmonia.Core/Playlists/PlaylistDto.cs new file mode 100644 index 0000000..cacf01d --- /dev/null +++ b/Harmonia.Core/Playlists/PlaylistDto.cs @@ -0,0 +1,60 @@ +using Harmonia.Core.Models; + +namespace Harmonia.Core.Playlists; + +internal sealed record PlaylistDto +{ + public string UID { get; init; } = Guid.NewGuid().ToString(); + public string? Name { get; init; } + public List Songs { get; init; } = []; + public List GroupOptions { get; init; } = []; + public List SortOptions { get; init; } = []; + public bool IsLocked { get; init; } + + public static PlaylistDto FromPlaylist(Playlist playlist) + { + return new PlaylistDto + { + UID = playlist.UID, + Name = playlist.Name, + Songs = [.. playlist.Songs.Select(PlaylistSongDto.FromPlaylistSong)], + GroupOptions = [.. playlist.GroupOptions], + SortOptions = [.. playlist.SortOptions], + IsLocked = playlist.IsLocked + }; + } + + public Playlist ToPlaylist() + { + return Playlist.Restore( + UID, + Name, + Songs.Select(song => song.ToPlaylistSong()), + GroupOptions, + SortOptions, + IsLocked); + } +} + +internal sealed record PlaylistSongDto +{ + public string UID { get; init; } = Guid.NewGuid().ToString(); + public required Song Song { get; init; } + + public static PlaylistSongDto FromPlaylistSong(PlaylistSong playlistSong) + { + return new PlaylistSongDto + { + UID = playlistSong.UID, + Song = playlistSong.Song + }; + } + + public PlaylistSong ToPlaylistSong() + { + return new PlaylistSong(Song) + { + UID = UID + }; + } +} diff --git a/Harmonia.Core/Playlists/PlaylistManager.cs b/Harmonia.Core/Playlists/PlaylistManager.cs index 41158e8..d0f662e 100644 --- a/Harmonia.Core/Playlists/PlaylistManager.cs +++ b/Harmonia.Core/Playlists/PlaylistManager.cs @@ -4,6 +4,7 @@ public class PlaylistManager : IPlaylistManager { private readonly IPlaylistRepository playlistRepository; private readonly List _playlists = []; + private readonly Dictionary _playlistsBySongUid = []; public IReadOnlyList Playlists => _playlists; @@ -37,6 +38,11 @@ public class PlaylistManager : IPlaylistManager foreach (Playlist playlist in _playlists) { playlist.PlaylistUpdated += OnPlaylistUpdated; + + foreach (PlaylistSong song in playlist.Songs) + { + _playlistsBySongUid[song.UID] = playlist; + } } CurrentPlaylist = _playlists.Count > 0 ? _playlists[0] : await AddPlaylistAsync(); @@ -44,14 +50,16 @@ public class PlaylistManager : IPlaylistManager public async Task AddPlaylistAsync() { - Playlist playlist = new() - { - Name = "New Playlist" - }; + Playlist playlist = new("New Playlist"); playlist.PlaylistUpdated += OnPlaylistUpdated; _playlists.Add(playlist); + foreach (PlaylistSong song in playlist.Songs) + { + _playlistsBySongUid[song.UID] = playlist; + } + await playlistRepository.SaveAsync(playlist); PlaylistAdded?.Invoke(this, new(playlist)); @@ -64,14 +72,19 @@ public class PlaylistManager : IPlaylistManager playlist.PlaylistUpdated -= OnPlaylistUpdated; _playlists.Remove(playlist); + foreach (PlaylistSong song in playlist.Songs) + { + _playlistsBySongUid.Remove(song.UID); + } + playlistRepository.Delete(playlist); PlaylistRemoved?.Invoke(this, new(playlist)); } - public Playlist? GetPlaylist(PlaylistSong playlistSong) + public Playlist? FindPlaylistContaining(PlaylistSong playlistSong) { - return _playlists.FirstOrDefault(playlist => playlist.Songs.Any(song => song.UID == playlistSong.UID)); + return _playlistsBySongUid.GetValueOrDefault(playlistSong.UID); } private async void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e) @@ -79,6 +92,22 @@ public class PlaylistManager : IPlaylistManager if (sender is not Playlist playlist) return; + switch (e.Action) + { + case PlaylistUpdateAction.Add: + foreach (PlaylistSong song in e.Songs) + { + _playlistsBySongUid[song.UID] = playlist; + } + break; + case PlaylistUpdateAction.Remove: + foreach (PlaylistSong song in e.Songs) + { + _playlistsBySongUid.Remove(song.UID); + } + break; + } + await playlistRepository.SaveAsync(playlist); } } \ No newline at end of file diff --git a/Harmonia.Core/Playlists/PlaylistRepository.cs b/Harmonia.Core/Playlists/PlaylistRepository.cs index e2735df..2382cf0 100644 --- a/Harmonia.Core/Playlists/PlaylistRepository.cs +++ b/Harmonia.Core/Playlists/PlaylistRepository.cs @@ -1,14 +1,32 @@ using Harmonia.Core.Data; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Harmonia.Core.Playlists; public class PlaylistRepository : JsonFileRepository, IPlaylistRepository { + private static readonly JsonSerializerOptions _options = new() + { + WriteIndented = true, + IgnoreReadOnlyProperties = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + protected override string DirectoryName => Path.Combine("Playlists"); - public Playlist? GetPlaylist(PlaylistSong playlistSong) + protected override async Task DeserializeAsync(Stream stream) { - return Get().FirstOrDefault(playlist => playlist.Songs.Contains(playlistSong)); + PlaylistDto dto = await JsonSerializer.DeserializeAsync(stream, _options) ?? new(); + + return dto.ToPlaylist(); + } + + protected override Task SerializeAsync(Playlist playlist) + { + PlaylistDto dto = PlaylistDto.FromPlaylist(playlist); + + return Task.FromResult(JsonSerializer.Serialize(dto, _options)); } protected override string GetNewFileName() diff --git a/Harmonia.Tests/AudioPlayerTests.cs b/Harmonia.Tests/AudioPlayerTests.cs index a66d3e8..0642655 100644 --- a/Harmonia.Tests/AudioPlayerTests.cs +++ b/Harmonia.Tests/AudioPlayerTests.cs @@ -21,7 +21,7 @@ internal class TestAudioPlayer(IAudioEngine audioEngine, IPlaylistManager playli if (Playlist == null || PlayingSong == null) return -1; - return Playlist.Songs.IndexOf(PlayingSong); + return Playlist.IndexOf(PlayingSong); } } @@ -54,10 +54,7 @@ public class AudioPlayerTests new Song() { FileName = "Song7.mp3" } ]; - Playlist playlist = new() - { - Name = "Playlist1" - }; + Playlist playlist = new("Playlist1"); playlist.AddSongs(songs); @@ -65,7 +62,7 @@ public class AudioPlayerTests _playlistManager = Substitute.For(); _playlistManager.Playlists.Returns([playlist]); - _playlistManager.GetPlaylist(Arg.Any()).Returns(playlist); + _playlistManager.FindPlaylistContaining(Arg.Any()).Returns(playlist); _audioPlayer = new TestAudioPlayer(_audioEngine, _playlistManager); _audioPlayer.SetPlaylist(playlist); diff --git a/Harmonia.Tests/PlaylistTests.cs b/Harmonia.Tests/PlaylistTests.cs index 10a3957..dd6f773 100644 --- a/Harmonia.Tests/PlaylistTests.cs +++ b/Harmonia.Tests/PlaylistTests.cs @@ -114,7 +114,7 @@ public class PlaylistTests playlist.AddSongs(songs); - playlist.IsLocked = true; + playlist.Lock(); Song song = new() { FileName = "Song4.mp3" }; playlist.AddSong(song); diff --git a/Harmonia.UI/ViewModels/PlaylistViewModel.cs b/Harmonia.UI/ViewModels/PlaylistViewModel.cs index 4407caa..d0ea7d5 100644 --- a/Harmonia.UI/ViewModels/PlaylistViewModel.cs +++ b/Harmonia.UI/ViewModels/PlaylistViewModel.cs @@ -433,7 +433,7 @@ public class PlaylistViewModel : ViewModelBase if (Playlist == null || SelectedPlaylistSongs.Count == 0) return; - int selectedPlaylistSongIndex = Playlist.Songs.IndexOf(SelectedPlaylistSongs[0]); + int selectedPlaylistSongIndex = Playlist.IndexOf(SelectedPlaylistSongs[0]); if (selectedPlaylistSongIndex == -1) return; diff --git a/Harmonia.WinUI/ViewModels/PlaylistDetailViewModel.cs b/Harmonia.WinUI/ViewModels/PlaylistDetailViewModel.cs index e8b2a9e..78fc0b7 100644 --- a/Harmonia.WinUI/ViewModels/PlaylistDetailViewModel.cs +++ b/Harmonia.WinUI/ViewModels/PlaylistDetailViewModel.cs @@ -611,7 +611,7 @@ public partial class PlaylistDetailViewModel : ViewModelBase if (Playlist == null || SelectedPlaylistSongs.Count == 0) return; - int selectedPlaylistSongIndex = Playlist.Songs.IndexOf(SelectedPlaylistSongs[0]); + int selectedPlaylistSongIndex = Playlist.IndexOf(SelectedPlaylistSongs[0]); if (selectedPlaylistSongIndex == -1) return; diff --git a/Harmonia.WinUI/ViewModels/PlaylistsViewModel.cs b/Harmonia.WinUI/ViewModels/PlaylistsViewModel.cs index 79bf6db..76eb5ac 100644 --- a/Harmonia.WinUI/ViewModels/PlaylistsViewModel.cs +++ b/Harmonia.WinUI/ViewModels/PlaylistsViewModel.cs @@ -1,4 +1,4 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using Harmonia.Core.Player; using Harmonia.Core.Playlists; using System; using System.Collections.ObjectModel; @@ -9,6 +9,7 @@ namespace Harmonia.WinUI.ViewModels; public partial class PlaylistsViewModel : ViewModelBase { private readonly IPlaylistManager _playlistManager; + private readonly IAudioPlayer _audioPlayer; private ObservableCollection _playlists = []; public ObservableCollection Playlists @@ -39,13 +40,29 @@ public partial class PlaylistsViewModel : ViewModelBase } } - public PlaylistsViewModel(IPlaylistManager playlistManager) + private PlaylistItemViewModel? _activePlaylist = null; + public PlaylistItemViewModel? ActivePlaylist + { + get + { + return _activePlaylist; + } + set + { + SetProperty(ref _activePlaylist, value); + } + } + + public PlaylistsViewModel(IPlaylistManager playlistManager, IAudioPlayer audioPlayer) { _playlistManager = playlistManager; _playlistManager.PlaylistAdded += OnPlaylistAdded; _playlistManager.PlaylistRemoved += OnPlaylistRemoved; _playlistManager.CurrentPlaylistChanged += OnCurrentPlaylistChanged; + _audioPlayer = audioPlayer; + _audioPlayer.PlayingSongChanged += OnPlayingSongChanged; + _playlists = new ObservableCollection(_playlistManager.Playlists.Select(p => new PlaylistItemViewModel(p))); _selectedPlaylist = _playlists.FirstOrDefault(pv => pv.Playlist == _playlistManager.CurrentPlaylist); } @@ -70,4 +87,9 @@ public partial class PlaylistsViewModel : ViewModelBase Playlists.Remove(playlistView); } } + + private void OnPlayingSongChanged(object? sender, EventArgs e) + { + ActivePlaylist = Playlists.FirstOrDefault(pv => pv.Playlist == _audioPlayer.Playlist); + } } \ No newline at end of file diff --git a/Harmonia.WinUI/Views/PlaylistsView.xaml b/Harmonia.WinUI/Views/PlaylistsView.xaml index 6d130d2..7539558 100644 --- a/Harmonia.WinUI/Views/PlaylistsView.xaml +++ b/Harmonia.WinUI/Views/PlaylistsView.xaml @@ -12,8 +12,34 @@ d:DataContext="{d:DesignInstance Type=vm:PlaylistsViewModel, IsDesignTimeCreatable=True}" mc:Ignorable="d"> + + - + + + + + + + + + + + + + - + - + - + - + diff --git a/Harmonia.WinUI/Views/PlaylistsView.xaml.cs b/Harmonia.WinUI/Views/PlaylistsView.xaml.cs index fc8f6f9..a0cd9bd 100644 --- a/Harmonia.WinUI/Views/PlaylistsView.xaml.cs +++ b/Harmonia.WinUI/Views/PlaylistsView.xaml.cs @@ -1,12 +1,99 @@ +using CommunityToolkit.WinUI; +using Harmonia.WinUI.ViewModels; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using System.ComponentModel; namespace Harmonia.WinUI.Views; public sealed partial class PlaylistsView : UserControl { + private readonly PlaylistsViewModel _viewModel; + public PlaylistsView() { InitializeComponent(); + + _viewModel = (PlaylistsViewModel)DataContext; + _viewModel.PropertyChanging += OnViewModelPropertyChanging; + _viewModel.PropertyChanged += OnViewModelPropertyChanged; } + + private void OnViewModelPropertyChanging(object? sender, PropertyChangingEventArgs e) + { + switch (e.PropertyName) + { + case nameof(_viewModel.ActivePlaylist): + UpdateListViewItemStyle(PlaylistItemStyle.Normal); + break; + } + } + + private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(_viewModel.ActivePlaylist): + UpdateListViewItemStyle(PlaylistItemStyle.Selected); + break; + } + } + + private void UpdateListViewItemStyle(PlaylistItemStyle style) + { + ListViewItem listViewItem = (ListViewItem)PlaylistsListView.ContainerFromItem(_viewModel.ActivePlaylist); + + if (listViewItem is null) + return; + + FrameworkElement? frameworkElement = TryGetFrameworkElement(listViewItem, "PlaylistListViewItem"); + + if (frameworkElement == null) + return; + + string playlistItemIconBrushName = style == PlaylistItemStyle.Selected ? "SelectedPlaylistIconPath" : "PlaylistIconPath"; + string playlistItemTitleBrushName = style == PlaylistItemStyle.Selected ? "SelectedPlaylistTitleTextBlock" : "PlaylistTitleTextBlock"; + string playlistItemSubtitleBrushName = style == PlaylistItemStyle.Selected ? "SelectedPlaylistSubtitleTextBlock" : "PlaylistSubtitleTextBlock"; + + UpdateElementStyle(frameworkElement, "MusicNoteListPathIcon", playlistItemIconBrushName); + UpdateElementStyle(frameworkElement, "PlaylistTitleTextBlock", playlistItemTitleBrushName); + UpdateElementStyle(frameworkElement, "PlaylistSubtitleTextBlock", playlistItemSubtitleBrushName); + } + + private static FrameworkElement? TryGetFrameworkElement(ListViewItem listViewItem, string elementName) + { + if (listViewItem.Name == elementName) + { + return listViewItem; + } + else + { + if (listViewItem.FindDescendant(elementName) is not FrameworkElement frameworkElement) + return null; + + return frameworkElement; + } + } + + private void UpdateElementStyle(FrameworkElement dependencyObject, string elementName, string resourceName) + { + if (dependencyObject.FindDescendant(elementName) is not FrameworkElement frameworkElement) + return; + + Resources.TryGetValue(resourceName, out object? resource); + + if (resource == null) + Application.Current.Resources.TryGetValue(resourceName, out resource); + + if (resource is not Style style) + return; + + frameworkElement.Style = style; + } +} + +public enum PlaylistItemStyle +{ + Normal, + Selected } \ No newline at end of file