diff --git a/AUTHORS b/AUTHORS index 7bf3727e..1ad8c70a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -2,12 +2,9 @@ Maraschino was created and is maintained by Bradley Abrahams: * Bradley Abrahams -An extra-special thanks to Gustavo Hoirisch for helping to manage the repository: +An extra-special thanks to the following people for helping to manage the repository, and keeping the updates and bug-fixes coming: * Gustavo Hoirisch - -And for David Gray for going above and beyond to create many awesome, much-used features: - * David Gray And of course thank you to everyone else who has contributed: diff --git a/Maraschino.py b/Maraschino.py index b4fa0949..cd7bdd65 100644 --- a/Maraschino.py +++ b/Maraschino.py @@ -28,6 +28,7 @@ def import_modules(): import modules.applications import modules.controls + import modules.couchpotato import modules.currently_playing import modules.diskspace import modules.index diff --git a/maraschino/modules.py b/maraschino/modules.py index 95c01df0..68de5956 100644 --- a/maraschino/modules.py +++ b/maraschino/modules.py @@ -3,7 +3,7 @@ except ImportError: import simplejson as json -from flask import Flask, jsonify, render_template, request +from flask import jsonify, render_template, request from maraschino.database import db_session import maraschino @@ -39,6 +39,53 @@ }, ] }, + { + 'name': 'couchpotato', + 'label': 'CouchPotato Manager', + 'description': 'Manage CouchPotato from within Maraschino', + 'static': True, + 'poll': 0, + 'delay': 0, + 'settings': [ + { + 'key': 'couchpotato_api', + 'value': '', + 'description': 'CouchPotato API Key', + }, + { + 'key': 'couchpotato_user', + 'value': '', + 'description': 'CouchPotato Username', + }, + { + 'key': 'couchpotato_password', + 'value': '', + 'description': 'CouchPotato Password', + }, + { + 'key': 'couchpotato_ip', + 'value': '', + 'description': 'CouchPotato Hostname', + }, + { + 'key': 'couchpotato_port', + 'value': '', + 'description': 'CouchPotato Port', + }, + { + 'key': 'couchpotato_https', + 'value': '0', + 'description': 'Use HTTPS', + 'type': 'bool', + }, + { + 'key': 'couchpotato_compact', + 'value': '0', + 'description': 'Compact view', + 'type': 'bool', + }, + ] + }, { 'name': 'diskspace', 'label': 'Disk space', diff --git a/maraschino/tools.py b/maraschino/tools.py index 304740e8..dbf55082 100644 --- a/maraschino/tools.py +++ b/maraschino/tools.py @@ -133,6 +133,14 @@ def xbmc_image(url): FILTERS['xbmc_image'] = xbmc_image + +def epochTime(seconds): + import time + return time.ctime(seconds) + +FILTERS['time'] = epochTime + + @app.route('/xhr/xbmc_image//') def xbmc_proxy(version, url): from maraschino.noneditable import server_address diff --git a/modules/controls.py b/modules/controls.py index a5bb6384..4d6ffe5e 100644 --- a/modules/controls.py +++ b/modules/controls.py @@ -281,14 +281,14 @@ def xhr_enqueue_file(file_type): return jsonify({ 'success': True }) -@app.route('/xhr/playlist//play/') +@app.route('/xhr/playlist//play/') @requires_auth -def xhr_playlist_play(playlistid, position): +def xhr_playlist_play(playerid, position): logger.log('CONTROLS :: playing playlist position %i' % position, 'INFO') xbmc = jsonrpclib.Server(server_api_address()) try: - xbmc.Player.Open({'playlistid': playlistid, 'position': position}) + xbmc.Player.GoTo(playerid=playerid, position=position) return jsonify({'success': True}) except: diff --git a/modules/couchpotato.py b/modules/couchpotato.py new file mode 100644 index 00000000..7297a11e --- /dev/null +++ b/modules/couchpotato.py @@ -0,0 +1,364 @@ +from flask import render_template, request, jsonify, json, send_file +from jinja2.filters import FILTERS +from maraschino.tools import get_setting_value, requires_auth +from maraschino import logger, app, WEBROOT +import urllib +import StringIO + + +def login_string(): + try: + login = '%s:%s@' % (get_setting_value('couchpotato_user'), get_setting_value('couchpotato_password')) + + except: + login = '' + + return login + + +def couchpotato_url(): + return 'http://%s%s:%s/api/%s' % (login_string(), get_setting_value('couchpotato_ip'), get_setting_value('couchpotato_port'), get_setting_value('couchpotato_api')) + + +def couchpotato_url_no_api(): + return 'http://%s%s:%s/' % (login_string(), get_setting_value('couchpotato_ip'), get_setting_value('couchpotato_port')) + + +def couchpotato_api(method, params=None, use_json=True, dev=False): + if params: + params = '/?%s' % params + else: + params = '/' + + url = '%s/%s%s' % (couchpotato_url(), method, params) + data = urllib.urlopen(url).read() + if dev: + print url + print data + if use_json: + data = json.JSONDecoder().decode(data) + return data + + +def log_exception(e): + logger.log('CouchPotato :: EXCEPTION -- %s' % e, 'DEBUG') + + +def couchpotato_image(path): + if path.startswith('/'): + path = path[1:] + return '%s/xhr/couchpotato/image/%s' % (WEBROOT, path) + + +FILTERS['cp_img'] = couchpotato_image + + +@app.route('/xhr/couchpotato/image/') +def couchpotato_proxy(url): + url = '%s/file.cache/%s' % (couchpotato_url(), url) + img = StringIO.StringIO(urllib.urlopen(url).read()) + return send_file(img, mimetype='image/jpeg') + + +@app.route('/xhr/couchpotato/') +@app.route('/xhr/couchpotato//') +def xhr_couchpotato(status=False): + if status: + status_string = 'status=%s' % status + template = 'couchpotato-all.html' + else: + status = 'wanted' + status_string = False + template = 'couchpotato.html' + try: + logger.log('CouchPotato :: Fetching "%s movies" list' % status, 'INFO') + couchpotato = couchpotato_api('movie.list', params=status_string) + if couchpotato['success'] and not couchpotato['empty']: + couchpotato = couchpotato['movies'] + + except Exception as e: + log_exception(e) + couchpotato = None + + logger.log('CouchPotato :: Fetching "%s movies" list (DONE)' % status, 'INFO') + return render_template(template, + url=couchpotato_url(), + couchpotato=couchpotato, + compact_view=get_setting_value('couchpotato_compact') == '1', + ) + + +@app.route('/xhr/couchpotato/search/') +def cp_search(): + couchpotato = {} + params = False + profiles = {} + + try: + params = 'q=' + request.args['name'] + except: + pass + + if params: + try: + logger.log('CouchPotato :: Searching for movie: %s' % (params), 'INFO') + couchpotato = couchpotato_api('movie.search', params=params) + amount = len(couchpotato['movies']) + logger.log('CouchPotato :: found %i movies for %s' % (amount, params), 'INFO') + if couchpotato['success'] and amount != 0: + couchpotato = couchpotato['movies'] + try: + logger.log('CouchPotato :: Getting quality profiles', 'INFO') + profiles = couchpotato_api('profile.list') + except Exception as e: + log_exception(e) + else: + return render_template('couchpotato-search.html', error='No movies with "%s" were found' % (params[2:]), couchpotato='results') + + except Exception as e: + log_exception(e) + couchpotato = None + + else: + logger.log('CouchPotato :: Loading search template', 'DEBUG') + couchpotato = None + + return render_template('couchpotato-search.html', + data=couchpotato, + couchpotato='results', + profiles=profiles, + ) + + +@app.route('/xhr/couchpotato/add_movie///') +@app.route('/xhr/couchpotato/add_movie/<imdbid>/<title>/<profile>/') +def add_movie(imdbid, title, profile=False): + if profile: + params = 'identifier=%s&title=%s&profile_id=%s' % (imdbid, title, profile) + else: + params = 'identifier=%s&title=%s' % (imdbid, title) + + try: + logger.log('CouchPotato :: Adding %s (%s) to wanted list' % (title, imdbid), 'INFO') + result = couchpotato_api('movie.add', params) + return jsonify(result) + except Exception as e: + log_exception(e) + + return jsonify({'success': False}) + + +@app.route('/xhr/couchpotato/restart/') +@requires_auth +def cp_restart(): + try: + logger.log('CouchPotato :: Restarting', 'INFO') + result = couchpotato_api('app.restart', use_json=False) + if 'restarting' in result: + return jsonify({'success': True}) + except Exception as e: + log_exception(e) + + return jsonify({'success': False}) + + +@app.route('/xhr/couchpotato/available/') +@requires_auth +def cp_available(): + try: + logger.log('CouchPotato :: Checking if CouchPotato is available', 'INFO') + result = couchpotato_api('app.available') + return jsonify(result) + except Exception as e: + log_exception(e) + + return jsonify({'success': False}) + + +@app.route('/xhr/couchpotato/shutdown/') +@requires_auth +def cp_shutdown(): + try: + logger.log('CouchPotato :: Shutting down', 'INFO') + result = couchpotato_api('app.shutdown', use_json=False) + if 'shutdown' in result: + return jsonify({'success': True}) + except Exception as e: + log_exception(e) + + return jsonify({'success': False}) + + +@app.route('/xhr/couchpotato/version/') +@requires_auth +def cp_version(): + try: + result = couchpotato_api('app.version') + return jsonify(result) + except Exception as e: + log_exception(e) + + return jsonify({'success': False}) + + +@app.route('/xhr/couchpotato/profiles/') +@requires_auth +def cp_profiles(): + try: + logger.log('CouchPotato :: Getting profiles', 'INFO') + result = couchpotato_api('profile.list') + return jsonify(result) + except Exception as e: + log_exception(e) + + return jsonify({'success': False}) + + +@app.route('/xhr/couchpotato/quality/') +@requires_auth +def cp_quality(): + try: + logger.log('CouchPotato :: Getting quality', 'INFO') + result = couchpotato_api('quality.list') + return jsonify(result) + except Exception as e: + log_exception(e) + + return jsonify({'success': False}) + + +@app.route('/xhr/couchpotato/update/check/') +@requires_auth +def cp_update_check(): + try: + logger.log('CouchPotato :: Getting update', 'INFO') + result = couchpotato_api('updater.check') + return jsonify(result) + except Exception as e: + log_exception(e) + + return jsonify({'success': False}) + + +@app.route('/xhr/couchpotato/delete_movie/<id>/') +@requires_auth +def movie_delete(id): + """ + Delete a movie from list + ----- Params ----- + id int (comma separated) Movie ID(s) you want to delete. + delete_from string: all (default), wanted, manage Delete movie from this page + """ + try: + logger.log('CouchPotato :: Deleting movie %s' % id, 'INFO') + result = couchpotato_api('movie.delete', 'id=%s' % id) + return jsonify(result) + except Exception as e: + log_exception(e) + + return jsonify({'success': False}) + + +@app.route('/xhr/couchpotato/refresh_movie/<id>/') +def movie_refresh(id): + """ + Refresh a movie from list + ----- Params ----- + id int (comma separated) Movie ID(s) you want to refresh. + """ + try: + logger.log('CouchPotato :: Refreshing movie %s' % id, 'INFO') + result = couchpotato_api('movie.refresh', 'id=%s' % id) + return jsonify(result) + except Exception as e: + log_exception(e) + + return jsonify({'success': False}) + + +@app.route('/xhr/couchpotato/settings/') +def cp_settings(): + """ + Retrieve settings from CP + """ + try: + logger.log('CouchPotato :: Retrieving settings', 'INFO') + result = couchpotato_api('settings') + logger.log('CouchPotato :: Retrieving settings (DONE)', 'INFO') + return render_template('couchpotato-settings.html', + couchpotato=result, + ) + except Exception as e: + log_exception(e) + + return jsonify({'success': False}) + + +@app.route('/xhr/couchpotato/get_movie/<id>/') +def cp_get_movie(id): + """ + Retrieve movie from CP + ---- Params ----- + id int (comma separated) The id of the movie + """ + try: + logger.log('CouchPotato :: Retrieving movie info', 'INFO') + result = couchpotato_api('movie.get', 'id=%s' % id) + try: + logger.log('CouchPotato :: Getting quality profiles', 'INFO') + profiles = couchpotato_api('profile.list') + except Exception as e: + log_exception(e) + logger.log('CouchPotato :: Retrieving movie info (DONE)', 'INFO') + return render_template('couchpotato-info.html', + couchpotato=result, + profiles=profiles, + ) + except Exception as e: + log_exception(e) + + return jsonify({'success': False}) + + +@app.route('/xhr/couchpotato/edit_movie/<movieid>/<profileid>/') +def cp_edit_movie(movieid, profileid): + """ + Edit movie in CP + ---- Params ----- + movieid int (comma separated) The id of the movie + profileid int Id of the profile to go to + """ + try: + logger.log('CouchPotato :: Retrieving movie info', 'INFO') + result = couchpotato_api('movie.edit', 'id=%s&profile_id=%s' % (movieid, profileid)) + if result['success']: + logger.log('CouchPotato :: Retrieving movie info (DONE)', 'INFO') + return jsonify({'success': True}) + except Exception as e: + log_exception(e) + + return jsonify({'success': False}) + + +@app.route('/xhr/couchpotato/log/') +@app.route('/xhr/couchpotato/log/<type>/<lines>/') +def cp_log(type='all', lines=30): + """ + Edit movie in CP + ---- Params ----- + type <optional> all, error, info, debug Type of log + lines <optional> int Number of lines - last to first + """ + try: + logger.log('CouchPotato :: Retrieving "%s" log' % type, 'INFO') + result = couchpotato_api('logging.partial', 'type=%s&lines=%s' % (type, lines)) + if result['success']: + logger.log('CouchPotato :: Retrieving "%s" log (DONE)' % type, 'INFO') + return render_template('couchpotato-log.html', + couchpotato=result, + level=type, + ) + except Exception as e: + log_exception(e) + + return jsonify({'success': False}) diff --git a/static/images/downloaded-banner.png b/static/images/downloaded-banner.png new file mode 100644 index 00000000..2085a251 Binary files /dev/null and b/static/images/downloaded-banner.png differ diff --git a/static/images/imdb.png b/static/images/imdb.png new file mode 100644 index 00000000..72fa3408 Binary files /dev/null and b/static/images/imdb.png differ diff --git a/static/images/poster.png b/static/images/poster.png new file mode 100644 index 00000000..34ba2758 Binary files /dev/null and b/static/images/poster.png differ diff --git a/static/images/snatched-banner.png b/static/images/snatched-banner.png new file mode 100644 index 00000000..92c6aa86 Binary files /dev/null and b/static/images/snatched-banner.png differ diff --git a/static/images/wanted-banner.png b/static/images/wanted-banner.png new file mode 100644 index 00000000..a0dbcd8e Binary files /dev/null and b/static/images/wanted-banner.png differ diff --git a/static/js/index.js b/static/js/index.js old mode 100644 new mode 100755 index e66e7f70..cbe6bb1c --- a/static/js/index.js +++ b/static/js/index.js @@ -1299,6 +1299,229 @@ $(document).ready(function() { /********* END SABNZBD ***********/ + /********* CouchPotato **********/ + // menu wanted click + $(document).on('click', '#couchpotato .menu .wanted', function(){ + $.get(WEBROOT + '/xhr/couchpotato/') + .success(function(data){ + $('#couchpotato').replaceWith(data); + }); + }); + + // menu '+'' click + $(document).on('click', '#couchpotato .menu .all', function(){ + $.get(WEBROOT + '/xhr/couchpotato/done/') + .success(function(data){ + $('#couchpotato').replaceWith(data); + }); + }); + + // menu settings click + $(document).on('click', '#couchpotato .menu .settings', function(){ + $.get(WEBROOT + '/xhr/couchpotato/settings/') + .success(function(data){ + $('#couchpotato').replaceWith(data); + }); + }); + + // Load search template + $(document).on('click', '#couchpotato .menu .add', function(){ + $.get(WEBROOT + '/xhr/couchpotato/search/') + .success(function(data){ + $('#couchpotato').replaceWith(data); + }) + .error(function(){ + popup_message('Could not reach Maraschino.'); + }); + }); + + // Load search results + // on enter + $(document).on('keypress', '#couchpotato .search .value', function(e){ + if(e.which == 13){ + e.preventDefault(); + var name = $('#couchpotato .search .value').attr('value'); + params = ''; + if(name !== ''){ + params = 'name='+encodeURIComponent(name); + } + $('#couchpotato .search span.search').text(''); + add_loading_gif($('#couchpotato .search span.search')); + $.get(WEBROOT + '/xhr/couchpotato/search/?'+params) + .success(function(data){ + $('#couchpotato').replaceWith(data); + }) + .error(function(){ + popup_message('Could not reach Maraschino.'); + }); + } + }); + // Load search results + // on button click + $(document).on('click', '#couchpotato .search span.search', function() { + var name = $('#couchpotato .search .value').attr('value'); + params = ''; + if(name !== ''){ + params = 'name='+encodeURIComponent(name); + } + $(this).text(''); + add_loading_gif($(this)); + $.get(WEBROOT + '/xhr/couchpotato/search/?'+params) + .success(function(data){ + $('#couchpotato').replaceWith(data); + }) + .error(function(){ + popup_message('Could not reach Maraschino.'); + }); + }); + // Search add movie click + $(document).on('click', '#couchpotato .search ul li .choices .add', function() { + var imdbid = $(this).parent().parent().data('imdbid'); + var title = $(this).parent().parent().data('title').replace('/','%20'); + var profile = $('#couchpotato .search ul li .choices .profiles').find(':selected').val(); + $.get(WEBROOT + '/xhr/couchpotato/add_movie/'+imdbid+'/'+encodeURIComponent(title)+'/'+profile, function(data) { + if(data.success){ + popup_message('Movie added successfully'); + } else { + popup_message('Failed to add movie to CouchPotato'); + } + }); + }); + // wanted slide option + $(document).on('click', '#couchpotato #cp_content .movie .image', function(e) { + e.stopPropagation(); + var id = $(this).parent().attr('id'); + var el = $('#'+id); + el.toggleClass('selected'); + if(el.hasClass('selected')){ + el.transition({x: '30px', opacity: 0.7}, function(){ + $('#couchpotato #cp_content .options&.'+id).transition({opacity: 1}); + }); + } else { + $('#couchpotato #cp_content .options&.'+id).transition({opacity: 0}, function(){ + el.transition({opacity: 1, x: '0px'}); + }); + } + }); + // wanted delete, info delete + $(document).on('click', '#couchpotato #cp_content .options img.delete, #couchpotato #info .options img.delete', function(selector) { + var id = $(this).parent().data('cpid'); + var imdbid = $(this).parent().data('imdbid'); + var el = $(this); + el.attr('src', WEBROOT + '/static/images/xhrloading.gif'); + $.get(WEBROOT+'/xhr/couchpotato/delete_movie/'+id, function(data) { + if(data.success){ + if(el.parent().parent().attr('id') === 'info'){ + el.attr('src', WEBROOT + '/static/images/yes.png'); + } else { + $('#couchpotato #cp_content #'+imdbid).transition({opacity: 0, duration: 1000}, function(){ + $(this).remove(); + }); + } + } else { + popup_message('Failed to delete movie, see log for more datials'); + $('#couchpotato #cp_content #'+imdbid).transition({opacity: 1, x: '0px'}); + } + }); + }); + // wanted refresh, info refresh + $(document).on('click', '#couchpotato #cp_content .options img.search, , #couchpotato #info .options img.search', function() { + var id = $(this).parent().data('cpid'); + var imdbid = $(this).parent().data('imdbid'); + var el = $(this); + el.attr('src', WEBROOT + '/static/images/xhrloading.gif'); + $.get(WEBROOT+'/xhr/couchpotato/refresh_movie/'+id, function(data) { + if(data.success){ + if(el.parent().parent().attr('id') === 'info'){ + el.attr('src', WEBROOT + '/static/images/yes.png'); + } else { + el.attr('src', WEBROOT + '/static/images/search.png'); + } + } else { + popup_message('Failed to refresh movie, see log for more datials'); + } + }); + }); + // movie info + $(document).on('click', '#couchpotato .movie', function() { + var id = $(this).data('cpid'); + add_loading_gif($(this)); + $.get(WEBROOT + '/xhr/couchpotato/get_movie/'+id, function(data) { + $('#couchpotato').replaceWith(data); + }); + }); + // movie info change profile + $(document).on('change', '#couchpotato #info td.profile select.profiles', function() { + var movieid = $(this).data('id'); + var profileid = $(this).find(':selected').val(); + var td = $(this).parent(); + add_loading_gif(td); + $.get(WEBROOT + '/xhr/couchpotato/edit_movie/'+movieid+'/'+profileid+'/', function(data){ + if(data.success){ + remove_loading_gif(td); + } else { + popup_message('Failed to get qulaity profiles from CouchPotato'); + } + }); + }); + // img popup + $(document).on('click', '#couchpotato #info .thumbs img', function() { + var popup = $('<div id="cp_image" class="dialog" align="center"><div class="close">x</div><img src ="'+$(this).attr('src')+'" style="max-height: 100%;max-width:100%;" /></div>'); + $('body').append(popup); + popup.showPopup({ dispose: true }); + $(document).on('keydown', 'body', function() { + $('#cp_image .close').click(); + $(document).off('keydown', 'body'); + }); + }); + // shutdown + $(document).on('click', '#couchpotato div.powerholder a.power', function() { + $('#couchpotato div.powerholder a.power img').attr('src', WEBROOT + '/static/images/yes.png'); + $.get(WEBROOT + '/xhr/couchpotato/shutdown/', function(data) { + console.log(data); + if(data.success){ + $('#couchpotato div.powerholder a.power img').attr('src', WEBROOT + '/static/images/yes.png'); + var x = setInterval(function(){ + $('#couchpotato').remove(); + clearInterval(x); + }, 3000); + } + }); + }); + // restart + $(document).on('click', '#couchpotato div.powerholder a.restart', function() { + $('#couchpotato div.powerholder a.restart img').attr('src', WEBROOT + '/static/images/yes.png'); + $.get(WEBROOT + '/xhr/couchpotato/restart/', function(data) { + console.log(data); + if(data.success){ + $('#couchpotato div.powerholder a.restart img').attr('src', WEBROOT + '/static/images/yes.png'); + var x = setInterval(function(){ + $.get(WEBROOT + '/xhr/couchpotato/') + .success(function(data){ + $('#couchpotato').replaceWith(data); + clearInterval(x); + }); + }, 3000); + } + }); + }); + // log + $(document).on('click', '#couchpotato div.powerholder a.log', function() { + $.get(WEBROOT + '/xhr/couchpotato/log/', function(data){ + if(data){ + $('#couchpotato').replaceWith(data); + } + }); + }); + // log level change + $(document).on('change', '#couchpotato #cp_log select.level', function() { + $.get(WEBROOT + '/xhr/couchpotato/log/' + $(this).find(':selected').val() + '/30/', function(data) { + $('#couchpotato').replaceWith(data); + }); + + }); + /********* END CouchPotato ***********/ + /********* SEARCH ***********/ $('#activate_search').live('click', function (e) { diff --git a/static/js/lib/jquery.transit.js b/static/js/lib/jquery.transit.js new file mode 100644 index 00000000..6f810b1c --- /dev/null +++ b/static/js/lib/jquery.transit.js @@ -0,0 +1,644 @@ +/*! + * jQuery Transit - CSS3 transitions and transformations + * Copyright(c) 2011 Rico Sta. Cruz <rico@ricostacruz.com> + * MIT Licensed. + * + * http://ricostacruz.com/jquery.transit + * http://github.com/rstacruz/jquery.transit + */ + +(function($) { + "use strict"; + + $.transit = { + version: "0.1.3", + + // Map of $.css() keys to values for 'transitionProperty'. + // See https://developer.mozilla.org/en/CSS/CSS_transitions#Properties_that_can_be_animated + propertyMap: { + marginLeft : 'margin', + marginRight : 'margin', + marginBottom : 'margin', + marginTop : 'margin', + paddingLeft : 'padding', + paddingRight : 'padding', + paddingBottom : 'padding', + paddingTop : 'padding' + }, + + // Will simply transition "instantly" if false + enabled: true, + + // Set this to false if you don't want to use the transition end property. + useTransitionEnd: false + }; + + var div = document.createElement('div'); + var support = {}; + + // Helper function to get the proper vendor property name. + // (`transition` => `WebkitTransition`) + function getVendorPropertyName(prop) { + var prefixes = ['Moz', 'Webkit', 'O', 'ms']; + var prop_ = prop.charAt(0).toUpperCase() + prop.substr(1); + + if (prop in div.style) { return prop; } + + for (var i=0; i<prefixes.length; ++i) { + var vendorProp = prefixes[i] + prop_; + if (vendorProp in div.style) { return vendorProp; } + } + } + + // Helper function to check if transform3D is supported. + // Should return true for Webkits and Firefox 10+. + function checkTransform3dSupport() { + div.style[support.transform] = ''; + div.style[support.transform] = 'rotateY(90deg)'; + return div.style[support.transform] !== ''; + } + + var isChrome = navigator.userAgent.toLowerCase().indexOf('chrome') > -1; + + // Check for the browser's transitions support. + // You can access this in jQuery's `$.support.transition`. + // As per [jQuery's cssHooks documentation](http://api.jquery.com/jQuery.cssHooks/), + // we set $.support.transition to a string of the actual property name used. + support.transition = getVendorPropertyName('transition'); + support.transitionDelay = getVendorPropertyName('transitionDelay'); + support.transform = getVendorPropertyName('transform'); + support.transformOrigin = getVendorPropertyName('transformOrigin'); + support.transform3d = checkTransform3dSupport(); + + $.extend($.support, support); + + var eventNames = { + 'MozTransition': 'transitionend', + 'OTransition': 'oTransitionEnd', + 'WebkitTransition': 'webkitTransitionEnd', + 'msTransition': 'MSTransitionEnd' + }; + + // Detect the 'transitionend' event needed. + var transitionEnd = support.transitionEnd = eventNames[support.transition] || null; + + // Avoid memory leak in IE. + div = null; + + // ## $.cssEase + // List of easing aliases that you can use with `$.fn.transition`. + $.cssEase = { + '_default': 'ease', + 'in': 'ease-in', + 'out': 'ease-out', + 'in-out': 'ease-in-out', + 'snap': 'cubic-bezier(0,1,.5,1)' + }; + + // ## 'transform' CSS hook + // Allows you to use the `transform` property in CSS. + // + // $("#hello").css({ transform: "rotate(90deg)" }); + // + // $("#hello").css('transform'); + // //=> { rotate: '90deg' } + // + $.cssHooks.transform = { + // The getter returns a `Transform` object. + get: function(elem) { + return $(elem).data('transform'); + }, + + // The setter accepts a `Transform` object or a string. + set: function(elem, v) { + var value = v; + + if (!(value instanceof Transform)) { + value = new Transform(value); + } + + // We've seen the 3D version of Scale() not work in Chrome when the + // element being scaled extends outside of the viewport. Thus, we're + // forcing Chrome to not use the 3d transforms as well. Not sure if + // translate is affectede, but not risking it. Detection code from + // http://davidwalsh.name/detecting-google-chrome-javascript + if (support.transform === 'WebkitTransform' && !isChrome) { + elem.style[support.transform] = value.toString(true); + } else { + elem.style[support.transform] = value.toString(); + } + + $(elem).data('transform', value); + } + }; + + // ## 'transformOrigin' CSS hook + // Allows the use for `transformOrigin` to define where scaling and rotation + // is pivoted. + // + // $("#hello").css({ transformOrigin: '0 0' }); + // + $.cssHooks.transformOrigin = { + get: function(elem) { + return elem.style[support.transformOrigin]; + }, + set: function(elem, value) { + elem.style[support.transformOrigin] = value; + } + }; + + // ## Other CSS hooks + // Allows you to rotate, scale and translate. + registerCssHook('scale'); + registerCssHook('translate'); + registerCssHook('rotate'); + registerCssHook('rotateX'); + registerCssHook('rotateY'); + registerCssHook('rotate3d'); + registerCssHook('perspective'); + registerCssHook('skewX'); + registerCssHook('skewY'); + registerCssHook('x', true); + registerCssHook('y', true); + + // ## Transform class + // This is the main class of a transformation property that powers + // `$.fn.css({ transform: '...' })`. + // + // This is, in essence, a dictionary object with key/values as `-transform` + // properties. + // + // var t = new Transform("rotate(90) scale(4)"); + // + // t.rotate //=> "90deg" + // t.scale //=> "4,4" + // + // Setters are accounted for. + // + // t.set('rotate', 4) + // t.rotate //=> "4deg" + // + // Convert it to a CSS string using the `toString()` and `toString(true)` (for WebKit) + // functions. + // + // t.toString() //=> "rotate(90deg) scale(4,4)" + // t.toString(true) //=> "rotate(90deg) scale3d(4,4,0)" (WebKit version) + // + function Transform(str) { + if (typeof str === 'string') { this.parse(str); } + return this; + } + + Transform.prototype = { + // ### setFromString() + // Sets a property from a string. + // + // t.setFromString('scale', '2,4'); + // // Same as set('scale', '2', '4'); + // + setFromString: function(prop, val) { + var args = + (typeof val === 'string') ? val.split(',') : + (val.constructor === Array) ? val : + [ val ]; + + args.unshift(prop); + + Transform.prototype.set.apply(this, args); + }, + + // ### set() + // Sets a property. + // + // t.set('scale', 2, 4); + // + set: function(prop) { + var args = Array.prototype.slice.apply(arguments, [1]); + if (this.setter[prop]) { + this.setter[prop].apply(this, args); + } else { + this[prop] = args.join(','); + } + }, + + get: function(prop) { + if (this.getter[prop]) { + return this.getter[prop].apply(this); + } else { + return this[prop] || 0; + } + }, + + setter: { + // ### rotate + // + // .css({ rotate: 30 }) + // .css({ rotate: "30" }) + // .css({ rotate: "30deg" }) + // .css({ rotate: "30deg" }) + // + rotate: function(theta) { + this.rotate = unit(theta, 'deg'); + }, + + rotateX: function(theta) { + this.rotateX = unit(theta, 'deg'); + }, + + rotateY: function(theta) { + this.rotateY = unit(theta, 'deg'); + }, + + // ### scale + // + // .css({ scale: 9 }) //=> "scale(9,9)" + // .css({ scale: '3,2' }) //=> "scale(3,2)" + // + scale: function(x, y) { + if (y === undefined) { y = x; } + this.scale = x + "," + y; + }, + + // ### skewX + skewY + skewX: function(x) { + this.skewX = unit(x, 'deg'); + }, + + skewY: function(y) { + this.skewY = unit(y, 'deg'); + }, + + // ### perspectvie + perspective: function(dist) { + this.perspective = unit(dist, 'px'); + }, + + // ### x / y + // Translations. Notice how this keeps the other value. + // + // .css({ x: 4 }) //=> "translate(4px, 0)" + // .css({ y: 10 }) //=> "translate(4px, 10px)" + // + x: function(x) { + this.set('translate', x, null); + }, + + y: function(y) { + this.set('translate', null, y); + }, + + // ### translate + // Notice how this keeps the other value. + // + // .css({ translate: '2, 5' }) //=> "translate(2px, 5px)" + // + translate: function(x, y) { + if (this._translateX === undefined) { this._translateX = 0; } + if (this._translateY === undefined) { this._translateY = 0; } + + if (x !== null) { this._translateX = unit(x, 'px'); } + if (y !== null) { this._translateY = unit(y, 'px'); } + + this.translate = this._translateX + "," + this._translateY; + } + }, + + getter: { + x: function() { + return this._translateX || 0; + }, + + y: function() { + return this._translateY || 0; + }, + + scale: function() { + var s = (this.scale || "1,1").split(','); + if (s[0]) { s[0] = parseFloat(s[0]); } + if (s[1]) { s[1] = parseFloat(s[1]); } + + // "2.5,2.5" => 2.5 + // "2.5,1" => [2.5,1] + return (s[0] === s[1]) ? s[0] : s; + }, + + rotate3d: function() { + var s = (this.rotate3d || "0,0,0,0deg").split(','); + for (var i=0; i<=3; ++i) { + if (s[i]) { s[i] = parseFloat(s[i]); } + } + if (s[3]) { s[3] = unit(s[3], 'deg'); } + + return s; + } + }, + + // ### parse() + // Parses from a string. Called on constructor. + parse: function(str) { + var self = this; + str.replace(/([a-zA-Z0-9]+)\((.*?)\)/g, function(x, prop, val) { + self.setFromString(prop, val); + }); + }, + + // ### toString() + // Converts to a `transition` CSS property string. If `use3d` is given, + // it converts to a `-webkit-transition` CSS property string instead. + toString: function(use3d) { + var re = []; + + for (var i in this) { + if (this.hasOwnProperty(i)) { + // Don't use 3D transformations if the browser can't support it. + if ((!support.transform3d) && ( + (i === 'rotateX') || + (i === 'rotateY') || + (i === 'perspective') || + (i === 'transformOrigin'))) { continue; } + + if (i[0] !== '_') { + if (use3d && (i === 'scale')) { + re.push(i + "3d(" + this[i] + ",1)"); + } else if (use3d && (i === 'translate')) { + re.push(i + "3d(" + this[i] + ",0)"); + } else { + re.push(i + "(" + this[i] + ")"); + } + } + } + } + + return re.join(" "); + } + }; + + function callOrQueue(self, queue, fn) { + if (queue === true) { + self.queue(fn); + } else if (queue) { + self.queue(queue, fn); + } else { + fn(); + } + } + + // ### getProperties(dict) + // Returns properties (for `transition-property`) for dictionary `props`. The + // value of `props` is what you would expect in `$.css(...)`. + function getProperties(props) { + var re = []; + + $.each(props, function(key) { + key = $.camelCase(key); // Convert "text-align" => "textAlign" + key = $.transit.propertyMap[key] || key; + key = uncamel(key); // Convert back to dasherized + + if ($.inArray(key, re) === -1) { re.push(key); } + }); + + return re; + } + + // ### getTransition() + // Returns the transition string to be used for the `transition` CSS property. + // + // Example: + // + // getTransition({ opacity: 1, rotate: 30 }, 500, 'ease'); + // //=> 'opacity 500ms ease, -webkit-transform 500ms ease' + // + function getTransition(properties, duration, easing, delay) { + // Get the CSS properties needed. + var props = getProperties(properties); + + // Account for aliases (`in` => `ease-in`). + if ($.cssEase[easing]) { easing = $.cssEase[easing]; } + + // Build the duration/easing/delay attributes for it. + var attribs = '' + toMS(duration) + ' ' + easing; + if (parseInt(delay, 10) > 0) { attribs += ' ' + toMS(delay); } + + // For more properties, add them this way: + // "margin 200ms ease, padding 200ms ease, ..." + var transitions = []; + $.each(props, function(i, name) { + transitions.push(name + ' ' + attribs); + }); + + return transitions.join(', '); + } + + // ## $.fn.transition + // Works like $.fn.animate(), but uses CSS transitions. + // + // $("...").transition({ opacity: 0.1, scale: 0.3 }); + // + // // Specific duration + // $("...").transition({ opacity: 0.1, scale: 0.3 }, 500); + // + // // With duration and easing + // $("...").transition({ opacity: 0.1, scale: 0.3 }, 500, 'in'); + // + // // With callback + // $("...").transition({ opacity: 0.1, scale: 0.3 }, function() { ... }); + // + // // With everything + // $("...").transition({ opacity: 0.1, scale: 0.3 }, 500, 'in', function() { ... }); + // + // // Alternate syntax + // $("...").transition({ + // opacity: 0.1, + // duration: 200, + // delay: 40, + // easing: 'in', + // complete: function() { /* ... */ } + // }); + // + $.fn.transition = $.fn.transit = function(properties, duration, easing, callback) { + var self = this; + var delay = 0; + var queue = true; + + // Account for `.transition(properties, callback)`. + if (typeof duration === 'function') { + callback = duration; + duration = undefined; + } + + // Account for `.transition(properties, duration, callback)`. + if (typeof easing === 'function') { + callback = easing; + easing = undefined; + } + + // Alternate syntax. + if (typeof properties.easing !== 'undefined') { + easing = properties.easing; + delete properties.easing; + } + + if (typeof properties.duration !== 'undefined') { + duration = properties.duration; + delete properties.duration; + } + + if (typeof properties.complete !== 'undefined') { + callback = properties.complete; + delete properties.complete; + } + + if (typeof properties.queue !== 'undefined') { + queue = properties.queue; + delete properties.queue; + } + + if (typeof properties.delay !== 'undefined') { + delay = properties.delay; + delete properties.delay; + } + + // Set defaults. (`400` duration, `ease` easing) + if (typeof duration === 'undefined') { duration = $.fx.speeds._default; } + if (typeof easing === 'undefined') { easing = $.cssEase._default; } + + duration = toMS(duration); + + // Build the `transition` property. + var transitionValue = getTransition(properties, duration, easing, delay); + + // Compute delay until callback. + // If this becomes 0, don't bother setting the transition property. + var work = $.transit.enabled && support.transition; + var i = work ? (parseInt(duration, 10) + parseInt(delay, 10)) : 0; + + // If there's nothing to do... + if (i === 0) { + var fn = function(next) { + self.css(properties); + if (callback) { callback(); } + next(); + }; + + callOrQueue(self, queue, fn); + return self; + } + + // Save the old transitions of each element so we can restore it later. + var oldTransitions = {}; + + var run = function(nextCall) { + var bound = false; + + // Prepare the callback. + var cb = function() { + if (bound) { self.unbind(transitionEnd, cb); } + + if (i > 0) { + self.each(function() { + this.style[support.transition] = (oldTransitions[this] || null); + }); + } + + if (typeof callback === 'function') { callback.apply(self); } + if (typeof nextCall === 'function') { nextCall(); } + }; + + if ((i > 0) && (transitionEnd) && ($.transit.useTransitionEnd)) { + // Use the 'transitionend' event if it's available. + bound = true; + self.bind(transitionEnd, cb); + } else { + // Fallback to timers if the 'transitionend' event isn't supported. + window.setTimeout(cb, i); + } + + // Apply transitions. + self.each(function() { + if (i > 0) { + this.style[support.transition] = transitionValue; + } + $(this).css(properties); + }); + }; + + // Defer running. This allows the browser to paint any pending CSS it hasn't + // painted yet before doing the transitions. + var deferredRun = function(next) { + var i = 0; + + // Durations that are too slow will get transitions mixed up. + // (Tested on Mac/FF 7.0.1) + if ((support.transition === 'MozTransition') && (i < 25)) { i = 25; } + + window.setTimeout(function() { run(next); }, i); + }; + + // Use jQuery's fx queue. + callOrQueue(self, queue, deferredRun); + + // Chainability. + return this; + }; + + function registerCssHook(prop, isPixels) { + // For certain properties, the 'px' should not be implied. + if (!isPixels) { $.cssNumber[prop] = true; } + + $.transit.propertyMap[prop] = support.transform; + + $.cssHooks[prop] = { + get: function(elem) { + var t = $(elem).css('transform') || new Transform(); + return t.get(prop); + }, + + set: function(elem, value) { + var t = $(elem).css('transform') || new Transform(); + t.setFromString(prop, value); + + $(elem).css({ transform: t }); + } + }; + } + + // ### uncamel(str) + // Converts a camelcase string to a dasherized string. + // (`marginLeft` => `margin-left`) + function uncamel(str) { + return str.replace(/([A-Z])/g, function(letter) { return '-' + letter.toLowerCase(); }); + } + + // ### unit(number, unit) + // Ensures that number `number` has a unit. If no unit is found, assume the + // default is `unit`. + // + // unit(2, 'px') //=> "2px" + // unit("30deg", 'rad') //=> "30deg" + // + function unit(i, units) { + if ((typeof i === "string") && (!i.match(/^[\-0-9\.]+$/))) { + return i; + } else { + return "" + i + units; + } + } + + // ### toMS(duration) + // Converts given `duration` to a millisecond string. + // + // toMS('fast') //=> '400ms' + // toMS(10) //=> '10ms' + // + function toMS(duration) { + var i = duration; + + // Allow for string durations like 'fast'. + if ($.fx.speeds[i]) { i = $.fx.speeds[i]; } + + return unit(i, 'ms'); + } + + // Export some functions for testable-ness. + $.transit.getTransitionValue = getTransition; +})(jQuery); diff --git a/static/less/common.less b/static/less/common.less index d59602d6..da4d7952 100755 --- a/static/less/common.less +++ b/static/less/common.less @@ -22,6 +22,26 @@ background: #000; } +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-thumb { + background-color: white; + -webkit-border-radius: 3ex; + -webkit-box-shadow: 1px 1px 3px rgb(0, 0, 0); +} + +::-webkit-scrollbar-track { + background: url(/static/images/alpha/fff_20.png); + -webkit-border-radius: 5ex; + -webkit-border: 1px rgb(0, 0, 0); +} + +::-webkit-scrollbar-corner { + background: #000; +} + #fanart { bottom: 0; diff --git a/static/less/module-couchpotato.less b/static/less/module-couchpotato.less new file mode 100644 index 00000000..14785835 --- /dev/null +++ b/static/less/module-couchpotato.less @@ -0,0 +1,388 @@ +@import "mixins.less"; + +#couchpotato { + padding-bottom: 10px; + .menu { + .settings span { + background: url('/@{webroot}/images/cog.png') center no-repeat; + } + .all { + text-indent: 0; + } + .add { + text-indent: 0; + } + .wanted span { + background: url('/@{webroot}/images/coming.png') center no-repeat; + } + } + + div.back { + background: url('/@{webroot}/images/alpha/fff_10.png'); + cursor: pointer; + display: inline-block; + height: 30px; + line-height: 30px; + margin-left: 4px; + position: absolute; + right: 121px; + text-align: center; + top: 10px; + width: 30px; + + .border-radius(4px); + + &:hover { + background: url('/@{webroot}/images/alpha/fff_20.png'); + } + } + + div.powerholder { + text-align:right; + margin-top: 10px; + margin: 5px; + cursor: default; + + .button { + background: url('/@{webroot}/images/alpha/fff_10.png'); + cursor: pointer; + display: inline-block; + height: 30px; + line-height: 30px; + margin: 5px 0 0 4px; + text-align: center; + right: 0px; + color: #ccc; + vertical-align: middle; + width: 30px; + .border-radius(4px); + + &:hover { + background: url('/@{webroot}/images/alpha/fff_20.png'); + } + + img { + height: 14px; + margin-top: 8px; + width: 14px; + } + } + + .log { + font-size: 0.8em; + } + } + + #cp_content { + max-height: 264px; + overflow-x: hidden; + &.compact { + div.image{ + display: none; + } + .movie { + height: 20px; + } + span { + line-height: 20px; + &.tagline{ + display: none; + } + &.quality { + display: block; + float: right; + font-size: 0.5em; + } + } + } + .movie { + background: url('/@{webroot}/images/alpha/fff_10.png'); + cursor: pointer; + display: inline-block; + margin: 5px; + text-align:justify; + color: #ccc; + vertical-align: middle; + height: 60px; + width: 95%; + padding: 6px; + overflow: hidden; + position: relative; + z-index: 10; + .border-radius(4px); + + &:hover { + background: url('/@{webroot}/images/alpha/fff_20.png'); + } + + div.active { + background: url('/@{webroot}/images/snatched-banner.png') no-repeat; + width: 55px; + height: 100%; + position: absolute; + top: 0; + right: 0; + } + + div.image { + float: left; + height: 100%; + padding-right: 5px; + position: relative; + cursor: pointer; + + img.poster { + height: 100%; + } + + span.profile { + position: absolute; + bottom: 0; + margin: auto; + width: 90%; + text-align: center; + background: rgb(0, 0, 0); /* fallback color */ + background: rgba(0, 0, 0, 0.8); + } + } + + span { + display: block; + padding-bottom: 5px; + &.title{ + font-size: 1.2em; + } + } + } + div.options { + opacity: 0; + -moz-opacity: 0; + filter:alpha(opacity=0); + top: -70px; + left: 10px; + height: 0px; + margin: 0px; + position: relative; + float: left; + z-index: 1; + + img { + display: block; + height: 15px; + width: 15px; + padding: 2px; + margin: 2px; + cursor: pointer; + background: url('/@{webroot}/images/alpha/fff_10.png'); + .border-radius(4px); + &:hover{ + background: url('/@{webroot}/images/alpha/fff_20.png'); + } + } + } + } + + #all { + max-height: 264px; + overflow-x: hidden; + + .movie { + background: url('/@{webroot}/images/alpha/fff_10.png'); + cursor: pointer; + display: inline-block; + margin: 5px; + height: 30px; + width: 96%; + padding: 5px; + overflow: hidden; + position: relative; + .border-radius(4px); + + &:hover { + background: url('/@{webroot}/images/alpha/fff_20.png'); + } + + div.active { + background: url('/@{webroot}/images/snatched-banner.png') no-repeat; + width: 55px; + height: 100%; + position: absolute; + top: 0; + right: 0; + } + + img.poster { + float: left; + height: 100%; + padding-right: 5px; + position: relative; + cursor: pointer; + } + + span.text { + display: inline; + line-height:30px; + font-size: 1.3em; + max-width: 300px; + overflow: hidden; + } + + span.profile { + float:right; + line-height:30px; + font-size: 0.5em; + } + } + } + + .search { + form { + input.value { + padding: 4px; + width: 150px; + margin: 4px; + } + span.search { + background: url('/@{webroot}/images/alpha/fff_10.png'); + padding: 4px; + cursor: pointer; + .border-radius(4px); + + &:hover{ + background: url('/@{webroot}/images/alpha/fff_20.png'); + } + } + } + .result{ + max-height: 200px; + overflow: auto; + .error{ + padding: 10px; + margin: 10px; + } + ul{ + padding: 10px; + li{ + background: url('/@{webroot}/images/alpha/fff_10.png'); + height: 115px; + margin: 8px; + padding: 10px; + width: 90%; + overflow: hidden; + .border-radius(6px); + + img{ + width: 60px; + height: 100px; + float: left; + padding: 2px; + } + + .name{ + font-size: 16px; + margin: 2px; + width: 80%; + text-overflow: ellipsis; + white-space: nowrap; + } + + .year{ + float: left; + } + + div.extra{ + height: 40px; + overflow: auto; + text-align: justify; + padding: 6px; + margin: 10px; + } + + div.choices{ + margin-top: -8px; + padding: 4px; + height: 10px; + float: right; + span.add { + cursor: pointer; + padding: 4px; + margin: 4px; + background: url('/@{webroot}/images/alpha/fff_10.png'); + .border-radius(4px); + + &:hover{ + background: url('/@{webroot}/images/alpha/fff_20.png'); + } + } + } + } + } + } + } + + #info { + margin: 5px; + width: 100%; + overflow: hidden; + table { + width: 70%; + } + .thumbs{ + width: 108px; + height:38px; + overflow: scroll; + white-space: nowrap; + } + + .options{ + margin: 5px; + float: right; + img { + width: 15px; + height: 15px; + padding: 5px; + cursor: pointer; + .border-radius(4px); + background: url('/@{webroot}/images/alpha/fff_10.png'); + + &:hover{ + background: url('/@{webroot}/images/alpha/fff_20.png'); + } + } + } + } + + #cp_settings { + padding: 5px; + max-height: 260px; + overflow-y: scroll; + overflow-x: hidden; + + ul li { + background: url('/@{webroot}/images/alpha/fff_10.png'); + padding: 10px; + margin: 4px; + width: 92%; + .border-radius(4px); + + .value { + float: right; + width: 50%; + top: -6px; + position: relative; + } + } + } + + #cp_log { + h3 { + margin-left: 15px; + } + div.log { + line-height: 1.3em; + overflow: auto; + height: 211px; + margin-left: 15px; + } + } +} \ No newline at end of file diff --git a/static/less/server-settings.less b/static/less/server-settings.less new file mode 100644 index 00000000..dbe16c1d --- /dev/null +++ b/static/less/server-settings.less @@ -0,0 +1,38 @@ +@import "module.less"; + +#server_settings { + .module; + position: absolute; + top: -15px; + left: 50%; + width: 400px; + margin-left: -200px; + z-index: 1; + border: 0px !important; + + + &.hidden { + .inner { display: none; } + } + + + .tab { + position: absolute; + cursor: pointer; + bottom: -34px; + padding: 10px; + left: 36%; + + background: url(/static/images/alpha/000_80.png); + border: 1px solid #333; + border-top: 0px; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + + .text { + margin: 0 auto; + position: relative; + top: 2px; + } + } +} \ No newline at end of file diff --git a/static/less/settings.less b/static/less/settings.less old mode 100644 new mode 100755 index 7d074981..9788c3c5 --- a/static/less/settings.less +++ b/static/less/settings.less @@ -177,3 +177,6 @@ cursor: pointer; opacity: 1; } + +// #server_settings { opacity: 0; } +.f_settings_mode #server_settings { opacity: 1; } diff --git a/static/less/site.less b/static/less/site.less index cf5fed0b..c5c9d0c5 100644 --- a/static/less/site.less +++ b/static/less/site.less @@ -1,5 +1,6 @@ @import "module-applications.less"; @import "module-currently_playing.less"; +@import "module-couchpotato.less"; @import "module-disk_space.less"; @import "module-library.less"; @import "module-log.less"; diff --git a/templates/applications.html b/templates/applications.html index 0e650908..46121f49 100644 --- a/templates/applications.html +++ b/templates/applications.html @@ -1,6 +1,5 @@ <div id="applications" class="module" data-module="applications"> - <div class="module_settings"><span>Settings</span></div> <div class="module_remove"><span>Remove</span></div> <h2>Applications</h2> diff --git a/templates/base.html b/templates/base.html index 49bb6b6e..7b345858 100644 --- a/templates/base.html +++ b/templates/base.html @@ -81,6 +81,7 @@ <script src="{{ url_for('static', filename='js/lib/jquery.ui.rcarousel.min.js') }}"></script> <script src="{{ url_for('static', filename='js/lib/jquery.ui.widget.min.js') }}"></script> <script src="{{ url_for('static', filename='js/lib/jquery.ui.core.min.js') }}"></script> + <script src="{{ url_for('static', filename='js/lib/jquery.transit.js') }}"></script> </body> </html> diff --git a/templates/couchpotato-all.html b/templates/couchpotato-all.html new file mode 100644 index 00000000..18e1d9a3 --- /dev/null +++ b/templates/couchpotato-all.html @@ -0,0 +1,19 @@ +{% extends "couchpotato-base.html" %} + +{% block couchpotato_content %} + +<div id="all"> + {% for item in couchpotato %} + <div class="movie" id="{{item.library.identifier}}" data-cpid="{{item.library_id}}"> + <div class="{{item.releases.status}}"></div> + <img class="poster" + src="{% if item.library.files[1] %}{{item.library.files[1].path|cp_img}} {% else %}{{ url_for('static', filename='images/poster.png') }}{% endif %}" + /> + <span class="title text">{{item.library.titles[0].title}} ({{item.library.year}})</span> + {% if item.library.rating %}<span class="rating text">Rating: {{item.library.rating}}</span>{% endif %} + <span class="profile" title="CouchPotato quality profile">{{item.profile.label}}</span> + </div> + {% endfor %} +</div> + +{% endblock %} diff --git a/templates/couchpotato-base.html b/templates/couchpotato-base.html new file mode 100644 index 00000000..bd6e4b60 --- /dev/null +++ b/templates/couchpotato-base.html @@ -0,0 +1,28 @@ +{% if couchpotato %} + +<div id="couchpotato" class="module{% if compact_view %} compact{% endif %}" data-module="couchpotato"> + <div class="module_settings"><span>Settings</span></div> + <div class="module_remove"><span>Remove</span></div> + + <h2 class="title">{% block couchpotato_title %}CouchPotato{% endblock %}</h2> + + <ul class="menu"> + <li class="settings" title="View Settings"><span>Settings</span></li> + <li class="add" title ="Add new show to CouchPotato"><span>+</span></li> + <li class="all" title="View all"><span>All</span></li> + <li class="wanted" title="View wanted"><span>Wanted</span></li> + </ul> + + {% block couchpotato_content %}{% endblock %} + +</div> + +{% else %} + +<div class="placeholder" data-module="couchpotato"> + <div class="module_settings"><span>Settings</span></div> + <div class="module_remove"><span>Remove</span></div> + <h2>CouchPotato</h2> +</div> + +{% endif %} diff --git a/templates/couchpotato-info.html b/templates/couchpotato-info.html new file mode 100644 index 00000000..4154f09f --- /dev/null +++ b/templates/couchpotato-info.html @@ -0,0 +1,89 @@ +{% extends "couchpotato-base.html" %} +{% block couchpotato_title %}{% if couchpotato['success'] %}{{ couchpotato['movie']['library']['info']['original_title'] }}{% endif %}{% endblock %} + +{% block couchpotato_content %} +{% if couchpotato['success'] %} + {% set info = couchpotato['movie']['library'] %} + <div id="info" class="{{couchpotato['movie']['status']['identifier']}}"> + <div class="options" data-imdbid="{{info.info.imdb}}" data-cpid="{{couchpotato.movie.library_id}}"> + <img class="delete" src="{{ url_for('static', filename='images/no.png')}}" title="Delete"/> + <img class="search" src="{{ url_for('static', filename='images/search.png')}}" title="Refresh"/> + <a href="http://imdb.com/title/{{info.info.imdb}}" target="_blank"> + <img src="{{ url_for('static', filename='images/imdb.png')}}" title="IMDB" /> + </a> + </div> + <img src="{{info['info']['images']['poster'][0]}}" alt="poster" style="float:left;" width="100px;"> + <div style="height: 30px; width: 100px; overflow: hidden; float: left; clear: left;"> + <div class="thumbs"> + {%for image in info['info']['images']%} + {% for src in info['info']['images'][image] %} + <img src="{{src}}" alt="{{image}}" style="height: 28px; border: 1px white solid; cursor: pointer;"> + {%endfor%} + {%endfor%} + </div> + </div> + <p class="p_info"> + <table border="0"> + <tr> + <td width="75" style="font-weight:bold;" align="right">Year:  </td> + <td style="font-size: 0.95em;">{{info['info']['year']}}</td> + </tr> + <tr> + <td width="75" style="font-weight:bold;" align="right">Rating:  </td> + <td style="font-size: 0.95em;">{{info['info']['rating']['imdb'][0]}}/10 ({{info['info']['rating']['imdb'][1]}} votes</td> + </tr> + <tr> + <td width="75" style="font-weight:bold;" align="right">Genres:  </td> + <td style="font-size: 0.95em;">{% for genre in info['info']['genres'] %} {{genre}} {%if not loop.last%}|{%endif%} {% endfor %}</td> + </tr> + <tr> + <td width="75" style="font-weight: bold;" align="right">DVD:  </td> + <td style="font-size:0.95em;">{{info['info']['release_date']['dvd']|time}}</td> + </tr> + <tr> + <td width="75" style="font-weight: bold;" align="right">Theater:  </td> + <td style="font-size:0.95em;">{{info['info']['released']}}</td> + </tr> + <tr> + <td width="75" style="font-weight: bold;" align="right">Writers:  </td> + <td style="font-size:0.95em;">{%for writer in info['info']['writers']%} {{writer}}{%if not loop.last%},{%endif%} {%endfor%}</td> + </tr> + <tr> + <td width="75" style="font-weight: bold;" align="right">Directors:  </td> + <td style="font-size:0.95em;">{%for director in info['info']['directors']%} {{director}}{%if not loop.last%},{%endif%} {%endfor%}</td> + </tr> + <tr> + <td width="75" style="font-weight: bold;" align="right">Actors:  </td> + <td style="font-size:0.95em;">{%for actor in info['info']['actors']%} {{actor}}{%if not loop.last%},{%endif%} {%endfor%}</td> + </tr> + <tr> + <td width="75" style="font-weight: bold;" align="right">Runtime:  </td> + <td style="font-size:0.95em;">{{info['info']['runtime']}}</td> + </tr> + <tr> + <td width="75" style="font-weight: bold;" align="right">Profile:  </td> + <td style="font-size:0.95em;" class="profile"> + {% if profiles %} + <select class="profiles" data-id="{{couchpotato.movie.library_id}}"> + {% for profile in profiles.list %} + <option value="{{profile.id}}" {% if couchpotato['movie']['profile']['label'] == profile.label %}selected="selected"{%endif%}>{{profile.label}}</option> + {% endfor %} + </select> + {% else %} + {{couchpotato['movie']['profile']['label']}} + {% endif %} + </td> + </tr> + </table> + </p> + <p style="clear: left;"> + <br /><span style="font-weight:bold;">Plot:</span> + <br /><span style="font-size: 0.950em;">{{info['plot']}}</span> + </p> + </div> +{% else %} +<div id="error"> + <p>An error ocurred</p> +</div> +{%endif%} +{% endblock %} diff --git a/templates/couchpotato-log.html b/templates/couchpotato-log.html new file mode 100644 index 00000000..94c0c413 --- /dev/null +++ b/templates/couchpotato-log.html @@ -0,0 +1,20 @@ +{% extends "couchpotato-base.html" %} +{% block couchpotato_title %}{% if couchpotato['success'] %}CouchPotato Log{% endif %}{% endblock %} + +{% block couchpotato_content %} +{% if couchpotato['success'] %} +<div id="cp_log"> + <h3> Log level: + <select class="level"> + <option value="all" {% if level == 'all' %} selected {% endif%}>All</option> + <option value="debug" {% if level == 'debug' %} selected {% endif%}>Debug</option> + <option value="info" {% if level == 'info' %} selected {% endif%}>Info</option> + <option value="error" {% if level == 'error' %} selected {% endif%}>Error</option> + </select> + </h3> + <div class="log"> + <pre>{{couchpotato.log|e}}</pre> + </div> +</div> +{%endif%} +{% endblock %} diff --git a/templates/couchpotato-search.html b/templates/couchpotato-search.html new file mode 100644 index 00000000..8e92683d --- /dev/null +++ b/templates/couchpotato-search.html @@ -0,0 +1,45 @@ +{% extends "couchpotato-base.html" %} + +{% block couchpotato_content %} + +<div class="search"> + <form> + <input class="value" type="search"><span class="search">search</span> + </form> + <div class="result"> + {% if error %} + <p class="error">{{error}}</p> + {% elif data %} + <ul> + {% for item in data %} + <li data-imdbid="{% if item.imdb %}{{item.imdb}}{%else%}{{item.tmdb_id}}{%endif%}" data-title="{{item.original_title}}"> + {% if item.images.poster %} + <img src="{{ (item.images.poster)|replace( '[u\'', '')|replace('\']', '') }}" /> + {% else %} + <img src="{{ url_for('static', filename='images/poster.png')}}" /> + {% endif %} + <span class="name">{{ item.original_title }}</span><br> + <span class="year">{{ item.released }}</span> + <br> + <div class="extra {% if item.in_library%} library {% endif %} {% if item.in_wanted %} wanted {% endif %}"> + <p>{{item.plot}}</p> + </div> + <div class="choices"> + {% if profiles %} + <select class="profiles"> + {% for profile in profiles.list %} + <option value="{{profile.id}}">{{profile.label}}</option> + {% endfor %} + </select> + {% endif %} + <span class="add">Add</span> + </div> + </li> + {% endfor %} + </ul> + + {% endif %} + </div> +</div> + +{% endblock %} diff --git a/templates/couchpotato-settings.html b/templates/couchpotato-settings.html new file mode 100644 index 00000000..0a2bafe2 --- /dev/null +++ b/templates/couchpotato-settings.html @@ -0,0 +1,19 @@ +{% extends "couchpotato-base.html" %} + +{% block couchpotato_content %} + +<div id="cp_settings"> + <ul> + {% for item in couchpotato['values']['core']%} + <li><span class="title">{{item|replace('_',' ')}}</span><input class="value" value="{{couchpotato['values']['core'][item]}}" type="{% if item == 'password' %}password{%else%}text{%endif%}" disabled="disabled" /></li> + {% endfor %} + </ul> +</div> + +<div class="powerholder"> + <a class="button log" title ="CouchPotato logs">Log</a> + <a class="button restart" title ="Restart CouchPotato"><img src="{{ url_for('static', filename='images/reboot.png') }}" /></a> + <a class="button power" title ="Shutdown CouchPotato"><img src="{{ url_for('static', filename='images/shutdown.png') }}" /></a> +</div> + +{% endblock %} diff --git a/templates/couchpotato.html b/templates/couchpotato.html new file mode 100644 index 00000000..bcd64783 --- /dev/null +++ b/templates/couchpotato.html @@ -0,0 +1,30 @@ +{% extends "couchpotato-base.html" %} + +{% block couchpotato_content %} + +<div id="cp_content" {%if compact_view %}class="compact"{%endif%}> + {% for item in couchpotato %} + <div class="movie" id="{{item.library.identifier}}" data-cpid="{{item.library_id}}"> + <div class="{{item.releases.status}}"></div> + <div class="image"> + <img class="poster" + src="{% if item.library.files[1] %}{{item.library.files[1].path|cp_img}}{% else %}{{ url_for('static', filename='images/poster.png') }}{% endif %}" + /> + <span class="profile" title="CouchPotato quality profile">{{item.profile.label}}</span> + </div> + <span class="title">{{item.library.titles[0].title}} ({{item.library.year}})</span> + {% if item.library.rating %}<span class="rating text"><h3>Rating: {{item.library.rating}}</h3></span>{% endif %} + <span class="tagline">{% if item.library.tagline %}{{item.library.tagline}}{% else %}{{item.library.plot|truncate(150)}}{% endif %}</span> + {%if compact_view %}<span class="quality">{{item.profile.label}}</span>{%endif%} + </div> + <div class="options {{item.library.identifier}}" data-imdbid="{{item.library.identifier}}" data-cpid="{{item.library_id}}"> + <img class="delete" src="{{ url_for('static', filename='images/no.png')}}"/> + <img class="search" src="{{ url_for('static', filename='images/search.png')}}"/> + <a href="http://imdb.com/title/{{item.library.identifier}}" target="_blank"> + <img src="{{ url_for('static', filename='images/imdb.png')}}" alt="" /> + </a> + </div> + {% endfor %} +</div> + +{% endblock %}