Improving media library browsing

- Adding 'Recent' menu item
- Adding sorting
- Adding filtering

--HG--
branch : 1.x
This commit is contained in:
Sebastien Ros
2013-05-30 18:12:17 -07:00
parent adfc1b53b4
commit 91a9c8669c
8 changed files with 265 additions and 76 deletions

View File

@@ -11,19 +11,24 @@ using Orchard.Mvc;
using Orchard.Themes;
using Orchard.UI.Navigation;
using Orchard.Utility.Extensions;
using Orchard.ContentManagement.MetaData;
using System.Collections.Generic;
namespace Orchard.MediaLibrary.Controllers {
[ValidateInput(false)]
public class AdminController : Controller {
private readonly IMediaLibraryService _mediaLibraryService;
private readonly INavigationManager _navigationManager;
private readonly IContentDefinitionManager _contentDefinitionManager;
public AdminController(
IOrchardServices services,
IMediaLibraryService mediaLibraryService,
INavigationManager navigationManager ) {
INavigationManager navigationManager,
IContentDefinitionManager contentDefinitionManager) {
_mediaLibraryService = mediaLibraryService;
_navigationManager = navigationManager;
_contentDefinitionManager = contentDefinitionManager;
Services = services;
T = NullLocalizer.Instance;
@@ -35,17 +40,22 @@ namespace Orchard.MediaLibrary.Controllers {
public ILogger Logger { get; set; }
public ActionResult Index(int? id, bool dialog = false) {
string stereotype;
var mediaTypes = new List<string>();
foreach(var contentTypeDefinition in _contentDefinitionManager.ListTypeDefinitions()) {
if (contentTypeDefinition.Settings.TryGetValue("Stereotype", out stereotype) && stereotype == "Media")
mediaTypes.Add(contentTypeDefinition.Name);
}
var viewModel = new MediaManagerIndexViewModel {
DialogMode = dialog,
Folders = _mediaLibraryService.GetMediaFolders(),
Folder = id,
Hierarchy = id.HasValue ? _mediaLibraryService.GetMediaFolderHierarchy(id.Value) : Enumerable.Empty<MediaFolder>()
Hierarchy = id.HasValue ? _mediaLibraryService.GetMediaFolderHierarchy(id.Value) : Enumerable.Empty<MediaFolder>(),
MediaTypes = mediaTypes.ToArray()
};
if (!id.HasValue && viewModel.Folders.Any()) {
viewModel.Folder = viewModel.Folders.First().TermId;
}
return View(viewModel);
}
@@ -55,20 +65,21 @@ namespace Orchard.MediaLibrary.Controllers {
var hierarchy = _mediaLibraryService.GetMediaFolderHierarchy(id);
var viewModel = new MediaManagerImportViewModel {
DialogMode = dialog,
Menu = mediaProviderMenu,
Hierarchy = hierarchy.ToReadOnlyCollection(),
ImageSets = imageSets
ImageSets = imageSets,
};
return View(viewModel);
}
[Themed(false)]
public ActionResult MediaItems(int id, int skip = 0, int count = 0) {
var mediaParts = _mediaLibraryService.GetMediaContentItemsForLocation(id, skip, count);
var mediaPartsCount = _mediaLibraryService.GetMediaContentItemsCountForLocation(id);
public ActionResult MediaItems(int id, int skip = 0, int count = 0, string order = "created", string mediaType = "") {
var mediaParts = _mediaLibraryService.GetMediaContentItems(id, skip, count, order, mediaType);
var mediaPartsCount = _mediaLibraryService.GetMediaContentItemsCount(id, mediaType);
var mediaItems = mediaParts.Select(x => new MediaManagerMediaItemViewModel {
MediaPart = x,
@@ -83,6 +94,24 @@ namespace Orchard.MediaLibrary.Controllers {
return View(viewModel);
}
[Themed(false)]
public ActionResult RecentMediaItems(int skip = 0, int count = 0, string order = "created", string mediaType = "") {
var mediaParts = _mediaLibraryService.GetMediaContentItems(skip, count, order, mediaType);
var mediaPartsCount = _mediaLibraryService.GetMediaContentItemsCount(mediaType);
var mediaItems = mediaParts.Select(x => new MediaManagerMediaItemViewModel {
MediaPart = x,
Shape = Services.ContentManager.BuildDisplay(x, "Thumbnail")
}).ToList();
var viewModel = new MediaManagerMediaItemsViewModel {
MediaItems = mediaItems,
MediaItemsCount = mediaPartsCount
};
return View("MediaItems", viewModel);
}
[Themed(false)]
public ActionResult MediaItem(int id, string displayType = "SummaryAdmin") {
var contentItem = Services.ContentManager.Get(id, VersionOptions.Latest);

View File

@@ -23,12 +23,22 @@ namespace Orchard.MediaLibrary {
.Column<string>("Resource", c => c.WithLength(2048))
);
// create the "Media Location" taxonomy
var taxonomy = _contentManager.New<TaxonomyPart>("Taxonomy");
taxonomy.IsInternal = true;
taxonomy.Name = MediaLibraryService.MediaLocation;
_contentManager.Create(taxonomy);
// create the "Media" term
var term = _contentManager.New<TermPart>(taxonomy.TermTypeName);
term.TaxonomyId = taxonomy.Id;
term.Container = taxonomy;
term.Name = "Media";
term.Path = "/";
_contentManager.Create(term, VersionOptions.Published);
ContentDefinitionManager.AlterTypeDefinition("Image", td => td
.DisplayedAs("Image")
.WithSetting("Stereotype", "Media")
@@ -65,21 +75,17 @@ namespace Orchard.MediaLibrary {
.WithPart("TitlePart")
);
return 1;
}
public int UpdateFrom1() {
ContentDefinitionManager.AlterTypeDefinition("OEmbed", td => td
.DisplayedAs("External Media")
.WithSetting("Stereotype", "Media")
.WithPart("CommonPart")
.WithPart("MediaPart")
.WithPart("OEmbedPart")
.WithPart("TitlePart")
);
.DisplayedAs("External Media")
.WithSetting("Stereotype", "Media")
.WithPart("CommonPart")
.WithPart("MediaPart")
.WithPart("OEmbedPart")
.WithPart("TitlePart")
);
return 2;
}
}
}

View File

@@ -19,8 +19,10 @@ namespace Orchard.MediaLibrary.Services {
/// <returns></returns>
IEnumerable<string> GetMediaTypes();
IEnumerable<MediaPart> GetMediaContentItemsForLocation(int? locationId, int skip, int count);
int GetMediaContentItemsCountForLocation(int? locationId);
IEnumerable<MediaPart> GetMediaContentItems(int skip, int count, string order, string mediaType);
IEnumerable<MediaPart> GetMediaContentItems(int folder, int skip, int count, string order, string mediaType);
int GetMediaContentItemsCount(string mediaType);
int GetMediaContentItemsCount(int folder, string mediaType);
MediaPart ImportStream(int termId, Stream stream, string filename);

View File

@@ -9,6 +9,7 @@ using Orchard.MediaLibrary.Factories;
using Orchard.MediaLibrary.Models;
using Orchard.Taxonomies.Models;
using Orchard.Taxonomies.Services;
using Orchard.Core.Title.Models;
namespace Orchard.MediaLibrary.Services {
public class MediaLibraryService : IMediaLibraryService {
@@ -37,24 +38,26 @@ namespace Orchard.MediaLibrary.Services {
var taxonomy = GetMediaLocationTaxonomy();
var terms = _taxonomyService.GetTerms(taxonomy.Id);
var result = new List<MediaFolder>();
var rootFolders = new List<MediaFolder>();
var index = new Dictionary<int, MediaFolder>();
_taxonomyService.CreateHierarchy(terms, (parent, child) => {
MediaFolder parentFolder;
MediaFolder childFolder = CreateMediaFolder(child.TermPart);
index.Add(child.TermPart.Id, childFolder);
// adding to root
if (parent.TermPart == null) {
result.Add(CreateMediaFolder(child.TermPart));
if (parent.TermPart != null) {
parentFolder = index.ContainsKey(parent.TermPart.Id) ? index[parent.TermPart.Id] : null;
parentFolder.Folders.Add(childFolder);
}
else {
var seek = result.FirstOrDefault(x => x.TermId == parent.TermPart.Id);
if (seek != null) {
seek.Folders.Add(CreateMediaFolder(child.TermPart));
}
rootFolders.Add(childFolder);
}
});
return result;
return rootFolders;
}
public MediaFolder GetMediaFolder(int id) {
@@ -90,33 +93,64 @@ namespace Orchard.MediaLibrary.Services {
return _contentManager.Query<MediaPart, MediaPartRecord>();
}
public IEnumerable<MediaPart> GetMediaContentItemsForLocation(int? locationId, int skip, int count) {
if (locationId.HasValue) {
return _contentManager.Query<MediaPart, MediaPartRecord>()
.Where(m => m.TermPartRecord.Id == locationId)
.Join<CommonPartRecord>()
.OrderByDescending(x => x.CreatedUtc)
.Slice(skip, count)
.ToArray();
public IEnumerable<MediaPart> GetMediaContentItems(int folder, int skip, int count, string order, string mediaType) {
var query = _contentManager.Query<MediaPart>();
if (!String.IsNullOrEmpty(mediaType)) {
query = query.ForType(new[] { mediaType });
}
return _contentManager.Query<MediaPart, MediaPartRecord>()
.Where(m => m.TermPartRecord == null)
.Join<CommonPartRecord>()
if (folder > 0) {
query = query.Join<MediaPartRecord>().Where(m => m.TermPartRecord.Id == folder);
}
switch(order) {
case "title":
return query.Join<TitlePartRecord>()
.OrderBy(x => x.Title)
.Slice(skip, count)
.ToArray();
case "modified":
return query.Join<CommonPartRecord>()
.OrderByDescending(x => x.ModifiedUtc)
.Slice(skip, count)
.ToArray();
case "published":
return query.Join<CommonPartRecord>()
.OrderByDescending(x => x.PublishedUtc)
.Slice(skip, count)
.ToArray();
default:
return query.Join<CommonPartRecord>()
.OrderByDescending(x => x.CreatedUtc)
.Slice(skip, count)
.ToArray();
}
}
public int GetMediaContentItemsCountForLocation(int? locationId) {
if (locationId.HasValue) {
return _contentManager.Query<MediaPart, MediaPartRecord>()
.Where(m => m.TermPartRecord.Id == locationId)
.Count();
public IEnumerable<MediaPart> GetMediaContentItems(int skip, int count, string order, string mediaType) {
return GetMediaContentItems(-1, skip, count, order, mediaType);
}
public int GetMediaContentItemsCount(int folder, string mediaType) {
var query = _contentManager.Query<MediaPart>();
if (!String.IsNullOrEmpty(mediaType)) {
query = query.ForType(new[] { mediaType });
}
return _contentManager.Query<MediaPart, MediaPartRecord>()
.Where(m => m.TermPartRecord == null)
.Count();
if (folder > 0) {
query = query.Join<MediaPartRecord>().Where(m => m.TermPartRecord.Id == folder);
}
return query.Count();
}
public int GetMediaContentItemsCount(string mediaType) {
return GetMediaContentItemsCount(-1, mediaType);
}
public MediaPart ImportStream(int termId, Stream stream, string filename) {

View File

@@ -49,6 +49,10 @@
float: right;
}
#media-library-toolbar label {
display: inline;
}
#media-library-main {
display: table-row;
border-bottom: 1px solid #e0e0e0;
@@ -63,9 +67,25 @@
border-right: 1px solid #e0e0e0;
}
#media-library-main-navigation > ul { /* sub-navigations, e.g. folders */
padding-left: 16px;
}
#button-recent {
display: block;
padding:2px;
}
#button-recent:hover, #button-recent.selected {
cursor: pointer;
background-color: #e0e0e0;
}
#button-recent i {
color: rgb(75,75,75);
padding-left:3px;
padding-right: 5px;
font-size:14px;
}
.media-library-folder-title {
box-sizing: border-box;
-moz-box-sizing: border-box;
@@ -82,17 +102,17 @@
background-color: #e0e0e0;
}
.media-library-navigation-folder-icon {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAQCAYAAADwMZRfAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAHNJREFUOE/tkjkOACEIRbmTJ7H1ht6Ai1FRMA6zxAVJjJluTB6RD/xQACKyjSmuYoqrvB9ElBBCQ865lPQ1Qz0amBlSSkJEJb0KljZDw8wkxjhs93Bv6Zt4VP2a92JJ7cGa32TkO5MT62I9hjvZxRTXEDgA7+gHJFX/VbUAAAAASUVORK5CYII=');
background-position: left;
background-repeat: no-repeat;
padding-left: 12px;
}
.media-library-navigation-folder-link {
padding: 0;
}
.media-library-navigation-folder-link i {
color: #808080;
padding-left:3px;
padding-right: 5px;
font-size:14px;
}
.media-library-folder ul {
margin: 0;
padding: 0;

View File

@@ -7,5 +7,6 @@ namespace Orchard.MediaLibrary.ViewModels {
public IEnumerable<MediaFolder> Hierarchy { get; set; }
public int? Folder { get; set; }
public bool DialogMode { get; set; }
public string[] MediaTypes { get; set; }
}
}

View File

@@ -9,7 +9,8 @@
Script.Require("jQueryUI_Droppable");
Script.Include("knockout-2.2.1.js");
Script.Include("history.js");
Style.Require("FontAwesome");
if (Model.DialogMode) {
Style.Include("dialog-mode.css");
}
@@ -20,28 +21,42 @@
<div id="media-library">
<div id="media-library-toolbar">
<a href="#" data-bind="visible: displayed(), click: importMedia" class="button" id="button-import">@T("Import")</a>
<a href="#" data-bind="visible: displayed(), attr: { href: '@HttpUtility.JavaScriptStringEncode(Url.Action("Create", "Folder", new { area = "Orchard.MediaLibrary"}))/' + displayed() }" class="button" id="button-create-folder">@T("Create Folder")</a>
<a href="#" data-bind="visible: displayed(), attr: { href: '@HttpUtility.JavaScriptStringEncode(Url.Action("Edit", "Folder", new { area = "Orchard.MediaLibrary"}))/' + displayed() }" class="button" id="button-edit-folder">@T("Edit Folder")</a>
<label for="filterMediaType">@T("Show")</label>
<select id="filterMediaType" name="FilteredMediaType" data-bind="selectedOptions: mediaType">
@Html.SelectOption("", true, T("Any (show all)").ToString())
@foreach(var mediaType in Model.MediaTypes) {
@Html.SelectOption(mediaType, false, mediaType)
}
</select>
<label for="orderMedia">@T("Ordered by")</label>
<select data-bind="selectedOptions: orderMedia" id="orderMedia" name="OrderMedia">
@Html.SelectOption("title", false, T("title").ToString())
@Html.SelectOption("created", true, T("recently created").ToString())
@Html.SelectOption("published", false, T("recently published").ToString())
@Html.SelectOption("modified", false, T("recently modified").ToString())
</select>
<div id="media-library-toolbar-actions">
<a href="@Url.Action("Create", "Folder", new {id = viewModel.Folder, area = "Orchard.MediaLibrary"})" class="button" id="button-create-folder">@T("Create Folder")</a>
<a href="#" data-bind="attr: { href: '@HttpUtility.JavaScriptStringEncode(Url.Action("Edit", "Folder", new { area = "Orchard.MediaLibrary"}))/' + displayed() }" class="button" id="button-edit-folder">@T("Edit Folder")</a>
</div>
</div>
<div id="media-library-main">
<div id="media-library-main-navigation">
<ul>
<li><a href="#" data-bind="click: selectRecent, css: { selected: !displayed() }" id="button-recent"><i class="icon-time"></i>@T("Recent")</a>
<li id="media-library-main-navigation-tree">
<h3>@T("Categories")</h3>
<ul class="">
<ul>
@foreach (var folder in viewModel.Folders) {
<li>
@Display.Partial(TemplateName: "MediaManagerFolder", Model: folder)
</li>
}
</ul>
</li>
<li><h3>@T("Recent")</h3></li>
<li><h3>@T("Starred")</h3></li>
</ul>
</div>
<div id="media-library-main-list-wrapper">
@@ -148,7 +163,9 @@
self.displayed = ko.observable();
self.pendingRequest = ko.observable(false);
self.mediaItemsCount = 0;
self.orderMedia = ko.observableArray(['created']);
self.mediaType = ko.observableArray([]);
self.getMediaItems = function (id, max) {
if (self.pendingRequest()) {
return;
@@ -161,7 +178,7 @@
self.pendingRequest(true);
var url = '@HttpUtility.JavaScriptStringEncode(Url.Action("MediaItems", "Admin"))/' + id + '?skip=' + self.results().length + '&count=' + max;
var url = '@HttpUtility.JavaScriptStringEncode(Url.Action("MediaItems", "Admin"))/' + id + '?skip=' + self.results().length + '&count=' + max + '&order=' + self.orderMedia() + '&mediaType=' + self.mediaType();
$.ajax({
type: "GET",
@@ -235,6 +252,51 @@
window.history.pushState({ action: 'displayFolder', folder: id }, '', '?folder=' + id);
self.displayFolder(id);
};
self.selectRecent = function () {
window.history.pushState({ action: 'selectRecent' }, '', '?recent');
self.results([]);
self.displayed(null);
var max = 20;
if (self.pendingRequest()) {
return;
}
if (self.results().length > 0 && self.results().length >= self.mediaItemsCount) {
console.log('no more content, mediaItemsCount: ' + self.mediaItemsCount);
return;
}
self.pendingRequest(true);
var url = '@HttpUtility.JavaScriptStringEncode(Url.Action("RecentMediaItems", "Admin"))?skip=' + self.results().length + '&count=' + max + '&order=' + self.orderMedia() + '&mediaType=' + self.mediaType();
$.ajax({
type: "GET",
url: url,
}).done(function (data) {
var mediaItems = data.mediaItems;
self.mediaItemsCount = data.mediaItemsCount;
for (var i = 0; i < mediaItems.length; i++) {
var item = new MediaPartViewModel(mediaItems[i]);
self.results.push(item);
// pre-select result which are already part of the selection
var selection = self.selection();
for (var j = 0; j < selection.length; j++) {
if (selection[j].data.id == item.data.id) {
viewModel.toggleSelect(item, true);
}
}
}
}).fail(function(data) {
console.error(data);
}).always(function () {
self.pendingRequest(false);
});
}
self.toggleSelect = function (searchResult, force) {
var index = $.inArray(searchResult, self.selection());
@@ -264,7 +326,13 @@
self.scrolled = function(data, event) {
var elem = event.target;
if (elem.scrollTop > (elem.scrollHeight - elem.offsetHeight - 300)) {
self.getMediaItems(self.displayed(), 20);
if(seld.displayed()) {
self.getMediaItems(self.displayed(), 20);
}
else {
// todo, infinite scrolling for 'recent'
}
}
};
@@ -272,6 +340,24 @@
var url = '@HttpUtility.JavaScriptStringEncode(Url.Action("Import", "Admin"))/' + self.displayed();
window.location = url;
};
self.orderMedia.subscribe(function (newValue) {
if(self.displayed()) {
self.selectFolder(self.displayed());
}
else {
self.selectRecent();
}
});
self.mediaType.subscribe(function (newValue) {
if(self.displayed()) {
self.selectFolder(self.displayed());
}
else {
self.selectRecent();
}
});
};
var viewModel = new MediaIndexViewModel();
@@ -283,11 +369,21 @@
window.history.pushState({ action: 'displayFolder', folder: @viewModel.Folder.Value }, '', '?folder=@viewModel.Folder.Value');
</text>
}
else {
<text>
viewModel.selectRecent();
window.history.pushState({ action: 'selectRecent' }, '', '?recent');
</text>
}
window.onpopstate = function (event) {
if (event && event.state && event.state.action == 'displayFolder') {
viewModel.displayFolder(event.state.folder);
}
if (event && event.state && event.state.action == 'selectRecent') {
viewModel.selectRecent();
}
};
$("#media-library-main-list").on("mousedown", "li", function (e) {
@@ -366,9 +462,11 @@
},
}).done(function (result) {
if (result) {
viewModel.results.remove(function(item) {
return ids.indexOf(item.data.id) != -1;
});
if(viewModel.displayed()) {
viewModel.results.remove(function(item) {
return ids.indexOf(item.data.id) != -1;
});
}
viewModel.clearSelection();
} else {

View File

@@ -2,8 +2,7 @@
<div class="media-library-folder">
<div class="media-library-folder-title" data-bind="click: function(data, event) { selectFolder(@Model.TermId); }, css: { selected: displayed() == $element.getAttribute('data-term-id') }" data-term-id="@Model.TermId" >
<span class="media-library-navigation-folder-icon">&nbsp;</span>
<a data-bind="disable: $root.pendingRequest" href="#" class="media-library-navigation-folder-link" data-mediapath="@Model.MediaPath">@Model.Name</a>
<a data-bind="disable: $root.pendingRequest" href="#" class="media-library-navigation-folder-link" data-mediapath="@Model.MediaPath"><i class="icon-folder-close-alt"></i>@Model.Name</a>
</div>
@if (Model.Folders.Any()) {
<ul>