diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py
index cf9150f4..8bf26359 100644
--- a/couchpotato/core/_base/updater/main.py
+++ b/couchpotato/core/_base/updater/main.py
@@ -5,6 +5,7 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from git.repository import LocalRepository
+from datetime import datetime
import os
import time
import traceback
@@ -48,12 +49,15 @@ class Updater(Plugin):
def getInfo(self):
- return jsonified({
+ return jsonified(self.info())
+
+ def info(self):
+ return {
'repo_name': self.repo_name,
'last_check': self.last_check,
'update_version': self.update_version,
'version': self.getVersion()
- })
+ }
def getVersion(self):
@@ -91,14 +95,14 @@ class Updater(Plugin):
log.info('Versions, local:%s, remote:%s' % (local.hash[:8], remote.hash[:8]))
if local.getDate() < remote.getDate():
+ self.update_version = {
+ 'hash': remote.hash[:8],
+ 'date': remote.getDate(),
+ }
if self.conf('automatic') and not self.update_failed:
if self.doUpdate():
fireEventAsync('app.crappy_restart')
else:
- self.update_version = {
- 'hash': remote.hash[:8],
- 'date': remote.getDate(),
- }
if self.conf('notification'):
fireEvent('updater.available', message = 'A new update is available', data = self.getVersion())
@@ -119,11 +123,16 @@ class Updater(Plugin):
self.repo.saveStash()
log.info('Updating to latest version')
+ info = self.info()
self.repo.pull()
# Delete leftover .pyc files
self.deletePyc()
+ # Notify before returning and restarting
+ version_date = datetime.fromtimestamp(info['update_version']['date'])
+ fireEvent('updater.updated', 'Updated to a new version with hash "%s", this version is from %s' % (info['update_version']['hash'], version_date), data = info)
+
return True
except:
log.error('Failed updating via GIT: %s' % traceback.format_exc())
diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py
index a5a478b2..df53347d 100644
--- a/couchpotato/core/notifications/core/main.py
+++ b/couchpotato/core/notifications/core/main.py
@@ -1,8 +1,13 @@
+from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent
-from couchpotato.core.helpers.request import jsonified
+from couchpotato.core.helpers.encoding import toUnicode
+from couchpotato.core.helpers.request import jsonified, getParam
+from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
+from couchpotato.core.settings.model import Notification as Notif
+from sqlalchemy.sql.expression import or_
import time
log = CPLog(__name__)
@@ -11,28 +16,65 @@ log = CPLog(__name__)
class CoreNotifier(Notification):
messages = []
+ listen_to = [
+ 'movie.downloaded', 'movie.snatched',
+ 'updater.available', 'updater.updated',
+ ]
def __init__(self):
+ super(CoreNotifier, self).__init__()
addEvent('notify', self.notify)
addEvent('notify.frontend', self.frontend)
- addApiView('core_notifier.listener', self.listener)
+ addApiView('notification.markread', self.markAsRead, docs = {
+ 'desc': 'Mark notifications as read',
+ 'params': {
+ 'id': {'desc': 'Notification id you want to mark as read.', 'type': 'int (comma separated)'},
+ },
+ })
+ addApiView('notification.listener', self.listener)
self.registerEvents()
-
def registerEvents(self):
# Library update, frontend refresh
addEvent('library.update_finish', lambda data: fireEvent('notify.frontend', type = 'library.update', data = data))
- def notify(self, message = '', data = {}):
- self.add(data = {
- 'message': message,
- 'raw': data,
+ def markAsRead(self):
+ ids = getParam('ids').split(',')
+
+ db = get_session()
+
+ q = db.query(Notif) \
+ .filter(or_(*[Notif.id == tryInt(s) for s in ids]))
+ q.update({Notif.read: True})
+
+ db.commit()
+
+ return jsonified({
+ 'success': True
})
+ def notify(self, message = '', data = {}):
+
+ db = get_session()
+
+ n = Notif(
+ message = toUnicode(message),
+ data = data
+ )
+ db.add(n)
+ db.commit()
+
+ ndict = n.to_dict()
+ ndict['type'] = 'notification'
+ ndict['time'] = time.time()
+ self.messages.append(ndict)
+
+ db.remove()
+
def frontend(self, type = 'notification', data = {}):
self.messages.append({
'time': time.time(),
@@ -48,6 +90,15 @@ class CoreNotifier(Notification):
if message['time'] > (time.time() - 15):
messages.append(message)
+ # Get unread
+ if getParam('init'):
+ db = get_session()
+ notifications = db.query(Notif).filter_by(read = False).all()
+ for n in notifications:
+ ndict = n.to_dict()
+ ndict['type'] = 'notification'
+ messages.append(ndict)
+
self.messages = []
return jsonified({
'success': True,
diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js
index e96fdba2..617ef2ac 100644
--- a/couchpotato/core/notifications/core/static/notification.js
+++ b/couchpotato/core/notifications/core/static/notification.js
@@ -10,19 +10,90 @@ var NotificationBase = new Class({
// Listener
App.addEvent('load', self.startInterval.bind(self));
App.addEvent('unload', self.stopTimer.bind(self));
- self.addEvent('notification', self.notify.bind(self))
+ App.addEvent('notification', self.notify.bind(self));
// Add test buttons to settings page
App.addEvent('load', self.addTestButtons.bind(self));
+ // Notification bar
+ self.notifications = []
+ App.addEvent('load', function(){
+
+ App.block.notification = new Block.Menu(self, {
+ 'class': 'notification_menu',
+ 'onOpen': self.markAsRead.bind(self)
+ })
+ $(App.block.notification).inject(App.getBlock('search'), 'after');
+ self.badge = new Element('div.badge').inject(App.block.notification, 'top').hide();
+
+ App.getBlock('notification').addLink(new Element('a.more', {
+ 'href': App.createUrl('notifications'),
+ 'text': 'See more notifications'
+ }));
+ })
+
+ },
+
+ notify: function(result){
+ var self = this;
+
+ var added = new Date();
+ added.setTime(result.added*1000)
+
+ result.el = App.getBlock('notification').addLink(
+ new Element('span').adopt(
+ new Element('span.message', {'text': result.message}),
+ new Element('span.added', {'text': added.timeDiffInWords(), 'title': added})
+ )
+ , 'top');
+ self.notifications.include(result);
+
+ if(!result.read)
+ self.setBadge(self.notifications.filter(function(n){ return !n.read}).length)
+
+ if(self.notifications.length >= 5){
+ var n = self.notifications[self.notifications.length-5];
+ n.el.destroy();
+ }
+
+ },
+
+ setBadge: function(value){
+ var self = this;
+ self.badge.set('text', value)
+ self.badge[value ? 'show' : 'hide']()
+ },
+
+ markAsRead: function(){
+ var self = this;
+
+ var rn = self.notifications.filter(function(n){
+ return !n.read
+ })
+
+ var ids = []
+ rn.each(function(n){
+ ids.include(n.id)
+ })
+
+ Api.request('notification.markread', {
+ 'data': {
+ 'ids': ids.join(',')
+ },
+ 'onSuccess': function(){
+ self.setBadge('')
+ }
+ })
+
},
startInterval: function(){
var self = this;
- self.request = Api.request('core_notifier.listener', {
+ self.request = Api.request('notification.listener', {
'initialDelay': 100,
'delay': 3000,
+ 'data': {'init':true},
'onSuccess': self.processData.bind(self)
})
@@ -40,16 +111,12 @@ var NotificationBase = new Class({
this.request.stopTimer()
},
- notify: function(data){
- var self = this;
-
- },
-
processData: function(json){
var self = this;
+ self.request.options.data = {}
Array.each(json.result, function(result){
- App.fireEvent(result.type, result.data)
+ App.fireEvent(result.type, result)
})
},
diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js
index e13fe2ee..9ddffd97 100644
--- a/couchpotato/core/plugins/movie/static/list.js
+++ b/couchpotato/core/plugins/movie/static/list.js
@@ -116,25 +116,7 @@ var MovieList = new Class({
'change': self.search.bind(self)
}
}),
- self.navigation_menu = new Element('div.more_menu').adopt(
- self.navigation_menu_ul = new Element('ul'),
- self.navigation_menu_toggle = new Element('a.button.onlay', {
- 'events': {
- 'click': function(){
- self.navigation_menu.toggleClass('show')
-
- if(self.navigation_menu.hasClass('show'))
- this.addEvent('outerClick', function(){
- self.navigation_menu.removeClass('show')
- this.removeEvents('outerClick');
- })
- else
- this.removeEvents('outerClick');
-
- }
- }
- })
- ),
+ self.navigation_menu = new Block.Menu(self),
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
new Element('span.select').adopt(
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
@@ -222,7 +204,7 @@ var MovieList = new Class({
// Add menu or hide
if (self.options.menu.length > 0)
self.options.menu.each(function(menu_item){
- self.navigation_menu_ul.adopt(new Element('li').adopt(menu_item));
+ self.navigation_menu.addLink(menu_item);
})
else
self.navigation_menu.hide()
@@ -268,7 +250,7 @@ var MovieList = new Class({
'class': 'delete',
'events': {
'click': function(e){
- (e).stop();
+ (e).preventDefault();
Api.request('movie.delete', {
'data': {
'id': ids.join(',')
diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css
index cdef5f22..36ff17c3 100644
--- a/couchpotato/core/plugins/movie/static/movie.css
+++ b/couchpotato/core/plugins/movie/static/movie.css
@@ -1,9 +1,3 @@
-/* @override
- http://localhost:5000/static/movie_plugin/movie.css
- http://192.168.1.20:5000/static/movie_plugin/movie.css
- http://127.0.0.1:5000/static/movie_plugin/movie.css
-*/
-
.movies {
padding: 60px 0 20px;
}
@@ -454,7 +448,7 @@
}
.movies .alph_nav .more_menu {
- float: right;
+ margin-left: 48px;
}
.movies .alph_nav .more_menu > a {
diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js
index cd81b652..53cd9e31 100644
--- a/couchpotato/core/plugins/movie/static/movie.js
+++ b/couchpotato/core/plugins/movie/static/movie.js
@@ -230,7 +230,7 @@ var IMDBAction = new Class({
gotoIMDB: function(e){
var self = this;
- (e).stop();
+ (e).preventDefault();
window.open('http://www.imdb.com/title/'+self.id+'/');
}
@@ -258,7 +258,7 @@ var ReleaseAction = new Class({
show: function(e){
var self = this;
- (e).stop();
+ (e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
@@ -292,7 +292,7 @@ var ReleaseAction = new Class({
new Element('a.download.icon', {
'events': {
'click': function(e){
- (e).stop();
+ (e).preventDefault();
self.download(release);
}
}
@@ -300,7 +300,7 @@ var ReleaseAction = new Class({
new Element('a.delete.icon', {
'events': {
'click': function(e){
- (e).stop();
+ (e).preventDefault();
self.ignore(release);
this.getParent('.item').toggleClass('ignored')
}
diff --git a/couchpotato/core/plugins/movie/static/search.css b/couchpotato/core/plugins/movie/static/search.css
index b3ef1e50..2be6ce1e 100644
--- a/couchpotato/core/plugins/movie/static/search.css
+++ b/couchpotato/core/plugins/movie/static/search.css
@@ -1,11 +1,6 @@
-/* @override
- http://localhost:5000/static/movie_plugin/search.css
- http://192.168.1.20:5000/static/movie_plugin/search.css
- http://127.0.0.1:5000/static/movie_plugin/search.css
-*/
-
.search_form {
display: inline-block;
+ vertical-align: middle;
width: 25%;
}
diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js
index 63d55dd3..a94b3438 100644
--- a/couchpotato/core/plugins/movie/static/search.js
+++ b/couchpotato/core/plugins/movie/static/search.js
@@ -53,7 +53,7 @@ Block.Search = new Class({
clear: function(e){
var self = this;
- (e).stop();
+ (e).preventDefault();
self.input.set('value', '');
self.input.focus()
@@ -283,7 +283,7 @@ Block.Search.Item = new Class({
add: function(e){
var self = this;
- (e).stop();
+ (e).preventDefault();
Api.request('movie.add', {
'data': {
diff --git a/couchpotato/core/plugins/profile/static/profile.css b/couchpotato/core/plugins/profile/static/profile.css
index c1f57196..b763cd80 100644
--- a/couchpotato/core/plugins/profile/static/profile.css
+++ b/couchpotato/core/plugins/profile/static/profile.css
@@ -1,5 +1,3 @@
-/* @override http://192.168.1.20:5000/static/profile_plugin/profile.css */
-
.add_new_profile {
padding: 20px;
display: block;
diff --git a/couchpotato/core/plugins/profile/static/profile.js b/couchpotato/core/plugins/profile/static/profile.js
index 7a6571e1..470fda8f 100644
--- a/couchpotato/core/plugins/profile/static/profile.js
+++ b/couchpotato/core/plugins/profile/static/profile.js
@@ -156,7 +156,7 @@ var Profile = new Class({
'class': 'delete',
'events': {
'click': function(e){
- (e).stop();
+ (e).preventDefault();
Api.request('profile.delete', {
'data': {
'id': self.data.id
diff --git a/couchpotato/core/plugins/quality/static/quality.css b/couchpotato/core/plugins/quality/static/quality.css
index e2081738..a66fefc4 100644
--- a/couchpotato/core/plugins/quality/static/quality.css
+++ b/couchpotato/core/plugins/quality/static/quality.css
@@ -1,5 +1,3 @@
-/* @override http://127.0.0.1:5000/static/quality_plugin/quality.css */
-
.group_sizes {
}
diff --git a/couchpotato/core/plugins/wizard/static/wizard.css b/couchpotato/core/plugins/wizard/static/wizard.css
index 8197b58a..3b19cf19 100644
--- a/couchpotato/core/plugins/wizard/static/wizard.css
+++ b/couchpotato/core/plugins/wizard/static/wizard.css
@@ -1,5 +1,3 @@
-/* @override http://127.0.0.1:5000/static/wizard/wizard.css */
-
.page.wizard h1 {
padding: 10px 30px;
margin: 0;
diff --git a/couchpotato/core/plugins/wizard/static/wizard.js b/couchpotato/core/plugins/wizard/static/wizard.js
index c4edd0d5..e1e07a87 100644
--- a/couchpotato/core/plugins/wizard/static/wizard.js
+++ b/couchpotato/core/plugins/wizard/static/wizard.js
@@ -35,7 +35,7 @@ Page.Wizard = new Class({
'text': 'I\'m ready to start the awesomeness, wow this button is big and green!',
'events': {
'click': function(e){
- (e).stop();
+ (e).preventDefault();
Api.request('settings.save', {
'data': {
'section': 'core',
diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py
index 886b3584..92d3ecdb 100644
--- a/couchpotato/core/settings/model.py
+++ b/couchpotato/core/settings/model.py
@@ -7,6 +7,7 @@ from libs.elixir.relationships import OneToOne
from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, Float, \
String, TypeDecorator
import json
+import time
options_defaults["shortnames"] = True
@@ -33,7 +34,7 @@ class Movie(Entity):
The files belonging to the movie object are global for the whole movie
such as trailers, nfo, thumbnails"""
- last_edit = Field(Integer)
+ last_edit = Field(Integer, default = lambda: int(time.time()))
library = ManyToOne('Library')
status = ManyToOne('Status')
@@ -191,8 +192,8 @@ class History(Entity):
"""History of actions that are connected to a certain release,
such as, renamed to, downloaded, deleted, download subtitles etc"""
- added = Field(Integer)
- message = Field(UnicodeText())
+ added = Field(Integer, default = lambda: int(time.time()))
+ message = Field(UnicodeText)
type = Field(Unicode(50))
release = ManyToOne('Release')
@@ -207,6 +208,15 @@ class RenameHistory(Entity):
file = ManyToOne('File')
+class Notification(Entity):
+ using_options(order_by = 'added')
+
+ added = Field(Integer, default = lambda: int(time.time()))
+ read = Field(Boolean, default = False)
+ message = Field(Unicode(255))
+ data = Field(JsonType)
+
+
class Folder(Entity):
"""Renamer destination folders."""
diff --git a/couchpotato/static/images/sprite.png b/couchpotato/static/images/sprite.png
index 8a61b25e..6af04a7e 100644
Binary files a/couchpotato/static/images/sprite.png and b/couchpotato/static/images/sprite.png differ
diff --git a/couchpotato/static/scripts/block/menu.js b/couchpotato/static/scripts/block/menu.js
new file mode 100644
index 00000000..f8ca6f11
--- /dev/null
+++ b/couchpotato/static/scripts/block/menu.js
@@ -0,0 +1,43 @@
+Block.Menu = new Class({
+
+ Extends: BlockBase,
+
+ options: {
+ 'class': 'menu'
+ },
+
+ create: function(){
+ var self = this;
+
+ self.el = new Element('div', {
+ 'class': 'more_menu '+self.options['class']
+ }).adopt(
+ self.more_option_ul = new Element('ul'),
+ new Element('a.button.onlay', {
+ 'events': {
+ 'click': function(){
+ self.el.toggleClass('show')
+ self.fireEvent(self.el.hasClass('show') ? 'open' : 'close')
+
+ if(self.el.hasClass('show'))
+ this.addEvent('outerClick', function(){
+ self.el.removeClass('show')
+ this.removeEvents('outerClick');
+ })
+ else
+ this.removeEvents('outerClick');
+
+ }
+ }
+ })
+ )
+
+ },
+
+ addLink: function(tab, position){
+ var self = this;
+ var el = new Element('li').adopt(tab).inject(self.more_option_ul, position || 'bottom');
+ return el;
+ }
+
+});
\ No newline at end of file
diff --git a/couchpotato/static/scripts/block/more.js b/couchpotato/static/scripts/block/more.js
deleted file mode 100644
index c9cd11b1..00000000
--- a/couchpotato/static/scripts/block/more.js
+++ /dev/null
@@ -1,54 +0,0 @@
-Block.More = new Class({
-
- Extends: BlockBase,
-
- create: function(){
- var self = this;
-
- self.el = new Element('div.more_menu').adopt(
- self.more_option_ul = new Element('ul').adopt(
- new Element('li').adopt(
- new Element('a.orange', {
- 'text': 'Restart',
- 'events': {
- 'click': App.restart.bind(App)
- }
- })
- ),
- new Element('li').adopt(
- new Element('a.red', {
- 'text': 'Shutdown',
- 'events': {
- 'click': App.shutdown.bind(App)
- }
- })
- )
- ),
- new Element('a.button.onlay', {
- 'events': {
- 'click': function(){
- self.el.toggleClass('show')
-
- if(self.el.hasClass('show'))
- this.addEvent('outerClick', function(){
- self.el.removeClass('show')
- this.removeEvents('outerClick');
- })
- else
- this.removeEvents('outerClick');
-
- }
- }
- })
- )
-
- },
-
- addLink: function(tab, position){
- var self = this
-
- return new Element('li').adopt(tab).inject(self.more_option_ul, position || 'bottom')
-
- }
-
-});
\ No newline at end of file
diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js
index c0f6a6a0..e4263884 100644
--- a/couchpotato/static/scripts/couchpotato.js
+++ b/couchpotato/static/scripts/couchpotato.js
@@ -43,7 +43,7 @@ var CouchPotato = new Class({
pushState: function(e){
var self = this;
if((!e.meta && Browser.Platform.mac) || (!e.control && !Browser.Platform.mac)){
- (e).stop();
+ (e).preventDefault();
var url = e.target.get('href');
if(History.getPath() != url)
History.push(url);
@@ -60,19 +60,34 @@ var CouchPotato = new Class({
new Element('div').adopt(
self.block.navigation = new Block.Navigation(self, {}),
self.block.search = new Block.Search(self, {}),
- self.block.more = new Block.More(self, {})
+ self.block.more = new Block.Menu(self, {})
)
),
self.content = new Element('div.content'),
self.block.footer = new Block.Footer(self, {})
);
- self.block.more.addLink(new Element('a', {
+ [new Element('a.orange', {
+ 'text': 'Restart',
+ 'events': {
+ 'click': App.restart.bind(App)
+ }
+ }),
+ new Element('a.red', {
+ 'text': 'Shutdown',
+ 'events': {
+ 'click': App.shutdown.bind(App)
+ }
+ }),
+ new Element('a', {
'text': 'Check for updates',
'events': {
'click': self.checkForUpdate.bind(self)
}
- }))
+ })].each(function(a){
+ self.block.more.addLink(a)
+ })
+
new ScrollSpy({
min: 10,
@@ -207,6 +222,13 @@ var CouchPotato = new Class({
createUrl: function(action, params){
return this.options.base_url + (action ? action+'/' : '') + (params ? '?'+Object.toQueryString(params) : '')
+ },
+
+ notify: function(options){
+ return this.growl.notify({
+ title: "this scrolls away",
+ text: "test - hello there. mouseover to pause away action"
+ });
}
});
diff --git a/couchpotato/static/scripts/library/mootools_more.js b/couchpotato/static/scripts/library/mootools_more.js
index 4ad1700f..d2d70369 100644
--- a/couchpotato/static/scripts/library/mootools_more.js
+++ b/couchpotato/static/scripts/library/mootools_more.js
@@ -1,6 +1,6 @@
// MooTools: the javascript framework.
-// Load this file's selection again by visiting: http://mootools.net/more/d7fedf16aa88d2a757ac66071dd56b3a
-// Or build this file again with packager using: packager build More/Events.Pseudos More/Element.Forms More/Element.Position More/Element.Shortcuts More/Fx.Scroll More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical
+// Load this file's selection again by visiting: http://mootools.net/more/43db227db7a621ebb062ee621432ae3d
+// Or build this file again with packager using: packager build More/Events.Pseudos More/Date More/Date.Extras More/Element.Forms More/Element.Position More/Element.Shortcuts More/Fx.Scroll More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical
/*
---
@@ -195,6 +195,993 @@ Events.implement(Events.Pseudos(pseudos, proto.addEvent, proto.removeEvent));
})();
+/*
+---
+
+script: Object.Extras.js
+
+name: Object.Extras
+
+description: Extra Object generics, like getFromPath which allows a path notation to child elements.
+
+license: MIT-style license
+
+authors:
+ - Aaron Newton
+
+requires:
+ - Core/Object
+ - /MooTools.More
+
+provides: [Object.Extras]
+
+...
+*/
+
+(function(){
+
+var defined = function(value){
+ return value != null;
+};
+
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+Object.extend({
+
+ getFromPath: function(source, parts){
+ if (typeof parts == 'string') parts = parts.split('.');
+ for (var i = 0, l = parts.length; i < l; i++){
+ if (hasOwnProperty.call(source, parts[i])) source = source[parts[i]];
+ else return null;
+ }
+ return source;
+ },
+
+ cleanValues: function(object, method){
+ method = method || defined;
+ for (var key in object) if (!method(object[key])){
+ delete object[key];
+ }
+ return object;
+ },
+
+ erase: function(object, key){
+ if (hasOwnProperty.call(object, key)) delete object[key];
+ return object;
+ },
+
+ run: function(object){
+ var args = Array.slice(arguments, 1);
+ for (var key in object) if (object[key].apply){
+ object[key].apply(object, args);
+ }
+ return object;
+ }
+
+});
+
+})();
+
+
+/*
+---
+
+script: Locale.js
+
+name: Locale
+
+description: Provides methods for localization.
+
+license: MIT-style license
+
+authors:
+ - Aaron Newton
+ - Arian Stolwijk
+
+requires:
+ - Core/Events
+ - /Object.Extras
+ - /MooTools.More
+
+provides: [Locale, Lang]
+
+...
+*/
+
+(function(){
+
+var current = null,
+ locales = {},
+ inherits = {};
+
+var getSet = function(set){
+ if (instanceOf(set, Locale.Set)) return set;
+ else return locales[set];
+};
+
+var Locale = this.Locale = {
+
+ define: function(locale, set, key, value){
+ var name;
+ if (instanceOf(locale, Locale.Set)){
+ name = locale.name;
+ if (name) locales[name] = locale;
+ } else {
+ name = locale;
+ if (!locales[name]) locales[name] = new Locale.Set(name);
+ locale = locales[name];
+ }
+
+ if (set) locale.define(set, key, value);
+
+
+
+ if (!current) current = locale;
+
+ return locale;
+ },
+
+ use: function(locale){
+ locale = getSet(locale);
+
+ if (locale){
+ current = locale;
+
+ this.fireEvent('change', locale);
+
+
+ }
+
+ return this;
+ },
+
+ getCurrent: function(){
+ return current;
+ },
+
+ get: function(key, args){
+ return (current) ? current.get(key, args) : '';
+ },
+
+ inherit: function(locale, inherits, set){
+ locale = getSet(locale);
+
+ if (locale) locale.inherit(inherits, set);
+ return this;
+ },
+
+ list: function(){
+ return Object.keys(locales);
+ }
+
+};
+
+Object.append(Locale, new Events);
+
+Locale.Set = new Class({
+
+ sets: {},
+
+ inherits: {
+ locales: [],
+ sets: {}
+ },
+
+ initialize: function(name){
+ this.name = name || '';
+ },
+
+ define: function(set, key, value){
+ var defineData = this.sets[set];
+ if (!defineData) defineData = {};
+
+ if (key){
+ if (typeOf(key) == 'object') defineData = Object.merge(defineData, key);
+ else defineData[key] = value;
+ }
+ this.sets[set] = defineData;
+
+ return this;
+ },
+
+ get: function(key, args, _base){
+ var value = Object.getFromPath(this.sets, key);
+ if (value != null){
+ var type = typeOf(value);
+ if (type == 'function') value = value.apply(null, Array.from(args));
+ else if (type == 'object') value = Object.clone(value);
+ return value;
+ }
+
+ // get value of inherited locales
+ var index = key.indexOf('.'),
+ set = index < 0 ? key : key.substr(0, index),
+ names = (this.inherits.sets[set] || []).combine(this.inherits.locales).include('en-US');
+ if (!_base) _base = [];
+
+ for (var i = 0, l = names.length; i < l; i++){
+ if (_base.contains(names[i])) continue;
+ _base.include(names[i]);
+
+ var locale = locales[names[i]];
+ if (!locale) continue;
+
+ value = locale.get(key, args, _base);
+ if (value != null) return value;
+ }
+
+ return '';
+ },
+
+ inherit: function(names, set){
+ names = Array.from(names);
+
+ if (set && !this.inherits.sets[set]) this.inherits.sets[set] = [];
+
+ var l = names.length;
+ while (l--) (set ? this.inherits.sets[set] : this.inherits.locales).unshift(names[l]);
+
+ return this;
+ }
+
+});
+
+
+
+})();
+
+
+/*
+---
+
+name: Locale.en-US.Date
+
+description: Date messages for US English.
+
+license: MIT-style license
+
+authors:
+ - Aaron Newton
+
+requires:
+ - /Locale
+
+provides: [Locale.en-US.Date]
+
+...
+*/
+
+Locale.define('en-US', 'Date', {
+
+ months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+ months_abbr: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+ days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+ days_abbr: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+
+ // Culture's date order: MM/DD/YYYY
+ dateOrder: ['month', 'date', 'year'],
+ shortDate: '%m/%d/%Y',
+ shortTime: '%I:%M%p',
+ AM: 'AM',
+ PM: 'PM',
+ firstDayOfWeek: 0,
+
+ // Date.Extras
+ ordinal: function(dayOfMonth){
+ // 1st, 2nd, 3rd, etc.
+ return (dayOfMonth > 3 && dayOfMonth < 21) ? 'th' : ['th', 'st', 'nd', 'rd', 'th'][Math.min(dayOfMonth % 10, 4)];
+ },
+
+ lessThanMinuteAgo: 'less than a minute ago',
+ minuteAgo: 'about a minute ago',
+ minutesAgo: '{delta} minutes ago',
+ hourAgo: 'about an hour ago',
+ hoursAgo: 'about {delta} hours ago',
+ dayAgo: '1 day ago',
+ daysAgo: '{delta} days ago',
+ weekAgo: '1 week ago',
+ weeksAgo: '{delta} weeks ago',
+ monthAgo: '1 month ago',
+ monthsAgo: '{delta} months ago',
+ yearAgo: '1 year ago',
+ yearsAgo: '{delta} years ago',
+
+ lessThanMinuteUntil: 'less than a minute from now',
+ minuteUntil: 'about a minute from now',
+ minutesUntil: '{delta} minutes from now',
+ hourUntil: 'about an hour from now',
+ hoursUntil: 'about {delta} hours from now',
+ dayUntil: '1 day from now',
+ daysUntil: '{delta} days from now',
+ weekUntil: '1 week from now',
+ weeksUntil: '{delta} weeks from now',
+ monthUntil: '1 month from now',
+ monthsUntil: '{delta} months from now',
+ yearUntil: '1 year from now',
+ yearsUntil: '{delta} years from now'
+
+});
+
+
+/*
+---
+
+script: Date.js
+
+name: Date
+
+description: Extends the Date native object to include methods useful in managing dates.
+
+license: MIT-style license
+
+authors:
+ - Aaron Newton
+ - Nicholas Barthelemy - https://svn.nbarthelemy.com/date-js/
+ - Harald Kirshner - mail [at] digitarald.de; http://digitarald.de
+ - Scott Kyle - scott [at] appden.com; http://appden.com
+
+requires:
+ - Core/Array
+ - Core/String
+ - Core/Number
+ - MooTools.More
+ - Locale
+ - Locale.en-US.Date
+
+provides: [Date]
+
+...
+*/
+
+(function(){
+
+var Date = this.Date;
+
+var DateMethods = Date.Methods = {
+ ms: 'Milliseconds',
+ year: 'FullYear',
+ min: 'Minutes',
+ mo: 'Month',
+ sec: 'Seconds',
+ hr: 'Hours'
+};
+
+['Date', 'Day', 'FullYear', 'Hours', 'Milliseconds', 'Minutes', 'Month', 'Seconds', 'Time', 'TimezoneOffset',
+ 'Week', 'Timezone', 'GMTOffset', 'DayOfYear', 'LastMonth', 'LastDayOfMonth', 'UTCDate', 'UTCDay', 'UTCFullYear',
+ 'AMPM', 'Ordinal', 'UTCHours', 'UTCMilliseconds', 'UTCMinutes', 'UTCMonth', 'UTCSeconds', 'UTCMilliseconds'].each(function(method){
+ Date.Methods[method.toLowerCase()] = method;
+});
+
+var pad = function(n, digits, string){
+ if (digits == 1) return n;
+ return n < Math.pow(10, digits - 1) ? (string || '0') + pad(n, digits - 1, string) : n;
+};
+
+Date.implement({
+
+ set: function(prop, value){
+ prop = prop.toLowerCase();
+ var method = DateMethods[prop] && 'set' + DateMethods[prop];
+ if (method && this[method]) this[method](value);
+ return this;
+ }.overloadSetter(),
+
+ get: function(prop){
+ prop = prop.toLowerCase();
+ var method = DateMethods[prop] && 'get' + DateMethods[prop];
+ if (method && this[method]) return this[method]();
+ return null;
+ }.overloadGetter(),
+
+ clone: function(){
+ return new Date(this.get('time'));
+ },
+
+ increment: function(interval, times){
+ interval = interval || 'day';
+ times = times != null ? times : 1;
+
+ switch (interval){
+ case 'year':
+ return this.increment('month', times * 12);
+ case 'month':
+ var d = this.get('date');
+ this.set('date', 1).set('mo', this.get('mo') + times);
+ return this.set('date', d.min(this.get('lastdayofmonth')));
+ case 'week':
+ return this.increment('day', times * 7);
+ case 'day':
+ return this.set('date', this.get('date') + times);
+ }
+
+ if (!Date.units[interval]) throw new Error(interval + ' is not a supported interval');
+
+ return this.set('time', this.get('time') + times * Date.units[interval]());
+ },
+
+ decrement: function(interval, times){
+ return this.increment(interval, -1 * (times != null ? times : 1));
+ },
+
+ isLeapYear: function(){
+ return Date.isLeapYear(this.get('year'));
+ },
+
+ clearTime: function(){
+ return this.set({hr: 0, min: 0, sec: 0, ms: 0});
+ },
+
+ diff: function(date, resolution){
+ if (typeOf(date) == 'string') date = Date.parse(date);
+
+ return ((date - this) / Date.units[resolution || 'day'](3, 3)).round(); // non-leap year, 30-day month
+ },
+
+ getLastDayOfMonth: function(){
+ return Date.daysInMonth(this.get('mo'), this.get('year'));
+ },
+
+ getDayOfYear: function(){
+ return (Date.UTC(this.get('year'), this.get('mo'), this.get('date') + 1)
+ - Date.UTC(this.get('year'), 0, 1)) / Date.units.day();
+ },
+
+ setDay: function(day, firstDayOfWeek){
+ if (firstDayOfWeek == null){
+ firstDayOfWeek = Date.getMsg('firstDayOfWeek');
+ if (firstDayOfWeek === '') firstDayOfWeek = 1;
+ }
+
+ day = (7 + Date.parseDay(day, true) - firstDayOfWeek) % 7;
+ var currentDay = (7 + this.get('day') - firstDayOfWeek) % 7;
+
+ return this.increment('day', day - currentDay);
+ },
+
+ getWeek: function(firstDayOfWeek){
+ if (firstDayOfWeek == null){
+ firstDayOfWeek = Date.getMsg('firstDayOfWeek');
+ if (firstDayOfWeek === '') firstDayOfWeek = 1;
+ }
+
+ var date = this,
+ dayOfWeek = (7 + date.get('day') - firstDayOfWeek) % 7,
+ dividend = 0,
+ firstDayOfYear;
+
+ if (firstDayOfWeek == 1){
+ // ISO-8601, week belongs to year that has the most days of the week (i.e. has the thursday of the week)
+ var month = date.get('month'),
+ startOfWeek = date.get('date') - dayOfWeek;
+
+ if (month == 11 && startOfWeek > 28) return 1; // Week 1 of next year
+
+ if (month == 0 && startOfWeek < -2){
+ // Use a date from last year to determine the week
+ date = new Date(date).decrement('day', dayOfWeek);
+ dayOfWeek = 0;
+ }
+
+ firstDayOfYear = new Date(date.get('year'), 0, 1).get('day') || 7;
+ if (firstDayOfYear > 4) dividend = -7; // First week of the year is not week 1
+ } else {
+ // In other cultures the first week of the year is always week 1 and the last week always 53 or 54.
+ // Days in the same week can have a different weeknumber if the week spreads across two years.
+ firstDayOfYear = new Date(date.get('year'), 0, 1).get('day');
+ }
+
+ dividend += date.get('dayofyear');
+ dividend += 6 - dayOfWeek; // Add days so we calculate the current date's week as a full week
+ dividend += (7 + firstDayOfYear - firstDayOfWeek) % 7; // Make up for first week of the year not being a full week
+
+ return (dividend / 7);
+ },
+
+ getOrdinal: function(day){
+ return Date.getMsg('ordinal', day || this.get('date'));
+ },
+
+ getTimezone: function(){
+ return this.toString()
+ .replace(/^.*? ([A-Z]{3}).[0-9]{4}.*$/, '$1')
+ .replace(/^.*?\(([A-Z])[a-z]+ ([A-Z])[a-z]+ ([A-Z])[a-z]+\)$/, '$1$2$3');
+ },
+
+ getGMTOffset: function(){
+ var off = this.get('timezoneOffset');
+ return ((off > 0) ? '-' : '+') + pad((off.abs() / 60).floor(), 2) + pad(off % 60, 2);
+ },
+
+ setAMPM: function(ampm){
+ ampm = ampm.toUpperCase();
+ var hr = this.get('hr');
+ if (hr > 11 && ampm == 'AM') return this.decrement('hour', 12);
+ else if (hr < 12 && ampm == 'PM') return this.increment('hour', 12);
+ return this;
+ },
+
+ getAMPM: function(){
+ return (this.get('hr') < 12) ? 'AM' : 'PM';
+ },
+
+ parse: function(str){
+ this.set('time', Date.parse(str));
+ return this;
+ },
+
+ isValid: function(date){
+ if (!date) date = this;
+ return typeOf(date) == 'date' && !isNaN(date.valueOf());
+ },
+
+ format: function(format){
+ if (!this.isValid()) return 'invalid date';
+
+ if (!format) format = '%x %X';
+ if (typeof format == 'string') format = formats[format.toLowerCase()] || format;
+ if (typeof format == 'function') return format(this);
+
+ var d = this;
+ return format.replace(/%([a-z%])/gi,
+ function($0, $1){
+ switch ($1){
+ case 'a': return Date.getMsg('days_abbr')[d.get('day')];
+ case 'A': return Date.getMsg('days')[d.get('day')];
+ case 'b': return Date.getMsg('months_abbr')[d.get('month')];
+ case 'B': return Date.getMsg('months')[d.get('month')];
+ case 'c': return d.format('%a %b %d %H:%M:%S %Y');
+ case 'd': return pad(d.get('date'), 2);
+ case 'e': return pad(d.get('date'), 2, ' ');
+ case 'H': return pad(d.get('hr'), 2);
+ case 'I': return pad((d.get('hr') % 12) || 12, 2);
+ case 'j': return pad(d.get('dayofyear'), 3);
+ case 'k': return pad(d.get('hr'), 2, ' ');
+ case 'l': return pad((d.get('hr') % 12) || 12, 2, ' ');
+ case 'L': return pad(d.get('ms'), 3);
+ case 'm': return pad((d.get('mo') + 1), 2);
+ case 'M': return pad(d.get('min'), 2);
+ case 'o': return d.get('ordinal');
+ case 'p': return Date.getMsg(d.get('ampm'));
+ case 's': return Math.round(d / 1000);
+ case 'S': return pad(d.get('seconds'), 2);
+ case 'T': return d.format('%H:%M:%S');
+ case 'U': return pad(d.get('week'), 2);
+ case 'w': return d.get('day');
+ case 'x': return d.format(Date.getMsg('shortDate'));
+ case 'X': return d.format(Date.getMsg('shortTime'));
+ case 'y': return d.get('year').toString().substr(2);
+ case 'Y': return d.get('year');
+ case 'z': return d.get('GMTOffset');
+ case 'Z': return d.get('Timezone');
+ }
+ return $1;
+ }
+ );
+ },
+
+ toISOString: function(){
+ return this.format('iso8601');
+ }
+
+}).alias({
+ toJSON: 'toISOString',
+ compare: 'diff',
+ strftime: 'format'
+});
+
+// The day and month abbreviations are standardized, so we cannot use simply %a and %b because they will get localized
+var rfcDayAbbr = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+ rfcMonthAbbr = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+
+var formats = {
+ db: '%Y-%m-%d %H:%M:%S',
+ compact: '%Y%m%dT%H%M%S',
+ 'short': '%d %b %H:%M',
+ 'long': '%B %d, %Y %H:%M',
+ rfc822: function(date){
+ return rfcDayAbbr[date.get('day')] + date.format(', %d ') + rfcMonthAbbr[date.get('month')] + date.format(' %Y %H:%M:%S %Z');
+ },
+ rfc2822: function(date){
+ return rfcDayAbbr[date.get('day')] + date.format(', %d ') + rfcMonthAbbr[date.get('month')] + date.format(' %Y %H:%M:%S %z');
+ },
+ iso8601: function(date){
+ return (
+ date.getUTCFullYear() + '-' +
+ pad(date.getUTCMonth() + 1, 2) + '-' +
+ pad(date.getUTCDate(), 2) + 'T' +
+ pad(date.getUTCHours(), 2) + ':' +
+ pad(date.getUTCMinutes(), 2) + ':' +
+ pad(date.getUTCSeconds(), 2) + '.' +
+ pad(date.getUTCMilliseconds(), 3) + 'Z'
+ );
+ }
+};
+
+var parsePatterns = [],
+ nativeParse = Date.parse;
+
+var parseWord = function(type, word, num){
+ var ret = -1,
+ translated = Date.getMsg(type + 's');
+ switch (typeOf(word)){
+ case 'object':
+ ret = translated[word.get(type)];
+ break;
+ case 'number':
+ ret = translated[word];
+ if (!ret) throw new Error('Invalid ' + type + ' index: ' + word);
+ break;
+ case 'string':
+ var match = translated.filter(function(name){
+ return this.test(name);
+ }, new RegExp('^' + word, 'i'));
+ if (!match.length) throw new Error('Invalid ' + type + ' string');
+ if (match.length > 1) throw new Error('Ambiguous ' + type);
+ ret = match[0];
+ }
+
+ return (num) ? translated.indexOf(ret) : ret;
+};
+
+var startCentury = 1900,
+ startYear = 70;
+
+Date.extend({
+
+ getMsg: function(key, args){
+ return Locale.get('Date.' + key, args);
+ },
+
+ units: {
+ ms: Function.from(1),
+ second: Function.from(1000),
+ minute: Function.from(60000),
+ hour: Function.from(3600000),
+ day: Function.from(86400000),
+ week: Function.from(608400000),
+ month: function(month, year){
+ var d = new Date;
+ return Date.daysInMonth(month != null ? month : d.get('mo'), year != null ? year : d.get('year')) * 86400000;
+ },
+ year: function(year){
+ year = year || new Date().get('year');
+ return Date.isLeapYear(year) ? 31622400000 : 31536000000;
+ }
+ },
+
+ daysInMonth: function(month, year){
+ return [31, Date.isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
+ },
+
+ isLeapYear: function(year){
+ return ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0);
+ },
+
+ parse: function(from){
+ var t = typeOf(from);
+ if (t == 'number') return new Date(from);
+ if (t != 'string') return from;
+ from = from.clean();
+ if (!from.length) return null;
+
+ var parsed;
+ parsePatterns.some(function(pattern){
+ var bits = pattern.re.exec(from);
+ return (bits) ? (parsed = pattern.handler(bits)) : false;
+ });
+
+ if (!(parsed && parsed.isValid())){
+ parsed = new Date(nativeParse(from));
+ if (!(parsed && parsed.isValid())) parsed = new Date(from.toInt());
+ }
+ return parsed;
+ },
+
+ parseDay: function(day, num){
+ return parseWord('day', day, num);
+ },
+
+ parseMonth: function(month, num){
+ return parseWord('month', month, num);
+ },
+
+ parseUTC: function(value){
+ var localDate = new Date(value);
+ var utcSeconds = Date.UTC(
+ localDate.get('year'),
+ localDate.get('mo'),
+ localDate.get('date'),
+ localDate.get('hr'),
+ localDate.get('min'),
+ localDate.get('sec'),
+ localDate.get('ms')
+ );
+ return new Date(utcSeconds);
+ },
+
+ orderIndex: function(unit){
+ return Date.getMsg('dateOrder').indexOf(unit) + 1;
+ },
+
+ defineFormat: function(name, format){
+ formats[name] = format;
+ return this;
+ },
+
+
+
+ defineParser: function(pattern){
+ parsePatterns.push((pattern.re && pattern.handler) ? pattern : build(pattern));
+ return this;
+ },
+
+ defineParsers: function(){
+ Array.flatten(arguments).each(Date.defineParser);
+ return this;
+ },
+
+ define2DigitYearStart: function(year){
+ startYear = year % 100;
+ startCentury = year - startYear;
+ return this;
+ }
+
+}).extend({
+ defineFormats: Date.defineFormat.overloadSetter()
+});
+
+var regexOf = function(type){
+ return new RegExp('(?:' + Date.getMsg(type).map(function(name){
+ return name.substr(0, 3);
+ }).join('|') + ')[a-z]*');
+};
+
+var replacers = function(key){
+ switch (key){
+ case 'T':
+ return '%H:%M:%S';
+ case 'x': // iso8601 covers yyyy-mm-dd, so just check if month is first
+ return ((Date.orderIndex('month') == 1) ? '%m[-./]%d' : '%d[-./]%m') + '([-./]%y)?';
+ case 'X':
+ return '%H([.:]%M)?([.:]%S([.:]%s)?)? ?%p? ?%z?';
+ }
+ return null;
+};
+
+var keys = {
+ d: /[0-2]?[0-9]|3[01]/,
+ H: /[01]?[0-9]|2[0-3]/,
+ I: /0?[1-9]|1[0-2]/,
+ M: /[0-5]?\d/,
+ s: /\d+/,
+ o: /[a-z]*/,
+ p: /[ap]\.?m\.?/,
+ y: /\d{2}|\d{4}/,
+ Y: /\d{4}/,
+ z: /Z|[+-]\d{2}(?::?\d{2})?/
+};
+
+keys.m = keys.I;
+keys.S = keys.M;
+
+var currentLanguage;
+
+var recompile = function(language){
+ currentLanguage = language;
+
+ keys.a = keys.A = regexOf('days');
+ keys.b = keys.B = regexOf('months');
+
+ parsePatterns.each(function(pattern, i){
+ if (pattern.format) parsePatterns[i] = build(pattern.format);
+ });
+};
+
+var build = function(format){
+ if (!currentLanguage) return {format: format};
+
+ var parsed = [];
+ var re = (format.source || format) // allow format to be regex
+ .replace(/%([a-z])/gi,
+ function($0, $1){
+ return replacers($1) || $0;
+ }
+ ).replace(/\((?!\?)/g, '(?:') // make all groups non-capturing
+ .replace(/ (?!\?|\*)/g, ',? ') // be forgiving with spaces and commas
+ .replace(/%([a-z%])/gi,
+ function($0, $1){
+ var p = keys[$1];
+ if (!p) return $1;
+ parsed.push($1);
+ return '(' + p.source + ')';
+ }
+ ).replace(/\[a-z\]/gi, '[a-z\\u00c0-\\uffff;\&]'); // handle unicode words
+
+ return {
+ format: format,
+ re: new RegExp('^' + re + '$', 'i'),
+ handler: function(bits){
+ bits = bits.slice(1).associate(parsed);
+ var date = new Date().clearTime(),
+ year = bits.y || bits.Y;
+
+ if (year != null) handle.call(date, 'y', year); // need to start in the right year
+ if ('d' in bits) handle.call(date, 'd', 1);
+ if ('m' in bits || bits.b || bits.B) handle.call(date, 'm', 1);
+
+ for (var key in bits) handle.call(date, key, bits[key]);
+ return date;
+ }
+ };
+};
+
+var handle = function(key, value){
+ if (!value) return this;
+
+ switch (key){
+ case 'a': case 'A': return this.set('day', Date.parseDay(value, true));
+ case 'b': case 'B': return this.set('mo', Date.parseMonth(value, true));
+ case 'd': return this.set('date', value);
+ case 'H': case 'I': return this.set('hr', value);
+ case 'm': return this.set('mo', value - 1);
+ case 'M': return this.set('min', value);
+ case 'p': return this.set('ampm', value.replace(/\./g, ''));
+ case 'S': return this.set('sec', value);
+ case 's': return this.set('ms', ('0.' + value) * 1000);
+ case 'w': return this.set('day', value);
+ case 'Y': return this.set('year', value);
+ case 'y':
+ value = +value;
+ if (value < 100) value += startCentury + (value < startYear ? 100 : 0);
+ return this.set('year', value);
+ case 'z':
+ if (value == 'Z') value = '+00';
+ var offset = value.match(/([+-])(\d{2}):?(\d{2})?/);
+ offset = (offset[1] + '1') * (offset[2] * 60 + (+offset[3] || 0)) + this.getTimezoneOffset();
+ return this.set('time', this - offset * 60000);
+ }
+
+ return this;
+};
+
+Date.defineParsers(
+ '%Y([-./]%m([-./]%d((T| )%X)?)?)?', // "1999-12-31", "1999-12-31 11:59pm", "1999-12-31 23:59:59", ISO8601
+ '%Y%m%d(T%H(%M%S?)?)?', // "19991231", "19991231T1159", compact
+ '%x( %X)?', // "12/31", "12.31.99", "12-31-1999", "12/31/2008 11:59 PM"
+ '%d%o( %b( %Y)?)?( %X)?', // "31st", "31st December", "31 Dec 1999", "31 Dec 1999 11:59pm"
+ '%b( %d%o)?( %Y)?( %X)?', // Same as above with month and day switched
+ '%Y %b( %d%o( %X)?)?', // Same as above with year coming first
+ '%o %b %d %X %z %Y', // "Thu Oct 22 08:11:23 +0000 2009"
+ '%T', // %H:%M:%S
+ '%H:%M( ?%p)?' // "11:05pm", "11:05 am" and "11:05"
+);
+
+Locale.addEvent('change', function(language){
+ if (Locale.get('Date')) recompile(language);
+}).fireEvent('change', Locale.getCurrent());
+
+})();
+
+
+/*
+---
+
+script: Date.Extras.js
+
+name: Date.Extras
+
+description: Extends the Date native object to include extra methods (on top of those in Date.js).
+
+license: MIT-style license
+
+authors:
+ - Aaron Newton
+ - Scott Kyle
+
+requires:
+ - /Date
+
+provides: [Date.Extras]
+
+...
+*/
+
+Date.implement({
+
+ timeDiffInWords: function(to){
+ return Date.distanceOfTimeInWords(this, to || new Date);
+ },
+
+ timeDiff: function(to, separator){
+ if (to == null) to = new Date;
+ var delta = ((to - this) / 1000).floor().abs();
+
+ var vals = [],
+ durations = [60, 60, 24, 365, 0],
+ names = ['s', 'm', 'h', 'd', 'y'],
+ value, duration;
+
+ for (var item = 0; item < durations.length; item++){
+ if (item && !delta) break;
+ value = delta;
+ if ((duration = durations[item])){
+ value = (delta % duration);
+ delta = (delta / duration).floor();
+ }
+ vals.unshift(value + (names[item] || ''));
+ }
+
+ return vals.join(separator || ':');
+ }
+
+}).extend({
+
+ distanceOfTimeInWords: function(from, to){
+ return Date.getTimePhrase(((to - from) / 1000).toInt());
+ },
+
+ getTimePhrase: function(delta){
+ var suffix = (delta < 0) ? 'Until' : 'Ago';
+ if (delta < 0) delta *= -1;
+
+ var units = {
+ minute: 60,
+ hour: 60,
+ day: 24,
+ week: 7,
+ month: 52 / 12,
+ year: 12,
+ eon: Infinity
+ };
+
+ var msg = 'lessThanMinute';
+
+ for (var unit in units){
+ var interval = units[unit];
+ if (delta < 1.5 * interval){
+ if (delta > 0.75 * interval) msg = unit;
+ break;
+ }
+ delta /= interval;
+ msg = unit + 's';
+ }
+
+ delta = delta.round();
+ return Date.getMsg(msg + suffix, delta).substitute({delta: delta});
+ }
+
+}).defineParsers(
+
+ {
+ // "today", "tomorrow", "yesterday"
+ re: /^(?:tod|tom|yes)/i,
+ handler: function(bits){
+ var d = new Date().clearTime();
+ switch (bits[0]){
+ case 'tom': return d.increment();
+ case 'yes': return d.decrement();
+ default: return d;
+ }
+ }
+ },
+
+ {
+ // "next Wednesday", "last Thursday"
+ re: /^(next|last) ([a-z]+)$/i,
+ handler: function(bits){
+ var d = new Date().clearTime();
+ var day = d.getDay();
+ var newDay = Date.parseDay(bits[2], true);
+ var addDays = newDay - day;
+ if (newDay <= day) addDays += 7;
+ if (bits[1] == 'last') addDays -= 7;
+ return d.set('date', d.getDate() + addDays);
+ }
+ }
+
+).alias('timeAgoInWords', 'timeDiffInWords');
+
+
/*
---
diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js
index 0a15efe6..442d61af 100644
--- a/couchpotato/static/scripts/page/settings.js
+++ b/couchpotato/static/scripts/page/settings.js
@@ -628,7 +628,7 @@ Option.Directory = new Class({
self.dir_list = new Element('ul', {
'events': {
'click:relay(li)': function(e, el){
- (e).stop();
+ (e).preventDefault();
self.selectDirectory(el.get('data-value'))
},
'mousewheel': function(e){
@@ -678,7 +678,7 @@ Option.Directory = new Class({
hideBrowser: function(e, save){
var self = this;
- (e).stop();
+ (e).preventDefault();
if(save)
self.save()
@@ -1241,7 +1241,7 @@ Option.Combined = new Class({
deleteCombinedItem: function(e){
var self = this;
- (e).stop();
+ (e).preventDefault();
var item = e.target.getParent();
diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js
index 9bf5e48a..8d1dd931 100644
--- a/couchpotato/static/scripts/page/wanted.js
+++ b/couchpotato/static/scripts/page/wanted.js
@@ -49,7 +49,7 @@ window.addEvent('domready', function(){
editMovie: function(e){
var self = this;
- (e).stop();
+ (e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
@@ -89,7 +89,7 @@ window.addEvent('domready', function(){
},
save: function(e){
- (e).stop();
+ (e).preventDefault();
var self = this;
Api.request('movie.edit', {
@@ -129,7 +129,7 @@ window.addEvent('domready', function(){
doRefresh: function(e){
var self = this;
- (e).stop();
+ (e).preventDefault();
Api.request('movie.refresh', {
'data': {
@@ -160,7 +160,7 @@ window.addEvent('domready', function(){
showConfirm: function(e){
var self = this;
- (e).stop();
+ (e).preventDefault();
if(!self.delete_container){
self.delete_container = new Element('div.delete_container').adopt(
@@ -188,13 +188,13 @@ window.addEvent('domready', function(){
hideConfirm: function(e){
var self = this;
- (e).stop();
+ (e).preventDefault();
self.movie.slide('out');
},
del: function(e){
- (e).stop();
+ (e).preventDefault();
var self = this;
var movie = $(self.movie);
@@ -253,7 +253,7 @@ window.addEvent('domready', function(){
showFiles: function(e){
var self = this;
- (e).stop();
+ (e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
diff --git a/couchpotato/static/style/api.css b/couchpotato/static/style/api.css
index afba2b5b..c6354098 100644
--- a/couchpotato/static/style/api.css
+++ b/couchpotato/static/style/api.css
@@ -1,4 +1,3 @@
-
html {
font-size: 12px;
line-height: 1.5;
diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css
index 96058721..fd66f0eb 100644
--- a/couchpotato/static/style/main.css
+++ b/couchpotato/static/style/main.css
@@ -1,10 +1,3 @@
-/* @override
- http://localhost:5000/static/style/main.css
- http://192.168.1.20:5000/static/style/main.css
- http://127.0.0.1:5000/static/style/main.css
- http://127.0.0.1:5000/v2/_api_/static/style/main.css
-*/
-
html {
color: #fff;
font-size: 12px;
@@ -184,6 +177,7 @@ body > .spinner, .mask{
}
.header .navigation {
display: inline-block;
+ vertical-align: middle;
width: 67.2%;
}
.header .navigation ul {
@@ -239,8 +233,7 @@ body > .spinner, .mask{
.header:hover .navigation .backtotop { color: #fff; }
.header .more_menu {
- float: right;
- margin-top: 20px;
+ margin-left: 12px;
}
.header .more_menu ul {
width: 150px;
@@ -252,6 +245,51 @@ body > .spinner, .mask{
.header .more_menu .red { color: red; }
.header .more_menu .orange { color: orange; }
+
+ .badge {
+ position: absolute;
+ width: 14px;
+ height: 14px;
+ text-align: center;
+ line-height: 14px;
+ border-radius: 50%;
+ font-size: 8px;
+ margin: -5px 0 0 15px;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.6), 0 0 3px rgba(0,0,0,.7);
+ background: -webkit-gradient(linear, left bottom, left top, from(rgba(255,255,255,.3)), to(rgba(255,255,255,.1)));
+ background: -moz-linear-gradient(center bottom, rgba(255,255,255,.3) 0%, rgba(255,255,255,.1) 100%);
+ background-color: #1b79b8;
+ text-shadow: none;
+ }
+
+ .header .notification_menu ul {
+ width: 300px;
+ margin-left: -260px;
+ text-align: left;
+ }
+ .header .notification_menu ul:before {
+ left: 296px;
+ }
+
+ .header .notification_menu > a {
+ background-position: center -209px;
+ }
+
+ .header .notification_menu li > span {
+ padding: 5px;
+ display: block;
+ border-bottom: 1px solid rgba(0,0,0,0.2);
+ }
+ .header .notification_menu li .added {
+ display: block;
+ font-size: 10px;
+ color: #aaa;
+ text-align: ;
+ }
+
+ .header .notification_menu li .more {
+ text-align: center;
+ }
.header .message.update {
text-align: center;
@@ -454,6 +492,10 @@ body > .spinner, .mask{
background-color: #4c5766;
}
+ .more_menu {
+ display: inline-block;
+ vertical-align: middle;
+ }
.more_menu > a {
display: block;
@@ -470,16 +512,30 @@ body > .spinner, .mask{
.more_menu ul {
display: none;
border: 1px solid #333;
- background: rgba(0,0,0,0.8);
+ background: rgba(255,255,255,0.98);
border-radius: 3px;
padding: 4px !important;
position: absolute;
z-index: 9;
margin: 32px 0 0 -145px;
width: 185px;
- box-shadow: 0 10px 20px -10px rgba(0,0,0,0.4);
+ box-shadow: 0 10px 10px -5px rgba(0,0,0,0.4);
list-style: none;
text-align: center;
+ color: #000;
+ text-shadow: none;
+ background-image: -webkit-gradient(
+ linear,
+ left bottom,
+ right top,
+ color-stop(0, rgb(200,200,200)),
+ color-stop(1, rgb(255,255,255))
+ );
+ background-image: -moz-linear-gradient(
+ left bottom,
+ rgb(200,200,200) 0%,
+ rgb(255,255,255) 100%
+ );
}
.more_menu ul:before {
@@ -488,7 +544,7 @@ body > .spinner, .mask{
position: relative;
width: 0;
border: 6px solid transparent;
- border-bottom-color: rgba(0,0,0,0.8);
+ border-bottom-color: #fff;
display: block;
top: -16px;
left: 146px;
@@ -510,7 +566,7 @@ body > .spinner, .mask{
text-transform: uppercase;
letter-spacing: 1px;
padding: 3px 0;
- color: #fff;
+ color: #000;
}
.more_menu ul li:first-child {
@@ -519,8 +575,7 @@ body > .spinner, .mask{
.more_menu ul li:last-child a {
border: none;
- color: #fff;
}
.more_menu ul li a:hover {
- background: rgba(255,255,255,0.1);
+ background: rgba(0,0,0,0.05);
}
\ No newline at end of file
diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css
index 40ae4700..98ac0332 100644
--- a/couchpotato/static/style/page/settings.css
+++ b/couchpotato/static/style/page/settings.css
@@ -1,9 +1,3 @@
-/* @override
- http://localhost:5000/static/style/page/settings.css
- http://192.168.1.20:5000/static/style/page/settings.css
- http://127.0.0.1:5000/static/style/page/settings.css
-*/
-
.page.settings:after {
content: ".";
display: block;
diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html
index 8df06c4b..751850f7 100644
--- a/couchpotato/templates/_desktop.html
+++ b/couchpotato/templates/_desktop.html
@@ -30,7 +30,7 @@
-
+