Calendar/Date localization

New: Choose calendar starting day of week
New: Choose prefered date/time formats
New: Option to disable relative dates and show absolute dates instead
This commit is contained in:
Mark McDowall 2014-08-04 22:44:09 -07:00
parent 0ba19f0cd7
commit 18874e2c79
32 changed files with 4049 additions and 1808 deletions

View File

@ -0,0 +1,45 @@
using System.Linq;
using System.Reflection;
using NzbDrone.Core.Configuration;
using Omu.ValueInjecter;
namespace NzbDrone.Api.Config
{
public class UiConfigModule : NzbDroneRestModule<UiConfigResource>
{
private readonly IConfigService _configService;
public UiConfigModule(IConfigService configService)
: base("/config/ui")
{
_configService = configService;
GetResourceSingle = GetUiConfig;
GetResourceById = GetUiConfig;
UpdateResource = SaveUiConfig;
}
private UiConfigResource GetUiConfig()
{
var resource = new UiConfigResource();
resource.InjectFrom(_configService);
resource.Id = 1;
return resource;
}
private UiConfigResource GetUiConfig(int id)
{
return GetUiConfig();
}
private void SaveUiConfig(UiConfigResource resource)
{
var dictionary = resource.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
_configService.SaveConfigDictionary(dictionary);
}
}
}

View File

@ -0,0 +1,18 @@
using System;
using NzbDrone.Api.REST;
namespace NzbDrone.Api.Config
{
public class UiConfigResource : RestResource
{
//Calendar
public Int32 FirstDayOfWeek { get; set; }
public String CalendarWeekColumnHeader { get; set; }
//Dates
public String ShortDateFormat { get; set; }
public String LongDateFormat { get; set; }
public String TimeFormat { get; set; }
public Boolean ShowRelativeDates { get; set; }
}
}

View File

@ -92,6 +92,8 @@
<Compile Include="ClientSchema\SelectOption.cs" /> <Compile Include="ClientSchema\SelectOption.cs" />
<Compile Include="Commands\CommandModule.cs" /> <Compile Include="Commands\CommandModule.cs" />
<Compile Include="Commands\CommandResource.cs" /> <Compile Include="Commands\CommandResource.cs" />
<Compile Include="Config\UiConfigModule.cs" />
<Compile Include="Config\UiConfigResource.cs" />
<Compile Include="Config\DownloadClientConfigModule.cs" /> <Compile Include="Config\DownloadClientConfigModule.cs" />
<Compile Include="Config\DownloadClientConfigResource.cs" /> <Compile Include="Config\DownloadClientConfigResource.cs" />
<Compile Include="Config\HostConfigModule.cs" /> <Compile Include="Config\HostConfigModule.cs" />

View File

@ -3,11 +3,9 @@ using Nancy.Routing;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Api.Extensions; using NzbDrone.Api.Extensions;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Processes;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Lifecycle.Commands;
namespace NzbDrone.Api.System namespace NzbDrone.Api.System
{ {
@ -60,7 +58,6 @@ namespace NzbDrone.Api.System
IsWindows = OsInfo.IsWindows, IsWindows = OsInfo.IsWindows,
Branch = _configFileProvider.Branch, Branch = _configFileProvider.Branch,
Authentication = _configFileProvider.AuthenticationEnabled, Authentication = _configFileProvider.AuthenticationEnabled,
StartOfWeek = (int)OsInfo.FirstDayOfWeek,
SqliteVersion = _database.Version, SqliteVersion = _database.Version,
UrlBase = _configFileProvider.UrlBase, UrlBase = _configFileProvider.UrlBase,
RuntimeVersion = OsInfo.IsMono ? _runtimeInfo.RuntimeVersion : null RuntimeVersion = OsInfo.IsMono ? _runtimeInfo.RuntimeVersion : null

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@ -254,6 +255,48 @@ namespace NzbDrone.Core.Configuration
set { SetValue("ChownGroup", value); } set { SetValue("ChownGroup", value); }
} }
public Int32 FirstDayOfWeek
{
get { return GetValueInt("FirstDayOfWeek", (int)OsInfo.FirstDayOfWeek); }
set { SetValue("FirstDayOfWeek", value); }
}
public String CalendarWeekColumnHeader
{
get { return GetValue("CalendarWeekColumnHeader", "ddd M/D"); }
set { SetValue("CalendarWeekColumnHeader", value); }
}
public String ShortDateFormat
{
get { return GetValue("ShortDateFormat", "MMM D YYYY"); }
set { SetValue("ShortDateFormat", value); }
}
public String LongDateFormat
{
get { return GetValue("LongDateFormat", "dddd, MMMM D YYYY"); }
set { SetValue("LongDateFormat", value); }
}
public String TimeFormat
{
get { return GetValue("TimeFormat", "h(:mm)a"); }
set { SetValue("TimeFormat", value); }
}
public Boolean ShowRelativeDates
{
get { return GetValueBoolean("ShowRelativeDates", true); }
set { SetValue("ShowRelativeDates", value); }
}
private string GetValue(string key) private string GetValue(string key)
{ {
return GetValue(key, String.Empty); return GetValue(key, String.Empty);

View File

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Update;
namespace NzbDrone.Core.Configuration namespace NzbDrone.Core.Configuration
{ {
@ -49,5 +48,14 @@ namespace NzbDrone.Core.Configuration
Int32 Retention { get; set; } Int32 Retention { get; set; }
Int32 RssSyncInterval { get; set; } Int32 RssSyncInterval { get; set; }
String ReleaseRestrictions { get; set; } String ReleaseRestrictions { get; set; }
//UI
Int32 FirstDayOfWeek { get; set; }
String CalendarWeekColumnHeader { get; set; }
String ShortDateFormat { get; set; }
String LongDateFormat { get; set; }
String TimeFormat { get; set; }
Boolean ShowRelativeDates { get; set; }
} }
} }

