// Copyright (c) Arlo Godfrey. All Rights Reserved. // Licensed under the GNU Lesser General Public License, Version 3.0 with additional terms. // See the LICENSE, LICENSE.LESSER and LICENSE.ADDITIONAL files in the project root for more information. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Diagnostics; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using OwlCore; using OwlCore.Events; using OwlCore.Extensions; using StrixMusic.Sdk.AdapterModels; using StrixMusic.Sdk.AppModels; using StrixMusic.Sdk.CoreModels; using StrixMusic.Sdk.Extensions; using StrixMusic.Sdk.MediaPlayback; using StrixMusic.Sdk.ViewModels.Helpers; namespace StrixMusic.Sdk.ViewModels { /// /// A ViewModel for . /// public sealed class PlaylistViewModel : ObservableObject, ISdkViewModel, IPlaylist, ITrackCollectionViewModel, IImageCollectionViewModel { private readonly IPlaylist _playlist; private readonly IUserProfile? _owner; private readonly SemaphoreSlim _populateTracksMutex = new(1, 1); private readonly SemaphoreSlim _populateImagesMutex = new(1, 1); private readonly SemaphoreSlim _populateUrlsMutex = new(1, 1); private readonly SynchronizationContext _syncContext; /// /// Initializes a new instance of the class. /// /// The to wrap. public PlaylistViewModel(IPlaylist playlist) { _syncContext = SynchronizationContext.Current; _playlist = playlist; PauseTrackCollectionAsyncCommand = new AsyncRelayCommand(PauseTrackCollectionAsync); PlayTrackCollectionAsyncCommand = new AsyncRelayCommand(PlayTrackCollectionAsync); PlayTrackAsyncCommand = new AsyncRelayCommand((x, y) => _playlist.PlayTrackCollectionAsync(x ?? ThrowHelper.ThrowArgumentNullException(), y)); ChangeNameAsyncCommand = new AsyncRelayCommand(ChangeNameInternalAsync); ChangeDescriptionAsyncCommand = new AsyncRelayCommand(ChangeDescriptionAsync); ChangeDurationAsyncCommand = new AsyncRelayCommand(ChangeDurationAsync); PopulateMoreTracksCommand = new AsyncRelayCommand(PopulateMoreTracksAsync); PopulateMoreImagesCommand = new AsyncRelayCommand(PopulateMoreImagesAsync); PopulateMoreUrlsCommand = new AsyncRelayCommand(PopulateMoreUrlsAsync); InitTrackCollectionAsyncCommand = new AsyncRelayCommand(InitTrackCollectionAsync); InitImageCollectionAsyncCommand = new AsyncRelayCommand(InitImageCollectionAsync); ChangeTrackCollectionSortingTypeCommand = new RelayCommand(x => SortTrackCollection(x, CurrentTracksSortingDirection)); ChangeTrackCollectionSortingDirectionCommand = new RelayCommand(x => SortTrackCollection(CurrentTracksSortingType, x)); if (_playlist.Owner != null) _owner = new UserProfileViewModel(_playlist.Owner); if (_playlist.RelatedItems != null) RelatedItems = new PlayableCollectionGroupViewModel(_playlist.RelatedItems); Tracks = new ObservableCollection(); Images = new ObservableCollection(); Urls = new ObservableCollection(); UnsortedTracks = new ObservableCollection(); AttachEvents(); } /// public Task InitAsync(CancellationToken cancellationToken = default) { if (IsInitialized) return Task.CompletedTask; IsInitialized = true; return Task.WhenAll(InitImageCollectionAsync(cancellationToken), InitTrackCollectionAsync(cancellationToken)); } private void AttachEvents() { DescriptionChanged += CorePlaylistDescriptionChanged; NameChanged += CorePlaylistNameChanged; PlaybackStateChanged += CorePlaylistPlaybackStateChanged; LastPlayedChanged += CorePlaylistLastPlayedChanged; DownloadInfoChanged += OnDownloadInfoChanged; IsPlayTrackCollectionAsyncAvailableChanged += OnIsPlayTrackCollectionAsyncAvailableChanged; IsPauseTrackCollectionAsyncAvailableChanged += OnIsPauseTrackCollectionAsyncAvailableChanged; IsChangeNameAsyncAvailableChanged += OnIsChangeNameAsyncAvailableChanged; IsChangeDurationAsyncAvailableChanged += OnIsChangeDurationAsyncAvailableChanged; IsChangeDescriptionAsyncAvailableChanged += OnIsChangeDescriptionAsyncAvailableChanged; TracksCountChanged += PlaylistOnTrackItemsCountChanged; TracksChanged += PlaylistViewModel_TrackItemsChanged; ImagesCountChanged += PlaylistViewModel_ImagesCountChanged; ImagesChanged += PlaylistViewModel_ImagesChanged; UrlsCountChanged += PlaylistViewModel_UrlsCountChanged; UrlsChanged += PlaylistViewModel_UrlsChanged; } private void DetachEvents() { DescriptionChanged -= CorePlaylistDescriptionChanged; NameChanged -= CorePlaylistNameChanged; PlaybackStateChanged -= CorePlaylistPlaybackStateChanged; LastPlayedChanged += CorePlaylistLastPlayedChanged; DownloadInfoChanged -= OnDownloadInfoChanged; IsPlayTrackCollectionAsyncAvailableChanged -= OnIsPlayTrackCollectionAsyncAvailableChanged; IsPauseTrackCollectionAsyncAvailableChanged -= OnIsPauseTrackCollectionAsyncAvailableChanged; IsChangeNameAsyncAvailableChanged -= OnIsChangeNameAsyncAvailableChanged; IsChangeDurationAsyncAvailableChanged -= OnIsChangeDurationAsyncAvailableChanged; IsChangeDescriptionAsyncAvailableChanged -= OnIsChangeDescriptionAsyncAvailableChanged; TracksCountChanged -= PlaylistOnTrackItemsCountChanged; TracksChanged -= PlaylistViewModel_TrackItemsChanged; ImagesCountChanged -= PlaylistViewModel_ImagesCountChanged; ImagesChanged -= PlaylistViewModel_ImagesChanged; UrlsCountChanged -= PlaylistViewModel_UrlsCountChanged; UrlsChanged -= PlaylistViewModel_UrlsChanged; } /// public event EventHandler? SourcesChanged { add => _playlist.SourcesChanged += value; remove => _playlist.SourcesChanged -= value; } /// public event EventHandler? PlaybackStateChanged { add => _playlist.PlaybackStateChanged += value; remove => _playlist.PlaybackStateChanged -= value; } /// public event EventHandler? DownloadInfoChanged { add => _playlist.DownloadInfoChanged += value; remove => _playlist.DownloadInfoChanged -= value; } /// public event EventHandler? NameChanged { add => _playlist.NameChanged += value; remove => _playlist.NameChanged -= value; } /// public event EventHandler? DescriptionChanged { add => _playlist.DescriptionChanged += value; remove => _playlist.DescriptionChanged -= value; } /// public event EventHandler? DurationChanged { add => _playlist.DurationChanged += value; remove => _playlist.DurationChanged -= value; } /// public event EventHandler? LastPlayedChanged { add => _playlist.LastPlayedChanged += value; remove => _playlist.LastPlayedChanged -= value; } /// public event EventHandler? IsChangeNameAsyncAvailableChanged { add => _playlist.IsChangeNameAsyncAvailableChanged += value; remove => _playlist.IsChangeNameAsyncAvailableChanged -= value; } /// public event EventHandler? IsChangeDescriptionAsyncAvailableChanged { add => _playlist.IsChangeDescriptionAsyncAvailableChanged += value; remove => _playlist.IsChangeDescriptionAsyncAvailableChanged -= value; } /// public event EventHandler? IsChangeDurationAsyncAvailableChanged { add => _playlist.IsChangeDurationAsyncAvailableChanged += value; remove => _playlist.IsChangeDurationAsyncAvailableChanged -= value; } /// public event EventHandler? IsPlayTrackCollectionAsyncAvailableChanged { add => _playlist.IsPlayTrackCollectionAsyncAvailableChanged += value; remove => _playlist.IsPlayTrackCollectionAsyncAvailableChanged -= value; } /// public event EventHandler? IsPauseTrackCollectionAsyncAvailableChanged { add => _playlist.IsPauseTrackCollectionAsyncAvailableChanged += value; remove => _playlist.IsPauseTrackCollectionAsyncAvailableChanged -= value; } /// public event EventHandler? TracksCountChanged { add => _playlist.TracksCountChanged += value; remove => _playlist.TracksCountChanged -= value; } /// public event EventHandler? ImagesCountChanged { add => _playlist.ImagesCountChanged += value; remove => _playlist.ImagesCountChanged -= value; } /// public event EventHandler? UrlsCountChanged { add => _playlist.UrlsCountChanged += value; remove => _playlist.UrlsCountChanged -= value; } /// public event CollectionChangedEventHandler? TracksChanged { add => _playlist.TracksChanged += value; remove => _playlist.TracksChanged -= value; } /// public event CollectionChangedEventHandler? ImagesChanged { add => _playlist.ImagesChanged += value; remove => _playlist.ImagesChanged -= value; } /// public event CollectionChangedEventHandler? UrlsChanged { add => _playlist.UrlsChanged += value; remove => _playlist.UrlsChanged -= value; } private void CorePlaylistPlaybackStateChanged(object sender, PlaybackState e) => _syncContext.Post(_ => OnPropertyChanged(nameof(PlaybackState)), null); private void OnDownloadInfoChanged(object sender, DownloadInfo e) => _syncContext.Post(_ => OnPropertyChanged(nameof(DownloadInfo)), null); private void CorePlaylistNameChanged(object sender, string e) => _syncContext.Post(_ => OnPropertyChanged(nameof(Name)), null); private void CorePlaylistDescriptionChanged(object sender, string? e) => _syncContext.Post(_ => OnPropertyChanged(nameof(Description)), null); private void PlaylistOnTrackItemsCountChanged(object sender, int e) => _syncContext.Post(_ => OnPropertyChanged(nameof(TotalTrackCount)), null); private void PlaylistViewModel_ImagesCountChanged(object sender, int e) => _syncContext.Post(_ => OnPropertyChanged(nameof(TotalImageCount)), null); private void PlaylistViewModel_UrlsCountChanged(object sender, int e) => _syncContext.Post(_ => OnPropertyChanged(nameof(TotalUrlCount)), null); private void CorePlaylistLastPlayedChanged(object sender, DateTime? e) => _syncContext.Post(_ => OnPropertyChanged(nameof(LastPlayed)), null); private void OnIsChangeDescriptionAsyncAvailableChanged(object sender, bool e) => _syncContext.Post(_ => OnPropertyChanged(nameof(IsChangeDescriptionAsyncAvailable)), null); private void OnIsChangeDurationAsyncAvailableChanged(object sender, bool e) => _syncContext.Post(_ => OnPropertyChanged(nameof(IsChangeDurationAsyncAvailable)), null); private void OnIsChangeNameAsyncAvailableChanged(object sender, bool e) => _syncContext.Post(_ => OnPropertyChanged(nameof(IsChangeNameAsyncAvailable)), null); private void OnIsPauseTrackCollectionAsyncAvailableChanged(object sender, bool e) => _syncContext.Post(_ => OnPropertyChanged(nameof(IsPauseTrackCollectionAsyncAvailable)), null); private void OnIsPlayTrackCollectionAsyncAvailableChanged(object sender, bool e) => _syncContext.Post(_ => OnPropertyChanged(nameof(IsPlayTrackCollectionAsyncAvailable)), null); private void PlaylistViewModel_TrackItemsChanged(object sender, IReadOnlyList> addedItems, IReadOnlyList> removedItems) => _syncContext.Post(_ => { if (CurrentTracksSortingType == TrackSortingType.Unsorted) { Tracks.ChangeCollection(addedItems, removedItems, x => new TrackViewModel(x.Data)); } else { // Preventing index issues during tracks emission from the core, also making sure that unordered tracks updated. UnsortedTracks.ChangeCollection(addedItems, removedItems, x => new TrackViewModel(x.Data)); // Avoiding direct assignment to prevent effect on UI. foreach (var item in UnsortedTracks) { if (!Tracks.Contains(item)) Tracks.Add(item); } foreach (var item in Tracks.ToArray()) { if (!UnsortedTracks.Contains(item)) Tracks.Remove(item); } SortTrackCollection(CurrentTracksSortingType, CurrentTracksSortingDirection); } }, null); private void PlaylistViewModel_ImagesChanged(object sender, IReadOnlyList> addedItems, IReadOnlyList> removedItems) => _syncContext.Post(_ => { Images.ChangeCollection(addedItems, removedItems); }, null); private void PlaylistViewModel_UrlsChanged(object sender, IReadOnlyList> addedItems, IReadOnlyList> removedItems) => _syncContext.Post(_ => { Urls.ChangeCollection(addedItems, removedItems); }, null); /// public bool IsInitialized { get; private set; } /// public TrackSortingType CurrentTracksSortingType { get; private set; } /// public SortDirection CurrentTracksSortingDirection { get; private set; } /// /// The merged sources that form this member. /// public IReadOnlyList Sources => _playlist.GetSources(); /// IReadOnlyList IMerged.Sources => Sources; /// IReadOnlyList IMerged.Sources => Sources; /// IReadOnlyList IMerged.Sources => Sources; /// IReadOnlyList IMerged.Sources => Sources; /// IReadOnlyList IMerged.Sources => Sources; /// public string Id => _playlist.Id; /// public TimeSpan Duration => _playlist.Duration; /// public DateTime? LastPlayed { get; } /// public DateTime? AddedAt { get; } /// public ObservableCollection UnsortedTracks { get; } /// public ObservableCollection Tracks { get; set; } /// public ObservableCollection Images { get; } /// public ObservableCollection Urls { get; } /// public IPlayableCollectionGroup? RelatedItems { get; } /// public IUserProfile? Owner => _owner; /// public string Name => _playlist.Name; /// public string? Description => _playlist.Description; /// public PlaybackState PlaybackState => _playlist.PlaybackState; /// public DownloadInfo DownloadInfo => _playlist.DownloadInfo; /// public int TotalTrackCount => _playlist.TotalTrackCount; /// public int TotalImageCount => _playlist.TotalImageCount; /// public int TotalUrlCount => _playlist.TotalUrlCount; /// public bool IsPlayTrackCollectionAsyncAvailable => _playlist.IsPlayTrackCollectionAsyncAvailable; /// public bool IsPauseTrackCollectionAsyncAvailable => _playlist.IsPauseTrackCollectionAsyncAvailable; /// public bool IsChangeNameAsyncAvailable => _playlist.IsChangeNameAsyncAvailable; /// public bool IsChangeDescriptionAsyncAvailable => _playlist.IsChangeDescriptionAsyncAvailable; /// public bool IsChangeDurationAsyncAvailable => _playlist.IsChangeDurationAsyncAvailable; /// public Task IsAddTrackAvailableAsync(int index, CancellationToken cancellationToken = default) => _playlist.IsAddTrackAvailableAsync(index, cancellationToken); /// public Task IsAddImageAvailableAsync(int index, CancellationToken cancellationToken = default) => _playlist.IsAddImageAvailableAsync(index, cancellationToken); /// public Task IsAddUrlAvailableAsync(int index, CancellationToken cancellationToken = default) => _playlist.IsAddUrlAvailableAsync(index, cancellationToken); /// public Task IsRemoveTrackAvailableAsync(int index, CancellationToken cancellationToken = default) => _playlist.IsRemoveTrackAvailableAsync(index, cancellationToken); /// public Task IsRemoveImageAvailableAsync(int index, CancellationToken cancellationToken = default) => _playlist.IsRemoveImageAvailableAsync(index, cancellationToken); /// public Task IsRemoveUrlAvailableAsync(int index, CancellationToken cancellationToken = default) => _playlist.IsRemoveUrlAvailableAsync(index, cancellationToken); /// public void SortTrackCollection(TrackSortingType trackSorting, SortDirection sortDirection) { CurrentTracksSortingType = trackSorting; CurrentTracksSortingDirection = sortDirection; CollectionSorting.SortTracks(Tracks, trackSorting, sortDirection, UnsortedTracks); } /// public Task PlayTrackCollectionAsync(ITrack track, CancellationToken cancellationToken = default) => _playlist.PlayTrackCollectionAsync(track, cancellationToken); /// public Task PlayTrackCollectionAsync(CancellationToken cancellationToken = default) => _playlist.PlayTrackCollectionAsync(cancellationToken); /// public Task PauseTrackCollectionAsync(CancellationToken cancellationToken = default) => _playlist.PauseTrackCollectionAsync(cancellationToken); /// public Task StartDownloadOperationAsync(DownloadOperation operation, CancellationToken cancellationToken = default) => _playlist.StartDownloadOperationAsync(operation, cancellationToken); /// public Task ChangeNameAsync(string name, CancellationToken cancellationToken = default) => ChangeNameInternalAsync(name, cancellationToken); /// public Task ChangeDescriptionAsync(string? description, CancellationToken cancellationToken = default) => _playlist.ChangeDescriptionAsync(description, cancellationToken); /// public Task ChangeDurationAsync(TimeSpan duration, CancellationToken cancellationToken = default) => _playlist.ChangeDurationAsync(duration, cancellationToken); /// public Task AddTrackAsync(ITrack track, int index, CancellationToken cancellationToken = default) => _playlist.AddTrackAsync(track, index, cancellationToken); /// public Task AddImageAsync(IImage image, int index, CancellationToken cancellationToken = default) => _playlist.AddImageAsync(image, index, cancellationToken); /// public Task AddUrlAsync(IUrl image, int index, CancellationToken cancellationToken = default) => _playlist.AddUrlAsync(image, index, cancellationToken); /// public Task RemoveTrackAsync(int index, CancellationToken cancellationToken = default) => _playlist.RemoveTrackAsync(index, cancellationToken); /// public Task RemoveImageAsync(int index, CancellationToken cancellationToken = default) => _playlist.RemoveImageAsync(index, cancellationToken); /// public Task RemoveUrlAsync(int index, CancellationToken cancellationToken = default) => _playlist.RemoveUrlAsync(index, cancellationToken); /// public IAsyncEnumerable GetTracksAsync(int limit, int offset, CancellationToken cancellationToken = default) => _playlist.GetTracksAsync(limit, offset, cancellationToken); /// public IAsyncEnumerable GetImagesAsync(int limit, int offset, CancellationToken cancellationToken = default) => _playlist.GetImagesAsync(limit, offset, cancellationToken); /// public IAsyncEnumerable GetUrlsAsync(int limit, int offset, CancellationToken cancellationToken = default) => _playlist.GetUrlsAsync(limit, offset, cancellationToken); /// public async Task PopulateMoreTracksAsync(int limit, CancellationToken cancellationToken = default) { using (await Flow.EasySemaphore(_populateTracksMutex)) { using var releaseReg = cancellationToken.Register(() => _populateTracksMutex.Release()); _syncContext.Post(async _ => { await foreach (var item in _playlist.GetTracksAsync(limit, Tracks.Count, cancellationToken)) { var tvm = new TrackViewModel(item); Tracks.Add(tvm); UnsortedTracks.Add(tvm); } }, null); } } /// public async Task PopulateMoreImagesAsync(int limit, CancellationToken cancellationToken = default) { using (await Flow.EasySemaphore(_populateImagesMutex)) { using var releaseReg = cancellationToken.Register(() => _populateImagesMutex.Release()); _syncContext.Post(async _ => { await foreach (var item in _playlist.GetImagesAsync(limit, Images.Count, cancellationToken)) Images.Add(item); }, null); } } /// public async Task PopulateMoreUrlsAsync(int limit, CancellationToken cancellationToken = default) { using (await Flow.EasySemaphore(_populateUrlsMutex)) { using var releaseReg = cancellationToken.Register(() => _populateUrlsMutex.Release()); _syncContext.Post(async _ => { await foreach (var item in _playlist.GetUrlsAsync(limit, Urls.Count, cancellationToken)) Urls.Add(item); }, null); } } /// public Task InitTrackCollectionAsync(CancellationToken cancellationToken = default) => CollectionInit.TrackCollection(this, cancellationToken); /// public Task InitImageCollectionAsync(CancellationToken cancellationToken = default) => CollectionInit.ImageCollection(this, cancellationToken); /// public IAsyncRelayCommand PopulateMoreTracksCommand { get; } /// public IAsyncRelayCommand PopulateMoreImagesCommand { get; } /// public IAsyncRelayCommand PopulateMoreUrlsCommand { get; } /// public IAsyncRelayCommand PlayTrackAsyncCommand { get; } /// public IAsyncRelayCommand PlayTrackCollectionAsyncCommand { get; } /// public IAsyncRelayCommand PauseTrackCollectionAsyncCommand { get; } /// /// Command to change the name. It triggers . /// public IAsyncRelayCommand ChangeNameAsyncCommand { get; } /// /// Command to change the description. It triggers . /// public IAsyncRelayCommand ChangeDescriptionAsyncCommand { get; } /// /// Command to change the duration. It triggers . /// public IAsyncRelayCommand ChangeDurationAsyncCommand { get; } /// public IRelayCommand ChangeTrackCollectionSortingTypeCommand { get; } /// public IRelayCommand ChangeTrackCollectionSortingDirectionCommand { get; } /// public IAsyncRelayCommand InitTrackCollectionAsyncCommand { get; } /// public IAsyncRelayCommand InitImageCollectionAsyncCommand { get; } /// public bool Equals(ICorePlaylistCollectionItem other) => _playlist.Equals(other); /// public bool Equals(ICorePlaylist other) => _playlist.Equals(other); /// public bool Equals(ICoreTrackCollection other) => _playlist.Equals(other); /// public bool Equals(ICoreImageCollection other) => _playlist.Equals(other); /// public bool Equals(ICoreUrlCollection other) => _playlist.Equals(other); private Task ChangeNameInternalAsync(string? name, CancellationToken cancellationToken = default) { Guard.IsNotNull(name, nameof(name)); return _playlist.ChangeNameAsync(name, cancellationToken); } /// public ValueTask DisposeAsync() { DetachEvents(); return _playlist.DisposeAsync(); } } }