Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions Espera.Core/Song.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using Espera.Network;
using Rareform.Validation;
using System;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Espera.Network;
using Rareform.Validation;

namespace Espera.Core
{
Expand All @@ -14,7 +14,7 @@ public abstract class Song : IEquatable<Song>, INotifyPropertyChanged
private bool isCorrupted;

/// <summary>
/// Initializes a new instance of the <see cref="Song" /> class.
/// Initializes a new instance of the <see cref="Song"/> class.
/// </summary>
/// <param name="path">The path of the song.</param>
/// <param name="duration">The duration of the song.</param>
Expand Down Expand Up @@ -45,7 +45,9 @@ protected Song(string path, TimeSpan duration)
public string Genre { get; set; }

/// <summary>
/// A runtime identifier for interaction with the mobile API.
/// A runtime identifier that uniquely identifies this songs.
///
/// This identifier changes at each startup, but is stable in a running instance of the application.
/// </summary>
public Guid Guid { get; private set; }

Expand All @@ -56,6 +58,7 @@ protected Song(string path, TimeSpan duration)
public bool IsCorrupted
{
get { return this.isCorrupted; }

set
{
if (this.isCorrupted != value)
Expand Down
12 changes: 12 additions & 0 deletions Espera.View/Espera.View.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@
<Reference Include="DeltaCompressionDotNet.PatchApi">
<HintPath>..\packages\DeltaCompressionDotNet.1.0.0\lib\net45\DeltaCompressionDotNet.PatchApi.dll</HintPath>
</Reference>
<Reference Include="DynamicData, Version=4.3.1.1090, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DynamicData.4.3.1.1090\lib\net45\DynamicData.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="DynamicData.Plinq, Version=4.3.1.1090, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DynamicData.4.3.1.1090\lib\net45\DynamicData.Plinq.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="DynamicData.ReactiveUI, Version=2.2.0.2007, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DynamicData.ReactiveUI.2.2.0.2007\lib\portable-net45+win+wpa81+wp80\DynamicData.ReactiveUI.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Espera.Network, Version=1.0.36.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Espera-Network.1.0.36\lib\portable-net45+monoandroid+wpa81\Espera.Network.dll</HintPath>
Expand Down
65 changes: 33 additions & 32 deletions Espera.View/SearchEngine.cs
Original file line number Diff line number Diff line change
@@ -1,47 +1,48 @@
using Espera.Core;
using Rareform.Validation;
using System;
using System.Collections.Generic;
using System;
using System.Linq;
using Espera.Core;

namespace Espera.View
{
public static class SearchEngine
public static class StringExtensions
{
/// <summary>
/// Filters the source by the specified search text.
/// </summary>
/// <param name="source">The songs to search.</param>
/// <param name="searchText">The search text.</param>
/// <returns>The filtered sequence of songs.</returns>
public static IEnumerable<Song> FilterSongs(this IEnumerable<Song> source, string searchText)
public static bool ContainsIgnoreCase(this string value, string other)
{
if (searchText == null)
Throw.ArgumentNullException(() => searchText);
return value.IndexOf(other, StringComparison.InvariantCultureIgnoreCase) >= 0;
}
}

if (String.IsNullOrWhiteSpace(searchText))
return source;
public class SearchEngine
{
private readonly string[] keywords;
private readonly bool passThrough;

IEnumerable<string> keyWords = searchText.Split(' ');
public SearchEngine(string searchText)
{
if (String.IsNullOrWhiteSpace(searchText))
{
this.passThrough = true;
return;
}

return source
.AsParallel()
.Where
(
song => keyWords.All
(
keyword =>
song.Artist.ContainsIgnoreCase(keyword) ||
song.Album.ContainsIgnoreCase(keyword) ||
song.Genre.ContainsIgnoreCase(keyword) ||
song.Title.ContainsIgnoreCase(keyword)
)
);
this.keywords = searchText.Split(' ');
}

private static bool ContainsIgnoreCase(this string value, string other)
public bool Filter(Song song)
{
return value.IndexOf(other, StringComparison.InvariantCultureIgnoreCase) >= 0;
if (this.passThrough)
{
return true;
}

return this.keywords.All
(
keyword =>
song.Artist.ContainsIgnoreCase(keyword) ||
song.Album.ContainsIgnoreCase(keyword) ||
song.Genre.ContainsIgnoreCase(keyword) ||
song.Title.ContainsIgnoreCase(keyword)
);
}
}
}
128 changes: 88 additions & 40 deletions Espera.View/ViewModels/ArtistViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,50 +1,43 @@
using Espera.Core;
using ReactiveUI;
using Splat;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;
using Espera.Core;
using ReactiveUI;
using Splat;

namespace Espera.View.ViewModels
{
public sealed class ArtistViewModel : ReactiveObject, IComparable<ArtistViewModel>, IEquatable<ArtistViewModel>, IDisposable
public sealed class ArtistViewModel : ReactiveObject, IEquatable<ArtistViewModel>, IDisposable
{
private readonly ObservableAsPropertyHelper<BitmapSource> cover;
private readonly int orderHint;
private readonly ReactiveList<LocalSong> songs;

/// <summary>
/// The constructor.
/// </summary>
/// <param name="artistName"></param>
/// <param name="songs"></param>
/// <param name="artworkKeys"></param>
/// <param name="orderHint">
/// A hint that tells this instance which position it has in the artist list. This helps for
/// priorizing the album cover loading. The higher the number, the earlier it is in the list
/// (Think of a reversed sorted list).
/// </param>
public ArtistViewModel(string artistName, IEnumerable<LocalSong> songs, int orderHint = 1)
public ArtistViewModel(string artistName, IObservable<string> artworkKeys, int orderHint = 1)
{
this.songs = new ReactiveList<LocalSong>();

this.orderHint = orderHint;

this.cover = this.songs.ItemsAdded.Select(x => x.WhenAnyValue(y => y.ArtworkKey))
.Merge()
this.cover = artworkKeys
.Where(x => x != null)
.Distinct() // Ignore duplicate artworks
.Select(LoadArtworkAsync)
.Select(key => Observable.FromAsync(() => this.LoadArtworkAsync(key)))
.Concat()
.FirstOrDefaultAsync(pic => pic != null)
.ToProperty(this, x => x.Cover);
var connect = this.Cover; // Connect the property to the source observable immediately

this.UpdateSongs(songs);

this.Name = artistName;
this.IsAllArtists = false;
}
Expand All @@ -64,46 +57,39 @@ public BitmapSource Cover

public string Name { get; private set; }

public int CompareTo(ArtistViewModel other)
public void Dispose()
{
if (this.IsAllArtists && other.IsAllArtists)
this.cover?.Dispose();
}

public bool Equals(ArtistViewModel other)
{
if (Object.ReferenceEquals(other, null))
{
return 0;
return false;
}

if (this.IsAllArtists)
if (this.IsAllArtists && other.IsAllArtists)
{
return -1;
return true;
}

if (other.IsAllArtists)
if (this.IsAllArtists || other.IsAllArtists)
{
return 1;
return false;
}

return String.Compare(SortHelpers.RemoveArtistPrefixes(this.Name), SortHelpers.RemoveArtistPrefixes(other.Name), StringComparison.InvariantCultureIgnoreCase);
return this.Name.Equals(other.Name, StringComparison.InvariantCultureIgnoreCase);
}

public void Dispose()
{
this.cover.Dispose();
}

public bool Equals(ArtistViewModel other)
public override bool Equals(object obj)
{
return this.Name == other.Name;
return base.Equals(obj as ArtistViewModel);
}

public void UpdateSongs(IEnumerable<LocalSong> songs)
public override int GetHashCode()
{
var songsToAdd = songs.Where(x => !this.songs.Contains(x)).ToList();

// Can't use AddRange here, ReactiveList resets the list on big changes and we don't get
// the add notification
foreach (LocalSong song in songsToAdd)
{
this.songs.Add(song);
}
return new { A = this.IsAllArtists, B = this.Name }.GetHashCode();
}

private async Task<BitmapSource> LoadArtworkAsync(string key)
Expand Down Expand Up @@ -136,5 +122,67 @@ private async Task<BitmapSource> LoadArtworkAsync(string key)
return null;
}
}

/// <summary>
/// A custom equality class for the artist grouping, until
/// https://github.com/RolandPheasant/DynamicData/issues/31 is resolved
/// </summary>
public class ArtistString : IEquatable<ArtistString>
{
private readonly string artistName;

public ArtistString(string artistName)
{
this.artistName = artistName;
}

public static implicit operator ArtistString(string source)
{
return new ArtistString(source);
}

public static implicit operator string(ArtistString source)
{
return source.artistName;
}

public bool Equals(ArtistString other)
{
return StringComparer.InvariantCultureIgnoreCase.Equals(this.artistName, other.artistName);
}

public override bool Equals(object obj)
{
return this.Equals(obj as ArtistString);
}

public override int GetHashCode()
{
return StringComparer.InvariantCultureIgnoreCase.GetHashCode(this.artistName);
}
}

public class Comparer : IComparer<ArtistViewModel>
{
public int Compare(ArtistViewModel x, ArtistViewModel y)
{
if (x.IsAllArtists && y.IsAllArtists)
{
return 0;
}

if (x.IsAllArtists)
{
return -1;
}

if (y.IsAllArtists)
{
return 1;
}

return String.Compare(SortHelpers.RemoveArtistPrefixes(x.Name), SortHelpers.RemoveArtistPrefixes(y.Name), StringComparison.InvariantCultureIgnoreCase);
}
}
}
}
Loading