diff --git a/Audiotica.Converters/WebToAlbumConverter.cs b/Audiotica.Converters/WebToAlbumConverter.cs index 30c4f982..151cbba3 100644 --- a/Audiotica.Converters/WebToAlbumConverter.cs +++ b/Audiotica.Converters/WebToAlbumConverter.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Audiotica.Core.Common; using Audiotica.Core.Extensions; @@ -50,19 +51,41 @@ public async Task ConvertAsync(WebAlbum other, bool ignoreLibrary = false { await FillPartialAsync(other); + // fill the album partial for each track + foreach (var webSong in other.Tracks) + webSong.Album = other; + var album = new Album { Title = other.Title, ArtworkUri = other.Artwork.ToString(), Artist = await _webArtistConverter.ConvertAsync(other.Artist), Year = other.ReleaseDate?.Year, - Tracks = - other.Tracks != null - ? new OptimizedObservableCollection( - await Task.WhenAll(other.Tracks.Select(p => _webTrackConverter.ConvertAsync(p)))) - : null }; + // TODO: ISupportIncrementalLoading? + if (other.Tracks != null) + { + // only let 10 concurrent conversions + using (var semaphoreSlim = new SemaphoreSlim(10, 10)) + { + // ReSharper disable AccessToDisposedClosure + var trackTasks = other.Tracks.Select(async p => + { + await semaphoreSlim.WaitAsync(); + var track = await _webTrackConverter.ConvertAsync(p); + semaphoreSlim.Release(); + return track; + }); + album.Tracks = + other.Tracks != null + ? new OptimizedObservableCollection( + await Task.WhenAll(trackTasks)) + : null; + // ReSharper restore AccessToDisposedClosure + } + } + var libraryAlbum = _libraryService.Albums.FirstOrDefault(p => p.Title.EqualsIgnoreCase(album.Title)); other.PreviousConversion = libraryAlbum ?? album; diff --git a/Audiotica.Core/Extensions/CollectionExtensions.cs b/Audiotica.Core/Extensions/CollectionExtensions.cs index fb0d7e4c..0161e410 100644 --- a/Audiotica.Core/Extensions/CollectionExtensions.cs +++ b/Audiotica.Core/Extensions/CollectionExtensions.cs @@ -1,7 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + namespace Audiotica.Core.Extensions { public static class CollectionExtensions { + private static readonly Random Random = new Random(); + public static void Fill(this T[] array, T value) { for (var i = 0; i < array.Length; i++) @@ -9,5 +16,53 @@ public static void Fill(this T[] array, T value) array[i] = value; } } + + public static void Sort(this ObservableCollection observable, Comparison comparison) + { + var sorted = observable.ToList(); + sorted.Sort(comparison); + + var ptr = 0; + while (ptr < sorted.Count) + { + if (!observable[ptr].Equals(sorted[ptr])) + { + var t = observable[ptr]; + observable.RemoveAt(ptr); + observable.Insert(sorted.IndexOf(t), t); + } + else + { + ptr++; + } + } + } + + public static void AddRange(this IList collection, IEnumerable items) + { + foreach (var item in items) + collection.Add(item); + } + + public static IEnumerable Shuffle(this IEnumerable list) + { + var arr = list.ToArray(); + Shuffle(arr); + return arr; + } + + private static void Shuffle(IList array) + { + var n = array.Count; + for (var i = 0; i < n; i++) + { + // NextDouble returns a random number between 0 and 1. + // ... It is equivalent to Math.random() in Java. + var r = i + (int) (Random.NextDouble()*(n - i)); + var t = array[r]; + array[r] = array[i]; + array[i] = t; + } + } } } \ No newline at end of file diff --git a/Audiotica.Core/Utilities/Interfaces/IAppSettingsUtility.cs b/Audiotica.Core/Utilities/Interfaces/IAppSettingsUtility.cs index 54c9a204..03b4402d 100644 --- a/Audiotica.Core/Utilities/Interfaces/IAppSettingsUtility.cs +++ b/Audiotica.Core/Utilities/Interfaces/IAppSettingsUtility.cs @@ -4,5 +4,6 @@ public interface IAppSettingsUtility { string DownloadsPath { get; set; } string TempDownloadsPath { get; } + int Theme { get; set; } } } \ No newline at end of file diff --git a/Audiotica.Database/Models/Playlist.cs b/Audiotica.Database/Models/Playlist.cs index f90f2aa7..55cfbec1 100644 --- a/Audiotica.Database/Models/Playlist.cs +++ b/Audiotica.Database/Models/Playlist.cs @@ -10,7 +10,10 @@ public class Playlist { public string Id { get; set; } public DateTime CreatedAt { get; set; } + public DateTime EditedAt { get; set; } + public DateTime DeletedAt { get; set; } public string Name { get; set; } + public bool IsVisible { get; set; } public List Tracks { get; set; } } } \ No newline at end of file diff --git a/Audiotica.Database/Models/PlaylistTrack.cs b/Audiotica.Database/Models/PlaylistTrack.cs index 6d2a9532..edf84a5a 100644 --- a/Audiotica.Database/Models/PlaylistTrack.cs +++ b/Audiotica.Database/Models/PlaylistTrack.cs @@ -7,7 +7,6 @@ namespace Audiotica.Database.Models /// public class PlaylistTrack { - public string Id { get; set; } public string TrackId { get; set; } [JsonIgnore] diff --git a/Audiotica.Database/Models/Track.cs b/Audiotica.Database/Models/Track.cs index 51eedaf7..8587d663 100644 --- a/Audiotica.Database/Models/Track.cs +++ b/Audiotica.Database/Models/Track.cs @@ -354,6 +354,8 @@ public class TrackComparer : IEqualityComparer { public bool Equals(Track x, Track y) { + if (x == null && y == null) return true; + if (x == null || y == null) return false; if (x.IsFromLibrary && y.IsFromLibrary) return x.Id == y.Id; return GetHashCode(x) == GetHashCode(y); diff --git a/Windows/Audiotica.Core.Windows/Helpers/ApplicationSettingsConstants.cs b/Windows/Audiotica.Core.Windows/Helpers/ApplicationSettingsConstants.cs index e489a6ac..b9c1c3ff 100644 --- a/Windows/Audiotica.Core.Windows/Helpers/ApplicationSettingsConstants.cs +++ b/Windows/Audiotica.Core.Windows/Helpers/ApplicationSettingsConstants.cs @@ -13,5 +13,6 @@ public static class ApplicationSettingsConstants public const string IsAlbumAdaptiveColorEnabled = "IsAlbumAdaptiveColorEnabled"; public const string SongSort = "SongSort"; public const string AlbumSort = "AlbumSort"; + public const string Theme = "Theme"; } } \ No newline at end of file diff --git a/Windows/Audiotica.Core.Windows/Helpers/FlyoutEx.cs b/Windows/Audiotica.Core.Windows/Helpers/FlyoutEx.cs index 42b703f9..fbc207f7 100644 --- a/Windows/Audiotica.Core.Windows/Helpers/FlyoutEx.cs +++ b/Windows/Audiotica.Core.Windows/Helpers/FlyoutEx.cs @@ -1,8 +1,10 @@ using System; using Windows.Foundation; +using Windows.UI; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Media; namespace Audiotica.Core.Windows.Helpers { diff --git a/Windows/Audiotica.Core.Windows/Messages/AddToPlaylistMessage.cs b/Windows/Audiotica.Core.Windows/Messages/AddToPlaylistMessage.cs index 1871749c..e73b6b96 100644 --- a/Windows/Audiotica.Core.Windows/Messages/AddToPlaylistMessage.cs +++ b/Windows/Audiotica.Core.Windows/Messages/AddToPlaylistMessage.cs @@ -1,16 +1,29 @@ -using Audiotica.Database.Models; +using System.Collections.Generic; +using Audiotica.Database.Models; +using Newtonsoft.Json; namespace Audiotica.Core.Windows.Messages { public class AddToPlaylistMessage { + public AddToPlaylistMessage() + { + } + public AddToPlaylistMessage(QueueTrack track, int position) { - Track = track; + Tracks = new List {track}; + Position = position; + } + + public AddToPlaylistMessage(List tracks, int position) + { + Tracks = tracks; Position = position; } - public QueueTrack Track { get; set; } + public List Tracks { get; set; } + public int Position { get; set; } } } \ No newline at end of file diff --git a/Windows/Audiotica.Core.Windows/Utilities/AppSettingsUtility.cs b/Windows/Audiotica.Core.Windows/Utilities/AppSettingsUtility.cs index a6af8267..5acab05d 100644 --- a/Windows/Audiotica.Core.Windows/Utilities/AppSettingsUtility.cs +++ b/Windows/Audiotica.Core.Windows/Utilities/AppSettingsUtility.cs @@ -1,18 +1,36 @@ using Windows.Storage; +using Windows.UI.Xaml; +using Audiotica.Core.Common; using Audiotica.Core.Utilities.Interfaces; +using Audiotica.Core.Windows.Helpers; namespace Audiotica.Core.Windows.Utilities { - public class AppSettingsUtility : IAppSettingsUtility + public class AppSettingsUtility : ObservableObject, IAppSettingsUtility { + private readonly ISettingsUtility _settingsUtility; + private int _theme; + public AppSettingsUtility(ISettingsUtility settingsUtility) { + _settingsUtility = settingsUtility; DownloadsPath = settingsUtility.Read("DownloadsPath", "virtual://Music/Audiotica/"); TempDownloadsPath = settingsUtility.Read("TempDownloadsPath", ApplicationData.Current.TemporaryFolder.Path); + _theme = _settingsUtility.Read(ApplicationSettingsConstants.Theme, (int)ElementTheme.Default); } public string DownloadsPath { get; set; } public string TempDownloadsPath { get; } + + public int Theme + { + get { return _theme; } + set + { + Set(ref _theme, value); + _settingsUtility.Write(ApplicationSettingsConstants.Theme, value); + } + } } } \ No newline at end of file diff --git a/Windows/Audiotica.Windows.Player/ForegroundMessenger.cs b/Windows/Audiotica.Windows.Player/ForegroundMessenger.cs index c17ef4f8..104f0a91 100644 --- a/Windows/Audiotica.Windows.Player/ForegroundMessenger.cs +++ b/Windows/Audiotica.Windows.Player/ForegroundMessenger.cs @@ -28,7 +28,7 @@ public void Dispose() public event EventHandler SkipToPrev; public event EventHandler TrackChanged; public event EventHandler> UpdatePlaylist; - public event TypedEventHandler AddToPlaylist; + public event TypedEventHandler, int> AddToPlaylist; /// /// Raised when a message is recieved from the foreground app @@ -66,7 +66,7 @@ private void BackgroundMediaPlayer_MessageReceivedFromForeground(object sender, { var addMessage = message as AddToPlaylistMessage; if (addMessage != null) - AddToPlaylist?.Invoke(addMessage.Track, addMessage.Position); + AddToPlaylist?.Invoke(addMessage.Tracks, addMessage.Position); } } } diff --git a/Windows/Audiotica.Windows.Player/PlayerWrapper.cs b/Windows/Audiotica.Windows.Player/PlayerWrapper.cs index 51aca699..76073242 100644 --- a/Windows/Audiotica.Windows.Player/PlayerWrapper.cs +++ b/Windows/Audiotica.Windows.Player/PlayerWrapper.cs @@ -114,8 +114,14 @@ public void SkipToNext() /// public void SkipToPrev() { - _smtcWrapper.PlaybackStatus = MediaPlaybackStatus.Changing; - _mediaPlaybackList.MovePrevious(); + if (BackgroundMediaPlayer.Current.Position.TotalSeconds > 5) + BackgroundMediaPlayer.Current.Position = TimeSpan.Zero; + + else + { + _smtcWrapper.PlaybackStatus = MediaPlaybackStatus.Changing; + _mediaPlaybackList.MovePrevious(); + } // TODO: Work around playlist bug that doesn't continue playing after a switch; remove later BackgroundMediaPlayer.Current.Play(); @@ -156,23 +162,32 @@ public async void CreatePlaybackList(IEnumerable queues) _mediaPlaybackList.CurrentItemChanged += MediaPlaybackListOnCurrentItemChanged; } - public void AddToPlaybackList(QueueTrack queue, int position) + public async void AddToPlaybackList(List queue, int position) { if (_mediaPlaybackList == null || BackgroundMediaPlayer.Current.Source != _mediaPlaybackList) - CreatePlaybackList(new[] {queue}); + CreatePlaybackList(queue); else { - var source = MediaSource.CreateFromUri(new Uri(queue.Track.AudioWebUri)); - source.Queue(queue); - - if (position > -1 && position < _mediaPlaybackList.Items.Count) + foreach (var item in queue) { - _mediaPlaybackList.Items.Insert(position, new MediaPlaybackItem(source)); + MediaSource source; + if (item.Track.Type == TrackType.Stream) + source = MediaSource.CreateFromUri(new Uri(item.Track.AudioWebUri)); + else + { + source = MediaSource.CreateFromStorageFile( + await StorageHelper.GetFileFromPathAsync(item.Track.AudioLocalUri)); + } + source.Queue(item); + var playbackItem = new MediaPlaybackItem(source); + + if (position > -1 && position < _mediaPlaybackList.Items.Count) + _mediaPlaybackList.Items.Insert(position++, playbackItem); + else + _mediaPlaybackList.Items.Add(playbackItem); } - else - _mediaPlaybackList.Items.Add(new MediaPlaybackItem(source)); } } @@ -338,7 +353,7 @@ private void UnsubscribeFromMessenger() _foregroundMessenger.UpdatePlaylist -= ForegroundMessengerOnUpdatePlaylist; } - private void ForegroundMessengerOnAddToPlaylist(QueueTrack queueTrack, int position) + private void ForegroundMessengerOnAddToPlaylist(List queueTrack, int position) { AddToPlaybackList(queueTrack, position); } @@ -367,21 +382,16 @@ private void ForegroundMessengerOnUpdatePlaylist(object sender, List private void ForegroundMessengerOnTrackChanged(object sender, string queueId) { - var index = _mediaPlaybackList.Items.ToList().FindIndex(i => i.Source.Queue().Id == queueId); + var queue = _mediaPlaybackList.Items.Select(p => p.Source.Queue()).ToList(); + var index = queue.FindIndex(i => i.Id == queueId); + if (index < 0) return; Debug.WriteLine("Skipping to track " + index); _smtcWrapper.PlaybackStatus = MediaPlaybackStatus.Changing; - try - { - _mediaPlaybackList.MoveTo((uint) index); + _mediaPlaybackList.MoveTo((uint)index); - // TODO: Work around playlist bug that doesn't continue playing after a switch; remove later - BackgroundMediaPlayer.Current.Play(); - } - catch - { - // ignored - } + // TODO: Work around playlist bug that doesn't continue playing after a switch; remove later + BackgroundMediaPlayer.Current.Play(); } private void ForegroundMessengerOnStartPlayback(object sender, EventArgs eventArgs) diff --git a/Windows/Audiotica.Windows/App.xaml b/Windows/Audiotica.Windows/App.xaml index 5cb5e2aa..dcc6f3f2 100644 --- a/Windows/Audiotica.Windows/App.xaml +++ b/Windows/Audiotica.Windows/App.xaml @@ -35,6 +35,27 @@ + + + + + + + Multiple + + + Single + + + + + + Multiple + + + None + + diff --git a/Windows/Audiotica.Windows/App.xaml.cs b/Windows/Audiotica.Windows/App.xaml.cs index 28a357c3..697d06c4 100644 --- a/Windows/Audiotica.Windows/App.xaml.cs +++ b/Windows/Audiotica.Windows/App.xaml.cs @@ -14,11 +14,6 @@ sealed partial class App public App() { WindowsAppInitializer.InitializeAsync(); - - // Only the dark theme is supported in everything else (they only have light option) - if (!DeviceHelper.IsType(DeviceFamily.Mobile)) - RequestedTheme = ApplicationTheme.Dark; - InitializeComponent(); } @@ -80,7 +75,7 @@ private void OnVisibleBoundsChanged(ApplicationView sender, object args) RootFrame.Margin = new Thickness(left, 0, right, bottom); Shell.HamburgerPadding = new Thickness(left, 0, 0, 0); Shell.NavBarMargin = new Thickness(0, 0, 0, bottom); - Shell.Padding = new Thickness(left, top, 0, bottom); + Shell.Padding = new Thickness(left, top, 0, 0); } } } \ No newline at end of file diff --git a/Windows/Audiotica.Windows/AppEngine/Modules/UtilityModule.cs b/Windows/Audiotica.Windows/AppEngine/Modules/UtilityModule.cs index c9df2fd1..de7d0082 100644 --- a/Windows/Audiotica.Windows/AppEngine/Modules/UtilityModule.cs +++ b/Windows/Audiotica.Windows/AppEngine/Modules/UtilityModule.cs @@ -24,7 +24,7 @@ public override void LoadRunTime(ContainerBuilder builder) builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().As(); - builder.RegisterType().As(); + builder.RegisterType().As().SingleInstance(); builder.RegisterType().As(); } } diff --git a/Windows/Audiotica.Windows/Audiotica.Windows.csproj b/Windows/Audiotica.Windows/Audiotica.Windows.csproj index e6524465..969c6f3c 100644 --- a/Windows/Audiotica.Windows/Audiotica.Windows.csproj +++ b/Windows/Audiotica.Windows/Audiotica.Windows.csproj @@ -104,6 +104,8 @@ + + @@ -136,6 +138,9 @@ LibraryHeader.xaml + + SelectModeCommandBar.xaml + LibraryDictionary.xaml @@ -157,6 +162,8 @@ + + @@ -195,7 +202,10 @@ + + + @@ -214,6 +224,7 @@ + @@ -294,6 +305,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + MSBuild:Compile Designer diff --git a/Windows/Audiotica.Windows/Controls/LibraryHeader.xaml b/Windows/Audiotica.Windows/Controls/LibraryHeader.xaml index d324db2d..a5beee6b 100644 --- a/Windows/Audiotica.Windows/Controls/LibraryHeader.xaml +++ b/Windows/Audiotica.Windows/Controls/LibraryHeader.xaml @@ -11,13 +11,19 @@ mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> - + - - + + + + + + + + @@ -35,6 +41,12 @@ + + + + + + diff --git a/Windows/Audiotica.Windows/Controls/LibraryHeader.xaml.cs b/Windows/Audiotica.Windows/Controls/LibraryHeader.xaml.cs index b0adc3d8..800e75ba 100644 --- a/Windows/Audiotica.Windows/Controls/LibraryHeader.xaml.cs +++ b/Windows/Audiotica.Windows/Controls/LibraryHeader.xaml.cs @@ -20,8 +20,13 @@ public sealed partial class LibraryHeader new PropertyMetadata(0)); public static readonly DependencyProperty CurrentSortChangedCommandProperty = - DependencyProperty.Register("CurrentSortChangedCommand", typeof (ICommand), typeof (LibraryHeader), - new PropertyMetadata(0)); + DependencyProperty.Register("CurrentSortChangedCommand", typeof (ICommand), typeof (LibraryHeader), null); + + public static readonly DependencyProperty ShuffleAllCommandProperty = + DependencyProperty.Register("ShuffleAllCommand", typeof (ICommand), typeof (LibraryHeader), null); + + public static readonly DependencyProperty IsSelectModeProperty = + DependencyProperty.Register("IsSelectMode", typeof(bool?), typeof(LibraryHeader), new PropertyMetadata(false)); public LibraryHeader() { @@ -52,7 +57,20 @@ public ICommand CurrentSortChangedCommand set { SetValue(CurrentSortChangedCommandProperty, value); } } + public ICommand ShuffleAllCommand + { + get { return (ICommand) GetValue(ShuffleAllCommandProperty); } + set { SetValue(ShuffleAllCommandProperty, value); } + } + + public bool? IsSelectMode + { + get { return (bool?)GetValue(IsSelectModeProperty); } + set { SetValue(IsSelectModeProperty, value); } + } + public event EventHandler CurrentSortChanged; + public event EventHandler ShuffleAll; private static void SortItemsPropertyChangedCallback(DependencyObject o, DependencyPropertyChangedEventArgs e) { @@ -71,5 +89,11 @@ private void ListBox_OnSelectionChanged(object sender, SelectionChangedEventArgs CurrentSortChangedCommand?.Execute(item); } } + + private void ShuffleAll_Click(object sender, RoutedEventArgs e) + { + ShuffleAll?.Invoke(this, EventArgs.Empty); + ShuffleAllCommand?.Execute(EventArgs.Empty); + } } } \ No newline at end of file diff --git a/Windows/Audiotica.Windows/Controls/SelectModeCommandBar.xaml b/Windows/Audiotica.Windows/Controls/SelectModeCommandBar.xaml new file mode 100644 index 00000000..59d7a310 --- /dev/null +++ b/Windows/Audiotica.Windows/Controls/SelectModeCommandBar.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Windows/Audiotica.Windows/Controls/SelectModeCommandBar.xaml.cs b/Windows/Audiotica.Windows/Controls/SelectModeCommandBar.xaml.cs new file mode 100644 index 00000000..aafee0c9 --- /dev/null +++ b/Windows/Audiotica.Windows/Controls/SelectModeCommandBar.xaml.cs @@ -0,0 +1,143 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Windows.UI.Xaml; +using Audiotica.Core.Exceptions; +using Audiotica.Core.Extensions; +using Audiotica.Core.Utilities.Interfaces; +using Audiotica.Database.Models; +using Audiotica.Database.Services.Interfaces; +using Audiotica.Windows.Common; +using Audiotica.Windows.Services.Interfaces; +using Audiotica.Windows.Views; +using Autofac; + +namespace Audiotica.Windows.Controls +{ + public sealed partial class SelectModeCommandBar + { + public static readonly DependencyProperty SelectedItemsProperty = + DependencyProperty.Register("SelectedItems", typeof (ObservableCollection), + typeof (SelectModeCommandBar), null); + + public SelectModeCommandBar() + { + InitializeComponent(); + AppSettings = App.Current.Kernel.Resolve(); + DataContext = this; + } + + public IAppSettingsUtility AppSettings { get; } + + public bool IsCatalog { get; set; } + + public ObservableCollection SelectedItems + { + get { return (ObservableCollection) GetValue(SelectedItemsProperty); } + set { SetValue(SelectedItemsProperty, value); } + } + + private IEnumerable GetTracks() + { + List tracks; + + if (SelectedItems.FirstOrDefault() is Album) + tracks = SelectedItems.Cast().SelectMany(p => p.Tracks).ToList(); + else if (SelectedItems.FirstOrDefault() is Artist) + tracks = + SelectedItems.Cast() + .SelectMany(p => p.Tracks) + .Union(SelectedItems.Cast().SelectMany(p => p.TracksThatAppearsIn)) + .ToList(); + else + tracks = SelectedItems.Cast().ToList(); + return tracks; + } + + private async void Play_Click(object sender, RoutedEventArgs e) + { + using (var lifetimeScope = App.Current.Kernel.BeginScope()) + { + var playerService = lifetimeScope.Resolve(); + var tracks = GetTracks().Where(p => p.Status == TrackStatus.None || p.Status == TrackStatus.Downloading) + .ToList(); + await playerService.NewQueueAsync(tracks); + } + } + + private async void AddQueue_Click(object sender, RoutedEventArgs e) + { + using (var scope = App.Current.Kernel.BeginScope()) + { + var backgroundAudioService = scope.Resolve(); + try + { + var tracks = GetTracks() + .Where(p => p.Status == TrackStatus.None || p.Status == TrackStatus.Downloading) + .ToList(); + await backgroundAudioService.AddAsync(tracks); + CurtainPrompt.Show("Added to queue"); + } + catch (AppException ex) + { + CurtainPrompt.ShowError(ex.Message ?? "Something happened."); + } + } + } + + private async void AddUpNext_Click(object sender, RoutedEventArgs e) + { + using (var scope = App.Current.Kernel.BeginScope()) + { + var backgroundAudioService = scope.Resolve(); + try + { + var tracks = GetTracks() + .Where(p => p.Status == TrackStatus.None || p.Status == TrackStatus.Downloading) + .ToList(); + await backgroundAudioService.AddUpNextAsync(tracks); + CurtainPrompt.Show("Added up next"); + } + catch (AppException ex) + { + CurtainPrompt.ShowError(ex.Message ?? "Something happened."); + } + } + } + + private void Download_Click(object sender, RoutedEventArgs e) + { + using (var scope = App.Current.Kernel.BeginScope()) + { + var downloadService = scope.Resolve(); + var tracks = GetTracks().Where(p => p.IsDownloadable); + foreach (var track in tracks) + downloadService.StartDownloadAsync(track); + } + } + + private async void Delete_Click(object sender, RoutedEventArgs e) + { + using (var scope = App.Current.Kernel.BeginScope()) + { + var libraryService = scope.Resolve(); + var tracks = GetTracks().ToList(); + foreach (var track in tracks) + await libraryService.DeleteTrackAsync(track); + + // make sure to navigate away if album turns out empty + if (!IsCatalog && App.Current.NavigationService.CurrentPageType == typeof (AlbumPage)) + { + if ( + tracks.Select( + track => + libraryService.Albums.FirstOrDefault(p => p.Title.EqualsIgnoreCase(track.AlbumTitle))) + .Any(album => album == null)) + { + App.Current.NavigationService.GoBack(); + } + } + } + } + } +} \ No newline at end of file diff --git a/Windows/Audiotica.Windows/Controls/TrackNarrowViewer.xaml b/Windows/Audiotica.Windows/Controls/TrackNarrowViewer.xaml index b5d2be58..c0a4eb74 100644 --- a/Windows/Audiotica.Windows/Controls/TrackNarrowViewer.xaml +++ b/Windows/Audiotica.Windows/Controls/TrackNarrowViewer.xaml @@ -9,7 +9,7 @@ d:DesignHeight="300" d:DesignWidth="400"> - + @@ -38,7 +38,7 @@ - + + + + diff --git a/Windows/Audiotica.Windows/Controls/TrackNarrowViewer.xaml.cs b/Windows/Audiotica.Windows/Controls/TrackNarrowViewer.xaml.cs index 59ab7992..e14c948d 100644 --- a/Windows/Audiotica.Windows/Controls/TrackNarrowViewer.xaml.cs +++ b/Windows/Audiotica.Windows/Controls/TrackNarrowViewer.xaml.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Input; @@ -16,7 +18,7 @@ namespace Audiotica.Windows.Controls { // TODO: find a way to get state triggers to work on usercontrol, then we won't need a seperate control _sight_ (hopefully just a bug on the current SDK) - public sealed partial class TrackNarrowViewer + public sealed partial class TrackNarrowViewer: INotifyPropertyChanged { public static readonly DependencyProperty IsSelectedProperty = DependencyProperty.Register("IsSelected", typeof (bool), typeof (TrackNarrowViewer), null); @@ -24,13 +26,31 @@ public sealed partial class TrackNarrowViewer public static readonly DependencyProperty IsCatalogProperty = DependencyProperty.Register("IsCatalog", typeof(bool), typeof(TrackViewer), null); + + public static readonly DependencyProperty IsQueueProperty = + DependencyProperty.Register("IsQueue", typeof(bool), typeof(TrackViewer), null); + + public static readonly DependencyProperty QueueIdProperty = + DependencyProperty.Register("QueueId", typeof(string), typeof(TrackViewer), null); + private Track _track; + private bool _isPlaying; public TrackNarrowViewer() { InitializeComponent(); } + public bool IsPlaying + { + get { return _isPlaying; } + set + { + _isPlaying = value; + OnPropertyChanged(); + } + } + public bool IsSelected { @@ -54,9 +74,48 @@ public Track Track { _track = value; Bindings.Update(); + TrackChanged(); + } + } + + public bool IsQueue + + { + get { return (bool)GetValue(IsQueueProperty); } + + set { SetValue(IsQueueProperty, value); } + } + + public string QueueId + { + get { return (string)GetValue(QueueIdProperty); } + + set { SetValue(QueueIdProperty, value); } + } + + private void TrackChanged() + { + var player = App.Current.Kernel.Resolve(); + + if (Track == null) + IsPlaying = false; + else + { + if (IsQueue && QueueId != null) + IsPlaying = player.CurrentQueueId == QueueId; + else if (!IsQueue && player.CurrentQueueTrack?.Track != null) + IsPlaying = (Track.Id > 0 && player.CurrentQueueTrack.Track.Id == Track.Id) + || TrackComparer.AreEqual(player.CurrentQueueTrack.Track, Track); + else + IsPlaying = false; } + + player.TrackChanged -= PlayerOnTrackChanged; + player.TrackChanged += PlayerOnTrackChanged; } + private void PlayerOnTrackChanged(object sender, string s) => TrackChanged(); + private async void PlayButton_Click(object sender, RoutedEventArgs e) { using (var lifetimeScope = App.Current.Kernel.BeginScope()) @@ -64,10 +123,11 @@ private async void PlayButton_Click(object sender, RoutedEventArgs e) var playerService = lifetimeScope.Resolve(); try { - var queue = await playerService.AddAsync(Track); + var queue = playerService.ContainsTrack(Track) ?? await playerService.AddAsync(Track); // player auto plays when there is only one track if (playerService.PlaybackQueue.Count > 1) playerService.Play(queue); + IsSelected = false; } catch (AppException ex) { @@ -176,5 +236,12 @@ private async void Delete_Click(object sender, RoutedEventArgs e) } } } + + public event PropertyChangedEventHandler PropertyChanged; + + private void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } } } \ No newline at end of file diff --git a/Windows/Audiotica.Windows/Controls/TrackViewer.xaml b/Windows/Audiotica.Windows/Controls/TrackViewer.xaml index 1bbca115..1d769ee7 100644 --- a/Windows/Audiotica.Windows/Controls/TrackViewer.xaml +++ b/Windows/Audiotica.Windows/Controls/TrackViewer.xaml @@ -9,7 +9,7 @@ d:DesignHeight="300" d:DesignWidth="400"> - + @@ -18,8 +18,8 @@ Visibility="{x:Bind Track.IsFromLibrary, Mode=OneWay, Converter={StaticResource ReverseVisibilityConverter}}" /> - - + + - + + + + diff --git a/Windows/Audiotica.Windows/Controls/TrackViewer.xaml.cs b/Windows/Audiotica.Windows/Controls/TrackViewer.xaml.cs index ed3ed140..b74c5cab 100644 --- a/Windows/Audiotica.Windows/Controls/TrackViewer.xaml.cs +++ b/Windows/Audiotica.Windows/Controls/TrackViewer.xaml.cs @@ -1,13 +1,9 @@ -using System; +using System.ComponentModel; using System.Linq; -using Windows.Foundation; -using Windows.UI; -using Windows.UI.Popups; +using System.Runtime.CompilerServices; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Controls.Primitives; using Windows.UI.Xaml.Input; -using Windows.UI.Xaml.Media; using Audiotica.Core.Exceptions; using Audiotica.Core.Extensions; using Audiotica.Core.Windows.Helpers; @@ -15,19 +11,26 @@ using Audiotica.Database.Services.Interfaces; using Audiotica.Windows.Common; using Audiotica.Windows.Services.Interfaces; -using Audiotica.Windows.Services.NavigationService; using Audiotica.Windows.Views; using Autofac; namespace Audiotica.Windows.Controls { - public sealed partial class TrackViewer + public sealed partial class TrackViewer : INotifyPropertyChanged { public static readonly DependencyProperty IsSelectedProperty = DependencyProperty.Register("IsSelected", typeof (bool), typeof (TrackViewer), null); public static readonly DependencyProperty IsCatalogProperty = - DependencyProperty.Register("IsCatalog", typeof(bool), typeof(TrackViewer), null); + DependencyProperty.Register("IsCatalog", typeof (bool), typeof (TrackViewer), null); + + public static readonly DependencyProperty IsQueueProperty = + DependencyProperty.Register("IsQueue", typeof (bool), typeof (TrackViewer), null); + + public static readonly DependencyProperty QueueIdProperty = + DependencyProperty.Register("QueueId", typeof(string), typeof(TrackViewer), null); + + private bool _isPlaying; private Track _track; @@ -36,6 +39,16 @@ public TrackViewer() InitializeComponent(); } + public bool IsPlaying + { + get { return _isPlaying; } + set + { + _isPlaying = value; + OnPropertyChanged(); + } + } + public bool IsSelected { @@ -52,6 +65,21 @@ public bool IsCatalog set { SetValue(IsCatalogProperty, value); } } + public bool IsQueue + + { + get { return (bool) GetValue(IsQueueProperty); } + + set { SetValue(IsQueueProperty, value); } + } + + public string QueueId + { + get { return (string)GetValue(QueueIdProperty); } + + set { SetValue(QueueIdProperty, value); } + } + public Track Track { get { return _track; } @@ -59,9 +87,35 @@ public Track Track { _track = value; Bindings.Update(); + TrackChanged(); } } + public event PropertyChangedEventHandler PropertyChanged; + + private void TrackChanged() + { + var player = App.Current.Kernel.Resolve(); + + if (Track == null) + IsPlaying = false; + else + { + if (IsQueue && QueueId != null) + IsPlaying = player.CurrentQueueId == QueueId; + else if (!IsQueue && player.CurrentQueueTrack?.Track != null) + IsPlaying = (Track.Id > 0 && player.CurrentQueueTrack.Track.Id == Track.Id) + || TrackComparer.AreEqual(player.CurrentQueueTrack.Track, Track); + else + IsPlaying = false; + } + + player.TrackChanged -= PlayerOnTrackChanged; + player.TrackChanged += PlayerOnTrackChanged; + } + + private void PlayerOnTrackChanged(object sender, string s) => TrackChanged(); + private async void PlayButton_Click(object sender, RoutedEventArgs e) { using (var lifetimeScope = App.Current.Kernel.BeginScope()) @@ -69,10 +123,11 @@ private async void PlayButton_Click(object sender, RoutedEventArgs e) var playerService = lifetimeScope.Resolve(); try { - var queue = await playerService.AddAsync(Track); + var queue = playerService.ContainsTrack(Track) ?? await playerService.AddAsync(Track); // player auto plays when there is only one track if (playerService.PlaybackQueue.Count > 1) playerService.Play(queue); + IsSelected = false; } catch (AppException ex) { @@ -97,7 +152,7 @@ private async void AddCollection_Click(object sender, RoutedEventArgs e) catch (AppException ex) { Track.Status = TrackStatus.None; - CurtainPrompt.ShowError(ex.Message ?? "Problem saving song."); + CurtainPrompt.ShowError(ex.Message ?? "Problem saving: " + Track); } finally { @@ -144,16 +199,11 @@ private void Viewer_RightTapped(object sender, RightTappedRoutedEventArgs e) { var grid = (Grid) sender; FlyoutEx.ShowAttachedFlyoutAtPointer(grid); - } private void ExploreArtist_Click(object sender, RoutedEventArgs e) { - using (var scope = App.Current.Kernel.BeginScope()) - { - var navigationService = scope.Resolve(); - navigationService.Navigate(typeof (ArtistPage), Track.DisplayArtist); - } + App.Current.NavigationService.Navigate(typeof (ArtistPage), Track.DisplayArtist); } private void Download_Click(object sender, RoutedEventArgs e) @@ -181,5 +231,10 @@ private async void Delete_Click(object sender, RoutedEventArgs e) } } } + + private void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } } } \ No newline at end of file diff --git a/Windows/Audiotica.Windows/DataTemplates/LibraryDictionary.xaml b/Windows/Audiotica.Windows/DataTemplates/LibraryDictionary.xaml index 7100e9ce..4f46137c 100644 --- a/Windows/Audiotica.Windows/DataTemplates/LibraryDictionary.xaml +++ b/Windows/Audiotica.Windows/DataTemplates/LibraryDictionary.xaml @@ -5,116 +5,188 @@ xmlns:controls="using:Audiotica.Windows.Controls" xmlns:databaseModels="using:Audiotica.Database.Models" xmlns:webModels="using:Audiotica.Web.Models"> - + + IsSelected="{Binding Tag, Mode=TwoWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" /> - + + IsSelected="{Binding Tag, Mode=TwoWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" /> + IsSelected="{Binding Tag, Mode=TwoWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" /> + IsSelected="{Binding Tag, Mode=TwoWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" /> - - - - - - - - + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - + MaxHeight="40" LineHeight="20" LineStackingStrategy="BlockLineHeight" /> + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -153,8 +225,9 @@ - + @@ -167,12 +240,12 @@ + ImageSource="{x:Bind Artwork, Converter={StaticResource ImageSourceConverter}, ConverterParameter=150}" /> + VerticalAlignment="Center" TextAlignment="Center" + TextTrimming="CharacterEllipsis" /> \ No newline at end of file diff --git a/Windows/Audiotica.Windows/DataTemplates/LibraryDictionary.xaml.cs b/Windows/Audiotica.Windows/DataTemplates/LibraryDictionary.xaml.cs index 83bba4b4..fb319bce 100644 --- a/Windows/Audiotica.Windows/DataTemplates/LibraryDictionary.xaml.cs +++ b/Windows/Audiotica.Windows/DataTemplates/LibraryDictionary.xaml.cs @@ -1,4 +1,19 @@ -namespace Audiotica.Windows.DataTemplates +using System.Collections.Generic; +using System.Linq; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Input; +using Audiotica.Core.Exceptions; +using Audiotica.Core.Extensions; +using Audiotica.Core.Windows.Helpers; +using Audiotica.Database.Models; +using Audiotica.Database.Services.Interfaces; +using Audiotica.Windows.Common; +using Audiotica.Windows.Services.Interfaces; +using Audiotica.Windows.Views; +using Autofac; + +namespace Audiotica.Windows.DataTemplates { public sealed partial class LibraryDictionary { @@ -6,5 +21,109 @@ public LibraryDictionary() { InitializeComponent(); } + + private void Panel_RightTapped(object sender, RightTappedRoutedEventArgs e) + { + var panel = (Grid) sender; + FlyoutEx.ShowAttachedFlyoutAtPointer(panel); + } + + private static IEnumerable GetTracks(object item) + { + List tracks; + + if (item is Album) + tracks = item.As().Tracks.ToList(); + else + tracks = + item.As().Tracks.Union(item.As().TracksThatAppearsIn) + .ToList(); + return tracks; + } + + private async void PlayButton_Click(object sender, RoutedEventArgs e) + { + var item = ((FrameworkElement) sender).DataContext; + using (var lifetimeScope = App.Current.Kernel.BeginScope()) + { + var playerService = lifetimeScope.Resolve(); + var tracks = GetTracks(item) + .Where(p => p.Status == TrackStatus.None || p.Status == TrackStatus.Downloading) + .ToList(); + await playerService.NewQueueAsync(tracks); + } + } + + private async void AddQueue_Click(object sender, RoutedEventArgs e) + { + var item = ((FrameworkElement) sender).DataContext; + using (var scope = App.Current.Kernel.BeginScope()) + { + var backgroundAudioService = scope.Resolve(); + try + { + var tracks = GetTracks(item) + .Where(p => p.Status == TrackStatus.None || p.Status == TrackStatus.Downloading) + .ToList(); + await backgroundAudioService.AddAsync(tracks); + CurtainPrompt.Show("Added to queue"); + } + catch (AppException ex) + { + CurtainPrompt.ShowError(ex.Message ?? "Something happened."); + } + } + } + + private async void AddUpNext_Click(object sender, RoutedEventArgs e) + { + var item = ((FrameworkElement) sender).DataContext; + using (var scope = App.Current.Kernel.BeginScope()) + { + var backgroundAudioService = scope.Resolve(); + try + { + var tracks = GetTracks(item) + .Where(p => p.Status == TrackStatus.None || p.Status == TrackStatus.Downloading) + .ToList(); + await backgroundAudioService.AddUpNextAsync(tracks); + CurtainPrompt.Show("Added up next"); + } + catch (AppException ex) + { + CurtainPrompt.ShowError(ex.Message ?? "Something happened."); + } + } + } + + private void Download_Click(object sender, RoutedEventArgs e) + { + var item = ((FrameworkElement) sender).DataContext; + using (var scope = App.Current.Kernel.BeginScope()) + { + var downloadService = scope.Resolve(); + var tracks = GetTracks(item).Where(p => p.IsDownloadable); + foreach (var track in tracks) + downloadService.StartDownloadAsync(track); + } + } + + private async void Delete_Click(object sender, RoutedEventArgs e) + { + var item = ((FrameworkElement) sender).DataContext; + using (var scope = App.Current.Kernel.BeginScope()) + { + var libraryService = scope.Resolve(); + var tracks = GetTracks(item); + foreach (var track in tracks) + await libraryService.DeleteTrackAsync(track); + } + } + + private void ExploreArtist_Click(object sender, RoutedEventArgs e) + { + var item = (Album) ((FrameworkElement) sender).DataContext; + App.Current.NavigationService.Navigate(typeof (ArtistPage), item.Artist.Name); + } } } \ No newline at end of file diff --git a/Windows/Audiotica.Windows/DeviceFamily-Mobile/Shell.xaml b/Windows/Audiotica.Windows/DeviceFamily-Mobile/Shell.xaml index a3a672be..98135877 100644 --- a/Windows/Audiotica.Windows/DeviceFamily-Mobile/Shell.xaml +++ b/Windows/Audiotica.Windows/DeviceFamily-Mobile/Shell.xaml @@ -6,6 +6,7 @@ xmlns:views="using:Audiotica.Windows.Views" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + RequestedTheme="{Binding AppSettings.Theme, Converter={StaticResource IntToThemeConverter}}" mc:Ignorable="d"> diff --git a/Windows/Audiotica.Windows/Extensions/ListViewBindableSelectionHandler.cs b/Windows/Audiotica.Windows/Extensions/ListViewBindableSelectionHandler.cs new file mode 100644 index 00000000..e00c31bb --- /dev/null +++ b/Windows/Audiotica.Windows/Extensions/ListViewBindableSelectionHandler.cs @@ -0,0 +1,108 @@ +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using Windows.UI.Xaml.Controls; +using Audiotica.Core.Helpers; + +namespace Audiotica.Windows.Extensions +{ + /// + /// Handles synchronization of ListViewExtensions.BindableSelection to a ListView. + /// + public class ListViewBindableSelectionHandler + { + private readonly NotifyCollectionChangedEventHandler _handler; + private ObservableCollection _boundSelection; + private ListViewBase _listView; + + /// + /// Initializes a new instance of the class. + /// + /// The ListView. + /// The bound selection. + public ListViewBindableSelectionHandler( + ListViewBase listView, ObservableCollection boundSelection) + { + _handler = OnBoundSelectionChanged; + Attach(listView, boundSelection); + } + + private void Attach(ListViewBase listView, ObservableCollection boundSelection) + { + _listView = listView; + _listView.SelectionChanged += OnListViewSelectionChanged; + _boundSelection = boundSelection; + + foreach (var item in _boundSelection.Where(item => !_listView.SelectedItems.Contains(item))) + { + _listView.SelectedItems.Add(item); + } + + _boundSelection.CollectionChanged += OnBoundSelectionChanged; + } + + private void OnListViewSelectionChanged( + object sender, SelectionChangedEventArgs e) + { + foreach (var item in e.RemovedItems.Where(item => _boundSelection.Contains(item))) + { + _boundSelection.Remove(item); + } + + foreach (var item in e.AddedItems.Where(item => !_boundSelection.Contains(item))) + { + _boundSelection.Add(item); + } + } + + private void OnBoundSelectionChanged( + object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == + NotifyCollectionChangedAction.Reset) + { + _listView.SelectedItems.Clear(); + + foreach (var item in _boundSelection) + { + if (!_listView.SelectedItems.Contains(item)) + { + _listView.SelectedItems.Add(item); + } + } + + return; + } + + try + { + if (e.OldItems != null) + { + foreach (var item in e.OldItems.Cast().Where(item => _listView.SelectedItems.Contains(item)) + ) + { + _listView.SelectedItems.Remove(item); + } + } + + if (e.NewItems != null) + { + foreach ( + var item in e.NewItems.Cast().Where(item => !_listView.SelectedItems.Contains(item))) + { + _listView.SelectedItems.Add(item); + } + } + } + catch { } + } + + internal void Detach() + { + _listView.SelectionChanged -= OnListViewSelectionChanged; + _listView = null; + _boundSelection.CollectionChanged -= _handler; + _boundSelection = null; + } + } +} \ No newline at end of file diff --git a/Windows/Audiotica.Windows/Extensions/ListViewExtensions.cs b/Windows/Audiotica.Windows/Extensions/ListViewExtensions.cs new file mode 100644 index 00000000..3a69ae04 --- /dev/null +++ b/Windows/Audiotica.Windows/Extensions/ListViewExtensions.cs @@ -0,0 +1,186 @@ +using System.Collections.ObjectModel; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Audiotica.Windows.Extensions +{ + /// + /// Extension methods and attached properties for the ListView class. + /// + public static class ListViewExtensions + { + /// + /// Scrolls a vertical ListView to the bottom. + /// + /// + public static void ScrollToBottom(this ListView listView) + { + var scrollViewer = listView.GetFirstDescendantOfType(); + scrollViewer.ChangeView(null, scrollViewer.ScrollableHeight, null); + } + + #region BindableSelection + + /// + /// BindableSelection Attached Dependency Property + /// + public static readonly DependencyProperty BindableSelectionProperty = + DependencyProperty.RegisterAttached( + "BindableSelection", + typeof (ObservableCollection), + typeof (ListViewExtensions), + new PropertyMetadata(null, OnBindableSelectionChanged)); + + /// + /// Gets the BindableSelection property. This dependency property + /// indicates the list of selected items that is synchronized + /// with the items selected in the ListView. + /// + public static ObservableCollection GetBindableSelection(DependencyObject d) + { + return (ObservableCollection) d.GetValue(BindableSelectionProperty); + } + + /// + /// Sets the BindableSelection property. This dependency property + /// indicates the list of selected items that is synchronized + /// with the items selected in the ListView. + /// + public static void SetBindableSelection( + DependencyObject d, + ObservableCollection value) + { + d.SetValue(BindableSelectionProperty, value); + } + + /// + /// Handles changes to the BindableSelection property. + /// + /// + /// The on which + /// the property has changed value. + /// + /// + /// Event data that is issued by any event that + /// tracks changes to the effective value of this property. + /// + private static void OnBindableSelectionChanged( + DependencyObject d, + DependencyPropertyChangedEventArgs e) + { + var oldBindableSelection = e.OldValue; + var newBindableSelection = GetBindableSelection(d); + + if (oldBindableSelection != null) + { + var handler = GetBindableSelectionHandler(d); + SetBindableSelectionHandler(d, null); + handler.Detach(); + } + + if (newBindableSelection != null) + { + var handler = new ListViewBindableSelectionHandler( + (ListViewBase) d, newBindableSelection); + SetBindableSelectionHandler(d, handler); + } + } + + #endregion + + #region BindableSelectionHandler + + /// + /// BindableSelectionHandler Attached Dependency Property + /// + public static readonly DependencyProperty BindableSelectionHandlerProperty = + DependencyProperty.RegisterAttached( + "BindableSelectionHandler", + typeof (ListViewBindableSelectionHandler), + typeof (ListViewExtensions), + new PropertyMetadata(null)); + + /// + /// Gets the BindableSelectionHandler property. This dependency property + /// indicates BindableSelectionHandler for a ListView - used + /// to manage synchronization of BindableSelection and SelectedItems. + /// + public static ListViewBindableSelectionHandler GetBindableSelectionHandler( + DependencyObject d) + { + return + (ListViewBindableSelectionHandler) + d.GetValue(BindableSelectionHandlerProperty); + } + + /// + /// Sets the BindableSelectionHandler property. This dependency property + /// indicates BindableSelectionHandler for a ListView - used to manage synchronization of BindableSelection and + /// SelectedItems. + /// + public static void SetBindableSelectionHandler( + DependencyObject d, + ListViewBindableSelectionHandler value) + { + d.SetValue(BindableSelectionHandlerProperty, value); + } + + #endregion + + #region ItemToBringIntoView + + /// + /// ItemToBringIntoView Attached Dependency Property + /// + public static readonly DependencyProperty ItemToBringIntoViewProperty = + DependencyProperty.RegisterAttached( + "ItemToBringIntoView", + typeof (object), + typeof (ListViewExtensions), + new PropertyMetadata(null, OnItemToBringIntoViewChanged)); + + /// + /// Gets the ItemToBringIntoView property. This dependency property + /// indicates the item that should be brought into view. + /// + public static object GetItemToBringIntoView(DependencyObject d) + { + return d.GetValue(ItemToBringIntoViewProperty); + } + + /// + /// Sets the ItemToBringIntoView property. This dependency property + /// indicates the item that should be brought into view when first set. + /// + public static void SetItemToBringIntoView(DependencyObject d, object value) + { + d.SetValue(ItemToBringIntoViewProperty, value); + } + + /// + /// Handles changes to the ItemToBringIntoView property. + /// + /// + /// The on which + /// the property has changed value. + /// + /// + /// Event data that is issued by any event that + /// tracks changes to the effective value of this property. + /// + private static void OnItemToBringIntoViewChanged( + DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var newItemToBringIntoView = + d.GetValue(ItemToBringIntoViewProperty); + + if (newItemToBringIntoView != null) + { + var listView = (ListView) d; + listView.ScrollIntoView(newItemToBringIntoView); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Windows/Audiotica.Windows/Package.StoreAssociation.xml b/Windows/Audiotica.Windows/Package.StoreAssociation.xml index 69c2bd10..ab0338f5 100644 --- a/Windows/Audiotica.Windows/Package.StoreAssociation.xml +++ b/Windows/Audiotica.Windows/Package.StoreAssociation.xml @@ -27,6 +27,7 @@ + @@ -92,6 +93,7 @@ + @@ -155,6 +157,7 @@ + @@ -274,6 +277,7 @@ + @@ -323,6 +327,7 @@ + @@ -334,6 +339,7 @@ + @@ -343,6 +349,8 @@ + + diff --git a/Windows/Audiotica.Windows/Package.appxmanifest b/Windows/Audiotica.Windows/Package.appxmanifest index c1f377ff..1d5ac711 100644 --- a/Windows/Audiotica.Windows/Package.appxmanifest +++ b/Windows/Audiotica.Windows/Package.appxmanifest @@ -1,6 +1,6 @@  - + Audiotica diff --git a/Windows/Audiotica.Windows/Services/DesignTime/DesignPlayerService.cs b/Windows/Audiotica.Windows/Services/DesignTime/DesignPlayerService.cs index d68067af..18772556 100644 --- a/Windows/Audiotica.Windows/Services/DesignTime/DesignPlayerService.cs +++ b/Windows/Audiotica.Windows/Services/DesignTime/DesignPlayerService.cs @@ -19,6 +19,7 @@ public DesignPlayerService() public bool IsBackgroundTaskRunning { get; } public MediaPlayerState CurrentState { get; set; } public string CurrentQueueId { get; } + public QueueTrack CurrentQueueTrack { get; } public OptimizedObservableCollection PlaybackQueue { get; } public event EventHandler MediaStateChanged; public event EventHandler TrackChanged; @@ -34,11 +35,21 @@ public void PlayOrPause() throw new NotImplementedException(); } + public QueueTrack ContainsTrack(Track track) + { + throw new NotImplementedException(); + } + public Task AddAsync(Track track, int position = -1) { throw new NotImplementedException(); } + public Task AddAsync(IEnumerable tracks, int position = -1) + { + throw new NotImplementedException(); + } + public Task AddAsync(WebSong webSong, int position = -1) { throw new NotImplementedException(); @@ -49,12 +60,17 @@ public Task AddUpNextAsync(Track track) throw new NotImplementedException(); } + public Task AddUpNextAsync(IEnumerable tracks) + { + throw new NotImplementedException(); + } + public Task AddUpNextAsync(WebSong webSong) { throw new NotImplementedException(); } - public Task NewQueueAsync(List tracks) + public Task NewQueueAsync(IEnumerable tracks) { throw new NotImplementedException(); } diff --git a/Windows/Audiotica.Windows/Services/Interfaces/IPlayerService.cs b/Windows/Audiotica.Windows/Services/Interfaces/IPlayerService.cs index 851088ce..77c4ebb3 100644 --- a/Windows/Audiotica.Windows/Services/Interfaces/IPlayerService.cs +++ b/Windows/Audiotica.Windows/Services/Interfaces/IPlayerService.cs @@ -13,6 +13,7 @@ public interface IPlayerService bool IsBackgroundTaskRunning { get; } MediaPlayerState CurrentState { get; set; } string CurrentQueueId { get; } + QueueTrack CurrentQueueTrack { get; } OptimizedObservableCollection PlaybackQueue { get; } event EventHandler MediaStateChanged; event EventHandler TrackChanged; @@ -27,6 +28,7 @@ public interface IPlayerService /// void PlayOrPause(); + QueueTrack ContainsTrack(Track track); /// /// Adds the specified track to the queue. /// @@ -34,11 +36,13 @@ public interface IPlayerService /// The position. /// Task AddAsync(Track track, int position = -1); + Task AddAsync(IEnumerable tracks, int position = -1); Task AddAsync(WebSong webSong, int position = -1); Task AddUpNextAsync(Track track); + Task AddUpNextAsync(IEnumerable tracks); Task AddUpNextAsync(WebSong webSong); - Task NewQueueAsync(List tracks); + Task NewQueueAsync(IEnumerable tracks); /// diff --git a/Windows/Audiotica.Windows/Services/RunTime/PlayerService.cs b/Windows/Audiotica.Windows/Services/RunTime/PlayerService.cs index b273464a..c643fedd 100644 --- a/Windows/Audiotica.Windows/Services/RunTime/PlayerService.cs +++ b/Windows/Audiotica.Windows/Services/RunTime/PlayerService.cs @@ -59,7 +59,7 @@ public bool IsBackgroundTaskRunning public string CurrentQueueId => _settingsUtility.Read(ApplicationSettingsConstants.QueueId, string.Empty); - + public QueueTrack CurrentQueueTrack { get; private set; } public OptimizedObservableCollection PlaybackQueue { get; private set; } public event EventHandler MediaStateChanged; public event EventHandler TrackChanged; @@ -77,10 +77,20 @@ public bool StartBackgroundTask() return started; } + public QueueTrack ContainsTrack(Track track) => PlaybackQueue.FirstOrDefault(p => track.Id == p.Track.Id); + public async Task AddAsync(Track track, int position = -1) { await PrepareTrackAsync(track); - return Add(track, position); + return await InternalAddAsync(track, position); + } + + public async Task AddAsync(IEnumerable tracks, int position = -1) + { + var arr = tracks.ToArray(); + foreach (var track in arr) + await PrepareTrackAsync(track); + Add(arr, position); } public async Task AddAsync(WebSong webSong, int position = -1) @@ -89,10 +99,16 @@ public async Task AddAsync(WebSong webSong, int position = -1) return await AddAsync(track, position); } - public async Task AddUpNextAsync(Track track) + public Task AddUpNextAsync(Track track) + { + var currentPosition = PlaybackQueue.IndexOf(PlaybackQueue.FirstOrDefault(p => p.Id == CurrentQueueId)); + return AddAsync(track, currentPosition + 1); + } + + public async Task AddUpNextAsync(IEnumerable tracks) { var currentPosition = PlaybackQueue.IndexOf(PlaybackQueue.FirstOrDefault(p => p.Id == CurrentQueueId)); - return await AddAsync(track, currentPosition + 1); + await AddAsync(tracks, currentPosition + 1); } public async Task AddUpNextAsync(WebSong webSong) @@ -101,11 +117,13 @@ public async Task AddUpNextAsync(WebSong webSong) return await AddUpNextAsync(track); } - public async Task NewQueueAsync(List tracks) + public async Task NewQueueAsync(IEnumerable tracks) { - foreach (var track in tracks) + var arr = tracks.ToArray(); + foreach (var track in arr) await PrepareTrackAsync(track); - var newQueue = tracks.Select(track => new QueueTrack(track)).ToList(); + var newQueue = arr.Select(track => new QueueTrack(track)).ToList(); + PlaybackQueue.SwitchTo(newQueue); MessageHelper.SendMessageToBackground(new UpdatePlaylistMessage(newQueue)); } @@ -238,17 +256,32 @@ private void UpdatePlaybackQueue() } } - private QueueTrack Add(Track track, int position = -1) + private async Task InternalAddAsync(Track track, int position = -1) { var queue = new QueueTrack(track); - MessageHelper.SendMessageToBackground(new AddToPlaylistMessage(queue, position)); if (position > -1 && position < PlaybackQueue.Count) PlaybackQueue.Insert(position, queue); else PlaybackQueue.Add(queue); + MessageHelper.SendMessageToBackground(new AddToPlaylistMessage(queue, position)); + await Task.Delay(50); return queue; } + private void Add(IEnumerable tracks, int position = -1) + { + var index = position; + var queue = tracks.Select(track => new QueueTrack(track)).ToList(); + if (index > -1 && index < PlaybackQueue.Count) + foreach (var item in queue) + { + PlaybackQueue.Insert(index++, item); + } + else + PlaybackQueue.AddRange(queue); + MessageHelper.SendMessageToBackground(new AddToPlaylistMessage(queue, position)); + } + private async Task PrepareTrackAsync(Track track) { switch (track.Status) @@ -317,6 +350,7 @@ private async void BackgroundMediaPlayer_MessageReceivedFromBackground(object se if (message is TrackChangedMessage) { var trackChangedMessage = message as TrackChangedMessage; + CurrentQueueTrack = PlaybackQueue.FirstOrDefault(p => p.Id == trackChangedMessage.QueueId); // When foreground app is active change track based on background message await _dispatcherUtility.RunAsync( () => { TrackChanged?.Invoke(sender, trackChangedMessage.QueueId); }); diff --git a/Windows/Audiotica.Windows/Shell.xaml b/Windows/Audiotica.Windows/Shell.xaml index 0ad3e836..e643353c 100644 --- a/Windows/Audiotica.Windows/Shell.xaml +++ b/Windows/Audiotica.Windows/Shell.xaml @@ -6,105 +6,111 @@ xmlns:views="using:Audiotica.Windows.Views" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + RequestedTheme="{Binding AppSettings.Theme, Converter={StaticResource IntToThemeConverter}}" mc:Ignorable="d"> - - - - - - + + \ No newline at end of file diff --git a/Windows/Audiotica.Windows/Styles/TextStyles.xaml b/Windows/Audiotica.Windows/Styles/TextStyles.xaml index 2b1e4853..fcd9150a 100644 --- a/Windows/Audiotica.Windows/Styles/TextStyles.xaml +++ b/Windows/Audiotica.Windows/Styles/TextStyles.xaml @@ -37,10 +37,15 @@ - + + \ No newline at end of file diff --git a/Windows/Audiotica.Windows/Tools/Converters/ContentConverter.cs b/Windows/Audiotica.Windows/Tools/Converters/ContentConverter.cs index 14166370..2640b8c2 100644 --- a/Windows/Audiotica.Windows/Tools/Converters/ContentConverter.cs +++ b/Windows/Audiotica.Windows/Tools/Converters/ContentConverter.cs @@ -10,6 +10,7 @@ public class ContentConverter : IValueConverter public object Convert(object value, Type targetType, object parameter, string language) { + if (!(value is bool)) return FalseContent; return ((bool) value) ? TrueContent : FalseContent; } diff --git a/Windows/Audiotica.Windows/Tools/Converters/IntToBoolConverter.cs b/Windows/Audiotica.Windows/Tools/Converters/IntToBoolConverter.cs new file mode 100644 index 00000000..ce883d76 --- /dev/null +++ b/Windows/Audiotica.Windows/Tools/Converters/IntToBoolConverter.cs @@ -0,0 +1,20 @@ +using System; +using Windows.UI.Xaml.Data; + +namespace Audiotica.Windows.Tools.Converters +{ + public class IntToBoolConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + var i = (int) value; + return i == 2; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + var i = (bool) value; + return i ? 2 : 1; + } + } +} \ No newline at end of file diff --git a/Windows/Audiotica.Windows/Tools/Converters/IntToThemeConverter.cs b/Windows/Audiotica.Windows/Tools/Converters/IntToThemeConverter.cs new file mode 100644 index 00000000..6c6abaa2 --- /dev/null +++ b/Windows/Audiotica.Windows/Tools/Converters/IntToThemeConverter.cs @@ -0,0 +1,21 @@ +using System; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Data; + +namespace Audiotica.Windows.Tools.Converters +{ + public class IntToThemeConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + var i = (int) value; + return (ElementTheme) i; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + var i = (ElementTheme)value; + return (int)i; + } + } +} \ No newline at end of file diff --git a/Windows/Audiotica.Windows/Tools/Converters/NotConverter.cs b/Windows/Audiotica.Windows/Tools/Converters/NotConverter.cs new file mode 100644 index 00000000..08ffb634 --- /dev/null +++ b/Windows/Audiotica.Windows/Tools/Converters/NotConverter.cs @@ -0,0 +1,19 @@ +using System; +using Windows.UI.Xaml.Data; + +namespace Audiotica.Windows.Tools.Converters +{ + public class NotConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + if (!(value is bool)) return false; + return !((bool) value); + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Windows/Audiotica.Windows/ViewModels/AlbumPageViewModel.cs b/Windows/Audiotica.Windows/ViewModels/AlbumPageViewModel.cs index 2941ed73..2df4d412 100644 --- a/Windows/Audiotica.Windows/ViewModels/AlbumPageViewModel.cs +++ b/Windows/Audiotica.Windows/ViewModels/AlbumPageViewModel.cs @@ -6,6 +6,7 @@ using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Navigation; using Audiotica.Core.Common; +using Audiotica.Core.Exceptions; using Audiotica.Core.Extensions; using Audiotica.Core.Utilities.Interfaces; using Audiotica.Core.Windows.Extensions; @@ -29,8 +30,9 @@ public class AlbumPageViewModel : ViewModelBase private readonly ILibraryService _libraryService; private readonly List _metadataProviders; private readonly INavigationService _navigationService; - private readonly ISettingsUtility _settingsUtility; private readonly IPlayerService _playerService; + private readonly ISettingsUtility _settingsUtility; + private readonly ITrackSaveService _trackSaveService; private readonly IConverter _webAlbumConverter; private Album _album; private SolidColorBrush _backgroundBrush; @@ -39,24 +41,28 @@ public class AlbumPageViewModel : ViewModelBase public AlbumPageViewModel(ILibraryService libraryService, INavigationService navigationService, IEnumerable metadataProviders, IConverter webAlbumConverter, - ISettingsUtility settingsUtility, IPlayerService playerService) + ISettingsUtility settingsUtility, IPlayerService playerService, ITrackSaveService trackSaveService) { _libraryService = libraryService; _navigationService = navigationService; _webAlbumConverter = webAlbumConverter; _settingsUtility = settingsUtility; _playerService = playerService; + _trackSaveService = trackSaveService; _metadataProviders = metadataProviders.FilterAndSort(); ViewInCatalogCommand = new Command(ViewInCatalogExecute); PlayAllCommand = new Command(PlayAllExecute); + SaveAllCommand = new Command(SaveAllExecute); if (IsInDesignMode) OnNavigatedTo(new AlbumPageParameter("Kauai", "Childish Gambino"), NavigationMode.New, new Dictionary()); } - public Command PlayAllCommand { get; set; } + public Command SaveAllCommand { get; } + + public Command PlayAllCommand { get; } public Command ViewInCatalogCommand { get; } @@ -84,8 +90,25 @@ public bool IsCatalogMode set { Set(ref _isCatalogMode, value); } } + private async void SaveAllExecute(object sender) + { + foreach (var track in Album.Tracks.Where(p => !p.IsFromLibrary)) + { + try + { + await _trackSaveService.SaveAsync(track); + } + catch (AppException ex) + { + track.Status = TrackStatus.None; + CurtainPrompt.ShowError(ex.Message ?? "Problem saving: " + track); + } + } + } + private void PlayAllExecute() { + if (Album.Tracks.Count == 0) return; var albumTracks = Album.Tracks.ToList(); _playerService.NewQueueAsync(albumTracks); } @@ -111,23 +134,7 @@ public override sealed async void OnNavigatedTo(object parameter, NavigationMode { try { - var webAlbum = albumParameter.WebAlbum; - - if (webAlbum == null) - { - if (albumParameter.Provider != null) - { - var provider = _metadataProviders.FirstOrDefault(p => p.GetType() == albumParameter.Provider); - webAlbum = await provider.GetAlbumAsync(albumParameter.Token); - } - else - { - webAlbum = await GetAlbumByTitleAsync(albumParameter.Title, albumParameter.Artist); - } - } - - if (webAlbum != null) - Album = await _webAlbumConverter.ConvertAsync(webAlbum, IsCatalogMode); + await LoadAsync(albumParameter); } catch { @@ -136,7 +143,8 @@ public override sealed async void OnNavigatedTo(object parameter, NavigationMode if (Album == null) { - _navigationService.GoBack(); + CurtainPrompt.ShowError("Problem loading album"); + return; } } @@ -144,26 +152,67 @@ public override sealed async void OnNavigatedTo(object parameter, NavigationMode DetectColorFromArtwork(); } - public override void OnNavigatedFrom() + private async Task LoadAsync(AlbumPageParameter albumParameter) { - // Bug: if we don't reset the theme when we go out it fucks with the TrackViewer control on other pages - RequestedTheme = ElementTheme.Default; + var webAlbum = albumParameter.WebAlbum; + if (webAlbum == null && albumParameter.Provider != null) + { + var provider = _metadataProviders.FirstOrDefault(p => p.GetType() == albumParameter.Provider); + webAlbum = await provider.GetAlbumAsync(albumParameter.Token); + } + if (webAlbum != null) + { + try + { + Album = await _webAlbumConverter.ConvertAsync(webAlbum, IsCatalogMode); + } + catch + { + await LoadByTitleAsync(albumParameter); + } + } + else + await LoadByTitleAsync(albumParameter); } - private async Task GetAlbumByTitleAsync(string title, string artist) + private async Task LoadByTitleAsync(AlbumPageParameter albumParameter) { - foreach (var provider in _metadataProviders) + for (var i = 0; i < _metadataProviders.Count; i++) { try { - var webAlbum = await provider.GetAlbumByTitleAsync(title, artist); - if (webAlbum != null) return webAlbum; + var webAlbum = await GetAlbumByTitleAsync(albumParameter.Title, albumParameter.Artist, i); + + if (webAlbum != null) + { + Album = await _webAlbumConverter.ConvertAsync(webAlbum, IsCatalogMode); + break; + } } catch { // ignored } } + } + + public override void OnNavigatedFrom() + { + // Bug: if we don't reset the theme when we go out it fucks with the TrackViewer control on other pages + RequestedTheme = ElementTheme.Default; + } + + private async Task GetAlbumByTitleAsync(string title, string artist, int providerIndex) + { + try + { + var webAlbum = await _metadataProviders[providerIndex].GetAlbumByTitleAsync(title, artist); + if (webAlbum != null) return webAlbum; + } + catch + { + // ignored + } return null; } diff --git a/Windows/Audiotica.Windows/ViewModels/AlbumsPageViewModel.cs b/Windows/Audiotica.Windows/ViewModels/AlbumsPageViewModel.cs index 2e67468c..c1f2c85d 100644 --- a/Windows/Audiotica.Windows/ViewModels/AlbumsPageViewModel.cs +++ b/Windows/Audiotica.Windows/ViewModels/AlbumsPageViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Data; @@ -21,21 +22,27 @@ public class AlbumsPageViewModel : ViewModelBase { private readonly ILibraryCollectionService _libraryCollectionService; private readonly INavigationService _navigationService; + private readonly IPlayerService _playerService; private readonly ISettingsUtility _settingsUtility; private double _gridViewVerticalOffset; + private bool? _isSelectMode = false; private double _listViewVerticalOffset; + private ObservableCollection _selectedItems = new ObservableCollection(); private CollectionViewSource _viewSource; public AlbumsPageViewModel(ILibraryService libraryService, ILibraryCollectionService libraryCollectionService, + IPlayerService playerService, ISettingsUtility settingsUtility, INavigationService navigationService) { _libraryCollectionService = libraryCollectionService; + _playerService = playerService; _settingsUtility = settingsUtility; _navigationService = navigationService; LibraryService = libraryService; AlbumClickCommand = new Command(AlbumClickExecute); SortChangedCommand = new Command(SortChangedExecute); + ShuffleAllCommand = new Command(ShuffleAllExecute); SortItems = Enum.GetValues(typeof (AlbumSort)) @@ -49,6 +56,8 @@ public AlbumsPageViewModel(ILibraryService libraryService, ILibraryCollectionSer ChangeSort(defaultSort); } + public Command ShuffleAllCommand { get; } + public Command SortChangedCommand { get; } public double GridViewVerticalOffset @@ -63,6 +72,12 @@ public double ListViewVerticalOffset set { Set(ref _listViewVerticalOffset, value); } } + public ObservableCollection SelectedItems + { + get { return _selectedItems; } + set { Set(ref _selectedItems, value); } + } + public CollectionViewSource ViewSource { get { return _viewSource; } @@ -77,6 +92,23 @@ public CollectionViewSource ViewSource public ILibraryService LibraryService { get; } + public bool? IsSelectMode + { + get { return _isSelectMode; } + set { Set(ref _isSelectMode, value); } + } + + private async void ShuffleAllExecute() + { + var playable = LibraryService.Tracks + .Where(p => p.Status == TrackStatus.None || p.Status == TrackStatus.Downloading) + .ToList(); + if (!playable.Any()) return; + + var tracks = playable.Shuffle(); + await _playerService.NewQueueAsync(tracks); + } + private void SortChangedExecute(ListBoxItem item) { if (!(item?.Tag is AlbumSort)) return; @@ -86,6 +118,7 @@ private void SortChangedExecute(ListBoxItem item) private void AlbumClickExecute(ItemClickEventArgs e) { + if (IsSelectMode == true) return; var album = (Album) e.ClickedItem; _navigationService.Navigate(typeof (AlbumPage), new AlbumPageViewModel.AlbumPageParameter(album.Title, album.Artist.Name)); diff --git a/Windows/Audiotica.Windows/ViewModels/ArtistsPageViewModel.cs b/Windows/Audiotica.Windows/ViewModels/ArtistsPageViewModel.cs index f69646c2..1d252bcc 100644 --- a/Windows/Audiotica.Windows/ViewModels/ArtistsPageViewModel.cs +++ b/Windows/Audiotica.Windows/ViewModels/ArtistsPageViewModel.cs @@ -1,17 +1,16 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Data; using Windows.UI.Xaml.Navigation; -using Audiotica.Core.Common; using Audiotica.Core.Extensions; using Audiotica.Database.Models; using Audiotica.Database.Services.Interfaces; using Audiotica.Windows.Enums; using Audiotica.Windows.Services.Interfaces; using Audiotica.Windows.Services.NavigationService; -using Audiotica.Windows.Tools; using Audiotica.Windows.Tools.Mvvm; using Audiotica.Windows.Views; @@ -21,20 +20,25 @@ public class ArtistsPageViewModel : ViewModelBase { private readonly ILibraryCollectionService _libraryCollectionService; private readonly INavigationService _navigationService; + private readonly IPlayerService _playerService; private double _gridViewVerticalOffset; + private bool? _isSelectMode = false; private double _listViewVerticalOffset; + private ObservableCollection _selectedItems = new ObservableCollection(); private CollectionViewSource _viewSource; public ArtistsPageViewModel(ILibraryCollectionService libraryCollectionService, - ILibraryService libraryService, + ILibraryService libraryService, IPlayerService playerService, INavigationService navigationService) { LibraryService = libraryService; _libraryCollectionService = libraryCollectionService; + _playerService = playerService; _navigationService = navigationService; ArtistClickCommand = new Command(ArtistClickExecute); SortChangedCommand = new Command(SortChangedExecute); + ShuffleAllCommand = new Command(ShuffleAllExecute); SortItems = Enum.GetValues(typeof (ArtistSort)) @@ -44,6 +48,8 @@ public ArtistsPageViewModel(ILibraryCollectionService libraryCollectionService, ChangeSort(ArtistSort.AtoZ); } + public Command ShuffleAllCommand { get; } + public Command SortChangedCommand { get; } public ILibraryService LibraryService { get; set; } @@ -70,6 +76,29 @@ public double GridViewVerticalOffset set { Set(ref _gridViewVerticalOffset, value); } } + public bool? IsSelectMode + { + get { return _isSelectMode; } + set { Set(ref _isSelectMode, value); } + } + + public ObservableCollection SelectedItems + { + get { return _selectedItems; } + set { Set(ref _selectedItems, value); } + } + + private async void ShuffleAllExecute() + { + var playable = LibraryService.Tracks + .Where(p => p.Status == TrackStatus.None || p.Status == TrackStatus.Downloading) + .ToList(); + if (!playable.Any()) return; + + var tracks = playable.Shuffle(); + await _playerService.NewQueueAsync(tracks); + } + private void SortChangedExecute(ListBoxItem item) { if (!(item?.Tag is ArtistSort)) return; @@ -93,6 +122,7 @@ public void ChangeSort(ArtistSort sort) private void ArtistClickExecute(ItemClickEventArgs e) { + if (IsSelectMode == true) return; var artist = (Artist) e.ClickedItem; _navigationService.Navigate(typeof (ArtistPage), artist.Name); } diff --git a/Windows/Audiotica.Windows/ViewModels/NowPlayingPageViewModel.cs b/Windows/Audiotica.Windows/ViewModels/NowPlayingPageViewModel.cs index 512cd7d5..f741d2da 100644 --- a/Windows/Audiotica.Windows/ViewModels/NowPlayingPageViewModel.cs +++ b/Windows/Audiotica.Windows/ViewModels/NowPlayingPageViewModel.cs @@ -1,8 +1,15 @@ +using Audiotica.Windows.Services.Interfaces; using Audiotica.Windows.Tools.Mvvm; namespace Audiotica.Windows.ViewModels { - internal class NowPlayingPageViewModel : ViewModelBase + public class NowPlayingPageViewModel : ViewModelBase { + public NowPlayingPageViewModel(IPlayerService playerService) + { + PlayerService = playerService; + } + + public IPlayerService PlayerService { get; } } } \ No newline at end of file diff --git a/Windows/Audiotica.Windows/ViewModels/PlayerBarViewModel.cs b/Windows/Audiotica.Windows/ViewModels/PlayerBarViewModel.cs new file mode 100644 index 00000000..113c9364 --- /dev/null +++ b/Windows/Audiotica.Windows/ViewModels/PlayerBarViewModel.cs @@ -0,0 +1,124 @@ +using System; +using System.Linq; +using Windows.Media.Playback; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Audiotica.Database.Models; +using Audiotica.Windows.Services.Interfaces; +using Audiotica.Windows.Tools.Mvvm; + +namespace Audiotica.Windows.ViewModels +{ + public class PlayerBarViewModel : ViewModelBase + { + private const int TimerInterval = 500; + private readonly IPlayerService _playerService; + private readonly DispatcherTimer _timer; + private QueueTrack _currentQueueTrack; + private double _playbackDuration; + private string _playbackDurationText; + private double _playbackPosition; + private string _playbackPositionText; + private IconElement _playPauseIcon = new SymbolIcon(Symbol.Play); + + public PlayerBarViewModel(IPlayerService playerService) + { + _playerService = playerService; + _playerService.TrackChanged += PlayerServiceOnTrackChanged; + _playerService.MediaStateChanged += PlayerServiceOnMediaStateChanged; + + PlayPauseCommand = new Command(() => _playerService.PlayOrPause()); + NextCommand = new Command(() => _playerService.Next()); + PrevCommand = new Command(() => _playerService.Previous()); + + _timer = new DispatcherTimer {Interval = TimeSpan.FromMilliseconds(TimerInterval)}; + _timer.Tick += TimerOnTick; + } + + public Command PrevCommand { get; } + + public Command NextCommand { get; } + + public Command PlayPauseCommand { get; } + + public QueueTrack CurrentQueueTrack + { + get { return _currentQueueTrack; } + set { Set(ref _currentQueueTrack, value); } + } + + public IconElement PlayPauseIcon + { + get { return _playPauseIcon; } + set { Set(ref _playPauseIcon, value); } + } + + public double PlaybackPosition + { + get { return _playbackPosition; } + set + { + Set(ref _playbackPosition, value); + UpdatePosition(); + } + } + + public double PlaybackDuration + { + get { return _playbackDuration; } + set { Set(ref _playbackDuration, value); } + } + + public string PlaybackPositionText + { + get { return _playbackPositionText; } + set { Set(ref _playbackPositionText, value); } + } + + public string PlaybackDurationText + { + get { return _playbackDurationText; } + set { Set(ref _playbackDurationText, value); } + } + + private void UpdatePosition() + { + var playerPosition = BackgroundMediaPlayer.Current.Position.TotalMilliseconds; + var difference = Math.Abs(PlaybackPosition - playerPosition); + if (difference > TimerInterval) + BackgroundMediaPlayer.Current.Position = TimeSpan.FromMilliseconds(PlaybackPosition); + } + + private void TimerOnTick(object sender, object o) + { + var position = BackgroundMediaPlayer.Current.Position; + var duration = BackgroundMediaPlayer.Current.NaturalDuration; + PlaybackPosition = position.TotalMilliseconds; + PlaybackDuration = duration.TotalMilliseconds; + PlaybackPositionText = position.ToString(@"m\:ss"); + PlaybackDurationText = duration.ToString(@"m\:ss"); + } + + private void PlayerServiceOnMediaStateChanged(object sender, MediaPlayerState mediaPlayerState) + { + var icon = Symbol.Play; + switch (mediaPlayerState) + { + case MediaPlayerState.Playing: + icon = Symbol.Pause; + _timer.Start(); + break; + default: + _timer.Stop(); + break; + } + PlayPauseIcon = new SymbolIcon(icon); + } + + private void PlayerServiceOnTrackChanged(object sender, string s) + { + CurrentQueueTrack = + _playerService.PlaybackQueue.FirstOrDefault(queueTrack => queueTrack.Id == s); + } + } +} \ No newline at end of file diff --git a/Windows/Audiotica.Windows/ViewModels/SettingsPageViewModel.cs b/Windows/Audiotica.Windows/ViewModels/SettingsPageViewModel.cs index de294fcc..0b12ebcc 100644 --- a/Windows/Audiotica.Windows/ViewModels/SettingsPageViewModel.cs +++ b/Windows/Audiotica.Windows/ViewModels/SettingsPageViewModel.cs @@ -1,8 +1,15 @@ -using Audiotica.Windows.Tools.Mvvm; +using Audiotica.Core.Utilities.Interfaces; +using Audiotica.Windows.Tools.Mvvm; namespace Audiotica.Windows.ViewModels { - internal class SettingsPageViewModel : ViewModelBase + public class SettingsPageViewModel : ViewModelBase { + public SettingsPageViewModel(IAppSettingsUtility appSettingsUtility) + { + AppSettingsUtility = appSettingsUtility; + } + + public IAppSettingsUtility AppSettingsUtility { get; } } } \ No newline at end of file diff --git a/Windows/Audiotica.Windows/ViewModels/SongsPageViewModel.cs b/Windows/Audiotica.Windows/ViewModels/SongsPageViewModel.cs index 804169e0..cea79f36 100644 --- a/Windows/Audiotica.Windows/ViewModels/SongsPageViewModel.cs +++ b/Windows/Audiotica.Windows/ViewModels/SongsPageViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Data; @@ -7,6 +8,7 @@ using Audiotica.Core.Extensions; using Audiotica.Core.Utilities.Interfaces; using Audiotica.Core.Windows.Helpers; +using Audiotica.Database.Models; using Audiotica.Database.Services.Interfaces; using Audiotica.Windows.Enums; using Audiotica.Windows.Services.Interfaces; @@ -17,16 +19,20 @@ namespace Audiotica.Windows.ViewModels public class SongsPageViewModel : ViewModelBase { private readonly ILibraryCollectionService _libraryCollectionService; + private readonly IPlayerService _playerService; private readonly ISettingsUtility _settingsUtility; + private bool? _isSelectMode = false; private int _selectedIndex; + private ObservableCollection _selectedItems = new ObservableCollection(); private double _verticalOffset; private CollectionViewSource _viewSource; public SongsPageViewModel(ILibraryCollectionService libraryCollectionService, ILibraryService libraryService, - ISettingsUtility settingsUtility) + ISettingsUtility settingsUtility, IPlayerService playerService) { _libraryCollectionService = libraryCollectionService; _settingsUtility = settingsUtility; + _playerService = playerService; LibraryService = libraryService; SortItems = @@ -35,6 +41,7 @@ public SongsPageViewModel(ILibraryCollectionService libraryCollectionService, IL .Select(sort => new ListBoxItem {Content = sort.GetEnumText(), Tag = sort}) .ToList(); SortChangedCommand = new Command(SortChangedExecute); + ShuffleAllCommand = new Command(ShuffleAllExecute); var defaultSort = _settingsUtility.Read(ApplicationSettingsConstants.SongSort, TrackSort.DateAdded, SettingsStrategy.Roam); @@ -42,6 +49,8 @@ public SongsPageViewModel(ILibraryCollectionService libraryCollectionService, IL ChangeSort(defaultSort); } + public Command ShuffleAllCommand { get; } + public Command SortChangedCommand { get; } public int DefaultSort { get; } @@ -50,6 +59,12 @@ public SongsPageViewModel(ILibraryCollectionService libraryCollectionService, IL public ILibraryService LibraryService { get; set; } + public ObservableCollection SelectedItems + { + get { return _selectedItems; } + set { Set(ref _selectedItems, value); } + } + public CollectionViewSource ViewSource { get { return _viewSource; } @@ -68,6 +83,23 @@ public double VerticalOffset set { Set(ref _verticalOffset, value); } } + public bool? IsSelectMode + { + get { return _isSelectMode; } + set { Set(ref _isSelectMode, value); } + } + + private async void ShuffleAllExecute() + { + var playable = LibraryService.Tracks + .Where(p => p.Status == TrackStatus.None || p.Status == TrackStatus.Downloading) + .ToList(); + if (!playable.Any()) return; + + var tracks = playable.Shuffle(); + await _playerService.NewQueueAsync(tracks); + } + private void SortChangedExecute(ListBoxItem item) { if (!(item?.Tag is TrackSort)) return; diff --git a/Windows/Audiotica.Windows/Views/AboutPage.xaml b/Windows/Audiotica.Windows/Views/AboutPage.xaml index 4b61fa0d..fbb69188 100644 --- a/Windows/Audiotica.Windows/Views/AboutPage.xaml +++ b/Windows/Audiotica.Windows/Views/AboutPage.xaml @@ -10,6 +10,8 @@ DataContext="{Binding AboutPage, Source={StaticResource ViewModelLocator}}"> - + + + \ No newline at end of file diff --git a/Windows/Audiotica.Windows/Views/AlbumPage.xaml b/Windows/Audiotica.Windows/Views/AlbumPage.xaml index 55757367..cd5bd04a 100644 --- a/Windows/Audiotica.Windows/Views/AlbumPage.xaml +++ b/Windows/Audiotica.Windows/Views/AlbumPage.xaml @@ -10,7 +10,7 @@ xmlns:customTriggers="using:Audiotica.Windows.CustomTriggers" xmlns:converters="using:Audiotica.Windows.Tools.Converters" mc:Ignorable="d" - RequestedTheme="{Binding RequestedTheme, Mode=OneWay}" + RequestedTheme="{Binding RequestedTheme}" DataContext="{Binding AlbumPage, Source={StaticResource ViewModelLocator}}"> @@ -49,19 +49,27 @@ - - + + - + + + + + + + diff --git a/Windows/Audiotica.Windows/Views/AlbumsPage.xaml b/Windows/Audiotica.Windows/Views/AlbumsPage.xaml index 20c50ebc..ad7cdcf5 100644 --- a/Windows/Audiotica.Windows/Views/AlbumsPage.xaml +++ b/Windows/Audiotica.Windows/Views/AlbumsPage.xaml @@ -12,6 +12,7 @@ xmlns:interactivity="using:Microsoft.Xaml.Interactivity" xmlns:core="using:Microsoft.Xaml.Interactions.Core" xmlns:common="using:Audiotica.Windows.Common" + xmlns:extensions="using:Audiotica.Windows.Extensions" mc:Ignorable="d" DataContext="{Binding AlbumsPage, Source={StaticResource ViewModelLocator}}"> @@ -49,8 +50,10 @@ + Margin="{StaticResource PageTopSideThickness}" + IsSelectMode="{x:Bind ViewModel.IsSelectMode, Mode=TwoWay}"/> @@ -95,9 +99,10 @@ @@ -161,4 +166,7 @@ + + + \ No newline at end of file diff --git a/Windows/Audiotica.Windows/Views/ArtistsPage.xaml b/Windows/Audiotica.Windows/Views/ArtistsPage.xaml index 1261ad69..ce62b95b 100644 --- a/Windows/Audiotica.Windows/Views/ArtistsPage.xaml +++ b/Windows/Audiotica.Windows/Views/ArtistsPage.xaml @@ -13,6 +13,7 @@ xmlns:tools="using:Audiotica.Windows.Tools" xmlns:converters="using:Audiotica.Windows.Tools.Converters" xmlns:common="using:Audiotica.Windows.Common" + xmlns:extensions="using:Audiotica.Windows.Extensions" mc:Ignorable="d" DataContext="{Binding ArtistsPage, Source={StaticResource ViewModelLocator}}"> @@ -49,8 +50,10 @@ + Margin="{StaticResource PageTopSideThickness}" + IsSelectMode="{x:Bind ViewModel.IsSelectMode, Mode=TwoWay}" /> @@ -95,9 +99,10 @@ Grid.Row="1"> @@ -155,4 +160,7 @@ + + + \ No newline at end of file diff --git a/Windows/Audiotica.Windows/Views/NowPlayingPage.xaml b/Windows/Audiotica.Windows/Views/NowPlayingPage.xaml index 7e33c347..89ba86e3 100644 --- a/Windows/Audiotica.Windows/Views/NowPlayingPage.xaml +++ b/Windows/Audiotica.Windows/Views/NowPlayingPage.xaml @@ -6,8 +6,14 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:navigationService="using:Audiotica.Windows.Services.NavigationService" + xmlns:controls="using:Audiotica.Windows.Controls" mc:Ignorable="d" DataContext="{Binding NowPlaying, Source={StaticResource ViewModelLocator}}"> - + + + \ No newline at end of file diff --git a/Windows/Audiotica.Windows/Views/NowPlayingPage.xaml.cs b/Windows/Audiotica.Windows/Views/NowPlayingPage.xaml.cs index 64c828ba..54bda8a8 100644 --- a/Windows/Audiotica.Windows/Views/NowPlayingPage.xaml.cs +++ b/Windows/Audiotica.Windows/Views/NowPlayingPage.xaml.cs @@ -1,10 +1,15 @@ -namespace Audiotica.Windows.Views +using Audiotica.Windows.ViewModels; + +namespace Audiotica.Windows.Views { public sealed partial class NowPlayingPage { public NowPlayingPage() { InitializeComponent(); + ViewModel = DataContext as NowPlayingPageViewModel; } + + public NowPlayingPageViewModel ViewModel { get; } } } \ No newline at end of file diff --git a/Windows/Audiotica.Windows/Views/SettingsPage.xaml b/Windows/Audiotica.Windows/Views/SettingsPage.xaml index 6a72055d..d234e9e9 100644 --- a/Windows/Audiotica.Windows/Views/SettingsPage.xaml +++ b/Windows/Audiotica.Windows/Views/SettingsPage.xaml @@ -9,7 +9,9 @@ mc:Ignorable="d" DataContext="{Binding SettingsPage, Source={StaticResource ViewModelLocator}}"> - - + + + + \ No newline at end of file diff --git a/Windows/Audiotica.Windows/Views/SettingsPage.xaml.cs b/Windows/Audiotica.Windows/Views/SettingsPage.xaml.cs index 2cd8c23e..f12a426b 100644 --- a/Windows/Audiotica.Windows/Views/SettingsPage.xaml.cs +++ b/Windows/Audiotica.Windows/Views/SettingsPage.xaml.cs @@ -1,10 +1,15 @@ -namespace Audiotica.Windows.Views +using Audiotica.Windows.ViewModels; + +namespace Audiotica.Windows.Views { public sealed partial class SettingsPage { public SettingsPage() { InitializeComponent(); + ViewModel = DataContext as SettingsPageViewModel; } + + public SettingsPageViewModel ViewModel { get; } } } \ No newline at end of file diff --git a/Windows/Audiotica.Windows/Views/SongsPage.xaml b/Windows/Audiotica.Windows/Views/SongsPage.xaml index 93c9a7f9..44e18912 100644 --- a/Windows/Audiotica.Windows/Views/SongsPage.xaml +++ b/Windows/Audiotica.Windows/Views/SongsPage.xaml @@ -12,6 +12,7 @@ xmlns:interactivity="using:Microsoft.Xaml.Interactivity" xmlns:core="using:Microsoft.Xaml.Interactions.Core" xmlns:interactions="using:Audiotica.Windows.Interactions" + xmlns:extensions="using:Audiotica.Windows.Extensions" mc:Ignorable="d" DataContext="{Binding SongsPage, Source={StaticResource ViewModelLocator}}"> @@ -23,8 +24,10 @@ + Margin="{StaticResource PageTopSideThickness}" + IsSelectMode="{x:Bind ViewModel.IsSelectMode, Mode=TwoWay}"/> - @@ -118,4 +122,7 @@ + + + \ No newline at end of file