Merge branch 'develop'

This commit is contained in:
Mark McDowall 2014-01-16 16:52:26 -08:00
commit 5c7ff0c6b4
85 changed files with 1853 additions and 940 deletions

View File

@ -18,7 +18,7 @@
"grunt": "*", "grunt": "*",
"grunt-contrib-handlebars": "*", "grunt-contrib-handlebars": "*",
"grunt-contrib-watch": "*", "grunt-contrib-watch": "*",
"grunt-contrib-less": "*", "grunt-contrib-less": "0.8.3",
"grunt-contrib-copy": "*", "grunt-contrib-copy": "*",
"grunt-notify": "*", "grunt-notify": "*",
"grunt-contrib-clean": "*", "grunt-contrib-clean": "*",

View File

@ -1,4 +1,5 @@
using System.IO; using System.IO;
using System.Text.RegularExpressions;
using Nancy; using Nancy;
using NLog; using NLog;
using NzbDrone.Common; using NzbDrone.Common;
@ -12,6 +13,7 @@ namespace NzbDrone.Api.Frontend.Mappers
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IConfigFileProvider _configFileProvider; private readonly IConfigFileProvider _configFileProvider;
private readonly string _indexPath; private readonly string _indexPath;
private static readonly Regex ReplaceRegex = new Regex("(?<=(?:href|src|data-main)=\").*?(?=\")", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public IndexHtmlMapper(IAppFolderInfo appFolderInfo, public IndexHtmlMapper(IAppFolderInfo appFolderInfo,
IDiskProvider diskProvider, IDiskProvider diskProvider,
@ -47,13 +49,15 @@ namespace NzbDrone.Api.Frontend.Mappers
return StringToStream(GetIndexText()); return StringToStream(GetIndexText());
} }
private string GetIndexText() private string GetIndexText()
{ {
var text = _diskProvider.ReadAllText(_indexPath); 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(".css", ".css?v=" + BuildInfo.Version);
text = text.Replace(".js", ".js?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("API_KEY", _configFileProvider.ApiKey);
text = text.Replace("APP_VERSION", BuildInfo.Version.ToString()); text = text.Replace("APP_VERSION", BuildInfo.Version.ToString());

View File

@ -1,21 +1,25 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Nancy.Responses;
using NLog; using NLog;
using Nancy; using Nancy;
using NzbDrone.Api.Frontend.Mappers; using NzbDrone.Api.Frontend.Mappers;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Api.Frontend namespace NzbDrone.Api.Frontend
{ {
public class StaticResourceModule : NancyModule public class StaticResourceModule : NancyModule
{ {
private readonly IEnumerable<IMapHttpRequestsToDisk> _requestMappers; private readonly IEnumerable<IMapHttpRequestsToDisk> _requestMappers;
private readonly IConfigFileProvider _configFileProvider;
private readonly Logger _logger; private readonly Logger _logger;
public StaticResourceModule(IEnumerable<IMapHttpRequestsToDisk> requestMappers, Logger logger) public StaticResourceModule(IEnumerable<IMapHttpRequestsToDisk> requestMappers, IConfigFileProvider configFileProvider, Logger logger)
{ {
_requestMappers = requestMappers; _requestMappers = requestMappers;
_configFileProvider = configFileProvider;
_logger = logger; _logger = logger;
Get["/{resource*}"] = x => Index(); Get["/{resource*}"] = x => Index();
@ -34,8 +38,21 @@ namespace NzbDrone.Api.Frontend
return new NotFoundResponse(); 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) if (mapper != null)
{ {

View File

@ -33,6 +33,13 @@ namespace NzbDrone.Api.History
SortDirection = pagingResource.SortDirection 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) if (episodeId.HasValue)
{ {
int i = (int)episodeId; int i = (int)episodeId;

View File

@ -43,27 +43,27 @@ namespace NzbDrone.Api
private List<TProviderResource> GetAll() private List<TProviderResource> GetAll()
{ {
var indexerDefinitions = _providerFactory.All(); var providerDefinitions = _providerFactory.All();
var result = new List<TProviderResource>(indexerDefinitions.Count); var result = new List<TProviderResource>(providerDefinitions.Count);
foreach (var definition in indexerDefinitions) foreach (var definition in providerDefinitions)
{ {
var indexerResource = new TProviderResource(); var providerResource = new TProviderResource();
indexerResource.InjectFrom(definition); providerResource.InjectFrom(definition);
indexerResource.Fields = SchemaBuilder.ToSchema(definition.Settings); providerResource.Fields = SchemaBuilder.ToSchema(definition.Settings);
result.Add(indexerResource); result.Add(providerResource);
} }
return result; return result;
} }
private int CreateProvider(TProviderResource indexerResource) private int CreateProvider(TProviderResource providerResource)
{ {
var indexer = GetDefinition(indexerResource); var provider = GetDefinition(providerResource);
indexer = _providerFactory.Create(indexer); provider = _providerFactory.Create(provider);
return indexer.Id; return provider.Id;
} }
private void UpdateProvider(TProviderResource providerResource) private void UpdateProvider(TProviderResource providerResource)

View File

@ -43,7 +43,8 @@ namespace NzbDrone.Api.System
IsWindows = OsInfo.IsWindows, IsWindows = OsInfo.IsWindows,
Branch = _configFileProvider.Branch, Branch = _configFileProvider.Branch,
Authentication = _configFileProvider.AuthenticationEnabled, Authentication = _configFileProvider.AuthenticationEnabled,
StartOfWeek = (int)OsInfo.FirstDayOfWeek StartOfWeek = (int)OsInfo.FirstDayOfWeek,
UrlBase = _configFileProvider.UrlBase
}.AsResponse(); }.AsResponse();
} }

View File

@ -17,8 +17,6 @@ namespace NzbDrone.Common.Composition
protected ContainerBuilderBase(IStartupContext args, params string[] assemblies) protected ContainerBuilderBase(IStartupContext args, params string[] assemblies)
{ {
_loadedTypes = new List<Type>(); _loadedTypes = new List<Type>();
foreach (var assembly in assemblies) foreach (var assembly in assemblies)
@ -55,8 +53,6 @@ namespace NzbDrone.Common.Composition
{ {
var implementations = Container.GetImplementations(contractType).Where(c => !c.IsGenericTypeDefinition).ToList(); var implementations = Container.GetImplementations(contractType).Where(c => !c.IsGenericTypeDefinition).ToList();
if (implementations.Count == 0) if (implementations.Count == 0)
{ {
return; return;

View File

@ -139,7 +139,6 @@ namespace NzbDrone.Common
} }
} }
public ServiceControllerStatus GetStatus(string serviceName) public ServiceControllerStatus GetStatus(string serviceName)
{ {
return GetService(serviceName).Status; return GetService(serviceName).Status;

View File

@ -16,12 +16,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
public void Setup() public void Setup()
{ {
_parseResult = new RemoteEpisode _parseResult = new RemoteEpisode
{ {
Release = new ReleaseInfo Release = new ReleaseInfo
{ {
Title = "Dexter.S08E01.EDITED.WEBRip.x264-KYR" Title = "Dexter.S08E01.EDITED.WEBRip.x264-KYR"
} }
}; };
} }
[Test] [Test]
@ -49,5 +49,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Mocker.GetMock<IConfigService>().SetupGet(c => c.ReleaseRestrictions).Returns(restrictions); Mocker.GetMock<IConfigService>().SetupGet(c => c.ReleaseRestrictions).Returns(restrictions);
Subject.IsSatisfiedBy(_parseResult, null).Should().BeTrue(); Subject.IsSatisfiedBy(_parseResult, null).Should().BeTrue();
} }
[Test]
public void should_not_try_to_find_empty_string_as_a_match()
{
Mocker.GetMock<IConfigService>().SetupGet(c => c.ReleaseRestrictions).Returns("test\n");
Subject.IsSatisfiedBy(_parseResult, null).Should().BeTrue();
}
} }
} }

View File

@ -27,7 +27,6 @@ namespace NzbDrone.Core.Test.IndexerTests
_indexers.Add(new Wombles()); _indexers.Add(new Wombles());
Mocker.SetConstant<IEnumerable<IIndexer>>(_indexers); Mocker.SetConstant<IEnumerable<IIndexer>>(_indexers);
} }
[Test] [Test]
@ -61,7 +60,6 @@ namespace NzbDrone.Core.Test.IndexerTests
indexers.Select(c => c.Name).Should().OnlyHaveUniqueItems(); indexers.Select(c => c.Name).Should().OnlyHaveUniqueItems();
} }
[Test] [Test]
public void should_remove_missing_indexers_on_startup() public void should_remove_missing_indexers_on_startup()
{ {
@ -69,13 +67,11 @@ namespace NzbDrone.Core.Test.IndexerTests
Mocker.SetConstant<IIndexerRepository>(repo); Mocker.SetConstant<IIndexerRepository>(repo);
var existingIndexers = Builder<IndexerDefinition>.CreateNew().BuildNew(); var existingIndexers = Builder<IndexerDefinition>.CreateNew().BuildNew();
existingIndexers.ConfigContract = typeof (NewznabSettings).Name; existingIndexers.ConfigContract = typeof (NewznabSettings).Name;
repo.Insert(existingIndexers); repo.Insert(existingIndexers);
Subject.Handle(new ApplicationStartedEvent()); Subject.Handle(new ApplicationStartedEvent());
AllStoredModels.Should().NotContain(c => c.Id == existingIndexers.Id); AllStoredModels.Should().NotContain(c => c.Id == existingIndexers.Id);

View File

@ -20,7 +20,6 @@ namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests
public void SetUp() public void SetUp()
{ {
UseRealHttp(); UseRealHttp();
} }
[Test] [Test]
@ -39,7 +38,6 @@ namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests
ValidateResult(result, skipSize: true, skipInfo: true); ValidateResult(result, skipSize: true, skipInfo: true);
} }
[Test] [Test]
public void extv_rss() public void extv_rss()
{ {
@ -55,7 +53,6 @@ namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests
ValidateTorrentResult(result, skipSize: false, skipInfo: true); ValidateTorrentResult(result, skipSize: false, skipInfo: true);
} }
[Test] [Test]
public void nzbsorg_rss() public void nzbsorg_rss()
{ {
@ -74,8 +71,6 @@ namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests
ValidateResult(result); ValidateResult(result);
} }
private void ValidateResult(IList<ReleaseInfo> reports, bool skipSize = false, bool skipInfo = false) private void ValidateResult(IList<ReleaseInfo> reports, bool skipSize = false, bool skipInfo = false)
{ {
reports.Should().NotBeEmpty(); reports.Should().NotBeEmpty();
@ -97,7 +92,6 @@ namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests
private void ValidateTorrentResult(IList<ReleaseInfo> reports, bool skipSize = false, bool skipInfo = false) private void ValidateTorrentResult(IList<ReleaseInfo> reports, bool skipSize = false, bool skipInfo = false)
{ {
reports.Should().OnlyContain(c => c.GetType() == typeof(TorrentInfo)); reports.Should().OnlyContain(c => c.GetType() == typeof(TorrentInfo));
ValidateResult(reports, skipSize, skipInfo); ValidateResult(reports, skipSize, skipInfo);

View File

@ -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<FetchFeedService>
{
private Series _series;
[SetUp]
public void Setup()
{
_series = Builder<Series>.CreateNew().Build();
Mocker.GetMock<IHttpProvider>().Setup(s => s.DownloadString(It.IsAny<String>())).Returns("<xml></xml>");
}
private IndexerBase<TestIndexerSettings> WithIndexer(bool paging, int resultCount)
{
var results = Builder<ReleaseInfo>.CreateListOfSize(resultCount)
.Build();
var indexer = Mocker.GetMock<IndexerBase<TestIndexerSettings>>();
indexer.Setup(s => s.Parser.Process(It.IsAny<String>(), It.IsAny<String>()))
.Returns(results);
indexer.Setup(s => s.GetSeasonSearchUrls(It.IsAny<String>(), It.IsAny<Int32>(), It.IsAny<Int32>(), It.IsAny<Int32>()))
.Returns(new List<string> { "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<IHttpProvider>().Verify(v => v.DownloadString(It.IsAny<String>()), 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<IHttpProvider>().Verify(v => v.DownloadString(It.IsAny<String>()), 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<IHttpProvider>().Verify(v => v.DownloadString(It.IsAny<String>()), Times.Exactly(10));
}
}
public class TestIndexerSettings : IProviderConfig
{
public ValidationResult Validate()
{
throw new NotImplementedException();
}
}
}

View File

@ -139,6 +139,7 @@
<Compile Include="IndexerTests\IndexerServiceFixture.cs" /> <Compile Include="IndexerTests\IndexerServiceFixture.cs" />
<Compile Include="IndexerTests\IntegrationTests\IndexerIntegrationTests.cs" /> <Compile Include="IndexerTests\IntegrationTests\IndexerIntegrationTests.cs" />
<Compile Include="IndexerTests\NewznabTests\NewznabSettingFixture.cs" /> <Compile Include="IndexerTests\NewznabTests\NewznabSettingFixture.cs" />
<Compile Include="IndexerTests\SeasonSearchFixture.cs" />
<Compile Include="IndexerTests\XElementExtensionsFixture.cs" /> <Compile Include="IndexerTests\XElementExtensionsFixture.cs" />
<Compile Include="JobTests\JobRepositoryFixture.cs" /> <Compile Include="JobTests\JobRepositoryFixture.cs" />
<Compile Include="DecisionEngineTests\LanguageSpecificationFixture.cs" /> <Compile Include="DecisionEngineTests\LanguageSpecificationFixture.cs" />

View File

@ -31,6 +31,7 @@ namespace NzbDrone.Core.Configuration
string ApiKey { get; } string ApiKey { get; }
bool Torrent { get; } bool Torrent { get; }
string SslCertHash { get; } string SslCertHash { get; }
string UrlBase { get; }
} }
public class ConfigFileProvider : IConfigFileProvider public class ConfigFileProvider : IConfigFileProvider
@ -152,6 +153,21 @@ namespace NzbDrone.Core.Configuration
get { return GetValue("SslCertHash", ""); } 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) public int GetValueInt(string key, int defaultValue)
{ {
return Convert.ToInt32(GetValue(key, defaultValue)); return Convert.ToInt32(GetValue(key, defaultValue));
@ -181,7 +197,7 @@ namespace NzbDrone.Core.Configuration
var valueHolder = parentContainer.Descendants(key).ToList(); var valueHolder = parentContainer.Descendants(key).ToList();
if (valueHolder.Count() == 1) if (valueHolder.Count() == 1)
return valueHolder.First().Value; return valueHolder.First().Value.Trim();
//Save the value //Save the value
if (persist) if (persist)
@ -198,6 +214,7 @@ namespace NzbDrone.Core.Configuration
{ {
EnsureDefaultConfigFile(); EnsureDefaultConfigFile();
var valueString = value.ToString().Trim();
var xDoc = LoadConfigFile(); var xDoc = LoadConfigFile();
var config = xDoc.Descendants(CONFIG_ELEMENT_NAME).Single(); var config = xDoc.Descendants(CONFIG_ELEMENT_NAME).Single();
@ -207,15 +224,15 @@ namespace NzbDrone.Core.Configuration
if (keyHolder.Count() != 1) if (keyHolder.Count() != 1)
{ {
parentContainer.Add(new XElement(key, value)); parentContainer.Add(new XElement(key, valueString));
} }
else 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); xDoc.Save(_configFile);
} }

View File

@ -231,8 +231,8 @@ namespace NzbDrone.Core.Configuration
public string ReleaseRestrictions public string ReleaseRestrictions
{ {
get { return GetValue("ReleaseRestrictions", String.Empty); } get { return GetValue("ReleaseRestrictions", String.Empty).Trim('\r', '\n'); }
set { SetValue("ReleaseRestrictions", value); } set { SetValue("ReleaseRestrictions", value.Trim('\r', '\n')); }
} }
public Int32 RssSyncInterval public Int32 RssSyncInterval

View File

@ -1,8 +0,0 @@
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.DataAugmentation.Xem
{
public class RefreshXemCacheCommand : Command
{
}
}

View File

@ -1,15 +1,15 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Web.UI.WebControls;
using NLog; using NLog;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events; using NzbDrone.Core.Tv.Events;
namespace NzbDrone.Core.DataAugmentation.Xem namespace NzbDrone.Core.DataAugmentation.Xem
{ {
public class XemService : IHandle<SeriesUpdatedEvent>, IExecute<RefreshXemCacheCommand> public class XemService : IHandle<SeriesUpdatedEvent>, IHandle<SeriesRefreshStartingEvent>
{ {
private readonly IEpisodeService _episodeService; private readonly IEpisodeService _episodeService;
private readonly IXemProxy _xemProxy; private readonly IXemProxy _xemProxy;
@ -84,10 +84,13 @@ namespace NzbDrone.Core.DataAugmentation.Xem
private void RefreshCache() private void RefreshCache()
{ {
_cache.Clear();
var ids = _xemProxy.GetXemSeriesIds(); var ids = _xemProxy.GetXemSeriesIds();
if (ids.Any())
{
_cache.Clear();
}
foreach (var id in ids) foreach (var id in ids)
{ {
_cache.Set(id.ToString(), true, TimeSpan.FromHours(1)); _cache.Set(id.ToString(), true, TimeSpan.FromHours(1));
@ -110,7 +113,7 @@ namespace NzbDrone.Core.DataAugmentation.Xem
PerformUpdate(message.Series); PerformUpdate(message.Series);
} }
public void Execute(RefreshXemCacheCommand message) public void Handle(SeriesRefreshStartingEvent message)
{ {
RefreshCache(); RefreshCache();
} }

View File

@ -37,7 +37,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
return true; return true;
} }
var restrictions = restrictionsString.Split('\n'); var restrictions = restrictionsString.Split(new []{ '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var restriction in restrictions) foreach (var restriction in restrictions)
{ {

View File

@ -124,7 +124,7 @@ namespace NzbDrone.Core.Download
private List<History.History> GetHistoryItems(List<History.History> grabbedHistory, string downloadClientId) private List<History.History> GetHistoryItems(List<History.History> 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)) h.Data[DOWNLOAD_CLIENT_ID].Equals(downloadClientId))
.ToList(); .ToList();
} }

View File

@ -14,6 +14,14 @@ namespace NzbDrone.Core.Indexers.Eztv
} }
} }
public override bool SupportsPaging
{
get
{
return false;
}
}
public override IParseFeed Parser public override IParseFeed Parser
{ {
get get

View File

@ -8,6 +8,7 @@ namespace NzbDrone.Core.Indexers
{ {
IParseFeed Parser { get; } IParseFeed Parser { get; }
DownloadProtocol Protocol { get; } DownloadProtocol Protocol { get; }
Boolean SupportsPaging { get; }
IEnumerable<string> RecentFeed { get; } IEnumerable<string> RecentFeed { get; }
IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber); IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber);

View File

@ -5,6 +5,6 @@ namespace NzbDrone.Core.Indexers
{ {
public interface IParseFeed public interface IParseFeed
{ {
IEnumerable<ReleaseInfo> Process(string source, string url); IEnumerable<ReleaseInfo> Process(string xml, string url);
} }
} }

View File

@ -34,6 +34,8 @@ namespace NzbDrone.Core.Indexers
public abstract DownloadProtocol Protocol { get; } public abstract DownloadProtocol Protocol { get; }
public abstract bool SupportsPaging { get; }
protected TSettings Settings protected TSettings Settings
{ {
get get

View File

@ -13,7 +13,6 @@ namespace NzbDrone.Core.Indexers
public interface IFetchFeedFromIndexers public interface IFetchFeedFromIndexers
{ {
IList<ReleaseInfo> FetchRss(IIndexer indexer); IList<ReleaseInfo> FetchRss(IIndexer indexer);
IList<ReleaseInfo> Fetch(IIndexer indexer, SeasonSearchCriteria searchCriteria); IList<ReleaseInfo> Fetch(IIndexer indexer, SeasonSearchCriteria searchCriteria);
IList<ReleaseInfo> Fetch(IIndexer indexer, SingleEpisodeSearchCriteria searchCriteria); IList<ReleaseInfo> Fetch(IIndexer indexer, SingleEpisodeSearchCriteria searchCriteria);
IList<ReleaseInfo> Fetch(IIndexer indexer, DailyEpisodeSearchCriteria searchCriteria); IList<ReleaseInfo> Fetch(IIndexer indexer, DailyEpisodeSearchCriteria searchCriteria);
@ -24,7 +23,6 @@ namespace NzbDrone.Core.Indexers
private readonly Logger _logger; private readonly Logger _logger;
private readonly IHttpProvider _httpProvider; private readonly IHttpProvider _httpProvider;
public FetchFeedService(IHttpProvider httpProvider, Logger logger) public FetchFeedService(IHttpProvider httpProvider, Logger logger)
{ {
_httpProvider = httpProvider; _httpProvider = httpProvider;
@ -60,12 +58,13 @@ namespace NzbDrone.Core.Indexers
var searchUrls = indexer.GetSeasonSearchUrls(searchCriteria.QueryTitle, searchCriteria.Series.TvRageId, searchCriteria.SeasonNumber, offset); var searchUrls = indexer.GetSeasonSearchUrls(searchCriteria.QueryTitle, searchCriteria.Series.TvRageId, searchCriteria.SeasonNumber, offset);
var result = Fetch(indexer, searchUrls); var result = Fetch(indexer, searchUrls);
_logger.Info("{0} offset {1}. Found {2}", indexer, searchCriteria, result.Count); _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; return result;

View File

@ -55,7 +55,6 @@ namespace NzbDrone.Core.Indexers.Newznab
}); });
return list; return list;
} }
} }
@ -73,6 +72,14 @@ namespace NzbDrone.Core.Indexers.Newznab
return settings; return settings;
} }
public override bool SupportsPaging
{
get
{
return true;
}
}
public override IEnumerable<string> RecentFeed public override IEnumerable<string> RecentFeed
{ {
get get

View File

@ -66,5 +66,13 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs
return searchUrls; return searchUrls;
} }
public override bool SupportsPaging
{
get
{
return false;
}
}
} }
} }

