diff options
14 files changed, 331 insertions, 124 deletions
diff --git a/MediaBrowser.Api/SearchService.cs b/MediaBrowser.Api/SearchService.cs index a3d228529..d8aad5283 100644 --- a/MediaBrowser.Api/SearchService.cs +++ b/MediaBrowser.Api/SearchService.cs @@ -20,7 +20,7 @@ namespace MediaBrowser.Api /// </summary> [Route("/Search/Hints", "GET")] [Api(Description = "Gets search hints based on a search term")] - public class GetSearchHints : IReturn<List<SearchHintResult>> + public class GetSearchHints : IReturn<SearchHintResult> { /// <summary> /// Skips over a given number of items within the results. Use for paging. @@ -96,7 +96,7 @@ namespace MediaBrowser.Api /// </summary> /// <param name="request">The request.</param> /// <returns>Task{IEnumerable{SearchHintResult}}.</returns> - private async Task<IEnumerable<SearchHintResult>> GetSearchHintsAsync(GetSearchHints request) + private async Task<SearchHintResult> GetSearchHintsAsync(GetSearchHints request) { IEnumerable<BaseItem> inputItems; @@ -113,34 +113,48 @@ namespace MediaBrowser.Api var results = await _searchEngine.GetSearchHints(inputItems, request.SearchTerm).ConfigureAwait(false); + var searchResultArray = results.ToArray(); + + IEnumerable<SearchHintInfo> returnResults = searchResultArray; + if (request.StartIndex.HasValue) { - results = results.Skip(request.StartIndex.Value); + returnResults = returnResults.Skip(request.StartIndex.Value); } if (request.Limit.HasValue) { - results = results.Take(request.Limit.Value); + returnResults = returnResults.Take(request.Limit.Value); } - return results.Select(GetSearchHintResult); + return new SearchHintResult + { + TotalRecordCount = searchResultArray.Length, + + SearchHints = returnResults.Select(GetSearchHintResult).ToArray() + }; } /// <summary> /// Gets the search hint result. /// </summary> - /// <param name="item">The item.</param> + /// <param name="hintInfo">The hint info.</param> /// <returns>SearchHintResult.</returns> - private SearchHintResult GetSearchHintResult(BaseItem item) + private SearchHint GetSearchHintResult(SearchHintInfo hintInfo) { - var result = new SearchHintResult + var item = hintInfo.Item; + + var result = new SearchHint { Name = item.Name, IndexNumber = item.IndexNumber, ParentIndexNumber = item.ParentIndexNumber, ItemId = DtoBuilder.GetClientItemId(item), Type = item.GetType().Name, - MediaType = item.MediaType + MediaType = item.MediaType, + MatchedTerm = hintInfo.MatchedTerm, + DisplayMediaType = item.DisplayMediaType, + RunTimeTicks = item.RunTimeTicks }; if (item.HasImage(ImageType.Primary)) @@ -160,14 +174,25 @@ namespace MediaBrowser.Api if (season != null) { result.Series = season.Series.Name; + + result.EpisodeCount = season.RecursiveChildren.OfType<Episode>().Count(); + } + + var series = item as Series; + + if (series != null) + { + result.EpisodeCount = series.RecursiveChildren.OfType<Episode>().Count(); } var album = item as MusicAlbum; if (album != null) { - var songs = album.Children.OfType<Audio>().ToList(); + var songs = album.RecursiveChildren.OfType<Audio>().ToList(); + result.SongCount = songs.Count; + result.Artists = songs .Select(i => i.Artist) .Where(i => !string.IsNullOrEmpty(i)) diff --git a/MediaBrowser.Controller/Dto/DtoBuilder.cs b/MediaBrowser.Controller/Dto/DtoBuilder.cs index 76a5f8517..93f5ba8cc 100644 --- a/MediaBrowser.Controller/Dto/DtoBuilder.cs +++ b/MediaBrowser.Controller/Dto/DtoBuilder.cs @@ -431,7 +431,7 @@ namespace MediaBrowser.Controller.Dto if (album != null) { - var songs = album.Children.OfType<Audio>().ToList(); + var songs = album.RecursiveChildren.OfType<Audio>().ToList(); dto.AlbumArtist = songs.Select(i => i.AlbumArtist).FirstOrDefault(i => !string.IsNullOrEmpty(i)); diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index adaec9fdc..cbf35f870 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -96,5 +96,17 @@ namespace MediaBrowser.Controller.Entities.Audio { return string.Equals(Artist, name, StringComparison.OrdinalIgnoreCase) || string.Equals(AlbumArtist, name, StringComparison.OrdinalIgnoreCase); } + + public override string DisplayMediaType + { + get + { + return "Song"; + } + set + { + base.DisplayMediaType = value; + } + } } } diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index 0366af8c7..2d1f5f337 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -147,7 +147,19 @@ namespace MediaBrowser.Controller.Entities.Audio /// <returns><c>true</c> if the specified artist has artist; otherwise, <c>false</c>.</returns> public bool HasArtist(string artist) { - return Children.OfType<Audio>().Any(i => i.HasArtist(artist)); + return RecursiveChildren.OfType<Audio>().Any(i => i.HasArtist(artist)); + } + + public override string DisplayMediaType + { + get + { + return "Album"; + } + set + { + base.DisplayMediaType = value; + } } } } diff --git a/MediaBrowser.Controller/Library/ILibrarySearchEngine.cs b/MediaBrowser.Controller/Library/ILibrarySearchEngine.cs index 315e75208..ff4dcbe5b 100644 --- a/MediaBrowser.Controller/Library/ILibrarySearchEngine.cs +++ b/MediaBrowser.Controller/Library/ILibrarySearchEngine.cs @@ -23,7 +23,7 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="inputItems">The input items.</param> /// <param name="searchTerm">The search term.</param> - /// <returns>Task{IEnumerable{BaseItem}}.</returns> - Task<IEnumerable<BaseItem>> GetSearchHints(IEnumerable<BaseItem> inputItems, string searchTerm); + /// <returns>Task{IEnumerable{SearchHintInfo}}.</returns> + Task<IEnumerable<SearchHintInfo>> GetSearchHints(IEnumerable<BaseItem> inputItems, string searchTerm); } } diff --git a/MediaBrowser.Controller/Library/SearchHintInfo.cs b/MediaBrowser.Controller/Library/SearchHintInfo.cs new file mode 100644 index 000000000..f832811c2 --- /dev/null +++ b/MediaBrowser.Controller/Library/SearchHintInfo.cs @@ -0,0 +1,22 @@ +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Library +{ + /// <summary> + /// Class SearchHintInfo + /// </summary> + public class SearchHintInfo + { + /// <summary> + /// Gets or sets the item. + /// </summary> + /// <value>The item.</value> + public BaseItem Item { get; set; } + + /// <summary> + /// Gets or sets the matched term. + /// </summary> + /// <value>The matched term.</value> + public string MatchedTerm { get; set; } + } +} diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 4c6d81dcf..5c894c8b3 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -112,6 +112,7 @@ <Compile Include="IServerApplicationPaths.cs" /> <Compile Include="Library\ChildrenChangedEventArgs.cs" /> <Compile Include="Dto\DtoBuilder.cs" /> + <Compile Include="Library\SearchHintInfo.cs" /> <Compile Include="Providers\IProviderManager.cs" /> <Compile Include="Providers\MediaInfo\MediaEncoderHelpers.cs" /> <Compile Include="Providers\MetadataProviderPriority.cs" /> diff --git a/MediaBrowser.Controller/Providers/MediaInfo/FFProbeAudioInfoProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/FFProbeAudioInfoProvider.cs index 983286502..da4250d79 100644 --- a/MediaBrowser.Controller/Providers/MediaInfo/FFProbeAudioInfoProvider.cs +++ b/MediaBrowser.Controller/Providers/MediaInfo/FFProbeAudioInfoProvider.cs @@ -167,7 +167,11 @@ namespace MediaBrowser.Controller.Providers.MediaInfo if (!string.IsNullOrEmpty(val)) { - audio.AddStudios(val.Split(new[] { '/', '|' }, StringSplitOptions.RemoveEmptyEntries)); + var studios = + val.Split(new[] {'/', '|'}, StringSplitOptions.RemoveEmptyEntries) + .Where(i => !string.Equals(i, audio.Artist, StringComparison.OrdinalIgnoreCase) && !string.Equals(i, audio.AlbumArtist, StringComparison.OrdinalIgnoreCase)); + + audio.AddStudios(studios); } } diff --git a/MediaBrowser.Controller/Providers/Music/LastfmAlbumProvider.cs b/MediaBrowser.Controller/Providers/Music/LastfmAlbumProvider.cs index b48999176..df490f5b6 100644 --- a/MediaBrowser.Controller/Providers/Music/LastfmAlbumProvider.cs +++ b/MediaBrowser.Controller/Providers/Music/LastfmAlbumProvider.cs @@ -65,7 +65,7 @@ namespace MediaBrowser.Controller.Providers.Music var folder = (Folder)item; // Get each song, distinct by the combination of AlbumArtist and Album - var songs = folder.Children.OfType<Audio>().DistinctBy(i => (i.AlbumArtist ?? string.Empty) + (i.Album ?? string.Empty), StringComparer.OrdinalIgnoreCase).ToList(); + var songs = folder.RecursiveChildren.OfType<Audio>().DistinctBy(i => (i.AlbumArtist ?? string.Empty) + (i.Album ?? string.Empty), StringComparer.OrdinalIgnoreCase).ToList(); foreach (var song in songs.Where(song => !string.IsNullOrEmpty(song.Album) && !string.IsNullOrEmpty(song.AlbumArtist))) { diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index cd54ec7c8..75725e589 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -86,6 +86,7 @@ <Compile Include="Net\NetworkShareType.cs" /> <Compile Include="Querying\PersonsQuery.cs" /> <Compile Include="Querying\ThemeSongsResult.cs" /> + <Compile Include="Search\SearchHint.cs" /> <Compile Include="Search\SearchHintResult.cs" /> <Compile Include="Serialization\IJsonSerializer.cs" /> <Compile Include="Serialization\IXmlSerializer.cs" /> diff --git a/MediaBrowser.Model/Search/SearchHint.cs b/MediaBrowser.Model/Search/SearchHint.cs new file mode 100644 index 000000000..1e16b0492 --- /dev/null +++ b/MediaBrowser.Model/Search/SearchHint.cs @@ -0,0 +1,106 @@ +using System; + +namespace MediaBrowser.Model.Search +{ + /// <summary> + /// Class SearchHintResult + /// </summary> + public class SearchHint + { + /// <summary> + /// Gets or sets the item id. + /// </summary> + /// <value>The item id.</value> + public string ItemId { get; set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + + /// <summary> + /// Gets or sets the matched term. + /// </summary> + /// <value>The matched term.</value> + public string MatchedTerm { get; set; } + + /// <summary> + /// Gets or sets the index number. + /// </summary> + /// <value>The index number.</value> + public int? IndexNumber { get; set; } + + /// <summary> + /// Gets or sets the parent index number. + /// </summary> + /// <value>The parent index number.</value> + public int? ParentIndexNumber { get; set; } + + /// <summary> + /// Gets or sets the image tag. + /// </summary> + /// <value>The image tag.</value> + public Guid? PrimaryImageTag { get; set; } + + /// <summary> + /// Gets or sets the type. + /// </summary> + /// <value>The type.</value> + public string Type { get; set; } + + /// <summary> + /// Gets or sets the run time ticks. + /// </summary> + /// <value>The run time ticks.</value> + public long? RunTimeTicks { get; set; } + + /// <summary> + /// Gets or sets the type of the media. + /// </summary> + /// <value>The type of the media.</value> + public string MediaType { get; set; } + + /// <summary> + /// Gets or sets the display type of the media. + /// </summary> + /// <value>The display type of the media.</value> + public string DisplayMediaType { get; set; } + + /// <summary> + /// Gets or sets the series. + /// </summary> + /// <value>The series.</value> + public string Series { get; set; } + + /// <summary> + /// Gets or sets the album. + /// </summary> + /// <value>The album.</value> + public string Album { get; set; } + + /// <summary> + /// Gets or sets the album artist. + /// </summary> + /// <value>The album artist.</value> + public string AlbumArtist { get; set; } + + /// <summary> + /// Gets or sets the artists. + /// </summary> + /// <value>The artists.</value> + public string[] Artists { get; set; } + + /// <summary> + /// Gets or sets the song count. + /// </summary> + /// <value>The song count.</value> + public int? SongCount { get; set; } + + /// <summary> + /// Gets or sets the episode count. + /// </summary> + /// <value>The episode count.</value> + public int? EpisodeCount { get; set; } + } +} diff --git a/MediaBrowser.Model/Search/SearchHintResult.cs b/MediaBrowser.Model/Search/SearchHintResult.cs index 2142ac3f7..372528f82 100644 --- a/MediaBrowser.Model/Search/SearchHintResult.cs +++ b/MediaBrowser.Model/Search/SearchHintResult.cs @@ -1,5 +1,4 @@ -using System; - + namespace MediaBrowser.Model.Search { /// <summary> @@ -8,69 +7,15 @@ namespace MediaBrowser.Model.Search public class SearchHintResult { /// <summary> - /// Gets or sets the item id. - /// </summary> - /// <value>The item id.</value> - public string ItemId { get; set; } - - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public string Name { get; set; } - - /// <summary> - /// Gets or sets the index number. - /// </summary> - /// <value>The index number.</value> - public int? IndexNumber { get; set; } - - /// <summary> - /// Gets or sets the parent index number. - /// </summary> - /// <value>The parent index number.</value> - public int? ParentIndexNumber { get; set; } - - /// <summary> - /// Gets or sets the image tag. - /// </summary> - /// <value>The image tag.</value> - public Guid? PrimaryImageTag { get; set; } - - /// <summary> - /// Gets or sets the type. - /// </summary> - /// <value>The type.</value> - public string Type { get; set; } - - /// <summary> - /// Gets or sets the type of the media. - /// </summary> - /// <value>The type of the media.</value> - public string MediaType { get; set; } - - /// <summary> - /// Gets or sets the series. - /// </summary> - /// <value>The series.</value> - public string Series { get; set; } - - /// <summary> - /// Gets or sets the album. - /// </summary> - /// <value>The album.</value> - public string Album { get; set; } - - /// <summary> - /// Gets or sets the album artist. + /// Gets or sets the search hints. /// </summary> - /// <value>The album artist.</value> - public string AlbumArtist { get; set; } + /// <value>The search hints.</value> + public SearchHint[] SearchHints { get; set; } /// <summary> - /// Gets or sets the artists. + /// Gets or sets the total record count. /// </summary> - /// <value>The artists.</value> - public string[] Artists { get; set; } + /// <value>The total record count.</value> + public int TotalRecordCount { get; set; } } } diff --git a/MediaBrowser.Server.Implementations/Library/LuceneSearchEngine.cs b/MediaBrowser.Server.Implementations/Library/LuceneSearchEngine.cs index d5675578d..5c309a896 100644 --- a/MediaBrowser.Server.Implementations/Library/LuceneSearchEngine.cs +++ b/MediaBrowser.Server.Implementations/Library/LuceneSearchEngine.cs @@ -97,24 +97,26 @@ namespace MediaBrowser.Server.Implementations.Library /// <param name="searchTerm">The search term.</param> /// <returns>IEnumerable{SearchHintResult}.</returns> /// <exception cref="System.ArgumentNullException">searchTerm</exception> - public async Task<IEnumerable<BaseItem>> GetSearchHints(IEnumerable<BaseItem> inputItems, string searchTerm) + public async Task<IEnumerable<SearchHintInfo>> GetSearchHints(IEnumerable<BaseItem> inputItems, string searchTerm) { if (string.IsNullOrEmpty(searchTerm)) { throw new ArgumentNullException("searchTerm"); } - var hints = new List<Tuple<BaseItem, int>>(); + var terms = GetWords(searchTerm); + + var hints = new List<Tuple<BaseItem, string, int>>(); var items = inputItems.Where(i => !(i is MusicArtist)).ToList(); foreach (var item in items) { - var index = IndexOf(item.Name, searchTerm); + var index = GetIndex(item.Name, searchTerm, terms); - if (index != -1) + if (index.Item2 != -1) { - hints.Add(new Tuple<BaseItem, int>(item, index)); + hints.Add(new Tuple<BaseItem, string, int>(item, index.Item1, index.Item2)); } } @@ -127,16 +129,23 @@ namespace MediaBrowser.Server.Implementations.Library foreach (var item in artists) { - var index = IndexOf(item, searchTerm); + var index = GetIndex(item, searchTerm, terms); - if (index != -1) + if (index.Item2 != -1) { - var artist = await _libraryManager.GetArtist(item).ConfigureAwait(false); + try + { + var artist = await _libraryManager.GetArtist(item).ConfigureAwait(false); - hints.Add(new Tuple<BaseItem, int>(artist, index)); + hints.Add(new Tuple<BaseItem, string, int>(artist, index.Item1, index.Item2)); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting {0}", ex, item); + } } } - + // Find genres var genres = items.SelectMany(i => i.Genres) .Where(i => !string.IsNullOrEmpty(i)) @@ -145,13 +154,20 @@ namespace MediaBrowser.Server.Implementations.Library foreach (var item in genres) { - var index = IndexOf(item, searchTerm); + var index = GetIndex(item, searchTerm, terms); - if (index != -1) + if (index.Item2 != -1) { - var genre = await _libraryManager.GetGenre(item).ConfigureAwait(false); + try + { + var genre = await _libraryManager.GetGenre(item).ConfigureAwait(false); - hints.Add(new Tuple<BaseItem, int>(genre, index)); + hints.Add(new Tuple<BaseItem, string, int>(genre, index.Item1, index.Item2)); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting {0}", ex, item); + } } } @@ -163,13 +179,20 @@ namespace MediaBrowser.Server.Implementations.Library foreach (var item in studios) { - var index = IndexOf(item, searchTerm); + var index = GetIndex(item, searchTerm, terms); - if (index != -1) + if (index.Item2 != -1) { - var studio = await _libraryManager.GetStudio(item).ConfigureAwait(false); + try + { + var studio = await _libraryManager.GetStudio(item).ConfigureAwait(false); - hints.Add(new Tuple<BaseItem, int>(studio, index)); + hints.Add(new Tuple<BaseItem, string, int>(studio, index.Item1, index.Item2)); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting {0}", ex, item); + } } } @@ -182,52 +205,93 @@ namespace MediaBrowser.Server.Implementations.Library foreach (var item in persons) { - var index = IndexOf(item, searchTerm); + var index = GetIndex(item, searchTerm, terms); - if (index != -1) + if (index.Item2 != -1) { - var person = await _libraryManager.GetPerson(item).ConfigureAwait(false); + try + { + var person = await _libraryManager.GetPerson(item).ConfigureAwait(false); - hints.Add(new Tuple<BaseItem, int>(person, index)); + hints.Add(new Tuple<BaseItem, string, int>(person, index.Item1, index.Item2)); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting {0}", ex, item); + } } } - return hints.OrderBy(i => i.Item2).Select(i => i.Item1); - } - - /// <summary> - /// Gets the words. - /// </summary> - /// <param name="term">The term.</param> - /// <returns>System.String[][].</returns> - private string[] GetWords(string term) - { - // TODO: Improve this to be more accurate and respect culture - var words = term.Split(' '); - - return words; + return hints.OrderBy(i => i.Item3).Select(i => new SearchHintInfo + { + Item = i.Item1, + MatchedTerm = i.Item2 + }); } /// <summary> - /// Indexes the of. + /// Gets the index. /// </summary> /// <param name="input">The input.</param> - /// <param name="term">The term.</param> + /// <param name="searchInput">The search input.</param> + /// <param name="searchWords">The search input.</param> /// <returns>System.Int32.</returns> - private int IndexOf(string input, string term) + private Tuple<string, int> GetIndex(string input, string searchInput, string[] searchWords) { - var index = 0; + if (string.Equals(input, searchInput, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple<string, int>(searchInput, 0); + } + + var index = input.IndexOf(searchInput, StringComparison.OrdinalIgnoreCase); - foreach (var word in GetWords(input)) + if (index == 0) { - if (word.IndexOf(term, StringComparison.OrdinalIgnoreCase) != -1) + return new Tuple<string, int>(searchInput, 1); + } + if (index > 0) + { + return new Tuple<string, int>(searchInput, 2); + } + + var items = GetWords(input); + + for (var i = 0; i < searchWords.Length; i++) + { + var searchTerm = searchWords[i]; + + for (var j = 0; j < items.Length; j++) { - return index; - } + var item = items[j]; + + if (string.Equals(item, searchTerm, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple<string, int>(searchTerm, 3 + (i + 1) * (j + 1)); + } + + index = item.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase); - index++; + if (index == 0) + { + return new Tuple<string, int>(searchTerm, 4 + (i + 1) * (j + 1)); + } + if (index > 0) + { + return new Tuple<string, int>(searchTerm, 5 + (i + 1) * (j + 1)); + } + } } - return -1; + return new Tuple<string, int>(null, -1); + } + + /// <summary> + /// Gets the words. + /// </summary> + /// <param name="term">The term.</param> + /// <returns>System.String[][].</returns> + private string[] GetWords(string term) + { + return term.Split().Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); } } diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index e0e4f8c9c..385998ca9 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -93,6 +93,21 @@ <Content Include="dashboard-ui\css\images\bgflip.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\css\images\items\searchhints\film.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\items\searchhints\game.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\items\searchhints\music.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\items\searchhints\person.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\css\images\items\searchhints\tv.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\css\images\searchbutton.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
|
