From c90791b266386c3319184614efd44ed041f5edc2 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 18 Jan 2014 12:44:36 +0100 Subject: [PATCH] Quality Order can now be change on per Quality Profile. Quality Title used in Renaming can now be changed by the user. Both options require Advanced Settings to be enabled. --- .../MappingTests/ResourceMappingFixture.cs | 1 - .../EpisodeFiles/EpisodeFileResource.cs | 1 + src/NzbDrone.Api/History/HistoryResource.cs | 1 + src/NzbDrone.Api/Indexers/ReleaseResource.cs | 1 + src/NzbDrone.Api/NzbDrone.Api.csproj | 4 +- .../Qualities/QualityDefinitionModule.cs | 38 + .../Qualities/QualityDefinitionResource.cs | 18 + .../Qualities/QualityProfileModule.cs | 40 +- .../Qualities/QualityProfileResource.cs | 3 +- .../Qualities/QualityProfileSchemaModule.cs | 37 +- .../Qualities/QualitySizeModule.cs | 38 - .../Qualities/QualitySizeResource.cs | 13 - src/NzbDrone.Api/Queue/QueueResource.cs | 1 + .../Datastore/DatabaseRelationshipFixture.cs | 1 + .../AlterFixture.cs | 12 +- .../AcceptableSizeSpecificationFixture.cs | 38 +- .../CutoffSpecificationFixture.cs | 10 +- .../HistorySpecificationFixture.cs | 6 +- .../NotInQueueSpecificationFixture.cs | 4 +- .../QualityUpgradeSpecificationFixture.cs | 19 +- .../UpgradeDiskSpecificationFixture.cs | 2 +- .../DownloadApprovedFixture.cs | 4 + .../GetQualifiedReportsFixture.cs | 4 + .../Download/DownloadServiceFixture.cs | 1 - .../DownloadedEpisodesImportServiceFixture.cs | 14 + .../ImportDecisionMakerFixture.cs | 15 +- .../UpgradeSpecificationFixture.cs | 6 +- .../ImportApprovedEpisodesFixture.cs | 1 + .../NzbDrone.Core.Test.csproj | 6 +- .../OrganizerTests/GetNewFilenameFixture.cs | 5 + .../ParserTests/QualityParserFixture.cs | 5 +- ... => QualityDefinitionRepositoryFixture.cs} | 16 +- .../QualityDefinitionServiceFixture.cs | 79 + .../Qualities/QualityFixture.cs | 92 +- .../Qualities/QualityModelComparerFixture.cs | 117 + .../QualityProfileRepositoryFixture.cs | 1 - .../Qualities/QualitySizeServiceFixture.cs | 39 - .../TvTests/QualityModelFixture.cs | 125 - src/NzbDrone.Core/Blacklisting/Blacklist.cs | 1 + .../Converters/QualityIntConverter.cs | 4 +- .../Converters/QualityListConverter.cs | 52 + .../Converters/QualityModelConverter.cs | 55 + .../036_update_with_quality_converters.cs | 97 + .../037_add_configurable_qualities.cs | 63 + .../Migration/Framework/MigrationExtension.cs | 6 + src/NzbDrone.Core/Datastore/TableMapping.cs | 4 +- .../QualityUpgradableSpecification.cs | 15 +- .../AcceptableSizeSpecification.cs | 12 +- .../Specifications/NotInQueueSpecification.cs | 4 +- .../RssSync/HistorySpecification.cs | 2 +- .../UpgradeDiskSpecification.cs | 2 +- .../Download/DownloadApprovedReports.cs | 16 +- .../Download/DownloadFailedEvent.cs | 1 + src/NzbDrone.Core/History/History.cs | 1 + .../History/HistoryRepository.cs | 1 + src/NzbDrone.Core/History/HistoryService.cs | 1 + .../DownloadedEpisodesImportService.cs | 1 + src/NzbDrone.Core/MediaFiles/EpisodeFile.cs | 1 + .../EpisodeImport/ImportApprovedEpisodes.cs | 9 +- .../EpisodeImport/ImportDecisionMaker.cs | 3 +- .../Specifications/UpgradeSpecification.cs | 5 +- .../Notifications/NotificationService.cs | 1 + src/NzbDrone.Core/NzbDrone.Core.csproj | 13 +- .../Organizer/FileNameBuilder.cs | 22 +- .../Parser/Model/LocalEpisode.cs | 3 +- .../Parser/Model/ParsedEpisodeInfo.cs | 1 + src/NzbDrone.Core/Qualities/Quality.cs | 181 +- .../Qualities/QualityDefinition.cs | 33 + .../Qualities/QualityDefinitionRepository.cs | 33 + .../Qualities/QualityDefinitionService.cs | 101 + src/NzbDrone.Core/Qualities/QualityModel.cs | 73 + .../Qualities/QualityModelComparer.cs | 67 + src/NzbDrone.Core/Qualities/QualitySize.cs | 18 - .../Qualities/QualitySizeRepository.cs | 33 - .../Qualities/QualitySizeService.cs | 64 - src/NzbDrone.Core/Queue/Queue.cs | 1 + src/NzbDrone.Core/Tv/QualityModel.cs | 120 - src/UI/JsLibraries/backbone.collectionview.js | 1072 +++++ src/UI/JsLibraries/jquery-ui.js | 4233 +++++++++++++++++ src/UI/Quality/QualityDefinitionCollection.js | 11 + ...SizeModel.js => QualityDefinitionModel.js} | 6 +- src/UI/Quality/QualitySizeCollection.js | 11 - .../QualityDefinitionCollectionTemplate.html | 16 + .../QualityDefinitionCollectionView.js | 17 + .../Definition/QualityDefinitionTemplate.html | 31 + .../Definition/QualityDefinitionView.js | 86 + .../Profile/EditQualityProfileItemView.js | 9 + .../EditQualityProfileItemViewTemplate.html | 5 + .../Profile/EditQualityProfileTemplate.html | 24 +- .../Quality/Profile/EditQualityProfileView.js | 108 +- src/UI/Settings/Quality/QualityLayout.js | 16 +- .../Quality/QualityLayoutTemplate.html | 2 +- .../Size/QualitySizeCollectionTemplate.html | 4 - .../Quality/Size/QualitySizeCollectionView.js | 9 - .../Quality/Size/QualitySizeTemplate.html | 20 - .../Settings/Quality/Size/QualitySizeView.js | 61 - src/UI/Settings/Quality/quality.less | 137 +- src/UI/Settings/SettingsLayout.js | 10 +- src/UI/app.js | 62 +- 99 files changed, 6898 insertions(+), 990 deletions(-) create mode 100644 src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs create mode 100644 src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs delete mode 100644 src/NzbDrone.Api/Qualities/QualitySizeModule.cs delete mode 100644 src/NzbDrone.Api/Qualities/QualitySizeResource.cs rename src/NzbDrone.Core.Test/Qualities/{QualitySizeRepositoryFixture.cs => QualityDefinitionRepositoryFixture.cs} (57%) create mode 100644 src/NzbDrone.Core.Test/Qualities/QualityDefinitionServiceFixture.cs create mode 100644 src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs delete mode 100644 src/NzbDrone.Core.Test/Qualities/QualitySizeServiceFixture.cs delete mode 100644 src/NzbDrone.Core.Test/TvTests/QualityModelFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Converters/QualityListConverter.cs create mode 100644 src/NzbDrone.Core/Datastore/Converters/QualityModelConverter.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs create mode 100644 src/NzbDrone.Core/Qualities/QualityDefinition.cs create mode 100644 src/NzbDrone.Core/Qualities/QualityDefinitionRepository.cs create mode 100644 src/NzbDrone.Core/Qualities/QualityDefinitionService.cs create mode 100644 src/NzbDrone.Core/Qualities/QualityModel.cs create mode 100644 src/NzbDrone.Core/Qualities/QualityModelComparer.cs delete mode 100644 src/NzbDrone.Core/Qualities/QualitySize.cs delete mode 100644 src/NzbDrone.Core/Qualities/QualitySizeRepository.cs delete mode 100644 src/NzbDrone.Core/Qualities/QualitySizeService.cs delete mode 100644 src/NzbDrone.Core/Tv/QualityModel.cs create mode 100644 src/UI/JsLibraries/backbone.collectionview.js create mode 100644 src/UI/JsLibraries/jquery-ui.js create mode 100644 src/UI/Quality/QualityDefinitionCollection.js rename src/UI/Quality/{QualitySizeModel.js => QualityDefinitionModel.js} (71%) delete mode 100644 src/UI/Quality/QualitySizeCollection.js create mode 100644 src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.html create mode 100644 src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js create mode 100644 src/UI/Settings/Quality/Definition/QualityDefinitionTemplate.html create mode 100644 src/UI/Settings/Quality/Definition/QualityDefinitionView.js create mode 100644 src/UI/Settings/Quality/Profile/EditQualityProfileItemView.js create mode 100644 src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html delete mode 100644 src/UI/Settings/Quality/Size/QualitySizeCollectionTemplate.html delete mode 100644 src/UI/Settings/Quality/Size/QualitySizeCollectionView.js delete mode 100644 src/UI/Settings/Quality/Size/QualitySizeTemplate.html delete mode 100644 src/UI/Settings/Quality/Size/QualitySizeView.js diff --git a/src/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs b/src/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs index e03e02c35..d31edcac6 100644 --- a/src/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs +++ b/src/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs @@ -107,7 +107,6 @@ namespace NzbDrone.Api.Test.MappingTests [Test] public void should_map_qualityprofile() { - var profileResource = new QualityProfileResource { Allowed = Builder.CreateListOfSize(1).Build().ToList(), diff --git a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs index f3bd37368..311253f3f 100644 --- a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs +++ b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs @@ -1,5 +1,6 @@ using System; using NzbDrone.Api.REST; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Api.EpisodeFiles diff --git a/src/NzbDrone.Api/History/HistoryResource.cs b/src/NzbDrone.Api/History/HistoryResource.cs index fe572a25c..e95330c52 100644 --- a/src/NzbDrone.Api/History/HistoryResource.cs +++ b/src/NzbDrone.Api/History/HistoryResource.cs @@ -4,6 +4,7 @@ using NzbDrone.Api.Episodes; using NzbDrone.Api.REST; using NzbDrone.Api.Series; using NzbDrone.Core.History; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Api.History diff --git a/src/NzbDrone.Api/Indexers/ReleaseResource.cs b/src/NzbDrone.Api/Indexers/ReleaseResource.cs index fda9ef67a..03955b5f8 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseResource.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseResource.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using NzbDrone.Api.REST; using NzbDrone.Core.Parser; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Api.Indexers diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 00f5f96e3..f6425304a 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -166,8 +166,8 @@ - - + + diff --git a/src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs b/src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs new file mode 100644 index 000000000..d675d54bc --- /dev/null +++ b/src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using NzbDrone.Core.Qualities; +using NzbDrone.Api.Mapping; + +namespace NzbDrone.Api.Qualities +{ + public class QualityDefinitionModule : NzbDroneRestModule + { + private readonly IQualityDefinitionService _qualityDefinitionService; + + public QualityDefinitionModule(IQualityDefinitionService qualityDefinitionService) + { + _qualityDefinitionService = qualityDefinitionService; + + GetResourceAll = GetAll; + + GetResourceById = GetById; + + UpdateResource = Update; + } + + private void Update(QualityDefinitionResource resource) + { + var model = resource.InjectTo(); + _qualityDefinitionService.Update(model); + } + + private QualityDefinitionResource GetById(int id) + { + return _qualityDefinitionService.Get((Quality)id).InjectTo(); + } + + private List GetAll() + { + return ToListResource(_qualityDefinitionService.All); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs b/src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs new file mode 100644 index 000000000..8750a6c33 --- /dev/null +++ b/src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs @@ -0,0 +1,18 @@ +using System; +using NzbDrone.Api.REST; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Api.Qualities +{ + public class QualityDefinitionResource : RestResource + { + public Quality Quality { get; set; } + + public String Title { get; set; } + + public Int32 Weight { get; set; } + + public Int32 MinSize { get; set; } + public Int32 MaxSize { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualityProfileModule.cs b/src/NzbDrone.Api/Qualities/QualityProfileModule.cs index a08fdb12c..250bddd08 100644 --- a/src/NzbDrone.Api/Qualities/QualityProfileModule.cs +++ b/src/NzbDrone.Api/Qualities/QualityProfileModule.cs @@ -9,11 +9,14 @@ namespace NzbDrone.Api.Qualities public class QualityProfileModule : NzbDroneRestModule { private readonly IQualityProfileService _qualityProfileService; + private readonly IQualityDefinitionService _qualityDefinitionService; - public QualityProfileModule(IQualityProfileService qualityProfileService) + public QualityProfileModule(IQualityProfileService qualityProfileService, + IQualityDefinitionService qualityDefinitionService) : base("/qualityprofiles") { _qualityProfileService = qualityProfileService; + _qualityDefinitionService = qualityDefinitionService; SharedValidator.RuleFor(c => c.Name).NotEmpty(); SharedValidator.RuleFor(c => c.Cutoff).NotNull(); @@ -44,38 +47,47 @@ namespace NzbDrone.Api.Qualities private void Update(QualityProfileResource resource) { - var model = resource.InjectTo(); + var model = _qualityProfileService.Get(resource.Id); + model.Name = resource.Name; + model.Cutoff = (Quality)resource.Cutoff.Id; + model.Allowed = resource.Allowed.Select(p => (Quality)p.Id).ToList(); _qualityProfileService.Update(model); } private QualityProfileResource GetById(int id) { - return QualityToResource(_qualityProfileService.Get(id)); + return MapToResource(_qualityProfileService.Get(id)); } private List GetAll() { - var allProfiles = _qualityProfileService.All(); - - - var profiles = allProfiles.Select(QualityToResource).ToList(); + var profiles = _qualityProfileService.All().Select(MapToResource).ToList(); return profiles; } - private static QualityProfileResource QualityToResource(QualityProfile profile) + private QualityProfileResource MapToResource(QualityProfile profile) { return new QualityProfileResource { - Cutoff = profile.Cutoff.InjectTo(), - Available = Quality.All() - .Where(c => !profile.Allowed.Any(q => c.Id == q.Id)) - .InjectTo>(), - - Allowed = profile.Allowed.InjectTo>(), + Cutoff = MapToResource(_qualityDefinitionService.Get(profile.Cutoff)), + Available = _qualityDefinitionService.All() + .Where(c => !profile.Allowed.Any(q => c.Quality == q)) + .Select(MapToResource).ToList(), + Allowed = profile.Allowed.Select(_qualityDefinitionService.Get).Select(MapToResource).ToList(), Name = profile.Name, Id = profile.Id }; } + + private QualityResource MapToResource(QualityDefinition config) + { + return new QualityResource + { + Id = config.Quality.Id, + Name = config.Quality.Name, + Weight = config.Weight + }; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualityProfileResource.cs b/src/NzbDrone.Api/Qualities/QualityProfileResource.cs index 25e6a9108..73ba94268 100644 --- a/src/NzbDrone.Api/Qualities/QualityProfileResource.cs +++ b/src/NzbDrone.Api/Qualities/QualityProfileResource.cs @@ -14,7 +14,8 @@ namespace NzbDrone.Api.Qualities public class QualityResource : RestResource { - public Int32 Weight { get; set; } public String Name { get; set; } + + public Int32 Weight { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualityProfileSchemaModule.cs b/src/NzbDrone.Api/Qualities/QualityProfileSchemaModule.cs index 06d941a2a..e4e23ca02 100644 --- a/src/NzbDrone.Api/Qualities/QualityProfileSchemaModule.cs +++ b/src/NzbDrone.Api/Qualities/QualityProfileSchemaModule.cs @@ -7,9 +7,13 @@ namespace NzbDrone.Api.Qualities { public class QualityProfileSchemaModule : NzbDroneRestModule { - public QualityProfileSchemaModule() + private readonly IQualityDefinitionService _qualityDefinitionService; + + public QualityProfileSchemaModule(IQualityDefinitionService qualityDefinitionService) : base("/qualityprofiles/schema") { + _qualityDefinitionService = qualityDefinitionService; + GetResourceAll = GetAll; } @@ -19,21 +23,30 @@ namespace NzbDrone.Api.Qualities profile.Cutoff = Quality.Unknown; profile.Allowed = new List(); - return new List{ QualityToResource(profile)}; + return new List { QualityToResource(profile) }; } - private static QualityProfileResource QualityToResource(QualityProfile profile) + private QualityProfileResource QualityToResource(QualityProfile profile) { return new QualityProfileResource - { - Available = Quality.All() - .Where(c => !profile.Allowed.Any(q => c.Id == q.Id)) - .InjectTo>(), - - Allowed = profile.Allowed.InjectTo>(), - Name = profile.Name, - Id = profile.Id - }; + { + Cutoff = QualityToResource(_qualityDefinitionService.Get(profile.Cutoff)), + Available = _qualityDefinitionService.All().Select(QualityToResource).ToList(), + Allowed = profile.Allowed.Select(_qualityDefinitionService.Get).Select(QualityToResource).ToList(), + Name = profile.Name, + Id = profile.Id + }; + } + + + private QualityResource QualityToResource(QualityDefinition config) + { + return new QualityResource + { + Id = config.Quality.Id, + Name = config.Quality.Name, + Weight = config.Weight + }; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualitySizeModule.cs b/src/NzbDrone.Api/Qualities/QualitySizeModule.cs deleted file mode 100644 index 206bc9c51..000000000 --- a/src/NzbDrone.Api/Qualities/QualitySizeModule.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Qualities; -using NzbDrone.Api.Mapping; - -namespace NzbDrone.Api.Qualities -{ - public class QualitySizeModule : NzbDroneRestModule - { - private readonly IQualitySizeService _qualityTypeProvider; - - public QualitySizeModule(IQualitySizeService qualityTypeProvider) - { - _qualityTypeProvider = qualityTypeProvider; - - GetResourceAll = GetAll; - - GetResourceById = GetById; - - UpdateResource = Update; - } - - private void Update(QualitySizeResource resource) - { - var model = resource.InjectTo(); - _qualityTypeProvider.Update(model); - } - - private QualitySizeResource GetById(int id) - { - return _qualityTypeProvider.Get(id).InjectTo(); - } - - private List GetAll() - { - return ToListResource(_qualityTypeProvider.All); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualitySizeResource.cs b/src/NzbDrone.Api/Qualities/QualitySizeResource.cs deleted file mode 100644 index 2d428f575..000000000 --- a/src/NzbDrone.Api/Qualities/QualitySizeResource.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using NzbDrone.Api.REST; - -namespace NzbDrone.Api.Qualities -{ - public class QualitySizeResource : RestResource - { - public Int32 QualityId { get; set; } - public String Name { get; set; } - public Int32 MinSize { get; set; } - public Int32 MaxSize { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Queue/QueueResource.cs b/src/NzbDrone.Api/Queue/QueueResource.cs index 94a7f07fc..0adfe1e79 100644 --- a/src/NzbDrone.Api/Queue/QueueResource.cs +++ b/src/NzbDrone.Api/Queue/QueueResource.cs @@ -1,5 +1,6 @@ using System; using NzbDrone.Api.REST; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Api.Queue diff --git a/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs b/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs index 75707c4e5..e852662e4 100644 --- a/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs @@ -75,6 +75,7 @@ namespace NzbDrone.Core.Test.Datastore public void one_to_one() { var episodeFile = Builder.CreateNew() + .With(c => c.Quality = new QualityModel()) .BuildNew(); Db.Insert(episodeFile); diff --git a/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/AlterFixture.cs b/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/AlterFixture.cs index aa07e1028..d93d0f262 100644 --- a/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/AlterFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/AlterFixture.cs @@ -81,25 +81,25 @@ namespace NzbDrone.Core.Test.Datastore.SQLiteMigrationHelperTests [Test] public void should_read_existing_indexes() { - var indexes = _subject.GetIndexes("QualitySizes"); + var indexes = _subject.GetIndexes("QualityDefinitions"); indexes.Should().NotBeEmpty(); indexes.Should().OnlyContain(c => c != null); indexes.Should().OnlyContain(c => !string.IsNullOrWhiteSpace(c.Column)); - indexes.Should().OnlyContain(c => c.Table == "QualitySizes"); + indexes.Should().OnlyContain(c => c.Table == "QualityDefinitions"); indexes.Should().OnlyContain(c => c.Unique); } [Test] public void should_add_indexes_when_creating_new_table() { - var columns = _subject.GetColumns("QualitySizes"); - var indexes = _subject.GetIndexes("QualitySizes"); + var columns = _subject.GetColumns("QualityDefinitions"); + var indexes = _subject.GetIndexes("QualityDefinitions"); - _subject.CreateTable("QualityB", columns.Values, indexes); + _subject.CreateTable("QualityDefinitionsB", columns.Values, indexes); - var newIndexes = _subject.GetIndexes("QualityB"); + var newIndexes = _subject.GetIndexes("QualityDefinitionsB"); newIndexes.Should().HaveSameCount(indexes); newIndexes.Select(c=>c.Column).Should().BeEquivalentTo(indexes.Select(c=>c.Column)); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs index 5f511e3dc..b586622ea 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private RemoteEpisode parseResultSingle; private Series series30minutes; private Series series60minutes; - private QualitySize qualityType; + private QualityDefinition qualityType; [SetUp] public void Setup() @@ -47,10 +47,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(c => c.Runtime = 60) .Build(); - qualityType = Builder.CreateNew() + qualityType = Builder.CreateNew() .With(q => q.MinSize = 0) .With(q => q.MaxSize = 10) - .With(q => q.QualityId = 1) + .With(q => q.Quality = Quality.SDTV) .Build(); } @@ -61,7 +61,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Series = series30minutes; parseResultSingle.Release.Size = 184572800; - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -80,7 +80,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Series = series60minutes; parseResultSingle.Release.Size = 368572800; - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -99,7 +99,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Series = series30minutes; parseResultSingle.Release.Size = 1.Gigabytes(); - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -118,7 +118,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Series = series60minutes; parseResultSingle.Release.Size = 1.Gigabytes(); - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -135,7 +135,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultMulti.Series = series30minutes; parseResultMulti.Release.Size = 184572800; - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -154,7 +154,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultMulti.Series = series60minutes; parseResultMulti.Release.Size = 368572800; - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -173,7 +173,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultMulti.Series = series30minutes; parseResultMulti.Release.Size = 1.Gigabytes(); - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -192,7 +192,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultMulti.Series = series60minutes; parseResultMulti.Release.Size = 10.Gigabytes(); - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -211,7 +211,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Series = series30minutes; parseResultSingle.Release.Size = 184572800; - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -230,7 +230,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Series = series60minutes; parseResultSingle.Release.Size = 368572800; - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -249,7 +249,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Series = series30minutes; parseResultSingle.Release.Size = 1.Gigabytes(); - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -270,7 +270,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Series = series60minutes; parseResultSingle.Release.Size = 10.Gigabytes(); - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -292,7 +292,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Release.Size = 18457280000; qualityType.MaxSize = 0; - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -314,7 +314,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Release.Size = 36857280000; qualityType.MaxSize = 0; - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -338,7 +338,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests qualityType.MaxSize = (int)600.Megabytes(); - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -374,7 +374,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsSatisfiedBy(parseResult, null).Should().BeFalse(); - Mocker.GetMock().Verify(c=>c.Get(It.IsAny()),Times.Never()); + Mocker.GetMock().Verify(c => c.Get(It.IsAny()), Times.Never()); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs index 7ff36a03b..be1f420da 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs @@ -13,35 +13,35 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_return_true_if_current_episode_is_less_than_cutoff() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.Bluray1080p }, + Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.Bluray1080p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.DVD, true)).Should().BeTrue(); } [Test] public void should_return_false_if_current_episode_is_equal_to_cutoff() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p }, + Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.HDTV720p, true)).Should().BeFalse(); } [Test] public void should_return_false_if_current_episode_is_greater_than_cutoff() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p }, + Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.Bluray1080p, true)).Should().BeFalse(); } [Test] public void should_return_true_when_new_episode_is_proper_but_existing_is_not() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p }, + Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.HDTV720p, false), new QualityModel(Quality.HDTV720p, true)).Should().BeTrue(); } [Test] public void should_return_false_if_cutoff_is_met_and_quality_is_higher() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p }, + Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.HDTV720p, true), new QualityModel(Quality.Bluray1080p, true)).Should().BeFalse(); } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs index 0fe7b7ba0..36d7db629 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs @@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests }; _fakeSeries = Builder.CreateNew() - .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.Bluray1080p }) + .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.Bluray1080p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _parseResultMulti = new RemoteEpisode @@ -62,8 +62,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _upgradableQuality = new QualityModel(Quality.SDTV, false); _notupgradableQuality = new QualityModel(Quality.HDTV1080p, true); - - Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(1)).Returns(_notupgradableQuality); Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(2)).Returns(_notupgradableQuality); Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(3)).Returns(null); @@ -132,7 +130,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_not_be_upgradable_if_episode_is_of_same_quality_as_existing() { - _fakeSeries.QualityProfile = new QualityProfile { Cutoff = Quality.WEBDL1080p }; + _fakeSeries.QualityProfile = new QualityProfile { Cutoff = Quality.WEBDL1080p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }; _parseResultSingle.ParsedEpisodeInfo.Quality = new QualityModel(Quality.WEBDL1080p, false); _upgradableQuality = new QualityModel(Quality.WEBDL1080p, false); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs index 1ca3e9c41..59361c70f 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs @@ -26,7 +26,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [SetUp] public void Setup() { - _series = Builder.CreateNew().Build(); + _series = Builder.CreateNew() + .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) + .Build(); _episode = Builder.CreateNew() .With(e => e.SeriesId = _series.Id) diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs index cc631b871..d3385c23b 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs @@ -1,4 +1,5 @@ -using FluentAssertions; +using System.Linq; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Configuration; using NzbDrone.Core.Qualities; @@ -23,6 +24,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests new object[] { Quality.SDTV, false, Quality.SDTV, true, Quality.SDTV, true }, new object[] { Quality.WEBDL1080p, false, Quality.WEBDL1080p, false, Quality.WEBDL1080p, false } }; + + [SetUp] + public void Setup() + { + + } private void GivenAutoDownloadPropers(bool autoDownloadPropers) { @@ -36,7 +43,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenAutoDownloadPropers(true); - Subject.IsUpgradable(new QualityModel(current, currentProper), new QualityModel(newQuality, newProper)) + var qualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }; + + Subject.IsUpgradable(qualityProfile, new QualityModel(current, currentProper), new QualityModel(newQuality, newProper)) .Should().Be(expected); } @@ -45,8 +54,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenAutoDownloadPropers(false); - Subject.IsUpgradable(new QualityModel(Quality.DVD, true), - new QualityModel(Quality.DVD, false)).Should().BeFalse(); + var qualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }; + + Subject.IsUpgradable(qualityProfile, new QualityModel(Quality.DVD, true), new QualityModel(Quality.DVD, false)) + .Should().BeFalse(); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs index eb85c757d..9135f0a75 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var doubleEpisodeList = new List { new Episode { EpisodeFile = _firstFile, EpisodeFileId = 1 }, new Episode { EpisodeFile = _secondFile, EpisodeFileId = 1 }, new Episode { EpisodeFile = null } }; var fakeSeries = Builder.CreateNew() - .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.Bluray1080p }) + .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.Bluray1080p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _parseResultMulti = new RemoteEpisode diff --git a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs index f13c81ccb..65ad262a1 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs @@ -37,6 +37,10 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests remoteEpisode.Release = new ReleaseInfo(); remoteEpisode.Release.PublishDate = DateTime.UtcNow; + remoteEpisode.Series = Builder.CreateNew() + .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) + .Build(); + return remoteEpisode; } diff --git a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/GetQualifiedReportsFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/GetQualifiedReportsFixture.cs index 370142d75..0036a8bcb 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/GetQualifiedReportsFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/GetQualifiedReportsFixture.cs @@ -37,6 +37,10 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests remoteEpisode.Release.PublishDate = DateTime.Now.AddDays(-Age); remoteEpisode.Release.Size = size; + remoteEpisode.Series = Builder.CreateNew() + .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) + .Build(); + return remoteEpisode; } diff --git a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs index cb3a0313c..f1a64dd7b 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs @@ -87,7 +87,6 @@ namespace NzbDrone.Core.Test.Download { Mocker.GetMock().Setup(c => c.IsConfigured).Returns(false); - Subject.DownloadReport(_parseResult); Mocker.GetMock().Verify(c => c.DownloadNzb(It.IsAny()), Times.Never()); diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs index 90208c4f5..8f33db4f0 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs @@ -80,6 +80,20 @@ namespace NzbDrone.Core.Test.MediaFiles .Returns(true); Subject.Execute(new DownloadedEpisodesScanCommand()); + + VerifyNoImport(); + } + + [Test] + public void should_skip_if_no_series_found() + { + Mocker.GetMock().Setup(c => c.GetSeries("foldername")).Returns((Series)null); + + Subject.Execute(new DownloadedEpisodesScanCommand()); + + Mocker.GetMock() + .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never()); VerifyNoImport(); } diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs index edcfa1a14..0e2bc1053 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs @@ -13,6 +13,7 @@ using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; +using FizzWare.NBuilder; namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport { @@ -63,7 +64,10 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport _fail3.Setup(c => c.RejectionReason).Returns("_fail3"); _videoFiles = new List { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi" }; - _series = new Series(); + _series = Builder.CreateNew() + .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) + .Build(); + _quality = new QualityModel(Quality.DVD); _localEpisode = new LocalEpisode { @@ -80,7 +84,6 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport Mocker.GetMock() .Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny())) .Returns(_videoFiles); - } private void GivenSpecifications(params Mock[] mocks) @@ -162,7 +165,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport .Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny())) .Returns(_videoFiles); - Subject.GetImportDecisions(_videoFiles, new Series(), false); + Subject.GetImportDecisions(_videoFiles, _series, false); Mocker.GetMock() .Verify(c => c.GetEpisodes(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(_videoFiles.Count)); @@ -176,7 +179,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport GivenSpecifications(_pass1, _pass2, _pass3); var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single()); - var result = Subject.GetImportDecisions(_videoFiles, new Series(), false, null); + var result = Subject.GetImportDecisions(_videoFiles, _series, false, null); result.Single().LocalEpisode.Quality.Should().Be(expectedQuality); } @@ -187,7 +190,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport GivenSpecifications(_pass1, _pass2, _pass3); var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single()); - var result = Subject.GetImportDecisions(_videoFiles, new Series(), false, new QualityModel(Quality.SDTV)); + var result = Subject.GetImportDecisions(_videoFiles, _series, false, new QualityModel(Quality.SDTV)); result.Single().LocalEpisode.Quality.Should().Be(expectedQuality); } @@ -198,7 +201,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport GivenSpecifications(_pass1, _pass2, _pass3); var expectedQuality = new QualityModel(Quality.Bluray1080p); - var result = Subject.GetImportDecisions(_videoFiles, new Series(), false, expectedQuality); + var result = Subject.GetImportDecisions(_videoFiles, _series, false, expectedQuality); result.Single().LocalEpisode.Quality.Should().Be(expectedQuality); } diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs index a0d0f7aef..f189f049b 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs @@ -22,13 +22,15 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications public void Setup() { _series = Builder.CreateNew() - .With(s => s.SeriesType = SeriesTypes.Standard) + .With(s => s.SeriesType = SeriesTypes.Standard) + .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _localEpisode = new LocalEpisode { Path = @"C:\Test\30 Rock\30.rock.s01e01.avi", - Quality = new QualityModel(Quality.HDTV720p, false) + Quality = new QualityModel(Quality.HDTV720p, false), + Series = _series }; } diff --git a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs index 7946b53aa..55789f9b8 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs @@ -29,6 +29,7 @@ namespace NzbDrone.Core.Test.MediaFiles _approvedDecisions = new List(); var series = Builder.CreateNew() + .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); var episodes = Builder.CreateListOfSize(5) diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 326789038..3e406aef7 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -182,7 +182,7 @@ - + @@ -217,14 +217,14 @@ - + - + diff --git a/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs index b67e571ec..338e3059c 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; @@ -50,6 +51,10 @@ namespace NzbDrone.Core.Test.OrganizerTests .Build(); _episodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "DRONE" }; + + Mocker.GetMock() + .Setup(v => v.Get(Moq.It.IsAny())) + .Returns(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v)); } private void GivenProper() diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index b716883cf..a3a7c4a58 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Qualities; @@ -150,9 +151,9 @@ namespace NzbDrone.Core.Test.ParserTests } [Test, TestCaseSource("SelfQualityParserCases")] - public void parsing_our_own_quality_enum(Quality quality) + public void parsing_our_own_quality_enum_name(Quality quality) { - var fileName = String.Format("My series S01E01 [{0}]", quality); + var fileName = String.Format("My series S01E01 [{0}]", quality.Name); var result = Parser.QualityParser.ParseQuality(fileName); result.Quality.Should().Be(quality); } diff --git a/src/NzbDrone.Core.Test/Qualities/QualitySizeRepositoryFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityDefinitionRepositoryFixture.cs similarity index 57% rename from src/NzbDrone.Core.Test/Qualities/QualitySizeRepositoryFixture.cs rename to src/NzbDrone.Core.Test/Qualities/QualityDefinitionRepositoryFixture.cs index 02454aa66..6156d9ab0 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualitySizeRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityDefinitionRepositoryFixture.cs @@ -4,25 +4,27 @@ using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using FluentAssertions; +using System.Collections.Generic; namespace NzbDrone.Core.Test.Qualities { [TestFixture] - - public class QualitySizeRepositoryFixture : DbTest + public class QualityDefinitionRepositoryFixture : DbTest { [SetUp] public void Setup() { - Mocker.SetConstant(Subject); - Mocker.Resolve().Handle(new ApplicationStartedEvent()); + foreach (var qualityDefault in Quality.DefaultQualityDefinitions) + { + qualityDefault.Id = 0; + Storage.Insert(qualityDefault); + } } - [Test] - public void should_get_quality_size_by_id() + public void should_get_qualitydefinition_by_id() { - var size = Subject.GetByQualityId(Quality.Bluray1080p.Id); + var size = Subject.GetByQualityId((int)Quality.Bluray1080p); size.Should().NotBeNull(); } diff --git a/src/NzbDrone.Core.Test/Qualities/QualityDefinitionServiceFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityDefinitionServiceFixture.cs new file mode 100644 index 000000000..90f25b5f7 --- /dev/null +++ b/src/NzbDrone.Core.Test/Qualities/QualityDefinitionServiceFixture.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Qualities +{ + [TestFixture] + public class QualityDefinitionServiceFixture : CoreTest + { + [Test] + public void init_should_add_all_definitions() + { + Subject.Handle(new ApplicationStartedEvent()); + + Mocker.GetMock() + .Verify(v => v.Insert(It.IsAny()), Times.Exactly(Quality.All.Count)); + } + + [Test] + public void init_should_insert_any_missing_definitions() + { + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(new List + { + new QualityDefinition(Quality.SDTV) { Weight = 1, MinSize = 0, MaxSize = 100, Id = 20 } + }); + + Subject.Handle(new ApplicationStartedEvent()); + + Mocker.GetMock() + .Verify(v => v.Insert(It.IsAny()), Times.Exactly(Quality.All.Count - 1)); + } + + [Test] + public void init_should_insert_missing_definitions_preserving_weight() + { + // User moved HDTV1080p to a higher weight. + var currentQualities = new List + { + new QualityDefinition(Quality.SDTV) { Id = 5, Title = "SDTV", Weight = 1, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.WEBDL720p) { Id = 2, Title = "720p WEB-DL", Weight = 2, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.HDTV1080p) { Id = 4, Title = "1080p HDTV", Weight = 3, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.WEBDL1080p) { Id = 8, Title = "1080p WEB-DL", Weight = 4, MinSize=0, MaxSize=100 }, + }; + + // Expected to insert Bluray720p above HDTV1080p. + // Expected to insert Bluray1080p above WEBDL1080p. + var addBluray1080p = new List + { + new QualityDefinition(Quality.SDTV) { Title = "SDTV", Weight = 1, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.HDTV1080p) { Title = "1080p HDTV", Weight = 2, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.WEBDL720p) { Title = "720p WEB-DL", Weight = 3, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.Bluray720p) { Title = "720p BluRay", Weight = 4, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.WEBDL1080p) { Title = "1080p WEB-DL", Weight = 5, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.Bluray1080p) { Title = "1080p BluRay", Weight = 6, MinSize=0, MaxSize=100 } + }; + + Mocker.GetMock() + .Setup(v => v.All()) + .Returns(currentQualities); + + Subject.InsertMissingDefinitions(addBluray1080p); + + Mocker.GetMock() + .Verify(v => v.Insert(It.Is(p => p.Quality == Quality.Bluray720p && p.Weight == 4)), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.Update(It.Is(p => p.Quality == Quality.WEBDL1080p && p.Weight == 5)), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.Insert(It.Is(p => p.Quality == Quality.Bluray1080p && p.Weight == 6)), Times.Once()); + + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs index a3fabfec0..bffb0f5be 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs @@ -1,4 +1,6 @@ -using FluentAssertions; +using System.Linq; +using System.Collections.Generic; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; @@ -42,81 +44,21 @@ namespace NzbDrone.Core.Test.Qualities i.Should().Be(expected); } - - [Test] - public void Icomparer_greater_test() - { - var first = Quality.DVD; - var second = Quality.Bluray1080p; - - second.Should().BeGreaterThan(first); - } - - [Test] - public void Icomparer_lesser() - { - var first = Quality.DVD; - var second = Quality.Bluray1080p; - - first.Should().BeLessThan(second); - } - - [Test] - public void equal_operand() - { - var first = Quality.Bluray1080p; - var second = Quality.Bluray1080p; - - (first == second).Should().BeTrue(); - (first >= second).Should().BeTrue(); - (first <= second).Should().BeTrue(); - } - - [Test] - public void equal_operand_false() - { - var first = Quality.Bluray1080p; - var second = Quality.Unknown; - - (first == second).Should().BeFalse(); - } - - [Test] - public void not_equal_operand() - { - var first = Quality.Bluray1080p; - var second = Quality.Bluray1080p; - - (first != second).Should().BeFalse(); - } - - [Test] - public void not_equal_operand_false() + public static List GetDefaultQualities() { - var first = Quality.Bluray1080p; - var second = Quality.Unknown; - - (first != second).Should().BeTrue(); - } - - [Test] - public void greater_operand() - { - var first = Quality.DVD; - var second = Quality.Bluray1080p; - - (first < second).Should().BeTrue(); - (first <= second).Should().BeTrue(); - } - - [Test] - public void lesser_operand() - { - var first = Quality.DVD; - var second = Quality.Bluray1080p; - - (second > first).Should().BeTrue(); - (second >= first).Should().BeTrue(); + return new List + { + Quality.SDTV, + Quality.WEBDL480p, + Quality.DVD, + Quality.HDTV720p, + Quality.HDTV1080p, + Quality.RAWHD, + Quality.WEBDL720p, + Quality.Bluray720p, + Quality.WEBDL1080p, + Quality.Bluray1080p + }; } } } diff --git a/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs new file mode 100644 index 000000000..416962658 --- /dev/null +++ b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs @@ -0,0 +1,117 @@ +using System.Linq; +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Qualities +{ + [TestFixture] + public class QualityModelComparerFixture : CoreTest + { + public QualityModelComparer Subject { get; set; } + + private void GivenDefaultQualityProfile() + { + Subject = new QualityModelComparer(new QualityProfile { Allowed = QualityFixture.GetDefaultQualities() }); + } + + private void GivenCustomQualityProfile() + { + Subject = new QualityModelComparer(new QualityProfile { Allowed = new List { Quality.Bluray720p, Quality.DVD } }); + } + + [Test] + public void Icomparer_greater_test() + { + GivenDefaultQualityProfile(); + + var first = new QualityModel(Quality.DVD, true); + var second = new QualityModel(Quality.Bluray1080p, true); + + var compare = Subject.Compare(second, first); + + compare.Should().BeGreaterThan(0); + } + + [Test] + public void Icomparer_greater_proper() + { + GivenDefaultQualityProfile(); + + var first = new QualityModel(Quality.Bluray1080p, false); + var second = new QualityModel(Quality.Bluray1080p, true); + + var compare = Subject.Compare(second, first); + + compare.Should().BeGreaterThan(0); + } + + [Test] + public void Icomparer_lesser() + { + GivenDefaultQualityProfile(); + + var first = new QualityModel(Quality.DVD, true); + var second = new QualityModel(Quality.Bluray1080p, true); + + var compare = Subject.Compare(first, second); + + compare.Should().BeLessThan(0); + } + + [Test] + public void Icomparer_lesser_proper() + { + GivenDefaultQualityProfile(); + + var first = new QualityModel(Quality.DVD, false); + var second = new QualityModel(Quality.DVD, true); + + var compare = Subject.Compare(first, second); + + compare.Should().BeLessThan(0); + } + + [Test] + public void Icomparer_greater_custom_order() + { + GivenCustomQualityProfile(); + + var first = new QualityModel(Quality.DVD, true); + var second = new QualityModel(Quality.Bluray720p, true); + + var compare = Subject.Compare(first, second); + + compare.Should().BeGreaterThan(0); + } + + [Test] + public void Icomparer_missing_custom_order() + { + GivenCustomQualityProfile(); + + var first = new QualityModel(Quality.Bluray720p, true); + var second = new QualityModel(Quality.Bluray1080p, true); + + var compare = Subject.Compare(first, second); + + compare.Should().BeGreaterThan(0); + } + + [Test] + public void Icomparer_missing_both_custom_order() + { + GivenCustomQualityProfile(); + + var first = new QualityModel(Quality.SDTV, true); + var second = new QualityModel(Quality.Bluray1080p, true); + + var compare = Subject.Compare(first, second); + + compare.Should().Be(0); + } + } +} diff --git a/src/NzbDrone.Core.Test/Qualities/QualityProfileRepositoryFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityProfileRepositoryFixture.cs index f82879953..f13c59f3d 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityProfileRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityProfileRepositoryFixture.cs @@ -7,7 +7,6 @@ using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.Qualities { [TestFixture] - public class QualityProfileRepositoryFixture : DbTest { [Test] diff --git a/src/NzbDrone.Core.Test/Qualities/QualitySizeServiceFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualitySizeServiceFixture.cs deleted file mode 100644 index 9c0b0b626..000000000 --- a/src/NzbDrone.Core.Test/Qualities/QualitySizeServiceFixture.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections.Generic; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.Lifecycle; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Qualities -{ - [TestFixture] - - public class QualitySizeServiceFixture : CoreTest - { - [Test] - public void Init_should_add_all_sizes() - { - Subject.Handle(new ApplicationStartedEvent()); - - Mocker.GetMock() - .Verify(v => v.Insert(It.IsAny()), Times.Exactly(Quality.All().Count)); - } - - [Test] - public void Init_should_insert_any_missing_sizes() - { - Mocker.GetMock() - .Setup(s => s.All()) - .Returns(new List - { - new QualitySize { QualityId = 1, Name = "SDTV", MinSize = 0, MaxSize = 100 } - }); - - Subject.Handle(new ApplicationStartedEvent()); - - Mocker.GetMock() - .Verify(v => v.Insert(It.IsAny()), Times.Exactly(Quality.All().Count - 1)); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/TvTests/QualityModelFixture.cs b/src/NzbDrone.Core.Test/TvTests/QualityModelFixture.cs deleted file mode 100644 index 322ef2e79..000000000 --- a/src/NzbDrone.Core.Test/TvTests/QualityModelFixture.cs +++ /dev/null @@ -1,125 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.TvTests -{ - [TestFixture] - - public class QualityModelFixture : CoreTest - { - [Test] - public void Icomparer_greater_test() - { - var first = new QualityModel(Quality.DVD, true); - var second = new QualityModel(Quality.Bluray1080p, true); - - second.Should().BeGreaterThan(first); - } - - [Test] - public void Icomparer_greater_proper() - { - var first = new QualityModel(Quality.Bluray1080p, false); - var second = new QualityModel(Quality.Bluray1080p, true); - - second.Should().BeGreaterThan(first); - } - - [Test] - public void Icomparer_lesser() - { - var first = new QualityModel(Quality.DVD, true); - var second = new QualityModel(Quality.Bluray1080p, true); - - first.Should().BeLessThan(second); - } - - [Test] - public void Icomparer_lesser_proper() - { - var first = new QualityModel(Quality.DVD, false); - var second = new QualityModel(Quality.DVD, true); - - first.Should().BeLessThan(second); - } - - [Test] - public void equal_operand() - { - var first = new QualityModel(Quality.Bluray1080p, true); - var second = new QualityModel(Quality.Bluray1080p, true); - - (first == second).Should().BeTrue(); - (first >= second).Should().BeTrue(); - (first <= second).Should().BeTrue(); - } - - [Test] - public void equal_operand_false() - { - var first = new QualityModel(Quality.Bluray1080p, true); - var second = new QualityModel(Quality.Unknown, true); - - (first == second).Should().BeFalse(); - } - - [Test] - public void equal_operand_false_proper() - { - var first = new QualityModel(Quality.Bluray1080p, true); - var second = new QualityModel(Quality.Bluray1080p, false); - - (first == second).Should().BeFalse(); - } - - [Test] - public void not_equal_operand() - { - var first = new QualityModel(Quality.Bluray1080p, true); - var second = new QualityModel(Quality.Bluray1080p, true); - - (first != second).Should().BeFalse(); - } - - [Test] - public void not_equal_operand_false() - { - var first = new QualityModel(Quality.Bluray1080p, true); - var second = new QualityModel(Quality.Unknown, true); - - (first != second).Should().BeTrue(); - } - - [Test] - public void not_equal_operand_false_proper() - { - var first = new QualityModel(Quality.Bluray1080p, true); - var second = new QualityModel(Quality.Bluray1080p, false); - - (first != second).Should().BeTrue(); - } - - [Test] - public void greater_operand() - { - var first = new QualityModel(Quality.DVD, true); - var second = new QualityModel(Quality.Bluray1080p, true); - - (first < second).Should().BeTrue(); - (first <= second).Should().BeTrue(); - } - - [Test] - public void lesser_operand() - { - var first = new QualityModel(Quality.DVD, true); - var second = new QualityModel(Quality.Bluray1080p, true); - - (second > first).Should().BeTrue(); - (second >= first).Should().BeTrue(); - } - } -} diff --git a/src/NzbDrone.Core/Blacklisting/Blacklist.cs b/src/NzbDrone.Core/Blacklisting/Blacklist.cs index 94cc5ffed..c68f55ff3 100644 --- a/src/NzbDrone.Core/Blacklisting/Blacklist.cs +++ b/src/NzbDrone.Core/Blacklisting/Blacklist.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Blacklisting diff --git a/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs b/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs index 6dc9d6c24..ea5dfd59f 100644 --- a/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs @@ -2,6 +2,8 @@ using Marr.Data.Converters; using Marr.Data.Mapping; using NzbDrone.Core.Qualities; +using System.Collections.Generic; +using NzbDrone.Common.Serializer; namespace NzbDrone.Core.Datastore.Converters { @@ -26,7 +28,7 @@ namespace NzbDrone.Core.Datastore.Converters public object ToDB(object clrValue) { - if(clrValue == null) return 0; + if(clrValue == DBNull.Value) return 0; if(clrValue as Quality == null) { diff --git a/src/NzbDrone.Core/Datastore/Converters/QualityListConverter.cs b/src/NzbDrone.Core/Datastore/Converters/QualityListConverter.cs new file mode 100644 index 000000000..c0c58d8f5 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Converters/QualityListConverter.cs @@ -0,0 +1,52 @@ +using System; +using Marr.Data.Converters; +using Marr.Data.Mapping; +using NzbDrone.Core.Qualities; +using System.Collections.Generic; +using NzbDrone.Common.Serializer; + +namespace NzbDrone.Core.Datastore.Converters +{ + public class QualityListConverter : IConverter + { + public object FromDB(ConverterContext context) + { + if (context.DbValue == DBNull.Value) + { + return DBNull.Value; + } + + var val = Convert.ToString(context.DbValue); + + var qualityList = Json.Deserialize>(val).ConvertAll(Quality.FindById); + + return qualityList; + } + + public object FromDB(ColumnMap map, object dbValue) + { + return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); + } + + public object ToDB(object clrValue) + { + if (clrValue == DBNull.Value) return null; + + var qualityList = clrValue as List; + + if (qualityList == null) + { + throw new InvalidOperationException("Can only store a list of qualities in this database column."); + } + + var intList = qualityList.ConvertAll(v => v.Id); + + return intList.ToJson(); + } + + public Type DbType + { + get { return typeof(string); } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Converters/QualityModelConverter.cs b/src/NzbDrone.Core/Datastore/Converters/QualityModelConverter.cs new file mode 100644 index 000000000..8e2917dcc --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Converters/QualityModelConverter.cs @@ -0,0 +1,55 @@ +using System; +using Marr.Data.Converters; +using Marr.Data.Mapping; +using NzbDrone.Core.Qualities; +using System.Collections.Generic; +using NzbDrone.Common.Serializer; + +namespace NzbDrone.Core.Datastore.Converters +{ + public class QualityModelConverter : IConverter + { + public object FromDB(ConverterContext context) + { + if (context.DbValue == DBNull.Value) + { + return new QualityModel(); + } + + var val = Convert.ToString(context.DbValue); + + var jsonObject = Json.Deserialize>(val); + + return new QualityModel((Quality)Convert.ToInt32(jsonObject["id"]), Convert.ToBoolean(jsonObject["proper"])); + } + + public object FromDB(ColumnMap map, object dbValue) + { + return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); + } + + public object ToDB(object clrValue) + { + if (clrValue == DBNull.Value) + clrValue = new QualityModel(); + + var qualityModel = clrValue as QualityModel; + + if (qualityModel == null) + { + throw new InvalidOperationException("Can only store a QualityModel in this database column."); + } + + var jsonObject = new Dictionary(); + jsonObject["id"] = (int)qualityModel.Quality; + jsonObject["proper"] = qualityModel.Proper; + + return jsonObject.ToJson(); + } + + public Type DbType + { + get { return typeof(string); } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs b/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs new file mode 100644 index 000000000..c29490f66 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs @@ -0,0 +1,97 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System.Data; +using System.Linq; +using System; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Qualities; +using System.Collections.Generic; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(36)] + public class update_with_quality_converters : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(ConvertQualityProfiles); + + Execute.WithConnection(ConvertQualityModels); + } + + private void ConvertQualityProfiles(IDbConnection conn, IDbTransaction tran) + { + var qualityListConverter = new NzbDrone.Core.Datastore.Converters.QualityListConverter(); + + // Convert 'Allowed' column in QualityProfiles from Json List to Json List (int = Quality) + using (IDbCommand qualityProfileCmd = conn.CreateCommand()) + { + qualityProfileCmd.Transaction = tran; + qualityProfileCmd.CommandText = @"SELECT Id, Allowed FROM QualityProfiles"; + using (IDataReader qualityProfileReader = qualityProfileCmd.ExecuteReader()) + { + while (qualityProfileReader.Read()) + { + var id = qualityProfileReader.GetInt32(0); + var allowedJson = qualityProfileReader.GetString(1); + + var allowed = Json.Deserialize>(allowedJson); + + var allowedNewJson = qualityListConverter.ToDB(allowed); + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "UPDATE QualityProfiles SET Allowed = ? WHERE Id = ?"; + updateCmd.AddParameter(allowedNewJson); + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + + private void ConvertQualityModels(IDbConnection conn, IDbTransaction tran) + { + // Converts the QualityModel JSON objects to their new format (only storing the QualityId instead of the entire object) + ConvertQualityModel(conn, tran, "Blacklist"); + ConvertQualityModel(conn, tran, "EpisodeFiles"); + ConvertQualityModel(conn, tran, "History"); + } + + private void ConvertQualityModel(IDbConnection conn, IDbTransaction tran, string tableName) + { + var qualityModelConverter = new NzbDrone.Core.Datastore.Converters.QualityModelConverter(); + + using (IDbCommand qualityModelCmd = conn.CreateCommand()) + { + qualityModelCmd.Transaction = tran; + qualityModelCmd.CommandText = @"SELECT Id, Quality FROM " + tableName; + using (IDataReader qualityModelReader = qualityModelCmd.ExecuteReader()) + { + while (qualityModelReader.Read()) + { + var id = qualityModelReader.GetInt32(0); + var qualityJson = qualityModelReader.GetString(1); + + var quality = Json.Deserialize(qualityJson); + + var qualityNewJson = qualityModelConverter.ToDB(quality); + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "UPDATE " + tableName + " SET Quality = ? WHERE Id = ?"; + updateCmd.AddParameter(qualityNewJson); + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs b/src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs new file mode 100644 index 000000000..1020dca2c --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs @@ -0,0 +1,63 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System.Data; +using System.Linq; +using System; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Qualities; +using System.Collections.Generic; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(37)] + public class add_configurable_qualities : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("QualityDefinitions") + .WithColumn("Quality").AsInt32().Unique() + .WithColumn("Title").AsString().Unique() + .WithColumn("Weight").AsInt32().Unique() + .WithColumn("MinSize").AsInt32() + .WithColumn("MaxSize").AsInt32(); + + Execute.WithConnection(ConvertQualities); + + Delete.Table("QualitySizes"); + } + + private void ConvertQualities(IDbConnection conn, IDbTransaction tran) + { + // Convert QualitySizes to a more generic QualityDefinitions table. + using (IDbCommand qualitySizeCmd = conn.CreateCommand()) + { + qualitySizeCmd.Transaction = tran; + qualitySizeCmd.CommandText = @"SELECT QualityId, MinSize, MaxSize FROM QualitySizes"; + using (IDataReader qualitySizeReader = qualitySizeCmd.ExecuteReader()) + { + while (qualitySizeReader.Read()) + { + var qualityId = qualitySizeReader.GetInt32(0); + var minSize = qualitySizeReader.GetInt32(1); + var maxSize = qualitySizeReader.GetInt32(2); + + var defaultConfig = Quality.DefaultQualityDefinitions.Single(p => (int)p.Quality == qualityId); + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "INSERT INTO QualityDefinitions (Quality, Title, Weight, MinSize, MaxSize) VALUES (?, ?, ?, ?, ?)"; + updateCmd.AddParameter(qualityId); + updateCmd.AddParameter(defaultConfig.Title); + updateCmd.AddParameter(defaultConfig.Weight); + updateCmd.AddParameter(minSize); + updateCmd.AddParameter(maxSize); + + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationExtension.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationExtension.cs index 565630165..807e73e2a 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationExtension.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationExtension.cs @@ -10,5 +10,11 @@ namespace NzbDrone.Core.Datastore.Migration.Framework return expressionRoot.Table(name).WithColumn("Id").AsInt32().PrimaryKey().Identity(); } + public static void AddParameter(this System.Data.IDbCommand command, object value) + { + var parameter = command.CreateParameter(); + parameter.Value = value; + command.Parameters.Add(parameter); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index f7ce5cd43..f164e925d 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -60,7 +60,7 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("QualityProfiles"); - Mapper.Entity().RegisterModel("QualitySizes"); + Mapper.Entity().RegisterModel("QualityDefinitions"); Mapper.Entity().RegisterModel("Logs"); @@ -81,6 +81,8 @@ namespace NzbDrone.Core.Datastore MapRepository.Instance.RegisterTypeConverter(typeof(Boolean), new BooleanIntConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(Enum), new EnumIntConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(Quality), new QualityIntConverter()); + MapRepository.Instance.RegisterTypeConverter(typeof(List), new QualityListConverter()); + MapRepository.Instance.RegisterTypeConverter(typeof(QualityModel), new QualityModelConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(Dictionary), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter()); } diff --git a/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs index 8bec31bc1..3330be7ab 100644 --- a/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs @@ -6,7 +6,7 @@ namespace NzbDrone.Core.DecisionEngine { public interface IQualityUpgradableSpecification { - bool IsUpgradable(QualityModel currentQuality, QualityModel newQuality = null); + bool IsUpgradable(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null); bool CutoffNotMet(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null); bool IsProperUpgrade(QualityModel currentQuality, QualityModel newQuality); } @@ -20,11 +20,12 @@ namespace NzbDrone.Core.DecisionEngine _logger = logger; } - public bool IsUpgradable(QualityModel currentQuality, QualityModel newQuality = null) + public bool IsUpgradable(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null) { if (newQuality != null) { - if (currentQuality >= newQuality) + int compare = new QualityModelComparer(profile).Compare(newQuality, currentQuality); + if (compare <= 0) { _logger.Trace("existing item has better or equal quality. skipping"); return false; @@ -41,7 +42,9 @@ namespace NzbDrone.Core.DecisionEngine public bool CutoffNotMet(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null) { - if (currentQuality.Quality >= profile.Cutoff) + int compare = new QualityModelComparer(profile).Compare(currentQuality.Quality, profile.Cutoff); + + if (compare >= 0) { if (newQuality != null && IsProperUpgrade(currentQuality, newQuality)) { @@ -57,7 +60,9 @@ namespace NzbDrone.Core.DecisionEngine public bool IsProperUpgrade(QualityModel currentQuality, QualityModel newQuality) { - if (currentQuality.Quality == newQuality.Quality && newQuality > currentQuality) + int compare = newQuality.Proper.CompareTo(currentQuality.Proper); + + if (currentQuality.Quality == newQuality.Quality && compare > 0) { _logger.Trace("New quality is a proper for existing quality"); return true; diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs index 1d6f5af7f..e63edaa8c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs @@ -9,13 +9,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { public class AcceptableSizeSpecification : IDecisionEngineSpecification { - private readonly IQualitySizeService _qualityTypeProvider; + private readonly IQualityDefinitionService _qualityDefinitionService; private readonly IEpisodeService _episodeService; private readonly Logger _logger; - public AcceptableSizeSpecification(IQualitySizeService qualityTypeProvider, IEpisodeService episodeService, Logger logger) + public AcceptableSizeSpecification(IQualityDefinitionService qualityDefinitionService, IEpisodeService episodeService, Logger logger) { - _qualityTypeProvider = qualityTypeProvider; + _qualityDefinitionService = qualityDefinitionService; _episodeService = episodeService; _logger = logger; } @@ -44,15 +44,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return false; } - var qualityType = _qualityTypeProvider.Get(quality.Id); + var qualityDefinition = _qualityDefinitionService.Get(quality); - if (qualityType.MaxSize == 0) + if (qualityDefinition.MaxSize == 0) { _logger.Trace("Max size is 0 (unlimited) - skipping check."); return true; } - var maxSize = qualityType.MaxSize.Megabytes(); + var maxSize = qualityDefinition.MaxSize.Megabytes(); //Multiply maxSize by Series.Runtime maxSize = maxSize * subject.Series.Runtime * subject.Episodes.Count; diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs index c0ccf0ecf..3827f26a7 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs @@ -4,6 +4,8 @@ using NLog; using NzbDrone.Core.Download; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.DecisionEngine.Specifications { @@ -44,7 +46,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications private bool IsInQueue(RemoteEpisode newEpisode, IEnumerable queue) { var matchingSeries = queue.Where(q => q.Series.Id == newEpisode.Series.Id); - var matchingSeriesAndQuality = matchingSeries.Where(q => q.ParsedEpisodeInfo.Quality >= newEpisode.ParsedEpisodeInfo.Quality); + var matchingSeriesAndQuality = matchingSeries.Where(q => new QualityModelComparer(q.Series.QualityProfile).Compare(q.ParsedEpisodeInfo.Quality, newEpisode.ParsedEpisodeInfo.Quality) >= 0); return matchingSeriesAndQuality.Any(q => q.Episodes.Select(e => e.Id).Intersect(newEpisode.Episodes.Select(e => e.Id)).Any()); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index e6d6ef366..a4c3b3745 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -63,7 +63,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync if (bestQualityInHistory != null) { _logger.Trace("Comparing history quality with report. History is {0}", bestQualityInHistory); - if (!_qualityUpgradableSpecification.IsUpgradable(bestQualityInHistory, subject.ParsedEpisodeInfo.Quality)) + if (!_qualityUpgradableSpecification.IsUpgradable(subject.Series.QualityProfile, bestQualityInHistory, subject.ParsedEpisodeInfo.Quality)) return false; } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index 004f20134..8d2c5f631 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -30,7 +30,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { _logger.Trace("Comparing file quality with report. Existing file is {0}", file.Quality); - if (!_qualityUpgradableSpecification.IsUpgradable(file.Quality, subject.ParsedEpisodeInfo.Quality)) + if (!_qualityUpgradableSpecification.IsUpgradable(subject.Series.QualityProfile, file.Quality, subject.ParsedEpisodeInfo.Quality)) { return false; } diff --git a/src/NzbDrone.Core/Download/DownloadApprovedReports.cs b/src/NzbDrone.Core/Download/DownloadApprovedReports.cs index 46f54d1aa..1added8cc 100644 --- a/src/NzbDrone.Core/Download/DownloadApprovedReports.cs +++ b/src/NzbDrone.Core/Download/DownloadApprovedReports.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.Download { @@ -16,7 +18,7 @@ namespace NzbDrone.Core.Download private readonly IDownloadService _downloadService; private readonly Logger _logger; - public DownloadApprovedReports(IDownloadService downloadService, Logger logger) + public DownloadApprovedReports(IDownloadService downloadService, Logger logger) { _downloadService = downloadService; _logger = logger; @@ -57,11 +59,13 @@ namespace NzbDrone.Core.Download public List GetQualifiedReports(IEnumerable decisions) { return decisions.Where(c => c.Approved && c.RemoteEpisode.Episodes.Any()) - .OrderByDescending(c => c.RemoteEpisode.ParsedEpisodeInfo.Quality) - .ThenBy(c => c.RemoteEpisode.Episodes.Select(e => e.EpisodeNumber).MinOrDefault()) - .ThenBy(c => c.RemoteEpisode.Release.Size.Round(200.Megabytes()) / c.RemoteEpisode.Episodes.Count) - .ThenBy(c => c.RemoteEpisode.Release.Age) - .ToList(); + .GroupBy(c => c.RemoteEpisode.Series.Id, (i,s) => s + .OrderByDescending(c => c.RemoteEpisode.ParsedEpisodeInfo.Quality, new QualityModelComparer(s.First().RemoteEpisode.Series.QualityProfile)) + .ThenBy(c => c.RemoteEpisode.Episodes.Select(e => e.EpisodeNumber).MinOrDefault()) + .ThenBy(c => c.RemoteEpisode.Release.Size.Round(200.Megabytes()) / c.RemoteEpisode.Episodes.Count) + .ThenBy(c => c.RemoteEpisode.Release.Age)) + .SelectMany(c => c) + .ToList(); } } } diff --git a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs index 188ab24f9..6950081c2 100644 --- a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs +++ b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using NzbDrone.Common.Messaging; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Download diff --git a/src/NzbDrone.Core/History/History.cs b/src/NzbDrone.Core/History/History.cs index 94b345e59..4208039e4 100644 --- a/src/NzbDrone.Core/History/History.cs +++ b/src/NzbDrone.Core/History/History.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.History diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index 59d54c745..f860afc70 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -4,6 +4,7 @@ using System.Linq; using Marr.Data.QGen; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.History diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 4c8758618..07e32b824 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -6,6 +6,7 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.Download; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.History diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index 174693c7c..cf82620de 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -10,6 +10,7 @@ using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Parser; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.MediaFiles diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs index 7fac2d1ae..12dccb7b0 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs @@ -1,5 +1,6 @@ using System; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.MediaFiles diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index c9fe15e58..a18b435c9 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -7,6 +7,8 @@ using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.MediaFiles.EpisodeImport @@ -40,8 +42,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public List Import(List decisions, bool newDownload = false) { var qualifiedImports = decisions.Where(c => c.Approved) - .OrderByDescending(c => c.LocalEpisode.Quality) - .ThenByDescending(c => c.LocalEpisode.Size); + .GroupBy(c => c.LocalEpisode.Series.Id, (i, s) => s + .OrderByDescending(c => c.LocalEpisode.Quality, new QualityModelComparer(s.First().LocalEpisode.Series.QualityProfile)) + .ThenByDescending(c => c.LocalEpisode.Size)) + .SelectMany(c => c) + .ToList(); var imported = new List(); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index 56e49d2c2..8b7c620bb 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; @@ -60,7 +61,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport if (parsedEpisode != null) { - if (quality != null && quality > parsedEpisode.Quality) + if (quality != null && new QualityModelComparer(parsedEpisode.Series.QualityProfile).Compare(quality, parsedEpisode.Quality) > 0) { _logger.Trace("Using quality from folder: {0}", quality); parsedEpisode.Quality = quality; diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs index 58b5ac75d..c637ac5cd 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs @@ -1,6 +1,8 @@ using System.Linq; using NLog; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications { @@ -17,7 +19,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications public bool IsSatisfiedBy(LocalEpisode localEpisode) { - if (localEpisode.Episodes.Any(e => e.EpisodeFileId != 0 && e.EpisodeFile.Value.Quality > localEpisode.Quality)) + var qualityComparer = new QualityModelComparer(localEpisode.Series.QualityProfile); + if (localEpisode.Episodes.Any(e => e.EpisodeFileId != 0 && qualityComparer.Compare(e.EpisodeFile.Value.Quality, localEpisode.Quality) > 0)) { _logger.Trace("This file isn't an upgrade for all episodes. Skipping {0}", localEpisode.Path); return false; diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index a948fa830..768c3fda3 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -5,6 +5,7 @@ using NLog; using NzbDrone.Core.Download; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Notifications diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 5dbf4fd48..b93b226a2 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -145,6 +145,8 @@ + + @@ -191,6 +193,8 @@ + + @@ -456,12 +460,13 @@ + - + @@ -489,7 +494,7 @@ - + @@ -575,7 +580,7 @@ Code - + Code @@ -597,7 +602,7 @@ - + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 72a4ab75c..9a35534be 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Organizer @@ -22,6 +23,7 @@ namespace NzbDrone.Core.Organizer public class FileNameBuilder : IBuildFileNames { private readonly INamingConfigService _namingConfigService; + private readonly IQualityDefinitionService _qualityDefinitionService; private readonly ICached _patternCache; private readonly Logger _logger; @@ -43,10 +45,12 @@ namespace NzbDrone.Core.Organizer RegexOptions.Compiled | RegexOptions.IgnoreCase); public FileNameBuilder(INamingConfigService namingConfigService, + IQualityDefinitionService qualityDefinitionService, ICacheManger cacheManger, Logger logger) { _namingConfigService = namingConfigService; + _qualityDefinitionService = qualityDefinitionService; _patternCache = cacheManger.GetCache(GetType()); _logger = logger; } @@ -87,12 +91,10 @@ namespace NzbDrone.Core.Organizer sortedEpisodes.First().Title }; - var tokenValues = new Dictionary(FilenameBuilderTokenEqualityComparer.Instance) - { - {"{Series Title}", series.Title}, - {"Original Title", episodeFile.SceneName} - }; + var tokenValues = new Dictionary(FilenameBuilderTokenEqualityComparer.Instance); + tokenValues.Add("{Series Title}", series.Title); + tokenValues.Add("{Original Title}", episodeFile.SceneName); tokenValues.Add("{Release Group}", episodeFile.ReleaseGroup); if (series.SeriesType == SeriesTypes.Daily) @@ -146,7 +148,7 @@ namespace NzbDrone.Core.Organizer } tokenValues.Add("{Episode Title}", GetEpisodeTitle(episodeTitles)); - tokenValues.Add("{Quality Title}", episodeFile.Quality.ToString()); + tokenValues.Add("{Quality Title}", GetQualityTitle(episodeFile.Quality)); return CleanFilename(ReplaceTokens(pattern, tokenValues).Trim()); @@ -341,6 +343,14 @@ namespace NzbDrone.Core.Organizer return String.Join(" + ", episodeTitles.Select(Parser.Parser.CleanupEpisodeTitle).Distinct()); } + + private string GetQualityTitle(QualityModel quality) + { + if (quality.Proper) + return _qualityDefinitionService.Get(quality.Quality).Title + " Proper"; + else + return _qualityDefinitionService.Get(quality.Quality).Title; + } } public enum MultiEpisodeStyle diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index 06f620a96..447055328 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -1,7 +1,8 @@ using System; +using System.Linq; using System.Collections.Generic; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; -using System.Linq; namespace NzbDrone.Core.Parser.Model { diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index 7ae94f647..fffc79c96 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Parser.Model diff --git a/src/NzbDrone.Core/Qualities/Quality.cs b/src/NzbDrone.Core/Qualities/Quality.cs index 5d62d3b04..656e522da 100644 --- a/src/NzbDrone.Core/Qualities/Quality.cs +++ b/src/NzbDrone.Core/Qualities/Quality.cs @@ -2,65 +2,24 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore.Converters; + namespace NzbDrone.Core.Qualities { - public class Quality : IComparable, IEmbeddedDocument + public class Quality : IEmbeddedDocument, IEquatable { public int Id { get; set; } public string Name { get; set; } - public int Weight { get; set; } - - public int CompareTo(Quality other) - { - if (other.Weight > Weight) - return -1; - - if (other.Weight < Weight) - return 1; - - if (other.Weight == Weight) - return 0; - - return 0; - } - public static bool operator !=(Quality x, Quality y) + public Quality() { - return !(x == y); - } - - public static bool operator ==(Quality x, Quality y) - { - var xObj = (Object)x; - var yObj = (object)y; - - if (xObj == null || yObj == null) - { - return xObj == yObj; - } - - return x.CompareTo(y) == 0; } - public static bool operator >(Quality x, Quality y) + private Quality(int id, string name) { - return x.CompareTo(y) > 0; - } - - public static bool operator <(Quality x, Quality y) - { - return x.CompareTo(y) < 0; - } - - public static bool operator <=(Quality x, Quality y) - { - return x.CompareTo(y) <= 0; - } - - public static bool operator >=(Quality x, Quality y) - { - return x.CompareTo(y) >= 0; + Id = id; + Name = name; } public override string ToString() @@ -70,110 +29,96 @@ namespace NzbDrone.Core.Qualities public override int GetHashCode() { - unchecked // Overflow is fine, just wrap - { - int hash = 17; - hash = hash * 23 + Weight.GetHashCode(); - return hash; - } + return Id.GetHashCode(); } public bool Equals(Quality other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return Equals(other.Weight, Weight); + return Id.Equals(other.Id); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != typeof(Quality)) return false; - return Equals((Quality)obj); - } - public static Quality Unknown - { - get { return new Quality { Id = 0, Name = "Unknown", Weight = 0 }; } + return Equals(obj as Quality); } - public static Quality SDTV + public static bool operator ==(Quality left, Quality right) { - get { return new Quality { Id = 1, Name = "SDTV", Weight = 1 }; } + return Equals(left, right); } - public static Quality WEBDL480p + public static bool operator !=(Quality left, Quality right) { - get { return new Quality { Id = 8, Name = "WEBDL-480p", Weight = 2 }; } + return !Equals(left, right); } - public static Quality DVD + public static Quality Unknown { get { return new Quality(0, "Unknown"); } } + public static Quality SDTV { get { return new Quality(1, "SDTV"); } } + public static Quality DVD { get { return new Quality(2, "DVD"); } } + public static Quality WEBDL1080p { get { return new Quality(3, "WEBDL-1080p"); } } + public static Quality HDTV720p { get { return new Quality(4, "HDTV-720p"); } } + public static Quality WEBDL720p { get { return new Quality(5, "WEBDL-720p"); } } + public static Quality Bluray720p { get { return new Quality(6, "Bluray-720p"); } } + public static Quality Bluray1080p { get { return new Quality(7, "Bluray-1080p"); } } + public static Quality WEBDL480p { get { return new Quality(8, "WEBDL-480p"); } } + public static Quality HDTV1080p { get { return new Quality(9, "HDTV-1080p"); } } + public static Quality RAWHD { get { return new Quality(10, "Raw-HD"); } } + public static Quality HDTV480p { get { return new Quality(11, "HDTV-480p"); } } + + public static List All { - get { return new Quality { Id = 2, Name = "DVD", Weight = 3 }; } - } - - public static Quality HDTV720p - { - get { return new Quality { Id = 4, Name = "HDTV-720p", Weight = 4 }; } - } - - public static Quality HDTV1080p - { - get { return new Quality { Id = 9, Name = "HDTV-1080p", Weight = 5 }; } - } - - public static Quality RAWHD - { - get { return new Quality { Id = 10, Name = "Raw-HD", Weight = 6 }; } - } - - public static Quality WEBDL720p - { - get { return new Quality { Id = 5, Name = "WEBDL-720p", Weight = 7 }; } - } - - public static Quality Bluray720p - { - get { return new Quality { Id = 6, Name = "Bluray720p", Weight = 8 }; } - } - - public static Quality WEBDL1080p - { - get { return new Quality { Id = 3, Name = "WEBDL-1080p", Weight = 9 }; } - } - - public static Quality Bluray1080p - { - get { return new Quality { Id = 7, Name = "Bluray1080p", Weight = 10 }; } + get + { + return new List + { + SDTV, + DVD, + WEBDL1080p, + HDTV720p, + WEBDL720p, + Bluray720p, + Bluray1080p, + WEBDL480p, + HDTV1080p, + RAWHD + }; + } } - public static List All() + public static HashSet DefaultQualityDefinitions { - return new List - { - SDTV, - WEBDL480p, - DVD, - HDTV720p, - HDTV1080p, - RAWHD, - WEBDL720p, - WEBDL1080p, - Bluray720p, - Bluray1080p - }; + get + { + return new HashSet + { + new QualityDefinition(Quality.SDTV) { /*Title = "SDTV", */ Weight = 1, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.WEBDL480p) { /*Title = "WEB-DL", */ Weight = 2, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.DVD) { /*Title = "DVD", */ Weight = 3, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.HDTV720p) { /*Title = "720p HDTV", */ Weight = 4, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.HDTV1080p) { /*Title = "1080p HDTV", */ Weight = 5, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.RAWHD) { /*Title = "RawHD", */ Weight = 6, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.WEBDL720p) { /*Title = "720p WEB-DL", */ Weight = 7, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.Bluray720p) { /*Title = "720p BluRay", */ Weight = 8, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.WEBDL1080p) { /*Title = "1080p WEB-DL",*/ Weight = 9, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.Bluray1080p) { /*Title = "1080p BluRay",*/ Weight = 10, MinSize=0, MaxSize=100 } + }; + } } public static Quality FindById(int id) { if (id == 0) return Unknown; - var quality = All().SingleOrDefault(q => q.Id == id); + Quality quality = All.FirstOrDefault(v => v.Id == id); if (quality == null) throw new ArgumentException("ID does not match a known quality", "id"); - + return quality; } diff --git a/src/NzbDrone.Core/Qualities/QualityDefinition.cs b/src/NzbDrone.Core/Qualities/QualityDefinition.cs new file mode 100644 index 000000000..14cc1f008 --- /dev/null +++ b/src/NzbDrone.Core/Qualities/QualityDefinition.cs @@ -0,0 +1,33 @@ +using NzbDrone.Core.Datastore; + + +namespace NzbDrone.Core.Qualities +{ + public class QualityDefinition : ModelBase + { + public Quality Quality { get; set; } + + public string Title { get; set; } + + public int Weight { get; set; } + + public int MinSize { get; set; } + public int MaxSize { get; set; } + + public QualityDefinition() + { + + } + + public QualityDefinition(Quality quality) + { + Quality = quality; + Title = quality.Name; + } + + public override string ToString() + { + return Quality.Name; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Qualities/QualityDefinitionRepository.cs b/src/NzbDrone.Core/Qualities/QualityDefinitionRepository.cs new file mode 100644 index 000000000..f73fb2de8 --- /dev/null +++ b/src/NzbDrone.Core/Qualities/QualityDefinitionRepository.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + + +namespace NzbDrone.Core.Qualities +{ + public interface IQualityDefinitionRepository : IBasicRepository + { + QualityDefinition GetByQualityId(int qualityId); + } + + public class QualityDefinitionRepository : BasicRepository, IQualityDefinitionRepository + { + public QualityDefinitionRepository(IDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public QualityDefinition GetByQualityId(int qualityId) + { + try + { + return Query.Single(q => (int)q.Quality == qualityId); + } + catch (InvalidOperationException e) + { + throw new ModelNotFoundException(typeof(QualityDefinition), qualityId); + } + } + } +} diff --git a/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs new file mode 100644 index 000000000..9895461ec --- /dev/null +++ b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Messaging.Events; +using System; + +namespace NzbDrone.Core.Qualities +{ + public interface IQualityDefinitionService + { + void Update(QualityDefinition qualityDefinition); + List All(); + QualityDefinition Get(Quality quality); + } + + public class QualityDefinitionService : IQualityDefinitionService, IHandle + { + private readonly IQualityDefinitionRepository _qualityDefinitionRepository; + private readonly Logger _logger; + + public QualityDefinitionService(IQualityDefinitionRepository qualityDefinitionRepository, Logger logger) + { + _qualityDefinitionRepository = qualityDefinitionRepository; + _logger = logger; + } + + public void Update(QualityDefinition qualityDefinition) + { + _qualityDefinitionRepository.Update(qualityDefinition); + } + + public List All() + { + return _qualityDefinitionRepository.All().ToList(); + } + + public QualityDefinition Get(Quality quality) + { + if (quality == Quality.Unknown) + return new QualityDefinition(Quality.Unknown); + + return _qualityDefinitionRepository.GetByQualityId((int)quality); + } + + public void InsertMissingDefinitions(List allDefinitions) + { + allDefinitions.OrderBy(v => v.Weight).ToList(); + var existingDefinitions = _qualityDefinitionRepository.All().OrderBy(v => v.Weight).ToList(); + + // Try insert each item intelligently to merge the lists preserving the Weight the user set. + for (int i = 0; i < allDefinitions.Count;i++) + { + // Skip if this definition isn't missing. + if (existingDefinitions.Any(v => v.Quality == allDefinitions[i].Quality)) + continue; + + int targetIndexMinimum = 0; + for (int j = 0; j < i; j++) + targetIndexMinimum = Math.Max(targetIndexMinimum, existingDefinitions.FindIndex(v => v.Quality == allDefinitions[j].Quality) + 1); + + int targetIndexMaximum = existingDefinitions.Count; + for (int j = i + 1; j < allDefinitions.Count; j++) + { + var index = existingDefinitions.FindIndex(v => v.Quality == allDefinitions[j].Quality); + if (index != -1) + targetIndexMaximum = Math.Min(targetIndexMaximum, index); + } + + // Rounded down average sounds reasonable. + int targetIndex = (targetIndexMinimum + targetIndexMaximum) / 2; + + existingDefinitions.Insert(targetIndex, allDefinitions[i]); + } + + // Update all Weights. + List insertList = new List(); + List updateList = new List(); + for (int i = 0; i < existingDefinitions.Count; i++) + { + if (existingDefinitions[i].Id == 0) + { + existingDefinitions[i].Weight = i + 1; + _qualityDefinitionRepository.Insert(existingDefinitions[i]); + } + else if (existingDefinitions[i].Weight != i + 1) + { + existingDefinitions[i].Weight = i + 1; + _qualityDefinitionRepository.Update(existingDefinitions[i]); + } + } + } + + public void Handle(ApplicationStartedEvent message) + { + _logger.Debug("Setting up default quality config"); + + InsertMissingDefinitions(Quality.DefaultQualityDefinitions.ToList()); + } + } +} diff --git a/src/NzbDrone.Core/Qualities/QualityModel.cs b/src/NzbDrone.Core/Qualities/QualityModel.cs new file mode 100644 index 000000000..ef4dc9bd0 --- /dev/null +++ b/src/NzbDrone.Core/Qualities/QualityModel.cs @@ -0,0 +1,73 @@ +using System; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Qualities +{ + public class QualityModel : IEmbeddedDocument, IEquatable + { + public Quality Quality { get; set; } + + public Boolean Proper { get; set; } + + public QualityModel() + : this(Quality.Unknown) + { + + } + + public QualityModel(Quality quality, Boolean proper = false) + { + Quality = quality; + Proper = proper; + } + + + public override string ToString() + { + string result = Quality.ToString(); + if (Proper) + { + result += " Proper"; + } + + return result; + } + + public override int GetHashCode() + { + unchecked // Overflow is fine, just wrap + { + int hash = 17; + hash = hash * 23 + Proper.GetHashCode(); + hash = hash * 23 + Quality.GetHashCode(); + return hash; + } + } + + public bool Equals(QualityModel other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return other.Quality.Equals(Quality) && other.Proper.Equals(Proper); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + + return Equals(obj as QualityModel); + } + + public static bool operator ==(QualityModel left, QualityModel right) + { + return Equals(left, right); + } + + public static bool operator !=(QualityModel left, QualityModel right) + { + return !Equals(left, right); + } + } +} diff --git a/src/NzbDrone.Core/Qualities/QualityModelComparer.cs b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs new file mode 100644 index 000000000..2859059af --- /dev/null +++ b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NLog; +using NzbDrone.Common.EnsureThat; + +namespace NzbDrone.Core.Qualities +{ + public class QualityModelComparer : IComparer, IComparer + { + private readonly QualityProfile _qualityProfile; + + public QualityModelComparer(QualityProfile qualityProfile) + { + Ensure.That(qualityProfile, () => qualityProfile).IsNotNull(); + Ensure.That(qualityProfile.Allowed, () => qualityProfile.Allowed).HasItems(); + + _qualityProfile = qualityProfile; + } + + public int Compare(Quality left, Quality right) + { + int leftIndex = _qualityProfile.Allowed.IndexOf(left); + int rightIndex = _qualityProfile.Allowed.IndexOf(right); + + return leftIndex.CompareTo(rightIndex); + } + + public int Compare(QualityModel left, QualityModel right) + { + int result = Compare(left.Quality, right.Quality); + + if (result == 0) + result = left.Proper.CompareTo(right.Proper); + + return result; + } + /* + public string GetName(Quality quality) + { + QualityDefinition qualityDefinition = _qualityDefinitionService.Get(quality); + + return qualityDefinition.Name; + } + + public string GetName(QualityModel quality) + { + QualityDefinition qualityDefinition = _qualityDefinitionService.Get(quality.Quality); + + if (quality.Proper) + return qualityDefinition.Name + " Proper"; + else + return qualityDefinition.Name; + } + + public string GetSceneName(QualityModel quality) + { + QualityDefinition qualityDefinition = _qualityDefinitionService.Get(quality.Quality); + + if (quality.Proper) + return qualityDefinition.SceneName + " PROPER"; + else + return qualityDefinition.SceneName; + }*/ + } +} diff --git a/src/NzbDrone.Core/Qualities/QualitySize.cs b/src/NzbDrone.Core/Qualities/QualitySize.cs deleted file mode 100644 index 39f7f4e29..000000000 --- a/src/NzbDrone.Core/Qualities/QualitySize.cs +++ /dev/null @@ -1,18 +0,0 @@ -using NzbDrone.Core.Datastore; - - -namespace NzbDrone.Core.Qualities -{ - public class QualitySize : ModelBase - { - public int QualityId { get; set; } - public string Name { get; set; } - public int MinSize { get; set; } - public int MaxSize { get; set; } - - public override string ToString() - { - return Name; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Qualities/QualitySizeRepository.cs b/src/NzbDrone.Core/Qualities/QualitySizeRepository.cs deleted file mode 100644 index f96f84be3..000000000 --- a/src/NzbDrone.Core/Qualities/QualitySizeRepository.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Linq; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Messaging.Events; - - -namespace NzbDrone.Core.Qualities -{ - public interface IQualitySizeRepository : IBasicRepository - { - QualitySize GetByQualityId(int qualityId); - } - - public class QualitySizeRepository : BasicRepository, IQualitySizeRepository - { - public QualitySizeRepository(IDatabase database, IEventAggregator eventAggregator) - : base(database, eventAggregator) - { - } - - public QualitySize GetByQualityId(int qualityId) - { - try - { - return Query.Single(q => q.QualityId == qualityId); - } - catch (InvalidOperationException e) - { - throw new ModelNotFoundException(typeof(QualitySize), qualityId); - } - } - } -} diff --git a/src/NzbDrone.Core/Qualities/QualitySizeService.cs b/src/NzbDrone.Core/Qualities/QualitySizeService.cs deleted file mode 100644 index 0671a6c10..000000000 --- a/src/NzbDrone.Core/Qualities/QualitySizeService.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Core.Lifecycle; -using NzbDrone.Core.Messaging.Events; - -namespace NzbDrone.Core.Qualities -{ - public interface IQualitySizeService - { - void Update(QualitySize qualitySize); - List All(); - QualitySize Get(int qualityId); - } - - public class QualitySizeService : IQualitySizeService, IHandle - { - private readonly IQualitySizeRepository _qualitySizeRepository; - private readonly Logger _logger; - - public QualitySizeService(IQualitySizeRepository qualitySizeRepository, Logger logger) - { - _qualitySizeRepository = qualitySizeRepository; - _logger = logger; - } - - public virtual void Update(QualitySize qualitySize) - { - _qualitySizeRepository.Update(qualitySize); - } - - - public virtual List All() - { - return _qualitySizeRepository.All().ToList(); - } - - public virtual QualitySize Get(int qualityId) - { - return _qualitySizeRepository.GetByQualityId(qualityId); - } - - public void Handle(ApplicationStartedEvent message) - { - var existing = All(); - - _logger.Debug("Setting up default quality sizes"); - - foreach (var quality in Quality.All()) - { - if (!existing.Any(s => s.QualityId == quality.Id)) - { - _qualitySizeRepository.Insert(new QualitySize - { - QualityId = quality.Id, - Name = quality.Name, - MinSize = 0, - MaxSize = 100 - }); - } - } - } - } -} diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index e1ceb3ff8..733ff2301 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -1,5 +1,6 @@ using System; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Queue diff --git a/src/NzbDrone.Core/Tv/QualityModel.cs b/src/NzbDrone.Core/Tv/QualityModel.cs deleted file mode 100644 index eaa7f7884..000000000 --- a/src/NzbDrone.Core/Tv/QualityModel.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.Tv -{ - public class QualityModel : IComparable, IEmbeddedDocument - { - public Quality Quality { get; set; } - - public Boolean Proper { get; set; } - - public QualityModel() - : this(Quality.Unknown) - { - - } - - public QualityModel(Quality quality, Boolean proper = false) - { - Quality = quality; - Proper = proper; - } - - public int CompareTo(QualityModel other) - { - if (other.Quality > Quality) - return -1; - - if (other.Quality < Quality) - return 1; - - if (other.Quality == Quality && other.Proper == Proper) - return 0; - - if (Proper && !other.Proper) - return 1; - - if (!Proper && other.Proper) - return -1; - - return 0; - } - - public static bool operator !=(QualityModel x, QualityModel y) - { - return !(x == y); - } - - public static bool operator ==(QualityModel x, QualityModel y) - { - var xObj = (Object)x; - var yObj = (object)y; - - if (xObj == null || yObj == null) - { - return xObj == yObj; - } - - return x.CompareTo(y) == 0; - } - - public static bool operator >(QualityModel x, QualityModel y) - { - return x.CompareTo(y) > 0; - } - - public static bool operator <(QualityModel x, QualityModel y) - { - return x.CompareTo(y) < 0; - } - - public static bool operator <=(QualityModel x, QualityModel y) - { - return x.CompareTo(y) <= 0; - } - - public static bool operator >=(QualityModel x, QualityModel y) - { - return x.CompareTo(y) >= 0; - } - - public override string ToString() - { - string result = Quality.ToString(); - if (Proper) - { - result += " Proper"; - } - - return result; - } - - public override int GetHashCode() - { - unchecked // Overflow is fine, just wrap - { - int hash = 17; - hash = hash * 23 + Proper.GetHashCode(); - hash = hash * 23 + Quality.GetHashCode(); - return hash; - } - } - - public bool Equals(QualityModel other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Equals(other.Quality, Quality) && other.Proper.Equals(Proper); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != typeof(QualityModel)) return false; - return Equals((QualityModel)obj); - } - } -} diff --git a/src/UI/JsLibraries/backbone.collectionview.js b/src/UI/JsLibraries/backbone.collectionview.js new file mode 100644 index 000000000..170871ec4 --- /dev/null +++ b/src/UI/JsLibraries/backbone.collectionview.js @@ -0,0 +1,1072 @@ +/*! +* Backbone.CollectionView, v0.8.1 +* Copyright (c)2013 Rotunda Software, LLC. +* Distributed under MIT license +* http://github.com/rotundasoftware/backbone-collection-view +*/ + + +(function() { + var mDefaultModelViewConstructor = Backbone.View; + + var kDefaultReferenceBy = "model"; + + var kAllowedOptions = [ + "collection", "modelView", "modelViewOptions", "itemTemplate", "emptyListCaption", + "selectable", "clickToSelect", "selectableModelsFilter", "visibleModelsFilter", + "selectMultiple", "clickToToggle", "processKeyEvents", "sortable", "sortableOptions", "sortableModelsFilter", "itemTemplateFunction", "detachedRendering" + ]; + + var kOptionsRequiringRerendering = [ "collection", "modelView", "modelViewOptions", "itemTemplate", "selectableModelsFilter", "sortableModelsFilter", "visibleModelsFilter", "itemTemplateFunction", "detachedRendering", "sortableOptions" ]; + + var kStylesForEmptyListCaption = { + "background" : "transparent", + "border" : "none", + "box-shadow" : "none" + }; + + Backbone.CollectionView = Backbone.View.extend( { + + tagName : "ul", + + events : { + "mousedown li, td" : "_listItem_onMousedown", + "dblclick li, td" : "_listItem_onDoubleClick", + "click" : "_listBackground_onClick", + "click ul.collection-list, table.collection-list" : "_listBackground_onClick", + "keydown" : "_onKeydown" + }, + + // only used if Backbone.Courier is available + spawnMessages : { + "focus" : "focus" + }, + + //only used if Backbone.Courier is available + passMessages : { "*" : "." }, + + initialize : function( options ) { + var _this = this; + + this._hasBeenRendered = false; + + // default options + options = _.extend( {}, { + collection : null, + modelView : this.modelView || null, + modelViewOptions : {}, + itemTemplate : null, + itemTemplateFunction : null, + selectable : true, + clickToSelect : true, + selectableModelsFilter : null, + visibleModelsFilter : null, + sortableModelsFilter : null, + selectMultiple : false, + clickToToggle : false, + processKeyEvents : true, + sortable : false, + sortableOptions : null, + detachedRendering : false, + emptyListCaption : null + }, options ); + + // add each of the white-listed options to the CollectionView object itself + _.each( kAllowedOptions, function( option ) { + _this[ option ] = options[option]; + } ); + + if( ! this.collection ) this.collection = new Backbone.Collection(); + + if( this._isBackboneCourierAvailable() ) { + Backbone.Courier.add( this ); + } + + this.$el.data( "view", this ); // needed for connected sortable lists + this.$el.addClass( "collection-list" ); + if( this.processKeyEvents ) + this.$el.attr( "tabindex", 0 ); // so we get keyboard events + + this.selectedItems = []; + + this._updateItemTemplate(); + + if( this.collection ) + this._registerCollectionEvents(); + + this.viewManager = new ChildViewContainer(); + + //this.listenTo( this.collection, "change", function() { this.render(); this.spawn( "change" ); } ); // don't want changes to models bubbling up and triggering the list's render() function + + // note we do NOT call render here anymore, because if we inherit from this class we will likely call this + // function using __super__ before the rest of the initialization logic for the decedent class. however, we may + // override the render() function in that decedent class as well, and that will certainly expect all the initialization + // to be done already. so we have to make sure to not jump the gun and start rending at this point. + // this.render(); + }, + + setOption : function( name, value ) { + + var _this = this; + + if( name === "collection" ) { + this._setCollection( value ); + } + else { + if( _.contains( kAllowedOptions, name ) ) { + + switch( name ) { + case "selectMultiple" : + this[ name ] = value; + if( !value && this.selectedItems.length > 1 ) + this.setSelectedModel( _.first( this.selectedItems ), { by : "cid" } ); + break; + case "selectable" : + if( !value && this.selectedItems.length > 0 ) + this.setSelectedModels( [] ); + this[ name ] = value; + break; + case "selectableModelsFilter" : + this[ name ] = value; + if( value && _.isFunction( value ) ) + this._validateSelection(); + break; + case "itemTemplate" : + this[ name ] = value; + this._updateItemTemplate(); + break; + case "processKeyEvents" : + this[ name ] = value; + if( value ) this.$el.attr( "tabindex", 0 ); // so we get keyboard events + break; + case "modelView" : + this[ name ] = value; + //need to remove all old view instances + this.viewManager.each( function( view ) { + _this.viewManager.remove( view ); + // destroy the View itself + view.remove(); + } ); + break; + default : + this[ name ] = value; + } + + if( _.contains( kOptionsRequiringRerendering, name ) ) this.render(); + } + else throw name + " is not an allowed option"; + } + }, + + getSelectedModel : function( options ) { + return _.first( this.getSelectedModels( options ) ); + }, + + getSelectedModels : function ( options ) { + var _this = this; + + options = _.extend( {}, { + by : kDefaultReferenceBy + }, options ); + + var referenceBy = options.by; + var items = []; + + switch( referenceBy ) { + case "id" : + _.each( this.selectedItems, function ( item ) { + items.push( _this.collection.get( item ).id ); + } ); + break; + case "cid" : + items = items.concat( this.selectedItems ); + break; + case "offset" : + var curLineNumber = 0; + + var itemElements; + if( this._isRenderedAsTable() ) + itemElements = this.$el.find( "> tbody > [data-model-cid]:not(.not-visible)" ); + else if( this._isRenderedAsList() ) + itemElements = this.$el.find( "> [data-model-cid]:not(.not-visible)" ); + + itemElements.each( function() { + var thisItemEl = $( this ); + if( thisItemEl.is( ".selected" ) ) + items.push( curLineNumber ); + curLineNumber++; + } ); + break; + case "model" : + _.each( this.selectedItems, function ( item ) { + items.push( _this.collection.get( item ) ); + } ); + break; + case "view" : + _.each( this.selectedItems, function ( item ) { + items.push( _this.viewManager.findByModel( _this.collection.get( item ) ) ); + } ); + break; + } + + return items; + + }, + + setSelectedModels : function( newSelectedItems, options ) { + if( ! this.selectable ) return; // used to throw error, but there are some circumstances in which a list can be selectable at times and not at others, don't want to have to worry about catching errors + if( ! _.isArray( newSelectedItems ) ) throw "Invalid parameter value"; + + options = _.extend( {}, { + silent : false, + by : kDefaultReferenceBy + }, options ); + + var referenceBy = options.by; + var newSelectedCids = []; + + switch( referenceBy ) { + case "cid" : + newSelectedCids = newSelectedItems; + break; + case "id" : + this.collection.each( function( thisModel ) { + if( _.contains( newSelectedItems, thisModel.id ) ) newSelectedCids.push( thisModel.cid ); + } ); + break; + case "model" : + newSelectedCids = _.pluck( newSelectedItems, "cid" ); + break; + case "view" : + _.each( newSelectedItems, function( item ) { + newSelectedCids.push( item.model.cid ); + } ); + break; + case "offset" : + var curLineNumber = 0; + var selectedItems = []; + + var itemElements; + if( this._isRenderedAsTable() ) + itemElements = this.$el.find( "> tbody > [data-model-cid]:not(.not-visible)" ); + else if( this._isRenderedAsList() ) + itemElements = this.$el.find( "> [data-model-cid]:not(.not-visible)" ); + + itemElements.each( function() { + var thisItemEl = $( this ); + if( _.contains( newSelectedItems, curLineNumber ) ) + newSelectedCids.push( thisItemEl.attr( "data-model-cid" ) ); + curLineNumber++; + } ); + break; + } + + var oldSelectedModels = this.getSelectedModels(); + var oldSelectedCids = _.clone( this.selectedItems ); + + this.selectedItems = this._convertStringsToInts( newSelectedCids ); + this._validateSelection(); + + var newSelectedModels = this.getSelectedModels(); + + if( ! this._containSameElements( oldSelectedCids, this.selectedItems ) ) + { + this._addSelectedClassToSelectedItems( oldSelectedCids ); + + if( ! options.silent ) + { + this.trigger( "selectionChanged", newSelectedModels, oldSelectedModels ); + if( this._isBackboneCourierAvailable() ) { + this.spawn( "selectionChanged", { + selectedModels : newSelectedModels, + oldSelectedModels : oldSelectedModels + } ); + } + } + + this.updateDependentControls(); + } + }, + + setSelectedModel : function( newSelectedItem, options ) { + if( ! newSelectedItem && newSelectedItem !== 0 ) + this.setSelectedModels( [], options ); + else + this.setSelectedModels( [ newSelectedItem ], options ); + }, + + render : function(){ + var _this = this; + + this._hasBeenRendered = true; + + if( this.selectable ) this._saveSelection(); + + var modelViewContainerEl; + + // If collection view element is a table and it has a tbody + // within it, render the model views inside of the tbody + if( this._isRenderedAsTable() ) { + var tbodyChild = this.$el.find( "> tbody" ); + if( tbodyChild.length > 0 ) + modelViewContainerEl = tbodyChild; + } + + if( _.isUndefined( modelViewContainerEl ) ) + modelViewContainerEl = this.$el; + + var oldViewManager = this.viewManager; + this.viewManager = new ChildViewContainer(); + + // detach each of our subviews that we have already created to represent models + // in the collection. We are going to re-use the ones that represent models that + // are still here, instead of creating new ones, so that we don't loose state + // information in the views. + oldViewManager.each( function( thisModelView ) { + // to boost performance, only detach those views that will be sticking around. + // we won't need the other ones later, so no need to detach them individually. + if( _this.collection.get( thisModelView.model.cid ) ) + thisModelView.$el.detach(); + else + thisModelView.remove(); + } ); + + modelViewContainerEl.empty(); + var fragmentContainer; + + if( this.detachedRendering ) + fragmentContainer = document.createDocumentFragment(); + + this.collection.each( function( thisModel ) { + var thisModelView; + + thisModelView = oldViewManager.findByModelCid( thisModel.cid ); + if( _.isUndefined( thisModelView ) ) { + // if the model view was not already created on previous render, + // then create and initialize it now. + + var modelViewOptions = this._getModelViewOptions( thisModel ); + thisModelView = this._createNewModelView( thisModel, modelViewOptions ); + + thisModelView.collectionListView = _this; + } + + var thisModelViewWrapped = this._wrapModelView( thisModelView ); + if( this.detachedRendering ) + fragmentContainer.appendChild( thisModelViewWrapped[0] ); + else + modelViewContainerEl.append( thisModelViewWrapped ); + + // we have to render the modelView after it has been put in context, as opposed to in the + // initialize function of the modelView, because some rendering might be dependent on + // the modelView's context in the DOM tree. For example, if the modelView stretch()'s itself, + // it must be in full context in the DOM tree or else the stretch will not behave as intended. + var renderResult = thisModelView.render(); + + // return false from the view's render function to hide this item + if( renderResult === false ) { + thisModelViewWrapped.hide(); + thisModelViewWrapped.addClass( "not-visible" ); + } + + if( _.isFunction( this.visibleModelsFilter ) ) { + if( ! this.visibleModelsFilter( thisModel ) ) { + if( thisModelViewWrapped.children().length === 1 ) + thisModelViewWrapped.hide(); + else thisModelView.$el.hide(); + + thisModelViewWrapped.addClass( "not-visible" ); + } + } + + this.viewManager.add( thisModelView ); + }, this ); + + if( this.detachedRendering ) + modelViewContainerEl.append( fragmentContainer ); + + if( this.sortable ) + { + var sortableOptions = _.extend( { + axis: "y", + distance: 10, + forcePlaceholderSize : true, + start : _.bind( this._sortStart, this ), + change : _.bind( this._sortChange, this ), + stop : _.bind( this._sortStop, this ), + receive : _.bind( this._receive, this ), + over : _.bind( this._over, this ) + }, _.result( this, "sortableOptions" ) ); + + if( _this._isRenderedAsTable() ) { + sortableOptions.items = "> tbody > tr:not(.not-sortable)"; + } + else if( _this._isRenderedAsList() ) { + sortableOptions.items = "> li:not(.not-sortable)"; + } + + this.$el = this.$el.sortable( sortableOptions ); + } + + if( this.emptyListCaption ) { + var visibleView = this.viewManager.find( function( view ) { + return ! view.$el.hasClass( "not-visible" ); + } ); + + if( _.isUndefined( visibleView ) ) { + var emptyListString; + + if( _.isFunction( this.emptyListCaption ) ) + emptyListString = this.emptyListCaption(); + else + emptyListString = this.emptyListCaption; + + var $emptyCaptionEl; + var $varEl = $( "" + emptyListString + "" ); + + //need to wrap the empty caption to make it fit the rendered list structure (either with an li or a tr td) + if( this._isRenderedAsList() ) + $emptyListCaptionEl = $varEl.wrapAll( "
  • " ).parent().css( kStylesForEmptyListCaption ); + else + $emptyListCaptionEl = $varEl.wrapAll( "" ).parent().parent().css( kStylesForEmptyListCaption ); + + this.$el.append( $emptyListCaptionEl ); + + } + } + + this.trigger( "render" ); + if( this._isBackboneCourierAvailable() ) + this.spawn( "render" ); + + if( this.selectable ) { + this._restoreSelection(); + this.updateDependentControls(); + } + + if( _.isFunction( this.onAfterRender ) ) + this.onAfterRender(); + }, + + updateDependentControls : function() { + this.trigger( "updateDependentControls", this.getSelectedModels() ); + if( this._isBackboneCourierAvailable() ) { + this.spawn( "updateDependentControls", { + selectedModels : this.getSelectedModels() + } ); + } + }, + + // Override `Backbone.View.remove` to also destroy all Views in `viewManager` + remove : function() { + this.viewManager.each( function( view ) { + view.remove(); + } ); + + Backbone.View.prototype.remove.apply( this, arguments ); + }, + + _validateSelectionAndRender : function() { + this._validateSelection(); + this.render(); + }, + + _registerCollectionEvents : function() { + this.listenTo( this.collection, "add", function() { + if( this._hasBeenRendered ) this.render(); + if( this._isBackboneCourierAvailable() ) + this.spawn( "add" ); + } ); + + this.listenTo( this.collection, "remove", function() { + if( this._hasBeenRendered ) this.render(); + if( this._isBackboneCourierAvailable() ) + this.spawn( "remove" ); + } ); + + this.listenTo( this.collection, "reset", function() { + if( this._hasBeenRendered ) this.render(); + if( this._isBackboneCourierAvailable() ) + this.spawn( "reset" ); + } ); + + // It should be up to the model to rerender itself when it changes. + // this.listenTo( this.collection, "change", function( model ) { + // if( this._hasBeenRendered ) this.viewManager.findByModel( model ).render(); + // if( this._isBackboneCourierAvailable() ) + // this.spawn( "change", { model : model } ); + // } ); + + this.listenTo( this.collection, "sort", function() { + if( this._hasBeenRendered ) this.render(); + if( this._isBackboneCourierAvailable() ) + this.spawn( "sort" ); + } ); + }, + + _getClickedItemId : function( theEvent ) { + var clickedItemId = null; + + // important to use currentTarget as opposed to target, since we could be bubbling + // an event that took place within another collectionList + var clickedItemEl = $( theEvent.currentTarget ); + if( clickedItemEl.closest( ".collection-list" ).get(0) !== this.$el.get(0) ) return; + + // determine which list item was clicked. If we clicked in the blank area + // underneath all the elements, we want to know that too, since in this + // case we will want to deselect all elements. so check to see if the clicked + // DOM element is the list itself to find that out. + var clickedItem = clickedItemEl.closest( "[data-model-cid]" ); + if( clickedItem.length > 0 ) + { + clickedItemId = clickedItem.attr( "data-model-cid" ); + if( $.isNumeric( clickedItemId ) ) clickedItemId = parseInt( clickedItemId, 10 ); + } + + return clickedItemId; + }, + + _setCollection : function( newCollection ) { + if( newCollection !== this.collection ) + { + this.stopListening( this.collection ); + this.collection = newCollection; + this._registerCollectionEvents(); + } + + if( this._hasBeenRendered ) this.render(); + }, + + _updateItemTemplate : function() { + var itemTemplateHtml; + if( this.itemTemplate ) + { + if( $( this.itemTemplate ).length === 0 ) + throw "Could not find item template from selector: " + this.itemTemplate; + + itemTemplateHtml = $( this.itemTemplate ).html(); + } + else + itemTemplateHtml = this.$( ".item-template" ).html(); + + if( itemTemplateHtml ) this.itemTemplateFunction = _.template( itemTemplateHtml ); + + }, + + _validateSelection : function() { + // note can't use the collection's proxy to underscore because "cid" ais not an attribute, + // but an element of the model object itself. + var modelReferenceIds = _.pluck( this.collection.models, "cid" ); + this.selectedItems = _.intersection( modelReferenceIds, this.selectedItems ); + + if( _.isFunction( this.selectableModelsFilter ) ) + { + this.selectedItems = _.filter( this.selectedItems, function( thisItemId ) { + return this.selectableModelsFilter.call( this, this.collection.get( thisItemId ) ); + }, this ); + } + }, + + _saveSelection : function() { + // save the current selection. use restoreSelection() to restore the selection to the state it was in the last time saveSelection() was called. + if( ! this.selectable ) throw "Attempt to save selection on non-selectable list"; + this.savedSelection = { + items : this.selectedItems, + offset : this.getSelectedModel( { by : "offset" } ) + }; + }, + + _restoreSelection : function() { + if( ! this.savedSelection ) throw "Attempt to restore selection but no selection has been saved!"; + + // reset selectedItems to empty so that we "redraw" all "selected" classes + // when we set our new selection. We do this because it is likely that our + // contents have been refreshed, and we have thus lost all old "selected" classes. + this.setSelectedModels( [], { silent : true } ); + + if( this.savedSelection.items.length > 0 ) + { + // first try to restore the old selected items using their reference ids. + this.setSelectedModels( this.savedSelection.items, { by : "cid", silent : true } ); + + // all the items with the saved reference ids have been removed from the list. + // ok. try to restore the selection based on the offset that used to be selected. + // this is the expected behavior after a item is deleted from a list (i.e. select + // the line that immediately follows the deleted line). + if( this.selectedItems.length === 0 ) + this.setSelectedModel( this.savedSelection.offset, { by : "offset" } ); + + // Trigger a selection changed if the previously selected items were not all found + if (this.selectedItems.length !== this.savedSelection.items.length) + { + this.trigger( "selectionChanged", this.getSelectedModels(), [] ); + if( this._isBackboneCourierAvailable() ) { + this.spawn( "selectionChanged", { + selectedModels : this.getSelectedModels(), + oldSelectedModels : [] + } ); + } + } + } + + delete this.savedSelection; + }, + + _addSelectedClassToSelectedItems : function( oldItemsIdsWithSelectedClass ) { + if( _.isUndefined( oldItemsIdsWithSelectedClass ) ) oldItemsIdsWithSelectedClass = []; + + // oldItemsIdsWithSelectedClass is used for optimization purposes only. If this info is supplied then we + // only have to add / remove the "selected" class from those items that "selected" state has changed. + + var itemsIdsFromWhichSelectedClassNeedsToBeRemoved = oldItemsIdsWithSelectedClass; + itemsIdsFromWhichSelectedClassNeedsToBeRemoved = _.without( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, this.selectedItems ); + + _.each( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, function( thisItemId ) { + this.$el.find( "[data-model-cid=" + thisItemId + "]" ).removeClass( "selected" ); + }, this ); + + var itemsIdsFromWhichSelectedClassNeedsToBeAdded = this.selectedItems; + itemsIdsFromWhichSelectedClassNeedsToBeAdded = _.without( itemsIdsFromWhichSelectedClassNeedsToBeAdded, oldItemsIdsWithSelectedClass ); + + _.each( itemsIdsFromWhichSelectedClassNeedsToBeAdded, function( thisItemId ) { + this.$el.find( "[data-model-cid=" + thisItemId + "]" ).addClass( "selected" ); + }, this ); + }, + + _reorderCollectionBasedOnHTML : function() { + var _this = this; + + this.$el.children().each( function() { + var thisModelCid = $( this ).attr( "data-model-cid" ); + + if( thisModelCid ) + { + // remove the current model and then add it back (at the end of the collection). + // When we are done looping through all models, they will be in the correct order. + var thisModel = _this.collection.get( thisModelCid ); + if( thisModel ) + { + _this.collection.remove( thisModel, { silent : true } ); + _this.collection.add( thisModel, { silent : true, sort : ! _this.collection.comparator } ); + } + } + } ); + + this.collection.trigger( "reorder" ); + + if( this._isBackboneCourierAvailable() ) this.spawn( "reorder" ); + + if( this.collection.comparator ) this.collection.sort(); + + }, + + _getModelViewConstructor : function( thisModel ) { + return this.modelView || mDefaultModelViewConstructor; + }, + + _getModelViewOptions : function( thisModel ) { + return _.extend( { model : thisModel }, this.modelViewOptions ); + }, + + _createNewModelView : function( model, modelViewOptions ) { + var modelViewConstructor = this._getModelViewConstructor( model ); + if( _.isUndefined( modelViewConstructor ) ) throw "Could not find modelView constructor for model"; + + return new ( modelViewConstructor )( modelViewOptions ); + }, + + _wrapModelView : function( modelView ) { + var _this = this; + + // we use items client ids as opposed to real ids, since we may not have a representation + // of these models on the server + var wrappedModelView; + + if( this._isRenderedAsTable() ) { + // if we are rendering the collection in a table, the template $el is a tr so we just need to set the data-model-cid + wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid ); + } + else if( this._isRenderedAsList() ) { + // if we are rendering the collection in a list, we need wrap each item in an
  • (if its not already an
  • ) + // and set the data-model-cid + if( modelView.$el.prop( "tagName" ).toLowerCase() === "li" ) { + wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid ); + } else { + wrappedModelView = modelView.$el.wrapAll( "
  • " ).parent(); + } + } + + if( _.isFunction( this.sortableModelsFilter ) ) + if( ! this.sortableModelsFilter.call( _this, modelView.model ) ) + wrappedModelView.addClass( "not-sortable" ); + + if( _.isFunction( this.selectableModelsFilter ) ) + if( ! this.selectableModelsFilter.call( _this, modelView.model ) ) + wrappedModelView.addClass( "not-selectable" ); + + return wrappedModelView; + }, + + _convertStringsToInts : function( theArray ) { + return _.map( theArray, function( thisEl ) { + if( ! _.isString( thisEl ) ) return thisEl; + var thisElAsNumber = parseInt( thisEl, 10 ); + return( thisElAsNumber == thisEl ? thisElAsNumber : thisEl ); + } ); + }, + + _containSameElements : function( arrayA, arrayB ) { + if( arrayA.length != arrayB.length ) return false; + var intersectionSize = _.intersection( arrayA, arrayB ).length; + return intersectionSize == arrayA.length; // and must also equal arrayB.length, since arrayA.length == arrayB.length + }, + + _isRenderedAsTable : function() { + return this.$el.prop('tagName').toLowerCase() === 'table'; + }, + + + _isRenderedAsList : function() { + return ! this._isRenderedAsTable(); + }, + + _charCodes : { + upArrow : 38, + downArrow : 40 + }, + + _isBackboneCourierAvailable : function() { + return !_.isUndefined( Backbone.Courier ); + }, + + _sortStart : function( event, ui ) { + var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) ); + this.trigger( "sortStart", modelBeingSorted ); + if( this._isBackboneCourierAvailable() ) + this.spawn( "sortStart", { modelBeingSorted : modelBeingSorted } ); + }, + + _sortChange : function( event, ui ) { + var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) ); + this.trigger( "sortChange", modelBeingSorted ); + if( this._isBackboneCourierAvailable() ) + this.spawn( "sortChange", { modelBeingSorted : modelBeingSorted } ); + }, + + _sortStop : function( event, ui ) { + var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) ); + var modelViewContainerEl = (this._isRenderedAsTable()) ? this.$el.find( "> tbody" ) : this.$el; + var newIndex = modelViewContainerEl.children().index( ui.item ); + + if( newIndex == -1 ) { + // the element was removed from this list. can happen if this sortable is connected + // to another sortable, and the item was dropped into the other sortable. + this.collection.remove( modelBeingSorted ); + } + + this._reorderCollectionBasedOnHTML(); + this.updateDependentControls(); + this.trigger( "sortStop", modelBeingSorted, newIndex ); + if( this._isBackboneCourierAvailable() ) + this.spawn( "sortStop", { modelBeingSorted : modelBeingSorted, newIndex : newIndex } ); + }, + + _receive : function( event, ui ) { + var senderListEl = ui.sender; + var senderCollectionListView = senderListEl.data( "view" ); + if( ! senderCollectionListView || ! senderCollectionListView.collection ) return; + + var newIndex = this.$el.children().index( ui.item ); + var modelReceived = senderCollectionListView.collection.get( ui.item.attr( "data-model-cid" ) ); + this.collection.add( modelReceived, { at : newIndex } ); + modelReceived.collection = this.collection; // otherwise will not get properly set, since modelReceived.collection might already have a value. + this.setSelectedModel( modelReceived ); + }, + + _over : function( event, ui ) { + // when an item is being dragged into the sortable, + // hide the empty list caption if it exists + this.$el.find( ".empty-list-caption" ).hide(); + }, + + _onKeydown : function( event ) { + if( ! this.processKeyEvents ) return true; + + var trap = false; + + if( this.getSelectedModels( { by : "offset" } ).length == 1 ) + { + // need to trap down and up arrows or else the browser + // will end up scrolling a autoscroll div. + + var currentOffset = this.getSelectedModel( { by : "offset" } ); + if( event.which === this._charCodes.upArrow && currentOffset !== 0 ) + { + this.setSelectedModel( currentOffset - 1, { by : "offset" } ); + trap = true; + } + else if( event.which === this._charCodes.downArrow && currentOffset !== this.collection.length - 1 ) + { + this.setSelectedModel( currentOffset + 1, { by : "offset" } ); + trap = true; + } + } + + return ! trap; + }, + + _listItem_onMousedown : function( theEvent ) { + if( ! this.selectable || ! this.clickToSelect ) return; + + var clickedItemId = this._getClickedItemId( theEvent ); + + if( clickedItemId ) + { + // Exit if an unselectable item was clicked + if( _.isFunction( this.selectableModelsFilter ) && + ! this.selectableModelsFilter.call( this, this.collection.get( clickedItemId ) ) ) + { + return; + } + + // a selectable list item was clicked + if( this.selectMultiple && theEvent.shiftKey ) + { + var firstSelectedItemIndex = -1; + + if( this.selectedItems.length > 0 ) + { + this.collection.find( function( thisItemModel ) { + firstSelectedItemIndex++; + + // exit when we find our first selected element + return _.contains( this.selectedItems, thisItemModel.cid ); + }, this ); + } + + var clickedItemIndex = -1; + this.collection.find( function( thisItemModel ) { + clickedItemIndex++; + + // exit when we find the clicked element + return thisItemModel.cid == clickedItemId; + }, this ); + + var shiftKeyRootSelectedItemIndex = firstSelectedItemIndex == -1 ? clickedItemIndex : firstSelectedItemIndex; + var minSelectedItemIndex = Math.min( clickedItemIndex, shiftKeyRootSelectedItemIndex ); + var maxSelectedItemIndex = Math.max( clickedItemIndex, shiftKeyRootSelectedItemIndex ); + + var newSelectedItems = []; + for( var thisIndex = minSelectedItemIndex; thisIndex <= maxSelectedItemIndex; thisIndex ++ ) + newSelectedItems.push( this.collection.at( thisIndex ).cid ); + this.setSelectedModels( newSelectedItems, { by : "cid" } ); + + // shift clicking will usually highlight selectable text, which we do not want. + // this is a cross browser (hopefully) snippet that deselects all text selection. + if( document.selection && document.selection.empty ) + document.selection.empty(); + else if(window.getSelection) { + var sel = window.getSelection(); + if( sel && sel.removeAllRanges ) + sel.removeAllRanges(); + } + } + else if( this.selectMultiple && ( this.clickToToggle || theEvent.metaKey ) ) + { + if( _.contains( this.selectedItems, clickedItemId ) ) + this.setSelectedModels( _.without( this.selectedItems, clickedItemId ), { by : "cid" } ); + else this.setSelectedModels( _.union( this.selectedItems, clickedItemId ), { by : "cid" } ); + } + else + this.setSelectedModels( [ clickedItemId ], { by : "cid" } ); + } + else + // the blank area of the list was clicked + this.setSelectedModels( [] ); + + }, + + _listItem_onDoubleClick : function( theEvent ) { + var clickedItemId = this._getClickedItemId( theEvent ); + + if( clickedItemId ) + { + var clickedModel = this.collection.get( clickedItemId ); + this.trigger( "doubleClick", clickedModel ); + if( this._isBackboneCourierAvailable() ) + this.spawn( "doubleClick", { clickedModel : clickedModel } ); + } + }, + + _listBackground_onClick : function( theEvent ) { + if( ! this.selectable ) return; + if( ! $( theEvent.target ).is( ".collection-list" ) ) return; + + this.setSelectedModels( [] ); + } + + }, { + setDefaultModelViewConstructor : function( theConstructor ) { + mDefaultModelViewConstructor = theConstructor; + } + }); + + + // Backbone.BabySitter + // ------------------- + // v0.0.6 + // + // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC. + // Distributed under MIT license + // + // http://github.com/babysitterjs/backbone.babysitter + + // Backbone.ChildViewContainer + // --------------------------- + // + // Provide a container to store, retrieve and + // shut down child views. + + ChildViewContainer = (function(Backbone, _){ + + // Container Constructor + // --------------------- + + var Container = function(views){ + this._views = {}; + this._indexByModel = {}; + this._indexByCustom = {}; + this._updateLength(); + + _.each(views, this.add, this); + }; + + // Container Methods + // ----------------- + + _.extend(Container.prototype, { + + // Add a view to this container. Stores the view + // by `cid` and makes it searchable by the model + // cid (and model itself). Optionally specify + // a custom key to store an retrieve the view. + add: function(view, customIndex){ + var viewCid = view.cid; + + // store the view + this._views[viewCid] = view; + + // index it by model + if (view.model){ + this._indexByModel[view.model.cid] = viewCid; + } + + // index by custom + if (customIndex){ + this._indexByCustom[customIndex] = viewCid; + } + + this._updateLength(); + }, + + // Find a view by the model that was attached to + // it. Uses the model's `cid` to find it. + findByModel: function(model){ + return this.findByModelCid(model.cid); + }, + + // Find a view by the `cid` of the model that was attached to + // it. Uses the model's `cid` to find the view `cid` and + // retrieve the view using it. + findByModelCid: function(modelCid){ + var viewCid = this._indexByModel[modelCid]; + return this.findByCid(viewCid); + }, + + // Find a view by a custom indexer. + findByCustom: function(index){ + var viewCid = this._indexByCustom[index]; + return this.findByCid(viewCid); + }, + + // Find by index. This is not guaranteed to be a + // stable index. + findByIndex: function(index){ + return _.values(this._views)[index]; + }, + + // retrieve a view by it's `cid` directly + findByCid: function(cid){ + return this._views[cid]; + }, + + // Remove a view + remove: function(view){ + var viewCid = view.cid; + + // delete model index + if (view.model){ + delete this._indexByModel[view.model.cid]; + } + + // delete custom index + _.any(this._indexByCustom, function(cid, key) { + if (cid === viewCid) { + delete this._indexByCustom[key]; + return true; + } + }, this); + + // remove the view from the container + delete this._views[viewCid]; + + // update the length + this._updateLength(); + }, + + // Call a method on every view in the container, + // passing parameters to the call method one at a + // time, like `function.call`. + call: function(method){ + this.apply(method, _.tail(arguments)); + }, + + // Apply a method on every view in the container, + // passing parameters to the call method one at a + // time, like `function.apply`. + apply: function(method, args){ + _.each(this._views, function(view){ + if (_.isFunction(view[method])){ + view[method].apply(view, args || []); + } + }); + }, + + // Update the `.length` attribute on this container + _updateLength: function(){ + this.length = _.size(this._views); + } + }); + + // Borrowing this code from Backbone.Collection: + // http://backbonejs.org/docs/backbone.html#section-106 + // + // Mix in methods from Underscore, for iteration, and other + // collection related features. + var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter', + 'select', 'reject', 'every', 'all', 'some', 'any', 'include', + 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest', + 'last', 'without', 'isEmpty', 'pluck']; + + _.each(methods, function(method) { + Container.prototype[method] = function() { + var views = _.values(this._views); + var args = [views].concat(_.toArray(arguments)); + return _[method].apply(_, args); + }; + }); + + // return the public API + return Container; + })(Backbone, _); +})(); \ No newline at end of file diff --git a/src/UI/JsLibraries/jquery-ui.js b/src/UI/JsLibraries/jquery-ui.js new file mode 100644 index 000000000..fe44a9c84 --- /dev/null +++ b/src/UI/JsLibraries/jquery-ui.js @@ -0,0 +1,4233 @@ +/*! jQuery UI - v1.10.4 - 2014-01-22 +* http://jqueryui.com +* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.draggable.js, jquery.ui.droppable.js, jquery.ui.sortable.js, jquery.ui.slider.js +* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ + +(function( $, undefined ) { + +var uuid = 0, + runiqueId = /^ui-id-\d+$/; + +// $.ui might exist from components with no dependencies, e.g., $.ui.position +$.ui = $.ui || {}; + +$.extend( $.ui, { + version: "1.10.4", + + keyCode: { + BACKSPACE: 8, + COMMA: 188, + DELETE: 46, + DOWN: 40, + END: 35, + ENTER: 13, + ESCAPE: 27, + HOME: 36, + LEFT: 37, + NUMPAD_ADD: 107, + NUMPAD_DECIMAL: 110, + NUMPAD_DIVIDE: 111, + NUMPAD_ENTER: 108, + NUMPAD_MULTIPLY: 106, + NUMPAD_SUBTRACT: 109, + PAGE_DOWN: 34, + PAGE_UP: 33, + PERIOD: 190, + RIGHT: 39, + SPACE: 32, + TAB: 9, + UP: 38 + } +}); + +// plugins +$.fn.extend({ + focus: (function( orig ) { + return function( delay, fn ) { + return typeof delay === "number" ? + this.each(function() { + var elem = this; + setTimeout(function() { + $( elem ).focus(); + if ( fn ) { + fn.call( elem ); + } + }, delay ); + }) : + orig.apply( this, arguments ); + }; + })( $.fn.focus ), + + scrollParent: function() { + var scrollParent; + if (($.ui.ie && (/(static|relative)/).test(this.css("position"))) || (/absolute/).test(this.css("position"))) { + scrollParent = this.parents().filter(function() { + return (/(relative|absolute|fixed)/).test($.css(this,"position")) && (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); + }).eq(0); + } else { + scrollParent = this.parents().filter(function() { + return (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); + }).eq(0); + } + + return (/fixed/).test(this.css("position")) || !scrollParent.length ? $(document) : scrollParent; + }, + + zIndex: function( zIndex ) { + if ( zIndex !== undefined ) { + return this.css( "zIndex", zIndex ); + } + + if ( this.length ) { + var elem = $( this[ 0 ] ), position, value; + while ( elem.length && elem[ 0 ] !== document ) { + // Ignore z-index if position is set to a value where z-index is ignored by the browser + // This makes behavior of this function consistent across browsers + // WebKit always returns auto if the element is positioned + position = elem.css( "position" ); + if ( position === "absolute" || position === "relative" || position === "fixed" ) { + // IE returns 0 when zIndex is not specified + // other browsers return a string + // we ignore the case of nested elements with an explicit value of 0 + //
    + value = parseInt( elem.css( "zIndex" ), 10 ); + if ( !isNaN( value ) && value !== 0 ) { + return value; + } + } + elem = elem.parent(); + } + } + + return 0; + }, + + uniqueId: function() { + return this.each(function() { + if ( !this.id ) { + this.id = "ui-id-" + (++uuid); + } + }); + }, + + removeUniqueId: function() { + return this.each(function() { + if ( runiqueId.test( this.id ) ) { + $( this ).removeAttr( "id" ); + } + }); + } +}); + +// selectors +function focusable( element, isTabIndexNotNaN ) { + var map, mapName, img, + nodeName = element.nodeName.toLowerCase(); + if ( "area" === nodeName ) { + map = element.parentNode; + mapName = map.name; + if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { + return false; + } + img = $( "img[usemap=#" + mapName + "]" )[0]; + return !!img && visible( img ); + } + return ( /input|select|textarea|button|object/.test( nodeName ) ? + !element.disabled : + "a" === nodeName ? + element.href || isTabIndexNotNaN : + isTabIndexNotNaN) && + // the element and all of its ancestors must be visible + visible( element ); +} + +function visible( element ) { + return $.expr.filters.visible( element ) && + !$( element ).parents().addBack().filter(function() { + return $.css( this, "visibility" ) === "hidden"; + }).length; +} + +$.extend( $.expr[ ":" ], { + data: $.expr.createPseudo ? + $.expr.createPseudo(function( dataName ) { + return function( elem ) { + return !!$.data( elem, dataName ); + }; + }) : + // support: jQuery <1.8 + function( elem, i, match ) { + return !!$.data( elem, match[ 3 ] ); + }, + + focusable: function( element ) { + return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) ); + }, + + tabbable: function( element ) { + var tabIndex = $.attr( element, "tabindex" ), + isTabIndexNaN = isNaN( tabIndex ); + return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN ); + } +}); + +// support: jQuery <1.8 +if ( !$( "" ).outerWidth( 1 ).jquery ) { + $.each( [ "Width", "Height" ], function( i, name ) { + var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ], + type = name.toLowerCase(), + orig = { + innerWidth: $.fn.innerWidth, + innerHeight: $.fn.innerHeight, + outerWidth: $.fn.outerWidth, + outerHeight: $.fn.outerHeight + }; + + function reduce( elem, size, border, margin ) { + $.each( side, function() { + size -= parseFloat( $.css( elem, "padding" + this ) ) || 0; + if ( border ) { + size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0; + } + if ( margin ) { + size -= parseFloat( $.css( elem, "margin" + this ) ) || 0; + } + }); + return size; + } + + $.fn[ "inner" + name ] = function( size ) { + if ( size === undefined ) { + return orig[ "inner" + name ].call( this ); + } + + return this.each(function() { + $( this ).css( type, reduce( this, size ) + "px" ); + }); + }; + + $.fn[ "outer" + name] = function( size, margin ) { + if ( typeof size !== "number" ) { + return orig[ "outer" + name ].call( this, size ); + } + + return this.each(function() { + $( this).css( type, reduce( this, size, true, margin ) + "px" ); + }); + }; + }); +} + +// support: jQuery <1.8 +if ( !$.fn.addBack ) { + $.fn.addBack = function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + }; +} + +// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413) +if ( $( "" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) { + $.fn.removeData = (function( removeData ) { + return function( key ) { + if ( arguments.length ) { + return removeData.call( this, $.camelCase( key ) ); + } else { + return removeData.call( this ); + } + }; + })( $.fn.removeData ); +} + + + + + +// deprecated +$.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); + +$.support.selectstart = "onselectstart" in document.createElement( "div" ); +$.fn.extend({ + disableSelection: function() { + return this.bind( ( $.support.selectstart ? "selectstart" : "mousedown" ) + + ".ui-disableSelection", function( event ) { + event.preventDefault(); + }); + }, + + enableSelection: function() { + return this.unbind( ".ui-disableSelection" ); + } +}); + +$.extend( $.ui, { + // $.ui.plugin is deprecated. Use $.widget() extensions instead. + plugin: { + add: function( module, option, set ) { + var i, + proto = $.ui[ module ].prototype; + for ( i in set ) { + proto.plugins[ i ] = proto.plugins[ i ] || []; + proto.plugins[ i ].push( [ option, set[ i ] ] ); + } + }, + call: function( instance, name, args ) { + var i, + set = instance.plugins[ name ]; + if ( !set || !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) { + return; + } + + for ( i = 0; i < set.length; i++ ) { + if ( instance.options[ set[ i ][ 0 ] ] ) { + set[ i ][ 1 ].apply( instance.element, args ); + } + } + } + }, + + // only used by resizable + hasScroll: function( el, a ) { + + //If overflow is hidden, the element might have extra content, but the user wants to hide it + if ( $( el ).css( "overflow" ) === "hidden") { + return false; + } + + var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop", + has = false; + + if ( el[ scroll ] > 0 ) { + return true; + } + + // TODO: determine which cases actually cause this to happen + // if the element doesn't have the scroll set, see if it's possible to + // set the scroll + el[ scroll ] = 1; + has = ( el[ scroll ] > 0 ); + el[ scroll ] = 0; + return has; + } +}); + +})( jQuery ); +(function( $, undefined ) { + +var uuid = 0, + slice = Array.prototype.slice, + _cleanData = $.cleanData; +$.cleanData = function( elems ) { + for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { + try { + $( elem ).triggerHandler( "remove" ); + // http://bugs.jquery.com/ticket/8235 + } catch( e ) {} + } + _cleanData( elems ); +}; + +$.widget = function( name, base, prototype ) { + var fullName, existingConstructor, constructor, basePrototype, + // proxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + proxiedPrototype = {}, + namespace = name.split( "." )[ 0 ]; + + name = name.split( "." )[ 1 ]; + fullName = namespace + "-" + name; + + if ( !prototype ) { + prototype = base; + base = $.Widget; + } + + // create selector for plugin + $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + return !!$.data( elem, fullName ); + }; + + $[ namespace ] = $[ namespace ] || {}; + existingConstructor = $[ namespace ][ name ]; + constructor = $[ namespace ][ name ] = function( options, element ) { + // allow instantiation without "new" keyword + if ( !this._createWidget ) { + return new constructor( options, element ); + } + + // allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if ( arguments.length ) { + this._createWidget( options, element ); + } + }; + // extend with the existing constructor to carry over any static properties + $.extend( constructor, existingConstructor, { + version: prototype.version, + // copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend( {}, prototype ), + // track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + }); + + basePrototype = new base(); + // we need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend( {}, basePrototype.options ); + $.each( prototype, function( prop, value ) { + if ( !$.isFunction( value ) ) { + proxiedPrototype[ prop ] = value; + return; + } + proxiedPrototype[ prop ] = (function() { + var _super = function() { + return base.prototype[ prop ].apply( this, arguments ); + }, + _superApply = function( args ) { + return base.prototype[ prop ].apply( this, args ); + }; + return function() { + var __super = this._super, + __superApply = this._superApply, + returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply( this, arguments ); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + })(); + }); + constructor.prototype = $.widget.extend( basePrototype, { + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name + }, proxiedPrototype, { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + }); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if ( existingConstructor ) { + $.each( existingConstructor._childConstructors, function( i, child ) { + var childPrototype = child.prototype; + + // redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); + }); + // remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push( constructor ); + } + + $.widget.bridge( name, constructor ); +}; + +$.widget.extend = function( target ) { + var input = slice.call( arguments, 1 ), + inputIndex = 0, + inputLength = input.length, + key, + value; + for ( ; inputIndex < inputLength; inputIndex++ ) { + for ( key in input[ inputIndex ] ) { + value = input[ inputIndex ][ key ]; + if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + // Clone objects + if ( $.isPlainObject( value ) ) { + target[ key ] = $.isPlainObject( target[ key ] ) ? + $.widget.extend( {}, target[ key ], value ) : + // Don't extend strings, arrays, etc. with objects + $.widget.extend( {}, value ); + // Copy everything else by reference + } else { + target[ key ] = value; + } + } + } + } + return target; +}; + +$.widget.bridge = function( name, object ) { + var fullName = object.prototype.widgetFullName || name; + $.fn[ name ] = function( options ) { + var isMethodCall = typeof options === "string", + args = slice.call( arguments, 1 ), + returnValue = this; + + // allow multiple hashes to be passed on init + options = !isMethodCall && args.length ? + $.widget.extend.apply( null, [ options ].concat(args) ) : + options; + + if ( isMethodCall ) { + this.each(function() { + var methodValue, + instance = $.data( this, fullName ); + if ( !instance ) { + return $.error( "cannot call methods on " + name + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } + if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + " widget instance" ); + } + methodValue = instance[ options ].apply( instance, args ); + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + }); + } else { + this.each(function() { + var instance = $.data( this, fullName ); + if ( instance ) { + instance.option( options || {} )._init(); + } else { + $.data( this, fullName, new object( options, this ) ); + } + }); + } + + return returnValue; + }; +}; + +$.Widget = function( /* options, element */ ) {}; +$.Widget._childConstructors = []; + +$.Widget.prototype = { + widgetName: "widget", + widgetEventPrefix: "", + defaultElement: "
    ", + options: { + disabled: false, + + // callbacks + create: null + }, + _createWidget: function( options, element ) { + element = $( element || this.defaultElement || this )[ 0 ]; + this.element = $( element ); + this.uuid = uuid++; + this.eventNamespace = "." + this.widgetName + this.uuid; + this.options = $.widget.extend( {}, + this.options, + this._getCreateOptions(), + options ); + + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + + if ( element !== this ) { + $.data( element, this.widgetFullName, this ); + this._on( true, this.element, { + remove: function( event ) { + if ( event.target === element ) { + this.destroy(); + } + } + }); + this.document = $( element.style ? + // element within the document + element.ownerDocument : + // element is window or document + element.document || element ); + this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); + } + + this._create(); + this._trigger( "create", null, this._getCreateEventData() ); + this._init(); + }, + _getCreateOptions: $.noop, + _getCreateEventData: $.noop, + _create: $.noop, + _init: $.noop, + + destroy: function() { + this._destroy(); + // we can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() + this.element + .unbind( this.eventNamespace ) + // 1.9 BC for #7810 + // TODO remove dual storage + .removeData( this.widgetName ) + .removeData( this.widgetFullName ) + // support: jquery <1.6.3 + // http://bugs.jquery.com/ticket/9413 + .removeData( $.camelCase( this.widgetFullName ) ); + this.widget() + .unbind( this.eventNamespace ) + .removeAttr( "aria-disabled" ) + .removeClass( + this.widgetFullName + "-disabled " + + "ui-state-disabled" ); + + // clean up events and states + this.bindings.unbind( this.eventNamespace ); + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + }, + _destroy: $.noop, + + widget: function() { + return this.element; + }, + + option: function( key, value ) { + var options = key, + parts, + curOption, + i; + + if ( arguments.length === 0 ) { + // don't return a reference to the internal hash + return $.widget.extend( {}, this.options ); + } + + if ( typeof key === "string" ) { + // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + options = {}; + parts = key.split( "." ); + key = parts.shift(); + if ( parts.length ) { + curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); + for ( i = 0; i < parts.length - 1; i++ ) { + curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; + curOption = curOption[ parts[ i ] ]; + } + key = parts.pop(); + if ( arguments.length === 1 ) { + return curOption[ key ] === undefined ? null : curOption[ key ]; + } + curOption[ key ] = value; + } else { + if ( arguments.length === 1 ) { + return this.options[ key ] === undefined ? null : this.options[ key ]; + } + options[ key ] = value; + } + } + + this._setOptions( options ); + + return this; + }, + _setOptions: function( options ) { + var key; + + for ( key in options ) { + this._setOption( key, options[ key ] ); + } + + return this; + }, + _setOption: function( key, value ) { + this.options[ key ] = value; + + if ( key === "disabled" ) { + this.widget() + .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) + .attr( "aria-disabled", value ); + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + } + + return this; + }, + + enable: function() { + return this._setOption( "disabled", false ); + }, + disable: function() { + return this._setOption( "disabled", true ); + }, + + _on: function( suppressDisabledCheck, element, handlers ) { + var delegateElement, + instance = this; + + // no suppressDisabledCheck flag, shuffle arguments + if ( typeof suppressDisabledCheck !== "boolean" ) { + handlers = element; + element = suppressDisabledCheck; + suppressDisabledCheck = false; + } + + // no element argument, shuffle and use this.element + if ( !handlers ) { + handlers = element; + element = this.element; + delegateElement = this.widget(); + } else { + // accept selectors, DOM elements + element = delegateElement = $( element ); + this.bindings = this.bindings.add( element ); + } + + $.each( handlers, function( event, handler ) { + function handlerProxy() { + // allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if ( !suppressDisabledCheck && + ( instance.options.disabled === true || + $( this ).hasClass( "ui-state-disabled" ) ) ) { + return; + } + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + + // copy the guid so direct unbinding works + if ( typeof handler !== "string" ) { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match( /^(\w+)\s*(.*)$/ ), + eventName = match[1] + instance.eventNamespace, + selector = match[2]; + if ( selector ) { + delegateElement.delegate( selector, eventName, handlerProxy ); + } else { + element.bind( eventName, handlerProxy ); + } + }); + }, + + _off: function( element, eventName ) { + eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; + element.unbind( eventName ).undelegate( eventName ); + }, + + _delay: function( handler, delay ) { + function handlerProxy() { + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + var instance = this; + return setTimeout( handlerProxy, delay || 0 ); + }, + + _hoverable: function( element ) { + this.hoverable = this.hoverable.add( element ); + this._on( element, { + mouseenter: function( event ) { + $( event.currentTarget ).addClass( "ui-state-hover" ); + }, + mouseleave: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-hover" ); + } + }); + }, + + _focusable: function( element ) { + this.focusable = this.focusable.add( element ); + this._on( element, { + focusin: function( event ) { + $( event.currentTarget ).addClass( "ui-state-focus" ); + }, + focusout: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-focus" ); + } + }); + }, + + _trigger: function( type, event, data ) { + var prop, orig, + callback = this.options[ type ]; + + data = data || {}; + event = $.Event( event ); + event.type = ( type === this.widgetEventPrefix ? + type : + this.widgetEventPrefix + type ).toLowerCase(); + // the original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[ 0 ]; + + // copy original event properties over to the new event + orig = event.originalEvent; + if ( orig ) { + for ( prop in orig ) { + if ( !( prop in event ) ) { + event[ prop ] = orig[ prop ]; + } + } + } + + this.element.trigger( event, data ); + return !( $.isFunction( callback ) && + callback.apply( this.element[0], [ event ].concat( data ) ) === false || + event.isDefaultPrevented() ); + } +}; + +$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { + $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { + if ( typeof options === "string" ) { + options = { effect: options }; + } + var hasOptions, + effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + options = options || {}; + if ( typeof options === "number" ) { + options = { duration: options }; + } + hasOptions = !$.isEmptyObject( options ); + options.complete = callback; + if ( options.delay ) { + element.delay( options.delay ); + } + if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { + element[ method ]( options ); + } else if ( effectName !== method && element[ effectName ] ) { + element[ effectName ]( options.duration, options.easing, callback ); + } else { + element.queue(function( next ) { + $( this )[ method ](); + if ( callback ) { + callback.call( element[ 0 ] ); + } + next(); + }); + } + }; +}); + +})( jQuery ); +(function( $, undefined ) { + +var mouseHandled = false; +$( document ).mouseup( function() { + mouseHandled = false; +}); + +$.widget("ui.mouse", { + version: "1.10.4", + options: { + cancel: "input,textarea,button,select,option", + distance: 1, + delay: 0 + }, + _mouseInit: function() { + var that = this; + + this.element + .bind("mousedown."+this.widgetName, function(event) { + return that._mouseDown(event); + }) + .bind("click."+this.widgetName, function(event) { + if (true === $.data(event.target, that.widgetName + ".preventClickEvent")) { + $.removeData(event.target, that.widgetName + ".preventClickEvent"); + event.stopImmediatePropagation(); + return false; + } + }); + + this.started = false; + }, + + // TODO: make sure destroying one instance of mouse doesn't mess with + // other instances of mouse + _mouseDestroy: function() { + this.element.unbind("."+this.widgetName); + if ( this._mouseMoveDelegate ) { + $(document) + .unbind("mousemove."+this.widgetName, this._mouseMoveDelegate) + .unbind("mouseup."+this.widgetName, this._mouseUpDelegate); + } + }, + + _mouseDown: function(event) { + // don't let more than one widget handle mouseStart + if( mouseHandled ) { return; } + + // we may have missed mouseup (out of window) + (this._mouseStarted && this._mouseUp(event)); + + this._mouseDownEvent = event; + + var that = this, + btnIsLeft = (event.which === 1), + // event.target.nodeName works around a bug in IE 8 with + // disabled inputs (#7620) + elIsCancel = (typeof this.options.cancel === "string" && event.target.nodeName ? $(event.target).closest(this.options.cancel).length : false); + if (!btnIsLeft || elIsCancel || !this._mouseCapture(event)) { + return true; + } + + this.mouseDelayMet = !this.options.delay; + if (!this.mouseDelayMet) { + this._mouseDelayTimer = setTimeout(function() { + that.mouseDelayMet = true; + }, this.options.delay); + } + + if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { + this._mouseStarted = (this._mouseStart(event) !== false); + if (!this._mouseStarted) { + event.preventDefault(); + return true; + } + } + + // Click event may never have fired (Gecko & Opera) + if (true === $.data(event.target, this.widgetName + ".preventClickEvent")) { + $.removeData(event.target, this.widgetName + ".preventClickEvent"); + } + + // these delegates are required to keep context + this._mouseMoveDelegate = function(event) { + return that._mouseMove(event); + }; + this._mouseUpDelegate = function(event) { + return that._mouseUp(event); + }; + $(document) + .bind("mousemove."+this.widgetName, this._mouseMoveDelegate) + .bind("mouseup."+this.widgetName, this._mouseUpDelegate); + + event.preventDefault(); + + mouseHandled = true; + return true; + }, + + _mouseMove: function(event) { + // IE mouseup check - mouseup happened when mouse was out of window + if ($.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && !event.button) { + return this._mouseUp(event); + } + + if (this._mouseStarted) { + this._mouseDrag(event); + return event.preventDefault(); + } + + if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { + this._mouseStarted = + (this._mouseStart(this._mouseDownEvent, event) !== false); + (this._mouseStarted ? this._mouseDrag(event) : this._mouseUp(event)); + } + + return !this._mouseStarted; + }, + + _mouseUp: function(event) { + $(document) + .unbind("mousemove."+this.widgetName, this._mouseMoveDelegate) + .unbind("mouseup."+this.widgetName, this._mouseUpDelegate); + + if (this._mouseStarted) { + this._mouseStarted = false; + + if (event.target === this._mouseDownEvent.target) { + $.data(event.target, this.widgetName + ".preventClickEvent", true); + } + + this._mouseStop(event); + } + + return false; + }, + + _mouseDistanceMet: function(event) { + return (Math.max( + Math.abs(this._mouseDownEvent.pageX - event.pageX), + Math.abs(this._mouseDownEvent.pageY - event.pageY) + ) >= this.options.distance + ); + }, + + _mouseDelayMet: function(/* event */) { + return this.mouseDelayMet; + }, + + // These are placeholder methods, to be overriden by extending plugin + _mouseStart: function(/* event */) {}, + _mouseDrag: function(/* event */) {}, + _mouseStop: function(/* event */) {}, + _mouseCapture: function(/* event */) { return true; } +}); + +})(jQuery); +(function( $, undefined ) { + +$.widget("ui.draggable", $.ui.mouse, { + version: "1.10.4", + widgetEventPrefix: "drag", + options: { + addClasses: true, + appendTo: "parent", + axis: false, + connectToSortable: false, + containment: false, + cursor: "auto", + cursorAt: false, + grid: false, + handle: false, + helper: "original", + iframeFix: false, + opacity: false, + refreshPositions: false, + revert: false, + revertDuration: 500, + scope: "default", + scroll: true, + scrollSensitivity: 20, + scrollSpeed: 20, + snap: false, + snapMode: "both", + snapTolerance: 20, + stack: false, + zIndex: false, + + // callbacks + drag: null, + start: null, + stop: null + }, + _create: function() { + + if (this.options.helper === "original" && !(/^(?:r|a|f)/).test(this.element.css("position"))) { + this.element[0].style.position = "relative"; + } + if (this.options.addClasses){ + this.element.addClass("ui-draggable"); + } + if (this.options.disabled){ + this.element.addClass("ui-draggable-disabled"); + } + + this._mouseInit(); + + }, + + _destroy: function() { + this.element.removeClass( "ui-draggable ui-draggable-dragging ui-draggable-disabled" ); + this._mouseDestroy(); + }, + + _mouseCapture: function(event) { + + var o = this.options; + + // among others, prevent a drag on a resizable-handle + if (this.helper || o.disabled || $(event.target).closest(".ui-resizable-handle").length > 0) { + return false; + } + + //Quit if we're not on a valid handle + this.handle = this._getHandle(event); + if (!this.handle) { + return false; + } + + $(o.iframeFix === true ? "iframe" : o.iframeFix).each(function() { + $("
    ") + .css({ + width: this.offsetWidth+"px", height: this.offsetHeight+"px", + position: "absolute", opacity: "0.001", zIndex: 1000 + }) + .css($(this).offset()) + .appendTo("body"); + }); + + return true; + + }, + + _mouseStart: function(event) { + + var o = this.options; + + //Create and append the visible helper + this.helper = this._createHelper(event); + + this.helper.addClass("ui-draggable-dragging"); + + //Cache the helper size + this._cacheHelperProportions(); + + //If ddmanager is used for droppables, set the global draggable + if($.ui.ddmanager) { + $.ui.ddmanager.current = this; + } + + /* + * - Position generation - + * This block generates everything position related - it's the core of draggables. + */ + + //Cache the margins of the original element + this._cacheMargins(); + + //Store the helper's css position + this.cssPosition = this.helper.css( "position" ); + this.scrollParent = this.helper.scrollParent(); + this.offsetParent = this.helper.offsetParent(); + this.offsetParentCssPosition = this.offsetParent.css( "position" ); + + //The element's absolute position on the page minus margins + this.offset = this.positionAbs = this.element.offset(); + this.offset = { + top: this.offset.top - this.margins.top, + left: this.offset.left - this.margins.left + }; + + //Reset scroll cache + this.offset.scroll = false; + + $.extend(this.offset, { + click: { //Where the click happened, relative to the element + left: event.pageX - this.offset.left, + top: event.pageY - this.offset.top + }, + parent: this._getParentOffset(), + relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper + }); + + //Generate the original position + this.originalPosition = this.position = this._generatePosition(event); + this.originalPageX = event.pageX; + this.originalPageY = event.pageY; + + //Adjust the mouse offset relative to the helper if "cursorAt" is supplied + (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt)); + + //Set a containment if given in the options + this._setContainment(); + + //Trigger event + callbacks + if(this._trigger("start", event) === false) { + this._clear(); + return false; + } + + //Recache the helper size + this._cacheHelperProportions(); + + //Prepare the droppable offsets + if ($.ui.ddmanager && !o.dropBehaviour) { + $.ui.ddmanager.prepareOffsets(this, event); + } + + + this._mouseDrag(event, true); //Execute the drag once - this causes the helper not to be visible before getting its correct position + + //If the ddmanager is used for droppables, inform the manager that dragging has started (see #5003) + if ( $.ui.ddmanager ) { + $.ui.ddmanager.dragStart(this, event); + } + + return true; + }, + + _mouseDrag: function(event, noPropagation) { + // reset any necessary cached properties (see #5009) + if ( this.offsetParentCssPosition === "fixed" ) { + this.offset.parent = this._getParentOffset(); + } + + //Compute the helpers position + this.position = this._generatePosition(event); + this.positionAbs = this._convertPositionTo("absolute"); + + //Call plugins and callbacks and use the resulting position if something is returned + if (!noPropagation) { + var ui = this._uiHash(); + if(this._trigger("drag", event, ui) === false) { + this._mouseUp({}); + return false; + } + this.position = ui.position; + } + + if(!this.options.axis || this.options.axis !== "y") { + this.helper[0].style.left = this.position.left+"px"; + } + if(!this.options.axis || this.options.axis !== "x") { + this.helper[0].style.top = this.position.top+"px"; + } + if($.ui.ddmanager) { + $.ui.ddmanager.drag(this, event); + } + + return false; + }, + + _mouseStop: function(event) { + + //If we are using droppables, inform the manager about the drop + var that = this, + dropped = false; + if ($.ui.ddmanager && !this.options.dropBehaviour) { + dropped = $.ui.ddmanager.drop(this, event); + } + + //if a drop comes from outside (a sortable) + if(this.dropped) { + dropped = this.dropped; + this.dropped = false; + } + + //if the original element is no longer in the DOM don't bother to continue (see #8269) + if ( this.options.helper === "original" && !$.contains( this.element[ 0 ].ownerDocument, this.element[ 0 ] ) ) { + return false; + } + + if((this.options.revert === "invalid" && !dropped) || (this.options.revert === "valid" && dropped) || this.options.revert === true || ($.isFunction(this.options.revert) && this.options.revert.call(this.element, dropped))) { + $(this.helper).animate(this.originalPosition, parseInt(this.options.revertDuration, 10), function() { + if(that._trigger("stop", event) !== false) { + that._clear(); + } + }); + } else { + if(this._trigger("stop", event) !== false) { + this._clear(); + } + } + + return false; + }, + + _mouseUp: function(event) { + //Remove frame helpers + $("div.ui-draggable-iframeFix").each(function() { + this.parentNode.removeChild(this); + }); + + //If the ddmanager is used for droppables, inform the manager that dragging has stopped (see #5003) + if( $.ui.ddmanager ) { + $.ui.ddmanager.dragStop(this, event); + } + + return $.ui.mouse.prototype._mouseUp.call(this, event); + }, + + cancel: function() { + + if(this.helper.is(".ui-draggable-dragging")) { + this._mouseUp({}); + } else { + this._clear(); + } + + return this; + + }, + + _getHandle: function(event) { + return this.options.handle ? + !!$( event.target ).closest( this.element.find( this.options.handle ) ).length : + true; + }, + + _createHelper: function(event) { + + var o = this.options, + helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event])) : (o.helper === "clone" ? this.element.clone().removeAttr("id") : this.element); + + if(!helper.parents("body").length) { + helper.appendTo((o.appendTo === "parent" ? this.element[0].parentNode : o.appendTo)); + } + + if(helper[0] !== this.element[0] && !(/(fixed|absolute)/).test(helper.css("position"))) { + helper.css("position", "absolute"); + } + + return helper; + + }, + + _adjustOffsetFromHelper: function(obj) { + if (typeof obj === "string") { + obj = obj.split(" "); + } + if ($.isArray(obj)) { + obj = {left: +obj[0], top: +obj[1] || 0}; + } + if ("left" in obj) { + this.offset.click.left = obj.left + this.margins.left; + } + if ("right" in obj) { + this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; + } + if ("top" in obj) { + this.offset.click.top = obj.top + this.margins.top; + } + if ("bottom" in obj) { + this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; + } + }, + + _getParentOffset: function() { + + //Get the offsetParent and cache its position + var po = this.offsetParent.offset(); + + // This is a special case where we need to modify a offset calculated on start, since the following happened: + // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent + // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that + // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag + if(this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) { + po.left += this.scrollParent.scrollLeft(); + po.top += this.scrollParent.scrollTop(); + } + + //This needs to be actually done for all browsers, since pageX/pageY includes this information + //Ugly IE fix + if((this.offsetParent[0] === document.body) || + (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) { + po = { top: 0, left: 0 }; + } + + return { + top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0), + left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0) + }; + + }, + + _getRelativeOffset: function() { + + if(this.cssPosition === "relative") { + var p = this.element.position(); + return { + top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(), + left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft() + }; + } else { + return { top: 0, left: 0 }; + } + + }, + + _cacheMargins: function() { + this.margins = { + left: (parseInt(this.element.css("marginLeft"),10) || 0), + top: (parseInt(this.element.css("marginTop"),10) || 0), + right: (parseInt(this.element.css("marginRight"),10) || 0), + bottom: (parseInt(this.element.css("marginBottom"),10) || 0) + }; + }, + + _cacheHelperProportions: function() { + this.helperProportions = { + width: this.helper.outerWidth(), + height: this.helper.outerHeight() + }; + }, + + _setContainment: function() { + + var over, c, ce, + o = this.options; + + if ( !o.containment ) { + this.containment = null; + return; + } + + if ( o.containment === "window" ) { + this.containment = [ + $( window ).scrollLeft() - this.offset.relative.left - this.offset.parent.left, + $( window ).scrollTop() - this.offset.relative.top - this.offset.parent.top, + $( window ).scrollLeft() + $( window ).width() - this.helperProportions.width - this.margins.left, + $( window ).scrollTop() + ( $( window ).height() || document.body.parentNode.scrollHeight ) - this.helperProportions.height - this.margins.top + ]; + return; + } + + if ( o.containment === "document") { + this.containment = [ + 0, + 0, + $( document ).width() - this.helperProportions.width - this.margins.left, + ( $( document ).height() || document.body.parentNode.scrollHeight ) - this.helperProportions.height - this.margins.top + ]; + return; + } + + if ( o.containment.constructor === Array ) { + this.containment = o.containment; + return; + } + + if ( o.containment === "parent" ) { + o.containment = this.helper[ 0 ].parentNode; + } + + c = $( o.containment ); + ce = c[ 0 ]; + + if( !ce ) { + return; + } + + over = c.css( "overflow" ) !== "hidden"; + + this.containment = [ + ( parseInt( c.css( "borderLeftWidth" ), 10 ) || 0 ) + ( parseInt( c.css( "paddingLeft" ), 10 ) || 0 ), + ( parseInt( c.css( "borderTopWidth" ), 10 ) || 0 ) + ( parseInt( c.css( "paddingTop" ), 10 ) || 0 ) , + ( over ? Math.max( ce.scrollWidth, ce.offsetWidth ) : ce.offsetWidth ) - ( parseInt( c.css( "borderRightWidth" ), 10 ) || 0 ) - ( parseInt( c.css( "paddingRight" ), 10 ) || 0 ) - this.helperProportions.width - this.margins.left - this.margins.right, + ( over ? Math.max( ce.scrollHeight, ce.offsetHeight ) : ce.offsetHeight ) - ( parseInt( c.css( "borderBottomWidth" ), 10 ) || 0 ) - ( parseInt( c.css( "paddingBottom" ), 10 ) || 0 ) - this.helperProportions.height - this.margins.top - this.margins.bottom + ]; + this.relative_container = c; + }, + + _convertPositionTo: function(d, pos) { + + if(!pos) { + pos = this.position; + } + + var mod = d === "absolute" ? 1 : -1, + scroll = this.cssPosition === "absolute" && !( this.scrollParent[ 0 ] !== document && $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? this.offsetParent : this.scrollParent; + + //Cache the scroll + if (!this.offset.scroll) { + this.offset.scroll = {top : scroll.scrollTop(), left : scroll.scrollLeft()}; + } + + return { + top: ( + pos.top + // The absolute mouse position + this.offset.relative.top * mod + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.top * mod - // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : this.offset.scroll.top ) * mod ) + ), + left: ( + pos.left + // The absolute mouse position + this.offset.relative.left * mod + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.left * mod - // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : this.offset.scroll.left ) * mod ) + ) + }; + + }, + + _generatePosition: function(event) { + + var containment, co, top, left, + o = this.options, + scroll = this.cssPosition === "absolute" && !( this.scrollParent[ 0 ] !== document && $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? this.offsetParent : this.scrollParent, + pageX = event.pageX, + pageY = event.pageY; + + //Cache the scroll + if (!this.offset.scroll) { + this.offset.scroll = {top : scroll.scrollTop(), left : scroll.scrollLeft()}; + } + + /* + * - Position constraining - + * Constrain the position to a mix of grid, containment. + */ + + // If we are not dragging yet, we won't check for options + if ( this.originalPosition ) { + if ( this.containment ) { + if ( this.relative_container ){ + co = this.relative_container.offset(); + containment = [ + this.containment[ 0 ] + co.left, + this.containment[ 1 ] + co.top, + this.containment[ 2 ] + co.left, + this.containment[ 3 ] + co.top + ]; + } + else { + containment = this.containment; + } + + if(event.pageX - this.offset.click.left < containment[0]) { + pageX = containment[0] + this.offset.click.left; + } + if(event.pageY - this.offset.click.top < containment[1]) { + pageY = containment[1] + this.offset.click.top; + } + if(event.pageX - this.offset.click.left > containment[2]) { + pageX = containment[2] + this.offset.click.left; + } + if(event.pageY - this.offset.click.top > containment[3]) { + pageY = containment[3] + this.offset.click.top; + } + } + + if(o.grid) { + //Check for grid elements set to 0 to prevent divide by 0 error causing invalid argument errors in IE (see ticket #6950) + top = o.grid[1] ? this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1] : this.originalPageY; + pageY = containment ? ((top - this.offset.click.top >= containment[1] || top - this.offset.click.top > containment[3]) ? top : ((top - this.offset.click.top >= containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top; + + left = o.grid[0] ? this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0] : this.originalPageX; + pageX = containment ? ((left - this.offset.click.left >= containment[0] || left - this.offset.click.left > containment[2]) ? left : ((left - this.offset.click.left >= containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left; + } + + } + + return { + top: ( + pageY - // The absolute mouse position + this.offset.click.top - // Click offset (relative to the element) + this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.top + // The offsetParent's offset without borders (offset + border) + ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : this.offset.scroll.top ) + ), + left: ( + pageX - // The absolute mouse position + this.offset.click.left - // Click offset (relative to the element) + this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.left + // The offsetParent's offset without borders (offset + border) + ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : this.offset.scroll.left ) + ) + }; + + }, + + _clear: function() { + this.helper.removeClass("ui-draggable-dragging"); + if(this.helper[0] !== this.element[0] && !this.cancelHelperRemoval) { + this.helper.remove(); + } + this.helper = null; + this.cancelHelperRemoval = false; + }, + + // From now on bulk stuff - mainly helpers + + _trigger: function(type, event, ui) { + ui = ui || this._uiHash(); + $.ui.plugin.call(this, type, [event, ui]); + //The absolute position has to be recalculated after plugins + if(type === "drag") { + this.positionAbs = this._convertPositionTo("absolute"); + } + return $.Widget.prototype._trigger.call(this, type, event, ui); + }, + + plugins: {}, + + _uiHash: function() { + return { + helper: this.helper, + position: this.position, + originalPosition: this.originalPosition, + offset: this.positionAbs + }; + } + +}); + +$.ui.plugin.add("draggable", "connectToSortable", { + start: function(event, ui) { + + var inst = $(this).data("ui-draggable"), o = inst.options, + uiSortable = $.extend({}, ui, { item: inst.element }); + inst.sortables = []; + $(o.connectToSortable).each(function() { + var sortable = $.data(this, "ui-sortable"); + if (sortable && !sortable.options.disabled) { + inst.sortables.push({ + instance: sortable, + shouldRevert: sortable.options.revert + }); + sortable.refreshPositions(); // Call the sortable's refreshPositions at drag start to refresh the containerCache since the sortable container cache is used in drag and needs to be up to date (this will ensure it's initialised as well as being kept in step with any changes that might have happened on the page). + sortable._trigger("activate", event, uiSortable); + } + }); + + }, + stop: function(event, ui) { + + //If we are still over the sortable, we fake the stop event of the sortable, but also remove helper + var inst = $(this).data("ui-draggable"), + uiSortable = $.extend({}, ui, { item: inst.element }); + + $.each(inst.sortables, function() { + if(this.instance.isOver) { + + this.instance.isOver = 0; + + inst.cancelHelperRemoval = true; //Don't remove the helper in the draggable instance + this.instance.cancelHelperRemoval = false; //Remove it in the sortable instance (so sortable plugins like revert still work) + + //The sortable revert is supported, and we have to set a temporary dropped variable on the draggable to support revert: "valid/invalid" + if(this.shouldRevert) { + this.instance.options.revert = this.shouldRevert; + } + + //Trigger the stop of the sortable + this.instance._mouseStop(event); + + this.instance.options.helper = this.instance.options._helper; + + //If the helper has been the original item, restore properties in the sortable + if(inst.options.helper === "original") { + this.instance.currentItem.css({ top: "auto", left: "auto" }); + } + + } else { + this.instance.cancelHelperRemoval = false; //Remove the helper in the sortable instance + this.instance._trigger("deactivate", event, uiSortable); + } + + }); + + }, + drag: function(event, ui) { + + var inst = $(this).data("ui-draggable"), that = this; + + $.each(inst.sortables, function() { + + var innermostIntersecting = false, + thisSortable = this; + + //Copy over some variables to allow calling the sortable's native _intersectsWith + this.instance.positionAbs = inst.positionAbs; + this.instance.helperProportions = inst.helperProportions; + this.instance.offset.click = inst.offset.click; + + if(this.instance._intersectsWith(this.instance.containerCache)) { + innermostIntersecting = true; + $.each(inst.sortables, function () { + this.instance.positionAbs = inst.positionAbs; + this.instance.helperProportions = inst.helperProportions; + this.instance.offset.click = inst.offset.click; + if (this !== thisSortable && + this.instance._intersectsWith(this.instance.containerCache) && + $.contains(thisSortable.instance.element[0], this.instance.element[0]) + ) { + innermostIntersecting = false; + } + return innermostIntersecting; + }); + } + + + if(innermostIntersecting) { + //If it intersects, we use a little isOver variable and set it once, so our move-in stuff gets fired only once + if(!this.instance.isOver) { + + this.instance.isOver = 1; + //Now we fake the start of dragging for the sortable instance, + //by cloning the list group item, appending it to the sortable and using it as inst.currentItem + //We can then fire the start event of the sortable with our passed browser event, and our own helper (so it doesn't create a new one) + this.instance.currentItem = $(that).clone().removeAttr("id").appendTo(this.instance.element).data("ui-sortable-item", true); + this.instance.options._helper = this.instance.options.helper; //Store helper option to later restore it + this.instance.options.helper = function() { return ui.helper[0]; }; + + event.target = this.instance.currentItem[0]; + this.instance._mouseCapture(event, true); + this.instance._mouseStart(event, true, true); + + //Because the browser event is way off the new appended portlet, we modify a couple of variables to reflect the changes + this.instance.offset.click.top = inst.offset.click.top; + this.instance.offset.click.left = inst.offset.click.left; + this.instance.offset.parent.left -= inst.offset.parent.left - this.instance.offset.parent.left; + this.instance.offset.parent.top -= inst.offset.parent.top - this.instance.offset.parent.top; + + inst._trigger("toSortable", event); + inst.dropped = this.instance.element; //draggable revert needs that + //hack so receive/update callbacks work (mostly) + inst.currentItem = inst.element; + this.instance.fromOutside = inst; + + } + + //Provided we did all the previous steps, we can fire the drag event of the sortable on every draggable drag, when it intersects with the sortable + if(this.instance.currentItem) { + this.instance._mouseDrag(event); + } + + } else { + + //If it doesn't intersect with the sortable, and it intersected before, + //we fake the drag stop of the sortable, but make sure it doesn't remove the helper by using cancelHelperRemoval + if(this.instance.isOver) { + + this.instance.isOver = 0; + this.instance.cancelHelperRemoval = true; + + //Prevent reverting on this forced stop + this.instance.options.revert = false; + + // The out event needs to be triggered independently + this.instance._trigger("out", event, this.instance._uiHash(this.instance)); + + this.instance._mouseStop(event, true); + this.instance.options.helper = this.instance.options._helper; + + //Now we remove our currentItem, the list group clone again, and the placeholder, and animate the helper back to it's original size + this.instance.currentItem.remove(); + if(this.instance.placeholder) { + this.instance.placeholder.remove(); + } + + inst._trigger("fromSortable", event); + inst.dropped = false; //draggable revert needs that + } + + } + + }); + + } +}); + +$.ui.plugin.add("draggable", "cursor", { + start: function() { + var t = $("body"), o = $(this).data("ui-draggable").options; + if (t.css("cursor")) { + o._cursor = t.css("cursor"); + } + t.css("cursor", o.cursor); + }, + stop: function() { + var o = $(this).data("ui-draggable").options; + if (o._cursor) { + $("body").css("cursor", o._cursor); + } + } +}); + +$.ui.plugin.add("draggable", "opacity", { + start: function(event, ui) { + var t = $(ui.helper), o = $(this).data("ui-draggable").options; + if(t.css("opacity")) { + o._opacity = t.css("opacity"); + } + t.css("opacity", o.opacity); + }, + stop: function(event, ui) { + var o = $(this).data("ui-draggable").options; + if(o._opacity) { + $(ui.helper).css("opacity", o._opacity); + } + } +}); + +$.ui.plugin.add("draggable", "scroll", { + start: function() { + var i = $(this).data("ui-draggable"); + if(i.scrollParent[0] !== document && i.scrollParent[0].tagName !== "HTML") { + i.overflowOffset = i.scrollParent.offset(); + } + }, + drag: function( event ) { + + var i = $(this).data("ui-draggable"), o = i.options, scrolled = false; + + if(i.scrollParent[0] !== document && i.scrollParent[0].tagName !== "HTML") { + + if(!o.axis || o.axis !== "x") { + if((i.overflowOffset.top + i.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) { + i.scrollParent[0].scrollTop = scrolled = i.scrollParent[0].scrollTop + o.scrollSpeed; + } else if(event.pageY - i.overflowOffset.top < o.scrollSensitivity) { + i.scrollParent[0].scrollTop = scrolled = i.scrollParent[0].scrollTop - o.scrollSpeed; + } + } + + if(!o.axis || o.axis !== "y") { + if((i.overflowOffset.left + i.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) { + i.scrollParent[0].scrollLeft = scrolled = i.scrollParent[0].scrollLeft + o.scrollSpeed; + } else if(event.pageX - i.overflowOffset.left < o.scrollSensitivity) { + i.scrollParent[0].scrollLeft = scrolled = i.scrollParent[0].scrollLeft - o.scrollSpeed; + } + } + + } else { + + if(!o.axis || o.axis !== "x") { + if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) { + scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); + } else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) { + scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); + } + } + + if(!o.axis || o.axis !== "y") { + if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) { + scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); + } else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) { + scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); + } + } + + } + + if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { + $.ui.ddmanager.prepareOffsets(i, event); + } + + } +}); + +$.ui.plugin.add("draggable", "snap", { + start: function() { + + var i = $(this).data("ui-draggable"), + o = i.options; + + i.snapElements = []; + + $(o.snap.constructor !== String ? ( o.snap.items || ":data(ui-draggable)" ) : o.snap).each(function() { + var $t = $(this), + $o = $t.offset(); + if(this !== i.element[0]) { + i.snapElements.push({ + item: this, + width: $t.outerWidth(), height: $t.outerHeight(), + top: $o.top, left: $o.left + }); + } + }); + + }, + drag: function(event, ui) { + + var ts, bs, ls, rs, l, r, t, b, i, first, + inst = $(this).data("ui-draggable"), + o = inst.options, + d = o.snapTolerance, + x1 = ui.offset.left, x2 = x1 + inst.helperProportions.width, + y1 = ui.offset.top, y2 = y1 + inst.helperProportions.height; + + for (i = inst.snapElements.length - 1; i >= 0; i--){ + + l = inst.snapElements[i].left; + r = l + inst.snapElements[i].width; + t = inst.snapElements[i].top; + b = t + inst.snapElements[i].height; + + if ( x2 < l - d || x1 > r + d || y2 < t - d || y1 > b + d || !$.contains( inst.snapElements[ i ].item.ownerDocument, inst.snapElements[ i ].item ) ) { + if(inst.snapElements[i].snapping) { + (inst.options.snap.release && inst.options.snap.release.call(inst.element, event, $.extend(inst._uiHash(), { snapItem: inst.snapElements[i].item }))); + } + inst.snapElements[i].snapping = false; + continue; + } + + if(o.snapMode !== "inner") { + ts = Math.abs(t - y2) <= d; + bs = Math.abs(b - y1) <= d; + ls = Math.abs(l - x2) <= d; + rs = Math.abs(r - x1) <= d; + if(ts) { + ui.position.top = inst._convertPositionTo("relative", { top: t - inst.helperProportions.height, left: 0 }).top - inst.margins.top; + } + if(bs) { + ui.position.top = inst._convertPositionTo("relative", { top: b, left: 0 }).top - inst.margins.top; + } + if(ls) { + ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l - inst.helperProportions.width }).left - inst.margins.left; + } + if(rs) { + ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r }).left - inst.margins.left; + } + } + + first = (ts || bs || ls || rs); + + if(o.snapMode !== "outer") { + ts = Math.abs(t - y1) <= d; + bs = Math.abs(b - y2) <= d; + ls = Math.abs(l - x1) <= d; + rs = Math.abs(r - x2) <= d; + if(ts) { + ui.position.top = inst._convertPositionTo("relative", { top: t, left: 0 }).top - inst.margins.top; + } + if(bs) { + ui.position.top = inst._convertPositionTo("relative", { top: b - inst.helperProportions.height, left: 0 }).top - inst.margins.top; + } + if(ls) { + ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l }).left - inst.margins.left; + } + if(rs) { + ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r - inst.helperProportions.width }).left - inst.margins.left; + } + } + + if(!inst.snapElements[i].snapping && (ts || bs || ls || rs || first)) { + (inst.options.snap.snap && inst.options.snap.snap.call(inst.element, event, $.extend(inst._uiHash(), { snapItem: inst.snapElements[i].item }))); + } + inst.snapElements[i].snapping = (ts || bs || ls || rs || first); + + } + + } +}); + +$.ui.plugin.add("draggable", "stack", { + start: function() { + var min, + o = this.data("ui-draggable").options, + group = $.makeArray($(o.stack)).sort(function(a,b) { + return (parseInt($(a).css("zIndex"),10) || 0) - (parseInt($(b).css("zIndex"),10) || 0); + }); + + if (!group.length) { return; } + + min = parseInt($(group[0]).css("zIndex"), 10) || 0; + $(group).each(function(i) { + $(this).css("zIndex", min + i); + }); + this.css("zIndex", (min + group.length)); + } +}); + +$.ui.plugin.add("draggable", "zIndex", { + start: function(event, ui) { + var t = $(ui.helper), o = $(this).data("ui-draggable").options; + if(t.css("zIndex")) { + o._zIndex = t.css("zIndex"); + } + t.css("zIndex", o.zIndex); + }, + stop: function(event, ui) { + var o = $(this).data("ui-draggable").options; + if(o._zIndex) { + $(ui.helper).css("zIndex", o._zIndex); + } + } +}); + +})(jQuery); +(function( $, undefined ) { + +function isOverAxis( x, reference, size ) { + return ( x > reference ) && ( x < ( reference + size ) ); +} + +$.widget("ui.droppable", { + version: "1.10.4", + widgetEventPrefix: "drop", + options: { + accept: "*", + activeClass: false, + addClasses: true, + greedy: false, + hoverClass: false, + scope: "default", + tolerance: "intersect", + + // callbacks + activate: null, + deactivate: null, + drop: null, + out: null, + over: null + }, + _create: function() { + + var proportions, + o = this.options, + accept = o.accept; + + this.isover = false; + this.isout = true; + + this.accept = $.isFunction(accept) ? accept : function(d) { + return d.is(accept); + }; + + this.proportions = function( /* valueToWrite */ ) { + if ( arguments.length ) { + // Store the droppable's proportions + proportions = arguments[ 0 ]; + } else { + // Retrieve or derive the droppable's proportions + return proportions ? + proportions : + proportions = { + width: this.element[ 0 ].offsetWidth, + height: this.element[ 0 ].offsetHeight + }; + } + }; + + // Add the reference and positions to the manager + $.ui.ddmanager.droppables[o.scope] = $.ui.ddmanager.droppables[o.scope] || []; + $.ui.ddmanager.droppables[o.scope].push(this); + + (o.addClasses && this.element.addClass("ui-droppable")); + + }, + + _destroy: function() { + var i = 0, + drop = $.ui.ddmanager.droppables[this.options.scope]; + + for ( ; i < drop.length; i++ ) { + if ( drop[i] === this ) { + drop.splice(i, 1); + } + } + + this.element.removeClass("ui-droppable ui-droppable-disabled"); + }, + + _setOption: function(key, value) { + + if(key === "accept") { + this.accept = $.isFunction(value) ? value : function(d) { + return d.is(value); + }; + } + $.Widget.prototype._setOption.apply(this, arguments); + }, + + _activate: function(event) { + var draggable = $.ui.ddmanager.current; + if(this.options.activeClass) { + this.element.addClass(this.options.activeClass); + } + if(draggable){ + this._trigger("activate", event, this.ui(draggable)); + } + }, + + _deactivate: function(event) { + var draggable = $.ui.ddmanager.current; + if(this.options.activeClass) { + this.element.removeClass(this.options.activeClass); + } + if(draggable){ + this._trigger("deactivate", event, this.ui(draggable)); + } + }, + + _over: function(event) { + + var draggable = $.ui.ddmanager.current; + + // Bail if draggable and droppable are same element + if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { + return; + } + + if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { + if(this.options.hoverClass) { + this.element.addClass(this.options.hoverClass); + } + this._trigger("over", event, this.ui(draggable)); + } + + }, + + _out: function(event) { + + var draggable = $.ui.ddmanager.current; + + // Bail if draggable and droppable are same element + if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { + return; + } + + if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { + if(this.options.hoverClass) { + this.element.removeClass(this.options.hoverClass); + } + this._trigger("out", event, this.ui(draggable)); + } + + }, + + _drop: function(event,custom) { + + var draggable = custom || $.ui.ddmanager.current, + childrenIntersection = false; + + // Bail if draggable and droppable are same element + if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { + return false; + } + + this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function() { + var inst = $.data(this, "ui-droppable"); + if( + inst.options.greedy && + !inst.options.disabled && + inst.options.scope === draggable.options.scope && + inst.accept.call(inst.element[0], (draggable.currentItem || draggable.element)) && + $.ui.intersect(draggable, $.extend(inst, { offset: inst.element.offset() }), inst.options.tolerance) + ) { childrenIntersection = true; return false; } + }); + if(childrenIntersection) { + return false; + } + + if(this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { + if(this.options.activeClass) { + this.element.removeClass(this.options.activeClass); + } + if(this.options.hoverClass) { + this.element.removeClass(this.options.hoverClass); + } + this._trigger("drop", event, this.ui(draggable)); + return this.element; + } + + return false; + + }, + + ui: function(c) { + return { + draggable: (c.currentItem || c.element), + helper: c.helper, + position: c.position, + offset: c.positionAbs + }; + } + +}); + +$.ui.intersect = function(draggable, droppable, toleranceMode) { + + if (!droppable.offset) { + return false; + } + + var draggableLeft, draggableTop, + x1 = (draggable.positionAbs || draggable.position.absolute).left, + y1 = (draggable.positionAbs || draggable.position.absolute).top, + x2 = x1 + draggable.helperProportions.width, + y2 = y1 + draggable.helperProportions.height, + l = droppable.offset.left, + t = droppable.offset.top, + r = l + droppable.proportions().width, + b = t + droppable.proportions().height; + + switch (toleranceMode) { + case "fit": + return (l <= x1 && x2 <= r && t <= y1 && y2 <= b); + case "intersect": + return (l < x1 + (draggable.helperProportions.width / 2) && // Right Half + x2 - (draggable.helperProportions.width / 2) < r && // Left Half + t < y1 + (draggable.helperProportions.height / 2) && // Bottom Half + y2 - (draggable.helperProportions.height / 2) < b ); // Top Half + case "pointer": + draggableLeft = ((draggable.positionAbs || draggable.position.absolute).left + (draggable.clickOffset || draggable.offset.click).left); + draggableTop = ((draggable.positionAbs || draggable.position.absolute).top + (draggable.clickOffset || draggable.offset.click).top); + return isOverAxis( draggableTop, t, droppable.proportions().height ) && isOverAxis( draggableLeft, l, droppable.proportions().width ); + case "touch": + return ( + (y1 >= t && y1 <= b) || // Top edge touching + (y2 >= t && y2 <= b) || // Bottom edge touching + (y1 < t && y2 > b) // Surrounded vertically + ) && ( + (x1 >= l && x1 <= r) || // Left edge touching + (x2 >= l && x2 <= r) || // Right edge touching + (x1 < l && x2 > r) // Surrounded horizontally + ); + default: + return false; + } + +}; + +/* + This manager tracks offsets of draggables and droppables +*/ +$.ui.ddmanager = { + current: null, + droppables: { "default": [] }, + prepareOffsets: function(t, event) { + + var i, j, + m = $.ui.ddmanager.droppables[t.options.scope] || [], + type = event ? event.type : null, // workaround for #2317 + list = (t.currentItem || t.element).find(":data(ui-droppable)").addBack(); + + droppablesLoop: for (i = 0; i < m.length; i++) { + + //No disabled and non-accepted + if(m[i].options.disabled || (t && !m[i].accept.call(m[i].element[0],(t.currentItem || t.element)))) { + continue; + } + + // Filter out elements in the current dragged item + for (j=0; j < list.length; j++) { + if(list[j] === m[i].element[0]) { + m[i].proportions().height = 0; + continue droppablesLoop; + } + } + + m[i].visible = m[i].element.css("display") !== "none"; + if(!m[i].visible) { + continue; + } + + //Activate the droppable if used directly from draggables + if(type === "mousedown") { + m[i]._activate.call(m[i], event); + } + + m[ i ].offset = m[ i ].element.offset(); + m[ i ].proportions({ width: m[ i ].element[ 0 ].offsetWidth, height: m[ i ].element[ 0 ].offsetHeight }); + + } + + }, + drop: function(draggable, event) { + + var dropped = false; + // Create a copy of the droppables in case the list changes during the drop (#9116) + $.each(($.ui.ddmanager.droppables[draggable.options.scope] || []).slice(), function() { + + if(!this.options) { + return; + } + if (!this.options.disabled && this.visible && $.ui.intersect(draggable, this, this.options.tolerance)) { + dropped = this._drop.call(this, event) || dropped; + } + + if (!this.options.disabled && this.visible && this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { + this.isout = true; + this.isover = false; + this._deactivate.call(this, event); + } + + }); + return dropped; + + }, + dragStart: function( draggable, event ) { + //Listen for scrolling so that if the dragging causes scrolling the position of the droppables can be recalculated (see #5003) + draggable.element.parentsUntil( "body" ).bind( "scroll.droppable", function() { + if( !draggable.options.refreshPositions ) { + $.ui.ddmanager.prepareOffsets( draggable, event ); + } + }); + }, + drag: function(draggable, event) { + + //If you have a highly dynamic page, you might try this option. It renders positions every time you move the mouse. + if(draggable.options.refreshPositions) { + $.ui.ddmanager.prepareOffsets(draggable, event); + } + + //Run through all droppables and check their positions based on specific tolerance options + $.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() { + + if(this.options.disabled || this.greedyChild || !this.visible) { + return; + } + + var parentInstance, scope, parent, + intersects = $.ui.intersect(draggable, this, this.options.tolerance), + c = !intersects && this.isover ? "isout" : (intersects && !this.isover ? "isover" : null); + if(!c) { + return; + } + + if (this.options.greedy) { + // find droppable parents with same scope + scope = this.options.scope; + parent = this.element.parents(":data(ui-droppable)").filter(function () { + return $.data(this, "ui-droppable").options.scope === scope; + }); + + if (parent.length) { + parentInstance = $.data(parent[0], "ui-droppable"); + parentInstance.greedyChild = (c === "isover"); + } + } + + // we just moved into a greedy child + if (parentInstance && c === "isover") { + parentInstance.isover = false; + parentInstance.isout = true; + parentInstance._out.call(parentInstance, event); + } + + this[c] = true; + this[c === "isout" ? "isover" : "isout"] = false; + this[c === "isover" ? "_over" : "_out"].call(this, event); + + // we just moved out of a greedy child + if (parentInstance && c === "isout") { + parentInstance.isout = false; + parentInstance.isover = true; + parentInstance._over.call(parentInstance, event); + } + }); + + }, + dragStop: function( draggable, event ) { + draggable.element.parentsUntil( "body" ).unbind( "scroll.droppable" ); + //Call prepareOffsets one final time since IE does not fire return scroll events when overflow was caused by drag (see #5003) + if( !draggable.options.refreshPositions ) { + $.ui.ddmanager.prepareOffsets( draggable, event ); + } + } +}; + +})(jQuery); +(function( $, undefined ) { + +function isOverAxis( x, reference, size ) { + return ( x > reference ) && ( x < ( reference + size ) ); +} + +function isFloating(item) { + return (/left|right/).test(item.css("float")) || (/inline|table-cell/).test(item.css("display")); +} + +$.widget("ui.sortable", $.ui.mouse, { + version: "1.10.4", + widgetEventPrefix: "sort", + ready: false, + options: { + appendTo: "parent", + axis: false, + connectWith: false, + containment: false, + cursor: "auto", + cursorAt: false, + dropOnEmpty: true, + forcePlaceholderSize: false, + forceHelperSize: false, + grid: false, + handle: false, + helper: "original", + items: "> *", + opacity: false, + placeholder: false, + revert: false, + scroll: true, + scrollSensitivity: 20, + scrollSpeed: 20, + scope: "default", + tolerance: "intersect", + zIndex: 1000, + + // callbacks + activate: null, + beforeStop: null, + change: null, + deactivate: null, + out: null, + over: null, + receive: null, + remove: null, + sort: null, + start: null, + stop: null, + update: null + }, + _create: function() { + + var o = this.options; + this.containerCache = {}; + this.element.addClass("ui-sortable"); + + //Get the items + this.refresh(); + + //Let's determine if the items are being displayed horizontally + this.floating = this.items.length ? o.axis === "x" || isFloating(this.items[0].item) : false; + + //Let's determine the parent's offset + this.offset = this.element.offset(); + + //Initialize mouse events for interaction + this._mouseInit(); + + //We're ready to go + this.ready = true; + + }, + + _destroy: function() { + this.element + .removeClass("ui-sortable ui-sortable-disabled"); + this._mouseDestroy(); + + for ( var i = this.items.length - 1; i >= 0; i-- ) { + this.items[i].item.removeData(this.widgetName + "-item"); + } + + return this; + }, + + _setOption: function(key, value){ + if ( key === "disabled" ) { + this.options[ key ] = value; + + this.widget().toggleClass( "ui-sortable-disabled", !!value ); + } else { + // Don't call widget base _setOption for disable as it adds ui-state-disabled class + $.Widget.prototype._setOption.apply(this, arguments); + } + }, + + _mouseCapture: function(event, overrideHandle) { + var currentItem = null, + validHandle = false, + that = this; + + if (this.reverting) { + return false; + } + + if(this.options.disabled || this.options.type === "static") { + return false; + } + + //We have to refresh the items data once first + this._refreshItems(event); + + //Find out if the clicked node (or one of its parents) is a actual item in this.items + $(event.target).parents().each(function() { + if($.data(this, that.widgetName + "-item") === that) { + currentItem = $(this); + return false; + } + }); + if($.data(event.target, that.widgetName + "-item") === that) { + currentItem = $(event.target); + } + + if(!currentItem) { + return false; + } + if(this.options.handle && !overrideHandle) { + $(this.options.handle, currentItem).find("*").addBack().each(function() { + if(this === event.target) { + validHandle = true; + } + }); + if(!validHandle) { + return false; + } + } + + this.currentItem = currentItem; + this._removeCurrentsFromItems(); + return true; + + }, + + _mouseStart: function(event, overrideHandle, noActivation) { + + var i, body, + o = this.options; + + this.currentContainer = this; + + //We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture + this.refreshPositions(); + + //Create and append the visible helper + this.helper = this._createHelper(event); + + //Cache the helper size + this._cacheHelperProportions(); + + /* + * - Position generation - + * This block generates everything position related - it's the core of draggables. + */ + + //Cache the margins of the original element + this._cacheMargins(); + + //Get the next scrolling parent + this.scrollParent = this.helper.scrollParent(); + + //The element's absolute position on the page minus margins + this.offset = this.currentItem.offset(); + this.offset = { + top: this.offset.top - this.margins.top, + left: this.offset.left - this.margins.left + }; + + $.extend(this.offset, { + click: { //Where the click happened, relative to the element + left: event.pageX - this.offset.left, + top: event.pageY - this.offset.top + }, + parent: this._getParentOffset(), + relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper + }); + + // Only after we got the offset, we can change the helper's position to absolute + // TODO: Still need to figure out a way to make relative sorting possible + this.helper.css("position", "absolute"); + this.cssPosition = this.helper.css("position"); + + //Generate the original position + this.originalPosition = this._generatePosition(event); + this.originalPageX = event.pageX; + this.originalPageY = event.pageY; + + //Adjust the mouse offset relative to the helper if "cursorAt" is supplied + (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt)); + + //Cache the former DOM position + this.domPosition = { prev: this.currentItem.prev()[0], parent: this.currentItem.parent()[0] }; + + //If the helper is not the original, hide the original so it's not playing any role during the drag, won't cause anything bad this way + if(this.helper[0] !== this.currentItem[0]) { + this.currentItem.hide(); + } + + //Create the placeholder + this._createPlaceholder(); + + //Set a containment if given in the options + if(o.containment) { + this._setContainment(); + } + + if( o.cursor && o.cursor !== "auto" ) { // cursor option + body = this.document.find( "body" ); + + // support: IE + this.storedCursor = body.css( "cursor" ); + body.css( "cursor", o.cursor ); + + this.storedStylesheet = $( "" ).appendTo( body ); + } + + if(o.opacity) { // opacity option + if (this.helper.css("opacity")) { + this._storedOpacity = this.helper.css("opacity"); + } + this.helper.css("opacity", o.opacity); + } + + if(o.zIndex) { // zIndex option + if (this.helper.css("zIndex")) { + this._storedZIndex = this.helper.css("zIndex"); + } + this.helper.css("zIndex", o.zIndex); + } + + //Prepare scrolling + if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { + this.overflowOffset = this.scrollParent.offset(); + } + + //Call callbacks + this._trigger("start", event, this._uiHash()); + + //Recache the helper size + if(!this._preserveHelperProportions) { + this._cacheHelperProportions(); + } + + + //Post "activate" events to possible containers + if( !noActivation ) { + for ( i = this.containers.length - 1; i >= 0; i-- ) { + this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) ); + } + } + + //Prepare possible droppables + if($.ui.ddmanager) { + $.ui.ddmanager.current = this; + } + + if ($.ui.ddmanager && !o.dropBehaviour) { + $.ui.ddmanager.prepareOffsets(this, event); + } + + this.dragging = true; + + this.helper.addClass("ui-sortable-helper"); + this._mouseDrag(event); //Execute the drag once - this causes the helper not to be visible before getting its correct position + return true; + + }, + + _mouseDrag: function(event) { + var i, item, itemElement, intersection, + o = this.options, + scrolled = false; + + //Compute the helpers position + this.position = this._generatePosition(event); + this.positionAbs = this._convertPositionTo("absolute"); + + if (!this.lastPositionAbs) { + this.lastPositionAbs = this.positionAbs; + } + + //Do scrolling + if(this.options.scroll) { + if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { + + if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) { + this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed; + } else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity) { + this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed; + } + + if((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) { + this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed; + } else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity) { + this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed; + } + + } else { + + if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) { + scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); + } else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) { + scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); + } + + if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) { + scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); + } else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) { + scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); + } + + } + + if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { + $.ui.ddmanager.prepareOffsets(this, event); + } + } + + //Regenerate the absolute position used for position checks + this.positionAbs = this._convertPositionTo("absolute"); + + //Set the helper position + if(!this.options.axis || this.options.axis !== "y") { + this.helper[0].style.left = this.position.left+"px"; + } + if(!this.options.axis || this.options.axis !== "x") { + this.helper[0].style.top = this.position.top+"px"; + } + + //Rearrange + for (i = this.items.length - 1; i >= 0; i--) { + + //Cache variables and intersection, continue if no intersection + item = this.items[i]; + itemElement = item.item[0]; + intersection = this._intersectsWithPointer(item); + if (!intersection) { + continue; + } + + // Only put the placeholder inside the current Container, skip all + // items from other containers. This works because when moving + // an item from one container to another the + // currentContainer is switched before the placeholder is moved. + // + // Without this, moving items in "sub-sortables" can cause + // the placeholder to jitter beetween the outer and inner container. + if (item.instance !== this.currentContainer) { + continue; + } + + // cannot intersect with itself + // no useless actions that have been done before + // no action if the item moved is the parent of the item checked + if (itemElement !== this.currentItem[0] && + this.placeholder[intersection === 1 ? "next" : "prev"]()[0] !== itemElement && + !$.contains(this.placeholder[0], itemElement) && + (this.options.type === "semi-dynamic" ? !$.contains(this.element[0], itemElement) : true) + ) { + + this.direction = intersection === 1 ? "down" : "up"; + + if (this.options.tolerance === "pointer" || this._intersectsWithSides(item)) { + this._rearrange(event, item); + } else { + break; + } + + this._trigger("change", event, this._uiHash()); + break; + } + } + + //Post events to containers + this._contactContainers(event); + + //Interconnect with droppables + if($.ui.ddmanager) { + $.ui.ddmanager.drag(this, event); + } + + //Call callbacks + this._trigger("sort", event, this._uiHash()); + + this.lastPositionAbs = this.positionAbs; + return false; + + }, + + _mouseStop: function(event, noPropagation) { + + if(!event) { + return; + } + + //If we are using droppables, inform the manager about the drop + if ($.ui.ddmanager && !this.options.dropBehaviour) { + $.ui.ddmanager.drop(this, event); + } + + if(this.options.revert) { + var that = this, + cur = this.placeholder.offset(), + axis = this.options.axis, + animation = {}; + + if ( !axis || axis === "x" ) { + animation.left = cur.left - this.offset.parent.left - this.margins.left + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollLeft); + } + if ( !axis || axis === "y" ) { + animation.top = cur.top - this.offset.parent.top - this.margins.top + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollTop); + } + this.reverting = true; + $(this.helper).animate( animation, parseInt(this.options.revert, 10) || 500, function() { + that._clear(event); + }); + } else { + this._clear(event, noPropagation); + } + + return false; + + }, + + cancel: function() { + + if(this.dragging) { + + this._mouseUp({ target: null }); + + if(this.options.helper === "original") { + this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"); + } else { + this.currentItem.show(); + } + + //Post deactivating events to containers + for (var i = this.containers.length - 1; i >= 0; i--){ + this.containers[i]._trigger("deactivate", null, this._uiHash(this)); + if(this.containers[i].containerCache.over) { + this.containers[i]._trigger("out", null, this._uiHash(this)); + this.containers[i].containerCache.over = 0; + } + } + + } + + if (this.placeholder) { + //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! + if(this.placeholder[0].parentNode) { + this.placeholder[0].parentNode.removeChild(this.placeholder[0]); + } + if(this.options.helper !== "original" && this.helper && this.helper[0].parentNode) { + this.helper.remove(); + } + + $.extend(this, { + helper: null, + dragging: false, + reverting: false, + _noFinalSort: null + }); + + if(this.domPosition.prev) { + $(this.domPosition.prev).after(this.currentItem); + } else { + $(this.domPosition.parent).prepend(this.currentItem); + } + } + + return this; + + }, + + serialize: function(o) { + + var items = this._getItemsAsjQuery(o && o.connected), + str = []; + o = o || {}; + + $(items).each(function() { + var res = ($(o.item || this).attr(o.attribute || "id") || "").match(o.expression || (/(.+)[\-=_](.+)/)); + if (res) { + str.push((o.key || res[1]+"[]")+"="+(o.key && o.expression ? res[1] : res[2])); + } + }); + + if(!str.length && o.key) { + str.push(o.key + "="); + } + + return str.join("&"); + + }, + + toArray: function(o) { + + var items = this._getItemsAsjQuery(o && o.connected), + ret = []; + + o = o || {}; + + items.each(function() { ret.push($(o.item || this).attr(o.attribute || "id") || ""); }); + return ret; + + }, + + /* Be careful with the following core functions */ + _intersectsWith: function(item) { + + var x1 = this.positionAbs.left, + x2 = x1 + this.helperProportions.width, + y1 = this.positionAbs.top, + y2 = y1 + this.helperProportions.height, + l = item.left, + r = l + item.width, + t = item.top, + b = t + item.height, + dyClick = this.offset.click.top, + dxClick = this.offset.click.left, + isOverElementHeight = ( this.options.axis === "x" ) || ( ( y1 + dyClick ) > t && ( y1 + dyClick ) < b ), + isOverElementWidth = ( this.options.axis === "y" ) || ( ( x1 + dxClick ) > l && ( x1 + dxClick ) < r ), + isOverElement = isOverElementHeight && isOverElementWidth; + + if ( this.options.tolerance === "pointer" || + this.options.forcePointerForContainers || + (this.options.tolerance !== "pointer" && this.helperProportions[this.floating ? "width" : "height"] > item[this.floating ? "width" : "height"]) + ) { + return isOverElement; + } else { + + return (l < x1 + (this.helperProportions.width / 2) && // Right Half + x2 - (this.helperProportions.width / 2) < r && // Left Half + t < y1 + (this.helperProportions.height / 2) && // Bottom Half + y2 - (this.helperProportions.height / 2) < b ); // Top Half + + } + }, + + _intersectsWithPointer: function(item) { + + var isOverElementHeight = (this.options.axis === "x") || isOverAxis(this.positionAbs.top + this.offset.click.top, item.top, item.height), + isOverElementWidth = (this.options.axis === "y") || isOverAxis(this.positionAbs.left + this.offset.click.left, item.left, item.width), + isOverElement = isOverElementHeight && isOverElementWidth, + verticalDirection = this._getDragVerticalDirection(), + horizontalDirection = this._getDragHorizontalDirection(); + + if (!isOverElement) { + return false; + } + + return this.floating ? + ( ((horizontalDirection && horizontalDirection === "right") || verticalDirection === "down") ? 2 : 1 ) + : ( verticalDirection && (verticalDirection === "down" ? 2 : 1) ); + + }, + + _intersectsWithSides: function(item) { + + var isOverBottomHalf = isOverAxis(this.positionAbs.top + this.offset.click.top, item.top + (item.height/2), item.height), + isOverRightHalf = isOverAxis(this.positionAbs.left + this.offset.click.left, item.left + (item.width/2), item.width), + verticalDirection = this._getDragVerticalDirection(), + horizontalDirection = this._getDragHorizontalDirection(); + + if (this.floating && horizontalDirection) { + return ((horizontalDirection === "right" && isOverRightHalf) || (horizontalDirection === "left" && !isOverRightHalf)); + } else { + return verticalDirection && ((verticalDirection === "down" && isOverBottomHalf) || (verticalDirection === "up" && !isOverBottomHalf)); + } + + }, + + _getDragVerticalDirection: function() { + var delta = this.positionAbs.top - this.lastPositionAbs.top; + return delta !== 0 && (delta > 0 ? "down" : "up"); + }, + + _getDragHorizontalDirection: function() { + var delta = this.positionAbs.left - this.lastPositionAbs.left; + return delta !== 0 && (delta > 0 ? "right" : "left"); + }, + + refresh: function(event) { + this._refreshItems(event); + this.refreshPositions(); + return this; + }, + + _connectWith: function() { + var options = this.options; + return options.connectWith.constructor === String ? [options.connectWith] : options.connectWith; + }, + + _getItemsAsjQuery: function(connected) { + + var i, j, cur, inst, + items = [], + queries = [], + connectWith = this._connectWith(); + + if(connectWith && connected) { + for (i = connectWith.length - 1; i >= 0; i--){ + cur = $(connectWith[i]); + for ( j = cur.length - 1; j >= 0; j--){ + inst = $.data(cur[j], this.widgetFullName); + if(inst && inst !== this && !inst.options.disabled) { + queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element) : $(inst.options.items, inst.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), inst]); + } + } + } + } + + queries.push([$.isFunction(this.options.items) ? this.options.items.call(this.element, null, { options: this.options, item: this.currentItem }) : $(this.options.items, this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), this]); + + function addItems() { + items.push( this ); + } + for (i = queries.length - 1; i >= 0; i--){ + queries[i][0].each( addItems ); + } + + return $(items); + + }, + + _removeCurrentsFromItems: function() { + + var list = this.currentItem.find(":data(" + this.widgetName + "-item)"); + + this.items = $.grep(this.items, function (item) { + for (var j=0; j < list.length; j++) { + if(list[j] === item.item[0]) { + return false; + } + } + return true; + }); + + }, + + _refreshItems: function(event) { + + this.items = []; + this.containers = [this]; + + var i, j, cur, inst, targetData, _queries, item, queriesLength, + items = this.items, + queries = [[$.isFunction(this.options.items) ? this.options.items.call(this.element[0], event, { item: this.currentItem }) : $(this.options.items, this.element), this]], + connectWith = this._connectWith(); + + if(connectWith && this.ready) { //Shouldn't be run the first time through due to massive slow-down + for (i = connectWith.length - 1; i >= 0; i--){ + cur = $(connectWith[i]); + for (j = cur.length - 1; j >= 0; j--){ + inst = $.data(cur[j], this.widgetFullName); + if(inst && inst !== this && !inst.options.disabled) { + queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element[0], event, { item: this.currentItem }) : $(inst.options.items, inst.element), inst]); + this.containers.push(inst); + } + } + } + } + + for (i = queries.length - 1; i >= 0; i--) { + targetData = queries[i][1]; + _queries = queries[i][0]; + + for (j=0, queriesLength = _queries.length; j < queriesLength; j++) { + item = $(_queries[j]); + + item.data(this.widgetName + "-item", targetData); // Data for target checking (mouse manager) + + items.push({ + item: item, + instance: targetData, + width: 0, height: 0, + left: 0, top: 0 + }); + } + } + + }, + + refreshPositions: function(fast) { + + //This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change + if(this.offsetParent && this.helper) { + this.offset.parent = this._getParentOffset(); + } + + var i, item, t, p; + + for (i = this.items.length - 1; i >= 0; i--){ + item = this.items[i]; + + //We ignore calculating positions of all connected containers when we're not over them + if(item.instance !== this.currentContainer && this.currentContainer && item.item[0] !== this.currentItem[0]) { + continue; + } + + t = this.options.toleranceElement ? $(this.options.toleranceElement, item.item) : item.item; + + if (!fast) { + item.width = t.outerWidth(); + item.height = t.outerHeight(); + } + + p = t.offset(); + item.left = p.left; + item.top = p.top; + } + + if(this.options.custom && this.options.custom.refreshContainers) { + this.options.custom.refreshContainers.call(this); + } else { + for (i = this.containers.length - 1; i >= 0; i--){ + p = this.containers[i].element.offset(); + this.containers[i].containerCache.left = p.left; + this.containers[i].containerCache.top = p.top; + this.containers[i].containerCache.width = this.containers[i].element.outerWidth(); + this.containers[i].containerCache.height = this.containers[i].element.outerHeight(); + } + } + + return this; + }, + + _createPlaceholder: function(that) { + that = that || this; + var className, + o = that.options; + + if(!o.placeholder || o.placeholder.constructor === String) { + className = o.placeholder; + o.placeholder = { + element: function() { + + var nodeName = that.currentItem[0].nodeName.toLowerCase(), + element = $( "<" + nodeName + ">", that.document[0] ) + .addClass(className || that.currentItem[0].className+" ui-sortable-placeholder") + .removeClass("ui-sortable-helper"); + + if ( nodeName === "tr" ) { + that.currentItem.children().each(function() { + $( " ", that.document[0] ) + .attr( "colspan", $( this ).attr( "colspan" ) || 1 ) + .appendTo( element ); + }); + } else if ( nodeName === "img" ) { + element.attr( "src", that.currentItem.attr( "src" ) ); + } + + if ( !className ) { + element.css( "visibility", "hidden" ); + } + + return element; + }, + update: function(container, p) { + + // 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that + // 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified + if(className && !o.forcePlaceholderSize) { + return; + } + + //If the element doesn't have a actual height by itself (without styles coming from a stylesheet), it receives the inline height from the dragged item + if(!p.height()) { p.height(that.currentItem.innerHeight() - parseInt(that.currentItem.css("paddingTop")||0, 10) - parseInt(that.currentItem.css("paddingBottom")||0, 10)); } + if(!p.width()) { p.width(that.currentItem.innerWidth() - parseInt(that.currentItem.css("paddingLeft")||0, 10) - parseInt(that.currentItem.css("paddingRight")||0, 10)); } + } + }; + } + + //Create the placeholder + that.placeholder = $(o.placeholder.element.call(that.element, that.currentItem)); + + //Append it after the actual current item + that.currentItem.after(that.placeholder); + + //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317) + o.placeholder.update(that, that.placeholder); + + }, + + _contactContainers: function(event) { + var i, j, dist, itemWithLeastDistance, posProperty, sizeProperty, base, cur, nearBottom, floating, + innermostContainer = null, + innermostIndex = null; + + // get innermost container that intersects with item + for (i = this.containers.length - 1; i >= 0; i--) { + + // never consider a container that's located within the item itself + if($.contains(this.currentItem[0], this.containers[i].element[0])) { + continue; + } + + if(this._intersectsWith(this.containers[i].containerCache)) { + + // if we've already found a container and it's more "inner" than this, then continue + if(innermostContainer && $.contains(this.containers[i].element[0], innermostContainer.element[0])) { + continue; + } + + innermostContainer = this.containers[i]; + innermostIndex = i; + + } else { + // container doesn't intersect. trigger "out" event if necessary + if(this.containers[i].containerCache.over) { + this.containers[i]._trigger("out", event, this._uiHash(this)); + this.containers[i].containerCache.over = 0; + } + } + + } + + // if no intersecting containers found, return + if(!innermostContainer) { + return; + } + + // move the item into the container if it's not there already + if(this.containers.length === 1) { + if (!this.containers[innermostIndex].containerCache.over) { + this.containers[innermostIndex]._trigger("over", event, this._uiHash(this)); + this.containers[innermostIndex].containerCache.over = 1; + } + } else { + + //When entering a new container, we will find the item with the least distance and append our item near it + dist = 10000; + itemWithLeastDistance = null; + floating = innermostContainer.floating || isFloating(this.currentItem); + posProperty = floating ? "left" : "top"; + sizeProperty = floating ? "width" : "height"; + base = this.positionAbs[posProperty] + this.offset.click[posProperty]; + for (j = this.items.length - 1; j >= 0; j--) { + if(!$.contains(this.containers[innermostIndex].element[0], this.items[j].item[0])) { + continue; + } + if(this.items[j].item[0] === this.currentItem[0]) { + continue; + } + if (floating && !isOverAxis(this.positionAbs.top + this.offset.click.top, this.items[j].top, this.items[j].height)) { + continue; + } + cur = this.items[j].item.offset()[posProperty]; + nearBottom = false; + if(Math.abs(cur - base) > Math.abs(cur + this.items[j][sizeProperty] - base)){ + nearBottom = true; + cur += this.items[j][sizeProperty]; + } + + if(Math.abs(cur - base) < dist) { + dist = Math.abs(cur - base); itemWithLeastDistance = this.items[j]; + this.direction = nearBottom ? "up": "down"; + } + } + + //Check if dropOnEmpty is enabled + if(!itemWithLeastDistance && !this.options.dropOnEmpty) { + return; + } + + if(this.currentContainer === this.containers[innermostIndex]) { + return; + } + + itemWithLeastDistance ? this._rearrange(event, itemWithLeastDistance, null, true) : this._rearrange(event, null, this.containers[innermostIndex].element, true); + this._trigger("change", event, this._uiHash()); + this.containers[innermostIndex]._trigger("change", event, this._uiHash(this)); + this.currentContainer = this.containers[innermostIndex]; + + //Update the placeholder + this.options.placeholder.update(this.currentContainer, this.placeholder); + + this.containers[innermostIndex]._trigger("over", event, this._uiHash(this)); + this.containers[innermostIndex].containerCache.over = 1; + } + + + }, + + _createHelper: function(event) { + + var o = this.options, + helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event, this.currentItem])) : (o.helper === "clone" ? this.currentItem.clone() : this.currentItem); + + //Add the helper to the DOM if that didn't happen already + if(!helper.parents("body").length) { + $(o.appendTo !== "parent" ? o.appendTo : this.currentItem[0].parentNode)[0].appendChild(helper[0]); + } + + if(helper[0] === this.currentItem[0]) { + this._storedCSS = { width: this.currentItem[0].style.width, height: this.currentItem[0].style.height, position: this.currentItem.css("position"), top: this.currentItem.css("top"), left: this.currentItem.css("left") }; + } + + if(!helper[0].style.width || o.forceHelperSize) { + helper.width(this.currentItem.width()); + } + if(!helper[0].style.height || o.forceHelperSize) { + helper.height(this.currentItem.height()); + } + + return helper; + + }, + + _adjustOffsetFromHelper: function(obj) { + if (typeof obj === "string") { + obj = obj.split(" "); + } + if ($.isArray(obj)) { + obj = {left: +obj[0], top: +obj[1] || 0}; + } + if ("left" in obj) { + this.offset.click.left = obj.left + this.margins.left; + } + if ("right" in obj) { + this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; + } + if ("top" in obj) { + this.offset.click.top = obj.top + this.margins.top; + } + if ("bottom" in obj) { + this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; + } + }, + + _getParentOffset: function() { + + + //Get the offsetParent and cache its position + this.offsetParent = this.helper.offsetParent(); + var po = this.offsetParent.offset(); + + // This is a special case where we need to modify a offset calculated on start, since the following happened: + // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent + // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that + // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag + if(this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) { + po.left += this.scrollParent.scrollLeft(); + po.top += this.scrollParent.scrollTop(); + } + + // This needs to be actually done for all browsers, since pageX/pageY includes this information + // with an ugly IE fix + if( this.offsetParent[0] === document.body || (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) { + po = { top: 0, left: 0 }; + } + + return { + top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0), + left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0) + }; + + }, + + _getRelativeOffset: function() { + + if(this.cssPosition === "relative") { + var p = this.currentItem.position(); + return { + top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(), + left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft() + }; + } else { + return { top: 0, left: 0 }; + } + + }, + + _cacheMargins: function() { + this.margins = { + left: (parseInt(this.currentItem.css("marginLeft"),10) || 0), + top: (parseInt(this.currentItem.css("marginTop"),10) || 0) + }; + }, + + _cacheHelperProportions: function() { + this.helperProportions = { + width: this.helper.outerWidth(), + height: this.helper.outerHeight() + }; + }, + + _setContainment: function() { + + var ce, co, over, + o = this.options; + if(o.containment === "parent") { + o.containment = this.helper[0].parentNode; + } + if(o.containment === "document" || o.containment === "window") { + this.containment = [ + 0 - this.offset.relative.left - this.offset.parent.left, + 0 - this.offset.relative.top - this.offset.parent.top, + $(o.containment === "document" ? document : window).width() - this.helperProportions.width - this.margins.left, + ($(o.containment === "document" ? document : window).height() || document.body.parentNode.scrollHeight) - this.helperProportions.height - this.margins.top + ]; + } + + if(!(/^(document|window|parent)$/).test(o.containment)) { + ce = $(o.containment)[0]; + co = $(o.containment).offset(); + over = ($(ce).css("overflow") !== "hidden"); + + this.containment = [ + co.left + (parseInt($(ce).css("borderLeftWidth"),10) || 0) + (parseInt($(ce).css("paddingLeft"),10) || 0) - this.margins.left, + co.top + (parseInt($(ce).css("borderTopWidth"),10) || 0) + (parseInt($(ce).css("paddingTop"),10) || 0) - this.margins.top, + co.left+(over ? Math.max(ce.scrollWidth,ce.offsetWidth) : ce.offsetWidth) - (parseInt($(ce).css("borderLeftWidth"),10) || 0) - (parseInt($(ce).css("paddingRight"),10) || 0) - this.helperProportions.width - this.margins.left, + co.top+(over ? Math.max(ce.scrollHeight,ce.offsetHeight) : ce.offsetHeight) - (parseInt($(ce).css("borderTopWidth"),10) || 0) - (parseInt($(ce).css("paddingBottom"),10) || 0) - this.helperProportions.height - this.margins.top + ]; + } + + }, + + _convertPositionTo: function(d, pos) { + + if(!pos) { + pos = this.position; + } + var mod = d === "absolute" ? 1 : -1, + scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, + scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); + + return { + top: ( + pos.top + // The absolute mouse position + this.offset.relative.top * mod + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.top * mod - // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod) + ), + left: ( + pos.left + // The absolute mouse position + this.offset.relative.left * mod + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.left * mod - // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ) * mod) + ) + }; + + }, + + _generatePosition: function(event) { + + var top, left, + o = this.options, + pageX = event.pageX, + pageY = event.pageY, + scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); + + // This is another very weird special case that only happens for relative elements: + // 1. If the css position is relative + // 2. and the scroll parent is the document or similar to the offset parent + // we have to refresh the relative offset during the scroll so there are no jumps + if(this.cssPosition === "relative" && !(this.scrollParent[0] !== document && this.scrollParent[0] !== this.offsetParent[0])) { + this.offset.relative = this._getRelativeOffset(); + } + + /* + * - Position constraining - + * Constrain the position to a mix of grid, containment. + */ + + if(this.originalPosition) { //If we are not dragging yet, we won't check for options + + if(this.containment) { + if(event.pageX - this.offset.click.left < this.containment[0]) { + pageX = this.containment[0] + this.offset.click.left; + } + if(event.pageY - this.offset.click.top < this.containment[1]) { + pageY = this.containment[1] + this.offset.click.top; + } + if(event.pageX - this.offset.click.left > this.containment[2]) { + pageX = this.containment[2] + this.offset.click.left; + } + if(event.pageY - this.offset.click.top > this.containment[3]) { + pageY = this.containment[3] + this.offset.click.top; + } + } + + if(o.grid) { + top = this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1]; + pageY = this.containment ? ( (top - this.offset.click.top >= this.containment[1] && top - this.offset.click.top <= this.containment[3]) ? top : ((top - this.offset.click.top >= this.containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top; + + left = this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0]; + pageX = this.containment ? ( (left - this.offset.click.left >= this.containment[0] && left - this.offset.click.left <= this.containment[2]) ? left : ((left - this.offset.click.left >= this.containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left; + } + + } + + return { + top: ( + pageY - // The absolute mouse position + this.offset.click.top - // Click offset (relative to the element) + this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.top + // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) )) + ), + left: ( + pageX - // The absolute mouse position + this.offset.click.left - // Click offset (relative to the element) + this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.left + // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() )) + ) + }; + + }, + + _rearrange: function(event, i, a, hardRefresh) { + + a ? a[0].appendChild(this.placeholder[0]) : i.item[0].parentNode.insertBefore(this.placeholder[0], (this.direction === "down" ? i.item[0] : i.item[0].nextSibling)); + + //Various things done here to improve the performance: + // 1. we create a setTimeout, that calls refreshPositions + // 2. on the instance, we have a counter variable, that get's higher after every append + // 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same + // 4. this lets only the last addition to the timeout stack through + this.counter = this.counter ? ++this.counter : 1; + var counter = this.counter; + + this._delay(function() { + if(counter === this.counter) { + this.refreshPositions(!hardRefresh); //Precompute after each DOM insertion, NOT on mousemove + } + }); + + }, + + _clear: function(event, noPropagation) { + + this.reverting = false; + // We delay all events that have to be triggered to after the point where the placeholder has been removed and + // everything else normalized again + var i, + delayedTriggers = []; + + // We first have to update the dom position of the actual currentItem + // Note: don't do it if the current item is already removed (by a user), or it gets reappended (see #4088) + if(!this._noFinalSort && this.currentItem.parent().length) { + this.placeholder.before(this.currentItem); + } + this._noFinalSort = null; + + if(this.helper[0] === this.currentItem[0]) { + for(i in this._storedCSS) { + if(this._storedCSS[i] === "auto" || this._storedCSS[i] === "static") { + this._storedCSS[i] = ""; + } + } + this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"); + } else { + this.currentItem.show(); + } + + if(this.fromOutside && !noPropagation) { + delayedTriggers.push(function(event) { this._trigger("receive", event, this._uiHash(this.fromOutside)); }); + } + if((this.fromOutside || this.domPosition.prev !== this.currentItem.prev().not(".ui-sortable-helper")[0] || this.domPosition.parent !== this.currentItem.parent()[0]) && !noPropagation) { + delayedTriggers.push(function(event) { this._trigger("update", event, this._uiHash()); }); //Trigger update callback if the DOM position has changed + } + + // Check if the items Container has Changed and trigger appropriate + // events. + if (this !== this.currentContainer) { + if(!noPropagation) { + delayedTriggers.push(function(event) { this._trigger("remove", event, this._uiHash()); }); + delayedTriggers.push((function(c) { return function(event) { c._trigger("receive", event, this._uiHash(this)); }; }).call(this, this.currentContainer)); + delayedTriggers.push((function(c) { return function(event) { c._trigger("update", event, this._uiHash(this)); }; }).call(this, this.currentContainer)); + } + } + + + //Post events to containers + function delayEvent( type, instance, container ) { + return function( event ) { + container._trigger( type, event, instance._uiHash( instance ) ); + }; + } + for (i = this.containers.length - 1; i >= 0; i--){ + if (!noPropagation) { + delayedTriggers.push( delayEvent( "deactivate", this, this.containers[ i ] ) ); + } + if(this.containers[i].containerCache.over) { + delayedTriggers.push( delayEvent( "out", this, this.containers[ i ] ) ); + this.containers[i].containerCache.over = 0; + } + } + + //Do what was originally in plugins + if ( this.storedCursor ) { + this.document.find( "body" ).css( "cursor", this.storedCursor ); + this.storedStylesheet.remove(); + } + if(this._storedOpacity) { + this.helper.css("opacity", this._storedOpacity); + } + if(this._storedZIndex) { + this.helper.css("zIndex", this._storedZIndex === "auto" ? "" : this._storedZIndex); + } + + this.dragging = false; + if(this.cancelHelperRemoval) { + if(!noPropagation) { + this._trigger("beforeStop", event, this._uiHash()); + for (i=0; i < delayedTriggers.length; i++) { + delayedTriggers[i].call(this, event); + } //Trigger all delayed events + this._trigger("stop", event, this._uiHash()); + } + + this.fromOutside = false; + return false; + } + + if(!noPropagation) { + this._trigger("beforeStop", event, this._uiHash()); + } + + //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! + this.placeholder[0].parentNode.removeChild(this.placeholder[0]); + + if(this.helper[0] !== this.currentItem[0]) { + this.helper.remove(); + } + this.helper = null; + + if(!noPropagation) { + for (i=0; i < delayedTriggers.length; i++) { + delayedTriggers[i].call(this, event); + } //Trigger all delayed events + this._trigger("stop", event, this._uiHash()); + } + + this.fromOutside = false; + return true; + + }, + + _trigger: function() { + if ($.Widget.prototype._trigger.apply(this, arguments) === false) { + this.cancel(); + } + }, + + _uiHash: function(_inst) { + var inst = _inst || this; + return { + helper: inst.helper, + placeholder: inst.placeholder || $([]), + position: inst.position, + originalPosition: inst.originalPosition, + offset: inst.positionAbs, + item: inst.currentItem, + sender: _inst ? _inst.element : null + }; + } + +}); + +})(jQuery); +(function( $, undefined ) { + +// number of pages in a slider +// (how many times can you page up/down to go through the whole range) +var numPages = 5; + +$.widget( "ui.slider", $.ui.mouse, { + version: "1.10.4", + widgetEventPrefix: "slide", + + options: { + animate: false, + distance: 0, + max: 100, + min: 0, + orientation: "horizontal", + range: false, + step: 1, + value: 0, + values: null, + + // callbacks + change: null, + slide: null, + start: null, + stop: null + }, + + _create: function() { + this._keySliding = false; + this._mouseSliding = false; + this._animateOff = true; + this._handleIndex = null; + this._detectOrientation(); + this._mouseInit(); + + this.element + .addClass( "ui-slider" + + " ui-slider-" + this.orientation + + " ui-widget" + + " ui-widget-content" + + " ui-corner-all"); + + this._refresh(); + this._setOption( "disabled", this.options.disabled ); + + this._animateOff = false; + }, + + _refresh: function() { + this._createRange(); + this._createHandles(); + this._setupEvents(); + this._refreshValue(); + }, + + _createHandles: function() { + var i, handleCount, + options = this.options, + existingHandles = this.element.find( ".ui-slider-handle" ).addClass( "ui-state-default ui-corner-all" ), + handle = "
    ", + handles = []; + + handleCount = ( options.values && options.values.length ) || 1; + + if ( existingHandles.length > handleCount ) { + existingHandles.slice( handleCount ).remove(); + existingHandles = existingHandles.slice( 0, handleCount ); + } + + for ( i = existingHandles.length; i < handleCount; i++ ) { + handles.push( handle ); + } + + this.handles = existingHandles.add( $( handles.join( "" ) ).appendTo( this.element ) ); + + this.handle = this.handles.eq( 0 ); + + this.handles.each(function( i ) { + $( this ).data( "ui-slider-handle-index", i ); + }); + }, + + _createRange: function() { + var options = this.options, + classes = ""; + + if ( options.range ) { + if ( options.range === true ) { + if ( !options.values ) { + options.values = [ this._valueMin(), this._valueMin() ]; + } else if ( options.values.length && options.values.length !== 2 ) { + options.values = [ options.values[0], options.values[0] ]; + } else if ( $.isArray( options.values ) ) { + options.values = options.values.slice(0); + } + } + + if ( !this.range || !this.range.length ) { + this.range = $( "
    " ) + .appendTo( this.element ); + + classes = "ui-slider-range" + + // note: this isn't the most fittingly semantic framework class for this element, + // but worked best visually with a variety of themes + " ui-widget-header ui-corner-all"; + } else { + this.range.removeClass( "ui-slider-range-min ui-slider-range-max" ) + // Handle range switching from true to min/max + .css({ + "left": "", + "bottom": "" + }); + } + + this.range.addClass( classes + + ( ( options.range === "min" || options.range === "max" ) ? " ui-slider-range-" + options.range : "" ) ); + } else { + if ( this.range ) { + this.range.remove(); + } + this.range = null; + } + }, + + _setupEvents: function() { + var elements = this.handles.add( this.range ).filter( "a" ); + this._off( elements ); + this._on( elements, this._handleEvents ); + this._hoverable( elements ); + this._focusable( elements ); + }, + + _destroy: function() { + this.handles.remove(); + if ( this.range ) { + this.range.remove(); + } + + this.element + .removeClass( "ui-slider" + + " ui-slider-horizontal" + + " ui-slider-vertical" + + " ui-widget" + + " ui-widget-content" + + " ui-corner-all" ); + + this._mouseDestroy(); + }, + + _mouseCapture: function( event ) { + var position, normValue, distance, closestHandle, index, allowed, offset, mouseOverHandle, + that = this, + o = this.options; + + if ( o.disabled ) { + return false; + } + + this.elementSize = { + width: this.element.outerWidth(), + height: this.element.outerHeight() + }; + this.elementOffset = this.element.offset(); + + position = { x: event.pageX, y: event.pageY }; + normValue = this._normValueFromMouse( position ); + distance = this._valueMax() - this._valueMin() + 1; + this.handles.each(function( i ) { + var thisDistance = Math.abs( normValue - that.values(i) ); + if (( distance > thisDistance ) || + ( distance === thisDistance && + (i === that._lastChangedValue || that.values(i) === o.min ))) { + distance = thisDistance; + closestHandle = $( this ); + index = i; + } + }); + + allowed = this._start( event, index ); + if ( allowed === false ) { + return false; + } + this._mouseSliding = true; + + this._handleIndex = index; + + closestHandle + .addClass( "ui-state-active" ) + .focus(); + + offset = closestHandle.offset(); + mouseOverHandle = !$( event.target ).parents().addBack().is( ".ui-slider-handle" ); + this._clickOffset = mouseOverHandle ? { left: 0, top: 0 } : { + left: event.pageX - offset.left - ( closestHandle.width() / 2 ), + top: event.pageY - offset.top - + ( closestHandle.height() / 2 ) - + ( parseInt( closestHandle.css("borderTopWidth"), 10 ) || 0 ) - + ( parseInt( closestHandle.css("borderBottomWidth"), 10 ) || 0) + + ( parseInt( closestHandle.css("marginTop"), 10 ) || 0) + }; + + if ( !this.handles.hasClass( "ui-state-hover" ) ) { + this._slide( event, index, normValue ); + } + this._animateOff = true; + return true; + }, + + _mouseStart: function() { + return true; + }, + + _mouseDrag: function( event ) { + var position = { x: event.pageX, y: event.pageY }, + normValue = this._normValueFromMouse( position ); + + this._slide( event, this._handleIndex, normValue ); + + return false; + }, + + _mouseStop: function( event ) { + this.handles.removeClass( "ui-state-active" ); + this._mouseSliding = false; + + this._stop( event, this._handleIndex ); + this._change( event, this._handleIndex ); + + this._handleIndex = null; + this._clickOffset = null; + this._animateOff = false; + + return false; + }, + + _detectOrientation: function() { + this.orientation = ( this.options.orientation === "vertical" ) ? "vertical" : "horizontal"; + }, + + _normValueFromMouse: function( position ) { + var pixelTotal, + pixelMouse, + percentMouse, + valueTotal, + valueMouse; + + if ( this.orientation === "horizontal" ) { + pixelTotal = this.elementSize.width; + pixelMouse = position.x - this.elementOffset.left - ( this._clickOffset ? this._clickOffset.left : 0 ); + } else { + pixelTotal = this.elementSize.height; + pixelMouse = position.y - this.elementOffset.top - ( this._clickOffset ? this._clickOffset.top : 0 ); + } + + percentMouse = ( pixelMouse / pixelTotal ); + if ( percentMouse > 1 ) { + percentMouse = 1; + } + if ( percentMouse < 0 ) { + percentMouse = 0; + } + if ( this.orientation === "vertical" ) { + percentMouse = 1 - percentMouse; + } + + valueTotal = this._valueMax() - this._valueMin(); + valueMouse = this._valueMin() + percentMouse * valueTotal; + + return this._trimAlignValue( valueMouse ); + }, + + _start: function( event, index ) { + var uiHash = { + handle: this.handles[ index ], + value: this.value() + }; + if ( this.options.values && this.options.values.length ) { + uiHash.value = this.values( index ); + uiHash.values = this.values(); + } + return this._trigger( "start", event, uiHash ); + }, + + _slide: function( event, index, newVal ) { + var otherVal, + newValues, + allowed; + + if ( this.options.values && this.options.values.length ) { + otherVal = this.values( index ? 0 : 1 ); + + if ( ( this.options.values.length === 2 && this.options.range === true ) && + ( ( index === 0 && newVal > otherVal) || ( index === 1 && newVal < otherVal ) ) + ) { + newVal = otherVal; + } + + if ( newVal !== this.values( index ) ) { + newValues = this.values(); + newValues[ index ] = newVal; + // A slide can be canceled by returning false from the slide callback + allowed = this._trigger( "slide", event, { + handle: this.handles[ index ], + value: newVal, + values: newValues + } ); + otherVal = this.values( index ? 0 : 1 ); + if ( allowed !== false ) { + this.values( index, newVal ); + } + } + } else { + if ( newVal !== this.value() ) { + // A slide can be canceled by returning false from the slide callback + allowed = this._trigger( "slide", event, { + handle: this.handles[ index ], + value: newVal + } ); + if ( allowed !== false ) { + this.value( newVal ); + } + } + } + }, + + _stop: function( event, index ) { + var uiHash = { + handle: this.handles[ index ], + value: this.value() + }; + if ( this.options.values && this.options.values.length ) { + uiHash.value = this.values( index ); + uiHash.values = this.values(); + } + + this._trigger( "stop", event, uiHash ); + }, + + _change: function( event, index ) { + if ( !this._keySliding && !this._mouseSliding ) { + var uiHash = { + handle: this.handles[ index ], + value: this.value() + }; + if ( this.options.values && this.options.values.length ) { + uiHash.value = this.values( index ); + uiHash.values = this.values(); + } + + //store the last changed value index for reference when handles overlap + this._lastChangedValue = index; + + this._trigger( "change", event, uiHash ); + } + }, + + value: function( newValue ) { + if ( arguments.length ) { + this.options.value = this._trimAlignValue( newValue ); + this._refreshValue(); + this._change( null, 0 ); + return; + } + + return this._value(); + }, + + values: function( index, newValue ) { + var vals, + newValues, + i; + + if ( arguments.length > 1 ) { + this.options.values[ index ] = this._trimAlignValue( newValue ); + this._refreshValue(); + this._change( null, index ); + return; + } + + if ( arguments.length ) { + if ( $.isArray( arguments[ 0 ] ) ) { + vals = this.options.values; + newValues = arguments[ 0 ]; + for ( i = 0; i < vals.length; i += 1 ) { + vals[ i ] = this._trimAlignValue( newValues[ i ] ); + this._change( null, i ); + } + this._refreshValue(); + } else { + if ( this.options.values && this.options.values.length ) { + return this._values( index ); + } else { + return this.value(); + } + } + } else { + return this._values(); + } + }, + + _setOption: function( key, value ) { + var i, + valsLength = 0; + + if ( key === "range" && this.options.range === true ) { + if ( value === "min" ) { + this.options.value = this._values( 0 ); + this.options.values = null; + } else if ( value === "max" ) { + this.options.value = this._values( this.options.values.length-1 ); + this.options.values = null; + } + } + + if ( $.isArray( this.options.values ) ) { + valsLength = this.options.values.length; + } + + $.Widget.prototype._setOption.apply( this, arguments ); + + switch ( key ) { + case "orientation": + this._detectOrientation(); + this.element + .removeClass( "ui-slider-horizontal ui-slider-vertical" ) + .addClass( "ui-slider-" + this.orientation ); + this._refreshValue(); + break; + case "value": + this._animateOff = true; + this._refreshValue(); + this._change( null, 0 ); + this._animateOff = false; + break; + case "values": + this._animateOff = true; + this._refreshValue(); + for ( i = 0; i < valsLength; i += 1 ) { + this._change( null, i ); + } + this._animateOff = false; + break; + case "min": + case "max": + this._animateOff = true; + this._refreshValue(); + this._animateOff = false; + break; + case "range": + this._animateOff = true; + this._refresh(); + this._animateOff = false; + break; + } + }, + + //internal value getter + // _value() returns value trimmed by min and max, aligned by step + _value: function() { + var val = this.options.value; + val = this._trimAlignValue( val ); + + return val; + }, + + //internal values getter + // _values() returns array of values trimmed by min and max, aligned by step + // _values( index ) returns single value trimmed by min and max, aligned by step + _values: function( index ) { + var val, + vals, + i; + + if ( arguments.length ) { + val = this.options.values[ index ]; + val = this._trimAlignValue( val ); + + return val; + } else if ( this.options.values && this.options.values.length ) { + // .slice() creates a copy of the array + // this copy gets trimmed by min and max and then returned + vals = this.options.values.slice(); + for ( i = 0; i < vals.length; i+= 1) { + vals[ i ] = this._trimAlignValue( vals[ i ] ); + } + + return vals; + } else { + return []; + } + }, + + // returns the step-aligned value that val is closest to, between (inclusive) min and max + _trimAlignValue: function( val ) { + if ( val <= this._valueMin() ) { + return this._valueMin(); + } + if ( val >= this._valueMax() ) { + return this._valueMax(); + } + var step = ( this.options.step > 0 ) ? this.options.step : 1, + valModStep = (val - this._valueMin()) % step, + alignValue = val - valModStep; + + if ( Math.abs(valModStep) * 2 >= step ) { + alignValue += ( valModStep > 0 ) ? step : ( -step ); + } + + // Since JavaScript has problems with large floats, round + // the final value to 5 digits after the decimal point (see #4124) + return parseFloat( alignValue.toFixed(5) ); + }, + + _valueMin: function() { + return this.options.min; + }, + + _valueMax: function() { + return this.options.max; + }, + + _refreshValue: function() { + var lastValPercent, valPercent, value, valueMin, valueMax, + oRange = this.options.range, + o = this.options, + that = this, + animate = ( !this._animateOff ) ? o.animate : false, + _set = {}; + + if ( this.options.values && this.options.values.length ) { + this.handles.each(function( i ) { + valPercent = ( that.values(i) - that._valueMin() ) / ( that._valueMax() - that._valueMin() ) * 100; + _set[ that.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; + $( this ).stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); + if ( that.options.range === true ) { + if ( that.orientation === "horizontal" ) { + if ( i === 0 ) { + that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { left: valPercent + "%" }, o.animate ); + } + if ( i === 1 ) { + that.range[ animate ? "animate" : "css" ]( { width: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + } else { + if ( i === 0 ) { + that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { bottom: ( valPercent ) + "%" }, o.animate ); + } + if ( i === 1 ) { + that.range[ animate ? "animate" : "css" ]( { height: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + } + } + lastValPercent = valPercent; + }); + } else { + value = this.value(); + valueMin = this._valueMin(); + valueMax = this._valueMax(); + valPercent = ( valueMax !== valueMin ) ? + ( value - valueMin ) / ( valueMax - valueMin ) * 100 : + 0; + _set[ this.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; + this.handle.stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); + + if ( oRange === "min" && this.orientation === "horizontal" ) { + this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { width: valPercent + "%" }, o.animate ); + } + if ( oRange === "max" && this.orientation === "horizontal" ) { + this.range[ animate ? "animate" : "css" ]( { width: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + if ( oRange === "min" && this.orientation === "vertical" ) { + this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { height: valPercent + "%" }, o.animate ); + } + if ( oRange === "max" && this.orientation === "vertical" ) { + this.range[ animate ? "animate" : "css" ]( { height: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + } + }, + + _handleEvents: { + keydown: function( event ) { + var allowed, curVal, newVal, step, + index = $( event.target ).data( "ui-slider-handle-index" ); + + switch ( event.keyCode ) { + case $.ui.keyCode.HOME: + case $.ui.keyCode.END: + case $.ui.keyCode.PAGE_UP: + case $.ui.keyCode.PAGE_DOWN: + case $.ui.keyCode.UP: + case $.ui.keyCode.RIGHT: + case $.ui.keyCode.DOWN: + case $.ui.keyCode.LEFT: + event.preventDefault(); + if ( !this._keySliding ) { + this._keySliding = true; + $( event.target ).addClass( "ui-state-active" ); + allowed = this._start( event, index ); + if ( allowed === false ) { + return; + } + } + break; + } + + step = this.options.step; + if ( this.options.values && this.options.values.length ) { + curVal = newVal = this.values( index ); + } else { + curVal = newVal = this.value(); + } + + switch ( event.keyCode ) { + case $.ui.keyCode.HOME: + newVal = this._valueMin(); + break; + case $.ui.keyCode.END: + newVal = this._valueMax(); + break; + case $.ui.keyCode.PAGE_UP: + newVal = this._trimAlignValue( curVal + ( (this._valueMax() - this._valueMin()) / numPages ) ); + break; + case $.ui.keyCode.PAGE_DOWN: + newVal = this._trimAlignValue( curVal - ( (this._valueMax() - this._valueMin()) / numPages ) ); + break; + case $.ui.keyCode.UP: + case $.ui.keyCode.RIGHT: + if ( curVal === this._valueMax() ) { + return; + } + newVal = this._trimAlignValue( curVal + step ); + break; + case $.ui.keyCode.DOWN: + case $.ui.keyCode.LEFT: + if ( curVal === this._valueMin() ) { + return; + } + newVal = this._trimAlignValue( curVal - step ); + break; + } + + this._slide( event, index, newVal ); + }, + click: function( event ) { + event.preventDefault(); + }, + keyup: function( event ) { + var index = $( event.target ).data( "ui-slider-handle-index" ); + + if ( this._keySliding ) { + this._keySliding = false; + this._stop( event, index ); + this._change( event, index ); + $( event.target ).removeClass( "ui-state-active" ); + } + } + } + +}); + +}(jQuery)); diff --git a/src/UI/Quality/QualityDefinitionCollection.js b/src/UI/Quality/QualityDefinitionCollection.js new file mode 100644 index 000000000..8da78717b --- /dev/null +++ b/src/UI/Quality/QualityDefinitionCollection.js @@ -0,0 +1,11 @@ +'use strict'; +define( + [ + 'backbone', + 'Quality/QualityDefinitionModel' + ], function (Backbone, QualityDefinitionModel) { + return Backbone.Collection.extend({ + model: QualityDefinitionModel, + url : window.NzbDrone.ApiRoot + '/qualitydefinition' + }); + }); diff --git a/src/UI/Quality/QualitySizeModel.js b/src/UI/Quality/QualityDefinitionModel.js similarity index 71% rename from src/UI/Quality/QualitySizeModel.js rename to src/UI/Quality/QualityDefinitionModel.js index d305969b3..e95c87a95 100644 --- a/src/UI/Quality/QualitySizeModel.js +++ b/src/UI/Quality/QualityDefinitionModel.js @@ -9,10 +9,10 @@ define( baseInitialize: ModelBase.prototype.initialize, initialize: function () { - var name = this.get('name'); + var name = this.get('quality').name; - this.successMessage = 'Saved ' + name + ' size settings'; - this.errorMessage = 'Couldn\'t save ' + name + ' size settings'; + this.successMessage = 'Saved ' + name + ' quality settings'; + this.errorMessage = 'Couldn\'t save ' + name + ' quality settings'; this.baseInitialize.call(this); } diff --git a/src/UI/Quality/QualitySizeCollection.js b/src/UI/Quality/QualitySizeCollection.js deleted file mode 100644 index 92c580647..000000000 --- a/src/UI/Quality/QualitySizeCollection.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; -define( - [ - 'backbone', - 'Quality/QualitySizeModel' - ], function (Backbone, QualitySizeModel) { - return Backbone.Collection.extend({ - model: QualitySizeModel, - url : window.NzbDrone.ApiRoot + '/qualitysize' - }); - }); diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.html b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.html new file mode 100644 index 000000000..bce6ecefa --- /dev/null +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.html @@ -0,0 +1,16 @@ +
    + Quality Definitions +
    +
    +
    +
    + Quality + Title + Size Limit +
    +
    +
    +
    +
    +
    +
    diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js new file mode 100644 index 000000000..48588bcbf --- /dev/null +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js @@ -0,0 +1,17 @@ +'use strict'; + +define( + [ + 'marionette', + 'backgrid', + 'Settings/Quality/Definition/QualityDefinitionView' + ], function (Marionette, Backgrid, QualityDefinitionView) { + + return Marionette.CompositeView.extend({ + template: 'Settings/Quality/Definition/QualityDefinitionCollectionTemplate', + + itemViewContainer: ".x-rows", + + itemView: QualityDefinitionView + }); + }); diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionTemplate.html b/src/UI/Settings/Quality/Definition/QualityDefinitionTemplate.html new file mode 100644 index 000000000..e61558bfc --- /dev/null +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionTemplate.html @@ -0,0 +1,31 @@ + + {{quality.name}} + + + + + +
    +
    +
    + + + + +
    +
    + + + + +
    +
    +
    \ No newline at end of file diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionView.js b/src/UI/Settings/Quality/Definition/QualityDefinitionView.js new file mode 100644 index 000000000..9b0465fe9 --- /dev/null +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionView.js @@ -0,0 +1,86 @@ +'use strict'; + +define( + [ + 'marionette', + 'Mixins/AsModelBoundView', + 'filesize', + 'jquery-ui' + ], function (Marionette, AsModelBoundView, fileSize) { + + var view = Marionette.ItemView.extend({ + template: 'Settings/Quality/Definition/QualityDefinitionTemplate', + className: 'row', + + ui: { + title : '.x-title', + sizeSlider : '.x-slider', + thirtyMinuteMinSize: '.x-min-thirty', + sixtyMinuteMinSize : '.x-min-sixty', + thirtyMinuteMaxSize: '.x-max-thirty', + sixtyMinuteMaxSize : '.x-max-sixty' + }, + + events: { + 'change .x-title': '_updateTitle', + 'slide .x-slider': '_updateSize' + }, + + initialize: function (options) { + this.qualityProfileCollection = options.qualityProfiles; + this.filesize = fileSize; + }, + + onRender: function () { + this.ui.sizeSlider.slider({ + range : true, + min : 0, + max : 200, + values : [ this.model.get('minSize'), this.model.get('maxSize') ], + }); + + this._changeSize(); + }, + + _updateTitle: function() { + this.model.set('title', this.ui.title.val()); + }, + + _updateSize: function (event, ui) { + this.model.set('minSize', ui.values[0]); + this.model.set('maxSize', ui.values[1]); + + this._changeSize(); + }, + + _changeSize: function () { + var minSize = this.model.get('minSize'); + var maxSize = this.model.get('maxSize'); + + { + var minBytes = minSize * 1024 * 1024; + var minThirty = fileSize(minBytes * 30, 1, false); + var minSixty = fileSize(minBytes * 60, 1, false); + + this.ui.thirtyMinuteMinSize.html(minThirty); + this.ui.sixtyMinuteMinSize.html(minSixty); + } + + { + var maxBytes = maxSize * 1024 * 1024; + var maxThirty = fileSize(maxBytes * 30, 1, false); + var maxSixty = fileSize(maxBytes * 60, 1, false); + + this.ui.thirtyMinuteMaxSize.html(maxThirty); + this.ui.sixtyMinuteMaxSize.html(maxSixty); + } + + /*if (parseInt(maxSize, 10) === 0) { + thirty = 'No Limit'; + sixty = 'No Limit'; + }*/ + } + }); + + return AsModelBoundView.call(view); + }); diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileItemView.js b/src/UI/Settings/Quality/Profile/EditQualityProfileItemView.js new file mode 100644 index 000000000..e93fa6dee --- /dev/null +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileItemView.js @@ -0,0 +1,9 @@ +'use strict'; +define( + [ + 'marionette' + ], function (Marionette) { + return Marionette.ItemView.extend({ + template : 'Settings/Quality/Profile/EditQualityProfileItemViewTemplate' + }); + }); diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html b/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html new file mode 100644 index 000000000..19d710178 --- /dev/null +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html @@ -0,0 +1,5 @@ + + + +{{name}} + diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html b/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html index f45c97dda..3a09e8922 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html @@ -29,25 +29,15 @@
    -
    +

    Available

    - +
      +
    -
    -
    -
    -

    Allowed

    - -
    -
    +
    +

    Allowed

    +
      +
    diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileView.js b/src/UI/Settings/Quality/Profile/EditQualityProfileView.js index 503d820fc..703c6b88c 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileView.js +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileView.js @@ -4,63 +4,113 @@ define( 'vent', 'marionette', 'backbone', + 'backbone.collectionview', + 'Settings/Quality/Profile/EditQualityProfileItemView', 'Mixins/AsModelBoundView', 'Mixins/AsValidatedView', 'underscore' - ], function (vent, Marionette, Backbone, AsModelBoundView, AsValidatedView, _) { + ], function (vent, Marionette, Backbone, BackboneSortableCollectionView, EditQualityProfileItemView, AsModelBoundView, AsValidatedView, _) { var view = Marionette.ItemView.extend({ template: 'Settings/Quality/Profile/EditQualityProfileTemplate', ui: { - cutoff: '.x-cutoff' + available: '.x-available-list', + allowed : '.x-allowed-list', + cutoff : '.x-cutoff' }, - + events: { 'click .x-save' : '_saveQualityProfile', - 'dblclick .x-available-list': '_moveQuality', - 'dblclick .x-allowed-list' : '_moveQuality' + //'click .x-qualityitem' : '_moveQuality', }, initialize: function (options) { this.profileCollection = options.profileCollection; + + this.availableCollection = new Backbone.Collection(this.model.get('available')); + this.availableCollection.comparator = function (model) { return -model.get('weight'); }; + this.availableCollection.sort(); + + this.allowedCollection = new Backbone.Collection(this.model.get('allowed').reverse()); + }, + + onRender: function() { + var listViewAvailable = new BackboneSortableCollectionView( { + el : this.ui.available, + modelView : EditQualityProfileItemView, + selectable: false, + sortable : false, + collection: this.availableCollection + }); + listViewAvailable.render(); + + var listViewAllowed = new BackboneSortableCollectionView( { + el : this.ui.allowed, + modelView : EditQualityProfileItemView, + selectable: false, + sortable : true, + sortableOptions : { + handle: ".x-drag-handle" + }, + collection : this.allowedCollection + } ); + listViewAllowed.render(); + + this.listenTo(listViewAvailable, "doubleClick", this._moveQuality); + this.listenTo(listViewAllowed, "doubleClick", this._moveQuality); + this.listenTo(listViewAllowed, "sortStop", this._updateModel); }, _moveQuality: function (event) { var quality; - var qualityId = event.target.value; - var availableCollection = new Backbone.Collection(this.model.get('available')); - availableCollection.comparator = function (model) { - return model.get('weight'); - }; - - var allowedCollection = new Backbone.Collection(this.model.get('allowed')); - allowedCollection.comparator = function (model) { - return model.get('weight'); - }; - - if (availableCollection.get(qualityId)) { - quality = availableCollection.get(qualityId); - availableCollection.remove(quality); - allowedCollection.add(quality); + var qualityId = event.get('id'); + + if (this.availableCollection.get(qualityId)) { + quality = this.availableCollection.get(qualityId); + var idealIndex = 0; + var idealMismatches = 1000; + // Insert it at the best possible spot. + for (var i = 0; i <= this.allowedCollection.length; i++) { + var mismatches = 0; + for (var j = 0; j < i; j++) { + if (this.allowedCollection.at(j).get('weight') < quality.get('weight')) + mismatches++; + } + for (j = i; j < this.allowedCollection.length; j++) { + if (this.allowedCollection.at(j).get('weight') > quality.get('weight')) + mismatches++; + } + if (mismatches <= idealMismatches) { + idealIndex = i; + idealMismatches = mismatches; + } + } + + this.availableCollection.remove(quality); + this.allowedCollection.add(quality, {at: idealIndex}); } - else if (allowedCollection.get(qualityId)) { - quality = allowedCollection.get(qualityId); + else if (this.allowedCollection.get(qualityId)) { + quality = this.allowedCollection.get(qualityId); - allowedCollection.remove(quality); - availableCollection.add(quality); + this.allowedCollection.remove(quality); + this.availableCollection.add(quality); } else { throw 'couldnt find quality id ' + qualityId; } - - this.model.set('available', availableCollection.toJSON()); - this.model.set('allowed', allowedCollection.toJSON()); - + + this._updateModel(); + }, + + _updateModel: function() { + this.model.set('available', this.availableCollection.toJSON().reverse()); + this.model.set('allowed', this.allowedCollection.toJSON().reverse()); + this.render(); }, - + _saveQualityProfile: function () { var self = this; var cutoff = _.findWhere(this.model.get('allowed'), { id: parseInt(this.ui.cutoff.val(), 10)}); diff --git a/src/UI/Settings/Quality/QualityLayout.js b/src/UI/Settings/Quality/QualityLayout.js index 09d4aca44..c80486209 100644 --- a/src/UI/Settings/Quality/QualityLayout.js +++ b/src/UI/Settings/Quality/QualityLayout.js @@ -5,27 +5,27 @@ define( 'marionette', 'Quality/QualityProfileCollection', 'Settings/Quality/Profile/QualityProfileCollectionView', - 'Quality/QualitySizeCollection', - 'Settings/Quality/Size/QualitySizeCollectionView' - ], function (Marionette, QualityProfileCollection, QualityProfileCollectionView, QualitySizeCollection, QualitySizeCollectionView) { + 'Quality/QualityDefinitionCollection', + 'Settings/Quality/Definition/QualityDefinitionCollectionView' + ], function (Marionette, QualityProfileCollection, QualityProfileCollectionView, QualityDefinitionCollection, QualityDefinitionCollectionView) { return Marionette.Layout.extend({ template: 'Settings/Quality/QualityLayoutTemplate', regions: { - qualityProfile : '#quality-profile', - qualitySize : '#quality-size' + qualityProfile : '#quality-profile', + qualityDefinition : '#quality-definition' }, initialize: function (options) { this.settings = options.settings; QualityProfileCollection.fetch(); - this.qualitySizeCollection = new QualitySizeCollection(); - this.qualitySizeCollection.fetch(); + this.qualityDefinitionCollection = new QualityDefinitionCollection(); + this.qualityDefinitionCollection.fetch(); }, onShow: function () { this.qualityProfile.show(new QualityProfileCollectionView({collection: QualityProfileCollection})); - this.qualitySize.show(new QualitySizeCollectionView({collection: this.qualitySizeCollection})); + this.qualityDefinition.show(new QualityDefinitionCollectionView({collection: this.qualityDefinitionCollection})); } }); }); diff --git a/src/UI/Settings/Quality/QualityLayoutTemplate.html b/src/UI/Settings/Quality/QualityLayoutTemplate.html index 5954e5eb4..9bd521d37 100644 --- a/src/UI/Settings/Quality/QualityLayoutTemplate.html +++ b/src/UI/Settings/Quality/QualityLayoutTemplate.html @@ -5,5 +5,5 @@
    -
    +
    diff --git a/src/UI/Settings/Quality/Size/QualitySizeCollectionTemplate.html b/src/UI/Settings/Quality/Size/QualitySizeCollectionTemplate.html deleted file mode 100644 index 6d406bdc0..000000000 --- a/src/UI/Settings/Quality/Size/QualitySizeCollectionTemplate.html +++ /dev/null @@ -1,4 +0,0 @@ -
    - Quality Size Limits -
      -
    diff --git a/src/UI/Settings/Quality/Size/QualitySizeCollectionView.js b/src/UI/Settings/Quality/Size/QualitySizeCollectionView.js deleted file mode 100644 index e27e01a41..000000000 --- a/src/UI/Settings/Quality/Size/QualitySizeCollectionView.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -define(['marionette', 'Settings/Quality/Size/QualitySizeView'], function (Marionette, QualitySizeView) { - return Marionette.CompositeView.extend({ - itemView : QualitySizeView, - itemViewContainer: '.quality-sizes', - template : 'Settings/Quality/Size/QualitySizeCollectionTemplate' - }); -}); diff --git a/src/UI/Settings/Quality/Size/QualitySizeTemplate.html b/src/UI/Settings/Quality/Size/QualitySizeTemplate.html deleted file mode 100644 index 657922e73..000000000 --- a/src/UI/Settings/Quality/Size/QualitySizeTemplate.html +++ /dev/null @@ -1,20 +0,0 @@ -
    -

    {{name}}

    -
    -
    -
    - - -
    -
    - - -
    -
    - -
    -
    \ No newline at end of file diff --git a/src/UI/Settings/Quality/Size/QualitySizeView.js b/src/UI/Settings/Quality/Size/QualitySizeView.js deleted file mode 100644 index 215304ba0..000000000 --- a/src/UI/Settings/Quality/Size/QualitySizeView.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -define( - [ - 'marionette', - 'Mixins/AsModelBoundView', - 'filesize', - 'jquery.knob' - ], function (Marionette, AsModelBoundView, fileSize) { - - var view = Marionette.ItemView.extend({ - template: 'Settings/Quality/Size/QualitySizeTemplate', - tagName : 'li', - - ui: { - knob : '.x-knob', - thirtyMinuteSize: '.x-size-thirty', - sixtyMinuteSize : '.x-size-sixty' - }, - - events: { - 'change .x-knob': '_changeMaxSize' - }, - - initialize: function (options) { - this.qualityProfileCollection = options.qualityProfiles; - this.filesize = fileSize; - }, - - onRender: function () { - this.ui.knob.knob({ - min : 0, - max : 200, - step : 1, - cursor : 25, - width : 150, - stopper : true, - displayInput: false - }); - - this._changeMaxSize(); - }, - - _changeMaxSize: function () { - var maxSize = this.model.get('maxSize'); - var bytes = maxSize * 1024 * 1024; - var thirty = fileSize(bytes * 30, 1, false); - var sixty = fileSize(bytes * 60, 1, false); - - if (parseInt(maxSize, 10) === 0) { - thirty = 'No Limit'; - sixty = 'No Limit'; - } - - this.ui.thirtyMinuteSize.html(thirty); - this.ui.sixtyMinuteSize.html(sixty); - } - }); - - return AsModelBoundView.call(view); - }); diff --git a/src/UI/Settings/Quality/quality.less b/src/UI/Settings/Quality/quality.less index 95d9f3a4f..60820eeca 100644 --- a/src/UI/Settings/Quality/quality.less +++ b/src/UI/Settings/Quality/quality.less @@ -1,6 +1,7 @@ @import "../../Shared/Styles/card"; +@import "../../Content/Bootstrap/mixins"; -.quality-profiles, .quality-sizes { +.quality-profiles { li { display: inline-block; vertical-align: top; @@ -35,41 +36,121 @@ } } -.quality-size-item { - - .card; - text-align: center; - - width: 200px; - height: 210px; - padding: 10px 15px; - - h3 { - margin-top: 0px; +ul.x-available-list, ul.x-allowed-list { + min-height: 100px; + + .user-select(none); + + margin: 0; + padding: 0; + list-style-type: none; + outline: none; + cursor: pointer; + + li { + margin: 2px; + padding: 2px; + line-height: 20px; + border: 1px solid #AAA; + border-radius: 4px; /* may need vendor varients */ + background: #FAFAFA; + + &:hover { + border-color: #888; + background: #EEE; + } + + .x-drag-handle, .x-moveleft-handle, .x-moveright-handle { + opacity: 0.0; + line-height: 20px; + } } +} - .size { - position: relative; - height: 100px; - margin: 10px; - text-align: center; +ul.x-available-list li { + .x-moveright-handle { + opacity: 0.2; + } + + .x-drag-handle { + display: none; } + + &:hover .x-moveright-handle { + opacity: 1.0; + } +} - .knob { - box-shadow: none; +ul.x-allowed-list li { + .x-drag-handle, .x-moveleft-handle { + opacity: 0.2; + } + + .x-drag-handle:hover { + opacity: 1.0; + cursor: pointer; } + + &:hover .x-moveleft-handle { + opacity: 1.0; + } +} - .size-value-wrapper { - position: absolute; - top: 50px; - width: 100%; +#quality-definition-list { - div { - margin-top: 2px; + .x-header .row { + font-weight: bold; + line-height: 40px; + } + + .x-rows .row { + line-height: 30px; + border-top: 1px solid #ddd; + vertical-align: middle; + padding: 5px; + + input { + margin-bottom: 0px; + } + + .size-label-wrapper { + line-height: 20px; + } + + .label { + min-width: 70px; + text-align: center; + margin: 0px 1px; + padding: 1px 4px; + } + + .ui-slider { + position: relative; + text-align: left; + background-color: #f5f5f5; + border-radius: 3px; + border: 1px solid #ccc; + height: 8px; + + .ui-slider-range { + position: absolute; + display: block; + background-color: #ddd; + height: 100%; + } + + .ui-slider-handle { + position: absolute; + z-index: 2; + width: 6px; + height: 12px; + cursor: default; + background-color: #ccc; + border: 1px solid #aaa; + border-radius: 3px; + top: -3px; + } } } } -#quality-size { - overflow: hidden; -} diff --git a/src/UI/Settings/SettingsLayout.js b/src/UI/Settings/SettingsLayout.js index 88d10205c..71948e76e 100644 --- a/src/UI/Settings/SettingsLayout.js +++ b/src/UI/Settings/SettingsLayout.js @@ -1,6 +1,7 @@ 'use strict'; define( [ + 'jquery', 'vent', 'marionette', 'backbone', @@ -17,7 +18,8 @@ define( 'Settings/General/GeneralView', 'Shared/LoadingView', 'Config' - ], function (vent, + ], function ($, + vent, Marionette, Backbone, SettingsModel, @@ -196,7 +198,7 @@ define( this.ui.advancedSettings.prop('checked', checked); if (checked) { - this.$el.addClass('show-advanced-settings'); + $('body').addClass('show-advanced-settings'); } }, @@ -205,11 +207,11 @@ define( Config.setValue('advancedSettings', checked); if (checked) { - this.$el.addClass('show-advanced-settings'); + $('body').addClass('show-advanced-settings'); } else { - this.$el.removeClass('show-advanced-settings'); + $('body').removeClass('show-advanced-settings'); } } }); diff --git a/src/UI/app.js b/src/UI/app.js index 736bd875a..d856cb62c 100644 --- a/src/UI/app.js +++ b/src/UI/app.js @@ -2,29 +2,31 @@ require.config({ paths: { - 'backbone' : 'JsLibraries/backbone', - 'moment' : 'JsLibraries/moment', - 'filesize' : 'JsLibraries/filesize', - 'handlebars' : 'JsLibraries/handlebars.runtime', - 'handlebars.helpers' : 'JsLibraries/handlebars.helpers', - 'bootstrap' : 'JsLibraries/bootstrap', - 'backbone.deepmodel' : 'JsLibraries/backbone.deep.model', - 'backbone.pageable' : 'JsLibraries/backbone.pageable', - 'backbone.validation' : 'JsLibraries/backbone.validation', - 'backbone.modelbinder': 'JsLibraries/backbone.modelbinder', - 'backgrid' : 'JsLibraries/backbone.backgrid', - 'backgrid.paginator' : 'JsLibraries/backbone.backgrid.paginator', - 'backgrid.selectall' : 'JsLibraries/backbone.backgrid.selectall', - 'fullcalendar' : 'JsLibraries/fullcalendar', - 'backstrech' : 'JsLibraries/jquery.backstretch', - 'underscore' : 'JsLibraries/lodash.underscore', - 'marionette' : 'JsLibraries/backbone.marionette', - 'signalR' : 'JsLibraries/jquery.signalR', - 'jquery.knob' : 'JsLibraries/jquery.knob', - 'jquery.dotdotdot' : 'JsLibraries/jquery.dotdotdot', - 'messenger' : 'JsLibraries/messenger', - 'jquery' : 'JsLibraries/jquery', - 'libs' : 'JsLibraries/', + 'backbone' : 'JsLibraries/backbone', + 'moment' : 'JsLibraries/moment', + 'filesize' : 'JsLibraries/filesize', + 'handlebars' : 'JsLibraries/handlebars.runtime', + 'handlebars.helpers' : 'JsLibraries/handlebars.helpers', + 'bootstrap' : 'JsLibraries/bootstrap', + 'backbone.deepmodel' : 'JsLibraries/backbone.deep.model', + 'backbone.pageable' : 'JsLibraries/backbone.pageable', + 'backbone.validation' : 'JsLibraries/backbone.validation', + 'backbone.modelbinder' : 'JsLibraries/backbone.modelbinder', + 'backbone.collectionview' : 'JsLibraries/backbone.collectionview', + 'backgrid' : 'JsLibraries/backbone.backgrid', + 'backgrid.paginator' : 'JsLibraries/backbone.backgrid.paginator', + 'backgrid.selectall' : 'JsLibraries/backbone.backgrid.selectall', + 'fullcalendar' : 'JsLibraries/fullcalendar', + 'backstrech' : 'JsLibraries/jquery.backstretch', + 'underscore' : 'JsLibraries/lodash.underscore', + 'marionette' : 'JsLibraries/backbone.marionette', + 'signalR' : 'JsLibraries/jquery.signalR', + 'jquery-ui' : 'JsLibraries/jquery-ui', + 'jquery.knob' : 'JsLibraries/jquery.knob', + 'jquery.dotdotdot' : 'JsLibraries/jquery.dotdotdot', + 'messenger' : 'JsLibraries/messenger', + 'jquery' : 'JsLibraries/jquery', + 'libs' : 'JsLibraries/', 'api': 'Require/require.api' }, @@ -105,6 +107,12 @@ require.config({ } }, + 'jquery-ui' : { + deps: + [ + 'jquery' + ] + }, 'jquery.knob' : { deps: [ @@ -143,6 +151,14 @@ require.config({ 'backbone' ] }, + 'backbone.collectionview': { + deps: + [ + 'backbone', + 'jquery-ui' + ], + exports: 'Backbone.CollectionView' + }, backgrid : { deps: [