diff --git a/package.json b/package.json index 67d88b98e..03f1da66d 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "grunt": "*", "grunt-contrib-handlebars": "*", "grunt-contrib-watch": "*", - "grunt-contrib-less": "*", + "grunt-contrib-less": "0.8.3", "grunt-contrib-copy": "*", "grunt-notify": "*", "grunt-contrib-clean": "*", diff --git a/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs b/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs index 6baebd32c..46af86380 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs +++ b/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Text.RegularExpressions; using Nancy; using NLog; using NzbDrone.Common; @@ -12,6 +13,7 @@ namespace NzbDrone.Api.Frontend.Mappers private readonly IDiskProvider _diskProvider; private readonly IConfigFileProvider _configFileProvider; private readonly string _indexPath; + private static readonly Regex ReplaceRegex = new Regex("(?<=(?:href|src|data-main)=\").*?(?=\")", RegexOptions.Compiled | RegexOptions.IgnoreCase); public IndexHtmlMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, @@ -47,13 +49,15 @@ namespace NzbDrone.Api.Frontend.Mappers return StringToStream(GetIndexText()); } - private string GetIndexText() { var text = _diskProvider.ReadAllText(_indexPath); + text = ReplaceRegex.Replace(text, match => _configFileProvider.UrlBase + match.Value); + text = text.Replace(".css", ".css?v=" + BuildInfo.Version); text = text.Replace(".js", ".js?v=" + BuildInfo.Version); + text = text.Replace("API_ROOT", _configFileProvider.UrlBase + "/api"); text = text.Replace("API_KEY", _configFileProvider.ApiKey); text = text.Replace("APP_VERSION", BuildInfo.Version.ToString()); diff --git a/src/NzbDrone.Api/Frontend/StaticResourceModule.cs b/src/NzbDrone.Api/Frontend/StaticResourceModule.cs index 3c7542075..227d783f0 100644 --- a/src/NzbDrone.Api/Frontend/StaticResourceModule.cs +++ b/src/NzbDrone.Api/Frontend/StaticResourceModule.cs @@ -1,21 +1,25 @@ using System; using System.Collections.Generic; using System.Linq; +using Nancy.Responses; using NLog; using Nancy; using NzbDrone.Api.Frontend.Mappers; +using NzbDrone.Core.Configuration; namespace NzbDrone.Api.Frontend { public class StaticResourceModule : NancyModule { private readonly IEnumerable _requestMappers; + private readonly IConfigFileProvider _configFileProvider; private readonly Logger _logger; - public StaticResourceModule(IEnumerable requestMappers, Logger logger) + public StaticResourceModule(IEnumerable requestMappers, IConfigFileProvider configFileProvider, Logger logger) { _requestMappers = requestMappers; + _configFileProvider = configFileProvider; _logger = logger; Get["/{resource*}"] = x => Index(); @@ -34,8 +38,21 @@ namespace NzbDrone.Api.Frontend return new NotFoundResponse(); } - var mapper = _requestMappers.SingleOrDefault(m => m.CanHandle(path)); + //Redirect to the subfolder if the request went to the base URL + if (path.Equals("/")) + { + var urlBase = _configFileProvider.UrlBase; + if (!String.IsNullOrEmpty(urlBase)) + { + if (Request.Url.BasePath != urlBase) + { + return new RedirectResponse(urlBase + "/"); + } + } + } + + var mapper = _requestMappers.SingleOrDefault(m => m.CanHandle(path)); if (mapper != null) { diff --git a/src/NzbDrone.Api/History/HistoryModule.cs b/src/NzbDrone.Api/History/HistoryModule.cs index 7b0aa3c7e..080e74a46 100644 --- a/src/NzbDrone.Api/History/HistoryModule.cs +++ b/src/NzbDrone.Api/History/HistoryModule.cs @@ -33,6 +33,13 @@ namespace NzbDrone.Api.History SortDirection = pagingResource.SortDirection }; + //This is a hack to deal with backgrid setting the sortKey to the column name instead of sortValue + if (pagingSpec.SortKey.Equals("series", StringComparison.InvariantCultureIgnoreCase)) + { + pagingSpec.SortKey = "series.title"; + } + + if (episodeId.HasValue) { int i = (int)episodeId; diff --git a/src/NzbDrone.Api/ProviderModuleBase.cs b/src/NzbDrone.Api/ProviderModuleBase.cs index 41e2ebe20..33f567850 100644 --- a/src/NzbDrone.Api/ProviderModuleBase.cs +++ b/src/NzbDrone.Api/ProviderModuleBase.cs @@ -43,27 +43,27 @@ namespace NzbDrone.Api private List GetAll() { - var indexerDefinitions = _providerFactory.All(); + var providerDefinitions = _providerFactory.All(); - var result = new List(indexerDefinitions.Count); + var result = new List(providerDefinitions.Count); - foreach (var definition in indexerDefinitions) + foreach (var definition in providerDefinitions) { - var indexerResource = new TProviderResource(); - indexerResource.InjectFrom(definition); - indexerResource.Fields = SchemaBuilder.ToSchema(definition.Settings); + var providerResource = new TProviderResource(); + providerResource.InjectFrom(definition); + providerResource.Fields = SchemaBuilder.ToSchema(definition.Settings); - result.Add(indexerResource); + result.Add(providerResource); } return result; } - private int CreateProvider(TProviderResource indexerResource) + private int CreateProvider(TProviderResource providerResource) { - var indexer = GetDefinition(indexerResource); - indexer = _providerFactory.Create(indexer); - return indexer.Id; + var provider = GetDefinition(providerResource); + provider = _providerFactory.Create(provider); + return provider.Id; } private void UpdateProvider(TProviderResource providerResource) diff --git a/src/NzbDrone.Api/System/SystemModule.cs b/src/NzbDrone.Api/System/SystemModule.cs index f409612fd..be22c390e 100644 --- a/src/NzbDrone.Api/System/SystemModule.cs +++ b/src/NzbDrone.Api/System/SystemModule.cs @@ -43,7 +43,8 @@ namespace NzbDrone.Api.System IsWindows = OsInfo.IsWindows, Branch = _configFileProvider.Branch, Authentication = _configFileProvider.AuthenticationEnabled, - StartOfWeek = (int)OsInfo.FirstDayOfWeek + StartOfWeek = (int)OsInfo.FirstDayOfWeek, + UrlBase = _configFileProvider.UrlBase }.AsResponse(); } diff --git a/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs b/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs index 7a9154672..d2041c6de 100644 --- a/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs +++ b/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs @@ -17,8 +17,6 @@ namespace NzbDrone.Common.Composition protected ContainerBuilderBase(IStartupContext args, params string[] assemblies) { - - _loadedTypes = new List(); foreach (var assembly in assemblies) @@ -55,8 +53,6 @@ namespace NzbDrone.Common.Composition { var implementations = Container.GetImplementations(contractType).Where(c => !c.IsGenericTypeDefinition).ToList(); - - if (implementations.Count == 0) { return; diff --git a/src/NzbDrone.Common/ServiceProvider.cs b/src/NzbDrone.Common/ServiceProvider.cs index 7b140fb9e..e669321cf 100644 --- a/src/NzbDrone.Common/ServiceProvider.cs +++ b/src/NzbDrone.Common/ServiceProvider.cs @@ -139,7 +139,6 @@ namespace NzbDrone.Common } } - public ServiceControllerStatus GetStatus(string serviceName) { return GetService(serviceName).Status; diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/NotRestrictedReleaseSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/NotRestrictedReleaseSpecificationFixture.cs index cdbce8077..ee09defa0 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/NotRestrictedReleaseSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/NotRestrictedReleaseSpecificationFixture.cs @@ -16,12 +16,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void Setup() { _parseResult = new RemoteEpisode - { - Release = new ReleaseInfo - { - Title = "Dexter.S08E01.EDITED.WEBRip.x264-KYR" - } - }; + { + Release = new ReleaseInfo + { + Title = "Dexter.S08E01.EDITED.WEBRip.x264-KYR" + } + }; } [Test] @@ -49,5 +49,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Mocker.GetMock().SetupGet(c => c.ReleaseRestrictions).Returns(restrictions); Subject.IsSatisfiedBy(_parseResult, null).Should().BeTrue(); } + + [Test] + public void should_not_try_to_find_empty_string_as_a_match() + { + Mocker.GetMock().SetupGet(c => c.ReleaseRestrictions).Returns("test\n"); + Subject.IsSatisfiedBy(_parseResult, null).Should().BeTrue(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/IndexerTests/IndexerServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/IndexerServiceFixture.cs index bbfb8404a..6a7a65736 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/IndexerServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/IndexerServiceFixture.cs @@ -27,7 +27,6 @@ namespace NzbDrone.Core.Test.IndexerTests _indexers.Add(new Wombles()); Mocker.SetConstant>(_indexers); - } [Test] @@ -61,7 +60,6 @@ namespace NzbDrone.Core.Test.IndexerTests indexers.Select(c => c.Name).Should().OnlyHaveUniqueItems(); } - [Test] public void should_remove_missing_indexers_on_startup() { @@ -69,13 +67,11 @@ namespace NzbDrone.Core.Test.IndexerTests Mocker.SetConstant(repo); - var existingIndexers = Builder.CreateNew().BuildNew(); existingIndexers.ConfigContract = typeof (NewznabSettings).Name; repo.Insert(existingIndexers); - Subject.Handle(new ApplicationStartedEvent()); AllStoredModels.Should().NotContain(c => c.Id == existingIndexers.Id); diff --git a/src/NzbDrone.Core.Test/IndexerTests/IntegrationTests/IndexerIntegrationTests.cs b/src/NzbDrone.Core.Test/IndexerTests/IntegrationTests/IndexerIntegrationTests.cs index 28dc053e4..1d12f233e 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/IntegrationTests/IndexerIntegrationTests.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/IntegrationTests/IndexerIntegrationTests.cs @@ -20,7 +20,6 @@ namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests public void SetUp() { UseRealHttp(); - } [Test] @@ -39,7 +38,6 @@ namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests ValidateResult(result, skipSize: true, skipInfo: true); } - [Test] public void extv_rss() { @@ -55,7 +53,6 @@ namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests ValidateTorrentResult(result, skipSize: false, skipInfo: true); } - [Test] public void nzbsorg_rss() { @@ -73,9 +70,7 @@ namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests ValidateResult(result); } - - - + private void ValidateResult(IList reports, bool skipSize = false, bool skipInfo = false) { reports.Should().NotBeEmpty(); @@ -97,7 +92,6 @@ namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests private void ValidateTorrentResult(IList reports, bool skipSize = false, bool skipInfo = false) { - reports.Should().OnlyContain(c => c.GetType() == typeof(TorrentInfo)); ValidateResult(reports, skipSize, skipInfo); diff --git a/src/NzbDrone.Core.Test/IndexerTests/SeasonSearchFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/SeasonSearchFixture.cs new file mode 100644 index 000000000..47670ba63 --- /dev/null +++ b/src/NzbDrone.Core.Test/IndexerTests/SeasonSearchFixture.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using FizzWare.NBuilder; +using FluentValidation.Results; +using Moq; +using NUnit.Framework; +using NzbDrone.Common; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.IndexerTests +{ + [TestFixture] + public class SeasonSearchFixture : TestBase + { + private Series _series; + + [SetUp] + public void Setup() + { + _series = Builder.CreateNew().Build(); + + Mocker.GetMock().Setup(s => s.DownloadString(It.IsAny())).Returns(""); + } + + private IndexerBase WithIndexer(bool paging, int resultCount) + { + var results = Builder.CreateListOfSize(resultCount) + .Build(); + + var indexer = Mocker.GetMock>(); + + indexer.Setup(s => s.Parser.Process(It.IsAny(), It.IsAny())) + .Returns(results); + + indexer.Setup(s => s.GetSeasonSearchUrls(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new List { "http://www.nzbdrone.com" }); + + indexer.SetupGet(s => s.SupportsPaging).Returns(paging); + + var definition = new IndexerDefinition(); + definition.Name = "Test"; + + indexer.SetupGet(s => s.Definition) + .Returns(definition); + + return indexer.Object; + } + + [Test] + public void should_not_use_offset_if_result_count_is_less_than_90() + { + var indexer = WithIndexer(true, 25); + Subject.Fetch(indexer, new SeasonSearchCriteria { Series = _series, SceneTitle = _series.Title }); + + Mocker.GetMock().Verify(v => v.DownloadString(It.IsAny()), Times.Once()); + } + + [Test] + public void should_not_use_offset_for_sites_that_do_not_support_it() + { + var indexer = WithIndexer(false, 125); + Subject.Fetch(indexer, new SeasonSearchCriteria { Series = _series, SceneTitle = _series.Title }); + + Mocker.GetMock().Verify(v => v.DownloadString(It.IsAny()), Times.Once()); + } + + [Test] + public void should_not_use_offset_if_its_already_tried_10_times() + { + var indexer = WithIndexer(true, 100); + Subject.Fetch(indexer, new SeasonSearchCriteria { Series = _series, SceneTitle = _series.Title }); + + Mocker.GetMock().Verify(v => v.DownloadString(It.IsAny()), Times.Exactly(10)); + } + } + + public class TestIndexerSettings : IProviderConfig + { + public ValidationResult Validate() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 8e4f7cf7c..d1da02111 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -139,6 +139,7 @@ + diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index 400f03a56..fffab7637 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -31,6 +31,7 @@ namespace NzbDrone.Core.Configuration string ApiKey { get; } bool Torrent { get; } string SslCertHash { get; } + string UrlBase { get; } } public class ConfigFileProvider : IConfigFileProvider @@ -152,6 +153,21 @@ namespace NzbDrone.Core.Configuration get { return GetValue("SslCertHash", ""); } } + public string UrlBase + { + get + { + var urlBase = GetValue("UrlBase", ""); + + if (String.IsNullOrEmpty(urlBase)) + { + return urlBase; + } + + return "/" + urlBase.Trim('/').ToLower(); + } + } + public int GetValueInt(string key, int defaultValue) { return Convert.ToInt32(GetValue(key, defaultValue)); @@ -181,7 +197,7 @@ namespace NzbDrone.Core.Configuration var valueHolder = parentContainer.Descendants(key).ToList(); if (valueHolder.Count() == 1) - return valueHolder.First().Value; + return valueHolder.First().Value.Trim(); //Save the value if (persist) @@ -198,6 +214,7 @@ namespace NzbDrone.Core.Configuration { EnsureDefaultConfigFile(); + var valueString = value.ToString().Trim(); var xDoc = LoadConfigFile(); var config = xDoc.Descendants(CONFIG_ELEMENT_NAME).Single(); @@ -207,15 +224,15 @@ namespace NzbDrone.Core.Configuration if (keyHolder.Count() != 1) { - parentContainer.Add(new XElement(key, value)); + parentContainer.Add(new XElement(key, valueString)); } else { - parentContainer.Descendants(key).Single().Value = value.ToString(); + parentContainer.Descendants(key).Single().Value = valueString; } - _cache.Set(key, value.ToString()); + _cache.Set(key, valueString); xDoc.Save(_configFile); } diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 4a4bda1f3..3b66cad13 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -231,8 +231,8 @@ namespace NzbDrone.Core.Configuration public string ReleaseRestrictions { - get { return GetValue("ReleaseRestrictions", String.Empty); } - set { SetValue("ReleaseRestrictions", value); } + get { return GetValue("ReleaseRestrictions", String.Empty).Trim('\r', '\n'); } + set { SetValue("ReleaseRestrictions", value.Trim('\r', '\n')); } } public Int32 RssSyncInterval diff --git a/src/NzbDrone.Core/DataAugmentation/Xem/RefreshXemCacheCommand.cs b/src/NzbDrone.Core/DataAugmentation/Xem/RefreshXemCacheCommand.cs deleted file mode 100644 index 67dd09657..000000000 --- a/src/NzbDrone.Core/DataAugmentation/Xem/RefreshXemCacheCommand.cs +++ /dev/null @@ -1,8 +0,0 @@ -using NzbDrone.Core.Messaging.Commands; - -namespace NzbDrone.Core.DataAugmentation.Xem -{ - public class RefreshXemCacheCommand : Command - { - } -} diff --git a/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs b/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs index 4dded7395..b7165a7f0 100644 --- a/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs +++ b/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs @@ -1,15 +1,15 @@ using System; using System.Linq; +using System.Web.UI.WebControls; using NLog; using NzbDrone.Common.Cache; -using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv; using NzbDrone.Core.Tv.Events; namespace NzbDrone.Core.DataAugmentation.Xem { - public class XemService : IHandle, IExecute + public class XemService : IHandle, IHandle { private readonly IEpisodeService _episodeService; private readonly IXemProxy _xemProxy; @@ -84,10 +84,13 @@ namespace NzbDrone.Core.DataAugmentation.Xem private void RefreshCache() { - _cache.Clear(); - var ids = _xemProxy.GetXemSeriesIds(); + if (ids.Any()) + { + _cache.Clear(); + } + foreach (var id in ids) { _cache.Set(id.ToString(), true, TimeSpan.FromHours(1)); @@ -110,7 +113,7 @@ namespace NzbDrone.Core.DataAugmentation.Xem PerformUpdate(message.Series); } - public void Execute(RefreshXemCacheCommand message) + public void Handle(SeriesRefreshStartingEvent message) { RefreshCache(); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotRestrictedReleaseSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotRestrictedReleaseSpecification.cs index 628b8183e..7ba369944 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotRestrictedReleaseSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotRestrictedReleaseSpecification.cs @@ -37,7 +37,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return true; } - var restrictions = restrictionsString.Split('\n'); + var restrictions = restrictionsString.Split(new []{ '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (var restriction in restrictions) { diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index d7334c990..487da94cb 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -124,7 +124,7 @@ namespace NzbDrone.Core.Download private List GetHistoryItems(List grabbedHistory, string downloadClientId) { - return grabbedHistory.Where(h => h.Data.ContainsKey(DOWNLOAD_CLIENT) && + return grabbedHistory.Where(h => h.Data.ContainsKey(DOWNLOAD_CLIENT_ID) && h.Data[DOWNLOAD_CLIENT_ID].Equals(downloadClientId)) .ToList(); } diff --git a/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs b/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs index 52c55df60..5f141eacb 100644 --- a/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs +++ b/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs @@ -14,6 +14,14 @@ namespace NzbDrone.Core.Indexers.Eztv } } + public override bool SupportsPaging + { + get + { + return false; + } + } + public override IParseFeed Parser { get diff --git a/src/NzbDrone.Core/Indexers/IIndexer.cs b/src/NzbDrone.Core/Indexers/IIndexer.cs index 34daa6a26..2f850d8de 100644 --- a/src/NzbDrone.Core/Indexers/IIndexer.cs +++ b/src/NzbDrone.Core/Indexers/IIndexer.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.Indexers { IParseFeed Parser { get; } DownloadProtocol Protocol { get; } + Boolean SupportsPaging { get; } IEnumerable RecentFeed { get; } IEnumerable GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber); diff --git a/src/NzbDrone.Core/Indexers/IParseFeed.cs b/src/NzbDrone.Core/Indexers/IParseFeed.cs index 2722669b8..8410d5e04 100644 --- a/src/NzbDrone.Core/Indexers/IParseFeed.cs +++ b/src/NzbDrone.Core/Indexers/IParseFeed.cs @@ -5,6 +5,6 @@ namespace NzbDrone.Core.Indexers { public interface IParseFeed { - IEnumerable Process(string source, string url); + IEnumerable Process(string xml, string url); } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs index 7d42847cd..bca173fcc 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs @@ -34,6 +34,8 @@ namespace NzbDrone.Core.Indexers public abstract DownloadProtocol Protocol { get; } + public abstract bool SupportsPaging { get; } + protected TSettings Settings { get diff --git a/src/NzbDrone.Core/Indexers/IndexerFetchService.cs b/src/NzbDrone.Core/Indexers/IndexerFetchService.cs index 2de0c51b0..57158fec0 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFetchService.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFetchService.cs @@ -13,7 +13,6 @@ namespace NzbDrone.Core.Indexers public interface IFetchFeedFromIndexers { IList FetchRss(IIndexer indexer); - IList Fetch(IIndexer indexer, SeasonSearchCriteria searchCriteria); IList Fetch(IIndexer indexer, SingleEpisodeSearchCriteria searchCriteria); IList Fetch(IIndexer indexer, DailyEpisodeSearchCriteria searchCriteria); @@ -24,7 +23,6 @@ namespace NzbDrone.Core.Indexers private readonly Logger _logger; private readonly IHttpProvider _httpProvider; - public FetchFeedService(IHttpProvider httpProvider, Logger logger) { _httpProvider = httpProvider; @@ -60,12 +58,13 @@ namespace NzbDrone.Core.Indexers var searchUrls = indexer.GetSeasonSearchUrls(searchCriteria.QueryTitle, searchCriteria.Series.TvRageId, searchCriteria.SeasonNumber, offset); var result = Fetch(indexer, searchUrls); - _logger.Info("{0} offset {1}. Found {2}", indexer, searchCriteria, result.Count); - if (result.Count > 90) + if (result.Count > 90 && + offset < 900 && + indexer.SupportsPaging) { - result.AddRange(Fetch(indexer, searchCriteria, offset + 90)); + result.AddRange(Fetch(indexer, searchCriteria, offset + 100)); } return result; diff --git a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs index 8a1be6f66..985ee208b 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs @@ -55,7 +55,6 @@ namespace NzbDrone.Core.Indexers.Newznab }); return list; - } } @@ -73,6 +72,14 @@ namespace NzbDrone.Core.Indexers.Newznab return settings; } + public override bool SupportsPaging + { + get + { + return true; + } + } + public override IEnumerable RecentFeed { get diff --git a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs index c4daeab66..4869b7c2b 100644 --- a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs +++ b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs @@ -66,5 +66,13 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs return searchUrls; } + + public override bool SupportsPaging + { + get + { + return false; + } + } } } diff --git a/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs b/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs index 5355d853d..17180ed99 100644 --- a/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs +++ b/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs @@ -14,6 +14,14 @@ namespace NzbDrone.Core.Indexers.Wombles } } + public override bool SupportsPaging + { + get + { + return false; + } + } + public override IParseFeed Parser { get @@ -24,7 +32,7 @@ namespace NzbDrone.Core.Indexers.Wombles public override IEnumerable RecentFeed { - get { yield return "http://nzb.isasecret.com/rss/?sec=TV&fr=false"; } + get { yield return "http://newshost.co.za/rss/?sec=TV&fr=false"; } } public override IEnumerable GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber) diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index e26eee045..0470c17ee 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -53,10 +53,8 @@ namespace NzbDrone.Core.Jobs new ScheduledTask{ Interval = 1*60, TypeName = typeof(ApplicationUpdateCommand).FullName}, new ScheduledTask{ Interval = 1*60, TypeName = typeof(TrimLogCommand).FullName}, new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName}, - new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshXemCacheCommand).FullName}, new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName}, new ScheduledTask{ Interval = 24*60, TypeName = typeof(HousekeepingCommand).FullName}, - }; var currentTasks = _scheduledTaskRepository.All(); diff --git a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs index a055dbef6..243b9d87f 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs @@ -5,6 +5,7 @@ using System.Net; using NLog; using NzbDrone.Common; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv; using NzbDrone.Core.Tv.Events; @@ -19,16 +20,18 @@ namespace NzbDrone.Core.MediaCover private readonly IHttpProvider _httpProvider; private readonly IDiskProvider _diskProvider; private readonly ICoverExistsSpecification _coverExistsSpecification; + private readonly IConfigFileProvider _configFileProvider; private readonly Logger _logger; private readonly string _coverRootFolder; public MediaCoverService(IHttpProvider httpProvider, IDiskProvider diskProvider, IAppFolderInfo appFolderInfo, - ICoverExistsSpecification coverExistsSpecification, Logger logger) + ICoverExistsSpecification coverExistsSpecification, IConfigFileProvider configFileProvider, Logger logger) { _httpProvider = httpProvider; _diskProvider = diskProvider; _coverExistsSpecification = coverExistsSpecification; + _configFileProvider = configFileProvider; _logger = logger; _coverRootFolder = appFolderInfo.GetMediaCoverPath(); @@ -96,7 +99,7 @@ namespace NzbDrone.Core.MediaCover { var filePath = GetCoverPath(seriesId, mediaCover.CoverType); - mediaCover.Url = @"/MediaCover/" + seriesId + "/" + mediaCover.CoverType.ToString().ToLower() + ".jpg"; + mediaCover.Url = _configFileProvider.UrlBase + @"/MediaCover/" + seriesId + "/" + mediaCover.CoverType.ToString().ToLower() + ".jpg"; if (_diskProvider.FileExists(filePath)) { diff --git a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs index 93d17bb62..eb85e573f 100644 --- a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs +++ b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs @@ -7,18 +7,18 @@ namespace NzbDrone.Core.Notifications.PushBullet { public interface IPushBulletProxy { - void SendNotification(string title, string message, string apiKey, long deviceId); + void SendNotification(string title, string message, string apiKey, string deviceId); } public class PushBulletProxy : IPushBulletProxy, IExecute { private const string URL = "https://api.pushbullet.com/api/pushes"; - public void SendNotification(string title, string message, string apiKey, long deviceId) + public void SendNotification(string title, string message, string apiKey, string deviceId) { var client = new RestClient(URL); - var request = new RestRequest(Method.POST); - request.AddParameter("device_id", deviceId); + var request = BuildRequest(deviceId); + request.AddParameter("type", "note"); request.AddParameter("title", title); request.AddParameter("body", message); @@ -27,6 +27,24 @@ namespace NzbDrone.Core.Notifications.PushBullet client.ExecuteAndValidate(request); } + public RestRequest BuildRequest(string deviceId) + { + var request = new RestRequest(Method.POST); + long integerId; + + if (Int64.TryParse(deviceId, out integerId)) + { + request.AddParameter("device_id", integerId); + } + + else + { + request.AddParameter("device_iden", deviceId); + } + + return request; + } + public void Execute(TestPushBulletCommand message) { const string title = "Test Notification"; diff --git a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletSettings.cs b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletSettings.cs index 1525f6413..375590a0b 100644 --- a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletSettings.cs +++ b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletSettings.cs @@ -11,7 +11,7 @@ namespace NzbDrone.Core.Notifications.PushBullet public PushBulletSettingsValidator() { RuleFor(c => c.ApiKey).NotEmpty(); - RuleFor(c => c.DeviceId).GreaterThan(0); + RuleFor(c => c.DeviceId).NotEmpty(); } } @@ -23,13 +23,13 @@ namespace NzbDrone.Core.Notifications.PushBullet public String ApiKey { get; set; } [FieldDefinition(1, Label = "Device ID")] - public Int64 DeviceId { get; set; } + public String DeviceId { get; set; } public bool IsValid { get { - return !String.IsNullOrWhiteSpace(ApiKey) && DeviceId > 0; + return !String.IsNullOrWhiteSpace(ApiKey) && !String.IsNullOrWhiteSpace(DeviceId); } } diff --git a/src/NzbDrone.Core/Notifications/PushBullet/TestPushBulletCommand.cs b/src/NzbDrone.Core/Notifications/PushBullet/TestPushBulletCommand.cs index e4fe19e83..fd0f6732d 100644 --- a/src/NzbDrone.Core/Notifications/PushBullet/TestPushBulletCommand.cs +++ b/src/NzbDrone.Core/Notifications/PushBullet/TestPushBulletCommand.cs @@ -13,6 +13,6 @@ namespace NzbDrone.Core.Notifications.PushBullet } } public string ApiKey { get; set; } - public long DeviceId { get; set; } + public string DeviceId { get; set; } } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 599e2cdb1..7967454d8 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -139,7 +139,6 @@ - @@ -477,6 +476,7 @@ + diff --git a/src/NzbDrone.Core/Tv/Events/SeriesRefreshStartingEvent.cs b/src/NzbDrone.Core/Tv/Events/SeriesRefreshStartingEvent.cs new file mode 100644 index 000000000..d0e8cca16 --- /dev/null +++ b/src/NzbDrone.Core/Tv/Events/SeriesRefreshStartingEvent.cs @@ -0,0 +1,8 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Tv.Events +{ + public class SeriesRefreshStartingEvent : IEvent + { + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs index acee4ee85..7400a3f43 100644 --- a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs +++ b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs @@ -99,6 +99,8 @@ namespace NzbDrone.Core.Tv public void Execute(RefreshSeriesCommand message) { + _eventAggregator.PublishEvent(new SeriesRefreshStartingEvent()); + if (message.SeriesId.HasValue) { var series = _seriesService.GetSeries(message.SeriesId.Value); diff --git a/src/NzbDrone.Core/Update/Commands/InstallUpdateCommand.cs b/src/NzbDrone.Core/Update/Commands/InstallUpdateCommand.cs index 6f2ec61fa..890a31199 100644 --- a/src/NzbDrone.Core/Update/Commands/InstallUpdateCommand.cs +++ b/src/NzbDrone.Core/Update/Commands/InstallUpdateCommand.cs @@ -5,5 +5,13 @@ namespace NzbDrone.Core.Update.Commands public class InstallUpdateCommand : Command { public UpdatePackage UpdatePackage { get; set; } + + public override bool SendUpdatesToClient + { + get + { + return true; + } + } } } diff --git a/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs b/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs index cca29eb2b..48b8c0209 100644 --- a/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs +++ b/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.EnvironmentInfo; @@ -9,8 +10,7 @@ namespace NzbDrone.Host.AccessControl public interface IUrlAclAdapter { void ConfigureUrl(); - string Url { get; } - string HttpsUrl { get; } + List Urls { get; } } public class UrlAclAdapter : IUrlAclAdapter @@ -20,13 +20,7 @@ namespace NzbDrone.Host.AccessControl private readonly IRuntimeInfo _runtimeInfo; private readonly Logger _logger; - public string Url { get; private set; } - public string HttpsUrl { get; private set; } - - private string _localUrl; - private string _wildcardUrl; - private string _localHttpsUrl; - private string _wildcardHttpsUrl; + public List Urls { get; private set; } public UrlAclAdapter(INetshProvider netshProvider, IConfigFileProvider configFileProvider, @@ -38,26 +32,35 @@ namespace NzbDrone.Host.AccessControl _runtimeInfo = runtimeInfo; _logger = logger; - _localUrl = String.Format("http://localhost:{0}/", _configFileProvider.Port); - _wildcardUrl = String.Format("http://*:{0}/", _configFileProvider.Port); - _localHttpsUrl = String.Format("https://localhost:{0}/", _configFileProvider.SslPort); - _wildcardHttpsUrl = String.Format("https://*:{0}/", _configFileProvider.SslPort); - - Url = _wildcardUrl; - HttpsUrl = _wildcardHttpsUrl; + Urls = new List(); } public void ConfigureUrl() { - if (!_runtimeInfo.IsAdmin) + var localHttpUrls = BuildUrls("http", "localhost", _configFileProvider.Port); + var wildcardHttpUrls = BuildUrls("http", "*", _configFileProvider.Port); + + var localHttpsUrls = BuildUrls("https", "localhost", _configFileProvider.SslPort); + var wildcardHttpsUrls = BuildUrls("https", "*", _configFileProvider.SslPort); + + if (OsInfo.IsWindows && !_runtimeInfo.IsAdmin) { - if (!IsRegistered(_wildcardUrl)) Url = _localUrl; - if (!IsRegistered(_wildcardHttpsUrl)) HttpsUrl = _localHttpsUrl; + var httpUrls = wildcardHttpUrls.All(IsRegistered) ? wildcardHttpUrls : localHttpUrls; + var httpsUrls = wildcardHttpsUrls.All(IsRegistered) ? wildcardHttpsUrls : localHttpsUrls; + + Urls.AddRange(httpUrls); + Urls.AddRange(httpsUrls); } - if (_runtimeInfo.IsAdmin) + else { - RefreshRegistration(); + Urls.AddRange(wildcardHttpUrls); + Urls.AddRange(wildcardHttpsUrls); + + if (OsInfo.IsWindows) + { + RefreshRegistration(); + } } } @@ -66,8 +69,7 @@ namespace NzbDrone.Host.AccessControl if (OsInfo.Version.Major < 6) return; - RegisterUrl(Url); - RegisterUrl(HttpsUrl); + Urls.ForEach(RegisterUrl); } private bool IsRegistered(string urlAcl) @@ -85,5 +87,28 @@ namespace NzbDrone.Host.AccessControl var arguments = String.Format("http add urlacl {0} sddl=D:(A;;GX;;;S-1-1-0)", urlAcl); _netshProvider.Run(arguments); } + + private string BuildUrl(string protocol, string url, int port, string urlBase) + { + var result = protocol + "://" + url + ":" + port; + result += String.IsNullOrEmpty(urlBase) ? "/" : urlBase + "/"; + + return result; + } + + private List BuildUrls(string protocol, string url, int port) + { + var urls = new List(); + var urlBase = _configFileProvider.UrlBase; + + if (!String.IsNullOrEmpty(urlBase)) + { + urls.Add(BuildUrl(protocol, url, port, urlBase)); + } + + urls.Add(BuildUrl(protocol, url, port, "")); + + return urls; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index c42a7cbf1..96ebc1ab6 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -40,7 +40,10 @@ namespace NzbDrone.Host startCallback(_container); } - SpinToExit(appMode); + else + { + SpinToExit(appMode); + } } catch (TerminateApplicationException e) { diff --git a/src/NzbDrone.Host/Owin/OwinHostController.cs b/src/NzbDrone.Host/Owin/OwinHostController.cs index 1a5d4ae9b..f4b967ffb 100644 --- a/src/NzbDrone.Host/Owin/OwinHostController.cs +++ b/src/NzbDrone.Host/Owin/OwinHostController.cs @@ -53,29 +53,25 @@ namespace NzbDrone.Host.Owin _firewallAdapter.MakeAccessible(); _sslAdapter.Register(); } - - _urlAclAdapter.ConfigureUrl(); } - var options = new StartOptions(_urlAclAdapter.Url) + _urlAclAdapter.ConfigureUrl(); + + var options = new StartOptions() { ServerFactory = "Microsoft.Owin.Host.HttpListener" }; - if (_configFileProvider.EnableSsl) + _urlAclAdapter.Urls.ForEach(options.Urls.Add); + + _logger.Info("Listening on the following URLs:"); + foreach (var url in options.Urls) { - _logger.Trace("SSL enabled, listening on: {0}", _urlAclAdapter.HttpsUrl); - options.Urls.Add(_urlAclAdapter.HttpsUrl); + _logger.Info(" {0}", url); } - - _logger.Info("starting server on {0}", _urlAclAdapter.Url); - + try { - // options.ServerFactory = new - //_host = WebApp.Start(OwinServiceProviderFactory.Create(), options, BuildApp); - //_host = WebApp.Start(options, BuildApp); - _host = WebApp.Start(OwinServiceProviderFactory.Create(), options, BuildApp); } catch (TargetInvocationException ex) diff --git a/src/NzbDrone/SysTray/SysTrayApp.cs b/src/NzbDrone/SysTray/SysTrayApp.cs index 1c410d895..bfbc96cb2 100644 --- a/src/NzbDrone/SysTray/SysTrayApp.cs +++ b/src/NzbDrone/SysTray/SysTrayApp.cs @@ -23,7 +23,6 @@ namespace NzbDrone.SysTray _browserService = browserService; } - public void Start() { Application.ThreadException += OnThreadException; diff --git a/src/NzbDrone/WindowsApp.cs b/src/NzbDrone/WindowsApp.cs index d2b82a2a1..452e284cd 100644 --- a/src/NzbDrone/WindowsApp.cs +++ b/src/NzbDrone/WindowsApp.cs @@ -34,7 +34,5 @@ namespace NzbDrone MessageBox.Show(text: message, buttons: MessageBoxButtons.OK, icon: MessageBoxIcon.Error, caption: "Epic Fail!"); } } - - } } diff --git a/src/UI/Cells/Header/QualityHeaderCell.js b/src/UI/Cells/Header/QualityHeaderCell.js deleted file mode 100644 index 533386cb2..000000000 --- a/src/UI/Cells/Header/QualityHeaderCell.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; - -define( - [ - 'backgrid', - 'Shared/Grid/HeaderCell' - ], function (Backgrid, NzbDroneHeaderCell) { - - Backgrid.QualityHeaderCell = NzbDroneHeaderCell.extend({ - events: { - 'click': 'onClick' - }, - - onClick: function (e) { - e.preventDefault(); - - var self = this; - var columnName = this.column.get('name'); - - if (this.column.get('sortable')) { - if (this.direction() === 'ascending') { - this.sort(columnName, 'descending', function (left, right) { - var leftVal = left.get(columnName); - var rightVal = right.get(columnName); - - return self._comparator(leftVal, rightVal); - }); - } - else { - this.sort(columnName, 'ascending', function (left, right) { - var leftVal = left.get(columnName); - var rightVal = right.get(columnName); - - return self._comparator(rightVal, leftVal); - }); - } - } - }, - - _comparator: function (leftVal, rightVal) { - var leftWeight = leftVal.quality.weight; - var rightWeight = rightVal.quality.weight; - - if (!leftWeight && !rightWeight) { - return 0; - } - - if (!leftWeight) { - return -1; - } - - if (!rightWeight) { - return 1; - } - - if (leftWeight === rightWeight) { - return 0; - } - - if (leftWeight > rightWeight) { - return -1; - } - - return 1; - } - }); - - return Backgrid.QualityHeaderCell; - }); diff --git a/src/UI/Cells/SeasonFolderCell.js b/src/UI/Cells/SeasonFolderCell.js index 5c9cc6d1b..e3b437b39 100644 --- a/src/UI/Cells/SeasonFolderCell.js +++ b/src/UI/Cells/SeasonFolderCell.js @@ -8,8 +8,11 @@ define( className : 'season-folder-cell', render: function () { - var seasonFolder = this.model.get('seasonFolder'); + this.$el.empty(); + + var seasonFolder = this.model.get(this.column.get('name')); this.$el.html(seasonFolder.toString()); + return this; } }); diff --git a/src/UI/Content/font.less b/src/UI/Content/font.less index 163ada0a6..f20b10dc1 100644 --- a/src/UI/Content/font.less +++ b/src/UI/Content/font.less @@ -2,46 +2,46 @@ font-family: 'Open Sans'; font-style: normal; font-weight: 300; - src: url('/Content/fonts/opensans-light.eot'); + src: url('./fonts/opensans-light.eot'); src: local('Open Sans Light'), local('OpenSans-Light'), - url('/Content/fonts/opensans-light.eot?#iefix') format('embedded-opentype'), - url('/Content/fonts/opensans-light.woff') format('woff'), - url('/Content/fonts/opensans-light.ttf') format('truetype'); + url('./fonts/opensans-light.eot?#iefix') format('embedded-opentype'), + url('./fonts/opensans-light.woff') format('woff'), + url('./fonts/opensans-light.ttf') format('truetype'); } @font-face { font-family: 'Open Sans'; font-style: normal; font-weight: 400; - src: url('/Content/fonts/opensans-regular.eot'); + src: url('./fonts/opensans-regular.eot'); src: local('Open Sans'), local('OpenSans'), - url('/Content/fonts/opensans-regular.eot?#iefix') format('embedded-opentype'), - url('/Content/fonts/opensans-regular.woff') format('woff'), - url('/Content/fonts/opensans-regular.ttf') format('truetype') + url('./fonts/opensans-regular.eot?#iefix') format('embedded-opentype'), + url('./fonts/opensans-regular.woff') format('woff'), + url('./fonts/opensans-regular.ttf') format('truetype') } @font-face { font-family: 'Open Sans'; font-style: normal; font-weight: 600; - src: url('/Content/fonts/opensans-semibold.eot'); + src: url('./fonts/opensans-semibold.eot'); src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), - url('/Content/fonts/opensans-semibold.eot?#iefix') format('embedded-opentype'), - url('/Content/fonts/opensans-semibold.woff') format('woff'), - url('/Content/fonts/opensans-semibold.ttf') format('truetype') + url('./fonts/opensans-semibold.eot?#iefix') format('embedded-opentype'), + url('./fonts/opensans-semibold.woff') format('woff'), + url('./fonts/opensans-semibold.ttf') format('truetype') } @font-face { font-family: 'Ubuntu Mono'; font-style: normal; font-weight: 400; - src: url('/Content/fonts/ubuntumono-regular.eot'); + src: url('./fonts/ubuntumono-regular.eot'); src: local('Open Sans'), local('OpenSans'), - url('/Content/fonts/ubuntumono-regular.eot?#iefix') format('embedded-opentype'), - url('/Content/fonts/ubuntumono-regular.woff') format('woff'), - url('/Content/fonts/ubuntumono-regular.ttf') format('truetype') + url('./fonts/ubuntumono-regular.eot?#iefix') format('embedded-opentype'), + url('./fonts/ubuntumono-regular.woff') format('woff'), + url('./fonts/ubuntumono-regular.ttf') format('truetype') } \ No newline at end of file diff --git a/src/UI/Content/theme.less b/src/UI/Content/theme.less index 8bf71358e..4e72e020b 100644 --- a/src/UI/Content/theme.less +++ b/src/UI/Content/theme.less @@ -46,6 +46,17 @@ .page-toolbar { margin-top : 10px; margin-bottom : 30px; + + .toolbar-group { + display: inline-block; + } + + .sorting-buttons { + .sorting-title { + display: inline-block; + width: 110px; + } + } } .page-container { diff --git a/src/UI/Episode/Activity/EpisodeActivityLayout.js b/src/UI/Episode/Activity/EpisodeActivityLayout.js index f1ed7cbbb..53de87fdc 100644 --- a/src/UI/Episode/Activity/EpisodeActivityLayout.js +++ b/src/UI/Episode/Activity/EpisodeActivityLayout.js @@ -47,7 +47,7 @@ define( this.model = options.model; this.series = options.series; - this.collection = new HistoryCollection({ episodeId: this.model.id }); + this.collection = new HistoryCollection({ episodeId: this.model.id, tableName: 'episodeActivity' }); this.collection.fetch(); this.listenTo(this.collection, 'sync', this._showTable); }, diff --git a/src/UI/Episode/Search/ManualLayout.js b/src/UI/Episode/Search/ManualLayout.js index c782179df..46dcff938 100644 --- a/src/UI/Episode/Search/ManualLayout.js +++ b/src/UI/Episode/Search/ManualLayout.js @@ -6,10 +6,8 @@ define( 'Cells/FileSizeCell', 'Cells/QualityCell', 'Cells/ApprovalStatusCell', - 'Release/DownloadReportCell', - 'Cells/Header/QualityHeaderCell' - - ], function (Marionette, Backgrid, FileSizeCell, QualityCell, ApprovalStatusCell, DownloadReportCell, QualityHeaderCell) { + 'Release/DownloadReportCell' + ], function (Marionette, Backgrid, FileSizeCell, QualityCell, ApprovalStatusCell, DownloadReportCell) { return Marionette.Layout.extend({ template: 'Episode/Search/ManualLayoutTemplate', @@ -49,7 +47,9 @@ define( label : 'Quality', sortable : true, cell : QualityCell, - headerCell: QualityHeaderCell + sortValue : function (model) { + return model.get('quality').quality.weight; + } }, { diff --git a/src/UI/Handlebars/Helpers/Html.js b/src/UI/Handlebars/Helpers/Html.js index 9c5556c0f..8f4b2b19a 100644 --- a/src/UI/Handlebars/Helpers/Html.js +++ b/src/UI/Handlebars/Helpers/Html.js @@ -2,10 +2,11 @@ define( [ - 'handlebars' - ], function (Handlebars) { + 'handlebars', + 'System/StatusModel' + ], function (Handlebars, StatusModel) { - var placeHolder = '/Content/Images/poster-dark.jpg'; + var placeHolder = StatusModel.get('urlBase') + '/Content/Images/poster-dark.jpg'; window.NzbDrone.imageError = function (img) { if (!img.src.contains(placeHolder)) { @@ -17,4 +18,8 @@ define( Handlebars.registerHelper('defaultImg', function () { return new Handlebars.SafeString('onerror=window.NzbDrone.imageError(this)'); }); + + Handlebars.registerHelper('UrlBase', function () { + return new Handlebars.SafeString(StatusModel.get('urlBase')); + }); }); diff --git a/src/UI/Handlebars/Helpers/Series.js b/src/UI/Handlebars/Helpers/Series.js index 077b87814..9618ccf96 100644 --- a/src/UI/Handlebars/Helpers/Series.js +++ b/src/UI/Handlebars/Helpers/Series.js @@ -2,8 +2,9 @@ define( [ 'handlebars', + 'System/StatusModel', 'underscore' - ], function (Handlebars, _) { + ], function (Handlebars, StatusModel, _) { Handlebars.registerHelper('poster', function () { var poster = _.where(this.images, {coverType: 'poster'}); @@ -32,7 +33,7 @@ define( }); Handlebars.registerHelper('route', function () { - return '/series/' + this.titleSlug; + return StatusModel.get('urlBase') + '/series/' + this.titleSlug; }); Handlebars.registerHelper('percentOfEpisodes', function () { diff --git a/src/UI/History/HistoryCollection.js b/src/UI/History/HistoryCollection.js index 5341c6507..89abc33a8 100644 --- a/src/UI/History/HistoryCollection.js +++ b/src/UI/History/HistoryCollection.js @@ -2,9 +2,10 @@ define( [ 'History/HistoryModel', - 'backbone.pageable' - ], function (HistoryModel, PageableCollection) { - return PageableCollection.extend({ + 'backbone.pageable', + 'Mixins/AsPersistedStateCollection' + ], function (HistoryModel, PageableCollection, AsPersistedStateCollection) { + var collection = PageableCollection.extend({ url : window.NzbDrone.ApiRoot + '/history', model: HistoryModel, @@ -48,4 +49,6 @@ define( return resp; } }); + + return AsPersistedStateCollection.apply(collection); }); diff --git a/src/UI/History/Table/HistoryTableLayout.js b/src/UI/History/Table/HistoryTableLayout.js index 9e3263c71..3571c69d1 100644 --- a/src/UI/History/Table/HistoryTableLayout.js +++ b/src/UI/History/Table/HistoryTableLayout.js @@ -45,7 +45,8 @@ define( { name : 'series', label: 'Series', - cell : SeriesTitleCell + cell : SeriesTitleCell, + sortValue: 'series.title' }, { name : 'episode', @@ -80,7 +81,7 @@ define( initialize: function () { - this.collection = new HistoryCollection(); + this.collection = new HistoryCollection({ tableName: 'history' }); this.listenTo(this.collection, 'sync', this._showTable); }, diff --git a/src/UI/JsLibraries/backbone.backgrid.js b/src/UI/JsLibraries/backbone.backgrid.js index 94932e472..6a0af616c 100644 --- a/src/UI/JsLibraries/backbone.backgrid.js +++ b/src/UI/JsLibraries/backbone.backgrid.js @@ -1,11 +1,25 @@ -/* +/*! backgrid http://github.com/wyuenho/backgrid - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT @license. + Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors + Licensed under the MIT license. */ -(function (root, $, _, Backbone) { + +(function (factory) { + + // CommonJS + if (typeof exports == "object") { + module.exports = factory(module.exports, + require("underscore"), + require("backbone")); + } + // Browser + else if (typeof _ !== "undefined" && + typeof Backbone !== "undefined") { + factory(window, _, Backbone); + } +}(function (root, _, Backbone) { "use strict"; /* @@ -13,11 +27,9 @@ http://github.com/wyuenho/backgrid Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT @license. + Licensed under the MIT license. */ -var window = root; - // Copyright 2009, 2010 Kristopher Michael Kowal // https://github.com/kriskowal/es5-shim // ES5 15.5.4.20 @@ -41,12 +53,6 @@ if (!String.prototype.trim || ws.trim()) { }; } -function capitalize(s) { - return s.charAt(0).toUpperCase() + s.slice(1); - -// return String.fromCharCode(s.charCodeAt(0) - 32) + s.slice(1); -} - function lpad(str, length, padstr) { var paddingLen = length - (str + '').length; paddingLen = paddingLen < 0 ? 0 : paddingLen; @@ -57,24 +63,19 @@ function lpad(str, length, padstr) { return padding + str; } +var $ = Backbone.$; + var Backgrid = root.Backgrid = { - VERSION: "0.2.6", + VERSION: "0.3.0", Extension: {}, - requireOptions: function (options, requireOptionKeys) { - for (var i = 0; i < requireOptionKeys.length; i++) { - var key = requireOptionKeys[i]; - if (_.isUndefined(options[key])) { - throw new TypeError("'" + key + "' is required"); - } - } - }, - resolveNameToClass: function (name, suffix) { if (_.isString(name)) { - var key = _.map(name.split('-'), function (e) { return capitalize(e); }).join('') + suffix; + var key = _.map(name.split('-'), function (e) { + return e.slice(0, 1).toUpperCase() + e.slice(1); + }).join('') + suffix; var klass = Backgrid[key] || Backgrid.Extension[key]; if (_.isUndefined(klass)) { throw new ReferenceError("Class '" + key + "' not found"); @@ -83,7 +84,17 @@ var Backgrid = root.Backgrid = { } return name; + }, + + callByNeed: function () { + var value = arguments[0]; + if (!_.isFunction(value)) return value; + + var context = arguments[1]; + var args = [].slice.call(arguments, 2); + return value.apply(context, !!(args + '') ? args : void 0); } + }; _.extend(Backgrid, Backbone.Events); @@ -101,7 +112,7 @@ _.extend(Backgrid, Backbone.Events); var Command = Backgrid.Command = function (evt) { _.extend(this, { altKey: !!evt.altKey, - char: evt.char, + "char": evt["char"], charCode: evt.charCode, ctrlKey: !!evt.ctrlKey, key: evt.key, @@ -162,12 +173,13 @@ _.extend(Command.prototype, { } }); + /* backgrid http://github.com/wyuenho/backgrid Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT @license. + Licensed under the MIT license. */ /** @@ -191,9 +203,10 @@ _.extend(CellFormatter.prototype, { @member Backgrid.CellFormatter @param {*} rawData + @param {Backbone.Model} model Used for more complicated formatting @return {*} */ - fromRaw: function (rawData) { + fromRaw: function (rawData, model) { return rawData; }, @@ -206,16 +219,18 @@ _.extend(CellFormatter.prototype, { @member Backgrid.CellFormatter @param {string} formattedData + @param {Backbone.Model} model Used for more complicated formatting @return {*|undefined} */ - toRaw: function (formattedData) { + toRaw: function (formattedData, model) { return formattedData; } }); /** - A floating point number formatter. Doesn't understand notation at the moment. + A floating point number formatter. Doesn't understand scientific notation at + the moment. @class Backgrid.NumberFormatter @extends Backgrid.CellFormatter @@ -223,8 +238,7 @@ _.extend(CellFormatter.prototype, { @throws {RangeError} If decimals < 0 or > 20. */ var NumberFormatter = Backgrid.NumberFormatter = function (options) { - options = options ? _.clone(options) : {}; - _.extend(this, this.defaults, options); + _.extend(this, this.defaults, options || {}); if (this.decimals < 0 || this.decimals > 20) { throw new RangeError("decimals must be between 0 and 20"); @@ -261,9 +275,10 @@ _.extend(NumberFormatter.prototype, { @member Backgrid.NumberFormatter @param {number} number + @param {Backbone.Model} model Used for more complicated formatting @return {string} */ - fromRaw: function (number) { + fromRaw: function (number, model) { if (_.isNull(number) || _.isUndefined(number)) return ''; number = number.toFixed(~~this.decimals); @@ -281,13 +296,18 @@ _.extend(NumberFormatter.prototype, { @member Backgrid.NumberFormatter @param {string} formattedData + @param {Backbone.Model} model Used for more complicated formatting @return {number|undefined} Undefined if the string cannot be converted to a number. */ - toRaw: function (formattedData) { + toRaw: function (formattedData, model) { + formattedData = formattedData.trim(); + + if (formattedData === '') return null; + var rawData = ''; - var thousands = formattedData.trim().split(this.orderSeparator); + var thousands = formattedData.split(this.orderSeparator); for (var i = 0; i < thousands.length; i++) { rawData += thousands[i]; } @@ -322,8 +342,7 @@ _.extend(NumberFormatter.prototype, { @throws {Error} If both `includeDate` and `includeTime` are false. */ var DatetimeFormatter = Backgrid.DatetimeFormatter = function (options) { - options = options ? _.clone(options) : {}; - _.extend(this, this.defaults, options); + _.extend(this, this.defaults, options || {}); if (!this.includeDate && !this.includeTime) { throw new Error("Either includeDate or includeTime must be true"); @@ -357,6 +376,8 @@ _.extend(DatetimeFormatter.prototype, { ISO_SPLITTER_RE: /T|Z| +/, _convert: function (data, validate) { + if ((data + '').trim() === '') return null; + var date, time = null; if (_.isNumber(data)) { var jsDate = new Date(data); @@ -415,10 +436,11 @@ _.extend(DatetimeFormatter.prototype, { @member Backgrid.DatetimeFormatter @param {string} rawData + @param {Backbone.Model} model Used for more complicated formatting @return {string|null|undefined} ISO-8601 string in UTC. Null and undefined values are returned as is. */ - fromRaw: function (rawData) { + fromRaw: function (rawData, model) { if (_.isNull(rawData) || _.isUndefined(rawData)) return ''; return this._convert(rawData); }, @@ -432,12 +454,13 @@ _.extend(DatetimeFormatter.prototype, { @member Backgrid.DatetimeFormatter @param {string} formattedData + @param {Backbone.Model} model Used for more complicated formatting @return {string|undefined} ISO-8601 string in UTC. Undefined if a date is found when `includeDate` is false, or a time is found when `includeTime` is false, or if `includeDate` is true and a date is not found, or if `includeTime` is true and a time is not found. */ - toRaw: function (formattedData) { + toRaw: function (formattedData, model) { return this._convert(formattedData, true); } @@ -460,9 +483,10 @@ _.extend(StringFormatter.prototype, { @member Backgrid.StringFormatter @param {*} rawValue + @param {Backbone.Model} model Used for more complicated formatting @return {string} */ - fromRaw: function (rawValue) { + fromRaw: function (rawValue, model) { if (_.isUndefined(rawValue) || _.isNull(rawValue)) return ''; return rawValue + ''; } @@ -485,9 +509,10 @@ _.extend(EmailFormatter.prototype, { @member Backgrid.EmailFormatter @param {*} formattedData + @param {Backbone.Model} model Used for more complicated formatting @return {string|undefined} */ - toRaw: function (formattedData) { + toRaw: function (formattedData, model) { var parts = formattedData.trim().split("@"); if (parts.length === 2 && _.all(parts)) { return formattedData; @@ -511,19 +536,21 @@ _.extend(SelectFormatter.prototype, { @member Backgrid.SelectFormatter @param {*} rawValue + @param {Backbone.Model} model Used for more complicated formatting @return {Array.<*>} */ - fromRaw: function (rawValue) { + fromRaw: function (rawValue, model) { return _.isArray(rawValue) ? rawValue : rawValue != null ? [rawValue] : []; } }); + /* backgrid http://github.com/wyuenho/backgrid Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT @license. + Licensed under the MIT license. */ /** @@ -548,7 +575,6 @@ var CellEditor = Backgrid.CellEditor = Backbone.View.extend({ `model` or `column` are undefined. */ initialize: function (options) { - Backgrid.requireOptions(options, ["formatter", "column", "model"]); this.formatter = options.formatter; this.column = options.column; if (!(this.column instanceof Column)) { @@ -607,7 +633,7 @@ var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({ @param {string} [options.placeholder] */ initialize: function (options) { - CellEditor.prototype.initialize.apply(this, arguments); + InputCellEditor.__super__.initialize.apply(this, arguments); if (options.placeholder) { this.$el.attr("placeholder", options.placeholder); @@ -619,7 +645,8 @@ var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({ exists. */ render: function () { - this.$el.val(this.formatter.fromRaw(this.model.get(this.column.get("name")))); + var model = this.model + this.$el.val(this.formatter.fromRaw(model.get(this.column.get("name")), model)); return this; }, @@ -656,7 +683,7 @@ var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({ e.stopPropagation(); var val = this.$el.val(); - var newValue = formatter.toRaw(val); + var newValue = formatter.toRaw(val, model); if (_.isUndefined(newValue)) { model.trigger("backgrid:error", model, column, val); } @@ -705,9 +732,9 @@ var Cell = Backgrid.Cell = Backbone.View.extend({ tagName: "td", /** - @property {Backgrid.CellFormatter|Object|string} [formatter=new CellFormatter()] + @property {Backgrid.CellFormatter|Object|string} [formatter=CellFormatter] */ - formatter: new CellFormatter(), + formatter: CellFormatter, /** @property {Backgrid.CellEditor} [editor=Backgrid.InputCellEditor] The @@ -734,17 +761,43 @@ var Cell = Backgrid.Cell = Backbone.View.extend({ said name cannot be found in the Backgrid module. */ initialize: function (options) { - Backgrid.requireOptions(options, ["model", "column"]); this.column = options.column; if (!(this.column instanceof Column)) { this.column = new Column(this.column); } - this.formatter = Backgrid.resolveNameToClass(this.column.get("formatter") || this.formatter, "Formatter"); + + var column = this.column, model = this.model, $el = this.$el; + + var formatter = Backgrid.resolveNameToClass(column.get("formatter") || + this.formatter, "Formatter"); + + if (!_.isFunction(formatter.fromRaw) && !_.isFunction(formatter.toRaw)) { + formatter = new formatter(); + } + + this.formatter = formatter; + this.editor = Backgrid.resolveNameToClass(this.editor, "CellEditor"); - this.listenTo(this.model, "change:" + this.column.get("name"), function () { - if (!this.$el.hasClass("editor")) this.render(); + + this.listenTo(model, "change:" + column.get("name"), function () { + if (!$el.hasClass("editor")) this.render(); }); - this.listenTo(this.model, "backgrid:error", this.renderError); + + this.listenTo(model, "backgrid:error", this.renderError); + + this.listenTo(column, "change:editable change:sortable change:renderable", + function (column) { + var changed = column.changedAttributes(); + for (var key in changed) { + if (changed.hasOwnProperty(key)) { + $el.toggleClass(key, changed[key]); + } + } + }); + + if (column.get("editable")) $el.addClass("editable"); + if (column.get("sortable")) $el.addClass("sortable"); + if (column.get("renderable")) $el.addClass("renderable"); }, /** @@ -753,7 +806,8 @@ var Cell = Backgrid.Cell = Backbone.View.extend({ */ render: function () { this.$el.empty(); - this.$el.text(this.formatter.fromRaw(this.model.get(this.column.get("name")))); + var model = this.model; + this.$el.text(this.formatter.fromRaw(model.get(this.column.get("name")), model)); this.delegateEvents(); return this; }, @@ -781,7 +835,8 @@ var Cell = Backgrid.Cell = Backbone.View.extend({ var model = this.model; var column = this.column; - if (column.get("editable")) { + var editable = Backgrid.callByNeed(column.editable(), column, model); + if (editable) { this.currentEditor = new this.editor({ column: this.column, @@ -830,10 +885,10 @@ var Cell = Backgrid.Cell = Backbone.View.extend({ */ remove: function () { if (this.currentEditor) { - this.currentEditor.remove.apply(this, arguments); + this.currentEditor.remove.apply(this.currentEditor, arguments); delete this.currentEditor; } - return Backbone.View.prototype.remove.apply(this, arguments); + return Cell.__super__.remove.apply(this, arguments); } }); @@ -849,7 +904,7 @@ var StringCell = Backgrid.StringCell = Cell.extend({ /** @property */ className: "string-cell", - formatter: new StringFormatter() + formatter: StringFormatter }); @@ -869,14 +924,33 @@ var UriCell = Backgrid.UriCell = Cell.extend({ /** @property */ className: "uri-cell", + /** + @property {string} [title] The title attribute of the generated anchor. It + uses the display value formatted by the `formatter.fromRaw` by default. + */ + title: null, + + /** + @property {string} [target="_blank"] The target attribute of the generated + anchor. + */ + target: "_blank", + + initialize: function (options) { + UriCell.__super__.initialize.apply(this, arguments); + this.title = options.title || this.title; + this.target = options.target || this.target; + }, + render: function () { this.$el.empty(); - var formattedValue = this.formatter.fromRaw(this.model.get(this.column.get("name"))); + var rawValue = this.model.get(this.column.get("name")); + var formattedValue = this.formatter.fromRaw(rawValue, this.model); this.$el.append($("", { tabIndex: -1, - href: formattedValue, - title: formattedValue, - target: "_blank" + href: rawValue, + title: this.title || formattedValue, + target: this.target, }).text(formattedValue)); this.delegateEvents(); return this; @@ -897,11 +971,12 @@ var EmailCell = Backgrid.EmailCell = StringCell.extend({ /** @property */ className: "email-cell", - formatter: new EmailFormatter(), + formatter: EmailFormatter, render: function () { this.$el.empty(); - var formattedValue = this.formatter.fromRaw(this.model.get(this.column.get("name"))); + var model = this.model; + var formattedValue = this.formatter.fromRaw(model.get(this.column.get("name")), model); this.$el.append($("", { tabIndex: -1, href: "mailto:" + formattedValue, @@ -947,12 +1022,11 @@ var NumberCell = Backgrid.NumberCell = Cell.extend({ @param {Backgrid.Column} options.column */ initialize: function (options) { - Cell.prototype.initialize.apply(this, arguments); - this.formatter = new this.formatter({ - decimals: this.decimals, - decimalSeparator: this.decimalSeparator, - orderSeparator: this.orderSeparator - }); + NumberCell.__super__.initialize.apply(this, arguments); + var formatter = this.formatter; + formatter.decimals = this.decimals; + formatter.decimalSeparator = this.decimalSeparator; + formatter.orderSeparator = this.orderSeparator; } }); @@ -1021,12 +1095,11 @@ var DatetimeCell = Backgrid.DatetimeCell = Cell.extend({ @param {Backgrid.Column} options.column */ initialize: function (options) { - Cell.prototype.initialize.apply(this, arguments); - this.formatter = new this.formatter({ - includeDate: this.includeDate, - includeTime: this.includeTime, - includeMilli: this.includeMilli - }); + DatetimeCell.__super__.initialize.apply(this, arguments); + var formatter = this.formatter; + formatter.includeDate = this.includeDate; + formatter.includeTime = this.includeTime; + formatter.includeMilli = this.includeMilli; var placeholder = this.includeDate ? "YYYY-MM-DD" : ""; placeholder += (this.includeDate && this.includeTime) ? "T" : ""; @@ -1109,7 +1182,8 @@ var BooleanCellEditor = Backgrid.BooleanCellEditor = CellEditor.extend({ uncheck otherwise. */ render: function () { - var val = this.formatter.fromRaw(this.model.get(this.column.get("name"))); + var model = this.model; + var val = this.formatter.fromRaw(model.get(this.column.get("name")), model); this.$el.prop("checked", val); return this; }, @@ -1147,12 +1221,12 @@ var BooleanCellEditor = Backgrid.BooleanCellEditor = CellEditor.extend({ command.moveDown()) { e.preventDefault(); e.stopPropagation(); - var val = formatter.toRaw($el.prop("checked")); + var val = formatter.toRaw($el.prop("checked"), model); model.set(column.get("name"), val); model.trigger("backgrid:edited", model, column, command); } else if (e.type == "change") { - var val = formatter.toRaw($el.prop("checked")); + var val = formatter.toRaw($el.prop("checked"), model); model.set(column.get("name"), val); $el.focus(); } @@ -1186,10 +1260,13 @@ var BooleanCell = Backgrid.BooleanCell = Cell.extend({ */ render: function () { this.$el.empty(); + var model = this.model, column = this.column; + var editable = Backgrid.callByNeed(column.editable(), column, model); this.$el.append($("", { tabIndex: -1, type: "checkbox", - checked: this.formatter.fromRaw(this.model.get(this.column.get("name"))) + checked: this.formatter.fromRaw(model.get(column.get("name")), model), + disabled: !editable })); this.delegateEvents(); return this; @@ -1216,10 +1293,11 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ }, /** @property {function(Object, ?Object=): string} template */ - template: _.template(''), + template: _.template('', null, {variable: null}), setOptionValues: function (optionValues) { this.optionValues = optionValues; + this.optionValues = _.result(this, "optionValues"); }, setMultiple: function (multiple) { @@ -1250,9 +1328,10 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ this.$el.empty(); var optionValues = _.result(this, "optionValues"); - var selectedValues = this.formatter.fromRaw(this.model.get(this.column.get("name"))); + var model = this.model; + var selectedValues = this.formatter.fromRaw(model.get(this.column.get("name")), model); - if (!_.isArray(optionValues)) throw TypeError("optionValues must be an array"); + if (!_.isArray(optionValues)) throw new TypeError("optionValues must be an array"); var optionValue = null; var optionText = null; @@ -1280,7 +1359,7 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ this.$el.append(optgroup); } else { - throw TypeError("optionValues elements must be a name-value pair or an object hash of { name: 'optgroup label', value: [option name-value pairs] }"); + throw new TypeError("optionValues elements must be a name-value pair or an object hash of { name: 'optgroup label', value: [option name-value pairs] }"); } } @@ -1296,7 +1375,7 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ save: function (e) { var model = this.model; var column = this.column; - model.set(column.get("name"), this.formatter.toRaw(this.$el.val())); + model.set(column.get("name"), this.formatter.toRaw(this.$el.val(), model)); model.trigger("backgrid:edited", model, column, new Command(e)); }, @@ -1317,7 +1396,7 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ e.preventDefault(); e.stopPropagation(); if (e.type == "blur" && this.$el.find("option").length === 1) { - model.set(column.get("name"), this.formatter.toRaw(this.$el.val())); + model.set(column.get("name"), this.formatter.toRaw(this.$el.val(), model)); } model.trigger("backgrid:edited", model, column, new Command(e)); } @@ -1369,7 +1448,7 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({ multiple: false, /** @property */ - formatter: new SelectFormatter(), + formatter: SelectFormatter, /** @property {Array.|Array.<{name: string, values: Array.}>} optionValues @@ -1389,8 +1468,7 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({ @throws {TypeError} If `optionsValues` is undefined. */ initialize: function (options) { - Cell.prototype.initialize.apply(this, arguments); - Backgrid.requireOptions(this, ["optionValues"]); + SelectCell.__super__.initialize.apply(this, arguments); this.listenTo(this.model, "backgrid:edit", function (model, column, cell, editor) { if (column.get("name") == this.column.get("name")) { editor.setOptionValues(this.optionValues); @@ -1407,8 +1485,9 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({ render: function () { this.$el.empty(); - var optionValues = this.optionValues; - var rawData = this.formatter.fromRaw(this.model.get(this.column.get("name"))); + var optionValues = _.result(this, "optionValues"); + var model = this.model; + var rawData = this.formatter.fromRaw(model.get(this.column.get("name")), model); var selectedText = []; @@ -1447,7 +1526,7 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({ } catch (ex) { if (ex instanceof TypeError) { - throw TypeError("'optionValues' must be of type {Array.|Array.<{name: string, values: Array.}>}"); + throw new TypeError("'optionValues' must be of type {Array.|Array.<{name: string, values: Array.}>}"); } throw ex; } @@ -1458,12 +1537,13 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({ } }); + /* backgrid http://github.com/wyuenho/backgrid Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT @license. + Licensed under the MIT license. */ /** @@ -1475,9 +1555,77 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({ @class Backgrid.Column @extends Backbone.Model - */ +*/ var Column = Backgrid.Column = Backbone.Model.extend({ + /** + @cfg {Object} defaults Column defaults. To override any of these default + values, you can either change the prototype directly to override + Column.defaults globally or extend Column and supply the custom class to + Backgrid.Grid: + + // Override Column defaults globally + Column.prototype.defaults.sortable = false; + + // Override Column defaults locally + var MyColumn = Column.extend({ + defaults: _.defaults({ + editable: false + }, Column.prototype.defaults) + }); + + var grid = new Backgrid.Grid(columns: new Columns([{...}, {...}], { + model: MyColumn + })); + + @cfg {string} [defaults.name] The default name of the model attribute. + + @cfg {string} [defaults.label] The default label to show in the header. + + @cfg {string|Backgrid.Cell} [defaults.cell] The default cell type. If this + is a string, the capitalized form will be used to look up a cell class in + Backbone, i.e.: string => StringCell. If a Cell subclass is supplied, it is + initialized with a hash of parameters. If a Cell instance is supplied, it + is used directly. + + @cfg {string|Backgrid.HeaderCell} [defaults.headerCell] The default header + cell type. + + @cfg {boolean|string} [defaults.sortable=true] Whether this column is + sortable. If the value is a string, a method will the same name will be + looked up from the column instance to determine whether the column should + be sortable. The method's signature must be `function (Backgrid.Column, + Backbone.Model): boolean`. + + @cfg {boolean|string} [defaults.editable=true] Whether this column is + editable. If the value is a string, a method will the same name will be + looked up from the column instance to determine whether the column should + be editable. The method's signature must be `function (Backgrid.Column, + Backbone.Model): boolean`. + + @cfg {boolean|string} [defaults.renderable=true] Whether this column is + renderable. If the value is a string, a method will the same name will be + looked up from the column instance to determine whether the column should + be renderable. The method's signature must be `function (Backrid.Column, + Backbone.Model): boolean`. + + @cfg {Backgrid.CellFormatter | Object | string} [defaults.formatter] The + formatter to use to convert between raw model values and user input. + + @cfg {"toggle"|"cycle"} [defaults.sortType="cycle"] Whether sorting will + toggle between ascending and descending order, or cycle between insertion + order, ascending and descending order. + + @cfg {(function(Backbone.Model, string): *) | string} [defaults.sortValue] + The function to use to extract a value from the model for comparison during + sorting. If this value is a string, a method with the same name will be + looked up from the column instance. + + @cfg {"ascending"|"descending"|null} [defaults.direction=null] The initial + sorting direction for this column. The default is ordered by + Backbone.Model.cid, which usually means the collection is ordered by + insertion order. + */ defaults: { name: undefined, label: undefined, @@ -1485,6 +1633,9 @@ var Column = Backgrid.Column = Backbone.Model.extend({ editable: true, renderable: true, formatter: undefined, + sortType: "cycle", + sortValue: undefined, + direction: null, cell: undefined, headerCell: undefined }, @@ -1492,42 +1643,103 @@ var Column = Backgrid.Column = Backbone.Model.extend({ /** Initializes this Column instance. - @param {Object} attrs Column attributes. - @param {string} attrs.name The name of the model attribute. - @param {string|Backgrid.Cell} attrs.cell The cell type. - If this is a string, the capitalized form will be used to look up a - cell class in Backbone, i.e.: string => StringCell. If a Cell subclass - is supplied, it is initialized with a hash of parameters. If a Cell - instance is supplied, it is used directly. - @param {string|Backgrid.HeaderCell} [attrs.headerCell] The header cell type. - @param {string} [attrs.label] The label to show in the header. - @param {boolean} [attrs.sortable=true] - @param {boolean} [attrs.editable=true] - @param {boolean} [attrs.renderable=true] - @param {Backgrid.CellFormatter|Object|string} [attrs.formatter] The - formatter to use to convert between raw model values and user input. + @param {Object} attrs + + @param {string} attrs.name The model attribute this column is responsible + for. + + @param {string|Backgrid.Cell} attrs.cell The cell type to use to render + this column. + + @param {string} [attrs.label] + + @param {string|Backgrid.HeaderCell} [attrs.headerCell] + + @param {boolean|string} [attrs.sortable=true] + + @param {boolean|string} [attrs.editable=true] + + @param {boolean|string} [attrs.renderable=true] + + @param {Backgrid.CellFormatter | Object | string} [attrs.formatter] + + @param {"toggle"|"cycle"} [attrs.sortType="cycle"] + + @param {(function(Backbone.Model, string): *) | string} [attrs.sortValue] @throws {TypeError} If attrs.cell or attrs.options are not supplied. - @throws {ReferenceError} If attrs.cell is a string but a cell class of + + @throws {ReferenceError} If formatter is a string but a formatter class of said name cannot be found in the Backgrid module. See: + - Backgrid.Column.defaults - Backgrid.Cell - Backgrid.CellFormatter */ initialize: function (attrs) { - Backgrid.requireOptions(attrs, ["cell", "name"]); - if (!this.has("label")) { this.set({ label: this.get("name") }, { silent: true }); } var headerCell = Backgrid.resolveNameToClass(this.get("headerCell"), "HeaderCell"); + var cell = Backgrid.resolveNameToClass(this.get("cell"), "Cell"); - this.set({ cell: cell, headerCell: headerCell }, { silent: true }); + + this.set({cell: cell, headerCell: headerCell}, { silent: true }); + }, + + /** + Returns an appropriate value extraction function from a model for sorting. + + If the column model contains an attribute `sortValue`, if it is a string, a + method from the column instance identifified by the `sortValue` string is + returned. If it is a function, it it returned as is. If `sortValue` isn't + found from the column model's attributes, a default value extraction + function is returned which will compare according to the natural order of + the value's type. + + @return {function(Backbone.Model, string): *} + */ + sortValue: function () { + var sortValue = this.get("sortValue"); + if (_.isString(sortValue)) return this[sortValue]; + else if (_.isFunction(sortValue)) return sortValue; + + return function (model, colName) { + return model.get(colName); + }; } + /** + @member Backgrid.Column + @protected + @method sortable + @return {function(Backgrid.Column, Backbone.Model): boolean | boolean} + */ + + /** + @member Backgrid.Column + @protected + @method editable + @return {function(Backgrid.Column, Backbone.Model): boolean | boolean} + */ + + /** + @member Backgrid.Column + @protected + @method renderable + @return {function(Backgrid.Column, Backbone.Model): boolean | boolean} + */ +}); + +_.each(["sortable", "renderable", "editable"], function (key) { + Column.prototype[key] = function () { + var value = this.get(key); + if (_.isString(value)) return this[value]; + return !!value; + }; }); /** @@ -1543,12 +1755,13 @@ var Columns = Backgrid.Columns = Backbone.Collection.extend({ */ model: Column }); + /* backgrid http://github.com/wyuenho/backgrid Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT @license. + Licensed under the MIT license. */ /** @@ -1564,8 +1777,6 @@ var Row = Backgrid.Row = Backbone.View.extend({ /** @property */ tagName: "tr", - requiredOptions: ["columns", "model"], - /** Initializes a row view instance. @@ -1577,8 +1788,6 @@ var Row = Backgrid.Row = Backbone.View.extend({ */ initialize: function (options) { - Backgrid.requireOptions(options, this.requiredOptions); - var columns = this.columns = options.columns; if (!(columns instanceof Backbone.Collection)) { columns = this.columns = new Columns(columns); @@ -1589,22 +1798,11 @@ var Row = Backgrid.Row = Backbone.View.extend({ cells.push(this.makeCell(columns.at(i), options)); } - this.listenTo(columns, "change:renderable", function (column, renderable) { - for (var i = 0; i < cells.length; i++) { - var cell = cells[i]; - if (cell.column.get("name") == column.get("name")) { - if (renderable) cell.$el.show(); else cell.$el.hide(); - } - } - }); - this.listenTo(columns, "add", function (column, columns) { var i = columns.indexOf(column); var cell = this.makeCell(column, options); cells.splice(i, 0, cell); - if (!cell.column.get("renderable")) cell.$el.hide(); - var $el = this.$el; if (i === 0) { $el.prepend(cell.render().$el); @@ -1648,11 +1846,8 @@ var Row = Backgrid.Row = Backbone.View.extend({ this.$el.empty(); var fragment = document.createDocumentFragment(); - for (var i = 0; i < this.cells.length; i++) { - var cell = this.cells[i]; - fragment.appendChild(cell.render().el); - if (!cell.column.get("renderable")) cell.$el.hide(); + fragment.appendChild(this.cells[i].render().el); } this.el.appendChild(fragment); @@ -1700,8 +1895,6 @@ var EmptyRow = Backgrid.EmptyRow = Backbone.View.extend({ @param {Backbone.Collection.|Array.|Array.} options.columns Column metadata. */ initialize: function (options) { - Backgrid.requireOptions(options, ["emptyText", "columns"]); - this.emptyText = options.emptyText; this.columns = options.columns; }, @@ -1722,12 +1915,13 @@ var EmptyRow = Backgrid.EmptyRow = Backbone.View.extend({ return this; } }); + /* backgrid http://github.com/wyuenho/backgrid Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT @license. + Licensed under the MIT license. */ /** @@ -1748,12 +1942,6 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ "click a": "onClick" }, - /** - @property {null|"ascending"|"descending"} _direction The current sorting - direction of this column. - */ - _direction: null, - /** Initializer. @@ -1763,12 +1951,30 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ @throws {TypeError} If options.column or options.collection is undefined. */ initialize: function (options) { - Backgrid.requireOptions(options, ["column", "collection"]); this.column = options.column; if (!(this.column instanceof Column)) { this.column = new Column(this.column); } + this.listenTo(this.collection, "backgrid:sort", this._resetCellDirection); + + var column = this.column, $el = this.$el; + + this.listenTo(column, "change:editable change:sortable change:renderable", + function (column) { + var changed = column.changedAttributes(); + for (var key in changed) { + if (changed.hasOwnProperty(key)) { + $el.toggleClass(key, changed[key]); + } + } + }); + + this.listenTo(column, "change:name change:label", this.render); + + if (column.get("editable")) $el.addClass("editable"); + if (column.get("sortable")) $el.addClass("sortable"); + if (column.get("renderable")) $el.addClass("renderable"); }, /** @@ -1781,12 +1987,13 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ */ direction: function (dir) { if (arguments.length) { - if (this._direction) this.$el.removeClass(this._direction); + var direction = this.column.get('direction'); + if (direction) this.$el.removeClass(direction); if (dir) this.$el.addClass(dir); - this._direction = dir; + this.column.set('direction', dir) } - return this._direction; + return this.column.get('direction'); }, /** @@ -1795,11 +2002,9 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ @private */ - _resetCellDirection: function (sortByColName, direction, comparator, collection) { - if (collection == this.collection) { - if (sortByColName !== this.column.get("name")) this.direction(null); - else this.direction(direction); - } + _resetCellDirection: function (columnToSort, direction) { + if (columnToSort !== this.column) this.direction(null); + else this.direction(direction); }, /** @@ -1810,118 +2015,44 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ onClick: function (e) { e.preventDefault(); - var columnName = this.column.get("name"); + var collection = this.collection, event = "backgrid:sort"; - if (this.column.get("sortable")) { - if (this.direction() === "ascending") { - this.sort(columnName, "descending", function (left, right) { - var leftVal = left.get(columnName); - var rightVal = right.get(columnName); - if (leftVal === rightVal) { - return 0; - } - else if (leftVal > rightVal) { return -1; } - return 1; - }); - } - else if (this.direction() === "descending") { - this.sort(columnName, null); - } - else { - this.sort(columnName, "ascending", function (left, right) { - var leftVal = left.get(columnName); - var rightVal = right.get(columnName); - if (leftVal === rightVal) { - return 0; - } - else if (leftVal < rightVal) { return -1; } - return 1; - }); - } + function cycleSort(header, col) { + if (header.direction() === "ascending") collection.trigger(event, col, "descending"); + else if (header.direction() === "descending") collection.trigger(event, col, null); + else collection.trigger(event, col, "ascending"); + } + + function toggleSort(header, col) { + if (header.direction() === "ascending") collection.trigger(event, col, "descending"); + else collection.trigger(event, col, "ascending"); + } + + var column = this.column; + var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection); + if (sortable) { + var sortType = column.get("sortType"); + if (sortType === "toggle") toggleSort(this, column); + else cycleSort(this, column); } }, /** - If the underlying collection is a Backbone.PageableCollection in - server-mode or infinite-mode, a page of models is fetched after sorting is - done on the server. - - If the underlying collection is a Backbone.PageableCollection in - client-mode, or any - [Backbone.Collection](http://backbonejs.org/#Collection) instance, sorting - is done on the client side. If the collection is an instance of a - Backbone.PageableCollection, sorting will be done globally on all the pages - and the current page will then be returned. - - Triggers a Backbone `backgrid:sort` event from the collection when done - with the column name, direction, comparator and a reference to the - collection. - - @param {string} columnName - @param {null|"ascending"|"descending"} direction - @param {function(*, *): number} [comparator] - - See [Backbone.Collection#comparator](http://backbonejs.org/#Collection-comparator) - */ - sort: function (columnName, direction, comparator) { - - comparator = comparator || this._cidComparator; - - var collection = this.collection; - - if (Backbone.PageableCollection && collection instanceof Backbone.PageableCollection) { - var order; - if (direction === "ascending") order = -1; - else if (direction === "descending") order = 1; - else order = null; - - collection.setSorting(order ? columnName : null, order); - - if (collection.mode == "client") { - if (!collection.fullCollection.comparator) { - collection.fullCollection.comparator = comparator; - } - collection.fullCollection.sort(); - } - else collection.fetch({reset: true}); - } - else { - collection.comparator = comparator; - collection.sort(); - } - - this.collection.trigger("backgrid:sort", columnName, direction, comparator, this.collection); - }, - - /** - Default comparator for Backbone.Collections. Sorts cids in ascending - order. The cids of the models are assumed to be in insertion order. - - @private - @param {*} left - @param {*} right - */ - _cidComparator: function (left, right) { - var lcid = left.cid, rcid = right.cid; - if (!_.isUndefined(lcid) && !_.isUndefined(rcid)) { - lcid = lcid.slice(1) * 1, rcid = rcid.slice(1) * 1; - if (lcid < rcid) return -1; - else if (lcid > rcid) return 1; - } - - return 0; - }, - - /** - Renders a header cell with a sorter and a label. + Renders a header cell with a sorter, a label, and a class name for this + column. */ render: function () { this.$el.empty(); - var $label = $("").text(this.column.get("label")).append(""); + var column = this.column; + var $label = $("").text(column.get("label")); + var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection); + if (sortable) $label.append(""); this.$el.append($label); + this.$el.addClass(column.get("name")); this.delegateEvents(); + this.direction(column.get("direction")); return this; - } +} }); @@ -1985,8 +2116,6 @@ var Header = Backgrid.Header = Backbone.View.extend({ @throws {TypeError} If options.columns or options.model is undefined. */ initialize: function (options) { - Backgrid.requireOptions(options, ["columns", "collection"]); - this.columns = options.columns; if (!(this.columns instanceof Backbone.Collection)) { this.columns = new Columns(this.columns); @@ -2018,12 +2147,13 @@ var Header = Backgrid.Header = Backbone.View.extend({ } }); + /* backgrid http://github.com/wyuenho/backgrid Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT @license. + Licensed under the MIT license. */ /** @@ -2053,7 +2183,6 @@ var Body = Backgrid.Body = Backbone.View.extend({ See Backgrid.Row. */ initialize: function (options) { - Backgrid.requireOptions(options, ["columns", "collection"]); this.columns = options.columns; if (!(this.columns instanceof Backbone.Collection)) { @@ -2078,6 +2207,7 @@ var Body = Backgrid.Body = Backbone.View.extend({ this.listenTo(collection, "remove", this.removeRow); this.listenTo(collection, "sort", this.refresh); this.listenTo(collection, "reset", this.refresh); + this.listenTo(collection, "backgrid:sort", this.sort); this.listenTo(collection, "backgrid:edited", this.moveToNextCell); }, @@ -2145,6 +2275,8 @@ var Body = Backgrid.Body = Backbone.View.extend({ $children.eq(index).before($rowEl); } } + + return this; }, /** @@ -2186,6 +2318,8 @@ var Body = Backgrid.Body = Backbone.View.extend({ this.rows.splice(options.index, 1); this._unshiftEmptyRowMayBe(); + + return this; }, /** @@ -2249,6 +2383,82 @@ var Body = Backgrid.Body = Backbone.View.extend({ return Backbone.View.prototype.remove.apply(this, arguments); }, + /** + If the underlying collection is a Backbone.PageableCollection in + server-mode or infinite-mode, a page of models is fetched after sorting is + done on the server. + + If the underlying collection is a Backbone.PageableCollection in + client-mode, or any + [Backbone.Collection](http://backbonejs.org/#Collection) instance, sorting + is done on the client side. If the collection is an instance of a + Backbone.PageableCollection, sorting will be done globally on all the pages + and the current page will then be returned. + + Triggers a Backbone `backgrid:sort` event from the collection when done + with the column, direction, comparator and a reference to the collection. + + @param {Backgrid.Column} column + @param {null|"ascending"|"descending"} direction + + See [Backbone.Collection#comparator](http://backbonejs.org/#Collection-comparator) + */ + sort: function (column, direction) { + + if (_.isString(column)) column = this.columns.findWhere({name: column}); + + var collection = this.collection; + + var order; + if (direction === "ascending") order = -1; + else if (direction === "descending") order = 1; + else order = null; + + var comparator = this.makeComparator(column.get("name"), order, + order ? + column.sortValue() : + function (model) { + return model.cid; + }); + + if (Backbone.PageableCollection && + collection instanceof Backbone.PageableCollection) { + + collection.setSorting(order && column.get("name"), order, + {sortValue: column.sortValue()}); + + if (collection.mode == "client") { + if (collection.fullCollection.comparator == null) { + collection.fullCollection.comparator = comparator; + } + collection.fullCollection.sort(); + } + else collection.fetch({reset: true}); + } + else { + collection.comparator = comparator; + collection.sort(); + } + + return this; + }, + + makeComparator: function (attr, order, func) { + + return function (left, right) { + // extract the values from the models + var l = func(left, attr), r = func(right, attr), t; + + // if descending order, swap left and right + if (order === 1) t = l, l = r, r = t; + + // compare as usual + if (l === r) return 0; + else if (l < r) return -1; + return 1; + }; + }, + /** Moves focus to the next renderable and editable cell and return the currently editing cell to display mode. @@ -2261,6 +2471,9 @@ var Body = Backgrid.Body = Backbone.View.extend({ moveToNextCell: function (model, column, command) { var i = this.collection.indexOf(model); var j = this.columns.indexOf(column); + var cell, renderable, editable; + + this.rows[i].cells[j].exitEditMode(); if (command.moveUp() || command.moveDown() || command.moveLeft() || command.moveRight() || command.save()) { @@ -2269,7 +2482,12 @@ var Body = Backgrid.Body = Backbone.View.extend({ if (command.moveUp() || command.moveDown()) { var row = this.rows[i + (command.moveUp() ? -1 : 1)]; - if (row) row.cells[j].enterEditMode(); + if (row) { + cell = row.cells[j]; + if (Backgrid.callByNeed(cell.column.editable(), cell.column, model)) { + cell.enterEditMode(); + } + } } else if (command.moveLeft() || command.moveRight()) { var right = command.moveRight(); @@ -2278,8 +2496,10 @@ var Body = Backgrid.Body = Backbone.View.extend({ right ? offset++ : offset--) { var m = ~~(offset / l); var n = offset - m * l; - var cell = this.rows[m].cells[n]; - if (cell.column.get("renderable") && cell.column.get("editable")) { + cell = this.rows[m].cells[n]; + renderable = Backgrid.callByNeed(cell.column.renderable(), cell.column, cell.model); + editable = Backgrid.callByNeed(cell.column.editable(), cell.column, model); + if (renderable && editable) { cell.enterEditMode(); break; } @@ -2287,15 +2507,16 @@ var Body = Backgrid.Body = Backbone.View.extend({ } } - this.rows[i].cells[j].exitEditMode(); + return this; } }); + /* backgrid http://github.com/wyuenho/backgrid Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT @license. + Licensed under the MIT license. */ /** @@ -2315,7 +2536,6 @@ var Footer = Backgrid.Footer = Backbone.View.extend({ Initializer. @param {Object} options - @param {*} options.parent The parent view class of this footer. @param {Backbone.Collection.|Array.|Array.} options.columns Column metadata. @param {Backbone.Collection} options.collection @@ -2323,7 +2543,6 @@ var Footer = Backgrid.Footer = Backbone.View.extend({ @throws {TypeError} If options.columns or options.collection is undefined. */ initialize: function (options) { - Backgrid.requireOptions(options, ["columns", "collection"]); this.columns = options.columns; if (!(this.columns instanceof Backbone.Collection)) { this.columns = new Backgrid.Columns(this.columns); @@ -2331,12 +2550,13 @@ var Footer = Backgrid.Footer = Backbone.View.extend({ } }); + /* backgrid http://github.com/wyuenho/backgrid Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT @license. + Licensed under the MIT license. */ /** @@ -2407,7 +2627,7 @@ var Grid = Backgrid.Grid = Backbone.View.extend({ Initializes a Grid instance. @param {Object} options - @param {Backbone.Collection.|Array.|Array.} options.columns Column metadata. + @param {Backbone.Collection.|Array.|Array.} options.columns Column metadata. @param {Backbone.Collection} options.collection The collection of tabular model data to display. @param {Backgrid.Header} [options.header=Backgrid.Header] An optional Header class to override the default. @param {Backgrid.Body} [options.body=Backgrid.Body] An optional Body class to override the default. @@ -2415,8 +2635,6 @@ var Grid = Backgrid.Grid = Backbone.View.extend({ @param {Backgrid.Footer} [options.footer=Backgrid.Footer] An optional Footer class. */ initialize: function (options) { - Backgrid.requireOptions(options, ["columns", "collection"]); - // Convert the list of column objects here first so the subviews don't have // to. if (!(options.columns instanceof Backbone.Collection)) { @@ -2424,25 +2642,30 @@ var Grid = Backgrid.Grid = Backbone.View.extend({ } this.columns = options.columns; - var passedThruOptions = _.omit(options, ["el", "id", "attributes", - "className", "tagName", "events"]); + var filteredOptions = _.omit(options, ["el", "id", "attributes", + "className", "tagName", "events"]); + + // must construct body first so it listens to backgrid:sort first + this.body = options.body || this.body; + this.body = new this.body(filteredOptions); this.header = options.header || this.header; - this.header = new this.header(passedThruOptions); - - this.body = options.body || this.body; - this.body = new this.body(passedThruOptions); + if (this.header) { + this.header = new this.header(filteredOptions); + } this.footer = options.footer || this.footer; if (this.footer) { - this.footer = new this.footer(passedThruOptions); + this.footer = new this.footer(filteredOptions); } this.listenTo(this.columns, "reset", function () { - this.header = new (this.header.remove().constructor)(passedThruOptions); - this.body = new (this.body.remove().constructor)(passedThruOptions); + if (this.header) { + this.header = new (this.header.remove().constructor)(filteredOptions); + } + this.body = new (this.body.remove().constructor)(filteredOptions); if (this.footer) { - this.footer = new (this.footer.remove().constructor)(passedThruOptions); + this.footer = new (this.footer.remove().constructor)(filteredOptions); } this.render(); }); @@ -2452,14 +2675,16 @@ var Grid = Backgrid.Grid = Backbone.View.extend({ Delegates to Backgrid.Body#insertRow. */ insertRow: function (model, collection, options) { - return this.body.insertRow(model, collection, options); + this.body.insertRow(model, collection, options); + return this; }, /** Delegates to Backgrid.Body#removeRow. */ removeRow: function (model, collection, options) { - return this.body.removeRow(model, collection, options); + this.body.removeRow(model, collection, options); + return this; }, /** @@ -2470,8 +2695,6 @@ var Grid = Backgrid.Grid = Backbone.View.extend({ @param {Object} [options] Options for `Backgrid.Columns#add`. @param {boolean} [options.render=true] Whether to render the column immediately after insertion. - - @chainable */ insertColumn: function (column, options) { options = options || {render: true}; @@ -2485,14 +2708,20 @@ var Grid = Backgrid.Grid = Backbone.View.extend({ needs to happen. @param {Object} [options] Options for `Backgrid.Columns#remove`. - - @chainable */ removeColumn: function (column, options) { this.columns.remove(column, options); return this; }, + /** + Delegates to Backgrid.Body#sort. + */ + sort: function () { + this.body.sort(arguments); + return this; + }, + /** Renders the grid's header, then footer, then finally the body. Triggers a Backbone `backgrid:rendered` event along with a reference to the grid when @@ -2501,7 +2730,9 @@ var Grid = Backgrid.Grid = Backbone.View.extend({ render: function () { this.$el.empty(); - this.$el.append(this.header.render().$el); + if (this.header) { + this.$el.append(this.header.render().$el); + } if (this.footer) { this.$el.append(this.footer.render().$el); @@ -2522,12 +2753,12 @@ var Grid = Backgrid.Grid = Backbone.View.extend({ @chainable */ remove: function () { - this.header.remove.apply(this.header, arguments); + this.header && this.header.remove.apply(this.header, arguments); this.body.remove.apply(this.body, arguments); this.footer && this.footer.remove.apply(this.footer, arguments); return Backbone.View.prototype.remove.apply(this, arguments); } }); - -}(this, jQuery, _, Backbone)); \ No newline at end of file +return Backgrid; +})); \ No newline at end of file diff --git a/src/UI/JsLibraries/backbone.backgrid.paginator.js b/src/UI/JsLibraries/backbone.backgrid.paginator.js index cceabca00..03255f84d 100644 --- a/src/UI/JsLibraries/backbone.backgrid.paginator.js +++ b/src/UI/JsLibraries/backbone.backgrid.paginator.js @@ -5,19 +5,202 @@ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors Licensed under the MIT @license. */ +(function (factory) { -(function ($, _, Backbone, Backgrid) { + // CommonJS + if (typeof exports == "object") { + module.exports = factory(require("underscore"), + require("backbone"), + require("backgrid"), + require("backbone-pageable")); + } + // Browser + else if (typeof _ !== "undefined" && + typeof Backbone !== "undefined" && + typeof Backgrid !== "undefined") { + factory(_, Backbone, Backgrid); + } + +}(function (_, Backbone, Backgrid) { "use strict"; + /** + PageHandle is a class that renders the actual page handles and reacts to + click events for pagination. + + This class acts in two modes - control or discrete page handle modes. If + one of the `is*` flags is `true`, an instance of this class is under + control page handle mode. Setting a `pageIndex` to an instance of this + class under control mode has no effect and the correct page index will + always be inferred from the `is*` flag. Only one of the `is*` flags should + be set to `true` at a time. For example, an instance of this class cannot + simultaneously be a rewind control and a fast forward control. A `label` + and a `title` template or a string are required to be passed to the + constuctor under this mode. If a `title` template is provided, it __MUST__ + accept a parameter `label`. When the `label` is provided to the `title` + template function, its result will be used to render the generated anchor's + title attribute. + + If all of the `is*` flags is set to `false`, which is the default, an + instance of this class will be in discrete page handle mode. An instance + under this mode requires the `pageIndex` to be passed from the constructor + as an option and it __MUST__ be a 0-based index of the list of page numbers + to render. The constuctor will normalize the base to the same base the + underlying PageableCollection collection instance uses. A `label` is not + required under this mode, which will default to the equivalent 1-based page + index calculated from `pageIndex` and the underlying PageableCollection + instance. A provided `label` will still be honored however. The `title` + parameter is also not required under this mode, in which case the default + `title` template will be used. You are encouraged to provide your own + `title` template however if you wish to localize the title strings. + + If this page handle represents the current page, an `active` class will be + placed on the root list element. + + if this page handle is at the border of the list of pages, a `disabled` + class will be placed on the root list element. + + Only page handles that are neither `active` nor `disabled` will respond to + click events and triggers pagination. + + @class Backgrid.Extension.PageHandle + */ + var PageHandle = Backgrid.Extension.PageHandle = Backbone.View.extend({ + + /** @property */ + tagName: "li", + + /** @property */ + events: { + "click a": "changePage" + }, + + /** + @property {string|function(Object.): string} title + The title to use for the `title` attribute of the generated page handle + anchor elements. It can be a string or an Underscore template function + that takes a mandatory `label` parameter. + */ + title: _.template('Page <%- label %>', null, {variable: null}), + + /** + @property {boolean} isRewind Whether this handle represents a rewind + control + */ + isRewind: false, + + /** + @property {boolean} isBack Whether this handle represents a back + control + */ + isBack: false, + + /** + @property {boolean} isForward Whether this handle represents a forward + control + */ + isForward: false, + + /** + @property {boolean} isFastForward Whether this handle represents a fast + forward control + */ + isFastForward: false, + + /** + Initializer. + + @param {Object} options + @param {Backbone.Collection} options.collection + @param {number} pageIndex 0-based index of the page number this handle + handles. This parameter will be normalized to the base the underlying + PageableCollection uses. + @param {string} [options.label] If provided it is used to render the + anchor text, otherwise the normalized pageIndex will be used + instead. Required if any of the `is*` flags is set to `true`. + @param {string} [options.title] + @param {boolean} [options.isRewind=false] + @param {boolean} [options.isBack=false] + @param {boolean} [options.isForward=false] + @param {boolean} [options.isFastForward=false] + */ + initialize: function (options) { + Backbone.View.prototype.initialize.apply(this, arguments); + + var collection = this.collection; + var state = collection.state; + var currentPage = state.currentPage; + var firstPage = state.firstPage; + var lastPage = state.lastPage; + + _.extend(this, _.pick(options, + ["isRewind", "isBack", "isForward", "isFastForward"])); + + var pageIndex; + if (this.isRewind) pageIndex = firstPage; + else if (this.isBack) pageIndex = Math.max(firstPage, currentPage - 1); + else if (this.isForward) pageIndex = Math.min(lastPage, currentPage + 1); + else if (this.isFastForward) pageIndex = lastPage; + else { + pageIndex = +options.pageIndex; + pageIndex = (firstPage ? pageIndex + 1 : pageIndex); + } + this.pageIndex = pageIndex; + + if (((this.isRewind || this.isBack) && currentPage == firstPage) || + ((this.isForward || this.isFastForward) && currentPage == lastPage)) { + this.$el.addClass("disabled"); + } + else if (!(this.isRewind || + this.isBack || + this.isForward || + this.isFastForward) && + currentPage == pageIndex) { + this.$el.addClass("active"); + } + + this.label = (options.label || (firstPage ? pageIndex : pageIndex + 1)) + ''; + var title = options.title || this.title; + this.title = _.isFunction(title) ? title({label: this.label}) : title; + }, + + /** + Renders a clickable anchor element under a list item. + */ + render: function () { + this.$el.empty(); + var anchor = document.createElement("a"); + anchor.href = '#'; + if (this.title) anchor.title = this.title; + anchor.innerHTML = this.label; + this.el.appendChild(anchor); + this.delegateEvents(); + return this; + }, + + /** + jQuery click event handler. Goes to the page this PageHandle instance + represents. No-op if this page handle is currently active or disabled. + */ + changePage: function (e) { + e.preventDefault(); + var $el = this.$el; + if (!$el.hasClass("active") && !$el.hasClass("disabled")) { + this.collection.getPage(this.pageIndex); + } + return this; + } + + }); + /** Paginator is a Backgrid extension that renders a series of configurable pagination handles. This extension is best used for splitting a large data set across multiple pages. If the number of pages is larger then a threshold, which is set to 10 by default, the page handles are rendered - within a sliding window, plus the fast forward, fast backward, previous and - next page handles. The fast forward, fast backward, previous and next page - handles can be turned off. + within a sliding window, plus the rewind, back, forward and fast forward + control handles. The individual control handles can be turned off. @class Backgrid.Extension.Paginator */ @@ -30,97 +213,63 @@ windowSize: 10, /** - @property {Object} fastForwardHandleLabels You can disable specific - handles by setting its value to `null`. + @property {Object.>} controls You can + disable specific control handles by omitting certain keys. */ - fastForwardHandleLabels: { - first: "《", - prev: "〈", - next: "〉", - last: "》" + controls: { + rewind: { + label: "《", + title: "First" + }, + back: { + label: "〈", + title: "Previous" + }, + forward: { + label: "〉", + title: "Next" + }, + fastForward: { + label: "》", + title: "Last" + } }, - /** @property */ - template: _.template(''), + /** + @property {Backgrid.Extension.PageHandle} pageHandle. The PageHandle + class to use for rendering individual handles + */ + pageHandle: PageHandle, /** @property */ - events: { - "click a": "changePage" - }, + goBackFirstOnSort: true, /** Initializer. @param {Object} options @param {Backbone.Collection} options.collection - @param {boolean} [options.fastForwardHandleLabels] Whether to render fast forward buttons. + @param {boolean} [options.controls] + @param {boolean} [options.pageHandle=Backgrid.Extension.PageHandle] + @param {boolean} [options.goBackFirstOnSort=true] */ initialize: function (options) { - Backgrid.requireOptions(options, ["collection"]); + this.controls = options.controls || this.controls; + this.pageHandle = options.pageHandle || this.pageHandle; var collection = this.collection; - var fullCollection = collection.fullCollection; - if (fullCollection) { - this.listenTo(fullCollection, "add", this.render); - this.listenTo(fullCollection, "remove", this.render); - this.listenTo(fullCollection, "reset", this.render); - } - else { - this.listenTo(collection, "add", this.render); - this.listenTo(collection, "remove", this.render); - this.listenTo(collection, "reset", this.render); + this.listenTo(collection, "add", this.render); + this.listenTo(collection, "remove", this.render); + this.listenTo(collection, "reset", this.render); + if ((options.goBackFirstOnSort || this.goBackFirstOnSort) && + collection.fullCollection) { + this.listenTo(collection.fullCollection, "sort", function () { + collection.getFirstPage(); + }); } }, - /** - jQuery event handler for the page handlers. Goes to the right page upon - clicking. - - @param {Event} e - */ - changePage: function (e) { - e.preventDefault(); - - var $li = $(e.target).parent(); - if (!$li.hasClass("active") && !$li.hasClass("disabled")) { - - var label = $(e.target).text(); - var ffLabels = this.fastForwardHandleLabels; - - var collection = this.collection; - - if (ffLabels) { - switch (label) { - case ffLabels.first: - collection.getFirstPage(); - return; - case ffLabels.prev: - collection.getPreviousPage(); - return; - case ffLabels.next: - collection.getNextPage(); - return; - case ffLabels.last: - collection.getLastPage(); - return; - } - } - - var state = collection.state; - var pageIndex = +label; - collection.getPage(state.firstPage === 0 ? pageIndex - 1 : pageIndex); - } - }, - - /** - Internal method to create a list of page handle objects for the template - to render them. - - @return {Array.} an array of page handle objects hashes - */ - makeHandles: function () { - - var handles = []; + _calculateWindow: function () { var collection = this.collection; var state = collection.state; @@ -132,48 +281,44 @@ currentPage = firstPage ? currentPage - 1 : currentPage; var windowStart = Math.floor(currentPage / this.windowSize) * this.windowSize; var windowEnd = Math.min(lastPage + 1, windowStart + this.windowSize); + return [windowStart, windowEnd]; + }, - if (collection.mode !== "infinite") { - for (var i = windowStart; i < windowEnd; i++) { - handles.push({ - label: i + 1, - title: "No. " + (i + 1), - className: currentPage === i ? "active" : undefined - }); - } + /** + Creates a list of page handle objects for rendering. + + @return {Array.} an array of page handle objects hashes + */ + makeHandles: function () { + + var handles = []; + var collection = this.collection; + + var window = this._calculateWindow(); + var winStart = window[0], winEnd = window[1]; + + for (var i = winStart; i < winEnd; i++) { + handles.push(new this.pageHandle({ + collection: collection, + pageIndex: i + })); } - var ffLabels = this.fastForwardHandleLabels; - if (ffLabels) { - - if (ffLabels.prev) { - handles.unshift({ - label: ffLabels.prev, - className: collection.hasPrevious() ? void 0 : "disabled" - }); + var controls = this.controls; + _.each(["back", "rewind", "forward", "fastForward"], function (key) { + var value = controls[key]; + if (value) { + var handleCtorOpts = { + collection: collection, + title: value.title, + label: value.label + }; + handleCtorOpts["is" + key.slice(0, 1).toUpperCase() + key.slice(1)] = true; + var handle = new this.pageHandle(handleCtorOpts); + if (key == "rewind" || key == "back") handles.unshift(handle); + else handles.push(handle); } - - if (ffLabels.first) { - handles.unshift({ - label: ffLabels.first, - className: collection.hasPrevious() ? void 0 : "disabled" - }); - } - - if (ffLabels.next) { - handles.push({ - label: ffLabels.next, - className: collection.hasNext() ? void 0 : "disabled" - }); - } - - if (ffLabels.last) { - handles.push({ - label: ffLabels.last, - className: collection.hasNext() ? void 0 : "disabled" - }); - } - } + }, this); return handles; }, @@ -184,15 +329,24 @@ render: function () { this.$el.empty(); - this.$el.append(this.template({ - handles: this.makeHandles() - })); + if (this.handles) { + for (var i = 0, l = this.handles.length; i < l; i++) { + this.handles[i].remove(); + } + } - this.delegateEvents(); + var handles = this.handles = this.makeHandles(); + + var ul = document.createElement("ul"); + for (var i = 0; i < handles.length; i++) { + ul.appendChild(handles[i].render().el); + } + + this.el.appendChild(ul); return this; } }); -}(jQuery, _, Backbone, Backgrid)); +})); diff --git a/src/UI/JsLibraries/backbone.pageable.js b/src/UI/JsLibraries/backbone.pageable.js index 83feb3f7e..f6cdbcacd 100644 --- a/src/UI/JsLibraries/backbone.pageable.js +++ b/src/UI/JsLibraries/backbone.pageable.js @@ -1,5 +1,5 @@ /* - backbone-pageable 1.3.2 + backbone-pageable 1.4.1 http://github.com/wyuenho/backbone-pageable Copyright (c) 2013 Jimmy Yuen Ho Wong @@ -83,7 +83,7 @@ for (var i = 0, l = kvps.length; i < l; i++) { var param = kvps[i]; kvp = param.split('='), k = kvp[0], v = kvp[1] || true; - k = decode(k), ls = params[k]; + k = decode(k), v = decode(v), ls = params[k]; if (_isArray(ls)) ls.push(v); else if (ls) params[k] = [ls, v]; else params[k] = v; @@ -91,6 +91,29 @@ return params; } + // hack to make sure the whatever event handlers for this event is run + // before func is, and the event handlers that func will trigger. + function runOnceAtLastHandler (col, event, func) { + var eventHandlers = col._events[event]; + if (eventHandlers && eventHandlers.length) { + var lastHandler = eventHandlers[eventHandlers.length - 1]; + var oldCallback = lastHandler.callback; + lastHandler.callback = function () { + try { + oldCallback.apply(this, arguments); + func(); + } + catch (e) { + throw e; + } + finally { + lastHandler.callback = oldCallback; + } + }; + } + else func(); + } + var PARAM_TRIM_RE = /[\s'"]/g; var URL_TRIM_RE = /[<>\s'"]/g; @@ -256,7 +279,7 @@ */ constructor: function (models, options) { - Backbone.Collection.apply(this, arguments); + BBColProto.constructor.apply(this, arguments); options = options || {}; @@ -299,7 +322,7 @@ var fullCollection = this.fullCollection; if (comparator && options.full) { - delete this.comparator; + this.comparator = null; fullCollection.comparator = comparator; } @@ -308,6 +331,7 @@ // make sure the models in the current page and full collection have the // same references if (models && !_isEmpty(models)) { + this.reset([].slice.call(models), _extend({silent: true}, options)); this.getPage(state.currentPage); models.splice.apply(models, [0, models.length].concat(this.models)); } @@ -412,22 +436,10 @@ pageCol.at(pageSize) : null; if (modelToRemove) { - var addHandlers = collection._events.add || [], - popOptions = {onAdd: true}; - if (addHandlers.length) { - var lastAddHandler = addHandlers[addHandlers.length - 1]; - var oldCallback = lastAddHandler.callback; - lastAddHandler.callback = function () { - try { - oldCallback.apply(this, arguments); - pageCol.remove(modelToRemove, popOptions); - } - finally { - lastAddHandler.callback = oldCallback; - } - }; - } - else pageCol.remove(modelToRemove, popOptions); + var popOptions = {onAdd: true}; + runOnceAtLastHandler(collection, event, function () { + pageCol.remove(modelToRemove, popOptions); + }); } } } @@ -442,20 +454,25 @@ } else { var totalPages = state.totalPages = ceil(state.totalRecords / pageSize); - state.lastPage = firstPage === 0 ? totalPages - 1 : totalPages; + state.lastPage = firstPage === 0 ? totalPages - 1 : totalPages || firstPage; if (state.currentPage > totalPages) state.currentPage = state.lastPage; } pageCol.state = pageCol._checkState(state); var nextModel, removedIndex = options.index; if (collection == pageCol) { - if (nextModel = fullCol.at(pageEnd)) pageCol.push(nextModel); + if (nextModel = fullCol.at(pageEnd)) { + runOnceAtLastHandler(pageCol, event, function () { + pageCol.push(nextModel); + }); + } fullCol.remove(model); } else if (removedIndex >= pageStart && removedIndex < pageEnd) { pageCol.remove(model); - nextModel = fullCol.at(currentPage * (pageSize + removedIndex)); - if (nextModel) pageCol.push(nextModel); + var at = removedIndex + 1 + nextModel = fullCol.at(at) || fullCol.last(); + if (nextModel) pageCol.add(nextModel, {at: at}); } } else delete options.onAdd; @@ -466,13 +483,13 @@ collection = model; // Reset that's not a result of getPage - if (collection === pageCol && options.from == null && + if (collection == pageCol && options.from == null && options.to == null) { var head = fullCol.models.slice(0, pageStart); var tail = fullCol.models.slice(pageStart + pageCol.models.length); fullCol.reset(head.concat(pageCol.models).concat(tail), options); } - else if (collection === fullCol) { + else if (collection == fullCol) { if (!(state.totalRecords = fullCol.models.length)) { state.totalRecords = null; state.totalPages = null; @@ -551,7 +568,7 @@ throw new RangeError("`firstPage must be 0 or 1`"); } - state.lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages; + state.lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages || firstPage; if (mode == "infinite") { if (!links[currentPage + '']) { @@ -561,6 +578,8 @@ else if (currentPage < firstPage || (totalPages > 0 && (firstPage ? currentPage > totalPages : currentPage >= totalPages))) { + var op = firstPage ? ">=" : ">"; + throw new RangeError("`currentPage` must be firstPage <= currentPage " + (firstPage ? ">" : ">=") + " totalPages if " + firstPage + "-based. Got " + @@ -681,7 +700,7 @@ var fullCollection = this.fullCollection; var handlers = this._handlers = this._handlers || {}, handler; if (mode != "server" && !fullCollection) { - fullCollection = this._makeFullCollection(options.models || []); + fullCollection = this._makeFullCollection(options.models || [], options); fullCollection.pageableCollection = this; this.fullCollection = fullCollection; var allHandler = this._makeCollectionEventHandler(this, fullCollection); @@ -856,7 +875,8 @@ []; if ((mode == "client" || (mode == "infinite" && !_isEmpty(pageModels))) && !options.fetch) { - return this.reset(pageModels, _omit(options, "fetch")); + this.reset(pageModels, _omit(options, "fetch")); + return this; } if (mode == "infinite") options.url = this.links[pageNum]; @@ -1310,8 +1330,8 @@ this.comparator = comparator; } - if (delComp) delete this.comparator; - if (delFullComp && fullCollection) delete fullCollection.comparator; + if (delComp) this.comparator = null; + if (delFullComp && fullCollection) fullCollection.comparator = null; return this; } diff --git a/src/UI/Missing/Collection.js b/src/UI/Missing/MissingCollection.js similarity index 76% rename from src/UI/Missing/Collection.js rename to src/UI/Missing/MissingCollection.js index 4b7a4fa96..d58b6d133 100644 --- a/src/UI/Missing/Collection.js +++ b/src/UI/Missing/MissingCollection.js @@ -2,11 +2,13 @@ define( [ 'Series/EpisodeModel', - 'backbone.pageable' - ], function (EpisodeModel, PagableCollection) { - return PagableCollection.extend({ + 'backbone.pageable', + 'Mixins/AsPersistedStateCollection' + ], function (EpisodeModel, PagableCollection, AsPersistedStateCollection) { + var collection = PagableCollection.extend({ url : window.NzbDrone.ApiRoot + '/missing', model: EpisodeModel, + tableName: 'missing', state: { pageSize: 15, @@ -38,4 +40,6 @@ define( return resp; } }); + + return AsPersistedStateCollection.call(collection); }); diff --git a/src/UI/Missing/MissingLayout.js b/src/UI/Missing/MissingLayout.js index a6ecef90a..7240277f5 100644 --- a/src/UI/Missing/MissingLayout.js +++ b/src/UI/Missing/MissingLayout.js @@ -4,7 +4,7 @@ define( 'underscore', 'marionette', 'backgrid', - 'Missing/Collection', + 'Missing/MissingCollection', 'Cells/SeriesTitleCell', 'Cells/EpisodeNumberCell', 'Cells/EpisodeTitleCell', @@ -121,7 +121,6 @@ define( ] }; - this.toolbar.show(new ToolbarLayout({ left : [ diff --git a/src/UI/Mixins/AsPersistedStateCollection.js b/src/UI/Mixins/AsPersistedStateCollection.js new file mode 100644 index 000000000..879c05427 --- /dev/null +++ b/src/UI/Mixins/AsPersistedStateCollection.js @@ -0,0 +1,86 @@ +'use strict'; + +define( + ['underscore', 'Config'], + function (_, Config) { + + return function () { + + var originalInit = this.prototype.initialize; + + this.prototype.initialize = function (options) { + + options = options || {}; + + if (options.tableName) { + this.tableName = options.tableName; + } + + if (!this.tableName && !options.tableName) { + throw 'tableName is required'; + } + + _setInitialState.call(this); + + this.on('backgrid:sort', _storeStateFromBackgrid, this); + this.on('drone:sort', _storeState, this); + + if (originalInit) { + originalInit.call(this, options); + } + }; + + var _setInitialState = function () { + var key = Config.getValue('{0}.sortKey'.format(this.tableName), this.state.sortKey); + var direction = Config.getValue('{0}.sortDirection'.format(this.tableName), this.state.order); + var order = parseInt(direction, 10); + + this.state.sortKey = key; + this.state.order = order; + }; + + var _storeStateFromBackgrid = function (column, sortDirection) { + var order = _convertDirectionToInt(sortDirection); + var sortKey = column.has('sortValue') && _.isString(column.get('sortValue')) ? column.get('sortValue') : column.get('name'); + + Config.setValue('{0}.sortKey'.format(this.tableName), sortKey); + Config.setValue('{0}.sortDirection'.format(this.tableName), order); + }; + + var _storeState = function (sortModel, sortDirection) { + var order = _convertDirectionToInt(sortDirection); + var sortKey = sortModel.get('name'); + + Config.setValue('{0}.sortKey'.format(this.tableName), sortKey); + Config.setValue('{0}.sortDirection'.format(this.tableName), order); + }; + + var _convertDirectionToInt = function (dir) { + if (dir === 'ascending') { + return '-1'; + } + + return '1'; + }; + + _.extend(this.prototype, { + initialSort: function () { + var key = this.state.sortKey; + var order = this.state.order; + + if (this[key] && this.mode === 'client') { + var sortValue = this[key]; + + this.setSorting(key, order, { sortValue: sortValue }); + + var comparator = this._makeComparator(key, order, sortValue); + this.fullCollection.comparator = comparator; + this.fullCollection.sort(); + } + } + }); + + return this; + }; + } +); diff --git a/src/UI/Navbar/NavbarTemplate.html b/src/UI/Navbar/NavbarTemplate.html index 34d3b7631..df18cf702 100644 --- a/src/UI/Navbar/NavbarTemplate.html +++ b/src/UI/Navbar/NavbarTemplate.html @@ -3,47 +3,47 @@