diff --git a/Play.Tests/IntegrationTestUrl.cs b/Play.Tests/IntegrationTestUrl.cs index ef06e56..9add0bc 100644 --- a/Play.Tests/IntegrationTestUrl.cs +++ b/Play.Tests/IntegrationTestUrl.cs @@ -10,14 +10,14 @@ public static class IntegrationTestUrl { public static string Current { get { - //return "https://play.yourcompany.com"; + return "https://play.githubapp.com"; throw new Exception("Configure IntegrationTestUrl.cs first"); } } public static string Token { get { - //return "a04d03"; + return "f07c8b"; throw new Exception("Configure IntegrationTestUrl.cs first"); } } diff --git a/Play.Tests/Models/PlayApiTests.cs b/Play.Tests/Models/PlayApiTests.cs index fd64c8e..5591a22 100644 --- a/Play.Tests/Models/PlayApiTests.cs +++ b/Play.Tests/Models/PlayApiTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reactive.Linq; using System.Text; @@ -112,5 +113,22 @@ public void MakeAlbumSearchIntegrationTest() result.Count.Should().BeGreaterThan(2); } + [Fact(Skip = "Only enable this to test downloads")] + public void DownloadAlbumIntegrationTest() + { + var kernel = new MoqMockingKernel(); + var client = new RestClient(IntegrationTestUrl.Current); + + client.AddDefaultHeader("Authorization", IntegrationTestUrl.Token); + kernel.Bind().To(); + + var api = new PlayApi(client, kernel.Get()); + + var result = api.DownloadAlbum("Beirut", "Lon Gisland EP").First(); + + using (var of = File.OpenWrite("C:\\Users\\Administrator\\" + result.Item1)) { + new MemoryStream(result.Item2).CopyTo(of); + } + } } -} +} \ No newline at end of file diff --git a/Play.Tests/Play.Tests.csproj b/Play.Tests/Play.Tests.csproj index d5b0a03..bbb2f26 100644 --- a/Play.Tests/Play.Tests.csproj +++ b/Play.Tests/Play.Tests.csproj @@ -43,9 +43,9 @@ ..\packages\Microsoft.CompilerServices.AsyncTargetingPack.1.0.0\lib\net40\Microsoft.CompilerServices.AsyncTargetingPack.Net4.dll - False - ..\ext\Microsoft.Reactive.Testing.dll + ..\packages\Rx_Experimental-Testing.1.1.11111\lib\Net4-Full\Microsoft.Reactive.Testing.dll + ..\packages\Moq.4.0.10827\lib\NET40\Moq.dll True @@ -74,18 +74,24 @@ ..\ext\PusherClientDotNet.dll - - ..\ext\ReactiveUI.dll + + False + ..\packages\reactiveui-core.3.2.0\lib\Net4\ReactiveUI.dll - - ..\ext\ReactiveUI.Routing.dll + + ..\packages\reactiveui-xaml.3.2.0\lib\Net4\ReactiveUI.Blend.dll - - ..\ext\ReactiveUI.Testing.dll + + False + ..\packages\reactiveui-xaml.3.2.0\lib\Net4\ReactiveUI.Routing.dll - + False - ..\ext\ReactiveUI.Xaml.dll + ..\packages\reactiveui-testing.3.2.0\lib\Net4\ReactiveUI.Testing.dll + + + False + ..\packages\reactiveui-xaml.3.2.0\lib\Net4\ReactiveUI.Xaml.dll ..\packages\RestSharp.103.0.0-nojsondotnet\lib\net35\RestSharp.dll @@ -94,12 +100,10 @@ - False - ..\ext\System.Reactive.dll + ..\packages\Rx_Experimental-Main.1.1.11111\lib\Net4\System.Reactive.dll - False - ..\ext\System.Reactive.Windows.Threading.dll + ..\packages\Rx_Experimental-Xaml.1.1.11111\lib\Net4\System.Reactive.Windows.Threading.dll @@ -121,6 +125,7 @@ + diff --git a/Play.Tests/ViewModels/BackgroundTaskTileViewModel.cs b/Play.Tests/ViewModels/BackgroundTaskTileViewModel.cs new file mode 100644 index 0000000..cc3c461 --- /dev/null +++ b/Play.Tests/ViewModels/BackgroundTaskTileViewModel.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using System.Text; +using System.Windows.Media.Imaging; +using Akavache; +using FluentAssertions; +using Moq; +using Ninject; +using Ninject.MockingKernel.Moq; +using Play.Models; +using Play.ViewModels; +using ReactiveUI; +using Xunit; + +namespace Play.Tests.ViewModels +{ + public class BackgroundTaskTileViewModelTests + { + [Fact] + public void DownloadAlbumShouldQueueABackgroundTask() + { + var kernel = new MoqMockingKernel(); + var taskHost = new BackgroundTaskHostViewModel(); + kernel.Bind().ToConstant(taskHost); + + var fixture = setupStandardFixture(Fakes.GetSong(), kernel); + + var list = new List(); + taskHost.BackgroundTasks.CollectionCountChanged.Subscribe(list.Add); + fixture.DownloadAlbum.Execute(null); + + list.Contains(1).Should().BeTrue(); + } + + [Fact] + public void DownloadSongShouldQueueABackgroundTask() + { + var kernel = new MoqMockingKernel(); + var taskHost = new BackgroundTaskHostViewModel(); + kernel.Bind().ToConstant(taskHost); + + var fixture = setupStandardFixture(Fakes.GetSong(), kernel); + + var list = new List(); + taskHost.BackgroundTasks.CollectionCountChanged.Subscribe(list.Add); + fixture.DownloadSong.Execute(null); + + list.Contains(1).Should().BeTrue(); + } + + static ISongTileViewModel setupStandardFixture(Song song, MoqMockingKernel kernel) + { + kernel.Bind().To().Named("UserAccount"); + kernel.Bind().To().Named("LocalMachine"); + RxApp.ConfigureServiceLocator((t,s) => kernel.Get(t,s), (t,s) => kernel.GetAll(t,s)); + + kernel.GetMock().Setup(x => x.FetchImageForAlbum(It.IsAny())) + .Returns(Observable.Return(new BitmapImage())); + + kernel.GetMock().Setup(x => x.DownloadAlbum(It.IsAny(), It.IsAny())) + .Returns(Observable.Return>(null)); + kernel.GetMock().Setup(x => x.DownloadSong(It.IsAny())) + .Returns(Observable.Return>(null)); + + return new SongTileViewModel(song, kernel.Get()); + } + } +} diff --git a/Play.Tests/packages.config b/Play.Tests/packages.config index 20dfd60..48b8d0d 100644 --- a/Play.Tests/packages.config +++ b/Play.Tests/packages.config @@ -7,7 +7,13 @@ + + + + + + \ No newline at end of file diff --git a/Play/Images/carbon_fibre_big.png b/Play/Images/carbon_fibre_big.png new file mode 100644 index 0000000..b693b37 Binary files /dev/null and b/Play/Images/carbon_fibre_big.png differ diff --git a/Play/Models/PlayApi.cs b/Play/Models/PlayApi.cs index c7ecc15..9574abf 100644 --- a/Play/Models/PlayApi.cs +++ b/Play/Models/PlayApi.cs @@ -5,6 +5,7 @@ using System.Reactive; using System.Reactive.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows.Media.Imaging; using Akavache; @@ -28,6 +29,8 @@ public interface IPlayApi IObservable> Search(string query); IObservable> AllSongsForArtist(string name); IObservable> AllSongsOnAlbum(string artist, string album); + IObservable> DownloadSong(Song song); + IObservable> DownloadAlbum(string artist, string album); IObservable ConnectToSongChangeNotifications(); } @@ -124,6 +127,24 @@ public IObservable> AllSongsOnAlbum(string artist, string album) return client.RequestAsync(rq).Select(x => x.Data.songs); } + public IObservable> DownloadSong(Song song) + { + var rq = new RestRequest(String.Format("song/{0}/download", song.id)); + + return client.RequestAsync(rq) + .Select(x => Tuple.Create(fileNameFromResponse(x) ?? "song.mp3", x.RawBytes)); + } + + public IObservable> DownloadAlbum(string artist, string album) + { + var rq = new RestRequest(String.Format("artist/{0}/album/{1}/download", + HttpUtility.UrlEncode(artist).Replace("+", "%20"), + HttpUtility.UrlEncode(album).Replace("+", "%20"))); + + return client.RequestAsync(rq) + .Select(x => Tuple.Create(fileNameFromResponse(x) ?? "songs.zip", x.RawBytes)); + } + public IObservable ConnectToSongChangeNotifications() { var rq = new RestRequest("streaming_info"); @@ -138,5 +159,18 @@ public IObservable ListenUrl() var rq = new RestRequest("streaming_info"); return client.RequestAsync(rq).Select(x => x.Data.stream_url); } + + string fileNameFromResponse(RestResponse response) + { + var disp = response.Headers.FirstOrDefault(y => y.Name.Equals("content-disposition", StringComparison.InvariantCultureIgnoreCase)); + if (disp == null) { + return null; + } + + var re = new Regex("filename=\"([^\"]+)\"(;|$)"); + var m = re.Match((string) disp.Value); + + return m.Success ? m.Groups[1].Value : null; + } } } diff --git a/Play/Play.csproj b/Play/Play.csproj index b83c134..3dd44ac 100644 --- a/Play/Play.csproj +++ b/Play/Play.csproj @@ -38,6 +38,8 @@ true true true + true + 4.0.30816.0 AnyCPU @@ -80,6 +82,9 @@ ..\packages\Hardcodet.Wpf.TaskbarNotification.1.0.4.0\lib\net40\Hardcodet.Wpf.TaskbarNotification.dll + + ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll + ..\packages\Microsoft.CompilerServices.AsyncTargetingPack.1.0.0\lib\net40\Microsoft.CompilerServices.AsyncTargetingPack.Net4.dll @@ -111,17 +116,21 @@ ..\ext\PusherClientDotNet.dll - - ..\ext\ReactiveUI.dll + + False + ..\packages\reactiveui-core.3.2.0\lib\Net4\ReactiveUI.dll - - ..\ext\ReactiveUI.Blend.dll + + False + ..\packages\reactiveui-xaml.3.2.0\lib\Net4\ReactiveUI.Blend.dll - - ..\ext\ReactiveUI.Routing.dll + + False + ..\packages\reactiveui-xaml.3.2.0\lib\Net4\ReactiveUI.Routing.dll - - ..\ext\ReactiveUI.Xaml.dll + + False + ..\packages\reactiveui-xaml.3.2.0\lib\Net4\ReactiveUI.Xaml.dll ..\packages\RestSharp.103.0.0-nojsondotnet\lib\net35\RestSharp.dll @@ -137,12 +146,10 @@ - False - ..\ext\System.Reactive.dll + ..\packages\Rx_Experimental-Main.1.1.11111\lib\Net4\System.Reactive.dll - False - ..\ext\System.Reactive.Windows.Threading.dll + ..\packages\Rx_Experimental-Xaml.1.1.11111\lib\Net4\System.Reactive.Windows.Threading.dll @@ -174,10 +181,20 @@ + + + BackgroundTaskHostView.xaml + + + BackgroundTaskTileView.xaml + + + Dummy.xaml + SearchView.xaml @@ -202,10 +219,27 @@ MainWindow.xaml Code + + MSBuild:Compile + Designer + true + Designer MSBuild:Compile + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + MSBuild:Compile Designer @@ -296,6 +330,9 @@ + + + + \ No newline at end of file diff --git a/Play/ViewModels/AppBootstrapper.cs b/Play/ViewModels/AppBootstrapper.cs index 6e965b1..8cb53bf 100644 --- a/Play/ViewModels/AppBootstrapper.cs +++ b/Play/ViewModels/AppBootstrapper.cs @@ -119,10 +119,12 @@ IKernel createDefaultKernel() ret.Bind().To(); ret.Bind().To(); ret.Bind().To(); + ret.Bind().To(); ret.Bind>().To(); ret.Bind>().To(); ret.Bind>().To(); ret.Bind>().To().InTransientScope(); + ret.Bind>().To(); #if DEBUG var testBlobCache = new TestBlobCache(); diff --git a/Play/ViewModels/BackgroundTaskTileViewModel.cs b/Play/ViewModels/BackgroundTaskTileViewModel.cs new file mode 100644 index 0000000..18731ab --- /dev/null +++ b/Play/ViewModels/BackgroundTaskTileViewModel.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Text; +using System.Threading; +using System.Windows; +using ReactiveUI; +using ReactiveUI.Xaml; + +namespace Play.ViewModels +{ + public interface IBackgroundTaskHostViewModel : IReactiveNotifyPropertyChanged + { + ReactiveCollection BackgroundTasks { get; } + Visibility ShouldShowBackgroundTaskPane { get; } + } + + public interface IBackgroundTaskTileViewModel : IReactiveNotifyPropertyChanged + { + int CurrentProgress { get; } + string CurrentText { get; set; } + object Tag { get; set; } + + ReactiveCommand Cancel { get; } + } + + public class BackgroundTaskHostViewModel : ReactiveObject, IBackgroundTaskHostViewModel + { + public ReactiveCollection BackgroundTasks { get; protected set; } + + ObservableAsPropertyHelper _ShouldShowBackgroundTaskPane; + public Visibility ShouldShowBackgroundTaskPane { + get { return _ShouldShowBackgroundTaskPane.Value; } + } + + public BackgroundTaskHostViewModel() + { + BackgroundTasks = new ReactiveCollection(); + + BackgroundTasks.CollectionCountChanged + .Select(x => x == 0 ? Visibility.Visible : Visibility.Hidden) + .ToProperty(this, x => x.ShouldShowBackgroundTaskPane); + } + } + + public class BackgroundTaskUserError : UserError + { + public BackgroundTaskUserError(string errorMessage, Exception innerException) : base(errorMessage, innerException: innerException) + { + RecoveryOptions.Add(new RecoveryCommand("Retry", _ => RecoveryOptionResult.RetryOperation)); + RecoveryOptions.Add(RecoveryCommand.Cancel); + } + } + + public class BackgroundTaskTileViewModel : ReactiveObject, IBackgroundTaskTileViewModel + { + ObservableAsPropertyHelper _CurrentProgress; + public int CurrentProgress { + get { return _CurrentProgress.Value; } + } + + string _CurrentText; + public string CurrentText { + get { return _CurrentText; } + set { this.RaiseAndSetIfChanged(x => x.CurrentText, value); } + } + + object _Tag; + public object Tag { + get { return _Tag; } + set { this.RaiseAndSetIfChanged(x => x.Tag, value); } + } + + public ReactiveCommand Cancel { get; protected set; } + + public BackgroundTaskTileViewModel(IObservable progress) + { + progress.ToProperty(this, x => x.CurrentProgress); + Cancel = new ReactiveCommand(); + } + + public static DisposableContainer Create(IObservable progress, string captionText, IDisposable workSubscription) + { + var prg = progress.Multicast(new Subject()); + var ret = new BackgroundTaskTileViewModel(prg) {CurrentText = captionText}; + + var collection = RxApp.GetService().BackgroundTasks; + + prg.ObserveOn(RxApp.DeferredScheduler).Subscribe( + x => { }, + ex => { collection.Remove(ret); UserError.Throw(new BackgroundTaskUserError("Transfer Failed", ex)); }, + () => collection.Remove(ret)); + + var disp = prg.Connect(); + + collection.Add(ret); + + var container = DisposableContainer.Create((IBackgroundTaskTileViewModel)ret, + new CompositeDisposable(disp, workSubscription ?? Disposable.Empty)); + + ret.Cancel.Subscribe(_ => container.Dispose()); + + return container; + } + } + + public static class DisposableContainer + { + public static DisposableContainer Create(T1 value, IDisposable disposable) + { + return new DisposableContainer(value, disposable); + } + } + + public sealed class DisposableContainer : IDisposable + { + IDisposable _inner; + + public T Value { get; private set; } + + public DisposableContainer(T value, IDisposable disposable) + { + Value = value; + _inner = disposable; + } + + public void Dispose() + { + var disp = Interlocked.Exchange(ref _inner, null); + if (disp != null) { + disp.Dispose(); + } + } + } +} diff --git a/Play/ViewModels/PlayViewModel.cs b/Play/ViewModels/PlayViewModel.cs index 53711dd..fc9b420 100644 --- a/Play/ViewModels/PlayViewModel.cs +++ b/Play/ViewModels/PlayViewModel.cs @@ -29,6 +29,7 @@ public interface IPlayViewModel : IRoutableViewModel ReactiveCommand TogglePlay { get; } ReactiveCommand Search { get; } ReactiveCommand Logout { get; } + IBackgroundTaskHostViewModel BackgroundTaskHost { get; } } public class PlayViewModel : ReactiveObject, IPlayViewModel @@ -74,10 +75,13 @@ public bool IsPlaying { public ReactiveCommand Search { get; protected set; } public ReactiveCommand Logout { get; protected set; } + public IBackgroundTaskHostViewModel BackgroundTaskHost { get; protected set; } + [Inject] public PlayViewModel(IScreen screen, ILoginMethods loginMethods) { HostScreen = screen; + BackgroundTaskHost = RxApp.GetService(); TogglePlay = new ReactiveCommand(); Logout = new ReactiveCommand(); Search = new ReactiveCommand(); diff --git a/Play/ViewModels/SongTileViewModel.cs b/Play/ViewModels/SongTileViewModel.cs index 4ab8482..95819e3 100644 --- a/Play/ViewModels/SongTileViewModel.cs +++ b/Play/ViewModels/SongTileViewModel.cs @@ -1,8 +1,14 @@ using System; +using System.IO; +using System.Linq; using System.Reactive; using System.Reactive.Linq; +using System.Reactive.Subjects; using System.Windows; using System.Windows.Media.Imaging; +using ICSharpCode.SharpZipLib.Tar; +using ICSharpCode.SharpZipLib.Zip; +using Ninject; using Play.Models; using ReactiveUI; using ReactiveUI.Xaml; @@ -19,6 +25,9 @@ public interface ISongTileViewModel : IReactiveNotifyPropertyChanged ReactiveAsyncCommand QueueSong { get; } ReactiveAsyncCommand QueueAlbum { get; } + ReactiveCommand DownloadSong { get; } + ReactiveCommand DownloadAlbum { get; } + ReactiveAsyncCommand ShowSongsFromArtist { get; } ReactiveAsyncCommand ShowSongsFromAlbum { get; } @@ -49,12 +58,15 @@ public Visibility QueueSongVisibility { public ReactiveAsyncCommand QueueSong { get; protected set; } public ReactiveAsyncCommand QueueAlbum { get; protected set; } + public ReactiveCommand DownloadSong { get; protected set; } + public ReactiveCommand DownloadAlbum { get; protected set; } + public ReactiveAsyncCommand ShowSongsFromArtist { get; protected set; } public ReactiveAsyncCommand ShowSongsFromAlbum { get; protected set; } public ReactiveAsyncCommand ToggleStarred { get; protected set; } - public SongTileViewModel(Song model, IPlayApi playApi) + public SongTileViewModel(Song model, IPlayApi playApi, [Optional][Named("MusicDir")] string musicDir = null) { Model = model; @@ -79,6 +91,33 @@ public SongTileViewModel(Song model, IPlayApi playApi) QueueAlbum.ThrownExceptions.Subscribe(x => { }); + DownloadAlbum = new ReactiveCommand(); + DownloadSong = new ReactiveCommand(); + + DownloadAlbum.Subscribe(_ => { + var dl = playApi.DownloadAlbum(Model.artist, model.album) + .SelectMany(x => postProcessAlbum(x, musicDir)) + .Multicast(new Subject()); + + var timer = Observable.Timer(DateTimeOffset.MinValue, TimeSpan.FromSeconds(3.0)) + .Select(x => (int)x * 5) + .Where(x => x < 100).TakeUntil(dl); + + BackgroundTaskTileViewModel.Create(timer, "Downloading " + Model.album, dl.Connect()); + }); + + DownloadSong.Subscribe(_ => { + var dl = playApi.DownloadSong(Model) + .SelectMany(x => postProcessSong(x, musicDir)) + .Multicast(new Subject()); + + var timer = Observable.Timer(DateTimeOffset.MinValue, TimeSpan.FromSeconds(3.0)) + .Select(x => (int)x * 5) + .Where(x => x < 100).TakeUntil(dl); + + BackgroundTaskTileViewModel.Create(timer, "Downloading " + Model.name, dl.Connect()); + }); + IsStarred = model.starred; ToggleStarred = new ReactiveAsyncCommand(); @@ -97,5 +136,56 @@ IObservable reallyTryToQueueSong(IPlayApi playApi, Song song) .Timeout(TimeSpan.FromSeconds(20), RxApp.TaskpoolScheduler) .Retry(3); } + + IObservable postProcessSong(Tuple fileAndData, string rootPath = null) + { + rootPath = rootPath ?? Environment.GetFolderPath(Environment.SpecialFolder.MyMusic); + var paths = new[] { + Model.artist, Model.album + }; + + return Observable.Start(() => { + var targetDir = paths.Aggregate(new DirectoryInfo(rootPath), (acc, x) => { + if (!acc.Exists) { + acc.Create(); + } + return new DirectoryInfo(Path.Combine(acc.FullName, x)); + }); + + if (!targetDir.Exists) targetDir.Create(); + + File.WriteAllBytes(Path.Combine(targetDir.FullName, fileAndData.Item1), fileAndData.Item2); + }, RxApp.TaskpoolScheduler); + } + + IObservable postProcessAlbum(Tuple fileAndData, string rootPath = null) + { + rootPath = rootPath ?? Environment.GetFolderPath(Environment.SpecialFolder.MyMusic); + var paths = new[] { + Model.artist, // NB: Zip folder itself has Album name + }; + + return Observable.Start(() => { + var targetDir = paths.Aggregate(new DirectoryInfo(rootPath), (acc, x) => { + if (!acc.Exists) { + acc.Create(); + } + + return new DirectoryInfo(Path.Combine(acc.FullName, x)); + }); + + if (!targetDir.Exists) targetDir.Create(); + + // NB: https://github.com/play/play/issues/169 + /* + using (var archive = TarArchive.CreateInputTarArchive(new MemoryStream(fileAndData.Item2))) { + archive.ExtractContents(targetDir.FullName); + } + */ + + var zip = new FastZip(); + zip.ExtractZip(new MemoryStream(fileAndData.Item2), targetDir.FullName, FastZip.Overwrite.Always, null, null, null, true, true); + }, RxApp.TaskpoolScheduler); + } } } \ No newline at end of file diff --git a/Play/Views/BackgroundTaskHostView.xaml b/Play/Views/BackgroundTaskHostView.xaml new file mode 100644 index 0000000..af01433 --- /dev/null +++ b/Play/Views/BackgroundTaskHostView.xaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Play/Views/BackgroundTaskHostView.xaml.cs b/Play/Views/BackgroundTaskHostView.xaml.cs new file mode 100644 index 0000000..e92dfb5 --- /dev/null +++ b/Play/Views/BackgroundTaskHostView.xaml.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using Play.ViewModels; +using ReactiveUI.Routing; + +namespace Play +{ + /// + /// Interaction logic for BackgroundTaskView.xaml + /// + public partial class BackgroundTaskHostView : UserControl, IViewForViewModel + { + public BackgroundTaskHostView() + { + this.InitializeComponent(); + } + + public BackgroundTaskHostViewModel ViewModel { + get { return (BackgroundTaskHostViewModel)GetValue(ViewModelProperty); } + set { SetValue(ViewModelProperty, value); } + } + public static readonly DependencyProperty ViewModelProperty = + DependencyProperty.Register("ViewModel", typeof(IBackgroundTaskHostViewModel), typeof(BackgroundTaskHostView), new UIPropertyMetadata(null)); + + object IViewForViewModel.ViewModel { get { return ViewModel; } set { ViewModel = (BackgroundTaskHostViewModel)value; } } + } +} \ No newline at end of file diff --git a/Play/Views/BackgroundTaskTileView.xaml b/Play/Views/BackgroundTaskTileView.xaml new file mode 100644 index 0000000..fe7b68c --- /dev/null +++ b/Play/Views/BackgroundTaskTileView.xaml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/Play/Views/BackgroundTaskTileView.xaml.cs b/Play/Views/BackgroundTaskTileView.xaml.cs new file mode 100644 index 0000000..4d7fc92 --- /dev/null +++ b/Play/Views/BackgroundTaskTileView.xaml.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using Play.ViewModels; +using ReactiveUI.Routing; + +namespace Play +{ + /// + /// Interaction logic for BackgroundTaskTileView.xaml + /// + public partial class BackgroundTaskTileView : UserControl, IViewForViewModel + { + public BackgroundTaskTileView() + { + this.InitializeComponent(); + } + + public BackgroundTaskTileViewModel ViewModel { + get { return (BackgroundTaskTileViewModel)GetValue(ViewModelProperty); } + set { SetValue(ViewModelProperty, value); } + } + public static readonly DependencyProperty ViewModelProperty = + DependencyProperty.Register("ViewModel", typeof(BackgroundTaskTileViewModel), typeof(BackgroundTaskTileView), new UIPropertyMetadata(null)); + + object IViewForViewModel.ViewModel { get { return ViewModel; } set { ViewModel = (BackgroundTaskTileViewModel)value; } } + } +} \ No newline at end of file diff --git a/Play/Views/Dummy.xaml b/Play/Views/Dummy.xaml new file mode 100644 index 0000000..9cc7309 --- /dev/null +++ b/Play/Views/Dummy.xaml @@ -0,0 +1,69 @@ + + + + + + + + diff --git a/Play/Views/Dummy.xaml.cs b/Play/Views/Dummy.xaml.cs new file mode 100644 index 0000000..25e89d7 --- /dev/null +++ b/Play/Views/Dummy.xaml.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace Play.Views +{ + /// + /// Interaction logic for Dummy.xaml + /// + public partial class Dummy : UserControl + { + public Dummy() + { + InitializeComponent(); + } + } +} diff --git a/Play/Views/PlayView.xaml b/Play/Views/PlayView.xaml index 4d1bc16..433e9da 100644 --- a/Play/Views/PlayView.xaml +++ b/Play/Views/PlayView.xaml @@ -8,6 +8,7 @@ + @@ -88,8 +89,10 @@ + + - + diff --git a/Play/Views/SongTileView.xaml b/Play/Views/SongTileView.xaml index 1ab3920..12f3226 100644 --- a/Play/Views/SongTileView.xaml +++ b/Play/Views/SongTileView.xaml @@ -73,7 +73,7 @@ - + @@ -94,7 +94,7 @@ - + diff --git a/Play/packages.config b/Play/packages.config index ee69bc0..72d7add 100644 --- a/Play/packages.config +++ b/Play/packages.config @@ -8,5 +8,10 @@ + + + + + \ No newline at end of file