View File

@ -7,13 +7,13 @@ define(
'marionette', 'marionette',
'moment', 'moment',
'Calendar/Collection', 'Calendar/Collection',
'System/StatusModel', 'Shared/UiSettingsModel',
'History/Queue/QueueCollection', 'History/Queue/QueueCollection',
'Config', 'Config',
'Mixins/backbone.signalr.mixin', 'Mixins/backbone.signalr.mixin',
'fullcalendar', 'fullcalendar',
'jquery.easypiechart' 'jquery.easypiechart'
], function ($, vent, Marionette, moment, CalendarCollection, StatusModel, QueueCollection, Config) { ], function ($, vent, Marionette, moment, CalendarCollection, UiSettings, QueueCollection, Config) {
return Marionette.ItemView.extend({ return Marionette.ItemView.extend({
storageKey: 'calendar.view', storageKey: 'calendar.view',
@ -44,39 +44,45 @@ define(
this.$(element).addClass(event.statusLevel); this.$(element).addClass(event.statusLevel);
this.$(element).children('.fc-event-inner').addClass(event.statusLevel); this.$(element).children('.fc-event-inner').addClass(event.statusLevel);
if (event.progress > 0) { if (event.downloading) {
this.$(element).find('.fc-event-time') var progress = 100 - (event.downloading.get('sizeleft') / event.downloading.get('size') * 100);
.after('<span class="chart pull-right" data-percent="{0}"></span>'.format(event.progress)); var releaseTitle = event.downloading.get('title');
var estimatedCompletionTime = moment(event.downloading.get('estimatedCompletionTime')).fromNow();
this.$(element).find('.chart').easyPieChart({ if (event.downloading.get('status').toLocaleLowerCase() === 'pending') {
barColor : '#ffffff', this.$(element).find('.fc-event-time')
trackColor: false, .after('<span class="pending pull-right"><i class="icon-time"></i></span>');
scaleColor: false,
lineWidth : 2,
size : 14,
animate : false
});
this.$(element).find('.chart').tooltip({ this.$(element).find('.pending').tooltip({
title: 'Episode is downloading - {0}% {1}'.format(event.progress.toFixed(1), event.releaseTitle), title: 'Release will be processed {0}'.format(estimatedCompletionTime),
container: 'body' container: 'body'
}); });
} }
if (event.pending) { else {
this.$(element).find('.fc-event-time') this.$(element).find('.fc-event-time')
.after('<span class="pending pull-right"><i class="icon-time"></i></span>'); .after('<span class="chart pull-right" data-percent="{0}"></span>'.format(progress));
this.$(element).find('.pending').tooltip({ this.$(element).find('.chart').easyPieChart({
title: 'Release will be processed {0}'.format(event.pending), barColor : '#ffffff',
container: 'body' trackColor: false,
}); scaleColor: false,
lineWidth : 2,
size : 14,
animate : false
});
this.$(element).find('.chart').tooltip({
title: 'Episode is downloading - {0}% {1}'.format(progress.toFixed(1), releaseTitle),
container: 'body'
});
}
} }
}, },
_getEvents: function (view) { _getEvents: function (view) {
var start = moment(view.visStart).toISOString(); var start = view.start.toISOString();
var end = moment(view.visEnd).toISOString(); var end = view.end.toISOString();
this.$el.fullCalendar('removeEvents'); this.$el.fullCalendar('removeEvents');
@ -99,13 +105,10 @@ define(
var event = { var event = {
title : seriesTitle, title : seriesTitle,
start : start, start : moment(start),
end : end, end : moment(end),
allDay : false, allDay : false,
statusLevel : self._getStatusLevel(model, end), statusLevel : self._getStatusLevel(model, end),
progress : self._getDownloadProgress(model),
pending : self._getPendingInfo(model),
releaseTitle: self._getReleaseTitle(model),
downloading : QueueCollection.findEpisode(model.get('id')), downloading : QueueCollection.findEpisode(model.get('id')),
model : model model : model
}; };
@ -153,47 +156,12 @@ define(
this._setEventData(this.collection); this._setEventData(this.collection);
}, },
_getDownloadProgress: function (element) {
var downloading = QueueCollection.findEpisode(element.get('id'));
if (!downloading) {
return 0;
}
return 100 - (downloading.get('sizeleft') / downloading.get('size') * 100);
},
_getPendingInfo: function (element) {
var pending = QueueCollection.findEpisode(element.get('id'));
if (!pending || pending.get('status').toLocaleLowerCase() !== 'pending') {
return undefined;
}
return moment(pending.get('estimatedCompletionTime')).fromNow();
},
_getReleaseTitle: function (element) {
var downloading = QueueCollection.findEpisode(element.get('id'));
if (!downloading) {
return '';
}
return downloading.get('title');
},
_getOptions: function () { _getOptions: function () {
var options = { var options = {
allDayDefault : false, allDayDefault : false,
ignoreTimezone: false,
weekMode : 'variable', weekMode : 'variable',
firstDay : StatusModel.get('startOfWeek'), firstDay : UiSettings.get('firstDayOfWeek'),
timeFormat : 'h(:mm)tt', timeFormat : 'h(:mm)a',
buttonText : {
prev: '<i class="icon-arrow-left"></i>',
next: '<i class="icon-arrow-right"></i>'
},
viewRender : this._viewRender.bind(this), viewRender : this._viewRender.bind(this),
eventRender : this._eventRender.bind(this), eventRender : this._eventRender.bind(this),
eventClick : function (event) { eventClick : function (event) {
@ -204,12 +172,6 @@ define(
if ($(window).width() < 768) { if ($(window).width() < 768) {
options.defaultView = Config.getValue(this.storageKey, 'basicDay'); options.defaultView = Config.getValue(this.storageKey, 'basicDay');
options.titleFormat = {
month: 'MMM yyyy', // September 2009
week: 'MMM d[ yyyy]{ \'&#8212;\'[ MMM] d yyyy}', // Sep 7 - 13 2009
day: 'ddd, MMM d, yyyy' // Tuesday, Sep 8, 2009
};
options.header = { options.header = {
left : 'prev,next today', left : 'prev,next today',
center: 'title', center: 'title',
@ -220,12 +182,6 @@ define(
else { else {
options.defaultView = Config.getValue(this.storageKey, 'basicWeek'); options.defaultView = Config.getValue(this.storageKey, 'basicWeek');
options.titleFormat = {
month: 'MMM yyyy', // September 2009
week: 'MMM d[ yyyy]{ \'&#8212;\'[ MMM] d yyyy}', // Sep 7 - 13 2009
day: 'dddd, MMM d, yyyy' // Tues, Sep 8, 2009
};
options.header = { options.header = {
left : 'prev,next today', left : 'prev,next today',
center: 'title', center: 'title',
@ -233,6 +189,22 @@ define(
}; };
} }
options.titleFormat = {
month : 'MMMM YYYY',
week : UiSettings.get('shortDateFormat'),
day : UiSettings.get('longDateFormat')
};
options.columnFormat = {
month : 'ddd', // Mon
week : UiSettings.get('calendarWeekColumnHeader'),
day : 'dddd' // Monday
};
options.timeFormat = {
'default': UiSettings.get('timeFormat')
};
return options; return options;
} }
}); });

View File

@ -4,18 +4,18 @@ define(
'backbone', 'backbone',
'moment', 'moment',
'Series/EpisodeModel' 'Series/EpisodeModel'
], function (Backbone, Moment, EpisodeModel) { ], function (Backbone, moment, EpisodeModel) {
return Backbone.Collection.extend({ return Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/calendar', url : window.NzbDrone.ApiRoot + '/calendar',
model: EpisodeModel, model: EpisodeModel,
comparator: function (model1, model2) { comparator: function (model1, model2) {
var airDate1 = model1.get('airDateUtc'); var airDate1 = model1.get('airDateUtc');
var date1 = Moment(airDate1); var date1 = moment(airDate1);
var time1 = date1.unix(); var time1 = date1.unix();
var airDate2 = model2.get('airDateUtc'); var airDate2 = model2.get('airDateUtc');
var date2 = Moment(airDate2); var date2 = moment(airDate2);
var time2 = date2.unix(); var time2 = date2.unix();
if (time1 < time2){ if (time1 < time2){

View File

@ -5,7 +5,7 @@ define(
'vent', 'vent',
'marionette', 'marionette',
'moment' 'moment'
], function (vent, Marionette, Moment) { ], function (vent, Marionette, moment) {
return Marionette.ItemView.extend({ return Marionette.ItemView.extend({
template: 'Calendar/UpcomingItemViewTemplate', template: 'Calendar/UpcomingItemViewTemplate',
tagName : 'div', tagName : 'div',
@ -17,7 +17,7 @@ define(
initialize: function () { initialize: function () {
var start = this.model.get('airDateUtc'); var start = this.model.get('airDateUtc');
var runtime = this.model.get('series').runtime; var runtime = this.model.get('series').runtime;
var end = Moment(start).add('minutes', runtime); var end = moment(start).add('minutes', runtime);
this.model.set({ this.model.set({
end: end.toISOString() end: end.toISOString()

View File

@ -8,7 +8,7 @@ define(
'History/Queue/QueueCollection', 'History/Queue/QueueCollection',
'moment', 'moment',
'Shared/FormatHelpers' 'Shared/FormatHelpers'
], function (reqres, Backbone, NzbDroneCell, QueueCollection, Moment, FormatHelpers) { ], function (reqres, Backbone, NzbDroneCell, QueueCollection, moment, FormatHelpers) {
return NzbDroneCell.extend({ return NzbDroneCell.extend({
className: 'episode-status-cell', className: 'episode-status-cell',
@ -29,7 +29,7 @@ define(
var icon; var icon;
var tooltip; var tooltip;
var hasAired = Moment(this.model.get('airDateUtc')).isBefore(Moment()); var hasAired = moment(this.model.get('airDateUtc')).isBefore(moment());
var hasFile = this.model.get('hasFile'); var hasFile = this.model.get('hasFile');
if (hasFile) { if (hasFile) {

View File

@ -3,18 +3,32 @@ define(
[ [
'Cells/NzbDroneCell', 'Cells/NzbDroneCell',
'moment', 'moment',
'Shared/FormatHelpers' 'Shared/FormatHelpers',
], function (NzbDroneCell, Moment, FormatHelpers) { 'Shared/UiSettingsModel'
], function (NzbDroneCell, moment, FormatHelpers, UiSettings) {
return NzbDroneCell.extend({ return NzbDroneCell.extend({
className: 'relative-date-cell', className: 'relative-date-cell',
render: function () { render: function () {
var date = this.model.get(this.column.get('name')); var dateStr = this.model.get(this.column.get('name'));
if (date) { if (dateStr) {
this.$el.html('<span title="' + Moment(date).format('LLLL') + '" >' + FormatHelpers.dateHelper(date) + '</span>'); var date = moment(dateStr);
var result = '<span title="{0}">{1}</span>';
var tooltip = date.format(UiSettings.longDateTime());
var text;
if (UiSettings.get('showRelativeDates')) {
text = FormatHelpers.relativeDate(dateStr);
}
else {
text = date.format(UiSettings.get('shortDateFormat'));
}
this.$el.html(result.format(tooltip, text));
} }
return this; return this;

View File

@ -1,5 +1,5 @@
/*! /*!
* FullCalendar v1.6.4 Stylesheet * FullCalendar v2.0.2 Stylesheet
* Docs & License: http://arshaw.com/fullcalendar/ * Docs & License: http://arshaw.com/fullcalendar/
* (c) 2013 Adam Shaw * (c) 2013 Adam Shaw
*/ */
@ -101,11 +101,14 @@ html .fc,
------------------------------------------------------------------------*/ ------------------------------------------------------------------------*/
.fc-content { .fc-content {
position: relative;
z-index: 1; /* scopes all other z-index's to be inside this container */
clear: both; clear: both;
zoom: 1; /* for IE7, gives accurate coordinates for [un]freezeContentHeight */ zoom: 1; /* for IE7, gives accurate coordinates for [un]freezeContentHeight */
} }
.fc-view { .fc-view {
position: relative;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
} }
@ -165,32 +168,38 @@ html .fc,
and we'll try to make them look good cross-browser. and we'll try to make them look good cross-browser.
*/ */
.fc-text-arrow { .fc-button .fc-icon {
margin: 0 .1em; margin: 0 .1em;
font-size: 2em; font-size: 2em;
font-family: "Courier New", Courier, monospace; font-family: "Courier New", Courier, monospace;
vertical-align: baseline; /* for IE7 */ vertical-align: baseline; /* for IE7 */
} }
.fc-button-prev .fc-text-arrow, .fc-icon-left-single-arrow:after {
.fc-button-next .fc-text-arrow { /* for &lsaquo; &rsaquo; */ content: "\02039";
font-weight: bold; font-weight: bold;
} }
.fc-icon-right-single-arrow:after {
content: "\0203A";
font-weight: bold;
}
.fc-icon-left-double-arrow:after {
content: "\000AB";
}
.fc-icon-right-double-arrow:after {
content: "\000BB";
}
/* icon (for jquery ui) */ /* icon (for jquery ui) */
.fc-button .fc-icon-wrap {
position: relative;
float: left;
top: 50%;
}
.fc-button .ui-icon { .fc-button .ui-icon {
position: relative; position: relative;
top: 50%;
float: left; float: left;
margin-top: -50%; margin-top: -8px; /* we know jqui icons are always 16px tall */
*margin-top: 0;
*top: -50%;
} }
/* /*
@ -447,10 +456,13 @@ table.fc-border-separate {
padding: 0 4px; padding: 0 4px;
vertical-align: middle; vertical-align: middle;
text-align: right; text-align: right;
white-space: nowrap;
font-weight: normal; font-weight: normal;
} }
.fc-agenda-slots .fc-agenda-axis {
white-space: nowrap;
}
.fc-agenda .fc-week-number { .fc-agenda .fc-week-number {
font-weight: bold; font-weight: bold;
} }

View File

@ -4,7 +4,7 @@
<span class="label label-info">{{network}}</span> <span class="label label-info">{{network}}</span>
{{/with}} {{/with}}
<span class="label label-info">{{StartTime airDateUtc}}</span> <span class="label label-info">{{StartTime airDateUtc}}</span>
<span class="label label-info">{{NextAiring airDateUtc}}</span> <span class="label label-info">{{RelativeDate airDateUtc}}</span>
</div> </div>
<div class="episode-overview"> <div class="episode-overview">

View File

@ -3,26 +3,39 @@ define(
[ [
'handlebars', 'handlebars',
'moment', 'moment',
'Shared/FormatHelpers' 'Shared/FormatHelpers',
], function (Handlebars, Moment, FormatHelpers) { 'Shared/UiSettingsModel'
], function (Handlebars, moment, FormatHelpers, UiSettings) {
Handlebars.registerHelper('ShortDate', function (input) { Handlebars.registerHelper('ShortDate', function (input) {
if (!input) { if (!input) {
return ''; return '';
} }
var date = Moment(input); var date = moment(input);
var result = '<span title="' + date.format('LLLL') + '">' + date.format('LL') + '</span>'; var result = '<span title="' + date.format(UiSettings.longDateTime()) + '">' + date.format(UiSettings.get('shortDateFormat')) + '</span>';
return new Handlebars.SafeString(result); return new Handlebars.SafeString(result);
}); });
Handlebars.registerHelper('NextAiring', function (input) { Handlebars.registerHelper('RelativeDate', function (input) {
if (!input) { if (!input) {
return ''; return '';
} }
var date = Moment(input); var date = moment(input);
var result = '<span title="' + date.format('LLLL') + '">' + FormatHelpers.dateHelper(input) + '</span>'; var result = '<span title="{0}">{1}</span>';
var tooltip = date.format(UiSettings.longDateTime());
var text;
if (UiSettings.get('showRelativeDates')) {
text = FormatHelpers.relativeDate(input);
}
else {
text = date.format(UiSettings.get('shortDateFormat'));
}
result = result.format(tooltip, text);
return new Handlebars.SafeString(result); return new Handlebars.SafeString(result);
}); });
@ -32,7 +45,7 @@ define(
return ''; return '';
} }
return Moment(input).format('DD'); return moment(input).format('DD');
}); });
Handlebars.registerHelper('Month', function (input) { Handlebars.registerHelper('Month', function (input) {
@ -40,7 +53,7 @@ define(
return ''; return '';
} }
return Moment(input).format('MMM'); return moment(input).format('MMM');
}); });
Handlebars.registerHelper('StartTime', function (input) { Handlebars.registerHelper('StartTime', function (input) {
@ -48,11 +61,11 @@ define(
return ''; return '';
} }
var date = Moment(input); var date = moment(input);
if (date.format('mm') === '00') { if (date.format('mm') === '00') {
return date.format('ha'); return date.format('ha');
} }
return date.format('h.mma'); return date.format('h:mma');
}); });
}); });

View File

@ -4,11 +4,11 @@ define(
'handlebars', 'handlebars',
'Shared/FormatHelpers', 'Shared/FormatHelpers',
'moment' 'moment'
], function (Handlebars, FormatHelpers, Moment) { ], function (Handlebars, FormatHelpers, moment) {
Handlebars.registerHelper('EpisodeNumber', function () { Handlebars.registerHelper('EpisodeNumber', function () {
if (this.series.seriesType === 'daily') { if (this.series.seriesType === 'daily') {
return Moment(this.airDate).format('L'); return moment(this.airDate).format('L');
} }
else { else {
@ -21,9 +21,9 @@ define(
var hasFile = this.hasFile; var hasFile = this.hasFile;
var downloading = require('History/Queue/QueueCollection').findEpisode(this.id) || this.downloading; var downloading = require('History/Queue/QueueCollection').findEpisode(this.id) || this.downloading;
var currentTime = Moment(); var currentTime = moment();
var start = Moment(this.airDateUtc); var start = moment(this.airDateUtc);
var end = Moment(this.end); var end = moment(this.end);
if (hasFile) { if (hasFile) {
return 'success'; return 'success';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,7 @@ define(
EpisodeNumberCell, EpisodeNumberCell,
EpisodeWarningCell, EpisodeWarningCell,
CommandController, CommandController,
Moment, moment,
_, _,
Messenger) { Messenger) {
return Marionette.Layout.extend({ return Marionette.Layout.extend({
@ -213,15 +213,15 @@ define(
}, },
_shouldShowEpisodes: function () { _shouldShowEpisodes: function () {
var startDate = Moment().add('month', -1); var startDate = moment().add('month', -1);
var endDate = Moment().add('year', 1); var endDate = moment().add('year', 1);
return this.episodeCollection.some(function (episode) { return this.episodeCollection.some(function (episode) {
var airDate = episode.get('airDateUtc'); var airDate = episode.get('airDateUtc');
if (airDate) { if (airDate) {
var airDateMoment = Moment(airDate); var airDateMoment = moment(airDate);
if (airDateMoment.isAfter(startDate) && airDateMoment.isBefore(endDate)) { if (airDateMoment.isAfter(startDate) && airDateMoment.isBefore(endDate)) {
return true; return true;
@ -235,7 +235,7 @@ define(
templateHelpers: function () { templateHelpers: function () {
var episodeCount = this.episodeCollection.filter(function (episode) { var episodeCount = this.episodeCollection.filter(function (episode) {
return episode.get('hasFile') || (episode.get('monitored') && Moment(episode.get('airDateUtc')).isBefore(Moment())); return episode.get('hasFile') || (episode.get('monitored') && moment(episode.get('airDateUtc')).isBefore(moment()));
}).length; }).length;
var episodeFileCount = this.episodeCollection.where({ hasFile: true }).length; var episodeFileCount = this.episodeCollection.where({ hasFile: true }).length;

View File

@ -37,7 +37,7 @@
<div class="col-md-10 col-xs-8"> <div class="col-md-10 col-xs-8">
{{#if_eq status compare="continuing"}} {{#if_eq status compare="continuing"}}
{{#if nextAiring}} {{#if nextAiring}}
<span class="label label-default">{{NextAiring nextAiring}}</span> <span class="label label-default">{{RelativeDate nextAiring}}</span>
{{/if}} {{/if}}
{{else}} {{else}}
<span class="label label-danger">Ended</span> <span class="label label-danger">Ended</span>

View File

@ -22,7 +22,7 @@
<div class="labels"> <div class="labels">
{{#if_eq status compare="continuing"}} {{#if_eq status compare="continuing"}}
{{#if nextAiring}} {{#if nextAiring}}
<span class="label label-default">{{NextAiring nextAiring}}</span> <span class="label label-default">{{RelativeDate nextAiring}}</span>
{{/if}} {{/if}}
{{/if_eq}} {{/if_eq}}
{{> EpisodeProgressPartial }} {{> EpisodeProgressPartial }}

View File

@ -65,9 +65,9 @@ define(
cell : 'integer' cell : 'integer'
}, },
{ {
name : 'profileId', name : 'profileId',
label: 'Profile', label : 'Profile',
cell : ProfileCell cell : ProfileCell
}, },
{ {
name : 'network', name : 'network',

View File

@ -10,7 +10,7 @@ define(
'Mixins/AsSortedCollection', 'Mixins/AsSortedCollection',
'Mixins/AsPersistedStateCollection', 'Mixins/AsPersistedStateCollection',
'moment' 'moment'
], function (_, Backbone, PageableCollection, SeriesModel, SeriesData, AsFilteredCollection, AsSortedCollection, AsPersistedStateCollection, Moment) { ], function (_, Backbone, PageableCollection, SeriesModel, SeriesData, AsFilteredCollection, AsSortedCollection, AsPersistedStateCollection, moment) {
var Collection = PageableCollection.extend({ var Collection = PageableCollection.extend({
url : window.NzbDrone.ApiRoot + '/series', url : window.NzbDrone.ApiRoot + '/series',
model: SeriesModel, model: SeriesModel,
@ -63,13 +63,13 @@ define(
var nextAiring = model.get(attr); var nextAiring = model.get(attr);
if (nextAiring) { if (nextAiring) {
return Moment(nextAiring).unix(); return moment(nextAiring).unix();
} }
var previousAiring = model.get(attr.replace('nextAiring', 'previousAiring')); var previousAiring = model.get(attr.replace('nextAiring', 'previousAiring'));
if (previousAiring) { if (previousAiring) {
return 10000000000 - Moment(previousAiring).unix(); return 10000000000 - moment(previousAiring).unix();
} }
return Number.MAX_VALUE; return Number.MAX_VALUE;

View File

@ -21,6 +21,8 @@ define(
'Settings/Notifications/NotificationCollection', 'Settings/Notifications/NotificationCollection',
'Settings/Metadata/MetadataLayout', 'Settings/Metadata/MetadataLayout',
'Settings/General/GeneralView', 'Settings/General/GeneralView',
'Settings/UI/UiView',
'Settings/UI/UiSettingsModel',
'Shared/LoadingView', 'Shared/LoadingView',
'Config' 'Config'
], function ($, ], function ($,
@ -43,6 +45,8 @@ define(
NotificationCollection, NotificationCollection,
MetadataLayout, MetadataLayout,
GeneralView, GeneralView,
UiView,
UiSettingsModel,
LoadingView, LoadingView,
Config) { Config) {
return Marionette.Layout.extend({ return Marionette.Layout.extend({
@ -57,6 +61,7 @@ define(
notifications : '#notifications', notifications : '#notifications',
metadata : '#metadata', metadata : '#metadata',
general : '#general', general : '#general',
uiRegion : '#ui',
loading : '#loading-region' loading : '#loading-region'
}, },
@ -69,6 +74,7 @@ define(
notificationsTab : '.x-notifications-tab', notificationsTab : '.x-notifications-tab',
metadataTab : '.x-metadata-tab', metadataTab : '.x-metadata-tab',
generalTab : '.x-general-tab', generalTab : '.x-general-tab',
uiTab : '.x-ui-tab',
advancedSettings : '.x-advanced-settings' advancedSettings : '.x-advanced-settings'
}, },
@ -81,6 +87,7 @@ define(
'click .x-notifications-tab' : '_showNotifications', 'click .x-notifications-tab' : '_showNotifications',
'click .x-metadata-tab' : '_showMetadata', 'click .x-metadata-tab' : '_showMetadata',
'click .x-general-tab' : '_showGeneral', 'click .x-general-tab' : '_showGeneral',
'click .x-ui-tab' : '_showUi',
'click .x-save-settings' : '_save', 'click .x-save-settings' : '_save',
'change .x-advanced-settings' : '_toggleAdvancedSettings' 'change .x-advanced-settings' : '_toggleAdvancedSettings'
}, },
@ -103,6 +110,7 @@ define(
this.downloadClientSettings = new DownloadClientSettingsModel(); this.downloadClientSettings = new DownloadClientSettingsModel();
this.notificationCollection = new NotificationCollection(); this.notificationCollection = new NotificationCollection();
this.generalSettings = new GeneralSettingsModel(); this.generalSettings = new GeneralSettingsModel();
this.uiSettings = new UiSettingsModel();
Backbone.$.when( Backbone.$.when(
this.mediaManagementSettings.fetch(), this.mediaManagementSettings.fetch(),
@ -110,7 +118,8 @@ define(
this.indexerSettings.fetch(), this.indexerSettings.fetch(),
this.downloadClientSettings.fetch(), this.downloadClientSettings.fetch(),
this.notificationCollection.fetch(), this.notificationCollection.fetch(),
this.generalSettings.fetch() this.generalSettings.fetch(),
this.uiSettings.fetch()
).done(function () { ).done(function () {
if(!self.isClosed) if(!self.isClosed)
{ {
@ -123,6 +132,7 @@ define(
self.notifications.show(new NotificationCollectionView({ collection: self.notificationCollection })); self.notifications.show(new NotificationCollectionView({ collection: self.notificationCollection }));
self.metadata.show(new MetadataLayout()); self.metadata.show(new MetadataLayout());
self.general.show(new GeneralView({ model: self.generalSettings })); self.general.show(new GeneralView({ model: self.generalSettings }));
self.uiRegion.show(new UiView({ model: self.uiSettings }));
} }
}); });
@ -155,6 +165,9 @@ define(
case 'general': case 'general':
this._showGeneral(); this._showGeneral();
break; break;
case 'ui':
this._showUi();
break;
default: default:
this._showMediaManagement(); this._showMediaManagement();
} }
@ -232,6 +245,15 @@ define(
this._navigate('settings/general'); this._navigate('settings/general');
}, },
_showUi: function (e) {
if (e) {
e.preventDefault();
}
this.ui.uiTab.tab('show');
this._navigate('settings/ui');
},
_navigate:function(route){ _navigate:function(route){
Backbone.history.navigate(route, { trigger: false, replace: true }); Backbone.history.navigate(route, { trigger: false, replace: true });
}, },

View File

@ -7,6 +7,7 @@
<li><a href="#notifications" class="x-notifications-tab no-router">Connect</a></li> <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="#metadata" class="x-metadata-tab no-router">Metadata</a></li>
<li><a href="#general" class="x-general-tab no-router">General</a></li> <li><a href="#general" class="x-general-tab no-router">General</a></li>
<li><a href="#ui" class="x-ui-tab no-router">UI</a></li>
</ul> </ul>
<div class="row settings-controls"> <div class="row settings-controls">
@ -42,6 +43,7 @@
<div class="tab-pane" id="notifications"></div> <div class="tab-pane" id="notifications"></div>
<div class="tab-pane" id="metadata"></div> <div class="tab-pane" id="metadata"></div>
<div class="tab-pane" id="general"></div> <div class="tab-pane" id="general"></div>
<div class="tab-pane" id="ui"></div>
</div> </div>
<div id="loading-region"></div> <div id="loading-region"></div>

View File

@ -0,0 +1,12 @@
'use strict';
define(
[
'Settings/SettingsModelBase'
], function (SettingsModelBase) {
return SettingsModelBase.extend({
url : window.NzbDrone.ApiRoot + '/config/ui',
successMessage: 'UI settings saved',
errorMessage : 'Failed to save UI settings'
});
});

View File

@ -0,0 +1,27 @@
'use strict';
define(
[
'vent',
'marionette',
'Shared/UiSettingsModel',
'Mixins/AsModelBoundView',
'Mixins/AsValidatedView'
], function (vent, Marionette, UiSettingsModel, AsModelBoundView, AsValidatedView) {
var view = Marionette.ItemView.extend({
template: 'Settings/UI/UiViewTemplate',
initialize: function () {
this.listenTo(this.model, 'sync', this._reloadUiSettings);
},
_reloadUiSettings: function() {
UiSettingsModel.fetch();
}
});
AsModelBoundView.call(view);
AsValidatedView.call(view);
return view;
});

View File

@ -0,0 +1,95 @@
<div class="form-horizontal">
<fieldset>
<legend>Calendar</legend>
<div class="form-group">
<label class="col-sm-3 control-label">First Day of Week</label>
<div class="col-sm-4">
<select name="firstDayOfWeek" class="form-control">
<option value="0">Sunday</option>
<option value="1">Monday</option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Week Column Header</label>
<div class="col-sm-1 col-sm-push-4 help-inline">
<i class="icon-nd-form-warning" title="Shown above each column when week is the active view"/>
</div>
<div class="col-sm-4 col-sm-pull-1">
<select name="calendarWeekColumnHeader" class="form-control">
<option value="ddd M/D">Tue 3/25</option>
<option value="ddd MM/DD">Tue 03/25</option>
<option value="ddd D/M">Tue 25/3</option>
<option value="ddd DD/MM">Tue 25/03</option>
</select>
</div>
</div>
</fieldset>
<fieldset>
<legend>Dates</legend>
<div class="form-group">
<label class="col-sm-3 control-label">Short Date Format</label>
<div class="col-sm-4">
<select name="shortDateFormat" class="form-control">
<option value="MMM D YYYY">Mar 25 2014</option>
<option value="DD MMM YYYY">25 Mar 2014</option>
<option value="MM/D/YYYY">03/25/2014</option>
<option value="DD/MM/YYYY">25/03/2014</option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Long Date Format</label>
<div class="col-sm-4">
<select name="longDateFormat" class="form-control">
<option value="dddd, MMMM D YYYY">Tuesday, March 25, 2014</option>
<option value="dddd, D MMMM YYYY">Tuesday, 25 March, 2014</option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Time Format</label>
<div class="col-sm-4">
<select name="timeFormat" class="form-control">
<option value="h(:mm)a">5pm/5:30pm</option>
<option value="HH:mm">17:00/17:30</option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Show Relative Dates</label>
<div class="col-sm-8">
<div class="input-group">
<label class="checkbox toggle well">
<input type="checkbox" name="showRelativeDates"/>
<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="Show relative (Today/Yesterday/etc) or absolute dates"/>
</span>
</div>
</div>
</div>
</fieldset>
</div>

View File

@ -109,6 +109,10 @@ li.save-and-add:hover {
} }
.settings-tabs { .settings-tabs {
li>a {
padding : 10px;
}
@media (min-width: @screen-sm-min) and (max-width: @screen-md-max) { @media (min-width: @screen-sm-min) and (max-width: @screen-md-max) {
li { li {
a { a {

View File

@ -3,8 +3,9 @@
define( define(
[ [
'moment', 'moment',
'filesize' 'filesize',
], function (Moment, Filesize) { 'Shared/UiSettingsModel'
], function (moment, filesize, UiSettings) {
return { return {
@ -15,16 +16,15 @@ define(
return ''; return '';
} }
return Filesize(size, { base: 2, round: 1 }); return filesize(size, { base: 2, round: 1 });
}, },
dateHelper: function (sourceDate) { relativeDate: function (sourceDate) {
if (!sourceDate) { if (!sourceDate) {
return ''; return '';
} }
var date = Moment(sourceDate); var date = moment(sourceDate);
var calendarDate = date.calendar(); var calendarDate = date.calendar();
//TODO: It would be nice to not have to hack this... //TODO: It would be nice to not have to hack this...
@ -34,12 +34,12 @@ define(
return strippedCalendarDate; return strippedCalendarDate;
} }
if (date.isAfter(Moment())) { if (date.isAfter(moment())) {
return date.fromNow(true); return date.fromNow(true);
} }
if (date.isBefore(Moment().add('years', -1))) { if (date.isBefore(moment().add('years', -1))) {
return date.format('ll'); return date.format(UiSettings.get('shortDateFormat'));
} }
return date.fromNow(); return date.fromNow();

View File

@ -0,0 +1,22 @@
'use strict';
define(
[
'backbone',
'api!config/ui'
], function (Backbone, uiSettings) {
var UiSettings = Backbone.Model.extend({
url : window.NzbDrone.ApiRoot + '/config/ui',
shortDateTime : function () {
return this.get('shortDateFormat') + ' ' + this.get('timeFormat').replace('(', '').replace(')', '');
},
longDateTime : function () {
return this.get('longDateFormat') + ' ' + this.get('timeFormat').replace('(', '').replace(')', '');
}
});
var instance = new UiSettings(uiSettings);
return instance;
});

View File

@ -2,16 +2,17 @@
define( define(
[ [
'Cells/NzbDroneCell', 'Cells/NzbDroneCell',
'moment' 'moment',
], function (NzbDroneCell, Moment) { 'Shared/UiSettingsModel'
], function (NzbDroneCell, moment, UiSettings) {
return NzbDroneCell.extend({ return NzbDroneCell.extend({
className: 'log-time-cell', className: 'log-time-cell',
render: function () { render: function () {
var date = Moment(this._getValue()); var date = moment(this._getValue());
this.$el.html('<span title="{1}">{0}</span>'.format(date.format('LT'), date.format('LLLL'))); this.$el.html('<span title="{1}">{0}</span>'.format(date.format(UiSettings.get('timeFormat')), date.format(UiSettings.longDateFormat())));
return this; return this;
} }

View File

@ -199,7 +199,6 @@ require.config({
headerCell: 'NzbDrone', headerCell: 'NzbDrone',
sortType : 'toggle' sortType : 'toggle'
}; };
}); });
} }
}, },
@ -247,7 +246,19 @@ define(
'Instrumentation/StringFormat', 'Instrumentation/StringFormat',
'LifeCycle', 'LifeCycle',
'Hotkeys/Hotkeys' 'Hotkeys/Hotkeys'
], function ($, Backbone, Marionette, RouteBinder, SignalRBroadcaster, NavbarView, AppLayout, SeriesController, Router, ModalController, ControlPanelController, serverStatusModel, Tooltip) { ], function ($,
Backbone,
Marionette,
RouteBinder,
SignalRBroadcaster,
NavbarView,
AppLayout,
SeriesController,
Router,
ModalController,
ControlPanelController,
serverStatusModel,
Tooltip) {
new SeriesController(); new SeriesController();
new ModalController(); new ModalController();