diff --git a/.gitignore b/.gitignore index 3662126cd..d1a9a9a44 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ src/**/[Oo]bj/ # ReSharper is a .NET coding add-in _ReSharper* +_dotCover* # NCrunch *.ncrunch* diff --git a/Gruntfile.js b/Gruntfile.js index 8956b314f..fc75499ec 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -27,7 +27,7 @@ module.exports = function (grunt) { }, bootstrap: { - src : srcContent + 'Bootstrap/bootstrap.less', + src : srcContent + 'bootstrap.less', dest: destContent + 'bootstrap.css' }, general : { @@ -118,7 +118,7 @@ module.exports = function (grunt) { requirejs: { compile:{ options: { - mainConfigFile: "src/UI/app.js", + mainConfigFile: 'src/UI/app.js', fileExclusionRegExp: /^.*\.(?!js$)[^.]+$/, preserveLicenseComments: false, dir: outputDir, @@ -139,11 +139,11 @@ module.exports = function (grunt) { nospawn: false }, bootstrap : { - files: [ srcContent + 'Bootstrap/**', srcContent + 'FontAwesome/**'], + files: [ srcContent + 'Bootstrap/**', srcContent + 'FontAwesome/**', srcContent + 'bootstrap.less'], tasks: ['less:bootstrap','less:general'] }, generalLess: { - files: [ srcRoot + '**/*.less', '!**/Bootstrap/**', '!**/FontAwesome/**'], + files: [ srcRoot + '**/*.less', '!**/Bootstrap/**', '!**/FontAwesome/**', '!' + srcContent + '/bootstrap.less'], tasks: ['less:general'] }, handlebars : { diff --git a/build.ps1 b/build.ps1 index 69b85fcfe..0a20c3af3 100644 --- a/build.ps1 +++ b/build.ps1 @@ -6,6 +6,7 @@ $testPackageFolder = '.\_tests\' $testSearchPattern = '*.Test\bin\x86\Release' $sourceFolder = '.\src' $updateFolder = $outputFolder + '\NzbDrone.Update' +$updateFolderMono = $outputFolderMono + '\NzbDrone.Update' Function Build() { @@ -73,9 +74,6 @@ Function PackageMono() Copy-Item $outputFolder $outputFolderMono -recurse - Write-Host Removing Update Client - Remove-Item -Recurse -Force "$outputFolderMono\NzbDrone.Update" - Write-Host Creating MDBs get-childitem $outputFolderMono -File -Include @("*.exe", "*.dll") -Exclude @("MediaInfo.dll", "sqlite3.dll") -Recurse | foreach ($_) { Write-Host "Creating .mdb for $_" @@ -110,6 +108,9 @@ Function PackageMono() Remove-Item "$outputFolderMono\NzbDrone.Console.vshost.exe" + Write-Host Adding NzbDrone.Mono to UpdatePackage + Copy-Item $outputFolderMono\* $updateFolderMono -Filter NzbDrone.Mono.* + Write-Host "##teamcity[progressFinish 'Creating Mono Package']" } diff --git a/src/NzbDrone.Api/Authentication/AuthenticationService.cs b/src/NzbDrone.Api/Authentication/AuthenticationService.cs index b71b7a07e..997e3a93b 100644 --- a/src/NzbDrone.Api/Authentication/AuthenticationService.cs +++ b/src/NzbDrone.Api/Authentication/AuthenticationService.cs @@ -1,6 +1,10 @@ -using Nancy; +using System; +using System.Linq; +using Nancy; using Nancy.Authentication.Basic; using Nancy.Security; +using NzbDrone.Api.Extensions; +using NzbDrone.Common; using NzbDrone.Core.Configuration; namespace NzbDrone.Api.Authentication @@ -15,10 +19,12 @@ namespace NzbDrone.Api.Authentication { private readonly IConfigFileProvider _configFileProvider; private static readonly NzbDroneUser AnonymousUser = new NzbDroneUser { UserName = "Anonymous" }; + private static String API_KEY; public AuthenticationService(IConfigFileProvider configFileProvider) { _configFileProvider = configFileProvider; + API_KEY = configFileProvider.ApiKey; } public IUserIdentity Validate(string username, string password) @@ -47,9 +53,71 @@ namespace NzbDrone.Api.Authentication public bool IsAuthenticated(NancyContext context) { - if (context.CurrentUser == null && _configFileProvider.AuthenticationEnabled) return false; + var apiKey = GetApiKey(context); - return true; + if (context.Request.IsApiRequest()) + { + return ValidApiKey(apiKey); + } + + if (context.Request.IsFeedRequest()) + { + if (!Enabled) + { + return true; + } + + if (ValidUser(context) || ValidApiKey(apiKey)) + { + return true; + } + + return false; + } + + if (!Enabled) + { + return true; + } + + if (ValidUser(context)) + { + return true; + } + + return false; + } + + private bool ValidUser(NancyContext context) + { + if (context.CurrentUser != null) return true; + + return false; + } + + private bool ValidApiKey(string apiKey) + { + if (API_KEY.Equals(apiKey)) return true; + + return false; + } + + private string GetApiKey(NancyContext context) + { + var apiKeyHeader = context.Request.Headers["X-Api-Key"].FirstOrDefault(); + var apiKeyQueryString = context.Request.Query["ApiKey"]; + + if (!apiKeyHeader.IsNullOrWhiteSpace()) + { + return apiKeyHeader; + } + + if (apiKeyQueryString.HasValue) + { + return apiKeyQueryString.Value; + } + + return context.Request.Headers.Authorization; } } } diff --git a/src/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs b/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs similarity index 75% rename from src/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs rename to src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs index c5622eb75..8ded9e32b 100644 --- a/src/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs +++ b/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs @@ -1,16 +1,15 @@ using Nancy; using Nancy.Authentication.Basic; using Nancy.Bootstrapper; -using NzbDrone.Api.Extensions; using NzbDrone.Api.Extensions.Pipelines; namespace NzbDrone.Api.Authentication { - public class EnableBasicAuthInNancy : IRegisterNancyPipeline + public class EnableAuthInNancy : IRegisterNancyPipeline { private readonly IAuthenticationService _authenticationService; - public EnableBasicAuthInNancy(IAuthenticationService authenticationService) + public EnableAuthInNancy(IAuthenticationService authenticationService) { _authenticationService = authenticationService; } @@ -25,7 +24,7 @@ namespace NzbDrone.Api.Authentication { Response response = null; - if (!context.Request.IsApiRequest() && !_authenticationService.IsAuthenticated(context)) + if (!_authenticationService.IsAuthenticated(context)) { response = new Response { StatusCode = HttpStatusCode.Unauthorized }; } diff --git a/src/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs b/src/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs deleted file mode 100644 index e6aaeae27..000000000 --- a/src/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Linq; -using Nancy; -using Nancy.Bootstrapper; -using NzbDrone.Api.Extensions; -using NzbDrone.Api.Extensions.Pipelines; -using NzbDrone.Common; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Api.Authentication -{ - public class EnableStatelessAuthInNancy : IRegisterNancyPipeline - { - private static String API_KEY; - - public EnableStatelessAuthInNancy(IConfigFileProvider configFileProvider) - { - API_KEY = configFileProvider.ApiKey; - } - - public void Register(IPipelines pipelines) - { - pipelines.BeforeRequest.AddItemToEndOfPipeline(ValidateApiKey); - } - - public Response ValidateApiKey(NancyContext context) - { - Response response = null; - - var apiKey = GetApiKey(context); - - if ((context.Request.IsApiRequest() || context.Request.IsFeedRequest()) && !ValidApiKey(apiKey)) - { - response = new Response { StatusCode = HttpStatusCode.Unauthorized }; - } - - return response; - } - - private bool ValidApiKey(string apiKey) - { - if (!API_KEY.Equals(apiKey)) return false; - - return true; - } - - private string GetApiKey(NancyContext context) - { - var apiKeyHeader = context.Request.Headers["X-Api-Key"].FirstOrDefault(); - var apiKeyQueryString = context.Request.Query["ApiKey"]; - - if (!apiKeyHeader.IsNullOrWhiteSpace()) - { - return apiKeyHeader; - } - - if (apiKeyQueryString.HasValue) - { - return apiKeyQueryString.Value; - } - - return context.Request.Headers.Authorization; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Blacklist/BlacklistResource.cs b/src/NzbDrone.Api/Blacklist/BlacklistResource.cs index b9638cf9c..e75ca83f3 100644 --- a/src/NzbDrone.Api/Blacklist/BlacklistResource.cs +++ b/src/NzbDrone.Api/Blacklist/BlacklistResource.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using NzbDrone.Api.REST; using NzbDrone.Core.Qualities; +using NzbDrone.Api.Series; namespace NzbDrone.Api.Blacklist { @@ -12,5 +13,7 @@ namespace NzbDrone.Api.Blacklist public string SourceTitle { get; set; } public QualityModel Quality { get; set; } public DateTime Date { get; set; } + + public SeriesResource Series { get; set; } } } diff --git a/src/NzbDrone.Api/Calendar/CalendarModule.cs b/src/NzbDrone.Api/Calendar/CalendarModule.cs index 141961ad5..484d3a52b 100644 --- a/src/NzbDrone.Api/Calendar/CalendarModule.cs +++ b/src/NzbDrone.Api/Calendar/CalendarModule.cs @@ -59,7 +59,7 @@ namespace NzbDrone.Api.Calendar foreach (var episode in message.Episode.Episodes) { var resource = episode.InjectTo(); - resource.Downloading = true; + resource.Grabbed = true; BroadcastResourceChange(ModelAction.Updated, resource); } diff --git a/src/NzbDrone.Api/Config/HostConfigModule.cs b/src/NzbDrone.Api/Config/HostConfigModule.cs index 26c184c45..1c9d37aa7 100644 --- a/src/NzbDrone.Api/Config/HostConfigModule.cs +++ b/src/NzbDrone.Api/Config/HostConfigModule.cs @@ -3,7 +3,9 @@ using System.Reflection; using FluentValidation; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Update; using NzbDrone.Core.Validation; +using NzbDrone.Core.Validation.Paths; using Omu.ValueInjecter; namespace NzbDrone.Api.Config @@ -12,7 +14,7 @@ namespace NzbDrone.Api.Config { private readonly IConfigFileProvider _configFileProvider; - public HostConfigModule(ConfigFileProvider configFileProvider) + public HostConfigModule(IConfigFileProvider configFileProvider) : base("/config/host") { _configFileProvider = configFileProvider; @@ -29,6 +31,8 @@ namespace NzbDrone.Api.Config SharedValidator.RuleFor(c => c.SslPort).ValidPort().When(c => c.EnableSsl); SharedValidator.RuleFor(c => c.SslCertHash).NotEmpty().When(c => c.EnableSsl && OsInfo.IsWindows); + + SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script); } private HostConfigResource GetHostConfig() diff --git a/src/NzbDrone.Api/Config/HostConfigResource.cs b/src/NzbDrone.Api/Config/HostConfigResource.cs index 3387ba421..cc52c01c4 100644 --- a/src/NzbDrone.Api/Config/HostConfigResource.cs +++ b/src/NzbDrone.Api/Config/HostConfigResource.cs @@ -1,5 +1,6 @@ using System; using NzbDrone.Api.REST; +using NzbDrone.Core.Update; namespace NzbDrone.Api.Config { @@ -14,10 +15,12 @@ namespace NzbDrone.Api.Config public String Password { get; set; } public String LogLevel { get; set; } public String Branch { get; set; } - public Boolean AutoUpdate { get; set; } public String ApiKey { get; set; } public Boolean Torrent { get; set; } public String SslCertHash { get; set; } public String UrlBase { get; set; } + public Boolean UpdateAutomatically { get; set; } + public UpdateMechanism UpdateMechanism { get; set; } + public String UpdateScriptPath { get; set; } } } diff --git a/src/NzbDrone.Api/Directories/DirectoryLookupService.cs b/src/NzbDrone.Api/Directories/DirectoryLookupService.cs index 6f372d0e9..39af46d0a 100644 --- a/src/NzbDrone.Api/Directories/DirectoryLookupService.cs +++ b/src/NzbDrone.Api/Directories/DirectoryLookupService.cs @@ -37,7 +37,6 @@ namespace NzbDrone.Api.Directories return dirs; } - private List GetSubDirectories(string path) { try @@ -47,12 +46,15 @@ namespace NzbDrone.Api.Directories catch (DirectoryNotFoundException) { return new List(); - } catch (ArgumentException) { return new List(); } + catch (IOException) + { + return new List(); + } } } } diff --git a/src/NzbDrone.Api/Episodes/EpisodeModule.cs b/src/NzbDrone.Api/Episodes/EpisodeModule.cs index 91589b850..4e24cb78f 100644 --- a/src/NzbDrone.Api/Episodes/EpisodeModule.cs +++ b/src/NzbDrone.Api/Episodes/EpisodeModule.cs @@ -54,7 +54,7 @@ namespace NzbDrone.Api.Episodes foreach (var episode in message.Episode.Episodes) { var resource = episode.InjectTo(); - resource.Downloading = true; + resource.Grabbed = true; BroadcastResourceChange(ModelAction.Updated, resource); } diff --git a/src/NzbDrone.Api/Episodes/EpisodeResource.cs b/src/NzbDrone.Api/Episodes/EpisodeResource.cs index a767ef9e6..e1210cc7d 100644 --- a/src/NzbDrone.Api/Episodes/EpisodeResource.cs +++ b/src/NzbDrone.Api/Episodes/EpisodeResource.cs @@ -1,4 +1,5 @@ using System; +using Newtonsoft.Json; using NzbDrone.Api.REST; using NzbDrone.Core.MediaFiles; @@ -27,6 +28,8 @@ namespace NzbDrone.Api.Episodes public Core.Tv.Series Series { get; set; } public String SeriesTitle { get; set; } - public Boolean Downloading { get; set; } + //Hiding this so people don't think its usable (only used to set the initial state) + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public Boolean Grabbed { get; set; } } } diff --git a/src/NzbDrone.Api/Extensions/Pipelines/CacheHeaderPipeline.cs b/src/NzbDrone.Api/Extensions/Pipelines/CacheHeaderPipeline.cs index 183326415..1ac4ae86c 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/CacheHeaderPipeline.cs +++ b/src/NzbDrone.Api/Extensions/Pipelines/CacheHeaderPipeline.cs @@ -1,18 +1,11 @@ using Nancy; using Nancy.Bootstrapper; -using NzbDrone.Api.Frontend; +using NzbDrone.Common.EnvironmentInfo; namespace NzbDrone.Api.Extensions.Pipelines { - public class CacheHeaderPipeline : IRegisterNancyPipeline + public class NzbDroneVersionPipeline : IRegisterNancyPipeline { - private readonly ICacheableSpecification _cacheableSpecification; - - public CacheHeaderPipeline(ICacheableSpecification cacheableSpecification) - { - _cacheableSpecification = cacheableSpecification; - } - public void Register(IPipelines pipelines) { pipelines.AfterRequest.AddItemToStartOfPipeline(Handle); @@ -20,14 +13,7 @@ namespace NzbDrone.Api.Extensions.Pipelines private void Handle(NancyContext context) { - if (_cacheableSpecification.IsCacheable(context)) - { - context.Response.Headers.EnableCache(); - } - else - { - context.Response.Headers.DisableCache(); - } + context.Response.Headers.Add("X-ApplicationVersion", BuildInfo.Version.ToString()); } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Extensions/Pipelines/NzbDroneVersionPipeline.cs b/src/NzbDrone.Api/Extensions/Pipelines/NzbDroneVersionPipeline.cs new file mode 100644 index 000000000..183326415 --- /dev/null +++ b/src/NzbDrone.Api/Extensions/Pipelines/NzbDroneVersionPipeline.cs @@ -0,0 +1,33 @@ +using Nancy; +using Nancy.Bootstrapper; +using NzbDrone.Api.Frontend; + +namespace NzbDrone.Api.Extensions.Pipelines +{ + public class CacheHeaderPipeline : IRegisterNancyPipeline + { + private readonly ICacheableSpecification _cacheableSpecification; + + public CacheHeaderPipeline(ICacheableSpecification cacheableSpecification) + { + _cacheableSpecification = cacheableSpecification; + } + + public void Register(IPipelines pipelines) + { + pipelines.AfterRequest.AddItemToStartOfPipeline(Handle); + } + + private void Handle(NancyContext context) + { + if (_cacheableSpecification.IsCacheable(context)) + { + context.Response.Headers.EnableCache(); + } + else + { + context.Response.Headers.DisableCache(); + } + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Frontend/IsCacheableSpecification.cs b/src/NzbDrone.Api/Frontend/IsCacheableSpecification.cs index 58307b4a6..875e5727d 100644 --- a/src/NzbDrone.Api/Frontend/IsCacheableSpecification.cs +++ b/src/NzbDrone.Api/Frontend/IsCacheableSpecification.cs @@ -23,6 +23,7 @@ namespace NzbDrone.Api.Frontend if (context.Request.Path.StartsWith("/api", StringComparison.CurrentCultureIgnoreCase)) return false; if (context.Request.Path.StartsWith("/signalr", StringComparison.CurrentCultureIgnoreCase)) return false; if (context.Request.Path.EndsWith("main.js")) return false; + if (context.Request.Path.StartsWith("/feed", StringComparison.CurrentCultureIgnoreCase)) return false; if (context.Request.Path.StartsWith("/log", StringComparison.CurrentCultureIgnoreCase) && context.Request.Path.EndsWith(".txt", StringComparison.CurrentCultureIgnoreCase)) diff --git a/src/NzbDrone.Api/Frontend/Mappers/FaviconMapper.cs b/src/NzbDrone.Api/Frontend/Mappers/FaviconMapper.cs new file mode 100644 index 000000000..e3c810a8d --- /dev/null +++ b/src/NzbDrone.Api/Frontend/Mappers/FaviconMapper.cs @@ -0,0 +1,30 @@ +using System.IO; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Api.Frontend.Mappers +{ + public class FaviconMapper : StaticResourceMapperBase + { + private readonly IAppFolderInfo _appFolderInfo; + + public FaviconMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, Logger logger) + : base(diskProvider, logger) + { + _appFolderInfo = appFolderInfo; + } + + protected override string Map(string resourceUrl) + { + var path = Path.Combine("Content", "Images", "favicon.ico"); + + return Path.Combine(_appFolderInfo.StartUpFolder, "UI", path); + } + + public override bool CanHandle(string resourceUrl) + { + return resourceUrl.Equals("/favicon.ico"); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapper.cs b/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapper.cs index a55fafc71..a81bb4ab7 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapper.cs +++ b/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapper.cs @@ -1,6 +1,5 @@ using System.IO; using NLog; -using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; @@ -29,7 +28,7 @@ namespace NzbDrone.Api.Frontend.Mappers return resourceUrl.StartsWith("/Content") || resourceUrl.EndsWith(".js") || resourceUrl.EndsWith(".css") || - resourceUrl.EndsWith(".ico") || + (resourceUrl.EndsWith(".ico") && !resourceUrl.Equals("/favicon.ico")) || resourceUrl.EndsWith(".swf"); } } diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index e6d6a32c9..da8ffac05 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -85,8 +85,7 @@ Properties\SharedAssemblyInfo.cs - - + @@ -123,12 +122,14 @@ + + @@ -161,6 +162,7 @@ + diff --git a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs b/src/NzbDrone.Api/RootFolders/RootFolderModule.cs index be1c43674..36e034946 100644 --- a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs +++ b/src/NzbDrone.Api/RootFolders/RootFolderModule.cs @@ -42,7 +42,7 @@ namespace NzbDrone.Api.RootFolders private int CreateRootFolder(RootFolderResource rootFolderResource) { - return GetNewId(_rootFolderService.Add, rootFolderResource); + return GetNewId(_rootFolderService.Add, rootFolderResource); } private List GetRootFolders() diff --git a/src/NzbDrone.Api/System/SystemModule.cs b/src/NzbDrone.Api/System/SystemModule.cs index b56f1d14b..e951a8da0 100644 --- a/src/NzbDrone.Api/System/SystemModule.cs +++ b/src/NzbDrone.Api/System/SystemModule.cs @@ -5,6 +5,8 @@ using NzbDrone.Api.Extensions; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Lifecycle.Commands; namespace NzbDrone.Api.System { @@ -15,12 +17,14 @@ namespace NzbDrone.Api.System private readonly IRouteCacheProvider _routeCacheProvider; private readonly IConfigFileProvider _configFileProvider; private readonly IDatabase _database; + private readonly ILifecycleService _lifecycleService; public SystemModule(IAppFolderInfo appFolderInfo, IRuntimeInfo runtimeInfo, IRouteCacheProvider routeCacheProvider, IConfigFileProvider configFileProvider, - IDatabase database) + IDatabase database, + ILifecycleService lifecycleService) : base("system") { _appFolderInfo = appFolderInfo; @@ -28,8 +32,11 @@ namespace NzbDrone.Api.System _routeCacheProvider = routeCacheProvider; _configFileProvider = configFileProvider; _database = database; + _lifecycleService = lifecycleService; Get["/status"] = x => GetStatus(); Get["/routes"] = x => GetRoutes(); + Post["/shutdown"] = x => Shutdown(); + Post["/restart"] = x => Restart(); } private Response GetStatus() @@ -62,5 +69,18 @@ namespace NzbDrone.Api.System { return _routeCacheProvider.GetCache().Values.AsResponse(); } + + private Response Shutdown() + { + _lifecycleService.Shutdown(); + return "".AsResponse(); + } + + private Response Restart() + { + _lifecycleService.Restart(); + return "".AsResponse(); + } } } + \ No newline at end of file diff --git a/src/NzbDrone.Api/Update/UpdateModule.cs b/src/NzbDrone.Api/Update/UpdateModule.cs index 02696a9b8..959dd0d7c 100644 --- a/src/NzbDrone.Api/Update/UpdateModule.cs +++ b/src/NzbDrone.Api/Update/UpdateModule.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; -using NzbDrone.Api.REST; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Update; using NzbDrone.Api.Mapping; @@ -44,18 +41,4 @@ namespace NzbDrone.Api.Update return resources; } } - - public class UpdateResource : RestResource - { - [JsonConverter(typeof(Newtonsoft.Json.Converters.VersionConverter))] - public Version Version { get; set; } - - public String Branch { get; set; } - public DateTime ReleaseDate { get; set; } - public String FileName { get; set; } - public String Url { get; set; } - public Boolean IsUpgrade { get; set; } - public Boolean Installed { get; set; } - public UpdateChanges Changes { get; set; } - } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Update/UpdateResource.cs b/src/NzbDrone.Api/Update/UpdateResource.cs new file mode 100644 index 000000000..c12edfbc1 --- /dev/null +++ b/src/NzbDrone.Api/Update/UpdateResource.cs @@ -0,0 +1,22 @@ +using System; +using Newtonsoft.Json; +using NzbDrone.Api.REST; +using NzbDrone.Core.Update; + +namespace NzbDrone.Api.Update +{ + public class UpdateResource : RestResource + { + [JsonConverter(typeof(Newtonsoft.Json.Converters.VersionConverter))] + public Version Version { get; set; } + + public String Branch { get; set; } + public DateTime ReleaseDate { get; set; } + public String FileName { get; set; } + public String Url { get; set; } + public Boolean IsUpgrade { get; set; } + public Boolean Installed { get; set; } + public UpdateChanges Changes { get; set; } + public String Hash { get; set; } + } +} diff --git a/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs b/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs index faef69f54..63693a88f 100644 --- a/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs +++ b/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Moq; using NUnit.Framework; using NzbDrone.Common.Model; @@ -18,17 +19,25 @@ namespace NzbDrone.App.Test { Mocker.GetMock().Setup(c => c.GetCurrentProcess()) .Returns(new ProcessInfo() { Id = CURRENT_PROCESS_ID }); + + Mocker.GetMock() + .Setup(s => s.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME)) + .Returns(new List()); + + Mocker.GetMock() + .Setup(s => s.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME)) + .Returns(new List()); } [Test] public void should_continue_if_only_instance() { - Mocker.GetMock().Setup(c => c.FindNzbDroneProcesses()) - .Returns(new List - { - new ProcessInfo{Id = CURRENT_PROCESS_ID} - }); - + Mocker.GetMock() + .Setup(c => c.FindProcessByName(It.Is(f => f.Contains("NzbDrone")))) + .Returns(new List + { + new ProcessInfo {Id = CURRENT_PROCESS_ID} + }); Subject.PreventStartIfAlreadyRunning(); @@ -38,13 +47,13 @@ namespace NzbDrone.App.Test [Test] public void should_enforce_if_another_console_is_running() { - Mocker.GetMock() - .Setup(c => c.FindNzbDroneProcesses()) - .Returns(new List - { - new ProcessInfo{Id = 10}, - new ProcessInfo{Id = CURRENT_PROCESS_ID} - }); + Mocker.GetMock() + .Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME)) + .Returns(new List + { + new ProcessInfo {Id = 10}, + new ProcessInfo {Id = CURRENT_PROCESS_ID} + }); Assert.Throws(() => Subject.PreventStartIfAlreadyRunning()); Mocker.GetMock().Verify(c => c.LaunchWebUI(), Times.Once()); @@ -54,14 +63,14 @@ namespace NzbDrone.App.Test [Test] public void should_return_false_if_another_gui_is_running() { - Mocker.GetMock() - .Setup(c => c.FindNzbDroneProcesses()) - .Returns(new List - { - new ProcessInfo{Id = CURRENT_PROCESS_ID}, - new ProcessInfo{Id = 10} - - }); + Mocker.GetMock() + .Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME)) + .Returns(new List + { + new ProcessInfo {Id = CURRENT_PROCESS_ID}, + new ProcessInfo {Id = 10} + + }); Assert.Throws(() => Subject.PreventStartIfAlreadyRunning()); Mocker.GetMock().Verify(c => c.LaunchWebUI(), Times.Once()); diff --git a/src/NzbDrone.Automation.Test/NzbDrone.Automation.Test.csproj b/src/NzbDrone.Automation.Test/NzbDrone.Automation.Test.csproj index c2a226a14..c09932bcb 100644 --- a/src/NzbDrone.Automation.Test/NzbDrone.Automation.Test.csproj +++ b/src/NzbDrone.Automation.Test/NzbDrone.Automation.Test.csproj @@ -52,15 +52,17 @@ - - ..\packages\Selenium.WebDriver.2.37.0\lib\net40\WebDriver.dll - - - ..\packages\Selenium.Support.2.37.0\lib\net40\WebDriver.Support.dll - ..\packages\FluentAssertions.2.1.0.0\lib\net40\FluentAssertions.dll + + False + ..\packages\Selenium.WebDriver.2.41.0\lib\net40\WebDriver.dll + + + False + ..\packages\Selenium.Support.2.41.0\lib\net40\WebDriver.Support.dll + diff --git a/src/NzbDrone.Automation.Test/PageModel/PageBase.cs b/src/NzbDrone.Automation.Test/PageModel/PageBase.cs index 032d14426..b8e56a44d 100644 --- a/src/NzbDrone.Automation.Test/PageModel/PageBase.cs +++ b/src/NzbDrone.Automation.Test/PageModel/PageBase.cs @@ -13,6 +13,7 @@ namespace NzbDrone.Automation.Test.PageModel public PageBase(RemoteWebDriver driver) { _driver = driver; + driver.Manage().Window.Maximize(); } public IWebElement FindByClass(string className, int timeout = 5) @@ -52,7 +53,7 @@ namespace NzbDrone.Automation.Test.PageModel { get { - return Find(By.LinkText("Series")); + return FindByClass("x-series-nav"); } } @@ -60,7 +61,7 @@ namespace NzbDrone.Automation.Test.PageModel { get { - return Find(By.LinkText("Calendar")); + return FindByClass("x-calendar-nav"); } } @@ -68,7 +69,7 @@ namespace NzbDrone.Automation.Test.PageModel { get { - return Find(By.LinkText("History")); + return FindByClass("x-history-nav"); } } @@ -76,7 +77,7 @@ namespace NzbDrone.Automation.Test.PageModel { get { - return Find(By.LinkText("Wanted")); + return FindByClass("x-wanted-nav"); } } @@ -84,7 +85,7 @@ namespace NzbDrone.Automation.Test.PageModel { get { - return Find(By.LinkText("Settings")); + return FindByClass("x-settings-nav"); } } @@ -92,7 +93,7 @@ namespace NzbDrone.Automation.Test.PageModel { get { - return Find(By.PartialLinkText("System")); + return FindByClass("x-system-nav"); } } diff --git a/src/NzbDrone.Automation.Test/packages.config b/src/NzbDrone.Automation.Test/packages.config index f51e94b57..5e1ee41b7 100644 --- a/src/NzbDrone.Automation.Test/packages.config +++ b/src/NzbDrone.Automation.Test/packages.config @@ -3,6 +3,6 @@ - - + + \ No newline at end of file diff --git a/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs b/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs index e4a953fce..8c62331de 100644 --- a/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs +++ b/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs @@ -10,30 +10,26 @@ namespace NzbDrone.Common.Test.DiskProviderTests { public class DiskProviderFixtureBase : TestBase where TSubject : class, IDiskProvider { - DirectoryInfo _binFolder; - DirectoryInfo _binFolderCopy; - DirectoryInfo _binFolderMove; - [SetUp] public void Setup() { - _binFolder = new DirectoryInfo(Directory.GetCurrentDirectory()); - _binFolderCopy = new DirectoryInfo(Path.Combine(_binFolder.Parent.FullName, "bin_copy")); - _binFolderMove = new DirectoryInfo(Path.Combine(_binFolder.Parent.FullName, "bin_move")); - if (_binFolderCopy.Exists) - { - foreach (var file in _binFolderCopy.GetFiles("*", SearchOption.AllDirectories)) - { - file.Attributes = FileAttributes.Normal; - } - _binFolderCopy.Delete(true); - } + } - if (_binFolderMove.Exists) - { - _binFolderMove.Delete(true); - } + public DirectoryInfo GetFilledTempFolder() + { + var tempFolder = GetTempFilePath(); + Directory.CreateDirectory(tempFolder); + + File.WriteAllText(Path.Combine(tempFolder, Path.GetRandomFileName()), "RootFile"); + + var subDir = Path.Combine(tempFolder, Path.GetRandomFileName()); + Directory.CreateDirectory(subDir); + + File.WriteAllText(Path.Combine(subDir, Path.GetRandomFileName()), "SubFile1"); + File.WriteAllText(Path.Combine(subDir, Path.GetRandomFileName()), "SubFile2"); + + return new DirectoryInfo(tempFolder); } [Test] @@ -57,79 +53,94 @@ namespace NzbDrone.Common.Test.DiskProviderTests } [Test] - public void moveFile_should_overwrite_existing_file() + public void MoveFile_should_overwrite_existing_file() { + var source1 = GetTempFilePath(); + var source2 = GetTempFilePath(); + var destination = GetTempFilePath(); - Subject.CopyFolder(_binFolder.FullName, _binFolderCopy.FullName); + File.WriteAllText(source1, "SourceFile1"); + File.WriteAllText(source2, "SourceFile2"); - var targetPath = Path.Combine(_binFolderCopy.FullName, "file.move"); + Subject.MoveFile(source1, destination); + Subject.MoveFile(source2, destination); - Subject.MoveFile(_binFolderCopy.GetFiles("*.dll", SearchOption.AllDirectories).First().FullName, targetPath); - Subject.MoveFile(_binFolderCopy.GetFiles("*.pdb", SearchOption.AllDirectories).First().FullName, targetPath); - - File.Exists(targetPath).Should().BeTrue(); + File.Exists(destination).Should().BeTrue(); } [Test] - public void moveFile_should_not_move_overwrite_itself() + public void MoveFile_should_not_move_overwrite_itself() { + var source = GetTempFilePath(); - Subject.CopyFolder(_binFolder.FullName, _binFolderCopy.FullName); + File.WriteAllText(source, "SourceFile1"); - var targetPath = _binFolderCopy.GetFiles("*.dll", SearchOption.AllDirectories).First().FullName; + Subject.MoveFile(source, source); - Subject.MoveFile(targetPath, targetPath); - - File.Exists(targetPath).Should().BeTrue(); + File.Exists(source).Should().BeTrue(); ExceptionVerification.ExpectedWarns(1); } [Test] public void CopyFolder_should_copy_folder() { - Subject.CopyFolder(_binFolder.FullName, _binFolderCopy.FullName); - VerifyCopy(); + var source = GetFilledTempFolder(); + var destination = new DirectoryInfo(GetTempFilePath()); + + Subject.CopyFolder(source.FullName, destination.FullName); + + VerifyCopy(source.FullName, destination.FullName); } [Test] public void CopyFolder_should_overwrite_existing_folder() { - - - - Subject.CopyFolder(_binFolder.FullName, _binFolderCopy.FullName); - + var source = GetFilledTempFolder(); + var destination = new DirectoryInfo(GetTempFilePath()); + Subject.CopyFolder(source.FullName, destination.FullName); + //Delete Random File - _binFolderCopy.Refresh(); - _binFolderCopy.GetFiles("*.*", SearchOption.AllDirectories).First().Delete(); + destination.GetFiles("*.*", SearchOption.AllDirectories).First().Delete(); - Subject.CopyFolder(_binFolder.FullName, _binFolderCopy.FullName); + Subject.CopyFolder(source.FullName, destination.FullName); + VerifyCopy(source.FullName, destination.FullName); + } - VerifyCopy(); + [Test] + public void MoveFolder_should_move_folder() + { + var original = GetFilledTempFolder(); + var source = new DirectoryInfo(GetTempFilePath()); + var destination = new DirectoryInfo(GetTempFilePath()); + + Subject.CopyFolder(original.FullName, source.FullName); + + Subject.MoveFolder(source.FullName, destination.FullName); + + VerifyMove(original.FullName, source.FullName, destination.FullName); } [Test] public void MoveFolder_should_overwrite_existing_folder() { + var original = GetFilledTempFolder(); + var source = new DirectoryInfo(GetTempFilePath()); + var destination = new DirectoryInfo(GetTempFilePath()); + Subject.CopyFolder(original.FullName, source.FullName); + Subject.CopyFolder(original.FullName, destination.FullName); - Subject.CopyFolder(_binFolder.FullName, _binFolderCopy.FullName); - Subject.CopyFolder(_binFolder.FullName, _binFolderMove.FullName); - VerifyCopy(); + Subject.MoveFolder(source.FullName, destination.FullName); - - Subject.MoveFolder(_binFolderCopy.FullName, _binFolderMove.FullName); - - - VerifyMove(); + VerifyMove(original.FullName, source.FullName, destination.FullName); } [Test] public void move_read_only_file() { - var source = GetTestFilePath(); - var destination = GetTestFilePath(); + var source = GetTempFilePath(); + var destination = GetTempFilePath(); Subject.WriteAllText(source, "SourceFile"); Subject.WriteAllText(destination, "DestinationFile"); @@ -150,23 +161,25 @@ namespace NzbDrone.Common.Test.DiskProviderTests [Test] public void folder_should_return_correct_value_for_last_write() { - var testDir = Path.Combine(SandboxFolder, "LastWrite"); + var testDir = GetTempFilePath(); var testFile = Path.Combine(testDir, Path.GetRandomFileName()); Directory.CreateDirectory(testDir); + Subject.FolderSetLastWriteTimeUtc(TempFolder, DateTime.UtcNow.AddMinutes(-5)); + TestLogger.Info("Path is: {0}", testFile); Subject.WriteAllText(testFile, "Test"); - Subject.FolderGetLastWrite(SandboxFolder).Should().BeOnOrAfter(DateTime.UtcNow.AddMinutes(-1)); - Subject.FolderGetLastWrite(SandboxFolder).Should().BeBefore(DateTime.UtcNow.AddMinutes(1)); + Subject.FolderGetLastWrite(TempFolder).Should().BeOnOrAfter(DateTime.UtcNow.AddMinutes(-1)); + Subject.FolderGetLastWrite(TempFolder).Should().BeBefore(DateTime.UtcNow.AddMinutes(1)); } [Test] public void should_return_false_for_unlocked_file() { - var testFile = GetTestFilePath(); + var testFile = GetTempFilePath(); Subject.WriteAllText(testFile, new Guid().ToString()); Subject.IsFileLocked(testFile).Should().BeFalse(); @@ -175,7 +188,7 @@ namespace NzbDrone.Common.Test.DiskProviderTests [Test] public void should_return_false_for_unlocked_and_readonly_file() { - var testFile = GetTestFilePath(); + var testFile = GetTempFilePath(); Subject.WriteAllText(testFile, new Guid().ToString()); File.SetAttributes(testFile, FileAttributes.ReadOnly); @@ -186,7 +199,7 @@ namespace NzbDrone.Common.Test.DiskProviderTests [Test] public void should_return_true_for_unlocked_file() { - var testFile = GetTestFilePath(); + var testFile = GetTempFilePath(); Subject.WriteAllText(testFile, new Guid().ToString()); using (var file = File.OpenWrite(testFile)) @@ -198,7 +211,7 @@ namespace NzbDrone.Common.Test.DiskProviderTests [Test] public void should_be_able_to_set_permission_from_parrent() { - var testFile = GetTestFilePath(); + var testFile = GetTempFilePath(); Subject.WriteAllText(testFile, new Guid().ToString()); Subject.InheritFolderPermissions(testFile); @@ -208,33 +221,26 @@ namespace NzbDrone.Common.Test.DiskProviderTests [Explicit] public void check_last_write() { - Console.WriteLine(Subject.FolderGetLastWrite(_binFolder.FullName)); - Console.WriteLine(_binFolder.LastWriteTimeUtc); + Console.WriteLine(Subject.FolderGetLastWrite(GetFilledTempFolder().FullName)); + Console.WriteLine(GetFilledTempFolder().LastWriteTimeUtc); } - private void VerifyCopy() + private void VerifyCopy(string source, string destination) { - _binFolder.Refresh(); - _binFolderCopy.Refresh(); + var sourceFiles = Directory.GetFileSystemEntries(source, "*", SearchOption.AllDirectories).Select(v => v.Substring(source.Length + 1)).ToArray(); + var destFiles = Directory.GetFileSystemEntries(destination, "*", SearchOption.AllDirectories).Select(v => v.Substring(destination.Length + 1)).ToArray(); - _binFolderCopy.GetFiles("*.*", SearchOption.AllDirectories) - .Should().HaveSameCount(_binFolder.GetFiles("*.*", SearchOption.AllDirectories)); - - _binFolderCopy.GetDirectories().Should().HaveSameCount(_binFolder.GetDirectories()); + CollectionAssert.AreEquivalent(sourceFiles, destFiles); } - private void VerifyMove() + private void VerifyMove(string source, string from, string destination) { - _binFolder.Refresh(); - _binFolderCopy.Refresh(); - _binFolderMove.Refresh(); + Directory.Exists(from).Should().BeFalse(); - _binFolderCopy.Exists.Should().BeFalse(); + var sourceFiles = Directory.GetFileSystemEntries(source, "*", SearchOption.AllDirectories).Select(v => v.Substring(source.Length + 1)).ToArray(); + var destFiles = Directory.GetFileSystemEntries(destination, "*", SearchOption.AllDirectories).Select(v => v.Substring(destination.Length + 1)).ToArray(); - _binFolderMove.GetFiles("*.*", SearchOption.AllDirectories) - .Should().HaveSameCount(_binFolder.GetFiles("*.*", SearchOption.AllDirectories)); - - _binFolderMove.GetDirectories().Should().HaveSameCount(_binFolder.GetDirectories()); + CollectionAssert.AreEquivalent(sourceFiles, destFiles); } } } diff --git a/src/NzbDrone.Common.Test/DiskProviderTests/FreeSpaceFixtureBase.cs b/src/NzbDrone.Common.Test/DiskProviderTests/FreeSpaceFixtureBase.cs index f63942a11..098456276 100644 --- a/src/NzbDrone.Common.Test/DiskProviderTests/FreeSpaceFixtureBase.cs +++ b/src/NzbDrone.Common.Test/DiskProviderTests/FreeSpaceFixtureBase.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using FluentAssertions; using NUnit.Framework; using NzbDrone.Common.Disk; @@ -61,7 +62,17 @@ namespace NzbDrone.Common.Test.DiskProviderTests { WindowsOnly(); - Assert.Throws(() => Subject.GetAvailableSpace(@"Z:\NOT_A_REAL_PATH\DOES_NOT_EXIST".AsOsAgnostic())); + // Find a drive that doesn't exist. + for (char driveletter = 'Z'; driveletter > 'D' ; driveletter--) + { + if (new DriveInfo(driveletter.ToString()).IsReady) + continue; + + Assert.Throws(() => Subject.GetAvailableSpace(driveletter + @":\NOT_A_REAL_PATH\DOES_NOT_EXIST".AsOsAgnostic())); + return; + } + + Assert.Inconclusive("No drive available for testing."); } [Test] diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs index 8c75708e2..fd59e7eec 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -120,7 +120,7 @@ namespace NzbDrone.Common.Test public void get_actual_casing_should_return_actual_casing_for_local_file_in_windows() { WindowsOnly(); - var path = Process.GetCurrentProcess().MainModule.FileName; + var path = Environment.ExpandEnvironmentVariables("%SystemRoot%\\System32"); path.ToUpper().GetActualCasing().Should().Be(path); path.ToLower().GetActualCasing().Should().Be(path); } @@ -168,25 +168,25 @@ namespace NzbDrone.Common.Test [Test] public void Sanbox() { - GetIAppDirectoryInfo().GetUpdateSandboxFolder().Should().BeEquivalentTo(@"C:\Temp\Nzbdrone_update\".AsOsAgnostic()); + GetIAppDirectoryInfo().GetUpdateSandboxFolder().Should().BeEquivalentTo(@"C:\Temp\nzbdrone_update\".AsOsAgnostic()); } [Test] public void GetUpdatePackageFolder() { - GetIAppDirectoryInfo().GetUpdatePackageFolder().Should().BeEquivalentTo(@"C:\Temp\Nzbdrone_update\NzbDrone\".AsOsAgnostic()); + GetIAppDirectoryInfo().GetUpdatePackageFolder().Should().BeEquivalentTo(@"C:\Temp\nzbdrone_update\NzbDrone\".AsOsAgnostic()); } [Test] public void GetUpdateClientFolder() { - GetIAppDirectoryInfo().GetUpdateClientFolder().Should().BeEquivalentTo(@"C:\Temp\Nzbdrone_update\NzbDrone\NzbDrone.Update\".AsOsAgnostic()); + GetIAppDirectoryInfo().GetUpdateClientFolder().Should().BeEquivalentTo(@"C:\Temp\nzbdrone_update\NzbDrone\NzbDrone.Update\".AsOsAgnostic()); } [Test] public void GetUpdateClientExePath() { - GetIAppDirectoryInfo().GetUpdateClientExePath().Should().BeEquivalentTo(@"C:\Temp\Nzbdrone_update\NzbDrone.Update.exe".AsOsAgnostic()); + GetIAppDirectoryInfo().GetUpdateClientExePath().Should().BeEquivalentTo(@"C:\Temp\nzbdrone_update\NzbDrone.Update.exe".AsOsAgnostic()); } [Test] diff --git a/src/NzbDrone.Common.Test/WebClientTests.cs b/src/NzbDrone.Common.Test/WebClientTests.cs index b68d66c56..a2dfdd78f 100644 --- a/src/NzbDrone.Common.Test/WebClientTests.cs +++ b/src/NzbDrone.Common.Test/WebClientTests.cs @@ -2,6 +2,7 @@ using System; using FluentAssertions; using NUnit.Framework; +using NzbDrone.Common.Http; using NzbDrone.Test.Common; namespace NzbDrone.Common.Test diff --git a/src/NzbDrone.Common/Cache/CacheManger.cs b/src/NzbDrone.Common/Cache/CacheManager.cs similarity index 100% rename from src/NzbDrone.Common/Cache/CacheManger.cs rename to src/NzbDrone.Common/Cache/CacheManager.cs diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index c2c4d0075..47a14be5c 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -447,5 +447,15 @@ namespace NzbDrone.Common.Disk return driveInfo.VolumeLabel; } + + public FileStream StreamFile(string path) + { + if (!FileExists(path)) + { + throw new FileNotFoundException("Unable to find file: " + path, path); + } + + return new FileStream(path, FileMode.Open); + } } } diff --git a/src/NzbDrone.Common/Disk/IDiskProvider.cs b/src/NzbDrone.Common/Disk/IDiskProvider.cs index 896f393bb..b57486ff9 100644 --- a/src/NzbDrone.Common/Disk/IDiskProvider.cs +++ b/src/NzbDrone.Common/Disk/IDiskProvider.cs @@ -11,7 +11,6 @@ namespace NzbDrone.Common.Disk void InheritFolderPermissions(string filename); void SetPermissions(string path, string mask, string user, string group); long? GetTotalSize(string path); - DateTime FolderGetLastWrite(string path); DateTime FileGetLastWrite(string path); DateTime FileGetLastWriteUtc(string path); @@ -44,5 +43,6 @@ namespace NzbDrone.Common.Disk void EmptyFolder(string path); string[] GetFixedDrives(); string GetVolumeLabel(string path); + FileStream StreamFile(string path); } } \ No newline at end of file diff --git a/src/NzbDrone.Common/EnvironmentInfo/StartupContext.cs b/src/NzbDrone.Common/EnvironmentInfo/StartupContext.cs index dbf4e6d65..1e21eeb48 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/StartupContext.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/StartupContext.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Text; namespace NzbDrone.Common.EnvironmentInfo { diff --git a/src/NzbDrone.Common/HttpProvider.cs b/src/NzbDrone.Common/Http/HttpProvider.cs similarity index 97% rename from src/NzbDrone.Common/HttpProvider.cs rename to src/NzbDrone.Common/Http/HttpProvider.cs index 283e8946b..35d9f2eb7 100644 --- a/src/NzbDrone.Common/HttpProvider.cs +++ b/src/NzbDrone.Common/Http/HttpProvider.cs @@ -7,7 +7,7 @@ using System.Text; using NLog; using NzbDrone.Common.EnvironmentInfo; -namespace NzbDrone.Common +namespace NzbDrone.Common.Http { public interface IHttpProvider { @@ -15,7 +15,6 @@ namespace NzbDrone.Common string DownloadString(string url, string username, string password); string DownloadString(string url, ICredentials credentials); Dictionary GetHeader(string url); - Stream DownloadStream(string url, NetworkCredential credential = null); void DownloadFile(string url, string fileName); string PostCommand(string address, string username, string password, string command); @@ -33,6 +32,7 @@ namespace NzbDrone.Common { _logger = logger; _userAgent = String.Format("NzbDrone {0}", BuildInfo.Version); + ServicePointManager.Expect100Continue = false; } public string DownloadString(string url) @@ -132,8 +132,9 @@ namespace NzbDrone.Common byte[] byteArray = Encoding.ASCII.GetBytes(command); - var wc = new WebClient(); + var wc = new NzbDroneWebClient(); wc.Credentials = new NetworkCredential(username, password); + var response = wc.UploadData(address, "POST", byteArray); var text = Encoding.ASCII.GetString(response); diff --git a/src/NzbDrone.Common/Http/NzbDroneWebClient.cs b/src/NzbDrone.Common/Http/NzbDroneWebClient.cs new file mode 100644 index 000000000..bc39ac4cb --- /dev/null +++ b/src/NzbDrone.Common/Http/NzbDroneWebClient.cs @@ -0,0 +1,19 @@ +using System; +using System.Net; + +namespace NzbDrone.Common.Http +{ + public class NzbDroneWebClient : WebClient + { + protected override WebRequest GetWebRequest(Uri address) + { + var request = base.GetWebRequest(address); + if (request is HttpWebRequest) + { + ((HttpWebRequest)request).KeepAlive = false; + } + + return request; + } + } +} diff --git a/src/NzbDrone.Common/IEnumerableExtensions.cs b/src/NzbDrone.Common/IEnumerableExtensions.cs index 11626292a..e4d3f5bfe 100644 --- a/src/NzbDrone.Common/IEnumerableExtensions.cs +++ b/src/NzbDrone.Common/IEnumerableExtensions.cs @@ -12,5 +12,15 @@ namespace NzbDrone.Common return source.Where(element => knownKeys.Add(keySelector(element))); } + + public static void AddIfNotNull(this List source, TSource item) + { + if (item == null) + { + return; + } + + source.Add(item); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs new file mode 100644 index 000000000..8c5096976 --- /dev/null +++ b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs @@ -0,0 +1,20 @@ +using System.Text.RegularExpressions; + +namespace NzbDrone.Common.Instrumentation +{ + public class CleanseLogMessage + { + //TODO: remove password= + private static readonly Regex CleansingRegex = new Regex(@"(?<=apikey=)(\w+?)(?=\W|$|_)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static string Cleanse(string message) + { + if (message.IsNullOrWhiteSpace()) + { + return message; + } + + return CleansingRegex.Replace(message, ""); + } + } +} diff --git a/src/NzbDrone.Common/Instrumentation/LogTargets.cs b/src/NzbDrone.Common/Instrumentation/LogTargets.cs index 67e8f69d3..9230a428d 100644 --- a/src/NzbDrone.Common/Instrumentation/LogTargets.cs +++ b/src/NzbDrone.Common/Instrumentation/LogTargets.cs @@ -78,7 +78,7 @@ namespace NzbDrone.Common.Instrumentation private static void RegisterAppFile(IAppFolderInfo appFolderInfo) { - var fileTarget = new FileTarget(); + var fileTarget = new NzbDroneFileTarget(); fileTarget.Name = "rollingFileLogger"; fileTarget.FileName = Path.Combine(appFolderInfo.GetLogFolder(), "nzbdrone.txt"); @@ -104,7 +104,7 @@ namespace NzbDrone.Common.Instrumentation var fileTarget = new FileTarget(); fileTarget.Name = "updateFileLogger"; - fileTarget.FileName = Path.Combine(appFolderInfo.GetUpdateLogFolder(), DateTime.Now.ToString("yy.MM.d-HH.mm") + ".txt"); + fileTarget.FileName = Path.Combine(appFolderInfo.GetUpdateLogFolder(), DateTime.Now.ToString("yyyy.MM.dd-HH.mm") + ".txt"); fileTarget.AutoFlush = true; fileTarget.KeepFileOpen = false; fileTarget.ConcurrentWrites = false; diff --git a/src/NzbDrone.Common/Instrumentation/NzbDroneFileTarget.cs b/src/NzbDrone.Common/Instrumentation/NzbDroneFileTarget.cs new file mode 100644 index 000000000..62e41b0e0 --- /dev/null +++ b/src/NzbDrone.Common/Instrumentation/NzbDroneFileTarget.cs @@ -0,0 +1,13 @@ +using NLog; +using NLog.Targets; + +namespace NzbDrone.Common.Instrumentation +{ + public class NzbDroneFileTarget : FileTarget + { + protected override string GetFormattedMessage(LogEventInfo logEvent) + { + return CleanseLogMessage.Cleanse(Layout.Render(logEvent)); + } + } +} diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index b2dd6301f..f2b61218e 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -61,7 +61,7 @@ - + @@ -93,16 +93,20 @@ + + Component + + + - @@ -125,7 +129,7 @@ - + diff --git a/src/NzbDrone.Common/PathExtensions.cs b/src/NzbDrone.Common/PathExtensions.cs index 16a9bddd1..a5c171039 100644 --- a/src/NzbDrone.Common/PathExtensions.cs +++ b/src/NzbDrone.Common/PathExtensions.cs @@ -13,10 +13,10 @@ namespace NzbDrone.Common private const string NZBDRONE_LOG_DB = "logs.db"; private const string BACKUP_ZIP_FILE = "NzbDrone_Backup.zip"; private const string NLOG_CONFIG_FILE = "nlog.config"; - private const string UPDATE_CLIENT_EXE = "nzbdrone.update.exe"; + private const string UPDATE_CLIENT_EXE = "NzbDrone.Update.exe"; private static readonly string UPDATE_SANDBOX_FOLDER_NAME = "nzbdrone_update" + Path.DirectorySeparatorChar; - private static readonly string UPDATE_PACKAGE_FOLDER_NAME = "nzbdrone" + Path.DirectorySeparatorChar; + private static readonly string UPDATE_PACKAGE_FOLDER_NAME = "NzbDrone" + Path.DirectorySeparatorChar; private static readonly string UPDATE_BACKUP_FOLDER_NAME = "nzbdrone_backup" + Path.DirectorySeparatorChar; private static readonly string UPDATE_BACKUP_APPDATA_FOLDER_NAME = "nzbdrone_appdata_backup" + Path.DirectorySeparatorChar; private static readonly string UPDATE_CLIENT_FOLDER_NAME = "NzbDrone.Update" + Path.DirectorySeparatorChar; diff --git a/src/NzbDrone.Common/Processes/INzbDroneProcessProvider.cs b/src/NzbDrone.Common/Processes/INzbDroneProcessProvider.cs deleted file mode 100644 index 71abd6d0e..000000000 --- a/src/NzbDrone.Common/Processes/INzbDroneProcessProvider.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Common.Model; - -namespace NzbDrone.Common.Processes -{ - public interface INzbDroneProcessProvider - { - List FindNzbDroneProcesses(); - } -} diff --git a/src/NzbDrone.Common/Processes/ProcessProvider.cs b/src/NzbDrone.Common/Processes/ProcessProvider.cs index 7581a4b5c..a2eff8d3d 100644 --- a/src/NzbDrone.Common/Processes/ProcessProvider.cs +++ b/src/NzbDrone.Common/Processes/ProcessProvider.cs @@ -21,7 +21,8 @@ namespace NzbDrone.Common.Processes void SetPriority(int processId, ProcessPriorityClass priority); void KillAll(string processName); void Kill(int processId); - bool Exists(string processName); + Boolean Exists(int processId); + Boolean Exists(string processName); ProcessPriorityClass GetCurrentProcessPriority(); Process Start(string path, string args = null, Action onOutputDataReceived = null, Action onErrorDataReceived = null); Process SpawnNewProcess(string path, string args = null); @@ -35,20 +36,17 @@ namespace NzbDrone.Common.Processes public const string NZB_DRONE_PROCESS_NAME = "NzbDrone"; public const string NZB_DRONE_CONSOLE_PROCESS_NAME = "NzbDrone.Console"; - private static List GetProcessesByName(string name) - { - var monoProcesses = Process.GetProcessesByName("mono") - .Where(process => process.Modules.Cast().Any(module => module.ModuleName.ToLower() == name.ToLower() + ".exe")); - return Process.GetProcessesByName(name) - .Union(monoProcesses).ToList(); - } - public ProcessInfo GetCurrentProcess() { return ConvertToProcessInfo(Process.GetCurrentProcess()); } - public bool Exists(string processName) + public bool Exists(int processId) + { + return GetProcessById(processId) != null; + } + + public Boolean Exists(string processName) { return GetProcessesByName(processName).Any(); } @@ -78,7 +76,7 @@ namespace NzbDrone.Common.Processes public List FindProcessByName(string name) { - return Process.GetProcessesByName(name).Select(ConvertToProcessInfo).Where(c => c != null).ToList(); + return GetProcessesByName(name).Select(ConvertToProcessInfo).Where(c => c != null).ToList(); } public void OpenDefaultBrowser(string url) @@ -203,12 +201,40 @@ namespace NzbDrone.Common.Processes process.PriorityClass = priority; } + public void Kill(int processId) + { + var process = Process.GetProcesses().FirstOrDefault(p => p.Id == processId); + + if (process == null) + { + Logger.Warn("Cannot find process with id: {0}", processId); + return; + } + + process.Refresh(); + + if (process.HasExited) + { + Logger.Debug("Process has already exited"); + return; + } + + Logger.Info("[{0}]: Killing process", process.Id); + process.Kill(); + Logger.Info("[{0}]: Waiting for exit", process.Id); + process.WaitForExit(); + Logger.Info("[{0}]: Process terminated successfully", process.Id); + } + public void KillAll(string processName) { - var processToKill = GetProcessesByName(processName); + var processes = GetProcessesByName(processName); - foreach (var processInfo in processToKill) + Logger.Debug("Found {0} processes to kill", processes.Count); + + foreach (var processInfo in processes) { + Logger.Debug("Killing process: {0} [{1}]", processInfo.Id, processInfo.ProcessName); Kill(processInfo.Id); } } @@ -254,29 +280,23 @@ namespace NzbDrone.Common.Processes return process.Modules.Cast().FirstOrDefault(module => module.ModuleName.ToLower().EndsWith(".exe")).FileName; } - public void Kill(int processId) + private static List GetProcessesByName(string name) { - var process = Process.GetProcesses().FirstOrDefault(p => p.Id == processId); + //TODO: move this to an OS specific class - if (process == null) - { - Logger.Warn("Cannot find process with id: {0}", processId); - return; - } + var monoProcesses = Process.GetProcessesByName("mono") + .Union(Process.GetProcessesByName("mono-sgen")) + .Where(process => + process.Modules.Cast() + .Any(module => + module.ModuleName.ToLower() == name.ToLower() + ".exe")); - process.Refresh(); + var processes = Process.GetProcessesByName(name) + .Union(monoProcesses).ToList(); - if (process.HasExited) - { - Logger.Debug("Process has already exited"); - return; - } + Logger.Debug("Found {0} processes with the name: {1}", processes.Count, name); - Logger.Info("[{0}]: Killing process", process.Id); - process.Kill(); - Logger.Info("[{0}]: Waiting for exit", process.Id); - process.WaitForExit(); - Logger.Info("[{0}]: Process terminated successfully", process.Id); + return processes; } } } diff --git a/src/NzbDrone.Common/Security/IgnoreCertErrorPolicy.cs b/src/NzbDrone.Common/Security/IgnoreCertErrorPolicy.cs index 8e09b8024..604d357ba 100644 --- a/src/NzbDrone.Common/Security/IgnoreCertErrorPolicy.cs +++ b/src/NzbDrone.Common/Security/IgnoreCertErrorPolicy.cs @@ -13,6 +13,15 @@ namespace NzbDrone.Common.Security private static bool ValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslpolicyerrors) { + var request = sender as HttpWebRequest; + + if (request != null && + request.Address.OriginalString.ContainsIgnoreCase("nzbdrone.com") && + sslpolicyerrors != SslPolicyErrors.None) + { + return false; + } + return true; } } diff --git a/src/NzbDrone.Common/StringExtensions.cs b/src/NzbDrone.Common/StringExtensions.cs index 21b1db970..77a6c9532 100644 --- a/src/NzbDrone.Common/StringExtensions.cs +++ b/src/NzbDrone.Common/StringExtensions.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using ICSharpCode.SharpZipLib.Zip; namespace NzbDrone.Common { @@ -65,5 +66,20 @@ namespace NzbDrone.Common { return String.IsNullOrWhiteSpace(text); } + + public static bool ContainsIgnoreCase(this string text, string contains) + { + return text.IndexOf(contains, StringComparison.InvariantCultureIgnoreCase) > -1; + } + + public static string WrapInQuotes(this string text) + { + if (!text.Contains(" ")) + { + return text; + } + + return "\"" + text + "\""; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/DataAugmentationFixture/Scene/SceneMappingProxyFixture.cs b/src/NzbDrone.Core.Test/DataAugmentationFixture/Scene/SceneMappingProxyFixture.cs index e2d18dc89..93e2664e3 100644 --- a/src/NzbDrone.Core.Test/DataAugmentationFixture/Scene/SceneMappingProxyFixture.cs +++ b/src/NzbDrone.Core.Test/DataAugmentationFixture/Scene/SceneMappingProxyFixture.cs @@ -4,6 +4,7 @@ using FluentAssertions; using Newtonsoft.Json; using NUnit.Framework; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.Test.Framework; diff --git a/src/NzbDrone.Core.Test/Datastore/MappingExtentionFixture.cs b/src/NzbDrone.Core.Test/Datastore/MappingExtentionFixture.cs index 0aaaf840f..cbc032849 100644 --- a/src/NzbDrone.Core.Test/Datastore/MappingExtentionFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/MappingExtentionFixture.cs @@ -5,7 +5,7 @@ using Marr.Data; using NUnit.Framework; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Converters; -using NzbDrone.Core.Datastore.Extentions; +using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Test.Datastore diff --git a/src/NzbDrone.Core.Test/Datastore/PagingSpecExtenstionsTests/PagingOffsetFixture.cs b/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/PagingOffsetFixture.cs similarity index 86% rename from src/NzbDrone.Core.Test/Datastore/PagingSpecExtenstionsTests/PagingOffsetFixture.cs rename to src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/PagingOffsetFixture.cs index 65819a686..5ece0f8a4 100644 --- a/src/NzbDrone.Core.Test/Datastore/PagingSpecExtenstionsTests/PagingOffsetFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/PagingOffsetFixture.cs @@ -1,10 +1,10 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Datastore; -using NzbDrone.Core.Datastore.Extentions; +using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.Test.Datastore.PagingSpecExtenstionsTests +namespace NzbDrone.Core.Test.Datastore.PagingSpecExtensionsTests { public class PagingOffsetFixture { diff --git a/src/NzbDrone.Core.Test/Datastore/PagingSpecExtenstionsTests/ToSortDirectionFixture.cs b/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/ToSortDirectionFixture.cs similarity index 90% rename from src/NzbDrone.Core.Test/Datastore/PagingSpecExtenstionsTests/ToSortDirectionFixture.cs rename to src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/ToSortDirectionFixture.cs index de74a63f2..b0eaa64c0 100644 --- a/src/NzbDrone.Core.Test/Datastore/PagingSpecExtenstionsTests/ToSortDirectionFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/ToSortDirectionFixture.cs @@ -1,10 +1,10 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Datastore; -using NzbDrone.Core.Datastore.Extentions; +using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.Test.Datastore.PagingSpecExtenstionsTests +namespace NzbDrone.Core.Test.Datastore.PagingSpecExtensionsTests { public class ToSortDirectionFixture { diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs index 117c81ec0..5515fcedf 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs @@ -48,7 +48,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Series = series, Release = new ReleaseInfo(), ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, true) }, - Episodes = new List { new Episode() } + Episodes = new List { new Episode() { Id = 2 } } }; @@ -59,13 +59,21 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .Build(); Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); + + Mocker.GetMock().Setup( + s => s.GetEpisodesBySeason(It.IsAny(), It.IsAny())) + .Returns(new List() { + new Episode(), new Episode(), new Episode(), new Episode(), new Episode(), + new Episode(), new Episode(), new Episode(), new Episode() { Id = 2 }, new Episode() }); } private void GivenLastEpisode() { Mocker.GetMock().Setup( - s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) - .Returns(true); + s => s.GetEpisodesBySeason(It.IsAny(), It.IsAny())) + .Returns(new List() { + new Episode(), new Episode(), new Episode(), new Episode(), new Episode(), + new Episode(), new Episode(), new Episode(), new Episode(), new Episode() { Id = 2 } }); } [TestCase(30, 50, false)] @@ -110,10 +118,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultMulti.Series = series; parseResultMulti.Release.Size = sizeInMegaBytes.Megabytes(); - Mocker.GetMock().Setup( - s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) - .Returns(false); - Subject.IsSatisfiedBy(parseResultMulti, null).Should().Be(expectedResult); } @@ -129,10 +133,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultMultiSet.Series = series; parseResultMultiSet.Release.Size = sizeInMegaBytes.Megabytes(); - Mocker.GetMock().Setup( - s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) - .Returns(false); - Subject.IsSatisfiedBy(parseResultMultiSet, null).Should().Be(expectedResult); } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/BlackholeProviderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/BlackholeProviderFixture.cs index 9f1c9d317..6e993c320 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/BlackholeProviderFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/BlackholeProviderFixture.cs @@ -4,6 +4,7 @@ using Moq; using NUnit.Framework; using NzbDrone.Common; using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients; using NzbDrone.Core.Download.Clients.Blackhole; diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs index 3d564a69f..5903fa993 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs @@ -5,6 +5,7 @@ using Moq; using NUnit.Framework; using NzbDrone.Common; using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients; diff --git a/src/NzbDrone.Core.Test/FluentTest.cs b/src/NzbDrone.Core.Test/FluentTest.cs index 7af26ed45..66addf0c7 100644 --- a/src/NzbDrone.Core.Test/FluentTest.cs +++ b/src/NzbDrone.Core.Test/FluentTest.cs @@ -4,6 +4,7 @@ using System.Text; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; namespace NzbDrone.Core.Test { @@ -22,10 +23,11 @@ namespace NzbDrone.Core.Test } [Test] - [ExpectedException(typeof(ArgumentNullException))] public void WithDefault_Fail() { - "test".WithDefault(null); + Assert.Throws(() => "test".WithDefault(null)); + + ExceptionVerification.IgnoreWarns(); } [Test] diff --git a/src/NzbDrone.Core.Test/Framework/CoreTest.cs b/src/NzbDrone.Core.Test/Framework/CoreTest.cs index 8e84294f7..4c9b90803 100644 --- a/src/NzbDrone.Core.Test/Framework/CoreTest.cs +++ b/src/NzbDrone.Core.Test/Framework/CoreTest.cs @@ -2,6 +2,7 @@ using NUnit.Framework; using NzbDrone.Common; using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Framework diff --git a/src/NzbDrone.Core.Test/IndexerTests/SeasonSearchFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/SeasonSearchFixture.cs index 47670ba63..7fb252ff3 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/SeasonSearchFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/SeasonSearchFixture.cs @@ -5,6 +5,7 @@ using FluentValidation.Results; using Moq; using NUnit.Framework; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Core.Indexers; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Core.Test/MediaCoverTests/CoverExistsSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaCoverTests/CoverExistsSpecificationFixture.cs index 62add23d1..58f842b65 100644 --- a/src/NzbDrone.Core.Test/MediaCoverTests/CoverExistsSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaCoverTests/CoverExistsSpecificationFixture.cs @@ -4,6 +4,7 @@ using Moq; using NUnit.Framework; using NzbDrone.Common; using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; using NzbDrone.Core.MediaCover; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common; diff --git a/src/NzbDrone.Core.Test/NotificationTests/PlexProviderTest.cs b/src/NzbDrone.Core.Test/NotificationTests/PlexProviderTest.cs index 90b0d2c5f..98de20e9a 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/PlexProviderTest.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/PlexProviderTest.cs @@ -6,6 +6,7 @@ using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Plex; using NzbDrone.Core.Test.Framework; diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/GetJsonVersionFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/GetJsonVersionFixture.cs index 7ade31623..1f92a27ec 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/GetJsonVersionFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/GetJsonVersionFixture.cs @@ -2,6 +2,7 @@ using Moq; using NUnit.Framework; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Xbmc; using NzbDrone.Core.Notifications.Xbmc.Model; using NzbDrone.Core.Test.Framework; diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/ActivePlayersFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/ActivePlayersFixture.cs index 22dd65139..a5c269444 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/ActivePlayersFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/ActivePlayersFixture.cs @@ -3,6 +3,7 @@ using System.Linq; using FluentAssertions; using NUnit.Framework; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Xbmc; using NzbDrone.Core.Test.Framework; diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/GetSeriesPathFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/GetSeriesPathFixture.cs index 236a8a952..c64062528 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/GetSeriesPathFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/GetSeriesPathFixture.cs @@ -2,6 +2,7 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Xbmc; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/UpdateFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/UpdateFixture.cs index 7f69613ea..ac4a19224 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/UpdateFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Http/UpdateFixture.cs @@ -1,6 +1,7 @@ using FizzWare.NBuilder; using NUnit.Framework; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Xbmc; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/ActivePlayersFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/ActivePlayersFixture.cs index 43f045b79..7047ffe5d 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/ActivePlayersFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/ActivePlayersFixture.cs @@ -3,6 +3,7 @@ using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Xbmc; using NzbDrone.Core.Test.Framework; diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/GetSeriesPathFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/GetSeriesPathFixture.cs index 5a28902c9..19c846c07 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/GetSeriesPathFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/GetSeriesPathFixture.cs @@ -2,6 +2,7 @@ using Moq; using NUnit.Framework; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Xbmc; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; diff --git a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/UpdateFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/UpdateFixture.cs index 9e5b81f80..e58aa6bc2 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/UpdateFixture.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/Xbmc/Json/UpdateFixture.cs @@ -3,6 +3,7 @@ using FizzWare.NBuilder; using Moq; using NUnit.Framework; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Xbmc; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -13,7 +14,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Json public class UpdateFixture : CoreTest { private XbmcSettings _settings; - const string _expectedJson = "{\"jsonrpc\":\"2.0\",\"method\":\"VideoLibrary.GetTvShows\",\"params\":{\"properties\":[\"file\",\"imdbnumber\"]},\"id\":10}"; + const string _expectedJson = "{\"jsonrpc\":\"2.0\",\"method\":\"VideoLibrary.GetTvShows\",\"params\":{\"properties\":[\"file\",\"imdbnumber\"]},\"id\":"; private const string _tvshowsResponse = "{\"id\":10,\"jsonrpc\":\"2.0\",\"result\":{\"limits\":" + "{\"end\":5,\"start\":0,\"total\":5},\"tvshows\":[{\"file\"" + @@ -41,7 +42,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc.Json Mocker.GetMock() .Setup(s => s.PostCommand(_settings.Address, _settings.Username, _settings.Password, - It.Is(e => e.Replace(" ", "").Replace("\r\n", "").Replace("\t", "") == _expectedJson.Replace(" ", "")))) + It.Is(e => e.Replace(" ", "").Replace("\r\n", "").Replace("\t", "").Contains(_expectedJson.Replace(" ", ""))))) .Returns(_tvshowsResponse); } diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 22a8d5042..52e2d0867 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -103,8 +103,8 @@ - - + + @@ -233,6 +233,7 @@ + diff --git a/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs index 66b9b5733..67f3bed2d 100644 --- a/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs @@ -25,6 +25,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("2020.NZ.2012.13.02.PDTV.XviD-C4TV", "2020nz", 2012, 2, 13)] [TestCase("2020.NZ.2011.12.02.PDTV.XviD-C4TV", "2020nz", 2011, 12, 2)] [TestCase("Series Title - 2013-10-30 - Episode Title (1) [HDTV-720p]", "Series Title", 2013, 10, 30)] + [TestCase("The_Voice_US_04.28.2014_hdtv.x264.Poke.mp4", "The Voice US", 2014, 4, 28)] public void should_parse_daily_episode(string postTitle, string title, int year, int month, int day) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs index 40644a880..f45ddc6b0 100644 --- a/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs @@ -37,6 +37,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Kaamelott.S01E91-E100", "Kaamelott", 1, new[] { 91, 92, 93, 94, 95, 96, 97, 98, 99, 100 })] [TestCase("Neighbours.S29E161-E165.PDTV.x264-FQM", "Neighbours", 29, new[] { 161, 162, 163, 164, 165 })] [TestCase("Shortland.Street.S22E5363-E5366.HDTV.x264-FiHTV", "Shortland Street", 22, new[] { 5363, 5364, 5365, 5366 })] + [TestCase("the.office.101.102.hdtv-lol", "The Office", 1, new[] { 1, 2 })] + //[TestCase("Adventure Time - 5x01 - x02 - Finn the Human (2) & Jake the Dog (3)", "Adventure Time", 5, new [] { 1, 2 })] public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index a3a7c4a58..aa8d13a37 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -55,6 +55,9 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("The.Girls.Next.Door.S03E06.DVDRip.XviD-WiDE", false)] [TestCase("The.Girls.Next.Door.S03E06.DVD.Rip.XviD-WiDE", false)] [TestCase("the.shield.1x13.circles.ws.xvidvd-tns", false)] + [TestCase("the_x-files.9x18.sunshine_days.ac3.ws_dvdrip_xvid-fov.avi", false)] + [TestCase("Hannibal.S01E05.576p.BluRay.DD5.1.x264-HiSD", false)] + [TestCase("Hannibal.S01E05.480p.BluRay.DD5.1.x264-HiSD", false)] public void should_parse_dvd_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.DVD, proper); @@ -115,6 +118,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Two and a Half Men S10E03 1080p WEB DL DD5 1 H 264 REPACK NFHD", true)] [TestCase("Glee.S04E09.Swan.Song.1080p.WEB-DL.DD5.1.H.264-ECI", false)] [TestCase("The.Big.Bang.Theory.S06E11.The.Santa.Simulation.1080p.WEB-DL.DD5.1.H.264", false)] + [TestCase("Rosemary's.Baby.S01E02.Night.2.[WEBDL-1080p].mkv", false)] public void should_parse_webdl1080p_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.WEBDL1080p, proper); @@ -123,6 +127,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("WEEDS.S03E01-06.DUAL.Bluray.AC3.-HELLYWOOD.avi", false)] [TestCase("Chuck - S01E03 - Come Fly With Me - 720p BluRay.mkv", false)] [TestCase("The Big Bang Theory.S03E01.The Electric Can Opener Fluctuation.m2ts", false)] + [TestCase("Revolution.S01E02.Chained.Heat.[Bluray720p].mkv", false)] public void should_parse_bluray720p_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.Bluray720p, proper); @@ -130,6 +135,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Chuck - S01E03 - Come Fly With Me - 1080p BluRay.mkv", false)] [TestCase("Sons.Of.Anarchy.S02E13.1080p.BluRay.x264-AVCDVD", false)] + [TestCase("Revolution.S01E02.Chained.Heat.[Bluray1080p].mkv", false)] public void should_parse_bluray1080p_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.Bluray1080p, proper); diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index 7e84877d4..7c1504d4a 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -9,8 +9,8 @@ namespace NzbDrone.Core.Test.ParserTests public class ReleaseGroupParserFixture : CoreTest { [TestCase("Castle.2009.S01E14.English.HDTV.XviD-LOL", "LOL")] - [TestCase("Castle 2009 S01E14 English HDTV XviD LOL", "LOL")] - [TestCase("Acropolis Now S05 EXTRAS DVDRip XviD RUNNER", "RUNNER")] + [TestCase("Castle 2009 S01E14 English HDTV XviD LOL", "DRONE")] + [TestCase("Acropolis Now S05 EXTRAS DVDRip XviD RUNNER", "DRONE")] [TestCase("Punky.Brewster.S01.EXTRAS.DVDRip.XviD-RUNNER", "RUNNER")] [TestCase("2020.NZ.2011.12.02.PDTV.XviD-C4TV", "C4TV")] [TestCase("The.Office.S03E115.DVDRip.XviD-OSiTV", "OSiTV")] @@ -18,6 +18,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("The Office - S01E01 - Pilot [HTDV-720p]", "DRONE")] [TestCase("The Office - S01E01 - Pilot [HTDV-1080p]", "DRONE")] [TestCase("The.Walking.Dead.S04E13.720p.WEB-DL.AAC2.0.H.264-Cyphanix", "Cyphanix")] + [TestCase("Arrow.S02E01.720p.WEB-DL.DD5.1.H.264.mkv", "DRONE")] public void should_parse_release_group(string title, string expected) { Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); diff --git a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs index fa6626113..965677a17 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs @@ -83,6 +83,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Homeland - 2x12 - The Choice [HDTV-1080p].mkv", "Homeland", 2, 12)] [TestCase("Homeland - 2x4 - New Car Smell [HDTV-1080p].mkv", "Homeland", 2, 4)] [TestCase("Top Gear - 06x11 - 2005.08.07", "Top Gear", 6, 11)] + [TestCase("The_Voice_US_s06e19_04.28.2014_hdtv.x264.Poke.mp4", "The Voice US", 6, 19)] + [TestCase("the.100.110.hdtv-lol", "The 100", 1, 10)] public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core.Test/ProviderTests/DiskProviderTests/ArchiveProviderFixture.cs b/src/NzbDrone.Core.Test/ProviderTests/DiskProviderTests/ArchiveProviderFixture.cs index a9f3e2f0d..6f4cacea5 100644 --- a/src/NzbDrone.Core.Test/ProviderTests/DiskProviderTests/ArchiveProviderFixture.cs +++ b/src/NzbDrone.Core.Test/ProviderTests/DiskProviderTests/ArchiveProviderFixture.cs @@ -13,12 +13,10 @@ namespace NzbDrone.Core.Test.ProviderTests.DiskProviderTests [Test] public void Should_extract_to_correct_folder() { - var destination = Path.Combine(TempFolder, "destination"); + var destinationFolder = new DirectoryInfo(GetTempFilePath()); var testArchive = OsInfo.IsWindows ? "TestArchive.zip" : "TestArchive.tar.gz"; - Subject.Extract(GetTestFilePath(testArchive), destination); - - var destinationFolder = new DirectoryInfo(destination); + Subject.Extract(Path.Combine("Files", testArchive), destinationFolder.FullName); destinationFolder.Exists.Should().BeTrue(); destinationFolder.GetDirectories().Should().HaveCount(1); diff --git a/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs b/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs new file mode 100644 index 000000000..7e263b235 --- /dev/null +++ b/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Tv.Commands; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.TvTests +{ + [TestFixture] + public class RefreshSeriesServiceFixture : CoreTest + { + private Series _series; + + [SetUp] + public void Setup() + { + var season1 = Builder.CreateNew() + .With(s => s.SeasonNumber = 1) + .Build(); + + _series = Builder.CreateNew() + .With(s => s.Seasons = new List + { + season1 + }) + .Build(); + + Mocker.GetMock() + .Setup(s => s.GetSeries(_series.Id)) + .Returns(_series); + + + } + + private void GivenNewSeriesInfo(Series series) + { + Mocker.GetMock() + .Setup(s => s.GetSeriesInfo(It.IsAny())) + .Returns(new Tuple>(series, new List())); + } + + [Test] + public void should_monitor_new_seasons_automatically() + { + var series = _series.JsonClone(); + series.Seasons.Add(Builder.CreateNew() + .With(s => s.SeasonNumber = 2) + .Build()); + + GivenNewSeriesInfo(series); + + Subject.Execute(new RefreshSeriesCommand(_series.Id)); + + Mocker.GetMock() + .Verify(v => v.UpdateSeries(It.Is(s => s.Seasons.Count == 2 && s.Seasons.Single(season => season.SeasonNumber == 2).Monitored == true))); + } + + [Test] + public void should_not_monitor_new_special_season_automatically() + { + var series = _series.JsonClone(); + series.Seasons.Add(Builder.CreateNew() + .With(s => s.SeasonNumber = 0) + .Build()); + + GivenNewSeriesInfo(series); + + Subject.Execute(new RefreshSeriesCommand(_series.Id)); + + Mocker.GetMock() + .Verify(v => v.UpdateSeries(It.Is(s => s.Seasons.Count == 2 && s.Seasons.Single(season => season.SeasonNumber == 0).Monitored == false))); + } + } +} diff --git a/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs b/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs index 357463bdf..e01a08761 100644 --- a/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs +++ b/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs @@ -6,8 +6,10 @@ using NUnit.Framework; using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Http; using NzbDrone.Common.Model; using NzbDrone.Common.Processes; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Update; using NzbDrone.Core.Update.Commands; @@ -48,12 +50,28 @@ namespace NzbDrone.Core.Test.UpdateTests Mocker.GetMock().SetupGet(c => c.TempFolder).Returns(TempFolder); Mocker.GetMock().Setup(c => c.AvailableUpdate()).Returns(_updatePackage); + Mocker.GetMock().Setup(c => c.Verify(It.IsAny(), It.IsAny())).Returns(true); Mocker.GetMock().Setup(c => c.GetCurrentProcess()).Returns(new ProcessInfo { Id = 12 }); + Mocker.GetMock().Setup(c => c.ExecutingApplication).Returns(@"C:\Test\NzbDrone.exe"); _sandboxFolder = Mocker.GetMock().Object.GetUpdateSandboxFolder(); } + private void GivenInstallScript(string path) + { + Mocker.GetMock() + .SetupGet(s => s.UpdateMechanism) + .Returns(UpdateMechanism.Script); + + Mocker.GetMock() + .SetupGet(s => s.UpdateScriptPath) + .Returns(path); + + Mocker.GetMock() + .Setup(s => s.FileExists(path, true)) + .Returns(true); + } [Test] public void should_delete_sandbox_before_update_if_folder_exists() @@ -76,7 +94,6 @@ namespace NzbDrone.Core.Test.UpdateTests Mocker.GetMock().Verify(c => c.DeleteFolder(_sandboxFolder, true), Times.Never()); } - [Test] public void Should_download_update_package() { @@ -117,18 +134,87 @@ namespace NzbDrone.Core.Test.UpdateTests Subject.Execute(new ApplicationUpdateCommand()); Mocker.GetMock() - .Verify(c => c.Start(It.IsAny(), "12", null, null), Times.Once()); + .Verify(c => c.Start(It.IsAny(), It.Is(s => s.StartsWith("12")), null, null), Times.Once()); } [Test] - public void when_no_updates_are_available_should_return_without_error_or_warnings() + public void should_return_without_error_or_warnings_when_no_updates_are_available() { Mocker.GetMock().Setup(c => c.AvailableUpdate()).Returns(null); Subject.Execute(new ApplicationUpdateCommand()); - ExceptionVerification.AssertNoUnexcpectedLogs(); + ExceptionVerification.AssertNoUnexpectedLogs(); + } + + [Test] + public void should_not_extract_if_verification_fails() + { + Mocker.GetMock().Setup(c => c.Verify(It.IsAny(), It.IsAny())).Returns(false); + + Subject.Execute(new ApplicationUpdateCommand()); + + Mocker.GetMock().Verify(v => v.Extract(It.IsAny(), It.IsAny()), Times.Never()); + } + + [Test] + [Platform("Mono")] + public void should_run_script_if_configured() + { + const string scriptPath = "/tmp/nzbdrone/update.sh"; + + GivenInstallScript(scriptPath); + + Subject.Execute(new ApplicationUpdateCommand()); + + Mocker.GetMock().Verify(v => v.Start(scriptPath, It.IsAny(), null, null), Times.Once()); + } + + [Test] + [Platform("Mono")] + public void should_throw_if_script_is_not_set() + { + const string scriptPath = "/tmp/nzbdrone/update.sh"; + + GivenInstallScript(""); + + Subject.Execute(new ApplicationUpdateCommand()); + + ExceptionVerification.ExpectedErrors(1); + Mocker.GetMock().Verify(v => v.Start(scriptPath, It.IsAny(), null, null), Times.Never()); + } + + [Test] + [Platform("Mono")] + public void should_throw_if_script_is_null() + { + const string scriptPath = "/tmp/nzbdrone/update.sh"; + + GivenInstallScript(null); + + Subject.Execute(new ApplicationUpdateCommand()); + + ExceptionVerification.ExpectedErrors(1); + Mocker.GetMock().Verify(v => v.Start(scriptPath, It.IsAny(), null, null), Times.Never()); + } + + [Test] + [Platform("Mono")] + public void should_throw_if_script_path_does_not_exist() + { + const string scriptPath = "/tmp/nzbdrone/update.sh"; + + GivenInstallScript(scriptPath); + + Mocker.GetMock() + .Setup(s => s.FileExists(scriptPath, true)) + .Returns(false); + + Subject.Execute(new ApplicationUpdateCommand()); + + ExceptionVerification.ExpectedErrors(1); + Mocker.GetMock().Verify(v => v.Start(scriptPath, It.IsAny(), null, null), Times.Never()); } [Test] diff --git a/src/NzbDrone.Core/Blacklisting/Blacklist.cs b/src/NzbDrone.Core/Blacklisting/Blacklist.cs index 19dd800a6..91c927f05 100644 --- a/src/NzbDrone.Core/Blacklisting/Blacklist.cs +++ b/src/NzbDrone.Core/Blacklisting/Blacklist.cs @@ -2,12 +2,14 @@ using System.Collections.Generic; using NzbDrone.Core.Datastore; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.Blacklisting { public class Blacklist : ModelBase { public Int32 SeriesId { get; set; } + public Series Series { get; set; } public List EpisodeIds { get; set; } public String SourceTitle { get; set; } public QualityModel Quality { get; set; } diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs b/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs index 4e105865f..681fcdf98 100644 --- a/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs +++ b/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; +using Marr.Data.QGen; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.Blacklisting { @@ -27,5 +29,12 @@ namespace NzbDrone.Core.Blacklisting { return Query.Where(b => b.SeriesId == seriesId); } + + protected override SortBuilder GetPagedQuery(QueryBuilder query, PagingSpec pagingSpec) + { + var baseQuery = query.Join(JoinType.Inner, h => h.Series, (h, s) => h.SeriesId == s.Id); + + return base.GetPagedQuery(baseQuery, pagingSpec); + } } } diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index ef9c5e4dd..d0954c221 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -11,6 +11,7 @@ using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Update; namespace NzbDrone.Core.Configuration @@ -30,11 +31,13 @@ namespace NzbDrone.Core.Configuration string Password { get; } string LogLevel { get; } string Branch { get; } - bool AutoUpdate { get; } string ApiKey { get; } bool Torrent { get; } string SslCertHash { get; } string UrlBase { get; } + Boolean UpdateAutomatically { get; } + UpdateMechanism UpdateMechanism { get; } + String UpdateScriptPath { get; } } public class ConfigFileProvider : IConfigFileProvider @@ -141,11 +144,6 @@ namespace NzbDrone.Core.Configuration get { return GetValue("Branch", "master").ToLowerInvariant(); } } - public bool AutoUpdate - { - get { return GetValueBoolean("AutoUpdate", false, persist: false); } - } - public string Username { get { return GetValue("Username", ""); } @@ -181,6 +179,21 @@ namespace NzbDrone.Core.Configuration } } + public bool UpdateAutomatically + { + get { return GetValueBoolean("UpdateAutomatically", false, false); } + } + + public UpdateMechanism UpdateMechanism + { + get { return GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false); } + } + + public string UpdateScriptPath + { + get { return GetValue("UpdateScriptPath", "", false ); } + } + public int GetValueInt(string key, int defaultValue) { return Convert.ToInt32(GetValue(key, defaultValue)); @@ -191,9 +204,9 @@ namespace NzbDrone.Core.Configuration return Convert.ToBoolean(GetValue(key, defaultValue, persist)); } - public T GetValueEnum(string key, T defaultValue) + public T GetValueEnum(string key, T defaultValue, bool persist = true) { - return (T)Enum.Parse(typeof(T), GetValue(key, defaultValue), true); + return (T)Enum.Parse(typeof(T), GetValue(key, defaultValue), persist); } public string GetValue(string key, object defaultValue, bool persist = true) @@ -210,7 +223,9 @@ namespace NzbDrone.Core.Configuration var valueHolder = parentContainer.Descendants(key).ToList(); if (valueHolder.Count() == 1) + { return valueHolder.First().Value.Trim(); + } //Save the value if (persist) diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 3fc54f350..ce3252221 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -6,6 +6,7 @@ using NzbDrone.Common.EnsureThat; using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Update; namespace NzbDrone.Core.Configuration diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index a3cbef057..10a1843a5 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Update; namespace NzbDrone.Core.Configuration { @@ -23,7 +24,6 @@ namespace NzbDrone.Core.Configuration Int32 BlacklistRetryInterval { get; set; } Int32 BlacklistRetryLimit { get; set; } - //Media Management Boolean AutoUnmonitorPreviouslyDownloadedEpisodes { get; set; } String RecycleBin { get; set; } diff --git a/src/NzbDrone.Core/DataAugmentation/DailySeries/DailySeriesDataProxy.cs b/src/NzbDrone.Core/DataAugmentation/DailySeries/DailySeriesDataProxy.cs index b5977e053..9040f3944 100644 --- a/src/NzbDrone.Core/DataAugmentation/DailySeries/DailySeriesDataProxy.cs +++ b/src/NzbDrone.Core/DataAugmentation/DailySeries/DailySeriesDataProxy.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using NLog; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; namespace NzbDrone.Core.DataAugmentation.DailySeries diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingProxy.cs b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingProxy.cs index 9bda36efc..54bea3a48 100644 --- a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingProxy.cs +++ b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingProxy.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; namespace NzbDrone.Core.DataAugmentation.Scene diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index bb1173b3b..ce5287472 100644 --- a/src/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs @@ -6,7 +6,7 @@ using Marr.Data; using Marr.Data.QGen; using NzbDrone.Core.Datastore.Events; using NzbDrone.Common; -using NzbDrone.Core.Datastore.Extentions; +using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Messaging.Events; diff --git a/src/NzbDrone.Core/Datastore/Extentions/MappingExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/MappingExtensions.cs similarity index 97% rename from src/NzbDrone.Core/Datastore/Extentions/MappingExtensions.cs rename to src/NzbDrone.Core/Datastore/Extensions/MappingExtensions.cs index e5a000004..090c2560c 100644 --- a/src/NzbDrone.Core/Datastore/Extentions/MappingExtensions.cs +++ b/src/NzbDrone.Core/Datastore/Extensions/MappingExtensions.cs @@ -4,7 +4,7 @@ using Marr.Data; using Marr.Data.Mapping; using NzbDrone.Common.Reflection; -namespace NzbDrone.Core.Datastore.Extentions +namespace NzbDrone.Core.Datastore.Extensions { public static class MappingExtensions { diff --git a/src/NzbDrone.Core/Datastore/Extentions/PagingSpecExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/PagingSpecExtensions.cs similarity index 96% rename from src/NzbDrone.Core/Datastore/Extentions/PagingSpecExtensions.cs rename to src/NzbDrone.Core/Datastore/Extensions/PagingSpecExtensions.cs index d5593876e..39cc5b7a6 100644 --- a/src/NzbDrone.Core/Datastore/Extentions/PagingSpecExtensions.cs +++ b/src/NzbDrone.Core/Datastore/Extensions/PagingSpecExtensions.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Linq.Expressions; -namespace NzbDrone.Core.Datastore.Extentions +namespace NzbDrone.Core.Datastore.Extensions { public static class PagingSpecExtensions { diff --git a/src/NzbDrone.Core/Datastore/Extentions/RelationshipExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/RelationshipExtensions.cs similarity index 97% rename from src/NzbDrone.Core/Datastore/Extentions/RelationshipExtensions.cs rename to src/NzbDrone.Core/Datastore/Extensions/RelationshipExtensions.cs index 6bade070f..2e40accd4 100644 --- a/src/NzbDrone.Core/Datastore/Extentions/RelationshipExtensions.cs +++ b/src/NzbDrone.Core/Datastore/Extensions/RelationshipExtensions.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; using Marr.Data; using Marr.Data.Mapping; -namespace NzbDrone.Core.Datastore.Extentions +namespace NzbDrone.Core.Datastore.Extensions { public static class RelationshipExtensions { diff --git a/src/NzbDrone.Core/Datastore/Migration/050_add_hash_to_metadata_files.cs b/src/NzbDrone.Core/Datastore/Migration/050_add_hash_to_metadata_files.cs new file mode 100644 index 000000000..8986a7fba --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/050_add_hash_to_metadata_files.cs @@ -0,0 +1,14 @@ +using NzbDrone.Core.Datastore.Migration.Framework; +using FluentMigrator; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(50)] + public class add_hash_to_metadata_files : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("MetadataFiles").AddColumn("Hash").AsString().Nullable(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 02c728f22..74ab43f69 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -7,7 +7,7 @@ using NzbDrone.Core.Blacklisting; using NzbDrone.Core.Configuration; using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.Datastore.Converters; -using NzbDrone.Core.Datastore.Extentions; +using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Download; using NzbDrone.Core.Indexers; using NzbDrone.Core.Instrumentation; diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs index 5073ca546..1d776f3e1 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs @@ -4,6 +4,7 @@ using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using System.Collections.Generic; namespace NzbDrone.Core.DecisionEngine.Specifications { @@ -66,10 +67,27 @@ namespace NzbDrone.Core.DecisionEngine.Specifications //Multiply maxSize by Series.Runtime maxSize = maxSize * subject.Series.Runtime * subject.Episodes.Count; - //Check if there was only one episode parsed and it is the first - if (subject.Episodes.Count == 1 && _episodeService.IsFirstOrLastEpisodeOfSeason(subject.Episodes.First().Id)) + if (subject.Episodes.Count == 1) { - maxSize = maxSize * 2; + Episode episode = subject.Episodes.First(); + List seasonEpisodes; + + var seasonSearchCriteria = searchCriteria as SeasonSearchCriteria; + if (seasonSearchCriteria != null && !seasonSearchCriteria.Series.UseSceneNumbering && seasonSearchCriteria.Episodes.Any(v => v.Id == episode.Id)) + { + seasonEpisodes = (searchCriteria as SeasonSearchCriteria).Episodes; + } + else + { + seasonEpisodes = _episodeService.GetEpisodesBySeason(episode.SeriesId, episode.SeasonNumber); + } + + //Ensure that this is either the first episode + //or is the last episode in a season that has 10 or more episodes + if (seasonEpisodes.First().Id == episode.Id || (seasonEpisodes.Count() >= 10 && seasonEpisodes.Last().Id == episode.Id)) + { + maxSize = maxSize * 2; + } } //If the parsed size is greater than maxSize we don't want it diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs index 949845b8f..057556420 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs @@ -4,6 +4,7 @@ using System.IO; using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index d0cd625f1..531f56898 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index 930a75ec0..639b4e545 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -4,6 +4,7 @@ using System.IO; using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; using NzbDrone.Common.Instrumentation; using NzbDrone.Core.Configuration; using NzbDrone.Core.Messaging.Commands; diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 0242a9c5d..931b919cf 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -4,6 +4,7 @@ using System.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Cache; +using NzbDrone.Common.Http; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs index 1623b6eab..4c699ab64 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs @@ -135,7 +135,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd action, authentication); - _logger.CleansedDebug(url); + _logger.Debug(url); return new RestClient(url); } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs new file mode 100644 index 000000000..23c9c44a2 --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + public class RootFolderCheck : HealthCheckBase + { + private readonly ISeriesService _seriesService; + private readonly IDiskProvider _diskProvider; + + public RootFolderCheck(ISeriesService seriesService, IDiskProvider diskProvider) + { + _seriesService = seriesService; + _diskProvider = diskProvider; + } + + public override HealthCheck Check() + { + var missingRootFolders = _seriesService.GetAllSeries() + .Select(s => _diskProvider.GetParentFolder(s.Path)) + .Distinct() + .Where(s => !_diskProvider.FolderExists(s)) + .ToList(); + + if (missingRootFolders.Any()) + { + if (missingRootFolders.Count == 1) + { + return new HealthCheck(GetType(), HealthCheckResult.Error, "Missing root folder: " + missingRootFolders.First()); + } + + var message = String.Format("Multiple root folders are missing: {0}", String.Join(" | ", missingRootFolders)); + return new HealthCheck(GetType(), HealthCheckResult.Error, message); + } + + return new HealthCheck(GetType()); + } + + public override bool CheckOnConfigChange + { + get + { + return false; + } + } + } +} diff --git a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs index 21b76bcc0..0eb187b29 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs @@ -22,6 +22,7 @@ namespace NzbDrone.Core.HealthCheck.Checks public override HealthCheck Check() { + //TODO: Check on mono as well if (OsInfo.IsWindows) { try diff --git a/src/NzbDrone.Core/Housekeeping/HousekeepingService.cs b/src/NzbDrone.Core/Housekeeping/HousekeepingService.cs index b48e953e0..9199fea9d 100644 --- a/src/NzbDrone.Core/Housekeeping/HousekeepingService.cs +++ b/src/NzbDrone.Core/Housekeeping/HousekeepingService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using NLog; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; @@ -14,7 +15,7 @@ namespace NzbDrone.Core.Housekeeping private readonly Logger _logger; private readonly IDatabase _mainDb; - public HousekeepingService(IEnumerable housekeepers, Logger logger, IDatabase mainDb) + public HousekeepingService(IEnumerable housekeepers, IDatabase mainDb, Logger logger) { _housekeepers = housekeepers; _logger = logger; @@ -37,9 +38,13 @@ namespace NzbDrone.Core.Housekeeping } } - // Vacuuming the log db isn't needed since that's done hourly at the TrimLogCommand. - _logger.Debug("Compressing main database after housekeeping"); - _mainDb.Vacuum(); + //Only Vaccuum the DB in production + if (RuntimeInfo.IsProduction) + { + // Vacuuming the log db isn't needed since that's done hourly at the TrimLogCommand. + _logger.Debug("Compressing main database after housekeeping"); + _mainDb.Vacuum(); + } } public void Execute(HousekeepingCommand message) diff --git a/src/NzbDrone.Core/Indexers/IndexerFetchService.cs b/src/NzbDrone.Core/Indexers/IndexerFetchService.cs index 4bbca036f..678a5be1a 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFetchService.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFetchService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Net; using NLog; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Instrumentation.Extensions; @@ -116,7 +117,7 @@ namespace NzbDrone.Core.Indexers { try { - _logger.CleansedDebug("Downloading Feed " + url); + _logger.Debug("Downloading Feed " + url); var xml = _httpProvider.DownloadString(url); if (!string.IsNullOrWhiteSpace(xml)) { diff --git a/src/NzbDrone.Core/Indexers/NewznabTestService.cs b/src/NzbDrone.Core/Indexers/NewznabTestService.cs index 5e25a5bac..bc923786e 100644 --- a/src/NzbDrone.Core/Indexers/NewznabTestService.cs +++ b/src/NzbDrone.Core/Indexers/NewznabTestService.cs @@ -5,6 +5,7 @@ using FluentValidation; using FluentValidation.Results; using NLog; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Indexers.Newznab; diff --git a/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs b/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs index 9f43eadbb..f2e14f7ea 100644 --- a/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs +++ b/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs @@ -5,6 +5,7 @@ using NLog.Config; using NLog; using NLog.Layouts; using NLog.Targets; +using NzbDrone.Common.Instrumentation; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; @@ -52,7 +53,7 @@ namespace NzbDrone.Core.Instrumentation { var log = new Log(); log.Time = logEvent.TimeStamp; - log.Message = logEvent.FormattedMessage; + log.Message = CleanseLogMessage.Cleanse(logEvent.FormattedMessage); log.Method = Layout.Render(logEvent); log.Logger = logEvent.LoggerName; diff --git a/src/NzbDrone.Core/Instrumentation/Extensions/LoggerCleansedExtensions.cs b/src/NzbDrone.Core/Instrumentation/Extensions/LoggerCleansedExtensions.cs deleted file mode 100644 index 3fb154c8c..000000000 --- a/src/NzbDrone.Core/Instrumentation/Extensions/LoggerCleansedExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using NLog; - -namespace NzbDrone.Core.Instrumentation.Extensions -{ - public static class LoggerCleansedExtensions - { - private static readonly Regex CleansingRegex = new Regex(@"(?<=apikey=)(\w+?)(?=\W|$|_)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static void CleansedInfo(this Logger logger, string message, params object[] args) - { - var formattedMessage = String.Format(message, args); - LogCleansedMessage(logger, LogLevel.Info, formattedMessage); - } - - public static void CleansedDebug(this Logger logger, string message, params object[] args) - { - var formattedMessage = String.Format(message, args); - LogCleansedMessage(logger, LogLevel.Debug, formattedMessage); - } - - public static void CleansedTrace(this Logger logger, string message, params object[] args) - { - var formattedMessage = String.Format(message, args); - LogCleansedMessage(logger, LogLevel.Trace, formattedMessage); - } - - private static void LogCleansedMessage(Logger logger, LogLevel level, string message) - { - message = Cleanse(message); - - var logEvent = new LogEventInfo(level, logger.Name, message); - - logger.Log(logEvent); - } - - private static string Cleanse(string message) - { - //TODO: password= - - return CleansingRegex.Replace(message, ""); - } - } -} diff --git a/src/NzbDrone.Core/Instrumentation/SetLoggingLevel.cs b/src/NzbDrone.Core/Instrumentation/SetLoggingLevel.cs index a436c0fc1..e4ca64919 100644 --- a/src/NzbDrone.Core/Instrumentation/SetLoggingLevel.cs +++ b/src/NzbDrone.Core/Instrumentation/SetLoggingLevel.cs @@ -2,7 +2,7 @@ using System.Linq; using NLog; using NLog.Config; -using NLog.Targets; +using NzbDrone.Common.Instrumentation; using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Lifecycle; @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Instrumentation var minimumLogLevel = LogLevel.FromString(_configFileProvider.LogLevel); var rules = LogManager.Configuration.LoggingRules; - var rollingFileLogger = rules.Single(s => s.Targets.Any(t => t is FileTarget)); + var rollingFileLogger = rules.Single(s => s.Targets.Any(t => t is NzbDroneFileTarget)); rollingFileLogger.EnableLoggingForLevel(LogLevel.Trace); SetMinimumLogLevel(rollingFileLogger, minimumLogLevel); diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index 4c7daba7e..459b2ddb5 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -100,6 +100,9 @@ namespace NzbDrone.Core.Jobs public void Handle(CommandExecutedEvent message) { + if (message.Command.GetType().Name == "BroadcastSignalRMessage") + return; + var scheduledTask = _scheduledTaskRepository.All().SingleOrDefault(c => c.TypeName == message.Command.GetType().FullName); if (scheduledTask != null) diff --git a/src/NzbDrone.Core/Lifecycle/LifecycleService.cs b/src/NzbDrone.Core/Lifecycle/LifecycleService.cs index df5c017b1..9fb5c4b32 100644 --- a/src/NzbDrone.Core/Lifecycle/LifecycleService.cs +++ b/src/NzbDrone.Core/Lifecycle/LifecycleService.cs @@ -1,4 +1,5 @@ -using NLog; +using System; +using NLog; using NzbDrone.Common; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Processes; @@ -9,40 +10,43 @@ using IServiceProvider = NzbDrone.Common.IServiceProvider; namespace NzbDrone.Core.Lifecycle { - public class LifecycleService: IExecute, IExecute + public interface ILifecycleService + { + void Shutdown(); + void Restart(); + } + + public class LifecycleService : ILifecycleService, IExecute, IExecute { private readonly IEventAggregator _eventAggregator; private readonly IRuntimeInfo _runtimeInfo; private readonly IServiceProvider _serviceProvider; - private readonly IProcessProvider _processProvider; private readonly Logger _logger; public LifecycleService(IEventAggregator eventAggregator, IRuntimeInfo runtimeInfo, IServiceProvider serviceProvider, - IProcessProvider processProvider, Logger logger) { _eventAggregator = eventAggregator; _runtimeInfo = runtimeInfo; _serviceProvider = serviceProvider; - _processProvider = processProvider; _logger = logger; } - public void Execute(ShutdownCommand message) + public void Shutdown() { _logger.Info("Shutdown requested."); _eventAggregator.PublishEvent(new ApplicationShutdownRequested()); - + if (_runtimeInfo.IsWindowsService) { _serviceProvider.Stop(ServiceProvider.NZBDRONE_SERVICE_NAME); } } - public void Execute(RestartCommand message) + public void Restart() { _logger.Info("Restart requested."); @@ -53,5 +57,15 @@ namespace NzbDrone.Core.Lifecycle _serviceProvider.Restart(ServiceProvider.NZBDRONE_SERVICE_NAME); } } + + public void Execute(ShutdownCommand message) + { + Shutdown(); + } + + public void Execute(RestartCommand message) + { + Restart(); + } } } diff --git a/src/NzbDrone.Core/MediaCover/CoverAlreadyExistsSpecification.cs b/src/NzbDrone.Core/MediaCover/CoverAlreadyExistsSpecification.cs index c8ba77f23..ef0c1104b 100644 --- a/src/NzbDrone.Core/MediaCover/CoverAlreadyExistsSpecification.cs +++ b/src/NzbDrone.Core/MediaCover/CoverAlreadyExistsSpecification.cs @@ -1,6 +1,7 @@ using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; namespace NzbDrone.Core.MediaCover diff --git a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs index 554a3dc51..fc3c20785 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs @@ -6,6 +6,7 @@ using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv; diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 4e2801c95..a4ee5ff99 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -58,13 +58,21 @@ namespace NzbDrone.Core.MediaFiles public void Scan(Series series) { + var rootFolder = _diskProvider.GetParentFolder(series.Path); + + if (!_diskProvider.FolderExists(rootFolder)) + { + _logger.Warn("Series' root folder ({0}) doesn't exist.", rootFolder); + return; + } + _logger.ProgressInfo("Scanning disk for {0}", series.Title); _commandExecutor.PublishCommand(new CleanMediaFileDb(series.Id)); if (!_diskProvider.FolderExists(series.Path)) { if (_configService.CreateEmptySeriesFolders && - _diskProvider.FolderExists(_diskProvider.GetParentFolder(series.Path))) + _diskProvider.FolderExists(rootFolder)) { _logger.Debug("Creating missing series folder: {0}", series.Path); _diskProvider.CreateFolder(series.Path); diff --git a/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs index b6b396d69..7031b7ff9 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs @@ -10,6 +10,7 @@ using System.Xml.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; @@ -21,117 +22,23 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox { public class RoksboxMetadata : MetadataBase { - private readonly IEventAggregator _eventAggregator; private readonly IMapCoversToLocal _mediaCoverService; - private readonly IMediaFileService _mediaFileService; - private readonly IMetadataFileService _metadataFileService; private readonly IDiskProvider _diskProvider; - private readonly IHttpProvider _httpProvider; - private readonly IEpisodeService _episodeService; private readonly Logger _logger; - public RoksboxMetadata(IEventAggregator eventAggregator, - IMapCoversToLocal mediaCoverService, - IMediaFileService mediaFileService, - IMetadataFileService metadataFileService, + public RoksboxMetadata(IMapCoversToLocal mediaCoverService, IDiskProvider diskProvider, - IHttpProvider httpProvider, - IEpisodeService episodeService, Logger logger) - : base(diskProvider, httpProvider, logger) { - _eventAggregator = eventAggregator; _mediaCoverService = mediaCoverService; - _mediaFileService = mediaFileService; - _metadataFileService = metadataFileService; _diskProvider = diskProvider; - _httpProvider = httpProvider; - _episodeService = episodeService; _logger = logger; } private static List ValidCertification = new List { "G", "NC-17", "PG", "PG-13", "R", "UR", "UNRATED", "NR", "TV-Y", "TV-Y7", "TV-Y7-FV", "TV-G", "TV-PG", "TV-14", "TV-MA" }; private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?\d+))|(?specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public override void OnSeriesUpdated(Series series, List existingMetadataFiles, List episodeFiles) - { - var metadataFiles = new List(); - - if (!_diskProvider.FolderExists(series.Path)) - { - _logger.Info("Series folder ({0}) does not exist, skipping metadata creation", series.Path); - return; - } - - if (Settings.SeriesImages) - { - var metadata = WriteSeriesImages(series, existingMetadataFiles); - if (metadata != null) - { - metadataFiles.Add(metadata); - } - } - - if (Settings.SeasonImages) - { - var metadata = WriteSeasonImages(series, existingMetadataFiles); - if (metadata != null) - { - metadataFiles.AddRange(metadata); - } - } - - foreach (var episodeFile in episodeFiles) - { - if (Settings.EpisodeMetadata) - { - var metadata = WriteEpisodeMetadata(series, episodeFile, existingMetadataFiles); - if (metadata != null) - { - metadataFiles.Add(metadata); - } - } - } - - foreach (var episodeFile in episodeFiles) - { - if (Settings.EpisodeImages) - { - var metadataFile = WriteEpisodeImages(series, episodeFile, existingMetadataFiles); - - if (metadataFile != null) - { - metadataFiles.Add(metadataFile); - } - } - } - metadataFiles.RemoveAll(c => c == null); - _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles)); - } - - public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload) - { - var metadataFiles = new List(); - - if (Settings.EpisodeMetadata) - { - metadataFiles.Add(WriteEpisodeMetadata(series, episodeFile, new List())); - } - - if (Settings.EpisodeImages) - { - var metadataFile = WriteEpisodeImages(series, episodeFile, new List()); - - if (metadataFile != null) - { - metadataFiles.Add(metadataFile); - } - } - - _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles)); - } - - public override void AfterRename(Series series, List existingMetadataFiles, List episodeFiles) + public override List AfterRename(Series series, List existingMetadataFiles, List episodeFiles) { var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList(); var updatedMetadataFiles = new List(); @@ -172,7 +79,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox } } - _eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles)); + return updatedMetadataFiles; } public override MetadataFile FindMetadataFile(Series series, string path) @@ -190,7 +97,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox }; //Series and season images are both named folder.jpg, only season ones sit in season folders - if (String.Compare(filename, parentdir.Name, true) == 0) + if (String.Compare(filename, parentdir.Name, StringComparison.InvariantCultureIgnoreCase) == 0) { var seasonMatch = SeasonImagesRegex.Match(parentdir.Name); if (seasonMatch.Success) @@ -222,24 +129,76 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox if (parseResult != null && !parseResult.FullSeason) { - switch (Path.GetExtension(filename).ToLowerInvariant()) + var extension = Path.GetExtension(filename).ToLowerInvariant(); + + if (extension == ".xml") { - case ".xml": - metadata.Type = MetadataType.EpisodeMetadata; - return metadata; - case ".jpg": + metadata.Type = MetadataType.EpisodeMetadata; + return metadata; + } + + if (extension == ".jpg") + { + if (!Path.GetFileNameWithoutExtension(filename).EndsWith("-thumb")) + { metadata.Type = MetadataType.EpisodeImage; return metadata; - } - + } + } } return null; } - private MetadataFile WriteSeriesImages(Series series, List existingMetadataFiles) + public override MetadataFileResult SeriesMetadata(Series series) + { + //Series metadata is not supported + return null; + } + + public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile) + { + if (!Settings.EpisodeMetadata) + { + return null; + } + + _logger.Debug("Generating Episode Metadata for: {0}", episodeFile.Path); + + var xmlResult = String.Empty; + foreach (var episode in episodeFile.Episodes.Value) + { + var sb = new StringBuilder(); + var xws = new XmlWriterSettings(); + xws.OmitXmlDeclaration = true; + xws.Indent = false; + + using (var xw = XmlWriter.Create(sb, xws)) + { + var doc = new XDocument(); + + var details = new XElement("video"); + details.Add(new XElement("title", String.Format("{0} - {1}x{2} - {3}", series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title))); + details.Add(new XElement("year", episode.AirDate)); + details.Add(new XElement("genre", String.Join(" / ", series.Genres))); + var actors = String.Join(" , ", series.Actors.ConvertAll(c => c.Name + " - " + c.Character).GetRange(0, Math.Min(3, series.Actors.Count))); + details.Add(new XElement("actors", actors)); + details.Add(new XElement("description", episode.Overview)); + details.Add(new XElement("length", series.Runtime)); + details.Add(new XElement("mpaa", ValidCertification.Contains(series.Certification.ToUpperInvariant()) ? series.Certification.ToUpperInvariant() : "UNRATED")); + doc.Add(details); + doc.Save(xw); + + xmlResult += doc.ToString(); + xmlResult += Environment.NewLine; + } + } + + return new MetadataFileResult(GetEpisodeMetadataFilename(episodeFile.Path), xmlResult.Trim(Environment.NewLine.ToCharArray())); + } + + public override List SeriesImages(Series series) { - //Because we only support one image, attempt to get the Poster type, then if that fails grab the first var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault(); if (image == null) { @@ -250,39 +209,66 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); var destination = Path.Combine(series.Path, Path.GetFileName(series.Path) + Path.GetExtension(source)); - //TODO: Do we want to overwrite the file if it exists? - if (_diskProvider.FileExists(destination)) - { - _logger.Debug("Series image: {0} already exists.", image.CoverType); - return null; - } - else - { - - _diskProvider.CopyFile(source, destination, false); - - var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage) ?? - new MetadataFile - { - SeriesId = series.Id, - Consumer = GetType().Name, - Type = MetadataType.SeriesImage, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, destination) - }; - - return metadata; - } + return new List{ new ImageFileResult(destination, source) }; } - private IEnumerable WriteSeasonImages(Series series, List existingMetadataFiles) + public override List SeasonImages(Series series, Season season) { - _logger.Debug("Writing season images for {0}.", series.Title); - //Create a dictionary between season number and output folder - var seasonFolderMap = new Dictionary(); - foreach (var folder in Directory.EnumerateDirectories(series.Path)) + var seasonFolders = GetSeasonFolders(series); + + string seasonFolder; + if (!seasonFolders.TryGetValue(season.SeasonNumber, out seasonFolder)) + { + _logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber); + return new List(); + } + + //Roksbox only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection + var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault(); + if (image == null) + { + _logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber); + return new List(); + } + + var filename = Path.GetFileName(seasonFolder) + ".jpg"; + var path = Path.Combine(series.Path, seasonFolder, filename); + + return new List { new ImageFileResult(path, image.Url) }; + } + + public override List EpisodeImages(Series series, EpisodeFile episodeFile) + { + var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); + + if (screenshot == null) + { + _logger.Trace("Episode screenshot not available"); + return new List(); + } + + return new List {new ImageFileResult(GetEpisodeImageFilename(episodeFile.Path), screenshot.Url)}; + } + + private string GetEpisodeMetadataFilename(string episodeFilePath) + { + return Path.ChangeExtension(episodeFilePath, "xml"); + } + + private string GetEpisodeImageFilename(string episodeFilePath) + { + return Path.ChangeExtension(episodeFilePath, "jpg"); + } + + private Dictionary GetSeasonFolders(Series series) + { + var seasonFolderMap = new Dictionary(); + + foreach (var folder in _diskProvider.GetDirectories(series.Path)) { var directoryinfo = new DirectoryInfo(folder); var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name); + if (seasonMatch.Success) { var seasonNumber = seasonMatch.Groups["season"].Value; @@ -309,160 +295,8 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox _logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title); } } - foreach (var season in series.Seasons) - { - //Work out the path to this season - if we don't have a matching path then skip this season. - string seasonFolder; - if (!seasonFolderMap.TryGetValue(season.SeasonNumber, out seasonFolder)) - { - _logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber); - continue; - } - //Roksbox only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection - var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault(); - if (image == null) - { - _logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber); - continue; - } - - - var filename = Path.GetFileName(seasonFolder) + ".jpg"; - - var path = Path.Combine(series.Path, seasonFolder, filename); - _logger.Debug("Writing season image for series {0}, season {1} to {2}.", series.Title, season.SeasonNumber, path); - DownloadImage(series, image.Url, path); - - var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage && - c.SeasonNumber == season.SeasonNumber) ?? - new MetadataFile - { - SeriesId = series.Id, - SeasonNumber = season.SeasonNumber, - Consumer = GetType().Name, - Type = MetadataType.SeasonImage, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, path) - }; - - yield return metadata; - } - } - - private MetadataFile WriteEpisodeMetadata(Series series, EpisodeFile episodeFile, List existingMetadataFiles) - { - var filename = GetEpisodeMetadataFilename(episodeFile.Path); - var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename); - - var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata && - c.EpisodeFileId == episodeFile.Id); - - if (existingMetadata != null) - { - var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath); - if (!filename.PathEquals(fullPath)) - { - _diskProvider.MoveFile(fullPath, filename); - existingMetadata.RelativePath = relativePath; - } - } - - _logger.Debug("Generating {0} for: {1}", filename, episodeFile.Path); - - var xmlResult = String.Empty; - foreach (var episode in episodeFile.Episodes.Value) - { - var sb = new StringBuilder(); - var xws = new XmlWriterSettings(); - xws.OmitXmlDeclaration = true; - xws.Indent = false; - - using (var xw = XmlWriter.Create(sb, xws)) - { - var doc = new XDocument(); - - var details = new XElement("video"); - details.Add(new XElement("title", String.Format("{0} - {1}x{2} - {3}", series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title))); - details.Add(new XElement("year", episode.AirDate)); - details.Add(new XElement("genre", String.Join(" / ", series.Genres))); - var actors = String.Join(" , ", series.Actors.ConvertAll(c => c.Name + " - " + c.Character).GetRange(0, Math.Min(3, series.Actors.Count))); - details.Add(new XElement("actors", actors)); - details.Add(new XElement("description", episode.Overview)); - details.Add(new XElement("length", series.Runtime)); - details.Add(new XElement("mpaa", ValidCertification.Contains( series.Certification.ToUpperInvariant() ) ? series.Certification.ToUpperInvariant() : "UNRATED" ) ); - doc.Add(details); - doc.Save(xw); - - xmlResult += doc.ToString(); - xmlResult += Environment.NewLine; - } - } - - _logger.Debug("Saving episodedetails to: {0}", filename); - _diskProvider.WriteAllText(filename, xmlResult.Trim(Environment.NewLine.ToCharArray())); - - var metadata = existingMetadata ?? - new MetadataFile - { - SeriesId = series.Id, - EpisodeFileId = episodeFile.Id, - Consumer = GetType().Name, - Type = MetadataType.EpisodeMetadata, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename) - }; - - return metadata; - } - - private MetadataFile WriteEpisodeImages(Series series, EpisodeFile episodeFile, List existingMetadataFiles) - { - var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); - - if (screenshot == null) - { - _logger.Trace("Episode screenshot not available"); - return null; - } - - var filename = GetEpisodeImageFilename(episodeFile.Path); - var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename); - - var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeImage && - c.EpisodeFileId == episodeFile.Id); - - if (existingMetadata != null) - { - var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath); - if (!filename.PathEquals(fullPath)) - { - _diskProvider.MoveFile(fullPath, filename); - existingMetadata.RelativePath = relativePath; - } - } - - DownloadImage(series, screenshot.Url, filename); - - var metadata = existingMetadata ?? - new MetadataFile - { - SeriesId = series.Id, - EpisodeFileId = episodeFile.Id, - Consumer = GetType().Name, - Type = MetadataType.EpisodeImage, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename) - }; - - return metadata; - } - - private string GetEpisodeMetadataFilename(string episodeFilePath) - { - return Path.ChangeExtension(episodeFilePath, "xml"); - } - - private string GetEpisodeImageFilename(string episodeFilePath) - { - return Path.ChangeExtension(episodeFilePath, "jpg"); + return seasonFolderMap; } } } diff --git a/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs index e76a341bc..a8399c0b8 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Runtime.Remoting.Messaging; @@ -10,6 +11,7 @@ using System.Xml.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; @@ -21,116 +23,22 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv { public class WdtvMetadata : MetadataBase { - private readonly IEventAggregator _eventAggregator; private readonly IMapCoversToLocal _mediaCoverService; - private readonly IMediaFileService _mediaFileService; - private readonly IMetadataFileService _metadataFileService; private readonly IDiskProvider _diskProvider; - private readonly IHttpProvider _httpProvider; - private readonly IEpisodeService _episodeService; private readonly Logger _logger; - public WdtvMetadata(IEventAggregator eventAggregator, - IMapCoversToLocal mediaCoverService, - IMediaFileService mediaFileService, - IMetadataFileService metadataFileService, + public WdtvMetadata(IMapCoversToLocal mediaCoverService, IDiskProvider diskProvider, - IHttpProvider httpProvider, - IEpisodeService episodeService, Logger logger) - : base(diskProvider, httpProvider, logger) { - _eventAggregator = eventAggregator; _mediaCoverService = mediaCoverService; - _mediaFileService = mediaFileService; - _metadataFileService = metadataFileService; _diskProvider = diskProvider; - _httpProvider = httpProvider; - _episodeService = episodeService; _logger = logger; } private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?\d+))|(?specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public override void OnSeriesUpdated(Series series, List existingMetadataFiles, List episodeFiles) - { - var metadataFiles = new List(); - - if (!_diskProvider.FolderExists(series.Path)) - { - _logger.Info("Series folder ({0}) does not exist, skipping metadata creation", series.Path); - return; - } - - if (Settings.SeriesImages) - { - var metadata = WriteSeriesImages(series, existingMetadataFiles); - if (metadata != null) - { - metadataFiles.Add(metadata); - } - } - - if (Settings.SeasonImages) - { - var metadata = WriteSeasonImages(series, existingMetadataFiles); - if (metadata != null) - { - metadataFiles.AddRange(metadata); - } - } - - foreach (var episodeFile in episodeFiles) - { - if (Settings.EpisodeMetadata) - { - var metadata = WriteEpisodeMetadata(series, episodeFile, existingMetadataFiles); - if (metadata != null) - { - metadataFiles.Add(metadata); - } - } - } - - foreach (var episodeFile in episodeFiles) - { - if (Settings.EpisodeImages) - { - var metadataFile = WriteEpisodeImages(series, episodeFile, existingMetadataFiles); - - if (metadataFile != null) - { - metadataFiles.Add(metadataFile); - } - } - } - metadataFiles.RemoveAll(c => c == null); - _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles)); - } - - public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload) - { - var metadataFiles = new List(); - - if (Settings.EpisodeMetadata) - { - metadataFiles.Add(WriteEpisodeMetadata(series, episodeFile, new List())); - } - - if (Settings.EpisodeImages) - { - var metadataFile = WriteEpisodeImages(series, episodeFile, new List()); - - if (metadataFile != null) - { - metadataFiles.Add(metadataFile); - } - } - - _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles)); - } - - public override void AfterRename(Series series, List existingMetadataFiles, List episodeFiles) + public override List AfterRename(Series series, List existingMetadataFiles, List episodeFiles) { var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList(); var updatedMetadataFiles = new List(); @@ -170,8 +78,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv } } } - - _eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles)); + return updatedMetadataFiles; } public override MetadataFile FindMetadataFile(Series series, string path) @@ -236,137 +143,20 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv return null; } - private MetadataFile WriteSeriesImages(Series series, List existingMetadataFiles) + public override MetadataFileResult SeriesMetadata(Series series) { - //Because we only support one image, attempt to get the Poster type, then if that fails grab the first - var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault(); - if (image == null) + //Series metadata is not supported + return null; + } + + public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile) + { + if (!Settings.EpisodeMetadata) { - _logger.Trace("Failed to find suitable Series image for series {0}.", series.Title); return null; } - var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); - var destination = Path.Combine(series.Path, "folder" + Path.GetExtension(source)); - - //TODO: Do we want to overwrite the file if it exists? - if (_diskProvider.FileExists(destination)) - { - _logger.Debug("Series image: {0} already exists.", image.CoverType); - return null; - } - else - { - - _diskProvider.CopyFile(source, destination, false); - - var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage) ?? - new MetadataFile - { - SeriesId = series.Id, - Consumer = GetType().Name, - Type = MetadataType.SeriesImage, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, destination) - }; - - return metadata; - } - } - - private IEnumerable WriteSeasonImages(Series series, List existingMetadataFiles) - { - _logger.Debug("Writing season images for {0}.", series.Title); - //Create a dictionary between season number and output folder - var seasonFolderMap = new Dictionary(); - foreach (var folder in Directory.EnumerateDirectories(series.Path)) - { - var directoryinfo = new DirectoryInfo(folder); - var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name); - if (seasonMatch.Success) - { - var seasonNumber = seasonMatch.Groups["season"].Value; - - if (seasonNumber.Contains("specials")) - { - seasonFolderMap[0] = folder; - } - else - { - int matchedSeason; - if (Int32.TryParse(seasonNumber, out matchedSeason)) - { - seasonFolderMap[matchedSeason] = folder; - } - else - { - _logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title); - } - } - } - else - { - _logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title); - } - } - foreach (var season in series.Seasons) - { - //Work out the path to this season - if we don't have a matching path then skip this season. - string seasonFolder; - if (!seasonFolderMap.TryGetValue(season.SeasonNumber, out seasonFolder)) - { - _logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber); - continue; - } - - //WDTV only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection - var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault(); - if (image == null) - { - _logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber); - continue; - } - - - var filename = "folder.jpg"; - - var path = Path.Combine(series.Path, seasonFolder, filename); - _logger.Debug("Writing season image for series {0}, season {1} to {2}.", series.Title, season.SeasonNumber, path); - DownloadImage(series, image.Url, path); - - var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage && - c.SeasonNumber == season.SeasonNumber) ?? - new MetadataFile - { - SeriesId = series.Id, - SeasonNumber = season.SeasonNumber, - Consumer = GetType().Name, - Type = MetadataType.SeasonImage, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, path) - }; - - yield return metadata; - } - } - - private MetadataFile WriteEpisodeMetadata(Series series, EpisodeFile episodeFile, List existingMetadataFiles) - { - var filename = GetEpisodeMetadataFilename(episodeFile.Path); - var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename); - - var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata && - c.EpisodeFileId == episodeFile.Id); - - if (existingMetadata != null) - { - var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath); - if (!filename.PathEquals(fullPath)) - { - _diskProvider.MoveFile(fullPath, filename); - existingMetadata.RelativePath = relativePath; - } - } - - _logger.Debug("Generating {0} for: {1}", filename, episodeFile.Path); + _logger.Debug("Generating Episode Metadata for: {0}", episodeFile.Path); var xmlResult = String.Empty; foreach (var episode in episodeFile.Episodes.Value) @@ -404,62 +194,82 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv xmlResult += Environment.NewLine; } } - - _logger.Debug("Saving episodedetails to: {0}", filename); - _diskProvider.WriteAllText(filename, xmlResult.Trim(Environment.NewLine.ToCharArray())); - var metadata = existingMetadata ?? - new MetadataFile - { - SeriesId = series.Id, - EpisodeFileId = episodeFile.Id, - Consumer = GetType().Name, - Type = MetadataType.EpisodeMetadata, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename) - }; + var filename = GetEpisodeMetadataFilename(episodeFile.Path); - return metadata; + return new MetadataFileResult(filename, xmlResult.Trim(Environment.NewLine.ToCharArray())); } - private MetadataFile WriteEpisodeImages(Series series, EpisodeFile episodeFile, List existingMetadataFiles) + public override List SeriesImages(Series series) { + if (!Settings.SeriesImages) + { + return new List(); + } + + //Because we only support one image, attempt to get the Poster type, then if that fails grab the first + var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault(); + if (image == null) + { + _logger.Trace("Failed to find suitable Series image for series {0}.", series.Title); + return new List(); + } + + var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); + var destination = Path.Combine(series.Path, "folder" + Path.GetExtension(source)); + + return new List + { + new ImageFileResult(destination, source) + }; + } + + public override List SeasonImages(Series series, Season season) + { + if (!Settings.SeasonImages) + { + return new List(); + } + + var seasonFolders = GetSeasonFolders(series); + + //Work out the path to this season - if we don't have a matching path then skip this season. + string seasonFolder; + if (!seasonFolders.TryGetValue(season.SeasonNumber, out seasonFolder)) + { + _logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber); + return new List(); + } + + //WDTV only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection + var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault(); + if (image == null) + { + _logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber); + return new List(); + } + + var path = Path.Combine(series.Path, seasonFolder, "folder.jpg"); + + return new List{ new ImageFileResult(path, image.Url) }; + } + + public override List EpisodeImages(Series series, EpisodeFile episodeFile) + { + if (!Settings.EpisodeImages) + { + return new List(); + } + var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); if (screenshot == null) { _logger.Trace("Episode screenshot not available"); - return null; + return new List(); } - var filename = GetEpisodeImageFilename(episodeFile.Path); - var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename); - - var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeImage && - c.EpisodeFileId == episodeFile.Id); - - if (existingMetadata != null) - { - var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath); - if (!filename.PathEquals(fullPath)) - { - _diskProvider.MoveFile(fullPath, filename); - existingMetadata.RelativePath = relativePath; - } - } - - DownloadImage(series, screenshot.Url, filename); - - var metadata = existingMetadata ?? - new MetadataFile - { - SeriesId = series.Id, - EpisodeFileId = episodeFile.Id, - Consumer = GetType().Name, - Type = MetadataType.EpisodeImage, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename) - }; - - return metadata; + return new List{ new ImageFileResult(GetEpisodeImageFilename(episodeFile.Path), screenshot.Url) }; } private string GetEpisodeMetadataFilename(string episodeFilePath) @@ -471,5 +281,45 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv { return Path.ChangeExtension(episodeFilePath, "metathumb"); } + + private Dictionary GetSeasonFolders(Series series) + { + var seasonFolderMap = new Dictionary(); + + foreach (var folder in _diskProvider.GetDirectories(series.Path)) + { + var directoryinfo = new DirectoryInfo(folder); + var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name); + + if (seasonMatch.Success) + { + var seasonNumber = seasonMatch.Groups["season"].Value; + + if (seasonNumber.Contains("specials")) + { + seasonFolderMap[0] = folder; + } + else + { + int matchedSeason; + if (Int32.TryParse(seasonNumber, out matchedSeason)) + { + seasonFolderMap[matchedSeason] = folder; + } + else + { + _logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title); + } + } + } + + else + { + _logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title); + } + } + + return seasonFolderMap; + } } } diff --git a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs index 4089c6863..310ffa5df 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs @@ -9,6 +9,7 @@ using System.Xml.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Messaging.Events; @@ -19,32 +20,16 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc { public class XbmcMetadata : MetadataBase { - private readonly IEventAggregator _eventAggregator; private readonly IMapCoversToLocal _mediaCoverService; - private readonly IMediaFileService _mediaFileService; - private readonly IMetadataFileService _metadataFileService; private readonly IDiskProvider _diskProvider; - private readonly IHttpProvider _httpProvider; - private readonly IEpisodeService _episodeService; private readonly Logger _logger; - public XbmcMetadata(IEventAggregator eventAggregator, - IMapCoversToLocal mediaCoverService, - IMediaFileService mediaFileService, - IMetadataFileService metadataFileService, + public XbmcMetadata(IMapCoversToLocal mediaCoverService, IDiskProvider diskProvider, - IHttpProvider httpProvider, - IEpisodeService episodeService, Logger logger) - : base(diskProvider, httpProvider, logger) { - _eventAggregator = eventAggregator; _mediaCoverService = mediaCoverService; - _mediaFileService = mediaFileService; - _metadataFileService = metadataFileService; _diskProvider = diskProvider; - _httpProvider = httpProvider; - _episodeService = episodeService; _logger = logger; } @@ -52,79 +37,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc private static readonly Regex SeasonImagesRegex = new Regex(@"^season(?\d{2,}|-all|-specials)-(?poster|banner|fanart)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex EpisodeImageRegex = new Regex(@"-thumb\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public override void OnSeriesUpdated(Series series, List existingMetadataFiles, List episodeFiles) - { - var metadataFiles = new List(); - - if (!_diskProvider.FolderExists(series.Path)) - { - _logger.Info("Series folder does not exist, skipping metadata creation"); - return; - } - - if (Settings.SeriesMetadata) - { - metadataFiles.Add(WriteTvShowNfo(series, existingMetadataFiles)); - } - - if (Settings.SeriesImages) - { - metadataFiles.AddRange(WriteSeriesImages(series, existingMetadataFiles)); - } - - if (Settings.SeasonImages) - { - metadataFiles.AddRange(WriteSeasonImages(series, existingMetadataFiles)); - } - - foreach (var episodeFile in episodeFiles) - { - if (Settings.EpisodeMetadata) - { - metadataFiles.Add(WriteEpisodeNfo(series, episodeFile, existingMetadataFiles)); - } - } - - foreach (var episodeFile in episodeFiles) - { - if (Settings.EpisodeImages) - { - var metadataFile = WriteEpisodeImages(series, episodeFile, existingMetadataFiles); - - if (metadataFile != null) - { - metadataFiles.Add(metadataFile); - } - } - } - - _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles)); - } - - public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload) - { - var metadataFiles = new List(); - - if (Settings.EpisodeMetadata) - { - metadataFiles.Add(WriteEpisodeNfo(series, episodeFile, new List())); - } - - if (Settings.EpisodeImages) - { - var metadataFile = WriteEpisodeImages(series, episodeFile, new List()); - - if (metadataFile != null) - { - metadataFiles.Add(metadataFile); - } - WriteEpisodeImages(series, episodeFile, new List()); - } - - _eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles)); - } - - public override void AfterRename(Series series, List existingMetadataFiles, List episodeFiles) + public override List AfterRename(Series series, List existingMetadataFiles, List episodeFiles) { var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList(); var updatedMetadataFiles = new List(); @@ -165,7 +78,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc } } - _eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles)); + return updatedMetadataFiles; } public override MetadataFile FindMetadataFile(Series series, string path) @@ -239,8 +152,13 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc return null; } - private MetadataFile WriteTvShowNfo(Series series, List existingMetadataFiles) + public override MetadataFileResult SeriesMetadata(Series series) { + if (!Settings.SeriesMetadata) + { + return null; + } + _logger.Debug("Generating tvshow.nfo for: {0}", series.Title); var sb = new StringBuilder(); var xws = new XmlWriterSettings(); @@ -254,7 +172,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc var tvShow = new XElement("tvshow"); tvShow.Add(new XElement("title", series.Title)); - tvShow.Add(new XElement("rating", (decimal)series.Ratings.Percentage/10)); + tvShow.Add(new XElement("rating", (decimal) series.Ratings.Percentage/10)); tvShow.Add(new XElement("plot", series.Overview)); tvShow.Add(new XElement("episodeguide", new XElement("url", episodeGuideUrl))); tvShow.Add(new XElement("episodeguideurl", episodeGuideUrl)); @@ -276,10 +194,10 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc foreach (var actor in series.Actors) { tvShow.Add(new XElement("actor", - new XElement("name", actor.Name), - new XElement("role", actor.Character), - new XElement("thumb", actor.Images.First().Url) - )); + new XElement("name", actor.Name), + new XElement("role", actor.Character), + new XElement("thumb", actor.Images.First().Url) + )); } var doc = new XDocument(tvShow); @@ -287,108 +205,13 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc _logger.Debug("Saving tvshow.nfo for {0}", series.Title); - var path = Path.Combine(series.Path, "tvshow.nfo"); - - _diskProvider.WriteAllText(path, doc.ToString()); - - var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesMetadata) ?? - new MetadataFile - { - SeriesId = series.Id, - Consumer = GetType().Name, - Type = MetadataType.SeriesMetadata, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, path) - }; - - return metadata; + return new MetadataFileResult(Path.Combine(series.Path, "tvshow.nfo"), doc.ToString()); } } - private IEnumerable WriteSeriesImages(Series series, List existingMetadataFiles) - { - foreach (var image in series.Images) - { - var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); - var destination = Path.Combine(series.Path, image.CoverType.ToString().ToLowerInvariant() + Path.GetExtension(source)); - - //TODO: Do we want to overwrite the file if it exists? - if (_diskProvider.FileExists(destination)) - { - _logger.Debug("Series image: {0} already exists.", image.CoverType); - continue; - } - - _diskProvider.CopyFile(source, destination, false); - var relativePath = DiskProviderBase.GetRelativePath(series.Path, destination); - - var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage && - c.RelativePath == relativePath) ?? - new MetadataFile - { - SeriesId = series.Id, - Consumer = GetType().Name, - Type = MetadataType.SeriesImage, - RelativePath = relativePath - }; - - yield return metadata; - } - } - - private IEnumerable WriteSeasonImages(Series series, List existingMetadataFiles) - { - foreach (var season in series.Seasons) - { - foreach (var image in season.Images) - { - var filename = String.Format("season{0:00}-{1}.jpg", season.SeasonNumber, image.CoverType.ToString().ToLower()); - - if (season.SeasonNumber == 0) - { - filename = String.Format("season-specials-{0}.jpg", image.CoverType.ToString().ToLower()); - } - - var path = Path.Combine(series.Path, filename); - var relativePath = DiskProviderBase.GetRelativePath(series.Path, path); - - DownloadImage(series, image.Url, path); - - var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage && - c.SeasonNumber == season.SeasonNumber && - c.RelativePath == relativePath) ?? - new MetadataFile - { - SeriesId = series.Id, - SeasonNumber = season.SeasonNumber, - Consumer = GetType().Name, - Type = MetadataType.SeasonImage, - RelativePath = relativePath - }; - - yield return metadata; - } - } - } - - private MetadataFile WriteEpisodeNfo(Series series, EpisodeFile episodeFile, List existingMetadataFiles) - { - var filename = GetEpisodeNfoFilename(episodeFile.Path); - var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename); - - var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata && - c.EpisodeFileId == episodeFile.Id); - - if (existingMetadata != null) - { - var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath); - if (!filename.PathEquals(fullPath)) - { - _diskProvider.MoveFile(fullPath, filename); - existingMetadata.RelativePath = relativePath; - } - } - - _logger.Debug("Generating {0} for: {1}", filename, episodeFile.Path); + public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile) + { + _logger.Debug("Generating Episode Metadata for: {0}", episodeFile.Path); var xmlResult = String.Empty; foreach (var episode in episodeFile.Episodes.Value) @@ -423,9 +246,9 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc { details.Add(new XElement("thumb", image.Url)); } - + details.Add(new XElement("watched", "false")); - details.Add(new XElement("rating", (decimal)episode.Ratings.Percentage/10)); + details.Add(new XElement("rating", (decimal)episode.Ratings.Percentage / 10)); //Todo: get guest stars, writer and director //details.Add(new XElement("credits", tvdbEpisode.Writer.FirstOrDefault())); @@ -438,25 +261,37 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc xmlResult += Environment.NewLine; } } - - _logger.Debug("Saving episodedetails to: {0}", filename); - _diskProvider.WriteAllText(filename, xmlResult.Trim(Environment.NewLine.ToCharArray())); - var metadata = existingMetadata ?? - new MetadataFile - { - SeriesId = series.Id, - EpisodeFileId = episodeFile.Id, - Consumer = GetType().Name, - Type = MetadataType.EpisodeMetadata, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename) - }; - - return metadata; + return new MetadataFileResult(GetEpisodeNfoFilename(episodeFile.Path), xmlResult.Trim(Environment.NewLine.ToCharArray())); } - private MetadataFile WriteEpisodeImages(Series series, EpisodeFile episodeFile, List existingMetadataFiles) + public override List SeriesImages(Series series) { + if (!Settings.SeriesImages) + { + return new List(); + } + + return ProcessSeriesImages(series).ToList(); + } + + public override List SeasonImages(Series series, Season season) + { + if (!Settings.SeasonImages) + { + return new List(); + } + + return ProcessSeasonImages(series, season).ToList(); + } + + public override List EpisodeImages(Series series, EpisodeFile episodeFile) + { + if (!Settings.EpisodeImages) + { + return new List(); + } + var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); if (screenshot == null) @@ -465,35 +300,38 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc return null; } - var filename = GetEpisodeImageFilename(episodeFile.Path); - var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename); + return new List + { + new ImageFileResult(GetEpisodeImageFilename(episodeFile.Path), screenshot.Url) + }; + } - var existingMetadata = existingMetadataFiles.FirstOrDefault(c => c.Type == MetadataType.EpisodeImage && - c.EpisodeFileId == episodeFile.Id); - - if (existingMetadata != null) + private IEnumerable ProcessSeriesImages(Series series) + { + foreach (var image in series.Images) { - var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath); - if (!filename.PathEquals(fullPath)) - { - _diskProvider.MoveFile(fullPath, filename); - existingMetadata.RelativePath = relativePath; - } + var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); + var destination = Path.Combine(series.Path, image.CoverType.ToString().ToLowerInvariant() + Path.GetExtension(source)); + + yield return new ImageFileResult(destination, source); } + } - DownloadImage(series, screenshot.Url, filename); + private IEnumerable ProcessSeasonImages(Series series, Season season) + { + foreach (var image in season.Images) + { + var filename = String.Format("season{0:00}-{1}.jpg", season.SeasonNumber, image.CoverType.ToString().ToLower()); - var metadata = existingMetadata ?? - new MetadataFile - { - SeriesId = series.Id, - EpisodeFileId = episodeFile.Id, - Consumer = GetType().Name, - Type = MetadataType.EpisodeImage, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename) - }; + if (season.SeasonNumber == 0) + { + filename = String.Format("season-specials-{0}.jpg", image.CoverType.ToString().ToLower()); + } - return metadata; + var path = Path.Combine(series.Path, filename); + + yield return new ImageFileResult(Path.Combine(series.Path, filename), image.Url); + } } private string GetEpisodeNfoFilename(string episodeFilePath) diff --git a/src/NzbDrone.Core/MetaData/Files/ImageFileResult.cs b/src/NzbDrone.Core/MetaData/Files/ImageFileResult.cs new file mode 100644 index 000000000..e0330be8a --- /dev/null +++ b/src/NzbDrone.Core/MetaData/Files/ImageFileResult.cs @@ -0,0 +1,16 @@ +using System; + +namespace NzbDrone.Core.Metadata.Files +{ + public class ImageFileResult + { + public String Path { get; set; } + public String Url { get; set; } + + public ImageFileResult(string path, string url) + { + Path = path; + Url = url; + } + } +} diff --git a/src/NzbDrone.Core/MetaData/Files/MetadataFileResult.cs b/src/NzbDrone.Core/MetaData/Files/MetadataFileResult.cs new file mode 100644 index 000000000..1152bd487 --- /dev/null +++ b/src/NzbDrone.Core/MetaData/Files/MetadataFileResult.cs @@ -0,0 +1,16 @@ +using System; + +namespace NzbDrone.Core.Metadata.Files +{ + public class MetadataFileResult + { + public String Path { get; set; } + public String Contents { get; set; } + + public MetadataFileResult(string path, string contents) + { + Path = path; + Contents = contents; + } + } +} diff --git a/src/NzbDrone.Core/MetaData/IMetadata.cs b/src/NzbDrone.Core/MetaData/IMetadata.cs index 63fa19d73..f9c1feae3 100644 --- a/src/NzbDrone.Core/MetaData/IMetadata.cs +++ b/src/NzbDrone.Core/MetaData/IMetadata.cs @@ -8,9 +8,14 @@ namespace NzbDrone.Core.Metadata { public interface IMetadata : IProvider { - void OnSeriesUpdated(Series series, List existingMetadataFiles, List episodeFiles); - void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload); - void AfterRename(Series series, List existingMetadataFiles, List episodeFiles); + List AfterRename(Series series, List existingMetadataFiles, List episodeFiles); MetadataFile FindMetadataFile(Series series, string path); + + MetadataFileResult SeriesMetadata(Series series); + MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile); + List SeriesImages(Series series); + List SeasonImages(Series series, Season season); + List EpisodeImages(Series series, EpisodeFile episodeFile); + } } diff --git a/src/NzbDrone.Core/MetaData/MetadataService.cs b/src/NzbDrone.Core/MetaData/MetadataService.cs index 681eed8d0..ca428d4fc 100644 --- a/src/NzbDrone.Core/MetaData/MetadataService.cs +++ b/src/NzbDrone.Core/MetaData/MetadataService.cs @@ -1,6 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Net; using NLog; +using NzbDrone.Common; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; @@ -21,6 +27,9 @@ namespace NzbDrone.Core.Metadata private readonly ICleanMetadataService _cleanMetadataService; private readonly IMediaFileService _mediaFileService; private readonly IEpisodeService _episodeService; + private readonly IDiskProvider _diskProvider; + private readonly IHttpProvider _httpProvider; + private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; public MetadataService(IMetadataFactory metadataFactory, @@ -28,6 +37,9 @@ namespace NzbDrone.Core.Metadata ICleanMetadataService cleanMetadataService, IMediaFileService mediaFileService, IEpisodeService episodeService, + IDiskProvider diskProvider, + IHttpProvider httpProvider, + IEventAggregator eventAggregator, Logger logger) { _metadataFactory = metadataFactory; @@ -35,17 +47,41 @@ namespace NzbDrone.Core.Metadata _cleanMetadataService = cleanMetadataService; _mediaFileService = mediaFileService; _episodeService = episodeService; + _diskProvider = diskProvider; + _httpProvider = httpProvider; + _eventAggregator = eventAggregator; _logger = logger; } public void Handle(MediaCoversUpdatedEvent message) { _cleanMetadataService.Clean(message.Series); - var seriesMetadata = _metadataFileService.GetFilesBySeries(message.Series.Id); + + if (!_diskProvider.FolderExists(message.Series.Path)) + { + _logger.Info("Series folder does not exist, skipping metadata creation"); + return; + } + + var seriesMetadataFiles = _metadataFileService.GetFilesBySeries(message.Series.Id); + var episodeFiles = GetEpisodeFiles(message.Series.Id); foreach (var consumer in _metadataFactory.Enabled()) { - consumer.OnSeriesUpdated(message.Series, GetMetadataFilesForConsumer(consumer, seriesMetadata), GetEpisodeFiles(message.Series.Id)); + var consumerFiles = GetMetadataFilesForConsumer(consumer, seriesMetadataFiles); + var files = new List(); + + files.AddIfNotNull(ProcessSeriesMetadata(consumer, message.Series, consumerFiles)); + files.AddRange(ProcessSeriesImages(consumer, message.Series, consumerFiles)); + files.AddRange(ProcessSeasonImages(consumer, message.Series, consumerFiles)); + + foreach (var episodeFile in episodeFiles) + { + files.AddIfNotNull(ProcessEpisodeMetadata(consumer, message.Series, episodeFile, consumerFiles)); + files.AddRange(ProcessEpisodeImages(consumer, message.Series, episodeFile, consumerFiles)); + } + + _eventAggregator.PublishEvent(new MetadataFilesUpdated(files)); } } @@ -53,17 +89,27 @@ namespace NzbDrone.Core.Metadata { foreach (var consumer in _metadataFactory.Enabled()) { - consumer.OnEpisodeImport(message.EpisodeInfo.Series, message.ImportedEpisode, message.NewDownload); + var files = new List(); + + files.AddIfNotNull(ProcessEpisodeMetadata(consumer, message.EpisodeInfo.Series, message.ImportedEpisode, new List())); + files.AddRange(ProcessEpisodeImages(consumer, message.EpisodeInfo.Series, message.ImportedEpisode, new List())); + + _eventAggregator.PublishEvent(new MetadataFilesUpdated(files)); } } public void Handle(SeriesRenamedEvent message) { var seriesMetadata = _metadataFileService.GetFilesBySeries(message.Series.Id); + var episodeFiles = GetEpisodeFiles(message.Series.Id); foreach (var consumer in _metadataFactory.Enabled()) { - consumer.AfterRename(message.Series, GetMetadataFilesForConsumer(consumer, seriesMetadata), GetEpisodeFiles(message.Series.Id)); + var updatedMetadataFiles = consumer.AfterRename(message.Series, + GetMetadataFilesForConsumer(consumer, seriesMetadata), + episodeFiles); + + _eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles)); } } @@ -85,5 +131,219 @@ namespace NzbDrone.Core.Metadata { return seriesMetadata.Where(c => c.Consumer == consumer.GetType().Name).ToList(); } + + private MetadataFile ProcessSeriesMetadata(IMetadata consumer, Series series, List existingMetadataFiles) + { + var seriesMetadata = consumer.SeriesMetadata(series); + + if (seriesMetadata == null) + { + return null; + } + + var hash = seriesMetadata.Contents.SHA256Hash(); + + var metadata = existingMetadataFiles.SingleOrDefault(e => e.Type == MetadataType.SeriesMetadata) ?? + new MetadataFile + { + SeriesId = series.Id, + Consumer = consumer.GetType().Name, + Type = MetadataType.SeriesMetadata, + }; + + if (hash == metadata.Hash) + { + return null; + } + + _logger.Debug("Writing Series Metadata to: {0}", seriesMetadata.Path); + _diskProvider.WriteAllText(seriesMetadata.Path, seriesMetadata.Contents); + + metadata.Hash = hash; + metadata.RelativePath = DiskProviderBase.GetRelativePath(series.Path, seriesMetadata.Path); + + return metadata; + } + + private MetadataFile ProcessEpisodeMetadata(IMetadata consumer, Series series, EpisodeFile episodeFile, List existingMetadataFiles) + { + var episodeMetadata = consumer.EpisodeMetadata(series, episodeFile); + + if (episodeMetadata == null) + { + return null; + } + + var relativePath = DiskProviderBase.GetRelativePath(series.Path, episodeMetadata.Path); + + var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata && + c.EpisodeFileId == episodeFile.Id); + + if (existingMetadata != null) + { + var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath); + if (!episodeMetadata.Path.PathEquals(fullPath)) + { + _diskProvider.MoveFile(fullPath, episodeMetadata.Path); + existingMetadata.RelativePath = relativePath; + } + } + + var hash = episodeMetadata.Contents.SHA256Hash(); + + var metadata = existingMetadata ?? + new MetadataFile + { + SeriesId = series.Id, + EpisodeFileId = episodeFile.Id, + Consumer = consumer.GetType().Name, + Type = MetadataType.EpisodeMetadata, + RelativePath = relativePath + }; + + if (hash == metadata.Hash) + { + return null; + } + + _logger.Debug("Writing Episode Metadata to: {0}", episodeMetadata.Path); + _diskProvider.WriteAllText(episodeMetadata.Path, episodeMetadata.Contents); + + metadata.Hash = hash; + + return metadata; + } + + private List ProcessSeriesImages(IMetadata consumer, Series series, List existingMetadataFiles) + { + var result = new List(); + + foreach (var image in consumer.SeriesImages(series)) + { + if (_diskProvider.FileExists(image.Path)) + { + _logger.Debug("Series image already exists: {0}", image.Path); + continue; + } + + var relativePath = DiskProviderBase.GetRelativePath(series.Path, image.Path); + + var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage && + c.RelativePath == relativePath) ?? + new MetadataFile + { + SeriesId = series.Id, + Consumer = consumer.GetType().Name, + Type = MetadataType.SeriesImage, + RelativePath = relativePath + }; + + _diskProvider.CopyFile(image.Url, image.Path); + + result.Add(metadata); + } + + return result; + } + + private List ProcessSeasonImages(IMetadata consumer, Series series, List existingMetadataFiles) + { + var result = new List(); + + foreach (var season in series.Seasons) + { + foreach (var image in consumer.SeasonImages(series, season)) + { + if (_diskProvider.FileExists(image.Path)) + { + _logger.Debug("Season image already exists: {0}", image.Path); + continue; + } + + var relativePath = DiskProviderBase.GetRelativePath(series.Path, image.Path); + + var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage && + c.SeasonNumber == season.SeasonNumber && + c.RelativePath == relativePath) ?? + new MetadataFile + { + SeriesId = series.Id, + SeasonNumber = season.SeasonNumber, + Consumer = consumer.GetType().Name, + Type = MetadataType.SeasonImage, + RelativePath = relativePath + }; + + DownloadImage(series, image.Url, image.Path); + + result.Add(metadata); + } + } + + return result; + } + + private List ProcessEpisodeImages(IMetadata consumer, Series series, EpisodeFile episodeFile, List existingMetadataFiles) + { + var result = new List(); + + foreach (var image in consumer.EpisodeImages(series, episodeFile)) + { + if (_diskProvider.FileExists(image.Path)) + { + _logger.Debug("Episode image already exists: {0}", image.Path); + continue; + } + + var relativePath = DiskProviderBase.GetRelativePath(series.Path, image.Path); + + var existingMetadata = existingMetadataFiles.FirstOrDefault(c => c.Type == MetadataType.EpisodeImage && + c.EpisodeFileId == episodeFile.Id); + + if (existingMetadata != null) + { + var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath); + if (!image.Path.PathEquals(fullPath)) + { + _diskProvider.MoveFile(fullPath, image.Path); + existingMetadata.RelativePath = relativePath; + + return new List{ existingMetadata }; + } + } + + var metadata = existingMetadata ?? + new MetadataFile + { + SeriesId = series.Id, + EpisodeFileId = episodeFile.Id, + Consumer = consumer.GetType().Name, + Type = MetadataType.EpisodeImage, + RelativePath = DiskProviderBase.GetRelativePath(series.Path, image.Path) + }; + + DownloadImage(series, image.Url, image.Path); + + result.Add(metadata); + } + + return result; + } + + private void DownloadImage(Series series, string url, string path) + { + try + { + _httpProvider.DownloadFile(url, path); + } + catch (WebException e) + { + _logger.Warn(string.Format("Couldn't download image {0} for {1}. {2}", url, series, e.Message)); + } + catch (Exception e) + { + _logger.ErrorException("Couldn't download image " + url + " for " + series, e); + } + } } } diff --git a/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs b/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs index 0a4c68cdf..00642823d 100644 --- a/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs +++ b/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs @@ -5,14 +5,14 @@ using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Parser; -using NzbDrone.Core.Tv.Events; namespace NzbDrone.Core.Metadata { - public class ExistingMetadataService : IHandle + public class ExistingMetadataService : IHandle { private readonly IDiskProvider _diskProvider; private readonly IMetadataFileService _metadataFileService; @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Metadata _consumers = consumers.ToList(); } - public void Handle(SeriesUpdatedEvent message) + public void Handle(SeriesScannedEvent message) { if (!_diskProvider.FolderExists(message.Series.Path)) return; diff --git a/src/NzbDrone.Core/Metadata/Files/MetadataFile.cs b/src/NzbDrone.Core/Metadata/Files/MetadataFile.cs index df52e2bc4..c30e9064d 100644 --- a/src/NzbDrone.Core/Metadata/Files/MetadataFile.cs +++ b/src/NzbDrone.Core/Metadata/Files/MetadataFile.cs @@ -12,5 +12,6 @@ namespace NzbDrone.Core.Metadata.Files public DateTime LastUpdated { get; set; } public Int32? EpisodeFileId { get; set; } public Int32? SeasonNumber { get; set; } + public String Hash { get; set; } } } diff --git a/src/NzbDrone.Core/Metadata/MetadataBase.cs b/src/NzbDrone.Core/Metadata/MetadataBase.cs index 3bebd9d90..a68d60174 100644 --- a/src/NzbDrone.Core/Metadata/MetadataBase.cs +++ b/src/NzbDrone.Core/Metadata/MetadataBase.cs @@ -4,6 +4,7 @@ using System.Net; using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.ThingiProvider; @@ -13,17 +14,6 @@ namespace NzbDrone.Core.Metadata { public abstract class MetadataBase : IMetadata where TSettings : IProviderConfig, new() { - private readonly IDiskProvider _diskProvider; - private readonly IHttpProvider _httpProvider; - private readonly Logger _logger; - - protected MetadataBase(IDiskProvider diskProvider, IHttpProvider httpProvider, Logger logger) - { - _diskProvider = diskProvider; - _httpProvider = httpProvider; - _logger = logger; - } - public Type ConfigContract { get @@ -42,11 +32,15 @@ namespace NzbDrone.Core.Metadata public ProviderDefinition Definition { get; set; } - public abstract void OnSeriesUpdated(Series series, List existingMetadataFiles, List episodeFiles); - public abstract void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload); - public abstract void AfterRename(Series series, List existingMetadataFiles, List episodeFiles); + public abstract List AfterRename(Series series, List existingMetadataFiles, List episodeFiles); public abstract MetadataFile FindMetadataFile(Series series, string path); + public abstract MetadataFileResult SeriesMetadata(Series series); + public abstract MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile); + public abstract List SeriesImages(Series series); + public abstract List SeasonImages(Series series, Season season); + public abstract List EpisodeImages(Series series, EpisodeFile episodeFile); + protected TSettings Settings { get @@ -55,28 +49,6 @@ namespace NzbDrone.Core.Metadata } } - protected virtual void DownloadImage(Series series, string url, string path) - { - try - { - if (_diskProvider.FileExists(path)) - { - _logger.Debug("Image already exists: {0}, will not download again.", path); - return; - } - - _httpProvider.DownloadFile(url, path); - } - catch (WebException e) - { - _logger.Warn(string.Format("Couldn't download image {0} for {1}. {2}", url, series, e.Message)); - } - catch (Exception e) - { - _logger.ErrorException("Couldn't download image " + url + " for " + series, e); - } - } - public override string ToString() { return GetType().Name; diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexService.cs b/src/NzbDrone.Core/Notifications/Plex/PlexService.cs index d141916c5..357668aae 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexService.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexService.cs @@ -5,6 +5,7 @@ using System.Net; using System.Xml.Linq; using NLog; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.Notifications.Plex diff --git a/src/NzbDrone.Core/Notifications/Xbmc/HttpApiProvider.cs b/src/NzbDrone.Core/Notifications/Xbmc/HttpApiProvider.cs index 1cb2e70b4..048778bed 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/HttpApiProvider.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/HttpApiProvider.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Xml.Linq; using NLog; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Core.Notifications.Xbmc.Model; using NzbDrone.Core.Tv; diff --git a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs index 99f40d830..ef39eb353 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs @@ -4,6 +4,7 @@ using System.Linq; using NLog; using Newtonsoft.Json.Linq; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; using NzbDrone.Core.Notifications.Xbmc.Model; using NzbDrone.Core.Tv; @@ -214,7 +215,7 @@ namespace NzbDrone.Core.Notifications.Xbmc postJson.Add(new JProperty("params", parameters)); } - postJson.Add(new JProperty("id", 10)); + postJson.Add(new JProperty("id", DateTime.Now.Ticks)); return postJson; } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs index 2845a5f6f..7e3c236e7 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs @@ -4,6 +4,7 @@ using System.Linq; using Newtonsoft.Json.Linq; using NLog; using NzbDrone.Common; +using NzbDrone.Common.Http; using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Serializer; using NzbDrone.Core.Messaging.Commands; @@ -57,7 +58,7 @@ namespace NzbDrone.Core.Notifications.Xbmc var postJson = new JObject(); postJson.Add(new JProperty("jsonrpc", "2.0")); postJson.Add(new JProperty("method", "JSONRPC.Version")); - postJson.Add(new JProperty("id", 10)); + postJson.Add(new JProperty("id", DateTime.Now.Ticks)); var response = _httpProvider.PostCommand(settings.Address, settings.Username, settings.Password, postJson.ToString()); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index b8938f15e..389d24a9d 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -138,9 +138,9 @@ - - - + + + @@ -194,6 +194,8 @@ + + @@ -279,6 +281,7 @@ + @@ -322,7 +325,6 @@ - @@ -361,6 +363,8 @@ + + @@ -539,6 +543,7 @@ + @@ -688,10 +693,13 @@ + + + diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 5b4cf8d2d..8b7987734 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -24,15 +24,15 @@ namespace NzbDrone.Core.Parser //Anime - [SubGroup] Title Absolute Episode Number + Season+Episode new Regex(@"^(?:\[(?.+?)\](?:_|-|\s|\.))(?.+?)(?:(?:\W|_)+(?<absoluteepisode>\d{2,3}))+(?:_|-|\s|\.)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), + RegexOptions.IgnoreCase | RegexOptions.Compiled), //Anime - [SubGroup] Title Season+Episode + Absolute Episode Number new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.))(?<title>.+?)(?:\W|_)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:\s|\.)(?:(?<absoluteepisode>\d{2,3})(?:_|-|\s|\.|$)+)+", - RegexOptions.IgnoreCase | RegexOptions.Compiled), + RegexOptions.IgnoreCase | RegexOptions.Compiled), //Anime - [SubGroup] Title Absolute Episode Number new Regex(@"^\[(?<subgroup>.+?)\](?:_|-|\s|\.)?(?<title>.+?)(?:(?:\W|_)+(?<absoluteepisode>\d{2,}))+", - RegexOptions.IgnoreCase | RegexOptions.Compiled), + RegexOptions.IgnoreCase | RegexOptions.Compiled), //Multi-Part episodes without a title (S01E05.S01E06) new Regex(@"^(?:\W*S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]){1,2}(?<episode>\d{1,3}(?!\d+)))+){2,}", @@ -60,10 +60,10 @@ namespace NzbDrone.Core.Parser //Anime - Title Absolute Episode Number [SubGroup] new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{3}(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\](?:\.|$)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), + RegexOptions.IgnoreCase | RegexOptions.Compiled), //Supports 103/113 naming - new Regex(@"^(?<title>.+?)?(?:\W?(?<season>(?<!\d+)\d{1})(?<episode>\d{2}(?!\w|\d+)))+", + new Regex(@"^(?<title>.+?)?(?:\W?(?<season>(?<!\d+)\d{1})(?<episode>[1-9][0-9]|[0][1-9])(?!\w|\d+))+", RegexOptions.IgnoreCase | RegexOptions.Compiled), //Mini-Series, treated as season 1, episodes are labelled as Part01, Part 01, Part.1 @@ -97,7 +97,7 @@ namespace NzbDrone.Core.Parser //Anime - Title Absolute Episode Number new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+e(?<absoluteepisode>\d{2,3}))+", - RegexOptions.IgnoreCase | RegexOptions.Compiled) + RegexOptions.IgnoreCase | RegexOptions.Compiled) }; private static readonly Regex[] RejectHashedReleasesRegex = new Regex[] @@ -113,10 +113,16 @@ namespace NzbDrone.Core.Parser private static readonly Regex ReversedTitleRegex = new Regex(@"\.p027\.|\.p0801\.|\.\d{2}E\d{2}S\.", RegexOptions.Compiled); private static readonly Regex NormalizeRegex = new Regex(@"((?:\b|_)(?<!^)(a|an|the|and|or|of)(?:\b|_))|\W|_", - RegexOptions.IgnoreCase | RegexOptions.Compiled); + RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex SimpleTitleRegex = new Regex(@"480[i|p]|720[i|p]|1080[i|p]|[xh][\W_]?264|DD\W?5\W1|\<|\>|\?|\*|\:|\|", - RegexOptions.IgnoreCase | RegexOptions.Compiled); + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly Regex AirDateRegex = new Regex(@"^(.*?)(?<!\d)((?<airyear>\d{4})[_.-](?<airmonth>[0-1][0-9])[_.-](?<airday>[0-3][0-9])|(?<airmonth>[0-1][0-9])[_.-](?<airday>[0-3][0-9])[_.-](?<airyear>\d{4}))(?!\d)", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly Regex ReleaseGroupRegex = new Regex(@"-(?<releasegroup>[a-z0-9]+)\b(?<!WEB-DL|480p|720p|1080p)", + RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex MultiPartCleanupRegex = new Regex(@"\(\d+\)$", RegexOptions.Compiled); @@ -124,14 +130,14 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex YearInTitleRegex = new Regex(@"^(?<title>.+?)(?:\W|_)?(?<year>\d{4})", - RegexOptions.IgnoreCase | RegexOptions.Compiled); + RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex WordDelimiterRegex = new Regex(@"(\s|\.|,|_|-|=|\|)+", RegexOptions.Compiled); private static readonly Regex PunctuationRegex = new Regex(@"[^\w\s]", RegexOptions.Compiled); private static readonly Regex CommonWordRegex = new Regex(@"\b(a|an|the|and|or|of)\b\s?", - RegexOptions.IgnoreCase | RegexOptions.Compiled); + RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex SpecialEpisodeWordRegex = new Regex(@"\b(part|special|edition)\b\s?", - RegexOptions.IgnoreCase | RegexOptions.Compiled); + RegexOptions.IgnoreCase | RegexOptions.Compiled); public static ParsedEpisodeInfo ParsePath(string path) { @@ -180,6 +186,12 @@ namespace NzbDrone.Core.Parser var simpleTitle = SimpleTitleRegex.Replace(title, String.Empty); + var airDateMatch = AirDateRegex.Match(simpleTitle); + if (airDateMatch.Success) + { + simpleTitle = airDateMatch.Groups[1].Value + airDateMatch.Groups["airyear"].Value + "." + airDateMatch.Groups["airmonth"].Value + "." + airDateMatch.Groups["airday"].Value; + } + foreach (var regex in ReportTitleRegex) { var match = regex.Matches(simpleTitle); @@ -272,24 +284,13 @@ namespace NzbDrone.Core.Parser title = title.TrimEnd("-RP"); - var index = title.LastIndexOf('-'); - - if (index < 0) - index = title.LastIndexOf(' '); - - if (index < 0) - return defaultReleaseGroup; - - var group = title.Substring(index + 1); - - if (group.Length == title.Length) - return String.Empty; - - group = group.Trim('-', ' ', '[', ']'); - - if (group.ToLower() == "480p" || - group.ToLower() == "720p" || - group.ToLower() == "1080p") + string group; + var matches = ReleaseGroupRegex.Matches(title); + if (matches.Count != 0) + { + group = matches.OfType<Match>().Last().Groups["releasegroup"].Value; + } + else { return defaultReleaseGroup; } diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index c21cb3e19..e8b6e5d5b 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -5,7 +5,6 @@ using NLog; using NzbDrone.Common; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; namespace NzbDrone.Core.Parser { @@ -13,37 +12,50 @@ namespace NzbDrone.Core.Parser { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - private static readonly Regex SourceRegex = new Regex(@"(?<bluray>BluRay)| + private static readonly Regex SourceRegex = new Regex(@"\b(?: + (?<bluray>BluRay)| (?<webdl>WEB-DL|WEBDL|WEB\sDL|WEB\-DL|WebRip)| (?<hdtv>HDTV)| - (?<bdrip>BDRiP)|(?<brrip>BRRip)|(?<dvd>\b(?:DVD|DVDRip|NTSC|PAL|xvidvd)\b)| - (?<dsr>WS\sDSR|WS_DSR|WS\.DSR|DSR)|(?<pdtv>PDTV)|(?<sdtv>SDTV)", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + (?<bdrip>BDRiP)| + (?<brrip>BRRip)| + (?<dvd>DVD|DVDRip|NTSC|PAL|xvidvd)| + (?<dsr>WS\sDSR|WS_DSR|WS\.DSR|DSR)| + (?<pdtv>PDTV)| + (?<sdtv>SDTV) + )\b", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - private static readonly Regex ResolutionRegex = new Regex(@"(?<_480p>480p)|(?<_720p>720p)|(?<_1080p>1080p)", - RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex RawHDRegex = new Regex(@"\b(?<rawhd>TrollHD|RawHD)\b", + RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex CodecRegex = new Regex(@"(?<x264>x264)|(?<h264>h264)|(?<xvidhd>XvidHD)|(?<xvid>Xvid)|(?<divx>divx)", - RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex ProperRegex = new Regex(@"\b(?<proper>proper|repack)\b", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex ResolutionRegex = new Regex(@"\b(?:(?<_480p>480p)|(?<_576p>576p)|(?<_720p>720p)|(?<_1080p>1080p))\b", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex CodecRegex = new Regex(@"\b(?:(?<x264>x264)|(?<h264>h264)|(?<xvidhd>XvidHD)|(?<xvid>Xvid)|(?<divx>divx))\b", + RegexOptions.Compiled | RegexOptions.IgnoreCase); public static QualityModel ParseQuality(string name) { Logger.Debug("Trying to parse quality for {0}", name); name = name.Trim(); - var normalizedName = name.CleanSeriesTitle(); + var normalizedName = name.Replace('_', ' ').Trim().ToLower(); var result = new QualityModel { Quality = Quality.Unknown }; - result.Proper = (normalizedName.Contains("proper") || normalizedName.Contains("repack")); - if (normalizedName.Contains("trollhd") || normalizedName.Contains("rawhd")) + result.Proper = ProperRegex.IsMatch(normalizedName); + + if (RawHDRegex.IsMatch(normalizedName)) { result.Quality = Quality.RAWHD; return result; } - - var sourceMatch = SourceRegex.Match(name); - var resolution = ParseResolution(name); - var codecRegex = CodecRegex.Match(name); + + var sourceMatch = SourceRegex.Match(normalizedName); + var resolution = ParseResolution(normalizedName); + var codecRegex = CodecRegex.Match(normalizedName); if (sourceMatch.Groups["bluray"].Success) { @@ -59,6 +71,12 @@ namespace NzbDrone.Core.Parser return result; } + if (resolution == Resolution._480p || resolution == Resolution._576p) + { + result.Quality = Quality.DVD; + return result; + } + result.Quality = Quality.Bluray720p; return result; } @@ -111,9 +129,27 @@ namespace NzbDrone.Core.Parser return result; } - if (sourceMatch.Groups["dvd"].Success || - sourceMatch.Groups["bdrip"].Success || + if (sourceMatch.Groups["bdrip"].Success || sourceMatch.Groups["brrip"].Success) + { + if (resolution == Resolution._720p) + { + result.Quality = Quality.Bluray720p; + return result; + } + else if (resolution == Resolution._1080p) + { + result.Quality = Quality.Bluray1080p; + return result; + } + else + { + result.Quality = Quality.DVD; + return result; + } + } + + if (sourceMatch.Groups["dvd"].Success) { result.Quality = Quality.DVD; return result; @@ -145,6 +181,16 @@ namespace NzbDrone.Core.Parser return result; } + if (normalizedName.Contains("bluray720p")) + { + result.Quality = Quality.Bluray720p; + } + + if (normalizedName.Contains("bluray1080p")) + { + result.Quality = Quality.Bluray1080p; + } + //Based on extension if (result.Quality == Quality.Unknown && !name.ContainsInvalidPathChars()) { @@ -168,6 +214,7 @@ namespace NzbDrone.Core.Parser if (!match.Success) return Resolution.Unknown; if (match.Groups["_480p"].Success) return Resolution._480p; + if (match.Groups["_576p"].Success) return Resolution._576p; if (match.Groups["_720p"].Success) return Resolution._720p; if (match.Groups["_1080p"].Success) return Resolution._1080p; @@ -178,6 +225,7 @@ namespace NzbDrone.Core.Parser public enum Resolution { _480p, + _576p, _720p, _1080p, Unknown diff --git a/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs index 9895461ec..a388f1667 100644 --- a/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs +++ b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs @@ -4,6 +4,7 @@ using NLog; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; using System; +using NzbDrone.Common.Cache; namespace NzbDrone.Core.Qualities { @@ -17,22 +18,31 @@ namespace NzbDrone.Core.Qualities public class QualityDefinitionService : IQualityDefinitionService, IHandle<ApplicationStartedEvent> { private readonly IQualityDefinitionRepository _qualityDefinitionRepository; + private readonly ICached<Dictionary<Quality, QualityDefinition>> _cache; private readonly Logger _logger; - public QualityDefinitionService(IQualityDefinitionRepository qualityDefinitionRepository, Logger logger) + public QualityDefinitionService(IQualityDefinitionRepository qualityDefinitionRepository, ICacheManager cacheManager, Logger logger) { _qualityDefinitionRepository = qualityDefinitionRepository; + _cache = cacheManager.GetCache<Dictionary<Quality, QualityDefinition>>(this.GetType()); _logger = logger; } + private Dictionary<Quality, QualityDefinition> GetAll() + { + return _cache.Get("all", () => _qualityDefinitionRepository.All().ToDictionary(v => v.Quality), TimeSpan.FromSeconds(5.0)); + } + public void Update(QualityDefinition qualityDefinition) { _qualityDefinitionRepository.Update(qualityDefinition); + + _cache.Clear(); } public List<QualityDefinition> All() { - return _qualityDefinitionRepository.All().ToList(); + return GetAll().Values.ToList(); } public QualityDefinition Get(Quality quality) @@ -40,7 +50,7 @@ namespace NzbDrone.Core.Qualities if (quality == Quality.Unknown) return new QualityDefinition(Quality.Unknown); - return _qualityDefinitionRepository.GetByQualityId((int)quality); + return GetAll()[quality]; } public void InsertMissingDefinitions(List<QualityDefinition> allDefinitions) @@ -89,10 +99,12 @@ namespace NzbDrone.Core.Qualities _qualityDefinitionRepository.Update(existingDefinitions[i]); } } + + _cache.Clear(); } public void Handle(ApplicationStartedEvent message) - { + { _logger.Debug("Setting up default quality config"); InsertMissingDefinitions(Quality.DefaultQualityDefinitions.ToList()); diff --git a/src/NzbDrone.Core/Security.cs b/src/NzbDrone.Core/Security.cs new file mode 100644 index 000000000..840466e98 --- /dev/null +++ b/src/NzbDrone.Core/Security.cs @@ -0,0 +1,38 @@ +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace NzbDrone.Core +{ + public static class Security + { + public static string SHA256Hash(this string input) + { + using (var hash = SHA256Managed.Create()) + { + var enc = Encoding.UTF8; + return GetHash(hash.ComputeHash(enc.GetBytes(input))); + } + } + + public static string SHA256Hash(this Stream input) + { + using (var hash = SHA256Managed.Create()) + { + return GetHash(hash.ComputeHash(input)); + } + } + + private static string GetHash(byte[] bytes) + { + var stringBuilder = new StringBuilder(); + + foreach (var b in bytes) + { + stringBuilder.Append(b.ToString("x2")); + } + + return stringBuilder.ToString(); + } + } +} diff --git a/src/NzbDrone.Core/Tv/EpisodeRepository.cs b/src/NzbDrone.Core/Tv/EpisodeRepository.cs index 2a6a1792e..db43e8f26 100644 --- a/src/NzbDrone.Core/Tv/EpisodeRepository.cs +++ b/src/NzbDrone.Core/Tv/EpisodeRepository.cs @@ -5,7 +5,7 @@ using System.Linq; using Marr.Data.QGen; using NLog; using NzbDrone.Core.Datastore; -using NzbDrone.Core.Datastore.Extentions; +using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Qualities; diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index 402642e0a..09c073e56 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -27,7 +27,6 @@ namespace NzbDrone.Core.Tv List<Episode> GetEpisodesByFileId(int episodeFileId); void UpdateEpisode(Episode episode); void SetEpisodeMonitored(int episodeId, bool monitored); - bool IsFirstOrLastEpisodeOfSeason(int episodeId); void UpdateEpisodes(List<Episode> episodes); List<Episode> EpisodesBetweenDates(DateTime start, DateTime end); void InsertMany(List<Episode> episodes); @@ -141,19 +140,6 @@ namespace NzbDrone.Core.Tv _episodeRepository.SetMonitoredBySeason(seriesId, seasonNumber, monitored); } - public bool IsFirstOrLastEpisodeOfSeason(int episodeId) - { - var episode = GetEpisode(episodeId); - var seasonEpisodes = GetEpisodesBySeason(episode.SeriesId, episode.SeasonNumber); - - //Ensure that this is either the first episode - //or is the last episode in a season that has 10 or more episodes - if (seasonEpisodes.First().EpisodeNumber == episode.EpisodeNumber || (seasonEpisodes.Count() >= 10 && seasonEpisodes.Last().EpisodeNumber == episode.EpisodeNumber)) - return true; - - return false; - } - public void UpdateEpisodes(List<Episode> episodes) { _episodeRepository.UpdateMany(episodes); diff --git a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs index 81b2f6ae8..5390d0502 100644 --- a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs +++ b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs @@ -101,6 +101,12 @@ namespace NzbDrone.Core.Tv //Todo: Should this should use the previous season's monitored state? if (existingSeason == null) { + if (season.SeasonNumber == 0) + { + season.Monitored = false; + continue; + } + _logger.Debug("New season ({0}) for series: [{1}] {2}, setting monitored to true", season.SeasonNumber, series.TvdbId, series.Title); season.Monitored = true; } diff --git a/src/NzbDrone.Core/Update/InstallUpdateService.cs b/src/NzbDrone.Core/Update/InstallUpdateService.cs index da353fa33..404ac8f12 100644 --- a/src/NzbDrone.Core/Update/InstallUpdateService.cs +++ b/src/NzbDrone.Core/Update/InstallUpdateService.cs @@ -4,7 +4,9 @@ using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Http; using NzbDrone.Common.Processes; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Instrumentation.Extensions; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Update.Commands; @@ -26,18 +28,31 @@ namespace NzbDrone.Core.Update private readonly IHttpProvider _httpProvider; private readonly IArchiveService _archiveService; private readonly IProcessProvider _processProvider; + private readonly IVerifyUpdates _updateVerifier; + private readonly IConfigFileProvider _configFileProvider; + private readonly IRuntimeInfo _runtimeInfo; public InstallUpdateService(ICheckUpdateService checkUpdateService, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IHttpProvider httpProvider, - IArchiveService archiveService, IProcessProvider processProvider, Logger logger) + IArchiveService archiveService, IProcessProvider processProvider, + IVerifyUpdates updateVerifier, + IConfigFileProvider configFileProvider, + IRuntimeInfo runtimeInfo, Logger logger) { + if (configFileProvider == null) + { + throw new ArgumentNullException("configFileProvider"); + } _checkUpdateService = checkUpdateService; _appFolderInfo = appFolderInfo; _diskProvider = diskProvider; _httpProvider = httpProvider; _archiveService = archiveService; _processProvider = processProvider; + _updateVerifier = updateVerifier; + _configFileProvider = configFileProvider; + _runtimeInfo = runtimeInfo; _logger = logger; } @@ -55,23 +70,38 @@ namespace NzbDrone.Core.Update _diskProvider.DeleteFolder(updateSandboxFolder, true); } - _logger.ProgressInfo("Downloading update {0} [{1}]", updatePackage.Version, updatePackage.Branch); + _logger.ProgressInfo("Downloading update {0}", updatePackage.Version); _logger.Debug("Downloading update package from [{0}] to [{1}]", updatePackage.Url, packageDestination); _httpProvider.DownloadFile(updatePackage.Url, packageDestination); + _logger.ProgressInfo("Verifying update package"); + + if (!_updateVerifier.Verify(updatePackage, packageDestination)) + { + _logger.Error("Update package is invalid"); + throw new UpdateVerificationFailedException("Update file '{0}' is invalid", packageDestination); + } + + _logger.Info("Update package verified successfully"); + _logger.ProgressInfo("Extracting Update package"); _archiveService.Extract(packageDestination, updateSandboxFolder); _logger.Info("Update package extracted successfully"); + if (OsInfo.IsMono && _configFileProvider.UpdateMechanism == UpdateMechanism.Script) + { + InstallUpdateWithScript(updateSandboxFolder); + return; + } + _logger.Info("Preparing client"); _diskProvider.MoveFolder(_appFolderInfo.GetUpdateClientFolder(), updateSandboxFolder); _logger.Info("Starting update client {0}", _appFolderInfo.GetUpdateClientExePath()); - _logger.ProgressInfo("NzbDrone will restart shortly."); - _processProvider.Start(_appFolderInfo.GetUpdateClientExePath(), _processProvider.GetCurrentProcess().Id.ToString()); + _processProvider.Start(_appFolderInfo.GetUpdateClientExePath(), GetUpdaterArgs(updateSandboxFolder)); } catch (Exception ex) { @@ -79,6 +109,36 @@ namespace NzbDrone.Core.Update } } + private void InstallUpdateWithScript(String updateSandboxFolder) + { + var scriptPath = _configFileProvider.UpdateScriptPath; + + if (scriptPath.IsNullOrWhiteSpace()) + { + throw new ArgumentException("Update Script has not been defined"); + } + + if (!_diskProvider.FileExists(scriptPath, true)) + { + var message = String.Format("Update Script: '{0}' does not exist", scriptPath); + throw new FileNotFoundException(message, scriptPath); + } + + _logger.Info("Removing NzbDrone.Update"); + _diskProvider.DeleteFolder(_appFolderInfo.GetUpdateClientFolder(), true); + + _logger.ProgressInfo("Starting update script: {0}", _configFileProvider.UpdateScriptPath); + _processProvider.Start(scriptPath, GetUpdaterArgs(updateSandboxFolder.WrapInQuotes())); + } + + private string GetUpdaterArgs(string updateSandboxFolder) + { + var processId = _processProvider.GetCurrentProcess().Id.ToString(); + var executingApplication = _runtimeInfo.ExecutingApplication; + + return String.Join(" ", processId, updateSandboxFolder.WrapInQuotes(), executingApplication.WrapInQuotes()); + } + public void Execute(ApplicationUpdateCommand message) { _logger.ProgressDebug("Checking for updates"); diff --git a/src/NzbDrone.Core/Update/UpdateCheckService.cs b/src/NzbDrone.Core/Update/UpdateCheckService.cs index 57cd97905..b7e787895 100644 --- a/src/NzbDrone.Core/Update/UpdateCheckService.cs +++ b/src/NzbDrone.Core/Update/UpdateCheckService.cs @@ -18,7 +18,9 @@ namespace NzbDrone.Core.Update private readonly Logger _logger; - public CheckUpdateService(IUpdatePackageProvider updatePackageProvider, IConfigFileProvider configFileProvider, Logger logger) + public CheckUpdateService(IUpdatePackageProvider updatePackageProvider, + IConfigFileProvider configFileProvider, + Logger logger) { _updatePackageProvider = updatePackageProvider; _configFileProvider = configFileProvider; @@ -27,7 +29,10 @@ namespace NzbDrone.Core.Update public UpdatePackage AvailableUpdate() { - if (OsInfo.IsMono) return null; + if (OsInfo.IsMono && !_configFileProvider.UpdateAutomatically) + { + return null; + } var latestAvailable = _updatePackageProvider.GetLatestUpdate(_configFileProvider.Branch, BuildInfo.Version); diff --git a/src/NzbDrone.Core/Update/UpdateMechanism.cs b/src/NzbDrone.Core/Update/UpdateMechanism.cs new file mode 100644 index 000000000..8b647a1e7 --- /dev/null +++ b/src/NzbDrone.Core/Update/UpdateMechanism.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.Update +{ + public enum UpdateMechanism + { + BuiltIn = 0, + Script = 1 + } +} diff --git a/src/NzbDrone.Core/Update/UpdatePackage.cs b/src/NzbDrone.Core/Update/UpdatePackage.cs index a7ed63b5a..94ffa1fd0 100644 --- a/src/NzbDrone.Core/Update/UpdatePackage.cs +++ b/src/NzbDrone.Core/Update/UpdatePackage.cs @@ -11,5 +11,6 @@ namespace NzbDrone.Core.Update public String FileName { get; set; } public String Url { get; set; } public UpdateChanges Changes { get; set; } + public String Hash { get; set; } } } diff --git a/src/NzbDrone.Core/Update/UpdateVerification.cs b/src/NzbDrone.Core/Update/UpdateVerification.cs new file mode 100644 index 000000000..bafbb4e5d --- /dev/null +++ b/src/NzbDrone.Core/Update/UpdateVerification.cs @@ -0,0 +1,30 @@ +using System; +using NzbDrone.Common.Disk; + +namespace NzbDrone.Core.Update +{ + public interface IVerifyUpdates + { + Boolean Verify(UpdatePackage updatePackage, String packagePath); + } + + public class UpdateVerification : IVerifyUpdates + { + private readonly IDiskProvider _diskProvider; + + public UpdateVerification(IDiskProvider diskProvider) + { + _diskProvider = diskProvider; + } + + public Boolean Verify(UpdatePackage updatePackage, String packagePath) + { + using (var fileStream = _diskProvider.StreamFile(packagePath)) + { + var hash = fileStream.SHA256Hash(); + + return hash.Equals(updatePackage.Hash, StringComparison.CurrentCultureIgnoreCase); + } + } + } +} diff --git a/src/NzbDrone.Core/Update/UpdateVerificationFailedException.cs b/src/NzbDrone.Core/Update/UpdateVerificationFailedException.cs new file mode 100644 index 000000000..56d9da122 --- /dev/null +++ b/src/NzbDrone.Core/Update/UpdateVerificationFailedException.cs @@ -0,0 +1,15 @@ +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Update +{ + public class UpdateVerificationFailedException : NzbDroneException + { + public UpdateVerificationFailedException(string message, params object[] args) : base(message, args) + { + } + + public UpdateVerificationFailedException(string message) : base(message) + { + } + } +} diff --git a/src/NzbDrone.Host/NzbDrone.Host.csproj b/src/NzbDrone.Host/NzbDrone.Host.csproj index 861ae81e9..8aba54433 100644 --- a/src/NzbDrone.Host/NzbDrone.Host.csproj +++ b/src/NzbDrone.Host/NzbDrone.Host.csproj @@ -101,6 +101,7 @@ <Compile Include="AccessControl\FirewallAdapter.cs" /> <Compile Include="AccessControl\UrlAclAdapter.cs" /> <Compile Include="BrowserService.cs" /> + <Compile Include="Owin\MiddleWare\NzbDroneVersionMiddleWare.cs" /> <Compile Include="SpinService.cs" /> <Compile Include="SingleInstancePolicy.cs" /> <Compile Include="IUserAlert.cs" /> diff --git a/src/NzbDrone.Host/Owin/MiddleWare/NancyMiddleWare.cs b/src/NzbDrone.Host/Owin/MiddleWare/NancyMiddleWare.cs index 9e74c1e71..c6116a46a 100644 --- a/src/NzbDrone.Host/Owin/MiddleWare/NancyMiddleWare.cs +++ b/src/NzbDrone.Host/Owin/MiddleWare/NancyMiddleWare.cs @@ -13,7 +13,7 @@ namespace NzbDrone.Host.Owin.MiddleWare _nancyBootstrapper = nancyBootstrapper; } - public int Order { get { return 1; } } + public int Order { get { return 2; } } public void Attach(IAppBuilder appBuilder) { diff --git a/src/NzbDrone.Host/Owin/MiddleWare/NzbDroneVersionMiddleWare.cs b/src/NzbDrone.Host/Owin/MiddleWare/NzbDroneVersionMiddleWare.cs new file mode 100644 index 000000000..a7a76644a --- /dev/null +++ b/src/NzbDrone.Host/Owin/MiddleWare/NzbDroneVersionMiddleWare.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using Microsoft.Owin; +using NzbDrone.Common.EnvironmentInfo; +using Owin; + +namespace NzbDrone.Host.Owin.MiddleWare +{ + public class NzbDroneVersionMiddleWare : IOwinMiddleWare + { + public int Order { get { return 0; } } + + public void Attach(IAppBuilder appBuilder) + { + appBuilder.Use(typeof (AddApplicationVersionHeader)); + } + } + + public class AddApplicationVersionHeader : OwinMiddleware + { + public AddApplicationVersionHeader(OwinMiddleware next) + : base(next) + { + } + + public override Task Invoke(OwinRequest request, OwinResponse response) + { + response.AddHeader("X-ApplicationVersion", BuildInfo.Version.ToString()); + + return Next.Invoke(request, response); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs b/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs index 35499eacd..395ff6f2f 100644 --- a/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs +++ b/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Host.Owin.MiddleWare { public class SignalRMiddleWare : IOwinMiddleWare { - public int Order { get { return 0; } } + public int Order { get { return 1; } } public SignalRMiddleWare(IContainer container) { diff --git a/src/NzbDrone.Host/PriorityMonitor.cs b/src/NzbDrone.Host/PriorityMonitor.cs index 72eda2ac1..2972024dd 100644 --- a/src/NzbDrone.Host/PriorityMonitor.cs +++ b/src/NzbDrone.Host/PriorityMonitor.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Threading; using NLog; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Processes; namespace NzbDrone.Host @@ -36,7 +37,15 @@ namespace NzbDrone.Host } catch (Exception e) { - _logger.WarnException("Unable to verify priority", e); + if (OsInfo.IsMono) + { + _logger.TraceException("Unable to verify priority", e); + } + + else + { + _logger.WarnException("Unable to verify priority", e); + } } } } diff --git a/src/NzbDrone.Host/SingleInstancePolicy.cs b/src/NzbDrone.Host/SingleInstancePolicy.cs index 2952997cb..a1b489705 100644 --- a/src/NzbDrone.Host/SingleInstancePolicy.cs +++ b/src/NzbDrone.Host/SingleInstancePolicy.cs @@ -16,17 +16,14 @@ namespace NzbDrone.Host { private readonly IProcessProvider _processProvider; private readonly IBrowserService _browserService; - private readonly INzbDroneProcessProvider _nzbDroneProcessProvider; private readonly Logger _logger; public SingleInstancePolicy(IProcessProvider processProvider, IBrowserService browserService, - INzbDroneProcessProvider nzbDroneProcessProvider, Logger logger) { _processProvider = processProvider; _browserService = browserService; - _nzbDroneProcessProvider = nzbDroneProcessProvider; _logger = logger; } @@ -56,10 +53,11 @@ namespace NzbDrone.Host private List<int> GetOtherNzbDroneProcessIds() { var currentId = _processProvider.GetCurrentProcess().Id; - var otherProcesses = _nzbDroneProcessProvider.FindNzbDroneProcesses() - .Select(c => c.Id) - .Except(new[] {currentId}) - .ToList(); + var otherProcesses = _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME) + .Union(_processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME)) + .Select(c => c.Id) + .Except(new[] {currentId}) + .ToList(); if (otherProcesses.Any()) { diff --git a/src/NzbDrone.Mono.Test/DiskProviderTests/DiskProviderFixture.cs b/src/NzbDrone.Mono.Test/DiskProviderTests/DiskProviderFixture.cs index dffd16153..542cf6d58 100644 --- a/src/NzbDrone.Mono.Test/DiskProviderTests/DiskProviderFixture.cs +++ b/src/NzbDrone.Mono.Test/DiskProviderTests/DiskProviderFixture.cs @@ -4,6 +4,7 @@ using NzbDrone.Common.Test.DiskProviderTests; namespace NzbDrone.Mono.Test.DiskProviderTests { [TestFixture] + [Platform("Mono")] public class DiskProviderFixture : DiskProviderFixtureBase<DiskProvider> { public DiskProviderFixture() diff --git a/src/NzbDrone.Mono.Test/DiskProviderTests/FreeSpaceFixture.cs b/src/NzbDrone.Mono.Test/DiskProviderTests/FreeSpaceFixture.cs index 109fb4825..768fb73fa 100644 --- a/src/NzbDrone.Mono.Test/DiskProviderTests/FreeSpaceFixture.cs +++ b/src/NzbDrone.Mono.Test/DiskProviderTests/FreeSpaceFixture.cs @@ -4,6 +4,7 @@ using NzbDrone.Common.Test.DiskProviderTests; namespace NzbDrone.Mono.Test.DiskProviderTests { [TestFixture] + [Platform("Mono")] public class FreeSpaceFixture : FreeSpaceFixtureBase<DiskProvider> { public FreeSpaceFixture() diff --git a/src/NzbDrone.Mono.Test/ServiceFactoryFixture.cs b/src/NzbDrone.Mono.Test/ServiceFactoryFixture.cs index 1d5e77f87..811edfc66 100644 --- a/src/NzbDrone.Mono.Test/ServiceFactoryFixture.cs +++ b/src/NzbDrone.Mono.Test/ServiceFactoryFixture.cs @@ -13,15 +13,13 @@ namespace NzbDrone.Mono.Test [TestFixture] public class ServiceFactoryFixture : TestBase<ServiceFactory> { - [SetUp] - public void setup() - { - Mocker.SetConstant(MainAppContainerBuilder.BuildContainer(new StartupContext())); - } - [Test] public void event_handlers_should_be_unique() { + MonoOnly(); + + Mocker.SetConstant(MainAppContainerBuilder.BuildContainer(new StartupContext())); + var handlers = Subject.BuildAll<IHandle<ApplicationShutdownRequested>>() .Select(c => c.GetType().FullName); diff --git a/src/NzbDrone.Mono/NzbDrone.Mono.csproj b/src/NzbDrone.Mono/NzbDrone.Mono.csproj index e342f9a16..91d0efeb3 100644 --- a/src/NzbDrone.Mono/NzbDrone.Mono.csproj +++ b/src/NzbDrone.Mono/NzbDrone.Mono.csproj @@ -70,7 +70,6 @@ <ItemGroup> <Compile Include="DiskProvider.cs" /> <Compile Include="LinuxPermissionsException.cs" /> - <Compile Include="NzbDroneProcessProvider.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> </ItemGroup> <ItemGroup> diff --git a/src/NzbDrone.Mono/NzbDroneProcessProvider.cs b/src/NzbDrone.Mono/NzbDroneProcessProvider.cs deleted file mode 100644 index 1804c077d..000000000 --- a/src/NzbDrone.Mono/NzbDroneProcessProvider.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Common.Model; -using NzbDrone.Common.Processes; - -namespace NzbDrone.Mono -{ - public class NzbDroneProcessProvider : INzbDroneProcessProvider - { - private readonly IProcessProvider _processProvider; - private readonly Logger _logger; - - public NzbDroneProcessProvider(IProcessProvider processProvider, Logger logger) - { - _processProvider = processProvider; - _logger = logger; - } - - public List<ProcessInfo> FindNzbDroneProcesses() - { - var monoProcesses = _processProvider.FindProcessByName("mono"); - - return monoProcesses.Where(c => - { - try - { - var processArgs = _processProvider.StartAndCapture("ps", String.Format("-p {0} -o args=", c.Id)); - - return processArgs.Standard.Any(p => p.Contains(ProcessProvider.NZB_DRONE_PROCESS_NAME + ".exe") || - p.Contains(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME + ".exe")); - } - catch (InvalidOperationException ex) - { - _logger.WarnException("Error getting process arguments", ex); - return false; - } - - }).ToList(); - } - } -} diff --git a/src/NzbDrone.Test.Common/ExceptionVerification.cs b/src/NzbDrone.Test.Common/ExceptionVerification.cs index 6d98c912f..79bb6c2c7 100644 --- a/src/NzbDrone.Test.Common/ExceptionVerification.cs +++ b/src/NzbDrone.Test.Common/ExceptionVerification.cs @@ -26,7 +26,7 @@ namespace NzbDrone.Test.Common _logs = new List<LogEventInfo>(); } - public static void AssertNoUnexcpectedLogs() + public static void AssertNoUnexpectedLogs() { ExpectedFatals(0); ExpectedErrors(0); diff --git a/src/NzbDrone.Test.Common/LoggingTest.cs b/src/NzbDrone.Test.Common/LoggingTest.cs index 4d11d696a..e706d3d04 100644 --- a/src/NzbDrone.Test.Common/LoggingTest.cs +++ b/src/NzbDrone.Test.Common/LoggingTest.cs @@ -21,7 +21,7 @@ namespace NzbDrone.Test.Common LogManager.Configuration = new LoggingConfiguration(); var consoleTarget = new ConsoleTarget { Layout = "${level}: ${message} ${exception}" }; LogManager.Configuration.AddTarget(consoleTarget.GetType().Name, consoleTarget); - LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Info, consoleTarget)); + LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Debug, consoleTarget)); RegisterExceptionVerification(); } @@ -50,7 +50,7 @@ namespace NzbDrone.Test.Common //https://bugs.launchpad.net/nunitv2/+bug/1076932 if (BuildInfo.IsDebug && TestContext.CurrentContext.Result.State == TestState.Success) { - ExceptionVerification.AssertNoUnexcpectedLogs(); + ExceptionVerification.AssertNoUnexpectedLogs(); } } } diff --git a/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj b/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj index 2274be620..e871e8478 100644 --- a/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj +++ b/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj @@ -83,7 +83,7 @@ <Compile Include="LoggingTest.cs" /> <Compile Include="MockerExtensions.cs" /> <Compile Include="NzbDroneRunner.cs" /> - <Compile Include="ObjectExtentions.cs" /> + <Compile Include="ObjectExtensions.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="ReflectionExtensions.cs" /> <Compile Include="StringExtensions.cs" /> diff --git a/src/NzbDrone.Test.Common/ObjectExtentions.cs b/src/NzbDrone.Test.Common/ObjectExtensions.cs similarity index 85% rename from src/NzbDrone.Test.Common/ObjectExtentions.cs rename to src/NzbDrone.Test.Common/ObjectExtensions.cs index 40667ef8d..702797269 100644 --- a/src/NzbDrone.Test.Common/ObjectExtentions.cs +++ b/src/NzbDrone.Test.Common/ObjectExtensions.cs @@ -2,7 +2,7 @@ namespace NzbDrone.Test.Common { - public static class ObjectExtentions + public static class ObjectExtensions { public static T JsonClone<T>(this T source) where T : new() { diff --git a/src/NzbDrone.Test.Common/TestBase.cs b/src/NzbDrone.Test.Common/TestBase.cs index 403b52fec..b04231d57 100644 --- a/src/NzbDrone.Test.Common/TestBase.cs +++ b/src/NzbDrone.Test.Common/TestBase.cs @@ -109,9 +109,15 @@ namespace NzbDrone.Test.Common try { - if (Directory.Exists(TempFolder)) + var tempFolder = new DirectoryInfo(TempFolder); + if (tempFolder.Exists) { - Directory.Delete(TempFolder, true); + foreach (var file in tempFolder.GetFiles("*", SearchOption.AllDirectories)) + { + file.IsReadOnly = false; + } + + tempFolder.Delete(true); } } catch (Exception) @@ -119,7 +125,6 @@ namespace NzbDrone.Test.Common } } - protected IAppFolderInfo TestFolderInfo { get; private set; } protected void WindowsOnly() @@ -148,26 +153,11 @@ namespace NzbDrone.Test.Common TestFolderInfo = Mocker.GetMock<IAppFolderInfo>().Object; } - protected string GetTestFilePath(string fileName) + protected string GetTempFilePath() { - return Path.Combine(SandboxFolder, fileName); + return Path.Combine(TempFolder, Path.GetRandomFileName()); } - protected string GetTestFilePath() - { - return GetTestFilePath(Path.GetRandomFileName()); - } - - protected string SandboxFolder - { - get - { - var folder = Path.Combine(Directory.GetCurrentDirectory(), "Files"); - Directory.CreateDirectory(folder); - return folder; - } - - } protected void VerifyEventPublished<TEvent>() where TEvent : class, IEvent { VerifyEventPublished<TEvent>(Times.Once()); diff --git a/src/NzbDrone.Test.Dummy/DummyApp.cs b/src/NzbDrone.Test.Dummy/DummyApp.cs index 87b04f712..5176dab6f 100644 --- a/src/NzbDrone.Test.Dummy/DummyApp.cs +++ b/src/NzbDrone.Test.Dummy/DummyApp.cs @@ -9,7 +9,9 @@ namespace NzbDrone.Test.Dummy static void Main(string[] args) { - Console.WriteLine("Dummy process. ID:{0} Path:{1}", Process.GetCurrentProcess().Id, Process.GetCurrentProcess().MainModule.FileName); + var process = Process.GetCurrentProcess(); + + Console.WriteLine("Dummy process. ID:{0} Name:{1} Path:{2}", process.Id, process.ProcessName, process.MainModule.FileName); Console.ReadLine(); } } diff --git a/src/NzbDrone.Update.Test/InstallUpdateServiceFixture.cs b/src/NzbDrone.Update.Test/InstallUpdateServiceFixture.cs index 2e240decd..6d9a95687 100644 --- a/src/NzbDrone.Update.Test/InstallUpdateServiceFixture.cs +++ b/src/NzbDrone.Update.Test/InstallUpdateServiceFixture.cs @@ -2,9 +2,9 @@ using System.IO; using FluentAssertions; using NUnit.Framework; -using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Processes; using NzbDrone.Test.Common; using NzbDrone.Update.UpdateEngine; @@ -13,11 +13,28 @@ namespace NzbDrone.Update.Test [TestFixture] public class InstallUpdateServiceFixture : TestBase<InstallUpdateService> { + private string _targetFolder = @"C:\NzbDrone\".AsOsAgnostic(); + private const int _processId = 12; + [SetUp] public void Setup() { Mocker.GetMock<IAppFolderInfo>() - .Setup(c => c.TempFolder).Returns(@"C:\Temp\"); + .Setup(c => c.TempFolder).Returns(@"C:\Temp\"); + } + + private void GivenTargetFolderExists() + { + Mocker.GetMock<IDiskProvider>() + .Setup(c => c.FolderExists(_targetFolder)) + .Returns(true); + } + + private void GivenProcessExists() + { + Mocker.GetMock<IProcessProvider>() + .Setup(c => c.Exists(_processId)) + .Returns(true); } [TestCase(null)] @@ -25,16 +42,14 @@ namespace NzbDrone.Update.Test [TestCase(" ")] public void update_should_throw_target_folder_is_blank(string target) { - Assert.Throws<ArgumentException>(() => Subject.Start(target)) + Assert.Throws<ArgumentException>(() => Subject.Start(target, _processId)) .Message.Should().StartWith("Target folder can not be null or empty"); } [Test] public void update_should_throw_if_target_folder_doesnt_exist() { - string targetFolder = "c:\\NzbDrone\\"; - - Assert.Throws<DirectoryNotFoundException>(() => Subject.Start(targetFolder)) + Assert.Throws<DirectoryNotFoundException>(() => Subject.Start(_targetFolder, _processId)) .Message.Should().StartWith("Target folder doesn't exist"); } @@ -42,18 +57,34 @@ namespace NzbDrone.Update.Test public void update_should_throw_if_update_folder_doesnt_exist() { const string sandboxFolder = @"C:\Temp\NzbDrone_update\nzbdrone"; - const string targetFolder = "c:\\NzbDrone\\"; - Mocker.GetMock<IDiskProvider>() - .Setup(c => c.FolderExists(targetFolder)) - .Returns(true); + GivenTargetFolderExists(); + GivenProcessExists(); Mocker.GetMock<IDiskProvider>() .Setup(c => c.FolderExists(sandboxFolder)) .Returns(false); - Assert.Throws<DirectoryNotFoundException>(() => Subject.Start(targetFolder)) + Assert.Throws<DirectoryNotFoundException>(() => Subject.Start(_targetFolder, _processId)) .Message.Should().StartWith("Update folder doesn't exist"); } + + [Test] + public void update_should_throw_if_process_is_zero() + { + GivenTargetFolderExists(); + + Assert.Throws<ArgumentException>(() => Subject.Start(_targetFolder, 0)) + .Message.Should().StartWith("Invalid process ID"); + } + + [Test] + public void update_should_throw_if_process_id_doesnt_exist() + { + GivenTargetFolderExists(); + + Assert.Throws<ArgumentException>(() => Subject.Start(_targetFolder, _processId)) + .Message.Should().StartWith("Process with ID doesn't exist"); + } } } diff --git a/src/NzbDrone.Update.Test/ProgramFixture.cs b/src/NzbDrone.Update.Test/ProgramFixture.cs index 5be4db66e..b4cc3049a 100644 --- a/src/NzbDrone.Update.Test/ProgramFixture.cs +++ b/src/NzbDrone.Update.Test/ProgramFixture.cs @@ -43,7 +43,7 @@ namespace NzbDrone.Update.Test Subject.Start(new[] { "12", "" }); - Mocker.GetMock<IInstallUpdateService>().Verify(c => c.Start(@"C:\NzbDrone"), Times.Once()); + Mocker.GetMock<IInstallUpdateService>().Verify(c => c.Start(@"C:\NzbDrone", 12), Times.Once()); } diff --git a/src/NzbDrone.Update/NzbDrone.Update.csproj b/src/NzbDrone.Update/NzbDrone.Update.csproj index 0e85877ba..399a39203 100644 --- a/src/NzbDrone.Update/NzbDrone.Update.csproj +++ b/src/NzbDrone.Update/NzbDrone.Update.csproj @@ -51,6 +51,7 @@ <Link>Properties\SharedAssemblyInfo.cs</Link> </Compile> <Compile Include="AppType.cs" /> + <Compile Include="UpdateStartupContext.cs" /> <Compile Include="UpdateApp.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="UpdateContainerBuilder.cs" /> diff --git a/src/NzbDrone.Update/UpdateApp.cs b/src/NzbDrone.Update/UpdateApp.cs index 35d7febca..582efd936 100644 --- a/src/NzbDrone.Update/UpdateApp.cs +++ b/src/NzbDrone.Update/UpdateApp.cs @@ -1,6 +1,8 @@ using System; using System.IO; +using System.Linq; using NLog; +using NzbDrone.Common; using NzbDrone.Common.Composition; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation; @@ -50,24 +52,60 @@ namespace NzbDrone.Update public void Start(string[] args) { - var processId = ParseProcessId(args); + var startupContext = ParseArgs(args); + string targetFolder; - var exeFileInfo = new FileInfo(_processProvider.GetProcessById(processId).StartPath); - var targetFolder = exeFileInfo.Directory.FullName; - - logger.Info("Starting update process. Target Path:{0}", targetFolder); - _installUpdateService.Start(targetFolder); - } - - private int ParseProcessId(string[] args) - { - int id; - if (args == null || !Int32.TryParse(args[0], out id) || id <= 0) + if (startupContext.ExecutingApplication.IsNullOrWhiteSpace()) { - throw new ArgumentOutOfRangeException("args", "Invalid process ID"); + var exeFileInfo = new FileInfo(_processProvider.GetProcessById(startupContext.ProcessId).StartPath); + targetFolder = exeFileInfo.Directory.FullName; } - logger.Debug("NzbDrone processId:{0}", id); + else + { + var exeFileInfo = new FileInfo(startupContext.ExecutingApplication); + targetFolder = exeFileInfo.Directory.FullName; + } + + logger.Info("Starting update process. Target Path:{0}", targetFolder); + _installUpdateService.Start(targetFolder, startupContext.ProcessId); + } + + private UpdateStartupContext ParseArgs(string[] args) + { + if (args == null || !args.Any()) + { + throw new ArgumentOutOfRangeException("args", "args must be specified"); + } + + var startupContext = new UpdateStartupContext + { + ProcessId = ParseProcessId(args[0]) + }; + + if (args.Count() == 1) + { + return startupContext; + } + + if (args.Count() >= 3) + { + startupContext.UpdateLocation = args[1]; + startupContext.ExecutingApplication = args[2]; + } + + return startupContext; + } + + private int ParseProcessId(string arg) + { + int id; + if (!Int32.TryParse(arg, out id) || id <= 0) + { + throw new ArgumentOutOfRangeException("arg", "Invalid process ID"); + } + + logger.Debug("NzbDrone process ID: {0}", id); return id; } } diff --git a/src/NzbDrone.Update/UpdateEngine/DetectApplicationType.cs b/src/NzbDrone.Update/UpdateEngine/DetectApplicationType.cs index ec9a80a96..aeed37c45 100644 --- a/src/NzbDrone.Update/UpdateEngine/DetectApplicationType.cs +++ b/src/NzbDrone.Update/UpdateEngine/DetectApplicationType.cs @@ -1,4 +1,5 @@ using NzbDrone.Common; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Processes; namespace NzbDrone.Update.UpdateEngine @@ -21,6 +22,12 @@ namespace NzbDrone.Update.UpdateEngine public AppType GetAppType() { + if (OsInfo.IsMono) + { + //Tehcnically its the console, but its been renamed for mono (Linux/OS X) + return AppType.Normal; + } + if (_serviceProvider.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME) && _serviceProvider.IsServiceRunning(ServiceProvider.NZBDRONE_SERVICE_NAME)) { diff --git a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs index f1b731e34..3109372ad 100644 --- a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs +++ b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs @@ -4,12 +4,13 @@ using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Processes; namespace NzbDrone.Update.UpdateEngine { public interface IInstallUpdateService { - void Start(string installationFolder); + void Start(string installationFolder, int processId); } public class InstallUpdateService : IInstallUpdateService @@ -21,6 +22,7 @@ namespace NzbDrone.Update.UpdateEngine private readonly IBackupAndRestore _backupAndRestore; private readonly IBackupAppData _backupAppData; private readonly IStartNzbDrone _startNzbDrone; + private readonly IProcessProvider _processProvider; private readonly Logger _logger; public InstallUpdateService(IDiskProvider diskProvider, @@ -30,6 +32,7 @@ namespace NzbDrone.Update.UpdateEngine IBackupAndRestore backupAndRestore, IBackupAppData backupAppData, IStartNzbDrone startNzbDrone, + IProcessProvider processProvider, Logger logger) { _diskProvider = diskProvider; @@ -39,10 +42,11 @@ namespace NzbDrone.Update.UpdateEngine _backupAndRestore = backupAndRestore; _backupAppData = backupAppData; _startNzbDrone = startNzbDrone; + _processProvider = processProvider; _logger = logger; } - private void Verify(string targetFolder) + private void Verify(string targetFolder, int processId) { _logger.Info("Verifying requirements before update..."); @@ -52,20 +56,30 @@ namespace NzbDrone.Update.UpdateEngine if (!_diskProvider.FolderExists(targetFolder)) throw new DirectoryNotFoundException("Target folder doesn't exist " + targetFolder); + if (processId < 1) + { + throw new ArgumentException("Invalid process ID: " + processId); + } + + if (!_processProvider.Exists(processId)) + { + throw new ArgumentException("Process with ID doesn't exist " + processId); + } + _logger.Info("Verifying Update Folder"); if (!_diskProvider.FolderExists(_appFolderInfo.GetUpdatePackageFolder())) throw new DirectoryNotFoundException("Update folder doesn't exist " + _appFolderInfo.GetUpdatePackageFolder()); } - public void Start(string installationFolder) + public void Start(string installationFolder, int processId) { - Verify(installationFolder); + Verify(installationFolder, processId); var appType = _detectApplicationType.GetAppType(); try { - _terminateNzbDrone.Terminate(); + _terminateNzbDrone.Terminate(processId); _backupAndRestore.Backup(installationFolder); _backupAppData.Backup(); @@ -82,7 +96,6 @@ namespace NzbDrone.Update.UpdateEngine _backupAndRestore.Restore(installationFolder); _logger.FatalException("Failed to copy upgrade package to target folder.", e); } - } finally { diff --git a/src/NzbDrone.Update/UpdateEngine/StartNzbDrone.cs b/src/NzbDrone.Update/UpdateEngine/StartNzbDrone.cs index 63079033a..94ddc8877 100644 --- a/src/NzbDrone.Update/UpdateEngine/StartNzbDrone.cs +++ b/src/NzbDrone.Update/UpdateEngine/StartNzbDrone.cs @@ -73,7 +73,7 @@ namespace NzbDrone.Update.UpdateEngine _logger.Info("Starting {0}", fileName); var path = Path.Combine(installationFolder, fileName); - _processProvider.SpawnNewProcess(path, StartupContext.NO_BROWSER); + _processProvider.SpawnNewProcess(path, "--" + StartupContext.NO_BROWSER); } } } \ No newline at end of file diff --git a/src/NzbDrone.Update/UpdateEngine/TerminateNzbDrone.cs b/src/NzbDrone.Update/UpdateEngine/TerminateNzbDrone.cs index 329c9555e..62d56b2d6 100644 --- a/src/NzbDrone.Update/UpdateEngine/TerminateNzbDrone.cs +++ b/src/NzbDrone.Update/UpdateEngine/TerminateNzbDrone.cs @@ -1,6 +1,7 @@ using System; using NLog; using NzbDrone.Common; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Processes; using IServiceProvider = NzbDrone.Common.IServiceProvider; @@ -8,7 +9,7 @@ namespace NzbDrone.Update.UpdateEngine { public interface ITerminateNzbDrone { - void Terminate(); + void Terminate(int processId); } public class TerminateNzbDrone : ITerminateNzbDrone @@ -24,8 +25,18 @@ namespace NzbDrone.Update.UpdateEngine _logger = logger; } - public void Terminate() + public void Terminate(int processId) { + if (OsInfo.IsMono) + { + _logger.Info("Stopping all instances"); + _processProvider.Kill(processId); + _processProvider.KillAll(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME); + _processProvider.KillAll(ProcessProvider.NZB_DRONE_PROCESS_NAME); + + return; + } + _logger.Info("Stopping all running services"); if (_serviceProvider.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME) @@ -35,7 +46,6 @@ namespace NzbDrone.Update.UpdateEngine { _logger.Info("NzbDrone Service is installed and running"); _serviceProvider.Stop(ServiceProvider.NZBDRONE_SERVICE_NAME); - } catch (Exception e) { diff --git a/src/NzbDrone.Update/UpdateStartupContext.cs b/src/NzbDrone.Update/UpdateStartupContext.cs new file mode 100644 index 000000000..51b6bf103 --- /dev/null +++ b/src/NzbDrone.Update/UpdateStartupContext.cs @@ -0,0 +1,11 @@ +using System; + +namespace NzbDrone.Update +{ + public class UpdateStartupContext + { + public Int32 ProcessId { get; set; } + public String ExecutingApplication { get; set; } + public String UpdateLocation { get; set; } + } +} diff --git a/src/NzbDrone.Windows.Test/DiskProviderTests/DiskProviderFixture.cs b/src/NzbDrone.Windows.Test/DiskProviderTests/DiskProviderFixture.cs index 68b2d1f0c..f81c379f1 100644 --- a/src/NzbDrone.Windows.Test/DiskProviderTests/DiskProviderFixture.cs +++ b/src/NzbDrone.Windows.Test/DiskProviderTests/DiskProviderFixture.cs @@ -4,6 +4,7 @@ using NzbDrone.Common.Test.DiskProviderTests; namespace NzbDrone.Windows.Test.DiskProviderTests { [TestFixture] + [Platform("Win")] public class DiskProviderFixture : DiskProviderFixtureBase<DiskProvider> { public DiskProviderFixture() diff --git a/src/NzbDrone.Windows.Test/DiskProviderTests/FreeSpaceFixture.cs b/src/NzbDrone.Windows.Test/DiskProviderTests/FreeSpaceFixture.cs index a642b49a9..0334a6d3c 100644 --- a/src/NzbDrone.Windows.Test/DiskProviderTests/FreeSpaceFixture.cs +++ b/src/NzbDrone.Windows.Test/DiskProviderTests/FreeSpaceFixture.cs @@ -4,6 +4,7 @@ using NzbDrone.Common.Test.DiskProviderTests; namespace NzbDrone.Windows.Test.DiskProviderTests { [TestFixture] + [Platform("Win")] public class FreeSpaceFixture : FreeSpaceFixtureBase<DiskProvider> { public FreeSpaceFixture() diff --git a/src/NzbDrone.Windows/NzbDrone.Windows.csproj b/src/NzbDrone.Windows/NzbDrone.Windows.csproj index 77e47f03c..607f09a4e 100644 --- a/src/NzbDrone.Windows/NzbDrone.Windows.csproj +++ b/src/NzbDrone.Windows/NzbDrone.Windows.csproj @@ -63,7 +63,6 @@ </ItemGroup> <ItemGroup> <Compile Include="DiskProvider.cs" /> - <Compile Include="NzbDroneProcessProvider.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> </ItemGroup> <ItemGroup> diff --git a/src/NzbDrone.Windows/NzbDroneProcessProvider.cs b/src/NzbDrone.Windows/NzbDroneProcessProvider.cs deleted file mode 100644 index 1e18b6c50..000000000 --- a/src/NzbDrone.Windows/NzbDroneProcessProvider.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Common.Model; -using NzbDrone.Common.Processes; - -namespace NzbDrone.Windows -{ - public class NzbDroneProcessProvider : INzbDroneProcessProvider - { - private readonly IProcessProvider _processProvider; - - public NzbDroneProcessProvider(IProcessProvider processProvider) - { - _processProvider = processProvider; - } - - public List<ProcessInfo> FindNzbDroneProcesses() - { - var consoleProcesses = _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME); - var winformProcesses = _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME); - - return consoleProcesses.Concat(winformProcesses).ToList(); - } - } -} diff --git a/src/NzbDrone.sln.DotSettings b/src/NzbDrone.sln.DotSettings index fdd2a0b42..be0105874 100644 --- a/src/NzbDrone.sln.DotSettings +++ b/src/NzbDrone.sln.DotSettings @@ -33,6 +33,8 @@ <s:Boolean x:Key="/Default/CodeStyle/CSharpUsing/PreferQualifiedReference/@EntryValue">False</s:Boolean> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Constants/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateConstants/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></s:String> + <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=6658173a_002Dfe71_002D4efa_002D9d9e_002Da36d4499375e/@EntryIndexedValue"><Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Public" Description="Test Methods"><ElementKinds><Kind Name="TEST_MEMBER" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="should_" Suffix="" Style="aa_bb" /></Policy></s:String> + <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=f5dc62ff_002De860_002D4dc4_002Dacef_002Dd674121c2124/@EntryIndexedValue"><Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Public" Description="Test Fixtures"><ElementKinds><Kind Name="TEST_TYPE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="Fixture" Style="AaBb" /></Policy></s:String> <s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FPARAMETER/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></s:String> <s:String x:Key="/Default/Environment/Editor/MatchingBraceHighlighting/Position/@EntryValue">BOTH_SIDES</s:String> <s:String x:Key="/Default/Environment/Editor/MatchingBraceHighlighting/Style/@EntryValue">COLOR</s:String> diff --git a/src/NzbDrone/SysTray/SysTrayApp.cs b/src/NzbDrone/SysTray/SysTrayApp.cs index 730ea346f..2b730e180 100644 --- a/src/NzbDrone/SysTray/SysTrayApp.cs +++ b/src/NzbDrone/SysTray/SysTrayApp.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Windows.Forms; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Processes; using NzbDrone.Host; namespace NzbDrone.SysTray @@ -14,13 +15,17 @@ namespace NzbDrone.SysTray public class SystemTrayApp : Form, ISystemTrayApp { private readonly IBrowserService _browserService; + private readonly IRuntimeInfo _runtimeInfo; + private readonly IProcessProvider _processProvider; private readonly NotifyIcon _trayIcon = new NotifyIcon(); private readonly ContextMenu _trayMenu = new ContextMenu(); - public SystemTrayApp(IBrowserService browserService) + public SystemTrayApp(IBrowserService browserService, IRuntimeInfo runtimeInfo, IProcessProvider processProvider) { _browserService = browserService; + _runtimeInfo = runtimeInfo; + _processProvider = processProvider; } public void Start() @@ -98,6 +103,11 @@ namespace NzbDrone.SysTray private void OnApplicationExit(object sender, EventArgs e) { + if (_runtimeInfo.RestartPending) + { + _processProvider.SpawnNewProcess(_runtimeInfo.ExecutingApplication, "--restart --nobrowser"); + } + DisposeTrayIcon(); } diff --git a/src/UI/.idea/codeStyleSettings.xml b/src/UI/.idea/codeStyleSettings.xml index 83bfe2c0c..c05f974b7 100644 --- a/src/UI/.idea/codeStyleSettings.xml +++ b/src/UI/.idea/codeStyleSettings.xml @@ -17,6 +17,7 @@ <option name="VALUE_ALIGNMENT" value="1" /> </CssCodeStyleSettings> <JSCodeStyleSettings> + <option name="SPACE_BEFORE_PROPERTY_COLON" value="true" /> <option name="ALIGN_OBJECT_PROPERTIES" value="2" /> </JSCodeStyleSettings> <XML> @@ -33,8 +34,7 @@ <option name="ELSE_ON_NEW_LINE" value="true" /> <option name="CATCH_ON_NEW_LINE" value="true" /> <option name="FINALLY_ON_NEW_LINE" value="true" /> - <option name="ALIGN_MULTILINE_PARAMETERS" value="false" /> - <option name="SPACE_AFTER_COLON" value="false" /> + <option name="SPACE_BEFORE_METHOD_PARENTHESES" value="true" /> <option name="METHOD_PARAMETERS_WRAP" value="5" /> <option name="ARRAY_INITIALIZER_WRAP" value="2" /> <option name="IF_BRACE_FORCE" value="3" /> diff --git a/src/UI/AddSeries/AddSeriesLayoutTemplate.html b/src/UI/AddSeries/AddSeriesLayoutTemplate.html index b9ccc6c17..401847f7f 100644 --- a/src/UI/AddSeries/AddSeriesLayoutTemplate.html +++ b/src/UI/AddSeries/AddSeriesLayoutTemplate.html @@ -1,23 +1,17 @@ -<div class="row operations-row"> - <div class="btn-group btn-block"> - <div class="btn btn-large add-series-import-btn x-import"> - <i class="icon-hdd"/> - Import existing series on disk +<div class="row"> + <div class="col-md-12"> + <div class="btn-group add-series-btn-group btn-group-lg btn-block"> + <button type="button" class="btn btn-default col-md-10 col-xs-8 add-series-import-btn x-import"> + <i class="icon-hdd"/> + Import existing series on disk + </button> + <button class="btn btn-default col-md-2 col-xs-4 x-add-new"><i class="icon-play hidden-xs"></i> Add new series</button> </div> - <button class="btn btn-large btn-icon-only dropdown-toggle" data-toggle="dropdown"> - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - <li class="add-new x-add-new"> - Add new series - </li> - </ul> </div> - - <!--<div class="btn btn-block btn-large add-series-import-btn x-import">--> - <!--<i class="icon-hdd"/>--> - <!--Import existing series on disk--> - <!--</div>--> </div> -<div id="add-series-workspace"/> +<div class="row"> + <div class="col-md-12"> + <div id="add-series-workspace"></div> + </div> +</div> diff --git a/src/UI/AddSeries/AddSeriesViewTemplate.html b/src/UI/AddSeries/AddSeriesViewTemplate.html index 8c0a87817..829527ddc 100644 --- a/src/UI/AddSeries/AddSeriesViewTemplate.html +++ b/src/UI/AddSeries/AddSeriesViewTemplate.html @@ -1,21 +1,22 @@ {{#if folder.path}} -<div class="row unmapped-folder-path"> - <div class="span11"> +<div class="unmapped-folder-path"> + <div class="col-md-12"> {{folder.path}} </div> </div>{{/if}} -<div class="row x-search-bar"> - <div class="input-prepend nz-input-large add-series-search span11"> - <i class="add-on icon-search"/> +<div class="x-search-bar"> + <div class="input-group input-group-lg add-series-search"> + <span class="input-group-addon"><i class="icon-search"/></span> + {{#if folder}} - <input type="text" class="input-block-level x-series-search" value="{{folder.name}}"> + <input type="text" class="form-control x-series-search" value="{{folder.name}}"> {{else}} - <input type="text" class="input-block-level x-series-search" placeholder="Start typing the name of series you want to add ..."> + <input type="text" class="form-control x-series-search" placeholder="Start typing the name of series you want to add ..."> {{/if}} </div> </div> <div class="row"> - <div id="search-result" class="result-list span12"/> + <div id="search-result" class="result-list col-md-12"/> </div> <div class="btn btn-block text-center new-series-loadmore x-load-more" style="display: none;"> <i class="icon-angle-down"/> diff --git a/src/UI/AddSeries/NotFoundTemplate.html b/src/UI/AddSeries/NotFoundTemplate.html index 2b0d75a9b..b4b74f16d 100644 --- a/src/UI/AddSeries/NotFoundTemplate.html +++ b/src/UI/AddSeries/NotFoundTemplate.html @@ -1,4 +1,4 @@ -<div class="text-center span12"> +<div class="text-center col-md-12"> <h3> Sorry. We couldn't find any series matching '{{term}}' </h3> diff --git a/src/UI/AddSeries/RootFolders/RootFolderItemViewTemplate.html b/src/UI/AddSeries/RootFolders/RootFolderItemViewTemplate.html index cc963b4cf..7e1ee2ea9 100644 --- a/src/UI/AddSeries/RootFolders/RootFolderItemViewTemplate.html +++ b/src/UI/AddSeries/RootFolders/RootFolderItemViewTemplate.html @@ -1,10 +1,10 @@ -<td class="span10 x-folder folder-path"> +<td class="col-md-10 x-folder folder-path"> {{path}} </td> -<td class="span3 x-folder folder-free-space"> +<td class="col-md-3 x-folder folder-free-space"> <span>{{Bytes freeSpace}}</span> </td> -<td class="span1 nz-row-action"> - <div class="btn btn-small btn-icon-only icon-nd-delete x-delete"> +<td class="col-md-1 nz-row-action"> + <div class="btn btn-sm btn-icon-only icon-nd-delete x-delete"> </div> </td> diff --git a/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html b/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html index c8ba616c5..2e16ae6e3 100644 --- a/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html +++ b/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html @@ -1,22 +1,30 @@ -<div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Select Folder</h3> -</div> -<div class="modal-body root-folders-modal"> - <div class="validation-errors"></div> - <div class="alert alert-info">Enter the path that contains some or all of your TV series, you will be able to choose which series you want to import<button type="button" class="close" data-dismiss="alert">×</button></div> - <div class="input-prepend input-append x-path control-group"> - <span class="add-on"> <i class="icon-folder-open"></i></span> - <input class="span9" type="text" validation-name="path" placeholder="Enter path to folder that contains your shows"> - <button class="btn btn-success x-add"> - <i class="icon-ok"/> - </button> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Select Folder</h3> + </div> + <div class="modal-body root-folders-modal"> + <div class="validation-errors"></div> + <div class="alert alert-info">Enter the path that contains some or all of your TV series, you will be able to choose which series you want to import<button type="button" class="close" data-dismiss="alert">×</button></div> + + <div class="input-group x-path form-group"> + <span class="input-group-addon"> <i class="icon-folder-open"></i></span> + <input class="col-md-9 form-control" type="text" validation-name="path" placeholder="Enter path to folder that contains your shows"> + <span class="input-group-btn "> + <button class="btn btn-success x-add"> + <i class="icon-ok"/> + </button> + </span> + </div> + + {{#if items}} + <h4>Recent Folders</h4> + {{/if}} + <div id="current-dirs" class="root-folders-list"></div> + </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal">close</button> + </div> </div> - {{#if items}} - <h4>Recent Folders</h4> - {{/if}} - <div id="current-dirs" class="root-folders-list"></div> -</div> -<div class="modal-footer"> - <button class="btn" data-dismiss="modal">close</button> </div> diff --git a/src/UI/AddSeries/RootFolders/RootFolderSelectionPartial.html b/src/UI/AddSeries/RootFolders/RootFolderSelectionPartial.html index 64e1e9358..2f12a3167 100644 --- a/src/UI/AddSeries/RootFolders/RootFolderSelectionPartial.html +++ b/src/UI/AddSeries/RootFolders/RootFolderSelectionPartial.html @@ -1,12 +1,11 @@ -<div> - <select class="span4 x-root-folder" validation-name="RootFolderPath"> - {{#if this}} - {{#each this}} - <option value="{{id}}">{{path}}</option> - {{/each}} - {{else}} - <option value="">Select Path</option> - {{/if}} - <option value="addNew">Add a different path</option> - </select> -</div> +<select class="col-md-4 form-control x-root-folder" validation-name="RootFolderPath"> + {{#if this}} + {{#each this}} + <option value="{{id}}">{{path}}</option> + {{/each}} + {{else}} + <option value="">Select Path</option> + {{/if}} + <option value="addNew">Add a different path</option> +</select> + diff --git a/src/UI/AddSeries/RootFolders/StartingSeasonSelectionPartial.html b/src/UI/AddSeries/RootFolders/StartingSeasonSelectionPartial.html index 82d28119c..db07cdda2 100644 --- a/src/UI/AddSeries/RootFolders/StartingSeasonSelectionPartial.html +++ b/src/UI/AddSeries/RootFolders/StartingSeasonSelectionPartial.html @@ -1,4 +1,4 @@ -<select class="starting-season x-starting-season"> +<select class="form-control md-col-2 starting-season x-starting-season"> {{#each this}} {{#if_eq seasonNumber compare="0"}} <option value="{{seasonNumber}}">Specials</option> diff --git a/src/UI/AddSeries/SearchResultViewTemplate.html b/src/UI/AddSeries/SearchResultViewTemplate.html index afe628724..d87a7e126 100644 --- a/src/UI/AddSeries/SearchResultViewTemplate.html +++ b/src/UI/AddSeries/SearchResultViewTemplate.html @@ -1,13 +1,12 @@ <div class="search-item {{#unless isExisting}}search-item-new{{/unless}}"> <div class="row"> - <div class="span2"> + <div class="col-md-2"> <a href="{{traktUrl}}" target="_blank"> <img class="new-series-poster" src="{{remotePoster}}" {{defaultImg}} > </a> </div> - <div class="span9"> - + <div class="col-md-10"> <div class="row"> <h2 class="series-title"> {{titleWithYear}} @@ -21,42 +20,51 @@ {{overview}} </div> </div> - {{#unless existing}} - <div class="row labels"> - {{#unless path}} - <div class="span4">Path</div> - {{/unless}} - - <div class="span1 starting-season starting-season-label">Starting Season</div> - <div class="span2">Quality Profile</div> - </div> - {{/unless}} <div class="row"> - <form class="form-inline"> - {{#if existing}} - <div class="btn add-series disabled pull-right"> - Already Exists - </div> - {{else}} + {{#unless existing}} {{#unless path}} - {{> RootFolderSelectionPartial rootFolders}} - {{/unless}} - - {{> StartingSeasonSelectionPartial seasons}} - {{> QualityProfileSelectionPartial qualityProfiles}} - - <label class="checkbox-button" title="Use season folders"> - <input type="checkbox" class="x-season-folder"/> - <div class="btn btn-primary btn-icon-only"> - <i class="icon-folder-close"></i> + <div class="form-group col-md-4"> + <label>Path</label> + {{> RootFolderSelectionPartial rootFolders}} </div> - </label> + {{/unless}} + <div class="form-group col-md-2"> + <label>Starting Season</label> + {{> StartingSeasonSelectionPartial seasons}} + </div> + <div class="form-group col-md-2"> + <label>Quality Profile</label> + {{> QualityProfileSelectionPartial qualityProfiles}} + </div> + <div class="form-group col-md-2"> + <label>Season Folders</label> - <span class="btn btn-success x-add add-series pull-right"> Add - <i class="icon-plus"></i> - </span> - {{/if}} - </form> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" class="x-season-folder"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + <div class="btn btn-primary slide-button"/> + </label> + </div> + </div> + <div class="form-group col-md-1 pull-right"> + <label> </label> + <button class="btn btn-success x-add add-series pull-right pull-none-xs"> Add + <i class="icon-plus"></i> + </button> + </div> + + {{else}} + <div class="col-md-1 col-md-offset-11"> + <button class="btn add-series disabled pull-right pull-none-xs"> + Already Exists + </button> + </div> + + {{/unless}} </div> </div> </div> diff --git a/src/UI/AddSeries/addSeries.less b/src/UI/AddSeries/addSeries.less index f690e6413..ee287bbac 100644 --- a/src/UI/AddSeries/addSeries.less +++ b/src/UI/AddSeries/addSeries.less @@ -2,20 +2,11 @@ @import "../Shared/Styles/clickable.less"; #add-series-screen { - - .operations-row { - margin-left : 0px; - } - .existing-series { .card(); margin : 30px 0px; - .add-series-search { - width : 970px; - } - .unmapped-folder-path { padding: 20px; margin-left : 0px; @@ -42,17 +33,6 @@ .add-series-search { margin-top : 20px; margin-bottom : 20px; - padding-left : 20px; - - *[class*='icon-'] { - font-size : 28px; - height : 30px; - width : 40px; - padding-top : 14px; - } - input { - height : 50px; - } } .search-item { @@ -101,12 +81,8 @@ } .checkbox { - width : 100px; - margin-left : 0px; - display : inline-block; - padding-top : 0px; - margin-bottom : 0px; - } + margin-top : 0px; + } .starting-season { width: 140px; @@ -116,9 +92,9 @@ } } - .labels { - [class*="span"] { - margin-left: 3px; + i { + &:before { + color: #ffffff; } } } @@ -147,16 +123,6 @@ li.add-new:hover { background-color: rgb(0, 129, 194); } -.btn-group { - .dropdown-menu { - right: 0px; - } - - .add-series-import-btn { - width: 93.3%; - } -} - .root-folders-modal { overflow: visible; diff --git a/src/UI/Calendar/CalendarFeedViewTemplate.html b/src/UI/Calendar/CalendarFeedViewTemplate.html index 4c4d8c0d9..d920089b3 100644 --- a/src/UI/Calendar/CalendarFeedViewTemplate.html +++ b/src/UI/Calendar/CalendarFeedViewTemplate.html @@ -1,29 +1,33 @@ -<div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>NzbDrone Calendar feed</h3> -</div> -<div class="modal-body edit-series-modal"> - <div class="row"> - <div> - <div class="form-horizontal"> - <div class="control-group"> - <label class="control-label">iCal feed</label> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>NzbDrone Calendar feed</h3> + </div> + <div class="modal-body edit-series-modal"> + <div class="row"> + <div> + <div class="form-horizontal"> + <div class="form-group"> + <label class="control-label">iCal feed</label> - <div class="controls ical-url"> - <div class="input-append"> - <input type="text" class="x-ical-url" value="{{icalHttpUrl}}" readonly="readonly" /> - <button class="btn btn-icon-only x-ical-copy" title="Copy to clipboard"><i class="icon-copy"></i></button> - <a class="btn btn-icon-only no-router" title="Subscribe" href="{{icalWebCalUrl}}" target="_blank"><i class="icon-calendar-empty"></i></a> + <div class="controls ical-url"> + <div class="input-group"> + <input type="text" class="x-ical-url" value="{{icalHttpUrl}}" readonly="readonly" /> + <button class="btn btn-icon-only x-ical-copy" title="Copy to clipboard"><i class="icon-copy"></i></button> + <a class="btn btn-icon-only no-router" title="Subscribe" href="{{icalWebCalUrl}}" target="_blank"><i class="icon-calendar-empty"></i></a> + </div> + <span class="help-inline"> + <i class="icon-nd-form-info" title="Copy this url into your clients subscription form or use the subscribe button if your browser support webcal"/> + </span> + </div> </div> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Copy this url into your clients subscription form or use the subscribe button if your browser support webcal"/> - </span> </div> </div> </div> </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal">close</button> + </div> </div> </div> -<div class="modal-footer"> - <button class="btn" data-dismiss="modal">close</button> -</div> \ No newline at end of file diff --git a/src/UI/Calendar/CalendarLayoutTemplate.html b/src/UI/Calendar/CalendarLayoutTemplate.html index 0df20bd10..f531ba3aa 100644 --- a/src/UI/Calendar/CalendarLayoutTemplate.html +++ b/src/UI/Calendar/CalendarLayoutTemplate.html @@ -1,5 +1,5 @@ <div class="row"> - <div class="span3"> + <div class="col-md-3 hidden-xs"> <div class="pull-left"> <h4>Upcoming</h4> </div> @@ -10,7 +10,7 @@ </div> <div id="x-upcoming"/> </div> - <div class=span9> + <div class="col-md-9 col-xs-12"> <div id="x-calendar" class="calendar"/> <div class="legend calendar"> <ul class='legend-labels'> diff --git a/src/UI/Calendar/CalendarView.js b/src/UI/Calendar/CalendarView.js index 4e3aec332..c6dc6de65 100644 --- a/src/UI/Calendar/CalendarView.js +++ b/src/UI/Calendar/CalendarView.js @@ -2,6 +2,7 @@ define( [ + 'jquery', 'vent', 'marionette', 'moment', @@ -12,7 +13,7 @@ define( 'Mixins/backbone.signalr.mixin', 'fullcalendar', 'jquery.easypiechart' - ], function (vent, Marionette, moment, CalendarCollection, StatusModel, QueueCollection, Config) { + ], function ($, vent, Marionette, moment, CalendarCollection, StatusModel, QueueCollection, Config) { return Marionette.ItemView.extend({ storageKey: 'calendar.view', @@ -24,28 +25,7 @@ define( }, render : function () { - this.$el.empty().fullCalendar({ - defaultView : Config.getValue(this.storageKey, 'basicWeek'), - allDayDefault : false, - ignoreTimezone: false, - weekMode : 'variable', - firstDay : StatusModel.get('startOfWeek'), - timeFormat : 'h(:mm)tt', - header : { - left : 'prev,next today', - center: 'title', - right : 'month,basicWeek,basicDay' - }, - buttonText : { - prev: '<i class="icon-arrow-left"></i>', - next: '<i class="icon-arrow-right"></i>' - }, - viewRender : this._viewRender.bind(this), - eventRender : this._eventRender.bind(this), - eventClick : function (event) { - vent.trigger(vent.Commands.ShowEpisodeDetails, {episode: event.model}); - } - }); + this.$el.empty().fullCalendar(this._getOptions()); }, onShow: function () { @@ -78,7 +58,8 @@ define( }); this.$(element).find('.chart').tooltip({ - title: 'Episode is downloading - {0}% {1}'.format(event.progress.toFixed(1), event.releaseTitle) + title: 'Episode is downloading - {0}% {1}'.format(event.progress.toFixed(1), event.releaseTitle), + container: 'body' }); } }, @@ -125,7 +106,7 @@ define( _getStatusLevel: function (element, endTime) { var hasFile = element.get('hasFile'); - var downloading = QueueCollection.findEpisode(element.get('id')) || element.get('downloading'); + var downloading = QueueCollection.findEpisode(element.get('id')) || element.get('grabbed'); var currentTime = moment(); var start = moment(element.get('airDateUtc')); var end = moment(endTime); @@ -178,6 +159,59 @@ define( } return downloading.get('title'); + }, + + _getOptions: function () { + var options = { + allDayDefault : false, + ignoreTimezone: false, + weekMode : 'variable', + firstDay : StatusModel.get('startOfWeek'), + timeFormat : 'h(:mm)tt', + buttonText : { + prev: '<i class="icon-arrow-left"></i>', + next: '<i class="icon-arrow-right"></i>' + }, + viewRender : this._viewRender.bind(this), + eventRender : this._eventRender.bind(this), + eventClick : function (event) { + vent.trigger(vent.Commands.ShowEpisodeDetails, {episode: event.model}); + } + }; + + if ($(window).width() < 768) { + options.defaultView = Config.getValue(this.storageKey, 'basicDay'); + + options.titleFormat = { + month: 'MMM yyyy', // September 2009 + week: 'MMM d[ yyyy]{ \'—\'[ MMM] d yyyy}', // Sep 7 - 13 2009 + day: 'ddd, MMM d, yyyy' // Tuesday, Sep 8, 2009 + }; + + options.header = { + left : 'prev,next today', + center: 'title', + right : 'basicWeek,basicDay' + }; + } + + else { + options.defaultView = Config.getValue(this.storageKey, 'basicWeek'); + + options.titleFormat = { + month: 'MMM yyyy', // September 2009 + week: 'MMM d[ yyyy]{ \'—\'[ MMM] d yyyy}', // Sep 7 - 13 2009 + day: 'dddd, MMM d, yyyy' // Tues, Sep 8, 2009 + }; + + options.header = { + left : 'prev,next today', + center: 'title', + right : 'month,basicWeek,basicDay' + }; + } + + return options; } }); }); diff --git a/src/UI/Calendar/calendar.less b/src/UI/Calendar/calendar.less index 430de3e0a..2f7c715e8 100644 --- a/src/UI/Calendar/calendar.less +++ b/src/UI/Calendar/calendar.less @@ -3,8 +3,11 @@ @import "../Content/Bootstrap/buttons"; @import "../Shared/Styles/clickable"; @import "../Content/variables"; +@import "../Content/Overrides/bootstrap"; .calendar { + width: 100%; + th, td { border-color : #eeeeee; } @@ -77,27 +80,27 @@ } .primary { - border-color : @btnPrimaryBackground; + border-color : @btn-primary-bg; } .info { - border-color : @btnInfoBackground; + border-color : @btn-info-bg; } .inverse { - border-color : @btnInverseBackground; + border-color : @btn-link-disabled-color; } .warning { - border-color : @btnWarningBackground; + border-color : @btn-warning-bg; } .danger { - border-color : @btnDangerBackground; + border-color : @btn-danger-bg; } .success { - border-color : @btnSuccessBackground;; + border-color : @btn-success-bg; } .purple { @@ -107,10 +110,17 @@ .episode-title { .btn-link; .text-overflow; - color : @linkColor; + color : @link-color; margin-top : 1px; - width : 140px; display : inline-block; + + @media (max-width: @screen-xs-min) { + width : 140px; + } + + @media (min-width: @screen-md-min) { + width : 140px; + } } } @@ -119,33 +129,33 @@ background-position : -160px -128px; .primary { - border-color : @btnPrimaryBackground; - background-color : @btnPrimaryBackground; + border-color : @btn-primary-bg; + background-color : @btn-primary-bg; } .info { - border-color : @btnInfoBackground; - background-color : @btnInfoBackground; + border-color : @btn-info-bg; + background-color : @btn-info-bg; } .inverse { - border-color : @btnInverseBackground; - background-color : @btnInverseBackground; + border-color : @btn-link-disabled-color; + background-color : @btn-link-disabled-color; } .warning { - border-color : @btnWarningBackground; - background-color : @btnWarningBackground; + border-color : @btn-warning-bg; + background-color : @btn-warning-bg; } .danger { - border-color : @btnDangerBackground; - background-color : @btnDangerBackground; + border-color : @btn-danger-bg; + background-color : @btn-danger-bg; } .success { - border-color : @btnSuccessBackground; - background-color : @btnSuccessBackground; + border-color : @btn-success-bg; + background-color : @btn-success-bg; } .purple { @@ -161,7 +171,7 @@ .ical { - color: @btnInverseBackground; + color: @btn-link-disabled-color; cursor: pointer; } diff --git a/src/UI/Cells/EpisodeActionsCellTemplate.html b/src/UI/Cells/EpisodeActionsCellTemplate.html index cea3bef98..77e0ef5cd 100644 --- a/src/UI/Cells/EpisodeActionsCellTemplate.html +++ b/src/UI/Cells/EpisodeActionsCellTemplate.html @@ -1,10 +1,14 @@ -<div class="btn-group"> - <button class="btn btn-mini x-automatic-search x-automatic-search-icon" title="Automatic Search" data-container="body"><i class="icon-search"></i></button> - <button class="btn btn-mini dropdown-toggle" data-toggle="dropdown"> +<div class="btn-group hidden-xs"> + <button class="btn btn-xs x-automatic-search x-automatic-search-icon" title="Automatic Search"><i class="icon-search"></i></button> + <button class="btn btn-xs dropdown-toggle" data-toggle="dropdown"> <span class="caret"></span> </button> <ul class="dropdown-menu"> <li class="x-automatic-search">Automatic Search</li> <li class="x-manual-search">Manual Search</li> </ul> +</div> + +<div class="visible-xs"> + <button class="btn btn-xs x-automatic-search x-automatic-search-icon" title="Automatic Search"><i class="icon-search"></i></button> </div> \ No newline at end of file diff --git a/src/UI/Cells/EpisodeStatusCell.js b/src/UI/Cells/EpisodeStatusCell.js index 1b4d6a68c..167ac8671 100644 --- a/src/UI/Cells/EpisodeStatusCell.js +++ b/src/UI/Cells/EpisodeStatusCell.js @@ -69,12 +69,19 @@ define( if (downloading) { var progress = 100 - (downloading.get('sizeleft') / downloading.get('size') * 100); - this.$el.html('<div class="progress progress-purple" title="Episode is downloading - {0}% {1}" data-container="body">'.format(progress.toFixed(1), downloading.get('title')) + - '<div class="bar" style="width: {0}%;"></div></div>'.format(progress)); - return; + if (progress === 0) { + icon = 'icon-nd-downloading'; + tooltip = 'Episode is downloading'; + } + + else { + this.$el.html('<div class="progress" title="Episode is downloading - {0}% {1}">'.format(progress.toFixed(1), downloading.get('title')) + + '<div class="progress-bar progress-bar-purple" style="width: {0}%;"></div></div>'.format(progress)); + return; + } } - else if (this.model.get('downloading')) { + else if (this.model.get('grabbed')) { icon = 'icon-nd-downloading'; tooltip = 'Episode is downloading'; } diff --git a/src/UI/Cells/SeriesActionsCell.js b/src/UI/Cells/SeriesActionsCell.js index 83a3ba1d2..313f8ef29 100644 --- a/src/UI/Cells/SeriesActionsCell.js +++ b/src/UI/Cells/SeriesActionsCell.js @@ -18,8 +18,8 @@ define( this.$el.empty(); this.$el.html( - '<i class="icon-cog x-edit-series" title="" data-original-title="Edit Series"></i> ' + - '<i class="icon-remove x-remove-series" title="" data-original-title="Delete Series"></i>' + '<i class="icon-nd-edit x-edit-series" title="" data-original-title="Edit Series"></i> ' + + '<i class="icon-remove x-remove-series hidden-xs" title="" data-original-title="Delete Series"></i>' ); this.delegateEvents(); diff --git a/src/UI/Cells/cells.less b/src/UI/Cells/cells.less index 712b66bb1..f5cf8301a 100644 --- a/src/UI/Cells/cells.less +++ b/src/UI/Cells/cells.less @@ -3,19 +3,47 @@ @import "../Content/Bootstrap/buttons"; @import "../Shared/Styles/clickable"; @import "../Content/mixins"; +@import "../Content/variables"; + +.table { + + //table-layout: fixed; +} + +.series-title { + .text-overflow(); + + @media @sm { + max-width: 250px + } +} .episode-title-cell { - .btn-link; + .text-overflow(); + + @media @lg { + max-width: 350px; + } + + @media @md { + max-width: 250px; + } + + @media @sm { + max-width: 200px; + } } .air-date-cell { width : 120px; cursor: default; + .text-overflow(); } .relative-date-cell { width : 150px; cursor: default; + .text-overflow(); } .history-event-type-cell { @@ -42,7 +70,7 @@ } i { - color : @red; + color : @brand-danger; } } @@ -71,12 +99,12 @@ td.episode-status-cell, td.quality-cell { } .nzb-title-cell { - max-width: 600px; + max-width: 400px; word-wrap: break-word; } .episode-actions-cell { - width: 50px; + width: 65px; li { .clickable(); @@ -98,12 +126,13 @@ td.episode-status-cell, td.quality-cell { } .series-actions-cell { - width: 40px; + width : 56px; + min-width : 56px; } .timeleft-cell { - cursor: default; - width: 80px; + cursor : default; + width : 80px; } .queue-status-cell { @@ -129,4 +158,4 @@ td.delete-episode-file-cell { .series-status-cell { width: 16px; -} \ No newline at end of file +} diff --git a/src/UI/Commands/CommandController.js b/src/UI/Commands/CommandController.js index 60684bbf5..90efe0829 100644 --- a/src/UI/Commands/CommandController.js +++ b/src/UI/Commands/CommandController.js @@ -6,8 +6,10 @@ define( 'Commands/CommandCollection', 'Commands/CommandMessengerCollectionView', 'underscore', + 'moment', + 'Shared/Messenger', 'jQuery/jquery.spin' - ], function (vent, CommandModel, CommandCollection, CommandMessengerCollectionView, _) { + ], function (vent, CommandModel, CommandCollection, CommandMessengerCollectionView, _, moment, Messenger) { CommandMessengerCollectionView.render(); @@ -16,15 +18,35 @@ define( return { + _lastCommand: {}, + Execute: function (name, properties) { var attr = _.extend({name: name.toLocaleLowerCase()}, properties); - var commandModel = new CommandModel(attr); - return commandModel.save().success(function () { + if (this._lastCommand.command && this._lastCommand.command.isSameCommand(attr) && moment().add('seconds', -5).isBefore(this._lastCommand.time)) { + + Messenger.show({ + message: 'Please wait at least 5 seconds before running this command again', + hideAfter: 5, + type: 'error' + }); + + return this._lastCommand.promise; + } + + var promise = commandModel.save().success(function () { CommandCollection.add(commandModel); }); + + this._lastCommand = { + command : commandModel, + promise : promise, + time : moment() + }; + + return promise; }, bindToCommand: function (options) { diff --git a/src/UI/Content/Backgrid/paginator.less b/src/UI/Content/Backgrid/paginator.less index 929dabd99..61fced052 100644 --- a/src/UI/Content/Backgrid/paginator.less +++ b/src/UI/Content/Backgrid/paginator.less @@ -17,6 +17,10 @@ font-size : 13px; position : absolute; right : 0; + + .label { + margin-top: 5px; + } } ul { diff --git a/src/UI/Content/Bootstrap/accordion.less b/src/UI/Content/Bootstrap/accordion.less deleted file mode 100644 index d63523bc8..000000000 --- a/src/UI/Content/Bootstrap/accordion.less +++ /dev/null @@ -1,34 +0,0 @@ -// -// Accordion -// -------------------------------------------------- - - -// Parent container -.accordion { - margin-bottom: @baseLineHeight; -} - -// Group == heading + body -.accordion-group { - margin-bottom: 2px; - border: 1px solid #e5e5e5; - .border-radius(@baseBorderRadius); -} -.accordion-heading { - border-bottom: 0; -} -.accordion-heading .accordion-toggle { - display: block; - padding: 8px 15px; -} - -// General toggle styles -.accordion-toggle { - cursor: pointer; -} - -// Inner needs the styles because you can't animate properly with any styles on the element -.accordion-inner { - padding: 9px 15px; - border-top: 1px solid #e5e5e5; -} diff --git a/src/UI/Content/Bootstrap/alerts.less b/src/UI/Content/Bootstrap/alerts.less index 0116b191b..3eab06629 100644 --- a/src/UI/Content/Bootstrap/alerts.less +++ b/src/UI/Content/Bootstrap/alerts.less @@ -7,73 +7,61 @@ // ------------------------- .alert { - padding: 8px 35px 8px 14px; - margin-bottom: @baseLineHeight; - text-shadow: 0 1px 0 rgba(255,255,255,.5); - background-color: @warningBackground; - border: 1px solid @warningBorder; - .border-radius(@baseBorderRadius); -} -.alert, -.alert h4 { - // Specified for the h4 to prevent conflicts of changing @headingsColor - color: @warningText; -} -.alert h4 { - margin: 0; + padding: @alert-padding; + margin-bottom: @line-height-computed; + border: 1px solid transparent; + border-radius: @alert-border-radius; + + // Headings for larger alerts + h4 { + margin-top: 0; + // Specified for the h4 to prevent conflicts of changing @headings-color + color: inherit; + } + // Provide class for links that match alerts + .alert-link { + font-weight: @alert-link-font-weight; + } + + // Improve alignment and spacing of inner content + > p, + > ul { + margin-bottom: 0; + } + > p + p { + margin-top: 5px; + } } -// Adjust close link position -.alert .close { - position: relative; - top: -2px; - right: -21px; - line-height: @baseLineHeight; -} +// Dismissable alerts +// +// Expand the right padding and account for the close button's positioning. +.alert-dismissable { + padding-right: (@alert-padding + 20); + + // Adjust close link position + .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; + } +} // Alternate styles -// ------------------------- +// +// Generate contextual modifier classes for colorizing the alert. .alert-success { - background-color: @successBackground; - border-color: @successBorder; - color: @successText; -} -.alert-success h4 { - color: @successText; -} -.alert-danger, -.alert-error { - background-color: @errorBackground; - border-color: @errorBorder; - color: @errorText; -} -.alert-danger h4, -.alert-error h4 { - color: @errorText; + .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text); } .alert-info { - background-color: @infoBackground; - border-color: @infoBorder; - color: @infoText; + .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text); } -.alert-info h4 { - color: @infoText; +.alert-warning { + .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text); } - - -// Block alerts -// ------------------------- - -.alert-block { - padding-top: 14px; - padding-bottom: 14px; -} -.alert-block > p, -.alert-block > ul { - margin-bottom: 0; -} -.alert-block p + p { - margin-top: 5px; +.alert-danger { + .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text); } diff --git a/src/UI/Content/Bootstrap/badges.less b/src/UI/Content/Bootstrap/badges.less new file mode 100644 index 000000000..56828cab7 --- /dev/null +++ b/src/UI/Content/Bootstrap/badges.less @@ -0,0 +1,55 @@ +// +// Badges +// -------------------------------------------------- + + +// Base classes +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: @font-size-small; + font-weight: @badge-font-weight; + color: @badge-color; + line-height: @badge-line-height; + vertical-align: baseline; + white-space: nowrap; + text-align: center; + background-color: @badge-bg; + border-radius: @badge-border-radius; + + // Empty badges collapse automatically (not available in IE8) + &:empty { + display: none; + } + + // Quick fix for badges in buttons + .btn & { + position: relative; + top: -1px; + } + .btn-xs & { + top: 0; + padding: 1px 5px; + } +} + +// Hover state, but only for links +a.badge { + &:hover, + &:focus { + color: @badge-link-hover-color; + text-decoration: none; + cursor: pointer; + } +} + +// Account for counters in navs +a.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: @badge-active-color; + background-color: @badge-active-bg; +} +.nav-pills > li > a > .badge { + margin-left: 3px; +} diff --git a/src/UI/Content/Bootstrap/bootstrap.less b/src/UI/Content/Bootstrap/bootstrap.less index 8ab6f0ee8..b368b8710 100644 --- a/src/UI/Content/Bootstrap/bootstrap.less +++ b/src/UI/Content/Bootstrap/bootstrap.less @@ -1,63 +1,49 @@ -/*! - * Bootstrap v2.3.2 - * - * Copyright 2012 Twitter, Inc - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Designed and built with all the love in the world @twitter by @mdo and @fat. - */ - // Core variables and mixins -@import "variables.less"; // Modify this for custom colors, font-sizes, etc +@import "variables.less"; @import "mixins.less"; -// CSS Reset -@import "reset.less"; +// Reset +@import "normalize.less"; +@import "print.less"; -// Grid system and page structure +// Core CSS @import "scaffolding.less"; -@import "grid.less"; -@import "layouts.less"; - -// Base CSS @import "type.less"; @import "code.less"; -@import "forms.less"; +@import "grid.less"; @import "tables.less"; - -// Components: common -@import "../FontAwesome/font-awesome.less"; -@import "dropdowns.less"; -@import "wells.less"; -@import "component-animations.less"; -@import "close.less"; - -// Components: Buttons & Alerts +@import "forms.less"; @import "buttons.less"; -@import "button-groups.less"; -@import "alerts.less"; // Note: alerts share common CSS with buttons and thus have styles in buttons.less -// Components: Nav +// Components +@import "component-animations.less"; +@import "glyphicons.less"; +@import "dropdowns.less"; +@import "button-groups.less"; +@import "input-groups.less"; @import "navs.less"; @import "navbar.less"; @import "breadcrumbs.less"; @import "pagination.less"; @import "pager.less"; +@import "labels.less"; +@import "badges.less"; +@import "jumbotron.less"; +@import "thumbnails.less"; +@import "alerts.less"; +@import "progress-bars.less"; +@import "media.less"; +@import "list-group.less"; +@import "panels.less"; +@import "wells.less"; +@import "close.less"; -// Components: Popovers +// Components w/ JavaScript @import "modals.less"; @import "tooltip.less"; @import "popovers.less"; - -// Components: Misc -@import "thumbnails.less"; -@import "media.less"; -@import "labels-badges.less"; -@import "progress-bars.less"; -@import "accordion.less"; @import "carousel.less"; -@import "hero-unit.less"; // Utility classes -@import "utilities.less"; // Has to be last to override when necessary +@import "utilities.less"; +@import "responsive-utilities.less"; diff --git a/src/UI/Content/Bootstrap/breadcrumbs.less b/src/UI/Content/Bootstrap/breadcrumbs.less index f753df6be..cb01d503f 100644 --- a/src/UI/Content/Bootstrap/breadcrumbs.less +++ b/src/UI/Content/Bootstrap/breadcrumbs.less @@ -4,21 +4,23 @@ .breadcrumb { - padding: 8px 15px; - margin: 0 0 @baseLineHeight; + padding: @breadcrumb-padding-vertical @breadcrumb-padding-horizontal; + margin-bottom: @line-height-computed; list-style: none; - background-color: #f5f5f5; - .border-radius(@baseBorderRadius); + background-color: @breadcrumb-bg; + border-radius: @border-radius-base; + > li { display: inline-block; - .ie7-inline-block(); - text-shadow: 0 1px 0 @white; - > .divider { + + + li:before { + content: "@{breadcrumb-separator}\00a0"; // Unicode space added since inline-block means non-collapsing white-space padding: 0 5px; - color: #ccc; + color: @breadcrumb-color; } } + > .active { - color: @grayLight; + color: @breadcrumb-active-color; } } diff --git a/src/UI/Content/Bootstrap/button-groups.less b/src/UI/Content/Bootstrap/button-groups.less index 55cdc6033..27eb796b8 100644 --- a/src/UI/Content/Bootstrap/button-groups.less +++ b/src/UI/Content/Bootstrap/button-groups.less @@ -2,90 +2,87 @@ // Button groups // -------------------------------------------------- - // Make the div behave like a button -.btn-group { +.btn-group, +.btn-group-vertical { position: relative; display: inline-block; - .ie7-inline-block(); - font-size: 0; // remove as part 1 of font-size inline-block hack vertical-align: middle; // match .btn alignment given font-size hack above - white-space: nowrap; // prevent buttons from wrapping when in tight spaces (e.g., the table on the tests page) - .ie7-restore-left-whitespace(); + > .btn { + position: relative; + float: left; + // Bring the "active" button to the front + &:hover, + &:focus, + &:active, + &.active { + z-index: 2; + } + &:focus { + // Remove focus outline when dropdown JS adds it after closing the menu + outline: none; + } + } } -// Space out series of button groups -.btn-group + .btn-group { - margin-left: 5px; +// Prevent double borders when buttons are next to each other +.btn-group { + .btn + .btn, + .btn + .btn-group, + .btn-group + .btn, + .btn-group + .btn-group { + margin-left: -1px; + } } // Optional: Group multiple button groups together for a toolbar .btn-toolbar { - font-size: 0; // Hack to remove whitespace that results from using inline-block - margin-top: @baseLineHeight / 2; - margin-bottom: @baseLineHeight / 2; - > .btn + .btn, - > .btn-group + .btn, - > .btn + .btn-group { + margin-left: -5px; // Offset the first child's margin + &:extend(.clearfix all); + + .btn-group, + .input-group { + float: left; + } + > .btn, + > .btn-group, + > .input-group { margin-left: 5px; } } -// Float them, remove border radius, then re-add to first and last elements -.btn-group > .btn { - position: relative; - .border-radius(0); -} -.btn-group > .btn + .btn { - margin-left: -1px; -} -.btn-group > .btn, -.btn-group > .dropdown-menu, -.btn-group > .popover { - font-size: @baseFontSize; // redeclare as part 2 of font-size inline-block hack -} - -// Reset fonts for other sizes -.btn-group > .btn-mini { - font-size: @fontSizeMini; -} -.btn-group > .btn-small { - font-size: @fontSizeSmall; -} -.btn-group > .btn-large { - font-size: @fontSizeLarge; +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; } // Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match .btn-group > .btn:first-child { margin-left: 0; - .border-top-left-radius(@baseBorderRadius); - .border-bottom-left-radius(@baseBorderRadius); + &:not(:last-child):not(.dropdown-toggle) { + .border-right-radius(0); + } } // Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it -.btn-group > .btn:last-child, -.btn-group > .dropdown-toggle { - .border-top-right-radius(@baseBorderRadius); - .border-bottom-right-radius(@baseBorderRadius); -} -// Reset corners for large buttons -.btn-group > .btn.large:first-child { - margin-left: 0; - .border-top-left-radius(@borderRadiusLarge); - .border-bottom-left-radius(@borderRadiusLarge); -} -.btn-group > .btn.large:last-child, -.btn-group > .large.dropdown-toggle { - .border-top-right-radius(@borderRadiusLarge); - .border-bottom-right-radius(@borderRadiusLarge); +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + .border-left-radius(0); } -// On hover/focus/active, bring the proper btn to front -.btn-group > .btn:hover, -.btn-group > .btn:focus, -.btn-group > .btn:active, -.btn-group > .btn.active { - z-index: 2; +// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group) +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child { + > .btn:last-child, + > .dropdown-toggle { + .border-right-radius(0); + } +} +.btn-group > .btn-group:last-child > .btn:first-child { + .border-left-radius(0); } // On active and open, don't show outline @@ -95,6 +92,14 @@ } +// Sizing +// +// Remix the default button sizing classes into new ones for easier manipulation. + +.btn-group-xs > .btn { &:extend(.btn-xs); } +.btn-group-sm > .btn { &:extend(.btn-sm); } +.btn-group-lg > .btn { &:extend(.btn-lg); } + // Split button dropdowns // ---------------------- @@ -103,127 +108,119 @@ .btn-group > .btn + .dropdown-toggle { padding-left: 8px; padding-right: 8px; - .box-shadow(~"inset 1px 0 0 rgba(255,255,255,.125), inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05)"); - *padding-top: 5px; - *padding-bottom: 5px; } -.btn-group > .btn-mini + .dropdown-toggle { - padding-left: 5px; - padding-right: 5px; - *padding-top: 2px; - *padding-bottom: 2px; -} -.btn-group > .btn-small + .dropdown-toggle { - *padding-top: 5px; - *padding-bottom: 4px; -} -.btn-group > .btn-large + .dropdown-toggle { +.btn-group > .btn-lg + .dropdown-toggle { padding-left: 12px; padding-right: 12px; - *padding-top: 7px; - *padding-bottom: 7px; } -.btn-group.open { +// The clickable button for toggling the menu +// Remove the gradient and set the same inset shadow as the :active state +.btn-group.open .dropdown-toggle { + .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); - // The clickable button for toggling the menu - // Remove the gradient and set the same inset shadow as the :active state - .dropdown-toggle { - background-image: none; - .box-shadow(~"inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)"); - } - - // Keep the hover's background when dropdown is open - .btn.dropdown-toggle { - background-color: @btnBackgroundHighlight; - } - .btn-primary.dropdown-toggle { - background-color: @btnPrimaryBackgroundHighlight; - } - .btn-warning.dropdown-toggle { - background-color: @btnWarningBackgroundHighlight; - } - .btn-danger.dropdown-toggle { - background-color: @btnDangerBackgroundHighlight; - } - .btn-success.dropdown-toggle { - background-color: @btnSuccessBackgroundHighlight; - } - .btn-info.dropdown-toggle { - background-color: @btnInfoBackgroundHighlight; - } - .btn-inverse.dropdown-toggle { - background-color: @btnInverseBackgroundHighlight; + // Show no shadow for `.btn-link` since it has no other button styles. + &.btn-link { + .box-shadow(none); } } // Reposition the caret .btn .caret { - margin-top: 8px; margin-left: 0; } // Carets in other button sizes -.btn-large .caret { - margin-top: 6px; -} -.btn-large .caret { - border-left-width: 5px; - border-right-width: 5px; - border-top-width: 5px; -} -.btn-mini .caret, -.btn-small .caret { - margin-top: 8px; +.btn-lg .caret { + border-width: @caret-width-large @caret-width-large 0; + border-bottom-width: 0; } // Upside down carets for .dropup -.dropup .btn-large .caret { - border-bottom-width: 5px; +.dropup .btn-lg .caret { + border-width: 0 @caret-width-large @caret-width-large; } - -// Account for other colors -.btn-primary, -.btn-warning, -.btn-danger, -.btn-info, -.btn-success, -.btn-inverse { - .caret { - border-top-color: @white; - border-bottom-color: @white; - } -} - - - // Vertical button groups // ---------------------- .btn-group-vertical { - display: inline-block; // makes buttons only take up the width they need - .ie7-inline-block(); + > .btn, + > .btn-group, + > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; + } + + // Clear floats so dropdown menus can be properly placed + > .btn-group { + &:extend(.clearfix all); + > .btn { + float: none; + } + } + + > .btn + .btn, + > .btn + .btn-group, + > .btn-group + .btn, + > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; + } } + .btn-group-vertical > .btn { - display: block; - float: none; - max-width: 100%; - .border-radius(0); + &:not(:first-child):not(:last-child) { + border-radius: 0; + } + &:first-child:not(:last-child) { + border-top-right-radius: @border-radius-base; + .border-bottom-radius(0); + } + &:last-child:not(:first-child) { + border-bottom-left-radius: @border-radius-base; + .border-top-radius(0); + } } -.btn-group-vertical > .btn + .btn { - margin-left: 0; - margin-top: -1px; +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; } -.btn-group-vertical > .btn:first-child { - .border-radius(@baseBorderRadius @baseBorderRadius 0 0); +.btn-group-vertical > .btn-group:first-child:not(:last-child) { + > .btn:last-child, + > .dropdown-toggle { + .border-bottom-radius(0); + } } -.btn-group-vertical > .btn:last-child { - .border-radius(0 0 @baseBorderRadius @baseBorderRadius); +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + .border-top-radius(0); } -.btn-group-vertical > .btn-large:first-child { - .border-radius(@borderRadiusLarge @borderRadiusLarge 0 0); + + + +// Justified button groups +// ---------------------- + +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; + > .btn, + > .btn-group { + float: none; + display: table-cell; + width: 1%; + } + > .btn-group .btn { + width: 100%; + } } -.btn-group-vertical > .btn-large:last-child { - .border-radius(0 0 @borderRadiusLarge @borderRadiusLarge); + + +// Checkbox and radio options +[data-toggle="buttons"] > .btn > input[type="radio"], +[data-toggle="buttons"] > .btn > input[type="checkbox"] { + display: none; } diff --git a/src/UI/Content/Bootstrap/buttons.less b/src/UI/Content/Bootstrap/buttons.less index 4cd4d862b..d4fc156be 100644 --- a/src/UI/Content/Bootstrap/buttons.less +++ b/src/UI/Content/Bootstrap/buttons.less @@ -6,109 +6,142 @@ // Base styles // -------------------------------------------------- -// Core .btn { display: inline-block; - .ie7-inline-block(); - padding: 4px 12px; margin-bottom: 0; // For input.btn - font-size: @baseFontSize; - line-height: @baseLineHeight; + font-weight: @btn-font-weight; text-align: center; vertical-align: middle; cursor: pointer; - .buttonBackground(@btnBackground, @btnBackgroundHighlight, @grayDark, 0 1px 1px rgba(255,255,255,.75)); - border: 1px solid @btnBorder; - *border: 0; // Remove the border to prevent IE7's black border on input:focus - border-bottom-color: darken(@btnBorder, 10%); - .border-radius(@baseBorderRadius); - .ie7-restore-left-whitespace(); // Give IE7 some love - .box-shadow(~"inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05)"); + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid transparent; + white-space: nowrap; + .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @border-radius-base); + .user-select(none); + + &, + &:active, + &.active { + &:focus { + .tab-focus(); + } + } - // Hover/focus state &:hover, &:focus { - color: @grayDark; + color: @btn-default-color; text-decoration: none; - background-position: 0 -15px; - - // transition is only when going to hover/focus, otherwise the background - // behind the gradient (there for IE<=9 fallback) gets mismatched - .transition(background-position .1s linear); } - // Focus state for keyboard and accessibility - &:focus { - .tab-focus(); - } - - // Active state - &.active, - &:active { - background-image: none; + &:active, + &.active { outline: 0; - .box-shadow(~"inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)"); + background-image: none; + .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); } - // Disabled state &.disabled, - &[disabled] { - cursor: default; - background-image: none; - .opacity(65); + &[disabled], + fieldset[disabled] & { + cursor: not-allowed; + pointer-events: none; // Future-proof disabling of clicks + .opacity(.65); .box-shadow(none); } - } +// Alternate buttons +// -------------------------------------------------- + +.btn-default { + .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border); +} +.btn-primary { + .button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border); +} +// Success appears as green +.btn-success { + .button-variant(@btn-success-color; @btn-success-bg; @btn-success-border); +} +// Info appears as blue-green +.btn-info { + .button-variant(@btn-info-color; @btn-info-bg; @btn-info-border); +} +// Warning appears as orange +.btn-warning { + .button-variant(@btn-warning-color; @btn-warning-bg; @btn-warning-border); +} +// Danger and error appear as red +.btn-danger { + .button-variant(@btn-danger-color; @btn-danger-bg; @btn-danger-border); +} + + +// Link buttons +// ------------------------- + +// Make a button look and behave like a link +.btn-link { + color: @link-color; + font-weight: normal; + cursor: pointer; + border-radius: 0; + + &, + &:active, + &[disabled], + fieldset[disabled] & { + background-color: transparent; + .box-shadow(none); + } + &, + &:hover, + &:focus, + &:active { + border-color: transparent; + } + &:hover, + &:focus { + color: @link-hover-color; + text-decoration: underline; + background-color: transparent; + } + &[disabled], + fieldset[disabled] & { + &:hover, + &:focus { + color: @btn-link-disabled-color; + text-decoration: none; + } + } +} + // Button Sizes // -------------------------------------------------- -// Large -.btn-large { - padding: @paddingLarge; - font-size: @fontSizeLarge; - .border-radius(@borderRadiusLarge); +.btn-lg { + // line-height: ensure even-numbered height of button next to large input + .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large); } -.btn-large [class^="icon-"], -.btn-large [class*=" icon-"] { - margin-top: 4px; +.btn-sm { + // line-height: ensure proper height of button next to small input + .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small); } - -// Small -.btn-small { - padding: @paddingSmall; - font-size: @fontSizeSmall; - .border-radius(@borderRadiusSmall); -} -.btn-small [class^="icon-"], -.btn-small [class*=" icon-"] { - margin-top: 0; -} -.btn-mini [class^="icon-"], -.btn-mini [class*=" icon-"] { - margin-top: -1px; -} - -// Mini -.btn-mini { - padding: @paddingMini; - font-size: @fontSizeMini; - .border-radius(@borderRadiusSmall); +.btn-xs { + .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @border-radius-small); } // Block button -// ------------------------- +// -------------------------------------------------- .btn-block { display: block; width: 100%; padding-left: 0; padding-right: 0; - .box-sizing(border-box); } // Vertically space out multiple block buttons @@ -124,105 +157,3 @@ input[type="button"] { width: 100%; } } - - - -// Alternate buttons -// -------------------------------------------------- - -// Provide *some* extra contrast for those who can get it -.btn-primary.active, -.btn-warning.active, -.btn-danger.active, -.btn-success.active, -.btn-info.active, -.btn-inverse.active { - color: rgba(255,255,255,.75); -} - -// Set the backgrounds -// ------------------------- -.btn-primary { - .buttonBackground(@btnPrimaryBackground, @btnPrimaryBackgroundHighlight); -} -// Warning appears are orange -.btn-warning { - .buttonBackground(@btnWarningBackground, @btnWarningBackgroundHighlight); -} -// Danger and error appear as red -.btn-danger { - .buttonBackground(@btnDangerBackground, @btnDangerBackgroundHighlight); -} -// Success appears as green -.btn-success { - .buttonBackground(@btnSuccessBackground, @btnSuccessBackgroundHighlight); -} -// Info appears as a neutral blue -.btn-info { - .buttonBackground(@btnInfoBackground, @btnInfoBackgroundHighlight); -} -// Inverse appears as dark gray -.btn-inverse { - .buttonBackground(@btnInverseBackground, @btnInverseBackgroundHighlight); -} - - -// Cross-browser Jank -// -------------------------------------------------- - -button.btn, -input[type="submit"].btn { - - // Firefox 3.6 only I believe - &::-moz-focus-inner { - padding: 0; - border: 0; - } - - // IE7 has some default padding on button controls - *padding-top: 3px; - *padding-bottom: 3px; - - &.btn-large { - *padding-top: 7px; - *padding-bottom: 7px; - } - &.btn-small { - *padding-top: 3px; - *padding-bottom: 3px; - } - &.btn-mini { - *padding-top: 1px; - *padding-bottom: 1px; - } -} - - -// Link buttons -// -------------------------------------------------- - -// Make a button look and behave like a link -.btn-link, -.btn-link:active, -.btn-link[disabled] { - background-color: transparent; - background-image: none; - .box-shadow(none); -} -.btn-link { - border-color: transparent; - cursor: pointer; - color: @linkColor; - .border-radius(0); -} -.btn-link:hover, -.btn-link:focus { - color: @linkColorHover; - text-decoration: underline; - background-color: transparent; -} -.btn-link[disabled]:hover, -.btn-link[disabled]:focus { - color: @grayDark; - text-decoration: none; -} diff --git a/src/UI/Content/Bootstrap/carousel.less b/src/UI/Content/Bootstrap/carousel.less index 55bc05014..e3fb8a2cf 100644 --- a/src/UI/Content/Bootstrap/carousel.less +++ b/src/UI/Content/Bootstrap/carousel.less @@ -3,19 +3,15 @@ // -------------------------------------------------- +// Wrapper for the slide container and indicators .carousel { position: relative; - margin-bottom: @baseLineHeight; - line-height: 1; } .carousel-inner { + position: relative; overflow: hidden; width: 100%; - position: relative; -} - -.carousel-inner { > .item { display: none; @@ -25,7 +21,7 @@ // Account for jankitude on images > img, > a > img { - display: block; + &:extend(.img-responsive); line-height: 1; } } @@ -70,89 +66,167 @@ .carousel-control { position: absolute; - top: 40%; - left: 15px; - width: 40px; - height: 40px; - margin-top: -20px; - font-size: 60px; - font-weight: 100; - line-height: 30px; - color: @white; + top: 0; + left: 0; + bottom: 0; + width: @carousel-control-width; + .opacity(@carousel-control-opacity); + font-size: @carousel-control-font-size; + color: @carousel-control-color; text-align: center; - background: @grayDarker; - border: 3px solid @white; - .border-radius(23px); - .opacity(50); + text-shadow: @carousel-text-shadow; + // We can't have this transition here because WebKit cancels the carousel + // animation if you trip this while in the middle of another animation. - // we can't have this transition here - // because webkit cancels the carousel - // animation if you trip this while - // in the middle of another animation - // ;_; - // .transition(opacity .2s linear); - - // Reposition the right one + // Set gradients for backgrounds + &.left { + #gradient > .horizontal(@start-color: rgba(0,0,0,.5); @end-color: rgba(0,0,0,.0001)); + } &.right { left: auto; - right: 15px; + right: 0; + #gradient > .horizontal(@start-color: rgba(0,0,0,.0001); @end-color: rgba(0,0,0,.5)); } // Hover/focus state &:hover, &:focus { - color: @white; + outline: none; + color: @carousel-control-color; text-decoration: none; - .opacity(90); + .opacity(.9); + } + + // Toggles + .icon-prev, + .icon-next, + .glyphicon-chevron-left, + .glyphicon-chevron-right { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; + } + .icon-prev, + .glyphicon-chevron-left { + left: 50%; + } + .icon-next, + .glyphicon-chevron-right { + right: 50%; + } + .icon-prev, + .icon-next { + width: 20px; + height: 20px; + margin-top: -10px; + margin-left: -10px; + font-family: serif; + } + + .icon-prev { + &:before { + content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039) + } + } + .icon-next { + &:before { + content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A) + } } } -// Carousel indicator pips -// ----------------------------- +// Optional indicator pips +// +// Add an unordered list with the following class and add a list item for each +// slide your carousel holds. + .carousel-indicators { position: absolute; - top: 15px; - right: 15px; - z-index: 5; - margin: 0; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + margin-left: -30%; + padding-left: 0; list-style: none; + text-align: center; li { - display: block; - float: left; - width: 10px; + display: inline-block; + width: 10px; height: 10px; - margin-left: 5px; + margin: 1px; text-indent: -999px; - background-color: #ccc; - background-color: rgba(255,255,255,.25); - border-radius: 5px; + border: 1px solid @carousel-indicator-border-color; + border-radius: 10px; + cursor: pointer; + + // IE8-9 hack for event handling + // + // Internet Explorer 8-9 does not support clicks on elements without a set + // `background-color`. We cannot use `filter` since that's not viewed as a + // background color by the browser. Thus, a hack is needed. + // + // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we + // set alpha transparency for the best results possible. + background-color: #000 \9; // IE8 + background-color: rgba(0,0,0,0); // IE9 } .active { - background-color: #fff; + margin: 0; + width: 12px; + height: 12px; + background-color: @carousel-indicator-active-bg; } } -// Caption for text below images +// Optional captions // ----------------------------- - +// Hidden by default for smaller viewports .carousel-caption { position: absolute; - left: 0; - right: 0; - bottom: 0; - padding: 15px; - background: @grayDark; - background: rgba(0,0,0,.75); + left: 15%; + right: 15%; + bottom: 20px; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: @carousel-caption-color; + text-align: center; + text-shadow: @carousel-text-shadow; + & .btn { + text-shadow: none; // No shadow for button elements in carousel-caption + } } -.carousel-caption h4, -.carousel-caption p { - color: @white; - line-height: @baseLineHeight; -} -.carousel-caption h4 { - margin: 0 0 5px; -} -.carousel-caption p { - margin-bottom: 0; + + +// Scale up controls for tablets and up +@media screen and (min-width: @screen-sm-min) { + + // Scale up the controls a smidge + .carousel-control { + .glyphicon-chevron-left, + .glyphicon-chevron-right, + .icon-prev, + .icon-next { + width: 30px; + height: 30px; + margin-top: -15px; + margin-left: -15px; + font-size: 30px; + } + } + + // Show and left align the captions + .carousel-caption { + left: 20%; + right: 20%; + padding-bottom: 30px; + } + + // Move up the indicators + .carousel-indicators { + bottom: 20px; + } } diff --git a/src/UI/Content/Bootstrap/close.less b/src/UI/Content/Bootstrap/close.less index 4c626bda6..9b4e74f2b 100644 --- a/src/UI/Content/Bootstrap/close.less +++ b/src/UI/Content/Bootstrap/close.less @@ -5,28 +5,29 @@ .close { float: right; - font-size: 20px; - font-weight: bold; - line-height: @baseLineHeight; - color: @black; - text-shadow: 0 1px 0 rgba(255,255,255,1); - .opacity(20); + font-size: (@font-size-base * 1.5); + font-weight: @close-font-weight; + line-height: 1; + color: @close-color; + text-shadow: @close-text-shadow; + .opacity(.2); + &:hover, &:focus { - color: @black; + color: @close-color; text-decoration: none; cursor: pointer; - .opacity(40); + .opacity(.5); + } + + // Additional properties for button version + // iOS requires the button element instead of an anchor tag. + // If you want the anchor version, it requires `href="#"`. + button& { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; } } - -// Additional properties for button version -// iOS requires the button element instead of an anchor tag. -// If you want the anchor version, it requires `href="#"`. -button.close { - padding: 0; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; -} \ No newline at end of file diff --git a/src/UI/Content/Bootstrap/code.less b/src/UI/Content/Bootstrap/code.less index 266a926e7..3eed26c05 100644 --- a/src/UI/Content/Bootstrap/code.less +++ b/src/UI/Content/Bootstrap/code.less @@ -1,61 +1,63 @@ // -// Code (inline and blocK) +// Code (inline and block) // -------------------------------------------------- // Inline and block code styles code, -pre { - padding: 0 3px 2px; - #font > #family > .monospace; - font-size: @baseFontSize - 2; - color: @grayDark; - .border-radius(3px); +kbd, +pre, +samp { + font-family: @font-family-monospace; } // Inline code code { padding: 2px 4px; - color: #d14; - background-color: #f7f7f9; - border: 1px solid #e1e1e8; + font-size: 90%; + color: @code-color; + background-color: @code-bg; white-space: nowrap; + border-radius: @border-radius-base; +} + +// User input typically entered via keyboard +kbd { + padding: 2px 4px; + font-size: 90%; + color: @kbd-color; + background-color: @kbd-bg; + border-radius: @border-radius-small; + box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); } // Blocks of code pre { display: block; - padding: (@baseLineHeight - 1) / 2; - margin: 0 0 @baseLineHeight / 2; - font-size: @baseFontSize - 1; // 14px to 13px - line-height: @baseLineHeight; + padding: ((@line-height-computed - 1) / 2); + margin: 0 0 (@line-height-computed / 2); + font-size: (@font-size-base - 1); // 14px to 13px + line-height: @line-height-base; word-break: break-all; word-wrap: break-word; - white-space: pre; - white-space: pre-wrap; - background-color: #f5f5f5; - border: 1px solid #ccc; // fallback for IE7-8 - border: 1px solid rgba(0,0,0,.15); - .border-radius(@baseBorderRadius); - - // Make prettyprint styles more spaced out for readability - &.prettyprint { - margin-bottom: @baseLineHeight; - } + color: @pre-color; + background-color: @pre-bg; + border: 1px solid @pre-border-color; + border-radius: @border-radius-base; // Account for some code outputs that place code tags in pre tags code { padding: 0; + font-size: inherit; color: inherit; - white-space: pre; white-space: pre-wrap; background-color: transparent; - border: 0; + border-radius: 0; } } // Enable scrollable blocks of code .pre-scrollable { - max-height: 340px; + max-height: @pre-scrollable-max-height; overflow-y: scroll; -} \ No newline at end of file +} diff --git a/src/UI/Content/Bootstrap/component-animations.less b/src/UI/Content/Bootstrap/component-animations.less index d614263a7..1efe45e2c 100644 --- a/src/UI/Content/Bootstrap/component-animations.less +++ b/src/UI/Content/Bootstrap/component-animations.less @@ -2,6 +2,10 @@ // Component animations // -------------------------------------------------- +// Heads up! +// +// We don't use the `.opacity()` mixin here since it causes a bug with text +// fields in IE7-8. Source: https://github.com/twitter/bootstrap/pull/3552. .fade { opacity: 0; @@ -12,11 +16,14 @@ } .collapse { + display: none; + &.in { + display: block; + } +} +.collapsing { position: relative; height: 0; overflow: hidden; .transition(height .35s ease); - &.in { - height: auto; - } } diff --git a/src/UI/Content/Bootstrap/dropdowns.less b/src/UI/Content/Bootstrap/dropdowns.less index 9e47b4715..f165165e7 100644 --- a/src/UI/Content/Bootstrap/dropdowns.less +++ b/src/UI/Content/Bootstrap/dropdowns.less @@ -3,64 +3,51 @@ // -------------------------------------------------- -// Use the .menu class on any <li> element within the topbar or ul.tabs and you'll get some superfancy dropdowns -.dropup, -.dropdown { - position: relative; -} -.dropdown-toggle { - // The caret makes the toggle a bit too tall in IE7 - *margin-bottom: -3px; -} -.dropdown-toggle:active, -.open .dropdown-toggle { - outline: 0; -} - // Dropdown arrow/caret -// -------------------- .caret { display: inline-block; width: 0; height: 0; - vertical-align: top; - border-top: 4px solid @black; - border-right: 4px solid transparent; - border-left: 4px solid transparent; - content: ""; + margin-left: 2px; + vertical-align: middle; + border-top: @caret-width-base solid; + border-right: @caret-width-base solid transparent; + border-left: @caret-width-base solid transparent; } -// Place the caret -.dropdown .caret { - margin-top: 8px; - margin-left: 2px; +// The dropdown wrapper (div) +.dropdown { + position: relative; +} + +// Prevent the focus on the dropdown toggle when closing dropdowns +.dropdown-toggle:focus { + outline: 0; } // The dropdown menu (ul) -// ---------------------- .dropdown-menu { position: absolute; top: 100%; left: 0; - z-index: @zindexDropdown; + z-index: @zindex-dropdown; display: none; // none by default, but block on "open" of the menu float: left; min-width: 160px; padding: 5px 0; margin: 2px 0 0; // override default ul list-style: none; - background-color: @dropdownBackground; - border: 1px solid #ccc; // Fallback for IE7-8 - border: 1px solid @dropdownBorder; - *border-right-width: 2px; - *border-bottom-width: 2px; - .border-radius(6px); - .box-shadow(0 5px 10px rgba(0,0,0,.2)); - -webkit-background-clip: padding-box; - -moz-background-clip: padding; - background-clip: padding-box; + font-size: @font-size-base; + background-color: @dropdown-bg; + border: 1px solid @dropdown-fallback-border; // IE8 fallback + border: 1px solid @dropdown-border; + border-radius: @border-radius-base; + .box-shadow(0 6px 12px rgba(0,0,0,.175)); + background-clip: padding-box; // Aligns the dropdown menu to right + // + // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]` &.pull-right { right: 0; left: auto; @@ -68,7 +55,7 @@ // Dividers (basically an hr) within the dropdown .divider { - .nav-divider(@dropdownDividerTop, @dropdownDividerBottom); + .nav-divider(@dropdown-divider-bg); } // Links within the dropdown menu @@ -77,92 +64,125 @@ padding: 3px 20px; clear: both; font-weight: normal; - line-height: @baseLineHeight; - color: @dropdownLinkColor; - white-space: nowrap; + line-height: @line-height-base; + color: @dropdown-link-color; + white-space: nowrap; // prevent links from randomly breaking onto new lines } } // Hover/Focus state -// ----------- -.dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus, -.dropdown-submenu:hover > a, -.dropdown-submenu:focus > a { - text-decoration: none; - color: @dropdownLinkColorHover; - #gradient > .vertical(@dropdownLinkBackgroundHover, darken(@dropdownLinkBackgroundHover, 5%)); -} - -// Active state -// ------------ -.dropdown-menu > .active > a, -.dropdown-menu > .active > a:hover, -.dropdown-menu > .active > a:focus { - color: @dropdownLinkColorActive; - text-decoration: none; - outline: 0; - #gradient > .vertical(@dropdownLinkBackgroundActive, darken(@dropdownLinkBackgroundActive, 5%)); -} - -// Disabled state -// -------------- -// Gray out text and ensure the hover/focus state remains gray -.dropdown-menu > .disabled > a, -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - color: @grayLight; -} -// Nuke hover/focus effects -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - text-decoration: none; - background-color: transparent; - background-image: none; // Remove CSS gradient - .reset-filter(); - cursor: default; -} - -// Open state for the dropdown -// --------------------------- -.open { - // IE7's z-index only goes to the nearest positioned ancestor, which would - // make the menu appear below buttons that appeared later on the page - *z-index: @zindexDropdown; - - & > .dropdown-menu { - display: block; +.dropdown-menu > li > a { + &:hover, + &:focus { + text-decoration: none; + color: @dropdown-link-hover-color; + background-color: @dropdown-link-hover-bg; } } +// Active state +.dropdown-menu > .active > a { + &, + &:hover, + &:focus { + color: @dropdown-link-active-color; + text-decoration: none; + outline: 0; + background-color: @dropdown-link-active-bg; + } +} + +// Disabled state +// +// Gray out text and ensure the hover/focus state remains gray + +.dropdown-menu > .disabled > a { + &, + &:hover, + &:focus { + color: @dropdown-link-disabled-color; + } +} +// Nuke hover/focus effects +.dropdown-menu > .disabled > a { + &:hover, + &:focus { + text-decoration: none; + background-color: transparent; + background-image: none; // Remove CSS gradient + .reset-filter(); + cursor: not-allowed; + } +} + +// Open state for the dropdown +.open { + // Show the menu + > .dropdown-menu { + display: block; + } + + // Remove the outline when :focus is triggered + > a { + outline: 0; + } +} + +// Menu positioning +// +// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown +// menu with the parent. +.dropdown-menu-right { + left: auto; // Reset the default from `.dropdown-menu` + right: 0; +} +// With v3, we enabled auto-flipping if you have a dropdown within a right +// aligned nav component. To enable the undoing of that, we provide an override +// to restore the default dropdown menu alignment. +// +// This is only for left-aligning a dropdown menu within a `.navbar-right` or +// `.pull-right` nav component. +.dropdown-menu-left { + left: 0; + right: auto; +} + +// Dropdown section headers +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: @font-size-small; + line-height: @line-height-base; + color: @dropdown-header-color; +} + // Backdrop to catch body clicks on mobile, etc. -// --------------------------- .dropdown-backdrop { position: fixed; left: 0; right: 0; bottom: 0; top: 0; - z-index: @zindexDropdown - 10; + z-index: (@zindex-dropdown - 10); } // Right aligned dropdowns -// --------------------------- .pull-right > .dropdown-menu { right: 0; left: auto; } // Allow for dropdowns to go bottom up (aka, dropup-menu) -// ------------------------------------------------------ +// // Just add .dropup after the standard .dropdown class and you're set, bro. // TODO: abstract this so that the navbar fixed styles are not placed here? + .dropup, .navbar-fixed-bottom .dropdown { // Reverse the caret .caret { border-top: 0; - border-bottom: 4px solid @black; + border-bottom: @caret-width-base solid; content: ""; } // Different positioning for bottom up menu @@ -173,76 +193,21 @@ } } -// Sub menus -// --------------------------- -.dropdown-submenu { - position: relative; -} -// Default dropdowns -.dropdown-submenu > .dropdown-menu { - top: 0; - left: 100%; - margin-top: -6px; - margin-left: -1px; - .border-radius(0 6px 6px 6px); -} -.dropdown-submenu:hover > .dropdown-menu { - display: block; -} -// Dropups -.dropup .dropdown-submenu > .dropdown-menu { - top: auto; - bottom: 0; - margin-top: 0; - margin-bottom: -2px; - .border-radius(5px 5px 5px 0); -} +// Component alignment +// +// Reiterate per navbar.less and the modified component alignment there. -// Caret to indicate there is a submenu -.dropdown-submenu > a:after { - display: block; - content: " "; - float: right; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - border-width: 5px 0 5px 5px; - border-left-color: darken(@dropdownBackground, 20%); - margin-top: 5px; - margin-right: -10px; -} -.dropdown-submenu:hover > a:after { - border-left-color: @dropdownLinkColorHover; -} - -// Left aligned submenus -.dropdown-submenu.pull-left { - // Undo the float - // Yes, this is awkward since .pull-left adds a float, but it sticks to our conventions elsewhere. - float: none; - - // Positioning the submenu - > .dropdown-menu { - left: -100%; - margin-left: 10px; - .border-radius(6px 0 6px 6px); +@media (min-width: @grid-float-breakpoint) { + .navbar-right { + .dropdown-menu { + .dropdown-menu-right(); + } + // Necessary for overrides of the default right aligned menu. + // Will remove come v4 in all likelihood. + .dropdown-menu-left { + .dropdown-menu-left(); + } } } -// Tweak nav headers -// ----------------- -// Increase padding from 15px to 20px on sides -.dropdown .dropdown-menu .nav-header { - padding-left: 20px; - padding-right: 20px; -} - -// Typeahead -// --------- -.typeahead { - z-index: 1051; - margin-top: 2px; // give it some space to breathe - .border-radius(@baseBorderRadius); -} diff --git a/src/UI/Content/Bootstrap/forms.less b/src/UI/Content/Bootstrap/forms.less index 06767bdd3..f607b8509 100644 --- a/src/UI/Content/Bootstrap/forms.less +++ b/src/UI/Content/Bootstrap/forms.less @@ -3,167 +3,67 @@ // -------------------------------------------------- -// GENERAL STYLES -// -------------- - -// Make all forms have space below them -form { - margin: 0 0 @baseLineHeight; -} +// Normalize non-controls +// +// Restyle and baseline non-control form elements. fieldset { padding: 0; margin: 0; border: 0; + // Chrome and Firefox set a `min-width: -webkit-min-content;` on fieldsets, + // so we reset that to ensure it behaves more like a standard block element. + // See https://github.com/twbs/bootstrap/issues/12359. + min-width: 0; } -// Groups of fields with labels on top (legends) legend { display: block; width: 100%; padding: 0; - margin-bottom: @baseLineHeight; - font-size: @baseFontSize * 1.5; - line-height: @baseLineHeight * 2; - color: @grayDark; + margin-bottom: @line-height-computed; + font-size: (@font-size-base * 1.5); + line-height: inherit; + color: @legend-color; border: 0; - border-bottom: 1px solid #e5e5e5; - - // Small - small { - font-size: @baseLineHeight * .75; - color: @grayLight; - } + border-bottom: 1px solid @legend-border-color; } -// Set font for forms -label, -input, -button, -select, -textarea { - #font > .shorthand(@baseFontSize,normal,@baseLineHeight); // Set size, weight, line-height here -} -input, -button, -select, -textarea { - font-family: @baseFontFamily; // And only set font-family here for those that need it (note the missing label element) -} - -// Identify controls by their labels label { - display: block; - margin-bottom: 5px; -} - -// Form controls -// ------------------------- - -// Shared size and type resets -select, -textarea, -input[type="text"], -input[type="password"], -input[type="datetime"], -input[type="datetime-local"], -input[type="date"], -input[type="month"], -input[type="time"], -input[type="week"], -input[type="number"], -input[type="email"], -input[type="url"], -input[type="search"], -input[type="tel"], -input[type="color"], -.uneditable-input { display: inline-block; - height: @baseLineHeight; - padding: 4px 6px; - margin-bottom: @baseLineHeight / 2; - font-size: @baseFontSize; - line-height: @baseLineHeight; - color: @gray; - .border-radius(@inputBorderRadius); - vertical-align: middle; + margin-bottom: 5px; + font-weight: bold; } -// Reset appearance properties for textual inputs and textarea -// Declare width for legacy (can't be on input[type=*] selectors or it's too specific) -input, -textarea, -.uneditable-input { - width: 206px; // plus 12px padding and 2px border -} -// Reset height since textareas have rows -textarea { - height: auto; -} -// Everything else -textarea, -input[type="text"], -input[type="password"], -input[type="datetime"], -input[type="datetime-local"], -input[type="date"], -input[type="month"], -input[type="time"], -input[type="week"], -input[type="number"], -input[type="email"], -input[type="url"], -input[type="search"], -input[type="tel"], -input[type="color"], -.uneditable-input { - background-color: @inputBackground; - border: 1px solid @inputBorder; - .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); - .transition(~"border linear .2s, box-shadow linear .2s"); - // Focus state - &:focus { - border-color: rgba(82,168,236,.8); - outline: 0; - outline: thin dotted \9; /* IE6-9 */ - .box-shadow(~"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6)"); - } +// Normalize form controls +// +// While most of our form styles require extra classes, some basic normalization +// is required to ensure optimum display with or without those classes to better +// address browser inconsistencies. + +// Override content-box in Normalize (* isn't specific enough) +input[type="search"] { + .box-sizing(border-box); } // Position radios and checkboxes better input[type="radio"], input[type="checkbox"] { margin: 4px 0 0; - *margin-top: 0; /* IE7 */ margin-top: 1px \9; /* IE8-9 */ line-height: normal; } -// Reset width of input images, buttons, radios, checkboxes -input[type="file"], -input[type="image"], -input[type="submit"], -input[type="reset"], -input[type="button"], -input[type="radio"], -input[type="checkbox"] { - width: auto; // Override of generic input selector -} - -// Set the height of select and file controls to match text inputs -select, +// Set the height of file controls to match text inputs input[type="file"] { - height: @inputHeight; /* In IE7, the height of the select element cannot be changed by height, only font-size */ - *margin-top: 4px; /* For IE7, add top margin to align select with labels */ - line-height: @inputHeight; + display: block; } -// Make select elements obey height by applying a border -select { - width: 220px; // default input width + 10px of padding that doesn't get applied - border: 1px solid @inputBorder; - background-color: @inputBackground; // Chrome on Linux and Mobile Safari need background-color +// Make range inputs behave like textual form controls +input[type="range"] { + display: block; + width: 100%; } // Make multiple select elements height not fixed @@ -172,519 +72,367 @@ select[size] { height: auto; } -// Focus for select, file, radio, and checkbox -select:focus, +// Focus for file, radio, and checkbox input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus { .tab-focus(); } - -// Uneditable inputs -// ------------------------- - -// Make uneditable inputs look inactive -.uneditable-input, -.uneditable-textarea { - color: @grayLight; - background-color: darken(@inputBackground, 1%); - border-color: @inputBorder; - .box-shadow(inset 0 1px 2px rgba(0,0,0,.025)); - cursor: not-allowed; -} - -// For text that needs to appear as an input but should not be an input -.uneditable-input { - overflow: hidden; // prevent text from wrapping, but still cut it off like an input does - white-space: nowrap; -} - -// Make uneditable textareas behave like a textarea -.uneditable-textarea { - width: auto; - height: auto; +// Adjust output element +output { + display: block; + padding-top: (@padding-base-vertical + 1); + font-size: @font-size-base; + line-height: @line-height-base; + color: @input-color; } -// Placeholder -// ------------------------- +// Common form controls +// +// Shared size and type resets for form controls. Apply `.form-control` to any +// of the following form controls: +// +// select +// textarea +// input[type="text"] +// input[type="password"] +// input[type="datetime"] +// input[type="datetime-local"] +// input[type="date"] +// input[type="month"] +// input[type="time"] +// input[type="week"] +// input[type="number"] +// input[type="email"] +// input[type="url"] +// input[type="search"] +// input[type="tel"] +// input[type="color"] -// Placeholder text gets special styles because when browsers invalidate entire lines if it doesn't understand a selector -input, -textarea { +.form-control { + display: block; + width: 100%; + height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border) + padding: @padding-base-vertical @padding-base-horizontal; + font-size: @font-size-base; + line-height: @line-height-base; + color: @input-color; + background-color: @input-bg; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid @input-border; + border-radius: @input-border-radius; + .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); + .transition(~"border-color ease-in-out .15s, box-shadow ease-in-out .15s"); + + // Customize the `:focus` state to imitate native WebKit styles. + .form-control-focus(); + + // Placeholder .placeholder(); + + // Disabled and read-only inputs + // + // HTML5 says that controls under a fieldset > legend:first-child won't be + // disabled if the fieldset is disabled. Due to implementation difficulty, we + // don't honor that edge case; we style them as disabled anyway. + &[disabled], + &[readonly], + fieldset[disabled] & { + cursor: not-allowed; + background-color: @input-bg-disabled; + opacity: 1; // iOS fix for unreadable disabled content + } + + // Reset height for `textarea`s + textarea& { + height: auto; + } } -// CHECKBOXES & RADIOS -// ------------------- +// Search inputs in iOS +// +// This overrides the extra rounded corners on search inputs in iOS so that our +// `.form-control` class can properly style them. Note that this cannot simply +// be added to `.form-control` as it's not specific enough. For details, see +// https://github.com/twbs/bootstrap/issues/11586. + +input[type="search"] { + -webkit-appearance: none; +} + + +// Special styles for iOS date input +// +// In Mobile Safari, date inputs require a pixel line-height that matches the +// given height of the input. + +input[type="date"] { + line-height: @input-height-base; +} + + +// Form groups +// +// Designed to help with the organization and spacing of vertical forms. For +// horizontal forms, use the predefined grid classes. + +.form-group { + margin-bottom: 15px; +} + + +// Checkboxes and radios +// +// Indent the labels to position radios/checkboxes as hanging controls. -// Indent the labels to position radios/checkboxes as hanging .radio, .checkbox { - min-height: @baseLineHeight; // clear the floating input if there is no label text + display: block; + min-height: @line-height-computed; // clear the floating input if there is no label text + margin-top: 10px; + margin-bottom: 10px; padding-left: 20px; + label { + display: inline; + font-weight: normal; + cursor: pointer; + } } .radio input[type="radio"], -.checkbox input[type="checkbox"] { +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { float: left; margin-left: -20px; } - -// Move the options list down to align with labels -.controls > .radio:first-child, -.controls > .checkbox:first-child { - padding-top: 5px; // has to be padding because margin collaspes +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing } // Radios and checkboxes on same line -// TODO v3: Convert .inline to .control-inline -.radio.inline, -.checkbox.inline { +.radio-inline, +.checkbox-inline { display: inline-block; - padding-top: 5px; + padding-left: 20px; margin-bottom: 0; vertical-align: middle; + font-weight: normal; + cursor: pointer; } -.radio.inline + .radio.inline, -.checkbox.inline + .checkbox.inline { +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; margin-left: 10px; // space out consecutive inline controls } - - -// INPUT SIZES -// ----------- - -// General classes for quick sizes -.input-mini { width: 60px; } -.input-small { width: 90px; } -.input-medium { width: 150px; } -.input-large { width: 210px; } -.input-xlarge { width: 270px; } -.input-xxlarge { width: 530px; } - -// Grid style input sizes -input[class*="span"], -select[class*="span"], -textarea[class*="span"], -.uneditable-input[class*="span"], -// Redeclare since the fluid row class is more specific -.row-fluid input[class*="span"], -.row-fluid select[class*="span"], -.row-fluid textarea[class*="span"], -.row-fluid .uneditable-input[class*="span"] { - float: none; - margin-left: 0; -} -// Ensure input-prepend/append never wraps -.input-append input[class*="span"], -.input-append .uneditable-input[class*="span"], -.input-prepend input[class*="span"], -.input-prepend .uneditable-input[class*="span"], -.row-fluid input[class*="span"], -.row-fluid select[class*="span"], -.row-fluid textarea[class*="span"], -.row-fluid .uneditable-input[class*="span"], -.row-fluid .input-prepend [class*="span"], -.row-fluid .input-append [class*="span"] { - display: inline-block; -} - - - -// GRID SIZING FOR INPUTS -// ---------------------- - -// Grid sizes -#grid > .input(@gridColumnWidth, @gridGutterWidth); - -// Control row for multiple inputs per line -.controls-row { - .clearfix(); // Clear the float from controls -} - -// Float to collapse white-space for proper grid alignment -.controls-row [class*="span"], -// Redeclare the fluid grid collapse since we undo the float for inputs -.row-fluid .controls-row [class*="span"] { - float: left; -} -// Explicity set top padding on all checkboxes/radios, not just first-child -.controls-row .checkbox[class*="span"], -.controls-row .radio[class*="span"] { - padding-top: 5px; -} - - - - -// DISABLED STATE -// -------------- - -// Disabled and read-only inputs -input[disabled], -select[disabled], -textarea[disabled], -input[readonly], -select[readonly], -textarea[readonly] { - cursor: not-allowed; - background-color: @inputDisabledBackground; -} -// Explicitly reset the colors here -input[type="radio"][disabled], -input[type="checkbox"][disabled], -input[type="radio"][readonly], -input[type="checkbox"][readonly] { - background-color: transparent; -} - - - - -// FORM FIELD FEEDBACK STATES -// -------------------------- - -// Warning -.control-group.warning { - .formFieldState(@warningText, @warningText, @warningBackground); -} -// Error -.control-group.error { - .formFieldState(@errorText, @errorText, @errorBackground); -} -// Success -.control-group.success { - .formFieldState(@successText, @successText, @successBackground); -} -// Success -.control-group.info { - .formFieldState(@infoText, @infoText, @infoBackground); -} - -// HTML5 invalid states -// Shares styles with the .control-group.error above -input:focus:invalid, -textarea:focus:invalid, -select:focus:invalid { - color: #b94a48; - border-color: #ee5f5b; - &:focus { - border-color: darken(#ee5f5b, 10%); - @shadow: 0 0 6px lighten(#ee5f5b, 20%); - .box-shadow(@shadow); +// Apply same disabled cursor tweak as for inputs +// +// Note: Neither radios nor checkboxes can be readonly. +input[type="radio"], +input[type="checkbox"], +.radio, +.radio-inline, +.checkbox, +.checkbox-inline { + &[disabled], + fieldset[disabled] & { + cursor: not-allowed; } } +// Form control sizing +// +// Build on `.form-control` with modifier classes to decrease or increase the +// height and font-size of form controls. -// FORM ACTIONS -// ------------ +.input-sm { + .input-size(@input-height-small; @padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small); +} -.form-actions { - padding: (@baseLineHeight - 1) 20px @baseLineHeight; - margin-top: @baseLineHeight; - margin-bottom: @baseLineHeight; - background-color: @formActionsBackground; - border-top: 1px solid #e5e5e5; - .clearfix(); // Adding clearfix to allow for .pull-right button containers +.input-lg { + .input-size(@input-height-large; @padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large); } +// Form control feedback states +// +// Apply contextual and semantic states to individual form controls. -// HELP TEXT -// --------- +.has-feedback { + // Enable absolute positioning + position: relative; -.help-block, -.help-inline { - color: lighten(@textColor, 15%); // lighten the text some for contrast + // Ensure icons don't overlap text + .form-control { + padding-right: (@input-height-base * 1.25); + } + + // Feedback icon (requires .glyphicon classes) + .form-control-feedback { + position: absolute; + top: (@line-height-computed + 5); // Height of the `label` and its margin + right: 0; + display: block; + width: @input-height-base; + height: @input-height-base; + line-height: @input-height-base; + text-align: center; + } } +// Feedback states +.has-success { + .form-control-validation(@state-success-text; @state-success-text; @state-success-bg); +} +.has-warning { + .form-control-validation(@state-warning-text; @state-warning-text; @state-warning-bg); +} +.has-error { + .form-control-validation(@state-danger-text; @state-danger-text; @state-danger-bg); +} + + +// Static form control text +// +// Apply class to a `p` element to make any string of text align with labels in +// a horizontal form layout. + +.form-control-static { + margin-bottom: 0; // Remove default margin from `p` +} + + +// Help text +// +// Apply to any element you wish to create light text for placement immediately +// below a form control. Use for general help, formatting, or instructional text. + .help-block { display: block; // account for any element using help-block - margin-bottom: @baseLineHeight / 2; -} - -.help-inline { - display: inline-block; - .ie7-inline-block(); - vertical-align: middle; - padding-left: 5px; + margin-top: 5px; + margin-bottom: 10px; + color: lighten(@text-color, 25%); // lighten the text some for contrast } -// INPUT GROUPS -// ------------ +// Inline forms +// +// Make forms appear inline(-block) by adding the `.form-inline` class. Inline +// forms begin stacked on extra small (mobile) devices and then go inline when +// viewports reach <768px. +// +// Requires wrapping inputs and labels with `.form-group` for proper display of +// default HTML form controls and our custom form controls (e.g., input groups). +// +// Heads up! This is mixin-ed into `.navbar-form` in navbars.less. -// Allow us to put symbols and text within the input field for a cleaner look -.input-append, -.input-prepend { - display: inline-block; - margin-bottom: @baseLineHeight / 2; - vertical-align: middle; - font-size: 0; // white space collapse hack - white-space: nowrap; // Prevent span and input from separating +.form-inline { - // Reset the white space collapse hack - input, - select, - .uneditable-input, - .dropdown-menu, - .popover { - font-size: @baseFontSize; - } + // Kick in the inline + @media (min-width: @screen-sm-min) { + // Inline-block all the things for "inline" + .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } - input, - select, - .uneditable-input { - position: relative; // placed here by default so that on :focus we can place the input above the .add-on for full border and box-shadow goodness - margin-bottom: 0; // prevent bottom margin from screwing up alignment in stacked forms - *margin-left: 0; - vertical-align: top; - .border-radius(0 @inputBorderRadius @inputBorderRadius 0); - // Make input on top when focused so blue border and shadow always show - &:focus { - z-index: 2; + // In navbar-form, allow folks to *not* use `.form-group` + .form-control { + display: inline-block; + width: auto; // Prevent labels from stacking above inputs in `.form-group` + vertical-align: middle; + } + // Input groups need that 100% width though + .input-group > .form-control { + width: 100%; + } + + .control-label { + margin-bottom: 0; + vertical-align: middle; + } + + // Remove default margin on radios/checkboxes that were used for stacking, and + // then undo the floating of radios and checkboxes to match (which also avoids + // a bug in WebKit: https://github.com/twbs/bootstrap/issues/1969). + .radio, + .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + padding-left: 0; + vertical-align: middle; + } + .radio input[type="radio"], + .checkbox input[type="checkbox"] { + float: none; + margin-left: 0; + } + + // Validation states + // + // Reposition the icon because it's now within a grid column and columns have + // `position: relative;` on them. Also accounts for the grid gutter padding. + .has-feedback .form-control-feedback { + top: 0; } } - .add-on { - display: inline-block; - width: auto; - height: @baseLineHeight; - min-width: 16px; - padding: 4px 5px; - font-size: @baseFontSize; - font-weight: normal; - line-height: @baseLineHeight; - text-align: center; - text-shadow: 0 1px 0 @white; - background-color: @grayLighter; - border: 1px solid #ccc; - } - .add-on, - .btn, - .btn-group > .dropdown-toggle { - vertical-align: top; - .border-radius(0); - } - .active { - background-color: lighten(@green, 30); - border-color: @green; - } -} - -.input-prepend { - .add-on, - .btn { - margin-right: -1px; - } - .add-on:first-child, - .btn:first-child { - // FYI, `.btn:first-child` accounts for a button group that's prepended - .border-radius(@inputBorderRadius 0 0 @inputBorderRadius); - } -} - -.input-append { - input, - select, - .uneditable-input { - .border-radius(@inputBorderRadius 0 0 @inputBorderRadius); - + .btn-group .btn:last-child { - .border-radius(0 @inputBorderRadius @inputBorderRadius 0); - } - } - .add-on, - .btn, - .btn-group { - margin-left: -1px; - } - .add-on:last-child, - .btn:last-child, - .btn-group:last-child > .dropdown-toggle { - .border-radius(0 @inputBorderRadius @inputBorderRadius 0); - } -} - -// Remove all border-radius for inputs with both prepend and append -.input-prepend.input-append { - input, - select, - .uneditable-input { - .border-radius(0); - + .btn-group .btn { - .border-radius(0 @inputBorderRadius @inputBorderRadius 0); - } - } - .add-on:first-child, - .btn:first-child { - margin-right: -1px; - .border-radius(@inputBorderRadius 0 0 @inputBorderRadius); - } - .add-on:last-child, - .btn:last-child { - margin-left: -1px; - .border-radius(0 @inputBorderRadius @inputBorderRadius 0); - } - .btn-group:first-child { - margin-left: 0; - } } - - -// SEARCH FORM -// ----------- - -input.search-query { - padding-right: 14px; - padding-right: 4px \9; - padding-left: 14px; - padding-left: 4px \9; /* IE7-8 doesn't have border-radius, so don't indent the padding */ - margin-bottom: 0; // Remove the default margin on all inputs - .border-radius(15px); -} - -/* Allow for input prepend/append in search forms */ -.form-search .input-append .search-query, -.form-search .input-prepend .search-query { - .border-radius(0); // Override due to specificity -} -.form-search .input-append .search-query { - .border-radius(14px 0 0 14px); -} -.form-search .input-append .btn { - .border-radius(0 14px 14px 0); -} -.form-search .input-prepend .search-query { - .border-radius(0 14px 14px 0); -} -.form-search .input-prepend .btn { - .border-radius(14px 0 0 14px); -} - - - - -// HORIZONTAL & VERTICAL FORMS -// --------------------------- - -// Common properties -// ----------------- - -.form-search, -.form-inline, -.form-horizontal { - input, - textarea, - select, - .help-inline, - .uneditable-input, - .input-prepend, - .input-append { - display: inline-block; - .ie7-inline-block(); - margin-bottom: 0; - vertical-align: middle; - } - // Re-hide hidden elements due to specifity - .hide { - display: none; - } -} -.form-search label, -.form-inline label, -.form-search .btn-group, -.form-inline .btn-group { - display: inline-block; -} -// Remove margin for input-prepend/-append -.form-search .input-append, -.form-inline .input-append, -.form-search .input-prepend, -.form-inline .input-prepend { - margin-bottom: 0; -} -// Inline checkbox/radio labels (remove padding on left) -.form-search .radio, -.form-search .checkbox, -.form-inline .radio, -.form-inline .checkbox { - padding-left: 0; - margin-bottom: 0; - vertical-align: middle; -} -// Remove float and margin, set to inline-block -.form-search .radio input[type="radio"], -.form-search .checkbox input[type="checkbox"], -.form-inline .radio input[type="radio"], -.form-inline .checkbox input[type="checkbox"] { - float: left; - margin-right: 3px; - margin-left: 0; -} - - -// Margin to space out fieldsets -.control-group { - margin-bottom: @baseLineHeight / 2; -} - -// Legend collapses margin, so next element is responsible for spacing -legend + .control-group { - margin-top: @baseLineHeight; - -webkit-margin-top-collapse: separate; -} - -// Horizontal-specific styles -// -------------------------- +// Horizontal forms +// +// Horizontal forms are built on grid classes and allow you to create forms with +// labels on the left and inputs on the right. .form-horizontal { - // Increase spacing between groups - .control-group { - margin-bottom: @baseLineHeight; - .clearfix(); - } - // Float the labels left - .control-label { - float: left; - width: @horizontalComponentOffset - 20; - padding-top: 5px; - text-align: right; - } - // Move over all input controls and content - .controls { - // Super jank IE7 fix to ensure the inputs in .input-append and input-prepend - // don't inherit the margin of the parent, in this case .controls - *display: inline-block; - *padding-left: 20px; - margin-left: @horizontalComponentOffset; - *margin-left: 0; - &:first-child { - *padding-left: @horizontalComponentOffset; - } - } - // Remove bottom margin on block level help text since that's accounted for on .control-group - .help-block { + + // Consistent vertical alignment of labels, radios, and checkboxes + .control-label, + .radio, + .checkbox, + .radio-inline, + .checkbox-inline { + margin-top: 0; margin-bottom: 0; + padding-top: (@padding-base-vertical + 1); // Default padding plus a border } - // And apply it only to .help-block instances that follow a form control - input, - select, - textarea, - .uneditable-input, - .input-prepend, - .input-append { - + .help-block { - margin-top: @baseLineHeight / 2; + // Account for padding we're adding to ensure the alignment and of help text + // and other content below items + .radio, + .checkbox { + min-height: (@line-height-computed + (@padding-base-vertical + 1)); + } + + // Make form groups behave like rows + .form-group { + .make-row(); + } + + .form-control-static { + padding-top: (@padding-base-vertical + 1); + } + + // Only right align form labels here when the columns stop stacking + @media (min-width: @screen-sm-min) { + .control-label { + text-align: right; } } - // Move over buttons in .form-actions to align with .controls - .form-actions { - padding-left: @horizontalComponentOffset; + + // Validation states + // + // Reposition the icon because it's now within a grid column and columns have + // `position: relative;` on them. Also accounts for the grid gutter padding. + .has-feedback .form-control-feedback { + top: 0; + right: (@grid-gutter-width / 2); } } diff --git a/src/UI/Content/Bootstrap/glyphicons.less b/src/UI/Content/Bootstrap/glyphicons.less new file mode 100644 index 000000000..789c5e7f4 --- /dev/null +++ b/src/UI/Content/Bootstrap/glyphicons.less @@ -0,0 +1,233 @@ +// +// Glyphicons for Bootstrap +// +// Since icons are fonts, they can be placed anywhere text is placed and are +// thus automatically sized to match the surrounding child. To use, create an +// inline element with the appropriate classes, like so: +// +// <a href="#"><span class="glyphicon glyphicon-star"></span> Star</a> + +// Import the fonts +@font-face { + font-family: 'Glyphicons Halflings'; + src: ~"url('@{icon-font-path}@{icon-font-name}.eot')"; + src: ~"url('@{icon-font-path}@{icon-font-name}.eot?#iefix') format('embedded-opentype')", + ~"url('@{icon-font-path}@{icon-font-name}.woff') format('woff')", + ~"url('@{icon-font-path}@{icon-font-name}.ttf') format('truetype')", + ~"url('@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}') format('svg')"; +} + +// Catchall baseclass +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +// Individual icons +.glyphicon-asterisk { &:before { content: "\2a"; } } +.glyphicon-plus { &:before { content: "\2b"; } } +.glyphicon-euro { &:before { content: "\20ac"; } } +.glyphicon-minus { &:before { content: "\2212"; } } +.glyphicon-cloud { &:before { content: "\2601"; } } +.glyphicon-envelope { &:before { content: "\2709"; } } +.glyphicon-pencil { &:before { content: "\270f"; } } +.glyphicon-glass { &:before { content: "\e001"; } } +.glyphicon-music { &:before { content: "\e002"; } } +.glyphicon-search { &:before { content: "\e003"; } } +.glyphicon-heart { &:before { content: "\e005"; } } +.glyphicon-star { &:before { content: "\e006"; } } +.glyphicon-star-empty { &:before { content: "\e007"; } } +.glyphicon-user { &:before { content: "\e008"; } } +.glyphicon-film { &:before { content: "\e009"; } } +.glyphicon-th-large { &:before { content: "\e010"; } } +.glyphicon-th { &:before { content: "\e011"; } } +.glyphicon-th-list { &:before { content: "\e012"; } } +.glyphicon-ok { &:before { content: "\e013"; } } +.glyphicon-remove { &:before { content: "\e014"; } } +.glyphicon-zoom-in { &:before { content: "\e015"; } } +.glyphicon-zoom-out { &:before { content: "\e016"; } } +.glyphicon-off { &:before { content: "\e017"; } } +.glyphicon-signal { &:before { content: "\e018"; } } +.glyphicon-cog { &:before { content: "\e019"; } } +.glyphicon-trash { &:before { content: "\e020"; } } +.glyphicon-home { &:before { content: "\e021"; } } +.glyphicon-file { &:before { content: "\e022"; } } +.glyphicon-time { &:before { content: "\e023"; } } +.glyphicon-road { &:before { content: "\e024"; } } +.glyphicon-download-alt { &:before { content: "\e025"; } } +.glyphicon-download { &:before { content: "\e026"; } } +.glyphicon-upload { &:before { content: "\e027"; } } +.glyphicon-inbox { &:before { content: "\e028"; } } +.glyphicon-play-circle { &:before { content: "\e029"; } } +.glyphicon-repeat { &:before { content: "\e030"; } } +.glyphicon-refresh { &:before { content: "\e031"; } } +.glyphicon-list-alt { &:before { content: "\e032"; } } +.glyphicon-lock { &:before { content: "\e033"; } } +.glyphicon-flag { &:before { content: "\e034"; } } +.glyphicon-headphones { &:before { content: "\e035"; } } +.glyphicon-volume-off { &:before { content: "\e036"; } } +.glyphicon-volume-down { &:before { content: "\e037"; } } +.glyphicon-volume-up { &:before { content: "\e038"; } } +.glyphicon-qrcode { &:before { content: "\e039"; } } +.glyphicon-barcode { &:before { content: "\e040"; } } +.glyphicon-tag { &:before { content: "\e041"; } } +.glyphicon-tags { &:before { content: "\e042"; } } +.glyphicon-book { &:before { content: "\e043"; } } +.glyphicon-bookmark { &:before { content: "\e044"; } } +.glyphicon-print { &:before { content: "\e045"; } } +.glyphicon-camera { &:before { content: "\e046"; } } +.glyphicon-font { &:before { content: "\e047"; } } +.glyphicon-bold { &:before { content: "\e048"; } } +.glyphicon-italic { &:before { content: "\e049"; } } +.glyphicon-text-height { &:before { content: "\e050"; } } +.glyphicon-text-width { &:before { content: "\e051"; } } +.glyphicon-align-left { &:before { content: "\e052"; } } +.glyphicon-align-center { &:before { content: "\e053"; } } +.glyphicon-align-right { &:before { content: "\e054"; } } +.glyphicon-align-justify { &:before { content: "\e055"; } } +.glyphicon-list { &:before { content: "\e056"; } } +.glyphicon-indent-left { &:before { content: "\e057"; } } +.glyphicon-indent-right { &:before { content: "\e058"; } } +.glyphicon-facetime-video { &:before { content: "\e059"; } } +.glyphicon-picture { &:before { content: "\e060"; } } +.glyphicon-map-marker { &:before { content: "\e062"; } } +.glyphicon-adjust { &:before { content: "\e063"; } } +.glyphicon-tint { &:before { content: "\e064"; } } +.glyphicon-edit { &:before { content: "\e065"; } } +.glyphicon-share { &:before { content: "\e066"; } } +.glyphicon-check { &:before { content: "\e067"; } } +.glyphicon-move { &:before { content: "\e068"; } } +.glyphicon-step-backward { &:before { content: "\e069"; } } +.glyphicon-fast-backward { &:before { content: "\e070"; } } +.glyphicon-backward { &:before { content: "\e071"; } } +.glyphicon-play { &:before { content: "\e072"; } } +.glyphicon-pause { &:before { content: "\e073"; } } +.glyphicon-stop { &:before { content: "\e074"; } } +.glyphicon-forward { &:before { content: "\e075"; } } +.glyphicon-fast-forward { &:before { content: "\e076"; } } +.glyphicon-step-forward { &:before { content: "\e077"; } } +.glyphicon-eject { &:before { content: "\e078"; } } +.glyphicon-chevron-left { &:before { content: "\e079"; } } +.glyphicon-chevron-right { &:before { content: "\e080"; } } +.glyphicon-plus-sign { &:before { content: "\e081"; } } +.glyphicon-minus-sign { &:before { content: "\e082"; } } +.glyphicon-remove-sign { &:before { content: "\e083"; } } +.glyphicon-ok-sign { &:before { content: "\e084"; } } +.glyphicon-question-sign { &:before { content: "\e085"; } } +.glyphicon-info-sign { &:before { content: "\e086"; } } +.glyphicon-screenshot { &:before { content: "\e087"; } } +.glyphicon-remove-circle { &:before { content: "\e088"; } } +.glyphicon-ok-circle { &:before { content: "\e089"; } } +.glyphicon-ban-circle { &:before { content: "\e090"; } } +.glyphicon-arrow-left { &:before { content: "\e091"; } } +.glyphicon-arrow-right { &:before { content: "\e092"; } } +.glyphicon-arrow-up { &:before { content: "\e093"; } } +.glyphicon-arrow-down { &:before { content: "\e094"; } } +.glyphicon-share-alt { &:before { content: "\e095"; } } +.glyphicon-resize-full { &:before { content: "\e096"; } } +.glyphicon-resize-small { &:before { content: "\e097"; } } +.glyphicon-exclamation-sign { &:before { content: "\e101"; } } +.glyphicon-gift { &:before { content: "\e102"; } } +.glyphicon-leaf { &:before { content: "\e103"; } } +.glyphicon-fire { &:before { content: "\e104"; } } +.glyphicon-eye-open { &:before { content: "\e105"; } } +.glyphicon-eye-close { &:before { content: "\e106"; } } +.glyphicon-warning-sign { &:before { content: "\e107"; } } +.glyphicon-plane { &:before { content: "\e108"; } } +.glyphicon-calendar { &:before { content: "\e109"; } } +.glyphicon-random { &:before { content: "\e110"; } } +.glyphicon-comment { &:before { content: "\e111"; } } +.glyphicon-magnet { &:before { content: "\e112"; } } +.glyphicon-chevron-up { &:before { content: "\e113"; } } +.glyphicon-chevron-down { &:before { content: "\e114"; } } +.glyphicon-retweet { &:before { content: "\e115"; } } +.glyphicon-shopping-cart { &:before { content: "\e116"; } } +.glyphicon-folder-close { &:before { content: "\e117"; } } +.glyphicon-folder-open { &:before { content: "\e118"; } } +.glyphicon-resize-vertical { &:before { content: "\e119"; } } +.glyphicon-resize-horizontal { &:before { content: "\e120"; } } +.glyphicon-hdd { &:before { content: "\e121"; } } +.glyphicon-bullhorn { &:before { content: "\e122"; } } +.glyphicon-bell { &:before { content: "\e123"; } } +.glyphicon-certificate { &:before { content: "\e124"; } } +.glyphicon-thumbs-up { &:before { content: "\e125"; } } +.glyphicon-thumbs-down { &:before { content: "\e126"; } } +.glyphicon-hand-right { &:before { content: "\e127"; } } +.glyphicon-hand-left { &:before { content: "\e128"; } } +.glyphicon-hand-up { &:before { content: "\e129"; } } +.glyphicon-hand-down { &:before { content: "\e130"; } } +.glyphicon-circle-arrow-right { &:before { content: "\e131"; } } +.glyphicon-circle-arrow-left { &:before { content: "\e132"; } } +.glyphicon-circle-arrow-up { &:before { content: "\e133"; } } +.glyphicon-circle-arrow-down { &:before { content: "\e134"; } } +.glyphicon-globe { &:before { content: "\e135"; } } +.glyphicon-wrench { &:before { content: "\e136"; } } +.glyphicon-tasks { &:before { content: "\e137"; } } +.glyphicon-filter { &:before { content: "\e138"; } } +.glyphicon-briefcase { &:before { content: "\e139"; } } +.glyphicon-fullscreen { &:before { content: "\e140"; } } +.glyphicon-dashboard { &:before { content: "\e141"; } } +.glyphicon-paperclip { &:before { content: "\e142"; } } +.glyphicon-heart-empty { &:before { content: "\e143"; } } +.glyphicon-link { &:before { content: "\e144"; } } +.glyphicon-phone { &:before { content: "\e145"; } } +.glyphicon-pushpin { &:before { content: "\e146"; } } +.glyphicon-usd { &:before { content: "\e148"; } } +.glyphicon-gbp { &:before { content: "\e149"; } } +.glyphicon-sort { &:before { content: "\e150"; } } +.glyphicon-sort-by-alphabet { &:before { content: "\e151"; } } +.glyphicon-sort-by-alphabet-alt { &:before { content: "\e152"; } } +.glyphicon-sort-by-order { &:before { content: "\e153"; } } +.glyphicon-sort-by-order-alt { &:before { content: "\e154"; } } +.glyphicon-sort-by-attributes { &:before { content: "\e155"; } } +.glyphicon-sort-by-attributes-alt { &:before { content: "\e156"; } } +.glyphicon-unchecked { &:before { content: "\e157"; } } +.glyphicon-expand { &:before { content: "\e158"; } } +.glyphicon-collapse-down { &:before { content: "\e159"; } } +.glyphicon-collapse-up { &:before { content: "\e160"; } } +.glyphicon-log-in { &:before { content: "\e161"; } } +.glyphicon-flash { &:before { content: "\e162"; } } +.glyphicon-log-out { &:before { content: "\e163"; } } +.glyphicon-new-window { &:before { content: "\e164"; } } +.glyphicon-record { &:before { content: "\e165"; } } +.glyphicon-save { &:before { content: "\e166"; } } +.glyphicon-open { &:before { content: "\e167"; } } +.glyphicon-saved { &:before { content: "\e168"; } } +.glyphicon-import { &:before { content: "\e169"; } } +.glyphicon-export { &:before { content: "\e170"; } } +.glyphicon-send { &:before { content: "\e171"; } } +.glyphicon-floppy-disk { &:before { content: "\e172"; } } +.glyphicon-floppy-saved { &:before { content: "\e173"; } } +.glyphicon-floppy-remove { &:before { content: "\e174"; } } +.glyphicon-floppy-save { &:before { content: "\e175"; } } +.glyphicon-floppy-open { &:before { content: "\e176"; } } +.glyphicon-credit-card { &:before { content: "\e177"; } } +.glyphicon-transfer { &:before { content: "\e178"; } } +.glyphicon-cutlery { &:before { content: "\e179"; } } +.glyphicon-header { &:before { content: "\e180"; } } +.glyphicon-compressed { &:before { content: "\e181"; } } +.glyphicon-earphone { &:before { content: "\e182"; } } +.glyphicon-phone-alt { &:before { content: "\e183"; } } +.glyphicon-tower { &:before { content: "\e184"; } } +.glyphicon-stats { &:before { content: "\e185"; } } +.glyphicon-sd-video { &:before { content: "\e186"; } } +.glyphicon-hd-video { &:before { content: "\e187"; } } +.glyphicon-subtitles { &:before { content: "\e188"; } } +.glyphicon-sound-stereo { &:before { content: "\e189"; } } +.glyphicon-sound-dolby { &:before { content: "\e190"; } } +.glyphicon-sound-5-1 { &:before { content: "\e191"; } } +.glyphicon-sound-6-1 { &:before { content: "\e192"; } } +.glyphicon-sound-7-1 { &:before { content: "\e193"; } } +.glyphicon-copyright-mark { &:before { content: "\e194"; } } +.glyphicon-registration-mark { &:before { content: "\e195"; } } +.glyphicon-cloud-download { &:before { content: "\e197"; } } +.glyphicon-cloud-upload { &:before { content: "\e198"; } } +.glyphicon-tree-conifer { &:before { content: "\e199"; } } +.glyphicon-tree-deciduous { &:before { content: "\e200"; } } diff --git a/src/UI/Content/Bootstrap/grid.less b/src/UI/Content/Bootstrap/grid.less index 750d20351..e100655b7 100644 --- a/src/UI/Content/Bootstrap/grid.less +++ b/src/UI/Content/Bootstrap/grid.less @@ -3,19 +3,82 @@ // -------------------------------------------------- -// Fixed (940px) -#grid > .core(@gridColumnWidth, @gridGutterWidth); +// Container widths +// +// Set the container width, and override it for fixed navbars in media queries. -// Fluid (940px) -#grid > .fluid(@fluidGridColumnWidth, @fluidGridGutterWidth); +.container { + .container-fixed(); -// Reset utility classes due to specificity -[class*="span"].hide, -.row-fluid [class*="span"].hide { - display: none; + @media (min-width: @screen-sm-min) { + width: @container-sm; + } + @media (min-width: @screen-md-min) { + width: @container-md; + } + @media (min-width: @screen-lg-min) { + width: @container-lg; + } } -[class*="span"].pull-right, -.row-fluid [class*="span"].pull-right { - float: right; + +// Fluid container +// +// Utilizes the mixin meant for fixed width containers, but without any defined +// width for fluid, full width layouts. + +.container-fluid { + .container-fixed(); +} + + +// Row +// +// Rows contain and clear the floats of your columns. + +.row { + .make-row(); +} + + +// Columns +// +// Common styles for small and large grid columns + +.make-grid-columns(); + + +// Extra small grid +// +// Columns, offsets, pushes, and pulls for extra small devices like +// smartphones. + +.make-grid(xs); + + +// Small grid +// +// Columns, offsets, pushes, and pulls for the small device range, from phones +// to tablets. + +@media (min-width: @screen-sm-min) { + .make-grid(sm); +} + + +// Medium grid +// +// Columns, offsets, pushes, and pulls for the desktop device range. + +@media (min-width: @screen-md-min) { + .make-grid(md); +} + + +// Large grid +// +// Columns, offsets, pushes, and pulls for the large desktop device range. + +@media (min-width: @screen-lg-min) { + .make-grid(lg); } diff --git a/src/UI/Content/Bootstrap/hero-unit.less b/src/UI/Content/Bootstrap/hero-unit.less deleted file mode 100644 index 763d86aee..000000000 --- a/src/UI/Content/Bootstrap/hero-unit.less +++ /dev/null @@ -1,25 +0,0 @@ -// -// Hero unit -// -------------------------------------------------- - - -.hero-unit { - padding: 60px; - margin-bottom: 30px; - font-size: 18px; - font-weight: 200; - line-height: @baseLineHeight * 1.5; - color: @heroUnitLeadColor; - background-color: @heroUnitBackground; - .border-radius(6px); - h1 { - margin-bottom: 0; - font-size: 60px; - line-height: 1; - color: @heroUnitHeadingColor; - letter-spacing: -1px; - } - li { - line-height: @baseLineHeight * 1.5; // Reset since we specify in type.less - } -} diff --git a/src/UI/Content/Bootstrap/input-groups.less b/src/UI/Content/Bootstrap/input-groups.less new file mode 100644 index 000000000..a11147463 --- /dev/null +++ b/src/UI/Content/Bootstrap/input-groups.less @@ -0,0 +1,162 @@ +// +// Input groups +// -------------------------------------------------- + +// Base styles +// ------------------------- +.input-group { + position: relative; // For dropdowns + display: table; + border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table + + // Undo padding and float of grid classes + &[class*="col-"] { + float: none; + padding-left: 0; + padding-right: 0; + } + + .form-control { + // Ensure that the input is always above the *appended* addon button for + // proper border colors. + position: relative; + z-index: 2; + + // IE9 fubars the placeholder attribute in text inputs and the arrows on + // select elements in input groups. To fix it, we float the input. Details: + // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855 + float: left; + + width: 100%; + margin-bottom: 0; + } +} + +// Sizing options +// +// Remix the default form control sizing classes into new ones for easier +// manipulation. + +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { .input-lg(); } +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { .input-sm(); } + + +// Display as table-cell +// ------------------------- +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; + + &:not(:first-child):not(:last-child) { + border-radius: 0; + } +} +// Addon and addon wrapper for buttons +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; // Match the inputs +} + +// Text input groups +// ------------------------- +.input-group-addon { + padding: @padding-base-vertical @padding-base-horizontal; + font-size: @font-size-base; + font-weight: normal; + line-height: 1; + color: @input-color; + text-align: center; + background-color: @input-group-addon-bg; + border: 1px solid @input-group-addon-border-color; + border-radius: @border-radius-base; + + // Sizing + &.input-sm { + padding: @padding-small-vertical @padding-small-horizontal; + font-size: @font-size-small; + border-radius: @border-radius-small; + } + &.input-lg { + padding: @padding-large-vertical @padding-large-horizontal; + font-size: @font-size-large; + border-radius: @border-radius-large; + } + + // Nuke default margins from checkboxes and radios to vertically center within. + input[type="radio"], + input[type="checkbox"] { + margin-top: 0; + } +} + +// Reset rounded corners +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + .border-right-radius(0); +} +.input-group-addon:first-child { + border-right: 0; +} +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + .border-left-radius(0); +} +.input-group-addon:last-child { + border-left: 0; +} + +// Button input groups +// ------------------------- +.input-group-btn { + position: relative; + // Jankily prevent input button groups from wrapping with `white-space` and + // `font-size` in combination with `inline-block` on buttons. + font-size: 0; + white-space: nowrap; + + // Negative margin for spacing, position for bringing hovered/focused/actived + // element above the siblings. + > .btn { + position: relative; + + .btn { + margin-left: -1px; + } + // Bring the "active" button to the front + &:hover, + &:focus, + &:active { + z-index: 2; + } + } + + // Negative margin to only have a 1px border between the two + &:first-child { + > .btn, + > .btn-group { + margin-right: -1px; + } + } + &:last-child { + > .btn, + > .btn-group { + margin-left: -1px; + } + } +} diff --git a/src/UI/Content/Bootstrap/jumbotron.less b/src/UI/Content/Bootstrap/jumbotron.less new file mode 100644 index 000000000..a15e16971 --- /dev/null +++ b/src/UI/Content/Bootstrap/jumbotron.less @@ -0,0 +1,44 @@ +// +// Jumbotron +// -------------------------------------------------- + + +.jumbotron { + padding: @jumbotron-padding; + margin-bottom: @jumbotron-padding; + color: @jumbotron-color; + background-color: @jumbotron-bg; + + h1, + .h1 { + color: @jumbotron-heading-color; + } + p { + margin-bottom: (@jumbotron-padding / 2); + font-size: @jumbotron-font-size; + font-weight: 200; + } + + .container & { + border-radius: @border-radius-large; // Only round corners at higher resolutions if contained in a container + } + + .container { + max-width: 100%; + } + + @media screen and (min-width: @screen-sm-min) { + padding-top: (@jumbotron-padding * 1.6); + padding-bottom: (@jumbotron-padding * 1.6); + + .container & { + padding-left: (@jumbotron-padding * 2); + padding-right: (@jumbotron-padding * 2); + } + + h1, + .h1 { + font-size: (@font-size-base * 4.5); + } + } +} diff --git a/src/UI/Content/Bootstrap/labels-badges.less b/src/UI/Content/Bootstrap/labels-badges.less deleted file mode 100644 index bc321fe5c..000000000 --- a/src/UI/Content/Bootstrap/labels-badges.less +++ /dev/null @@ -1,84 +0,0 @@ -// -// Labels and badges -// -------------------------------------------------- - - -// Base classes -.label, -.badge { - display: inline-block; - padding: 2px 4px; - font-size: @baseFontSize * .846; - font-weight: bold; - line-height: 14px; // ensure proper line-height if floated - color: @white; - vertical-align: baseline; - white-space: nowrap; - text-shadow: 0 -1px 0 rgba(0,0,0,.25); - background-color: @grayLight; -} -// Set unique padding and border-radii -.label { - .border-radius(3px); -} -.badge { - padding-left: 9px; - padding-right: 9px; - .border-radius(9px); -} - -// Empty labels/badges collapse -.label, -.badge { - &:empty { - display: none; - } -} - -// Hover/focus state, but only for links -a { - &.label:hover, - &.label:focus, - &.badge:hover, - &.badge:focus { - color: @white; - text-decoration: none; - cursor: pointer; - } -} - -// Colors -// Only give background-color difference to links (and to simplify, we don't qualifty with `a` but [href] attribute) -.label, -.badge { - // Important (red) - &-important { background-color: @errorText; } - &-important[href] { background-color: darken(@errorText, 10%); } - // Warnings (orange) - &-warning { background-color: @orange; } - &-warning[href] { background-color: darken(@orange, 10%); } - // Success (green) - &-success { background-color: @successText; } - &-success[href] { background-color: darken(@successText, 10%); } - // Info (turquoise) - &-info { background-color: @infoText; } - &-info[href] { background-color: darken(@infoText, 10%); } - // Inverse (black) - &-inverse { background-color: @grayDark; } - &-inverse[href] { background-color: darken(@grayDark, 10%); } -} - -// Quick fix for labels/badges in buttons -.btn { - .label, - .badge { - position: relative; - top: -1px; - } -} -.btn-mini { - .label, - .badge { - top: 0; - } -} diff --git a/src/UI/Content/Bootstrap/labels.less b/src/UI/Content/Bootstrap/labels.less new file mode 100644 index 000000000..5db1ed12c --- /dev/null +++ b/src/UI/Content/Bootstrap/labels.less @@ -0,0 +1,64 @@ +// +// Labels +// -------------------------------------------------- + +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: @label-color; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; + + // Add hover effects, but only for links + &[href] { + &:hover, + &:focus { + color: @label-link-hover-color; + text-decoration: none; + cursor: pointer; + } + } + + // Empty labels collapse automatically (not available in IE8) + &:empty { + display: none; + } + + // Quick fix for labels in buttons + .btn & { + position: relative; + top: -1px; + } +} + +// Colors +// Contextual variations (linked labels get darker on :hover) + +.label-default { + .label-variant(@label-default-bg); +} + +.label-primary { + .label-variant(@label-primary-bg); +} + +.label-success { + .label-variant(@label-success-bg); +} + +.label-info { + .label-variant(@label-info-bg); +} + +.label-warning { + .label-variant(@label-warning-bg); +} + +.label-danger { + .label-variant(@label-danger-bg); +} diff --git a/src/UI/Content/Bootstrap/layouts.less b/src/UI/Content/Bootstrap/layouts.less deleted file mode 100644 index 24a206211..000000000 --- a/src/UI/Content/Bootstrap/layouts.less +++ /dev/null @@ -1,16 +0,0 @@ -// -// Layouts -// -------------------------------------------------- - - -// Container (centered, fixed-width layouts) -.container { - .container-fixed(); -} - -// Fluid layouts (left aligned, with sidebar, min- & max-width content) -.container-fluid { - padding-right: @gridGutterWidth; - padding-left: @gridGutterWidth; - .clearfix(); -} \ No newline at end of file diff --git a/src/UI/Content/Bootstrap/list-group.less b/src/UI/Content/Bootstrap/list-group.less new file mode 100644 index 000000000..3343f8e5e --- /dev/null +++ b/src/UI/Content/Bootstrap/list-group.less @@ -0,0 +1,110 @@ +// +// List groups +// -------------------------------------------------- + + +// Base class +// +// Easily usable on <ul>, <ol>, or <div>. + +.list-group { + // No need to set list-style: none; since .list-group-item is block level + margin-bottom: 20px; + padding-left: 0; // reset padding because ul and ol +} + + +// Individual list items +// +// Use on `li`s or `div`s within the `.list-group` parent. + +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + // Place the border on the list items and negative margin up for better styling + margin-bottom: -1px; + background-color: @list-group-bg; + border: 1px solid @list-group-border; + + // Round the first and last items + &:first-child { + .border-top-radius(@list-group-border-radius); + } + &:last-child { + margin-bottom: 0; + .border-bottom-radius(@list-group-border-radius); + } + + // Align badges within list items + > .badge { + float: right; + } + > .badge + .badge { + margin-right: 5px; + } +} + + +// Linked list items +// +// Use anchor elements instead of `li`s or `div`s to create linked list items. +// Includes an extra `.active` modifier class for showing selected items. + +a.list-group-item { + color: @list-group-link-color; + + .list-group-item-heading { + color: @list-group-link-heading-color; + } + + // Hover state + &:hover, + &:focus { + text-decoration: none; + background-color: @list-group-hover-bg; + } + + // Active class on item itself, not parent + &.active, + &.active:hover, + &.active:focus { + z-index: 2; // Place active items above their siblings for proper border styling + color: @list-group-active-color; + background-color: @list-group-active-bg; + border-color: @list-group-active-border; + + // Force color to inherit for custom content + .list-group-item-heading { + color: inherit; + } + .list-group-item-text { + color: @list-group-active-text-color; + } + } +} + + +// Contextual variants +// +// Add modifier classes to change text and background color on individual items. +// Organizationally, this must come after the `:hover` states. + +.list-group-item-variant(success; @state-success-bg; @state-success-text); +.list-group-item-variant(info; @state-info-bg; @state-info-text); +.list-group-item-variant(warning; @state-warning-bg; @state-warning-text); +.list-group-item-variant(danger; @state-danger-bg; @state-danger-text); + + +// Custom content options +// +// Extra classes for creating well-formatted content within `.list-group-item`s. + +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} diff --git a/src/UI/Content/Bootstrap/media.less b/src/UI/Content/Bootstrap/media.less index e461e446d..5ad22cd6d 100644 --- a/src/UI/Content/Bootstrap/media.less +++ b/src/UI/Content/Bootstrap/media.less @@ -10,7 +10,6 @@ .media, .media-body { overflow: hidden; - *overflow: visible; zoom: 1; } @@ -37,11 +36,13 @@ // Media image alignment // ------------------------- -.media > .pull-left { - margin-right: 10px; -} -.media > .pull-right { - margin-left: 10px; +.media { + > .pull-left { + margin-right: 10px; + } + > .pull-right { + margin-left: 10px; + } } @@ -50,6 +51,6 @@ // Undo default ul/ol styles .media-list { - margin-left: 0; + padding-left: 0; list-style: none; } diff --git a/src/UI/Content/Bootstrap/mixins.less b/src/UI/Content/Bootstrap/mixins.less index 79d889219..71723dba4 100644 --- a/src/UI/Content/Bootstrap/mixins.less +++ b/src/UI/Content/Bootstrap/mixins.less @@ -3,96 +3,64 @@ // -------------------------------------------------- -// UTILITY MIXINS -// -------------------------------------------------- +// Utilities +// ------------------------- // Clearfix -// -------- -// For clearing floats like a boss h5bp.com/q -.clearfix { - *zoom: 1; +// Source: http://nicolasgallagher.com/micro-clearfix-hack/ +// +// For modern browsers +// 1. The space content is one way to avoid an Opera bug when the +// contenteditable attribute is included anywhere else in the document. +// Otherwise it causes space to appear at the top and bottom of elements +// that are clearfixed. +// 2. The use of `table` rather than `block` is only necessary if using +// `:before` to contain the top-margins of child elements. +.clearfix() { &:before, &:after { - display: table; - content: ""; - // Fixes Opera/contenteditable bug: - // http://nicolasgallagher.com/micro-clearfix-hack/#comment-36952 - line-height: 0; + content: " "; // 1 + display: table; // 2 } &:after { clear: both; } } -// Webkit-style focus -// ------------------ +// WebKit-style focus .tab-focus() { // Default - outline: thin dotted #333; - // Webkit + outline: thin dotted; + // WebKit outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } // Center-align a block level element -// ---------------------------------- .center-block() { display: block; margin-left: auto; margin-right: auto; } -// IE7 inline-block -// ---------------- -.ie7-inline-block() { - *display: inline; /* IE7 inline-block hack */ - *zoom: 1; -} - -// IE7 likes to collapse whitespace on either side of the inline-block elements. -// Ems because we're attempting to match the width of a space character. Left -// version is for form buttons, which typically come after other elements, and -// right version is for icons, which come before. Applying both is ok, but it will -// mean that space between those elements will be .6em (~2 space characters) in IE7, -// instead of the 1 space in other browsers. -.ie7-restore-left-whitespace() { - *margin-left: .3em; - - &:first-child { - *margin-left: 0; - } -} - -.ie7-restore-right-whitespace() { - *margin-right: .3em; -} - // Sizing shortcuts -// ------------------------- -.size(@height, @width) { +.size(@width; @height) { width: @width; height: @height; } .square(@size) { - .size(@size, @size); + .size(@size; @size); } // Placeholder text -// ------------------------- -.placeholder(@color: @placeholderText) { - &:-moz-placeholder { - color: @color; - } - &:-ms-input-placeholder { - color: @color; - } - &::-webkit-input-placeholder { - color: @color; - } +.placeholder(@color: @input-color-placeholder) { + &::-moz-placeholder { color: @color; // Firefox + opacity: 1; } // See https://github.com/twbs/bootstrap/pull/11526 + &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+ + &::-webkit-input-placeholder { color: @color; } // Safari and Chrome } // Text overflow -// ------------------------- // Requires inline-block or block for proper styling .text-overflow() { overflow: hidden; @@ -101,99 +69,25 @@ } // CSS image replacement -// ------------------------- +// +// Heads up! v3 launched with with only `.hide-text()`, but per our pattern for +// mixins being reused as classes with the same name, this doesn't hold up. As +// of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`. Note +// that we cannot chain the mixins together in Less, so they are repeated. +// // Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757 -.hide-text { - font: 0/0 a; + +// Deprecated as of v3.0.1 (will be removed in v4) +.hide-text() { + font: ~"0/0" a; color: transparent; text-shadow: none; background-color: transparent; border: 0; } - - -// FONTS -// -------------------------------------------------- - -#font { - #family { - .serif() { - font-family: @serifFontFamily; - } - .sans-serif() { - font-family: @sansFontFamily; - } - .monospace() { - font-family: @monoFontFamily; - } - } - .shorthand(@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight) { - font-size: @size; - font-weight: @weight; - line-height: @lineHeight; - } - .serif(@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight) { - #font > #family > .serif; - #font > .shorthand(@size, @weight, @lineHeight); - } - .sans-serif(@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight) { - #font > #family > .sans-serif; - #font > .shorthand(@size, @weight, @lineHeight); - } - .monospace(@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight) { - #font > #family > .monospace; - #font > .shorthand(@size, @weight, @lineHeight); - } -} - - -// FORMS -// -------------------------------------------------- - -// Block level inputs -.input-block-level { - display: block; - width: 100%; - min-height: @inputHeight; // Make inputs at least the height of their button counterpart (base line-height + padding + border) - .box-sizing(border-box); // Makes inputs behave like true block-level elements -} - - - -// Mixin for form field states -.formFieldState(@textColor: #555, @borderColor: #ccc, @backgroundColor: #f5f5f5) { - // Set the text color - .control-label, - .help-block, - .help-inline { - color: @textColor; - } - // Style inputs accordingly - .checkbox, - .radio, - input, - select, - textarea { - color: @textColor; - } - input, - select, - textarea { - border-color: @borderColor; - .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work - &:focus { - border-color: darken(@borderColor, 10%); - @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@borderColor, 20%); - .box-shadow(@shadow); - } - } - // Give a small background color for input-prepend/-append - .input-prepend .add-on, - .input-append .add-on { - color: @textColor; - background-color: @backgroundColor; - border-color: @textColor; - } +// New mixin to use as of v3.0.1 +.text-hide() { + .hide-text(); } @@ -201,144 +95,150 @@ // CSS3 PROPERTIES // -------------------------------------------------- -// Border Radius -.border-radius(@radius) { - -webkit-border-radius: @radius; - -moz-border-radius: @radius; - border-radius: @radius; -} - -// Single Corner Border Radius -.border-top-left-radius(@radius) { - -webkit-border-top-left-radius: @radius; - -moz-border-radius-topleft: @radius; - border-top-left-radius: @radius; -} -.border-top-right-radius(@radius) { - -webkit-border-top-right-radius: @radius; - -moz-border-radius-topright: @radius; - border-top-right-radius: @radius; -} -.border-bottom-right-radius(@radius) { - -webkit-border-bottom-right-radius: @radius; - -moz-border-radius-bottomright: @radius; - border-bottom-right-radius: @radius; -} -.border-bottom-left-radius(@radius) { - -webkit-border-bottom-left-radius: @radius; - -moz-border-radius-bottomleft: @radius; - border-bottom-left-radius: @radius; -} - -// Single Side Border Radius +// Single side border-radius .border-top-radius(@radius) { - .border-top-right-radius(@radius); - .border-top-left-radius(@radius); + border-top-right-radius: @radius; + border-top-left-radius: @radius; } .border-right-radius(@radius) { - .border-top-right-radius(@radius); - .border-bottom-right-radius(@radius); + border-bottom-right-radius: @radius; + border-top-right-radius: @radius; } .border-bottom-radius(@radius) { - .border-bottom-right-radius(@radius); - .border-bottom-left-radius(@radius); + border-bottom-right-radius: @radius; + border-bottom-left-radius: @radius; } .border-left-radius(@radius) { - .border-top-left-radius(@radius); - .border-bottom-left-radius(@radius); + border-bottom-left-radius: @radius; + border-top-left-radius: @radius; } // Drop shadows +// +// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's +// supported browsers that have box shadow capabilities now support the +// standard `box-shadow` property. .box-shadow(@shadow) { - -webkit-box-shadow: @shadow; - -moz-box-shadow: @shadow; + -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1 box-shadow: @shadow; } // Transitions .transition(@transition) { -webkit-transition: @transition; - -moz-transition: @transition; - -o-transition: @transition; transition: @transition; } +.transition-property(@transition-property) { + -webkit-transition-property: @transition-property; + transition-property: @transition-property; +} .transition-delay(@transition-delay) { -webkit-transition-delay: @transition-delay; - -moz-transition-delay: @transition-delay; - -o-transition-delay: @transition-delay; transition-delay: @transition-delay; } .transition-duration(@transition-duration) { -webkit-transition-duration: @transition-duration; - -moz-transition-duration: @transition-duration; - -o-transition-duration: @transition-duration; transition-duration: @transition-duration; } +.transition-transform(@transition) { + -webkit-transition: -webkit-transform @transition; + -moz-transition: -moz-transform @transition; + -o-transition: -o-transform @transition; + transition: transform @transition; +} // Transformations .rotate(@degrees) { -webkit-transform: rotate(@degrees); - -moz-transform: rotate(@degrees); - -ms-transform: rotate(@degrees); - -o-transform: rotate(@degrees); + -ms-transform: rotate(@degrees); // IE9 only transform: rotate(@degrees); } -.scale(@ratio) { - -webkit-transform: scale(@ratio); - -moz-transform: scale(@ratio); - -ms-transform: scale(@ratio); - -o-transform: scale(@ratio); - transform: scale(@ratio); +.scale(@ratio; @ratio-y...) { + -webkit-transform: scale(@ratio, @ratio-y); + -ms-transform: scale(@ratio, @ratio-y); // IE9 only + transform: scale(@ratio, @ratio-y); } -.translate(@x, @y) { +.translate(@x; @y) { -webkit-transform: translate(@x, @y); - -moz-transform: translate(@x, @y); - -ms-transform: translate(@x, @y); - -o-transform: translate(@x, @y); + -ms-transform: translate(@x, @y); // IE9 only transform: translate(@x, @y); } -.skew(@x, @y) { +.skew(@x; @y) { -webkit-transform: skew(@x, @y); - -moz-transform: skew(@x, @y); - -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twitter/bootstrap/issues/4885 - -o-transform: skew(@x, @y); + -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+ transform: skew(@x, @y); - -webkit-backface-visibility: hidden; // See https://github.com/twitter/bootstrap/issues/5319 } -.translate3d(@x, @y, @z) { +.translate3d(@x; @y; @z) { -webkit-transform: translate3d(@x, @y, @z); - -moz-transform: translate3d(@x, @y, @z); - -o-transform: translate3d(@x, @y, @z); transform: translate3d(@x, @y, @z); } +.rotateX(@degrees) { + -webkit-transform: rotateX(@degrees); + -ms-transform: rotateX(@degrees); // IE9 only + transform: rotateX(@degrees); +} +.rotateY(@degrees) { + -webkit-transform: rotateY(@degrees); + -ms-transform: rotateY(@degrees); // IE9 only + transform: rotateY(@degrees); +} +.perspective(@perspective) { + -webkit-perspective: @perspective; + -moz-perspective: @perspective; + perspective: @perspective; +} +.perspective-origin(@perspective) { + -webkit-perspective-origin: @perspective; + -moz-perspective-origin: @perspective; + perspective-origin: @perspective; +} +.transform-origin(@origin) { + -webkit-transform-origin: @origin; + -moz-transform-origin: @origin; + -ms-transform-origin: @origin; // IE9 only + transform-origin: @origin; +} + +// Animations +.animation(@animation) { + -webkit-animation: @animation; + animation: @animation; +} +.animation-name(@name) { + -webkit-animation-name: @name; + animation-name: @name; +} +.animation-duration(@duration) { + -webkit-animation-duration: @duration; + animation-duration: @duration; +} +.animation-timing-function(@timing-function) { + -webkit-animation-timing-function: @timing-function; + animation-timing-function: @timing-function; +} +.animation-delay(@delay) { + -webkit-animation-delay: @delay; + animation-delay: @delay; +} +.animation-iteration-count(@iteration-count) { + -webkit-animation-iteration-count: @iteration-count; + animation-iteration-count: @iteration-count; +} +.animation-direction(@direction) { + -webkit-animation-direction: @direction; + animation-direction: @direction; +} + // Backface visibility // Prevent browsers from flickering when using CSS 3D transforms. -// Default value is `visible`, but can be changed to `hidden -// See git pull https://github.com/dannykeane/bootstrap.git backface-visibility for examples +// Default value is `visible`, but can be changed to `hidden` .backface-visibility(@visibility){ - -webkit-backface-visibility: @visibility; - -moz-backface-visibility: @visibility; - backface-visibility: @visibility; + -webkit-backface-visibility: @visibility; + -moz-backface-visibility: @visibility; + backface-visibility: @visibility; } -// Background clipping -// Heads up: FF 3.6 and under need "padding" instead of "padding-box" -.background-clip(@clip) { - -webkit-background-clip: @clip; - -moz-background-clip: @clip; - background-clip: @clip; -} - -// Background sizing -.background-size(@size) { - -webkit-background-size: @size; - -moz-background-size: @size; - -o-background-size: @size; - background-size: @size; -} - - // Box sizing .box-sizing(@boxmodel) { -webkit-box-sizing: @boxmodel; @@ -351,8 +251,7 @@ .user-select(@select) { -webkit-user-select: @select; -moz-user-select: @select; - -ms-user-select: @select; - -o-user-select: @select; + -ms-user-select: @select; // IE10+ user-select: @select; } @@ -363,13 +262,13 @@ } // CSS3 Content Columns -.content-columns(@columnCount, @columnGap: @gridGutterWidth) { - -webkit-column-count: @columnCount; - -moz-column-count: @columnCount; - column-count: @columnCount; - -webkit-column-gap: @columnGap; - -moz-column-gap: @columnGap; - column-gap: @columnGap; +.content-columns(@column-count; @column-gap: @grid-gutter-width) { + -webkit-column-count: @column-count; + -moz-column-count: @column-count; + column-count: @column-count; + -webkit-column-gap: @column-gap; + -moz-column-gap: @column-gap; + column-gap: @column-gap; } // Optional hyphenation @@ -377,167 +276,359 @@ word-wrap: break-word; -webkit-hyphens: @mode; -moz-hyphens: @mode; - -ms-hyphens: @mode; + -ms-hyphens: @mode; // IE10+ -o-hyphens: @mode; hyphens: @mode; } // Opacity .opacity(@opacity) { - opacity: @opacity / 100; - filter: ~"alpha(opacity=@{opacity})"; + opacity: @opacity; + // IE8 filter + @opacity-ie: (@opacity * 100); + filter: ~"alpha(opacity=@{opacity-ie})"; } -// BACKGROUNDS +// GRADIENTS // -------------------------------------------------- -// Add an alphatransparency value to any background or border color (via Elyse Holladay) -#translucent { - .background(@color: @white, @alpha: 1) { - background-color: hsla(hue(@color), saturation(@color), lightness(@color), @alpha); - } - .border(@color: @white, @alpha: 1) { - border-color: hsla(hue(@color), saturation(@color), lightness(@color), @alpha); - .background-clip(padding-box); - } -} - -// Gradient Bar Colors for buttons and alerts -.gradientBar(@primaryColor, @secondaryColor, @textColor: #fff, @textShadow: 0 -1px 0 rgba(0,0,0,.25)) { - color: @textColor; - text-shadow: @textShadow; - #gradient > .vertical(@primaryColor, @secondaryColor); - border-color: @secondaryColor @secondaryColor darken(@secondaryColor, 15%); - border-color: rgba(0,0,0,.1) rgba(0,0,0,.1) fadein(rgba(0,0,0,.1), 15%); -} - -// Gradients #gradient { - .horizontal(@startColor: #555, @endColor: #333) { - background-color: @endColor; - background-image: -moz-linear-gradient(left, @startColor, @endColor); // FF 3.6+ - background-image: -webkit-gradient(linear, 0 0, 100% 0, from(@startColor), to(@endColor)); // Safari 4+, Chrome 2+ - background-image: -webkit-linear-gradient(left, @startColor, @endColor); // Safari 5.1+, Chrome 10+ - background-image: -o-linear-gradient(left, @startColor, @endColor); // Opera 11.10 - background-image: linear-gradient(to right, @startColor, @endColor); // Standard, IE10 + + // Horizontal gradient, from left to right + // + // Creates two color stops, start and end, by specifying a color and position for each color stop. + // Color stops are not available in IE9 and below. + .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) { + background-image: -webkit-linear-gradient(left, color-stop(@start-color @start-percent), color-stop(@end-color @end-percent)); // Safari 5.1-6, Chrome 10+ + background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ background-repeat: repeat-x; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",argb(@startColor),argb(@endColor))); // IE9 and down - } - .vertical(@startColor: #555, @endColor: #333) { - background-color: mix(@startColor, @endColor, 60%); - background-image: -moz-linear-gradient(top, @startColor, @endColor); // FF 3.6+ - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(@startColor), to(@endColor)); // Safari 4+, Chrome 2+ - background-image: -webkit-linear-gradient(top, @startColor, @endColor); // Safari 5.1+, Chrome 10+ - background-image: -o-linear-gradient(top, @startColor, @endColor); // Opera 11.10 - background-image: linear-gradient(to bottom, @startColor, @endColor); // Standard, IE10 - background-repeat: repeat-x; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@startColor),argb(@endColor))); // IE9 and down - } - .directional(@startColor: #555, @endColor: #333, @deg: 45deg) { - background-color: @endColor; - background-repeat: repeat-x; - background-image: -moz-linear-gradient(@deg, @startColor, @endColor); // FF 3.6+ - background-image: -webkit-linear-gradient(@deg, @startColor, @endColor); // Safari 5.1+, Chrome 10+ - background-image: -o-linear-gradient(@deg, @startColor, @endColor); // Opera 11.10 - background-image: linear-gradient(@deg, @startColor, @endColor); // Standard, IE10 - } - .horizontal-three-colors(@startColor: #00b3ee, @midColor: #7a43b6, @colorStop: 50%, @endColor: #c3325f) { - background-color: mix(@midColor, @endColor, 80%); - background-image: -webkit-gradient(left, linear, 0 0, 0 100%, from(@startColor), color-stop(@colorStop, @midColor), to(@endColor)); - background-image: -webkit-linear-gradient(left, @startColor, @midColor @colorStop, @endColor); - background-image: -moz-linear-gradient(left, @startColor, @midColor @colorStop, @endColor); - background-image: -o-linear-gradient(left, @startColor, @midColor @colorStop, @endColor); - background-image: linear-gradient(to right, @startColor, @midColor @colorStop, @endColor); - background-repeat: no-repeat; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@startColor),argb(@endColor))); // IE9 and down, gets no color-stop at all for proper fallback + filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",argb(@start-color),argb(@end-color))); // IE9 and down } - .vertical-three-colors(@startColor: #00b3ee, @midColor: #7a43b6, @colorStop: 50%, @endColor: #c3325f) { - background-color: mix(@midColor, @endColor, 80%); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(@startColor), color-stop(@colorStop, @midColor), to(@endColor)); - background-image: -webkit-linear-gradient(@startColor, @midColor @colorStop, @endColor); - background-image: -moz-linear-gradient(top, @startColor, @midColor @colorStop, @endColor); - background-image: -o-linear-gradient(@startColor, @midColor @colorStop, @endColor); - background-image: linear-gradient(@startColor, @midColor @colorStop, @endColor); - background-repeat: no-repeat; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@startColor),argb(@endColor))); // IE9 and down, gets no color-stop at all for proper fallback + // Vertical gradient, from top to bottom + // + // Creates two color stops, start and end, by specifying a color and position for each color stop. + // Color stops are not available in IE9 and below. + .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) { + background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+ + background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ + background-repeat: repeat-x; + filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@start-color),argb(@end-color))); // IE9 and down } - .radial(@innerColor: #555, @outerColor: #333) { - background-color: @outerColor; - background-image: -webkit-gradient(radial, center center, 0, center center, 460, from(@innerColor), to(@outerColor)); - background-image: -webkit-radial-gradient(circle, @innerColor, @outerColor); - background-image: -moz-radial-gradient(circle, @innerColor, @outerColor); - background-image: -o-radial-gradient(circle, @innerColor, @outerColor); + + .directional(@start-color: #555; @end-color: #333; @deg: 45deg) { + background-repeat: repeat-x; + background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+ + background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ + } + .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) { + background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color); + background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color); + background-repeat: no-repeat; + filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback + } + .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) { + background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color); + background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color); + background-repeat: no-repeat; + filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback + } + .radial(@inner-color: #555; @outer-color: #333) { + background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color); + background-image: radial-gradient(circle, @inner-color, @outer-color); background-repeat: no-repeat; } - .striped(@color: #555, @angle: 45deg) { - background-color: @color; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(.25, rgba(255,255,255,.15)), color-stop(.25, transparent), color-stop(.5, transparent), color-stop(.5, rgba(255,255,255,.15)), color-stop(.75, rgba(255,255,255,.15)), color-stop(.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); + .striped(@color: rgba(255,255,255,.15); @angle: 45deg) { + background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent); + background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent); } } + // Reset filters for IE +// +// When you need to remove a gradient background, do not forget to use this to reset +// the IE filter for IE9 and below. .reset-filter() { filter: e(%("progid:DXImageTransform.Microsoft.gradient(enabled = false)")); } +// Retina images +// +// Short retina mixin for setting background-image and -size + +.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) { + background-image: url("@{file-1x}"); + + @media + only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and ( min--moz-device-pixel-ratio: 2), + only screen and ( -o-min-device-pixel-ratio: 2/1), + only screen and ( min-device-pixel-ratio: 2), + only screen and ( min-resolution: 192dpi), + only screen and ( min-resolution: 2dppx) { + background-image: url("@{file-2x}"); + background-size: @width-1x @height-1x; + } +} + + +// Responsive image +// +// Keep images from scaling beyond the width of their parents. + +.img-responsive(@display: block) { + display: @display; + max-width: 100%; // Part 1: Set a maximum relative to the parent + height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching +} + + // COMPONENT MIXINS // -------------------------------------------------- // Horizontal dividers // ------------------------- // Dividers (basically an hr) within dropdowns and nav lists -.nav-divider(@top: #e5e5e5, @bottom: @white) { - // IE7 needs a set width since we gave a height. Restricting just - // to IE7 to keep the 1px left/right space in other browsers. - // It is unclear where IE is getting the extra space that we need - // to negative-margin away, but so it goes. - *width: 100%; +.nav-divider(@color: #e5e5e5) { height: 1px; - margin: ((@baseLineHeight / 2) - 1) 1px; // 8px 1px - *margin: -5px 0 5px; + margin: ((@line-height-computed / 2) - 1) 0; overflow: hidden; - background-color: @top; - border-bottom: 1px solid @bottom; + background-color: @color; } -// Button backgrounds -// ------------------ -.buttonBackground(@startColor, @endColor, @textColor: #fff, @textShadow: 0 -1px 0 rgba(0,0,0,.25)) { - // gradientBar will set the background to a pleasing blend of these, to support IE<=9 - .gradientBar(@startColor, @endColor, @textColor, @textShadow); - *background-color: @endColor; /* Darken IE7 buttons by default so they stand out more given they won't have borders */ - .reset-filter(); +// Panels +// ------------------------- +.panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) { + border-color: @border; - // in these cases the gradient won't cover the background, so we override - &:hover, &:focus, &:active, &.active, &.disabled, &[disabled] { - color: @textColor; - background-color: @endColor; - *background-color: darken(@endColor, 5%); + & > .panel-heading { + color: @heading-text-color; + background-color: @heading-bg-color; + border-color: @heading-border; + + + .panel-collapse .panel-body { + border-top-color: @border; + } + } + & > .panel-footer { + + .panel-collapse .panel-body { + border-bottom-color: @border; + } + } +} + +// Alerts +// ------------------------- +.alert-variant(@background; @border; @text-color) { + background-color: @background; + border-color: @border; + color: @text-color; + + hr { + border-top-color: darken(@border, 5%); + } + .alert-link { + color: darken(@text-color, 10%); + } +} + +// Tables +// ------------------------- +.table-row-variant(@state; @background) { + // Exact selectors below required to override `.table-striped` and prevent + // inheritance to nested tables. + .table > thead > tr, + .table > tbody > tr, + .table > tfoot > tr { + > td.@{state}, + > th.@{state}, + &.@{state} > td, + &.@{state} > th { + background-color: @background; + } } - // IE 7 + 8 can't handle box-shadow to show active, so we darken a bit ourselves + // Hover states for `.table-hover` + // Note: this is not available for cells or rows within `thead` or `tfoot`. + .table-hover > tbody > tr { + > td.@{state}:hover, + > th.@{state}:hover, + &.@{state}:hover > td, + &.@{state}:hover > th { + background-color: darken(@background, 5%); + } + } +} + +// List Groups +// ------------------------- +.list-group-item-variant(@state; @background; @color) { + .list-group-item-@{state} { + color: @color; + background-color: @background; + + a& { + color: @color; + + .list-group-item-heading { color: inherit; } + + &:hover, + &:focus { + color: @color; + background-color: darken(@background, 5%); + } + &.active, + &.active:hover, + &.active:focus { + color: #fff; + background-color: @color; + border-color: @color; + } + } + } +} + +// Button variants +// ------------------------- +// Easily pump out default styles, as well as :hover, :focus, :active, +// and disabled options for all buttons +.button-variant(@color; @background; @border) { + color: @color; + background-color: @background; + border-color: @border; + + &:hover, + &:focus, &:active, - &.active { - background-color: darken(@endColor, 10%) e("\9"); + &.active, + .open .dropdown-toggle& { + color: @color; + background-color: darken(@background, 8%); + border-color: darken(@border, 12%); + } + &:active, + &.active, + .open .dropdown-toggle& { + background-image: none; + } + &.disabled, + &[disabled], + fieldset[disabled] & { + &, + &:hover, + &:focus, + &:active, + &.active { + background-color: @background; + border-color: @border; + } + } + + .badge { + color: @background; + background-color: @color; + } +} + +// Button sizes +// ------------------------- +.button-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) { + padding: @padding-vertical @padding-horizontal; + font-size: @font-size; + line-height: @line-height; + border-radius: @border-radius; +} + +// Pagination +// ------------------------- +.pagination-size(@padding-vertical; @padding-horizontal; @font-size; @border-radius) { + > li { + > a, + > span { + padding: @padding-vertical @padding-horizontal; + font-size: @font-size; + } + &:first-child { + > a, + > span { + .border-left-radius(@border-radius); + } + } + &:last-child { + > a, + > span { + .border-right-radius(@border-radius); + } + } + } +} + +// Labels +// ------------------------- +.label-variant(@color) { + background-color: @color; + &[href] { + &:hover, + &:focus { + background-color: darken(@color, 10%); + } + } +} + +// Contextual backgrounds +// ------------------------- +.bg-variant(@color) { + background-color: @color; + a&:hover { + background-color: darken(@color, 10%); + } +} + +// Typography +// ------------------------- +.text-emphasis-variant(@color) { + color: @color; + a&:hover { + color: darken(@color, 10%); } } // Navbar vertical align // ------------------------- // Vertically center elements in the navbar. -// Example: an element has a height of 30px, so write out `.navbarVerticalAlign(30px);` to calculate the appropriate top margin. -.navbarVerticalAlign(@elementHeight) { - margin-top: (@navbarHeight - @elementHeight) / 2; +// Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin. +.navbar-vertical-align(@element-height) { + margin-top: ((@navbar-height - @element-height) / 2); + margin-bottom: ((@navbar-height - @element-height) / 2); } +// Progress bars +// ------------------------- +.progress-bar-variant(@color) { + background-color: @color; + .progress-striped & { + #gradient > .striped(); + } +} + +// Responsive utilities +// ------------------------- +// More easily include all the states for responsive-utilities.less. +.responsive-visibility() { + display: block !important; + table& { display: table; } + tr& { display: table-row !important; } + th&, + td& { display: table-cell !important; } +} + +.responsive-invisibility() { + display: none !important; +} // Grid System @@ -547,156 +638,292 @@ .container-fixed() { margin-right: auto; margin-left: auto; - .clearfix(); + padding-left: (@grid-gutter-width / 2); + padding-right: (@grid-gutter-width / 2); + &:extend(.clearfix all); } -// Table columns -.tableColumns(@columnSpan: 1) { - float: none; // undo default grid column styles - width: ((@gridColumnWidth) * @columnSpan) + (@gridGutterWidth * (@columnSpan - 1)) - 16; // 16 is total padding on left and right of table cells - margin-left: 0; // undo default grid column styles +// Creates a wrapper for a series of columns +.make-row(@gutter: @grid-gutter-width) { + margin-left: (@gutter / -2); + margin-right: (@gutter / -2); + &:extend(.clearfix all); } -// Make a Grid -// Use .makeRow and .makeColumn to assign semantic layouts grid system behavior -.makeRow() { - margin-left: @gridGutterWidth * -1; - .clearfix(); -} -.makeColumn(@columns: 1, @offset: 0) { +// Generate the extra small columns +.make-xs-column(@columns; @gutter: @grid-gutter-width) { + position: relative; float: left; - margin-left: (@gridColumnWidth * @offset) + (@gridGutterWidth * (@offset - 1)) + (@gridGutterWidth * 2); - width: (@gridColumnWidth * @columns) + (@gridGutterWidth * (@columns - 1)); + width: percentage((@columns / @grid-columns)); + min-height: 1px; + padding-left: (@gutter / 2); + padding-right: (@gutter / 2); +} +.make-xs-column-offset(@columns) { + @media (min-width: @screen-xs-min) { + margin-left: percentage((@columns / @grid-columns)); + } +} +.make-xs-column-push(@columns) { + @media (min-width: @screen-xs-min) { + left: percentage((@columns / @grid-columns)); + } +} +.make-xs-column-pull(@columns) { + @media (min-width: @screen-xs-min) { + right: percentage((@columns / @grid-columns)); + } } -// The Grid -#grid { - .core (@gridColumnWidth, @gridGutterWidth) { +// Generate the small columns +.make-sm-column(@columns; @gutter: @grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: (@gutter / 2); + padding-right: (@gutter / 2); - .spanX (@index) when (@index > 0) { - .span@{index} { .span(@index); } - .spanX(@index - 1); + @media (min-width: @screen-sm-min) { + float: left; + width: percentage((@columns / @grid-columns)); + } +} +.make-sm-column-offset(@columns) { + @media (min-width: @screen-sm-min) { + margin-left: percentage((@columns / @grid-columns)); + } +} +.make-sm-column-push(@columns) { + @media (min-width: @screen-sm-min) { + left: percentage((@columns / @grid-columns)); + } +} +.make-sm-column-pull(@columns) { + @media (min-width: @screen-sm-min) { + right: percentage((@columns / @grid-columns)); + } +} + + +// Generate the medium columns +.make-md-column(@columns; @gutter: @grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: (@gutter / 2); + padding-right: (@gutter / 2); + + @media (min-width: @screen-md-min) { + float: left; + width: percentage((@columns / @grid-columns)); + } +} +.make-md-column-offset(@columns) { + @media (min-width: @screen-md-min) { + margin-left: percentage((@columns / @grid-columns)); + } +} +.make-md-column-push(@columns) { + @media (min-width: @screen-md-min) { + left: percentage((@columns / @grid-columns)); + } +} +.make-md-column-pull(@columns) { + @media (min-width: @screen-md-min) { + right: percentage((@columns / @grid-columns)); + } +} + + +// Generate the large columns +.make-lg-column(@columns; @gutter: @grid-gutter-width) { + position: relative; + min-height: 1px; + padding-left: (@gutter / 2); + padding-right: (@gutter / 2); + + @media (min-width: @screen-lg-min) { + float: left; + width: percentage((@columns / @grid-columns)); + } +} +.make-lg-column-offset(@columns) { + @media (min-width: @screen-lg-min) { + margin-left: percentage((@columns / @grid-columns)); + } +} +.make-lg-column-push(@columns) { + @media (min-width: @screen-lg-min) { + left: percentage((@columns / @grid-columns)); + } +} +.make-lg-column-pull(@columns) { + @media (min-width: @screen-lg-min) { + right: percentage((@columns / @grid-columns)); + } +} + + +// Framework grid generation +// +// Used only by Bootstrap to generate the correct number of grid classes given +// any value of `@grid-columns`. + +.make-grid-columns() { + // Common styles for all sizes of grid columns, widths 1-12 + .col(@index) when (@index = 1) { // initial + @item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}"; + .col((@index + 1), @item); + } + .col(@index, @list) when (@index =< @grid-columns) { // general; "=<" isn't a typo + @item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}"; + .col((@index + 1), ~"@{list}, @{item}"); + } + .col(@index, @list) when (@index > @grid-columns) { // terminal + @{list} { + position: relative; + // Prevent columns from collapsing when empty + min-height: 1px; + // Inner gutter via padding + padding-left: (@grid-gutter-width / 2); + padding-right: (@grid-gutter-width / 2); } - .spanX (0) {} + } + .col(1); // kickstart it +} - .offsetX (@index) when (@index > 0) { - .offset@{index} { .offset(@index); } - .offsetX(@index - 1); - } - .offsetX (0) {} - - .offset (@columns) { - margin-left: (@gridColumnWidth * @columns) + (@gridGutterWidth * (@columns + 1)); - } - - .span (@columns) { - width: (@gridColumnWidth * @columns) + (@gridGutterWidth * (@columns - 1)); - } - - .row { - margin-left: @gridGutterWidth * -1; - .clearfix(); - } - - [class*="span"] { +.float-grid-columns(@class) { + .col(@index) when (@index = 1) { // initial + @item: ~".col-@{class}-@{index}"; + .col((@index + 1), @item); + } + .col(@index, @list) when (@index =< @grid-columns) { // general + @item: ~".col-@{class}-@{index}"; + .col((@index + 1), ~"@{list}, @{item}"); + } + .col(@index, @list) when (@index > @grid-columns) { // terminal + @{list} { float: left; - min-height: 1px; // prevent collapsing columns - margin-left: @gridGutterWidth; } - - // Set the container width, and override it for fixed navbars in media queries - .container, - .navbar-static-top .container, - .navbar-fixed-top .container, - .navbar-fixed-bottom .container { .span(@gridColumns); } - - // generate .spanX and .offsetX - .spanX (@gridColumns); - .offsetX (@gridColumns); - } + .col(1); // kickstart it +} - .fluid (@fluidGridColumnWidth, @fluidGridGutterWidth) { - - .spanX (@index) when (@index > 0) { - .span@{index} { .span(@index); } - .spanX(@index - 1); - } - .spanX (0) {} - - .offsetX (@index) when (@index > 0) { - .offset@{index} { .offset(@index); } - .offset@{index}:first-child { .offsetFirstChild(@index); } - .offsetX(@index - 1); - } - .offsetX (0) {} - - .offset (@columns) { - margin-left: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)) + (@fluidGridGutterWidth*2); - *margin-left: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)) - (.5 / @gridRowWidth * 100 * 1%) + (@fluidGridGutterWidth*2) - (.5 / @gridRowWidth * 100 * 1%); - } - - .offsetFirstChild (@columns) { - margin-left: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)) + (@fluidGridGutterWidth); - *margin-left: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)) - (.5 / @gridRowWidth * 100 * 1%) + @fluidGridGutterWidth - (.5 / @gridRowWidth * 100 * 1%); - } - - .span (@columns) { - width: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)); - *width: (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1)) - (.5 / @gridRowWidth * 100 * 1%); - } - - .row-fluid { - width: 100%; - .clearfix(); - [class*="span"] { - .input-block-level(); - float: left; - margin-left: @fluidGridGutterWidth; - *margin-left: @fluidGridGutterWidth - (.5 / @gridRowWidth * 100 * 1%); - } - [class*="span"]:first-child { - margin-left: 0; - } - - // Space grid-sized controls properly if multiple per line - .controls-row [class*="span"] + [class*="span"] { - margin-left: @fluidGridGutterWidth; - } - - // generate .spanX and .offsetX - .spanX (@gridColumns); - .offsetX (@gridColumns); - } - - } - - .input(@gridColumnWidth, @gridGutterWidth) { - - .spanX (@index) when (@index > 0) { - input.span@{index}, textarea.span@{index}, .uneditable-input.span@{index} { .span(@index); } - .spanX(@index - 1); - } - .spanX (0) {} - - .span(@columns) { - width: ((@gridColumnWidth) * @columns) + (@gridGutterWidth * (@columns - 1)) - 14; - } - - input, - textarea, - .uneditable-input { - margin-left: 0; // override margin-left from core grid system - } - - // Space grid-sized controls properly if multiple per line - .controls-row [class*="span"] + [class*="span"] { - margin-left: @gridGutterWidth; - } - - // generate .spanX - .spanX (@gridColumns); - +.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) { + .col-@{class}-@{index} { + width: percentage((@index / @grid-columns)); + } +} +.calc-grid-column(@index, @class, @type) when (@type = push) { + .col-@{class}-push-@{index} { + left: percentage((@index / @grid-columns)); + } +} +.calc-grid-column(@index, @class, @type) when (@type = pull) { + .col-@{class}-pull-@{index} { + right: percentage((@index / @grid-columns)); + } +} +.calc-grid-column(@index, @class, @type) when (@type = offset) { + .col-@{class}-offset-@{index} { + margin-left: percentage((@index / @grid-columns)); + } +} + +// Basic looping in LESS +.loop-grid-columns(@index, @class, @type) when (@index >= 0) { + .calc-grid-column(@index, @class, @type); + // next iteration + .loop-grid-columns((@index - 1), @class, @type); +} + +// Create grid for specific class +.make-grid(@class) { + .float-grid-columns(@class); + .loop-grid-columns(@grid-columns, @class, width); + .loop-grid-columns(@grid-columns, @class, pull); + .loop-grid-columns(@grid-columns, @class, push); + .loop-grid-columns(@grid-columns, @class, offset); +} + +// Form validation states +// +// Used in forms.less to generate the form validation CSS for warnings, errors, +// and successes. + +.form-control-validation(@text-color: #555; @border-color: #ccc; @background-color: #f5f5f5) { + // Color the label and help text + .help-block, + .control-label, + .radio, + .checkbox, + .radio-inline, + .checkbox-inline { + color: @text-color; + } + // Set the border and box shadow on specific inputs to match + .form-control { + border-color: @border-color; + .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work + &:focus { + border-color: darken(@border-color, 10%); + @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@border-color, 20%); + .box-shadow(@shadow); + } + } + // Set validation states also for addons + .input-group-addon { + color: @text-color; + border-color: @border-color; + background-color: @background-color; + } + // Optional feedback icon + .form-control-feedback { + color: @text-color; + } +} + +// Form control focus state +// +// Generate a customized focus state and for any input with the specified color, +// which defaults to the `@input-focus-border` variable. +// +// We highly encourage you to not customize the default value, but instead use +// this to tweak colors on an as-needed basis. This aesthetic change is based on +// WebKit's default styles, but applicable to a wider range of browsers. Its +// usability and accessibility should be taken into account with any change. +// +// Example usage: change the default blue border and shadow to white for better +// contrast against a dark gray background. + +.form-control-focus(@color: @input-border-focus) { + @color-rgba: rgba(red(@color), green(@color), blue(@color), .6); + &:focus { + border-color: @color; + outline: 0; + .box-shadow(~"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}"); + } +} + +// Form control sizing +// +// Relative text size, padding, and border-radii changes for form controls. For +// horizontal sizing, wrap controls in the predefined grid classes. `<select>` +// element gets special love because it's special, and that's a fact! + +.input-size(@input-height; @padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) { + height: @input-height; + padding: @padding-vertical @padding-horizontal; + font-size: @font-size; + line-height: @line-height; + border-radius: @border-radius; + + select& { + height: @input-height; + line-height: @input-height; + } + + textarea&, + select[multiple]& { + height: auto; } } diff --git a/src/UI/Content/Bootstrap/modals.less b/src/UI/Content/Bootstrap/modals.less index 66392f87d..21cdee0f4 100644 --- a/src/UI/Content/Bootstrap/modals.less +++ b/src/UI/Content/Bootstrap/modals.less @@ -2,82 +2,107 @@ // Modals // -------------------------------------------------- -// Background +// .modal-open - body class for killing the scroll +// .modal - container to scroll within +// .modal-dialog - positioning shell for the actual modal +// .modal-content - actual modal w/ bg and corners and shit + +// Kill the scroll on the body +.modal-open { + overflow: hidden; +} + +// Container that the modal scrolls within +.modal { + display: none; + overflow: auto; + overflow-y: scroll; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: @zindex-modal; + -webkit-overflow-scrolling: touch; + + // Prevent Chrome on Windows from adding a focus outline. For details, see + // https://github.com/twbs/bootstrap/pull/10951. + outline: 0; + + // When fading in the modal, animate it to slide down + &.fade .modal-dialog { + .translate(0, -25%); + .transition-transform(~"0.3s ease-out"); + } + &.in .modal-dialog { .translate(0, 0)} +} + +// Shell div to position the modal with bottom padding +.modal-dialog { + position: relative; + width: auto; + margin: 10px; +} + +// Actual modal +.modal-content { + position: relative; + background-color: @modal-content-bg; + border: 1px solid @modal-content-fallback-border-color; //old browsers fallback (ie8 etc) + border: 1px solid @modal-content-border-color; + border-radius: @border-radius-large; + .box-shadow(0 3px 9px rgba(0,0,0,.5)); + background-clip: padding-box; + // Remove focus outline from opened modal + outline: none; +} + +// Modal background .modal-backdrop { position: fixed; top: 0; right: 0; bottom: 0; left: 0; - z-index: @zindexModalBackdrop; - background-color: @black; + z-index: @zindex-modal-background; + background-color: @modal-backdrop-bg; // Fade for backdrop - &.fade { opacity: 0; } + &.fade { .opacity(0); } + &.in { .opacity(@modal-backdrop-opacity); } } -.modal-backdrop, -.modal-backdrop.fade.in { - .opacity(80); -} - -// Base modal -.modal { - position: fixed; - top: 10%; - left: 50%; - z-index: @zindexModal; - width: 1000px; - margin-left: -500px; - background-color: @white; - border: 1px solid #999; - border: 1px solid rgba(0,0,0,.3); - *border: 1px solid #999; /* IE6-7 */ - .border-radius(6px); - .box-shadow(0 3px 7px rgba(0,0,0,0.3)); - .background-clip(padding-box); - // Remove focus outline from opened modal - outline: none; - - &.fade { - .transition(e('opacity .3s linear, top .3s ease-out')); - top: -25%; - } - &.fade.in { top: 10%; } -} +// Modal header +// Top section of the modal w/ title and dismiss .modal-header { - padding: 9px 15px; - border-bottom: 1px solid #eee; - // Close icon - .close { margin-top: 2px; } - // Heading - h3 { - margin: 0; - line-height: 30px; - } + padding: @modal-title-padding; + border-bottom: 1px solid @modal-header-border-color; + min-height: (@modal-title-padding + @modal-title-line-height); +} +// Close icon +.modal-header .close { + margin-top: -2px; } -// Body (where all modal content resides) +// Title text within header +.modal-title { + margin: 0; + line-height: @modal-title-line-height; +} + +// Modal body +// Where all modal content resides (sibling of .modal-header and .modal-footer) .modal-body { position: relative; - overflow-y: auto; - max-height: 400px; - padding: 15px; -} -// Remove bottom margin if need be -.modal-form { - margin-bottom: 0; + padding: @modal-inner-padding; } // Footer (for actions) .modal-footer { - padding: 14px 15px 15px; - margin-bottom: 0; + margin-top: 15px; + padding: (@modal-inner-padding - 1) @modal-inner-padding @modal-inner-padding; text-align: right; // right align buttons - background-color: #f5f5f5; - border-top: 1px solid #ddd; - .border-radius(0 0 6px 6px); - .box-shadow(inset 0 1px 0 @white); - .clearfix(); // clear it in case folks use .pull-* classes on buttons + border-top: 1px solid @modal-footer-border-color; + &:extend(.clearfix all); // clear it in case folks use .pull-* classes on buttons // Properly space out buttons .btn + .btn { @@ -93,3 +118,22 @@ margin-left: 0; } } + +// Scale up the modal +@media (min-width: @screen-sm-min) { + // Automatically set modal's width for larger viewports + .modal-dialog { + width: @modal-md; + margin: 30px auto; + } + .modal-content { + .box-shadow(0 5px 15px rgba(0,0,0,.5)); + } + + // Modal sizes + .modal-sm { width: @modal-sm; } +} + +@media (min-width: @screen-md-min) { + .modal-lg { width: @modal-lg; } +} diff --git a/src/UI/Content/Bootstrap/navbar.less b/src/UI/Content/Bootstrap/navbar.less index 93d09bcad..8c4c210b2 100644 --- a/src/UI/Content/Bootstrap/navbar.less +++ b/src/UI/Content/Bootstrap/navbar.less @@ -1,497 +1,616 @@ // -// Navbars (Redux) +// Navbars // -------------------------------------------------- -// COMMON STYLES -// ------------- +// Wrapper and base class +// +// Provide a static navbar from which we expand to create full-width, fixed, and +// other navbar variations. -// Base class and wrapper .navbar { - overflow: visible; - margin-bottom: @baseLineHeight; - - // Fix for IE7's bad z-indexing so dropdowns don't appear below content that follows the navbar - *position: relative; - *z-index: 2; -} - -// Inner for background effects -// Gradient is applied to its own element because overflow visible is not honored by IE when filter is present -.navbar-inner { - min-height: @navbarHeight; - padding-left: 20px; - padding-right: 20px; - #gradient > .vertical(@navbarBackgroundHighlight, @navbarBackground); - border: 1px solid @navbarBorder; - .border-radius(@baseBorderRadius); - .box-shadow(0 1px 4px rgba(0,0,0,.065)); + position: relative; + min-height: @navbar-height; // Ensure a navbar always shows (e.g., without a .navbar-brand in collapsed mode) + margin-bottom: @navbar-margin-bottom; + border: 1px solid transparent; // Prevent floats from breaking the navbar - .clearfix(); -} + &:extend(.clearfix all); -// Set width to auto for default container -// We then reset it for fixed navbars in the #gridSystem mixin -.navbar .container { - width: auto; -} - -// Override the default collapsed state -.nav-collapse.collapse { - height: auto; - overflow: visible; -} - - -// Brand: website or project name -// ------------------------- -.navbar .brand { - float: left; - display: block; - // Vertically center the text given @navbarHeight - padding: ((@navbarHeight - @baseLineHeight) / 2) 20px ((@navbarHeight - @baseLineHeight) / 2); - margin-left: -20px; // negative indent to left-align the text down the page - font-size: 20px; - font-weight: 200; - color: @navbarBrandColor; - text-shadow: 0 1px 0 @navbarBackgroundHighlight; - &:hover, - &:focus { - text-decoration: none; + @media (min-width: @grid-float-breakpoint) { + border-radius: @navbar-border-radius; } } -// Plain text in topbar -// ------------------------- -.navbar-text { - margin-bottom: 0; - line-height: @navbarHeight; - color: @navbarText; -} -// Janky solution for now to account for links outside the .nav -// ------------------------- -.navbar-link { - color: @navbarLinkColor; - &:hover, - &:focus { - color: @navbarLinkColorHover; +// Navbar heading +// +// Groups `.navbar-brand` and `.navbar-toggle` into a single component for easy +// styling of responsive aspects. + +.navbar-header { + &:extend(.clearfix all); + + @media (min-width: @grid-float-breakpoint) { + float: left; } } -// Dividers in navbar -// ------------------------- -.navbar .divider-vertical { - height: @navbarHeight; - margin: 0 9px; - border-left: 1px solid @navbarBackground; - border-right: 1px solid @navbarBackgroundHighlight; -} -// Buttons in navbar -// ------------------------- -.navbar .btn, -.navbar .btn-group { - .navbarVerticalAlign(30px); // Vertically center in navbar -} -.navbar .btn-group .btn, -.navbar .input-prepend .btn, -.navbar .input-append .btn, -.navbar .input-prepend .btn-group, -.navbar .input-append .btn-group { - margin-top: 0; // then undo the margin here so we don't accidentally double it -} +// Navbar collapse (body) +// +// Group your navbar content into this for easy collapsing and expanding across +// various device sizes. By default, this content is collapsed when <768px, but +// will expand past that for a horizontal display. +// +// To start (on mobile devices) the navbar links, forms, and buttons are stacked +// vertically and include a `max-height` to overflow in case you have too much +// content for the user's viewport. -// Navbar forms -// ------------------------- -.navbar-form { - margin-bottom: 0; // remove default bottom margin - .clearfix(); - input, - select, - .radio, - .checkbox { - .navbarVerticalAlign(30px); // Vertically center in navbar +.navbar-collapse { + max-height: @navbar-collapse-max-height; + overflow-x: visible; + padding-right: @navbar-padding-horizontal; + padding-left: @navbar-padding-horizontal; + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255,255,255,.1); + &:extend(.clearfix all); + -webkit-overflow-scrolling: touch; + + &.in { + overflow-y: auto; } - input, - select, - .btn { - display: inline-block; - margin-bottom: 0; - } - input[type="image"], - input[type="checkbox"], - input[type="radio"] { - margin-top: 3px; - } - .input-append, - .input-prepend { - margin-top: 5px; - white-space: nowrap; // preven two items from separating within a .navbar-form that has .pull-left - input { - margin-top: 0; // remove the margin on top since it's on the parent + + @media (min-width: @grid-float-breakpoint) { + width: auto; + border-top: 0; + box-shadow: none; + + &.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; // Override default setting + overflow: visible !important; + } + + &.in { + overflow-y: visible; + } + + // Undo the collapse side padding for navbars with containers to ensure + // alignment of right-aligned contents. + .navbar-fixed-top &, + .navbar-static-top &, + .navbar-fixed-bottom & { + padding-left: 0; + padding-right: 0; } } } -// Navbar search -// ------------------------- -.navbar-search { - position: relative; - float: left; - .navbarVerticalAlign(30px); // Vertically center in navbar - margin-bottom: 0; - .search-query { - margin-bottom: 0; - padding: 4px 14px; - #font > .sans-serif(13px, normal, 1); - .border-radius(15px); // redeclare because of specificity of the type attribute + +// Both navbar header and collapse +// +// When a container is present, change the behavior of the header and collapse. + +.container, +.container-fluid { + > .navbar-header, + > .navbar-collapse { + margin-right: -@navbar-padding-horizontal; + margin-left: -@navbar-padding-horizontal; + + @media (min-width: @grid-float-breakpoint) { + margin-right: 0; + margin-left: 0; + } } } +// +// Navbar alignment options +// +// Display the navbar across the entirety of the page or fixed it to the top or +// bottom of the page. -// Static navbar -// ------------------------- - +// Static top (unfixed, but 100% wide) navbar .navbar-static-top { - position: static; - margin-bottom: 0; // remove 18px margin for default navbar - .navbar-inner { - .border-radius(0); + z-index: @zindex-navbar; + border-width: 0 0 1px; + + @media (min-width: @grid-float-breakpoint) { + border-radius: 0; } } - - -// Fixed navbar -// ------------------------- - -// Shared (top/bottom) styles +// Fix the top/bottom navbars when screen real estate supports it .navbar-fixed-top, .navbar-fixed-bottom { position: fixed; right: 0; left: 0; - z-index: @zindexFixedNavbar; - margin-bottom: 0; // remove 18px margin for default navbar -} -.navbar-fixed-top .navbar-inner, -.navbar-static-top .navbar-inner { - border-width: 0 0 1px; -} -.navbar-fixed-bottom .navbar-inner { - border-width: 1px 0 0; -} -.navbar-fixed-top .navbar-inner, -.navbar-fixed-bottom .navbar-inner { - padding-left: 0; - padding-right: 0; - .border-radius(0); -} + z-index: @zindex-navbar-fixed; -// Reset container width -// Required here as we reset the width earlier on and the grid mixins don't override early enough -.navbar-static-top .container, -.navbar-fixed-top .container, -.navbar-fixed-bottom .container { - #grid > .core > .span(@gridColumns); + // Undo the rounded corners + @media (min-width: @grid-float-breakpoint) { + border-radius: 0; + } } - -// Fixed to top .navbar-fixed-top { top: 0; + border-width: 0 0 1px; } -.navbar-fixed-top, -.navbar-static-top { - .navbar-inner { - .box-shadow(~"0 1px 10px rgba(0,0,0,.1)"); - } -} - -// Fixed to bottom .navbar-fixed-bottom { bottom: 0; - .navbar-inner { - .box-shadow(~"0 -1px 10px rgba(0,0,0,.1)"); + margin-bottom: 0; // override .navbar defaults + border-width: 1px 0 0; +} + + +// Brand/project name + +.navbar-brand { + float: left; + padding: @navbar-padding-vertical @navbar-padding-horizontal; + font-size: @font-size-large; + line-height: @line-height-computed; + height: @navbar-height; + + &:hover, + &:focus { + text-decoration: none; + } + + @media (min-width: @grid-float-breakpoint) { + .navbar > .container &, + .navbar > .container-fluid & { + margin-left: -@navbar-padding-horizontal; + } } } +// Navbar toggle +// +// Custom button for toggling the `.navbar-collapse`, powered by the collapse +// JavaScript plugin. -// NAVIGATION -// ---------- - -.navbar .nav { +.navbar-toggle { position: relative; - left: 0; - display: block; - float: left; - margin: 0 10px 0 0; -} -.navbar .nav.pull-right { - float: right; // redeclare due to specificity - margin-right: 0; // remove margin on float right nav -} -.navbar .nav > li { - float: left; -} - -// Links -.navbar .nav > li > a { - float: none; - // Vertically center the text given @navbarHeight - padding: ((@navbarHeight - @baseLineHeight) / 2) 15px ((@navbarHeight - @baseLineHeight) / 2); - color: @navbarLinkColor; - text-decoration: none; - text-shadow: 0 1px 0 @navbarBackgroundHighlight; -} -.navbar .nav .dropdown-toggle .caret { - margin-top: 8px; -} - -// Hover/focus -.navbar .nav > li > a:focus, -.navbar .nav > li > a:hover { - background-color: @navbarLinkBackgroundHover; // "transparent" is default to differentiate :hover/:focus from .active - color: @navbarLinkColorHover; - text-decoration: none; -} - -// Active nav items -.navbar .nav > .active > a, -.navbar .nav > .active > a:hover, -.navbar .nav > .active > a:focus { - color: @navbarLinkColorActive; - text-decoration: none; - background-color: @navbarLinkBackgroundActive; - .box-shadow(inset 0 3px 8px rgba(0,0,0,.125)); -} - -// Navbar button for toggling navbar items in responsive layouts -// These definitions need to come after '.navbar .btn' -.navbar .btn-navbar { - display: none; float: right; - padding: 7px 10px; - margin-left: 5px; - margin-right: 5px; - .buttonBackground(darken(@navbarBackgroundHighlight, 5%), darken(@navbarBackground, 5%)); - .box-shadow(~"inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.075)"); -} -.navbar .btn-navbar .icon-bar { - display: block; - width: 18px; - height: 2px; - background-color: #f5f5f5; - .border-radius(1px); - .box-shadow(0 1px 0 rgba(0,0,0,.25)); -} -.btn-navbar .icon-bar + .icon-bar { - margin-top: 3px; -} + margin-right: @navbar-padding-horizontal; + padding: 9px 10px; + .navbar-vertical-align(34px); + background-color: transparent; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid transparent; + border-radius: @border-radius-base; - - -// Dropdown menus -// -------------- - -// Menu position and menu carets -.navbar .nav > li > .dropdown-menu { - &:before { - content: ''; - display: inline-block; - border-left: 7px solid transparent; - border-right: 7px solid transparent; - border-bottom: 7px solid #ccc; - border-bottom-color: @dropdownBorder; - position: absolute; - top: -7px; - left: 9px; + // We remove the `outline` here, but later compensate by attaching `:hover` + // styles to `:focus`. + &:focus { + outline: none; } - &:after { - content: ''; - display: inline-block; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-bottom: 6px solid @dropdownBackground; - position: absolute; - top: -6px; - left: 10px; - } -} -// Menu position and menu caret support for dropups via extra dropup class -.navbar-fixed-bottom .nav > li > .dropdown-menu { - &:before { - border-top: 7px solid #ccc; - border-top-color: @dropdownBorder; - border-bottom: 0; - bottom: -7px; - top: auto; - } - &:after { - border-top: 6px solid @dropdownBackground; - border-bottom: 0; - bottom: -6px; - top: auto; - } -} -// Caret should match text color on hover/focus -.navbar .nav li.dropdown > a:hover .caret, -.navbar .nav li.dropdown > a:focus .caret { - border-top-color: @navbarLinkColorHover; - border-bottom-color: @navbarLinkColorHover; -} - -// Remove background color from open dropdown -.navbar .nav li.dropdown.open > .dropdown-toggle, -.navbar .nav li.dropdown.active > .dropdown-toggle, -.navbar .nav li.dropdown.open.active > .dropdown-toggle { - background-color: @navbarLinkBackgroundActive; - color: @navbarLinkColorActive; -} -.navbar .nav li.dropdown > .dropdown-toggle .caret { - border-top-color: @navbarLinkColor; - border-bottom-color: @navbarLinkColor; -} -.navbar .nav li.dropdown.open > .dropdown-toggle .caret, -.navbar .nav li.dropdown.active > .dropdown-toggle .caret, -.navbar .nav li.dropdown.open.active > .dropdown-toggle .caret { - border-top-color: @navbarLinkColorActive; - border-bottom-color: @navbarLinkColorActive; -} - -// Right aligned menus need alt position -.navbar .pull-right > li > .dropdown-menu, -.navbar .nav > li > .dropdown-menu.pull-right { - left: auto; - right: 0; - &:before { - left: auto; - right: 12px; + // Bars + .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; } - &:after { - left: auto; - right: 13px; + .icon-bar + .icon-bar { + margin-top: 4px; } - .dropdown-menu { - left: auto; - right: 100%; - margin-left: 0; - margin-right: -1px; - .border-radius(6px 0 6px 6px); + + @media (min-width: @grid-float-breakpoint) { + display: none; } } -// Inverted navbar -// ------------------------- +// Navbar nav links +// +// Builds on top of the `.nav` components with its own modifier class to make +// the nav the full height of the horizontal nav (above 768px). -.navbar-inverse { +.navbar-nav { + margin: (@navbar-padding-vertical / 2) -@navbar-padding-horizontal; - .navbar-inner { - #gradient > .vertical(@navbarInverseBackgroundHighlight, @navbarInverseBackground); - border-color: @navbarInverseBorder; + > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: @line-height-computed; } - .brand, - .nav > li > a { - color: @navbarInverseLinkColor; - text-shadow: 0 -1px 0 rgba(0,0,0,.25); - &:hover, - &:focus { - color: @navbarInverseLinkColorHover; - } - } - - .brand { - color: @navbarInverseBrandColor; - } - - .navbar-text { - color: @navbarInverseText; - } - - .nav > li > a:focus, - .nav > li > a:hover { - background-color: @navbarInverseLinkBackgroundHover; - color: @navbarInverseLinkColorHover; - } - - .nav .active > a, - .nav .active > a:hover, - .nav .active > a:focus { - color: @navbarInverseLinkColorActive; - background-color: @navbarInverseLinkBackgroundActive; - } - - // Inline text links - .navbar-link { - color: @navbarInverseLinkColor; - &:hover, - &:focus { - color: @navbarInverseLinkColorHover; - } - } - - // Dividers in navbar - .divider-vertical { - border-left-color: @navbarInverseBackground; - border-right-color: @navbarInverseBackgroundHighlight; - } - - // Dropdowns - .nav li.dropdown.open > .dropdown-toggle, - .nav li.dropdown.active > .dropdown-toggle, - .nav li.dropdown.open.active > .dropdown-toggle { - background-color: @navbarInverseLinkBackgroundActive; - color: @navbarInverseLinkColorActive; - } - .nav li.dropdown > a:hover .caret, - .nav li.dropdown > a:focus .caret { - border-top-color: @navbarInverseLinkColorActive; - border-bottom-color: @navbarInverseLinkColorActive; - } - .nav li.dropdown > .dropdown-toggle .caret { - border-top-color: @navbarInverseLinkColor; - border-bottom-color: @navbarInverseLinkColor; - } - .nav li.dropdown.open > .dropdown-toggle .caret, - .nav li.dropdown.active > .dropdown-toggle .caret, - .nav li.dropdown.open.active > .dropdown-toggle .caret { - border-top-color: @navbarInverseLinkColorActive; - border-bottom-color: @navbarInverseLinkColorActive; - } - - // Navbar search - .navbar-search { - .search-query { - color: @white; - background-color: @navbarInverseSearchBackground; - border-color: @navbarInverseSearchBorder; - .box-shadow(~"inset 0 1px 2px rgba(0,0,0,.1), 0 1px 0 rgba(255,255,255,.15)"); - .transition(none); - .placeholder(@navbarInverseSearchPlaceholderColor); - - // Focus states (we use .focused since IE7-8 and down doesn't support :focus) - &:focus, - &.focused { - padding: 5px 15px; - color: @grayDark; - text-shadow: 0 1px 0 @white; - background-color: @navbarInverseSearchBackgroundFocus; - border: 0; - .box-shadow(0 0 3px rgba(0,0,0,.15)); - outline: 0; + @media (max-width: @grid-float-breakpoint-max) { + // Dropdowns get custom display when collapsed + .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + box-shadow: none; + > li > a, + .dropdown-header { + padding: 5px 15px 5px 25px; + } + > li > a { + line-height: @line-height-computed; + &:hover, + &:focus { + background-image: none; + } } } } - // Navbar collapse button - .btn-navbar { - .buttonBackground(darken(@navbarInverseBackgroundHighlight, 5%), darken(@navbarInverseBackground, 5%)); + // Uncollapse the nav + @media (min-width: @grid-float-breakpoint) { + float: left; + margin: 0; + + > li { + float: left; + > a { + padding-top: @navbar-padding-vertical; + padding-bottom: @navbar-padding-vertical; + } + } + + &.navbar-right:last-child { + margin-right: -@navbar-padding-horizontal; + } + } +} + + +// Component alignment +// +// Repurpose the pull utilities as their own navbar utilities to avoid specificity +// issues with parents and chaining. Only do this when the navbar is uncollapsed +// though so that navbar contents properly stack and align in mobile. + +@media (min-width: @grid-float-breakpoint) { + .navbar-left { .pull-left(); } + .navbar-right { .pull-right(); } +} + + +// Navbar form +// +// Extension of the `.form-inline` with some extra flavor for optimum display in +// our navbars. + +.navbar-form { + margin-left: -@navbar-padding-horizontal; + margin-right: -@navbar-padding-horizontal; + padding: 10px @navbar-padding-horizontal; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + @shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1); + .box-shadow(@shadow); + + // Mixin behavior for optimum display + .form-inline(); + + .form-group { + @media (max-width: @grid-float-breakpoint-max) { + margin-bottom: 5px; + } + } + + // Vertically center in expanded, horizontal navbar + .navbar-vertical-align(@input-height-base); + + // Undo 100% width for pull classes + @media (min-width: @grid-float-breakpoint) { + width: auto; + border: 0; + margin-left: 0; + margin-right: 0; + padding-top: 0; + padding-bottom: 0; + .box-shadow(none); + + // Outdent the form if last child to line up with content down the page + &.navbar-right:last-child { + margin-right: -@navbar-padding-horizontal; + } + } +} + + +// Dropdown menus + +// Menu position and menu carets +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + .border-top-radius(0); +} +// Menu position and menu caret support for dropups via extra dropup class +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + .border-bottom-radius(0); +} + + +// Buttons in navbars +// +// Vertically center a button within a navbar (when *not* in a form). + +.navbar-btn { + .navbar-vertical-align(@input-height-base); + + &.btn-sm { + .navbar-vertical-align(@input-height-small); + } + &.btn-xs { + .navbar-vertical-align(22); + } +} + + +// Text in navbars +// +// Add a class to make any element properly align itself vertically within the navbars. + +.navbar-text { + .navbar-vertical-align(@line-height-computed); + + @media (min-width: @grid-float-breakpoint) { + float: left; + margin-left: @navbar-padding-horizontal; + margin-right: @navbar-padding-horizontal; + + // Outdent the form if last child to line up with content down the page + &.navbar-right:last-child { + margin-right: 0; + } + } +} + +// Alternate navbars +// -------------------------------------------------- + +// Default navbar +.navbar-default { + background-color: @navbar-default-bg; + border-color: @navbar-default-border; + + .navbar-brand { + color: @navbar-default-brand-color; + &:hover, + &:focus { + color: @navbar-default-brand-hover-color; + background-color: @navbar-default-brand-hover-bg; + } + } + + .navbar-text { + color: @navbar-default-color; + } + + .navbar-nav { + > li > a { + color: @navbar-default-link-color; + + &:hover, + &:focus { + color: @navbar-default-link-hover-color; + background-color: @navbar-default-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: @navbar-default-link-active-color; + background-color: @navbar-default-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: @navbar-default-link-disabled-color; + background-color: @navbar-default-link-disabled-bg; + } + } + } + + .navbar-toggle { + border-color: @navbar-default-toggle-border-color; + &:hover, + &:focus { + background-color: @navbar-default-toggle-hover-bg; + } + .icon-bar { + background-color: @navbar-default-toggle-icon-bar-bg; + } + } + + .navbar-collapse, + .navbar-form { + border-color: @navbar-default-border; + } + + // Dropdown menu items + .navbar-nav { + // Remove background color from open dropdown + > .open > a { + &, + &:hover, + &:focus { + background-color: @navbar-default-link-active-bg; + color: @navbar-default-link-active-color; + } + } + + @media (max-width: @grid-float-breakpoint-max) { + // Dropdowns get custom display when collapsed + .open .dropdown-menu { + > li > a { + color: @navbar-default-link-color; + &:hover, + &:focus { + color: @navbar-default-link-hover-color; + background-color: @navbar-default-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: @navbar-default-link-active-color; + background-color: @navbar-default-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: @navbar-default-link-disabled-color; + background-color: @navbar-default-link-disabled-bg; + } + } + } + } + } + + + // Links in navbars + // + // Add a class to ensure links outside the navbar nav are colored correctly. + + .navbar-link { + color: @navbar-default-link-color; + &:hover { + color: @navbar-default-link-hover-color; + } + } + +} + +// Inverse navbar + +.navbar-inverse { + background-color: @navbar-inverse-bg; + border-color: @navbar-inverse-border; + + .navbar-brand { + color: @navbar-inverse-brand-color; + &:hover, + &:focus { + color: @navbar-inverse-brand-hover-color; + background-color: @navbar-inverse-brand-hover-bg; + } + } + + .navbar-text { + color: @navbar-inverse-color; + } + + .navbar-nav { + > li > a { + color: @navbar-inverse-link-color; + + &:hover, + &:focus { + color: @navbar-inverse-link-hover-color; + background-color: @navbar-inverse-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: @navbar-inverse-link-active-color; + background-color: @navbar-inverse-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: @navbar-inverse-link-disabled-color; + background-color: @navbar-inverse-link-disabled-bg; + } + } + } + + // Darken the responsive nav toggle + .navbar-toggle { + border-color: @navbar-inverse-toggle-border-color; + &:hover, + &:focus { + background-color: @navbar-inverse-toggle-hover-bg; + } + .icon-bar { + background-color: @navbar-inverse-toggle-icon-bar-bg; + } + } + + .navbar-collapse, + .navbar-form { + border-color: darken(@navbar-inverse-bg, 7%); + } + + // Dropdowns + .navbar-nav { + > .open > a { + &, + &:hover, + &:focus { + background-color: @navbar-inverse-link-active-bg; + color: @navbar-inverse-link-active-color; + } + } + + @media (max-width: @grid-float-breakpoint-max) { + // Dropdowns get custom display + .open .dropdown-menu { + > .dropdown-header { + border-color: @navbar-inverse-border; + } + .divider { + background-color: @navbar-inverse-border; + } + > li > a { + color: @navbar-inverse-link-color; + &:hover, + &:focus { + color: @navbar-inverse-link-hover-color; + background-color: @navbar-inverse-link-hover-bg; + } + } + > .active > a { + &, + &:hover, + &:focus { + color: @navbar-inverse-link-active-color; + background-color: @navbar-inverse-link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + color: @navbar-inverse-link-disabled-color; + background-color: @navbar-inverse-link-disabled-bg; + } + } + } + } + } + + .navbar-link { + color: @navbar-inverse-link-color; + &:hover { + color: @navbar-inverse-link-hover-color; + } } } diff --git a/src/UI/Content/Bootstrap/navs.less b/src/UI/Content/Bootstrap/navs.less index 01cd805bd..9e729b39f 100644 --- a/src/UI/Content/Bootstrap/navs.less +++ b/src/UI/Content/Bootstrap/navs.less @@ -3,407 +3,240 @@ // -------------------------------------------------- -// BASE CLASS -// ---------- +// Base class +// -------------------------------------------------- .nav { - margin-left: 0; - margin-bottom: @baseLineHeight; - list-style: none; -} - -// Make links block level -.nav > li > a { - display: block; -} -.nav > li > a:hover, -.nav > li > a:focus { - text-decoration: none; - background-color: @grayLighter; -} - -// Prevent IE8 from misplacing imgs -// See https://github.com/h5bp/html5-boilerplate/issues/984#issuecomment-3985989 -.nav > li > a > img { - max-width: none; -} - -// Redeclare pull classes because of specifity -.nav > .pull-right { - float: right; -} - -// Nav headers (for dropdowns and lists) -.nav-header { - display: block; - padding: 3px 15px; - font-size: 11px; - font-weight: bold; - line-height: @baseLineHeight; - color: @grayLight; - text-shadow: 0 1px 0 rgba(255,255,255,.5); - text-transform: uppercase; -} -// Space them out when they follow another list item (link) -.nav li + .nav-header { - margin-top: 9px; -} - - - -// NAV LIST -// -------- - -.nav-list { - padding-left: 15px; - padding-right: 15px; margin-bottom: 0; -} -.nav-list > li > a, -.nav-list .nav-header { - margin-left: -15px; - margin-right: -15px; - text-shadow: 0 1px 0 rgba(255,255,255,.5); -} -.nav-list > li > a { - padding: 3px 15px; -} -.nav-list > .active > a, -.nav-list > .active > a:hover, -.nav-list > .active > a:focus { - color: @white; - text-shadow: 0 -1px 0 rgba(0,0,0,.2); - background-color: @linkColor; -} -.nav-list [class^="icon-"], -.nav-list [class*=" icon-"] { - margin-right: 2px; -} -// Dividers (basically an hr) within the dropdown -.nav-list .divider { - .nav-divider(); + padding-left: 0; // Override default ul/ol + list-style: none; + &:extend(.clearfix all); + + > li { + position: relative; + display: block; + + > a { + position: relative; + display: block; + padding: @nav-link-padding; + &:hover, + &:focus { + text-decoration: none; + background-color: @nav-link-hover-bg; + } + } + + // Disabled state sets text to gray and nukes hover/tab effects + &.disabled > a { + color: @nav-disabled-link-color; + + &:hover, + &:focus { + color: @nav-disabled-link-hover-color; + text-decoration: none; + background-color: transparent; + cursor: not-allowed; + } + } + } + + // Open dropdowns + .open > a { + &, + &:hover, + &:focus { + background-color: @nav-link-hover-bg; + border-color: @link-color; + } + } + + // Nav dividers (deprecated with v3.0.1) + // + // This should have been removed in v3 with the dropping of `.nav-list`, but + // we missed it. We don't currently support this anywhere, but in the interest + // of maintaining backward compatibility in case you use it, it's deprecated. + .nav-divider { + .nav-divider(); + } + + // Prevent IE8 from misplacing imgs + // + // See https://github.com/h5bp/html5-boilerplate/issues/984#issuecomment-3985989 + > li > a > img { + max-width: none; + } } - -// TABS AND PILLS -// ------------- - -// Common styles -.nav-tabs, -.nav-pills { - .clearfix(); -} -.nav-tabs > li, -.nav-pills > li { - float: left; -} -.nav-tabs > li > a, -.nav-pills > li > a { - padding-right: 12px; - padding-left: 12px; - margin-right: 2px; - line-height: 14px; // keeps the overall height an even number -} - -// TABS -// ---- +// Tabs +// ------------------------- // Give the tabs something to sit on .nav-tabs { - border-bottom: 1px solid #ddd; -} -// Make the list-items overlay the bottom border -.nav-tabs > li { - margin-bottom: -1px; -} -// Actual tabs (as links) -.nav-tabs > li > a { - padding-top: 8px; - padding-bottom: 8px; - line-height: @baseLineHeight; - border: 1px solid transparent; - .border-radius(4px 4px 0 0); - &:hover, - &:focus { - border-color: @grayLighter @grayLighter #ddd; + border-bottom: 1px solid @nav-tabs-border-color; + > li { + float: left; + // Make the list-items overlay the bottom border + margin-bottom: -1px; + + // Actual tabs (as links) + > a { + margin-right: 2px; + line-height: @line-height-base; + border: 1px solid transparent; + border-radius: @border-radius-base @border-radius-base 0 0; + &:hover { + border-color: @nav-tabs-link-hover-border-color @nav-tabs-link-hover-border-color @nav-tabs-border-color; + } + } + + // Active state, and its :hover to override normal :hover + &.active > a { + &, + &:hover, + &:focus { + color: @nav-tabs-active-link-hover-color; + background-color: @nav-tabs-active-link-hover-bg; + border: 1px solid @nav-tabs-active-link-hover-border-color; + border-bottom-color: transparent; + cursor: default; + } + } + } + // pulling this in mainly for less shorthand + &.nav-justified { + .nav-justified(); + .nav-tabs-justified(); } } -// Active state, and it's :hover/:focus to override normal :hover/:focus -.nav-tabs > .active > a, -.nav-tabs > .active > a:hover, -.nav-tabs > .active > a:focus { - color: @gray; - background-color: @bodyBackground; - border: 1px solid #ddd; - border-bottom-color: transparent; - cursor: default; -} -// PILLS -// ----- - -// Links rendered as pills -.nav-pills > li > a { - padding-top: 8px; - padding-bottom: 8px; - margin-top: 2px; - margin-bottom: 2px; - .border-radius(5px); -} - -// Active state -.nav-pills > .active > a, -.nav-pills > .active > a:hover, -.nav-pills > .active > a:focus { - color: @white; - background-color: @linkColor; -} - - - -// STACKED NAV -// ----------- - -// Stacked tabs and pills -.nav-stacked > li { - float: none; -} -.nav-stacked > li > a { - margin-right: 0; // no need for the gap between nav items -} - -// Tabs -.nav-tabs.nav-stacked { - border-bottom: 0; -} -.nav-tabs.nav-stacked > li > a { - border: 1px solid #ddd; - .border-radius(0); -} -.nav-tabs.nav-stacked > li:first-child > a { - .border-top-radius(4px); -} -.nav-tabs.nav-stacked > li:last-child > a { - .border-bottom-radius(4px); -} -.nav-tabs.nav-stacked > li > a:hover, -.nav-tabs.nav-stacked > li > a:focus { - border-color: #ddd; - z-index: 2; -} - // Pills -.nav-pills.nav-stacked > li > a { - margin-bottom: 3px; -} -.nav-pills.nav-stacked > li:last-child > a { - margin-bottom: 1px; // decrease margin to match sizing of stacked tabs -} - - - -// DROPDOWNS -// --------- - -.nav-tabs .dropdown-menu { - .border-radius(0 0 6px 6px); // remove the top rounded corners here since there is a hard edge above the menu -} -.nav-pills .dropdown-menu { - .border-radius(6px); // make rounded corners match the pills -} - -// Default dropdown links // ------------------------- -// Make carets use linkColor to start -.nav .dropdown-toggle .caret { - border-top-color: @linkColor; - border-bottom-color: @linkColor; - margin-top: 6px; -} -.nav .dropdown-toggle:hover .caret, -.nav .dropdown-toggle:focus .caret { - border-top-color: @linkColorHover; - border-bottom-color: @linkColorHover; -} -/* move down carets for tabs */ -.nav-tabs .dropdown-toggle .caret { - margin-top: 8px; -} +.nav-pills { + > li { + float: left; -// Active dropdown links -// ------------------------- -.nav .active .dropdown-toggle .caret { - border-top-color: #fff; - border-bottom-color: #fff; -} -.nav-tabs .active .dropdown-toggle .caret { - border-top-color: @gray; - border-bottom-color: @gray; -} + // Links rendered as pills + > a { + border-radius: @nav-pills-border-radius; + } + + li { + margin-left: 2px; + } -// Active:hover/:focus dropdown links -// ------------------------- -.nav > .dropdown.active > a:hover, -.nav > .dropdown.active > a:focus { - cursor: pointer; -} - -// Open dropdowns -// ------------------------- -.nav-tabs .open .dropdown-toggle, -.nav-pills .open .dropdown-toggle, -.nav > li.dropdown.open.active > a:hover, -.nav > li.dropdown.open.active > a:focus { - color: @white; - background-color: @grayLight; - border-color: @grayLight; -} -.nav li.dropdown.open .caret, -.nav li.dropdown.open.active .caret, -.nav li.dropdown.open a:hover .caret, -.nav li.dropdown.open a:focus .caret { - border-top-color: @white; - border-bottom-color: @white; - .opacity(100); -} - -// Dropdowns in stacked tabs -.tabs-stacked .open > a:hover, -.tabs-stacked .open > a:focus { - border-color: @grayLight; -} - - - -// TABBABLE -// -------- - - -// COMMON STYLES -// ------------- - -// Clear any floats -.tabbable { - .clearfix(); -} -.tab-content { - overflow: auto; // prevent content from running below tabs -} - -// Remove border on bottom, left, right -.tabs-below > .nav-tabs, -.tabs-right > .nav-tabs, -.tabs-left > .nav-tabs { - border-bottom: 0; -} - -// Show/hide tabbable areas -.tab-content > .tab-pane, -.pill-content > .pill-pane { - display: none; -} -.tab-content > .active, -.pill-content > .active { - display: block; -} - - -// BOTTOM -// ------ - -.tabs-below > .nav-tabs { - border-top: 1px solid #ddd; -} -.tabs-below > .nav-tabs > li { - margin-top: -1px; - margin-bottom: 0; -} -.tabs-below > .nav-tabs > li > a { - .border-radius(0 0 4px 4px); - &:hover, - &:focus { - border-bottom-color: transparent; - border-top-color: #ddd; + // Active state + &.active > a { + &, + &:hover, + &:focus { + color: @nav-pills-active-link-hover-color; + background-color: @nav-pills-active-link-hover-bg; + } + } } } -.tabs-below > .nav-tabs > .active > a, -.tabs-below > .nav-tabs > .active > a:hover, -.tabs-below > .nav-tabs > .active > a:focus { - border-color: transparent #ddd #ddd #ddd; -} -// LEFT & RIGHT -// ------------ -// Common styles -.tabs-left > .nav-tabs > li, -.tabs-right > .nav-tabs > li { - float: none; -} -.tabs-left > .nav-tabs > li > a, -.tabs-right > .nav-tabs > li > a { - min-width: 74px; - margin-right: 0; - margin-bottom: 3px; -} - -// Tabs on the left -.tabs-left > .nav-tabs { - float: left; - margin-right: 19px; - border-right: 1px solid #ddd; -} -.tabs-left > .nav-tabs > li > a { - margin-right: -1px; - .border-radius(4px 0 0 4px); -} -.tabs-left > .nav-tabs > li > a:hover, -.tabs-left > .nav-tabs > li > a:focus { - border-color: @grayLighter #ddd @grayLighter @grayLighter; -} -.tabs-left > .nav-tabs .active > a, -.tabs-left > .nav-tabs .active > a:hover, -.tabs-left > .nav-tabs .active > a:focus { - border-color: #ddd transparent #ddd #ddd; - *border-right-color: @white; -} - -// Tabs on the right -.tabs-right > .nav-tabs { - float: right; - margin-left: 19px; - border-left: 1px solid #ddd; -} -.tabs-right > .nav-tabs > li > a { - margin-left: -1px; - .border-radius(0 4px 4px 0); -} -.tabs-right > .nav-tabs > li > a:hover, -.tabs-right > .nav-tabs > li > a:focus { - border-color: @grayLighter @grayLighter @grayLighter #ddd; -} -.tabs-right > .nav-tabs .active > a, -.tabs-right > .nav-tabs .active > a:hover, -.tabs-right > .nav-tabs .active > a:focus { - border-color: #ddd #ddd #ddd transparent; - *border-left-color: @white; +// Stacked pills +.nav-stacked { + > li { + float: none; + + li { + margin-top: 2px; + margin-left: 0; // no need for this gap between nav items + } + } } +// Nav variations +// -------------------------------------------------- -// DISABLED STATES -// --------------- +// Justified nav links +// ------------------------- -// Gray out text -.nav > .disabled > a { - color: @grayLight; +.nav-justified { + width: 100%; + + > li { + float: none; + > a { + text-align: center; + margin-bottom: 5px; + } + } + + > .dropdown .dropdown-menu { + top: auto; + left: auto; + } + + @media (min-width: @screen-sm-min) { + > li { + display: table-cell; + width: 1%; + > a { + margin-bottom: 0; + } + } + } } -// Nuke hover/focus effects -.nav > .disabled > a:hover, -.nav > .disabled > a:focus { - text-decoration: none; - background-color: transparent; - cursor: default; + +// Move borders to anchors instead of bottom of list +// +// Mixin for adding on top the shared `.nav-justified` styles for our tabs +.nav-tabs-justified { + border-bottom: 0; + + > li > a { + // Override margin from .nav-tabs + margin-right: 0; + border-radius: @border-radius-base; + } + + > .active > a, + > .active > a:hover, + > .active > a:focus { + border: 1px solid @nav-tabs-justified-link-border-color; + } + + @media (min-width: @screen-sm-min) { + > li > a { + border-bottom: 1px solid @nav-tabs-justified-link-border-color; + border-radius: @border-radius-base @border-radius-base 0 0; + } + > .active > a, + > .active > a:hover, + > .active > a:focus { + border-bottom-color: @nav-tabs-justified-active-link-border-color; + } + } +} + + +// Tabbable tabs +// ------------------------- + +// Hide tabbable panes to start, show them when `.active` +.tab-content { + > .tab-pane { + display: none; + } + > .active { + display: block; + } +} + + +// Dropdowns +// ------------------------- + +// Specific dropdowns +.nav-tabs .dropdown-menu { + // make dropdown border overlap tab border + margin-top: -1px; + // Remove the top rounded corners here since there is a hard edge above the menu + .border-top-radius(0); } diff --git a/src/UI/Content/Bootstrap/normalize.less b/src/UI/Content/Bootstrap/normalize.less new file mode 100644 index 000000000..024e257c1 --- /dev/null +++ b/src/UI/Content/Bootstrap/normalize.less @@ -0,0 +1,423 @@ +/*! normalize.css v3.0.0 | MIT License | git.io/normalize */ + +// +// 1. Set default font family to sans-serif. +// 2. Prevent iOS text size adjust after orientation change, without disabling +// user zoom. +// + +html { + font-family: sans-serif; // 1 + -ms-text-size-adjust: 100%; // 2 + -webkit-text-size-adjust: 100%; // 2 +} + +// +// Remove default margin. +// + +body { + margin: 0; +} + +// HTML5 display definitions +// ========================================================================== + +// +// Correct `block` display not defined in IE 8/9. +// + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section, +summary { + display: block; +} + +// +// 1. Correct `inline-block` display not defined in IE 8/9. +// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. +// + +audio, +canvas, +progress, +video { + display: inline-block; // 1 + vertical-align: baseline; // 2 +} + +// +// Prevent modern browsers from displaying `audio` without controls. +// Remove excess height in iOS 5 devices. +// + +audio:not([controls]) { + display: none; + height: 0; +} + +// +// Address `[hidden]` styling not present in IE 8/9. +// Hide the `template` element in IE, Safari, and Firefox < 22. +// + +[hidden], +template { + display: none; +} + +// Links +// ========================================================================== + +// +// Remove the gray background color from active links in IE 10. +// + +a { + background: transparent; +} + +// +// Improve readability when focused and also mouse hovered in all browsers. +// + +a:active, +a:hover { + outline: 0; +} + +// Text-level semantics +// ========================================================================== + +// +// Address styling not present in IE 8/9, Safari 5, and Chrome. +// + +abbr[title] { + border-bottom: 1px dotted; +} + +// +// Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. +// + +b, +strong { + font-weight: bold; +} + +// +// Address styling not present in Safari 5 and Chrome. +// + +dfn { + font-style: italic; +} + +// +// Address variable `h1` font-size and margin within `section` and `article` +// contexts in Firefox 4+, Safari 5, and Chrome. +// + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +// +// Address styling not present in IE 8/9. +// + +mark { + background: #ff0; + color: #000; +} + +// +// Address inconsistent and variable font size in all browsers. +// + +small { + font-size: 80%; +} + +// +// Prevent `sub` and `sup` affecting `line-height` in all browsers. +// + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +// Embedded content +// ========================================================================== + +// +// Remove border when inside `a` element in IE 8/9. +// + +img { + border: 0; +} + +// +// Correct overflow displayed oddly in IE 9. +// + +svg:not(:root) { + overflow: hidden; +} + +// Grouping content +// ========================================================================== + +// +// Address margin not present in IE 8/9 and Safari 5. +// + +figure { + margin: 1em 40px; +} + +// +// Address differences between Firefox and other browsers. +// + +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +// +// Contain overflow in all browsers. +// + +pre { + overflow: auto; +} + +// +// Address odd `em`-unit font size rendering in all browsers. +// + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +// Forms +// ========================================================================== + +// +// Known limitation: by default, Chrome and Safari on OS X allow very limited +// styling of `select`, unless a `border` property is set. +// + +// +// 1. Correct color not being inherited. +// Known issue: affects color of disabled elements. +// 2. Correct font properties not being inherited. +// 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. +// + +button, +input, +optgroup, +select, +textarea { + color: inherit; // 1 + font: inherit; // 2 + margin: 0; // 3 +} + +// +// Address `overflow` set to `hidden` in IE 8/9/10. +// + +button { + overflow: visible; +} + +// +// Address inconsistent `text-transform` inheritance for `button` and `select`. +// All other form control elements do not inherit `text-transform` values. +// Correct `button` style inheritance in Firefox, IE 8+, and Opera +// Correct `select` style inheritance in Firefox. +// + +button, +select { + text-transform: none; +} + +// +// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` +// and `video` controls. +// 2. Correct inability to style clickable `input` types in iOS. +// 3. Improve usability and consistency of cursor style between image-type +// `input` and others. +// + +button, +html input[type="button"], // 1 +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; // 2 + cursor: pointer; // 3 +} + +// +// Re-set default cursor for disabled elements. +// + +button[disabled], +html input[disabled] { + cursor: default; +} + +// +// Remove inner padding and border in Firefox 4+. +// + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +// +// Address Firefox 4+ setting `line-height` on `input` using `!important` in +// the UA stylesheet. +// + +input { + line-height: normal; +} + +// +// It's recommended that you don't attempt to style these elements. +// Firefox's implementation doesn't respect box-sizing, padding, or width. +// +// 1. Address box sizing set to `content-box` in IE 8/9/10. +// 2. Remove excess padding in IE 8/9/10. +// + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; // 1 + padding: 0; // 2 +} + +// +// Fix the cursor style for Chrome's increment/decrement buttons. For certain +// `font-size` values of the `input`, it causes the cursor style of the +// decrement button to change from `default` to `text`. +// + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +// +// 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. +// 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome +// (include `-moz` to future-proof). +// + +input[type="search"] { + -webkit-appearance: textfield; // 1 + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; // 2 + box-sizing: content-box; +} + +// +// Remove inner padding and search cancel button in Safari and Chrome on OS X. +// Safari (but not Chrome) clips the cancel button when the search input has +// padding (and `textfield` appearance). +// + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +// +// Define consistent border, margin, and padding. +// + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +// +// 1. Correct `color` not being inherited in IE 8/9. +// 2. Remove padding so people aren't caught out if they zero out fieldsets. +// + +legend { + border: 0; // 1 + padding: 0; // 2 +} + +// +// Remove default vertical scrollbar in IE 8/9. +// + +textarea { + overflow: auto; +} + +// +// Don't inherit the `font-weight` (applied by a rule above). +// NOTE: the default cannot safely be changed in Chrome and Safari on OS X. +// + +optgroup { + font-weight: bold; +} + +// Tables +// ========================================================================== + +// +// Remove most spacing between table cells. +// + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} \ No newline at end of file diff --git a/src/UI/Content/Bootstrap/pager.less b/src/UI/Content/Bootstrap/pager.less index 147618829..59103f445 100644 --- a/src/UI/Content/Bootstrap/pager.less +++ b/src/UI/Content/Bootstrap/pager.less @@ -4,40 +4,52 @@ .pager { - margin: @baseLineHeight 0; + padding-left: 0; + margin: @line-height-computed 0; list-style: none; text-align: center; - .clearfix(); + &:extend(.clearfix all); + li { + display: inline; + > a, + > span { + display: inline-block; + padding: 5px 14px; + background-color: @pager-bg; + border: 1px solid @pager-border; + border-radius: @pager-border-radius; + } + + > a:hover, + > a:focus { + text-decoration: none; + background-color: @pager-hover-bg; + } + } + + .next { + > a, + > span { + float: right; + } + } + + .previous { + > a, + > span { + float: left; + } + } + + .disabled { + > a, + > a:hover, + > a:focus, + > span { + color: @pager-disabled-color; + background-color: @pager-bg; + cursor: not-allowed; + } + } + } -.pager li { - display: inline; -} -.pager li > a, -.pager li > span { - display: inline-block; - padding: 5px 14px; - background-color: #fff; - border: 1px solid #ddd; - .border-radius(15px); -} -.pager li > a:hover, -.pager li > a:focus { - text-decoration: none; - background-color: #f5f5f5; -} -.pager .next > a, -.pager .next > span { - float: right; -} -.pager .previous > a, -.pager .previous > span { - float: left; -} -.pager .disabled > a, -.pager .disabled > a:hover, -.pager .disabled > a:focus, -.pager .disabled > span { - color: @grayLight; - background-color: #fff; - cursor: default; -} \ No newline at end of file diff --git a/src/UI/Content/Bootstrap/pagination.less b/src/UI/Content/Bootstrap/pagination.less index a789db2d2..b2856ae60 100644 --- a/src/UI/Content/Bootstrap/pagination.less +++ b/src/UI/Content/Bootstrap/pagination.less @@ -1,123 +1,88 @@ // // Pagination (multiple pages) // -------------------------------------------------- - -// Space out pagination from surrounding content .pagination { - margin: @baseLineHeight 0; -} - -.pagination ul { - // Allow for text-based alignment display: inline-block; - .ie7-inline-block(); - // Reset default ul styles - margin-left: 0; - margin-bottom: 0; - // Visuals - .border-radius(@baseBorderRadius); - .box-shadow(0 1px 2px rgba(0,0,0,.05)); -} -.pagination ul > li { - display: inline; // Remove list-style and block-level defaults -} -.pagination ul > li > a, -.pagination ul > li > span { - float: left; // Collapse white-space - padding: 4px 12px; - line-height: @baseLineHeight; - text-decoration: none; - background-color: @paginationBackground; - border: 1px solid @paginationBorder; - border-left-width: 0; -} -.pagination ul > li > a:hover, -.pagination ul > li > a:focus, -.pagination ul > .active > a, -.pagination ul > .active > span { - background-color: @paginationActiveBackground; -} -.pagination ul > .active > a, -.pagination ul > .active > span { - color: @grayLight; - cursor: default; -} -.pagination ul > .disabled > span, -.pagination ul > .disabled > a, -.pagination ul > .disabled > a:hover, -.pagination ul > .disabled > a:focus { - color: @grayLight; - background-color: transparent; - cursor: default; -} -.pagination ul > li:first-child > a, -.pagination ul > li:first-child > span { - border-left-width: 1px; - .border-left-radius(@baseBorderRadius); -} -.pagination ul > li:last-child > a, -.pagination ul > li:last-child > span { - .border-right-radius(@baseBorderRadius); -} + padding-left: 0; + margin: @line-height-computed 0; + border-radius: @border-radius-base; + > li { + display: inline; // Remove list-style and block-level defaults + > a, + > span { + position: relative; + float: left; // Collapse white-space + padding: @padding-base-vertical @padding-base-horizontal; + line-height: @line-height-base; + text-decoration: none; + color: @pagination-color; + background-color: @pagination-bg; + border: 1px solid @pagination-border; + margin-left: -1px; + } + &:first-child { + > a, + > span { + margin-left: 0; + .border-left-radius(@border-radius-base); + } + } + &:last-child { + > a, + > span { + .border-right-radius(@border-radius-base); + } + } + } -// Alignment -// -------------------------------------------------- + > li > a, + > li > span { + &:hover, + &:focus { + color: @pagination-hover-color; + background-color: @pagination-hover-bg; + border-color: @pagination-hover-border; + } + } -.pagination-centered { - text-align: center; + > .active > a, + > .active > span { + &, + &:hover, + &:focus { + z-index: 2; + color: @pagination-active-color; + background-color: @pagination-active-bg; + border-color: @pagination-active-border; + cursor: default; + } + } + + > .disabled { + > span, + > span:hover, + > span:focus, + > a, + > a:hover, + > a:focus { + color: @pagination-disabled-color; + background-color: @pagination-disabled-bg; + border-color: @pagination-disabled-border; + cursor: not-allowed; + } + } } -.pagination-right { - text-align: right; -} - // Sizing // -------------------------------------------------- // Large -.pagination-large { - ul > li > a, - ul > li > span { - padding: @paddingLarge; - font-size: @fontSizeLarge; - } - ul > li:first-child > a, - ul > li:first-child > span { - .border-left-radius(@borderRadiusLarge); - } - ul > li:last-child > a, - ul > li:last-child > span { - .border-right-radius(@borderRadiusLarge); - } -} - -// Small and mini -.pagination-mini, -.pagination-small { - ul > li:first-child > a, - ul > li:first-child > span { - .border-left-radius(@borderRadiusSmall); - } - ul > li:last-child > a, - ul > li:last-child > span { - .border-right-radius(@borderRadiusSmall); - } +.pagination-lg { + .pagination-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @border-radius-large); } // Small -.pagination-small { - ul > li > a, - ul > li > span { - padding: @paddingSmall; - font-size: @fontSizeSmall; - } -} -// Mini -.pagination-mini { - ul > li > a, - ul > li > span { - padding: @paddingMini; - font-size: @fontSizeMini; - } +.pagination-sm { + .pagination-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @border-radius-small); } diff --git a/src/UI/Content/Bootstrap/panels.less b/src/UI/Content/Bootstrap/panels.less new file mode 100644 index 000000000..20dd14938 --- /dev/null +++ b/src/UI/Content/Bootstrap/panels.less @@ -0,0 +1,241 @@ +// +// Panels +// -------------------------------------------------- + + +// Base class +.panel { + margin-bottom: @line-height-computed; + background-color: @panel-bg; + border: 1px solid transparent; + border-radius: @panel-border-radius; + .box-shadow(0 1px 1px rgba(0,0,0,.05)); +} + +// Panel contents +.panel-body { + padding: @panel-body-padding; + &:extend(.clearfix all); +} + +// Optional heading +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + .border-top-radius((@panel-border-radius - 1)); + + > .dropdown .dropdown-toggle { + color: inherit; + } +} + +// Within heading, strip any `h*` tag of its default margins for spacing. +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: ceil((@font-size-base * 1.125)); + color: inherit; + + > a { + color: inherit; + } +} + +// Optional footer (stays gray in every modifier class) +.panel-footer { + padding: 10px 15px; + background-color: @panel-footer-bg; + border-top: 1px solid @panel-inner-border; + .border-bottom-radius((@panel-border-radius - 1)); +} + + +// List groups in panels +// +// By default, space out list group content from panel headings to account for +// any kind of custom content between the two. + +.panel { + > .list-group { + margin-bottom: 0; + + .list-group-item { + border-width: 1px 0; + border-radius: 0; + } + + // Add border top radius for first one + &:first-child { + .list-group-item:first-child { + border-top: 0; + .border-top-radius((@panel-border-radius - 1)); + } + } + // Add border bottom radius for last one + &:last-child { + .list-group-item:last-child { + border-bottom: 0; + .border-bottom-radius((@panel-border-radius - 1)); + } + } + } +} +// Collapse space between when there's no additional content. +.panel-heading + .list-group { + .list-group-item:first-child { + border-top-width: 0; + } +} + + +// Tables in panels +// +// Place a non-bordered `.table` within a panel (not within a `.panel-body`) and +// watch it go full width. + +.panel { + > .table, + > .table-responsive > .table { + margin-bottom: 0; + } + // Add border top radius for first one + > .table:first-child, + > .table-responsive:first-child > .table:first-child { + .border-top-radius((@panel-border-radius - 1)); + + > thead:first-child, + > tbody:first-child { + > tr:first-child { + td:first-child, + th:first-child { + border-top-left-radius: (@panel-border-radius - 1); + } + td:last-child, + th:last-child { + border-top-right-radius: (@panel-border-radius - 1); + } + } + } + } + // Add border bottom radius for last one + > .table:last-child, + > .table-responsive:last-child > .table:last-child { + .border-bottom-radius((@panel-border-radius - 1)); + + > tbody:last-child, + > tfoot:last-child { + > tr:last-child { + td:first-child, + th:first-child { + border-bottom-left-radius: (@panel-border-radius - 1); + } + td:last-child, + th:last-child { + border-bottom-right-radius: (@panel-border-radius - 1); + } + } + } + } + > .panel-body + .table, + > .panel-body + .table-responsive { + border-top: 1px solid @table-border-color; + } + > .table > tbody:first-child > tr:first-child th, + > .table > tbody:first-child > tr:first-child td { + border-top: 0; + } + > .table-bordered, + > .table-responsive > .table-bordered { + border: 0; + > thead, + > tbody, + > tfoot { + > tr { + > th:first-child, + > td:first-child { + border-left: 0; + } + > th:last-child, + > td:last-child { + border-right: 0; + } + } + } + > thead, + > tbody { + > tr:first-child { + > td, + > th { + border-bottom: 0; + } + } + } + > tbody, + > tfoot { + > tr:last-child { + > td, + > th { + border-bottom: 0; + } + } + } + } + > .table-responsive { + border: 0; + margin-bottom: 0; + } +} + + +// Collapsable panels (aka, accordion) +// +// Wrap a series of panels in `.panel-group` to turn them into an accordion with +// the help of our collapse JavaScript plugin. + +.panel-group { + margin-bottom: @line-height-computed; + + // Tighten up margin so it's only between panels + .panel { + margin-bottom: 0; + border-radius: @panel-border-radius; + overflow: hidden; // crop contents when collapsed + + .panel { + margin-top: 5px; + } + } + + .panel-heading { + border-bottom: 0; + + .panel-collapse .panel-body { + border-top: 1px solid @panel-inner-border; + } + } + .panel-footer { + border-top: 0; + + .panel-collapse .panel-body { + border-bottom: 1px solid @panel-inner-border; + } + } +} + + +// Contextual variations +.panel-default { + .panel-variant(@panel-default-border; @panel-default-text; @panel-default-heading-bg; @panel-default-border); +} +.panel-primary { + .panel-variant(@panel-primary-border; @panel-primary-text; @panel-primary-heading-bg; @panel-primary-border); +} +.panel-success { + .panel-variant(@panel-success-border; @panel-success-text; @panel-success-heading-bg; @panel-success-border); +} +.panel-info { + .panel-variant(@panel-info-border; @panel-info-text; @panel-info-heading-bg; @panel-info-border); +} +.panel-warning { + .panel-variant(@panel-warning-border; @panel-warning-text; @panel-warning-heading-bg; @panel-warning-border); +} +.panel-danger { + .panel-variant(@panel-danger-border; @panel-danger-text; @panel-danger-heading-bg; @panel-danger-border); +} diff --git a/src/UI/Content/Bootstrap/popovers.less b/src/UI/Content/Bootstrap/popovers.less index aae35c8cd..696d74c7d 100644 --- a/src/UI/Content/Bootstrap/popovers.less +++ b/src/UI/Content/Bootstrap/popovers.less @@ -7,43 +7,37 @@ position: absolute; top: 0; left: 0; - z-index: @zindexPopover; + z-index: @zindex-popover; display: none; - max-width: 276px; + max-width: @popover-max-width; padding: 1px; text-align: left; // Reset given new insertion method - background-color: @popoverBackground; - -webkit-background-clip: padding-box; - -moz-background-clip: padding; - background-clip: padding-box; - border: 1px solid #ccc; - border: 1px solid rgba(0,0,0,.2); - .border-radius(6px); + background-color: @popover-bg; + background-clip: padding-box; + border: 1px solid @popover-fallback-border-color; + border: 1px solid @popover-border-color; + border-radius: @border-radius-large; .box-shadow(0 5px 10px rgba(0,0,0,.2)); // Overrides for proper insertion white-space: normal; // Offset the popover to account for the popover arrow - &.top { margin-top: -10px; } - &.right { margin-left: 10px; } - &.bottom { margin-top: 10px; } - &.left { margin-left: -10px; } + &.top { margin-top: -@popover-arrow-width; } + &.right { margin-left: @popover-arrow-width; } + &.bottom { margin-top: @popover-arrow-width; } + &.left { margin-left: -@popover-arrow-width; } } .popover-title { margin: 0; // reset heading margin padding: 8px 14px; - font-size: 14px; + font-size: @font-size-base; font-weight: normal; line-height: 18px; - background-color: @popoverTitleBackground; - border-bottom: 1px solid darken(@popoverTitleBackground, 5%); - .border-radius(5px 5px 0 0); - - &:empty { - display: none; - } + background-color: @popover-title-bg; + border-bottom: 1px solid darken(@popover-title-bg, 5%); + border-radius: 5px 5px 0 0; } .popover-content { @@ -54,79 +48,85 @@ // // .arrow is outer, .arrow:after is inner -.popover .arrow, -.popover .arrow:after { - position: absolute; - display: block; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; +.popover > .arrow { + &, + &:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + } } -.popover .arrow { - border-width: @popoverArrowOuterWidth; +.popover > .arrow { + border-width: @popover-arrow-outer-width; } -.popover .arrow:after { - border-width: @popoverArrowWidth; +.popover > .arrow:after { + border-width: @popover-arrow-width; content: ""; } .popover { - &.top .arrow { + &.top > .arrow { left: 50%; - margin-left: -@popoverArrowOuterWidth; + margin-left: -@popover-arrow-outer-width; border-bottom-width: 0; - border-top-color: #999; // IE8 fallback - border-top-color: @popoverArrowOuterColor; - bottom: -@popoverArrowOuterWidth; + border-top-color: @popover-arrow-outer-fallback-color; // IE8 fallback + border-top-color: @popover-arrow-outer-color; + bottom: -@popover-arrow-outer-width; &:after { + content: " "; bottom: 1px; - margin-left: -@popoverArrowWidth; + margin-left: -@popover-arrow-width; border-bottom-width: 0; - border-top-color: @popoverArrowColor; + border-top-color: @popover-arrow-color; } } - &.right .arrow { + &.right > .arrow { top: 50%; - left: -@popoverArrowOuterWidth; - margin-top: -@popoverArrowOuterWidth; + left: -@popover-arrow-outer-width; + margin-top: -@popover-arrow-outer-width; border-left-width: 0; - border-right-color: #999; // IE8 fallback - border-right-color: @popoverArrowOuterColor; + border-right-color: @popover-arrow-outer-fallback-color; // IE8 fallback + border-right-color: @popover-arrow-outer-color; &:after { + content: " "; left: 1px; - bottom: -@popoverArrowWidth; + bottom: -@popover-arrow-width; border-left-width: 0; - border-right-color: @popoverArrowColor; + border-right-color: @popover-arrow-color; } } - &.bottom .arrow { + &.bottom > .arrow { left: 50%; - margin-left: -@popoverArrowOuterWidth; + margin-left: -@popover-arrow-outer-width; border-top-width: 0; - border-bottom-color: #999; // IE8 fallback - border-bottom-color: @popoverArrowOuterColor; - top: -@popoverArrowOuterWidth; + border-bottom-color: @popover-arrow-outer-fallback-color; // IE8 fallback + border-bottom-color: @popover-arrow-outer-color; + top: -@popover-arrow-outer-width; &:after { + content: " "; top: 1px; - margin-left: -@popoverArrowWidth; + margin-left: -@popover-arrow-width; border-top-width: 0; - border-bottom-color: @popoverArrowColor; + border-bottom-color: @popover-arrow-color; } } - &.left .arrow { + &.left > .arrow { top: 50%; - right: -@popoverArrowOuterWidth; - margin-top: -@popoverArrowOuterWidth; + right: -@popover-arrow-outer-width; + margin-top: -@popover-arrow-outer-width; border-right-width: 0; - border-left-color: #999; // IE8 fallback - border-left-color: @popoverArrowOuterColor; + border-left-color: @popover-arrow-outer-fallback-color; // IE8 fallback + border-left-color: @popover-arrow-outer-color; &:after { + content: " "; right: 1px; border-right-width: 0; - border-left-color: @popoverArrowColor; - bottom: -@popoverArrowWidth; + border-left-color: @popover-arrow-color; + bottom: -@popover-arrow-width; } } diff --git a/src/UI/Content/Bootstrap/print.less b/src/UI/Content/Bootstrap/print.less new file mode 100644 index 000000000..3655d0395 --- /dev/null +++ b/src/UI/Content/Bootstrap/print.less @@ -0,0 +1,101 @@ +// +// Basic print styles +// -------------------------------------------------- +// Source: https://github.com/h5bp/html5-boilerplate/blob/master/css/main.css + +@media print { + + * { + text-shadow: none !important; + color: #000 !important; // Black prints faster: h5bp.com/s + background: transparent !important; + box-shadow: none !important; + } + + a, + a:visited { + text-decoration: underline; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + // Don't show links for images, or javascript/internal links + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + + thead { + display: table-header-group; // h5bp.com/t + } + + tr, + img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + + h2, + h3 { + page-break-after: avoid; + } + + // Chrome (OSX) fix for https://github.com/twbs/bootstrap/issues/11245 + // Once fixed, we can just straight up remove this. + select { + background: #fff !important; + } + + // Bootstrap components + .navbar { + display: none; + } + .table { + td, + th { + background-color: #fff !important; + } + } + .btn, + .dropup > .btn { + > .caret { + border-top-color: #000 !important; + } + } + .label { + border: 1px solid #000; + } + + .table { + border-collapse: collapse !important; + } + .table-bordered { + th, + td { + border: 1px solid #ddd !important; + } + } + +} diff --git a/src/UI/Content/Bootstrap/progress-bars.less b/src/UI/Content/Bootstrap/progress-bars.less index 5e0c3dda0..76c87be17 100644 --- a/src/UI/Content/Bootstrap/progress-bars.less +++ b/src/UI/Content/Bootstrap/progress-bars.less @@ -3,34 +3,16 @@ // -------------------------------------------------- -// ANIMATIONS -// ---------- +// Bar animations +// ------------------------- -// Webkit +// WebKit @-webkit-keyframes progress-bar-stripes { from { background-position: 40px 0; } to { background-position: 0 0; } } -// Firefox -@-moz-keyframes progress-bar-stripes { - from { background-position: 40px 0; } - to { background-position: 0 0; } -} - -// IE9 -@-ms-keyframes progress-bar-stripes { - from { background-position: 40px 0; } - to { background-position: 0 0; } -} - -// Opera -@-o-keyframes progress-bar-stripes { - from { background-position: 0 0; } - to { background-position: 40px 0; } -} - -// Spec +// Spec and IE10+ @keyframes progress-bar-stripes { from { background-position: 40px 0; } to { background-position: 0 0; } @@ -38,85 +20,61 @@ -// THE BARS -// -------- +// Bar itself +// ------------------------- // Outer container .progress { overflow: hidden; - height: @baseLineHeight; - margin-bottom: @baseLineHeight; - #gradient > .vertical(#f5f5f5, #f9f9f9); + height: @line-height-computed; + margin-bottom: @line-height-computed; + background-color: @progress-bg; + border-radius: @border-radius-base; .box-shadow(inset 0 1px 2px rgba(0,0,0,.1)); - .border-radius(@baseBorderRadius); } // Bar of progress -.progress .bar { +.progress-bar { + float: left; width: 0%; height: 100%; - color: @white; - float: left; - font-size: 12px; + font-size: @font-size-small; + line-height: @line-height-computed; + color: @progress-bar-color; text-align: center; - text-shadow: 0 -1px 0 rgba(0,0,0,.25); - #gradient > .vertical(#149bdf, #0480be); + background-color: @progress-bar-bg; .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15)); - .box-sizing(border-box); .transition(width .6s ease); } -.progress .bar + .bar { - .box-shadow(~"inset 1px 0 0 rgba(0,0,0,.15), inset 0 -1px 0 rgba(0,0,0,.15)"); -} // Striped bars -.progress-striped .bar { - #gradient > .striped(#149bdf); - .background-size(40px 40px); +.progress-striped .progress-bar { + #gradient > .striped(); + background-size: 40px 40px; } // Call animation for the active one -.progress.active .bar { - -webkit-animation: progress-bar-stripes 2s linear infinite; - -moz-animation: progress-bar-stripes 2s linear infinite; - -ms-animation: progress-bar-stripes 2s linear infinite; - -o-animation: progress-bar-stripes 2s linear infinite; - animation: progress-bar-stripes 2s linear infinite; +.progress.active .progress-bar { + .animation(progress-bar-stripes 2s linear infinite); } -// COLORS -// ------ +// Variations +// ------------------------- -// Danger (red) -.progress-danger .bar, .progress .bar-danger { - #gradient > .vertical(#ee5f5b, #c43c35); -} -.progress-danger.progress-striped .bar, .progress-striped .bar-danger { - #gradient > .striped(#ee5f5b); +.progress-bar-success { + .progress-bar-variant(@progress-bar-success-bg); } -// Success (green) -.progress-success .bar, .progress .bar-success { - #gradient > .vertical(#62c462, #57a957); -} -.progress-success.progress-striped .bar, .progress-striped .bar-success { - #gradient > .striped(#62c462); +.progress-bar-info { + .progress-bar-variant(@progress-bar-info-bg); } -// Info (teal) -.progress-info .bar, .progress .bar-info { - #gradient > .vertical(#5bc0de, #339bb9); -} -.progress-info.progress-striped .bar, .progress-striped .bar-info { - #gradient > .striped(#5bc0de); +.progress-bar-warning { + .progress-bar-variant(@progress-bar-warning-bg); } -// Warning (orange) -.progress-warning .bar, .progress .bar-warning { - #gradient > .vertical(lighten(@orange, 15%), @orange); -} -.progress-warning.progress-striped .bar, .progress-striped .bar-warning { - #gradient > .striped(lighten(@orange, 15%)); +.progress-bar-danger { + .progress-bar-variant(@progress-bar-danger-bg); } diff --git a/src/UI/Content/Bootstrap/reset.less b/src/UI/Content/Bootstrap/reset.less deleted file mode 100644 index 4806bd5e5..000000000 --- a/src/UI/Content/Bootstrap/reset.less +++ /dev/null @@ -1,216 +0,0 @@ -// -// Reset CSS -// Adapted from http://github.com/necolas/normalize.css -// -------------------------------------------------- - - -// Display in IE6-9 and FF3 -// ------------------------- - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -nav, -section { - display: block; -} - -// Display block in IE6-9 and FF3 -// ------------------------- - -audio, -canvas, -video { - display: inline-block; - *display: inline; - *zoom: 1; -} - -// Prevents modern browsers from displaying 'audio' without controls -// ------------------------- - -audio:not([controls]) { - display: none; -} - -// Base settings -// ------------------------- - -html { - font-size: 100%; - -webkit-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; -} -// Focus states -a:focus { - .tab-focus(); -} -// Hover & Active -a:hover, -a:active { - outline: 0; -} - -// Prevents sub and sup affecting line-height in all browsers -// ------------------------- - -sub, -sup { - position: relative; - font-size: 75%; - line-height: 0; - vertical-align: baseline; -} -sup { - top: -0.5em; -} -sub { - bottom: -0.25em; -} - -// Img border in a's and image quality -// ------------------------- - -img { - /* Responsive images (ensure images don't scale beyond their parents) */ - max-width: 100%; /* Part 1: Set a maxium relative to the parent */ - width: auto\9; /* IE7-8 need help adjusting responsive images */ - height: auto; /* Part 2: Scale the height according to the width, otherwise you get stretching */ - - vertical-align: middle; - border: 0; - -ms-interpolation-mode: bicubic; -} - -// Prevent max-width from affecting Google Maps -#map_canvas img, -.google-maps img { - max-width: none; -} - -// Forms -// ------------------------- - -// Font size in all browsers, margin changes, misc consistency -button, -input, -select, -textarea { - margin: 0; - font-size: 100%; - vertical-align: middle; -} -button, -input { - *overflow: visible; // Inner spacing ie IE6/7 - line-height: normal; // FF3/4 have !important on line-height in UA stylesheet -} -button::-moz-focus-inner, -input::-moz-focus-inner { // Inner padding and border oddities in FF3/4 - padding: 0; - border: 0; -} -button, -html input[type="button"], // Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; // Corrects inability to style clickable `input` types in iOS. - cursor: pointer; // Improves usability and consistency of cursor style between image-type `input` and others. -} -label, -select, -button, -input[type="button"], -input[type="reset"], -input[type="submit"], -input[type="radio"], -input[type="checkbox"] { - cursor: pointer; // Improves usability and consistency of cursor style between image-type `input` and others. -} -input[type="search"] { // Appearance in Safari/Chrome - .box-sizing(content-box); - -webkit-appearance: textfield; -} -input[type="search"]::-webkit-search-decoration, -input[type="search"]::-webkit-search-cancel-button { - -webkit-appearance: none; // Inner-padding issues in Chrome OSX, Safari 5 -} -textarea { - overflow: auto; // Remove vertical scrollbar in IE6-9 - vertical-align: top; // Readability and alignment cross-browser -} - - -// Printing -// ------------------------- -// Source: https://github.com/h5bp/html5-boilerplate/blob/master/css/main.css - -@media print { - - * { - text-shadow: none !important; - color: #000 !important; // Black prints faster: h5bp.com/s - background: transparent !important; - box-shadow: none !important; - } - - a, - a:visited { - text-decoration: underline; - } - - a[href]:after { - content: " (" attr(href) ")"; - } - - abbr[title]:after { - content: " (" attr(title) ")"; - } - - // Don't show links for images, or javascript/internal links - .ir a:after, - a[href^="javascript:"]:after, - a[href^="#"]:after { - content: ""; - } - - pre, - blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - - thead { - display: table-header-group; // h5bp.com/t - } - - tr, - img { - page-break-inside: avoid; - } - - img { - max-width: 100% !important; - } - - @page { - margin: 0.5cm; - } - - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - - h2, - h3 { - page-break-after: avoid; - } -} diff --git a/src/UI/Content/Bootstrap/responsive-utilities.less b/src/UI/Content/Bootstrap/responsive-utilities.less new file mode 100644 index 000000000..027a26410 --- /dev/null +++ b/src/UI/Content/Bootstrap/responsive-utilities.less @@ -0,0 +1,92 @@ +// +// Responsive: Utility classes +// -------------------------------------------------- + + +// IE10 in Windows (Phone) 8 +// +// Support for responsive views via media queries is kind of borked in IE10, for +// Surface/desktop in split view and for Windows Phone 8. This particular fix +// must be accompanied by a snippet of JavaScript to sniff the user agent and +// apply some conditional CSS to *only* the Surface/desktop Windows 8. Look at +// our Getting Started page for more information on this bug. +// +// For more information, see the following: +// +// Issue: https://github.com/twbs/bootstrap/issues/10497 +// Docs: http://getbootstrap.com/getting-started/#browsers +// Source: http://timkadlec.com/2012/10/ie10-snap-mode-and-responsive-design/ + +@-ms-viewport { + width: device-width; +} + + +// Visibility utilities +.visible-xs, +.visible-sm, +.visible-md, +.visible-lg { + .responsive-invisibility(); +} + +.visible-xs { + @media (max-width: @screen-xs-max) { + .responsive-visibility(); + } +} +.visible-sm { + @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { + .responsive-visibility(); + } +} +.visible-md { + @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { + .responsive-visibility(); + } +} +.visible-lg { + @media (min-width: @screen-lg-min) { + .responsive-visibility(); + } +} + +.hidden-xs { + @media (max-width: @screen-xs-max) { + .responsive-invisibility(); + } +} +.hidden-sm { + @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { + .responsive-invisibility(); + } +} +.hidden-md { + @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { + .responsive-invisibility(); + } +} +.hidden-lg { + @media (min-width: @screen-lg-min) { + .responsive-invisibility(); + } +} + + +// Print utilities +// +// Media queries are placed on the inside to be mixin-friendly. + +.visible-print { + .responsive-invisibility(); + + @media print { + .responsive-visibility(); + } +} + +.hidden-print { + @media print { + .responsive-invisibility(); + } +} diff --git a/src/UI/Content/Bootstrap/scaffolding.less b/src/UI/Content/Bootstrap/scaffolding.less index f17e8cadb..fe29f2d62 100644 --- a/src/UI/Content/Bootstrap/scaffolding.less +++ b/src/UI/Content/Bootstrap/scaffolding.less @@ -3,51 +3,132 @@ // -------------------------------------------------- +// Reset the box-sizing +// +// Heads up! This reset may cause conflicts with some third-party widgets. +// For recommendations on resolving such conflicts, see +// http://getbootstrap.com/getting-started/#third-box-sizing +* { + .box-sizing(border-box); +} +*:before, +*:after { + .box-sizing(border-box); +} + + // Body reset -// ------------------------- + +html { + font-size: 62.5%; + -webkit-tap-highlight-color: rgba(0,0,0,0); +} body { - margin: 0; - font-family: @baseFontFamily; - font-size: @baseFontSize; - line-height: @baseLineHeight; - color: @textColor; - background-color: @bodyBackground; + font-family: @font-family-base; + font-size: @font-size-base; + line-height: @line-height-base; + color: @text-color; + background-color: @body-bg; +} + +// Reset fonts for relevant elements +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; } // Links -// ------------------------- a { - color: @linkColor; + color: @link-color; text-decoration: none; + + &:hover, + &:focus { + color: @link-hover-color; + text-decoration: underline; + } + + &:focus { + .tab-focus(); + } } -a:hover, -a:focus { - color: @linkColorHover; - text-decoration: underline; + + +// Figures +// +// We reset this here because previously Normalize had no `figure` margins. This +// ensures we don't break anyone's use of the element. + +figure { + margin: 0; } // Images -// ------------------------- + +img { + vertical-align: middle; +} + +// Responsive images (ensure images don't scale beyond their parents) +.img-responsive { + .img-responsive(); +} // Rounded corners .img-rounded { - .border-radius(6px); + border-radius: @border-radius-large; } -// Add polaroid-esque trim -.img-polaroid { - padding: 4px; - background-color: #fff; - border: 1px solid #ccc; - border: 1px solid rgba(0,0,0,.2); - .box-shadow(0 1px 3px rgba(0,0,0,.1)); +// Image thumbnails +// +// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`. +.img-thumbnail { + padding: @thumbnail-padding; + line-height: @line-height-base; + background-color: @thumbnail-bg; + border: 1px solid @thumbnail-border; + border-radius: @thumbnail-border-radius; + .transition(all .2s ease-in-out); + + // Keep them at most 100% wide + .img-responsive(inline-block); } // Perfect circle .img-circle { - .border-radius(500px); // crank the border-radius so it works with most reasonably sized images + border-radius: 50%; // set radius in percents +} + + +// Horizontal rules + +hr { + margin-top: @line-height-computed; + margin-bottom: @line-height-computed; + border: 0; + border-top: 1px solid @hr-border; +} + + +// Only display content to screen readers +// +// See: http://a11yproject.com/posts/how-to-hide-content/ + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; } diff --git a/src/UI/Content/Bootstrap/sprites.less b/src/UI/Content/Bootstrap/sprites.less deleted file mode 100644 index 1812bf71a..000000000 --- a/src/UI/Content/Bootstrap/sprites.less +++ /dev/null @@ -1,197 +0,0 @@ -// -// Sprites -// -------------------------------------------------- - - -// ICONS -// ----- - -// All icons receive the styles of the <i> tag with a base class -// of .i and are then given a unique class to add width, height, -// and background-position. Your resulting HTML will look like -// <i class="icon-inbox"></i>. - -// For the white version of the icons, just add the .icon-white class: -// <i class="icon-inbox icon-white"></i> - -[class^="icon-"], -[class*=" icon-"] { - display: inline-block; - width: 14px; - height: 14px; - .ie7-restore-right-whitespace(); - line-height: 14px; - vertical-align: text-top; - background-image: url("@{iconSpritePath}"); - background-position: 14px 14px; - background-repeat: no-repeat; - margin-top: 1px; -} - -/* White icons with optional class, or on hover/focus/active states of certain elements */ -.icon-white, -.nav-pills > .active > a > [class^="icon-"], -.nav-pills > .active > a > [class*=" icon-"], -.nav-list > .active > a > [class^="icon-"], -.nav-list > .active > a > [class*=" icon-"], -.navbar-inverse .nav > .active > a > [class^="icon-"], -.navbar-inverse .nav > .active > a > [class*=" icon-"], -.dropdown-menu > li > a:hover > [class^="icon-"], -.dropdown-menu > li > a:focus > [class^="icon-"], -.dropdown-menu > li > a:hover > [class*=" icon-"], -.dropdown-menu > li > a:focus > [class*=" icon-"], -.dropdown-menu > .active > a > [class^="icon-"], -.dropdown-menu > .active > a > [class*=" icon-"], -.dropdown-submenu:hover > a > [class^="icon-"], -.dropdown-submenu:focus > a > [class^="icon-"], -.dropdown-submenu:hover > a > [class*=" icon-"], -.dropdown-submenu:focus > a > [class*=" icon-"] { - background-image: url("@{iconWhiteSpritePath}"); -} - -.icon-glass { background-position: 0 0; } -.icon-music { background-position: -24px 0; } -.icon-search { background-position: -48px 0; } -.icon-envelope { background-position: -72px 0; } -.icon-heart { background-position: -96px 0; } -.icon-star { background-position: -120px 0; } -.icon-star-empty { background-position: -144px 0; } -.icon-user { background-position: -168px 0; } -.icon-film { background-position: -192px 0; } -.icon-th-large { background-position: -216px 0; } -.icon-th { background-position: -240px 0; } -.icon-th-list { background-position: -264px 0; } -.icon-ok { background-position: -288px 0; } -.icon-remove { background-position: -312px 0; } -.icon-zoom-in { background-position: -336px 0; } -.icon-zoom-out { background-position: -360px 0; } -.icon-off { background-position: -384px 0; } -.icon-signal { background-position: -408px 0; } -.icon-cog { background-position: -432px 0; } -.icon-trash { background-position: -456px 0; } - -.icon-home { background-position: 0 -24px; } -.icon-file { background-position: -24px -24px; } -.icon-time { background-position: -48px -24px; } -.icon-road { background-position: -72px -24px; } -.icon-download-alt { background-position: -96px -24px; } -.icon-download { background-position: -120px -24px; } -.icon-upload { background-position: -144px -24px; } -.icon-inbox { background-position: -168px -24px; } -.icon-play-circle { background-position: -192px -24px; } -.icon-repeat { background-position: -216px -24px; } -.icon-refresh { background-position: -240px -24px; } -.icon-list-alt { background-position: -264px -24px; } -.icon-lock { background-position: -287px -24px; } // 1px off -.icon-flag { background-position: -312px -24px; } -.icon-headphones { background-position: -336px -24px; } -.icon-volume-off { background-position: -360px -24px; } -.icon-volume-down { background-position: -384px -24px; } -.icon-volume-up { background-position: -408px -24px; } -.icon-qrcode { background-position: -432px -24px; } -.icon-barcode { background-position: -456px -24px; } - -.icon-tag { background-position: 0 -48px; } -.icon-tags { background-position: -25px -48px; } // 1px off -.icon-book { background-position: -48px -48px; } -.icon-bookmark { background-position: -72px -48px; } -.icon-print { background-position: -96px -48px; } -.icon-camera { background-position: -120px -48px; } -.icon-font { background-position: -144px -48px; } -.icon-bold { background-position: -167px -48px; } // 1px off -.icon-italic { background-position: -192px -48px; } -.icon-text-height { background-position: -216px -48px; } -.icon-text-width { background-position: -240px -48px; } -.icon-align-left { background-position: -264px -48px; } -.icon-align-center { background-position: -288px -48px; } -.icon-align-right { background-position: -312px -48px; } -.icon-align-justify { background-position: -336px -48px; } -.icon-list { background-position: -360px -48px; } -.icon-indent-left { background-position: -384px -48px; } -.icon-indent-right { background-position: -408px -48px; } -.icon-facetime-video { background-position: -432px -48px; } -.icon-picture { background-position: -456px -48px; } - -.icon-pencil { background-position: 0 -72px; } -.icon-map-marker { background-position: -24px -72px; } -.icon-adjust { background-position: -48px -72px; } -.icon-tint { background-position: -72px -72px; } -.icon-edit { background-position: -96px -72px; } -.icon-share { background-position: -120px -72px; } -.icon-check { background-position: -144px -72px; } -.icon-move { background-position: -168px -72px; } -.icon-step-backward { background-position: -192px -72px; } -.icon-fast-backward { background-position: -216px -72px; } -.icon-backward { background-position: -240px -72px; } -.icon-play { background-position: -264px -72px; } -.icon-pause { background-position: -288px -72px; } -.icon-stop { background-position: -312px -72px; } -.icon-forward { background-position: -336px -72px; } -.icon-fast-forward { background-position: -360px -72px; } -.icon-step-forward { background-position: -384px -72px; } -.icon-eject { background-position: -408px -72px; } -.icon-chevron-left { background-position: -432px -72px; } -.icon-chevron-right { background-position: -456px -72px; } - -.icon-plus-sign { background-position: 0 -96px; } -.icon-minus-sign { background-position: -24px -96px; } -.icon-remove-sign { background-position: -48px -96px; } -.icon-ok-sign { background-position: -72px -96px; } -.icon-question-sign { background-position: -96px -96px; } -.icon-info-sign { background-position: -120px -96px; } -.icon-screenshot { background-position: -144px -96px; } -.icon-remove-circle { background-position: -168px -96px; } -.icon-ok-circle { background-position: -192px -96px; } -.icon-ban-circle { background-position: -216px -96px; } -.icon-arrow-left { background-position: -240px -96px; } -.icon-arrow-right { background-position: -264px -96px; } -.icon-arrow-up { background-position: -289px -96px; } // 1px off -.icon-arrow-down { background-position: -312px -96px; } -.icon-share-alt { background-position: -336px -96px; } -.icon-resize-full { background-position: -360px -96px; } -.icon-resize-small { background-position: -384px -96px; } -.icon-plus { background-position: -408px -96px; } -.icon-minus { background-position: -433px -96px; } -.icon-asterisk { background-position: -456px -96px; } - -.icon-exclamation-sign { background-position: 0 -120px; } -.icon-gift { background-position: -24px -120px; } -.icon-leaf { background-position: -48px -120px; } -.icon-fire { background-position: -72px -120px; } -.icon-eye-open { background-position: -96px -120px; } -.icon-eye-close { background-position: -120px -120px; } -.icon-warning-sign { background-position: -144px -120px; } -.icon-plane { background-position: -168px -120px; } -.icon-calendar { background-position: -192px -120px; } -.icon-random { background-position: -216px -120px; width: 16px; } -.icon-comment { background-position: -240px -120px; } -.icon-magnet { background-position: -264px -120px; } -.icon-chevron-up { background-position: -288px -120px; } -.icon-chevron-down { background-position: -313px -119px; } // 1px, 1px off -.icon-retweet { background-position: -336px -120px; } -.icon-shopping-cart { background-position: -360px -120px; } -.icon-folder-close { background-position: -384px -120px; width: 16px; } -.icon-folder-open { background-position: -408px -120px; width: 16px; } -.icon-resize-vertical { background-position: -432px -119px; } // 1px, 1px off -.icon-resize-horizontal { background-position: -456px -118px; } // 1px, 2px off - -.icon-hdd { background-position: 0 -144px; } -.icon-bullhorn { background-position: -24px -144px; } -.icon-bell { background-position: -48px -144px; } -.icon-certificate { background-position: -72px -144px; } -.icon-thumbs-up { background-position: -96px -144px; } -.icon-thumbs-down { background-position: -120px -144px; } -.icon-hand-right { background-position: -144px -144px; } -.icon-hand-left { background-position: -168px -144px; } -.icon-hand-up { background-position: -192px -144px; } -.icon-hand-down { background-position: -216px -144px; } -.icon-circle-arrow-right { background-position: -240px -144px; } -.icon-circle-arrow-left { background-position: -264px -144px; } -.icon-circle-arrow-up { background-position: -288px -144px; } -.icon-circle-arrow-down { background-position: -312px -144px; } -.icon-globe { background-position: -336px -144px; } -.icon-wrench { background-position: -360px -144px; } -.icon-tasks { background-position: -384px -144px; } -.icon-filter { background-position: -408px -144px; } -.icon-briefcase { background-position: -432px -144px; } -.icon-fullscreen { background-position: -456px -144px; } diff --git a/src/UI/Content/Bootstrap/tables.less b/src/UI/Content/Bootstrap/tables.less index 0e35271e1..c41989c04 100644 --- a/src/UI/Content/Bootstrap/tables.less +++ b/src/UI/Content/Bootstrap/tables.less @@ -3,242 +3,231 @@ // -------------------------------------------------- -// BASE TABLES -// ----------------- - table { max-width: 100%; - background-color: @tableBackground; - border-collapse: collapse; - border-spacing: 0; + background-color: @table-bg; +} +th { + text-align: left; } -// BASELINE STYLES -// --------------- + +// Baseline styles .table { width: 100%; - margin-bottom: @baseLineHeight; + margin-bottom: @line-height-computed; // Cells - th, - td { - padding: 8px; - line-height: @baseLineHeight; - text-align: left; - vertical-align: top; - border-top: 1px solid @tableBorder; - } - th { - font-weight: bold; + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + padding: @table-cell-padding; + line-height: @line-height-base; + vertical-align: top; + border-top: 1px solid @table-border-color; + } + } } // Bottom align for column headings - thead th { + > thead > tr > th { vertical-align: bottom; + border-bottom: 2px solid @table-border-color; } // Remove top border from thead by default - caption + thead tr:first-child th, - caption + thead tr:first-child td, - colgroup + thead tr:first-child th, - colgroup + thead tr:first-child td, - thead:first-child tr:first-child th, - thead:first-child tr:first-child td { - border-top: 0; + > caption + thead, + > colgroup + thead, + > thead:first-child { + > tr:first-child { + > th, + > td { + border-top: 0; + } + } } // Account for multiple tbody instances - tbody + tbody { - border-top: 2px solid @tableBorder; + > tbody + tbody { + border-top: 2px solid @table-border-color; } // Nesting .table { - background-color: @bodyBackground; + background-color: @body-bg; } } - -// CONDENSED TABLE W/ HALF PADDING -// ------------------------------- +// Condensed table w/ half padding .table-condensed { - th, - td { - padding: 4px 5px; + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + padding: @table-condensed-cell-padding; + } + } } } -// BORDERED VERSION -// ---------------- +// Bordered version +// +// Add borders all around the table and between all the columns. .table-bordered { - border: 1px solid @tableBorder; - border-collapse: separate; // Done so we can round those corners! - *border-collapse: collapse; // IE7 can't round corners anyway - border-left: 0; - .border-radius(@baseBorderRadius); - th, - td { - border-left: 1px solid @tableBorder; + border: 1px solid @table-border-color; + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + border: 1px solid @table-border-color; + } + } } - // Prevent a double border - caption + thead tr:first-child th, - caption + tbody tr:first-child th, - caption + tbody tr:first-child td, - colgroup + thead tr:first-child th, - colgroup + tbody tr:first-child th, - colgroup + tbody tr:first-child td, - thead:first-child tr:first-child th, - tbody:first-child tr:first-child th, - tbody:first-child tr:first-child td { - border-top: 0; + > thead > tr { + > th, + > td { + border-bottom-width: 2px; + } } - // For first th/td in the first row in the first thead or tbody - thead:first-child tr:first-child > th:first-child, - tbody:first-child tr:first-child > td:first-child, - tbody:first-child tr:first-child > th:first-child { - .border-top-left-radius(@baseBorderRadius); - } - // For last th/td in the first row in the first thead or tbody - thead:first-child tr:first-child > th:last-child, - tbody:first-child tr:first-child > td:last-child, - tbody:first-child tr:first-child > th:last-child { - .border-top-right-radius(@baseBorderRadius); - } - // For first th/td (can be either) in the last row in the last thead, tbody, and tfoot - thead:last-child tr:last-child > th:first-child, - tbody:last-child tr:last-child > td:first-child, - tbody:last-child tr:last-child > th:first-child, - tfoot:last-child tr:last-child > td:first-child, - tfoot:last-child tr:last-child > th:first-child { - .border-bottom-left-radius(@baseBorderRadius); - } - // For last th/td (can be either) in the last row in the last thead, tbody, and tfoot - thead:last-child tr:last-child > th:last-child, - tbody:last-child tr:last-child > td:last-child, - tbody:last-child tr:last-child > th:last-child, - tfoot:last-child tr:last-child > td:last-child, - tfoot:last-child tr:last-child > th:last-child { - .border-bottom-right-radius(@baseBorderRadius); - } - - // Clear border-radius for first and last td in the last row in the last tbody for table with tfoot - tfoot + tbody:last-child tr:last-child td:first-child { - .border-bottom-left-radius(0); - } - tfoot + tbody:last-child tr:last-child td:last-child { - .border-bottom-right-radius(0); - } - - // Special fixes to round the left border on the first td/th - caption + thead tr:first-child th:first-child, - caption + tbody tr:first-child td:first-child, - colgroup + thead tr:first-child th:first-child, - colgroup + tbody tr:first-child td:first-child { - .border-top-left-radius(@baseBorderRadius); - } - caption + thead tr:first-child th:last-child, - caption + tbody tr:first-child td:last-child, - colgroup + thead tr:first-child th:last-child, - colgroup + tbody tr:first-child td:last-child { - .border-top-right-radius(@baseBorderRadius); - } - } - - -// ZEBRA-STRIPING -// -------------- - +// Zebra-striping +// // Default zebra-stripe styles (alternating gray and transparent backgrounds) + .table-striped { - tbody { - > tr:nth-child(odd) > td, - > tr:nth-child(odd) > th { - background-color: @tableBackgroundAccent; + > tbody > tr:nth-child(odd) { + > td, + > th { + background-color: @table-bg-accent; } } } -// HOVER EFFECT -// ------------ +// Hover effect +// // Placed here since it has to come after the potential zebra striping + .table-hover { - tbody { - tr:hover > td, - tr:hover > th { - background-color: @tableBackgroundHover; + > tbody > tr:hover { + > td, + > th { + background-color: @table-bg-hover; } } } -// TABLE CELL SIZING -// ----------------- +// Table cell sizing +// +// Reset default table behavior -// Reset default grid behavior -table td[class*="span"], -table th[class*="span"], -.row-fluid table td[class*="span"], -.row-fluid table th[class*="span"] { - display: table-cell; - float: none; // undo default grid column styles - margin-left: 0; // undo default grid column styles +table col[class*="col-"] { + position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623) + float: none; + display: table-column; } - -// Change the column widths to account for td/th padding -.table td, -.table th { - &.span1 { .tableColumns(1); } - &.span2 { .tableColumns(2); } - &.span3 { .tableColumns(3); } - &.span4 { .tableColumns(4); } - &.span5 { .tableColumns(5); } - &.span6 { .tableColumns(6); } - &.span7 { .tableColumns(7); } - &.span8 { .tableColumns(8); } - &.span9 { .tableColumns(9); } - &.span10 { .tableColumns(10); } - &.span11 { .tableColumns(11); } - &.span12 { .tableColumns(12); } +table { + td, + th { + &[class*="col-"] { + position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623) + float: none; + display: table-cell; + } + } } +// Table backgrounds +// +// Exact selectors below required to override `.table-striped` and prevent +// inheritance to nested tables. -// TABLE BACKGROUNDS -// ----------------- -// Exact selectors below required to override .table-striped +// Generate the contextual variants +.table-row-variant(active; @table-bg-active); +.table-row-variant(success; @state-success-bg); +.table-row-variant(info; @state-info-bg); +.table-row-variant(warning; @state-warning-bg); +.table-row-variant(danger; @state-danger-bg); -.table tbody tr { - &.success > td { - background-color: @successBackground; - } - &.error > td { - background-color: @errorBackground; - } - &.warning > td { - background-color: @warningBackground; - } - &.info > td { - background-color: @infoBackground; - } -} - -// Hover states for .table-hover -.table-hover tbody tr { - &.success:hover > td { - background-color: darken(@successBackground, 5%); - } - &.error:hover > td { - background-color: darken(@errorBackground, 5%); - } - &.warning:hover > td { - background-color: darken(@warningBackground, 5%); - } - &.info:hover > td { - background-color: darken(@infoBackground, 5%); + +// Responsive tables +// +// Wrap your tables in `.table-responsive` and we'll make them mobile friendly +// by enabling horizontal scrolling. Only applies <768px. Everything above that +// will display normally. + +@media (max-width: @screen-xs-max) { + .table-responsive { + width: 100%; + margin-bottom: (@line-height-computed * 0.75); + overflow-y: hidden; + overflow-x: scroll; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid @table-border-color; + -webkit-overflow-scrolling: touch; + + // Tighten up spacing + > .table { + margin-bottom: 0; + + // Ensure the content doesn't wrap + > thead, + > tbody, + > tfoot { + > tr { + > th, + > td { + white-space: nowrap; + } + } + } + } + + // Special overrides for the bordered tables + > .table-bordered { + border: 0; + + // Nuke the appropriate borders so that the parent can handle them + > thead, + > tbody, + > tfoot { + > tr { + > th:first-child, + > td:first-child { + border-left: 0; + } + > th:last-child, + > td:last-child { + border-right: 0; + } + } + } + + // Only nuke the last row's bottom-border in `tbody` and `tfoot` since + // chances are there will be only one `tr` in a `thead` and that would + // remove the border altogether. + > tbody, + > tfoot { + > tr:last-child { + > th, + > td { + border-bottom: 0; + } + } + } + + } } } diff --git a/src/UI/Content/Bootstrap/theme.less b/src/UI/Content/Bootstrap/theme.less new file mode 100644 index 000000000..6f957fb39 --- /dev/null +++ b/src/UI/Content/Bootstrap/theme.less @@ -0,0 +1,247 @@ + +// +// Load core variables and mixins +// -------------------------------------------------- + +@import "variables.less"; +@import "mixins.less"; + + + +// +// Buttons +// -------------------------------------------------- + +// Common styles +.btn-default, +.btn-primary, +.btn-success, +.btn-info, +.btn-warning, +.btn-danger { + text-shadow: 0 -1px 0 rgba(0,0,0,.2); + @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075); + .box-shadow(@shadow); + + // Reset the shadow + &:active, + &.active { + .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + } +} + +// Mixin for generating new styles +.btn-styles(@btn-color: #555) { + #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%)); + .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners + background-repeat: repeat-x; + border-color: darken(@btn-color, 14%); + + &:hover, + &:focus { + background-color: darken(@btn-color, 12%); + background-position: 0 -15px; + } + + &:active, + &.active { + background-color: darken(@btn-color, 12%); + border-color: darken(@btn-color, 14%); + } +} + +// Common styles +.btn { + // Remove the gradient for the pressed/active state + &:active, + &.active { + background-image: none; + } +} + +// Apply the mixin to the buttons +.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; } +.btn-primary { .btn-styles(@btn-primary-bg); } +.btn-success { .btn-styles(@btn-success-bg); } +.btn-info { .btn-styles(@btn-info-bg); } +.btn-warning { .btn-styles(@btn-warning-bg); } +.btn-danger { .btn-styles(@btn-danger-bg); } + + + +// +// Images +// -------------------------------------------------- + +.thumbnail, +.img-thumbnail { + .box-shadow(0 1px 2px rgba(0,0,0,.075)); +} + + + +// +// Dropdowns +// -------------------------------------------------- + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%)); + background-color: darken(@dropdown-link-hover-bg, 5%); +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%)); + background-color: darken(@dropdown-link-active-bg, 5%); +} + + + +// +// Navbar +// -------------------------------------------------- + +// Default navbar +.navbar-default { + #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg); + .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered + border-radius: @navbar-border-radius; + @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075); + .box-shadow(@shadow); + + .navbar-nav > .active > a { + #gradient > .vertical(@start-color: darken(@navbar-default-bg, 5%); @end-color: darken(@navbar-default-bg, 2%)); + .box-shadow(inset 0 3px 9px rgba(0,0,0,.075)); + } +} +.navbar-brand, +.navbar-nav > li > a { + text-shadow: 0 1px 0 rgba(255,255,255,.25); +} + +// Inverted navbar +.navbar-inverse { + #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg); + .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered + + .navbar-nav > .active > a { + #gradient > .vertical(@start-color: @navbar-inverse-bg; @end-color: lighten(@navbar-inverse-bg, 2.5%)); + .box-shadow(inset 0 3px 9px rgba(0,0,0,.25)); + } + + .navbar-brand, + .navbar-nav > li > a { + text-shadow: 0 -1px 0 rgba(0,0,0,.25); + } +} + +// Undo rounded corners in static and fixed navbars +.navbar-static-top, +.navbar-fixed-top, +.navbar-fixed-bottom { + border-radius: 0; +} + + + +// +// Alerts +// -------------------------------------------------- + +// Common styles +.alert { + text-shadow: 0 1px 0 rgba(255,255,255,.2); + @shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05); + .box-shadow(@shadow); +} + +// Mixin for generating new styles +.alert-styles(@color) { + #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%)); + border-color: darken(@color, 15%); +} + +// Apply the mixin to the alerts +.alert-success { .alert-styles(@alert-success-bg); } +.alert-info { .alert-styles(@alert-info-bg); } +.alert-warning { .alert-styles(@alert-warning-bg); } +.alert-danger { .alert-styles(@alert-danger-bg); } + + + +// +// Progress bars +// -------------------------------------------------- + +// Give the progress background some depth +.progress { + #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg) +} + +// Mixin for generating new styles +.progress-bar-styles(@color) { + #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%)); +} + +// Apply the mixin to the progress bars +.progress-bar { .progress-bar-styles(@progress-bar-bg); } +.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); } +.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); } +.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); } +.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); } + + + +// +// List groups +// -------------------------------------------------- + +.list-group { + border-radius: @border-radius-base; + .box-shadow(0 1px 2px rgba(0,0,0,.075)); +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%); + #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%)); + border-color: darken(@list-group-active-border, 7.5%); +} + + + +// +// Panels +// -------------------------------------------------- + +// Common styles +.panel { + .box-shadow(0 1px 2px rgba(0,0,0,.05)); +} + +// Mixin for generating new styles +.panel-heading-styles(@color) { + #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%)); +} + +// Apply the mixin to the panel headings only +.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); } +.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); } +.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); } +.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); } +.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); } +.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); } + + + +// +// Wells +// -------------------------------------------------- + +.well { + #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg); + border-color: darken(@well-bg, 10%); + @shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1); + .box-shadow(@shadow); +} diff --git a/src/UI/Content/Bootstrap/thumbnails.less b/src/UI/Content/Bootstrap/thumbnails.less index 4fd07d253..c428920bc 100644 --- a/src/UI/Content/Bootstrap/thumbnails.less +++ b/src/UI/Content/Bootstrap/thumbnails.less @@ -3,51 +3,34 @@ // -------------------------------------------------- -// Note: `.thumbnails` and `.thumbnails > li` are overriden in responsive files - -// Make wrapper ul behave like the grid -.thumbnails { - margin-left: -@gridGutterWidth; - list-style: none; - .clearfix(); -} -// Fluid rows have no left margin -.row-fluid .thumbnails { - margin-left: 0; -} - -// Float li to make thumbnails appear in a row -.thumbnails > li { - float: left; // Explicity set the float since we don't require .span* classes - margin-bottom: @baseLineHeight; - margin-left: @gridGutterWidth; -} - -// The actual thumbnail (can be `a` or `div`) +// Mixin and adjust the regular image class .thumbnail { display: block; - padding: 4px; - line-height: @baseLineHeight; - border: 1px solid #ddd; - .border-radius(@baseBorderRadius); - .box-shadow(0 1px 3px rgba(0,0,0,.055)); + padding: @thumbnail-padding; + margin-bottom: @line-height-computed; + line-height: @line-height-base; + background-color: @thumbnail-bg; + border: 1px solid @thumbnail-border; + border-radius: @thumbnail-border-radius; .transition(all .2s ease-in-out); -} -// Add a hover/focus state for linked versions only -a.thumbnail:hover, -a.thumbnail:focus { - border-color: @linkColor; - .box-shadow(0 1px 4px rgba(0,105,214,.25)); -} -// Images and captions -.thumbnail > img { - display: block; - max-width: 100%; - margin-left: auto; - margin-right: auto; -} -.thumbnail .caption { - padding: 9px; - color: @gray; + > img, + a > img { + &:extend(.img-responsive); + margin-left: auto; + margin-right: auto; + } + + // Add a hover state for linked versions only + a&:hover, + a&:focus, + a&.active { + border-color: @link-color; + } + + // Image captions + .caption { + padding: @thumbnail-caption-padding; + color: @thumbnail-caption-color; + } } diff --git a/src/UI/Content/Bootstrap/tooltip.less b/src/UI/Content/Bootstrap/tooltip.less index 83d5f2bd7..bd626996f 100644 --- a/src/UI/Content/Bootstrap/tooltip.less +++ b/src/UI/Content/Bootstrap/tooltip.less @@ -6,28 +6,29 @@ // Base class .tooltip { position: absolute; - z-index: @zindexTooltip; + z-index: @zindex-tooltip; display: block; visibility: visible; - font-size: 11px; + font-size: @font-size-small; line-height: 1.4; .opacity(0); - &.in { .opacity(80); } - &.top { margin-top: -3px; padding: 5px 0; } - &.right { margin-left: 3px; padding: 0 5px; } - &.bottom { margin-top: 3px; padding: 5px 0; } - &.left { margin-left: -3px; padding: 0 5px; } + + &.in { .opacity(@tooltip-opacity); } + &.top { margin-top: -3px; padding: @tooltip-arrow-width 0; } + &.right { margin-left: 3px; padding: 0 @tooltip-arrow-width; } + &.bottom { margin-top: 3px; padding: @tooltip-arrow-width 0; } + &.left { margin-left: -3px; padding: 0 @tooltip-arrow-width; } } // Wrapper for the tooltip content .tooltip-inner { - max-width: 200px; - padding: 8px; - color: @tooltipColor; + max-width: @tooltip-max-width; + padding: 3px 8px; + color: @tooltip-color; text-align: center; text-decoration: none; - background-color: @tooltipBackground; - .border-radius(@baseBorderRadius); + background-color: @tooltip-bg; + border-radius: @border-radius-base; } // Arrows @@ -42,29 +43,53 @@ &.top .tooltip-arrow { bottom: 0; left: 50%; - margin-left: -@tooltipArrowWidth; - border-width: @tooltipArrowWidth @tooltipArrowWidth 0; - border-top-color: @tooltipArrowColor; + margin-left: -@tooltip-arrow-width; + border-width: @tooltip-arrow-width @tooltip-arrow-width 0; + border-top-color: @tooltip-arrow-color; + } + &.top-left .tooltip-arrow { + bottom: 0; + left: @tooltip-arrow-width; + border-width: @tooltip-arrow-width @tooltip-arrow-width 0; + border-top-color: @tooltip-arrow-color; + } + &.top-right .tooltip-arrow { + bottom: 0; + right: @tooltip-arrow-width; + border-width: @tooltip-arrow-width @tooltip-arrow-width 0; + border-top-color: @tooltip-arrow-color; } &.right .tooltip-arrow { top: 50%; left: 0; - margin-top: -@tooltipArrowWidth; - border-width: @tooltipArrowWidth @tooltipArrowWidth @tooltipArrowWidth 0; - border-right-color: @tooltipArrowColor; + margin-top: -@tooltip-arrow-width; + border-width: @tooltip-arrow-width @tooltip-arrow-width @tooltip-arrow-width 0; + border-right-color: @tooltip-arrow-color; } &.left .tooltip-arrow { top: 50%; right: 0; - margin-top: -@tooltipArrowWidth; - border-width: @tooltipArrowWidth 0 @tooltipArrowWidth @tooltipArrowWidth; - border-left-color: @tooltipArrowColor; + margin-top: -@tooltip-arrow-width; + border-width: @tooltip-arrow-width 0 @tooltip-arrow-width @tooltip-arrow-width; + border-left-color: @tooltip-arrow-color; } &.bottom .tooltip-arrow { top: 0; left: 50%; - margin-left: -@tooltipArrowWidth; - border-width: 0 @tooltipArrowWidth @tooltipArrowWidth; - border-bottom-color: @tooltipArrowColor; + margin-left: -@tooltip-arrow-width; + border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; + border-bottom-color: @tooltip-arrow-color; + } + &.bottom-left .tooltip-arrow { + top: 0; + left: @tooltip-arrow-width; + border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; + border-bottom-color: @tooltip-arrow-color; + } + &.bottom-right .tooltip-arrow { + top: 0; + right: @tooltip-arrow-width; + border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; + border-bottom-color: @tooltip-arrow-color; } } diff --git a/src/UI/Content/Bootstrap/type.less b/src/UI/Content/Bootstrap/type.less index 337138ac8..5e2a21905 100644 --- a/src/UI/Content/Bootstrap/type.less +++ b/src/UI/Content/Bootstrap/type.less @@ -3,17 +3,71 @@ // -------------------------------------------------- +// Headings +// ------------------------- + +h1, h2, h3, h4, h5, h6, +.h1, .h2, .h3, .h4, .h5, .h6 { + font-family: @headings-font-family; + font-weight: @headings-font-weight; + line-height: @headings-line-height; + color: @headings-color; + + small, + .small { + font-weight: normal; + line-height: 1; + color: @headings-small-color; + } +} + +h1, .h1, +h2, .h2, +h3, .h3 { + margin-top: @line-height-computed; + margin-bottom: (@line-height-computed / 2); + + small, + .small { + font-size: 65%; + } +} +h4, .h4, +h5, .h5, +h6, .h6 { + margin-top: (@line-height-computed / 2); + margin-bottom: (@line-height-computed / 2); + + small, + .small { + font-size: 75%; + } +} + +h1, .h1 { font-size: @font-size-h1; } +h2, .h2 { font-size: @font-size-h2; } +h3, .h3 { font-size: @font-size-h3; } +h4, .h4 { font-size: @font-size-h4; } +h5, .h5 { font-size: @font-size-h5; } +h6, .h6 { font-size: @font-size-h6; } + + // Body text // ------------------------- p { - margin: 0 0 @baseLineHeight / 2; + margin: 0 0 (@line-height-computed / 2); } + .lead { - margin-bottom: @baseLineHeight; - font-size: @baseFontSize * 1.5; + margin-bottom: @line-height-computed; + font-size: floor((@font-size-base * 1.15)); font-weight: 200; - line-height: @baseLineHeight * 1.5; + line-height: 1.4; + + @media (min-width: @screen-sm-min) { + font-size: (@font-size-base * 1.5); + } } @@ -21,116 +75,100 @@ p { // ------------------------- // Ex: 14px base font * 85% = about 12px -small { font-size: 85%; } +small, +.small { font-size: 85%; } -strong { font-weight: bold; } -em { font-style: italic; } +// Undo browser default styling cite { font-style: normal; } -// Utility classes -.muted { color: @grayLight; } -a.muted:hover, -a.muted:focus { color: darken(@grayLight, 10%); } - -.text-warning { color: @warningText; } -a.text-warning:hover, -a.text-warning:focus { color: darken(@warningText, 10%); } - -.text-error { color: @errorText; } -a.text-error:hover, -a.text-error:focus { color: darken(@errorText, 10%); } - -.text-info { color: @infoText; } -a.text-info:hover, -a.text-info:focus { color: darken(@infoText, 10%); } - -.text-success { color: @successText; } -a.text-success:hover, -a.text-success:focus { color: darken(@successText, 10%); } - +// Alignment .text-left { text-align: left; } .text-right { text-align: right; } .text-center { text-align: center; } +.text-justify { text-align: justify; } - -// Headings -// ------------------------- - -h1, h2, h3, h4, h5, h6 { - margin: (@baseLineHeight / 2) 0; - font-family: @headingsFontFamily; - font-weight: @headingsFontWeight; - line-height: @baseLineHeight; - color: @headingsColor; - text-rendering: optimizelegibility; // Fix the character spacing for headings - small { - font-weight: normal; - line-height: 1; - color: @grayLight; - } +// Contextual colors +.text-muted { + color: @text-muted; +} +.text-primary { + .text-emphasis-variant(@brand-primary); +} +.text-success { + .text-emphasis-variant(@state-success-text); +} +.text-info { + .text-emphasis-variant(@state-info-text); +} +.text-warning { + .text-emphasis-variant(@state-warning-text); +} +.text-danger { + .text-emphasis-variant(@state-danger-text); } -h1, -h2, -h3 { line-height: @baseLineHeight * 2; } - -h1 { font-size: @baseFontSize * 2.75; } // ~38px -h2 { font-size: @baseFontSize * 2.25; } // ~32px -h3 { font-size: @baseFontSize * 1.75; } // ~24px -h4 { font-size: @baseFontSize * 1.25; } // ~18px -h5 { font-size: @baseFontSize; } -h6 { font-size: @baseFontSize * 0.85; } // ~12px - -h1 small { font-size: @baseFontSize * 1.75; } // ~24px -h2 small { font-size: @baseFontSize * 1.25; } // ~18px -h3 small { font-size: @baseFontSize; } -h4 small { font-size: @baseFontSize; } +// Contextual backgrounds +// For now we'll leave these alongside the text classes until v4 when we can +// safely shift things around (per SemVer rules). +.bg-primary { + // Given the contrast here, this is the only class to have its color inverted + // automatically. + color: #fff; + .bg-variant(@brand-primary); +} +.bg-success { + .bg-variant(@state-success-bg); +} +.bg-info { + .bg-variant(@state-info-bg); +} +.bg-warning { + .bg-variant(@state-warning-bg); +} +.bg-danger { + .bg-variant(@state-danger-bg); +} // Page header // ------------------------- .page-header { - padding-bottom: (@baseLineHeight / 2) - 1; - margin: @baseLineHeight 0 (@baseLineHeight * 1.5); - border-bottom: 1px solid @grayLighter; + padding-bottom: ((@line-height-computed / 2) - 1); + margin: (@line-height-computed * 2) 0 @line-height-computed; + border-bottom: 1px solid @page-header-border-color; } - // Lists // -------------------------------------------------- // Unordered and Ordered lists -ul, ol { - padding: 0; - margin: 0 0 @baseLineHeight / 2 25px; -} -ul ul, -ul ol, -ol ol, -ol ul { - margin-bottom: 0; -} -li { - line-height: @baseLineHeight; +ul, +ol { + margin-top: 0; + margin-bottom: (@line-height-computed / 2); + ul, + ol { + margin-bottom: 0; + } } -// Remove default list styles -ul.unstyled, -ol.unstyled { - margin-left: 0; +// List options + +// Unstyled keeps list items block level, just removes default browser padding and list-style +.list-unstyled { + padding-left: 0; list-style: none; } -// Single-line list items -ul.inline, -ol.inline { - margin-left: 0; - list-style: none; +// Inline turns list items into inline-block +.list-inline { + .list-unstyled(); + margin-left: -5px; + > li { display: inline-block; - .ie7-inline-block(); padding-left: 5px; padding-right: 5px; } @@ -138,101 +176,110 @@ ol.inline { // Description Lists dl { - margin-bottom: @baseLineHeight; + margin-top: 0; // Remove browser default + margin-bottom: @line-height-computed; } dt, dd { - line-height: @baseLineHeight; + line-height: @line-height-base; } dt { font-weight: bold; } dd { - margin-left: @baseLineHeight / 2; + margin-left: 0; // Undo browser default } -// Horizontal layout (like forms) -.dl-horizontal { - .clearfix(); // Ensure dl clears floats if empty dd elements present - dt { - float: left; - width: @horizontalComponentOffset - 20; - clear: left; - text-align: right; - .text-overflow(); - } - dd { - margin-left: @horizontalComponentOffset; + +// Horizontal description lists +// +// Defaults to being stacked without any of the below styles applied, until the +// grid breakpoint is reached (default of ~768px). + +@media (min-width: @grid-float-breakpoint) { + .dl-horizontal { + dt { + float: left; + width: (@component-offset-horizontal - 20); + clear: left; + text-align: right; + .text-overflow(); + } + dd { + margin-left: @component-offset-horizontal; + &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present + } } } // MISC // ---- -// Horizontal rules -hr { - margin: @baseLineHeight 0; - border: 0; - border-top: 1px solid @hrBorder; - border-bottom: 1px solid @white; -} - // Abbreviations and acronyms abbr[title], -// Added data-* attribute to help out our tooltip plugin, per https://github.com/twitter/bootstrap/issues/5257 +// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257 abbr[data-original-title] { cursor: help; - border-bottom: 1px dotted @grayLight; + border-bottom: 1px dotted @abbr-border-color; } -abbr.initialism { +.initialism { font-size: 90%; text-transform: uppercase; } // Blockquotes blockquote { - padding: 0 0 0 15px; - margin: 0 0 @baseLineHeight; - border-left: 5px solid @grayLighter; - p { - margin-bottom: 0; - font-size: @baseFontSize * 1.25; - font-weight: 300; - line-height: 1.25; - } - small { - display: block; - line-height: @baseLineHeight; - color: @grayLight; - &:before { - content: '\2014 \00A0'; + padding: (@line-height-computed / 2) @line-height-computed; + margin: 0 0 @line-height-computed; + font-size: @blockquote-font-size; + border-left: 5px solid @blockquote-border-color; + + p, + ul, + ol { + &:last-child { + margin-bottom: 0; } } - // Float right with text-align: right - &.pull-right { - float: right; - padding-right: 15px; - padding-left: 0; - border-right: 5px solid @grayLighter; - border-left: 0; - p, - small { - text-align: right; + // Note: Deprecated small and .small as of v3.1.0 + // Context: https://github.com/twbs/bootstrap/issues/11660 + footer, + small, + .small { + display: block; + font-size: 80%; // back to default font-size + line-height: @line-height-base; + color: @blockquote-small-color; + + &:before { + content: '\2014 \00A0'; // em dash, nbsp } - small { - &:before { - content: ''; - } - &:after { - content: '\00A0 \2014'; - } + } +} + +// Opposite alignment of blockquote +// +// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0. +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid @blockquote-border-color; + border-left: 0; + text-align: right; + + // Account for citation + footer, + small, + .small { + &:before { content: ''; } + &:after { + content: '\00A0 \2014'; // nbsp, em dash } } } // Quotes -q:before, -q:after, blockquote:before, blockquote:after { content: ""; @@ -240,8 +287,7 @@ blockquote:after { // Addresses address { - display: block; - margin-bottom: @baseLineHeight; + margin-bottom: @line-height-computed; font-style: normal; - line-height: @baseLineHeight; + line-height: @line-height-base; } diff --git a/src/UI/Content/Bootstrap/utilities.less b/src/UI/Content/Bootstrap/utilities.less index 314b4ffdb..a26031214 100644 --- a/src/UI/Content/Bootstrap/utilities.less +++ b/src/UI/Content/Bootstrap/utilities.less @@ -3,28 +3,54 @@ // -------------------------------------------------- -// Quick floats +// Floats +// ------------------------- + +.clearfix { + .clearfix(); +} +.center-block { + .center-block(); +} .pull-right { - float: right; + float: right !important; } .pull-left { - float: left; + float: left !important; } + // Toggling content +// ------------------------- + +// Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1 .hide { - display: none; + display: none !important; } .show { - display: block; + display: block !important; } - -// Visibility .invisible { visibility: hidden; } +.text-hide { + .text-hide(); +} + + +// Hide from screenreaders and browsers +// +// Credit: HTML5 Boilerplate + +.hidden { + display: none !important; + visibility: hidden !important; +} + // For Affix plugin +// ------------------------- + .affix { position: fixed; } diff --git a/src/UI/Content/Bootstrap/variables.less b/src/UI/Content/Bootstrap/variables.less index 9652eaac1..3846adc59 100644 --- a/src/UI/Content/Bootstrap/variables.less +++ b/src/UI/Content/Bootstrap/variables.less @@ -3,299 +3,827 @@ // -------------------------------------------------- -// Global values -// -------------------------------------------------- +//== Colors +// +//## Gray and brand colors for use across Bootstrap. + +@gray-darker: lighten(#000, 13.5%); // #222 +@gray-dark: lighten(#000, 20%); // #333 +@gray: lighten(#000, 33.5%); // #555 +@gray-light: lighten(#000, 60%); // #999 +@gray-lighter: lighten(#000, 93.5%); // #eee + +@brand-primary: #428bca; +@brand-success: #5cb85c; +@brand-info: #5bc0de; +@brand-warning: #f0ad4e; +@brand-danger: #d9534f; -// Grays -// ------------------------- -@black: #000; -@grayDarker: #222; -@grayDark: #333; -@gray: #555; -@grayLight: #999; -@grayLighter: #eee; -@white: #fff; +//== Scaffolding +// +// ## Settings for some of the most global styles. + +//** Background color for `<body>`. +@body-bg: #fff; +//** Global text color on `<body>`. +@text-color: @gray-dark; + +//** Global textual link color. +@link-color: @brand-primary; +//** Link hover color set via `darken()` function. +@link-hover-color: darken(@link-color, 15%); -// Accent colors -// ------------------------- -@blue: #049cdb; -@blueDark: #0064cd; -@green: #46a546; -@red: #9d261d; -@yellow: #ffc40d; -@orange: #f89406; -@pink: #c3325f; -@purple: #7a43b6; +//== Typography +// +//## Font, line-height, and color for body text, headings, and more. + +@font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif; +@font-family-serif: Georgia, "Times New Roman", Times, serif; +//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`. +@font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace; +@font-family-base: @font-family-sans-serif; + +@font-size-base: 14px; +@font-size-large: ceil((@font-size-base * 1.25)); // ~18px +@font-size-small: ceil((@font-size-base * 0.85)); // ~12px + +@font-size-h1: floor((@font-size-base * 2.6)); // ~36px +@font-size-h2: floor((@font-size-base * 2.15)); // ~30px +@font-size-h3: ceil((@font-size-base * 1.7)); // ~24px +@font-size-h4: ceil((@font-size-base * 1.25)); // ~18px +@font-size-h5: @font-size-base; +@font-size-h6: ceil((@font-size-base * 0.85)); // ~12px + +//** Unit-less `line-height` for use in components like buttons. +@line-height-base: 1.428571429; // 20/14 +//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc. +@line-height-computed: floor((@font-size-base * @line-height-base)); // ~20px + +//** By default, this inherits from the `<body>`. +@headings-font-family: inherit; +@headings-font-weight: 500; +@headings-line-height: 1.1; +@headings-color: inherit; -// Scaffolding -// ------------------------- -@bodyBackground: @white; -@textColor: @grayDark; +//-- Iconography +// +//## Specify custom locations of the include Glyphicons icon font. Useful for those including Bootstrap via Bower. + +@icon-font-path: "../fonts/"; +@icon-font-name: "glyphicons-halflings-regular"; +@icon-font-svg-id: "glyphicons_halflingsregular"; + +//== Components +// +//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). + +@padding-base-vertical: 6px; +@padding-base-horizontal: 12px; + +@padding-large-vertical: 10px; +@padding-large-horizontal: 16px; + +@padding-small-vertical: 5px; +@padding-small-horizontal: 10px; + +@padding-xs-vertical: 1px; +@padding-xs-horizontal: 5px; + +@line-height-large: 1.33; +@line-height-small: 1.5; + +@border-radius-base: 4px; +@border-radius-large: 6px; +@border-radius-small: 3px; + +//** Global color for active items (e.g., navs or dropdowns). +@component-active-color: #fff; +//** Global background color for active items (e.g., navs or dropdowns). +@component-active-bg: @brand-primary; + +//** Width of the `border` for generating carets that indicator dropdowns. +@caret-width-base: 4px; +//** Carets increase slightly in size for larger components. +@caret-width-large: 5px; -// Links -// ------------------------- -@linkColor: #08c; -@linkColorHover: darken(@linkColor, 15%); +//== Tables +// +//## Customizes the `.table` component with basic values, each used across all table variations. + +//** Padding for `<th>`s and `<td>`s. +@table-cell-padding: 8px; +//** Padding for cells in `.table-condensed`. +@table-condensed-cell-padding: 5px; + +//** Default background color used for all tables. +@table-bg: transparent; +//** Background color used for `.table-striped`. +@table-bg-accent: #f9f9f9; +//** Background color used for `.table-hover`. +@table-bg-hover: #f5f5f5; +@table-bg-active: @table-bg-hover; + +//** Border color for table and cell borders. +@table-border-color: #ddd; -// Typography -// ------------------------- -@sansFontFamily: "open sans", "Segoe UI","Segoe WP", "Helvetica Neue", Helvetica, Arial, sans-serif; -@serifFontFamily: Georgia, "Times New Roman", Times, serif; -@monoFontFamily: Monaco, Menlo, Consolas, "Courier New", monospace; +//== Buttons +// +//## For each of Bootstrap's buttons, define text, background and border color. -@baseFontSize: 14px; -@baseFontFamily: @sansFontFamily; -@baseLineHeight: 20px; -@altFontFamily: @serifFontFamily; +@btn-font-weight: normal; -@headingsFontFamily: inherit; // empty to use BS default, @baseFontFamily -@headingsFontWeight: 100; // instead of browser default, bold -@headingsColor: inherit; // empty to use BS default, @textColor +@btn-default-color: #333; +@btn-default-bg: #fff; +@btn-default-border: #ccc; + +@btn-primary-color: #fff; +@btn-primary-bg: @brand-primary; +@btn-primary-border: darken(@btn-primary-bg, 5%); + +@btn-success-color: #fff; +@btn-success-bg: @brand-success; +@btn-success-border: darken(@btn-success-bg, 5%); + +@btn-info-color: #fff; +@btn-info-bg: @brand-info; +@btn-info-border: darken(@btn-info-bg, 5%); + +@btn-warning-color: #fff; +@btn-warning-bg: @brand-warning; +@btn-warning-border: darken(@btn-warning-bg, 5%); + +@btn-danger-color: #fff; +@btn-danger-bg: @brand-danger; +@btn-danger-border: darken(@btn-danger-bg, 5%); + +@btn-link-disabled-color: @gray-light; -// Component sizing -// ------------------------- -// Based on 14px font-size and 20px line-height +//== Forms +// +//## -@fontSizeLarge: @baseFontSize * 1.25; // ~18px -@fontSizeSmall: @baseFontSize * 0.85; // ~12px -@fontSizeMini: @baseFontSize * 0.75; // ~11px +//** `<input>` background color +@input-bg: #fff; +//** `<input disabled>` background color +@input-bg-disabled: @gray-lighter; -@paddingLarge: 11px 19px; // 44px -@paddingSmall: 2px 10px; // 26px -@paddingMini: 0 6px; // 22px +//** Text color for `<input>`s +@input-color: @gray; +//** `<input>` border color +@input-border: #ccc; +//** `<input>` border radius +@input-border-radius: @border-radius-base; +//** Border color for inputs on focus +@input-border-focus: #66afe9; -@baseBorderRadius: 4px; -@borderRadiusLarge: 6px; -@borderRadiusSmall: 3px; +//** Placeholder text color +@input-color-placeholder: @gray-light; + +//** Default `.form-control` height +@input-height-base: (@line-height-computed + (@padding-base-vertical * 2) + 2); +//** Large `.form-control` height +@input-height-large: (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2); +//** Small `.form-control` height +@input-height-small: (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2); + +@legend-color: @gray-dark; +@legend-border-color: #e5e5e5; + +//** Background color for textual input addons +@input-group-addon-bg: @gray-lighter; +//** Border color for textual input addons +@input-group-addon-border-color: @input-border; -// Tables -// ------------------------- -@tableBackground: transparent; // overall background-color -@tableBackgroundAccent: #f9f9f9; // for striping -@tableBackgroundHover: #f5f5f5; // for hover -@tableBorder: #ddd; // table and cell border +//== Dropdowns +// +//## Dropdown menu container and contents. -// Buttons -// ------------------------- -@btnBackground: @white; -@btnBackgroundHighlight: darken(@white, 10%); -@btnBorder: #ccc; +//** Background for the dropdown menu. +@dropdown-bg: #fff; +//** Dropdown menu `border-color`. +@dropdown-border: rgba(0,0,0,.15); +//** Dropdown menu `border-color` **for IE8**. +@dropdown-fallback-border: #ccc; +//** Divider color for between dropdown items. +@dropdown-divider-bg: #e5e5e5; -@btnPrimaryBackground: @linkColor; -@btnPrimaryBackgroundHighlight: spin(@btnPrimaryBackground, 20%); +//** Dropdown link text color. +@dropdown-link-color: @gray-dark; +//** Hover color for dropdown links. +@dropdown-link-hover-color: darken(@gray-dark, 5%); +//** Hover background for dropdown links. +@dropdown-link-hover-bg: #f5f5f5; -@btnInfoBackground: #5bc0de; -@btnInfoBackgroundHighlight: #2f96b4; +//** Active dropdown menu item text color. +@dropdown-link-active-color: @component-active-color; +//** Active dropdown menu item background color. +@dropdown-link-active-bg: @component-active-bg; -@btnSuccessBackground: #62c462; -@btnSuccessBackgroundHighlight: #51a351; +//** Disabled dropdown menu item background color. +@dropdown-link-disabled-color: @gray-light; -@btnWarningBackground: lighten(@orange, 15%); -@btnWarningBackgroundHighlight: @orange; +//** Text color for headers within dropdown menus. +@dropdown-header-color: @gray-light; -@btnDangerBackground: #ee5f5b; -@btnDangerBackgroundHighlight: #bd362f; - -@btnInverseBackground: #444; -@btnInverseBackgroundHighlight: @grayDarker; +// Note: Deprecated @dropdown-caret-color as of v3.1.0 +@dropdown-caret-color: #000; -// Forms -// ------------------------- -@inputBackground: @white; -@inputBorder: #ccc; -@inputBorderRadius: @baseBorderRadius; -@inputDisabledBackground: @grayLighter; -@formActionsBackground: #f5f5f5; -@inputHeight: @baseLineHeight + 10px; // base line-height + 8px vertical padding + 2px top/bottom border +//-- Z-index master list +// +// Warning: Avoid customizing these values. They're used for a bird's eye view +// of components dependent on the z-axis and are designed to all work together. +// +// Note: These variables are not generated into the Customizer. + +@zindex-navbar: 1000; +@zindex-dropdown: 1000; +@zindex-popover: 1010; +@zindex-tooltip: 1030; +@zindex-navbar-fixed: 1030; +@zindex-modal-background: 1040; +@zindex-modal: 1050; -// Dropdowns -// ------------------------- -@dropdownBackground: @white; -@dropdownBorder: rgba(0,0,0,.2); -@dropdownDividerTop: #e5e5e5; -@dropdownDividerBottom: @white; +//== Media queries breakpoints +// +//## Define the breakpoints at which your layout will change, adapting to different screen sizes. -@dropdownLinkColor: @grayDark; -@dropdownLinkColorHover: @white; -@dropdownLinkColorActive: @white; +// Extra small screen / phone +// Note: Deprecated @screen-xs and @screen-phone as of v3.0.1 +@screen-xs: 480px; +@screen-xs-min: @screen-xs; +@screen-phone: @screen-xs-min; -@dropdownLinkBackgroundActive: @linkColor; -@dropdownLinkBackgroundHover: @dropdownLinkBackgroundActive; +// Small screen / tablet +// Note: Deprecated @screen-sm and @screen-tablet as of v3.0.1 +@screen-sm: 768px; +@screen-sm-min: @screen-sm; +@screen-tablet: @screen-sm-min; + +// Medium screen / desktop +// Note: Deprecated @screen-md and @screen-desktop as of v3.0.1 +@screen-md: 992px; +@screen-md-min: @screen-md; +@screen-desktop: @screen-md-min; + +// Large screen / wide desktop +// Note: Deprecated @screen-lg and @screen-lg-desktop as of v3.0.1 +@screen-lg: 1200px; +@screen-lg-min: @screen-lg; +@screen-lg-desktop: @screen-lg-min; + +// So media queries don't overlap when required, provide a maximum +@screen-xs-max: (@screen-sm-min - 1); +@screen-sm-max: (@screen-md-min - 1); +@screen-md-max: (@screen-lg-min - 1); +//== Grid system +// +//## Define your custom responsive grid. -// COMPONENT VARIABLES -// -------------------------------------------------- +//** Number of columns in the grid. +@grid-columns: 12; +//** Padding between columns. Gets divided in half for the left and right. +@grid-gutter-width: 30px; +// Navbar collapse +//** Point at which the navbar becomes uncollapsed. +@grid-float-breakpoint: @screen-sm-min; +//** Point at which the navbar begins collapsing. +@grid-float-breakpoint-max: (@grid-float-breakpoint - 1); -// Z-index master list -// ------------------------- -// Used for a bird's eye view of components dependent on the z-axis -// Try to avoid customizing these :) -@zindexDropdown: 1000; -@zindexPopover: 1010; -@zindexTooltip: 1030; -@zindexFixedNavbar: 1030; -@zindexModalBackdrop: 1040; -@zindexModal: 1050; +//== Container sizes +// +//## Define the maximum width of `.container` for different screen sizes. + +// Small screen / tablet +@container-tablet: ((720px + @grid-gutter-width)); +//** For `@screen-sm-min` and up. +@container-sm: @container-tablet; + +// Medium screen / desktop +@container-desktop: ((940px + @grid-gutter-width)); +//** For `@screen-md-min` and up. +@container-md: @container-desktop; + +// Large screen / wide desktop +@container-large-desktop: ((1140px + @grid-gutter-width)); +//** For `@screen-lg-min` and up. +@container-lg: @container-large-desktop; -// Sprite icons path -// ------------------------- -@iconSpritePath: "../img/glyphicons-halflings.png"; -@iconWhiteSpritePath: "../img/glyphicons-halflings-white.png"; +//== Navbar +// +//## +// Basics of a navbar +@navbar-height: 50px; +@navbar-margin-bottom: @line-height-computed; +@navbar-border-radius: @border-radius-base; +@navbar-padding-horizontal: floor((@grid-gutter-width / 2)); +@navbar-padding-vertical: ((@navbar-height - @line-height-computed) / 2); +@navbar-collapse-max-height: 340px; -// Input placeholder text color -// ------------------------- -@placeholderText: @grayLight; +@navbar-default-color: #777; +@navbar-default-bg: #f8f8f8; +@navbar-default-border: darken(@navbar-default-bg, 6.5%); +// Navbar links +@navbar-default-link-color: #777; +@navbar-default-link-hover-color: #333; +@navbar-default-link-hover-bg: transparent; +@navbar-default-link-active-color: #555; +@navbar-default-link-active-bg: darken(@navbar-default-bg, 6.5%); +@navbar-default-link-disabled-color: #ccc; +@navbar-default-link-disabled-bg: transparent; -// Hr border color -// ------------------------- -@hrBorder: @grayLighter; +// Navbar brand label +@navbar-default-brand-color: @navbar-default-link-color; +@navbar-default-brand-hover-color: darken(@navbar-default-brand-color, 10%); +@navbar-default-brand-hover-bg: transparent; +// Navbar toggle +@navbar-default-toggle-hover-bg: #ddd; +@navbar-default-toggle-icon-bar-bg: #888; +@navbar-default-toggle-border-color: #ddd; -// Horizontal forms & lists -// ------------------------- -@horizontalComponentOffset: 180px; - - -// Wells -// ------------------------- -@wellBackground: #f5f5f5; - - -// Navbar -// ------------------------- -@navbarCollapseWidth: 979px; -@navbarCollapseDesktopWidth: @navbarCollapseWidth + 1; - -@navbarHeight: 40px; -@navbarBackgroundHighlight: #ffffff; -@navbarBackground: darken(@navbarBackgroundHighlight, 5%); -@navbarBorder: darken(@navbarBackground, 12%); - -@navbarText: #777; -@navbarLinkColor: #777; -@navbarLinkColorHover: @grayDark; -@navbarLinkColorActive: @gray; -@navbarLinkBackgroundHover: transparent; -@navbarLinkBackgroundActive: darken(@navbarBackground, 5%); - -@navbarBrandColor: @navbarLinkColor; // Inverted navbar -@navbarInverseBackground: #111111; -@navbarInverseBackgroundHighlight: #222222; -@navbarInverseBorder: #252525; +// Reset inverted navbar basics +@navbar-inverse-color: @gray-light; +@navbar-inverse-bg: #222; +@navbar-inverse-border: darken(@navbar-inverse-bg, 10%); -@navbarInverseText: @grayLight; -@navbarInverseLinkColor: @grayLight; -@navbarInverseLinkColorHover: @white; -@navbarInverseLinkColorActive: @navbarInverseLinkColorHover; -@navbarInverseLinkBackgroundHover: transparent; -@navbarInverseLinkBackgroundActive: @navbarInverseBackground; +// Inverted navbar links +@navbar-inverse-link-color: @gray-light; +@navbar-inverse-link-hover-color: #fff; +@navbar-inverse-link-hover-bg: transparent; +@navbar-inverse-link-active-color: @navbar-inverse-link-hover-color; +@navbar-inverse-link-active-bg: darken(@navbar-inverse-bg, 10%); +@navbar-inverse-link-disabled-color: #444; +@navbar-inverse-link-disabled-bg: transparent; -@navbarInverseSearchBackground: lighten(@navbarInverseBackground, 25%); -@navbarInverseSearchBackgroundFocus: @white; -@navbarInverseSearchBorder: @navbarInverseBackground; -@navbarInverseSearchPlaceholderColor: #ccc; +// Inverted navbar brand label +@navbar-inverse-brand-color: @navbar-inverse-link-color; +@navbar-inverse-brand-hover-color: #fff; +@navbar-inverse-brand-hover-bg: transparent; -@navbarInverseBrandColor: @navbarInverseLinkColor; +// Inverted navbar toggle +@navbar-inverse-toggle-hover-bg: #333; +@navbar-inverse-toggle-icon-bar-bg: #fff; +@navbar-inverse-toggle-border-color: #333; -// Pagination -// ------------------------- -@paginationBackground: #fff; -@paginationBorder: #ddd; -@paginationActiveBackground: #f5f5f5; +//== Navs +// +//## + +//=== Shared nav styles +@nav-link-padding: 10px 15px; +@nav-link-hover-bg: @gray-lighter; + +@nav-disabled-link-color: @gray-light; +@nav-disabled-link-hover-color: @gray-light; + +@nav-open-link-hover-color: #fff; + +//== Tabs +@nav-tabs-border-color: #ddd; + +@nav-tabs-link-hover-border-color: @gray-lighter; + +@nav-tabs-active-link-hover-bg: @body-bg; +@nav-tabs-active-link-hover-color: @gray; +@nav-tabs-active-link-hover-border-color: #ddd; + +@nav-tabs-justified-link-border-color: #ddd; +@nav-tabs-justified-active-link-border-color: @body-bg; + +//== Pills +@nav-pills-border-radius: @border-radius-base; +@nav-pills-active-link-hover-bg: @component-active-bg; +@nav-pills-active-link-hover-color: @component-active-color; -// Hero unit -// ------------------------- -@heroUnitBackground: @grayLighter; -@heroUnitHeadingColor: inherit; -@heroUnitLeadColor: inherit; +//== Pagination +// +//## + +@pagination-color: @link-color; +@pagination-bg: #fff; +@pagination-border: #ddd; + +@pagination-hover-color: @link-hover-color; +@pagination-hover-bg: @gray-lighter; +@pagination-hover-border: #ddd; + +@pagination-active-color: #fff; +@pagination-active-bg: @brand-primary; +@pagination-active-border: @brand-primary; + +@pagination-disabled-color: @gray-light; +@pagination-disabled-bg: #fff; +@pagination-disabled-border: #ddd; -// Form states and alerts -// ------------------------- -@warningText: #c09853; -@warningBackground: #fcf8e3; -@warningBorder: darken(spin(@warningBackground, -10), 3%); +//== Pager +// +//## -@errorText: #b94a48; -@errorBackground: #f2dede; -@errorBorder: darken(spin(@errorBackground, -10), 3%); +@pager-bg: @pagination-bg; +@pager-border: @pagination-border; +@pager-border-radius: 15px; -@successText: #468847; -@successBackground: #dff0d8; -@successBorder: darken(spin(@successBackground, -10), 5%); +@pager-hover-bg: @pagination-hover-bg; -@infoText: #3a87ad; -@infoBackground: #d9edf7; -@infoBorder: darken(spin(@infoBackground, -10), 7%); +@pager-active-bg: @pagination-active-bg; +@pager-active-color: @pagination-active-color; + +@pager-disabled-color: @pagination-disabled-color; -// Tooltips and popovers -// ------------------------- -@tooltipColor: #fff; -@tooltipBackground: #000; -@tooltipArrowWidth: 5px; -@tooltipArrowColor: @tooltipBackground; +//== Jumbotron +// +//## -@popoverBackground: #fff; -@popoverArrowWidth: 10px; -@popoverArrowColor: #fff; -@popoverTitleBackground: darken(@popoverBackground, 3%); - -// Special enhancement for popovers -@popoverArrowOuterWidth: @popoverArrowWidth + 1; -@popoverArrowOuterColor: rgba(0,0,0,.25); +@jumbotron-padding: 30px; +@jumbotron-color: inherit; +@jumbotron-bg: @gray-lighter; +@jumbotron-heading-color: inherit; +@jumbotron-font-size: ceil((@font-size-base * 1.5)); +//== Form states and alerts +// +//## Define colors for form feedback states and, by default, alerts. -// GRID -// -------------------------------------------------- +@state-success-text: #3c763d; +@state-success-bg: #dff0d8; +@state-success-border: darken(spin(@state-success-bg, -10), 5%); + +@state-info-text: #31708f; +@state-info-bg: #d9edf7; +@state-info-border: darken(spin(@state-info-bg, -10), 7%); + +@state-warning-text: #8a6d3b; +@state-warning-bg: #fcf8e3; +@state-warning-border: darken(spin(@state-warning-bg, -10), 5%); + +@state-danger-text: #a94442; +@state-danger-bg: #f2dede; +@state-danger-border: darken(spin(@state-danger-bg, -10), 5%); -// Default 940px grid -// ------------------------- -@gridColumns: 12; -@gridColumnWidth: 75px; -@gridGutterWidth: 20px; -@gridRowWidth: (@gridColumns * @gridColumnWidth) + (@gridGutterWidth * (@gridColumns - 1)); +//== Tooltips +// +//## -// 1200px min -@gridColumnWidth1200: 70px; -@gridGutterWidth1200: 30px; -@gridRowWidth1200: (@gridColumns * @gridColumnWidth1200) + (@gridGutterWidth1200 * (@gridColumns - 1)); +//** Tooltip max width +@tooltip-max-width: 200px; +//** Tooltip text color +@tooltip-color: #fff; +//** Tooltip background color +@tooltip-bg: #000; +@tooltip-opacity: .9; -// 768px-979px -@gridColumnWidth768: 42px; -@gridGutterWidth768: 20px; -@gridRowWidth768: (@gridColumns * @gridColumnWidth768) + (@gridGutterWidth768 * (@gridColumns - 1)); +//** Tooltip arrow width +@tooltip-arrow-width: 5px; +//** Tooltip arrow color +@tooltip-arrow-color: @tooltip-bg; -// Fluid grid -// ------------------------- -@fluidGridColumnWidth: percentage(@gridColumnWidth/@gridRowWidth); -@fluidGridGutterWidth: percentage(@gridGutterWidth/@gridRowWidth); +//== Popovers +// +//## -// 1200px min -@fluidGridColumnWidth1200: percentage(@gridColumnWidth1200/@gridRowWidth1200); -@fluidGridGutterWidth1200: percentage(@gridGutterWidth1200/@gridRowWidth1200); +//** Popover body background color +@popover-bg: #fff; +//** Popover maximum width +@popover-max-width: 276px; +//** Popover border color +@popover-border-color: rgba(0,0,0,.2); +//** Popover fallback border color +@popover-fallback-border-color: #ccc; -// 768px-979px -@fluidGridColumnWidth768: percentage(@gridColumnWidth768/@gridRowWidth768); -@fluidGridGutterWidth768: percentage(@gridGutterWidth768/@gridRowWidth768); +//** Popover title background color +@popover-title-bg: darken(@popover-bg, 3%); + +//** Popover arrow width +@popover-arrow-width: 10px; +//** Popover arrow color +@popover-arrow-color: #fff; + +//** Popover outer arrow width +@popover-arrow-outer-width: (@popover-arrow-width + 1); +//** Popover outer arrow color +@popover-arrow-outer-color: fadein(@popover-border-color, 5%); +//** Popover outer arrow fallback color +@popover-arrow-outer-fallback-color: darken(@popover-fallback-border-color, 20%); + + +//== Labels +// +//## + +//** Default label background color +@label-default-bg: @gray-light; +//** Primary label background color +@label-primary-bg: @brand-primary; +//** Success label background color +@label-success-bg: @brand-success; +//** Info label background color +@label-info-bg: @brand-info; +//** Warning label background color +@label-warning-bg: @brand-warning; +//** Danger label background color +@label-danger-bg: @brand-danger; + +//** Default label text color +@label-color: #fff; +//** Default text color of a linked label +@label-link-hover-color: #fff; + + +//== Modals +// +//## + +//** Padding applied to the modal body +@modal-inner-padding: 20px; + +//** Padding applied to the modal title +@modal-title-padding: 15px; +//** Modal title line-height +@modal-title-line-height: @line-height-base; + +//** Background color of modal content area +@modal-content-bg: #fff; +//** Modal content border color +@modal-content-border-color: rgba(0,0,0,.2); +//** Modal content border color **for IE8** +@modal-content-fallback-border-color: #999; + +//** Modal backdrop background color +@modal-backdrop-bg: #000; +//** Modal backdrop opacity +@modal-backdrop-opacity: .5; +//** Modal header border color +@modal-header-border-color: #e5e5e5; +//** Modal footer border color +@modal-footer-border-color: @modal-header-border-color; + +@modal-lg: 900px; +@modal-md: 600px; +@modal-sm: 300px; + + +//== Alerts +// +//## Define alert colors, border radius, and padding. + +@alert-padding: 15px; +@alert-border-radius: @border-radius-base; +@alert-link-font-weight: bold; + +@alert-success-bg: @state-success-bg; +@alert-success-text: @state-success-text; +@alert-success-border: @state-success-border; + +@alert-info-bg: @state-info-bg; +@alert-info-text: @state-info-text; +@alert-info-border: @state-info-border; + +@alert-warning-bg: @state-warning-bg; +@alert-warning-text: @state-warning-text; +@alert-warning-border: @state-warning-border; + +@alert-danger-bg: @state-danger-bg; +@alert-danger-text: @state-danger-text; +@alert-danger-border: @state-danger-border; + + +//== Progress bars +// +//## + +//** Background color of the whole progress component +@progress-bg: #f5f5f5; +//** Progress bar text color +@progress-bar-color: #fff; + +//** Default progress bar color +@progress-bar-bg: @brand-primary; +//** Success progress bar color +@progress-bar-success-bg: @brand-success; +//** Warning progress bar color +@progress-bar-warning-bg: @brand-warning; +//** Danger progress bar color +@progress-bar-danger-bg: @brand-danger; +//** Info progress bar color +@progress-bar-info-bg: @brand-info; + + +//== List group +// +//## + +//** Background color on `.list-group-item` +@list-group-bg: #fff; +//** `.list-group-item` border color +@list-group-border: #ddd; +//** List group border radius +@list-group-border-radius: @border-radius-base; + +//** Background color of single list elements on hover +@list-group-hover-bg: #f5f5f5; +//** Text color of active list elements +@list-group-active-color: @component-active-color; +//** Background color of active list elements +@list-group-active-bg: @component-active-bg; +//** Border color of active list elements +@list-group-active-border: @list-group-active-bg; +@list-group-active-text-color: lighten(@list-group-active-bg, 40%); + +@list-group-link-color: #555; +@list-group-link-heading-color: #333; + + +//== Panels +// +//## + +@panel-bg: #fff; +@panel-body-padding: 15px; +@panel-border-radius: @border-radius-base; + +//** Border color for elements within panels +@panel-inner-border: #ddd; +@panel-footer-bg: #f5f5f5; + +@panel-default-text: @gray-dark; +@panel-default-border: #ddd; +@panel-default-heading-bg: #f5f5f5; + +@panel-primary-text: #fff; +@panel-primary-border: @brand-primary; +@panel-primary-heading-bg: @brand-primary; + +@panel-success-text: @state-success-text; +@panel-success-border: @state-success-border; +@panel-success-heading-bg: @state-success-bg; + +@panel-info-text: @state-info-text; +@panel-info-border: @state-info-border; +@panel-info-heading-bg: @state-info-bg; + +@panel-warning-text: @state-warning-text; +@panel-warning-border: @state-warning-border; +@panel-warning-heading-bg: @state-warning-bg; + +@panel-danger-text: @state-danger-text; +@panel-danger-border: @state-danger-border; +@panel-danger-heading-bg: @state-danger-bg; + + +//== Thumbnails +// +//## + +//** Padding around the thumbnail image +@thumbnail-padding: 4px; +//** Thumbnail background color +@thumbnail-bg: @body-bg; +//** Thumbnail border color +@thumbnail-border: #ddd; +//** Thumbnail border radius +@thumbnail-border-radius: @border-radius-base; + +//** Custom text color for thumbnail captions +@thumbnail-caption-color: @text-color; +//** Padding around the thumbnail caption +@thumbnail-caption-padding: 9px; + + +//== Wells +// +//## + +@well-bg: #f5f5f5; +@well-border: darken(@well-bg, 7%); + + +//== Badges +// +//## + +@badge-color: #fff; +//** Linked badge text color on hover +@badge-link-hover-color: #fff; +@badge-bg: @gray-light; + +//** Badge text color in active nav link +@badge-active-color: @link-color; +//** Badge background color in active nav link +@badge-active-bg: #fff; + +@badge-font-weight: bold; +@badge-line-height: 1; +@badge-border-radius: 10px; + + +//== Breadcrumbs +// +//## + +@breadcrumb-padding-vertical: 8px; +@breadcrumb-padding-horizontal: 15px; +//** Breadcrumb background color +@breadcrumb-bg: #f5f5f5; +//** Breadcrumb text color +@breadcrumb-color: #ccc; +//** Text color of current page in the breadcrumb +@breadcrumb-active-color: @gray-light; +//** Textual separator for between breadcrumb elements +@breadcrumb-separator: "/"; + + +//== Carousel +// +//## + +@carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6); + +@carousel-control-color: #fff; +@carousel-control-width: 15%; +@carousel-control-opacity: .5; +@carousel-control-font-size: 20px; + +@carousel-indicator-active-bg: #fff; +@carousel-indicator-border-color: #fff; + +@carousel-caption-color: #fff; + + +//== Close +// +//## + +@close-font-weight: bold; +@close-color: #000; +@close-text-shadow: 0 1px 0 #fff; + + +//== Code +// +//## + +@code-color: #c7254e; +@code-bg: #f9f2f4; + +@kbd-color: #fff; +@kbd-bg: #333; + +@pre-bg: #f5f5f5; +@pre-color: @gray-dark; +@pre-border-color: #ccc; +@pre-scrollable-max-height: 340px; + + +//== Type +// +//## + +//** Text muted color +@text-muted: @gray-light; +//** Abbreviations and acronyms border color +@abbr-border-color: @gray-light; +//** Headings small color +@headings-small-color: @gray-light; +//** Blockquote small color +@blockquote-small-color: @gray-light; +//** Blockquote font size +@blockquote-font-size: (@font-size-base * 1.25); +//** Blockquote border color +@blockquote-border-color: @gray-lighter; +//** Page header border color +@page-header-border-color: @gray-lighter; + + +//== Miscellaneous +// +//## + +//** Horizontal line color. +@hr-border: @gray-lighter; + +//** Horizontal offset for forms and lists. +@component-offset-horizontal: 180px; diff --git a/src/UI/Content/Bootstrap/wells.less b/src/UI/Content/Bootstrap/wells.less index 84a744b1c..15d072b0c 100644 --- a/src/UI/Content/Bootstrap/wells.less +++ b/src/UI/Content/Bootstrap/wells.less @@ -8,9 +8,9 @@ min-height: 20px; padding: 19px; margin-bottom: 20px; - background-color: @wellBackground; - border: 1px solid darken(@wellBackground, 7%); - .border-radius(@baseBorderRadius); + background-color: @well-bg; + border: 1px solid @well-border; + border-radius: @border-radius-base; .box-shadow(inset 0 1px 1px rgba(0,0,0,.05)); blockquote { border-color: #ddd; @@ -19,11 +19,11 @@ } // Sizes -.well-large { +.well-lg { padding: 24px; - .border-radius(@borderRadiusLarge); + border-radius: @border-radius-large; } -.well-small { +.well-sm { padding: 9px; - .border-radius(@borderRadiusSmall); + border-radius: @border-radius-small; } diff --git a/src/UI/Content/FontAwesome/bootstrap.less b/src/UI/Content/FontAwesome/bootstrap.less index a2c96046b..f45378143 100644 --- a/src/UI/Content/FontAwesome/bootstrap.less +++ b/src/UI/Content/FontAwesome/bootstrap.less @@ -57,7 +57,7 @@ &.icon-spin.icon-large { line-height: .8em; } } } -.btn.btn-small { +.btn.btn-sm { [class^="icon-"], [class*=" icon-"] { &.pull-left, &.pull-right { @@ -65,7 +65,7 @@ } } } -.btn.btn-large { +.btn.btn-lg { [class^="icon-"], [class*=" icon-"] { margin-top: 0; // overrides bootstrap default diff --git a/src/UI/Content/Images/logos/128.png b/src/UI/Content/Images/logos/128.png new file mode 100644 index 000000000..ae8cf56c7 Binary files /dev/null and b/src/UI/Content/Images/logos/128.png differ diff --git a/src/UI/Content/Images/logos/32.png b/src/UI/Content/Images/logos/32.png new file mode 100644 index 000000000..a079d7afc Binary files /dev/null and b/src/UI/Content/Images/logos/32.png differ diff --git a/src/UI/Content/Images/logos/48.png b/src/UI/Content/Images/logos/48.png new file mode 100644 index 000000000..b4a009323 Binary files /dev/null and b/src/UI/Content/Images/logos/48.png differ diff --git a/src/UI/Content/Images/logo.png b/src/UI/Content/Images/logos/64.png similarity index 100% rename from src/UI/Content/Images/logo.png rename to src/UI/Content/Images/logos/64.png diff --git a/src/UI/Content/Messenger/messenger.css b/src/UI/Content/Messenger/messenger.css index 9d6786bb0..9fc58c936 100644 --- a/src/UI/Content/Messenger/messenger.css +++ b/src/UI/Content/Messenger/messenger.css @@ -4,93 +4,98 @@ ul.messenger { padding: 0; } /* line 8, ../../src/sass/messenger.sass */ -ul.messenger li { +ul.messenger > li { list-style: none; margin: 0; padding: 0; } /* line 14, ../../src/sass/messenger.sass */ +ul.messenger.messenger-empty { + display: none; +} +/* line 17, ../../src/sass/messenger.sass */ ul.messenger .messenger-message { overflow: hidden; *zoom: 1; } -/* line 17, ../../src/sass/messenger.sass */ +/* line 20, ../../src/sass/messenger.sass */ ul.messenger .messenger-message.messenger-hidden { display: none; } -/* line 20, ../../src/sass/messenger.sass */ +/* line 23, ../../src/sass/messenger.sass */ ul.messenger .messenger-message .messenger-phrase, ul.messenger .messenger-message .messenger-actions a { padding-right: 5px; } -/* line 23, ../../src/sass/messenger.sass */ +/* line 26, ../../src/sass/messenger.sass */ ul.messenger .messenger-message .messenger-actions { float: right; } -/* line 26, ../../src/sass/messenger.sass */ +/* line 29, ../../src/sass/messenger.sass */ ul.messenger .messenger-message .messenger-actions a { cursor: pointer; text-decoration: underline; } -/* line 30, ../../src/sass/messenger.sass */ +/* line 33, ../../src/sass/messenger.sass */ +ul.messenger .messenger-message ul, ul.messenger .messenger-message ol { + margin: 10px 18px 0; +} +/* line 36, ../../src/sass/messenger.sass */ ul.messenger.messenger-fixed { position: fixed; z-index: 10000; } -/* line 34, ../../src/sass/messenger.sass */ +/* line 40, ../../src/sass/messenger.sass */ ul.messenger.messenger-fixed .messenger-message { min-width: 0; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } -/* line 39, ../../src/sass/messenger.sass */ +/* line 45, ../../src/sass/messenger.sass */ ul.messenger.messenger-fixed .message .messenger-actions { float: left; } -/* line 42, ../../src/sass/messenger.sass */ +/* line 48, ../../src/sass/messenger.sass */ ul.messenger.messenger-fixed.messenger-on-top { top: 20px; } -/* line 45, ../../src/sass/messenger.sass */ +/* line 51, ../../src/sass/messenger.sass */ ul.messenger.messenger-fixed.messenger-on-bottom { bottom: 20px; } -/* line 48, ../../src/sass/messenger.sass */ +/* line 54, ../../src/sass/messenger.sass */ ul.messenger.messenger-fixed.messenger-on-top, ul.messenger.messenger-fixed.messenger-on-bottom { left: 50%; width: 800px; margin-left: -400px; } @media (max-width: 960px) { - /* line 48, ../../src/sass/messenger.sass */ + /* line 54, ../../src/sass/messenger.sass */ ul.messenger.messenger-fixed.messenger-on-top, ul.messenger.messenger-fixed.messenger-on-bottom { left: 10%; width: 80%; margin-left: 0px; } } -/* line 58, ../../src/sass/messenger.sass */ +/* line 64, ../../src/sass/messenger.sass */ ul.messenger.messenger-fixed.messenger-on-top.messenger-on-right, ul.messenger.messenger-fixed.messenger-on-bottom.messenger-on-right { right: 20px; left: auto; } -/* line 62, ../../src/sass/messenger.sass */ +/* line 68, ../../src/sass/messenger.sass */ ul.messenger.messenger-fixed.messenger-on-top.messenger-on-left, ul.messenger.messenger-fixed.messenger-on-bottom.messenger-on-left { left: 20px; margin-left: 0px; } -/* line 66, ../../src/sass/messenger.sass */ +/* line 72, ../../src/sass/messenger.sass */ ul.messenger.messenger-fixed.messenger-on-right, ul.messenger.messenger-fixed.messenger-on-left { - width: auto; - min-width: 350px; - max-width: 800px; - word-wrap: break-word; + width: 350px; } -/* line 69, ../../src/sass/messenger.sass */ +/* line 75, ../../src/sass/messenger.sass */ ul.messenger.messenger-fixed.messenger-on-right .messenger-actions, ul.messenger.messenger-fixed.messenger-on-left .messenger-actions { float: left; } -/* line 72, ../../src/sass/messenger.sass */ +/* line 78, ../../src/sass/messenger.sass */ ul.messenger .messenger-spinner { display: none; } diff --git a/src/UI/Content/Messenger/messenger.future.css b/src/UI/Content/Messenger/messenger.flat.css similarity index 56% rename from src/UI/Content/Messenger/messenger.future.css rename to src/UI/Content/Messenger/messenger.flat.css index 89aa3bd66..df8d35aeb 100644 --- a/src/UI/Content/Messenger/messenger.future.css +++ b/src/UI/Content/Messenger/messenger.flat.css @@ -234,33 +234,28 @@ ul.messenger.messenger-spinner-active .messenger-spinner .messenger-spinner { transform-origin: 100% 50%; } -/* line 15, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future { - -webkit-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.24), 0px 1px 5px rgba(0, 0, 0, 0.6); - -moz-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.24), 0px 1px 5px rgba(0, 0, 0, 0.6); - box-shadow: inset 0px 1px rgba(255, 255, 255, 0.24), 0px 1px 5px rgba(0, 0, 0, 0.6); +/* line 15, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat { -webkit-border-radius: 4px; -moz-border-radius: 4px; -ms-border-radius: 4px; -o-border-radius: 4px; border-radius: 4px; - background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #5c5b5b), color-stop(100%, #353535)); - background-image: -webkit-linear-gradient(#5c5b5b, #353535); - background-image: -moz-linear-gradient(#5c5b5b, #353535); - background-image: -o-linear-gradient(#5c5b5b, #353535); - background-image: linear-gradient(#5c5b5b, #353535); - background-color: #5c5b5b; - border: 1px solid rgba(0, 0, 0, 0.5); + -moz-user-select: none; + -webkit-user-select: none; + -o-user-select: none; + user-select: none; + background: #404040; } -/* line 24, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future.messenger-empty { +/* line 20, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat.messenger-empty { display: none; } -/* line 27, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message { - -webkit-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 0px -1px rgba(0, 0, 0, 0.23), inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); - -moz-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 0px -1px rgba(0, 0, 0, 0.23), inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); - box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 0px -1px rgba(0, 0, 0, 0.23), inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); +/* line 23, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat .messenger-message { + -webkit-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; + -moz-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; + box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; -webkit-border-radius: 0px; -moz-border-radius: 0px; -ms-border-radius: 0px; @@ -272,17 +267,15 @@ ul.messenger-theme-future .messenger-message { font-size: 13px; background: transparent; color: #f0f0f0; - text-shadow: 0px 1px #111111; font-weight: 500; padding: 10px 30px 13px 65px; } -/* line 40, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .close { +/* line 35, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat .messenger-message .messenger-close { position: absolute; top: 0px; right: 0px; color: #888888; - text-shadow: 0px 1px black; opacity: 1; font-weight: bold; display: block; @@ -294,62 +287,51 @@ ul.messenger-theme-future .messenger-message .close { border: 0; -webkit-appearance: none; } -/* line 57, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .close:hover { +/* line 51, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat .messenger-message .messenger-close:hover { color: #bbbbbb; } -/* line 60, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .close:active { +/* line 54, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat .messenger-message .messenger-close:active { color: #777777; } -/* line 63, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .messenger-actions { +/* line 57, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat .messenger-message .messenger-actions { float: none; margin-top: 10px; } -/* line 67, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .messenger-actions a { - -webkit-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.2), inset 0px 1px rgba(255, 255, 255, 0.1); - -moz-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.2), inset 0px 1px rgba(255, 255, 255, 0.1); - box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.2), inset 0px 1px rgba(255, 255, 255, 0.1); +/* line 61, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat .messenger-message .messenger-actions a { -webkit-border-radius: 4px; -moz-border-radius: 4px; -ms-border-radius: 4px; -o-border-radius: 4px; border-radius: 4px; text-decoration: none; + color: #aaaaaa; + background: #2e2e2e; display: inline-block; padding: 10px; - color: #aaaaaa; - text-shadow: 0px 1px #222222; margin-right: 10px; - padding: 3px 10px 5px; + padding: 4px 11px 6px; text-transform: capitalize; } -/* line 79, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .messenger-actions a:hover { - -webkit-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.2), inset 0px 1px rgba(255, 255, 255, 0.2); - -moz-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.2), inset 0px 1px rgba(255, 255, 255, 0.2); - box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.2), inset 0px 1px rgba(255, 255, 255, 0.2); +/* line 72, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat .messenger-message .messenger-actions a:hover { color: #f0f0f0; + background: #2e2e2e; } -/* line 83, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .messenger-actions a:active { - -webkit-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.28), inset 0px 1px rgba(0, 0, 0, 0.1); - -moz-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.28), inset 0px 1px rgba(0, 0, 0, 0.1); - box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.28), inset 0px 1px rgba(0, 0, 0, 0.1); - background: rgba(0, 0, 0, 0.04); +/* line 76, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat .messenger-message .messenger-actions a:active { + background: #292929; color: #aaaaaa; } -/* line 88, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .messenger-actions .messenger-phrase { +/* line 80, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat .messenger-message .messenger-actions .messenger-phrase { display: none; } -/* line 91, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .messenger-message-inner:before { - -webkit-box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.6), 0px 0px 0px 1px rgba(0, 0, 0, 0.2); - -moz-box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.6), 0px 0px 0px 1px rgba(0, 0, 0, 0.2); - box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.6), 0px 0px 0px 1px rgba(0, 0, 0, 0.2); +/* line 83, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat .messenger-message .messenger-message-inner:before { -webkit-border-radius: 50%; -moz-border-radius: 50%; -ms-border-radius: 50%; @@ -365,23 +347,26 @@ ul.messenger-theme-future .messenger-message .messenger-message-inner:before { width: 13px; z-index: 20; } -/* line 105, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message.alert-success .messenger-message-inner:before { - background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #5fca4a), color-stop(100%, #098d38)); - background-image: -webkit-linear-gradient(top, #5fca4a, #098d38); - background-image: -moz-linear-gradient(top, #5fca4a, #098d38); - background-image: -o-linear-gradient(top, #5fca4a, #098d38); - background-image: linear-gradient(top, #5fca4a, #098d38); - background-color: #5fca4a; +/* line 95, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat .messenger-message.alert-success .messenger-message-inner:before { + background: #5fca4a; +} +/* line 98, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat .messenger-message.alert-info .messenger-message-inner:before { + background: #61c4b8; +} +/* line 103, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat .messenger-message.alert-error .messenger-message-inner:before { + background: #dd6a45; } /* line 32, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-future .messenger-message.alert-error.messenger-retry-soon .messenger-spinner { +ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-soon .messenger-spinner { width: 32px; height: 32px; background: transparent; } /* line 37, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-future .messenger-message.alert-error.messenger-retry-soon .messenger-spinner .messenger-spinner-side .messenger-spinner-fill { +ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-soon .messenger-spinner .messenger-spinner-side .messenger-spinner-fill { background: #dd6a45; -webkit-animation-duration: 20s; -moz-animation-duration: 20s; @@ -391,9 +376,9 @@ ul.messenger-theme-future .messenger-message.alert-error.messenger-retry-soon .m opacity: 1; } /* line 45, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-future .messenger-message.alert-error.messenger-retry-soon .messenger-spinner:after { +ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-soon .messenger-spinner:after { content: ""; - background: #333333; + background: #292929; position: absolute; width: 26px; height: 26px; @@ -403,13 +388,13 @@ ul.messenger-theme-future .messenger-message.alert-error.messenger-retry-soon .m display: block; } /* line 32, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-future .messenger-message.alert-error.messenger-retry-later .messenger-spinner { +ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-later .messenger-spinner { width: 32px; height: 32px; background: transparent; } /* line 37, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-future .messenger-message.alert-error.messenger-retry-later .messenger-spinner .messenger-spinner-side .messenger-spinner-fill { +ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-later .messenger-spinner .messenger-spinner-side .messenger-spinner-fill { background: #dd6a45; -webkit-animation-duration: 600s; -moz-animation-duration: 600s; @@ -419,9 +404,9 @@ ul.messenger-theme-future .messenger-message.alert-error.messenger-retry-later . opacity: 1; } /* line 45, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-future .messenger-message.alert-error.messenger-retry-later .messenger-spinner:after { +ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-later .messenger-spinner:after { content: ""; - background: #333333; + background: #292929; position: absolute; width: 26px; height: 26px; @@ -430,56 +415,41 @@ ul.messenger-theme-future .messenger-message.alert-error.messenger-retry-later . left: 3px; display: block; } -/* line 116, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message.alert-error .messenger-message-inner:before { - background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #dd6a45), color-stop(100%, #91361a)); - background-image: -webkit-linear-gradient(top, #dd6a45, #91361a); - background-image: -moz-linear-gradient(top, #dd6a45, #91361a); - background-image: -o-linear-gradient(top, #dd6a45, #91361a); - background-image: linear-gradient(top, #dd6a45, #91361a); - background-color: #dd6a45; -} -/* line 121, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message.alert-info .messenger-message-inner:before { - background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #61c4b8), color-stop(100%, #1992a3)); - background-image: -webkit-linear-gradient(top, #61c4b8, #1992a3); - background-image: -moz-linear-gradient(top, #61c4b8, #1992a3); - background-image: -o-linear-gradient(top, #61c4b8, #1992a3); - background-image: linear-gradient(top, #61c4b8, #1992a3); - background-color: #61c4b8; -} -/* line 127, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message-slot.last .messenger-message { +/* line 114, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat .messenger-message-slot.messenger-last .messenger-message { -webkit-border-radius: 4px 4px 0px 0px; -moz-border-radius: 4px 4px 0px 0px; -ms-border-radius: 4px 4px 0px 0px; -o-border-radius: 4px 4px 0px 0px; border-radius: 4px 4px 0px 0px; + -webkit-box-shadow: inset 48px 0px 0px #292929; + -moz-box-shadow: inset 48px 0px 0px #292929; + box-shadow: inset 48px 0px 0px #292929; } -/* line 130, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message-slot.first .messenger-message { +/* line 118, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat .messenger-message-slot.messenger-first .messenger-message { -webkit-border-radius: 0px 0px 4px 4px; -moz-border-radius: 0px 0px 4px 4px; -ms-border-radius: 0px 0px 4px 4px; -o-border-radius: 0px 0px 4px 4px; border-radius: 0px 0px 4px 4px; - -webkit-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); - -moz-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); - box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); + -webkit-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; + -moz-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; + box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; } -/* line 134, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message-slot.first.last .messenger-message { - -webkit-box-shadow: inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); - -moz-box-shadow: inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); - box-shadow: inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); +/* line 122, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat .messenger-message-slot.messenger-first.messenger-last .messenger-message { -webkit-border-radius: 4px; -moz-border-radius: 4px; -ms-border-radius: 4px; -o-border-radius: 4px; border-radius: 4px; + -webkit-box-shadow: inset 48px 0px 0px #292929; + -moz-box-shadow: inset 48px 0px 0px #292929; + box-shadow: inset 48px 0px 0px #292929; } -/* line 138, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-spinner { +/* line 126, ../../src/sass/messenger-theme-flat.sass */ +ul.messenger-theme-flat .messenger-spinner { display: block; position: absolute; left: 7px; diff --git a/src/UI/Content/Overrides/bootstrap.less b/src/UI/Content/Overrides/bootstrap.less index 86f2f92a9..ac73020b5 100644 --- a/src/UI/Content/Overrides/bootstrap.less +++ b/src/UI/Content/Overrides/bootstrap.less @@ -1,33 +1,16 @@ @import "../prefixer"; +@import "../Bootstrap/variables"; +@import "../variables"; + +@input-border-focus: @droneTeal; +@font-family-sans-serif: "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif; +@modal-md: 800px; +@modal-lg: 800px; .label, .badge, i { cursor : default; } -.input-append { - .add-on { - margin-left : 0; - } -} - -.label, .badge, .btn { - .text-shadow(none); -} - -.btn { - - text-transform : capitalize; - min-width : 80px; - - &.btn-mini { - min-width : 0px; - } - - &.btn-icon-only { - min-width : 15px; - } -} - .slide-button { min-width : 0px; } @@ -45,3 +28,47 @@ .tooltip-inner { word-wrap: break-word; } + +.dropdown-submenu { + position:relative; + & > .dropdown-menu { + top:0; + left:100%; + margin-top:-6px; + margin-left:-1px; + -webkit-border-radius:0 6px 6px 6px; + -moz-border-radius:0 6px 6px 6px; + border-radius:0 6px 6px 6px; + } + & > a:after { + display:block; + content:" "; + float:right; + width:0; + height:0; + border-color:transparent; + border-style:solid; + border-width:5px 0 5px 5px; + border-left-color:#cccccc; + margin-top:5px; + margin-right:-10px; + } +} +.dropdown-submenu:hover { + & > .dropdown-menu { + display:block; + } + & > a:after { + border-left-color:#ffffff; + } +} +.dropdown-submenu.pull-left { + float:none; + & > .dropdown-menu { + left:-100%; + margin-left:10px; + -webkit-border-radius:6px 0 6px 6px; + -moz-border-radius:6px 0 6px 6px; + border-radius:6px 0 6px 6px; + } +} diff --git a/src/UI/Content/Overrides/bootstrap.toggle-switch.less b/src/UI/Content/Overrides/bootstrap.toggle-switch.less index 1c8da8516..762656bb8 100644 --- a/src/UI/Content/Overrides/bootstrap.toggle-switch.less +++ b/src/UI/Content/Overrides/bootstrap.toggle-switch.less @@ -2,23 +2,27 @@ @import "Bootstrap/mixins"; .toggle { + height: 34px; + box-sizing: border-box; + .slide-button { - .buttonBackground(@btnDangerBackground, @btnDangerBackgroundHighlight); + .button-variant(@btn-danger-color, @btn-danger-bg, @btn-danger-border); &.btn-danger, &.btn-warning { - .buttonBackground(@btnInverseBackground, @btnInverseBackgroundHighlight); + //.buttonBackground(@btnInverseBackground, @btnInverseBackgroundHighlight); + .button-variant(@btn-warning-color, @btn-warning-bg, @btn-warning-border); } } input:first-of-type:checked ~ .slide-button { - .buttonBackground(@btnPrimaryBackground, @btnPrimaryBackgroundHighlight); + .button-variant(@btn-primary-color, @btn-primary-bg, @btn-primary-border); &.btn-danger { - .buttonBackground(@btnDangerBackground, @btnDangerBackgroundHighlight); + .button-variant(@btn-danger-color, @btn-danger-bg, @btn-danger-border); } &.btn-warning { - .buttonBackground(@btnWarningBackground, @btnWarningBackgroundHighlight); + .button-variant(@btn-warning-color, @btn-warning-bg, @btn-warning-border); } } } \ No newline at end of file diff --git a/src/UI/Content/Overrides/fullcalendar.less b/src/UI/Content/Overrides/fullcalendar.less index 3f7f29c30..d8f3accd9 100644 --- a/src/UI/Content/Overrides/fullcalendar.less +++ b/src/UI/Content/Overrides/fullcalendar.less @@ -8,4 +8,14 @@ text-overflow: ellipsis; white-space: nowrap; overflow: hidden; -} \ No newline at end of file +} + +@media (max-width: @screen-xs-max) { + .fc-button { + padding: 0px 5px; + } + + .fc-header-space { + padding-left: 5px; + } +} diff --git a/src/UI/Content/Overrides/messenger.less b/src/UI/Content/Overrides/messenger.less index dec17fe5a..160ed9512 100644 --- a/src/UI/Content/Overrides/messenger.less +++ b/src/UI/Content/Overrides/messenger.less @@ -1,5 +1,23 @@ +@import "../variables"; + body.control-panel-visible { ul.messenger.messenger-fixed.messenger-on-bottom { bottom: 95px; } +} + +ul.messenger-theme-flat .messenger-message.alert-info .messenger-message-inner:before { + background: @droneTeal; +} + +@media (max-width: @screen-xs-max) { + ul.messenger.messenger-fixed.messenger-on-bottom { + width: 100%; + bottom: 0px; + .border-bottom-radius(0); + + &.messenger-on-right { + right : 0px; + } + } } \ No newline at end of file diff --git a/src/UI/Content/bootstrap.less b/src/UI/Content/bootstrap.less new file mode 100644 index 000000000..d51ca8329 --- /dev/null +++ b/src/UI/Content/bootstrap.less @@ -0,0 +1,2 @@ +@import "./Bootstrap/bootstrap"; +@import "./Overrides/bootstrap"; \ No newline at end of file diff --git a/src/UI/Content/checkbox-button.less b/src/UI/Content/checkbox-button.less index 2de5b3dc1..51f54c7d8 100644 --- a/src/UI/Content/checkbox-button.less +++ b/src/UI/Content/checkbox-button.less @@ -18,7 +18,7 @@ } .btn { - .buttonBackground(@btnBackground, @btnBackgroundHighlight); + .button-variant(@btn-default-color, @btn-default-bg, @btn-default-border); color: #333333; } @@ -27,7 +27,7 @@ } input:first-of-type:checked ~ .btn-primary { - .buttonBackground(@btnPrimaryBackground, @btnPrimaryBackgroundHighlight); + .button-variant(@btn-primary-color, @btn-primary-bg, @btn-primary-border); } } } diff --git a/src/UI/Content/form.less b/src/UI/Content/form.less index d19884933..86ade923a 100644 --- a/src/UI/Content/form.less +++ b/src/UI/Content/form.less @@ -1,13 +1,7 @@ @import "../Shared/Styles/clickable.less"; -.control-group { - .controls { - i { - font-size : 16px; - color : #595959; - margin-right : 5px; - } - +.form-group { + .input-group { .checkbox { width : 100px; margin-left : 0px; @@ -20,6 +14,7 @@ display : inline-block; margin-top : -20px; margin-bottom : 0; + margin-left : 10px; vertical-align : middle; } @@ -30,6 +25,22 @@ } } } + + i { + font-size : 16px; + color : #595959; + margin-right : 5px; + } + + .help-inline { + display : inline-block; + margin-top : 8px; + padding-left : 0px; + + @media (max-width: @screen-xs-max) { + margin-left: 0px; + } + } } .text-area-help { @@ -39,7 +50,12 @@ } textarea.release-restrictions { - width : 260px; + width : 100%; + max-width : 100%; +} + +.help-inline-text-area { + margin-top: 25px !important; } .help-link { @@ -58,3 +74,16 @@ h3 { text-transform: none; } } + +.form-inline { + div { + display : inline-block; + } +} + +.has-error { + .help-inline { + color: #b94a48; + margin-left: 0px; + } +} diff --git a/src/UI/Content/icons.less b/src/UI/Content/icons.less index 843e6db10..1a6ca1c24 100644 --- a/src/UI/Content/icons.less +++ b/src/UI/Content/icons.less @@ -48,7 +48,7 @@ .icon-nd-warning:before { .icon(@warning-sign); - color : @orange; + color : orange; } .icon-nd-edit:before { @@ -57,7 +57,7 @@ .icon-nd-delete:before { .icon(@remove); - color : @errorText; + color : @brand-danger; } .icon-nd-spinner:before { @@ -79,7 +79,7 @@ .icon-nd-form-warning:before { .icon(@warning-sign); - color: @orange; + color: orange; } .icon-nd-form-danger:before { @@ -165,12 +165,12 @@ .icon-nd-download-failed:before { .icon(@cloud-download); - color: @errorText; + color: @brand-danger; } .icon-nd-shutdown:before { .icon(@off); - color: @errorText; + color: @brand-danger; } .icon-nd-restart:before { @@ -179,10 +179,10 @@ .icon-nd-health-warning:before { .icon(@exclamation-sign); - color : @orange + color : orange } .icon-nd-health-error:before { .icon(@exclamation-sign); - color : @errorText + color : @brand-danger; } \ No newline at end of file diff --git a/src/UI/Content/menu.less b/src/UI/Content/menu.less deleted file mode 100644 index 27c5cd7b2..000000000 --- a/src/UI/Content/menu.less +++ /dev/null @@ -1,102 +0,0 @@ -@import "prefixer"; - -#main-menu-region { - text-align : center; - margin-bottom : 10px; - - i:before { - font-size : 35px; - } - - i { - width : 40px; - } - - .logo { - margin-top : 25px; - vertical-align : middle; - height : 70px; - width : 70px; - } - - li { - list-style-type : none; - display : inline-block; - position : relative; - - a { - - &:focus { - text-decoration : none; - } - - display : block; - border-radius : 6px; - padding : 15px 10px 5px; - min-height : 56px; - min-width : 64px; - margin : 20px 10px 5px; - color : #b9b9b9; - font-weight : 100; - } - span.label.pull-right { - position : absolute; - top : 28px; - right : 18px; - } - } -} - -.backdrop { - #nav-region { - background-color : #000000; - .opacity(0.85); - } - - -} - -#nav-region { - margin-bottom : 80px; - height : 150px; - - .span12 { - margin-left : 0px; - } - - li { - a { - &:hover { - background-color : #555555; - text-decoration : none; - } - - .label { - cursor: pointer; - } - } - } -} - -.search { - text-align: center; - - input, .add-on { - background-color: #333333; - border-color: #333333; - color: #cccccc; - } - - ul { - text-align: left; - } - - .dropdown-menu { - background-color: #333333; - color: #cccccc; - - > li > a { - color: #cccccc; - } - } -} \ No newline at end of file diff --git a/src/UI/Content/navbar.less b/src/UI/Content/navbar.less new file mode 100644 index 000000000..19bf8327b --- /dev/null +++ b/src/UI/Content/navbar.less @@ -0,0 +1,228 @@ +@import "prefixer"; +@import "variables"; + +@grid-float-breakpoint: @screen-xs-min; + +.backdrop { + .navbar-nzbdrone { + background-color : #000000; + .opacity(0.85); + padding-bottom: 10px; + } +} + +.navbar-nzbdrone { + text-align : center; + + i:before { + font-size : 35px; + display: block; + } + + .navbar-nav, .navbar-nav>li { + float : none; + } + + .navbar-toggle { + border-color: #333; + + &:hover, + &:focus { + background-color: #333; + } + + .icon-bar { + background-color: #ffffff; + } + } + + .navbar-brand { + position: absolute; + + @media (max-width: @screen-xs-max) { + padding: 9px 15px; + font-size: 14px; + } + + @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { + padding: 20px 15px; + } + + @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { + padding: 30px 15px; + } + + @media (min-width: @screen-lg-min) { + padding: 22px 15px; + } + } + + .logo-text { + color: white; + font-weight: 300; + + .highlight { + font-weight: 400; + color: @droneTeal; + } + } + + li { + list-style-type : none; + display : inline-block; + position : relative; + + a { + display : block; + color : #b9b9b9; + font-weight : 100; + + &:focus { + background-color : transparent; + text-decoration : none; + } + + &:hover { + background-color : #555555; + text-decoration : none; + } + + .label { + cursor: pointer; + } + + @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { + border-radius : 6px; + padding : 5px 0px 5px; + min-height : 76px; + min-width : 64px; + margin : 20px 5px 5px; + } + + @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { + border-radius : 6px; + padding : 15px 10px 5px; + min-height : 76px; + min-width : 64px; + margin : 20px 10px 5px; + } + + @media (min-width: @screen-lg-min) { + border-radius : 6px; + padding : 15px 10px 5px; + min-height : 76px; + min-width : 84px; + margin : 20px 10px 5px; + } + } + + .health { + .label { + position : absolute; + top : 10px; + right : 10px; + } + } + } + + @media (max-width: @screen-xs-max) { + text-align : left; + + i:before { + font-size : 14px; + display: inline-block; + } + + li { + display: block; + + a:hover { + background-color: transparent; + } + + .health { + margin-left: 5px; + + .label { + position : static; + } + } + } + } +} + +.search { + + i:before { + font-size: 14px; + } + + .input-group { + input, .input-group-addon { + background-color: #333333; + } + } + + input, .input-group-addon { + border-color: #333333; + color: #cccccc; + } + + ul { + text-align: left; + } + + .tt-dropdown-menu { + + background-color: #333333; + color: #cccccc; + opacity: .95; + + .tt-suggestion { + color: #cccccc; + + &.tt-cursor { + //item selected + + background-color: @droneTeal; + color: #222222; + + a { + //link in item selected + color: #222222; + } + } + } + } +} + +//.screen-size { +// color: @droneTeal; +// &:after { +// content: "unknown"; +// } +// +// @media (min-width: @screen-xs-min) { +// &:after { +// content: "xs"; +// } +// } +// +// @media (min-width: @screen-sm-min) { +// &:after { +// content: "sm"; +// } +// } +// +// @media (min-width: @screen-md-min) { +// &:after { +// content: "md"; +// } +// } +// +// @media (min-width: @screen-lg-min) { +// &:after { +// content: "lg"; +// } +// } +//} diff --git a/src/UI/Content/progress-bars.less b/src/UI/Content/progress-bars.less index 20b69c063..9211b1c87 100644 --- a/src/UI/Content/progress-bars.less +++ b/src/UI/Content/progress-bars.less @@ -3,34 +3,37 @@ @import "variables"; .progress.episode-progress { - width : 125px; position : relative; margin-bottom : 2px; - + + &, .progressbar-back-text, .progressbar-front-text { + width : 125px; + } + .progressbar-back-text, .progressbar-front-text { - font-size : 11.844px; + font-size : 12px; font-weight : bold; text-align : center; cursor : default; + line-height : 20px; } .progressbar-back-text { - position : absolute; - width : 100%; - height : 100%; + position : absolute; + height : 100%; } .progressbar-front-text { - display : block; - width : 125px; + display : block; + height : 100%; } - .bar { + .progress-bar { position : absolute; overflow : hidden; } } -.progress-purple .bar, .progress .bar-purple { +.progress-bar-purple { #gradient > .vertical(@purple, @nzbdronePurple); } diff --git a/src/UI/Content/spinner.less b/src/UI/Content/spinner.less index a922b36aa..2a02f136b 100644 --- a/src/UI/Content/spinner.less +++ b/src/UI/Content/spinner.less @@ -1,8 +1,8 @@ @import "prefixer"; @import "Bootstrap/variables"; -@colorDark : @grayDark; -@colorLight : @grayLighter; +@colorDark : @gray-dark; +@colorLight : @gray-lighter; #followingBalls { position : relative; diff --git a/src/UI/Content/theme.less b/src/UI/Content/theme.less index 374dc607b..47a0b323f 100644 --- a/src/UI/Content/theme.less +++ b/src/UI/Content/theme.less @@ -3,7 +3,7 @@ @import "Bootstrap/type"; @import "font"; @import "form"; -@import "menu"; +@import "navbar"; @import "Backgrid/backgrid"; @import "prefixer"; @import "icons"; @@ -14,6 +14,15 @@ @import "../Shared/Styles/clickable"; @import "../Shared/Styles/card"; @import "../Rename/rename"; +@import "typeahead"; +@import "utilities"; + +.main-region { + @media (min-width: @screen-lg-min) { + padding-left : 30px; + padding-right : 30px; + } +} .toolbar { @@ -88,7 +97,7 @@ th { &.sortable { &:hover { - background : @tableBackgroundHover; + background : @table-bg-hover; } .clickable(); @@ -146,11 +155,13 @@ body { .started #page { .card(#aaaaaa); - width : 1210px; - min-width : 1210px; + /* width : 1210px; + min-width : 1210px; */ + max-width : 1210px; margin : auto; - margin-top : -70px; +// margin-top : -70px; padding : 20px 0px; + .header { padding-bottom : 10px; margin-bottom : 20px; @@ -170,29 +181,23 @@ body { } .status-primary { - color : @linkColor; + color : @link-color; } .status-success { - color : @successText; + color : @state-success-text; } .status-warning { - color : @warningText; + color : @state-warning-text; } .status-danger { - color : @errorText; -} - -.form-inline { - div { - display : inline-block; - } + color : @state-danger-text; } .error { - .formFieldState(@errorText, @errorText, @errorBackground); + background: #FF0000; } #errors{ @@ -218,6 +223,17 @@ body { left: 0; bottom: 0; width: 100%; - height: 55px; + height: 80px; opacity: 0; + + @media (max-width: @screen-sm-max) { + height: initial; + position: static; + } +} + +.tab-content { + .tab-pane { + padding-top: 10px; + } } diff --git a/src/UI/Content/typeahead.less b/src/UI/Content/typeahead.less new file mode 100644 index 000000000..5a901c3ee --- /dev/null +++ b/src/UI/Content/typeahead.less @@ -0,0 +1,152 @@ +/* + * typehead.js-bootstrap3.less + * @version 0.2.3 + * https://github.com/hyspace/typeahead.js-bootstrap3.less + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +//custom mixin for .form-control-validation +.typeahead-form-control(@border-color: #ccc;) { + border-color: @border-color; + .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work + &:focus { + border-color: darken(@border-color, 10%); + @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@border-color, 20%); + .box-shadow(@shadow); + } +} + +//main styles for control +.tt-input, +.tt-hint { + .twitter-typeahead &{ + //validation states + .has-warning &{ + .typeahead-form-control(@state-warning-text); + } + .has-error &{ + .typeahead-form-control(@state-danger-text); + } + .has-success &{ + .typeahead-form-control(@state-success-text); + } + } + + //border + .input-group .twitter-typeahead:first-child &{ + .border-left-radius(@border-radius-base); + } + .input-group .twitter-typeahead:last-child &{ + .border-right-radius(@border-radius-base); + } + + //sizing - small:size and border + .input-group.input-group-sm .twitter-typeahead &{ + .input-size(@input-height-small; @padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small); + } + .input-group.input-group-sm .twitter-typeahead:not(:first-child):not(:last-child) &{ + border-radius: 0; + } + .input-group.input-group-sm .twitter-typeahead:first-child &{ + .border-left-radius(@border-radius-small); + .border-right-radius(0); + } + .input-group.input-group-sm .twitter-typeahead:last-child &{ + .border-left-radius(0); + .border-right-radius(@border-radius-small); + } + + //sizing - large:size and border + .input-group.input-group-lg .twitter-typeahead &{ + .input-size(@input-height-large; @padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large); + } + .input-group.input-group-lg .twitter-typeahead:not(:first-child):not(:last-child) &{ + border-radius: 0; + } + .input-group.input-group-lg .twitter-typeahead:first-child &{ + .border-left-radius(@border-radius-large); + .border-right-radius(0); + } + .input-group.input-group-lg .twitter-typeahead:last-child &{ + .border-left-radius(0); + .border-right-radius(@border-radius-large); + } +} + +//for wrapper +.twitter-typeahead { + width: 100%; + .input-group &{ + //overwrite `display:inline-block` style + display: table-cell!important; + float: left; + } +} + +//particular style for each other +.twitter-typeahead .tt-hint { + color: @text-muted;//color - hint +} +.twitter-typeahead .tt-input { + z-index: 2; + //disabled status + //overwrite inline styles of .tt-query + &[disabled], + &[readonly], + fieldset[disabled] & { + cursor: not-allowed; + //overwirte inline style + background-color: @input-bg-disabled!important; + } +} + +//dropdown styles +.tt-dropdown-menu { + //dropdown menu + position: absolute; + top: 100%; + left: 0; + z-index: @zindex-dropdown; + min-width: 160px; + width: 100%; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + font-size: @font-size-base; + background-color: @dropdown-bg; + border: 1px solid @dropdown-fallback-border; + border: 1px solid @dropdown-border; + border-radius: @border-radius-base; + .box-shadow(0 6px 12px rgba(0,0,0,.175)); + background-clip: padding-box; + *border-right-width: 2px; + *border-bottom-width: 2px; + + .tt-suggestion { + //item + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: @line-height-base; + color: @dropdown-link-color; + white-space: nowrap; + + &.tt-cursor { + //item selected + text-decoration: none; + outline: 0; + background-color: @dropdown-link-hover-bg; + color: @dropdown-link-hover-color; + a { + //link in item selected + color: @dropdown-link-hover-color; + } + } + p { + margin: 0; + } + } +} diff --git a/src/UI/Content/utilities.less b/src/UI/Content/utilities.less new file mode 100644 index 000000000..cc2f2cc75 --- /dev/null +++ b/src/UI/Content/utilities.less @@ -0,0 +1,19 @@ +@import "Bootstrap/variables"; +@import "Bootstrap/mixins"; + +@media (max-width: @screen-sm-max) { + .pull-none-xs { + float : none !important; + } + + .btn-group { + &.btn-group-collapse { + > .btn { + margin : 2px; + display : block; + float : none; + border-radius : @border-radius-base !important; + } + } + } +} \ No newline at end of file diff --git a/src/UI/Content/variables.less b/src/UI/Content/variables.less index 5a97d2217..24b2846d3 100644 --- a/src/UI/Content/variables.less +++ b/src/UI/Content/variables.less @@ -1,3 +1,11 @@ -@nzbdroneRed: #c4273c; +@nzbdroneRed: #c4273c; +@purple: #7a43b6; @nzbdronePurple: #7932ea; -@droneTeal: #35c5f4; \ No newline at end of file +@droneTeal: #35c5f4; + +@screen-tn-max: @screen-xs-min - 1; +@tn: ~'(max-width: @{screen-tn-max})'; +@xs: ~'(min-width: @{screen-xs-max}) and (max-width: @{screen-xs-max})'; +@sm: ~'(min-width: @{screen-sm-min}) and (max-width: @{screen-sm-max})'; +@md: ~'(min-width: @{screen-md-min}) and (max-width: @{screen-md-max})'; +@lg: ~'(min-width: @{screen-lg-min})'; \ No newline at end of file diff --git a/src/UI/Episode/Activity/EpisodeActivityLayoutTemplate.html b/src/UI/Episode/Activity/EpisodeActivityLayoutTemplate.html index 459032937..7f2818c53 100644 --- a/src/UI/Episode/Activity/EpisodeActivityLayoutTemplate.html +++ b/src/UI/Episode/Activity/EpisodeActivityLayoutTemplate.html @@ -1 +1 @@ -<div class="activity-table"></div> \ No newline at end of file +<div class="activity-table table-responsive"></div> \ No newline at end of file diff --git a/src/UI/Episode/EpisodeDetailsLayoutTemplate.html b/src/UI/Episode/EpisodeDetailsLayoutTemplate.html index 33a0c5514..9a8036be5 100644 --- a/src/UI/Episode/EpisodeDetailsLayoutTemplate.html +++ b/src/UI/Episode/EpisodeDetailsLayoutTemplate.html @@ -1,33 +1,37 @@ -<div class="episode-detail-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="episode-detail-modal"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3> - <i class="icon-bookmark x-episode-monitored episode-monitored" title="Toggle monitored status" /> - {{series.title}} - {{EpisodeNumber}} - {{title}} - </h3> + <h3> + <i class="icon-bookmark x-episode-monitored episode-monitored" title="Toggle monitored status" /> + {{series.title}} - {{EpisodeNumber}} - {{title}} + </h3> - </div> - <div class="modal-body"> - <ul class="nav nav-tabs" id="myTab"> - <li><a href="#episode-summary" class="x-episode-summary">Summary</a></li> - <li><a href="#episode-activity" class="x-episode-activity">Activity</a></li> - <li><a href="#episode-search" class="x-episode-search">Search</a></li> - </ul> - <div class="tab-content"> - <div class="tab-pane" id="episode-summary"/> - <div class="tab-pane" id="episode-activity"/> - <div class="tab-pane" id="episode-search"/> + </div> + <div class="modal-body"> + <ul class="nav nav-tabs" id="myTab"> + <li><a href="#episode-summary" class="x-episode-summary">Summary</a></li> + <li><a href="#episode-activity" class="x-episode-activity">Activity</a></li> + <li><a href="#episode-search" class="x-episode-search">Search</a></li> + </ul> + <div class="tab-content"> + <div class="tab-pane" id="episode-summary"/> + <div class="tab-pane" id="episode-activity"/> + <div class="tab-pane" id="episode-search"/> + </div> + </div> + <div class="modal-footer"> + {{#unless hideSeriesLink}} + {{#with series}} + <a href="{{route}}" class="btn btn-default pull-left" data-dismiss="modal">open series</a> + {{/with}} + {{/unless}} + + <button class="btn btn-default" data-dismiss="modal">close</button> + </div> </div> </div> - <div class="modal-footer"> - {{#unless hideSeriesLink}} - {{#with series}} - <a href="{{route}}" class="btn pull-left" data-dismiss="modal">open series</a> - {{/with}} - {{/unless}} - - <button class="btn" data-dismiss="modal">close</button> - </div> </div> diff --git a/src/UI/Episode/Search/ButtonsViewTemplate.html b/src/UI/Episode/Search/ButtonsViewTemplate.html index 96456d3f4..698e75430 100644 --- a/src/UI/Episode/Search/ButtonsViewTemplate.html +++ b/src/UI/Episode/Search/ButtonsViewTemplate.html @@ -1,4 +1,4 @@ <div class="search-buttons"> - <button class="btn btn-large btn-block x-search-auto"><i class="icon-rocket"/> Automatic Search</button> - <button class="btn btn-large btn-block btn-primary x-search-manual"><i class="icon-user"/> Manual Search</button> + <button class="btn btn-lg btn-block x-search-auto"><i class="icon-rocket"/> Automatic Search</button> + <button class="btn btn-lg btn-block btn-primary x-search-manual"><i class="icon-user"/> Manual Search</button> </div> \ No newline at end of file diff --git a/src/UI/Episode/Search/ManualLayoutTemplate.html b/src/UI/Episode/Search/ManualLayoutTemplate.html index 84945e3bb..9238c1959 100644 --- a/src/UI/Episode/Search/ManualLayoutTemplate.html +++ b/src/UI/Episode/Search/ManualLayoutTemplate.html @@ -1,2 +1,2 @@ -<div id="episode-release-grid"/> +<div id="episode-release-grid" class="table-responsive"></div> <button class="btn x-search-back">Back</button> \ No newline at end of file diff --git a/src/UI/Form/CheckboxTemplate.html b/src/UI/Form/CheckboxTemplate.html index 68a6fdb26..1da8fa23f 100644 --- a/src/UI/Form/CheckboxTemplate.html +++ b/src/UI/Form/CheckboxTemplate.html @@ -1,21 +1,23 @@ -<div class="control-group"> - <label class="control-label">{{label}}</label> +<div class="form-group"> + <label class="col-sm-3 control-label">{{label}}</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="fields.{{order}}.value"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <div class="col-sm-5"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="fields.{{order}}.value"/> + <p> + <span>Yes</span> + <span>No</span> + </p> - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn btn-primary slide-button"/> + </label> - {{#if helpText}} - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="{{helpText}}"/> - </span> - {{/if}} + {{#if helpText}} + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="{{helpText}}"/> + </span> + {{/if}} + </div> </div> </div> diff --git a/src/UI/Form/FormHelpPartial.html b/src/UI/Form/FormHelpPartial.html index 53a2a495b..c50ed3cf8 100644 --- a/src/UI/Form/FormHelpPartial.html +++ b/src/UI/Form/FormHelpPartial.html @@ -1,11 +1,8 @@ -{{#if helpText}} - <span class="help-inline"> +<span class="col-sm-1 help-inline"> + {{#if helpText}} <i class="icon-nd-form-info" title="{{helpText}}"/> - </span> -{{/if}} - -{{#if helpLink}} - <span class="help-inline"> + {{/if}} + {{#if helpLink}} <a href="{{helpLink}}" class="help-link"><i class="icon-info-sign"/></a> - </span> -{{/if}} \ No newline at end of file + {{/if}} +</span> diff --git a/src/UI/Form/PasswordTemplate.html b/src/UI/Form/PasswordTemplate.html index c03abbaed..610eec1cc 100644 --- a/src/UI/Form/PasswordTemplate.html +++ b/src/UI/Form/PasswordTemplate.html @@ -1,12 +1,8 @@ -<div class="control-group"> - <label class="control-label">{{label}}</label> +<div class="form-group"> + <label class="col-sm-3 control-label">{{label}}</label> - <div class="controls"> - <input type="password" name="fields.{{order}}.value" validation-name="{{name}}"/> - {{#if helpText}} - <span class="help-inline"> - <i class="icon-nd-form-info" title="{{helpText}}"/> - </span> - {{/if}} + <div class="col-sm-5"> + <input type="password" name="fields.{{order}}.value" validation-name="{{name}}" class="form-control"/> </div> + {{> FormHelpPartial}} </div> diff --git a/src/UI/Form/PathTemplate.html b/src/UI/Form/PathTemplate.html index 6b5d16bc6..93992c733 100644 --- a/src/UI/Form/PathTemplate.html +++ b/src/UI/Form/PathTemplate.html @@ -1,12 +1,8 @@ -<div class="control-group"> - <label class="control-label">{{label}}</label> +<div class="form-group"> + <label class="col-sm-3 control-label">{{label}}</label> - <div class="controls"> - <input type="text" name="fields.{{order}}.value" validation-name="{{name}}" class="x-path"/> - {{#if helpText}} - <span class="help-inline"> - <i class="icon-nd-form-info" title="{{helpText}}"/> - </span> - {{/if}} + <div class="col-sm-5"> + <input type="text" name="fields.{{order}}.value" validation-name="{{name}}" class="form-control x-path"/> </div> + {{> FormHelpPartial}} </div> diff --git a/src/UI/Form/SelectTemplate.html b/src/UI/Form/SelectTemplate.html index 89e5065ce..29274525d 100644 --- a/src/UI/Form/SelectTemplate.html +++ b/src/UI/Form/SelectTemplate.html @@ -1,16 +1,12 @@ -<div class="control-group"> - <label class="control-label">{{label}}</label> +<div class="form-group"> + <label class="col-sm-3 control-label">{{label}}</label> - <div class="controls"> - <select name="fields.{{order}}.value"> + <div class="col-sm-5"> + <select name="fields.{{order}}.value" class="form-control"> {{#each selectOptions}} <option value="{{value}}">{{name}}</option> {{/each}} </select> - {{#if helpText}} - <span class="help-inline"> - <i class="icon-nd-form-info" title="{{helpText}}"/> - </span> - {{/if}} </div> + {{> FormHelpPartial}} </div> diff --git a/src/UI/Form/TextboxTemplate.html b/src/UI/Form/TextboxTemplate.html index b257c7f03..e607b466c 100644 --- a/src/UI/Form/TextboxTemplate.html +++ b/src/UI/Form/TextboxTemplate.html @@ -1,8 +1,8 @@ -<div class="control-group"> - <label class="control-label">{{label}}</label> +<div class="form-group"> + <label class="col-sm-3 control-label">{{label}}</label> - <div class="controls"> - <input type="text" name="fields.{{order}}.value" validation-name="{{name}}" spellcheck="false"/> - {{> FormHelpPartial}} + <div class="col-sm-5"> + <input type="text" name="fields.{{order}}.value" validation-name="{{name}}" spellcheck="false" class="form-control"/> </div> + {{> FormHelpPartial}} </div> diff --git a/src/UI/Handlebars/Helpers/Episode.js b/src/UI/Handlebars/Helpers/Episode.js index 115317e7f..ac4bb4e8a 100644 --- a/src/UI/Handlebars/Helpers/Episode.js +++ b/src/UI/Handlebars/Helpers/Episode.js @@ -44,4 +44,16 @@ define( return 'primary'; }); + + Handlebars.registerHelper('EpisodeProgressClass', function () { + if (this.episodeFileCount === this.episodeCount) { + if (this.continuing) { + return ''; + } + + return 'progress-bar-success'; + } + + return 'progress-bar-danger'; + }); }); diff --git a/src/UI/Handlebars/Helpers/Quality.js b/src/UI/Handlebars/Helpers/Quality.js index c4a1399d5..d0050f577 100644 --- a/src/UI/Handlebars/Helpers/Quality.js +++ b/src/UI/Handlebars/Helpers/Quality.js @@ -11,7 +11,7 @@ define( var profile = QualityProfileCollection.get(profileId); if (profile) { - return new Handlebars.SafeString('<span class="label quality-profile-label">' + profile.get("name") + '</span>'); + return new Handlebars.SafeString('<span class="label label-default quality-profile-label">' + profile.get("name") + '</span>'); } return undefined; diff --git a/src/UI/Health/HealthView.js b/src/UI/Health/HealthView.js index 77060366e..f8f51d046 100644 --- a/src/UI/Health/HealthView.js +++ b/src/UI/Health/HealthView.js @@ -6,6 +6,8 @@ define( 'Health/HealthCollection' ], function (_, Marionette, HealthCollection) { return Marionette.ItemView.extend({ + tagName: 'span', + initialize: function () { this.listenTo(HealthCollection, 'sync', this._healthSync); HealthCollection.fetch(); @@ -25,10 +27,10 @@ define( }); if (errors) { - label = 'label-important'; + label = 'label-danger'; } - this.$el.html('<span class="label pull-right {0}">{1}</span>'.format(label, count)); + this.$el.html('<span class="label {0}">{1}</span>'.format(label, count)); return this; }, diff --git a/src/UI/History/Blacklist/BlacklistLayoutTemplate.html b/src/UI/History/Blacklist/BlacklistLayoutTemplate.html index f90d55c39..7f6def076 100644 --- a/src/UI/History/Blacklist/BlacklistLayoutTemplate.html +++ b/src/UI/History/Blacklist/BlacklistLayoutTemplate.html @@ -1,11 +1,11 @@ <div id="x-toolbar"/> <div class="row"> - <div class="span12"> + <div class="col-md-12 table-responsive"> <div id="x-blacklist"/> </div> </div> <div class="row"> - <div class="span12"> + <div class="col-md-12"> <div id="x-pager"/> </div> </div> diff --git a/src/UI/History/Details/HistoryDetailsViewTemplate.html b/src/UI/History/Details/HistoryDetailsViewTemplate.html index a5232f41d..3cc969e84 100644 --- a/src/UI/History/Details/HistoryDetailsViewTemplate.html +++ b/src/UI/History/Details/HistoryDetailsViewTemplate.html @@ -1,86 +1,89 @@ -<div class="history-detail-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="history-detail-modal"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3> - {{#if_eq eventType compare="grabbed"}}Grabbed{{/if_eq}} - {{#if_eq eventType compare="downloadFailed"}}Download Failed{{/if_eq}} - {{#if_eq eventType compare="downloadFolderImported"}}Episode Imported{{/if_eq}} - </h3> + <h3> + {{#if_eq eventType compare="grabbed"}}Grabbed{{/if_eq}} + {{#if_eq eventType compare="downloadFailed"}}Download Failed{{/if_eq}} + {{#if_eq eventType compare="downloadFolderImported"}}Episode Imported{{/if_eq}} + </h3> - </div> - <div class="modal-body"> - {{#if_eq eventType compare="grabbed"}} - <dl class="dl-horizontal"> + </div> + <div class="modal-body"> + {{#if_eq eventType compare="grabbed"}} + <dl class="dl-horizontal"> - <dt>Name:</dt> - <dd>{{sourceTitle}}</dd> + <dt>Name:</dt> + <dd>{{sourceTitle}}</dd> - {{#with data}} - {{#if indexer}} - <dt>Indexer:</dt> - <dd>{{indexer}}</dd> - {{/if}} + {{#with data}} + {{#if indexer}} + <dt>Indexer:</dt> + <dd>{{indexer}}</dd> + {{/if}} - {{#if releaseGroup}} - <dt>Release Group:</dt> - <dd>{{releaseGroup}}</dd> - {{/if}} + {{#if releaseGroup}} + <dt>Release Group:</dt> + <dd>{{releaseGroup}}</dd> + {{/if}} - {{#if nzbInfoUrl}} - <dt>Info:</dt> - <dd><a href="{{nzbInfoUrl}}">{{nzbInfoUrl}}</a></dd> - {{/if}} + {{#if nzbInfoUrl}} + <dt>Info:</dt> + <dd><a href="{{nzbInfoUrl}}">{{nzbInfoUrl}}</a></dd> + {{/if}} - {{#if downloadClient}} - <dt>Download Client:</dt> - <dd>{{downloadClient}}</dd> - {{/if}} + {{#if downloadClient}} + <dt>Download Client:</dt> + <dd>{{downloadClient}}</dd> + {{/if}} - {{#if downloadClientId}} - <dt>Download Client ID:</dt> - <dd>{{downloadClientId}}</dd> - {{/if}} - {{/with}} - </dl> - {{/if_eq}} - {{#if_eq eventType compare="downloadFailed"}} - <dl class="dl-horizontal"> + {{#if downloadClientId}} + <dt>Download Client ID:</dt> + <dd>{{downloadClientId}}</dd> + {{/if}} + {{/with}} + </dl> + {{/if_eq}} + {{#if_eq eventType compare="downloadFailed"}} + <dl class="dl-horizontal"> - <dt>Name:</dt> - <dd>{{sourceTitle}}</dd> + <dt>Name:</dt> + <dd>{{sourceTitle}}</dd> - {{#with data}} - <dt>Message:</dt> - <dd>{{message}}</dd> - {{/with}} - </dl> - {{/if_eq}} - {{#if_eq eventType compare="downloadFolderImported"}} - <dl class="dl-horizontal"> - - {{#if sourceTitle}} - <dt>Name:</dt> - <dd>{{sourceTitle}}</dd> - {{/if}} - - {{#with data}} - {{#if droppedPath}} - <dt>Source:</dt> - <dd>{{droppedPath}}</dd> - {{/if}} + {{#with data}} + <dt>Message:</dt> + <dd>{{message}}</dd> + {{/with}} + </dl> + {{/if_eq}} + {{#if_eq eventType compare="downloadFolderImported"}} + <dl class="dl-horizontal"> - {{#if importedPath}} - <dt>Imported To:</dt> - <dd>{{importedPath}}</dd> - {{/if}} - {{/with}} - </dl> - {{/if_eq}} - </div> - <div class="modal-footer"> - {{#if_eq eventType compare="grabbed"}}<button class="btn btn-danger x-mark-as-failed">mark as failed</button>{{/if_eq}} - <button class="btn" data-dismiss="modal">close</button> + {{#if sourceTitle}} + <dt>Name:</dt> + <dd>{{sourceTitle}}</dd> + {{/if}} + + {{#with data}} + {{#if droppedPath}} + <dt>Source:</dt> + <dd>{{droppedPath}}</dd> + {{/if}} + + {{#if importedPath}} + <dt>Imported To:</dt> + <dd>{{importedPath}}</dd> + {{/if}} + {{/with}} + </dl> + {{/if_eq}} + </div> + <div class="modal-footer"> + {{#if_eq eventType compare="grabbed"}}<button class="btn btn-danger x-mark-as-failed">mark as failed</button>{{/if_eq}} + <button class="btn" data-dismiss="modal">close</button> + </div> + </div> </div> </div> - diff --git a/src/UI/History/Queue/QueueLayoutTemplate.html b/src/UI/History/Queue/QueueLayoutTemplate.html index 89041b644..bdf520b80 100644 --- a/src/UI/History/Queue/QueueLayoutTemplate.html +++ b/src/UI/History/Queue/QueueLayoutTemplate.html @@ -1,11 +1,11 @@ <div class="row"> - <div class="span12"> + <div class="col-md-12"> <div id="x-queue"/> </div> </div> <div class="row"> - <div class="span12"> + <div class="col-md-12 table-responsive"> <div id="x-queue-pager"/> </div> </div> \ No newline at end of file diff --git a/src/UI/History/Queue/QueueStatusCell.js b/src/UI/History/Queue/QueueStatusCell.js index 67442cb09..2c01a0246 100644 --- a/src/UI/History/Queue/QueueStatusCell.js +++ b/src/UI/History/Queue/QueueStatusCell.js @@ -26,10 +26,6 @@ define( title = 'Queued'; } - var timeleft = this.cellValue.get('timeleft'); - var size = this.cellValue.get('size'); - var sizeleft = this.cellValue.get('sizeleft'); - this.$el.html('<i class="{0}" title="{1}"></i>'.format(icon, title)); } diff --git a/src/UI/History/Table/HistoryTableLayout.js b/src/UI/History/Table/HistoryTableLayout.js index 3bfd2efb0..cb7c66331 100644 --- a/src/UI/History/Table/HistoryTableLayout.js +++ b/src/UI/History/Table/HistoryTableLayout.js @@ -88,7 +88,6 @@ define( onShow: function () { this.history.show(new LoadingView()); - //this.collection.fetch(); this._showToolbar(); }, @@ -160,8 +159,9 @@ define( this.collection.state.currentPage = 1; var promise = this.collection.setFilterMode(mode); - if (buttonContext) + if (buttonContext) { buttonContext.ui.icon.spinForPromise(promise); + } } }); }); diff --git a/src/UI/History/Table/HistoryTableLayoutTemplate.html b/src/UI/History/Table/HistoryTableLayoutTemplate.html index 753b8b049..d9a65aa9e 100644 --- a/src/UI/History/Table/HistoryTableLayoutTemplate.html +++ b/src/UI/History/Table/HistoryTableLayoutTemplate.html @@ -1,11 +1,11 @@ <div id="x-history-toolbar"/> <div class="row"> - <div class="span12"> + <div class="col-md-12 table-responsive"> <div id="x-history"/> </div> </div> <div class="row"> - <div class="span12"> + <div class="col-md-12"> <div id="x-history-pager"/> </div> </div> diff --git a/src/UI/Instrumentation/ErrorHandler.js b/src/UI/Instrumentation/ErrorHandler.js index d74eafc22..899f34bac 100644 --- a/src/UI/Instrumentation/ErrorHandler.js +++ b/src/UI/Instrumentation/ErrorHandler.js @@ -1,5 +1,4 @@ -define( - [ +define([ 'jquery', 'messenger' ], function ($, Messenger) { @@ -29,10 +28,10 @@ var messageText = filename + ' : ' + line + '</br>' + msg; var message = { - message : messageText, - type : 'error', - hideAfter : 1000, - showCloseButton: true + message : messageText, + type : 'error', + hideAfter : 1000, + showCloseButton : true }; new Messenger().post(message); @@ -62,9 +61,9 @@ } var message = { - type : 'error', - hideAfter : 1000, - showCloseButton: true + type : 'error', + hideAfter : 1000, + showCloseButton : true }; if (xmlHttpRequest.status === 0 && xmlHttpRequest.readyState === 0) { diff --git a/src/UI/Instrumentation/StringFormat.js b/src/UI/Instrumentation/StringFormat.js index b197987d3..b8d594d03 100644 --- a/src/UI/Instrumentation/StringFormat.js +++ b/src/UI/Instrumentation/StringFormat.js @@ -4,7 +4,8 @@ String.prototype.format = function () { return this.replace(/{(\d+)}/g, function (match, number) { if (typeof args[number] !== 'undefined') { return args[number]; - } else { + } + else { return match; } }); diff --git a/src/UI/JsLibraries/bootstrap.js b/src/UI/JsLibraries/bootstrap.js index 643e71cdf..8ae571b6d 100644 --- a/src/UI/JsLibraries/bootstrap.js +++ b/src/UI/JsLibraries/bootstrap.js @@ -1,139 +1,126 @@ -/* =================================================== - * bootstrap-transition.js v2.3.2 - * http://twitter.github.com/bootstrap/javascript.html#transitions - * =================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ +/*! + * Bootstrap v3.1.1 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +if (typeof jQuery === 'undefined') { throw new Error('Bootstrap\'s JavaScript requires jQuery') } + +/* ======================================================================== + * Bootstrap: transition.js v3.1.1 + * http://getbootstrap.com/javascript/#transitions + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ -!function ($) { ++function ($) { + 'use strict'; - "use strict"; // jshint ;_; + // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) + // ============================================================ + function transitionEnd() { + var el = document.createElement('bootstrap') - /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) - * ======================================================= */ + var transEndEventNames = { + 'WebkitTransition' : 'webkitTransitionEnd', + 'MozTransition' : 'transitionend', + 'OTransition' : 'oTransitionEnd otransitionend', + 'transition' : 'transitionend' + } + + for (var name in transEndEventNames) { + if (el.style[name] !== undefined) { + return { end: transEndEventNames[name] } + } + } + + return false // explicit for ie8 ( ._.) + } + + // http://blog.alexmaccaw.com/css-transitions + $.fn.emulateTransitionEnd = function (duration) { + var called = false, $el = this + $(this).one($.support.transition.end, function () { called = true }) + var callback = function () { if (!called) $($el).trigger($.support.transition.end) } + setTimeout(callback, duration) + return this + } $(function () { - - $.support.transition = (function () { - - var transitionEnd = (function () { - - var el = document.createElement('bootstrap') - , transEndEventNames = { - 'WebkitTransition' : 'webkitTransitionEnd' - , 'MozTransition' : 'transitionend' - , 'OTransition' : 'oTransitionEnd otransitionend' - , 'transition' : 'transitionend' - } - , name - - for (name in transEndEventNames){ - if (el.style[name] !== undefined) { - return transEndEventNames[name] - } - } - - }()) - - return transitionEnd && { - end: transitionEnd - } - - })() - + $.support.transition = transitionEnd() }) -}(window.jQuery);/* ========================================================== - * bootstrap-alert.js v2.3.2 - * http://twitter.github.com/bootstrap/javascript.html#alerts - * ========================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ +}(jQuery); + +/* ======================================================================== + * Bootstrap: alert.js v3.1.1 + * http://getbootstrap.com/javascript/#alerts + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ -!function ($) { ++function ($) { + 'use strict'; - "use strict"; // jshint ;_; - - - /* ALERT CLASS DEFINITION - * ====================== */ + // ALERT CLASS DEFINITION + // ====================== var dismiss = '[data-dismiss="alert"]' - , Alert = function (el) { - $(el).on('click', dismiss, this.close) - } + var Alert = function (el) { + $(el).on('click', dismiss, this.close) + } Alert.prototype.close = function (e) { - var $this = $(this) - , selector = $this.attr('data-target') - , $parent + var $this = $(this) + var selector = $this.attr('data-target') if (!selector) { selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } - $parent = $(selector) + var $parent = $(selector) - e && e.preventDefault() + if (e) e.preventDefault() - $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) + if (!$parent.length) { + $parent = $this.hasClass('alert') ? $this : $this.parent() + } - $parent.trigger(e = $.Event('close')) + $parent.trigger(e = $.Event('close.bs.alert')) if (e.isDefaultPrevented()) return $parent.removeClass('in') function removeElement() { - $parent - .trigger('closed') - .remove() + $parent.trigger('closed.bs.alert').remove() } $.support.transition && $parent.hasClass('fade') ? - $parent.on($.support.transition.end, removeElement) : + $parent + .one($.support.transition.end, removeElement) + .emulateTransitionEnd(150) : removeElement() } - /* ALERT PLUGIN DEFINITION - * ======================= */ + // ALERT PLUGIN DEFINITION + // ======================= var old = $.fn.alert $.fn.alert = function (option) { return this.each(function () { var $this = $(this) - , data = $this.data('alert') - if (!data) $this.data('alert', (data = new Alert(this))) + var data = $this.data('bs.alert') + + if (!data) $this.data('bs.alert', (data = new Alert(this))) if (typeof option == 'string') data[option].call($this) }) } @@ -141,8 +128,8 @@ $.fn.alert.Constructor = Alert - /* ALERT NO CONFLICT - * ================= */ + // ALERT NO CONFLICT + // ================= $.fn.alert.noConflict = function () { $.fn.alert = old @@ -150,99 +137,102 @@ } - /* ALERT DATA-API - * ============== */ + // ALERT DATA-API + // ============== - $(document).on('click.alert.data-api', dismiss, Alert.prototype.close) + $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) -}(window.jQuery);/* ============================================================ - * bootstrap-button.js v2.3.2 - * http://twitter.github.com/bootstrap/javascript.html#buttons - * ============================================================ - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ +}(jQuery); + +/* ======================================================================== + * Bootstrap: button.js v3.1.1 + * http://getbootstrap.com/javascript/#buttons + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ -!function ($) { ++function ($) { + 'use strict'; - "use strict"; // jshint ;_; - - - /* BUTTON PUBLIC CLASS DEFINITION - * ============================== */ + // BUTTON PUBLIC CLASS DEFINITION + // ============================== var Button = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, $.fn.button.defaults, options) + this.$element = $(element) + this.options = $.extend({}, Button.DEFAULTS, options) + this.isLoading = false + } + + Button.DEFAULTS = { + loadingText: 'loading...' } Button.prototype.setState = function (state) { - var d = 'disabled' - , $el = this.$element - , data = $el.data() - , val = $el.is('input') ? 'val' : 'html' + var d = 'disabled' + var $el = this.$element + var val = $el.is('input') ? 'val' : 'html' + var data = $el.data() state = state + 'Text' - data.resetText || $el.data('resetText', $el[val]()) + + if (!data.resetText) $el.data('resetText', $el[val]()) $el[val](data[state] || this.options[state]) // push to event loop to allow forms to submit - setTimeout(function () { - state == 'loadingText' ? - $el.addClass(d).attr(d, d) : + setTimeout($.proxy(function () { + if (state == 'loadingText') { + this.isLoading = true + $el.addClass(d).attr(d, d) + } else if (this.isLoading) { + this.isLoading = false $el.removeClass(d).removeAttr(d) - }, 0) + } + }, this), 0) } Button.prototype.toggle = function () { - var $parent = this.$element.closest('[data-toggle="buttons-radio"]') + var changed = true + var $parent = this.$element.closest('[data-toggle="buttons"]') - $parent && $parent - .find('.active') - .removeClass('active') + if ($parent.length) { + var $input = this.$element.find('input') + if ($input.prop('type') == 'radio') { + if ($input.prop('checked') && this.$element.hasClass('active')) changed = false + else $parent.find('.active').removeClass('active') + } + if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change') + } - this.$element.toggleClass('active') + if (changed) this.$element.toggleClass('active') } - /* BUTTON PLUGIN DEFINITION - * ======================== */ + // BUTTON PLUGIN DEFINITION + // ======================== var old = $.fn.button $.fn.button = function (option) { return this.each(function () { - var $this = $(this) - , data = $this.data('button') - , options = typeof option == 'object' && option - if (!data) $this.data('button', (data = new Button(this, options))) + var $this = $(this) + var data = $this.data('bs.button') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.button', (data = new Button(this, options))) + if (option == 'toggle') data.toggle() else if (option) data.setState(option) }) } - $.fn.button.defaults = { - loadingText: 'loading...' - } - $.fn.button.Constructor = Button - /* BUTTON NO CONFLICT - * ================== */ + // BUTTON NO CONFLICT + // ================== $.fn.button.noConflict = function () { $.fn.button = old @@ -250,367 +240,364 @@ } - /* BUTTON DATA-API - * =============== */ + // BUTTON DATA-API + // =============== - $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) { + $(document).on('click.bs.button.data-api', '[data-toggle^=button]', function (e) { var $btn = $(e.target) if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') $btn.button('toggle') + e.preventDefault() }) -}(window.jQuery);/* ========================================================== - * bootstrap-carousel.js v2.3.2 - * http://twitter.github.com/bootstrap/javascript.html#carousel - * ========================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ +}(jQuery); + +/* ======================================================================== + * Bootstrap: carousel.js v3.1.1 + * http://getbootstrap.com/javascript/#carousel + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ -!function ($) { ++function ($) { + 'use strict'; - "use strict"; // jshint ;_; - - - /* CAROUSEL CLASS DEFINITION - * ========================= */ + // CAROUSEL CLASS DEFINITION + // ========================= var Carousel = function (element, options) { - this.$element = $(element) + this.$element = $(element) this.$indicators = this.$element.find('.carousel-indicators') - this.options = options + this.options = options + this.paused = + this.sliding = + this.interval = + this.$active = + this.$items = null + this.options.pause == 'hover' && this.$element .on('mouseenter', $.proxy(this.pause, this)) .on('mouseleave', $.proxy(this.cycle, this)) } - Carousel.prototype = { + Carousel.DEFAULTS = { + interval: 5000, + pause: 'hover', + wrap: true + } - cycle: function (e) { - if (!e) this.paused = false - if (this.interval) clearInterval(this.interval); - this.options.interval - && !this.paused - && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) - return this + Carousel.prototype.cycle = function (e) { + e || (this.paused = false) + + this.interval && clearInterval(this.interval) + + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + + return this + } + + Carousel.prototype.getActiveIndex = function () { + this.$active = this.$element.find('.item.active') + this.$items = this.$active.parent().children() + + return this.$items.index(this.$active) + } + + Carousel.prototype.to = function (pos) { + var that = this + var activeIndex = this.getActiveIndex() + + if (pos > (this.$items.length - 1) || pos < 0) return + + if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) + if (activeIndex == pos) return this.pause().cycle() + + return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) + } + + Carousel.prototype.pause = function (e) { + e || (this.paused = true) + + if (this.$element.find('.next, .prev').length && $.support.transition) { + this.$element.trigger($.support.transition.end) + this.cycle(true) } - , getActiveIndex: function () { - this.$active = this.$element.find('.item.active') - this.$items = this.$active.parent().children() - return this.$items.index(this.$active) + this.interval = clearInterval(this.interval) + + return this + } + + Carousel.prototype.next = function () { + if (this.sliding) return + return this.slide('next') + } + + Carousel.prototype.prev = function () { + if (this.sliding) return + return this.slide('prev') + } + + Carousel.prototype.slide = function (type, next) { + var $active = this.$element.find('.item.active') + var $next = next || $active[type]() + var isCycling = this.interval + var direction = type == 'next' ? 'left' : 'right' + var fallback = type == 'next' ? 'first' : 'last' + var that = this + + if (!$next.length) { + if (!this.options.wrap) return + $next = this.$element.find('.item')[fallback]() } - , to: function (pos) { - var activeIndex = this.getActiveIndex() - , that = this + if ($next.hasClass('active')) return this.sliding = false - if (pos > (this.$items.length - 1) || pos < 0) return + var e = $.Event('slide.bs.carousel', { relatedTarget: $next[0], direction: direction }) + this.$element.trigger(e) + if (e.isDefaultPrevented()) return - if (this.sliding) { - return this.$element.one('slid', function () { - that.to(pos) - }) - } + this.sliding = true - if (activeIndex == pos) { - return this.pause().cycle() - } + isCycling && this.pause() - return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) - } - - , pause: function (e) { - if (!e) this.paused = true - if (this.$element.find('.next, .prev').length && $.support.transition.end) { - this.$element.trigger($.support.transition.end) - this.cycle(true) - } - clearInterval(this.interval) - this.interval = null - return this - } - - , next: function () { - if (this.sliding) return - return this.slide('next') - } - - , prev: function () { - if (this.sliding) return - return this.slide('prev') - } - - , slide: function (type, next) { - var $active = this.$element.find('.item.active') - , $next = next || $active[type]() - , isCycling = this.interval - , direction = type == 'next' ? 'left' : 'right' - , fallback = type == 'next' ? 'first' : 'last' - , that = this - , e - - this.sliding = true - - isCycling && this.pause() - - $next = $next.length ? $next : this.$element.find('.item')[fallback]() - - e = $.Event('slide', { - relatedTarget: $next[0] - , direction: direction + if (this.$indicators.length) { + this.$indicators.find('.active').removeClass('active') + this.$element.one('slid.bs.carousel', function () { + var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) + $nextIndicator && $nextIndicator.addClass('active') }) + } - if ($next.hasClass('active')) return - - if (this.$indicators.length) { - this.$indicators.find('.active').removeClass('active') - this.$element.one('slid', function () { - var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) - $nextIndicator && $nextIndicator.addClass('active') - }) - } - - if ($.support.transition && this.$element.hasClass('slide')) { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $next.addClass(type) - $next[0].offsetWidth // force reflow - $active.addClass(direction) - $next.addClass(direction) - this.$element.one($.support.transition.end, function () { + if ($.support.transition && this.$element.hasClass('slide')) { + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + $active + .one($.support.transition.end, function () { $next.removeClass([type, direction].join(' ')).addClass('active') $active.removeClass(['active', direction].join(' ')) that.sliding = false - setTimeout(function () { that.$element.trigger('slid') }, 0) + setTimeout(function () { that.$element.trigger('slid.bs.carousel') }, 0) }) - } else { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $active.removeClass('active') - $next.addClass('active') - this.sliding = false - this.$element.trigger('slid') - } - - isCycling && this.cycle() - - return this + .emulateTransitionEnd($active.css('transition-duration').slice(0, -1) * 1000) + } else { + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger('slid.bs.carousel') } + isCycling && this.cycle() + + return this } - /* CAROUSEL PLUGIN DEFINITION - * ========================== */ + // CAROUSEL PLUGIN DEFINITION + // ========================== var old = $.fn.carousel $.fn.carousel = function (option) { return this.each(function () { - var $this = $(this) - , data = $this.data('carousel') - , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) - , action = typeof option == 'string' ? option : options.slide - if (!data) $this.data('carousel', (data = new Carousel(this, options))) + var $this = $(this) + var data = $this.data('bs.carousel') + var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) + var action = typeof option == 'string' ? option : options.slide + + if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) if (typeof option == 'number') data.to(option) else if (action) data[action]() else if (options.interval) data.pause().cycle() }) } - $.fn.carousel.defaults = { - interval: 5000 - , pause: 'hover' - } - $.fn.carousel.Constructor = Carousel - /* CAROUSEL NO CONFLICT - * ==================== */ + // CAROUSEL NO CONFLICT + // ==================== $.fn.carousel.noConflict = function () { $.fn.carousel = old return this } - /* CAROUSEL DATA-API - * ================= */ - $(document).on('click.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { - var $this = $(this), href - , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 - , options = $.extend({}, $target.data(), $this.data()) - , slideIndex + // CAROUSEL DATA-API + // ================= + + $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { + var $this = $(this), href + var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + var options = $.extend({}, $target.data(), $this.data()) + var slideIndex = $this.attr('data-slide-to') + if (slideIndex) options.interval = false $target.carousel(options) if (slideIndex = $this.attr('data-slide-to')) { - $target.data('carousel').pause().to(slideIndex).cycle() + $target.data('bs.carousel').to(slideIndex) } e.preventDefault() }) -}(window.jQuery);/* ============================================================= - * bootstrap-collapse.js v2.3.2 - * http://twitter.github.com/bootstrap/javascript.html#collapse - * ============================================================= - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ + $(window).on('load', function () { + $('[data-ride="carousel"]').each(function () { + var $carousel = $(this) + $carousel.carousel($carousel.data()) + }) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: collapse.js v3.1.1 + * http://getbootstrap.com/javascript/#collapse + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ -!function ($) { ++function ($) { + 'use strict'; - "use strict"; // jshint ;_; - - - /* COLLAPSE PUBLIC CLASS DEFINITION - * ================================ */ + // COLLAPSE PUBLIC CLASS DEFINITION + // ================================ var Collapse = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, $.fn.collapse.defaults, options) + this.$element = $(element) + this.options = $.extend({}, Collapse.DEFAULTS, options) + this.transitioning = null - if (this.options.parent) { - this.$parent = $(this.options.parent) - } - - this.options.toggle && this.toggle() + if (this.options.parent) this.$parent = $(this.options.parent) + if (this.options.toggle) this.toggle() } - Collapse.prototype = { + Collapse.DEFAULTS = { + toggle: true + } - constructor: Collapse + Collapse.prototype.dimension = function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } - , dimension: function () { - var hasWidth = this.$element.hasClass('width') - return hasWidth ? 'width' : 'height' + Collapse.prototype.show = function () { + if (this.transitioning || this.$element.hasClass('in')) return + + var startEvent = $.Event('show.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + var actives = this.$parent && this.$parent.find('> .panel > .in') + + if (actives && actives.length) { + var hasData = actives.data('bs.collapse') + if (hasData && hasData.transitioning) return + actives.collapse('hide') + hasData || actives.data('bs.collapse', null) } - , show: function () { - var dimension - , scroll - , actives - , hasData + var dimension = this.dimension() - if (this.transitioning || this.$element.hasClass('in')) return + this.$element + .removeClass('collapse') + .addClass('collapsing') + [dimension](0) - dimension = this.dimension() - scroll = $.camelCase(['scroll', dimension].join('-')) - actives = this.$parent && this.$parent.find('> .accordion-group > .in') - - if (actives && actives.length) { - hasData = actives.data('collapse') - if (hasData && hasData.transitioning) return - actives.collapse('hide') - hasData || actives.data('collapse', null) - } - - this.$element[dimension](0) - this.transition('addClass', $.Event('show'), 'shown') - $.support.transition && this.$element[dimension](this.$element[0][scroll]) - } - - , hide: function () { - var dimension - if (this.transitioning || !this.$element.hasClass('in')) return - dimension = this.dimension() - this.reset(this.$element[dimension]()) - this.transition('removeClass', $.Event('hide'), 'hidden') - this.$element[dimension](0) - } - - , reset: function (size) { - var dimension = this.dimension() + this.transitioning = 1 + var complete = function () { this.$element - .removeClass('collapse') - [dimension](size || 'auto') - [0].offsetWidth - - this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') - - return this + .removeClass('collapsing') + .addClass('collapse in') + [dimension]('auto') + this.transitioning = 0 + this.$element.trigger('shown.bs.collapse') } - , transition: function (method, startEvent, completeEvent) { - var that = this - , complete = function () { - if (startEvent.type == 'show') that.reset() - that.transitioning = 0 - that.$element.trigger(completeEvent) - } + if (!$.support.transition) return complete.call(this) - this.$element.trigger(startEvent) + var scrollSize = $.camelCase(['scroll', dimension].join('-')) - if (startEvent.isDefaultPrevented()) return + this.$element + .one($.support.transition.end, $.proxy(complete, this)) + .emulateTransitionEnd(350) + [dimension](this.$element[0][scrollSize]) + } - this.transitioning = 1 + Collapse.prototype.hide = function () { + if (this.transitioning || !this.$element.hasClass('in')) return - this.$element[method]('in') + var startEvent = $.Event('hide.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return - $.support.transition && this.$element.hasClass('collapse') ? - this.$element.one($.support.transition.end, complete) : - complete() + var dimension = this.dimension() + + this.$element + [dimension](this.$element[dimension]()) + [0].offsetHeight + + this.$element + .addClass('collapsing') + .removeClass('collapse') + .removeClass('in') + + this.transitioning = 1 + + var complete = function () { + this.transitioning = 0 + this.$element + .trigger('hidden.bs.collapse') + .removeClass('collapsing') + .addClass('collapse') } - , toggle: function () { - this[this.$element.hasClass('in') ? 'hide' : 'show']() - } + if (!$.support.transition) return complete.call(this) + this.$element + [dimension](0) + .one($.support.transition.end, $.proxy(complete, this)) + .emulateTransitionEnd(350) + } + + Collapse.prototype.toggle = function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() } - /* COLLAPSE PLUGIN DEFINITION - * ========================== */ + // COLLAPSE PLUGIN DEFINITION + // ========================== var old = $.fn.collapse $.fn.collapse = function (option) { return this.each(function () { - var $this = $(this) - , data = $this.data('collapse') - , options = $.extend({}, $.fn.collapse.defaults, $this.data(), typeof option == 'object' && option) - if (!data) $this.data('collapse', (data = new Collapse(this, options))) + var $this = $(this) + var data = $this.data('bs.collapse') + var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data && options.toggle && option == 'show') option = !option + if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) if (typeof option == 'string') data[option]() }) } - $.fn.collapse.defaults = { - toggle: true - } - $.fn.collapse.Constructor = Collapse - /* COLLAPSE NO CONFLICT - * ==================== */ + // COLLAPSE NO CONFLICT + // ==================== $.fn.collapse.noConflict = function () { $.fn.collapse = old @@ -618,162 +605,151 @@ } - /* COLLAPSE DATA-API - * ================= */ + // COLLAPSE DATA-API + // ================= - $(document).on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { - var $this = $(this), href - , target = $this.attr('data-target') + $(document).on('click.bs.collapse.data-api', '[data-toggle=collapse]', function (e) { + var $this = $(this), href + var target = $this.attr('data-target') || e.preventDefault() || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 - , option = $(target).data('collapse') ? 'toggle' : $this.data() - $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') - $(target).collapse(option) + var $target = $(target) + var data = $target.data('bs.collapse') + var option = data ? 'toggle' : $this.data() + var parent = $this.attr('data-parent') + var $parent = parent && $(parent) + + if (!data || !data.transitioning) { + if ($parent) $parent.find('[data-toggle=collapse][data-parent="' + parent + '"]').not($this).addClass('collapsed') + $this[$target.hasClass('in') ? 'addClass' : 'removeClass']('collapsed') + } + + $target.collapse(option) }) -}(window.jQuery);/* ============================================================ - * bootstrap-dropdown.js v2.3.2 - * http://twitter.github.com/bootstrap/javascript.html#dropdowns - * ============================================================ - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ +}(jQuery); + +/* ======================================================================== + * Bootstrap: dropdown.js v3.1.1 + * http://getbootstrap.com/javascript/#dropdowns + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ -!function ($) { ++function ($) { + 'use strict'; - "use strict"; // jshint ;_; - - - /* DROPDOWN CLASS DEFINITION - * ========================= */ - - var toggle = '[data-toggle=dropdown]' - , Dropdown = function (element) { - var $el = $(element).on('click.dropdown.data-api', this.toggle) - $('html').on('click.dropdown.data-api', function () { - $el.parent().removeClass('open') - }) - } - - Dropdown.prototype = { - - constructor: Dropdown - - , toggle: function (e) { - var $this = $(this) - , $parent - , isActive - - if ($this.is('.disabled, :disabled')) return - - $parent = getParent($this) - - isActive = $parent.hasClass('open') - - clearMenus() - - if (!isActive) { - if ('ontouchstart' in document.documentElement) { - // if mobile we we use a backdrop because click events don't delegate - $('<div class="dropdown-backdrop"/>').insertBefore($(this)).on('click', clearMenus) - } - $parent.toggleClass('open') - } - - $this.focus() - - return false - } - - , keydown: function (e) { - var $this - , $items - , $active - , $parent - , isActive - , index - - if (!/(38|40|27)/.test(e.keyCode)) return - - $this = $(this) - - e.preventDefault() - e.stopPropagation() - - if ($this.is('.disabled, :disabled')) return - - $parent = getParent($this) - - isActive = $parent.hasClass('open') - - if (!isActive || (isActive && e.keyCode == 27)) { - if (e.which == 27) $parent.find(toggle).focus() - return $this.click() - } - - $items = $('[role=menu] li:not(.divider):visible a', $parent) - - if (!$items.length) return - - index = $items.index($items.filter(':focus')) - - if (e.keyCode == 38 && index > 0) index-- // up - if (e.keyCode == 40 && index < $items.length - 1) index++ // down - if (!~index) index = 0 - - $items - .eq(index) - .focus() - } + // DROPDOWN CLASS DEFINITION + // ========================= + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle=dropdown]' + var Dropdown = function (element) { + $(element).on('click.bs.dropdown', this.toggle) } - function clearMenus() { - $('.dropdown-backdrop').remove() + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we use a backdrop because click events don't delegate + $('<div class="dropdown-backdrop"/>').insertAfter($(this)).on('click', clearMenus) + } + + var relatedTarget = { relatedTarget: this } + $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $parent + .toggleClass('open') + .trigger('shown.bs.dropdown', relatedTarget) + + $this.focus() + } + + return false + } + + Dropdown.prototype.keydown = function (e) { + if (!/(38|40|27)/.test(e.keyCode)) return + + var $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + if (!isActive || (isActive && e.keyCode == 27)) { + if (e.which == 27) $parent.find(toggle).focus() + return $this.click() + } + + var desc = ' li:not(.divider):visible a' + var $items = $parent.find('[role=menu]' + desc + ', [role=listbox]' + desc) + + if (!$items.length) return + + var index = $items.index($items.filter(':focus')) + + if (e.keyCode == 38 && index > 0) index-- // up + if (e.keyCode == 40 && index < $items.length - 1) index++ // down + if (!~index) index = 0 + + $items.eq(index).focus() + } + + function clearMenus(e) { + $(backdrop).remove() $(toggle).each(function () { - getParent($(this)).removeClass('open') + var $parent = getParent($(this)) + var relatedTarget = { relatedTarget: this } + if (!$parent.hasClass('open')) return + $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) + if (e.isDefaultPrevented()) return + $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget) }) } function getParent($this) { var selector = $this.attr('data-target') - , $parent if (!selector) { selector = $this.attr('href') - selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 } - $parent = selector && $(selector) + var $parent = selector && $(selector) - if (!$parent || !$parent.length) $parent = $this.parent() - - return $parent + return $parent && $parent.length ? $parent : $this.parent() } - /* DROPDOWN PLUGIN DEFINITION - * ========================== */ + // DROPDOWN PLUGIN DEFINITION + // ========================== var old = $.fn.dropdown $.fn.dropdown = function (option) { return this.each(function () { var $this = $(this) - , data = $this.data('dropdown') - if (!data) $this.data('dropdown', (data = new Dropdown(this))) + var data = $this.data('bs.dropdown') + + if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) if (typeof option == 'string') data[option].call($this) }) } @@ -781,8 +757,8 @@ $.fn.dropdown.Constructor = Dropdown - /* DROPDOWN NO CONFLICT - * ==================== */ + // DROPDOWN NO CONFLICT + // ==================== $.fn.dropdown.noConflict = function () { $.fn.dropdown = old @@ -790,237 +766,230 @@ } - /* APPLY TO STANDARD DROPDOWN ELEMENTS - * =================================== */ + // APPLY TO STANDARD DROPDOWN ELEMENTS + // =================================== $(document) - .on('click.dropdown.data-api', clearMenus) - .on('click.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) - .on('click.dropdown.data-api' , toggle, Dropdown.prototype.toggle) - .on('keydown.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) + .on('click.bs.dropdown.data-api', clearMenus) + .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) + .on('keydown.bs.dropdown.data-api', toggle + ', [role=menu], [role=listbox]', Dropdown.prototype.keydown) -}(window.jQuery); -/* ========================================================= - * bootstrap-modal.js v2.3.2 - * http://twitter.github.com/bootstrap/javascript.html#modals - * ========================================================= - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================= */ +}(jQuery); + +/* ======================================================================== + * Bootstrap: modal.js v3.1.1 + * http://getbootstrap.com/javascript/#modals + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ -!function ($) { ++function ($) { + 'use strict'; - "use strict"; // jshint ;_; - - - /* MODAL CLASS DEFINITION - * ====================== */ + // MODAL CLASS DEFINITION + // ====================== var Modal = function (element, options) { - this.options = options - this.$element = $(element) - .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) - this.options.remote && this.$element.find('.modal-body').load(this.options.remote) + this.options = options + this.$element = $(element) + this.$backdrop = + this.isShown = null + + if (this.options.remote) { + this.$element + .find('.modal-content') + .load(this.options.remote, $.proxy(function () { + this.$element.trigger('loaded.bs.modal') + }, this)) + } } - Modal.prototype = { + Modal.DEFAULTS = { + backdrop: true, + keyboard: true, + show: true + } - constructor: Modal + Modal.prototype.toggle = function (_relatedTarget) { + return this[!this.isShown ? 'show' : 'hide'](_relatedTarget) + } - , toggle: function () { - return this[!this.isShown ? 'show' : 'hide']() + Modal.prototype.show = function (_relatedTarget) { + var that = this + var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) + + this.$element.trigger(e) + + if (this.isShown || e.isDefaultPrevented()) return + + this.isShown = true + + this.escape() + + this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) + + this.backdrop(function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + if (!that.$element.parent().length) { + that.$element.appendTo(document.body) // don't move modals dom position } - , show: function () { - var that = this - , e = $.Event('show') + that.$element + .show() + .scrollTop(0) - this.$element.trigger(e) - - if (this.isShown || e.isDefaultPrevented()) return - - this.isShown = true - - this.escape() - - this.backdrop(function () { - var transition = $.support.transition && that.$element.hasClass('fade') - - if (!that.$element.parent().length) { - that.$element.appendTo(document.body) //don't move modals dom position - } - - that.$element.show() - - if (transition) { - that.$element[0].offsetWidth // force reflow - } - - that.$element - .addClass('in') - .attr('aria-hidden', false) - - that.enforceFocus() - - transition ? - that.$element.one($.support.transition.end, function () { that.$element.focus().trigger('shown') }) : - that.$element.focus().trigger('shown') - - }) + if (transition) { + that.$element[0].offsetWidth // force reflow } - , hide: function (e) { - e && e.preventDefault() + that.$element + .addClass('in') + .attr('aria-hidden', false) - var that = this + that.enforceFocus() - e = $.Event('hide') + var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) - this.$element.trigger(e) - - if (!this.isShown || e.isDefaultPrevented()) return - - this.isShown = false - - this.escape() - - $(document).off('focusin.modal') - - this.$element - .removeClass('in') - .attr('aria-hidden', true) - - $.support.transition && this.$element.hasClass('fade') ? - this.hideWithTransition() : - this.hideModal() - } - - , enforceFocus: function () { - var that = this - $(document).on('focusin.modal', function (e) { - if (that.$element[0] !== e.target && !that.$element.has(e.target).length) { - that.$element.focus() - } - }) - } - - , escape: function () { - var that = this - if (this.isShown && this.options.keyboard) { - this.$element.on('keyup.dismiss.modal', function ( e ) { - e.which == 27 && that.hide() + transition ? + that.$element.find('.modal-dialog') // wait for modal to slide in + .one($.support.transition.end, function () { + that.$element.focus().trigger(e) }) - } else if (!this.isShown) { - this.$element.off('keyup.dismiss.modal') - } - } - - , hideWithTransition: function () { - var that = this - , timeout = setTimeout(function () { - that.$element.off($.support.transition.end) - that.hideModal() - }, 500) - - this.$element.one($.support.transition.end, function () { - clearTimeout(timeout) - that.hideModal() - }) - } - - , hideModal: function () { - var that = this - this.$element.hide() - this.backdrop(function () { - that.removeBackdrop() - that.$element.trigger('hidden') - }) - } - - , removeBackdrop: function () { - this.$backdrop && this.$backdrop.remove() - this.$backdrop = null - } - - , backdrop: function (callback) { - var that = this - , animate = this.$element.hasClass('fade') ? 'fade' : '' - - if (this.isShown && this.options.backdrop) { - var doAnimate = $.support.transition && animate - - this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />') - .appendTo(document.body) - - this.$backdrop.click( - this.options.backdrop == 'static' ? - $.proxy(this.$element[0].focus, this.$element[0]) - : $.proxy(this.hide, this) - ) - - if (doAnimate) this.$backdrop[0].offsetWidth // force reflow - - this.$backdrop.addClass('in') - - if (!callback) return - - doAnimate ? - this.$backdrop.one($.support.transition.end, callback) : - callback() - - } else if (!this.isShown && this.$backdrop) { - this.$backdrop.removeClass('in') - - $.support.transition && this.$element.hasClass('fade')? - this.$backdrop.one($.support.transition.end, callback) : - callback() - - } else if (callback) { - callback() - } - } - } - - - /* MODAL PLUGIN DEFINITION - * ======================= */ - - var old = $.fn.modal - - $.fn.modal = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('modal') - , options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option) - if (!data) $this.data('modal', (data = new Modal(this, options))) - if (typeof option == 'string') data[option]() - else if (options.show) data.show() + .emulateTransitionEnd(300) : + that.$element.focus().trigger(e) }) } - $.fn.modal.defaults = { - backdrop: true - , keyboard: true - , show: true + Modal.prototype.hide = function (e) { + if (e) e.preventDefault() + + e = $.Event('hide.bs.modal') + + this.$element.trigger(e) + + if (!this.isShown || e.isDefaultPrevented()) return + + this.isShown = false + + this.escape() + + $(document).off('focusin.bs.modal') + + this.$element + .removeClass('in') + .attr('aria-hidden', true) + .off('click.dismiss.bs.modal') + + $.support.transition && this.$element.hasClass('fade') ? + this.$element + .one($.support.transition.end, $.proxy(this.hideModal, this)) + .emulateTransitionEnd(300) : + this.hideModal() + } + + Modal.prototype.enforceFocus = function () { + $(document) + .off('focusin.bs.modal') // guard against infinite focus loop + .on('focusin.bs.modal', $.proxy(function (e) { + if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { + this.$element.focus() + } + }, this)) + } + + Modal.prototype.escape = function () { + if (this.isShown && this.options.keyboard) { + this.$element.on('keyup.dismiss.bs.modal', $.proxy(function (e) { + e.which == 27 && this.hide() + }, this)) + } else if (!this.isShown) { + this.$element.off('keyup.dismiss.bs.modal') + } + } + + Modal.prototype.hideModal = function () { + var that = this + this.$element.hide() + this.backdrop(function () { + that.removeBackdrop() + that.$element.trigger('hidden.bs.modal') + }) + } + + Modal.prototype.removeBackdrop = function () { + this.$backdrop && this.$backdrop.remove() + this.$backdrop = null + } + + Modal.prototype.backdrop = function (callback) { + var animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />') + .appendTo(document.body) + + this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { + if (e.target !== e.currentTarget) return + this.options.backdrop == 'static' + ? this.$element[0].focus.call(this.$element[0]) + : this.hide.call(this) + }, this)) + + if (doAnimate) this.$backdrop[0].offsetWidth // force reflow + + this.$backdrop.addClass('in') + + if (!callback) return + + doAnimate ? + this.$backdrop + .one($.support.transition.end, callback) + .emulateTransitionEnd(150) : + callback() + + } else if (!this.isShown && this.$backdrop) { + this.$backdrop.removeClass('in') + + $.support.transition && this.$element.hasClass('fade') ? + this.$backdrop + .one($.support.transition.end, callback) + .emulateTransitionEnd(150) : + callback() + + } else if (callback) { + callback() + } + } + + + // MODAL PLUGIN DEFINITION + // ======================= + + var old = $.fn.modal + + $.fn.modal = function (option, _relatedTarget) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.modal') + var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data) $this.data('bs.modal', (data = new Modal(this, options))) + if (typeof option == 'string') data[option](_relatedTarget) + else if (options.show) data.show(_relatedTarget) + }) } $.fn.modal.Constructor = Modal - /* MODAL NO CONFLICT - * ================= */ + // MODAL NO CONFLICT + // ================= $.fn.modal.noConflict = function () { $.fn.modal = old @@ -1028,644 +997,676 @@ } - /* MODAL DATA-API - * ============== */ + // MODAL DATA-API + // ============== - $(document).on('click.modal.data-api', '[data-toggle="modal"]', function (e) { - var $this = $(this) - , href = $this.attr('href') - , $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7 - , option = $target.data('modal') ? 'toggle' : $.extend({ remote:!/#/.test(href) && href }, $target.data(), $this.data()) + $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { + var $this = $(this) + var href = $this.attr('href') + var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7 + var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) - e.preventDefault() + if ($this.is('a')) e.preventDefault() $target - .modal(option) + .modal(option, this) .one('hide', function () { - $this.focus() + $this.is(':visible') && $this.focus() }) }) -}(window.jQuery); -/* =========================================================== - * bootstrap-tooltip.js v2.3.2 - * http://twitter.github.com/bootstrap/javascript.html#tooltips + $(document) + .on('show.bs.modal', '.modal', function () { $(document.body).addClass('modal-open') }) + .on('hidden.bs.modal', '.modal', function () { $(document.body).removeClass('modal-open') }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: tooltip.js v3.1.1 + * http://getbootstrap.com/javascript/#tooltip * Inspired by the original jQuery.tipsy by Jason Frame - * =========================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ -!function ($) { ++function ($) { + 'use strict'; - "use strict"; // jshint ;_; - - - /* TOOLTIP PUBLIC CLASS DEFINITION - * =============================== */ + // TOOLTIP PUBLIC CLASS DEFINITION + // =============================== var Tooltip = function (element, options) { + this.type = + this.options = + this.enabled = + this.timeout = + this.hoverState = + this.$element = null + this.init('tooltip', element, options) } - Tooltip.prototype = { + Tooltip.DEFAULTS = { + animation: true, + placement: 'top', + selector: false, + template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>', + trigger: 'hover focus', + title: '', + delay: 0, + html: false, + container: false + } - constructor: Tooltip + Tooltip.prototype.init = function (type, element, options) { + this.enabled = true + this.type = type + this.$element = $(element) + this.options = this.getOptions(options) - , init: function (type, element, options) { - var eventIn - , eventOut - , triggers - , trigger - , i + var triggers = this.options.trigger.split(' ') - this.type = type - this.$element = $(element) - this.options = this.getOptions(options) - this.enabled = true + for (var i = triggers.length; i--;) { + var trigger = triggers[i] - triggers = this.options.trigger.split(' ') + if (trigger == 'click') { + this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) + } else if (trigger != 'manual') { + var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' + var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' - for (i = triggers.length; i--;) { - trigger = triggers[i] - if (trigger == 'click') { - this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) - } else if (trigger != 'manual') { - eventIn = trigger == 'hover' ? 'mouseenter' : 'focus' - eventOut = trigger == 'hover' ? 'mouseleave' : 'blur' - this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) - this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) - } - } - - this.options.selector ? - (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : - this.fixTitle() - } - - , getOptions: function (options) { - options = $.extend({}, $.fn[this.type].defaults, this.$element.data(), options) - - if (options.delay && typeof options.delay == 'number') { - options.delay = { - show: options.delay - , hide: options.delay - } - } - - return options - } - - , enter: function (e) { - var defaults = $.fn[this.type].defaults - , options = {} - , self - - this._options && $.each(this._options, function (key, value) { - if (defaults[key] != value) options[key] = value - }, this) - - self = $(e.currentTarget)[this.type](options).data(this.type) - - if (!self.options.delay || !self.options.delay.show) return self.show() - - clearTimeout(this.timeout) - self.hoverState = 'in' - this.timeout = setTimeout(function() { - if (self.hoverState == 'in') self.show() - }, self.options.delay.show) - } - - , leave: function (e) { - var self = $(e.currentTarget)[this.type](this._options).data(this.type) - - if (this.timeout) clearTimeout(this.timeout) - if (!self.options.delay || !self.options.delay.hide) return self.hide() - - self.hoverState = 'out' - this.timeout = setTimeout(function() { - if (self.hoverState == 'out') self.hide() - }, self.options.delay.hide) - } - - , show: function () { - var $tip - , pos - , actualWidth - , actualHeight - , placement - , tp - , e = $.Event('show') - - if (this.hasContent() && this.enabled) { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $tip = this.tip() - this.setContent() - - if (this.options.animation) { - $tip.addClass('fade') - } - - placement = typeof this.options.placement == 'function' ? - this.options.placement.call(this, $tip[0], this.$element[0]) : - this.options.placement - - $tip - .detach() - .css({ top: 0, left: 0, display: 'block' }) - - this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) - - pos = this.getPosition() - - actualWidth = $tip[0].offsetWidth - actualHeight = $tip[0].offsetHeight - - switch (placement) { - case 'bottom': - tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2} - break - case 'top': - tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2} - break - case 'left': - tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth} - break - case 'right': - tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width} - break - } - - this.applyPlacement(tp, placement) - this.$element.trigger('shown') + this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) + this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) } } - , applyPlacement: function(offset, placement){ + this.options.selector ? + (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : + this.fixTitle() + } + + Tooltip.prototype.getDefaults = function () { + return Tooltip.DEFAULTS + } + + Tooltip.prototype.getOptions = function (options) { + options = $.extend({}, this.getDefaults(), this.$element.data(), options) + + if (options.delay && typeof options.delay == 'number') { + options.delay = { + show: options.delay, + hide: options.delay + } + } + + return options + } + + Tooltip.prototype.getDelegateOptions = function () { + var options = {} + var defaults = this.getDefaults() + + this._options && $.each(this._options, function (key, value) { + if (defaults[key] != value) options[key] = value + }) + + return options + } + + Tooltip.prototype.enter = function (obj) { + var self = obj instanceof this.constructor ? + obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) + + clearTimeout(self.timeout) + + self.hoverState = 'in' + + if (!self.options.delay || !self.options.delay.show) return self.show() + + self.timeout = setTimeout(function () { + if (self.hoverState == 'in') self.show() + }, self.options.delay.show) + } + + Tooltip.prototype.leave = function (obj) { + var self = obj instanceof this.constructor ? + obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) + + clearTimeout(self.timeout) + + self.hoverState = 'out' + + if (!self.options.delay || !self.options.delay.hide) return self.hide() + + self.timeout = setTimeout(function () { + if (self.hoverState == 'out') self.hide() + }, self.options.delay.hide) + } + + Tooltip.prototype.show = function () { + var e = $.Event('show.bs.' + this.type) + + if (this.hasContent() && this.enabled) { + this.$element.trigger(e) + + if (e.isDefaultPrevented()) return + var that = this; + var $tip = this.tip() - , width = $tip[0].offsetWidth - , height = $tip[0].offsetHeight - , actualWidth - , actualHeight - , delta - , replace + + this.setContent() + + if (this.options.animation) $tip.addClass('fade') + + var placement = typeof this.options.placement == 'function' ? + this.options.placement.call(this, $tip[0], this.$element[0]) : + this.options.placement + + var autoToken = /\s?auto?\s?/i + var autoPlace = autoToken.test(placement) + if (autoPlace) placement = placement.replace(autoToken, '') || 'top' $tip - .offset(offset) + .detach() + .css({ top: 0, left: 0, display: 'block' }) .addClass(placement) - .addClass('in') - actualWidth = $tip[0].offsetWidth - actualHeight = $tip[0].offsetHeight + this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) - if (placement == 'top' && actualHeight != height) { - offset.top = offset.top + height - actualHeight - replace = true + var pos = this.getPosition() + var actualWidth = $tip[0].offsetWidth + var actualHeight = $tip[0].offsetHeight + + if (autoPlace) { + var $parent = this.$element.parent() + + var orgPlacement = placement + var docScroll = document.documentElement.scrollTop || document.body.scrollTop + var parentWidth = this.options.container == 'body' ? window.innerWidth : $parent.outerWidth() + var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight() + var parentLeft = this.options.container == 'body' ? 0 : $parent.offset().left + + placement = placement == 'bottom' && pos.top + pos.height + actualHeight - docScroll > parentHeight ? 'top' : + placement == 'top' && pos.top - docScroll - actualHeight < 0 ? 'bottom' : + placement == 'right' && pos.right + actualWidth > parentWidth ? 'left' : + placement == 'left' && pos.left - actualWidth < parentLeft ? 'right' : + placement + + $tip + .removeClass(orgPlacement) + .addClass(placement) } - if (placement == 'bottom' || placement == 'top') { - delta = 0 + var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) - if (offset.left < 0){ - delta = offset.left * -2 - offset.left = 0 - $tip.offset(offset) - actualWidth = $tip[0].offsetWidth - actualHeight = $tip[0].offsetHeight - } + this.applyPlacement(calculatedOffset, placement) + this.hoverState = null - this.replaceArrow(delta - width + actualWidth, actualWidth, 'left') - } else { - this.replaceArrow(actualHeight - height, actualHeight, 'top') - } - - if (replace) $tip.offset(offset) - } - - , replaceArrow: function(delta, dimension, position){ - this - .arrow() - .css(position, delta ? (50 * (1 - delta / dimension) + "%") : '') - } - - , setContent: function () { - var $tip = this.tip() - , title = this.getTitle() - - $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) - $tip.removeClass('fade in top bottom left right') - } - - , hide: function () { - var that = this - , $tip = this.tip() - , e = $.Event('hide') - - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - - $tip.removeClass('in') - - function removeWithAnimation() { - var timeout = setTimeout(function () { - $tip.off($.support.transition.end).detach() - }, 500) - - $tip.one($.support.transition.end, function () { - clearTimeout(timeout) - $tip.detach() - }) + var complete = function() { + that.$element.trigger('shown.bs.' + that.type) } $.support.transition && this.$tip.hasClass('fade') ? - removeWithAnimation() : - $tip.detach() - - this.$element.trigger('hidden') - - return this + $tip + .one($.support.transition.end, complete) + .emulateTransitionEnd(150) : + complete() } + } - , fixTitle: function () { - var $e = this.$element - if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { - $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') + Tooltip.prototype.applyPlacement = function (offset, placement) { + var replace + var $tip = this.tip() + var width = $tip[0].offsetWidth + var height = $tip[0].offsetHeight + + // manually read margins because getBoundingClientRect includes difference + var marginTop = parseInt($tip.css('margin-top'), 10) + var marginLeft = parseInt($tip.css('margin-left'), 10) + + // we must check for NaN for ie 8/9 + if (isNaN(marginTop)) marginTop = 0 + if (isNaN(marginLeft)) marginLeft = 0 + + offset.top = offset.top + marginTop + offset.left = offset.left + marginLeft + + // $.fn.offset doesn't round pixel values + // so we use setOffset directly with our own function B-0 + $.offset.setOffset($tip[0], $.extend({ + using: function (props) { + $tip.css({ + top: Math.round(props.top), + left: Math.round(props.left) + }) } + }, offset), 0) + + $tip.addClass('in') + + // check to see if placing tip in new offset caused the tip to resize itself + var actualWidth = $tip[0].offsetWidth + var actualHeight = $tip[0].offsetHeight + + if (placement == 'top' && actualHeight != height) { + replace = true + offset.top = offset.top + height - actualHeight } - , hasContent: function () { - return this.getTitle() - } + if (/bottom|top/.test(placement)) { + var delta = 0 - , getPosition: function () { - var el = this.$element[0] - return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : { - width: el.offsetWidth - , height: el.offsetHeight - }, this.$element.offset()) - } + if (offset.left < 0) { + delta = offset.left * -2 + offset.left = 0 - , getTitle: function () { - var title - , $e = this.$element - , o = this.options + $tip.offset(offset) - title = $e.attr('data-original-title') - || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) - - return title - } - - , tip: function () { - return this.$tip = this.$tip || $(this.options.template) - } - - , arrow: function(){ - return this.$arrow = this.$arrow || this.tip().find(".tooltip-arrow") - } - - , validate: function () { - if (!this.$element[0].parentNode) { - this.hide() - this.$element = null - this.options = null + actualWidth = $tip[0].offsetWidth + actualHeight = $tip[0].offsetHeight } + + this.replaceArrow(delta - width + actualWidth, actualWidth, 'left') + } else { + this.replaceArrow(actualHeight - height, actualHeight, 'top') } - , enable: function () { - this.enabled = true + if (replace) $tip.offset(offset) + } + + Tooltip.prototype.replaceArrow = function (delta, dimension, position) { + this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + '%') : '') + } + + Tooltip.prototype.setContent = function () { + var $tip = this.tip() + var title = this.getTitle() + + $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) + $tip.removeClass('fade in top bottom left right') + } + + Tooltip.prototype.hide = function () { + var that = this + var $tip = this.tip() + var e = $.Event('hide.bs.' + this.type) + + function complete() { + if (that.hoverState != 'in') $tip.detach() + that.$element.trigger('hidden.bs.' + that.type) } - , disable: function () { - this.enabled = false - } + this.$element.trigger(e) - , toggleEnabled: function () { - this.enabled = !this.enabled - } + if (e.isDefaultPrevented()) return - , toggle: function (e) { - var self = e ? $(e.currentTarget)[this.type](this._options).data(this.type) : this - self.tip().hasClass('in') ? self.hide() : self.show() - } + $tip.removeClass('in') - , destroy: function () { - this.hide().$element.off('.' + this.type).removeData(this.type) - } + $.support.transition && this.$tip.hasClass('fade') ? + $tip + .one($.support.transition.end, complete) + .emulateTransitionEnd(150) : + complete() + this.hoverState = null + + return this + } + + Tooltip.prototype.fixTitle = function () { + var $e = this.$element + if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { + $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') + } + } + + Tooltip.prototype.hasContent = function () { + return this.getTitle() + } + + Tooltip.prototype.getPosition = function () { + var el = this.$element[0] + return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : { + width: el.offsetWidth, + height: el.offsetHeight + }, this.$element.offset()) + } + + Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { + return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : + placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : + placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : + /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } + } + + Tooltip.prototype.getTitle = function () { + var title + var $e = this.$element + var o = this.options + + title = $e.attr('data-original-title') + || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) + + return title + } + + Tooltip.prototype.tip = function () { + return this.$tip = this.$tip || $(this.options.template) + } + + Tooltip.prototype.arrow = function () { + return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow') + } + + Tooltip.prototype.validate = function () { + if (!this.$element[0].parentNode) { + this.hide() + this.$element = null + this.options = null + } + } + + Tooltip.prototype.enable = function () { + this.enabled = true + } + + Tooltip.prototype.disable = function () { + this.enabled = false + } + + Tooltip.prototype.toggleEnabled = function () { + this.enabled = !this.enabled + } + + Tooltip.prototype.toggle = function (e) { + var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this + self.tip().hasClass('in') ? self.leave(self) : self.enter(self) + } + + Tooltip.prototype.destroy = function () { + clearTimeout(this.timeout) + this.hide().$element.off('.' + this.type).removeData('bs.' + this.type) } - /* TOOLTIP PLUGIN DEFINITION - * ========================= */ + // TOOLTIP PLUGIN DEFINITION + // ========================= var old = $.fn.tooltip - $.fn.tooltip = function ( option ) { + $.fn.tooltip = function (option) { return this.each(function () { - var $this = $(this) - , data = $this.data('tooltip') - , options = typeof option == 'object' && option - if (!data) $this.data('tooltip', (data = new Tooltip(this, options))) + var $this = $(this) + var data = $this.data('bs.tooltip') + var options = typeof option == 'object' && option + + if (!data && option == 'destroy') return + if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) if (typeof option == 'string') data[option]() }) } $.fn.tooltip.Constructor = Tooltip - $.fn.tooltip.defaults = { - animation: true - , placement: 'top' - , selector: false - , template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' - , trigger: 'hover focus' - , title: '' - , delay: 0 - , html: false - , container: false - } - - /* TOOLTIP NO CONFLICT - * =================== */ + // TOOLTIP NO CONFLICT + // =================== $.fn.tooltip.noConflict = function () { $.fn.tooltip = old return this } -}(window.jQuery); -/* =========================================================== - * bootstrap-popover.js v2.3.2 - * http://twitter.github.com/bootstrap/javascript.html#popovers - * =========================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * =========================================================== */ +}(jQuery); + +/* ======================================================================== + * Bootstrap: popover.js v3.1.1 + * http://getbootstrap.com/javascript/#popovers + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ -!function ($) { ++function ($) { + 'use strict'; - "use strict"; // jshint ;_; - - - /* POPOVER PUBLIC CLASS DEFINITION - * =============================== */ + // POPOVER PUBLIC CLASS DEFINITION + // =============================== var Popover = function (element, options) { this.init('popover', element, options) } + if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') - /* NOTE: POPOVER EXTENDS BOOTSTRAP-TOOLTIP.js - ========================================== */ - - Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype, { - - constructor: Popover - - , setContent: function () { - var $tip = this.tip() - , title = this.getTitle() - , content = this.getContent() - - $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) - $tip.find('.popover-content')[this.options.html ? 'html' : 'text'](content) - - $tip.removeClass('fade top bottom left right in') - } - - , hasContent: function () { - return this.getTitle() || this.getContent() - } - - , getContent: function () { - var content - , $e = this.$element - , o = this.options - - content = (typeof o.content == 'function' ? o.content.call($e[0]) : o.content) - || $e.attr('data-content') - - return content - } - - , tip: function () { - if (!this.$tip) { - this.$tip = $(this.options.template) - } - return this.$tip - } - - , destroy: function () { - this.hide().$element.off('.' + this.type).removeData(this.type) - } - + Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { + placement: 'right', + trigger: 'click', + content: '', + template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>' }) - /* POPOVER PLUGIN DEFINITION - * ======================= */ + // NOTE: POPOVER EXTENDS tooltip.js + // ================================ + + Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) + + Popover.prototype.constructor = Popover + + Popover.prototype.getDefaults = function () { + return Popover.DEFAULTS + } + + Popover.prototype.setContent = function () { + var $tip = this.tip() + var title = this.getTitle() + var content = this.getContent() + + $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) + $tip.find('.popover-content')[ // we use append for html objects to maintain js events + this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' + ](content) + + $tip.removeClass('fade top bottom left right in') + + // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do + // this manually by checking the contents. + if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() + } + + Popover.prototype.hasContent = function () { + return this.getTitle() || this.getContent() + } + + Popover.prototype.getContent = function () { + var $e = this.$element + var o = this.options + + return $e.attr('data-content') + || (typeof o.content == 'function' ? + o.content.call($e[0]) : + o.content) + } + + Popover.prototype.arrow = function () { + return this.$arrow = this.$arrow || this.tip().find('.arrow') + } + + Popover.prototype.tip = function () { + if (!this.$tip) this.$tip = $(this.options.template) + return this.$tip + } + + + // POPOVER PLUGIN DEFINITION + // ========================= var old = $.fn.popover $.fn.popover = function (option) { return this.each(function () { - var $this = $(this) - , data = $this.data('popover') - , options = typeof option == 'object' && option - if (!data) $this.data('popover', (data = new Popover(this, options))) + var $this = $(this) + var data = $this.data('bs.popover') + var options = typeof option == 'object' && option + + if (!data && option == 'destroy') return + if (!data) $this.data('bs.popover', (data = new Popover(this, options))) if (typeof option == 'string') data[option]() }) } $.fn.popover.Constructor = Popover - $.fn.popover.defaults = $.extend({} , $.fn.tooltip.defaults, { - placement: 'right' - , trigger: 'click' - , content: '' - , template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>' - }) - - /* POPOVER NO CONFLICT - * =================== */ + // POPOVER NO CONFLICT + // =================== $.fn.popover.noConflict = function () { $.fn.popover = old return this } -}(window.jQuery); -/* ============================================================= - * bootstrap-scrollspy.js v2.3.2 - * http://twitter.github.com/bootstrap/javascript.html#scrollspy - * ============================================================= - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================== */ +}(jQuery); + +/* ======================================================================== + * Bootstrap: scrollspy.js v3.1.1 + * http://getbootstrap.com/javascript/#scrollspy + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ -!function ($) { ++function ($) { + 'use strict'; - "use strict"; // jshint ;_; - - - /* SCROLLSPY CLASS DEFINITION - * ========================== */ + // SCROLLSPY CLASS DEFINITION + // ========================== function ScrollSpy(element, options) { - var process = $.proxy(this.process, this) - , $element = $(element).is('body') ? $(window) : $(element) - , href - this.options = $.extend({}, $.fn.scrollspy.defaults, options) - this.$scrollElement = $element.on('scroll.scroll-spy.data-api', process) - this.selector = (this.options.target + var href + var process = $.proxy(this.process, this) + + this.$element = $(element).is('body') ? $(window) : $(element) + this.$body = $('body') + this.$scrollElement = this.$element.on('scroll.bs.scroll-spy.data-api', process) + this.options = $.extend({}, ScrollSpy.DEFAULTS, options) + this.selector = (this.options.target || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 || '') + ' .nav li > a' - this.$body = $('body') + this.offsets = $([]) + this.targets = $([]) + this.activeTarget = null + this.refresh() this.process() } - ScrollSpy.prototype = { + ScrollSpy.DEFAULTS = { + offset: 10 + } - constructor: ScrollSpy + ScrollSpy.prototype.refresh = function () { + var offsetMethod = this.$element[0] == window ? 'offset' : 'position' - , refresh: function () { - var self = this - , $targets + this.offsets = $([]) + this.targets = $([]) - this.offsets = $([]) - this.targets = $([]) + var self = this + var $targets = this.$body + .find(this.selector) + .map(function () { + var $el = $(this) + var href = $el.data('target') || $el.attr('href') + var $href = /^#./.test(href) && $(href) - $targets = this.$body - .find(this.selector) - .map(function () { - var $el = $(this) - , href = $el.data('target') || $el.attr('href') - , $href = /^#\w/.test(href) && $(href) - return ( $href - && $href.length - && [[ $href.position().top + (!$.isWindow(self.$scrollElement.get(0)) && self.$scrollElement.scrollTop()), href ]] ) || null - }) - .sort(function (a, b) { return a[0] - b[0] }) - .each(function () { - self.offsets.push(this[0]) - self.targets.push(this[1]) - }) - } + return ($href + && $href.length + && $href.is(':visible') + && [[ $href[offsetMethod]().top + (!$.isWindow(self.$scrollElement.get(0)) && self.$scrollElement.scrollTop()), href ]]) || null + }) + .sort(function (a, b) { return a[0] - b[0] }) + .each(function () { + self.offsets.push(this[0]) + self.targets.push(this[1]) + }) + } - , process: function () { - var scrollTop = this.$scrollElement.scrollTop() + this.options.offset - , scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight - , maxScroll = scrollHeight - this.$scrollElement.height() - , offsets = this.offsets - , targets = this.targets - , activeTarget = this.activeTarget - , i + ScrollSpy.prototype.process = function () { + var scrollTop = this.$scrollElement.scrollTop() + this.options.offset + var scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight + var maxScroll = scrollHeight - this.$scrollElement.height() + var offsets = this.offsets + var targets = this.targets + var activeTarget = this.activeTarget + var i - if (scrollTop >= maxScroll) { - return activeTarget != (i = targets.last()[0]) - && this.activate ( i ) - } + if (scrollTop >= maxScroll) { + return activeTarget != (i = targets.last()[0]) && this.activate(i) + } - for (i = offsets.length; i--;) { - activeTarget != targets[i] - && scrollTop >= offsets[i] - && (!offsets[i + 1] || scrollTop <= offsets[i + 1]) - && this.activate( targets[i] ) - } - } + if (activeTarget && scrollTop <= offsets[0]) { + return activeTarget != (i = targets[0]) && this.activate(i) + } - , activate: function (target) { - var active - , selector + for (i = offsets.length; i--;) { + activeTarget != targets[i] + && scrollTop >= offsets[i] + && (!offsets[i + 1] || scrollTop <= offsets[i + 1]) + && this.activate( targets[i] ) + } + } - this.activeTarget = target + ScrollSpy.prototype.activate = function (target) { + this.activeTarget = target - $(this.selector) - .parent('.active') - .removeClass('active') + $(this.selector) + .parentsUntil(this.options.target, '.active') + .removeClass('active') - selector = this.selector - + '[data-target="' + target + '"],' - + this.selector + '[href="' + target + '"]' + var selector = this.selector + + '[data-target="' + target + '"],' + + this.selector + '[href="' + target + '"]' - active = $(selector) - .parent('li') - .addClass('active') + var active = $(selector) + .parents('li') + .addClass('active') - if (active.parent('.dropdown-menu').length) { - active = active.closest('li.dropdown').addClass('active') - } - - active.trigger('activate') - } + if (active.parent('.dropdown-menu').length) { + active = active + .closest('li.dropdown') + .addClass('active') + } + active.trigger('activate.bs.scrollspy') } - /* SCROLLSPY PLUGIN DEFINITION - * =========================== */ + // SCROLLSPY PLUGIN DEFINITION + // =========================== var old = $.fn.scrollspy $.fn.scrollspy = function (option) { return this.each(function () { - var $this = $(this) - , data = $this.data('scrollspy') - , options = typeof option == 'object' && option - if (!data) $this.data('scrollspy', (data = new ScrollSpy(this, options))) + var $this = $(this) + var data = $this.data('bs.scrollspy') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) if (typeof option == 'string') data[option]() }) } $.fn.scrollspy.Constructor = ScrollSpy - $.fn.scrollspy.defaults = { - offset: 10 - } - - /* SCROLLSPY NO CONFLICT - * ===================== */ + // SCROLLSPY NO CONFLICT + // ===================== $.fn.scrollspy.noConflict = function () { $.fn.scrollspy = old @@ -1673,8 +1674,8 @@ } - /* SCROLLSPY DATA-API - * ================== */ + // SCROLLSPY DATA-API + // ================== $(window).on('load', function () { $('[data-spy="scroll"]').each(function () { @@ -1683,125 +1684,108 @@ }) }) -}(window.jQuery);/* ======================================================== - * bootstrap-tab.js v2.3.2 - * http://twitter.github.com/bootstrap/javascript.html#tabs - * ======================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================== */ +}(jQuery); + +/* ======================================================================== + * Bootstrap: tab.js v3.1.1 + * http://getbootstrap.com/javascript/#tabs + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ -!function ($) { ++function ($) { + 'use strict'; - "use strict"; // jshint ;_; - - - /* TAB CLASS DEFINITION - * ==================== */ + // TAB CLASS DEFINITION + // ==================== var Tab = function (element) { this.element = $(element) } - Tab.prototype = { + Tab.prototype.show = function () { + var $this = this.element + var $ul = $this.closest('ul:not(.dropdown-menu)') + var selector = $this.data('target') - constructor: Tab + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } - , show: function () { - var $this = this.element - , $ul = $this.closest('ul:not(.dropdown-menu)') - , selector = $this.attr('data-target') - , previous - , $target - , e + if ($this.parent('li').hasClass('active')) return - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 - } + var previous = $ul.find('.active:last a')[0] + var e = $.Event('show.bs.tab', { + relatedTarget: previous + }) - if ( $this.parent('li').hasClass('active') ) return + $this.trigger(e) - previous = $ul.find('.active:last a')[0] + if (e.isDefaultPrevented()) return - e = $.Event('show', { + var $target = $(selector) + + this.activate($this.parent('li'), $ul) + this.activate($target, $target.parent(), function () { + $this.trigger({ + type: 'shown.bs.tab', relatedTarget: previous }) + }) + } - $this.trigger(e) + Tab.prototype.activate = function (element, container, callback) { + var $active = container.find('> .active') + var transition = callback + && $.support.transition + && $active.hasClass('fade') - if (e.isDefaultPrevented()) return + function next() { + $active + .removeClass('active') + .find('> .dropdown-menu > .active') + .removeClass('active') - $target = $(selector) + element.addClass('active') - this.activate($this.parent('li'), $ul) - this.activate($target, $target.parent(), function () { - $this.trigger({ - type: 'shown' - , relatedTarget: previous - }) - }) - } - - , activate: function ( element, container, callback) { - var $active = container.find('> .active') - , transition = callback - && $.support.transition - && $active.hasClass('fade') - - function next() { - $active - .removeClass('active') - .find('> .dropdown-menu > .active') - .removeClass('active') - - element.addClass('active') - - if (transition) { - element[0].offsetWidth // reflow for transition - element.addClass('in') - } else { - element.removeClass('fade') - } - - if ( element.parent('.dropdown-menu') ) { - element.closest('li.dropdown').addClass('active') - } - - callback && callback() + if (transition) { + element[0].offsetWidth // reflow for transition + element.addClass('in') + } else { + element.removeClass('fade') } - transition ? - $active.one($.support.transition.end, next) : - next() + if (element.parent('.dropdown-menu')) { + element.closest('li.dropdown').addClass('active') + } - $active.removeClass('in') + callback && callback() } + + transition ? + $active + .one($.support.transition.end, next) + .emulateTransitionEnd(150) : + next() + + $active.removeClass('in') } - /* TAB PLUGIN DEFINITION - * ===================== */ + // TAB PLUGIN DEFINITION + // ===================== var old = $.fn.tab $.fn.tab = function ( option ) { return this.each(function () { var $this = $(this) - , data = $this.data('tab') - if (!data) $this.data('tab', (data = new Tab(this))) + var data = $this.data('bs.tab') + + if (!data) $this.data('bs.tab', (data = new Tab(this))) if (typeof option == 'string') data[option]() }) } @@ -1809,8 +1793,8 @@ $.fn.tab.Constructor = Tab - /* TAB NO CONFLICT - * =============== */ + // TAB NO CONFLICT + // =============== $.fn.tab.noConflict = function () { $.fn.tab = old @@ -1818,440 +1802,128 @@ } - /* TAB DATA-API - * ============ */ + // TAB DATA-API + // ============ - $(document).on('click.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) { + $(document).on('click.bs.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) { e.preventDefault() $(this).tab('show') }) -}(window.jQuery);/* ============================================================= - * bootstrap-typeahead.js v2.3.2 - * http://twitter.github.com/bootstrap/javascript.html#typeahead - * ============================================================= - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ +}(jQuery); +/* ======================================================================== + * Bootstrap: affix.js v3.1.1 + * http://getbootstrap.com/javascript/#affix + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ -!function($){ - "use strict"; // jshint ;_; ++function ($) { + 'use strict'; - - /* TYPEAHEAD PUBLIC CLASS DEFINITION - * ================================= */ - - var Typeahead = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, $.fn.typeahead.defaults, options) - this.matcher = this.options.matcher || this.matcher - this.sorter = this.options.sorter || this.sorter - this.highlighter = this.options.highlighter || this.highlighter - this.updater = this.options.updater || this.updater - this.source = this.options.source - this.$menu = $(this.options.menu) - this.shown = false - this.listen() - } - - Typeahead.prototype = { - - constructor: Typeahead - - , select: function () { - var val = this.$menu.find('.active').attr('data-value') - this.$element - .val(this.updater(val)) - .change() - return this.hide() - } - - , updater: function (item) { - return item - } - - , show: function () { - var pos = $.extend({}, this.$element.position(), { - height: this.$element[0].offsetHeight - }) - - this.$menu - .insertAfter(this.$element) - .css({ - top: pos.top + pos.height - , left: pos.left - }) - .show() - - this.shown = true - return this - } - - , hide: function () { - this.$menu.hide() - this.shown = false - return this - } - - , lookup: function (event) { - var items - - this.query = this.$element.val() - - if (!this.query || this.query.length < this.options.minLength) { - return this.shown ? this.hide() : this - } - - items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source - - return items ? this.process(items) : this - } - - , process: function (items) { - var that = this - - items = $.grep(items, function (item) { - return that.matcher(item) - }) - - items = this.sorter(items) - - if (!items.length) { - return this.shown ? this.hide() : this - } - - return this.render(items.slice(0, this.options.items)).show() - } - - , matcher: function (item) { - return ~item.toLowerCase().indexOf(this.query.toLowerCase()) - } - - , sorter: function (items) { - var beginswith = [] - , caseSensitive = [] - , caseInsensitive = [] - , item - - while (item = items.shift()) { - if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item) - else if (~item.indexOf(this.query)) caseSensitive.push(item) - else caseInsensitive.push(item) - } - - return beginswith.concat(caseSensitive, caseInsensitive) - } - - , highlighter: function (item) { - var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&') - return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) { - return '<strong>' + match + '</strong>' - }) - } - - , render: function (items) { - var that = this - - items = $(items).map(function (i, item) { - i = $(that.options.item).attr('data-value', item) - i.find('a').html(that.highlighter(item)) - return i[0] - }) - - items.first().addClass('active') - this.$menu.html(items) - return this - } - - , next: function (event) { - var active = this.$menu.find('.active').removeClass('active') - , next = active.next() - - if (!next.length) { - next = $(this.$menu.find('li')[0]) - } - - next.addClass('active') - } - - , prev: function (event) { - var active = this.$menu.find('.active').removeClass('active') - , prev = active.prev() - - if (!prev.length) { - prev = this.$menu.find('li').last() - } - - prev.addClass('active') - } - - , listen: function () { - this.$element - .on('focus', $.proxy(this.focus, this)) - .on('blur', $.proxy(this.blur, this)) - .on('keypress', $.proxy(this.keypress, this)) - .on('keyup', $.proxy(this.keyup, this)) - - if (this.eventSupported('keydown')) { - this.$element.on('keydown', $.proxy(this.keydown, this)) - } - - this.$menu - .on('click', $.proxy(this.click, this)) - .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) - .on('mouseleave', 'li', $.proxy(this.mouseleave, this)) - } - - , eventSupported: function(eventName) { - var isSupported = eventName in this.$element - if (!isSupported) { - this.$element.setAttribute(eventName, 'return;') - isSupported = typeof this.$element[eventName] === 'function' - } - return isSupported - } - - , move: function (e) { - if (!this.shown) return - - switch(e.keyCode) { - case 9: // tab - case 13: // enter - case 27: // escape - e.preventDefault() - break - - case 38: // up arrow - e.preventDefault() - this.prev() - break - - case 40: // down arrow - e.preventDefault() - this.next() - break - } - - e.stopPropagation() - } - - , keydown: function (e) { - this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27]) - this.move(e) - } - - , keypress: function (e) { - if (this.suppressKeyPressRepeat) return - this.move(e) - } - - , keyup: function (e) { - switch(e.keyCode) { - case 40: // down arrow - case 38: // up arrow - case 16: // shift - case 17: // ctrl - case 18: // alt - break - - case 9: // tab - case 13: // enter - if (!this.shown) return - this.select() - break - - case 27: // escape - if (!this.shown) return - this.hide() - break - - default: - this.lookup() - } - - e.stopPropagation() - e.preventDefault() - } - - , focus: function (e) { - this.focused = true - } - - , blur: function (e) { - this.focused = false - if (!this.mousedover && this.shown) this.hide() - } - - , click: function (e) { - e.stopPropagation() - e.preventDefault() - this.select() - this.$element.focus() - } - - , mouseenter: function (e) { - this.mousedover = true - this.$menu.find('.active').removeClass('active') - $(e.currentTarget).addClass('active') - } - - , mouseleave: function (e) { - this.mousedover = false - if (!this.focused && this.shown) this.hide() - } - - } - - - /* TYPEAHEAD PLUGIN DEFINITION - * =========================== */ - - var old = $.fn.typeahead - - $.fn.typeahead = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('typeahead') - , options = typeof option == 'object' && option - if (!data) $this.data('typeahead', (data = new Typeahead(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.typeahead.defaults = { - source: [] - , items: 8 - , menu: '<ul class="typeahead dropdown-menu"></ul>' - , item: '<li><a href="#"></a></li>' - , minLength: 1 - } - - $.fn.typeahead.Constructor = Typeahead - - - /* TYPEAHEAD NO CONFLICT - * =================== */ - - $.fn.typeahead.noConflict = function () { - $.fn.typeahead = old - return this - } - - - /* TYPEAHEAD DATA-API - * ================== */ - - $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { - var $this = $(this) - if ($this.data('typeahead')) return - $this.typeahead($this.data()) - }) - -}(window.jQuery); -/* ========================================================== - * bootstrap-affix.js v2.3.2 - * http://twitter.github.com/bootstrap/javascript.html#affix - * ========================================================== - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* AFFIX CLASS DEFINITION - * ====================== */ + // AFFIX CLASS DEFINITION + // ====================== var Affix = function (element, options) { - this.options = $.extend({}, $.fn.affix.defaults, options) + this.options = $.extend({}, Affix.DEFAULTS, options) this.$window = $(window) - .on('scroll.affix.data-api', $.proxy(this.checkPosition, this)) - .on('click.affix.data-api', $.proxy(function () { setTimeout($.proxy(this.checkPosition, this), 1) }, this)) - this.$element = $(element) + .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) + .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) + + this.$element = $(element) + this.affixed = + this.unpin = + this.pinnedOffset = null + this.checkPosition() } + Affix.RESET = 'affix affix-top affix-bottom' + + Affix.DEFAULTS = { + offset: 0 + } + + Affix.prototype.getPinnedOffset = function () { + if (this.pinnedOffset) return this.pinnedOffset + this.$element.removeClass(Affix.RESET).addClass('affix') + var scrollTop = this.$window.scrollTop() + var position = this.$element.offset() + return (this.pinnedOffset = position.top - scrollTop) + } + + Affix.prototype.checkPositionWithEventLoop = function () { + setTimeout($.proxy(this.checkPosition, this), 1) + } + Affix.prototype.checkPosition = function () { if (!this.$element.is(':visible')) return var scrollHeight = $(document).height() - , scrollTop = this.$window.scrollTop() - , position = this.$element.offset() - , offset = this.options.offset - , offsetBottom = offset.bottom - , offsetTop = offset.top - , reset = 'affix affix-top affix-bottom' - , affix + var scrollTop = this.$window.scrollTop() + var position = this.$element.offset() + var offset = this.options.offset + var offsetTop = offset.top + var offsetBottom = offset.bottom - if (typeof offset != 'object') offsetBottom = offsetTop = offset - if (typeof offsetTop == 'function') offsetTop = offset.top() - if (typeof offsetBottom == 'function') offsetBottom = offset.bottom() + if (this.affixed == 'top') position.top += scrollTop - affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? - false : offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? - 'bottom' : offsetTop != null && scrollTop <= offsetTop ? - 'top' : false + if (typeof offset != 'object') offsetBottom = offsetTop = offset + if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) + if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) + + var affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? false : + offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? 'bottom' : + offsetTop != null && (scrollTop <= offsetTop) ? 'top' : false if (this.affixed === affix) return + if (this.unpin) this.$element.css('top', '') + + var affixType = 'affix' + (affix ? '-' + affix : '') + var e = $.Event(affixType + '.bs.affix') + + this.$element.trigger(e) + + if (e.isDefaultPrevented()) return this.affixed = affix - this.unpin = affix == 'bottom' ? position.top - scrollTop : null + this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null - this.$element.removeClass(reset).addClass('affix' + (affix ? '-' + affix : '')) + this.$element + .removeClass(Affix.RESET) + .addClass(affixType) + .trigger($.Event(affixType.replace('affix', 'affixed'))) + + if (affix == 'bottom') { + this.$element.offset({ top: scrollHeight - offsetBottom - this.$element.height() }) + } } - /* AFFIX PLUGIN DEFINITION - * ======================= */ + // AFFIX PLUGIN DEFINITION + // ======================= var old = $.fn.affix $.fn.affix = function (option) { return this.each(function () { - var $this = $(this) - , data = $this.data('affix') - , options = typeof option == 'object' && option - if (!data) $this.data('affix', (data = new Affix(this, options))) + var $this = $(this) + var data = $this.data('bs.affix') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.affix', (data = new Affix(this, options))) if (typeof option == 'string') data[option]() }) } $.fn.affix.Constructor = Affix - $.fn.affix.defaults = { - offset: 0 - } - - /* AFFIX NO CONFLICT - * ================= */ + // AFFIX NO CONFLICT + // ================= $.fn.affix.noConflict = function () { $.fn.affix = old @@ -2259,22 +1931,21 @@ } - /* AFFIX DATA-API - * ============== */ + // AFFIX DATA-API + // ============== $(window).on('load', function () { $('[data-spy="affix"]').each(function () { var $spy = $(this) - , data = $spy.data() + var data = $spy.data() data.offset = data.offset || {} - data.offsetBottom && (data.offset.bottom = data.offsetBottom) - data.offsetTop && (data.offset.top = data.offsetTop) + if (data.offsetBottom) data.offset.bottom = data.offsetBottom + if (data.offsetTop) data.offset.top = data.offsetTop $spy.affix(data) }) }) - -}(window.jQuery); \ No newline at end of file +}(jQuery); diff --git a/src/UI/JsLibraries/fullcalendar.js b/src/UI/JsLibraries/fullcalendar.js index 41c50856c..28f2a04c9 100644 --- a/src/UI/JsLibraries/fullcalendar.js +++ b/src/UI/JsLibraries/fullcalendar.js @@ -2441,7 +2441,7 @@ function BasicView(element, calendar, viewName) { } colWidth = Math.floor((viewWidth - weekNumberWidth) / colCnt); - setOuterWidth(headCells.slice(0, -1), colWidth); + setOuterWidth(headCells, colWidth); } diff --git a/src/UI/JsLibraries/messenger.js b/src/UI/JsLibraries/messenger.js index 65da2f03d..8acdbcff3 100644 --- a/src/UI/JsLibraries/messenger.js +++ b/src/UI/JsLibraries/messenger.js @@ -1,4 +1,4 @@ -/*! messenger 1.3.6 */ +/*! messenger 1.4.1 */ /* * This file begins the output concatenated into messenger.js * @@ -331,11 +331,11 @@ window.Messenger.Events = (function() { BaseView.prototype.delegateEvents = function(events) { var delegateEventSplitter, eventName, key, match, method, selector, _results; - if (!(events || (events = _.result(this, 'events')))) { + if (!(events || (events = _.result(this, "events")))) { return; } - delegateEventSplitter = /^(\S+)\s*(.*)$/; this.undelegateEvents(); + delegateEventSplitter = /^(\S+)\s*(.*)$/; _results = []; for (key in events) { method = events[key]; @@ -343,7 +343,7 @@ window.Messenger.Events = (function() { method = this[events[key]]; } if (!method) { - throw new Error("Method " + events[key] + " does not exist"); + throw new Error("Method \"" + events[key] + "\" does not exist"); } match = key.match(delegateEventSplitter); eventName = match[1]; @@ -409,7 +409,8 @@ window.Messenger.Events = (function() { _Message.prototype.defaults = { hideAfter: 10, - scroll: true + scroll: true, + closeButtonText: "×" }; _Message.prototype.initialize = function(opts) { @@ -574,7 +575,8 @@ window.Messenger.Events = (function() { _this = this; $message = $("<div class='messenger-message message alert " + opts.type + " message-" + opts.type + " alert-" + opts.type + "'>"); if (opts.showCloseButton) { - $cancel = $('<button type="button" class="close" data-dismiss="alert">×</button>'); + $cancel = $('<button type="button" class="messenger-close" data-dismiss="alert">'); + $cancel.html(opts.closeButtonText); $cancel.click(function() { _this.cancel(); return true; @@ -992,7 +994,7 @@ window.Messenger.Events = (function() { }; ActionMessenger.prototype.run = function() { - var args, attr, events, getMessageText, handler, handlers, m_opts, msg, old, opts, promiseAttrs, type, _i, _len, _ref2, _ref3, + var args, events, getMessageText, handler, handlers, m_opts, msg, old, opts, type, _ref2, _this = this; m_opts = arguments[0], opts = arguments[1], args = 3 <= arguments.length ? __slice.call(arguments, 2) : []; if (opts == null) { @@ -1101,7 +1103,9 @@ window.Messenger.Events = (function() { } msg.update(msgOpts); if (responseOpts && msgOpts.message) { - Messenger(); + Messenger(_.extend({}, _this.options, { + instance: _this + })); return msg.show(); } else { return msg.hide(); @@ -1119,14 +1123,6 @@ window.Messenger.Events = (function() { if (m_opts.returnsPromise) { msg._actionInstance.then(handlers.success, handlers.error); } - promiseAttrs = ['done', 'progress', 'fail', 'state', 'then']; - for (_i = 0, _len = promiseAttrs.length; _i < _len; _i++) { - attr = promiseAttrs[_i]; - if (msg[attr] != null) { - delete msg[attr]; - } - msg[attr] = (_ref3 = msg._actionInstance) != null ? _ref3[attr] : void 0; - } return msg; }; @@ -1147,6 +1143,45 @@ window.Messenger.Events = (function() { return this.run(m_opts); }; + ActionMessenger.prototype.error = function(m_opts) { + if (m_opts == null) { + m_opts = {}; + } + if (typeof m_opts === 'string') { + m_opts = { + message: m_opts + }; + } + m_opts.type = 'error'; + return this.post(m_opts); + }; + + ActionMessenger.prototype.info = function(m_opts) { + if (m_opts == null) { + m_opts = {}; + } + if (typeof m_opts === 'string') { + m_opts = { + message: m_opts + }; + } + m_opts.type = 'info'; + return this.post(m_opts); + }; + + ActionMessenger.prototype.success = function(m_opts) { + if (m_opts == null) { + m_opts = {}; + } + if (typeof m_opts === 'string') { + m_opts = { + message: m_opts + }; + } + m_opts.type = 'success'; + return this.post(m_opts); + }; + return ActionMessenger; })(_Messenger); @@ -1204,7 +1239,7 @@ window.Messenger.Events = (function() { inst = $el.messenger(opts); inst._location = chosen_loc; Messenger.instance = inst; - } else if ($(inst._location) !== $(chosen_loc)) { + } else if (!$(inst._location).is($(chosen_loc))) { inst.$el.detach(); $parent.prepend(inst.$el); } diff --git a/src/UI/JsLibraries/typeahead.js b/src/UI/JsLibraries/typeahead.js new file mode 100644 index 000000000..450a6ca43 --- /dev/null +++ b/src/UI/JsLibraries/typeahead.js @@ -0,0 +1,1716 @@ +/*! + * typeahead.js 0.10.2 + * https://github.com/twitter/typeahead.js + * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT + */ + +(function($) { + var _ = { + isMsie: function() { + return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; + }, + isBlankString: function(str) { + return !str || /^\s*$/.test(str); + }, + escapeRegExChars: function(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + }, + isString: function(obj) { + return typeof obj === "string"; + }, + isNumber: function(obj) { + return typeof obj === "number"; + }, + isArray: $.isArray, + isFunction: $.isFunction, + isObject: $.isPlainObject, + isUndefined: function(obj) { + return typeof obj === "undefined"; + }, + bind: $.proxy, + each: function(collection, cb) { + $.each(collection, reverseArgs); + function reverseArgs(index, value) { + return cb(value, index); + } + }, + map: $.map, + filter: $.grep, + every: function(obj, test) { + var result = true; + if (!obj) { + return result; + } + $.each(obj, function(key, val) { + if (!(result = test.call(null, val, key, obj))) { + return false; + } + }); + return !!result; + }, + some: function(obj, test) { + var result = false; + if (!obj) { + return result; + } + $.each(obj, function(key, val) { + if (result = test.call(null, val, key, obj)) { + return false; + } + }); + return !!result; + }, + mixin: $.extend, + getUniqueId: function() { + var counter = 0; + return function() { + return counter++; + }; + }(), + templatify: function templatify(obj) { + return $.isFunction(obj) ? obj : template; + function template() { + return String(obj); + } + }, + defer: function(fn) { + setTimeout(fn, 0); + }, + debounce: function(func, wait, immediate) { + var timeout, result; + return function() { + var context = this, args = arguments, later, callNow; + later = function() { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + } + }; + callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + } + return result; + }; + }, + throttle: function(func, wait) { + var context, args, timeout, result, previous, later; + previous = 0; + later = function() { + previous = new Date(); + timeout = null; + result = func.apply(context, args); + }; + return function() { + var now = new Date(), remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + } else if (!timeout) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }, + noop: function() {} + }; + var VERSION = "0.10.2"; + var tokenizers = function(root) { + return { + nonword: nonword, + whitespace: whitespace, + obj: { + nonword: getObjTokenizer(nonword), + whitespace: getObjTokenizer(whitespace) + } + }; + function whitespace(s) { + return s.split(/\s+/); + } + function nonword(s) { + return s.split(/\W+/); + } + function getObjTokenizer(tokenizer) { + return function setKey(key) { + return function tokenize(o) { + return tokenizer(o[key]); + }; + }; + } + }(); + var LruCache = function() { + function LruCache(maxSize) { + this.maxSize = maxSize || 100; + this.size = 0; + this.hash = {}; + this.list = new List(); + } + _.mixin(LruCache.prototype, { + set: function set(key, val) { + var tailItem = this.list.tail, node; + if (this.size >= this.maxSize) { + this.list.remove(tailItem); + delete this.hash[tailItem.key]; + } + if (node = this.hash[key]) { + node.val = val; + this.list.moveToFront(node); + } else { + node = new Node(key, val); + this.list.add(node); + this.hash[key] = node; + this.size++; + } + }, + get: function get(key) { + var node = this.hash[key]; + if (node) { + this.list.moveToFront(node); + return node.val; + } + } + }); + function List() { + this.head = this.tail = null; + } + _.mixin(List.prototype, { + add: function add(node) { + if (this.head) { + node.next = this.head; + this.head.prev = node; + } + this.head = node; + this.tail = this.tail || node; + }, + remove: function remove(node) { + node.prev ? node.prev.next = node.next : this.head = node.next; + node.next ? node.next.prev = node.prev : this.tail = node.prev; + }, + moveToFront: function(node) { + this.remove(node); + this.add(node); + } + }); + function Node(key, val) { + this.key = key; + this.val = val; + this.prev = this.next = null; + } + return LruCache; + }(); + var PersistentStorage = function() { + var ls, methods; + try { + ls = window.localStorage; + ls.setItem("~~~", "!"); + ls.removeItem("~~~"); + } catch (err) { + ls = null; + } + function PersistentStorage(namespace) { + this.prefix = [ "__", namespace, "__" ].join(""); + this.ttlKey = "__ttl__"; + this.keyMatcher = new RegExp("^" + this.prefix); + } + if (ls && window.JSON) { + methods = { + _prefix: function(key) { + return this.prefix + key; + }, + _ttlKey: function(key) { + return this._prefix(key) + this.ttlKey; + }, + get: function(key) { + if (this.isExpired(key)) { + this.remove(key); + } + return decode(ls.getItem(this._prefix(key))); + }, + set: function(key, val, ttl) { + if (_.isNumber(ttl)) { + ls.setItem(this._ttlKey(key), encode(now() + ttl)); + } else { + ls.removeItem(this._ttlKey(key)); + } + return ls.setItem(this._prefix(key), encode(val)); + }, + remove: function(key) { + ls.removeItem(this._ttlKey(key)); + ls.removeItem(this._prefix(key)); + return this; + }, + clear: function() { + var i, key, keys = [], len = ls.length; + for (i = 0; i < len; i++) { + if ((key = ls.key(i)).match(this.keyMatcher)) { + keys.push(key.replace(this.keyMatcher, "")); + } + } + for (i = keys.length; i--; ) { + this.remove(keys[i]); + } + return this; + }, + isExpired: function(key) { + var ttl = decode(ls.getItem(this._ttlKey(key))); + return _.isNumber(ttl) && now() > ttl ? true : false; + } + }; + } else { + methods = { + get: _.noop, + set: _.noop, + remove: _.noop, + clear: _.noop, + isExpired: _.noop + }; + } + _.mixin(PersistentStorage.prototype, methods); + return PersistentStorage; + function now() { + return new Date().getTime(); + } + function encode(val) { + return JSON.stringify(_.isUndefined(val) ? null : val); + } + function decode(val) { + return JSON.parse(val); + } + }(); + var Transport = function() { + var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, requestCache = new LruCache(10); + function Transport(o) { + o = o || {}; + this._send = o.transport ? callbackToDeferred(o.transport) : $.ajax; + this._get = o.rateLimiter ? o.rateLimiter(this._get) : this._get; + } + Transport.setMaxPendingRequests = function setMaxPendingRequests(num) { + maxPendingRequests = num; + }; + Transport.resetCache = function clearCache() { + requestCache = new LruCache(10); + }; + _.mixin(Transport.prototype, { + _get: function(url, o, cb) { + var that = this, jqXhr; + if (jqXhr = pendingRequests[url]) { + jqXhr.done(done).fail(fail); + } else if (pendingRequestsCount < maxPendingRequests) { + pendingRequestsCount++; + pendingRequests[url] = this._send(url, o).done(done).fail(fail).always(always); + } else { + this.onDeckRequestArgs = [].slice.call(arguments, 0); + } + function done(resp) { + cb && cb(null, resp); + requestCache.set(url, resp); + } + function fail() { + cb && cb(true); + } + function always() { + pendingRequestsCount--; + delete pendingRequests[url]; + if (that.onDeckRequestArgs) { + that._get.apply(that, that.onDeckRequestArgs); + that.onDeckRequestArgs = null; + } + } + }, + get: function(url, o, cb) { + var resp; + if (_.isFunction(o)) { + cb = o; + o = {}; + } + if (resp = requestCache.get(url)) { + _.defer(function() { + cb && cb(null, resp); + }); + } else { + this._get(url, o, cb); + } + return !!resp; + } + }); + return Transport; + function callbackToDeferred(fn) { + return function customSendWrapper(url, o) { + var deferred = $.Deferred(); + fn(url, o, onSuccess, onError); + return deferred; + function onSuccess(resp) { + _.defer(function() { + deferred.resolve(resp); + }); + } + function onError(err) { + _.defer(function() { + deferred.reject(err); + }); + } + }; + } + }(); + var SearchIndex = function() { + function SearchIndex(o) { + o = o || {}; + if (!o.datumTokenizer || !o.queryTokenizer) { + $.error("datumTokenizer and queryTokenizer are both required"); + } + this.datumTokenizer = o.datumTokenizer; + this.queryTokenizer = o.queryTokenizer; + this.reset(); + } + _.mixin(SearchIndex.prototype, { + bootstrap: function bootstrap(o) { + this.datums = o.datums; + this.trie = o.trie; + }, + add: function(data) { + var that = this; + data = _.isArray(data) ? data : [ data ]; + _.each(data, function(datum) { + var id, tokens; + id = that.datums.push(datum) - 1; + tokens = normalizeTokens(that.datumTokenizer(datum)); + _.each(tokens, function(token) { + var node, chars, ch; + node = that.trie; + chars = token.split(""); + while (ch = chars.shift()) { + node = node.children[ch] || (node.children[ch] = newNode()); + node.ids.push(id); + } + }); + }); + }, + get: function get(query) { + var that = this, tokens, matches; + tokens = normalizeTokens(this.queryTokenizer(query)); + _.each(tokens, function(token) { + var node, chars, ch, ids; + if (matches && matches.length === 0) { + return false; + } + node = that.trie; + chars = token.split(""); + while (node && (ch = chars.shift())) { + node = node.children[ch]; + } + if (node && chars.length === 0) { + ids = node.ids.slice(0); + matches = matches ? getIntersection(matches, ids) : ids; + } else { + matches = []; + return false; + } + }); + return matches ? _.map(unique(matches), function(id) { + return that.datums[id]; + }) : []; + }, + reset: function reset() { + this.datums = []; + this.trie = newNode(); + }, + serialize: function serialize() { + return { + datums: this.datums, + trie: this.trie + }; + } + }); + return SearchIndex; + function normalizeTokens(tokens) { + tokens = _.filter(tokens, function(token) { + return !!token; + }); + tokens = _.map(tokens, function(token) { + return token.toLowerCase(); + }); + return tokens; + } + function newNode() { + return { + ids: [], + children: {} + }; + } + function unique(array) { + var seen = {}, uniques = []; + for (var i = 0; i < array.length; i++) { + if (!seen[array[i]]) { + seen[array[i]] = true; + uniques.push(array[i]); + } + } + return uniques; + } + function getIntersection(arrayA, arrayB) { + var ai = 0, bi = 0, intersection = []; + arrayA = arrayA.sort(compare); + arrayB = arrayB.sort(compare); + while (ai < arrayA.length && bi < arrayB.length) { + if (arrayA[ai] < arrayB[bi]) { + ai++; + } else if (arrayA[ai] > arrayB[bi]) { + bi++; + } else { + intersection.push(arrayA[ai]); + ai++; + bi++; + } + } + return intersection; + function compare(a, b) { + return a - b; + } + } + }(); + var oParser = function() { + return { + local: getLocal, + prefetch: getPrefetch, + remote: getRemote + }; + function getLocal(o) { + return o.local || null; + } + function getPrefetch(o) { + var prefetch, defaults; + defaults = { + url: null, + thumbprint: "", + ttl: 24 * 60 * 60 * 1e3, + filter: null, + ajax: {} + }; + if (prefetch = o.prefetch || null) { + prefetch = _.isString(prefetch) ? { + url: prefetch + } : prefetch; + prefetch = _.mixin(defaults, prefetch); + prefetch.thumbprint = VERSION + prefetch.thumbprint; + prefetch.ajax.type = prefetch.ajax.type || "GET"; + prefetch.ajax.dataType = prefetch.ajax.dataType || "json"; + !prefetch.url && $.error("prefetch requires url to be set"); + } + return prefetch; + } + function getRemote(o) { + var remote, defaults; + defaults = { + url: null, + wildcard: "%QUERY", + replace: null, + rateLimitBy: "debounce", + rateLimitWait: 300, + send: null, + filter: null, + ajax: {} + }; + if (remote = o.remote || null) { + remote = _.isString(remote) ? { + url: remote + } : remote; + remote = _.mixin(defaults, remote); + remote.rateLimiter = /^throttle$/i.test(remote.rateLimitBy) ? byThrottle(remote.rateLimitWait) : byDebounce(remote.rateLimitWait); + remote.ajax.type = remote.ajax.type || "GET"; + remote.ajax.dataType = remote.ajax.dataType || "json"; + delete remote.rateLimitBy; + delete remote.rateLimitWait; + !remote.url && $.error("remote requires url to be set"); + } + return remote; + function byDebounce(wait) { + return function(fn) { + return _.debounce(fn, wait); + }; + } + function byThrottle(wait) { + return function(fn) { + return _.throttle(fn, wait); + }; + } + } + }(); + (function(root) { + var old, keys; + old = root.Bloodhound; + keys = { + data: "data", + protocol: "protocol", + thumbprint: "thumbprint" + }; + root.Bloodhound = Bloodhound; + function Bloodhound(o) { + if (!o || !o.local && !o.prefetch && !o.remote) { + $.error("one of local, prefetch, or remote is required"); + } + this.limit = o.limit || 5; + this.sorter = getSorter(o.sorter); + this.dupDetector = o.dupDetector || ignoreDuplicates; + this.local = oParser.local(o); + this.prefetch = oParser.prefetch(o); + this.remote = oParser.remote(o); + this.cacheKey = this.prefetch ? this.prefetch.cacheKey || this.prefetch.url : null; + this.index = new SearchIndex({ + datumTokenizer: o.datumTokenizer, + queryTokenizer: o.queryTokenizer + }); + this.storage = this.cacheKey ? new PersistentStorage(this.cacheKey) : null; + } + Bloodhound.noConflict = function noConflict() { + root.Bloodhound = old; + return Bloodhound; + }; + Bloodhound.tokenizers = tokenizers; + _.mixin(Bloodhound.prototype, { + _loadPrefetch: function loadPrefetch(o) { + var that = this, serialized, deferred; + if (serialized = this._readFromStorage(o.thumbprint)) { + this.index.bootstrap(serialized); + deferred = $.Deferred().resolve(); + } else { + deferred = $.ajax(o.url, o.ajax).done(handlePrefetchResponse); + } + return deferred; + function handlePrefetchResponse(resp) { + that.clear(); + that.add(o.filter ? o.filter(resp) : resp); + that._saveToStorage(that.index.serialize(), o.thumbprint, o.ttl); + } + }, + _getFromRemote: function getFromRemote(query, cb) { + var that = this, url, uriEncodedQuery; + query = query || ""; + uriEncodedQuery = encodeURIComponent(query); + url = this.remote.replace ? this.remote.replace(this.remote.url, query) : this.remote.url.replace(this.remote.wildcard, uriEncodedQuery); + return this.transport.get(url, this.remote.ajax, handleRemoteResponse); + function handleRemoteResponse(err, resp) { + err ? cb([]) : cb(that.remote.filter ? that.remote.filter(resp) : resp); + } + }, + _saveToStorage: function saveToStorage(data, thumbprint, ttl) { + if (this.storage) { + this.storage.set(keys.data, data, ttl); + this.storage.set(keys.protocol, location.protocol, ttl); + this.storage.set(keys.thumbprint, thumbprint, ttl); + } + }, + _readFromStorage: function readFromStorage(thumbprint) { + var stored = {}, isExpired; + if (this.storage) { + stored.data = this.storage.get(keys.data); + stored.protocol = this.storage.get(keys.protocol); + stored.thumbprint = this.storage.get(keys.thumbprint); + } + isExpired = stored.thumbprint !== thumbprint || stored.protocol !== location.protocol; + return stored.data && !isExpired ? stored.data : null; + }, + _initialize: function initialize() { + var that = this, local = this.local, deferred; + deferred = this.prefetch ? this._loadPrefetch(this.prefetch) : $.Deferred().resolve(); + local && deferred.done(addLocalToIndex); + this.transport = this.remote ? new Transport(this.remote) : null; + return this.initPromise = deferred.promise(); + function addLocalToIndex() { + that.add(_.isFunction(local) ? local() : local); + } + }, + initialize: function initialize(force) { + return !this.initPromise || force ? this._initialize() : this.initPromise; + }, + add: function add(data) { + this.index.add(data); + }, + get: function get(query, cb) { + var that = this, matches = [], cacheHit = false; + matches = this.index.get(query); + matches = this.sorter(matches).slice(0, this.limit); + if (matches.length < this.limit && this.transport) { + cacheHit = this._getFromRemote(query, returnRemoteMatches); + } + if (!cacheHit) { + (matches.length > 0 || !this.transport) && cb && cb(matches); + } + function returnRemoteMatches(remoteMatches) { + var matchesWithBackfill = matches.slice(0); + _.each(remoteMatches, function(remoteMatch) { + var isDuplicate; + isDuplicate = _.some(matchesWithBackfill, function(match) { + return that.dupDetector(remoteMatch, match); + }); + !isDuplicate && matchesWithBackfill.push(remoteMatch); + return matchesWithBackfill.length < that.limit; + }); + cb && cb(that.sorter(matchesWithBackfill)); + } + }, + clear: function clear() { + this.index.reset(); + }, + clearPrefetchCache: function clearPrefetchCache() { + this.storage && this.storage.clear(); + }, + clearRemoteCache: function clearRemoteCache() { + this.transport && Transport.resetCache(); + }, + ttAdapter: function ttAdapter() { + return _.bind(this.get, this); + } + }); + return Bloodhound; + function getSorter(sortFn) { + return _.isFunction(sortFn) ? sort : noSort; + function sort(array) { + return array.sort(sortFn); + } + function noSort(array) { + return array; + } + } + function ignoreDuplicates() { + return false; + } + })(this); + var html = { + wrapper: '<span class="twitter-typeahead"></span>', + dropdown: '<span class="tt-dropdown-menu"></span>', + dataset: '<div class="tt-dataset-%CLASS%"></div>', + suggestions: '<span class="tt-suggestions"></span>', + suggestion: '<div class="tt-suggestion"></div>' + }; + var css = { + wrapper: { + position: "relative", + display: "inline-block" + }, + hint: { + position: "absolute", + top: "0", + left: "0", + borderColor: "transparent", + boxShadow: "none" + }, + input: { + position: "relative", + verticalAlign: "top", + backgroundColor: "transparent" + }, + inputWithNoHint: { + position: "relative", + verticalAlign: "top" + }, + dropdown: { + position: "absolute", + top: "100%", + left: "0", + zIndex: "100", + display: "none" + }, + suggestions: { + display: "block" + }, + suggestion: { + whiteSpace: "nowrap", + cursor: "pointer" + }, + suggestionChild: { + whiteSpace: "normal" + }, + ltr: { + left: "0", + right: "auto" + }, + rtl: { + left: "auto", + right: " 0" + } + }; + if (_.isMsie()) { + _.mixin(css.input, { + backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" + }); + } + if (_.isMsie() && _.isMsie() <= 7) { + _.mixin(css.input, { + marginTop: "-1px" + }); + } + var EventBus = function() { + var namespace = "typeahead:"; + function EventBus(o) { + if (!o || !o.el) { + $.error("EventBus initialized without el"); + } + this.$el = $(o.el); + } + _.mixin(EventBus.prototype, { + trigger: function(type) { + var args = [].slice.call(arguments, 1); + this.$el.trigger(namespace + type, args); + } + }); + return EventBus; + }(); + var EventEmitter = function() { + var splitter = /\s+/, nextTick = getNextTick(); + return { + onSync: onSync, + onAsync: onAsync, + off: off, + trigger: trigger + }; + function on(method, types, cb, context) { + var type; + if (!cb) { + return this; + } + types = types.split(splitter); + cb = context ? bindContext(cb, context) : cb; + this._callbacks = this._callbacks || {}; + while (type = types.shift()) { + this._callbacks[type] = this._callbacks[type] || { + sync: [], + async: [] + }; + this._callbacks[type][method].push(cb); + } + return this; + } + function onAsync(types, cb, context) { + return on.call(this, "async", types, cb, context); + } + function onSync(types, cb, context) { + return on.call(this, "sync", types, cb, context); + } + function off(types) { + var type; + if (!this._callbacks) { + return this; + } + types = types.split(splitter); + while (type = types.shift()) { + delete this._callbacks[type]; + } + return this; + } + function trigger(types) { + var type, callbacks, args, syncFlush, asyncFlush; + if (!this._callbacks) { + return this; + } + types = types.split(splitter); + args = [].slice.call(arguments, 1); + while ((type = types.shift()) && (callbacks = this._callbacks[type])) { + syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args)); + asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args)); + syncFlush() && nextTick(asyncFlush); + } + return this; + } + function getFlush(callbacks, context, args) { + return flush; + function flush() { + var cancelled; + for (var i = 0; !cancelled && i < callbacks.length; i += 1) { + cancelled = callbacks[i].apply(context, args) === false; + } + return !cancelled; + } + } + function getNextTick() { + var nextTickFn; + if (window.setImmediate) { + nextTickFn = function nextTickSetImmediate(fn) { + setImmediate(function() { + fn(); + }); + }; + } else { + nextTickFn = function nextTickSetTimeout(fn) { + setTimeout(function() { + fn(); + }, 0); + }; + } + return nextTickFn; + } + function bindContext(fn, context) { + return fn.bind ? fn.bind(context) : function() { + fn.apply(context, [].slice.call(arguments, 0)); + }; + } + }(); + var highlight = function(doc) { + var defaults = { + node: null, + pattern: null, + tagName: "strong", + className: null, + wordsOnly: false, + caseSensitive: false + }; + return function hightlight(o) { + var regex; + o = _.mixin({}, defaults, o); + if (!o.node || !o.pattern) { + return; + } + o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ]; + regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly); + traverse(o.node, hightlightTextNode); + function hightlightTextNode(textNode) { + var match, patternNode; + if (match = regex.exec(textNode.data)) { + wrapperNode = doc.createElement(o.tagName); + o.className && (wrapperNode.className = o.className); + patternNode = textNode.splitText(match.index); + patternNode.splitText(match[0].length); + wrapperNode.appendChild(patternNode.cloneNode(true)); + textNode.parentNode.replaceChild(wrapperNode, patternNode); + } + return !!match; + } + function traverse(el, hightlightTextNode) { + var childNode, TEXT_NODE_TYPE = 3; + for (var i = 0; i < el.childNodes.length; i++) { + childNode = el.childNodes[i]; + if (childNode.nodeType === TEXT_NODE_TYPE) { + i += hightlightTextNode(childNode) ? 1 : 0; + } else { + traverse(childNode, hightlightTextNode); + } + } + } + }; + function getRegex(patterns, caseSensitive, wordsOnly) { + var escapedPatterns = [], regexStr; + for (var i = 0; i < patterns.length; i++) { + escapedPatterns.push(_.escapeRegExChars(patterns[i])); + } + regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")"; + return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i"); + } + }(window.document); + var Input = function() { + var specialKeyCodeMap; + specialKeyCodeMap = { + 9: "tab", + 27: "esc", + 37: "left", + 39: "right", + 13: "enter", + 38: "up", + 40: "down" + }; + function Input(o) { + var that = this, onBlur, onFocus, onKeydown, onInput; + o = o || {}; + if (!o.input) { + $.error("input is missing"); + } + onBlur = _.bind(this._onBlur, this); + onFocus = _.bind(this._onFocus, this); + onKeydown = _.bind(this._onKeydown, this); + onInput = _.bind(this._onInput, this); + this.$hint = $(o.hint); + this.$input = $(o.input).on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown); + if (this.$hint.length === 0) { + this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; + } + if (!_.isMsie()) { + this.$input.on("input.tt", onInput); + } else { + this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { + if (specialKeyCodeMap[$e.which || $e.keyCode]) { + return; + } + _.defer(_.bind(that._onInput, that, $e)); + }); + } + this.query = this.$input.val(); + this.$overflowHelper = buildOverflowHelper(this.$input); + } + Input.normalizeQuery = function(str) { + return (str || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " "); + }; + _.mixin(Input.prototype, EventEmitter, { + _onBlur: function onBlur() { + this.resetInputValue(); + this.trigger("blurred"); + }, + _onFocus: function onFocus() { + this.trigger("focused"); + }, + _onKeydown: function onKeydown($e) { + var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; + this._managePreventDefault(keyName, $e); + if (keyName && this._shouldTrigger(keyName, $e)) { + this.trigger(keyName + "Keyed", $e); + } + }, + _onInput: function onInput() { + this._checkInputValue(); + }, + _managePreventDefault: function managePreventDefault(keyName, $e) { + var preventDefault, hintValue, inputValue; + switch (keyName) { + case "tab": + hintValue = this.getHint(); + inputValue = this.getInputValue(); + preventDefault = hintValue && hintValue !== inputValue && !withModifier($e); + break; + + case "up": + case "down": + preventDefault = !withModifier($e); + break; + + default: + preventDefault = false; + } + preventDefault && $e.preventDefault(); + }, + _shouldTrigger: function shouldTrigger(keyName, $e) { + var trigger; + switch (keyName) { + case "tab": + trigger = !withModifier($e); + break; + + default: + trigger = true; + } + return trigger; + }, + _checkInputValue: function checkInputValue() { + var inputValue, areEquivalent, hasDifferentWhitespace; + inputValue = this.getInputValue(); + areEquivalent = areQueriesEquivalent(inputValue, this.query); + hasDifferentWhitespace = areEquivalent ? this.query.length !== inputValue.length : false; + if (!areEquivalent) { + this.trigger("queryChanged", this.query = inputValue); + } else if (hasDifferentWhitespace) { + this.trigger("whitespaceChanged", this.query); + } + }, + focus: function focus() { + this.$input.focus(); + }, + blur: function blur() { + this.$input.blur(); + }, + getQuery: function getQuery() { + return this.query; + }, + setQuery: function setQuery(query) { + this.query = query; + }, + getInputValue: function getInputValue() { + return this.$input.val(); + }, + setInputValue: function setInputValue(value, silent) { + this.$input.val(value); + silent ? this.clearHint() : this._checkInputValue(); + }, + resetInputValue: function resetInputValue() { + this.setInputValue(this.query, true); + }, + getHint: function getHint() { + return this.$hint.val(); + }, + setHint: function setHint(value) { + this.$hint.val(value); + }, + clearHint: function clearHint() { + this.setHint(""); + }, + clearHintIfInvalid: function clearHintIfInvalid() { + var val, hint, valIsPrefixOfHint, isValid; + val = this.getInputValue(); + hint = this.getHint(); + valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0; + isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow(); + !isValid && this.clearHint(); + }, + getLanguageDirection: function getLanguageDirection() { + return (this.$input.css("direction") || "ltr").toLowerCase(); + }, + hasOverflow: function hasOverflow() { + var constraint = this.$input.width() - 2; + this.$overflowHelper.text(this.getInputValue()); + return this.$overflowHelper.width() >= constraint; + }, + isCursorAtEnd: function() { + var valueLength, selectionStart, range; + valueLength = this.$input.val().length; + selectionStart = this.$input[0].selectionStart; + if (_.isNumber(selectionStart)) { + return selectionStart === valueLength; + } else if (document.selection) { + range = document.selection.createRange(); + range.moveStart("character", -valueLength); + return valueLength === range.text.length; + } + return true; + }, + destroy: function destroy() { + this.$hint.off(".tt"); + this.$input.off(".tt"); + this.$hint = this.$input = this.$overflowHelper = null; + } + }); + return Input; + function buildOverflowHelper($input) { + return $('<pre aria-hidden="true"></pre>').css({ + position: "absolute", + visibility: "hidden", + whiteSpace: "pre", + fontFamily: $input.css("font-family"), + fontSize: $input.css("font-size"), + fontStyle: $input.css("font-style"), + fontVariant: $input.css("font-variant"), + fontWeight: $input.css("font-weight"), + wordSpacing: $input.css("word-spacing"), + letterSpacing: $input.css("letter-spacing"), + textIndent: $input.css("text-indent"), + textRendering: $input.css("text-rendering"), + textTransform: $input.css("text-transform") + }).insertAfter($input); + } + function areQueriesEquivalent(a, b) { + return Input.normalizeQuery(a) === Input.normalizeQuery(b); + } + function withModifier($e) { + return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey; + } + }(); + var Dataset = function() { + var datasetKey = "ttDataset", valueKey = "ttValue", datumKey = "ttDatum"; + function Dataset(o) { + o = o || {}; + o.templates = o.templates || {}; + if (!o.source) { + $.error("missing source"); + } + if (o.name && !isValidName(o.name)) { + $.error("invalid dataset name: " + o.name); + } + this.query = null; + this.highlight = !!o.highlight; + this.name = o.name || _.getUniqueId(); + this.source = o.source; + this.displayFn = getDisplayFn(o.display || o.displayKey); + this.templates = getTemplates(o.templates, this.displayFn); + this.$el = $(html.dataset.replace("%CLASS%", this.name)); + } + Dataset.extractDatasetName = function extractDatasetName(el) { + return $(el).data(datasetKey); + }; + Dataset.extractValue = function extractDatum(el) { + return $(el).data(valueKey); + }; + Dataset.extractDatum = function extractDatum(el) { + return $(el).data(datumKey); + }; + _.mixin(Dataset.prototype, EventEmitter, { + _render: function render(query, suggestions) { + if (!this.$el) { + return; + } + var that = this, hasSuggestions; + this.$el.empty(); + hasSuggestions = suggestions && suggestions.length; + if (!hasSuggestions && this.templates.empty) { + this.$el.html(getEmptyHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null); + } else if (hasSuggestions) { + this.$el.html(getSuggestionsHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null); + } + this.trigger("rendered"); + function getEmptyHtml() { + return that.templates.empty({ + query: query, + isEmpty: true + }); + } + function getSuggestionsHtml() { + var $suggestions, nodes; + $suggestions = $(html.suggestions).css(css.suggestions); + nodes = _.map(suggestions, getSuggestionNode); + $suggestions.append.apply($suggestions, nodes); + that.highlight && highlight({ + node: $suggestions[0], + pattern: query + }); + return $suggestions; + function getSuggestionNode(suggestion) { + var $el; + $el = $(html.suggestion).append(that.templates.suggestion(suggestion)).data(datasetKey, that.name).data(valueKey, that.displayFn(suggestion)).data(datumKey, suggestion); + $el.children().each(function() { + $(this).css(css.suggestionChild); + }); + return $el; + } + } + function getHeaderHtml() { + return that.templates.header({ + query: query, + isEmpty: !hasSuggestions + }); + } + function getFooterHtml() { + return that.templates.footer({ + query: query, + isEmpty: !hasSuggestions + }); + } + }, + getRoot: function getRoot() { + return this.$el; + }, + update: function update(query) { + var that = this; + this.query = query; + this.canceled = false; + this.source(query, render); + function render(suggestions) { + if (!that.canceled && query === that.query) { + that._render(query, suggestions); + } + } + }, + cancel: function cancel() { + this.canceled = true; + }, + clear: function clear() { + this.cancel(); + this.$el.empty(); + this.trigger("rendered"); + }, + isEmpty: function isEmpty() { + return this.$el.is(":empty"); + }, + destroy: function destroy() { + this.$el = null; + } + }); + return Dataset; + function getDisplayFn(display) { + display = display || "value"; + return _.isFunction(display) ? display : displayFn; + function displayFn(obj) { + return obj[display]; + } + } + function getTemplates(templates, displayFn) { + return { + empty: templates.empty && _.templatify(templates.empty), + header: templates.header && _.templatify(templates.header), + footer: templates.footer && _.templatify(templates.footer), + suggestion: templates.suggestion || suggestionTemplate + }; + function suggestionTemplate(context) { + return "<p>" + displayFn(context) + "</p>"; + } + } + function isValidName(str) { + return /^[_a-zA-Z0-9-]+$/.test(str); + } + }(); + var Dropdown = function() { + function Dropdown(o) { + var that = this, onSuggestionClick, onSuggestionMouseEnter, onSuggestionMouseLeave; + o = o || {}; + if (!o.menu) { + $.error("menu is required"); + } + this.isOpen = false; + this.isEmpty = true; + this.datasets = _.map(o.datasets, initializeDataset); + onSuggestionClick = _.bind(this._onSuggestionClick, this); + onSuggestionMouseEnter = _.bind(this._onSuggestionMouseEnter, this); + onSuggestionMouseLeave = _.bind(this._onSuggestionMouseLeave, this); + this.$menu = $(o.menu).on("click.tt", ".tt-suggestion", onSuggestionClick).on("mouseenter.tt", ".tt-suggestion", onSuggestionMouseEnter).on("mouseleave.tt", ".tt-suggestion", onSuggestionMouseLeave); + _.each(this.datasets, function(dataset) { + that.$menu.append(dataset.getRoot()); + dataset.onSync("rendered", that._onRendered, that); + }); + } + _.mixin(Dropdown.prototype, EventEmitter, { + _onSuggestionClick: function onSuggestionClick($e) { + this.trigger("suggestionClicked", $($e.currentTarget)); + }, + _onSuggestionMouseEnter: function onSuggestionMouseEnter($e) { + this._removeCursor(); + this._setCursor($($e.currentTarget), true); + }, + _onSuggestionMouseLeave: function onSuggestionMouseLeave() { + this._removeCursor(); + }, + _onRendered: function onRendered() { + this.isEmpty = _.every(this.datasets, isDatasetEmpty); + this.isEmpty ? this._hide() : this.isOpen && this._show(); + this.trigger("datasetRendered"); + function isDatasetEmpty(dataset) { + return dataset.isEmpty(); + } + }, + _hide: function() { + this.$menu.hide(); + }, + _show: function() { + this.$menu.css("display", "block"); + }, + _getSuggestions: function getSuggestions() { + return this.$menu.find(".tt-suggestion"); + }, + _getCursor: function getCursor() { + return this.$menu.find(".tt-cursor").first(); + }, + _setCursor: function setCursor($el, silent) { + $el.first().addClass("tt-cursor"); + !silent && this.trigger("cursorMoved"); + }, + _removeCursor: function removeCursor() { + this._getCursor().removeClass("tt-cursor"); + }, + _moveCursor: function moveCursor(increment) { + var $suggestions, $oldCursor, newCursorIndex, $newCursor; + if (!this.isOpen) { + return; + } + $oldCursor = this._getCursor(); + $suggestions = this._getSuggestions(); + this._removeCursor(); + newCursorIndex = $suggestions.index($oldCursor) + increment; + newCursorIndex = (newCursorIndex + 1) % ($suggestions.length + 1) - 1; + if (newCursorIndex === -1) { + this.trigger("cursorRemoved"); + return; + } else if (newCursorIndex < -1) { + newCursorIndex = $suggestions.length - 1; + } + this._setCursor($newCursor = $suggestions.eq(newCursorIndex)); + this._ensureVisible($newCursor); + }, + _ensureVisible: function ensureVisible($el) { + var elTop, elBottom, menuScrollTop, menuHeight; + elTop = $el.position().top; + elBottom = elTop + $el.outerHeight(true); + menuScrollTop = this.$menu.scrollTop(); + menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10); + if (elTop < 0) { + this.$menu.scrollTop(menuScrollTop + elTop); + } else if (menuHeight < elBottom) { + this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight)); + } + }, + close: function close() { + if (this.isOpen) { + this.isOpen = false; + this._removeCursor(); + this._hide(); + this.trigger("closed"); + } + }, + open: function open() { + if (!this.isOpen) { + this.isOpen = true; + !this.isEmpty && this._show(); + this.trigger("opened"); + } + }, + setLanguageDirection: function setLanguageDirection(dir) { + this.$menu.css(dir === "ltr" ? css.ltr : css.rtl); + }, + moveCursorUp: function moveCursorUp() { + this._moveCursor(-1); + }, + moveCursorDown: function moveCursorDown() { + this._moveCursor(+1); + }, + getDatumForSuggestion: function getDatumForSuggestion($el) { + var datum = null; + if ($el.length) { + datum = { + raw: Dataset.extractDatum($el), + value: Dataset.extractValue($el), + datasetName: Dataset.extractDatasetName($el) + }; + } + return datum; + }, + getDatumForCursor: function getDatumForCursor() { + return this.getDatumForSuggestion(this._getCursor().first()); + }, + getDatumForTopSuggestion: function getDatumForTopSuggestion() { + return this.getDatumForSuggestion(this._getSuggestions().first()); + }, + update: function update(query) { + _.each(this.datasets, updateDataset); + function updateDataset(dataset) { + dataset.update(query); + } + }, + empty: function empty() { + _.each(this.datasets, clearDataset); + this.isEmpty = true; + function clearDataset(dataset) { + dataset.clear(); + } + }, + isVisible: function isVisible() { + return this.isOpen && !this.isEmpty; + }, + destroy: function destroy() { + this.$menu.off(".tt"); + this.$menu = null; + _.each(this.datasets, destroyDataset); + function destroyDataset(dataset) { + dataset.destroy(); + } + } + }); + return Dropdown; + function initializeDataset(oDataset) { + return new Dataset(oDataset); + } + }(); + var Typeahead = function() { + var attrsKey = "ttAttrs"; + function Typeahead(o) { + var $menu, $input, $hint; + o = o || {}; + if (!o.input) { + $.error("missing input"); + } + this.isActivated = false; + this.autoselect = !!o.autoselect; + this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; + this.$node = buildDomStructure(o.input, o.withHint); + $menu = this.$node.find(".tt-dropdown-menu"); + $input = this.$node.find(".tt-input"); + $hint = this.$node.find(".tt-hint"); + $input.on("blur.tt", function($e) { + var active, isActive, hasActive; + active = document.activeElement; + isActive = $menu.is(active); + hasActive = $menu.has(active).length > 0; + if (_.isMsie() && (isActive || hasActive)) { + $e.preventDefault(); + $e.stopImmediatePropagation(); + _.defer(function() { + $input.focus(); + }); + } + }); + $menu.on("mousedown.tt", function($e) { + $e.preventDefault(); + }); + this.eventBus = o.eventBus || new EventBus({ + el: $input + }); + this.dropdown = new Dropdown({ + menu: $menu, + datasets: o.datasets + }).onSync("suggestionClicked", this._onSuggestionClicked, this).onSync("cursorMoved", this._onCursorMoved, this).onSync("cursorRemoved", this._onCursorRemoved, this).onSync("opened", this._onOpened, this).onSync("closed", this._onClosed, this).onAsync("datasetRendered", this._onDatasetRendered, this); + this.input = new Input({ + input: $input, + hint: $hint + }).onSync("focused", this._onFocused, this).onSync("blurred", this._onBlurred, this).onSync("enterKeyed", this._onEnterKeyed, this).onSync("tabKeyed", this._onTabKeyed, this).onSync("escKeyed", this._onEscKeyed, this).onSync("upKeyed", this._onUpKeyed, this).onSync("downKeyed", this._onDownKeyed, this).onSync("leftKeyed", this._onLeftKeyed, this).onSync("rightKeyed", this._onRightKeyed, this).onSync("queryChanged", this._onQueryChanged, this).onSync("whitespaceChanged", this._onWhitespaceChanged, this); + this._setLanguageDirection(); + } + _.mixin(Typeahead.prototype, { + _onSuggestionClicked: function onSuggestionClicked(type, $el) { + var datum; + if (datum = this.dropdown.getDatumForSuggestion($el)) { + this._select(datum); + } + }, + _onCursorMoved: function onCursorMoved() { + var datum = this.dropdown.getDatumForCursor(); + this.input.setInputValue(datum.value, true); + this.eventBus.trigger("cursorchanged", datum.raw, datum.datasetName); + }, + _onCursorRemoved: function onCursorRemoved() { + this.input.resetInputValue(); + this._updateHint(); + }, + _onDatasetRendered: function onDatasetRendered() { + this._updateHint(); + }, + _onOpened: function onOpened() { + this._updateHint(); + this.eventBus.trigger("opened"); + }, + _onClosed: function onClosed() { + this.input.clearHint(); + this.eventBus.trigger("closed"); + }, + _onFocused: function onFocused() { + this.isActivated = true; + this.dropdown.open(); + }, + _onBlurred: function onBlurred() { + this.isActivated = false; + this.dropdown.empty(); + this.dropdown.close(); + }, + _onEnterKeyed: function onEnterKeyed(type, $e) { + var cursorDatum, topSuggestionDatum; + cursorDatum = this.dropdown.getDatumForCursor(); + topSuggestionDatum = this.dropdown.getDatumForTopSuggestion(); + if (cursorDatum) { + this._select(cursorDatum); + $e.preventDefault(); + } else if (this.autoselect && topSuggestionDatum) { + this._select(topSuggestionDatum); + $e.preventDefault(); + } + }, + _onTabKeyed: function onTabKeyed(type, $e) { + var datum; + if (datum = this.dropdown.getDatumForCursor()) { + this._select(datum); + $e.preventDefault(); + } else { + this._autocomplete(true); + } + }, + _onEscKeyed: function onEscKeyed() { + this.dropdown.close(); + this.input.resetInputValue(); + }, + _onUpKeyed: function onUpKeyed() { + var query = this.input.getQuery(); + this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorUp(); + this.dropdown.open(); + }, + _onDownKeyed: function onDownKeyed() { + var query = this.input.getQuery(); + this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorDown(); + this.dropdown.open(); + }, + _onLeftKeyed: function onLeftKeyed() { + this.dir === "rtl" && this._autocomplete(); + }, + _onRightKeyed: function onRightKeyed() { + this.dir === "ltr" && this._autocomplete(); + }, + _onQueryChanged: function onQueryChanged(e, query) { + this.input.clearHintIfInvalid(); + query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.empty(); + this.dropdown.open(); + this._setLanguageDirection(); + }, + _onWhitespaceChanged: function onWhitespaceChanged() { + this._updateHint(); + this.dropdown.open(); + }, + _setLanguageDirection: function setLanguageDirection() { + var dir; + if (this.dir !== (dir = this.input.getLanguageDirection())) { + this.dir = dir; + this.$node.css("direction", dir); + this.dropdown.setLanguageDirection(dir); + } + }, + _updateHint: function updateHint() { + var datum, val, query, escapedQuery, frontMatchRegEx, match; + datum = this.dropdown.getDatumForTopSuggestion(); + if (datum && this.dropdown.isVisible() && !this.input.hasOverflow()) { + val = this.input.getInputValue(); + query = Input.normalizeQuery(val); + escapedQuery = _.escapeRegExChars(query); + frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i"); + match = frontMatchRegEx.exec(datum.value); + match ? this.input.setHint(val + match[1]) : this.input.clearHint(); + } else { + this.input.clearHint(); + } + }, + _autocomplete: function autocomplete(laxCursor) { + var hint, query, isCursorAtEnd, datum; + hint = this.input.getHint(); + query = this.input.getQuery(); + isCursorAtEnd = laxCursor || this.input.isCursorAtEnd(); + if (hint && query !== hint && isCursorAtEnd) { + datum = this.dropdown.getDatumForTopSuggestion(); + datum && this.input.setInputValue(datum.value); + this.eventBus.trigger("autocompleted", datum.raw, datum.datasetName); + } + }, + _select: function select(datum) { + this.input.setQuery(datum.value); + this.input.setInputValue(datum.value, true); + this._setLanguageDirection(); + this.eventBus.trigger("selected", datum.raw, datum.datasetName); + this.dropdown.close(); + _.defer(_.bind(this.dropdown.empty, this.dropdown)); + }, + open: function open() { + this.dropdown.open(); + }, + close: function close() { + this.dropdown.close(); + }, + setVal: function setVal(val) { + if (this.isActivated) { + this.input.setInputValue(val); + } else { + this.input.setQuery(val); + this.input.setInputValue(val, true); + } + this._setLanguageDirection(); + }, + getVal: function getVal() { + return this.input.getQuery(); + }, + destroy: function destroy() { + this.input.destroy(); + this.dropdown.destroy(); + destroyDomStructure(this.$node); + this.$node = null; + } + }); + return Typeahead; + function buildDomStructure(input, withHint) { + var $input, $wrapper, $dropdown, $hint; + $input = $(input); + $wrapper = $(html.wrapper).css(css.wrapper); + $dropdown = $(html.dropdown).css(css.dropdown); + $hint = $input.clone().css(css.hint).css(getBackgroundStyles($input)); + $hint.val("").removeData().addClass("tt-hint").removeAttr("id name placeholder").prop("disabled", true).attr({ + autocomplete: "off", + spellcheck: "false" + }); + $input.data(attrsKey, { + dir: $input.attr("dir"), + autocomplete: $input.attr("autocomplete"), + spellcheck: $input.attr("spellcheck"), + style: $input.attr("style") + }); + $input.addClass("tt-input").attr({ + autocomplete: "off", + spellcheck: false + }).css(withHint ? css.input : css.inputWithNoHint); + try { + !$input.attr("dir") && $input.attr("dir", "auto"); + } catch (e) {} + return $input.wrap($wrapper).parent().prepend(withHint ? $hint : null).append($dropdown); + } + function getBackgroundStyles($el) { + return { + backgroundAttachment: $el.css("background-attachment"), + backgroundClip: $el.css("background-clip"), + backgroundColor: $el.css("background-color"), + backgroundImage: $el.css("background-image"), + backgroundOrigin: $el.css("background-origin"), + backgroundPosition: $el.css("background-position"), + backgroundRepeat: $el.css("background-repeat"), + backgroundSize: $el.css("background-size") + }; + } + function destroyDomStructure($node) { + var $input = $node.find(".tt-input"); + _.each($input.data(attrsKey), function(val, key) { + _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); + }); + $input.detach().removeData(attrsKey).removeClass("tt-input").insertAfter($node); + $node.remove(); + } + }(); + (function() { + var old, typeaheadKey, methods; + old = $.fn.typeahead; + typeaheadKey = "ttTypeahead"; + methods = { + initialize: function initialize(o, datasets) { + datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); + o = o || {}; + return this.each(attach); + function attach() { + var $input = $(this), eventBus, typeahead; + _.each(datasets, function(d) { + d.highlight = !!o.highlight; + }); + typeahead = new Typeahead({ + input: $input, + eventBus: eventBus = new EventBus({ + el: $input + }), + withHint: _.isUndefined(o.hint) ? true : !!o.hint, + minLength: o.minLength, + autoselect: o.autoselect, + datasets: datasets + }); + $input.data(typeaheadKey, typeahead); + } + }, + open: function open() { + return this.each(openTypeahead); + function openTypeahead() { + var $input = $(this), typeahead; + if (typeahead = $input.data(typeaheadKey)) { + typeahead.open(); + } + } + }, + close: function close() { + return this.each(closeTypeahead); + function closeTypeahead() { + var $input = $(this), typeahead; + if (typeahead = $input.data(typeaheadKey)) { + typeahead.close(); + } + } + }, + val: function val(newVal) { + return !arguments.length ? getVal(this.first()) : this.each(setVal); + function setVal() { + var $input = $(this), typeahead; + if (typeahead = $input.data(typeaheadKey)) { + typeahead.setVal(newVal); + } + } + function getVal($input) { + var typeahead, query; + if (typeahead = $input.data(typeaheadKey)) { + query = typeahead.getVal(); + } + return query; + } + }, + destroy: function destroy() { + return this.each(unattach); + function unattach() { + var $input = $(this), typeahead; + if (typeahead = $input.data(typeaheadKey)) { + typeahead.destroy(); + $input.removeData(typeaheadKey); + } + } + } + }; + $.fn.typeahead = function(method) { + if (methods[method]) { + return methods[method].apply(this, [].slice.call(arguments, 1)); + } else { + return methods.initialize.apply(this, arguments); + } + }; + $.fn.typeahead.noConflict = function noConflict() { + $.fn.typeahead = old; + return this; + }; + })(); +})(window.jQuery); \ No newline at end of file diff --git a/src/UI/Mixins/AsFilteredCollection.js b/src/UI/Mixins/AsFilteredCollection.js index 469059cfc..ff1fd5103 100644 --- a/src/UI/Mixins/AsFilteredCollection.js +++ b/src/UI/Mixins/AsFilteredCollection.js @@ -15,7 +15,7 @@ define( this.state.filterValue = filter[1]; if (options.reset) { - if (this.mode != 'server') { + if (this.mode !== 'server') { this.fullCollection.resetFiltered(); } else { return this.fetch(); @@ -35,10 +35,12 @@ define( self.shadowCollection = originalMakeFullCollection.call(this, models, options); var filterModel = function(model) { - if (!self.state.filterKey || !self.state.filterValue) + if (!self.state.filterKey || !self.state.filterValue) { return true; - else + } + else { return model.get(self.state.filterKey) === self.state.filterValue; + } }; self.shadowCollection.filtered = function() { diff --git a/src/UI/Mixins/AsModelBoundView.js b/src/UI/Mixins/AsModelBoundView.js index d35f8a5a0..be70ec65b 100644 --- a/src/UI/Mixins/AsModelBoundView.js +++ b/src/UI/Mixins/AsModelBoundView.js @@ -20,7 +20,7 @@ define( } var options = { - changeTriggers: {'': 'change', '[contenteditable]': 'blur', '[data-onkeyup]': 'keyup'} + changeTriggers: {'': 'change typeahead:selected typeahead:autocompleted', '[contenteditable]': 'blur', '[data-onkeyup]': 'keyup'} }; this._modelBinder.bind(this.model, this.el, null, options); diff --git a/src/UI/Mixins/AutoComplete.js b/src/UI/Mixins/AutoComplete.js index a80610313..a55dadfdc 100644 --- a/src/UI/Mixins/AutoComplete.js +++ b/src/UI/Mixins/AutoComplete.js @@ -1,22 +1,40 @@ 'use strict'; - -define(['jquery'],function ($) { +define( + [ + 'jquery', + 'typeahead' + ], function ($) { $.fn.autoComplete = function (resource) { $(this).typeahead({ - source : function (filter, callback) { - $.ajax({ - url : window.NzbDrone.ApiRoot + resource, - dataType: 'json', - type : 'GET', - data : { query: filter }, - success : function (data) { - callback(data); - } - }); + hint : true, + highlight : true, + minLength : 3, + items : 20 }, - minLength: 3, - items : 20 + { + name: resource.replace('/'), + displayKey: '', + source : function (filter, callback) { + $.ajax({ + url : window.NzbDrone.ApiRoot + resource, + dataType: 'json', + type : 'GET', + data : { query: filter }, + success : function (data) { + + var matches = []; + + $.each(data, function(i, d) { + if (d.startsWith(filter)) { + matches.push({ value: d }); + } + }); + + callback(matches); + } + }); + } }); }; }); diff --git a/src/UI/Mixins/jquery.ajax.js b/src/UI/Mixins/jquery.ajax.js index 5105d8ae8..8186a7784 100644 --- a/src/UI/Mixins/jquery.ajax.js +++ b/src/UI/Mixins/jquery.ajax.js @@ -28,6 +28,29 @@ define( xhr.headers['X-Api-Key'] = window.NzbDrone.ApiKey; } - return original.apply(this, arguments); + return original.apply(this, arguments).done(function (response, status, xhr){ + var version = xhr.getResponseHeader('X-ApplicationVersion'); + + if (!window.NzbDrone || !window.NzbDrone.Version) { + return; + } + + if (version !== window.NzbDrone.Version) { + var vent = require('vent'); + var messenger = require('Shared/Messenger'); + + if (!vent || !messenger) { + return; + } + + messenger.show({ + message : 'NzbDrone has been updated', + hideAfter : 0, + id : 'droneUpdated' + }); + + vent.trigger(vent.Events.ServerUpdated); + } + }); }; }); diff --git a/src/UI/Navbar/NavbarLayoutTemplate.html b/src/UI/Navbar/NavbarLayoutTemplate.html index c040f15b6..38168a440 100644 --- a/src/UI/Navbar/NavbarLayoutTemplate.html +++ b/src/UI/Navbar/NavbarLayoutTemplate.html @@ -1,74 +1,46 @@ -<div class="container"> - <div class="row"> - <div class="span12"> - <ul id="main-menu-region"> - <div class="pull-left logo"> - <a href="{{UrlBase}}/"> - <img src="{{UrlBase}}/Content/Images/logo.png?v=2" alt="NzbDrone"> - </a> - </div> - <li> - <a href="{{UrlBase}}/"> - <i class="icon-play"></i> - <br> - Series - </a> - </li> - <li> - <a href="{{UrlBase}}/calendar"> - <i class="icon-calendar"></i> - <br> - Calendar - </a> - </li> - <li> - <a href="{{UrlBase}}/history"> - <i class="icon-time"></i> - <br> - History - </a> - </li> - <li> - <a href="{{UrlBase}}/wanted"> - <i class="icon-warning-sign"></i> - <br> - Wanted - </a> - </li> - <li> - <a href="{{UrlBase}}/settings"> - <i class="icon-cogs"></i> - <br> - Settings - </a> - </li> - <li> - <a href="{{UrlBase}}/system"> - <i class="icon-laptop"></i> - <br> - System - <span id="x-health"></span> - </a> - </li> - <li> - <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HGGGM7JT5YVSS" target="_blank"> - <i class="icon-nd-donate"></i> - <br> - Donate - </a> - </li> - </ul> - </div> - </div> +<!-- Static navbar --> +<div class="navbar navbar-nzbdrone" role="navigation"> + <div class="container-fluid"> + <div class="navbar-header"> + <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> + <span class="sr-only">Toggle navigation</span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + </button> + <a class="navbar-brand" href="{{UrlBase}}/"> + <!--<img src="{{UrlBase}}/Content/Images/logo.png?v=2" alt="nzbdrone">--> + <img src="{{UrlBase}}/Content/Images/logos/128.png" class="visible-lg"/> + <img src="{{UrlBase}}/Content/Images/logos/64.png" class="visible-md visible-sm"/> + <span class="visible-xs"> + <img src="{{UrlBase}}/Content/Images/logos/32.png"/> + <span class="logo-text"><span class="highlight">nzb</span>drone</span> + </span> - <div class="row"> - <div class="span12"> - <div class="search"> - <div class="input-prepend"> - <span class="add-on"><i class="icon-search"></i></span> - <input class="span4 x-series-search" id="prependedInput" type="text"> - </div> + </a> + </div> + <div class="navbar-collapse collapse x-navbar-collapse"> + <ul class="nav navbar-nav"> + <li><a href="{{UrlBase}}/" class="x-series-nav"><i class="icon-play"></i> Series</a></li> + <li><a href="{{UrlBase}}/calendar" class="x-calendar-nav"><i class="icon-calendar"></i> Calendar</a></li> + <li><a href="{{UrlBase}}/history" class="x-history-nav"><i class="icon-time"></i> History</a></li> + <li><a href="{{UrlBase}}/wanted" class="x-wanted-nav"><i class="icon-warning-sign"></i> Wanted</a></li> + <li><a href="{{UrlBase}}/settings" class="x-settings-nav"><i class="icon-cogs"></i> Settings</a></li> + <li><a href="{{UrlBase}}/system" class="x-system-nav"><i class="icon-laptop"></i> System<span id="x-health" class="health"></span></a></li> + <li><a href="http://nzbdrone.com/#donate" target="_blank"><i class="icon-nd-donate"></i> Donate</a></li> + </ul> + <ul class="nav navbar-nav navbar-right"> + <li class="active screen-size"></li> + </ul> + </div><!--/.nav-collapse --> + </div><!--/.container-fluid --> + + <div class="col-md-12 search"> + <div class="col-md-6 col-md-offset-3"> + <div class="input-group"> + <span class="input-group-addon"><i class="icon-search"></i></span> + <input type="text" class="col-md-6 form-control x-series-search" > </div> </div> </div> -</div> +</div> \ No newline at end of file diff --git a/src/UI/Navbar/NavbarView.js b/src/UI/Navbar/NavbarView.js index 3cdb8e2f1..d6af8d16a 100644 --- a/src/UI/Navbar/NavbarView.js +++ b/src/UI/Navbar/NavbarView.js @@ -14,7 +14,8 @@ define( }, ui: { - search: '.x-series-search' + search: '.x-series-search', + collapse: '.x-navbar-collapse' }, events: { @@ -46,6 +47,10 @@ define( else { this.setActive(event.target); } + + if ($(window).width() < 768) { + this.ui.collapse.collapse('hide'); + } }, setActive: function (element) { diff --git a/src/UI/Navbar/Search.js b/src/UI/Navbar/Search.js index 324c7c5d4..d92bbfcad 100644 --- a/src/UI/Navbar/Search.js +++ b/src/UI/Navbar/Search.js @@ -1,10 +1,12 @@ 'use strict'; define( [ - 'backbone', + 'underscore', 'jquery', - 'Series/SeriesCollection' - ], function (Backbone, $, SeriesCollection) { + 'backbone', + 'Series/SeriesCollection', + 'typeahead' + ], function (_, $, Backbone, SeriesCollection) { $(document).on('keydown', function (e) { if ($(e.target).is('input') || $(e.target).is('textarea')) { return; @@ -22,20 +24,33 @@ define( $.fn.bindSearch = function () { $(this).typeahead({ - source: function () { - return SeriesCollection.pluck('title'); + hint: true, + highlight: true, + minLength: 1 }, + { + name: 'series', + displayKey: 'title', + source: substringMatcher() + }); - sorter: function (items) { - return items.sort(); - }, - - updater: function (item) { - var series = SeriesCollection.findWhere({ title: item }); - - this.$element.blur(); - Backbone.history.navigate('/series/{0}'.format(series.get('titleSlug')), { trigger: true }); - } + $(this).on('typeahead:selected typeahead:autocompleted', function (e, series) { + this.blur(); + $(this).val(''); + Backbone.history.navigate('/series/{0}'.format(series.titleSlug), { trigger: true }); }); }; + + var substringMatcher = function() { + return function findMatches(q, cb) { + // regex used to determine if a string contains the substring `q` + var substrRegex = new RegExp(q, 'i'); + + var matches = _.select(SeriesCollection.toJSON(), function (series) { + return substrRegex.test(series.title); + }); + + cb(matches); + }; + }; }); diff --git a/src/UI/Quality/QualityProfileSelectionPartial.html b/src/UI/Quality/QualityProfileSelectionPartial.html index a414fd13a..688d1b276 100644 --- a/src/UI/Quality/QualityProfileSelectionPartial.html +++ b/src/UI/Quality/QualityProfileSelectionPartial.html @@ -1,4 +1,4 @@ -<select class="span2 x-quality-profile"> +<select class="col-md-2 form-control x-quality-profile"> {{#each this}} <option value="{{id}}">{{name}}</option> {{/each}} diff --git a/src/UI/Release/ReleaseLayoutTemplate.html b/src/UI/Release/ReleaseLayoutTemplate.html index 4da95b683..6df720d21 100644 --- a/src/UI/Release/ReleaseLayoutTemplate.html +++ b/src/UI/Release/ReleaseLayoutTemplate.html @@ -1,6 +1,6 @@ <div id="x-toolbar"/> <div class="row"> - <div class="span12"> + <div class="col-md-12"> <div id="x-grid"/> </div> </div> diff --git a/src/UI/Rename/RenamePreviewItemViewTemplate.html b/src/UI/Rename/RenamePreviewItemViewTemplate.html index 799eaf1b7..17652e584 100644 --- a/src/UI/Rename/RenamePreviewItemViewTemplate.html +++ b/src/UI/Rename/RenamePreviewItemViewTemplate.html @@ -8,12 +8,12 @@ </div> </label> </div> - <div class="span9"> + <div class="col-md-9"> <div class="row"> - <div class="span9 file-path"><i class="icon-nd-existing" title="Existing path" /> {{existingPath}}</div> + <div class="col-md-9 file-path"><i class="icon-nd-existing" title="Existing path" /> {{existingPath}}</div> </div> <div class="row"> - <div class="span9 file-path"><i class="icon-nd-suggested" title="Suggested path" /> {{newPath}}</div> + <div class="col-md-9 file-path"><i class="icon-nd-suggested" title="Suggested path" /> {{newPath}}</div> </div> </div> </div> diff --git a/src/UI/Rename/RenamePreviewLayoutTemplate.html b/src/UI/Rename/RenamePreviewLayoutTemplate.html index 399801f0d..4418753b8 100644 --- a/src/UI/Rename/RenamePreviewLayoutTemplate.html +++ b/src/UI/Rename/RenamePreviewLayoutTemplate.html @@ -1,26 +1,29 @@ -<div class="rename-preview-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3> - <i class="icon-nd-rename"></i> Organize & Rename - </h3> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="rename-preview-modal"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3> + <i class="icon-nd-rename"></i> Organize & Rename + </h3> - </div> - <div class="modal-body"> - <div class="alert alert-info path-info x-path-info">All paths are relative to: <strong>{{path}}</strong></div> - <div id="rename-previews"></div> - </div> - <div class="modal-footer"> - <span class="rename-all-button x-rename-all-button pull-left"> - <label class="checkbox-button" title="Toggle all"> - <input type="checkbox" checked="checked" class="x-rename-all"/> - <div class="btn btn-icon-only"> - <i class="icon-check"></i> - </div> - </label> - </span> - <button class="btn" data-dismiss="modal">close</button> - <button class="btn btn-primary x-organize">Organize</button> + </div> + <div class="modal-body"> + <div class="alert alert-info path-info x-path-info">All paths are relative to: <strong>{{path}}</strong></div> + <div id="rename-previews"></div> + </div> + <div class="modal-footer"> + <span class="rename-all-button x-rename-all-button pull-left"> + <label class="checkbox-button" title="Toggle all"> + <input type="checkbox" checked="checked" class="x-rename-all"/> + <div class="btn btn-icon-only"> + <i class="icon-check"></i> + </div> + </label> + </span> + <button class="btn" data-dismiss="modal">close</button> + <button class="btn btn-primary x-organize">Organize</button> + </div> + </div> </div> </div> - diff --git a/src/UI/Rename/rename.less b/src/UI/Rename/rename.less index 65f9a3007..cb6c33a4f 100644 --- a/src/UI/Rename/rename.less +++ b/src/UI/Rename/rename.less @@ -14,12 +14,12 @@ .icon-nd-existing:before { .icon(@minus); - color : @errorText; + color : @brand-danger; } .icon-nd-suggested:before { .icon(@plus); - color : @successText; + color : @brand-success; } .rename-checkbox { diff --git a/src/UI/SeasonPass/SeasonPassLayoutTemplate.html b/src/UI/SeasonPass/SeasonPassLayoutTemplate.html index 152d9d158..d64978a30 100644 --- a/src/UI/SeasonPass/SeasonPassLayoutTemplate.html +++ b/src/UI/SeasonPass/SeasonPassLayoutTemplate.html @@ -1,11 +1,11 @@ <div class="row"> - <div class="span12"> + <div class="col-md-12"> <div class="alert alert-info">Season Pass allows you to quickly change the monitored status of seasons for all your series in one place</div> </div> </div> <div class="row"> - <div class="span12"> + <div class="col-md-12"> <div id="x-series"></div> </div> </div> \ No newline at end of file diff --git a/src/UI/SeasonPass/SeriesLayoutTemplate.html b/src/UI/SeasonPass/SeriesLayoutTemplate.html index be084a498..24ce4e9b1 100644 --- a/src/UI/SeasonPass/SeriesLayoutTemplate.html +++ b/src/UI/SeasonPass/SeriesLayoutTemplate.html @@ -1,15 +1,15 @@ <div class="seasonpass-series"> <div class="row"> - <div class="span12"> + <div class="col-md-12"> <i class="icon-chevron-right x-expander expander pull-left"/> <i class="x-series-monitored series-monitor-toggle pull-left" title="Toggle monitored state for entire series"/> - <span class="title span5"> + <span class="title col-md-5"> <a href="{{route}}"> {{title}} </a> </span> - <span class="span3"> + <span class="col-md-3"> <select class="x-season-select season-select"> <option value="-1">Select season...</option> {{#each seasons}} @@ -37,7 +37,7 @@ </div> <div class="row"> - <div class="span11"> + <div class="col-md-11"> <div class="x-season-grid season-grid"> <table class="table table-striped"> <thead> diff --git a/src/UI/Series/Delete/DeleteSeriesTemplate.html b/src/UI/Series/Delete/DeleteSeriesTemplate.html index e54fc750d..c4e047c77 100644 --- a/src/UI/Series/Delete/DeleteSeriesTemplate.html +++ b/src/UI/Series/Delete/DeleteSeriesTemplate.html @@ -1,42 +1,48 @@ -<div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete {{title}}</h3> -</div> -<div class="modal-body delete-series-modal"> - - <div class="row"> - <div class="span2"> - <img class="series-poster" src="{{poster}}" {{defaultImg}}> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Delete {{title}}</h3> </div> - <div class="span7"> - <div class="form-horizontal"> - <h3 class="path">{{path}}</h3> + <div class="modal-body delete-series-modal"> - <div class="control-group"> - <label class="control-label">Delete all files</label> + <div class="row"> + <div class="col-sm-3 hidden-xs"> + <img class="series-poster" src="{{poster}}" {{defaultImg}}> + </div> + <div class="col-sm-9"> + <div class="form-horizontal"> + <h3 class="path">{{path}}</h3> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" class="x-delete-files"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <div class="form-group"> + <label class="col-sm-4 control-label">Delete all files</label> - <div class="btn slide-button btn-danger"/> - </label> + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" class="x-delete-files"/> + <p> + <span>Yes</span> + <span>No</span> + </p> - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Do you want to delete all files from disk?"/> - <i class="icon-nd-form-warning" title="This option is irreversible, use with extreme caution"/> - </span> + <div class="btn slide-button btn-danger"/> + </label> + + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Do you want to delete all files from disk?"/> + <i class="icon-nd-form-warning" title="This option is irreversible, use with extreme caution"/> + </span> + </div> + </div> + </div> </div> </div> </div> </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal">cancel</button> + <button class="btn btn-danger x-confirm-delete">delete</button> + </div> </div> </div> -<div class="modal-footer"> - <button class="btn" data-dismiss="modal">cancel</button> - <button class="btn btn-danger x-confirm-delete">delete</button> -</div> diff --git a/src/UI/Series/Details/InfoViewTemplate.html b/src/UI/Series/Details/InfoViewTemplate.html index 25d203f5b..81fb8d4e5 100644 --- a/src/UI/Series/Details/InfoViewTemplate.html +++ b/src/UI/Series/Details/InfoViewTemplate.html @@ -1,32 +1,37 @@ -<div> - {{qualityProfile qualityProfileId}} - <span class="label label-info">{{network}}</span> - <span class="label label-info">{{runtime}} minutes</span> - <span class="label label-info">{{path}}</span> - {{#if_eq status compare="continuing"}} - <span class="label label-info">Continuing</span> - {{else}} - <span class="label">Ended</span> - {{/if_eq}} +<div class="row"> + <div class="col-md-9"> + {{qualityProfile qualityProfileId}} + <span class="label label-info">{{network}}</span> + <span class="label label-info">{{runtime}} minutes</span> + <span class="label label-info">{{path}}</span> + {{#if_eq status compare="continuing"}} + <span class="label label-info">Continuing</span> + {{else}} + <span class="label label-default">Ended</span> + {{/if_eq}} + </div> + <div class="col-md-3"> + <span class="series-info-links"> + <a href="{{traktUrl}}" class="label label-info">Trakt</a> - <span class="pull-right"> - <a href="{{traktUrl}}" class="label label-info">Trakt</a> + {{#if imdbId}} + <a href="{{imdbUrl}}" class="label label-info">IMDB</a> + {{/if}} - {{#if imdbId}} - <a href="{{imdbUrl}}" class="label label-info">IMDB</a> - {{/if}} + {{#if tvdbId}} + <a href="{{tvdbUrl}}" class="label label-info">TVDB</a> + {{/if}} - {{#if tvdbId}} - <a href="{{tvdbUrl}}" class="label label-info">TVDB</a> - {{/if}} - - {{#if tvRageId}} - <a href="{{tvRageUrl}}" class="label label-info">TVRage</a> - {{/if}} - </span> + {{#if tvRageId}} + <a href="{{tvRageUrl}}" class="label label-info">TVRage</a> + {{/if}} + </span> + </div> </div> -<div> - {{#each alternativeTitles}} - <span class="label">{{this}}</span> - {{/each}} +<div class="row"> + <div class="col-md-12"> + {{#each alternativeTitles}} + <span class="label label-default">{{this}}</span> + {{/each}} + </div> </div> \ No newline at end of file diff --git a/src/UI/Series/Details/SeasonLayout.js b/src/UI/Series/Details/SeasonLayout.js index 6a0ee80d4..f9f3f08b3 100644 --- a/src/UI/Series/Details/SeasonLayout.js +++ b/src/UI/Series/Details/SeasonLayout.js @@ -92,7 +92,6 @@ define( initialize: function (options) { - if (!options.episodeCollection) { throw 'episodeCollection is needed'; } diff --git a/src/UI/Series/Details/SeasonLayoutTemplate.html b/src/UI/Series/Details/SeasonLayoutTemplate.html index 4e5f598de..ff5e8b95a 100644 --- a/src/UI/Series/Details/SeasonLayoutTemplate.html +++ b/src/UI/Series/Details/SeasonLayoutTemplate.html @@ -31,7 +31,7 @@ </div> </span> </h2> - <div class="x-episode-grid"></div> + <div class="x-episode-grid table-responsive"></div> <div class="show-hide-episodes x-show-hide-episodes"> <h4> {{#if showingEpisodes}} diff --git a/src/UI/Series/Details/SeriesDetailsTemplate.html b/src/UI/Series/Details/SeriesDetailsTemplate.html index c653d54cb..b05a0ef86 100644 --- a/src/UI/Series/Details/SeriesDetailsTemplate.html +++ b/src/UI/Series/Details/SeriesDetailsTemplate.html @@ -1,5 +1,5 @@ <div class="row series-page-header"> - <div class="span11"> + <div class="col-md-12"> <div class="row"> <h1> <i class="x-monitored" title="Toggle monitored state for entire series"/> @@ -24,9 +24,7 @@ <div class="row series-detail-overview"> {{overview}} </div> - <div id="info" class="row"> - - </div> + <div id="info" class="row series-info"></div> </div> </div> <div id="seasons"></div> diff --git a/src/UI/Series/Edit/EditSeriesView.js b/src/UI/Series/Edit/EditSeriesView.js index 29d922fbb..76801c137 100644 --- a/src/UI/Series/Edit/EditSeriesView.js +++ b/src/UI/Series/Edit/EditSeriesView.js @@ -27,7 +27,6 @@ define( this.model.set('qualityProfiles', QualityProfiles); }, - _saveSeries: function () { var self = this; diff --git a/src/UI/Series/Edit/EditSeriesViewTemplate.html b/src/UI/Series/Edit/EditSeriesViewTemplate.html index 4476abad0..777e7986f 100644 --- a/src/UI/Series/Edit/EditSeriesViewTemplate.html +++ b/src/UI/Series/Edit/EditSeriesViewTemplate.html @@ -1,88 +1,90 @@ -<div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>{{title}}</h3> -</div> -<div class="modal-body edit-series-modal"> - <div class="row"> - <div class="span2"> - <img class="series-poster" src="{{poster}}" - {{defaultImg}}> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>{{title}}</h3> </div> - <div class="span7"> - <div class="form-horizontal"> - - <div class="control-group"> - <label class="control-label">Monitored</label> - - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="monitored"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Should NzbDrone download episodes for this series?"/> - </span> - </div> + <div class="modal-body edit-series-modal"> + <div class="row"> + <div class="col-sm-3 hidden-xs"> + <img class="series-poster" src="{{poster}}" + {{defaultImg}}> </div> + <div class="col-sm-9"> + <div class="form-horizontal"> - <div class="control-group"> - <label class="control-label">Use Season Folder</label> + <div class="form-group"> + <label class="col-sm-4 control-label">Monitored</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="seasonFolder"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="monitored"/> + <p> + <span>Yes</span> + <span>No</span> + </p> - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn btn-primary slide-button"/> + </label> - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Should downloaded episodes be stored in season folders?"/> - </span> - </div> - </div> + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Should NzbDrone download episodes for this series?"/> + </span> + </div> + </div> + </div> - <div class="control-group"> - <label class="control-label" for="inputQualityProfile">Quality Profile</label> + <div class="form-group"> + <label class="col-sm-4 control-label">Use Season Folder</label> - <div class="controls"> + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="seasonFolder"/> + <p> + <span>Yes</span> + <span>No</span> + </p> - <select class="x-quality-profile" id="inputQualityProfile" name="qualityProfileId"> - {{#each qualityProfiles.models}} - <option value="{{id}}">{{attributes.name}}</option> - {{/each}} - </select> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Which Quality Profile should NzbDrone use to download episodes?"/> - </span> - </div> - </div> + <div class="btn btn-primary slide-button"/> + </label> - <div class="control-group"> - <label class="control-label" for="inputPath">Path</label> + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Should downloaded episodes be stored in season folders?"/> + </span> + </div> + </div> + </div> - <div class="controls"> - <input type="text" id="inputPath" class="x-path" placeholder="Path" name="path"> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Where should NzbDrone store episodes for this series?"/> - </span> + <div class="form-group"> + <label class="col-sm-4 control-label" for="inputQualityProfile">Quality Profile</label> + + <div class="col-sm-4"> + <select class="form-control x-quality-profile" id="inputQualityProfile" name="qualityProfileId"> + {{#each qualityProfiles.models}} + <option value="{{id}}">{{attributes.name}}</option> + {{/each}} + </select> + + </div> + </div> + + <div class="form-group"> + <label class="col-sm-4 control-label" for="inputPath">Path</label> + + <div class="col-sm-6"> + <input type="text" id="inputPath" class="form-control x-path" placeholder="Path" name="path"> + </div> + </div> </div> </div> </div> </div> + <div class="modal-footer"> + <button class="btn btn-danger pull-left x-remove">delete</button> + <button class="btn" data-dismiss="modal">cancel</button> + <button class="btn btn-primary x-save">save</button> + </div> </div> </div> -<div class="modal-footer"> - <button class="btn btn-danger pull-left x-remove">delete</button> - <button class="btn" data-dismiss="modal">cancel</button> - <button class="btn btn-primary x-save">save</button> -</div> diff --git a/src/UI/Series/Editor/Organize/OrganizeFilesViewTemplate.html b/src/UI/Series/Editor/Organize/OrganizeFilesViewTemplate.html index 09465b198..a81eb1e16 100644 --- a/src/UI/Series/Editor/Organize/OrganizeFilesViewTemplate.html +++ b/src/UI/Series/Editor/Organize/OrganizeFilesViewTemplate.html @@ -1,23 +1,27 @@ -<div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Organize of Selected Series</h3> -</div> -<div class="modal-body update-files-series-modal"> - <div class="alert alert-info"> - <button type="button" class="close" data-dismiss="alert">×</button> - Tip: To preview a rename... select "Cancel" then any series title and use the <i data-original-title="" class="icon-nd-rename" title=""></i> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Organize of Selected Series</h3> + </div> + <div class="modal-body update-files-series-modal"> + <div class="alert alert-info"> + <button type="button" class="close" data-dismiss="alert">×</button> + Tip: To preview a rename... select "Cancel" then any series title and use the <i data-original-title="" class="icon-nd-rename" title=""></i> + </div> + + Are you sure you want to update all files in the {{numberOfSeries}} selected series? + + {{debug}} + <ul class="selected-series"> + {{#each series}} + <li>{{title}}</li> + {{/each}} + </ul> + </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal">cancel</button> + <button class="btn btn-danger x-confirm-organize">organize</button> + </div> </div> - - Are you sure you want to update all files in the {{numberOfSeries}} selected series? - - {{debug}} - <ul class="selected-series"> - {{#each series}} - <li>{{title}}</li> - {{/each}} - </ul> -</div> -<div class="modal-footer"> - <button class="btn" data-dismiss="modal">cancel</button> - <button class="btn btn-danger x-confirm-organize">organize</button> </div> diff --git a/src/UI/Series/Editor/SeriesEditorFooterViewTemplate.html b/src/UI/Series/Editor/SeriesEditorFooterViewTemplate.html index b6827fbb5..3ff399d6a 100644 --- a/src/UI/Series/Editor/SeriesEditorFooterViewTemplate.html +++ b/src/UI/Series/Editor/SeriesEditorFooterViewTemplate.html @@ -1,22 +1,29 @@ <div class="series-editor-footer"> <div class="row"> - <div class="span2">Monitored</div> - <div class="span2">Quality Profile</div> - <div class="span2">Season Folder</div> - <div class="span4">Root Folder</div> - </div> + <div class="form-group col-md-2"> + <label>Monitored</label> - <div class="row"> - <div class="span2"> - <select class="span2 x-monitored"> + <select class="form-control x-monitored"> <option value="noChange">No change</option> <option value="true">Monitored</option> <option value="false">Unmonitored</option> </select> </div> - <div class="span2"> - <select class="span2 x-quality-profiles"> + <div class="form-group col-md-2"> + <label>Season Folder</label> + + <select class="form-control x-season-folder"> + <option value="noChange">No change</option> + <option value="true">Yes</option> + <option value="false">No</option> + </select> + </div> + + <div class="form-group col-md-2"> + <label>Quality Profile</label> + + <select class="form-control x-quality-profiles"> <option value="noChange">No change</option> {{#each qualityProfiles.models}} <option value="{{id}}">{{attributes.name}}</option> @@ -24,28 +31,24 @@ </select> </div> - <div class="span2"> - <select class="span2 x-season-folder"> - <option value="noChange">No change</option> - <option value="true">Yes</option> - <option value="false">No</option> - </select> - </div> + <div class="form-group col-md-3"> + <label>Root Folder</label> - <div class="span3"> - <select class="span3 x-root-folder" validation-name="RootFolderPath"> + <select class="form-control x-root-folder" validation-name="RootFolderPath"> <option value="noChange">No change</option> {{#each rootFolders}} - <option value="{{id}}">{{path}}</option> + <option value="{{id}}">{{path}}</option> {{/each}} <option value="addNew">Add a different path</option> </select> </div> - <span class="pull-right"> - <span class="selected-count x-selected-count">0 series selected</span> - <button class="btn btn-primary x-save">Save</button> - <button class="btn btn-danger x-organize-files">Organize</button> - </span> + <div class="form-group col-md-3 actions"> + <label class="x-selected-count">0 series selected</label> + <div> + <button class="btn btn-primary x-save">Save</button> + <button class="btn btn-danger x-organize-files">Organize</button> + </div> + </div> </div> -</div> \ No newline at end of file +</div> diff --git a/src/UI/Series/Editor/SeriesEditorLayoutTemplate.html b/src/UI/Series/Editor/SeriesEditorLayoutTemplate.html index 470e663eb..8ecc8cf5d 100644 --- a/src/UI/Series/Editor/SeriesEditorLayoutTemplate.html +++ b/src/UI/Series/Editor/SeriesEditorLayoutTemplate.html @@ -1,7 +1,7 @@ <div id="x-toolbar"></div> <div class="row"> - <div class="span12"> - <div id="x-series-editor" class="series-"></div> + <div class="col-md-12"> + <div id="x-series-editor" class="table-responsive"></div> </div> </div> \ No newline at end of file diff --git a/src/UI/Series/Index/EmptyTemplate.html b/src/UI/Series/Index/EmptyTemplate.html index 95b3d1f2e..4dcbb9624 100644 --- a/src/UI/Series/Index/EmptyTemplate.html +++ b/src/UI/Series/Index/EmptyTemplate.html @@ -1,11 +1,11 @@ <div class="row"> - <div class="well span11"> + <div class="well col-md-11"> <i class="icon-comment"/> You must be new around here, You should add some series. </div> </div> -<div class="row span3 offset4"> - <a href="/addseries" class='btn btn-large btn-block btn-success x-add-series'> +<div class="row col-md-3 offset4"> + <a href="/addseries" class='btn btn-lg btn-block btn-success x-add-series'> <i class='icon-nd-add'></i> Add Series </a> diff --git a/src/UI/Series/Index/EpisodeProgressPartial.html b/src/UI/Series/Index/EpisodeProgressPartial.html index 51ff6588d..3f1072eee 100644 --- a/src/UI/Series/Index/EpisodeProgressPartial.html +++ b/src/UI/Series/Index/EpisodeProgressPartial.html @@ -1,15 +1,4 @@ -{{#if_eq episodeFileCount compare=episodeCount}} - {{#if_eq status compare="continuing"}} - <div class="progress episode-progress"> - {{else}} - <div class="progress progress-success episode-progress"> - {{/if_eq}} - -{{else}} - <div class="progress progress-danger episode-progress"> -{{/if_eq}} - +<div class="progress episode-progress"> <span class="progressbar-back-text">{{episodeFileCount}} / {{episodeCount}}</span> - - <div class="bar" style="width:{{percentOfEpisodes}}%"><span class="progressbar-front-text">{{episodeFileCount}} / {{episodeCount}}</span></div> + <div class="progress-bar {{EpisodeProgressClass}} episode-progress" style="width:{{percentOfEpisodes}}%"><span class="progressbar-front-text">{{episodeFileCount}} / {{episodeCount}}</span></div> </div> \ No newline at end of file diff --git a/src/UI/Series/Index/FooterViewTemplate.html b/src/UI/Series/Index/FooterViewTemplate.html index 72e28d22b..ea0da0473 100644 --- a/src/UI/Series/Index/FooterViewTemplate.html +++ b/src/UI/Series/Index/FooterViewTemplate.html @@ -1,42 +1,45 @@ <div class="row"> - <div class="series-legend legend span3"> + <div class="series-legend legend col-xs-6 col-sm-4"> <ul class='legend-labels'> <li><span class="progress-primary"></span>Continuing (All Episodes downloaded)</li> <li><span class="progress-success"></span>Ended (All Episodes downloaded)</li> <li><span class="progress-danger"></span>Missing Episodes</li> </ul> </div> + <div class="col-xs-5 col-sm-7"> + <div class="row"> + <div class="series-stats col-sm-4"> + <dl class="dl-horizontal"> + <dt>Series</dt> + <dd>{{series}}</dd> - <div class="series-stats span2"> - <dl class="dl-horizontal"> - <dt>Series</dt> - <dd>{{series}}</dd> + <dt>Ended</dt> + <dd>{{ended}}</dd> - <dt>Ended</dt> - <dd>{{ended}}</dd> + <dt>Continuing</dt> + <dd>{{continuing}}</dd> + </dl> + </div> - <dt>Continuing</dt> - <dd>{{continuing}}</dd> - </dl> - </div> + <div class="series-stats col-sm-4"> + <dl class="dl-horizontal"> + <dt>Monitored</dt> + <dd>{{monitored}}</dd> - <div class="series-stats span2"> - <dl class="dl-horizontal"> - <dt>Monitored</dt> - <dd>{{monitored}}</dd> + <dt>Unmonitored</dt> + <dd>{{unmonitored}}</dd> + </dl> + </div> - <dt>Unmonitored</dt> - <dd>{{unmonitored}}</dd> - </dl> - </div> + <div class="series-stats col-sm-4"> + <dl class="dl-horizontal"> + <dt>Episodes</dt> + <dd>{{episodes}}</dd> - <div class="series-stats span2"> - <dl class="dl-horizontal"> - <dt>Episodes</dt> - <dd>{{episodes}}</dd> - - <dt>Files</dt> - <dd>{{episodeFiles}}</dd> - </dl> + <dt>Files</dt> + <dd>{{episodeFiles}}</dd> + </dl> + </div> + </div> </div> </div> diff --git a/src/UI/Series/Index/Overview/SeriesOverviewCollectionViewTemplate.html b/src/UI/Series/Index/Overview/SeriesOverviewCollectionViewTemplate.html index 3c7a8f8d1..831ad1fea 100644 --- a/src/UI/Series/Index/Overview/SeriesOverviewCollectionViewTemplate.html +++ b/src/UI/Series/Index/Overview/SeriesOverviewCollectionViewTemplate.html @@ -1,5 +1 @@ -<div class="row"> - <div class="span12"> - <div id="x-series-list"/> - </div> -</div> +<div id="x-series-list"/> diff --git a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html b/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html index 886e977df..a43ee6b6e 100644 --- a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html +++ b/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html @@ -1,47 +1,53 @@ <div class="series-item"> <div class="row"> - <div class="span2"> + <div class="col-md-2 col-xs-3"> <a href="{{route}}"> <img class="series-poster" src="{{poster}}" {{defaultImg}}> </a> </div> - <div class="span10"> + <div class="col-md-10 col-xs-9"> <div class="row"> - <div class="span9"> + <div class="col-md-10 col-xs-10"> <a href="{{route}}" target="_blank"> <h2>{{title}}</h2> </a> </div> - <div class="span1"> - <div class="pull-right"> - <i class="icon-cog x-edit" title="Edit Series"/> + <div class="col-md-2 col-xs-2"> + <div class="pull-right series-overview-list-actions"> + <i class="icon-nd-edit x-edit" title="Edit Series"/> <i class="icon-remove x-remove" title="Delete Series"/> </div> </div> </div> <div class="row"> - <a href="{{route}}"> - <div class="span10"> - {{overview}} - </div> - </a> + <div class="col-md-12 col-xs-12"> + <a href="{{route}}"> + <div> + {{overview}} + </div> + </a> + </div> </div> - <div class="row"> </div> <div class="row"> - <div class="span8"> + <div class="col-md-12"> +   + </div> + </div> + <div class="row"> + <div class="col-md-10 col-xs-8"> {{#if_eq status compare="continuing"}} {{#if nextAiring}} - <span class="label">{{NextAiring nextAiring}}</span> + <span class="label label-default">{{NextAiring nextAiring}}</span> {{/if}} {{else}} - <span class="label label-important">Ended</span> + <span class="label label-danger">Ended</span> {{/if_eq}} {{seasonCountHelper}} {{qualityProfile qualityProfileId}} </div> - <div class="span2"> + <div class="col-md-2 col-xs-4"> {{> EpisodeProgressPartial }} </div> </div> diff --git a/src/UI/Series/Index/Posters/SeriesPostersCollectionViewTemplate.html b/src/UI/Series/Index/Posters/SeriesPostersCollectionViewTemplate.html index a09e98ac6..1e2dac8b4 100644 --- a/src/UI/Series/Index/Posters/SeriesPostersCollectionViewTemplate.html +++ b/src/UI/Series/Index/Posters/SeriesPostersCollectionViewTemplate.html @@ -1,5 +1 @@ -<div class="row"> - <div class="span12"> - <ul id="x-series-posters" class="series-posters"></ul> - </div> -</div> \ No newline at end of file +<ul id="x-series-posters" class="series-posters"></ul> \ No newline at end of file diff --git a/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.html b/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.html index 7ef1c9c4e..bf3de36b8 100644 --- a/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.html +++ b/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.html @@ -1,32 +1,28 @@ <div class="series-posters-item"> - <div class="row"> - <div class="span2"> - <div class="center"> - <div class="series-poster-container x-series-poster"> - <div class="series-controls"> - <i class="icon-cog x-edit" title="Edit Series"/> - <i class="icon-remove x-remove" title="Delete Series"/> - </div> - {{#unless_eq status compare="continuing"}} - <div class="ended-banner">Ended</div> - {{/unless_eq}} - <a href="{{route}}"> - <img class="series-poster" src="{{poster}}" {{defaultImg}}> - <div class="center title">{{title}}</div> - </a> - </div> + <div class="center"> + <div class="series-poster-container x-series-poster"> + <div class="series-controls"> + <i class="icon-nd-edit x-edit" title="Edit Series"/> + <i class="icon-remove x-remove" title="Delete Series"/> </div> + {{#unless_eq status compare="continuing"}} + <div class="ended-banner">Ended</div> + {{/unless_eq}} + <a href="{{route}}"> + <img class="series-poster" src="{{poster}}" {{defaultImg}}> + <div class="center title">{{title}}</div> + </a> + </div> + </div> - <div class="center"> - <div class="labels"> - {{#if_eq status compare="continuing"}} - {{#if nextAiring}} - <span class="label label-inverse">{{NextAiring nextAiring}}</span> - {{/if}} - {{/if_eq}} - {{> EpisodeProgressPartial }} - </div> - </div> + <div class="center"> + <div class="labels"> + {{#if_eq status compare="continuing"}} + {{#if nextAiring}} + <span class="label label-default">{{NextAiring nextAiring}}</span> + {{/if}} + {{/if_eq}} + {{> EpisodeProgressPartial }} </div> </div> </div> diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index bfd37e8e7..292d7d37e 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -96,6 +96,7 @@ define( leftSideButtons: { type : 'default', storeState: false, + collapse : true, items : [ { diff --git a/src/UI/Series/Index/SeriesIndexLayoutTemplate.html b/src/UI/Series/Index/SeriesIndexLayoutTemplate.html index 08353e5c1..12426b7d1 100644 --- a/src/UI/Series/Index/SeriesIndexLayoutTemplate.html +++ b/src/UI/Series/Index/SeriesIndexLayoutTemplate.html @@ -4,7 +4,7 @@ </div> <div class="row"> - <div class="span12"> + <div class="col-md-12 table-responsive"> <div id="x-series"></div> </div> </div> diff --git a/src/UI/Series/SeriesController.js b/src/UI/Series/SeriesController.js index ed9334b08..2247ccc02 100644 --- a/src/UI/Series/SeriesController.js +++ b/src/UI/Series/SeriesController.js @@ -10,16 +10,19 @@ define( return NzbDroneController.extend({ + _originalInit: NzbDroneController.prototype.initialize, initialize: function () { this.route('', this.series); this.route('series', this.series); this.route('series/:query', this.seriesDetails); + + this._originalInit.apply(this, arguments); }, series: function () { this.setTitle('NzbDrone'); - AppLayout.mainRegion.show(new SeriesIndexLayout()); + this.showMainRegion(new SeriesIndexLayout()); }, seriesDetails: function (query) { @@ -28,7 +31,7 @@ define( if (series.length !== 0) { var targetSeries = series[0]; this.setTitle(targetSeries.get('title')); - AppLayout.mainRegion.show(new SeriesDetailsLayout({ model: targetSeries })); + this.showMainRegion(new SeriesDetailsLayout({ model: targetSeries })); } else { this.showNotFound(); diff --git a/src/UI/Series/series.less b/src/UI/Series/series.less index 60cceb63f..e6b32703c 100644 --- a/src/UI/Series/series.less +++ b/src/UI/Series/series.less @@ -1,7 +1,13 @@ +@import "../Content/Bootstrap/variables"; @import "../Shared/Styles/card.less"; @import "../Shared/Styles/clickable.less"; @import "../Content/prefixer"; +.series-poster { + min-width: 56px; + max-width: 100%; +} + .edit-series-modal, .delete-series-modal { overflow : visible; @@ -63,7 +69,11 @@ } .series-posters { - list-style-type : none; + list-style-type: none; + + @media (max-width: @screen-xs-max) { + padding : 0px; + } li { display : inline-block; @@ -75,7 +85,7 @@ .card; .clickable; margin-bottom : 20px; - height : 295px; + height : 315px; .center { display : block; @@ -89,7 +99,7 @@ left : 0px; width : 170px; - .progressbar-front-text { + .progressbar-front-text, .progressbar-back-text { width : 170px; } } @@ -113,6 +123,26 @@ .opacity(1); } } + + @media (max-width: @screen-xs-max) { + height : 235px; + margin : 5px; + padding : 6px 5px; + + .center { + .progress { + width : 125px; + + .progressbar-front-text, .progressbar-back-text { + width : 125px + } + } + } + + .labels { + width: 125px; + } + } } .series-poster-container { @@ -170,6 +200,18 @@ font-size : 34px; line-height : 34px; } + + @media (max-width: @screen-xs-max) { + .series-poster { + width : 120px; + height : 176px; + } + + .ended-banner { + top : 145px; + left : -137px; + } + } } } @@ -209,12 +251,6 @@ margin-top : 30px; font-size : 12px; } - - .search-buttons { - width : 400px; - margin-left : auto; - margin-right : auto; - } } .season-grid { @@ -300,24 +336,22 @@ } } +//Overview List +.series-overview-list-actions { + min-width: 56px; + max-width: 56px; +} + //Editor .series-editor-footer { - width: 1160px; + max-width: 1160px; color: #f5f5f5; margin-left: auto; margin-right: auto; - .selected-count { - margin-right: 10px; - } - - .row { - margin-left: -40px; - } - - .span2 { - width: 160px; + .form-group { + padding-top: 0px; } } @@ -338,4 +372,17 @@ cursor: not-allowed; } } +} + +.series-info { + .row { + margin-bottom: 5px; + } + + .series-info-links { + @media (max-width: @screen-sm-max) { + display : inline-block; + margin-top : 5px; + } + } } \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate.html b/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate.html index 0dc1bb250..60fd4ed0b 100644 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate.html +++ b/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate.html @@ -1,12 +1,16 @@ -<div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Add Download Client</h3> -</div> -<div class="modal-body"> - <div class="add-download-client add-thingies"> - <ul class="items"></ul> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Add Download Client</h3> + </div> + <div class="modal-body"> + <div class="add-download-client add-thingies"> + <ul class="items"></ul> + </div> + </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal">close</button> + </div> </div> </div> -<div class="modal-footer"> - <button class="btn" data-dismiss="modal">close</button> -</div> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.html b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.html index dfaee211e..f892a4d01 100644 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.html +++ b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.html @@ -1,10 +1,6 @@ -<div class="add-thingy span3"> - <div class="row"> - <div class="span3"> - {{implementation}} - {{#if link}} - <a href="{{link}}"><i class="icon-info-sign"/></a> - {{/if}} - </div> - </div> +<div class="add-thingy"> + {{implementation}} + {{#if link}} + <a href="{{link}}"><i class="icon-info-sign"/></a> + {{/if}} </div> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate.html b/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate.html index b4fff099b..fe950ffdc 100644 --- a/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate.html +++ b/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate.html @@ -1,11 +1,15 @@ -<div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete Download Client</h3> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Delete Download Client</h3> + </div> + <div class="modal-body"> + <p>Are you sure you want to delete '{{name}}'?</p> + </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal">cancel</button> + <button class="btn btn-danger x-confirm-delete">delete</button> + </div> + </div> </div> -<div class="modal-body"> - <p>Are you sure you want to delete '{{name}}'?</p> -</div> -<div class="modal-footer"> - <button class="btn" data-dismiss="modal">cancel</button> - <button class="btn btn-danger x-confirm-delete">delete</button> -</div> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.html b/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.html index 9b9692658..be4c04f09 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.html +++ b/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.html @@ -1,8 +1,8 @@ <fieldset> <legend>Download Clients</legend> <div class="row"> - <div class="span12"> - <ul id="x-download-clients" class="download-client-list"> + <div class="col-md-12"> + <ul id="x-download-clients" class="download-client-list thingies"> <li> <div class="download-client-item thingy add-card x-add-card"> <span class="center well"> diff --git a/src/UI/Settings/DownloadClient/DownloadClientItemView.js b/src/UI/Settings/DownloadClient/DownloadClientItemView.js index 0d021c059..ae552f53c 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientItemView.js +++ b/src/UI/Settings/DownloadClient/DownloadClientItemView.js @@ -4,17 +4,15 @@ define( [ 'AppLayout', 'marionette', - 'Settings/DownloadClient/Edit/DownloadClientEditView', - 'Settings/DownloadClient/Delete/DownloadClientDeleteView' - ], function (AppLayout, Marionette, EditView, DeleteView) { + 'Settings/DownloadClient/Edit/DownloadClientEditView' + ], function (AppLayout, Marionette, EditView) { return Marionette.ItemView.extend({ template: 'Settings/DownloadClient/DownloadClientItemViewTemplate', tagName : 'li', events: { - 'click .x-edit' : '_edit', - 'click .x-delete' : '_delete' + 'click' : '_edit' }, initialize: function () { @@ -24,11 +22,6 @@ define( _edit: function () { var view = new EditView({ model: this.model, downloadClientCollection: this.model.collection }); AppLayout.modalRegion.show(view); - }, - - _delete: function () { - var view = new DeleteView({ model: this.model}); - AppLayout.modalRegion.show(view); } }); }); diff --git a/src/UI/Settings/DownloadClient/DownloadClientItemViewTemplate.html b/src/UI/Settings/DownloadClient/DownloadClientItemViewTemplate.html index e8550d7f5..80ab05412 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientItemViewTemplate.html +++ b/src/UI/Settings/DownloadClient/DownloadClientItemViewTemplate.html @@ -1,17 +1,13 @@ -<div class="download-client-item thingy"> +<div class="download-client-item thingy" title="Click to edit"> <div> <h3>{{name}}</h3> - <span class="btn-group pull-right"> - <button class="btn btn-mini btn-icon-only x-edit"><i class="icon-nd-edit"/></button> - <button class="btn btn-mini btn-icon-only x-delete"><i class="icon-nd-delete"/></button> - </span> </div> <div class="settings"> {{#if enable}} <span class="label label-success">Enabled</span> {{else}} - <span class="label">Not Enabled</span> + <span class="label label-default">Not Enabled</span> {{/if}} </div> </div> diff --git a/src/UI/Settings/DownloadClient/Edit/DownloadClientEditViewTemplate.html b/src/UI/Settings/DownloadClient/Edit/DownloadClientEditViewTemplate.html index 53aa004dd..27032ead8 100644 --- a/src/UI/Settings/DownloadClient/Edit/DownloadClientEditViewTemplate.html +++ b/src/UI/Settings/DownloadClient/Edit/DownloadClientEditViewTemplate.html @@ -1,59 +1,65 @@ -<div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - {{#if id}} - <h3>Edit - {{implementation}}</h3> - {{else}} - <h3>Add - {{implementation}}</h3> - {{/if}} -</div> -<div class="modal-body download-client-modal"> - <div class="form-horizontal"> - <div class="control-group"> - <label class="control-label">Name</label> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + {{#if id}} + <h3>Edit - {{implementation}}</h3> + {{else}} + <h3>Add - {{implementation}}</h3> + {{/if}} + </div> + <div class="modal-body download-client-modal"> + <div class="form-horizontal"> + <div class="form-group"> + <label class="col-sm-3 control-label">Name</label> - <div class="controls"> - <input type="text" name="name"/> + <div class="col-sm-5"> + <input type="text" name="name" class="form-control"/> + </div> + </div> + + <div class="form-group"> + <label class="col-sm-3 control-label">Enable</label> + + <div class="col-sm-5"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="enable"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + </div> + </div> + </div> + + {{formBuilder}} </div> </div> + <div class="modal-footer"> + {{#if id}} + <button class="btn btn-danger pull-left x-delete">delete</button> + {{else}} + <button class="btn pull-left x-back">back</button> + {{/if}} - <div class="control-group"> - <label class="control-label">Enable</label> + <button class="btn x-test">test <i class="x-test-icon icon-nd-test"/></button> + <button class="btn" data-dismiss="modal">cancel</button> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enable"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn-group"> + <button class="btn btn-primary x-save">save</button> + <button class="btn btn-icon-only btn-primary dropdown-toggle" data-toggle="dropdown"> + <span class="caret"></span> + </button> + <ul class="dropdown-menu"> + <li class="save-and-add x-save-and-add"> + save and add + </li> + </ul> </div> </div> - - {{formBuilder}} - </div> -</div> -<div class="modal-footer"> - {{#if id}} - <button class="btn btn-danger pull-left x-delete">delete</button> - {{else}} - <button class="btn pull-left x-back">back</button> - {{/if}} - - <button class="btn x-test">test <i class="x-test-icon icon-nd-test"/></button> - <button class="btn" data-dismiss="modal">cancel</button> - - <div class="btn-group"> - <button class="btn btn-primary x-save">save</button> - <button class="btn btn-icon-only btn-primary dropdown-toggle" data-toggle="dropdown"> - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - <li class="save-and-add x-save-and-add"> - save and add - </li> - </ul> </div> </div> diff --git a/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html b/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html index 70f0a0dba..0bf3acc39 100644 --- a/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html +++ b/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html @@ -1,100 +1,106 @@ <fieldset class="advanced-setting"> <legend>Failed Download Handling</legend> - <div class="control-group"> - <label class="control-label">Enable</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Enable</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enableFailedDownloadHandling" class="x-failed-download-handling"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="enableFailedDownloadHandling" class="x-failed-download-handling"/> + <p> + <span>Yes</span> + <span>No</span> + </p> - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn btn-primary slide-button"/> + </label> - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Process failed downloads and blacklist the release"/> - </span> + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Process failed downloads and blacklist the release"/> + </span> + </div> </div> </div> <div class="x-failed-download-options"> - <div class="control-group"> - <label class="control-label">Redownload</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Redownload</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="autoRedownloadFailed"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="autoRedownloadFailed"/> + <p> + <span>Yes</span> + <span>No</span> + </p> - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn btn-primary slide-button"/> + </label> - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Automatically search for and attempt to download another release when a download fails?"/> - </span> + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Automatically search for and attempt to download another release when a download fails?"/> + </span> + </div> </div> </div> - <div class="control-group"> - <label class="control-label">Remove</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Remove</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="removeFailedDownloads"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="removeFailedDownloads"/> + <p> + <span>Yes</span> + <span>No</span> + </p> - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn btn-primary slide-button"/> + </label> - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Automatically remove failed downloads from history and encrypted downloads from queue?"/> - </span> + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Automatically remove failed downloads from history and encrypted downloads from queue?"/> + </span> + </div> </div> </div> - <div class="control-group advanced-setting"> - <label class="control-label">Grace Period</label> + <div class="form-group advanced-setting"> + <label class="col-sm-3 control-label">Grace Period</label> - <div class="controls"> - <input type="number" min="1" max="24" name="blacklistGracePeriod"/> + <div class="col-sm-1 col-sm-push-2 help-inline"> + <i class="icon-nd-form-info" title="Age in hours (since posting) where a release can be retried instead of immediately blacklisted"/> + </div> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Age in hours that a release will remain in the download client and retried"/> - </span> + <div class="col-sm-2 col-sm-pull-1"> + <input type="number" min="1" max="24" name="blacklistGracePeriod" class="form-control"/> </div> </div> - <div class="control-group advanced-setting"> - <label class="control-label">Retry Interval</label> + <div class="form-group advanced-setting"> + <label class="col-sm-3 control-label">Retry Interval</label> - <div class="controls"> - <input type="number" min="5" max="120" name="blacklistRetryInterval"/> - - <span class="help-inline"> + <div class="col-sm-1 col-sm-push-2 help-inline"> <i class="icon-nd-form-info" title="Time in minutes before a failed download for a recent release will be retried"/> - </span> + </div> + + <div class="col-sm-2 col-sm-pull-1"> + <input type="number" min="5" max="120" name="blacklistRetryInterval" class="form-control"/> </div> </div> - <div class="control-group advanced-setting"> - <label class="control-label">Retry Count</label> + <div class="form-group advanced-setting"> + <label class="col-sm-3 control-label">Retry Count</label> - <div class="controls"> - <input type="number" min="0" max="10" name="blacklistRetryLimit"/> - - <span class="help-inline"> + <div class="col-sm-1 col-sm-push-2 help-inline"> <i class="icon-nd-form-info" title="Number of times to retry a release before it is blacklisted"/> - </span> + </div> + + <div class="col-sm-2 col-sm-pull-1"> + <input type="number" min="0" max="10" name="blacklistRetryLimit" class="form-control"/> </div> </div> </div> diff --git a/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsViewTemplate.html b/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsViewTemplate.html index 352cb45f4..f5feb5a57 100644 --- a/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsViewTemplate.html +++ b/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsViewTemplate.html @@ -1,26 +1,28 @@ -<fieldset"> +<fieldset> <legend>Options</legend> - <div class="control-group"> - <label class="control-label">Drone Factory</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Drone Factory</label> - <div class="controls"> - <input type="text" name="downloadedEpisodesFolder" class="x-path"/> - <span class="help-inline"> - <i class="icon-nd-form-info" title="The folder where your download client downloads TV shows to (Completed Download Directory)"/> - <i class="icon-nd-form-warning" title="Do not use the folder that contains some or all of your sorted and named TV shows - doing so could cause data loss"></i> - </span> + <div class="col-sm-1 col-sm-push-8 help-inline"> + <i class="icon-nd-form-info" title="The folder where your download client downloads TV shows to (Completed Download Directory)"/> + <i class="icon-nd-form-warning" title="Do not use the folder that contains some or all of your sorted and named TV shows - doing so could cause data loss"></i> + </div> + + <div class="col-sm-8 col-sm-pull-1"> + <input type="text" name="downloadedEpisodesFolder" class="form-control x-path" /> </div> </div> - <div class="control-group advanced-setting"> - <label class="control-label">Drone Factory Interval</label> + <div class="form-group advanced-setting"> + <label class="col-sm-3 control-label">Drone Factory Interval</label> - <div class="controls"> - <input type="number" name="downloadedEpisodesScanInterval"/> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Interval in minutes to scan the Drone Factory. Set to zero to disable."/> - <i class="icon-nd-form-warning" title="Setting a high interval or disabling scanning will prevent episodes from being imported."></i> - </span> + <div class="col-sm-1 col-sm-push-2 help-inline"> + <i class="icon-nd-form-info" title="Interval in minutes to scan the Drone Factory. Set to zero to disable."/> + <i class="icon-nd-form-warning" title="Setting a high interval or disabling scanning will prevent episodes from being imported."></i> + </div> + + <div class="col-sm-2 col-sm-pull-1"> + <input type="number" name="downloadedEpisodesScanInterval" class="form-control" /> </div> </div> </fieldset> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/downloadclient.less b/src/UI/Settings/DownloadClient/downloadclient.less index a41bec135..bd8da872e 100644 --- a/src/UI/Settings/DownloadClient/downloadclient.less +++ b/src/UI/Settings/DownloadClient/downloadclient.less @@ -1,3 +1,5 @@ +@import "../../Shared/Styles/clickable.less"; + .download-client-list { li { display: inline-block; @@ -7,21 +9,25 @@ .download-client-item { + .clickable; + width: 290px; height: 90px; padding: 10px 15px; - h3 { - width: 230px; - } - &.add-card { .center { - margin-top: 15px; + margin-top: -3px; } } } .modal-overflow { overflow-y: visible; +} + +.add-download-client { + li { + width: 33%; + } } \ No newline at end of file diff --git a/src/UI/Settings/General/GeneralView.js b/src/UI/Settings/General/GeneralView.js index 2e63d2de5..b28ed659c 100644 --- a/src/UI/Settings/General/GeneralView.js +++ b/src/UI/Settings/General/GeneralView.js @@ -12,19 +12,22 @@ define( template: 'Settings/General/GeneralViewTemplate', events: { - 'change .x-auth' : '_setAuthOptionsVisibility', - 'change .x-ssl' : '_setSslOptionsVisibility', - 'click .x-reset-api-key' : '_resetApiKey' + 'change .x-auth' : '_setAuthOptionsVisibility', + 'change .x-ssl' : '_setSslOptionsVisibility', + 'click .x-reset-api-key' : '_resetApiKey', + 'change .x-update-mechanism' : '_setScriptGroupVisibility' }, ui: { - authToggle : '.x-auth', - authOptions : '.x-auth-options', - sslToggle : '.x-ssl', - sslOptions : '.x-ssl-options', - resetApiKey : '.x-reset-api-key', - copyApiKey : '.x-copy-api-key', - apiKeyInput : '.x-api-key' + authToggle : '.x-auth', + authOptions : '.x-auth-options', + sslToggle : '.x-ssl', + sslOptions : '.x-ssl-options', + resetApiKey : '.x-reset-api-key', + copyApiKey : '.x-copy-api-key', + apiKeyInput : '.x-api-key', + updateMechanism : '.x-update-mechanism', + scriptGroup : '.x-script-group' }, initialize: function () { @@ -40,6 +43,10 @@ define( this.ui.sslOptions.hide(); } + if (!this._showScriptGroup()) { + this.ui.scriptGroup.hide(); + } + CommandController.bindToCommand({ element: this.ui.resetApiKey, command: { @@ -79,7 +86,7 @@ define( }, _resetApiKey: function () { - if (window.confirm("Reset API Key?")) { + if (window.confirm('Reset API Key?')) { CommandController.Execute('resetApiKey', { name : 'resetApiKey' }); @@ -90,6 +97,21 @@ define( if (options.command.get('name') === 'resetapikey') { this.model.fetch(); } + }, + + _setScriptGroupVisibility: function () { + + if (this._showScriptGroup()) { + this.ui.scriptGroup.slideDown(); + } + + else { + this.ui.scriptGroup.slideUp(); + } + }, + + _showScriptGroup: function () { + return this.ui.updateMechanism.val() === 'script'; } }); diff --git a/src/UI/Settings/General/GeneralViewTemplate.html b/src/UI/Settings/General/GeneralViewTemplate.html index f26bfb6fe..60e42c462 100644 --- a/src/UI/Settings/General/GeneralViewTemplate.html +++ b/src/UI/Settings/General/GeneralViewTemplate.html @@ -2,138 +2,154 @@ <fieldset> <legend>Start-Up</legend> - <div class="control-group"> - <label class="control-label">Port Number</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Port Number</label> - <div class="controls"> - <input type="number" placeholder="8989" name="port"/> - <span> - <i class="icon-nd-form-warning" title="Requires restart to take effect"/> - </span> + <div class="col-sm-1 col-sm-push-4 help-inline"> + <i class="icon-nd-form-warning" title="Requires restart to take effect"/> + </div> + + <div class="col-sm-4 col-sm-pull-1"> + <input type="number" placeholder="8989" name="port" class="form-control"/> </div> </div> - <div class="control-group"> - <label class="control-label">URL Base</label> + <div class="form-group"> + <label class="col-sm-3 control-label">URL Base</label> - <div class="controls"> - <input type="text" name="urlBase"/> - <span> - <i class="icon-nd-form-warning" title="Requires restart to take effect"/> - <i class="icon-nd-form-info" title="For reverse proxy support, default is empty"/> - </span> + <div class="col-sm-1 col-sm-push-4 help-inline"> + <i class="icon-nd-form-warning" title="Requires restart to take effect"/> + <i class="icon-nd-form-info" title="For reverse proxy support, default is empty"/> + </div> + + <div class="col-sm-4 col-sm-pull-1"> + <input type="text" name="urlBase" class="form-control"/> </div> </div> - <div class="control-group advanced-setting"> - <label class="control-label">Enable SSL</label> + <div class="form-group advanced-setting"> + <label class="col-sm-3 control-label">Enable SSL</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enableSsl" class="x-ssl"/> + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="enableSsl" class="x-ssl"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <p> + <span>Yes</span> + <span>No</span> + </p> - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn btn-primary slide-button"/> + </label> - <span class="help-inline-checkbox"> - <i class="icon-nd-form-warning" title="Requires restart running as administrator to take effect"/> - </span> + <span class="help-inline-checkbox"> + <i class="icon-nd-form-warning" title="Requires restart running as administrator to take effect"/> + </span> + </div> </div> </div> <div class="x-ssl-options"> - <div class="control-group advanced-setting"> - <label class="control-label">SSL Port Number</label> + <div class="form-group advanced-setting"> + <label class="col-sm-3 control-label">SSL Port Number</label> - <div class="controls"> - <input type="number" placeholder="8989" name="sslPort"/> + <div class="col-sm-4"> + <input type="number" placeholder="8989" name="sslPort" class="form-control"/> </div> </div> {{#if_windows}} - <div class="control-group advanced-setting"> - <label class="control-label">SSL Cert Hash</label> + <div class="form-group advanced-setting"> + <label class="col-sm-3 control-label">SSL Cert Hash</label> - <div class="controls"> - <input type="text" name="sslCertHash"/> + <div class="col-sm-4"> + <input type="text" name="sslCertHash" class="form-control"/> </div> </div> {{/if_windows}} </div> - <div class="control-group"> - <label class="control-label">Open browser on start</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Open browser on start</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="launchBrowser"/> + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="launchBrowser" class="form-control"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <p> + <span>Yes</span> + <span>No</span> + </p> - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn btn-primary slide-button"/> + </label> - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Open a web browser and navigate to NzbDrone homepage on app start. Has no effect if installed as a windows service"/> - </span> + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Open a web browser and navigate to NzbDrone homepage on app start. Has no effect if installed as a windows service"/> + </span> + </div> </div> </div> </fieldset> <fieldset> <legend>Security</legend> - <div class="control-group"> - <label class="control-label">Authentication</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" class='x-auth' name="authenticationEnabled"/> - <p> - <span>On</span> - <span>Off</span> - </p> - <div class="btn btn-primary slide-button"/> - </label> + <div class="form-group"> + <label class="col-sm-3 control-label">Authentication</label> - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Require Username and Password to access Nzbdrone"/> - </span> - </div> - </div> - <div class='x-auth-options'> - <div class="control-group"> - <label class="control-label">Username</label> - <div class="controls"> - <input type="text" placeholder="Username" name="username"/> - </div> - </div> - <div class="control-group"> - <label class="control-label">Password</label> - <div class="controls"> - <input type="password" name="password"/> + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" class="x-auth" name="authenticationEnabled"/> + <p> + <span>On</span> + <span>Off</span> + </p> + <div class="btn btn-primary slide-button"/> + </label> + + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Require Username and Password to access Nzbdrone"/> + </span> </div> </div> </div> - <div class="control-group api-key"> - <label class="control-label">API Key</label> - <div class="controls"> - <div class="input-append"> - <input type="text" name="apiKey" readonly="readonly" class="x-api-key"/> - <button class="btn btn-icon-only x-copy-api-key" title="Copy to clipboard"><i class="icon-copy"></i></button> - <button class="btn btn-danger btn-icon-only x-reset-api-key" title="Reset API Key"><i class="icon-refresh"></i></button> - </div> + <div class="x-auth-options"> + <div class="form-group"> + <label class="col-sm-3 control-label">Username</label> - <span> - <i class="icon-nd-form-warning" title="Requires restart to take effect"/> - </span> + <div class="col-sm-4"> + <input type="text" placeholder="Username" name="username" class="form-control"/> + </div> + </div> + + <div class="form-group"> + <label class="col-sm-3 control-label">Password</label> + + <div class="col-sm-4"> + <input type="password" name="password" class="form-control"/> + </div> + </div> + </div> + + <div class="form-group api-key"> + <label class="col-sm-3 control-label">API Key</label> + + <div class="col-sm-1 col-sm-push-4 help-inline"> + <i class="icon-nd-form-warning" title="Requires restart to take effect"/> + </div> + + <div class="col-sm-4 col-sm-pull-1"> + <div class="input-group"> + <input type="text" name="apiKey" readonly="readonly" class="form-control x-api-key"/> + <div class="input-group-btn"> + <button class="btn btn-icon-only x-copy-api-key hidden-xs"><i class="icon-copy"></i></button> + <button class="btn btn-danger btn-icon-only x-reset-api-key" title="Reset API Key"><i class="icon-refresh"></i></button> + </div> + </div> </div> </div> </fieldset> @@ -141,58 +157,84 @@ <fieldset> <legend>Logging</legend> - <div class="control-group"> - <label class="control-label">Log Level</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Log Level</label> - <div class="controls"> - <select name="logLevel"> + <div class="col-sm-1 col-sm-push-2 help-inline"> + <i class="icon-nd-form-warning" title="Trace and Debug logging should only be enabled temporarily"/> + </div> + + <div class="col-sm-2 col-sm-pull-1"> + <select name="logLevel" class="form-control"> <option value="Trace">Trace</option> <option value="Debug">Debug</option> <option value="Info">Info</option> </select> - - <span> - <i class="icon-nd-form-warning" title="Trace and Debug logging should only be enabled temporarily"/> - </span> </div> </div> </fieldset> <fieldset class="advanced-setting"> - <legend>Development</legend> - <div class="alert"> - <i class="icon-nd-warning"></i> - Don't change anything here unless you know what you are doing. - </div> - <div class="control-group"> - <label class="control-label">Branch</label> + <legend>Updating</legend> - <div class="controls"> - <input type="text" placeholder="master" name="branch"/> + <div class="form-group"> + <label class="col-sm-3 control-label">Branch</label> + + <div class="col-sm-4"> + <input type="text" placeholder="master" name="branch" class="form-control"/> </div> </div> - <!--{{#if_mono}}--> - <!--<div class="control-group">--> - <!--<label class="control-label">Auto Update</label>--> + {{#if_mono}} + <div class="alert alert-warning">Please see: <a href="https://github.com/NzbDrone/NzbDrone/wiki/Updating">the wiki</a> for more information</div> - <!--<div class="controls">--> - <!--<label class="checkbox toggle well">--> - <!--<input type="checkbox" name="autoUpdate"/>--> + <div class="form-group"> + <label class="col-sm-3 control-label">Automatic</label> - <!--<p>--> - <!--<span>Yes</span>--> - <!--<span>No</span>--> - <!--</p>--> + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="updateAutomatically"/> + <p> + <span>On</span> + <span>Off</span> + </p> + <div class="btn btn-primary slide-button"/> + </label> - <!--<div class="btn btn-primary slide-button"/>--> - <!--</label>--> + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Automatically download and install updates. You will still be able to install from System: Updates"/> + </span> + </div> + </div> + </div> - <!--<span class="help-inline-checkbox">--> - <!--<i class="icon-nd-form-info" title="Use drone's built in auto update instead of package manager/manual updating"/>--> - <!--</span>--> - <!--</div>--> - <!--</div>--> - <!--{{/if_mono}}--> + <div class="form-group"> + <label class="col-sm-3 control-label">Mechanism</label> + + <div class="col-sm-1 col-sm-push-4 help-inline"> + <i class="icon-nd-form-info" title="Use built-in updater or external script"/> + </div> + + <div class="col-sm-4 col-sm-pull-1"> + <select name="updateMechanism" class="form-control x-update-mechanism"> + <option value="builtIn">Built-in</option> + <option value="script">Script</option> + </select> + </div> + </div> + + <div class="form-group x-script-group"> + <label class="col-sm-3 control-label">Script Path</label> + + <div class="col-sm-1 col-sm-push-4 help-inline"> + <i class="icon-nd-form-info" title="Path to a custom script that take an extracted update package and handle the remainder of the update process"/> + </div> + + <div class="col-sm-4 col-sm-pull-1"> + <input type="text" name="updateScriptPath" class="form-control"/> + </div> + </div> + {{/if_mono}} </fieldset> </div> diff --git a/src/UI/Settings/Indexers/CollectionTemplate.html b/src/UI/Settings/Indexers/CollectionTemplate.html index f2f4aff77..657ee83d7 100644 --- a/src/UI/Settings/Indexers/CollectionTemplate.html +++ b/src/UI/Settings/Indexers/CollectionTemplate.html @@ -1,7 +1,7 @@ <fieldset> <legend>Indexers</legend> <div class="row"> - <div class="span12"> + <div class="col-md-12"> <ul id="x-indexers" class="indexer-list thingies"> <li> <div class="indexer-settings-item add-card x-add-card"> diff --git a/src/UI/Settings/Indexers/DeleteViewTemplate.html b/src/UI/Settings/Indexers/DeleteViewTemplate.html index b152b9708..86d9763d0 100644 --- a/src/UI/Settings/Indexers/DeleteViewTemplate.html +++ b/src/UI/Settings/Indexers/DeleteViewTemplate.html @@ -1,11 +1,15 @@ -<div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete Indexer</h3> -</div> -<div class="modal-body"> - <p>Are you sure you want to delete '{{name}}'?</p> -</div> -<div class="modal-footer"> - <button class="btn" data-dismiss="modal">cancel</button> - <button class="btn btn-danger x-confirm-delete">delete</button> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Delete Indexer</h3> + </div> + <div class="modal-body"> + <p>Are you sure you want to delete '{{name}}'?</p> + </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal">cancel</button> + <button class="btn btn-danger x-confirm-delete">delete</button> + </div> + </div> </div> \ No newline at end of file diff --git a/src/UI/Settings/Indexers/EditTemplate.html b/src/UI/Settings/Indexers/EditTemplate.html index 0bf5bdc06..7e7eee4e0 100644 --- a/src/UI/Settings/Indexers/EditTemplate.html +++ b/src/UI/Settings/Indexers/EditTemplate.html @@ -1,58 +1,64 @@ -<div class="modal-header"> - <button type="button" class="close x-cancel"aria-hidden="true">×</button> - {{#if id}} - <h3>Edit</h3> - {{else}} - <h3>Add Newznab</h3> - {{/if}} -</div> -<div class="modal-body"> - <div class="form-horizontal"> - <div class="control-group"> - <label class="control-label">Name</label> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close x-cancel"aria-hidden="true">×</button> + {{#if id}} + <h3>Edit</h3> + {{else}} + <h3>Add Newznab</h3> + {{/if}} + </div> + <div class="modal-body"> + <div class="form-horizontal"> + <div class="form-group"> + <label class="col-sm-3 control-label">Name</label> - <div class="controls"> - <input type="text" name="name"/> + <div class="col-sm-5"> + <input type="text" name="name" class="form-control"/> + </div> + </div> + + <div class="form-group"> + <label class="col-sm-3 control-label">Enable</label> + + <div class="col-sm-5"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="enable"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + </div> + </div> + </div> + + {{formBuilder}} </div> </div> + <div class="modal-footer"> + {{#if id}} + <button class="btn btn-danger pull-left x-remove">delete</button> + {{/if}} - <div class="control-group"> - <label class="control-label">Enable</label> + <span class="x-activity"></span> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enable"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <button class="btn x-cancel">cancel</button> - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn-group"> + <button class="btn btn-primary x-save">save</button> + <button class="btn btn-icon-only btn-primary dropdown-toggle" data-toggle="dropdown"> + <span class="caret"></span> + </button> + <ul class="dropdown-menu"> + <li class="save-and-add x-save-and-add"> + save and add + </li> + </ul> </div> </div> - - {{formBuilder}} - </div> -</div> -<div class="modal-footer"> - {{#if id}} - <button class="btn btn-danger pull-left x-remove">delete</button> - {{/if}} - - <span class="x-activity"></span> - - <button class="btn x-cancel">cancel</button> - - <div class="btn-group"> - <button class="btn btn-primary x-save">save</button> - <button class="btn btn-icon-only btn-primary dropdown-toggle" data-toggle="dropdown"> - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - <li class="save-and-add x-save-and-add"> - save and add - </li> - </ul> </div> </div> diff --git a/src/UI/Settings/Indexers/ItemTemplate.html b/src/UI/Settings/Indexers/ItemTemplate.html index 6ab71d071..4f38978ad 100644 --- a/src/UI/Settings/Indexers/ItemTemplate.html +++ b/src/UI/Settings/Indexers/ItemTemplate.html @@ -3,17 +3,17 @@ <h3>{{name}}</h3> {{#if_eq implementation compare="Newznab"}} <span class="btn-group pull-right"> - <button class="btn btn-mini btn-icon-only x-delete"> + <button class="btn btn-xs btn-icon-only x-delete"> <i class="icon-nd-delete"/> </button> </span> {{/if_eq}} </div> - <div class="control-group"> + <div class="form-group"> <label class="control-label">Enable</label> - <div class="controls"> + <div class="input-group"> <label class="checkbox toggle well"> <input type="checkbox" name="enable"/> <p> diff --git a/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html b/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html index 074bd219c..d281e639a 100644 --- a/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html +++ b/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html @@ -1,38 +1,37 @@ <fieldset> <legend>Options</legend> - <div class="control-group"> - <label class="control-label">Retention</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Retention</label> - <div class="controls"> - <input type="number" min="0" name="retention"/> + <div class="col-sm-2"> + <input type="number" min="0" name="retention" class="form-control"/> </div> </div> - <div class="control-group advanced-setting"> - <label class="control-label">RSS Sync Interval</label> + <div class="form-group advanced-setting"> + <label class="col-sm-3 control-label">RSS Sync Interval</label> - <div class="controls"> - <input type="number" name="rssSyncInterval"/> + <div class="col-sm-1 col-sm-push-2 help-inline"> + <i class="icon-nd-form-warning" title="This will apply to all indexers, please follow the rules set forth by them"/> + <i class="icon-nd-form-info" title="Set to zero to disable (this will stop all automatic release grabbing)"/> + </div> - <span class="help-inline"> - <i class="icon-nd-form-warning" title="This will apply to all indexers, please follow the rules set forth by them"/> - <i class="icon-nd-form-info" title="Set to zero to disable (this will stop all automatic release grabbing)"/> - </span> + <div class="col-sm-2 col-sm-pull-1"> + <input type="number" name="rssSyncInterval" class="form-control"/> </div> </div> - <div class="control-group advanced-setting"> - <label class="control-label">Release Restrictions</label> + <div class="form-group advanced-setting"> + <label class="col-sm-3 control-label">Release Restrictions</label> - <div class="controls"> - <textarea rows="3" name="releaseRestrictions" class="release-restrictions"></textarea> + <div class="col-sm-1 col-sm-push-4 help-inline help-inline-text-area"> + <i class="icon-nd-form-info" title="Blacklist NZBs based on these words (case-insensitive)"/> + </div> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Blacklist NZBs based on these words (case-insensitive)"/> - </span> - - <span class="text-area-help">Newline-delimited set of rules</span> + <div class="col-sm-4 col-sm-pull-1"> + <textarea rows="3" name="releaseRestrictions" class="form-control release-restrictions"></textarea> + <div class="text-area-help">Newline-delimited set of rules</div> </div> </div> </fieldset> diff --git a/src/UI/Settings/Indexers/indexers.less b/src/UI/Settings/Indexers/indexers.less index 79923b909..281e70e90 100644 --- a/src/UI/Settings/Indexers/indexers.less +++ b/src/UI/Settings/Indexers/indexers.less @@ -1,16 +1,29 @@ .indexer-settings-item { width: 220px; - height: 260px; + height: 295px; padding: 10px 15px; h3 { - width: 190px; + width: 175px; + overflow: visible; } &.add-card { + margin-top: 10px; + margin-left: 10px; + .center { - margin-top: 100px; + margin-top: 90px; } } + + /* Super hack to keep using form builder, this should be dead when we do proper modals for editing */ + .col-sm-1, .col-sm-3, .col-sm-5 { + display : block; + width : 100%; + padding: 0px; + float: none; + position: inherit; + } } \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html b/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html index 6e7de342c..4de24419c 100644 --- a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html +++ b/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html @@ -1,69 +1,75 @@ -<fieldset class="advanced-setting"> +<fieldset> <legend>File Management</legend> - <div class="control-group"> - <label class="control-label">Ignore Deleted Episodes</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Ignore Deleted Episodes</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="autoUnmonitorPreviouslyDownloadedEpisodes"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <div class="col-sm-9"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="autoUnmonitorPreviouslyDownloadedEpisodes"/> + <p> + <span>Yes</span> + <span>No</span> + </p> - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn btn-primary slide-button"/> + </label> - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Episodes deleted from disk are automatically unmonitored in NzbDrone"/> - </span> + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Episodes deleted from disk are automatically unmonitored in NzbDrone"/> + </span> + </div> </div> </div> - <div class="control-group"> - <label class="control-label">Download Propers</label> + <div class="form-group advanced-setting"> + <label class="col-sm-3 control-label">Download Propers</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="autoDownloadPropers"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <div class="col-sm-9"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="autoDownloadPropers"/> + <p> + <span>Yes</span> + <span>No</span> + </p> - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn btn-primary slide-button"/> + </label> - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Should NzbDrone automatically upgrade to propers when available?"/> - </span> + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Should NzbDrone automatically upgrade to propers when available?"/> + </span> + </div> </div> </div> - <div class="control-group"> - <label class="control-label">Recycling Bin</label> + <div class="form-group advanced-setting"> + <label class="col-sm-3 control-label">Change File Date</label> - <div class="controls"> - <input type="text" name="recycleBin" class="x-path"/> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Episode files will go here when deleted instead of being permanently deleted"/> - </span> + <div class="col-sm-1 col-sm-push-2 help-inline"> + <i class="icon-nd-form-info" title="Change file date on import/rescan"/> </div> - </div> - - <div class="control-group"> - <label class="control-label">Change File Date</label> - - <div class="controls"> - <select class="inputClass" name="fileDate"> + + <div class="col-sm-2 col-sm-pull-1"> + <select class="form-control" name="fileDate"> <option value="none">None</option> <option value="localAirDate">Local Air Date</option> <option value="utcAirDate">UTC Air Date</option> </select> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Change file date on import/rescan"/> - </span> + </div> + </div> + + <div class="form-group"> + <label class="col-sm-3 control-label">Recycling Bin</label> + + <div class="col-sm-1 col-sm-push-8 help-inline"> + <i class="icon-nd-form-info" title="Episode files will go here when deleted instead of being permanently deleted"/> + </div> + + <div class="col-sm-8 col-sm-pull-1"> + <input type="text" name="recycleBin" class="form-control x-path"/> </div> </div> </fieldset> diff --git a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.html b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.html index 53cddefa4..793a7a2fe 100644 --- a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.html @@ -1,76 +1,86 @@ -<div class="control-group"> - <label class="control-label">Include Series Title</label> +<div class="form-group"> + <label class="col-sm-3 control-label">Include Series Title</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="includeSeriesTitle"/> + <div class="col-sm-9"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="includeSeriesTitle"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <p> + <span>Yes</span> + <span>No</span> + </p> - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn btn-primary slide-button"/> + </label> + </div> </div> </div> -<div class="control-group"> - <label class="control-label">Include Episode Title</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="includeEpisodeTitle"/> - <p> - <span>Yes</span> - <span>No</span> - </p> +<div class="form-group"> + <label class="col-sm-3 control-label">Include Episode Title</label> - <div class="btn btn-primary slide-button"/> - </label> + <div class="col-sm-9"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="includeEpisodeTitle"/> + + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + </div> </div> </div> -<div class="control-group"> - <label class="control-label">Include Quality</label> +<div class="form-group"> + <label class="col-sm-3 control-label">Include Quality</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="includeQuality"/> + <div class="col-sm-9"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="includeQuality"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <p> + <span>Yes</span> + <span>No</span> + </p> - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn btn-primary slide-button"/> + </label> + </div> </div> </div> -<div class="control-group"> - <label class="control-label">Replace Spaces</label> +<div class="form-group"> + <label class="col-sm-3 control-label">Replace Spaces</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="replaceSpaces"/> + <div class="col-sm-9"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="replaceSpaces"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <p> + <span>Yes</span> + <span>No</span> + </p> - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn btn-primary slide-button"/> + </label> + </div> </div> </div> -<div class="control-group"> - <label class="control-label">Separator</label> +<div class="form-group"> + <label class="col-sm-3 control-label">Separator</label> - <div class="controls"> - <select class="inputClass" name="separator"> + <div class="col-sm-9"> + <select class="form-control" name="separator"> <option value=" - ">Dash</option> <option value=" ">Space</option> <option value=".">Period</option> @@ -78,11 +88,11 @@ </div> </div> -<div class="control-group"> - <label class="control-label">Numbering Style</label> +<div class="form-group"> + <label class="col-sm-3 control-label">Numbering Style</label> - <div class="controls"> - <select class="inputClass" name="numberStyle"> + <div class="col-sm-9"> + <select class="form-control" name="numberStyle"> <option value="{season}x{episode:00}">1x05</option> <option value="{season:00}x{episode:00}">01x05</option> <option value="S{season:00}E{episode:00}">S01E05</option> diff --git a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html index df3917db9..125ec4c71 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html @@ -1,37 +1,44 @@ <fieldset> <legend>Episode Naming</legend> - <div class="control-group"> - <label class="control-label">Rename Episodes</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Rename Episodes</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="renameEpisodes" class="x-rename-episodes"/> + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="renameEpisodes" class="x-rename-episodes"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <p> + <span>Yes</span> + <span>No</span> + </p> - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn btn-primary slide-button"/> + </label> - <span class="help-inline-checkbox"> - <i class="icon-nd-form-warning" title="NzbDrone will use the existing file name if set to no"/> - </span> + <span class="help-inline-checkbox"> + <i class="icon-nd-form-warning" title="NzbDrone will use the existing file name if set to no"/> + </span> + </div> </div> </div> <div class="x-naming-options"> <div class="basic-setting x-basic-naming"></div> - <div class="control-group advanced-setting"> - <label class="control-label">Standard Episode Format</label> + <div class="form-group advanced-setting"> + <label class="col-sm-3 control-label">Standard Episode Format</label> - <div class="controls"> - <div class="input-append x-helper-input"> - <input type="text" class="naming-format" name="standardEpisodeFormat" data-onkeyup="true" /> - <div class="btn-group x-naming-token-helper"> + <div class="col-sm-1 col-sm-push-8 help-inline"> + <i class="icon-nd-form-info" title="" data-original-title="All caps or all lower-case can also be used"></i> + <a href="https://github.com/NzbDrone/NzbDrone/wiki/Sorting-and-Renaming" class="help-link" title="More information"><i class="icon-nd-form-info-link"/></a> + </div> + + <div class="col-sm-8 col-sm-pull-1"> + <div class="input-group x-helper-input"> + <input type="text" class="form-control naming-format" name="standardEpisodeFormat" data-onkeyup="true" /> + <div class="input-group-btn btn-group x-naming-token-helper"> <button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown"> <i class="icon-plus"></i> </button> @@ -47,20 +54,21 @@ </ul> </div> </div> - <span class="help-inline"> - <i class="icon-nd-form-info" title="" data-original-title="All caps or all lower-case can also be used"></i> - <a href="https://github.com/NzbDrone/NzbDrone/wiki/Sorting-and-Renaming" class="help-link" title="More information"><i class="icon-nd-form-info-link"/></a> - </span> </div> </div> - <div class="control-group advanced-setting"> - <label class="control-label">Daily Episode Format</label> + <div class="form-group advanced-setting"> + <label class="col-sm-3 control-label">Daily Episode Format</label> - <div class="controls"> - <div class="input-append x-helper-input"> - <input type="text" class="naming-format" name="dailyEpisodeFormat" data-onkeyup="true" /> - <div class="btn-group x-naming-token-helper"> + <div class="col-sm-1 col-sm-push-8 help-inline"> + <i class="icon-nd-form-info" title="" data-original-title="All caps or all lower-case can also be used"></i> + <a href="https://github.com/NzbDrone/NzbDrone/wiki/Sorting-and-Renaming" class="help-link" title="More information"><i class="icon-nd-form-info-link"/></a> + </div> + + <div class="col-sm-8 col-sm-pull-1"> + <div class="input-group x-helper-input"> + <input type="text" class="form-control naming-format" name="dailyEpisodeFormat" data-onkeyup="true" /> + <div class="input-group-btn btn-group x-naming-token-helper"> <button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown"> <i class="icon-plus"></i> </button> @@ -77,34 +85,21 @@ </ul> </div> </div> - <span class="help-inline"> - <i class="icon-nd-form-info" title="" data-original-title="All caps or all lower-case can also be used"></i> - <a href="https://github.com/NzbDrone/NzbDrone/wiki/Sorting-and-Renaming" class="help-link" title="More information"><i class="icon-nd-form-info-link"/></a> - </span> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Multi-Episode Style</label> - - <div class="controls"> - <select class="inputClass x-multi-episode-style" name="multiEpisodeStyle"> - <option value="0">Extend</option> - <option value="1">Duplicate</option> - <option value="2">Repeat</option> - <option value="3">Scene</option> - </select> </div> </div> </div> - <div class="control-group advanced-setting"> - <label class="control-label">Series Folder Format</label> + <div class="form-group advanced-setting"> + <label class="col-sm-3 control-label">Series Folder Format</label> - <div class="controls"> - <div class="input-append x-helper-input"> - <input type="text" class="naming-format" name="seriesFolderFormat" data-onkeyup="true"/> - <div class="btn-group x-naming-token-helper"> + <div class="col-sm-1 col-sm-push-8 help-inline"> + <i class="icon-nd-form-info" title="" data-original-title="All caps or all lower-case can also be used. Only used when adding a new series."></i> + </div> + + <div class="col-sm-8 col-sm-pull-1"> + <div class="input-group x-helper-input"> + <input type="text" class="form-control naming-format" name="seriesFolderFormat" data-onkeyup="true"/> + <div class="input-group-btn btn-group x-naming-token-helper"> <button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown"> <i class="icon-plus"></i> </button> @@ -113,19 +108,16 @@ </ul> </div> </div> - <span class="help-inline"> - <i class="icon-nd-form-info" title="" data-original-title="All caps or all lower-case can also be used. Only used when adding a new series."></i> - </span> </div> </div> - <div class="control-group"> - <label class="control-label">Season Folder Format</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Season Folder Format</label> - <div class="controls"> - <div class="input-append x-helper-input"> - <input type="text" class="naming-format" name="seasonFolderFormat" data-onkeyup="true"/> - <div class="btn-group x-naming-token-helper"> + <div class="col-sm-8"> + <div class="input-group x-helper-input"> + <input type="text" class="form-control naming-format" name="seasonFolderFormat" data-onkeyup="true"/> + <div class="input-group-btn btn-group x-naming-token-helper"> <button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown"> <i class="icon-plus"></i> </button> @@ -139,43 +131,58 @@ </div> </div> - <div class="control-group"> - <label class="control-label">Single Episode Example</label> + <div class="x-naming-options"> + <div class="form-group"> + <label class="col-sm-3 control-label">Multi-Episode Style</label> - <div class="controls"> - <span class="x-single-episode-example naming-example"></span> + <div class="col-sm-2"> + <select class="form-control x-multi-episode-style" name="multiEpisodeStyle"> + <option value="0">Extend</option> + <option value="1">Duplicate</option> + <option value="2">Repeat</option> + <option value="3">Scene</option> + </select> + </div> </div> </div> - <div class="control-group"> - <label class="control-label">Multi-Episode Example</label> - - <div class="controls"> - <span class="x-multi-episode-example naming-example"></span> + <div class="form-group"> + <label class="col-sm-3 control-label">Single Episode Example</label> + + <div class="col-sm-8"> + <p class="form-control-static x-single-episode-example naming-example"></p> </div> </div> - <div class="control-group"> - <label class="control-label">Daily-Episode Example</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Multi-Episode Example</label> - <div class="controls"> - <span class="x-daily-episode-example naming-example"></span> + <div class="col-sm-8"> + <p class="form-control-static x-multi-episode-example naming-example"></p> </div> </div> - <div class="control-group"> - <label class="control-label">Series Folder Example</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Daily-Episode Example</label> - <div class="controls"> - <span class="x-series-folder-example naming-example"></span> + <div class="col-sm-8"> + <p class="form-control-static x-daily-episode-example naming-example"></p> </div> </div> - <div class="control-group"> - <label class="control-label">Season Folder Example</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Series Folder Example</label> - <div class="controls"> - <span class="x-season-folder-example naming-example"></span> + <div class="col-sm-8"> + <p class="form-control-static x-series-folder-example naming-example"></p> + </div> + </div> + + <div class="form-group"> + <label class="col-sm-3 control-label">Season Folder Example</label> + + <div class="col-sm-8"> + <p class="form-control-static x-season-folder-example naming-example"></p> </div> </div> </fieldset> diff --git a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html index 2a15e6420..fd8ddcc69 100644 --- a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html @@ -1,70 +1,76 @@ -{{#if_mono}} +{{#if_mono}} <fieldset class="advanced-setting"> <legend>Permissions</legend> - <div class="control-group"> - <label class="control-label">Set Permissions</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Set Permissions</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="setPermissionsLinux"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="setPermissionsLinux"/> + <p> + <span>Yes</span> + <span>No</span> + </p> - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn btn-primary slide-button"/> + </label> - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Should chmod/chown be run when files are imported/renamed?"/> - <i class="icon-nd-form-warning" title="If you're unsure what these settings do, do not alter them."/> - </span> + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Should chmod/chown be run when files are imported/renamed?"/> + <i class="icon-nd-form-warning" title="If you're unsure what these settings do, do not alter them."/> + </span> + </div> </div> </div> - <div class="control-group"> - <label class="control-label">File chmod mask</label> + <div class="form-group"> + <label class="col-sm-3 control-label">File chmod mask</label> - <div class="controls"> - <input type="text" name="fileChmod"/> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Octal, applied to media files when imported/renamed by NzbDrone"/> - </span> + <div class="col-sm-1 col-sm-push-4 help-inline"> + <i class="icon-nd-form-info" title="Octal, applied to media files when imported/renamed by NzbDrone"/> + </div> + + <div class="col-sm-4 col-sm-pull-1"> + <input type="text" name="fileChmod" class="form-control"/> </div> </div> - <div class="control-group"> - <label class="control-label">Folder chmod mask</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Folder chmod mask</label> - <div class="controls"> - <input type="text" name="folderChmod"/> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Octal, applied to series/season folders created by NzbDrone"/> - </span> + <div class="col-sm-1 col-sm-push-4 help-inline"> + <i class="icon-nd-form-info" title="Octal, applied to series/season folders created by NzbDrone"/> + </div> + + <div class="col-sm-4 col-sm-pull-1"> + <input type="text" name="folderChmod" class="form-control"/> </div> </div> - <div class="control-group"> - <label class="control-label">chown User</label> + <div class="form-group"> + <label class="col-sm-3 control-label">chown User</label> - <div class="controls"> - <input type="text" name="chownUser"/> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Username or uid. Use uid for remote file systems."/> - </span> + <div class="col-sm-1 col-sm-push-4 help-inline"> + <i class="icon-nd-form-info" title="Username or uid. Use uid for remote file systems."/> + </div> + + <div class="col-sm-4 col-sm-pull-1"> + <input type="text" name="chownUser" class="form-control"/> </div> </div> - <div class="control-group"> - <label class="control-label">chown Group</label> + <div class="form-group"> + <label class="col-sm-3 control-label">chown Group</label> - <div class="controls"> - <input type="text" name="chownGroup"/> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Group name or gid. Use gid for remote file systems."/> - </span> + <div class="col-sm-1 col-sm-push-4 help-inline"> + <i class="icon-nd-form-info" title="Group name or gid. Use gid for remote file systems."/> + </div> + + <div class="col-sm-4 col-sm-pull-1"> + <input type="text" name="chownGroup" class="form-control"/> </div> </div> </fieldset> -{{/if_mono}} \ No newline at end of file +{{/if_mono}} diff --git a/src/UI/Settings/MediaManagement/Sorting/SortingViewTemplate.html b/src/UI/Settings/MediaManagement/Sorting/SortingViewTemplate.html index 1f1a22f8a..c137a1de3 100644 --- a/src/UI/Settings/MediaManagement/Sorting/SortingViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Sorting/SortingViewTemplate.html @@ -1,24 +1,26 @@ <fieldset class="advanced-setting"> <legend>Folders</legend> - <div class="control-group"> - <label class="control-label">Create empty series folders</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Create empty series folders</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="createEmptySeriesFolders"/> + <div class="col-sm-9"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="createEmptySeriesFolders"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <p> + <span>Yes</span> + <span>No</span> + </p> - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn btn-primary slide-button"/> + </label> - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Create missing series folders during disk scan"/> - </span> + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Create missing series folders during disk scan"/> + </span> + </div> </div> </div> </fieldset> @@ -27,24 +29,26 @@ <fieldset class="advanced-setting"> <legend>Importing</legend> - <div class="control-group"> - <label class="control-label">Skip Free Space Check</label> + <div class="form-group"> + <label class="col-sm-3 control-label">Skip Free Space Check</label> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="skipFreeSpaceCheckWhenImporting"/> + <div class="col-sm-9"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="skipFreeSpaceCheckWhenImporting"/> - <p> - <span>Yes</span> - <span>No</span> - </p> + <p> + <span>Yes</span> + <span>No</span> + </p> - <div class="btn btn-primary slide-button"/> - </label> + <div class="btn btn-primary slide-button"/> + </label> - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Use when drone is unable to detect free space from your series root folder"/> - </span> + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Use when drone is unable to detect free space from your series root folder"/> + </span> + </div> </div> </div> </fieldset> diff --git a/src/UI/Settings/Metadata/MetadataCollectionViewTemplate.html b/src/UI/Settings/Metadata/MetadataCollectionViewTemplate.html index 523afe5e4..0562678a1 100644 --- a/src/UI/Settings/Metadata/MetadataCollectionViewTemplate.html +++ b/src/UI/Settings/Metadata/MetadataCollectionViewTemplate.html @@ -1,7 +1,7 @@ <fieldset> <legend>Metadata</legend> <div class="row"> - <div class="span12"> + <div class="col-md-12"> <ul id="x-metadata" class="metadata-list"></ul> </div> </div> diff --git a/src/UI/Settings/Metadata/MetadataEditViewTemplate.html b/src/UI/Settings/Metadata/MetadataEditViewTemplate.html index 3a8f9643e..072dfbf3b 100644 --- a/src/UI/Settings/Metadata/MetadataEditViewTemplate.html +++ b/src/UI/Settings/Metadata/MetadataEditViewTemplate.html @@ -1,39 +1,45 @@ -<div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Edit</h3> -</div> -<div class="modal-body"> - <div class="form-horizontal"> - <div class="control-group"> - <label class="control-label">Name</label> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Edit</h3> + </div> + <div class="modal-body"> + <div class="form-horizontal"> + <div class="form-group"> + <label class="col-sm-3 control-label">Name</label> - <div class="controls"> - <input type="text" name="name"/> + <div class="col-sm-5 controls"> + <input type="text" name="name" class="form-control"/> + </div> + </div> + + <div class="form-group"> + <label class="col-sm-3 control-label">Enable</label> + + <div class="col-sm-5"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="enable"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + </div> + </div> + </div> + + {{formBuilder}} </div> </div> + <div class="modal-footer"> + <span class="x-activity"></span> - <div class="control-group"> - <label class="control-label">Enable</label> - - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enable"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> + <button class="btn" data-dismiss="modal">cancel</button> + <button class="btn btn-primary x-save">save</button> </div> - - {{formBuilder}} </div> </div> -<div class="modal-footer"> - <span class="x-activity"></span> - - <button class="btn" data-dismiss="modal">cancel</button> - <button class="btn btn-primary x-save">save</button> -</div> diff --git a/src/UI/Settings/Metadata/MetadataItemViewTemplate.html b/src/UI/Settings/Metadata/MetadataItemViewTemplate.html index 5761db26c..f461ea4fb 100644 --- a/src/UI/Settings/Metadata/MetadataItemViewTemplate.html +++ b/src/UI/Settings/Metadata/MetadataItemViewTemplate.html @@ -2,7 +2,7 @@ <div> <h3>{{name}}</h3> <span class="btn-group pull-right"> - <button class="btn btn-mini btn-icon-only x-edit"><i class="icon-nd-edit"/></button> + <button class="btn btn-xs btn-icon-only x-edit"><i class="icon-nd-edit"/></button> </span> </div> @@ -10,7 +10,7 @@ {{#if enable}} <span class="label label-success">Enabled</span> {{else}} - <span class="label">Not Enabled</span> + <span class="label label-default">Not Enabled</span> {{/if}} <hr> {{#each fields}} diff --git a/src/UI/Settings/Metadata/MetadataLayoutTemplate.html b/src/UI/Settings/Metadata/MetadataLayoutTemplate.html index a251cbd43..0d4e79f32 100644 --- a/src/UI/Settings/Metadata/MetadataLayoutTemplate.html +++ b/src/UI/Settings/Metadata/MetadataLayoutTemplate.html @@ -1,3 +1,3 @@ <div class="row"> - <div class="span12" id="x-metadata-providers"/> + <div class="col-md-12" id="x-metadata-providers"/> </div> diff --git a/src/UI/Settings/Metadata/metadata.less b/src/UI/Settings/Metadata/metadata.less index 919503592..b90674017 100644 --- a/src/UI/Settings/Metadata/metadata.less +++ b/src/UI/Settings/Metadata/metadata.less @@ -18,7 +18,7 @@ h3 { margin-top: 0px; display: inline-block; - width: 150px; + width: 140px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/src/UI/Settings/Notifications/AddItemTemplate.html b/src/UI/Settings/Notifications/AddItemTemplate.html index dfaee211e..f892a4d01 100644 --- a/src/UI/Settings/Notifications/AddItemTemplate.html +++ b/src/UI/Settings/Notifications/AddItemTemplate.html @@ -1,10 +1,6 @@ -<div class="add-thingy span3"> - <div class="row"> - <div class="span3"> - {{implementation}} - {{#if link}} - <a href="{{link}}"><i class="icon-info-sign"/></a> - {{/if}} - </div> - </div> +<div class="add-thingy"> + {{implementation}} + {{#if link}} + <a href="{{link}}"><i class="icon-info-sign"/></a> + {{/if}} </div> \ No newline at end of file diff --git a/src/UI/Settings/Notifications/AddTemplate.html b/src/UI/Settings/Notifications/AddTemplate.html index 06a241428..a106c7c10 100644 --- a/src/UI/Settings/Notifications/AddTemplate.html +++ b/src/UI/Settings/Notifications/AddTemplate.html @@ -1,12 +1,16 @@ -<div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Add Notification</h3> -</div> -<div class="modal-body"> - <div class="add-notifications add-thingies"> - <ul class="items"></ul> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Add Notification</h3> + </div> + <div class="modal-body"> + <div class="add-notifications add-thingies"> + <ul class="items"></ul> + </div> + </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal">close</button> + </div> </div> </div> -<div class="modal-footer"> - <button class="btn" data-dismiss="modal">close</button> -</div> \ No newline at end of file diff --git a/src/UI/Settings/Notifications/CollectionTemplate.html b/src/UI/Settings/Notifications/CollectionTemplate.html index 2d4cc25f4..b9cfad00d 100644 --- a/src/UI/Settings/Notifications/CollectionTemplate.html +++ b/src/UI/Settings/Notifications/CollectionTemplate.html @@ -1,5 +1,5 @@ <div class="row"> - <div class="span12"> + <div class="col-md-12"> <ul class="notifications thingies"> <li> <div class="notification-item thingy add-card x-add-card"> diff --git a/src/UI/Settings/Notifications/CollectionView.js b/src/UI/Settings/Notifications/CollectionView.js index 571c26f9d..efae31c1b 100644 --- a/src/UI/Settings/Notifications/CollectionView.js +++ b/src/UI/Settings/Notifications/CollectionView.js @@ -1,7 +1,7 @@ 'use strict'; define([ 'marionette', - 'Settings/Notifications/ItemView', + 'Settings/Notifications/NotificationsItemView', 'Settings/Notifications/SchemaModal' ], function (Marionette, NotificationItemView, SchemaModal) { return Marionette.CompositeView.extend({ diff --git a/src/UI/Settings/Notifications/DeleteTemplate.html b/src/UI/Settings/Notifications/DeleteTemplate.html index 82f71cbb8..ed724d494 100644 --- a/src/UI/Settings/Notifications/DeleteTemplate.html +++ b/src/UI/Settings/Notifications/DeleteTemplate.html @@ -1,11 +1,15 @@ -<div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete Notification</h3> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Delete Notification</h3> + </div> + <div class="modal-body"> + <p>Are you sure you want to delete '{{name}}'?</p> + </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal">cancel</button> + <button class="btn btn-danger x-confirm-delete">delete</button> + </div> + </div> </div> -<div class="modal-body"> - <p>Are you sure you want to delete '{{name}}'?</p> -</div> -<div class="modal-footer"> - <button class="btn" data-dismiss="modal">cancel</button> - <button class="btn btn-danger x-confirm-delete">delete</button> -</div> \ No newline at end of file diff --git a/src/UI/Settings/Notifications/ItemTemplate.html b/src/UI/Settings/Notifications/ItemTemplate.html deleted file mode 100644 index 64125cea6..000000000 --- a/src/UI/Settings/Notifications/ItemTemplate.html +++ /dev/null @@ -1,23 +0,0 @@ -<div class="notification-item thingy"> - <div> - <h3>{{name}}</h3> - <span class="btn-group pull-right"> - <button class="btn btn-mini btn-icon-only x-edit"><i class="icon-nd-edit"/></button> - <button class="btn btn-mini btn-icon-only x-delete"><i class="icon-nd-delete"/></button> - </span> - </div> - - <div class="settings"> - {{#if onGrab}} - <span class="label label-success">On Grab</span> - {{else}} - <span class="label">On Grab</span> - {{/if}} - - {{#if onDownload}} - <span class="label label-success">On Download</span> - {{else}} - <span class="label">On Download</span> - {{/if}} - </div> -</div> diff --git a/src/UI/Settings/Notifications/NotificationEditView.js b/src/UI/Settings/Notifications/NotificationEditView.js index f36caa448..a11c507f3 100644 --- a/src/UI/Settings/Notifications/NotificationEditView.js +++ b/src/UI/Settings/Notifications/NotificationEditView.js @@ -67,8 +67,9 @@ define( _cancel: function () { if (this.model.isNew()) { this.model.destroy(); - vent.trigger(vent.Commands.CloseModalCommand); } + + vent.trigger(vent.Commands.CloseModalCommand); }, _deleteNotification: function () { diff --git a/src/UI/Settings/Notifications/NotificationEditViewTemplate.html b/src/UI/Settings/Notifications/NotificationEditViewTemplate.html index 1676b8755..a68594879 100644 --- a/src/UI/Settings/Notifications/NotificationEditViewTemplate.html +++ b/src/UI/Settings/Notifications/NotificationEditViewTemplate.html @@ -1,103 +1,113 @@ -<div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - {{#if id}} - <h3>Edit - {{implementation}}</h3> - {{else}} - <h3>Add - {{implementation}}</h3> - {{/if}} -</div> -<div class="modal-body"> - <div class="form-horizontal"> - <div class="control-group"> - <label class="control-label">Name</label> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close x-cancel" aria-hidden="true">×</button> + {{#if id}} + <h3>Edit - {{implementation}}</h3> + {{else}} + <h3>Add - {{implementation}}</h3> + {{/if}} + </div> + <div class="modal-body"> + <div class="form-horizontal"> + <div class="form-group"> + <label class="col-sm-3 control-label">Name</label> - <div class="controls"> - <input type="text" name="name"/> + <div class="col-sm-5"> + <input type="text" name="name" class="form-control"/> + </div> + </div> + + <div class="form-group"> + <label class="col-sm-3 control-label">On Grab</label> + + <div class="col-sm-5"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="onGrab"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Do you want to get notifications when episodes are grabbed?"/> + </span> + </div> + </div> + </div> + + <div class="form-group"> + <label class="col-sm-3 control-label">On Download</label> + + <div class="col-sm-5"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="onDownload" class="x-on-download"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Do you want to get notifications when episodes are downloaded?"/> + </span> + </div> + </div> + </div> + + <div class="form-group x-on-upgrade"> + <label class="col-sm-3 control-label">On Upgrade</label> + + <div class="col-sm-5"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="onUpgrade"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Do you want to get notifications when episodes are upgraded to a better quality?"/> + </span> + </div> + </div> + </div> + + {{formBuilder}} </div> </div> + <div class="modal-footer"> + {{#if id}} + <button class="btn btn-danger pull-left x-delete">delete</button> + {{else}} + <button class="btn pull-left x-back">back</button> + {{/if}} - <div class="control-group"> - <label class="control-label">On Grab</label> + <button class="btn x-test">test <i class="x-test-icon icon-nd-test"/></button> + <button class="btn x-cancel">cancel</button> - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="onGrab"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Do you want to get notifications when episodes are grabbed?"/> - </span> + <div class="btn-group"> + <button class="btn btn-primary x-save">save</button> + <button class="btn btn-icon-only btn-primary dropdown-toggle" data-toggle="dropdown"> + <span class="caret"></span> + </button> + <ul class="dropdown-menu"> + <li class="save-and-add x-save-and-add"> + save and add + </li> + </ul> </div> </div> - - <div class="control-group"> - <label class="control-label">On Download</label> - - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="onDownload" class="x-on-download"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Do you want to get notifications when episodes are downloaded?"/> - </span> - </div> - </div> - - <div class="control-group x-on-upgrade"> - <label class="control-label">On Upgrade</label> - - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="onUpgrade"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Do you want to get notifications when episodes are upgraded to a better quality?"/> - </span> - </div> - </div> - - {{formBuilder}} - </div> -</div> -<div class="modal-footer"> - {{#if id}} - <button class="btn btn-danger pull-left x-delete">delete</button> - {{else}} - <button class="btn pull-left x-back">back</button> - {{/if}} - - <button class="btn x-test">test <i class="x-test-icon icon-nd-test"/></button> - <button class="btn x-cancel">cancel</button> - - <div class="btn-group"> - <button class="btn btn-primary x-save">save</button> - <button class="btn btn-icon-only btn-primary dropdown-toggle" data-toggle="dropdown"> - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - <li class="save-and-add x-save-and-add"> - save and add - </li> - </ul> </div> </div> diff --git a/src/UI/Settings/Notifications/NotificationItemViewTemplate.html b/src/UI/Settings/Notifications/NotificationItemViewTemplate.html new file mode 100644 index 000000000..f6db5f697 --- /dev/null +++ b/src/UI/Settings/Notifications/NotificationItemViewTemplate.html @@ -0,0 +1,19 @@ +<div class="notification-item thingy" title="Click to edit"> + <div> + <h3>{{name}}</h3> + </div> + + <div class="settings"> + {{#if onGrab}} + <span class="label label-success">On Grab</span> + {{else}} + <span class="label label-default">On Grab</span> + {{/if}} + + {{#if onDownload}} + <span class="label label-success">On Download</span> + {{else}} + <span class="label label-default">On Download</span> + {{/if}} + </div> +</div> diff --git a/src/UI/Settings/Notifications/ItemView.js b/src/UI/Settings/Notifications/NotificationsItemView.js similarity index 50% rename from src/UI/Settings/Notifications/ItemView.js rename to src/UI/Settings/Notifications/NotificationsItemView.js index 5cbaa4100..3cde28bf9 100644 --- a/src/UI/Settings/Notifications/ItemView.js +++ b/src/UI/Settings/Notifications/NotificationsItemView.js @@ -3,18 +3,16 @@ define([ 'AppLayout', 'marionette', - 'Settings/Notifications/NotificationEditView', - 'Settings/Notifications/DeleteView' + 'Settings/Notifications/NotificationEditView' -], function (AppLayout, Marionette, EditView, DeleteView) { +], function (AppLayout, Marionette, EditView) { return Marionette.ItemView.extend({ - template: 'Settings/Notifications/ItemTemplate', + template: 'Settings/Notifications/NotificationItemViewTemplate', tagName : 'li', events: { - 'click .x-edit' : '_editNotification', - 'click .x-delete': '_deleteNotification' + 'click' : '_editNotification' }, initialize: function () { @@ -24,11 +22,6 @@ define([ _editNotification: function () { var view = new EditView({ model: this.model, notificationCollection: this.model.collection}); AppLayout.modalRegion.show(view); - }, - - _deleteNotification: function () { - var view = new DeleteView({ model: this.model}); - AppLayout.modalRegion.show(view); } }); }); diff --git a/src/UI/Settings/Notifications/notifications.less b/src/UI/Settings/Notifications/notifications.less index c8e059594..3a20ab1bf 100644 --- a/src/UI/Settings/Notifications/notifications.less +++ b/src/UI/Settings/Notifications/notifications.less @@ -1,26 +1,31 @@ -.notifications { - width: -webkit-fit-content; - width: -moz-fit-content; - width: fit-content; -} +@import "../../Shared/Styles/clickable.less"; + +//.notifications { +// width: -webkit-fit-content; +// width: -moz-fit-content; +// width: fit-content; +//} .notification-item { + .clickable; width: 290px; height: 90px; padding: 20px 20px; - h3 { - width: 230px; - } - .settings { margin-top: 5px; } &.add-card { .center { - margin-top: 15px; + margin-top: -4px; } } +} + +.add-notifications { + li { + width: 40%; + } } \ No newline at end of file diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.html b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.html index d3287100b..8ed291585 100644 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.html +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.html @@ -1,12 +1,12 @@ <fieldset> <legend>Quality Definitions</legend> - <div class="span11"> + <div class="col-md-11"> <div id="quality-definition-list"> - <div class="quality-header x-header"> + <div class="quality-header x-header hidden-xs"> <div class="row"> - <span class="span2">Quality</span> - <span class="span2">Title</span> - <span class="offset1 span4">Size Limit</span> + <span class="col-md-2 col-sm-3">Quality</span> + <span class="col-md-2 col-sm-3">Title</span> + <span class="col-md-4 col-sm-6">Size Limit</span> </div> </div> <div class="rows x-rows"> diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionTemplate.html b/src/UI/Settings/Quality/Definition/QualityDefinitionTemplate.html index e61558bfc..315f2bcde 100644 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionTemplate.html +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionTemplate.html @@ -1,10 +1,10 @@ - <span class="span2"> + <span class="col-md-2 col-sm-3"> {{quality.name}} </span> - <span class="span2"> - <input type="text" class="x-title input-block-level" value="{{title}}"> + <span class="col-md-2 col-sm-3"> + <input type="text" class="form-control" name="title"> </span> - <span class="offset1 span4"> + <span class="col-md-4 col-sm-6"> <div class="x-slider"></div> <div class="size-label-wrapper"> <div class="pull-left"> diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionView.js b/src/UI/Settings/Quality/Definition/QualityDefinitionView.js index 9b0465fe9..003d6520d 100644 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionView.js +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionView.js @@ -13,7 +13,6 @@ define( className: 'row', ui: { - title : '.x-title', sizeSlider : '.x-slider', thirtyMinuteMinSize: '.x-min-thirty', sixtyMinuteMinSize : '.x-min-sixty', @@ -22,7 +21,6 @@ define( }, events: { - 'change .x-title': '_updateTitle', 'slide .x-slider': '_updateSize' }, @@ -36,15 +34,11 @@ define( range : true, min : 0, max : 200, - values : [ this.model.get('minSize'), this.model.get('maxSize') ], + 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]); diff --git a/src/UI/Settings/Quality/Profile/AllowedLabeler.js b/src/UI/Settings/Quality/Profile/AllowedLabeler.js index ab09626e9..dbad67498 100644 --- a/src/UI/Settings/Quality/Profile/AllowedLabeler.js +++ b/src/UI/Settings/Quality/Profile/AllowedLabeler.js @@ -10,10 +10,10 @@ define( _.each(this.items, function (item) { if (item.allowed) { if (item.quality.id === cutoff.id) { - ret += '<span class="label label-info" title="Cutoff">' + item.quality.name + '</span> '; + ret += '<li><span class="label label-info" title="Cutoff">' + item.quality.name + '</span></li>'; } else { - ret += '<span class="label">' + item.quality.name + '</span> '; + ret += '<li><span class="label label-default">' + item.quality.name + '</span></li>'; } } }); diff --git a/src/UI/Settings/Quality/Profile/DeleteView.js b/src/UI/Settings/Quality/Profile/DeleteQualityProfileView.js similarity index 86% rename from src/UI/Settings/Quality/Profile/DeleteView.js rename to src/UI/Settings/Quality/Profile/DeleteQualityProfileView.js index 313ffb4cc..0aee00971 100644 --- a/src/UI/Settings/Quality/Profile/DeleteView.js +++ b/src/UI/Settings/Quality/Profile/DeleteQualityProfileView.js @@ -6,7 +6,7 @@ define( ], function (vent, Marionette) { return Marionette.ItemView.extend({ - template: 'Settings/Quality/Profile/DeleteTemplate', + template: 'Settings/Quality/Profile/DeleteQualityProfileViewTemplate', events: { 'click .x-confirm-delete': '_removeProfile' diff --git a/src/UI/Settings/Quality/Profile/DeleteQualityProfileViewTemplate.html b/src/UI/Settings/Quality/Profile/DeleteQualityProfileViewTemplate.html new file mode 100644 index 000000000..39c02fc1c --- /dev/null +++ b/src/UI/Settings/Quality/Profile/DeleteQualityProfileViewTemplate.html @@ -0,0 +1,15 @@ +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Delete: {{name}}</h3> + </div> + <div class="modal-body"> + <p>Are you sure you want to delete '{{name}}'?</p> + </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal">cancel</button> + <button class="btn btn-danger x-confirm-delete">delete</button> + </div> + </div> +</div> diff --git a/src/UI/Settings/Quality/Profile/DeleteTemplate.html b/src/UI/Settings/Quality/Profile/DeleteTemplate.html deleted file mode 100644 index d6e0f61e2..000000000 --- a/src/UI/Settings/Quality/Profile/DeleteTemplate.html +++ /dev/null @@ -1,11 +0,0 @@ -<div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete: {{name}}</h3> -</div> -<div class="modal-body"> - <p>Are you sure you want to delete '{{name}}'?</p> -</div> -<div class="modal-footer"> - <button class="btn" data-dismiss="modal">cancel</button> - <button class="btn btn-danger x-confirm-delete">delete</button> -</div> diff --git a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayout.js b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayout.js index 1ef9825d5..2a5ea7586 100644 --- a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayout.js +++ b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayout.js @@ -3,13 +3,26 @@ define( [ 'underscore', 'vent', + 'AppLayout', 'marionette', 'backbone', 'Settings/Quality/Profile/Edit/EditQualityProfileItemView', 'Settings/Quality/Profile/Edit/QualitySortableCollectionView', 'Settings/Quality/Profile/Edit/EditQualityProfileView', + 'Settings/Quality/Profile/DeleteQualityProfileView', + 'Series/SeriesCollection', 'Config' - ], function (_, vent, Marionette, Backbone, EditQualityProfileItemView, QualitySortableCollectionView, EditQualityProfileView, Config) { + ], function (_, + vent, + AppLayout, + Marionette, + Backbone, + EditQualityProfileItemView, + QualitySortableCollectionView, + EditQualityProfileView, + DeleteView, + SeriesCollection, + Config) { return Marionette.Layout.extend({ template: 'Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate', @@ -19,14 +32,24 @@ define( qualities: '#x-qualities' }, + ui: { + deleteButton: '.x-delete' + }, + events: { - 'click .x-save' : '_saveQualityProfile', - 'click .x-cancel': '_cancelQualityProfile' + 'click .x-save' : '_saveQualityProfile', + 'click .x-cancel' : '_cancelQualityProfile', + 'click .x-delete' : '_delete' }, initialize: function (options) { this.profileCollection = options.profileCollection; this.itemsCollection = new Backbone.Collection(_.toArray(this.model.get('items')).reverse()); + this.listenTo(SeriesCollection, 'all', this._updateDisableStatus); + }, + + onRender: function () { + this._updateDisableStatus(); }, onShow: function () { @@ -103,6 +126,25 @@ define( _showFieldsView: function () { this.fields.show(this.fieldsView); + }, + + _delete: function () { + var view = new DeleteView({ model: this.model }); + AppLayout.modalRegion.show(view); + }, + + _updateDisableStatus: function () { + if (this._isQualityInUse()) { + this.ui.deleteButton.addClass('disabled'); + this.ui.deleteButton.attr('title', 'Can\'t delete quality profiles attached to a series.'); + } + else { + this.ui.deleteButton.removeClass('disabled'); + } + }, + + _isQualityInUse: function () { + return SeriesCollection.where({'qualityProfileId': this.model.id}).length !== 0; } }); }); diff --git a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html index 7f6f1359f..ca134f24a 100644 --- a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html +++ b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html @@ -1,29 +1,37 @@ -<div class="modal-header"> - <button type="button" class="close x-cancel" aria-hidden="true">×</button> - {{#if id}} +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close x-cancel" aria-hidden="true">×</button> + {{#if id}} <h3>Edit</h3> - {{else}} + {{else}} <h3>Add</h3> - {{/if}} -</div> -<div class="modal-body"> - <div class="form-horizontal"> - <div id="x-fields"></div> - <div class="control-group"> - <label class="control-label">Qualities</label> - <div class="controls qualities-controls"> - <span id="x-qualities"></span> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Qualities higher in the list are more preferred. Only checked qualities will be wanted."/> - </span> + {{/if}} + </div> + <div class="modal-body"> + <div class="form-horizontal"> + <div id="x-fields"></div> + <div class="form-group"> + <label class="col-sm-3 control-label">Qualities</label> + + <div class="col-sm-5"> + <div class="controls qualities-controls"> + <span id="x-qualities"></span> + </div> + </div> + + <div class="col-sm-1 help-inline"> + <i class="icon-nd-form-info" title="Qualities higher in the list are more preferred. Only checked qualities will be wanted."/> + </div> + </div> </div> </div> + <div class="modal-footer"> + {{#if id}} + <button class="btn btn-danger pull-left x-delete">delete</button> + {{/if}} + <button class="btn x-cancel">cancel</button> + <button class="btn btn-primary x-save">save</button> + </div> </div> -</div> -<div class="modal-footer"> - {{#if id}} - <button class="btn btn-danger pull-left x-delete">delete</button> - {{/if}} - <button class="btn x-cancel">cancel</button> - <button class="btn btn-primary x-save">save</button> -</div> +</div> \ No newline at end of file diff --git a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate.html b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate.html index 87ba4deef..ef0da0ef3 100644 --- a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate.html +++ b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate.html @@ -1,21 +1,23 @@ -<div class="control-group"> - <label class="control-label">Name</label> - <div class="controls"> - <input type="text" name="name"> +<div class="form-group"> + <label class="col-sm-3 control-label">Name</label> + <div class="col-sm-5"> + <input type="text" name="name" class="form-control"> </div> </div> -<div class="control-group"> - <label class="control-label">Cutoff</label> - <div class="controls"> - <select class="x-cutoff" name="cutoff.id" validation-name="cutoff"> +<div class="form-group"> + <label class="col-sm-3 control-label">Cutoff</label> + + <div class="col-sm-5"> + <select class="form-control x-cutoff" name="cutoff.id" validation-name="cutoff"> {{#eachReverse items}} {{#if allowed}} <option value="{{quality.id}}">{{quality.name}}</option> {{/if}} {{/eachReverse}} </select> - <span class="help-inline"> - <i class="icon-nd-form-info" title="Once this quality is reached NzbDrone will no longer download episodes"/> - </span> + </div> + + <div class="col-sm-1 help-inline"> + <i class="icon-nd-form-info" title="Once this quality is reached NzbDrone will no longer download episodes"/> </div> </div> \ No newline at end of file diff --git a/src/UI/Settings/Quality/Profile/QualityProfileCollectionTemplate.html b/src/UI/Settings/Quality/Profile/QualityProfileCollectionTemplate.html index 7a00ff46b..88182fda8 100644 --- a/src/UI/Settings/Quality/Profile/QualityProfileCollectionTemplate.html +++ b/src/UI/Settings/Quality/Profile/QualityProfileCollectionTemplate.html @@ -1,7 +1,7 @@ <fieldset> <legend>Quality Profiles</legend> <div class="row"> - <div class="span12"> + <div class="col-md-12"> <ul class="quality-profiles thingies"> <li> <div class="quality-profile-item thingy add-card x-add-card"> diff --git a/src/UI/Settings/Quality/Profile/QualityProfileView.js b/src/UI/Settings/Quality/Profile/QualityProfileView.js index 4bf9e466d..45a5ede8e 100644 --- a/src/UI/Settings/Quality/Profile/QualityProfileView.js +++ b/src/UI/Settings/Quality/Profile/QualityProfileView.js @@ -5,12 +5,10 @@ define( 'AppLayout', 'marionette', 'Settings/Quality/Profile/Edit/EditQualityProfileLayout', - 'Settings/Quality/Profile/DeleteView', - 'Series/SeriesCollection', 'Mixins/AsModelBoundView', 'Settings/Quality/Profile/AllowedLabeler', 'bootstrap' - ], function (AppLayout, Marionette, EditProfileView, DeleteProfileView, SeriesCollection, AsModelBoundView) { + ], function (AppLayout, Marionette, EditProfileView, AsModelBoundView) { var view = Marionette.ItemView.extend({ template: 'Settings/Quality/Profile/QualityProfileViewTemplate', @@ -22,47 +20,16 @@ define( }, events: { - 'click .x-edit' : '_editProfile', - 'click .x-delete': '_deleteProfile' + 'click' : '_editProfile' }, initialize: function () { this.listenTo(this.model, 'sync', this.render); - this.listenTo(SeriesCollection, 'all', this._updateDisableStatus); }, _editProfile: function () { var view = new EditProfileView({ model: this.model, profileCollection: this.model.collection }); AppLayout.modalRegion.show(view); - }, - - _deleteProfile: function () { - if (this._isQualityInUse()) { - return; - } - - var view = new DeleteProfileView({ model: this.model }); - AppLayout.modalRegion.show(view); - }, - - onRender: function () { - this._updateDisableStatus(); - }, - - _updateDisableStatus: function () { - if (this._isQualityInUse()) { - this.ui.deleteButton.addClass('disabled'); - this.ui.deleteButton.attr('title', 'Can\'t delete quality profiles attached to a series.'); - } - else { - this.ui.deleteButton.removeClass('disabled'); - this.ui.deleteButton.attr('title', 'Delete Quality Profile'); - } - }, - - _isQualityInUse: function () { - return SeriesCollection.where({'qualityProfileId': this.model.id}).length !== 0; - } }); diff --git a/src/UI/Settings/Quality/Profile/QualityProfileViewTemplate.html b/src/UI/Settings/Quality/Profile/QualityProfileViewTemplate.html index 6d5247760..6dfc5d3ac 100644 --- a/src/UI/Settings/Quality/Profile/QualityProfileViewTemplate.html +++ b/src/UI/Settings/Quality/Profile/QualityProfileViewTemplate.html @@ -1,11 +1,9 @@ -<div class="quality-profile-item thingy"> +<div class="quality-profile-item thingy" title="Click to edit"> <div> <h3 name="name"></h3> - <span class="btn-group pull-right"> - <button class="btn btn-mini btn-icon-only x-edit"><i class="icon-nd-edit"/></button> - <button class="btn btn-mini btn-icon-only x-delete"><i class="icon-nd-delete"/></button> - </span> </div> - {{allowedLabeler}} + <ul class="allowed-qualities"> + {{allowedLabeler}} + </ul> </div> \ No newline at end of file diff --git a/src/UI/Settings/Quality/QualityLayoutTemplate.html b/src/UI/Settings/Quality/QualityLayoutTemplate.html index 9bd521d37..1684faf98 100644 --- a/src/UI/Settings/Quality/QualityLayoutTemplate.html +++ b/src/UI/Settings/Quality/QualityLayoutTemplate.html @@ -1,9 +1,9 @@ <div class="row"> - <div class="span12" id="quality-profile"/> + <div class="col-md-12" id="quality-profile"/> </div> <br/> <div class="row advanced-setting"> - <div class="span12" id="quality-definition"/> + <div class="col-md-12" id="quality-definition"/> </div> diff --git a/src/UI/Settings/Quality/quality.less b/src/UI/Settings/Quality/quality.less index c10c990f1..0376373b4 100644 --- a/src/UI/Settings/Quality/quality.less +++ b/src/UI/Settings/Quality/quality.less @@ -1,19 +1,27 @@ @import "../../Content/Bootstrap/mixins"; @import "../../Content/FontAwesome/font-awesome"; +@import "../../Shared/Styles/clickable.less"; .quality-profile-item { + .clickable; width: 300px; height: 120px; padding: 10px 15px; - h3 { - width: 240px; - } - &.add-card { .center { - margin-top: 30px; + margin-top: 10px; + } + } + + .allowed-qualities { + + padding-left: 0px; + + li { + list-style-type : none; + margin: 1px; } } } @@ -66,6 +74,7 @@ ul.qualities { .quality-label { color: #c6c6c6; } + .drag-handle, .select-handle { opacity: 0.2; line-height: 20px; diff --git a/src/UI/Settings/SettingsLayoutTemplate.html b/src/UI/Settings/SettingsLayoutTemplate.html index f5717ede2..84a826ecb 100644 --- a/src/UI/Settings/SettingsLayoutTemplate.html +++ b/src/UI/Settings/SettingsLayoutTemplate.html @@ -1,4 +1,4 @@ -<ul class="nav nav-tabs" id="myTab"> +<ul class="nav nav-tabs nav-justified settings-tabs"> <li><a href="#media-management" class="x-media-management-tab no-router">Media Management</a></li> <li><a href="#quality" class="x-quality-tab no-router">Quality</a></li> <li><a href="#indexers" class="x-indexers-tab no-router">Indexers</a></li> @@ -6,22 +6,32 @@ <li><a href="#notifications" class="x-notifications-tab no-router">Connect</a></li> <li><a href="#metadata" class="x-metadata-tab no-router">Metadata</a></li> <li><a href="#general" class="x-general-tab no-router">General</a></li> - <li class="pull-right"><button class="btn btn-primary x-save-settings">Save</button></li> - <li class="pull-right advanced-settings-toggle"> - <label class="checkbox toggle well"> - <input type="checkbox" class="x-advanced-settings"/> - <p> - <span>Shown</span> - <span>Hidden</span> - </p> - <div class="btn btn-warning slide-button"/> - </label> - <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Show advanced options"/> - </span> - </li> </ul> +<div class="row settings-controls"> + <div class="col-sm-4 col-sm-offset-7 col-md-3 col-md-offset-8"> + <div class="advanced-settings-toggle"> + <span class="help-inline-checkbox hidden-xs"> + Advanced Settings + </span> + <label class="checkbox toggle well"> + <input type="checkbox" class="x-advanced-settings"/> + <p> + <span>Shown</span> + <span>Hidden</span> + </p> + <div class="btn btn-warning slide-button"/> + </label> + <span class="help-inline-checkbox hidden-sm hidden-md hidden-lg"> + Advanced Settings + </span> + </div> + </div> + <div class="col-sm-1 col-md-1"> + <button class="btn btn-primary x-save-settings">Save</button> + </div> +</div> + <div class="tab-content"> <div class="tab-pane" id="media-management"></div> <div class="tab-pane" id="quality"></div> diff --git a/src/UI/Settings/settings.less b/src/UI/Settings/settings.less index c46a629bc..831d3d763 100644 --- a/src/UI/Settings/settings.less +++ b/src/UI/Settings/settings.less @@ -52,21 +52,26 @@ li.save-and-add:hover { width: 500px; } +.settings-controls { + margin-top: 10px; +} + .advanced-settings-toggle { - margin-right: 40px; + display: inline-block; + margin-bottom: 10px; .checkbox { width : 100px; margin-left : 0px; display : inline-block; padding-top : 0px; - margin-bottom : 0px; + margin-bottom : -10px; margin-top : -1px; } .help-inline-checkbox { display : inline-block; - margin-top : -23px; + margin-top : -3px; margin-bottom : 0; vertical-align : middle; } @@ -76,7 +81,7 @@ li.save-and-add:hover { display: none; .control-label { - color: @warningText; + color: @brand-warning; } } @@ -101,3 +106,14 @@ li.save-and-add:hover { cursor : text; } } + +.settings-tabs { + @media (min-width: @screen-sm-min) and (max-width: @screen-md-max) { + li { + a { + white-space : nowrap; + padding : 10px; + } + } + } +} diff --git a/src/UI/Settings/thingy.less b/src/UI/Settings/thingy.less index 6e8dad971..a9ccce814 100644 --- a/src/UI/Settings/thingy.less +++ b/src/UI/Settings/thingy.less @@ -28,6 +28,7 @@ .items { list-style-type: none; margin: 0px; + padding: 0px; li { display: inline-block; @@ -45,6 +46,7 @@ display: inline-block; white-space: nowrap; overflow: hidden; + line-height: 30px; text-overflow: ellipsis; } @@ -62,4 +64,8 @@ display: inline-block; vertical-align: top; } + + @media (max-width: @screen-xs-max) { + padding-left: 0px; + } } \ No newline at end of file diff --git a/src/UI/Shared/Grid/PagerTemplate.html b/src/UI/Shared/Grid/PagerTemplate.html index 44b6f9d78..2320337ea 100644 --- a/src/UI/Shared/Grid/PagerTemplate.html +++ b/src/UI/Shared/Grid/PagerTemplate.html @@ -11,5 +11,6 @@ </ul> <span class="total-records"> - Total Records: {{Number state.totalRecords}} + <span class="hidden-xs">Total records: {{Number state.totalRecords}}</span> + <span class="visible-xs label label-info" title="Total records">{{Number state.totalRecords}}</span> </span> \ No newline at end of file diff --git a/src/UI/Shared/Modal/ModalRegion.js b/src/UI/Shared/Modal/ModalRegion.js index 414138ee7..5aef6748e 100644 --- a/src/UI/Shared/Modal/ModalRegion.js +++ b/src/UI/Shared/Modal/ModalRegion.js @@ -21,7 +21,7 @@ define( }, showPanel: function () { - this.$el.addClass('modal hide fade'); + this.$el.addClass('modal fade'); //need tab index so close on escape works //https://github.com/twitter/bootstrap/issues/4663 diff --git a/src/UI/Shared/SignalRBroadcaster.js b/src/UI/Shared/SignalRBroadcaster.js index 9de613a4f..928b14e9a 100644 --- a/src/UI/Shared/SignalRBroadcaster.js +++ b/src/UI/Shared/SignalRBroadcaster.js @@ -50,20 +50,11 @@ define( this.signalRconnection.reconnected(function() { tryingToReconnect = false; - - var currentVersion = StatusModel.get('version'); - - var promise = StatusModel.fetch(); - promise.done(function () { - if (StatusModel.get('version') !== currentVersion) { - vent.trigger(vent.Events.ServerUpdated); - } - }); }); this.signalRconnection.disconnected(function () { if (tryingToReconnect) { - $('<div class="modal-backdrop"></div>').appendTo(document.body); + $('<div class="modal-backdrop fade in"></div>').appendTo(document.body); Messenger.show({ id : messengerId, diff --git a/src/UI/Shared/Toolbar/Button/ButtonCollectionView.js b/src/UI/Shared/Toolbar/Button/ButtonCollectionView.js index b5d5d19d4..f1207dfba 100644 --- a/src/UI/Shared/Toolbar/Button/ButtonCollectionView.js +++ b/src/UI/Shared/Toolbar/Button/ButtonCollectionView.js @@ -5,8 +5,23 @@ define( 'Shared/Toolbar/Button/ButtonView' ], function (Marionette, ButtonView) { return Marionette.CollectionView.extend({ - className: 'btn-group', - itemView : ButtonView + className : 'btn-group', + itemView : ButtonView, + + initialize: function (options) { + this.menu = options.menu; + this.className = 'btn-group'; + + if (options.menu.collapse) { + this.className += ' btn-group-collapse'; + } + }, + + onRender: function () { + if (this.menu.collapse) { + this.$el.addClass('btn-group-collapse'); + } + } }); }); diff --git a/src/UI/Shared/Toolbar/Button/ButtonView.js b/src/UI/Shared/Toolbar/Button/ButtonView.js index 45300e988..9356acc21 100644 --- a/src/UI/Shared/Toolbar/Button/ButtonView.js +++ b/src/UI/Shared/Toolbar/Button/ButtonView.js @@ -9,7 +9,7 @@ define( return Marionette.ItemView.extend({ template : 'Shared/Toolbar/ButtonTemplate', - className: 'btn', + className: 'btn btn-default btn-icon-only-xs', ui: { icon: 'i' @@ -33,6 +33,10 @@ define( this.$el.addClass('btn-icon-only'); } + if (this.model.get('className')) { + this.$el.addClass(this.model.get('className')); + } + var command = this.model.get('command'); if (command) { var properties = _.extend({ name: command }, this.model.get('properties')); diff --git a/src/UI/Shared/Toolbar/ButtonTemplate.html b/src/UI/Shared/Toolbar/ButtonTemplate.html index 428c15470..f664ce3f0 100644 --- a/src/UI/Shared/Toolbar/ButtonTemplate.html +++ b/src/UI/Shared/Toolbar/ButtonTemplate.html @@ -1 +1 @@ -<i class="{{icon}} x-icon"/> {{title}} +<i class="{{icon}} x-icon"/><span> {{title}}</span> diff --git a/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js b/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js index c6b66abc8..4ba258a2e 100644 --- a/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js +++ b/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js @@ -10,7 +10,7 @@ define( itemView : RadioButtonView, attributes: { - 'data-toggle': 'buttons-radio' + 'data-toggle': 'buttons' }, initialize: function (options) { diff --git a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js index af2f0c903..fe67f68cf 100644 --- a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js +++ b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js @@ -6,9 +6,9 @@ define( ], function (Marionette, Config) { return Marionette.ItemView.extend({ - template : 'Shared/Toolbar/ButtonTemplate', - className: 'btn', - + template : 'Shared/Toolbar/RadioButtonTemplate', + className: 'btn btn-default', + ui: { icon: 'i' }, @@ -34,7 +34,6 @@ define( if (this.model.get('tooltip')) { this.$el.attr('title', this.model.get('tooltip')); - this.$el.attr('data-container', 'body'); } }, diff --git a/src/UI/Shared/Toolbar/RadioButtonTemplate.html b/src/UI/Shared/Toolbar/RadioButtonTemplate.html new file mode 100644 index 000000000..3e75e7332 --- /dev/null +++ b/src/UI/Shared/Toolbar/RadioButtonTemplate.html @@ -0,0 +1 @@ +<input type="radio"><i class="{{icon}} x-icon"/><span> {{title}}</span> diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.html b/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.html index 62f6da91e..7085453c3 100644 --- a/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.html +++ b/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.html @@ -1,5 +1,5 @@ <div class="btn-group sorting-buttons"> - <a class="btn dropdown-toggle" data-toggle="dropdown" href="#"> + <a class="btn btn-default dropdown-toggle" data-toggle="dropdown" href="#"> Sort <span class="caret"></span> </a> <ul class="dropdown-menu"> diff --git a/src/UI/Shared/Toolbar/ToolbarLayout.js b/src/UI/Shared/Toolbar/ToolbarLayout.js index b1e2ee148..147c7f694 100644 --- a/src/UI/Shared/Toolbar/ToolbarLayout.js +++ b/src/UI/Shared/Toolbar/ToolbarLayout.js @@ -97,13 +97,13 @@ define( } } - var regionId = position + "_" + (index + 1); + var regionId = position + '_' + (index + 1); var region = this[regionId]; if (!region) { - var regionClassName = "x-toolbar-" + position + "-" + (index + 1); - this.ui[position + '_x'].append('<div class="toolbar-group '+regionClassName+'" />\r\n'); - region = this.addRegion(regionId, "." + regionClassName); + var regionClassName = 'x-toolbar-' + position + '-' + (index + 1); + this.ui[position + '_x'].append('<div class="toolbar-group ' + regionClassName + '" />\r\n'); + region = this.addRegion(regionId, '.' + regionClassName); } region.show(buttonGroupView); diff --git a/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html b/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html index 533f83bf9..9b676649e 100644 --- a/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html +++ b/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html @@ -1,2 +1,2 @@ -<div class="pull-left page-toolbar x-toolbar-left" /> -<div class="pull-right page-toolbar x-toolbar-right" /> +<div class="page-toolbar pull-left pull-none-xs x-toolbar-left" /> +<div class="page-toolbar pull-right pull-none-xs x-toolbar-right" /> diff --git a/src/UI/Shared/Tooltip.js b/src/UI/Shared/Tooltip.js new file mode 100644 index 000000000..11906faf9 --- /dev/null +++ b/src/UI/Shared/Tooltip.js @@ -0,0 +1,28 @@ +'use strict'; +define( + [ + 'jquery' + ], function ($) { + return { + + appInitializer: function () { + console.log('starting signalR'); + + $('body').tooltip({ + selector: '[title]', + container: 'body' + }); + + $(document).click(function(e) { + + if ($(e.target).attr('title') !== undefined) { + return; + } + + $('.tooltip').remove(); + }); + + return this; + } + }; + }); diff --git a/src/UI/System/Info/SystemInfoLayoutTemplate.html b/src/UI/System/Info/SystemInfoLayoutTemplate.html index 2a60a3487..2fa34bb09 100644 --- a/src/UI/System/Info/SystemInfoLayoutTemplate.html +++ b/src/UI/System/Info/SystemInfoLayoutTemplate.html @@ -1,11 +1,11 @@ <div class="row"> - <div class="span12" id="health"></div> + <div class="col-md-12" id="health"></div> </div> <div class="row"> - <div class="span12" id="about"></div> + <div class="col-md-12" id="about"></div> </div> <div class="row"> - <div class="span12" id="diskspace"></div> + <div class="col-md-12" id="diskspace"></div> </div> \ No newline at end of file diff --git a/src/UI/System/Logs/Files/ContentsViewTemplate.html b/src/UI/System/Logs/Files/ContentsViewTemplate.html index 2f779020e..ae6b5d35e 100644 --- a/src/UI/System/Logs/Files/ContentsViewTemplate.html +++ b/src/UI/System/Logs/Files/ContentsViewTemplate.html @@ -1,11 +1,11 @@ <div class="row"> - <div class="span12"> + <div class="col-md-12"> <h3>{{filename}}</h3> </div> </div> <div class="row"> - <div class="span12"> + <div class="col-md-12"> <pre>{{contents}}</pre> </div> </div> \ No newline at end of file diff --git a/src/UI/System/Logs/Files/LogFileLayoutTemplate.html b/src/UI/System/Logs/Files/LogFileLayoutTemplate.html index 024af966d..d1ccae5df 100644 --- a/src/UI/System/Logs/Files/LogFileLayoutTemplate.html +++ b/src/UI/System/Logs/Files/LogFileLayoutTemplate.html @@ -1,12 +1,12 @@ <div id="x-toolbar"/> <div class="row"> - <div class="span10"> + <div class="col-md-12"> <div id="x-grid"/> </div> </div> <div class="row"> - <div class="span10"> + <div class="col-md-12"> <div id="x-contents"/> </div> </div> \ No newline at end of file diff --git a/src/UI/System/Logs/LogsLayoutTemplate.html b/src/UI/System/Logs/LogsLayoutTemplate.html index 23881fc79..aa250b933 100644 --- a/src/UI/System/Logs/LogsLayoutTemplate.html +++ b/src/UI/System/Logs/LogsLayoutTemplate.html @@ -1,11 +1,15 @@ -<div class="tabbable tabs-left"> - <ul class="nav nav-tabs"> - <li><a href="#table" class="x-table-tab no-router">Table</a></li> - <li><a href="#files" class="x-files-tab no-router">Files</a></li> - </ul> +<div class="row"> + <div class="col-md-1 col-sm-2"> + <ul class="nav nav-pills nav-stacked"> + <li><a href="#table" class="x-table-tab no-router">Table</a></li> + <li><a href="#files" class="x-files-tab no-router">Files</a></li> + </ul> + </div> - <div class="tab-content"> - <div class="tab-pane" id="table"></div> - <div class="tab-pane" id="files"></div> + <div class="col-md-11 col-sm-10"> + <div class="tab-content"> + <div class="tab-pane" id="table"></div> + <div class="tab-pane" id="files"></div> + </div> </div> </div> \ No newline at end of file diff --git a/src/UI/System/Logs/Table/Details/LogDetailsViewTemplate.html b/src/UI/System/Logs/Table/Details/LogDetailsViewTemplate.html index 5ad8f7594..a8ec6bc52 100644 --- a/src/UI/System/Logs/Table/Details/LogDetailsViewTemplate.html +++ b/src/UI/System/Logs/Table/Details/LogDetailsViewTemplate.html @@ -1,22 +1,25 @@ -<div class="log-details-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> +<div class="modal-dialog"> + <div class="modal-content"> + <div class="log-details-modal"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Details</h3> + <h3>Details</h3> - </div> - <div class="modal-body"> - Message - <pre>{{message}}</pre> + </div> + <div class="modal-body"> + Message + <pre>{{message}}</pre> - {{#if exception}} - <br/> - Exception - <pre>{{exception}}</pre> - {{/if}} - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">close</button> + {{#if exception}} + <br/> + Exception + <pre>{{exception}}</pre> + {{/if}} + </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal">close</button> + </div> + </div> </div> </div> - diff --git a/src/UI/System/Logs/Table/LogsTableLayoutTemplate.html b/src/UI/System/Logs/Table/LogsTableLayoutTemplate.html index aae6c4dfc..f0f162d05 100644 --- a/src/UI/System/Logs/Table/LogsTableLayoutTemplate.html +++ b/src/UI/System/Logs/Table/LogsTableLayoutTemplate.html @@ -1,11 +1,11 @@ <div id="x-toolbar"/> <div class="row"> - <div class="span10"> + <div class="col-md-12 table-responsive"> <div id="x-grid"/> </div> </div> <div class="row"> - <div class="span10"> + <div class="col-md-12"> <div id="x-pager"/> </div> </div> diff --git a/src/UI/System/SystemLayout.js b/src/UI/System/SystemLayout.js index 7a5d51368..3500681c7 100644 --- a/src/UI/System/SystemLayout.js +++ b/src/UI/System/SystemLayout.js @@ -1,18 +1,20 @@ 'use strict'; define( [ + 'jquery', 'backbone', 'marionette', 'System/Info/SystemInfoLayout', 'System/Logs/LogsLayout', 'System/Update/UpdateLayout', - 'Commands/CommandController' - ], function (Backbone, + 'Shared/Messenger' + ], function ($, + Backbone, Marionette, SystemInfoLayout, LogsLayout, UpdateLayout, - CommandController) { + Messenger) { return Marionette.Layout.extend({ template: 'System/SystemLayoutTemplate', @@ -90,14 +92,26 @@ define( }, _shutdown: function () { - CommandController.Execute('shutdown', { - name : 'shutdown' + $.ajax({ + url: window.NzbDrone.ApiRoot + '/system/shutdown', + type: 'POST' + }); + + Messenger.show({ + message: 'NzbDrone will shutdown shortly', + type: 'info' }); }, _restart: function () { - CommandController.Execute('restart', { - name : 'restart' + $.ajax({ + url: window.NzbDrone.ApiRoot + '/system/restart', + type: 'POST' + }); + + Messenger.show({ + message: 'NzbDrone will restart shortly', + type: 'info' }); } }); diff --git a/src/UI/System/SystemLayoutTemplate.html b/src/UI/System/SystemLayoutTemplate.html index 1ee3a2349..c84df1ca7 100644 --- a/src/UI/System/SystemLayoutTemplate.html +++ b/src/UI/System/SystemLayoutTemplate.html @@ -4,10 +4,10 @@ <li><a href="#updates" class="x-updates-tab no-router">Updates</a></li> <li class="lifecycle-controls pull-right"> <div class="btn-group"> - <button class="btn btn-icon-only x-shutdown" title="Shutdown" data-container="body"> + <button class="btn btn-default btn-icon-only x-shutdown" title="Shutdown"> <i class="icon-nd-shutdown"></i> </button> - <button class="btn btn-icon-only x-restart" title="Restart" data-container="body"> + <button class="btn btn-default btn-icon-only x-restart" title="Restart"> <i class="icon-nd-restart"></i> </button> </div> diff --git a/src/UI/System/Update/UpdateItemView.js b/src/UI/System/Update/UpdateItemView.js index 362b76690..d51ddb19a 100644 --- a/src/UI/System/Update/UpdateItemView.js +++ b/src/UI/System/Update/UpdateItemView.js @@ -12,8 +12,27 @@ define( 'click .x-install-update': '_installUpdate' }, + initialize: function () { + this.updating = false; + }, + _installUpdate: function () { - CommandController.Execute('installUpdate', { updatePackage: this.model.toJSON() }); + if (this.updating) { + return; + } + + this.updating = true; + var self = this; + + var promise = CommandController.Execute('installUpdate', { updatePackage: this.model.toJSON() }); + + promise.done(function () { + window.setTimeout(function () { + self.updating = false; + }, 5000); + }); + + } }); }); diff --git a/src/UI/System/Update/UpdateItemViewTemplate.html b/src/UI/System/Update/UpdateItemViewTemplate.html index 796f1070e..95b56de51 100644 --- a/src/UI/System/Update/UpdateItemViewTemplate.html +++ b/src/UI/System/Update/UpdateItemViewTemplate.html @@ -5,17 +5,9 @@ - {{ShortDate releaseDate}} {{#if installed}}<i class="icon-ok" title="Installed"></i>{{/if}} - {{#if_windows}} - {{#if isUpgrade}} - <span class="label label-inverse install-update x-install-update">Install</span> - {{/if}} - {{else}} - {{#if isUpgrade}} - <span class="label label-inverse install-update"> - <a href="https://github.com/NzbDrone/NzbDrone/wiki/Installation#linux">Install</a> - </span> - {{/if}} - {{/if_windows}} + {{#if isUpgrade}} + <span class="label label-default install-update x-install-update">Install</span> + {{/if}} </span> </legend> diff --git a/src/UI/System/Update/UpdateLayoutTemplate.html b/src/UI/System/Update/UpdateLayoutTemplate.html index e77ec44f7..b3361c31d 100644 --- a/src/UI/System/Update/UpdateLayoutTemplate.html +++ b/src/UI/System/Update/UpdateLayoutTemplate.html @@ -1,5 +1,5 @@ <div class="row"> - <div class="span12"> + <div class="col-md-12"> <div id="x-updates"/> </div> </div> diff --git a/src/UI/Wanted/Cutoff/CutoffUnmetLayout.js b/src/UI/Wanted/Cutoff/CutoffUnmetLayout.js index aeca14fbb..d01397bd4 100644 --- a/src/UI/Wanted/Cutoff/CutoffUnmetLayout.js +++ b/src/UI/Wanted/Cutoff/CutoffUnmetLayout.js @@ -1,206 +1,202 @@ 'use strict'; -define( - [ - 'underscore', - 'marionette', - 'backgrid', - 'Wanted/Cutoff/CutoffUnmetCollection', - 'Cells/SeriesTitleCell', - 'Cells/EpisodeNumberCell', - 'Cells/EpisodeTitleCell', - 'Cells/RelativeDateCell', - 'Cells/EpisodeStatusCell', - 'Shared/Grid/Pager', - 'Shared/Toolbar/ToolbarLayout', - 'Shared/LoadingView', - 'Shared/Messenger', - 'Commands/CommandController', - 'backgrid.selectall' - ], function (_, - Marionette, - Backgrid, - CutoffUnmetCollection, - SeriesTitleCell, - EpisodeNumberCell, - EpisodeTitleCell, - RelativeDateCell, - EpisodeStatusCell, - GridPager, - ToolbarLayout, - LoadingView, - Messenger, - CommandController) { - return Marionette.Layout.extend({ - template: 'Wanted/Cutoff/CutoffUnmetLayoutTemplate', +define([ + 'underscore', + 'marionette', + 'backgrid', + 'Wanted/Cutoff/CutoffUnmetCollection', + 'Cells/SeriesTitleCell', + 'Cells/EpisodeNumberCell', + 'Cells/EpisodeTitleCell', + 'Cells/RelativeDateCell', + 'Cells/EpisodeStatusCell', + 'Shared/Grid/Pager', + 'Shared/Toolbar/ToolbarLayout', + 'Shared/LoadingView', + 'Shared/Messenger', + 'Commands/CommandController', + 'backgrid.selectall' +], function (_, + Marionette, + Backgrid, + CutoffUnmetCollection, + SeriesTitleCell, + EpisodeNumberCell, + EpisodeTitleCell, + RelativeDateCell, + EpisodeStatusCell, + GridPager, + ToolbarLayout, + LoadingView, + Messenger, + CommandController) { + return Marionette.Layout.extend({ + template : 'Wanted/Cutoff/CutoffUnmetLayoutTemplate', - regions: { - missing: '#x-missing', - toolbar: '#x-toolbar', - pager : '#x-pager' + regions : { + cutoff : '#x-cutoff-unmet', + toolbar : '#x-toolbar', + pager : '#x-pager' + }, + + ui : { + searchSelectedButton : '.btn i.icon-search' + }, + + columns : [ + { + name : '', + cell : 'select-row', + headerCell : 'select-all', + sortable : false }, - - ui: { - searchSelectedButton: '.btn i.icon-search' + { + name : 'series', + label : 'Series Title', + sortable : false, + cell : SeriesTitleCell }, - - columns: - [ - { - name : '', - cell : 'select-row', - headerCell: 'select-all', - sortable : false - }, - { - name : 'series', - label : 'Series Title', - sortable : false, - cell : SeriesTitleCell - }, - { - name : 'this', - label : 'Episode', - sortable : false, - cell : EpisodeNumberCell - }, - { - name : 'this', - label : 'Episode Title', - sortable : false, - cell : EpisodeTitleCell, - }, - { - name : 'airDateUtc', - label : 'Air Date', - cell : RelativeDateCell - }, - { - name : 'status', - label : 'Status', - cell : EpisodeStatusCell, - sortable: false - } - ], - - initialize: function () { - this.collection = new CutoffUnmetCollection(); - - this.listenTo(this.collection, 'sync', this._showTable); + { + name : 'this', + label : 'Episode', + sortable : false, + cell : EpisodeNumberCell }, - - onShow: function () { - this.missing.show(new LoadingView()); - this._showToolbar(); - this.collection.fetch(); + { + name : 'this', + label : 'Episode Title', + sortable : false, + cell : EpisodeTitleCell }, - - _showTable: function () { - this.missingGrid = new Backgrid.Grid({ - columns : this.columns, - collection: this.collection, - className : 'table table-hover' - }); - - this.missing.show(this.missingGrid); - - this.pager.show(new GridPager({ - columns : this.columns, - collection: this.collection - })); + { + name : 'airDateUtc', + label : 'Air Date', + cell : RelativeDateCell }, - - _showToolbar: function () { - var leftSideButtons = { - type : 'default', - storeState: false, - items : - [ - { - title: 'Search Selected', - icon : 'icon-search', - callback: this._searchSelected, - ownerContext: this - }, - { - title: 'Season Pass', - icon : 'icon-bookmark', - route: 'seasonpass' - } - ] - }; - - var filterOptions = { - type : 'radio', - storeState : false, - menuKey : 'wanted.filterMode', - defaultAction : 'monitored', - items : - [ - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-nd-monitored', - callback : this._setFilter - }, - { - key : 'unmonitored', - title : '', - tooltip : 'Unmonitored Only', - icon : 'icon-nd-unmonitored', - callback : this._setFilter - }, - ] - }; - - this.toolbar.show(new ToolbarLayout({ - left : - [ - leftSideButtons - ], - right : - [ - filterOptions - ], - context: this - })); - - CommandController.bindToCommand({ - element: this.$('.x-toolbar-left-1 .btn i.icon-search'), - command: { - name: 'episodeSearch' - } - }); - }, - - _setFilter: function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.collection.state.currentPage = 1; - var promise = this.collection.setFilterMode(mode); - - if (buttonContext) - buttonContext.ui.icon.spinForPromise(promise); - }, - - _searchSelected: function () { - var selected = this.missingGrid.getSelectedModels(); - - if (selected.length === 0) { - Messenger.show({ - type: 'error', - message: 'No episodes selected' - }); - - return; - } - - var ids = _.pluck(selected, 'id'); - - CommandController.Execute('episodeSearch', { - name : 'episodeSearch', - episodeIds: ids - }); + { + name : 'status', + label : 'Status', + cell : EpisodeStatusCell, + sortable : false } - }); + ], + + initialize : function () { + this.collection = new CutoffUnmetCollection(); + + this.listenTo(this.collection, 'sync', this._showTable); + }, + + onShow : function () { + this.cutoff.show(new LoadingView()); + this._showToolbar(); + this.collection.fetch(); + }, + + _showTable : function () { + this.cutoffGrid = new Backgrid.Grid({ + columns : this.columns, + collection : this.collection, + className : 'table table-hover' + }); + + this.cutoff.show(this.cutoffGrid); + + this.pager.show(new GridPager({ + columns : this.columns, + collection : this.collection + })); + }, + + _showToolbar : function () { + var leftSideButtons = { + type : 'default', + storeState : false, + items : [ + { + title : 'Search Selected', + icon : 'icon-search', + callback : this._searchSelected, + ownerContext : this, + className : 'x-search-selected' + }, + { + title : 'Season Pass', + icon : 'icon-bookmark', + route : 'seasonpass' + } + ] + }; + + var filterOptions = { + type : 'radio', + storeState : false, + menuKey : 'wanted.filterMode', + defaultAction : 'monitored', + items : [ + { + key : 'monitored', + title : '', + tooltip : 'Monitored Only', + icon : 'icon-nd-monitored', + callback : this._setFilter + }, + { + key : 'unmonitored', + title : '', + tooltip : 'Unmonitored Only', + icon : 'icon-nd-unmonitored', + callback : this._setFilter + } + ] + }; + + this.toolbar.show(new ToolbarLayout({ + left : [ + leftSideButtons + ], + right : [ + filterOptions + ], + context : this + })); + + CommandController.bindToCommand({ + element : this.$('.x-search-selected'), + command : { + name : 'episodeSearch' + } + }); + }, + + _setFilter : function (buttonContext) { + var mode = buttonContext.model.get('key'); + + this.collection.state.currentPage = 1; + var promise = this.collection.setFilterMode(mode); + + if (buttonContext) { + buttonContext.ui.icon.spinForPromise(promise); + } + }, + + _searchSelected : function () { + var selected = this.cutoffGrid.getSelectedModels(); + + if (selected.length === 0) { + Messenger.show({ + type : 'error', + message : 'No episodes selected' + }); + + return; + } + + var ids = _.pluck(selected, 'id'); + + CommandController.Execute('episodeSearch', { + name : 'episodeSearch', + episodeIds : ids + }); + } }); +}); diff --git a/src/UI/Wanted/Cutoff/CutoffUnmetLayoutTemplate.html b/src/UI/Wanted/Cutoff/CutoffUnmetLayoutTemplate.html index 958d5aa5e..13527bd29 100644 --- a/src/UI/Wanted/Cutoff/CutoffUnmetLayoutTemplate.html +++ b/src/UI/Wanted/Cutoff/CutoffUnmetLayoutTemplate.html @@ -1,11 +1,11 @@ <div id="x-toolbar"/> <div class="row"> - <div class="span12"> - <div id="x-missing"/> + <div class="col-md-12"> + <div id="x-cutoff-unmet"/> </div> </div> <div class="row"> - <div class="span12"> + <div class="col-md-12"> <div id="x-pager"/> </div> </div> diff --git a/src/UI/Wanted/Missing/MissingLayout.js b/src/UI/Wanted/Missing/MissingLayout.js index 234e7cfea..45bfb9d4e 100644 --- a/src/UI/Wanted/Missing/MissingLayout.js +++ b/src/UI/Wanted/Missing/MissingLayout.js @@ -1,230 +1,233 @@ 'use strict'; -define( - [ - 'underscore', - 'marionette', - 'backgrid', - 'Wanted/Missing/MissingCollection', - 'Cells/SeriesTitleCell', - 'Cells/EpisodeNumberCell', - 'Cells/EpisodeTitleCell', - 'Cells/RelativeDateCell', - 'Cells/EpisodeStatusCell', - 'Shared/Grid/Pager', - 'Shared/Toolbar/ToolbarLayout', - 'Shared/LoadingView', - 'Shared/Messenger', - 'Commands/CommandController', - 'backgrid.selectall' - ], function (_, - Marionette, - Backgrid, - MissingCollection, - SeriesTitleCell, - EpisodeNumberCell, - EpisodeTitleCell, - RelativeDateCell, - EpisodeStatusCell, - GridPager, - ToolbarLayout, - LoadingView, - Messenger, - CommandController) { - return Marionette.Layout.extend({ - template: 'Wanted/Missing/MissingLayoutTemplate', +define([ + 'underscore', + 'marionette', + 'backgrid', + 'Wanted/Missing/MissingCollection', + 'Cells/SeriesTitleCell', + 'Cells/EpisodeNumberCell', + 'Cells/EpisodeTitleCell', + 'Cells/RelativeDateCell', + 'Cells/EpisodeStatusCell', + 'Shared/Grid/Pager', + 'Shared/Toolbar/ToolbarLayout', + 'Shared/LoadingView', + 'Shared/Messenger', + 'Commands/CommandController', + 'backgrid.selectall' +], function (_, + Marionette, + Backgrid, + MissingCollection, + SeriesTitleCell, + EpisodeNumberCell, + EpisodeTitleCell, + RelativeDateCell, + EpisodeStatusCell, + GridPager, + ToolbarLayout, + LoadingView, + Messenger, + CommandController) { + return Marionette.Layout.extend({ + template : 'Wanted/Missing/MissingLayoutTemplate', - regions: { - missing: '#x-missing', - toolbar: '#x-toolbar', - pager : '#x-pager' + regions : { + missing : '#x-missing', + toolbar : '#x-toolbar', + pager : '#x-pager' + }, + + ui : { + searchSelectedButton : '.btn i.icon-search' + }, + + columns : [ + { + name : '', + cell : 'select-row', + headerCell : 'select-all', + sortable : false }, - - ui: { - searchSelectedButton: '.btn i.icon-search' + { + name : 'series', + label : 'Series Title', + sortable : false, + cell : SeriesTitleCell }, - - columns: - [ - { - name : '', - cell : 'select-row', - headerCell: 'select-all', - sortable : false - }, - { - name : 'series', - label : 'Series Title', - sortable : false, - cell : SeriesTitleCell - }, - { - name : 'this', - label : 'Episode', - sortable : false, - cell : EpisodeNumberCell - }, - { - name : 'this', - label : 'Episode Title', - sortable : false, - cell : EpisodeTitleCell - }, - { - name : 'airDateUtc', - label : 'Air Date', - cell : RelativeDateCell - }, - { - name : 'status', - label : 'Status', - cell : EpisodeStatusCell, - sortable: false - } - ], - - initialize: function () { - this.collection = new MissingCollection(); - - this.listenTo(this.collection, 'sync', this._showTable); + { + name : 'this', + label : 'Episode', + sortable : false, + cell : EpisodeNumberCell }, - - onShow: function () { - this.missing.show(new LoadingView()); - this._showToolbar(); - this.collection.fetch(); + { + name : 'this', + label : 'Episode Title', + sortable : false, + cell : EpisodeTitleCell }, - - _showTable: function () { - this.missingGrid = new Backgrid.Grid({ - columns : this.columns, - collection: this.collection, - className : 'table table-hover' - }); - - this.missing.show(this.missingGrid); - - this.pager.show(new GridPager({ - columns : this.columns, - collection: this.collection - })); + { + name : 'airDateUtc', + label : 'Air Date', + cell : RelativeDateCell }, - - _showToolbar: function () { - var leftSideButtons = { - type : 'default', - storeState: false, - items : - [ - { - title: 'Search Selected', - icon : 'icon-search', - callback: this._searchSelected, - ownerContext: this - }, - { - title: 'Search All Missing', - icon : 'icon-search', - callback: this._searchMissing, - ownerContext: this - }, - { - title: 'Season Pass', - icon : 'icon-bookmark', - route: 'seasonpass' - }, - { - title: 'Rescan Drone Factory Folder', - icon : 'icon-refresh', - command: 'downloadedepisodesscan', - properties: { - sendUpdates: true - } - } - ] - }; - - var filterOptions = { - type : 'radio', - storeState : false, - menuKey : 'wanted.filterMode', - defaultAction : 'monitored', - items : - [ - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-nd-monitored', - callback : this._setFilter - }, - { - key : 'unmonitored', - title : '', - tooltip : 'Unmonitored Only', - icon : 'icon-nd-unmonitored', - callback : this._setFilter - } - ] - }; - - this.toolbar.show(new ToolbarLayout({ - left : - [ - leftSideButtons - ], - right : - [ - filterOptions - ], - context: this - })); - - CommandController.bindToCommand({ - element: this.$('.x-toolbar-left-1 .btn i.icon-search'), - command: { - name: 'episodeSearch' - } - }); - }, - - _setFilter: function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.collection.state.currentPage = 1; - var promise = this.collection.setFilterMode(mode); - - if (buttonContext) - buttonContext.ui.icon.spinForPromise(promise); - }, - - _searchSelected: function () { - var selected = this.missingGrid.getSelectedModels(); - - if (selected.length === 0) { - Messenger.show({ - type: 'error', - message: 'No episodes selected' - }); - - return; - } - - var ids = _.pluck(selected, 'id'); - - CommandController.Execute('episodeSearch', { - name : 'episodeSearch', - episodeIds: ids - }); - }, - - _searchMissing: function () { - if (window.confirm('Are you sure you want to search for {0} missing episodes? '.format(this.collection.state.totalRecords) + - 'One API request to each indexer will be used for each episode. ' + - 'This cannot be stopped once started.')) { - CommandController.Execute('missingEpisodeSearch', { - name : 'missingEpisodeSearch' - }); - } + { + name : 'status', + label : 'Status', + cell : EpisodeStatusCell, + sortable : false } - }); + ], + + initialize : function () { + this.collection = new MissingCollection(); + + this.listenTo(this.collection, 'sync', this._showTable); + }, + + onShow : function () { + this.missing.show(new LoadingView()); + this._showToolbar(); + this.collection.fetch(); + }, + + _showTable : function () { + this.missingGrid = new Backgrid.Grid({ + columns : this.columns, + collection : this.collection, + className : 'table table-hover' + }); + + this.missing.show(this.missingGrid); + + this.pager.show(new GridPager({ + columns : this.columns, + collection : this.collection + })); + }, + + _showToolbar : function () { + var leftSideButtons = { + type : 'default', + storeState : false, + collapse : true, + items : [ + { + title : 'Search Selected', + icon : 'icon-search', + callback : this._searchSelected, + ownerContext : this, + className : 'x-search-selected' + }, + { + title : 'Search All Missing', + icon : 'icon-search', + callback : this._searchMissing, + ownerContext : this, + className : 'x-search-missing' + }, + { + title : 'Season Pass', + icon : 'icon-bookmark', + route : 'seasonpass' + }, + { + title : 'Rescan Drone Factory Folder', + icon : 'icon-refresh', + command : 'downloadedepisodesscan', + properties : { + sendUpdates : true + } + } + ] + }; + + var filterOptions = { + type : 'radio', + storeState : false, + menuKey : 'wanted.filterMode', + defaultAction : 'monitored', + items : [ + { + key : 'monitored', + title : '', + tooltip : 'Monitored Only', + icon : 'icon-nd-monitored', + callback : this._setFilter + }, + { + key : 'unmonitored', + title : '', + tooltip : 'Unmonitored Only', + icon : 'icon-nd-unmonitored', + callback : this._setFilter + } + ] + }; + + this.toolbar.show(new ToolbarLayout({ + left : [ + leftSideButtons + ], + right : [ + filterOptions + ], + context : this + })); + + CommandController.bindToCommand({ + element : this.$('.x-search-selected'), + command : { + name : 'episodeSearch' + } + }); + + CommandController.bindToCommand({ + element : this.$('.x-search-missing'), + command : { + name : 'missingEpisodeSearch' + } + }); + }, + + _setFilter : function (buttonContext) { + var mode = buttonContext.model.get('key'); + + this.collection.state.currentPage = 1; + var promise = this.collection.setFilterMode(mode); + + if (buttonContext) { + buttonContext.ui.icon.spinForPromise(promise); + } + }, + + _searchSelected : function () { + var selected = this.missingGrid.getSelectedModels(); + + if (selected.length === 0) { + Messenger.show({ + type : 'error', + message : 'No episodes selected' + }); + + return; + } + + var ids = _.pluck(selected, 'id'); + + CommandController.Execute('episodeSearch', { + name : 'episodeSearch', + episodeIds : ids + }); + }, + + _searchMissing : function () { + if (window.confirm('Are you sure you want to search for {0} missing episodes? '.format(this.collection.state.totalRecords) + 'One API request to each indexer will be used for each episode. ' + 'This cannot be stopped once started.')) { + CommandController.Execute('missingEpisodeSearch', { + name : 'missingEpisodeSearch' + }); + } + } }); +}); diff --git a/src/UI/Wanted/Missing/MissingLayoutTemplate.html b/src/UI/Wanted/Missing/MissingLayoutTemplate.html index 958d5aa5e..04c4df110 100644 --- a/src/UI/Wanted/Missing/MissingLayoutTemplate.html +++ b/src/UI/Wanted/Missing/MissingLayoutTemplate.html @@ -1,11 +1,11 @@ <div id="x-toolbar"/> <div class="row"> - <div class="span12"> + <div class="col-md-12 table-responsive"> <div id="x-missing"/> </div> </div> <div class="row"> - <div class="span12"> + <div class="col-md-12"> <div id="x-pager"/> </div> </div> diff --git a/src/UI/app.js b/src/UI/app.js index 85d8a566d..016761351 100644 --- a/src/UI/app.js +++ b/src/UI/app.js @@ -27,6 +27,7 @@ require.config({ 'jquery.dotdotdot' : 'JsLibraries/jquery.dotdotdot', 'messenger' : 'JsLibraries/messenger', 'jquery' : 'JsLibraries/jquery', + 'typeahead' : 'JsLibraries/typeahead', 'zero.clipboard' : 'JsLibraries/zero.clipboard', 'libs' : 'JsLibraries/', @@ -50,7 +51,12 @@ require.config({ [ 'jquery' ], - exports: 'Messenger' + exports: 'Messenger', + init : function () { + window.Messenger.options = { + theme: 'flat' + }; + } }, signalR : { deps: @@ -62,12 +68,7 @@ require.config({ deps: [ 'jquery' - ], - init: function ($) { - $('body').tooltip({ - selector: '[title]' - }); - } + ] }, backstrech : { deps: @@ -109,6 +110,12 @@ require.config({ } }, + 'typeahead' : { + deps: + [ + 'jquery' + ] + }, 'jquery-ui' : { deps: [ @@ -236,9 +243,10 @@ define( 'Shared/Modal/ModalController', 'Shared/ControlPanel/ControlPanelController', 'System/StatusModel', + 'Shared/Tooltip', 'Instrumentation/StringFormat', 'LifeCycle' - ], function ($, Backbone, Marionette, RouteBinder, SignalRBroadcaster, NavbarView, AppLayout, SeriesController, Router, ModalController, ControlPanelController, serverStatusModel) { + ], function ($, Backbone, Marionette, RouteBinder, SignalRBroadcaster, NavbarView, AppLayout, SeriesController, Router, ModalController, ControlPanelController, serverStatusModel, Tooltip) { new SeriesController(); new ModalController(); @@ -255,6 +263,10 @@ define( app: app }); + app.addInitializer(Tooltip.appInitializer, { + app: app + }); + app.addInitializer(function () { Backbone.history.start({ pushState: true, root: serverStatusModel.get('urlBase') }); RouteBinder.bind(); diff --git a/src/UI/index.html b/src/UI/index.html index 7fe301c61..43eb62eb9 100644 --- a/src/UI/index.html +++ b/src/UI/index.html @@ -3,10 +3,11 @@ <head runat="server"> <title>NzbDrone + - + @@ -27,21 +28,21 @@ - +
+ +
-
+
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
@@ -52,9 +53,9 @@