View File

@ -14,6 +14,14 @@ namespace NzbDrone.Core.Indexers.Wombles
} }
} }
public override bool SupportsPaging
{
get
{
return false;
}
}
public override IParseFeed Parser public override IParseFeed Parser
{ {
get get
@ -24,7 +32,7 @@ namespace NzbDrone.Core.Indexers.Wombles
public override IEnumerable<string> RecentFeed public override IEnumerable<string> 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<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber) public override IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber)

View File

@ -53,10 +53,8 @@ namespace NzbDrone.Core.Jobs
new ScheduledTask{ Interval = 1*60, TypeName = typeof(ApplicationUpdateCommand).FullName}, new ScheduledTask{ Interval = 1*60, TypeName = typeof(ApplicationUpdateCommand).FullName},
new ScheduledTask{ Interval = 1*60, TypeName = typeof(TrimLogCommand).FullName}, new ScheduledTask{ Interval = 1*60, TypeName = typeof(TrimLogCommand).FullName},
new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).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 = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName},
new ScheduledTask{ Interval = 24*60, TypeName = typeof(HousekeepingCommand).FullName}, new ScheduledTask{ Interval = 24*60, TypeName = typeof(HousekeepingCommand).FullName},
}; };
var currentTasks = _scheduledTaskRepository.All(); var currentTasks = _scheduledTaskRepository.All();

