diff --git a/couchpotato/__init__.py b/couchpotato/__init__.py index 8ce29933..2fb49520 100644 --- a/couchpotato/__init__.py +++ b/couchpotato/__init__.py @@ -6,6 +6,7 @@ from flask.helpers import url_for from flask.module import Module from flask.templating import render_template from werkzeug.utils import redirect +import os app = Flask(__name__) log = CPLog(__name__) @@ -15,7 +16,7 @@ web = Module(__name__, 'web') @web.route('/') @requires_auth def index(): - return render_template('index.html') + return render_template('index.html', sep = os.sep) @app.errorhandler(404) def page_not_found(error): diff --git a/couchpotato/api/__init__.py b/couchpotato/api/__init__.py index a4f14c3e..4c9c4b87 100644 --- a/couchpotato/api/__init__.py +++ b/couchpotato/api/__init__.py @@ -1,8 +1,66 @@ +from couchpotato.api.file_browser import FileBrowser +from couchpotato.core.settings import settings +from couchpotato.core.settings.loader import settings_loader from flask import Module from flask.helpers import jsonify +import flask api = Module(__name__) -@api.route('/') +@api.route('') def index(): return jsonify({'test': 'bla'}) + + +@api.route('settings/') +def settings_view(): + return jsonify({ + 'sections': settings_loader.sections, + 'values': settings.getValues() + }) + +@api.route('setting.save/') +def setting_save_view(): + a = flask.request.args + + section = a.get('section') + option = a.get('name') + value = a.get('value') + + settings.set(section, option, value) + settings.save() + + return jsonify({ + 'success': True, + }); + +@api.route('movie/') +def movie(): + return jsonify({ + 'success': True, + 'movies': [ + { + 'name': 'Movie 1', + 'description': 'Description 1', + }, + { + 'name': 'Movie 2', + 'description': 'Description 2', + } + ] + }) + +@api.route('directory.list/') +def director_list(): + a = flask.request.args + + try: + fb = FileBrowser(a.get('path', '/')) + dirs = fb.getDirectories() + except: + dirs = [] + + return jsonify({ + 'empty': len(dirs) == 0, + 'dirs': dirs, + }) diff --git a/couchpotato/api/file_browser.py b/couchpotato/api/file_browser.py new file mode 100644 index 00000000..f274e096 --- /dev/null +++ b/couchpotato/api/file_browser.py @@ -0,0 +1,38 @@ +import os +import string + +if os.name == 'nt': + import win32file + +class FileBrowser(): + + def __init__(self, path = '/'): + self.path = path + + def getDirectories(self): + + # Return driveletters or root if path is empty + if self.path == '/' or not self.path: + if os.name == 'nt': + return self.getDriveLetters() + self.path = '/' + + dirs = [] + for f in os.listdir(self.path): + path = os.path.join(self.path, f) + if(os.path.isdir(path)): + dirs.append(path) + + return dirs + + def getFiles(self): + pass + + def getDriveLetters(self): + + driveletters = [] + for drive in string.ascii_uppercase: + if win32file.GetDriveType(drive + ":") == win32file.DRIVE_FIXED: + driveletters.append(drive + ":") + + return driveletters diff --git a/couchpotato/cli.py b/couchpotato/cli.py index 36308fd1..89851cd5 100644 --- a/couchpotato/cli.py +++ b/couchpotato/cli.py @@ -77,10 +77,10 @@ def cmd_couchpotato(base_path, args): # Load configs - from couchpotato.core.settings.loader import SettingsLoader - sl = SettingsLoader(root = base_path) - sl.addConfig('couchpotato', 'core') - sl.run() + from couchpotato.core.settings.loader import settings_loader + settings_loader.load(root = base_path) + settings_loader.addConfig('couchpotato', 'core') + settings_loader.run() # Create app @@ -103,7 +103,7 @@ def cmd_couchpotato(base_path, args): # Register modules app.register_module(web, url_prefix = '%s/' % url_base) - app.register_module(api, url_prefix = '%s/%s/%s/' % (url_base, 'api', api_key)) + app.register_module(api, url_prefix = '%s/%s/%s/' % (url_base, 'api', api_key if not debug else 'apikey')) # Go go go! app.run(use_reloader = reloader) diff --git a/couchpotato/core/__init__.py b/couchpotato/core/__init__.py index 81fce1ff..f0aaee77 100644 --- a/couchpotato/core/__init__.py +++ b/couchpotato/core/__init__.py @@ -1,12 +1,59 @@ from uuid import uuid4 -config = ('global', { - 'debug': False, - 'host': '0.0.0.0', - 'port': 5000, - 'username': '', - 'password': '', - 'launch_browser': True, - 'url_base': '', - 'api_key': uuid4().hex, -}) +config = [{ + 'name': 'global', + 'tab': 'general', + 'options': { + 'debug': { + 'advanced': True, + 'default': False, + 'type': 'bool', + 'label': 'Debug', + 'description': 'Enable debugging.', + }, + 'host': { + 'advanced': True, + 'default': '0.0.0.0', + 'type': 'string', + 'label': 'Host', + 'description': 'Host that I should listen to 0.0.0.0 listens to everything.', + }, + 'port': { + 'default': 5000, + 'type': 'int', + 'label': 'Port', + 'description': 'The port I should listen to.', + }, + 'username': { + 'default': '', + 'type': 'string', + 'label': 'Username', + }, + 'password': { + 'default': '', + 'password': True, + 'type': 'string', + 'label': 'Password', + }, + 'launch_browser': { + 'default': True, + 'type': 'bool', + 'label': 'Launch Browser', + 'description': 'Launch the browser when I start.', + }, + 'url_base': { + 'advanced': True, + 'default': '', + 'type': 'string', + 'label': 'Url Base', + 'description': 'When using mod_proxy use this to prepend the url with this.', + }, + 'api_key': { + 'default': uuid4().hex, + 'type': 'string', + 'readonly': True, + 'label': 'Api Key', + 'description': 'This is top-secret! Don\'t share this!', + } + } +}] diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index 8b3176fa..9f014bdc 100644 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -1,4 +1,30 @@ -config = ('Renamer', { - 'enabled': False, - 'cleanup': False -}) +config = [{ + 'name': 'Renamer', + 'tab': 'renaming', + 'options': { + 'enabled': { + 'default': False, + 'type': 'bool', + 'description': 'Enable renaming', + }, + 'from': { + 'default': '', + 'type': 'directory', + 'label': 'From', + 'description': 'Folder where the movies are downloaded to.', + }, + 'to': { + 'default': '', + 'type': 'directory', + 'label': 'To', + 'description': 'Folder where the movies will be moved to.', + }, + 'run_every': { + 'default': 1, + 'type': 'int', + 'unit': 'min(s)', + 'description': 'Search for new movies inside the folder every X minutes.', + } + } +}] + diff --git a/couchpotato/core/providers/tmdb/__init__.py b/couchpotato/core/providers/tmdb/__init__.py index 432f4fed..2e1bc40d 100644 --- a/couchpotato/core/providers/tmdb/__init__.py +++ b/couchpotato/core/providers/tmdb/__init__.py @@ -1,3 +1,12 @@ -config = ('TheMovieDB', { - 'key': '9b939aee0aaafc12a65bf448e4af9543' -}) +config = [{ + 'name': 'TheMovieDB', + 'tab': 'providers', + 'options': { + 'api_key': { + 'advanced': True, + 'default': '9b939aee0aaafc12a65bf448e4af9543', + 'type': 'string', + 'description': 'Api key to use for calls to TheMovieDB.', + } + } +}] diff --git a/couchpotato/core/settings/__init__.py b/couchpotato/core/settings/__init__.py index e3ac67cf..52bd00e0 100644 --- a/couchpotato/core/settings/__init__.py +++ b/couchpotato/core/settings/__init__.py @@ -27,7 +27,7 @@ class Settings(): return self.p def sections(self): - return self.s + return self.p.sections() def connectSignals(self): signal('settings.register').connect(self.registerDefaults) @@ -46,24 +46,34 @@ class Settings(): self.save(self) def set(self, section, option, value): - return self.p.set(section, option, value) + return self.p.set(section, option, self.cleanValue(value)) def get(self, option = '', section = 'global', default = ''): - try: value = self.p.get(section, option) - - if(self.is_int(value)): - return int(value) - - if str(value).lower() in self.bool: - return self.bool.get(str(value).lower()) - - return value if type(value) != str else value.strip() + return self.cleanValue(value) except: return default - def save(self, caller): + def cleanValue(self, value): + if(self.is_int(value)): + return int(value) + + if str(value).lower() in self.bool: + return self.bool.get(str(value).lower()) + + return value.strip() + + def getValues(self): + values = {} + for section in self.sections(): + values[section] = {} + for option in self.p.items(section): + (option_name, option_value) = option + values[section][option_name] = option_value + return values + + def save(self, caller = ''): with open(self.file, 'wb') as configfile: self.p.write(configfile) diff --git a/couchpotato/core/settings/loader.py b/couchpotato/core/settings/loader.py index 91a056e1..70687e3f 100644 --- a/couchpotato/core/settings/loader.py +++ b/couchpotato/core/settings/loader.py @@ -8,12 +8,14 @@ log = CPLog(__name__) class SettingsLoader: configs = {} + sections = {} - def __init__(self, root = ''): - + def __init__(self): self.settings_register = signal('settings.register') self.settings_save = signal('settings.save') + def load(self, root = ''): + self.paths = { 'plugins' : ('couchpotato.core.plugins', os.path.join(root, 'couchpotato', 'core', 'plugins')), 'providers' : ('couchpotato.core.providers', os.path.join(root, 'couchpotato', 'core', 'providers')), @@ -42,8 +44,13 @@ class SettingsLoader: module_name = '%s.%s' % (module, name) try: m = getattr(self.loadModule(module_name), name) - (section, options) = m.config - self.settings_register.send(section, options = options, save = save) + + for section in m.config: + self.addSection(section['name'], section) + options = {} + for key, option in section['options'].iteritems(): + options[key] = option['default'] + self.settings_register.send(section['name'], options = options, save = save) return True except Exception, e: @@ -62,3 +69,11 @@ class SettingsLoader: return m except: raise + + def addSection(self, section_name, options): + self.sections[section_name] = options + + def getSections(self): + return self.sections + +settings_loader = SettingsLoader() diff --git a/couchpotato/static/scripts/block.js b/couchpotato/static/scripts/block.js new file mode 100644 index 00000000..8fea9a53 --- /dev/null +++ b/couchpotato/static/scripts/block.js @@ -0,0 +1,26 @@ +var BlockBase = new Class({ + + Implements: [Options, Events], + + options: {}, + + initialize: function(parent, options){ + var self = this; + self.setOptions(options); + + self.parent = parent; + + self.create(); + }, + + create: function(){ + this.el = new Element('div.block'); + }, + + toElement: function(){ + return this.el + } + +}); + +var Block = {} \ No newline at end of file diff --git a/couchpotato/static/scripts/block/footer.js b/couchpotato/static/scripts/block/footer.js new file mode 100644 index 00000000..acec1585 --- /dev/null +++ b/couchpotato/static/scripts/block/footer.js @@ -0,0 +1,11 @@ +Block.Footer = new Class({ + + Extends: BlockBase, + + create: function(){ + var self = this; + + self.el = new Element('div.footer'); + } + +}); \ No newline at end of file diff --git a/couchpotato/static/scripts/block/navigation.js b/couchpotato/static/scripts/block/navigation.js new file mode 100644 index 00000000..6167f494 --- /dev/null +++ b/couchpotato/static/scripts/block/navigation.js @@ -0,0 +1,20 @@ +Block.Navigation = new Class({ + + Extends: BlockBase, + + create: function(){ + var self = this; + + self.el = new Element('ul.navigation'); + }, + + addTab: function(tab){ + var self = this + + return new Element('li').adopt( + new Element('a', tab) + ).inject(self.el) + + } + +}); \ No newline at end of file diff --git a/couchpotato/static/scripts/block/search.js b/couchpotato/static/scripts/block/search.js new file mode 100644 index 00000000..23cfe211 --- /dev/null +++ b/couchpotato/static/scripts/block/search.js @@ -0,0 +1,11 @@ +Block.Search = new Class({ + + Extends: BlockBase, + + create: function(){ + var self = this; + + self.el = new Element('div.search_form'); + } + +}); \ No newline at end of file diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index bc23fd3e..7434a619 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -3,20 +3,13 @@ var CouchPotato = new Class({ Implements: [Options], defaults: { - page: 'movie', + page: 'wanted', action: 'index', params: {} }, pages: [], - - tabs: [ - {'href': '/movie/', 'title':'Gimmy gimmy gimmy!', 'label':'Wanted'}, - {'href': '/manage/', 'title':'Do stuff to your existing movies!', 'label':'Manage'}, - {'href': '/feed/', 'title':'Which wanted movies are released soon?', 'label':'Soon'}, - {'href': '/log/', 'title':'Show recent logs.', 'class':'logLink', 'label':'Logs'}, - {'href': '/config/', 'title':'Change settings.', 'id':'showConfig'} - ], + block: [], initialize: function(options) { var self = this; @@ -25,38 +18,24 @@ var CouchPotato = new Class({ self.c = $(document.body) self.route = new Route(self.defaults); - self.api = new Api(self.options.api_url) + self.api = new Api(self.options.api) - History.addEvent('change', self.createPage.bind(self)); + self.createLayout(); + self.createPages(); + + History.addEvent('change', self.openPage.bind(self)); History.handleInitialState(); - self.createLayout() - self.createNavigation() - - self.c.addEvent('click:relay(a)', self.openPage.bind(self)) + self.c.addEvent('click:relay(a)', self.pushState.bind(self)); }, - openPage: function(e){ + pushState: function(e){ var self = this; - (e).stop() - - var url = e.target.get('href') - History.push(url) - }, - - createNavigation: function(){ - var self = this - - self.tabs.each(function(tab){ - new Element('li').adopt( - new Element('a', { - 'href': tab.href, - 'title': tab.title, - 'text': tab.label - }) - ).inject(self.navigation) - }) + (e).stop(); + var url = e.target.get('href'); + if(History.getPath() != url) + History.push(url); }, createLayout: function(){ @@ -64,15 +43,27 @@ var CouchPotato = new Class({ self.c.adopt( self.header = new Element('div.header').adopt( - self.navigation = new Element('ul.navigation'), - self.add_form = new Element('div.add_form') + self.block.navigation = new Block.Navigation(self, {}), + self.block.search = new Block.Search(self, {}) ), self.content = new Element('div.content'), - self.footer = new Element('div.footer') - ) + self.block.footer = new Block.Footer(self, {}) + ); }, - createPage: function(url) { + createPages: function(){ + var self = this; + + Object.each(Page, function(page_class, class_name){ + pg = new Page[class_name](self, {}); + self.pages[class_name] = pg; + + $(pg).inject(self.content); + }); + + }, + + openPage: function(url) { var self = this; self.route.parse(url); @@ -80,14 +71,19 @@ var CouchPotato = new Class({ var action = self.route.getAction(); var params = self.route.getParams(); - var pg = self.pages[page_name] - if(!pg){ - pg = new Page[page_name](); - pg.setParent(self) - self.pages[page_name] = pg; - } - pg.open(action, params) + var page = self.pages[page_name]; + page.open(action, params); + page.show(); + if(self.current_page) + self.current_page.hide() + + self.current_page = page; + + }, + + getBlock: function(block_name){ + return this.block[block_name] }, getApi: function(){ @@ -95,77 +91,41 @@ var CouchPotato = new Class({ } }); -var PageBase = new Class({ - - Implements: [Options], - - initialize: function(options) { - - }, - - open: function(action, params){ - var self = this; - p('Opening: ' +self.getName() + ', ' + action + ', ' + Object.toQueryString(params)); - - try { - self[action+'Action'](params) - } - catch (e){ - self.errorAction(e) - } - }, - - errorAction: function(e){ - p('Error, action not found', e); - }, - - getName: function(){ - return this.name - }, - - setParent: function(parent){ - this.parent = parent - }, - - getParent: function(){ - return this.parent - }, - - api: function(){ - return this.parent.getApi() - } -}); var Api = new Class({ url: '', - initialize: function(url){ + initialize: function(options){ var self = this - self.url = url + self.options = options; self.req = new Request.JSON({ 'method': 'get' }) }, - request: function(type, params, data){ + request: function(type, options){ var self = this; - self.req.setOptions({ - 'url': self.createUrl(type, params), - 'data': data - }) - self.req.send() + new Request.JSON(Object.merge({ + 'method': 'get', + 'url': self.createUrl(type), + }, options)).send() }, - createUrl: function(action, params){ - return this.url + (action || 'default') + '/?' + Object.toQueryString(params) + createUrl: function(action){ + return this.options.url + (action || 'default') + '/' + }, + + getOption: function(name){ + return this.options[name] } }); + var Route = new Class({ defaults: {}, diff --git a/couchpotato/static/scripts/mootools.js b/couchpotato/static/scripts/mootools.js index 917fd162..efaec23c 100644 --- a/couchpotato/static/scripts/mootools.js +++ b/couchpotato/static/scripts/mootools.js @@ -3,10 +3,10 @@ MooTools: the javascript framework web build: - - http://mootools.net/core/755c9fd7bb9342a1fa57db060c2cd141 + - http://mootools.net/core/efcfcd2923b4129a00a22580c45b1d75 packager build: - - packager build Core/Class Core/Class.Extras Core/Element Core/Request.JSON Core/DOMReady + - packager build Core/Class Core/Class.Extras Core/Element Core/Element.Style Core/Request.JSON Core/DOMReady /* --- @@ -3120,6 +3120,187 @@ Element.Properties.html = (function(){ })(); +/* +--- + +name: Element.Style + +description: Contains methods for interacting with the styles of Elements in a fashionable way. + +license: MIT-style license. + +requires: Element + +provides: Element.Style + +... +*/ + +(function(){ + +var html = document.html; + +Element.Properties.styles = {set: function(styles){ + this.setStyles(styles); +}}; + +var hasOpacity = (html.style.opacity != null); +var reAlpha = /alpha\(opacity=([\d.]+)\)/i; + +var setOpacity = function(element, opacity){ + if (!element.currentStyle || !element.currentStyle.hasLayout) element.style.zoom = 1; + if (hasOpacity){ + element.style.opacity = opacity; + } else { + opacity = (opacity == 1) ? '' : 'alpha(opacity=' + opacity * 100 + ')'; + var filter = element.style.filter || element.getComputedStyle('filter') || ''; + element.style.filter = filter.test(reAlpha) ? filter.replace(reAlpha, opacity) : filter + opacity; + } +}; + +Element.Properties.opacity = { + + set: function(opacity){ + var visibility = this.style.visibility; + if (opacity == 0 && visibility != 'hidden') this.style.visibility = 'hidden'; + else if (opacity != 0 && visibility != 'visible') this.style.visibility = 'visible'; + + setOpacity(this, opacity); + }, + + get: (hasOpacity) ? function(){ + var opacity = this.style.opacity || this.getComputedStyle('opacity'); + return (opacity == '') ? 1 : opacity; + } : function(){ + var opacity, filter = (this.style.filter || this.getComputedStyle('filter')); + if (filter) opacity = filter.match(reAlpha); + return (opacity == null || filter == null) ? 1 : (opacity[1] / 100); + } + +}; + +var floatName = (html.style.cssFloat == null) ? 'styleFloat' : 'cssFloat'; + +Element.implement({ + + getComputedStyle: function(property){ + if (this.currentStyle) return this.currentStyle[property.camelCase()]; + var defaultView = Element.getDocument(this).defaultView, + computed = defaultView ? defaultView.getComputedStyle(this, null) : null; + return (computed) ? computed.getPropertyValue((property == floatName) ? 'float' : property.hyphenate()) : null; + }, + + setOpacity: function(value){ + setOpacity(this, value); + return this; + }, + + getOpacity: function(){ + return this.get('opacity'); + }, + + setStyle: function(property, value){ + switch (property){ + case 'opacity': return this.set('opacity', parseFloat(value)); + case 'float': property = floatName; + } + property = property.camelCase(); + if (typeOf(value) != 'string'){ + var map = (Element.Styles[property] || '@').split(' '); + value = Array.from(value).map(function(val, i){ + if (!map[i]) return ''; + return (typeOf(val) == 'number') ? map[i].replace('@', Math.round(val)) : val; + }).join(' '); + } else if (value == String(Number(value))){ + value = Math.round(value); + } + this.style[property] = value; + return this; + }, + + getStyle: function(property){ + switch (property){ + case 'opacity': return this.get('opacity'); + case 'float': property = floatName; + } + property = property.camelCase(); + var result = this.style[property]; + if (!result || property == 'zIndex'){ + result = []; + for (var style in Element.ShortStyles){ + if (property != style) continue; + for (var s in Element.ShortStyles[style]) result.push(this.getStyle(s)); + return result.join(' '); + } + result = this.getComputedStyle(property); + } + if (result){ + result = String(result); + var color = result.match(/rgba?\([\d\s,]+\)/); + if (color) result = result.replace(color[0], color[0].rgbToHex()); + } + if (Browser.opera || (Browser.ie && isNaN(parseFloat(result)))){ + if (property.test(/^(height|width)$/)){ + var values = (property == 'width') ? ['left', 'right'] : ['top', 'bottom'], size = 0; + values.each(function(value){ + size += this.getStyle('border-' + value + '-width').toInt() + this.getStyle('padding-' + value).toInt(); + }, this); + return this['offset' + property.capitalize()] - size + 'px'; + } + if (Browser.opera && String(result).indexOf('px') != -1) return result; + if (property.test(/(border(.+)Width|margin|padding)/)) return '0px'; + } + return result; + }, + + setStyles: function(styles){ + for (var style in styles) this.setStyle(style, styles[style]); + return this; + }, + + getStyles: function(){ + var result = {}; + Array.flatten(arguments).each(function(key){ + result[key] = this.getStyle(key); + }, this); + return result; + } + +}); + +Element.Styles = { + left: '@px', top: '@px', bottom: '@px', right: '@px', + width: '@px', height: '@px', maxWidth: '@px', maxHeight: '@px', minWidth: '@px', minHeight: '@px', + backgroundColor: 'rgb(@, @, @)', backgroundPosition: '@px @px', color: 'rgb(@, @, @)', + fontSize: '@px', letterSpacing: '@px', lineHeight: '@px', clip: 'rect(@px @px @px @px)', + margin: '@px @px @px @px', padding: '@px @px @px @px', border: '@px @ rgb(@, @, @) @px @ rgb(@, @, @) @px @ rgb(@, @, @)', + borderWidth: '@px @px @px @px', borderStyle: '@ @ @ @', borderColor: 'rgb(@, @, @) rgb(@, @, @) rgb(@, @, @) rgb(@, @, @)', + zIndex: '@', 'zoom': '@', fontWeight: '@', textIndent: '@px', opacity: '@' +}; + + + +Element.ShortStyles = {margin: {}, padding: {}, border: {}, borderWidth: {}, borderStyle: {}, borderColor: {}}; + +['Top', 'Right', 'Bottom', 'Left'].each(function(direction){ + var Short = Element.ShortStyles; + var All = Element.Styles; + ['margin', 'padding'].each(function(style){ + var sd = style + direction; + Short[style][sd] = All[sd] = '@px'; + }); + var bd = 'border' + direction; + Short.border[bd] = All[bd] = '@px @ rgb(@, @, @)'; + var bdw = bd + 'Width', bds = bd + 'Style', bdc = bd + 'Color'; + Short[bd] = {}; + Short.borderWidth[bdw] = Short[bd][bdw] = All[bdw] = '@px'; + Short.borderStyle[bds] = Short[bd][bds] = All[bds] = '@'; + Short.borderColor[bdc] = Short[bd][bdc] = All[bdc] = 'rgb(@, @, @)'; +}); + +})(); + + /* --- diff --git a/couchpotato/static/scripts/mootools_more.js b/couchpotato/static/scripts/mootools_more.js index 8b0b43e8..25027408 100644 --- a/couchpotato/static/scripts/mootools_more.js +++ b/couchpotato/static/scripts/mootools_more.js @@ -1,6 +1,141 @@ // MooTools: the javascript framework. -// Load this file's selection again by visiting: http://mootools.net/more/82d3c4f6ee721f808321f6bf6818d8fb -// Or build this file again with packager using: packager build More/Element.Delegation More/Element.Shortcuts +// Load this file's selection again by visiting: http://mootools.net/more/4da9e3082bf0d4194b76d7e5b3874113 +// Or build this file again with packager using: packager build More/Element.Forms More/Element.Delegation More/Element.Shortcuts +/* +--- + +script: String.Extras.js + +name: String.Extras + +description: Extends the String native object to include methods useful in managing various kinds of strings (query strings, urls, html, etc). + +license: MIT-style license + +authors: + - Aaron Newton + - Guillermo Rauch + - Christopher Pitt + +requires: + - Core/String + - Core/Array + +provides: [String.Extras] + +... +*/ + +(function(){ + +var special = { + 'a': /[àáâãäåăą]/g, + 'A': /[ÀÁÂÃÄÅĂĄ]/g, + 'c': /[ćčç]/g, + 'C': /[ĆČÇ]/g, + 'd': /[ďđ]/g, + 'D': /[ĎÐ]/g, + 'e': /[èéêëěę]/g, + 'E': /[ÈÉÊËĚĘ]/g, + 'g': /[ğ]/g, + 'G': /[Ğ]/g, + 'i': /[ìíîï]/g, + 'I': /[ÌÍÎÏ]/g, + 'l': /[ĺľł]/g, + 'L': /[ĹĽŁ]/g, + 'n': /[ñňń]/g, + 'N': /[ÑŇŃ]/g, + 'o': /[òóôõöøő]/g, + 'O': /[ÒÓÔÕÖØ]/g, + 'r': /[řŕ]/g, + 'R': /[ŘŔ]/g, + 's': /[ššş]/g, + 'S': /[ŠŞŚ]/g, + 't': /[ťţ]/g, + 'T': /[ŤŢ]/g, + 'ue': /[ü]/g, + 'UE': /[Ü]/g, + 'u': /[ùúûůµ]/g, + 'U': /[ÙÚÛŮ]/g, + 'y': /[ÿý]/g, + 'Y': /[ŸÝ]/g, + 'z': /[žźż]/g, + 'Z': /[ŽŹŻ]/g, + 'th': /[þ]/g, + 'TH': /[Þ]/g, + 'dh': /[ð]/g, + 'DH': /[Ð]/g, + 'ss': /[ß]/g, + 'oe': /[œ]/g, + 'OE': /[Œ]/g, + 'ae': /[æ]/g, + 'AE': /[Æ]/g +}, + +tidy = { + ' ': /[\xa0\u2002\u2003\u2009]/g, + '*': /[\xb7]/g, + '\'': /[\u2018\u2019]/g, + '"': /[\u201c\u201d]/g, + '...': /[\u2026]/g, + '-': /[\u2013]/g, +// '--': /[\u2014]/g, + '»': /[\uFFFD]/g +}; + +var walk = function(string, replacements){ + var result = string; + for (key in replacements) result = result.replace(replacements[key], key); + return result; +}; + +var getRegexForTag = function(tag, contents){ + tag = tag || ''; + var regstr = contents ? "<" + tag + "(?!\\w)[^>]*>([\\s\\S]*?)<\/" + tag + "(?!\\w)>" : "<\/?" + tag + "([^>]+)?>"; + reg = new RegExp(regstr, "gi"); + return reg; +}; + +String.implement({ + + standardize: function(){ + return walk(this, special); + }, + + repeat: function(times){ + return new Array(times + 1).join(this); + }, + + pad: function(length, str, direction){ + if (this.length >= length) return this; + + var pad = (str == null ? ' ' : '' + str) + .repeat(length - this.length) + .substr(0, length - this.length); + + if (!direction || direction == 'right') return this + pad; + if (direction == 'left') return pad + this; + + return pad.substr(0, (pad.length / 2).floor()) + this + pad.substr(0, (pad.length / 2).ceil()); + }, + + getTags: function(tag, contents){ + return this.match(getRegexForTag(tag, contents)) || []; + }, + + stripTags: function(tag, contents){ + return this.replace(getRegexForTag(tag, contents), ''); + }, + + tidy: function(){ + return walk(this, tidy); + } + +}); + +})(); + + /* --- @@ -35,6 +170,148 @@ MooTools.More = { }; +/* +--- + +script: Element.Forms.js + +name: Element.Forms + +description: Extends the Element native object to include methods useful in managing inputs. + +license: MIT-style license + +authors: + - Aaron Newton + +requires: + - Core/Element + - /String.Extras + - /MooTools.More + +provides: [Element.Forms] + +... +*/ + +Element.implement({ + + tidy: function(){ + this.set('value', this.get('value').tidy()); + }, + + getTextInRange: function(start, end){ + return this.get('value').substring(start, end); + }, + + getSelectedText: function(){ + if (this.setSelectionRange) return this.getTextInRange(this.getSelectionStart(), this.getSelectionEnd()); + return document.selection.createRange().text; + }, + + getSelectedRange: function(){ + if (this.selectionStart != null){ + return { + start: this.selectionStart, + end: this.selectionEnd + }; + } + + var pos = { + start: 0, + end: 0 + }; + var range = this.getDocument().selection.createRange(); + if (!range || range.parentElement() != this) return pos; + var duplicate = range.duplicate(); + + if (this.type == 'text'){ + pos.start = 0 - duplicate.moveStart('character', -100000); + pos.end = pos.start + range.text.length; + } else { + var value = this.get('value'); + var offset = value.length; + duplicate.moveToElementText(this); + duplicate.setEndPoint('StartToEnd', range); + if (duplicate.text.length) offset -= value.match(/[\n\r]*$/)[0].length; + pos.end = offset - duplicate.text.length; + duplicate.setEndPoint('StartToStart', range); + pos.start = offset - duplicate.text.length; + } + return pos; + }, + + getSelectionStart: function(){ + return this.getSelectedRange().start; + }, + + getSelectionEnd: function(){ + return this.getSelectedRange().end; + }, + + setCaretPosition: function(pos){ + if (pos == 'end') pos = this.get('value').length; + this.selectRange(pos, pos); + return this; + }, + + getCaretPosition: function(){ + return this.getSelectedRange().start; + }, + + selectRange: function(start, end){ + if (this.setSelectionRange){ + this.focus(); + this.setSelectionRange(start, end); + } else { + var value = this.get('value'); + var diff = value.substr(start, end - start).replace(/\r/g, '').length; + start = value.substr(0, start).replace(/\r/g, '').length; + var range = this.createTextRange(); + range.collapse(true); + range.moveEnd('character', start + diff); + range.moveStart('character', start); + range.select(); + } + return this; + }, + + insertAtCursor: function(value, select){ + var pos = this.getSelectedRange(); + var text = this.get('value'); + this.set('value', text.substring(0, pos.start) + value + text.substring(pos.end, text.length)); + if (select !== false) this.selectRange(pos.start, pos.start + value.length); + else this.setCaretPosition(pos.start + value.length); + return this; + }, + + insertAroundCursor: function(options, select){ + options = Object.append({ + before: '', + defaultMiddle: '', + after: '' + }, options); + + var value = this.getSelectedText() || options.defaultMiddle; + var pos = this.getSelectedRange(); + var text = this.get('value'); + + if (pos.start == pos.end){ + this.set('value', text.substring(0, pos.start) + options.before + value + options.after + text.substring(pos.end, text.length)); + this.selectRange(pos.start + options.before.length, pos.end + options.before.length + value.length); + } else { + var current = text.substring(pos.start, pos.end); + this.set('value', text.substring(0, pos.start) + options.before + current + options.after + text.substring(pos.end, text.length)); + var selStart = pos.start + options.before.length; + if (select !== false) this.selectRange(selStart, selStart + current.length); + else this.setCaretPosition(selStart + text.length); + } + return this; + } + +}); + + /* --- diff --git a/couchpotato/static/scripts/page.js b/couchpotato/static/scripts/page.js new file mode 100644 index 00000000..80cfa940 --- /dev/null +++ b/couchpotato/static/scripts/page.js @@ -0,0 +1,78 @@ +var PageBase = new Class({ + + Implements: [Options, Events], + + options: { + + }, + + has_tab: true, + + initialize: function(parent, options) { + var self = this; + + self.setOptions(options) + self.setParent(parent) + + // Create main page container + self.el = new Element('div.page.'+self.name); + + // Create tab for page + if(self.has_tab){ + var nav = self.getParent().getBlock('navigation'); + self.tab = nav.addTab({ + 'href': '/'+self.name, + 'title': self.title, + 'text': self.name.capitalize() + }); + } + }, + + open: function(action, params){ + var self = this; + //p('Opening: ' +self.getName() + ', ' + action + ', ' + Object.toQueryString(params)); + + try { + self[action+'Action'](params); + self.fireEvent('opened'); + } + catch (e){ + self.errorAction(e); + self.fireEvent('error'); + } + }, + + errorAction: function(e){ + p('Error, action not found', e); + }, + + getName: function(){ + return this.name + }, + + setParent: function(parent){ + this.app = parent + }, + + getParent: function(){ + return this.app + }, + + api: function(){ + return this.getParent().getApi() + }, + + show: function(){ + this.el.addClass('active'); + }, + + hide: function(){ + this.el.removeClass('active'); + }, + + toElement: function(){ + return this.el + } +}); + +var Page = {} diff --git a/couchpotato/static/scripts/page/log.js b/couchpotato/static/scripts/page/log.js new file mode 100644 index 00000000..8c9be331 --- /dev/null +++ b/couchpotato/static/scripts/page/log.js @@ -0,0 +1,8 @@ +Page.Log = new Class({ + + Extends: PageBase, + + name: 'log', + title: 'Show recent logs.' + +}) \ No newline at end of file diff --git a/couchpotato/static/scripts/page/manage.js b/couchpotato/static/scripts/page/manage.js new file mode 100644 index 00000000..35258361 --- /dev/null +++ b/couchpotato/static/scripts/page/manage.js @@ -0,0 +1,8 @@ +Page.Manage = new Class({ + + Extends: PageBase, + + name: 'manage', + title: 'Do stuff to your existing movies!' + +}) \ No newline at end of file diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js new file mode 100644 index 00000000..390ef608 --- /dev/null +++ b/couchpotato/static/scripts/page/settings.js @@ -0,0 +1,426 @@ +Page.Settings = new Class({ + + Extends: PageBase, + + name: 'settings', + title: 'Change settings.', + + groups: {}, + + open: function(action, params){ + var self = this + //p('open config', action, params) + + if(!self.data) + self.getData(self.create.bind(self)) + else + self.openTab(action); + }, + + openTab: function(action){ + var self = this; + + if(self.current) + self.toggleTab(self.current, true); + + self.toggleTab(action) + self.current = action; + + }, + + toggleTab: function(tab, hide){ + var self = this; + + var a = hide ? 'removeClass' : 'addClass'; + var c = 'active'; + + var g = self.groups[tab] || self.groups.general + g.tab[a](c); + g.group[a](c); + + }, + + getData: function(onComplete){ + var self = this; + + if(onComplete) + self.api().request('settings', { + 'useSpinner': true, + 'spinnerOptions': { + 'target': self.el + }, + 'onComplete': function(json){ + self.data = json; + onComplete(json); + } + }) + + return self.data; + }, + + getValue: function(section, name){ + var self = this; + try { + return self.data.values[section][name] || ''; + } + catch(e){ + return '' + } + }, + + create: function(json){ + var self = this + + self.el.adopt( + self.tabs = new Element('ul.tabs'), + self.containers = new Element('form.uniForm.containers') + ); + + Object.each(json.sections, function(section, section_name){ + + // Create tab + var tab = new Element('li').adopt( + new Element('a', { + 'href': '/'+self.name+'/'+section.tab+'/', + 'text': section.tab.capitalize() + }) + ).inject(self.tabs); + var group = new Element('div.group').inject(self.containers); + + self.groups[section.tab] = { + 'tab': tab, + 'group': group + } + + // Add section + var fieldset = new Element('fieldset.inlineLabels').inject(group) + Object.each(section.options, function(option, option_name){ + var class_name = (option.type || 'input').capitalize(); + var input = new Option[class_name](self, section_name, option_name, option); + input.inject(fieldset); + }); + + }); + + self.openTab(); + + } + +}); + +var OptionBase = new Class({ + + Implements: [Options, Events], + + klass: 'textInput', + focused_class : 'focused', + save_on_change: true, + + initialize: function(parent, section, name, options){ + var self = this + self.setOptions(options) + + self.page = parent; + self.section = section; + self.name = name; + + self.createBase(); + self.create(); + self.createHint(); + self.setAdvanced(); + + // Add focus events + self.input.addEvents({ + 'change': self.changed.bind(self), + 'keyup': self.changed.bind(self), + 'focus': function() { + self.el.addClass(self.focused_class); + }, + 'blur' : function() { + self.el.removeClass(self.focused_class); + } + }); + + }, + + /** + * Create the element + */ + createBase: function(){ + var self = this + self.el = new Element('div.ctrlHolder') + }, + + create: function(){}, + + setAdvanced: function(){ + this.el.addClass(this.options.advanced ? 'advanced': '') + }, + + createHint: function(){ + var self = this; + if(self.options.description) + new Element('p.formHint', { + 'text': self.options.description + }).inject(self.el); + }, + + // Element has changed, do something + changed: function(){ + var self = this; + + if(self.getValue() != self.previous_value){ + if(self.save_on_change){ + if(self.changed_timer) clearTimeout(self.changed_timer); + self.changed_timer = self.save.delay(300, self); + } + self.fireEvent('change') + } + + }, + + save: function(){ + var self = this; + + self.api().request('setting.save', { + 'data': { + 'section': self.section, + 'name': self.name, + 'value': self.getValue() + }, + 'useSpinner': true, + 'spinnerOptions': { + 'target': self.el + }, + 'onComplete': self.saveCompleted.bind(self) + }); + + }, + + saveCompleted: function(json){ + var self = this; + + var sc = json.success ? 'save_success' : 'save_failed'; + + self.previous_value = self.getValue(); + self.el.addClass(sc); + + (function(){ + self.el.removeClass(sc); + }).delay(3000, self); + }, + + setName: function(name){ + this.name = name; + }, + + postName: function(){ + var self = this; + return self.section +'['+self.name+']'; + }, + + getValue: function(){ + var self = this; + return self.input.get('value'); + }, + + getSettingValue: function(){ + var self = this; + return self.page.getValue(self.section, self.name); + }, + + api: function(){ + return this.page.api(); + }, + + inject: function(el, position){ + this.el.inject(el, position); + return this.el; + }, + + toElement: function(){ + return this.el; + } +}) + +var Option = {} +Option.String = new Class({ + Extends: OptionBase, + + type: 'input', + + create: function(){ + var self = this + + self.el.adopt( + new Element('label', { + 'text': self.options.label + }), + self.input = new Element('input', { + 'name': self.postName(), + 'value': self.getSettingValue() + }) + ); + } +}); + +Option.Dropdown = new Class({ + Extends: OptionBase, + + create: function(){ + var self = this + + new Element('label', { + 'text': self.options.label + }).adopt( + self.input = new Element('select', { + 'name': self.postName() + }) + ).inject(self.el) + + Object.each(self.options.values, function(label, value){ + new Element('option', { + 'text': label, + 'value': value + }).inject(self.input) + }) + + self.input.set('value', self.getSettingValue()); + } +}); + +Option.Checkbox = new Class({ + Extends: OptionBase, + + type: 'checkbox', + + create: function(){ + var self = this; + + new Element('label', { + 'text': self.options.label + }).adopt( + self.input = new Element('input', { + 'type': 'checkbox', + 'value': self.getSettingValue(), + 'checked': self.getSettingValue() !== undefined + }) + ).inject(self.el); + } +}); + +Option.Bool = new Class({ + Extends: Option.Checkbox +}); + +Option.Int = new Class({ + Extends: Option.String +}); + +Option.Directory = new Class({ + + Extends: OptionBase, + + type: 'span', + browser: '', + save_on_change: false, + + create: function(){ + var self = this; + + + self.el.adopt( + new Element('label', { + 'text': self.options.label + }), + self.input = new Element('span', { + 'text': self.getSettingValue(), + 'events': { + 'click': self.showBrowser.bind(self), + 'outerClick': self.hideBrowser.bind(self) + } + }) + ); + + self.cached = {}; + }, + + showBrowser: function(){ + var self = this; + + if(!self.browser) + self.browser = new Element('div.directory_list').adopt( + self.dir_list = new Element('ul') + ).inject(self.input, 'after') + + self.getDirs() + self.browser.show() + }, + + hideBrowser: function(){ + this.browser.hide() + }, + + fillBrowser: function(json){ + var self = this; + + var c = self.getCurrentDir(); + var v = self.input.get('value'); + var add = true + + if(!json){ + json = self.cached[c]; + } + else { + self.cached[c] = json; + } + + self.dir_list.empty(); + json.dirs.each(function(dir){ + if(dir.indexOf(v) != -1){ + new Element('li', { + 'text': dir + }).inject(self.dir_list) + + if(add){ + self.input.insertAtCursor(dir.substring(v.length), true); + add = false + } + } + }) + }, + + getDirs: function(){ + var self = this; + + var c = self.getCurrentDir(); + + if(self.cached[c]){ + self.fillBrowser() + } + else { + self.api().request('directory.list', { + 'data': { + 'path': c + }, + 'onComplete': self.fillBrowser.bind(self) + }) + } + }, + + getCurrentDir: function(){ + var self = this; + + var v = self.input.get('value'); + var sep = self.api().getOption('path_sep'); + var dirs = v.split(sep); + dirs.pop(); + + return dirs.join(sep) + }, + + getValue: function(){ + var self = this; + return self.input.get('text'); + } +}); diff --git a/couchpotato/static/scripts/page/soon.js b/couchpotato/static/scripts/page/soon.js new file mode 100644 index 00000000..eeef446c --- /dev/null +++ b/couchpotato/static/scripts/page/soon.js @@ -0,0 +1,8 @@ +Page.Soon = new Class({ + + Extends: PageBase, + + name: 'soon', + title: 'Which wanted movies are released soon?' + +}) \ No newline at end of file diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js new file mode 100644 index 00000000..586a2771 --- /dev/null +++ b/couchpotato/static/scripts/page/wanted.js @@ -0,0 +1,73 @@ +Page.Wanted = new Class({ + + Extends: PageBase, + + name: 'wanted', + title: 'Gimmy gimmy gimmy!', + + movies: [], + + indexAction: function(param){ + var self = this; + + self.get() + }, + + list: function(){ + var self = this; + + if(!self.movie_container) + self.movie_container = new Element('div.movies').inject(self.el); + + self.movie_container.empty(); + Object.each(self.movies, function(info){ + var m = new Movie(self, {}, info); + $(m).inject(self.movie_container); + }); + }, + + get: function(status, onComplete){ + var self = this + + if(self.movies.length == 0) + self.api().request('movie', { + 'data': {}, + 'onComplete': function(json){ + self.store(json.movies); + self.list(); + } + }) + else + self.list() + }, + + store: function(movies){ + var self = this; + + self.movies = movies; + } + +}); + +var Movie = new Class({ + + Extends: BlockBase, + + initialize: function(self, options, data){ + var self = this; + + self.data = data; + + self.parent(self, options); + }, + + create: function(){ + var self = this; + + self.el = new Element('div.movie', { + 'text': self.data.name + }); + + } + +}) diff --git a/couchpotato/static/scripts/pages.js b/couchpotato/static/scripts/pages.js deleted file mode 100644 index 21920849..00000000 --- a/couchpotato/static/scripts/pages.js +++ /dev/null @@ -1,72 +0,0 @@ -var Page = {} - -Page.Movie = new Class({ - - Extends: PageBase, - - name: 'movie', - - initialize: function(options){ - - }, - - indexAction: function(param){ - var self = this - self.getMovies() - }, - - getMovies: function(){ - var self = this - - this.api().request('movie', {'status': 'wanted'}) - } - -}) - -Page.Manage = new Class({ - - Extends: PageBase, - - name: 'manage', - - initialize: function(options){ - - } - -}) - -Page.Soon = new Class({ - - Extends: PageBase, - - name: 'soon', - - initialize: function(options){ - - } - -}) - -Page.Config = new Class({ - - Extends: PageBase, - - name: 'config', - - initialize: function(options){ - - } - -}) - -Page.Log = new Class({ - - Extends: PageBase, - - name: 'log', - - initialize: function(options){ - - } - -}) \ No newline at end of file diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index 1331a814..85806665 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -96,7 +96,7 @@ form { } /*** Navigation ***/ -#header { +.header { background: #f7f7f7; padding:10px; margin-bottom: 20px; @@ -106,22 +106,16 @@ form { -webkit-box-shadow: 0 0 30px rgba(0,0,0,0.1); position: fixed; width: 99%; + z-index: 9999; } -#header .navigation { +.header .navigation { width: 960px; margin: 0 auto; - overflow:hidden; + overflow: hidden; } - #header .navigation ul { - float: left; - padding: 0; - margin: 0 0 0 6px; - width: 450px; - } - - #header .navigation ul li { + .header .navigation li { color: #8b8b8b; display: block; font-size:20px; @@ -131,31 +125,31 @@ form { float: left; } - #header .navigation ul li a { + .header .navigation li a { display: block; padding: 15px; } - #header .navigation ul li:first-child a { padding-left: 10px; } - #header .navigation ul li a.logLink { font-size: 13px; padding: 23px 20px 15px; } - #header .navigation ul li a#showConfig { + .header .navigation li:first-child a { padding-left: 10px; } + .header .navigation li a.logLink { font-size: 13px; padding: 23px 20px 15px; } + .header .navigation li a#showConfig { background: url('../../media/images/gear.png') no-repeat center; height: 35px; width: 10px; } - #header .navigation ul li span { + .header .navigation li span { display: block; margin-top: 5px; } - #header .navigation ul li.disabled { + .header .navigation li.disabled { color: #e5e5e5; } - #header .navigation ul li a:link, #header .navigation ul li a:visited { + .header .navigation li a:link, .header .navigation li a:visited { color: #2c2c2c; } - #header .navigation ul li a:hover, #header .navigation ul li a:active { + .header .navigation li a:hover, .header .navigation li a:active { color: #8b8b8b; } diff --git a/couchpotato/static/style/uniform.css b/couchpotato/static/style/uniform.css new file mode 100644 index 00000000..a64359cf --- /dev/null +++ b/couchpotato/static/style/uniform.css @@ -0,0 +1,154 @@ +/* ------------------------------------------------------------------------------ + + Copyright (c) 2010, Dragan Babic + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + ------------------------------------------------------------------------------ */ +/* ############################# GENERALS ################################### */ +/* ------------------------------------------------------------------------------ */ + +.uniForm{ margin: 0; padding: 0; position: relative; z-index: 1; } /* reset stuff */ + + /* Some generals and more resets */ + .uniForm fieldset{ border: none; margin: 0; padding: 0; } + .uniForm fieldset legend{ margin: 0; padding: 0; } + + /* This are the main units that contain form elements */ + .uniForm .ctrlHolder, + .uniForm .buttonHolder{ margin: 0; padding: 0; clear: both; } + + /* Clear all floats */ + .uniForm:after, + .uniForm .buttonHolder:after, + .uniForm .ctrlHolder:after, + .uniForm .ctrlHolder .multiField:after, + .uniForm .inlineLabel:after{ content: "."; display: block; height: 0; line-height: 0; font-size: 0; clear: both; min-height: 0; visibility: hidden; } + + .uniForm label, + .uniForm button{ cursor: pointer; } + +/* ------------------------------------------------------------------------------ */ +/* ########################## DEFAULT LAYOUT ################################ */ +/* ------------------------------------------------------------------------------ */ +/* Styles for form controls where labels are above the input elements */ +/* ------------------------------------------------------------------------------ */ + + .uniForm label, + .uniForm .label{ display: block; float: none; margin: 0 0 .5em 0; padding: 0; line-height: 100%; width: auto; } + + /* Float the input elements */ + .uniForm .textInput, + .uniForm .fileUpload, + .uniForm .selectInput, + .uniForm select, + .uniForm textarea{ float: left; width: 53%; margin: 0; } + + /* Postition the hints */ + .uniForm .formHint{ float: right; width: 43%; margin: 0; clear: none; } + + /* Position the elements inside combo boxes (multiple inputs/selects/checkboxes/radio buttons per unit) */ + .uniForm ul{ float: left; width: 53%; margin: 0; padding: 0; } + .uniForm ul li{ margin: 0 0 .5em 0; list-style: none; } + .uniForm ul li label{ margin: 0; float: none; display: block; overflow: visible; } + /* Alternate layout */ + .uniForm ul.alternate li{ float: left; width: 30%; margin-right: 3%; } + .uniForm ul.alternate li label{ float: none; display: block; width: 98%; } + .uniForm ul .textInput, + .uniForm ul .selectInput, + .uniForm ul select, + .uniForm ul.alternate .textInput, + .uniForm ul.alternate .selectInput, + .uniForm ul.alternate select{ width: 98%; margin-top: .5em; display: block; float: none; } + + /* Required fields asterisk styling */ + .uniForm label em, + .uniForm .label em{ float: left; width: 1em; margin: 0 0 0 -1em; } + +/* ------------------------------------------------------------------------------ */ +/* ######################### ALTERNATE LAYOUT ############################### */ +/* ------------------------------------------------------------------------------ */ +/* Styles for form controls where labels are in line with the input elements */ +/* Set the class of the parent (preferably to a fieldset) to .inlineLabels */ +/* ------------------------------------------------------------------------------ */ + + .uniForm .inlineLabels label, + .uniForm .inlineLabels .label{ float: left; margin: .3em 2% 0 0; padding: 0; line-height: 1; position: relative; width: 32%; } + + /* Float the input elements */ + .uniForm .inlineLabels .textInput, + .uniForm .inlineLabels .fileUpload, + .uniForm .inlineLabels .selectInput, + .uniForm .inlineLabels select, + .uniForm .inlineLabels textarea{ float: left; width: 64%; } + + /* Postition the hints */ + .uniForm .inlineLabels .formHint{ clear: both; float: none; width: auto; margin-left: 34%; position: static; } + + /* Position the elements inside combo boxes (multiple inputs/selects/checkboxes/radio buttons per unit) */ + .uniForm .inlineLabels ul{ float: left; width: 66%; } + .uniForm .inlineLabels ul li{ margin: .5em 0; } + .uniForm .inlineLabels ul li label{ float: none; display: block; width: 100%; } + /* Alternate layout */ + .uniForm .inlineLabels ul.alternate li{ margin-right: 3%; margin-top: .25em; } + .uniForm .inlineLabels ul li label .textInput, + .uniForm .inlineLabels ul li label textarea, + .uniForm .inlineLabels ul li label select{ float: none; display: block; width: 98%; } + + /* Required fields asterisk styling */ + .uniForm .inlineLabels label em, + .uniForm .inlineLabels .label em{ display: block; float: none; margin: 0; position: absolute; right: 0; } + +/* ----------------------------------------------------------------------------- */ +/* ########################### Additional Stuff ################################ */ +/* ----------------------------------------------------------------------------- */ + + /* Generals */ + .uniForm legend{ color: inherit; } + + .uniForm .secondaryAction{ float: left; } + + /* .inlineLabel is used for inputs within labels - checkboxes and radio buttons */ + .uniForm .inlineLabel input, + .uniForm .inlineLabels .inlineLabel input, + .uniForm .blockLabels .inlineLabel input, + /* class .inlineLabel is depreciated */ + .uniForm label input{ float: none; display: inline; margin: 0; padding: 0; border: none; } + + .uniForm .buttonHolder .inlineLabel, + .uniForm .buttonHolder label{ float: left; margin: .5em 0 0 0; width: auto; max-width: 60%; text-align: left; } + + /* When you don't want to use a label */ + .uniForm .inlineLabels .noLabel ul{ margin-left: 34%; /* Match to width of label + gap to field */ } + + /* Classes for control of the widths of the fields */ + .uniForm .small { width: 30% !important; } + .uniForm .medium{ width: 45% !important; } + .uniForm .large { } /* Large is default and should match the value you set for .textInput, textarea or select */ + .uniForm .auto { width: auto !important; } + .uniForm .small, + .uniForm .medium, + .uniForm .auto{ margin-right: 4px; } + +/* Columns */ +.uniForm .col{ float: left; } +.uniForm .col{ width: 50%; } \ No newline at end of file diff --git a/couchpotato/static/style/uniform.generic.css b/couchpotato/static/style/uniform.generic.css new file mode 100644 index 00000000..179ab3a7 --- /dev/null +++ b/couchpotato/static/style/uniform.generic.css @@ -0,0 +1,153 @@ +/* ------------------------------------------------------------------------------ + + UNI-FORM DEFAULT by DRAGAN BABIC (v2) | Wed, 31 Mar 10 + + ------------------------------------------------------------------------------ + + Copyright (c) 2010, Dragan Babic + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + ------------------------------------------------------------------------------ */ + +.uniForm{} + + .uniForm legend{ font-weight: bold; font-size: 100%; margin: 0; padding: 1.5em 0; } + + .uniForm .ctrlHolder{ padding: 1em; border-bottom: 1px solid #efefef; } + .uniForm .ctrlHolder.focused{ background: #fffcdf; } + + .uniForm .inlineLabels .noLabel{} + + .uniForm .buttonHolder{ background: #efefef; text-align: right; margin: 1.5em 0 0 0; padding: 1.5em; + /* CSS3 */ + border-radius: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -o-border-radius: 4px; + -khtml-border-radius: 4px; + } + .uniForm .buttonHolder .primaryAction{ padding: 10px 22px; line-height: 1; background: #254a86; border: 1px solid #163362; font-size: 12px; font-weight: bold; color: #fff; + /* CSS3 */ + border-radius: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -o-border-radius: 4px; + -khtml-border-radius: 4px; + box-shadow: 1px 1px 0 #fff; + -webkit-box-shadow: 1px 1px 0 #fff; + -moz-box-shadow: 1px 1px 0 #fff; + text-shadow: -1px -1px 0 rgba(0,0,0,.25); + } + .uniForm .buttonHolder .primaryAction:active{ position: relative; top: 1px; } + .uniForm .secondaryAction { text-align: left; } + .uniForm button.secondaryAction { background: transparent; border: none; color: #777; margin: 1.25em 0 0 0; padding: 0; } + + .uniForm .inlineLabels label em, + .uniForm .inlineLabels .label em{ font-style: normal; font-weight: bold; } + .uniForm label small{ font-size: .75em; color: #777; } + + .uniForm .textInput, + .uniForm textarea { padding: 4px 2px; border: 1px solid #aaa; background: #fff; } + .uniForm textarea { height: 12em; } + .uniForm select {} + .uniForm .fileUpload {} + + .uniForm ul{} + .uniForm li{} + .uniForm ul li label{ font-size: .85em; } + + .uniForm .small {} + .uniForm .medium{} + .uniForm .large {} /* Large is default and should match the value you set for .textInput, textarea or select */ + .uniForm .auto {} + .uniForm .small, + .uniForm .medium, + .uniForm .auto{} + + /* Get rid of the 'glow' effect in WebKit, optional */ + .uniForm .ctrlHolder .textInput:focus, + .uniForm .ctrlHolder textarea:focus{ outline: none; } + + .uniForm .formHint { font-size: .85em; color: #777; } + .uniForm .inlineLabels .formHint { padding-top: .5em; } + .uniForm .ctrlHolder.focused .formHint{ color: #333; } + +/* ----------------------------------------------------------------------------- */ +/* ############################### Messages #################################### */ +/* ----------------------------------------------------------------------------- */ + + /* Error message at the top of the form */ + .uniForm #errorMsg{ background: #ffdfdf; border: 1px solid #f3afb5; margin: 0 0 1.5em 0; padding: 0 1.5em; + /* CSS3 */ + border-radius: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -o-border-radius: 4px; + -khtml-border-radius: 4px; + } + .uniForm #errorMsg h3{} /* Feel free to use a heading level suitable to your page structure */ + .uniForm #errorMsg ol{ margin: 0 0 1.5em 0; padding: 0; } + .uniForm #errorMsg ol li{ margin: 0 0 3px 1.5em; padding: 7px; background: #f6bec1; position: relative; font-size: .85em; + /* CSS3 */ + border-radius: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -o-border-radius: 4px; + -khtml-border-radius: 4px; + } + + .uniForm .ctrlHolder.error, + .uniForm .ctrlHolder.focused.error{ background: #ffdfdf; border: 1px solid #f3afb5; + /* CSS3 */ + border-radius: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -o-border-radius: 4px; + -khtml-border-radius: 4px; + } + .uniForm .ctrlHolder.error input.error, + .uniForm .ctrlHolder.error select.error, + .uniForm .ctrlHolder.error textarea.error{ color: #af4c4c; margin: 0 0 6px 0; padding: 4px; } + + /* Success messages at the top of the form */ + .uniForm #okMsg{ background: #c8ffbf; border: 1px solid #a2ef95; margin: 0 0 1.5em 0; padding: 0 1.5em; text-align: center; + /* CSS3 */ + border-radius: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + -o-border-radius: 4px; + -khtml-border-radius: 4px; + } + .uniForm #OKMsg p{ margin: 0; } + +/* ----------------------------------------------------------------------------- */ +/* ############################### Columns ##################################### */ +/* ----------------------------------------------------------------------------- */ + + .uniForm .col{} + .uniForm .col.first{} + .uniForm .col.last{} + .uniForm .col{ margin-bottom: 1.5em; } + /* Use .first and .last classes to control the layout/spacing of your columns */ + .uniForm .col.first{ width: 49%; float: left; clear: none; } + .uniForm .col.last { width: 49%; float: right; clear: none; margin-right: 0; } \ No newline at end of file diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html index b4a313ee..dc40b6ee 100644 --- a/couchpotato/templates/_desktop.html +++ b/couchpotato/templates/_desktop.html @@ -2,18 +2,35 @@
+ + - + + + + + + + + + + + + +