// 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();
}
}
}