View File

@ -5,6 +5,7 @@ using System.Net;
using NLog; using NLog;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events; using NzbDrone.Core.Tv.Events;
@ -19,16 +20,18 @@ namespace NzbDrone.Core.MediaCover
private readonly IHttpProvider _httpProvider; private readonly IHttpProvider _httpProvider;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly ICoverExistsSpecification _coverExistsSpecification; private readonly ICoverExistsSpecification _coverExistsSpecification;
private readonly IConfigFileProvider _configFileProvider;
private readonly Logger _logger; private readonly Logger _logger;
private readonly string _coverRootFolder; private readonly string _coverRootFolder;
public MediaCoverService(IHttpProvider httpProvider, IDiskProvider diskProvider, IAppFolderInfo appFolderInfo, public MediaCoverService(IHttpProvider httpProvider, IDiskProvider diskProvider, IAppFolderInfo appFolderInfo,
ICoverExistsSpecification coverExistsSpecification, Logger logger) ICoverExistsSpecification coverExistsSpecification, IConfigFileProvider configFileProvider, Logger logger)
{ {
_httpProvider = httpProvider; _httpProvider = httpProvider;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_coverExistsSpecification = coverExistsSpecification; _coverExistsSpecification = coverExistsSpecification;
_configFileProvider = configFileProvider;
_logger = logger; _logger = logger;
_coverRootFolder = appFolderInfo.GetMediaCoverPath(); _coverRootFolder = appFolderInfo.GetMediaCoverPath();
@ -96,7 +99,7 @@ namespace NzbDrone.Core.MediaCover
{ {
var filePath = GetCoverPath(seriesId, mediaCover.CoverType); 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)) if (_diskProvider.FileExists(filePath))
{ {

View File

@ -7,18 +7,18 @@ namespace NzbDrone.Core.Notifications.PushBullet
{ {
public interface IPushBulletProxy 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<TestPushBulletCommand> public class PushBulletProxy : IPushBulletProxy, IExecute<TestPushBulletCommand>
{ {
private const string URL = "https://api.pushbullet.com/api/pushes"; 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 client = new RestClient(URL);
var request = new RestRequest(Method.POST); var request = BuildRequest(deviceId);
request.AddParameter("device_id", deviceId);
request.AddParameter("type", "note"); request.AddParameter("type", "note");
request.AddParameter("title", title); request.AddParameter("title", title);
request.AddParameter("body", message); request.AddParameter("body", message);
@ -27,6 +27,24 @@ namespace NzbDrone.Core.Notifications.PushBullet
client.ExecuteAndValidate(request); 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) public void Execute(TestPushBulletCommand message)
{ {
const string title = "Test Notification"; const string title = "Test Notification";

View File

@ -11,7 +11,7 @@ namespace NzbDrone.Core.Notifications.PushBullet
public PushBulletSettingsValidator() public PushBulletSettingsValidator()
{ {
RuleFor(c => c.ApiKey).NotEmpty(); 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; } public String ApiKey { get; set; }
[FieldDefinition(1, Label = "Device ID")] [FieldDefinition(1, Label = "Device ID")]
public Int64 DeviceId { get; set; } public String DeviceId { get; set; }
public bool IsValid public bool IsValid
{ {
get get
{ {
return !String.IsNullOrWhiteSpace(ApiKey) && DeviceId > 0; return !String.IsNullOrWhiteSpace(ApiKey) && !String.IsNullOrWhiteSpace(DeviceId);
} }
} }

View File

@ -13,6 +13,6 @@ namespace NzbDrone.Core.Notifications.PushBullet
} }
} }
public string ApiKey { get; set; } public string ApiKey { get; set; }
public long DeviceId { get; set; } public string DeviceId { get; set; }
} }
} }

View File

@ -139,7 +139,6 @@
<Compile Include="DataAugmentation\Xem\Model\XemResult.cs" /> <Compile Include="DataAugmentation\Xem\Model\XemResult.cs" />
<Compile Include="DataAugmentation\Xem\Model\XemSceneTvdbMapping.cs" /> <Compile Include="DataAugmentation\Xem\Model\XemSceneTvdbMapping.cs" />
<Compile Include="DataAugmentation\Xem\Model\XemValues.cs" /> <Compile Include="DataAugmentation\Xem\Model\XemValues.cs" />
<Compile Include="DataAugmentation\Xem\RefreshXemCacheCommand.cs" />
<Compile Include="DataAugmentation\Xem\XemProxy.cs" /> <Compile Include="DataAugmentation\Xem\XemProxy.cs" />
<Compile Include="DataAugmentation\Xem\XemService.cs" /> <Compile Include="DataAugmentation\Xem\XemService.cs" />
<Compile Include="Datastore\ConnectionStringFactory.cs" /> <Compile Include="Datastore\ConnectionStringFactory.cs" />
@ -477,6 +476,7 @@
<Compile Include="ThingiProvider\ProviderRepository.cs" /> <Compile Include="ThingiProvider\ProviderRepository.cs" />
<Compile Include="ThingiProvider\ProviderFactory.cs" /> <Compile Include="ThingiProvider\ProviderFactory.cs" />
<Compile Include="Tv\EpisodeService.cs" /> <Compile Include="Tv\EpisodeService.cs" />
<Compile Include="Tv\Events\SeriesRefreshStartingEvent.cs" />
<Compile Include="Tv\Events\EpisodeInfoDeletedEvent.cs" /> <Compile Include="Tv\Events\EpisodeInfoDeletedEvent.cs" />
<Compile Include="Tv\Events\EpisodeInfoUpdatedEvent.cs" /> <Compile Include="Tv\Events\EpisodeInfoUpdatedEvent.cs" />
<Compile Include="Tv\Events\EpisodeInfoAddedEvent.cs" /> <Compile Include="Tv\Events\EpisodeInfoAddedEvent.cs" />

View File

@ -0,0 +1,8 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Tv.Events
{
public class SeriesRefreshStartingEvent : IEvent
{
}
}

View File

@ -99,6 +99,8 @@ namespace NzbDrone.Core.Tv
public void Execute(RefreshSeriesCommand message) public void Execute(RefreshSeriesCommand message)
{ {
_eventAggregator.PublishEvent(new SeriesRefreshStartingEvent());
if (message.SeriesId.HasValue) if (message.SeriesId.HasValue)
{ {
var series = _seriesService.GetSeries(message.SeriesId.Value); var series = _seriesService.GetSeries(message.SeriesId.Value);

View File

@ -5,5 +5,13 @@ namespace NzbDrone.Core.Update.Commands
public class InstallUpdateCommand : Command public class InstallUpdateCommand : Command
{ {
public UpdatePackage UpdatePackage { get; set; } public UpdatePackage UpdatePackage { get; set; }
public override bool SendUpdatesToClient
{
get
{
return true;
}
}
} }
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
@ -9,8 +10,7 @@ namespace NzbDrone.Host.AccessControl
public interface IUrlAclAdapter public interface IUrlAclAdapter
{ {
void ConfigureUrl(); void ConfigureUrl();
string Url { get; } List<String> Urls { get; }
string HttpsUrl { get; }
} }
public class UrlAclAdapter : IUrlAclAdapter public class UrlAclAdapter : IUrlAclAdapter
@ -20,13 +20,7 @@ namespace NzbDrone.Host.AccessControl
private readonly IRuntimeInfo _runtimeInfo; private readonly IRuntimeInfo _runtimeInfo;
private readonly Logger _logger; private readonly Logger _logger;
public string Url { get; private set; } public List<String> Urls { get; private set; }
public string HttpsUrl { get; private set; }
private string _localUrl;
private string _wildcardUrl;
private string _localHttpsUrl;
private string _wildcardHttpsUrl;
public UrlAclAdapter(INetshProvider netshProvider, public UrlAclAdapter(INetshProvider netshProvider,
IConfigFileProvider configFileProvider, IConfigFileProvider configFileProvider,
@ -38,26 +32,35 @@ namespace NzbDrone.Host.AccessControl
_runtimeInfo = runtimeInfo; _runtimeInfo = runtimeInfo;
_logger = logger; _logger = logger;
_localUrl = String.Format("http://localhost:{0}/", _configFileProvider.Port); Urls = new List<String>();
_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;
} }
public void ConfigureUrl() 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; var httpUrls = wildcardHttpUrls.All(IsRegistered) ? wildcardHttpUrls : localHttpUrls;
if (!IsRegistered(_wildcardHttpsUrl)) HttpsUrl = _localHttpsUrl; 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) if (OsInfo.Version.Major < 6)
return; return;
RegisterUrl(Url); Urls.ForEach(RegisterUrl);
RegisterUrl(HttpsUrl);
} }
private bool IsRegistered(string urlAcl) 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); var arguments = String.Format("http add urlacl {0} sddl=D:(A;;GX;;;S-1-1-0)", urlAcl);
_netshProvider.Run(arguments); _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<String> BuildUrls(string protocol, string url, int port)
{
var urls = new List<String>();
var urlBase = _configFileProvider.UrlBase;
if (!String.IsNullOrEmpty(urlBase))
{
urls.Add(BuildUrl(protocol, url, port, urlBase));
}
urls.Add(BuildUrl(protocol, url, port, ""));
return urls;
}
} }
} }

View File

@ -40,7 +40,10 @@ namespace NzbDrone.Host
startCallback(_container); startCallback(_container);
} }
SpinToExit(appMode); else
{
SpinToExit(appMode);
}
} }
catch (TerminateApplicationException e) catch (TerminateApplicationException e)
{ {

View File

@ -53,29 +53,25 @@ namespace NzbDrone.Host.Owin
_firewallAdapter.MakeAccessible(); _firewallAdapter.MakeAccessible();
_sslAdapter.Register(); _sslAdapter.Register();
} }
_urlAclAdapter.ConfigureUrl();
} }
var options = new StartOptions(_urlAclAdapter.Url) _urlAclAdapter.ConfigureUrl();
var options = new StartOptions()
{ {
ServerFactory = "Microsoft.Owin.Host.HttpListener" ServerFactory = "Microsoft.Owin.Host.HttpListener"
}; };
if (_configFileProvider.EnableSsl) _urlAclAdapter.Urls.ForEach(options.Urls.Add);
{
_logger.Trace("SSL enabled, listening on: {0}", _urlAclAdapter.HttpsUrl);
options.Urls.Add(_urlAclAdapter.HttpsUrl);
}
_logger.Info("starting server on {0}", _urlAclAdapter.Url); _logger.Info("Listening on the following URLs:");
foreach (var url in options.Urls)
{
_logger.Info(" {0}", url);
}
try try
{ {
// options.ServerFactory = new
//_host = WebApp.Start(OwinServiceProviderFactory.Create(), options, BuildApp);
//_host = WebApp.Start(options, BuildApp);
_host = WebApp.Start(OwinServiceProviderFactory.Create(), options, BuildApp); _host = WebApp.Start(OwinServiceProviderFactory.Create(), options, BuildApp);
} }
catch (TargetInvocationException ex) catch (TargetInvocationException ex)

View File

@ -23,7 +23,6 @@ namespace NzbDrone.SysTray
_browserService = browserService; _browserService = browserService;
} }
public void Start() public void Start()
{ {
Application.ThreadException += OnThreadException; Application.ThreadException += OnThreadException;

View File

@ -34,7 +34,5 @@ namespace NzbDrone
MessageBox.Show(text: message, buttons: MessageBoxButtons.OK, icon: MessageBoxIcon.Error, caption: "Epic Fail!"); MessageBox.Show(text: message, buttons: MessageBoxButtons.OK, icon: MessageBoxIcon.Error, caption: "Epic Fail!");
} }
} }
} }
} }

View File

@ -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;
});

View File

@ -8,8 +8,11 @@ define(
className : 'season-folder-cell', className : 'season-folder-cell',
render: function () { 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()); this.$el.html(seasonFolder.toString());
return this; return this;
} }
}); });

View File

@ -2,46 +2,46 @@
font-family: 'Open Sans'; font-family: 'Open Sans';
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
src: url('/Content/fonts/opensans-light.eot'); src: url('./fonts/opensans-light.eot');
src: local('Open Sans Light'), src: local('Open Sans Light'),
local('OpenSans-Light'), local('OpenSans-Light'),
url('/Content/fonts/opensans-light.eot?#iefix') format('embedded-opentype'), url('./fonts/opensans-light.eot?#iefix') format('embedded-opentype'),
url('/Content/fonts/opensans-light.woff') format('woff'), url('./fonts/opensans-light.woff') format('woff'),
url('/Content/fonts/opensans-light.ttf') format('truetype'); url('./fonts/opensans-light.ttf') format('truetype');
} }
@font-face { @font-face {
font-family: 'Open Sans'; font-family: 'Open Sans';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url('/Content/fonts/opensans-regular.eot'); src: url('./fonts/opensans-regular.eot');
src: local('Open Sans'), src: local('Open Sans'),
local('OpenSans'), local('OpenSans'),
url('/Content/fonts/opensans-regular.eot?#iefix') format('embedded-opentype'), url('./fonts/opensans-regular.eot?#iefix') format('embedded-opentype'),
url('/Content/fonts/opensans-regular.woff') format('woff'), url('./fonts/opensans-regular.woff') format('woff'),
url('/Content/fonts/opensans-regular.ttf') format('truetype') url('./fonts/opensans-regular.ttf') format('truetype')
} }
@font-face { @font-face {
font-family: 'Open Sans'; font-family: 'Open Sans';
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
src: url('/Content/fonts/opensans-semibold.eot'); src: url('./fonts/opensans-semibold.eot');
src: local('Open Sans SemiBold'), src: local('Open Sans SemiBold'),
local('OpenSans-SemiBold'), local('OpenSans-SemiBold'),
url('/Content/fonts/opensans-semibold.eot?#iefix') format('embedded-opentype'), url('./fonts/opensans-semibold.eot?#iefix') format('embedded-opentype'),
url('/Content/fonts/opensans-semibold.woff') format('woff'), url('./fonts/opensans-semibold.woff') format('woff'),
url('/Content/fonts/opensans-semibold.ttf') format('truetype') url('./fonts/opensans-semibold.ttf') format('truetype')
} }
@font-face { @font-face {
font-family: 'Ubuntu Mono'; font-family: 'Ubuntu Mono';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url('/Content/fonts/ubuntumono-regular.eot'); src: url('./fonts/ubuntumono-regular.eot');
src: local('Open Sans'), src: local('Open Sans'),
local('OpenSans'), local('OpenSans'),
url('/Content/fonts/ubuntumono-regular.eot?#iefix') format('embedded-opentype'), url('./fonts/ubuntumono-regular.eot?#iefix') format('embedded-opentype'),
url('/Content/fonts/ubuntumono-regular.woff') format('woff'), url('./fonts/ubuntumono-regular.woff') format('woff'),
url('/Content/fonts/ubuntumono-regular.ttf') format('truetype') url('./fonts/ubuntumono-regular.ttf') format('truetype')
} }

View File

@ -46,6 +46,17 @@
.page-toolbar { .page-toolbar {
margin-top : 10px; margin-top : 10px;
margin-bottom : 30px; margin-bottom : 30px;
.toolbar-group {
display: inline-block;
}
.sorting-buttons {
.sorting-title {
display: inline-block;
width: 110px;
}
}
} }
.page-container { .page-container {

View File

@ -47,7 +47,7 @@ define(
this.model = options.model; this.model = options.model;
this.series = options.series; 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.collection.fetch();
this.listenTo(this.collection, 'sync', this._showTable); this.listenTo(this.collection, 'sync', this._showTable);
}, },

View File

@ -6,10 +6,8 @@ define(
'Cells/FileSizeCell', 'Cells/FileSizeCell',
'Cells/QualityCell', 'Cells/QualityCell',
'Cells/ApprovalStatusCell', 'Cells/ApprovalStatusCell',
'Release/DownloadReportCell', 'Release/DownloadReportCell'
'Cells/Header/QualityHeaderCell' ], function (Marionette, Backgrid, FileSizeCell, QualityCell, ApprovalStatusCell, DownloadReportCell) {
], function (Marionette, Backgrid, FileSizeCell, QualityCell, ApprovalStatusCell, DownloadReportCell, QualityHeaderCell) {
return Marionette.Layout.extend({ return Marionette.Layout.extend({
template: 'Episode/Search/ManualLayoutTemplate', template: 'Episode/Search/ManualLayoutTemplate',
@ -49,7 +47,9 @@ define(
label : 'Quality', label : 'Quality',
sortable : true, sortable : true,
cell : QualityCell, cell : QualityCell,
headerCell: QualityHeaderCell sortValue : function (model) {
return model.get('quality').quality.weight;
}
}, },
{ {

View File

@ -2,10 +2,11 @@
define( define(
[ [
'handlebars' 'handlebars',
], function (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) { window.NzbDrone.imageError = function (img) {
if (!img.src.contains(placeHolder)) { if (!img.src.contains(placeHolder)) {
@ -17,4 +18,8 @@ define(
Handlebars.registerHelper('defaultImg', function () { Handlebars.registerHelper('defaultImg', function () {
return new Handlebars.SafeString('onerror=window.NzbDrone.imageError(this)'); return new Handlebars.SafeString('onerror=window.NzbDrone.imageError(this)');
}); });
Handlebars.registerHelper('UrlBase', function () {
return new Handlebars.SafeString(StatusModel.get('urlBase'));
});
}); });

View File

@ -2,8 +2,9 @@
define( define(
[ [
'handlebars', 'handlebars',
'System/StatusModel',
'underscore' 'underscore'
], function (Handlebars, _) { ], function (Handlebars, StatusModel, _) {
Handlebars.registerHelper('poster', function () { Handlebars.registerHelper('poster', function () {
var poster = _.where(this.images, {coverType: 'poster'}); var poster = _.where(this.images, {coverType: 'poster'});
@ -32,7 +33,7 @@ define(
}); });
Handlebars.registerHelper('route', function () { Handlebars.registerHelper('route', function () {
return '/series/' + this.titleSlug; return StatusModel.get('urlBase') + '/series/' + this.titleSlug;
}); });
Handlebars.registerHelper('percentOfEpisodes', function () { Handlebars.registerHelper('percentOfEpisodes', function () {

View File

@ -2,9 +2,10 @@
define( define(
[ [
'History/HistoryModel', 'History/HistoryModel',
'backbone.pageable' 'backbone.pageable',
], function (HistoryModel, PageableCollection) { 'Mixins/AsPersistedStateCollection'
return PageableCollection.extend({ ], function (HistoryModel, PageableCollection, AsPersistedStateCollection) {
var collection = PageableCollection.extend({
url : window.NzbDrone.ApiRoot + '/history', url : window.NzbDrone.ApiRoot + '/history',
model: HistoryModel, model: HistoryModel,
@ -48,4 +49,6 @@ define(
return resp; return resp;
} }
}); });
return AsPersistedStateCollection.apply(collection);
}); });

View File

@ -45,7 +45,8 @@ define(
{ {
name : 'series', name : 'series',
label: 'Series', label: 'Series',
cell : SeriesTitleCell cell : SeriesTitleCell,
sortValue: 'series.title'
}, },
{ {
name : 'episode', name : 'episode',
@ -80,7 +81,7 @@ define(
initialize: function () { initialize: function () {
this.collection = new HistoryCollection(); this.collection = new HistoryCollection({ tableName: 'history' });
this.listenTo(this.collection, 'sync', this._showTable); this.listenTo(this.collection, 'sync', this._showTable);
}, },

File diff suppressed because it is too large Load Diff

View File

@ -5,19 +5,202 @@
Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
Licensed under the MIT @license. 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"; "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, string>): 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 Paginator is a Backgrid extension that renders a series of configurable
pagination handles. This extension is best used for splitting a large data 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 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 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 within a sliding window, plus the rewind, back, forward and fast forward
next page handles. The fast forward, fast backward, previous and next page control handles. The individual control handles can be turned off.
handles can be turned off.
@class Backgrid.Extension.Paginator @class Backgrid.Extension.Paginator
*/ */
@ -30,97 +213,63 @@
windowSize: 10, windowSize: 10,
/** /**
@property {Object} fastForwardHandleLabels You can disable specific @property {Object.<string, Object.<string, string>>} controls You can
handles by setting its value to `null`. disable specific control handles by omitting certain keys.
*/ */
fastForwardHandleLabels: { controls: {
first: "《", rewind: {
prev: "〈", label: "《",
next: "〉", title: "First"
last: "》" },
back: {
label: "〈",
title: "Previous"
},
forward: {
label: "〉",
title: "Next"
},
fastForward: {
label: "》",
title: "Last"
}
}, },
/** @property */ /**
template: _.template('<ul><% _.each(handles, function (handle) { %><li <% if (handle.className) { %>class="<%= handle.className %>"<% } %>><a href="#" <% if (handle.title) {%> title="<%= handle.title %>"<% } %>><%= handle.label %></a></li><% }); %></ul>'), @property {Backgrid.Extension.PageHandle} pageHandle. The PageHandle
class to use for rendering individual handles
*/
pageHandle: PageHandle,
/** @property */ /** @property */
events: { goBackFirstOnSort: true,
"click a": "changePage"
},
/** /**
Initializer. Initializer.
@param {Object} options @param {Object} options
@param {Backbone.Collection} options.collection @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) { initialize: function (options) {
Backgrid.requireOptions(options, ["collection"]); this.controls = options.controls || this.controls;
this.pageHandle = options.pageHandle || this.pageHandle;
var collection = this.collection; var collection = this.collection;
var fullCollection = collection.fullCollection; this.listenTo(collection, "add", this.render);
if (fullCollection) { this.listenTo(collection, "remove", this.render);
this.listenTo(fullCollection, "add", this.render); this.listenTo(collection, "reset", this.render);
this.listenTo(fullCollection, "remove", this.render); if ((options.goBackFirstOnSort || this.goBackFirstOnSort) &&
this.listenTo(fullCollection, "reset", this.render); collection.fullCollection) {
} this.listenTo(collection.fullCollection, "sort", function () {
else { collection.getFirstPage();
this.listenTo(collection, "add", this.render); });
this.listenTo(collection, "remove", this.render);
this.listenTo(collection, "reset", this.render);
} }
}, },
/** _calculateWindow: function () {
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.<Object>} an array of page handle objects hashes
*/
makeHandles: function () {
var handles = [];
var collection = this.collection; var collection = this.collection;
var state = collection.state; var state = collection.state;
@ -132,48 +281,44 @@
currentPage = firstPage ? currentPage - 1 : currentPage; currentPage = firstPage ? currentPage - 1 : currentPage;
var windowStart = Math.floor(currentPage / this.windowSize) * this.windowSize; var windowStart = Math.floor(currentPage / this.windowSize) * this.windowSize;
var windowEnd = Math.min(lastPage + 1, windowStart + 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++) { Creates a list of page handle objects for rendering.
handles.push({
label: i + 1, @return {Array.<Object>} an array of page handle objects hashes
title: "No. " + (i + 1), */
className: currentPage === i ? "active" : undefined 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; var controls = this.controls;
if (ffLabels) { _.each(["back", "rewind", "forward", "fastForward"], function (key) {
var value = controls[key];
if (ffLabels.prev) { if (value) {
handles.unshift({ var handleCtorOpts = {
label: ffLabels.prev, collection: collection,
className: collection.hasPrevious() ? void 0 : "disabled" 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);
} }
}, this);
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"
});
}
}
return handles; return handles;
}, },
@ -184,15 +329,24 @@
render: function () { render: function () {
this.$el.empty(); this.$el.empty();
this.$el.append(this.template({ if (this.handles) {
handles: this.makeHandles() 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; return this;
} }
}); });
}(jQuery, _, Backbone, Backgrid)); }));

View File

@ -1,5 +1,5 @@
/* /*
backbone-pageable 1.3.2 backbone-pageable 1.4.1
http://github.com/wyuenho/backbone-pageable http://github.com/wyuenho/backbone-pageable
Copyright (c) 2013 Jimmy Yuen Ho Wong Copyright (c) 2013 Jimmy Yuen Ho Wong
@ -83,7 +83,7 @@
for (var i = 0, l = kvps.length; i < l; i++) { for (var i = 0, l = kvps.length; i < l; i++) {
var param = kvps[i]; var param = kvps[i];
kvp = param.split('='), k = kvp[0], v = kvp[1] || true; 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); if (_isArray(ls)) ls.push(v);
else if (ls) params[k] = [ls, v]; else if (ls) params[k] = [ls, v];
else params[k] = v; else params[k] = v;
@ -91,6 +91,29 @@
return params; 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 PARAM_TRIM_RE = /[\s'"]/g;
var URL_TRIM_RE = /[<>\s'"]/g; var URL_TRIM_RE = /[<>\s'"]/g;
@ -256,7 +279,7 @@
*/ */
constructor: function (models, options) { constructor: function (models, options) {
Backbone.Collection.apply(this, arguments); BBColProto.constructor.apply(this, arguments);
options = options || {}; options = options || {};
@ -299,7 +322,7 @@
var fullCollection = this.fullCollection; var fullCollection = this.fullCollection;
if (comparator && options.full) { if (comparator && options.full) {
delete this.comparator; this.comparator = null;
fullCollection.comparator = comparator; fullCollection.comparator = comparator;
} }
@ -308,6 +331,7 @@
// make sure the models in the current page and full collection have the // make sure the models in the current page and full collection have the
// same references // same references
if (models && !_isEmpty(models)) { if (models && !_isEmpty(models)) {
this.reset([].slice.call(models), _extend({silent: true}, options));
this.getPage(state.currentPage); this.getPage(state.currentPage);
models.splice.apply(models, [0, models.length].concat(this.models)); models.splice.apply(models, [0, models.length].concat(this.models));
} }
@ -412,22 +436,10 @@
pageCol.at(pageSize) : pageCol.at(pageSize) :
null; null;
if (modelToRemove) { if (modelToRemove) {
var addHandlers = collection._events.add || [], var popOptions = {onAdd: true};
popOptions = {onAdd: true}; runOnceAtLastHandler(collection, event, function () {
if (addHandlers.length) { pageCol.remove(modelToRemove, popOptions);
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);
} }
} }
} }
@ -442,20 +454,25 @@
} }
else { else {
var totalPages = state.totalPages = ceil(state.totalRecords / pageSize); 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; if (state.currentPage > totalPages) state.currentPage = state.lastPage;
} }
pageCol.state = pageCol._checkState(state); pageCol.state = pageCol._checkState(state);
var nextModel, removedIndex = options.index; var nextModel, removedIndex = options.index;
if (collection == pageCol) { 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); fullCol.remove(model);
} }
else if (removedIndex >= pageStart && removedIndex < pageEnd) { else if (removedIndex >= pageStart && removedIndex < pageEnd) {
pageCol.remove(model); pageCol.remove(model);
nextModel = fullCol.at(currentPage * (pageSize + removedIndex)); var at = removedIndex + 1
if (nextModel) pageCol.push(nextModel); nextModel = fullCol.at(at) || fullCol.last();
if (nextModel) pageCol.add(nextModel, {at: at});
} }
} }
else delete options.onAdd; else delete options.onAdd;
@ -466,13 +483,13 @@
collection = model; collection = model;
// Reset that's not a result of getPage // Reset that's not a result of getPage
if (collection === pageCol && options.from == null && if (collection == pageCol && options.from == null &&
options.to == null) { options.to == null) {
var head = fullCol.models.slice(0, pageStart); var head = fullCol.models.slice(0, pageStart);
var tail = fullCol.models.slice(pageStart + pageCol.models.length); var tail = fullCol.models.slice(pageStart + pageCol.models.length);
fullCol.reset(head.concat(pageCol.models).concat(tail), options); fullCol.reset(head.concat(pageCol.models).concat(tail), options);
} }
else if (collection === fullCol) { else if (collection == fullCol) {
if (!(state.totalRecords = fullCol.models.length)) { if (!(state.totalRecords = fullCol.models.length)) {
state.totalRecords = null; state.totalRecords = null;
state.totalPages = null; state.totalPages = null;
@ -551,7 +568,7 @@
throw new RangeError("`firstPage must be 0 or 1`"); 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 (mode == "infinite") {
if (!links[currentPage + '']) { if (!links[currentPage + '']) {
@ -561,6 +578,8 @@
else if (currentPage < firstPage || else if (currentPage < firstPage ||
(totalPages > 0 && (totalPages > 0 &&
(firstPage ? currentPage > totalPages : currentPage >= totalPages))) { (firstPage ? currentPage > totalPages : currentPage >= totalPages))) {
var op = firstPage ? ">=" : ">";
throw new RangeError("`currentPage` must be firstPage <= currentPage " + throw new RangeError("`currentPage` must be firstPage <= currentPage " +
(firstPage ? ">" : ">=") + (firstPage ? ">" : ">=") +
" totalPages if " + firstPage + "-based. Got " + " totalPages if " + firstPage + "-based. Got " +
@ -681,7 +700,7 @@
var fullCollection = this.fullCollection; var fullCollection = this.fullCollection;
var handlers = this._handlers = this._handlers || {}, handler; var handlers = this._handlers = this._handlers || {}, handler;
if (mode != "server" && !fullCollection) { if (mode != "server" && !fullCollection) {
fullCollection = this._makeFullCollection(options.models || []); fullCollection = this._makeFullCollection(options.models || [], options);
fullCollection.pageableCollection = this; fullCollection.pageableCollection = this;
this.fullCollection = fullCollection; this.fullCollection = fullCollection;
var allHandler = this._makeCollectionEventHandler(this, fullCollection); var allHandler = this._makeCollectionEventHandler(this, fullCollection);
@ -856,7 +875,8 @@
[]; [];
if ((mode == "client" || (mode == "infinite" && !_isEmpty(pageModels))) && if ((mode == "client" || (mode == "infinite" && !_isEmpty(pageModels))) &&
!options.fetch) { !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]; if (mode == "infinite") options.url = this.links[pageNum];
@ -1310,8 +1330,8 @@
this.comparator = comparator; this.comparator = comparator;
} }
if (delComp) delete this.comparator; if (delComp) this.comparator = null;
if (delFullComp && fullCollection) delete fullCollection.comparator; if (delFullComp && fullCollection) fullCollection.comparator = null;
return this; return this;
} }

View File

@ -2,11 +2,13 @@
define( define(
[ [
'Series/EpisodeModel', 'Series/EpisodeModel',
'backbone.pageable' 'backbone.pageable',
], function (EpisodeModel, PagableCollection) { 'Mixins/AsPersistedStateCollection'
return PagableCollection.extend({ ], function (EpisodeModel, PagableCollection, AsPersistedStateCollection) {
var collection = PagableCollection.extend({
url : window.NzbDrone.ApiRoot + '/missing', url : window.NzbDrone.ApiRoot + '/missing',
model: EpisodeModel, model: EpisodeModel,
tableName: 'missing',
state: { state: {
pageSize: 15, pageSize: 15,
@ -38,4 +40,6 @@ define(
return resp; return resp;
} }
}); });
return AsPersistedStateCollection.call(collection);
}); });

View File

@ -4,7 +4,7 @@ define(
'underscore', 'underscore',
'marionette', 'marionette',
'backgrid', 'backgrid',
'Missing/Collection', 'Missing/MissingCollection',
'Cells/SeriesTitleCell', 'Cells/SeriesTitleCell',
'Cells/EpisodeNumberCell', 'Cells/EpisodeNumberCell',
'Cells/EpisodeTitleCell', 'Cells/EpisodeTitleCell',
@ -121,7 +121,6 @@ define(
] ]
}; };
this.toolbar.show(new ToolbarLayout({ this.toolbar.show(new ToolbarLayout({
left : left :
[ [

View File

@ -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;
};
}
);

View File

@ -3,47 +3,47 @@
<div class="span12"> <div class="span12">
<ul id="main-menu-region"> <ul id="main-menu-region">
<div class="pull-left logo"> <div class="pull-left logo">
<a href="/"> <a href="{{UrlBase}}/">
<img src="/Content/Images/logo.png" alt="NzbDrone"> <img src="{{UrlBase}}/Content/Images/logo.png" alt="NzbDrone">
</a> </a>
</div> </div>
<li> <li>
<a href="/"> <a href="{{UrlBase}}/">
<i class="icon-play"></i> <i class="icon-play"></i>
<br> <br>
Series Series
</a> </a>
</li> </li>
<li> <li>
<a href="/calendar"> <a href="{{UrlBase}}/calendar">
<i class="icon-calendar"></i> <i class="icon-calendar"></i>
<br> <br>
Calendar Calendar
</a> </a>
</li> </li>
<li> <li>
<a href="/history"> <a href="{{UrlBase}}/history">
<i class="icon-time"></i> <i class="icon-time"></i>
<br> <br>
History History
</a> </a>
</li> </li>
<li> <li>
<a href="/missing"> <a href="{{UrlBase}}/missing">
<i class="icon-warning-sign"></i> <i class="icon-warning-sign"></i>
<br> <br>
Missing Missing
</a> </a>
</li> </li>
<li> <li>
<a href="/settings"> <a href="{{UrlBase}}/settings">
<i class="icon-cogs"></i> <i class="icon-cogs"></i>
<br> <br>
Settings Settings
</a> </a>
</li> </li>
<li> <li>
<a href="/system"> <a href="{{UrlBase}}/system">
<i class="icon-laptop"></i> <i class="icon-laptop"></i>
<br> <br>
System System

View File

@ -29,7 +29,7 @@ define(
//look down for <a/> //look down for <a/>
var href = event.target.getAttribute('href'); var href = event.target.getAttribute('href');
//if couldn't find it look up //if couldn't find it look up'
if (!href && target.parent('a') && target.parent('a')[0]) { if (!href && target.parent('a') && target.parent('a')[0]) {
var linkElement = target.parent('a')[0]; var linkElement = target.parent('a')[0];

View File

@ -6,7 +6,7 @@ define(
'Series/SeriesCollection' 'Series/SeriesCollection'
], function (Backbone, $, SeriesCollection) { ], function (Backbone, $, SeriesCollection) {
$(document).on('keydown', function (e) { $(document).on('keydown', function (e) {
if ($(e.target).is('input')) { if ($(e.target).is('input') || $(e.target).is('textarea')) {
return; return;
} }

View File

@ -94,6 +94,8 @@ define(
model.set('rootFolderPath', rootFolderPath.get('path')); model.set('rootFolderPath', rootFolderPath.get('path'));
} }
model.edited = true;
}); });
SeriesCollection.save(); SeriesCollection.save();
@ -150,6 +152,7 @@ define(
SeriesCollection.each(function (model) { SeriesCollection.each(function (model) {
model.trigger('backgrid:select', model, false); model.trigger('backgrid:select', model, false);
model.edited = false;
}); });
} }
}); });

View File

@ -68,7 +68,7 @@ define(
cell : QualityProfileCell cell : QualityProfileCell
}, },
{ {
name : 'monitored', name : 'seasonFolder',
label: 'Season Folder', label: 'Season Folder',
cell : SeasonFolderCell cell : SeasonFolderCell
}, },

View File

@ -13,7 +13,6 @@ define(
'Cells/QualityProfileCell', 'Cells/QualityProfileCell',
'Cells/EpisodeProgressCell', 'Cells/EpisodeProgressCell',
'Cells/SeriesActionsCell', 'Cells/SeriesActionsCell',
'Shared/Grid/DateHeaderCell',
'Cells/SeriesStatusCell', 'Cells/SeriesStatusCell',
'Series/Index/FooterView', 'Series/Index/FooterView',
'Series/Index/FooterModel', 'Series/Index/FooterModel',
@ -31,7 +30,6 @@ define(
QualityProfileCell, QualityProfileCell,
EpisodeProgressCell, EpisodeProgressCell,
SeriesActionsCell, SeriesActionsCell,
DateHeaderCell,
SeriesStatusCell, SeriesStatusCell,
FooterView, FooterView,
FooterModel, FooterModel,
@ -41,58 +39,57 @@ define(
template: 'Series/Index/SeriesIndexLayoutTemplate', template: 'Series/Index/SeriesIndexLayoutTemplate',
regions: { regions: {
seriesRegion: '#x-series', seriesRegion : '#x-series',
toolbar : '#x-toolbar', toolbar : '#x-toolbar',
footer : '#x-series-footer' footer : '#x-series-footer'
}, },
columns: columns: [
[ {
{ name : 'statusWeight',
name : 'statusWeight', label : '',
label : '', cell : SeriesStatusCell
cell : SeriesStatusCell },
}, {
{ name : 'title',
name : 'title', label : 'Title',
label : 'Title', cell : SeriesTitleCell,
cell : SeriesTitleCell, cellValue: 'this'
cellValue: 'this' },
}, {
{ name : 'seasonCount',
name : 'seasonCount', label: 'Seasons',
label: 'Seasons', cell : 'integer'
cell : 'integer' },
}, {
{ name : 'qualityProfileId',
name : 'qualityProfileId', label: 'Quality',
label: 'Quality', cell : QualityProfileCell
cell : QualityProfileCell },
}, {
{ name : 'network',
name : 'network', label: 'Network',
label: 'Network', cell : 'string'
cell : 'string' },
}, {
{ name : 'nextAiring',
name : 'nextAiring', label : 'Next Airing',
label : 'Next Airing', cell : RelativeDateCell,
cell : RelativeDateCell, sortValue : SeriesCollection.nextAiring
headerCell: DateHeaderCell },
}, {
{ name : 'percentOfEpisodes',
name : 'percentOfEpisodes', label : 'Episodes',
label : 'Episodes', cell : EpisodeProgressCell,
cell : EpisodeProgressCell, className: 'episode-progress-cell'
className: 'episode-progress-cell' },
}, {
{ name : 'this',
name : 'this', label : '',
label : '', sortable: false,
sortable: false, cell : SeriesActionsCell
cell : SeriesActionsCell }
} ],
],
leftSideButtons: { leftSideButtons: {
type : 'default', type : 'default',
@ -130,25 +127,38 @@ define(
] ]
}, },
_showTable: function () { sortingOptions: {
this.currentView = new Backgrid.Grid({ type : 'sorting',
collection: SeriesCollection, storeState : false,
columns : this.columns, viewCollection: SeriesCollection,
className : 'table table-hover' items :
}); [
{
this._renderView(); title: 'Title',
this._fetchCollection(); name : 'title'
}, },
{
_showList: function () { title: 'Seasons',
this.currentView = new ListCollectionView(); name : 'seasonCount'
this._fetchCollection(); },
}, {
title: 'Quality',
_showPosters: function () { name : 'qualityProfileId'
this.currentView = new PosterCollectionView(); },
this._fetchCollection(); {
title: 'Network',
name : 'network'
},
{
title : 'Next Airing',
name : 'nextAiring',
sortValue : SeriesCollection.nextAiring
},
{
title: 'Episodes',
name : 'percentOfEpisodes'
}
]
}, },
initialize: function () { initialize: function () {
@ -156,39 +166,8 @@ define(
this.listenTo(SeriesCollection, 'sync', this._renderView); this.listenTo(SeriesCollection, 'sync', this._renderView);
this.listenTo(SeriesCollection, 'remove', this._renderView); this.listenTo(SeriesCollection, 'remove', this._renderView);
},
_renderView: function () { this.viewButtons = {
if (SeriesCollection.length === 0) {
this.seriesRegion.show(new EmptyView());
this.toolbar.close();
}
else {
this.currentView.collection = SeriesCollection;
this.seriesRegion.show(this.currentView);
this._showToolbar();
this._showFooter();
}
},
onShow: function () {
this._showToolbar();
this._renderView();
},
_fetchCollection: function () {
SeriesCollection.fetch();
},
_showToolbar: function () {
if (this.toolbar.currentView) {
return;
}
var viewButtons = {
type : 'radio', type : 'radio',
storeState : true, storeState : true,
menuKey : 'seriesViewMode', menuKey : 'seriesViewMode',
@ -218,12 +197,67 @@ define(
} }
] ]
}; };
},
_showTable: function () {
this.currentView = new Backgrid.Grid({
collection: SeriesCollection,
columns : this.columns,
className : 'table table-hover'
});
this._fetchCollection();
},
_showList: function () {
this.currentView = new ListCollectionView({ collection: SeriesCollection });
this._fetchCollection();
},
_showPosters: function () {
this.currentView = new PosterCollectionView({ collection: SeriesCollection });
this._fetchCollection();
},
_renderView: function () {
if (SeriesCollection.length === 0) {
this.seriesRegion.show(new EmptyView());
this.toolbar.close();
}
else {
this.seriesRegion.show(this.currentView);
this._showToolbar();
this._showFooter();
}
},
onShow: function () {
this._showToolbar();
this._renderView();
},
_fetchCollection: function () {
SeriesCollection.fetch();
},
_showToolbar: function () {
if (this.toolbar.currentView) {
return;
}
var rightButtons = [
this.viewButtons
];
rightButtons.splice(0, 0, this.sortingOptions);
this.toolbar.show(new ToolbarLayout({ this.toolbar.show(new ToolbarLayout({
right : right : rightButtons,
[
viewButtons
],
left : left :
[ [
this.leftSideButtons this.leftSideButtons

View File

@ -3,22 +3,25 @@ define(
[ [
'underscore', 'underscore',
'backbone', 'backbone',
'backbone.pageable',
'Series/SeriesModel', 'Series/SeriesModel',
'api!series' 'api!series',
], function (_, Backbone, SeriesModel, SeriesData) { 'Mixins/AsPersistedStateCollection',
var Collection = Backbone.Collection.extend({ 'moment'
], function (_, Backbone, PageableCollection, SeriesModel, SeriesData, AsPersistedStateCollection, Moment) {
var Collection = PageableCollection.extend({
url : window.NzbDrone.ApiRoot + '/series', url : window.NzbDrone.ApiRoot + '/series',
model: SeriesModel, model: SeriesModel,
tableName: 'series',
comparator: function (model) {
return model.get('title');
},
state: { state: {
sortKey: 'title', sortKey: 'title',
order : -1 order : -1,
pageSize: 1000
}, },
mode: 'client',
save: function () { save: function () {
var self = this; var self = this;
@ -31,7 +34,7 @@ define(
toJSON: function() toJSON: function()
{ {
return self.filter(function (model) { return self.filter(function (model) {
return model.hasChanged(); return model.edited;
}); });
} }
}); });
@ -42,9 +45,23 @@ define(
}); });
return proxy.save(); return proxy.save();
},
//Sorters
nextAiring: function (model, attr) {
var nextAiring = model.get(attr);
if (!nextAiring) {
return Number.MAX_VALUE;
}
return Moment(nextAiring).unix();
} }
}); });
var collection = new Collection(SeriesData); var MixedIn = AsPersistedStateCollection.call(Collection);
var collection = new MixedIn(SeriesData);
collection.initialSort();
return collection; return collection;
}); });

View File

@ -17,7 +17,6 @@ define(
this.route('series/:query', this.seriesDetails); this.route('series/:query', this.seriesDetails);
}, },
series: function () { series: function () {
this.setTitle('NzbDrone'); this.setTitle('NzbDrone');
AppLayout.mainRegion.show(new SeriesIndexLayout()); AppLayout.mainRegion.show(new SeriesIndexLayout());

View File

@ -6,8 +6,9 @@ define(
'Settings/Indexers/ItemView', 'Settings/Indexers/ItemView',
'Settings/Indexers/EditView', 'Settings/Indexers/EditView',
'Settings/Indexers/Collection', 'Settings/Indexers/Collection',
'System/StatusModel',
'underscore' 'underscore'
], function (AppLayout, Marionette, IndexerItemView, IndexerEditView, IndexerCollection, _) { ], function (AppLayout, Marionette, IndexerItemView, IndexerEditView, IndexerCollection, StatusModel, _) {
return Marionette.CompositeView.extend({ return Marionette.CompositeView.extend({
itemView : IndexerItemView, itemView : IndexerItemView,
itemViewContainer: '#x-indexers', itemViewContainer: '#x-indexers',
@ -29,10 +30,10 @@ define(
var self = this; var self = this;
//TODO: Is there a better way to deal with changing URLs? //TODO: Is there a better way to deal with changing URLs?
var schemaCollection = new IndexerCollection(); var schemaCollection = new IndexerCollection();
schemaCollection.url = '/api/indexer/schema'; schemaCollection.url = StatusModel.get('urlBase') + '/api/indexer/schema';
schemaCollection.fetch({ schemaCollection.fetch({
success: function (collection) { success: function (collection) {
collection.url = '/api/indexer'; collection.url = StatusModel.get('urlBase') + '/api/indexer';
var model = _.first(collection.models); var model = _.first(collection.models);
model.set({ model.set({

View File

@ -2,15 +2,16 @@
define([ define([
'AppLayout', 'AppLayout',
'Settings/Notifications/Collection', 'Settings/Notifications/Collection',
'Settings/Notifications/AddView' 'Settings/Notifications/AddView',
], function (AppLayout, NotificationCollection, AddSelectionNotificationView) { 'System/StatusModel'
], function (AppLayout, NotificationCollection, AddSelectionNotificationView, StatusModel) {
return ({ return ({
open: function (collection) { open: function (collection) {
var schemaCollection = new NotificationCollection(); var schemaCollection = new NotificationCollection();
schemaCollection.url = '/api/notification/schema'; schemaCollection.url = StatusModel.get('urlBase') + '/api/notification/schema';
schemaCollection.fetch(); schemaCollection.fetch();
schemaCollection.url = '/api/notification'; schemaCollection.url = StatusModel.get('urlBase') + '/api/notification';
var view = new AddSelectionNotificationView({ collection: schemaCollection, notificationCollection: collection}); var view = new AddSelectionNotificationView({ collection: schemaCollection, notificationCollection: collection});
AppLayout.modalRegion.show(view); AppLayout.modalRegion.show(view);

View File

@ -1,66 +0,0 @@
'use strict';
define(
[
'backgrid',
'Shared/Grid/HeaderCell'
], function (Backgrid, NzbDroneHeaderCell) {
Backgrid.DateHeaderCell = 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) {
if (!leftVal && !rightVal) {
return 0;
}
if (!leftVal) {
return -1;
}
if (!rightVal) {
return 1;
}
if (leftVal === rightVal) {
return 0;
}
if (leftVal > rightVal) {
return -1;
}
return 1;
}
});
return Backgrid.DateHeaderCell;
});

View File

@ -6,95 +6,134 @@ define(
], function (Backgrid) { ], function (Backgrid) {
Backgrid.NzbDroneHeaderCell = Backgrid.HeaderCell.extend({ Backgrid.NzbDroneHeaderCell = Backgrid.HeaderCell.extend({
events: { events: {
'click': 'onClick' 'click': 'onClick'
}, },
_originalInit: Backgrid.HeaderCell.prototype.initialize,
initialize: function (options) {
this._originalInit.call(this, options);
this.listenTo(this.collection, 'drone:sort', this.render);
},
render: function () { render: function () {
this.$el.empty(); this.$el.empty();
this.$el.append(this.column.get('label')); this.$el.append(this.column.get('label'));
if (this.column.get('sortable')) { var column = this.column;
var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection);
if (sortable)
{
this.$el.addClass('sortable'); this.$el.addClass('sortable');
this.$el.append(' <i class="pull-right"></i>'); this.$el.append(' <i class="pull-right"></i>');
}
if (this.collection.state) { //Do we need this?
var sortKey = this.collection.state.sortKey; this.$el.addClass(column.get('name'));
var sortDir = this._convertIntToDirection(this.collection.state.order);
if (sortKey === this.column.get('name')) { this.delegateEvents();
this.$el.children('i').addClass(this._convertDirectionToIcon(sortDir)); this.direction(column.get('direction'));
this._direction = sortDir;
} if (this.collection.state) {
var key = this.collection.state.sortKey;
var order = this.collection.state.order;
if (key === this.column.get('name')) {
this._setSortIcon(order);
}
else {
this._removeSortIcon();
} }
} }
this.delegateEvents();
return this; return this;
}, },
direction: function (dir) { direction: function (dir) {
this.$el.children('i').removeClass('icon-sort-up icon-sort-down');
if (arguments.length) { if (arguments.length) {
if (this._direction) { if (dir)
this.$el.children('i').removeClass(this._convertDirectionToIcon(this._direction)); {
this._setSortIcon(dir);
} }
if (dir) {
this.$el.children('i').addClass(this._convertDirectionToIcon(dir)); this.column.set('direction', dir);
}
this._direction = dir;
} }
return this._direction; var columnDirection = this.column.get('direction');
if (!columnDirection && this.collection.state) {
var key = this.collection.state.sortKey;
var order = this.collection.state.order;
if (key === this.column.get('name')) {
columnDirection = order;
}
}
return columnDirection;
}, },
onClick: function (e) { onClick: function (e) {
e.preventDefault(); e.preventDefault();
var columnName = this.column.get('name'); var collection = this.collection;
var event = 'backgrid:sort';
if (this.column.get('sortable')) { function toggleSort(header, col) {
if (this.direction() === 'ascending') { collection.state.sortKey = col.get('name');
this.sort(columnName, 'descending', function (left, right) { var direction = header.direction();
var leftVal = left.get(columnName); if (direction === 'ascending' || direction === -1)
var rightVal = right.get(columnName); {
if (leftVal === rightVal) { collection.state.order = 'descending';
return 0; collection.trigger(event, col, 'descending');
}
else if (leftVal > rightVal) {
return -1;
}
return 1;
});
} }
else { else
this.sort(columnName, 'ascending', function (left, right) { {
var leftVal = left.get(columnName); collection.state.order = 'ascending';
var rightVal = right.get(columnName); collection.trigger(event, col, 'ascending');
if (leftVal === rightVal) {
return 0;
}
else if (leftVal < rightVal) {
return -1;
}
return 1;
});
} }
} }
var column = this.column;
var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection);
if (sortable) {
toggleSort(this, column);
}
},
_resetCellDirection: function (columnToSort, direction) {
if (columnToSort !== this.column)
{
this.direction(null);
}
else
{
this.direction(direction);
}
}, },
_convertDirectionToIcon: function (dir) { _convertDirectionToIcon: function (dir) {
if (dir === 'ascending') { if (dir === 'ascending' || dir === -1) {
return 'icon-sort-up'; return 'icon-sort-up';
} }
return 'icon-sort-down'; return 'icon-sort-down';
}, },
_convertIntToDirection: function (dir) { _setSortIcon: function (dir) {
if (dir === '-1') { this._removeSortIcon();
return 'ascending'; this.$el.children('i').addClass(this._convertDirectionToIcon(dir));
} },
return 'descending'; _removeSortIcon: function () {
this.$el.children('i').removeClass('icon-sort-up icon-sort-down');
} }
}); });

View File

@ -1,3 +1,4 @@
<div> <div>
<img src="/Content/Images/404.png" style="height:400px; margin-top: 50px"/> <img src="{{UrlBase}}/Content/Images/404.png" style="height:400px; margin-top: 50px"/>
</div> </div>

View File

@ -30,7 +30,7 @@ define(
var tryingToReconnect = false; var tryingToReconnect = false;
var messengerId = 'signalR'; var messengerId = 'signalR';
this.signalRconnection = $.connection('/signalr'); this.signalRconnection = $.connection(StatusModel.get('urlBase') + '/signalr');
this.signalRconnection.stateChanged(function (change) { this.signalRconnection.stateChanged(function (change) {
console.debug('SignalR: [{0}]'.format(getStatus(change.newState))); console.debug('SignalR: [{0}]'.format(getStatus(change.newState)));

View File

@ -1,13 +1,29 @@
'use strict'; 'use strict';
define( define(
[ [
'underscore',
'backbone' 'backbone'
], function (Backbone) { ], function (_, Backbone) {
return Backbone.Model.extend({ return Backbone.Model.extend({
defaults: { defaults: {
'target' : '/nzbdrone/route', 'target' : '/nzbdrone/route',
'title' : '', 'title' : '',
'active' : false, 'active' : false,
'tooltip': undefined } 'tooltip': undefined
},
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);
};
}
}); });
}); });

View File

@ -13,7 +13,6 @@ define(
'click': 'onClick' 'click': 'onClick'
}, },
initialize: function () { initialize: function () {
this.storageKey = this.model.get('menuKey') + ':' + this.model.get('key'); this.storageKey = this.model.get('menuKey') + ':' + this.model.get('key');
@ -53,7 +52,6 @@ define(
callback.call(this.model.ownerContext); callback.call(this.model.ownerContext);
} }
} }
}); });
}); });

View File

@ -0,0 +1,87 @@
'use strict';
define(
[
'backbone.pageable',
'marionette',
'Shared/Toolbar/Sorting/SortingButtonView'
], function (PageableCollection, Marionette, ButtonView) {
return Marionette.CompositeView.extend({
itemView : ButtonView,
template : 'Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate',
itemViewContainer: '.dropdown-menu',
initialize: function (options) {
this.viewCollection = options.viewCollection;
this.listenTo(this.viewCollection, 'drone:sort', this.sort);
},
itemViewOptions: function () {
return {
viewCollection: this.viewCollection
};
},
sort: function (sortModel, sortDirection) {
var collection = this.viewCollection;
var order;
if (sortDirection === 'ascending') {
order = -1;
}
else if (sortDirection === 'descending') {
order = 1;
}
else {
order = null;
}
var comparator = this.makeComparator(sortModel.get('name'), order,
order ?
sortModel.sortValue() :
function (model) {
return model.cid;
});
if (PageableCollection &&
collection instanceof PageableCollection) {
collection.setSorting(order && sortModel.get('name'), order,
{sortValue: sortModel.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;
};
}
});
});

View File

@ -0,0 +1,8 @@
<div class="btn-group sorting-buttons">
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
Sort <span class="caret"></span>
</a>
<ul class="dropdown-menu">
</ul>
</div>

View File

@ -0,0 +1,84 @@
'use strict';
define(
[
'backbone',
'marionette',
'underscore'
], function (Backbone, Marionette, _) {
return Marionette.ItemView.extend({
template : 'Shared/Toolbar/Sorting/SortingButtonViewTemplate',
tagName : 'li',
ui: {
icon: 'i'
},
events: {
'click': 'onClick'
},
initialize: function (options) {
this.viewCollection = options.viewCollection;
this.listenTo(this.viewCollection, 'drone:sort', this.render);
this.listenTo(this.viewCollection, 'backgrid:sort', this.render);
},
onRender: function () {
if (this.viewCollection.state) {
var key = this.viewCollection.state.sortKey;
var order = this.viewCollection.state.order;
if (key === this.model.get('name')) {
this._setSortIcon(order);
}
else {
this._removeSortIcon();
}
}
},
onClick: function (e) {
e.preventDefault();
var collection = this.viewCollection;
var event = 'drone:sort';
collection.state.sortKey = this.model.get('name');
var direction = collection.state.order;
if (direction === 'ascending' || direction === -1)
{
collection.state.order = 'descending';
collection.trigger(event, this.model, 'descending');
}
else
{
collection.state.order = 'ascending';
collection.trigger(event, this.model, 'ascending');
}
},
_convertDirectionToIcon: function (dir) {
if (dir === 'ascending' || dir === -1) {
return 'icon-sort-up';
}
return 'icon-sort-down';
},
_setSortIcon: function (dir) {
this._removeSortIcon();
this.ui.icon.addClass(this._convertDirectionToIcon(dir));
},
_removeSortIcon: function () {
this.ui.icon.removeClass('icon-sort-up icon-sort-down');
}
});
});

View File

@ -0,0 +1,4 @@
<a href="#">
<span class="sorting-title">{{title}}</span>
<i class=""></i>
</a>

View File

@ -6,8 +6,9 @@ define(
'Shared/Toolbar/ButtonModel', 'Shared/Toolbar/ButtonModel',
'Shared/Toolbar/Radio/RadioButtonCollectionView', 'Shared/Toolbar/Radio/RadioButtonCollectionView',
'Shared/Toolbar/Button/ButtonCollectionView', 'Shared/Toolbar/Button/ButtonCollectionView',
'Shared/Toolbar/Sorting/SortingButtonCollectionView',
'underscore' 'underscore'
], function (Marionette, ButtonCollection, ButtonModel, RadioButtonCollectionView, ButtonCollectionView,_) { ], function (Marionette, ButtonCollection, ButtonModel, RadioButtonCollectionView, ButtonCollectionView, SortingButtonCollectionView, _) {
return Marionette.Layout.extend({ return Marionette.Layout.extend({
template: 'Shared/Toolbar/ToolbarLayoutTemplate', template: 'Shared/Toolbar/ToolbarLayoutTemplate',
@ -78,6 +79,15 @@ define(
}); });
break; break;
} }
case 'sorting':
{
buttonGroupView = new SortingButtonCollectionView({
collection : groupCollection,
menu : buttonGroup,
viewCollection: buttonGroup.viewCollection
});
break;
}
default : default :
{ {
buttonGroupView = new ButtonCollectionView({ buttonGroupView = new ButtonCollectionView({

View File

@ -1,8 +1,8 @@
<div class="pull-left page-toolbar"> <div class="pull-left page-toolbar">
<div class="x-toolbar-left-1"/> <div class="toolbar-group x-toolbar-left-1"/>
<div class="x-toolbar-left-2"/> <div class="toolbar-group x-toolbar-left-2"/>
</div> </div>
<div class="pull-right page-toolbar"> <div class="pull-right page-toolbar">
<div class="x-toolbar-right-1"/> <div class="toolbar-group x-toolbar-right-1"/>
<div class="x-toolbar-right-2"/> <div class="toolbar-group x-toolbar-right-2"/>
</div> </div>

View File

@ -1,11 +1,12 @@
'use strict'; 'use strict';
define( define(
[ [
'backbone' 'backbone',
], function (Backbone) { 'System/StatusModel'
], function (Backbone, StatusModel) {
return Backbone.Model.extend({ return Backbone.Model.extend({
url: function () { url: function () {
return '/log/' + this.get('filename'); return StatusModel.get('urlBase') + '/log/' + this.get('filename');
}, },
parse: function (contents) { parse: function (contents) {

View File

@ -1,15 +1,16 @@
'use strict'; 'use strict';
define( define(
[ [
'Cells/NzbDroneCell' 'Cells/NzbDroneCell',
], function (NzbDroneCell) { 'System/StatusModel'
], function (NzbDroneCell, StatusModel) {
return NzbDroneCell.extend({ return NzbDroneCell.extend({
className: 'download-log-cell', className: 'download-log-cell',
render: function () { render: function () {
this.$el.empty(); this.$el.empty();
this.$el.html('<a href="/log/{0}" class="no-router" target="_blank">Download</a>'.format(this.cellValue)); this.$el.html('<a href="{0}/log/{1}" class="no-router" target="_blank">Download</a>'.format(StatusModel.get('urlBase'), this.cellValue));
return this; return this;
} }

View File

@ -1,10 +1,16 @@
'use strict'; 'use strict';
define(['backbone.pageable', 'System/Logs/LogsModel'], define(
function (PagableCollection, LogsModel) { [
return PagableCollection.extend({ 'backbone.pageable',
'System/Logs/LogsModel',
'Mixins/AsPersistedStateCollection'
],
function (PagableCollection, LogsModel, AsPersistedStateCollection) {
var collection = PagableCollection.extend({
url : window.NzbDrone.ApiRoot + '/log', url : window.NzbDrone.ApiRoot + '/log',
model: LogsModel, model: LogsModel,
tableName: 'logs',
state: { state: {
pageSize: 50, pageSize: 50,
@ -36,4 +42,6 @@ define(['backbone.pageable', 'System/Logs/LogsModel'],
return resp; return resp;
} }
}); });
return AsPersistedStateCollection.call(collection);
}); });

View File

@ -165,7 +165,8 @@ require.config({
renderable: true, renderable: true,
formatter : undefined, formatter : undefined,
cell : undefined, cell : undefined,
headerCell: 'NzbDrone' headerCell: 'NzbDrone',
sortType : 'toggle'
}; };
}); });
@ -231,7 +232,7 @@ define(
}); });
app.addInitializer(function () { app.addInitializer(function () {
Backbone.history.start({ pushState: true }); Backbone.history.start({ pushState: true, root: serverStatusModel.get('urlBase') });
RouteBinder.bind(); RouteBinder.bind();
AppLayout.navbarRegion.show(new NavbarView()); AppLayout.navbarRegion.show(new NavbarView());
$('body').addClass('started'); $('body').addClass('started');

View File

@ -65,7 +65,7 @@
<div id="errors"></div> <div id="errors"></div>
<script type="text/javascript"> <script type="text/javascript">
window.NzbDrone = { window.NzbDrone = {
ApiRoot: '/api', ApiRoot: 'API_ROOT',
ApiKey : 'API_KEY', ApiKey : 'API_KEY',
Version: 'APP_VERSION' Version: 'APP_VERSION'
}; };

View File

@ -2,8 +2,9 @@
define( define(
[ [
'backbone', 'backbone',
'jquery' 'jquery',
], function (Backbone,$) { 'System/StatusModel'
], function (Backbone, $, StatusModel) {
//This module will automatically route all relative links through backbone router rather than //This module will automatically route all relative links through backbone router rather than
//causing links to reload pages. //causing links to reload pages.
@ -45,7 +46,9 @@ define(
if (!href.startsWith('http')) { if (!href.startsWith('http')) {
Backbone.history.navigate(href, { trigger: true }); var relativeHref = href.replace(StatusModel.get('urlBase'), '');
Backbone.history.navigate(relativeHref, { trigger: true });
} }
else { else {