88 Commits

Author SHA1 Message Date
1bfa16fac8 Add local copy of bootstrap and fix scheme for jquery ui themes 2013-03-31 00:00:57 +00:00
b7800eaca5 Use same URL scheme for accessing bootstrap library 2013-03-30 23:53:28 +00:00
4bd3c2778b Fix smarty warning 2013-03-30 23:52:54 +00:00
d2f678427b Merge branch 'release-0.4.1' 2012-01-22 17:46:13 +00:00
b9d624c345 version bump 2012-01-22 17:45:59 +00:00
1f6ef74030 Bug fix: remove warning in pidfile removal 2012-01-22 16:12:07 +00:00
1049f5e708 Remove deprecated init script option 2012-01-22 16:05:09 +00:00
05b19e68de Remove duplicate attribution from footer 2012-01-22 02:33:39 +00:00
aa6685ce3d Merge branch 'master' into develop 2012-01-22 00:16:05 +00:00
311b93772c Version bump 2012-01-21 21:59:13 +00:00
a4e34b3b8e Merge branch 'develop' 2012-01-21 21:54:16 +00:00
537c49751b Merge branch 'feature-layout' into develop 2012-01-21 21:47:22 +00:00
662f38b85c Bug fix: prevent readding most recently used directory 2012-01-21 19:52:37 +00:00
f0507d635c Update JS to use sihnon-js-library 2012-01-19 01:22:34 +00:00
7908a61e2d Add sihnon js library and symlink into public scripts dir 2012-01-18 23:21:15 +00:00
27b92ba5e4 add database schema and default settings 2012-01-15 14:51:35 +00:00
c195a733eb Update initscript to work with new file paths 2012-01-15 14:31:30 +00:00
56d499b1aa Update ui to use bootstrap ui in more places 2012-01-15 14:23:47 +00:00
c14fdcd2e3 Change job status ordering to account for time desync between nodes 2012-01-11 00:50:29 +00:00
d3e6e0524e Fix run-jobs path for new layout 2012-01-11 00:49:43 +00:00
a6ddb25eae Bootstrapify the webui 2012-01-11 00:49:05 +00:00
4486af7be8 Update directory layout to fit with other projects 2012-01-11 00:28:17 +00:00
06e3d65ffa Remove nested tmp directory 2011-12-03 19:47:57 +00:00
b00c7de09b Move smarty tmp files to /var/tmp so the directories don't get flushed on boot 2011-12-03 19:30:04 +00:00
1031aab192 Update job queue to pass along custom config file via environment 2011-10-06 22:54:52 +01:00
5d17baf107 Merge branch 'feature-bluray' into develop 2011-10-06 21:26:13 +01:00
97d03125d3 Bug fix: Bluray Source class forgot to actually return the sources list 2011-10-06 21:22:16 +01:00
f10ac4bc76 Update run-jobs to use standard init code, fix bug in previous commit 2011-09-25 12:08:31 +01:00
f8224652aa Simplify data returned from runAllJobs and fix logging bug in run-jobs 2011-09-25 11:07:22 +01:00
735224b83f Bug fix: retry job redirects to an old URL 2011-09-24 18:49:46 +01:00
85019b54c2 Merge branch 'feature-destinations' into develop 2011-09-24 18:49:35 +01:00
d703f1ff0d Add fuzzy ETA to homepage for running jobs 2011-09-24 18:17:18 +01:00
532e7873ef Show progress bars on the home page for running jobs 2011-09-24 17:58:56 +01:00
920d392f99 Update setup-rips page to use new jquery plugins
* Output directory is now an editable combo, with predefined and
recently used directories available.
* Audio/Subtitle track selects now use asmSelect
2011-09-24 17:58:40 +01:00
7477df269e Add 3rdparty jquery/ui plugins 2011-09-24 17:57:22 +01:00
553c256574 Add support for config hashes in the admin settings page. 2011-09-24 16:14:10 +01:00
1737106618 Replace local jquery with google CDN copy 2011-09-24 12:53:04 +01:00
342887257f Bug fix: copypasta errors in rip options 2011-08-31 23:54:53 +01:00
2b2102792d Bug fix: regression in writing job details to database 2011-08-31 23:48:36 +01:00
c80e40c292 Bug fix: typo in ajax response 2011-08-29 21:51:12 +01:00
adcd54714a Merge commit 'release-0.3' into develop 2011-08-29 19:28:22 +01:00
8aa3aa1415 Merge branch 'release-0.3' 2011-08-29 19:28:03 +01:00
ab5d242750 Version bump to 0.3 2011-08-29 19:27:40 +01:00
f8ba99932c Merge branch 'feature-settings' into develop 2011-08-29 19:24:09 +01:00
4818193d37 Sort admin settings by name to make searching easier 2011-08-29 19:22:56 +01:00
196f80bbf5 Expose UI for renaming admin settings 2011-08-29 19:20:28 +01:00
cf250c39f6 Merge branch 'feature-settings' into develop 2011-08-28 16:19:52 +01:00
5d851dd977 Add support for removing admin settings 2011-08-28 16:19:26 +01:00
7be1279a80 Merge branch 'feature-settings' into develop 2011-08-28 13:04:01 +01:00
aec18cb810 Admin settings updated with add new setting feature 2011-08-28 13:03:31 +01:00
25449d4d3d Rename add-setting to new-setting. Will reuse name for the backend 2011-08-28 10:32:54 +01:00
cab3a51323 Add initial support for adding settings.
Adds "Add Setting" button to table, and shows a popup with form
elements for adding a new setting. Does not yet add setting to the db.
2011-08-27 20:25:48 +01:00
5807af8b8f Move admin settings value into dedicated template file 2011-08-27 19:53:35 +01:00
c377d6bd84 Merge branch 'feature-settings' into develop 2011-08-26 19:05:05 +01:00
7e823d5d91 Tidy names of javascript functions in setting handling 2011-08-26 19:03:54 +01:00
7ddacb2426 Removed temporary directories, as these have moved to /tmp (default) 2011-08-26 19:02:46 +01:00
4603a290e8 Merge branch 'feature-settings' into develop 2011-08-26 18:58:39 +01:00
f884e93021 Complete settings update including success dialog 2011-08-26 18:41:40 +01:00
91749f7f85 Set dialog title for source deletion 2011-08-26 18:40:48 +01:00
e7cf24bb4f Bug fix: add dir to smarty plugin dir list rather than replacing orig 2011-08-26 17:41:07 +01:00
3497eb8e1e First attempt at admin settings page with writeable database backend.
Currently broken due to PHP's transparent conversion of "." to "_" in
POST variable names!
2011-08-24 02:40:16 +01:00
807514daa8 Merge branch 'release-0.2.1' into develop 2011-08-23 02:11:48 +01:00
6648fd8b24 Merge branch 'release-0.2.1' 2011-08-23 02:11:32 +01:00
073da2c4e2 Bumped version to 0.2.1 2011-08-23 02:11:02 +01:00
d474fba2b3 Merge branch 'feature-summaries' into develop 2011-08-23 02:08:40 +01:00
c5eb93dd46 Add hostname to job details worker logs 2011-08-23 01:35:51 +01:00
fbc6f7da48 Add date to job details page. 2011-08-23 01:29:54 +01:00
49e5635a71 Bug fix: save plugin information in job record 2011-08-23 01:28:37 +01:00
8739f6c516 Expose job log display options in the UI 2011-08-23 00:05:21 +01:00
5f786d16d7 Add control of job log message count and sort order via URL 2011-08-22 23:18:57 +01:00
22a3d94dc3 Move job details page and update references 2011-08-22 22:51:46 +01:00
f3415ff57a Merge commit 'release-0.2' into develop 2011-08-22 19:05:40 +01:00
6a57a6fca5 Merge branch 'release-0.2' 2011-08-22 19:04:36 +01:00
a061c23041 Bumped version number to 0.2 2011-08-22 19:03:36 +01:00
4300034afa Merge branch 'feature-ajax' into develop 2011-08-22 18:48:59 +01:00
eb1e330bc4 Added ajax support, and verified with an ajaxified Delete Source feature 2011-08-22 18:47:45 +01:00
41fc0a2cc3 Improve logging in HandBrake worker 2011-08-21 16:12:32 +01:00
95fe2e7641 Add additional logging for batch job runner 2011-08-21 16:11:51 +01:00
8d2ca716df Updated worker to copy rather than move output to final location
This causes ACLs to be inherited from the destination, rather than
preserving permissions from the temporary build location on the local
machine.
2011-08-20 11:07:30 +01:00
47946bcf98 Update handbrake backend to rip to temp dir 2011-08-07 15:40:17 +01:00
d2b2dc7925 Add output filesize to jobs summary page 2011-08-06 23:11:15 +01:00
e84c1eba42 Set logging program name 2011-08-06 14:57:14 +01:00
b93efc9878 Replace if block with switch case for extensibility 2011-08-06 14:55:29 +01:00
9697654594 Propagate changes to logging system from framework library 2011-08-06 14:54:22 +01:00
5121f78cea Merge branch 'master' of git+ssh://git.sihnon.net/home/git/public/handbrake-cluster-webui 2011-08-05 19:40:42 +01:00
dbc1252bef Added config file override for worker script 2011-08-05 19:35:55 +01:00
a3e58e4ee4 Merge branch 'master' of git+ssh://git.sihnon.net/home/git/public/handbrake-cluster-webui 2011-06-28 20:11:50 +01:00
3b22b0f2c9 Bug fix: Undefined index $values[9]
RippingCluster_LogEntry overwrote base class field list, causing
undefined index notices when the base class was explicitly used.
2011-06-28 20:06:24 +01:00
120 changed files with 4819 additions and 7093 deletions

4
.gitignore vendored
View File

@@ -4,5 +4,5 @@
.settings
/config.php
/dbconfig.conf
/webui/.htaccess
/webui/tmp/*
/public/.htaccess

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "externals/sihnon-js-lib"]
path = externals/sihnon-js-lib
url = ../sihnon-js-lib.git

View File

@@ -0,0 +1,5 @@
# Which user to run the worker daemon as
USER="media"
# File to store the running daemon's PID in
PID_FILE="/var/run/ripping-cluster-worker.pid"

View File

@@ -2,8 +2,6 @@
# Copyright 1999-2010 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
PID_FILE="/var/run/ripping-cluster-worker.pid"
depend() {
need localmount net
use dns logger puppetmaster netmount nfsmount
@@ -13,7 +11,8 @@ start() {
ebegin "Starting ripping-cluster-worker"
start-stop-daemon --start --quiet \
--background --make-pidfile --pidfile ${PID_FILE} \
--exec /usr/bin/php /usr/lib/ripping-cluster/worker/ripping-cluster-worker.php
--user ${USER} \
--exec /usr/bin/php /usr/lib/ripping-cluster/source/worker/ripping-cluster-worker.php
eend $? "Failed to start ripping-cluster-worker"
}
@@ -22,8 +21,12 @@ stop() {
start-stop-daemon --stop --quiet \
--pidfile ${PID_FILE}
local ret=$?
eend ${ret} "Failed to stop puppet"
rm -f ${PID_FILE}
eend ${ret} "Failed to stop ripping-cluster-worker"
if [[ "${ret}" == 0 ]]; then
rm -f ${PID_FILE}
fi
return ${ret}
}

208
build/schema/mysql.sql Normal file
View File

@@ -0,0 +1,208 @@
-- phpMyAdmin SQL Dump
-- version 3.3.0
-- http://www.phpmyadmin.net
--
-- Host: localhost:3306
-- Generation Time: Jan 11, 2012 at 12:27 AM
-- Server version: 5.1.53
-- PHP Version: 5.3.6-pl1-gentoo
SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
--
-- Database: `ripping-cluster`
--
-- --------------------------------------------------------
--
-- Table structure for table `client_log`
--
DROP TABLE IF EXISTS `client_log`;
CREATE TABLE IF NOT EXISTS `client_log` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`job_id` int(10) unsigned DEFAULT NULL,
`level` varchar(32) NOT NULL,
`category` varchar(32) NOT NULL,
`ctime` int(11) NOT NULL,
`pid` int(11) NOT NULL,
`hostname` varchar(32) NOT NULL,
`progname` varchar(64) NOT NULL,
`file` text NOT NULL,
`line` int(11) NOT NULL,
`message` text NOT NULL,
PRIMARY KEY (`id`),
KEY `job_id` (`job_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
-- --------------------------------------------------------
--
-- Table structure for table `jobs`
--
DROP TABLE IF EXISTS `jobs`;
CREATE TABLE IF NOT EXISTS `jobs` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`source_plugin` varchar(255) NOT NULL COMMENT 'Partial classname of the plugin used to read the source',
`rip_plugin` varchar(255) NOT NULL COMMENT 'Partial classname of the plugin used to perform the rip',
`source` text NOT NULL,
`destination` text NOT NULL,
`title` varchar(64) NOT NULL,
`format` varchar(4) NOT NULL,
`video_codec` varchar(8) NOT NULL,
`video_width` int(11) DEFAULT NULL,
`video_height` int(11) DEFAULT NULL,
`quantizer` float NOT NULL,
`deinterlace` double NOT NULL,
`audio_tracks` varchar(64) NOT NULL,
`audio_codecs` varchar(64) NOT NULL,
`audio_names` varchar(255) NOT NULL,
`subtitle_tracks` varchar(64) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
-- --------------------------------------------------------
--
-- Table structure for table `job_status`
--
DROP TABLE IF EXISTS `job_status`;
CREATE TABLE IF NOT EXISTS `job_status` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`job_id` int(10) unsigned NOT NULL,
`status` int(10) unsigned NOT NULL,
`ctime` int(10) unsigned NOT NULL,
`mtime` int(10) unsigned NOT NULL,
`rip_progress` double DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `job_id` (`job_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
-- --------------------------------------------------------
--
-- Table structure for table `job_status_current`
--
DROP TABLE IF EXISTS `job_status_current`;
CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `handbrake_cluster`.`job_status_current` AS select `js`.`id` AS `id`,`js`.`job_id` AS `job_id`,`js`.`status` AS `status`,`js`.`ctime` AS `ctime`,`js`.`mtime` AS `mtime`,`js`.`rip_progress` AS `rip_progress` from (`handbrake_cluster`.`job_status` `js` join `handbrake_cluster`.`job_status_current_int` `js2`) where ((`js2`.`job_id` = `js`.`job_id`) and (`js`.`id` = `js2`.`latest`));
-- --------------------------------------------------------
--
-- Table structure for table `job_status_current_int`
--
DROP TABLE IF EXISTS `job_status_current_int`;
CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `handbrake_cluster`.`job_status_current_int` AS (select `handbrake_cluster`.`job_status`.`job_id` AS `job_id`,max(`handbrake_cluster`.`job_status`.`id`) AS `latest` from `handbrake_cluster`.`job_status` group by `handbrake_cluster`.`job_status`.`job_id`);
-- --------------------------------------------------------
--
-- Table structure for table `settings`
--
DROP TABLE IF EXISTS `settings`;
CREATE TABLE IF NOT EXISTS `settings` (
`name` varchar(255) NOT NULL,
`value` text NOT NULL,
`type` enum('bool','int','float','string','array(string)','hash') DEFAULT 'string',
PRIMARY KEY (`name`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
--
-- Dumping data for table `settings`
--
INSERT INTO `settings` (`name`, `value`, `type`) VALUES
('debug.display_exceptions', '1', 'bool'),
('rips.nice', '15', 'int'),
('cache.base_dir', '/dev/shm/hbc/', 'string'),
('rips.cache_ttl', '86400', 'int'),
('rips.job_servers', 'localhost:7003', 'string'),
('rips.context', 'dev', 'string'),
('rips.default.output_directory', '', 'string'),
('rips.handbrake_binary', '/usr/bin/HandBrakeCLI', 'string'),
('rips.nice_binary', '/usr/bin/nice', 'string'),
('source.handbrake.dir', '', 'array(string)'),
('source.mkvinfo.dir', '', 'array(string)'),
('source.bluray.dir', '', 'array(string)'),
('logging.plugins', 'Database\nFlatFile\nConsole', 'array(string)'),
('logging.Console', 'stdout', 'array(string)'),
('logging.Console.stdout.format', '%ctime% %hostname%:%pid% %progname%:%file%[%line%] %message%', 'string'),
('logging.Console.stdout.severity', 'debug\ninfo\nwarning\nerror', 'array(string)'),
('logging.Console.stdout.category', 'client\nworker', 'array(string)'),
('logging.Database', 'webui\nworker', 'array(string)'),
('logging.Database.webui.table', 'client_log', 'string'),
('logging.Database.webui.severity', 'debug\ninfo\nwarning\ndebug', 'array(string)'),
('logging.Database.webui.category', 'batch\nclient\ndefault', 'array(string)'),
('logging.Database.worker.table', 'worker_log', 'string'),
('logging.Database.worker.severity', 'debug\ninfo\nwarning\nerror', 'array(string)'),
('logging.Database.worker.category', 'worker', 'array(string)'),
('logging.FlatFile', 'stderr\nvarlog_worker', 'array(string)'),
('logging.FlatFile.stderr.filename', 'php://stderr', 'string'),
('logging.FlatFile.stderr.format', '%timestamp% %hostname%:%pid% %progname%:%file%[%line%] %message%', 'string'),
('logging.FlatFile.stderr.severity', 'warning\nerror', 'array(string)'),
('logging.FlatFile.stderr.category', 'batch\nclient\ndefault\nworker', 'array(string)'),
('logging.FlatFile.varlog_worker.filename', '/var/log/ripping-cluster/worker.log', 'string'),
('logging.FlatFile.varlog_worker.format', '%timestamp% %hostname%:%pid% %progname%:%file%[%line%] %message%', 'string'),
('logging.FlatFile.varlog_worker.severity', 'debug\ninfo\nwarning\nerror', 'array(string)'),
('logging.FlatFile.varlog_worker.category', 'worker', 'array(string)'),
('logging.Syslog', 'local0', 'array(string)'),
('logging.Syslog.local0.facility', '128', 'int'),
('logging.Syslog.local0.severity', 'debug\ninfo\nwarning\nerror', 'array(string)'),
('logging.Syslog.local0.category', 'batch\nclient\ndefault\nworker', 'array(string)'),
('logging.Syslog.local0.format', '%file%[%line%] %message%', 'string'),
('templates.tmp_path', '/var/tmp/ripping-cluster', 'string'),
('rips.temp_dir', '/tmp', 'string'),
('job.logs.default_display_count', '30', 'int'),
('job.logs.default_order', 'DESC', 'string'),
('rips.output_directories.default', '', 'hash'),
('rips.output_directories.recent', '', 'array(string)'),
('rips.output_directories.recent_limit', '10', 'int'),
('auth', 'Config', 'string'),
('auth.admin.username', 'admin', 'string'),
('auth.admin_password', '489152af89501a7dc72f6e589123b8c337c01623', 'string');
-- --------------------------------------------------------
--
-- Table structure for table `worker_log`
--
DROP TABLE IF EXISTS `worker_log`;
CREATE TABLE IF NOT EXISTS `worker_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`job_id` int(10) unsigned DEFAULT NULL,
`level` varchar(32) NOT NULL,
`category` varchar(32) NOT NULL,
`ctime` int(11) NOT NULL,
`pid` int(11) NOT NULL,
`hostname` varchar(32) NOT NULL,
`progname` varchar(64) NOT NULL,
`file` text NOT NULL,
`line` int(11) NOT NULL,
`message` text NOT NULL,
PRIMARY KEY (`id`),
KEY `job_id` (`job_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
--
-- Constraints for dumped tables
--
--
-- Constraints for table `client_log`
--
ALTER TABLE `client_log`
ADD CONSTRAINT `client_log_ibfk_1` FOREIGN KEY (`job_id`) REFERENCES `jobs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
--
-- Constraints for table `job_status`
--
ALTER TABLE `job_status`
ADD CONSTRAINT `job_status_ibfk_1` FOREIGN KEY (`job_id`) REFERENCES `jobs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;

1
externals/sihnon-js-lib vendored Submodule

Submodule externals/sihnon-js-lib added at 0499e7ecaa

16
public/_inc.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
if (isset($_SERVER['RIPPING_CLUSTER_CONFIG']) &&
file_exists($_SERVER['RIPPING_CLUSTER_CONFIG']) &&
is_readable($_SERVER['RIPPING_CLUSTER_CONFIG'])) {
require_once($_SERVER['RIPPING_CLUSTER_CONFIG']);
} else {
require_once '/etc/ripping-cluster/config.php';
}
require_once SihnonFramework_Lib . 'SihnonFramework/Main.class.php';
SihnonFramework_Main::registerAutoloadClasses('SihnonFramework', SihnonFramework_Lib,
'RippingCluster', RippingCluster_Lib);
?>

22
public/a.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
define('HBC_File', 'ajax');
require '_inc.php';
try {
$main = RippingCluster_Main::instance();
RippingCluster_LogEntry::setLocalProgname('webui');
$smarty = $main->smarty();
$page = new RippingCluster_Page($smarty, $main->request());
if ($page->evaluate()) {
//header('Content-Type: text/json');
$smarty->display('ajax.tpl');
}
} catch (RippingCluster_Exception $e) {
die("Uncaught Exception: " . $e->getMessage());
}
?>

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -6,6 +6,7 @@ require '_inc.php';
try {
$main = RippingCluster_Main::instance();
RippingCluster_LogEntry::setLocalProgname('webui');
$smarty = $main->smarty();
$page = new RippingCluster_Page($smarty, $main->request());

View File

@@ -0,0 +1,113 @@
/* ==========================================================
* bootstrap-alerts.js v1.4.0
* http://twitter.github.com/bootstrap/javascript.html#alerts
* ==========================================================
* Copyright 2011 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================== */
!function( $ ){
"use strict"
/* CSS TRANSITION SUPPORT (https://gist.github.com/373874)
* ======================================================= */
var transitionEnd
$(document).ready(function () {
$.support.transition = (function () {
var thisBody = document.body || document.documentElement
, thisStyle = thisBody.style
, support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined
return support
})()
// set CSS transition event type
if ( $.support.transition ) {
transitionEnd = "TransitionEnd"
if ( $.browser.webkit ) {
transitionEnd = "webkitTransitionEnd"
} else if ( $.browser.mozilla ) {
transitionEnd = "transitionend"
} else if ( $.browser.opera ) {
transitionEnd = "oTransitionEnd"
}
}
})
/* ALERT CLASS DEFINITION
* ====================== */
var Alert = function ( content, options ) {
this.settings = $.extend({}, $.fn.alert.defaults, options)
this.$element = $(content)
.delegate(this.settings.selector, 'click', this.close)
}
Alert.prototype = {
close: function (e) {
var $element = $(this).parent('.alert-message')
e && e.preventDefault()
$element.removeClass('in')
function removeElement () {
$element.remove()
}
$.support.transition && $element.hasClass('fade') ?
$element.bind(transitionEnd, removeElement) :
removeElement()
}
}
/* ALERT PLUGIN DEFINITION
* ======================= */
$.fn.alert = function ( options ) {
if ( options === true ) {
return this.data('alert')
}
return this.each(function () {
var $this = $(this)
if ( typeof options == 'string' ) {
return $this.data('alert')[options]()
}
$(this).data('alert', new Alert( this, options ))
})
}
$.fn.alert.defaults = {
selector: '.close'
}
$(document).ready(function () {
new Alert($('body'), {
selector: '.alert-message[data-alert] .close'
})
})
}( window.jQuery || window.ender );

View File

@@ -0,0 +1,55 @@
/* ============================================================
* bootstrap-dropdown.js v1.4.0
* http://twitter.github.com/bootstrap/javascript.html#dropdown
* ============================================================
* Copyright 2011 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ============================================================ */
!function( $ ){
"use strict"
/* DROPDOWN PLUGIN DEFINITION
* ========================== */
$.fn.dropdown = function ( selector ) {
return this.each(function () {
$(this).delegate(selector || d, 'click', function (e) {
var li = $(this).parent('li')
, isActive = li.hasClass('open')
clearMenus()
!isActive && li.toggleClass('open')
return false
})
})
}
/* APPLY TO STANDARD DROPDOWN ELEMENTS
* =================================== */
var d = 'a.menu, .dropdown-toggle'
function clearMenus() {
$(d).parent('li').removeClass('open')
}
$(function () {
$('html').bind("click", clearMenus)
$('body').dropdown( '[data-dropdown] a.menu, [data-dropdown] .dropdown-toggle' )
})
}( window.jQuery || window.ender );

View File

@@ -0,0 +1,260 @@
/* =========================================================
* bootstrap-modal.js v1.4.0
* http://twitter.github.com/bootstrap/javascript.html#modal
* =========================================================
* Copyright 2011 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================= */
!function( $ ){
"use strict"
/* CSS TRANSITION SUPPORT (https://gist.github.com/373874)
* ======================================================= */
var transitionEnd
$(document).ready(function () {
$.support.transition = (function () {
var thisBody = document.body || document.documentElement
, thisStyle = thisBody.style
, support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined
return support
})()
// set CSS transition event type
if ( $.support.transition ) {
transitionEnd = "TransitionEnd"
if ( $.browser.webkit ) {
transitionEnd = "webkitTransitionEnd"
} else if ( $.browser.mozilla ) {
transitionEnd = "transitionend"
} else if ( $.browser.opera ) {
transitionEnd = "oTransitionEnd"
}
}
})
/* MODAL PUBLIC CLASS DEFINITION
* ============================= */
var Modal = function ( content, options ) {
this.settings = $.extend({}, $.fn.modal.defaults, options)
this.$element = $(content)
.delegate('.close', 'click.modal', $.proxy(this.hide, this))
if ( this.settings.show ) {
this.show()
}
return this
}
Modal.prototype = {
toggle: function () {
return this[!this.isShown ? 'show' : 'hide']()
}
, show: function () {
var that = this
this.isShown = true
this.$element.trigger('show')
escape.call(this)
backdrop.call(this, function () {
var transition = $.support.transition && that.$element.hasClass('fade')
that.$element
.appendTo(document.body)
.show()
if (transition) {
that.$element[0].offsetWidth // force reflow
}
that.$element.addClass('in')
transition ?
that.$element.one(transitionEnd, function () { that.$element.trigger('shown') }) :
that.$element.trigger('shown')
})
return this
}
, hide: function (e) {
e && e.preventDefault()
if ( !this.isShown ) {
return this
}
var that = this
this.isShown = false
escape.call(this)
this.$element
.trigger('hide')
.removeClass('in')
$.support.transition && this.$element.hasClass('fade') ?
hideWithTransition.call(this) :
hideModal.call(this)
return this
}
}
/* MODAL PRIVATE METHODS
* ===================== */
function hideWithTransition() {
// firefox drops transitionEnd events :{o
var that = this
, timeout = setTimeout(function () {
that.$element.unbind(transitionEnd)
hideModal.call(that)
}, 500)
this.$element.one(transitionEnd, function () {
clearTimeout(timeout)
hideModal.call(that)
})
}
function hideModal (that) {
this.$element
.hide()
.trigger('hidden')
backdrop.call(this)
}
function backdrop ( callback ) {
var that = this
, animate = this.$element.hasClass('fade') ? 'fade' : ''
if ( this.isShown && this.settings.backdrop ) {
var doAnimate = $.support.transition && animate
this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
.appendTo(document.body)
if ( this.settings.backdrop != 'static' ) {
this.$backdrop.click($.proxy(this.hide, this))
}
if ( doAnimate ) {
this.$backdrop[0].offsetWidth // force reflow
}
this.$backdrop.addClass('in')
doAnimate ?
this.$backdrop.one(transitionEnd, callback) :
callback()
} else if ( !this.isShown && this.$backdrop ) {
this.$backdrop.removeClass('in')
$.support.transition && this.$element.hasClass('fade')?
this.$backdrop.one(transitionEnd, $.proxy(removeBackdrop, this)) :
removeBackdrop.call(this)
} else if ( callback ) {
callback()
}
}
function removeBackdrop() {
this.$backdrop.remove()
this.$backdrop = null
}
function escape() {
var that = this
if ( this.isShown && this.settings.keyboard ) {
$(document).bind('keyup.modal', function ( e ) {
if ( e.which == 27 ) {
that.hide()
}
})
} else if ( !this.isShown ) {
$(document).unbind('keyup.modal')
}
}
/* MODAL PLUGIN DEFINITION
* ======================= */
$.fn.modal = function ( options ) {
var modal = this.data('modal')
if (!modal) {
if (typeof options == 'string') {
options = {
show: /show|toggle/.test(options)
}
}
return this.each(function () {
$(this).data('modal', new Modal(this, options))
})
}
if ( options === true ) {
return modal
}
if ( typeof options == 'string' ) {
modal[options]()
} else if ( modal ) {
modal.toggle()
}
return this
}
$.fn.modal.Modal = Modal
$.fn.modal.defaults = {
backdrop: false
, keyboard: false
, show: false
}
/* MODAL DATA- IMPLEMENTATION
* ========================== */
$(document).ready(function () {
$('body').delegate('[data-controls-modal]', 'click', function (e) {
e.preventDefault()
var $this = $(this).data('show', true)
$('#' + $this.attr('data-controls-modal')).modal( $this.data() )
})
})
}( window.jQuery || window.ender );

View File

@@ -0,0 +1,90 @@
/* ===========================================================
* bootstrap-popover.js v1.4.0
* http://twitter.github.com/bootstrap/javascript.html#popover
* ===========================================================
* Copyright 2011 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* =========================================================== */
!function( $ ) {
"use strict"
var Popover = function ( element, options ) {
this.$element = $(element)
this.options = options
this.enabled = true
this.fixTitle()
}
/* NOTE: POPOVER EXTENDS BOOTSTRAP-TWIPSY.js
========================================= */
Popover.prototype = $.extend({}, $.fn.twipsy.Twipsy.prototype, {
setContent: function () {
var $tip = this.tip()
$tip.find('.title')[this.options.html ? 'html' : 'text'](this.getTitle())
$tip.find('.content p')[this.options.html ? 'html' : 'text'](this.getContent())
$tip[0].className = 'popover'
}
, hasContent: function () {
return this.getTitle() || this.getContent()
}
, getContent: function () {
var content
, $e = this.$element
, o = this.options
if (typeof this.options.content == 'string') {
content = $e.attr(this.options.content)
} else if (typeof this.options.content == 'function') {
content = this.options.content.call(this.$element[0])
}
return content
}
, tip: function() {
if (!this.$tip) {
this.$tip = $('<div class="popover" />')
.html(this.options.template)
}
return this.$tip
}
})
/* POPOVER PLUGIN DEFINITION
* ======================= */
$.fn.popover = function (options) {
if (typeof options == 'object') options = $.extend({}, $.fn.popover.defaults, options)
$.fn.twipsy.initWith.call(this, options, Popover, 'popover')
return this
}
$.fn.popover.defaults = $.extend({} , $.fn.twipsy.defaults, {
placement: 'right'
, content: 'data-content'
, template: '<div class="arrow"></div><div class="inner"><h3 class="title"></h3><div class="content"><p></p></div></div>'
})
$.fn.twipsy.rejectAttrOptions.push( 'content' )
}( window.jQuery || window.ender );

View File

@@ -0,0 +1,80 @@
/* ========================================================
* bootstrap-tabs.js v1.4.0
* http://twitter.github.com/bootstrap/javascript.html#tabs
* ========================================================
* Copyright 2011 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ======================================================== */
!function( $ ){
"use strict"
function activate ( element, container ) {
container
.find('> .active')
.removeClass('active')
.find('> .dropdown-menu > .active')
.removeClass('active')
element.addClass('active')
if ( element.parent('.dropdown-menu') ) {
element.closest('li.dropdown').addClass('active')
}
}
function tab( e ) {
var $this = $(this)
, $ul = $this.closest('ul:not(.dropdown-menu)')
, href = $this.attr('href')
, previous
, $href
if ( /^#\w+/.test(href) ) {
e.preventDefault()
if ( $this.parent('li').hasClass('active') ) {
return
}
previous = $ul.find('.active a').last()[0]
$href = $(href)
activate($this.parent('li'), $ul)
activate($href, $href.parent())
$this.trigger({
type: 'change'
, relatedTarget: previous
})
}
}
/* TABS/PILLS PLUGIN DEFINITION
* ============================ */
$.fn.tabs = $.fn.pills = function ( selector ) {
return this.each(function () {
$(this).delegate(selector || '.tabs li > a, .pills > li > a', 'click', tab)
})
}
$(document).ready(function () {
$('body').tabs('ul[data-tabs] li > a, ul[data-pills] > li > a')
})
}( window.jQuery || window.ender );

View File

@@ -0,0 +1,321 @@
/* ==========================================================
* bootstrap-twipsy.js v1.4.0
* http://twitter.github.com/bootstrap/javascript.html#twipsy
* Adapted from the original jQuery.tipsy by Jason Frame
* ==========================================================
* Copyright 2011 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================== */
!function( $ ) {
"use strict"
/* CSS TRANSITION SUPPORT (https://gist.github.com/373874)
* ======================================================= */
var transitionEnd
$(document).ready(function () {
$.support.transition = (function () {
var thisBody = document.body || document.documentElement
, thisStyle = thisBody.style
, support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined
return support
})()
// set CSS transition event type
if ( $.support.transition ) {
transitionEnd = "TransitionEnd"
if ( $.browser.webkit ) {
transitionEnd = "webkitTransitionEnd"
} else if ( $.browser.mozilla ) {
transitionEnd = "transitionend"
} else if ( $.browser.opera ) {
transitionEnd = "oTransitionEnd"
}
}
})
/* TWIPSY PUBLIC CLASS DEFINITION
* ============================== */
var Twipsy = function ( element, options ) {
this.$element = $(element)
this.options = options
this.enabled = true
this.fixTitle()
}
Twipsy.prototype = {
show: function() {
var pos
, actualWidth
, actualHeight
, placement
, $tip
, tp
if (this.hasContent() && this.enabled) {
$tip = this.tip()
this.setContent()
if (this.options.animate) {
$tip.addClass('fade')
}
$tip
.remove()
.css({ top: 0, left: 0, display: 'block' })
.prependTo(document.body)
pos = $.extend({}, this.$element.offset(), {
width: this.$element[0].offsetWidth
, height: this.$element[0].offsetHeight
})
actualWidth = $tip[0].offsetWidth
actualHeight = $tip[0].offsetHeight
placement = maybeCall(this.options.placement, this, [ $tip[0], this.$element[0] ])
switch (placement) {
case 'below':
tp = {top: pos.top + pos.height + this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2}
break
case 'above':
tp = {top: pos.top - actualHeight - this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2}
break
case 'left':
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth - this.options.offset}
break
case 'right':
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width + this.options.offset}
break
}
$tip
.css(tp)
.addClass(placement)
.addClass('in')
}
}
, setContent: function () {
var $tip = this.tip()
$tip.find('.twipsy-inner')[this.options.html ? 'html' : 'text'](this.getTitle())
$tip[0].className = 'twipsy'
}
, hide: function() {
var that = this
, $tip = this.tip()
$tip.removeClass('in')
function removeElement () {
$tip.remove()
}
$.support.transition && this.$tip.hasClass('fade') ?
$tip.bind(transitionEnd, removeElement) :
removeElement()
}
, fixTitle: function() {
var $e = this.$element
if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
$e.attr('data-original-title', $e.attr('title') || '').removeAttr('title')
}
}
, hasContent: function () {
return this.getTitle()
}
, getTitle: function() {
var title
, $e = this.$element
, o = this.options
this.fixTitle()
if (typeof o.title == 'string') {
title = $e.attr(o.title == 'title' ? 'data-original-title' : o.title)
} else if (typeof o.title == 'function') {
title = o.title.call($e[0])
}
title = ('' + title).replace(/(^\s*|\s*$)/, "")
return title || o.fallback
}
, tip: function() {
return this.$tip = this.$tip || $('<div class="twipsy" />').html(this.options.template)
}
, validate: function() {
if (!this.$element[0].parentNode) {
this.hide()
this.$element = null
this.options = null
}
}
, enable: function() {
this.enabled = true
}
, disable: function() {
this.enabled = false
}
, toggleEnabled: function() {
this.enabled = !this.enabled
}
, toggle: function () {
this[this.tip().hasClass('in') ? 'hide' : 'show']()
}
}
/* TWIPSY PRIVATE METHODS
* ====================== */
function maybeCall ( thing, ctx, args ) {
return typeof thing == 'function' ? thing.apply(ctx, args) : thing
}
/* TWIPSY PLUGIN DEFINITION
* ======================== */
$.fn.twipsy = function (options) {
$.fn.twipsy.initWith.call(this, options, Twipsy, 'twipsy')
return this
}
$.fn.twipsy.initWith = function (options, Constructor, name) {
var twipsy
, binder
, eventIn
, eventOut
if (options === true) {
return this.data(name)
} else if (typeof options == 'string') {
twipsy = this.data(name)
if (twipsy) {
twipsy[options]()
}
return this
}
options = $.extend({}, $.fn[name].defaults, options)
function get(ele) {
var twipsy = $.data(ele, name)
if (!twipsy) {
twipsy = new Constructor(ele, $.fn.twipsy.elementOptions(ele, options))
$.data(ele, name, twipsy)
}
return twipsy
}
function enter() {
var twipsy = get(this)
twipsy.hoverState = 'in'
if (options.delayIn == 0) {
twipsy.show()
} else {
twipsy.fixTitle()
setTimeout(function() {
if (twipsy.hoverState == 'in') {
twipsy.show()
}
}, options.delayIn)
}
}
function leave() {
var twipsy = get(this)
twipsy.hoverState = 'out'
if (options.delayOut == 0) {
twipsy.hide()
} else {
setTimeout(function() {
if (twipsy.hoverState == 'out') {
twipsy.hide()
}
}, options.delayOut)
}
}
if (!options.live) {
this.each(function() {
get(this)
})
}
if (options.trigger != 'manual') {
binder = options.live ? 'live' : 'bind'
eventIn = options.trigger == 'hover' ? 'mouseenter' : 'focus'
eventOut = options.trigger == 'hover' ? 'mouseleave' : 'blur'
this[binder](eventIn, enter)[binder](eventOut, leave)
}
return this
}
$.fn.twipsy.Twipsy = Twipsy
$.fn.twipsy.defaults = {
animate: true
, delayIn: 0
, delayOut: 0
, fallback: ''
, placement: 'above'
, html: false
, live: false
, offset: 0
, title: 'title'
, trigger: 'hover'
, template: '<div class="twipsy-arrow"></div><div class="twipsy-inner"></div>'
}
$.fn.twipsy.rejectAttrOptions = [ 'title' ]
$.fn.twipsy.elementOptions = function(ele, options) {
var data = $(ele).data()
, rejects = $.fn.twipsy.rejectAttrOptions
, i = rejects.length
while (i--) {
delete data[rejects[i]]
}
return $.extend({}, options, data)
}
}( window.jQuery || window.ender );

View File

@@ -0,0 +1,407 @@
/*
* Alternate Select Multiple (asmSelect) 1.0.4 beta - jQuery Plugin
* http://www.ryancramer.com/projects/asmselect/
*
* Copyright (c) 2008 by Ryan Cramer - http://www.ryancramer.com
*
* Dual licensed under the MIT (MIT-LICENSE.txt)
* and GPL (GPL-LICENSE.txt) licenses.
*
*/
(function($) {
$.fn.asmSelect = function(customOptions) {
var options = {
listType: 'ol', // Ordered list 'ol', or unordered list 'ul'
sortable: false, // Should the list be sortable?
highlight: false, // Use the highlight feature?
animate: false, // Animate the the adding/removing of items in the list?
addItemTarget: 'bottom', // Where to place new selected items in list: top or bottom
hideWhenAdded: false, // Hide the option when added to the list? works only in FF
debugMode: false, // Debug mode keeps original select visible
removeLabel: 'remove', // Text used in the "remove" link
highlightAddedLabel: 'Added: ', // Text that precedes highlight of added item
highlightRemovedLabel: 'Removed: ', // Text that precedes highlight of removed item
containerClass: 'asmContainer', // Class for container that wraps this widget
selectClass: 'asmSelect', // Class for the newly created <select>
optionDisabledClass: 'asmOptionDisabled', // Class for items that are already selected / disabled
listClass: 'asmList', // Class for the list ($ol)
listSortableClass: 'asmListSortable', // Another class given to the list when it is sortable
listItemClass: 'asmListItem', // Class for the <li> list items
listItemLabelClass: 'asmListItemLabel', // Class for the label text that appears in list items
removeClass: 'asmListItemRemove', // Class given to the "remove" link
highlightClass: 'asmHighlight' // Class given to the highlight <span>
};
$.extend(options, customOptions);
return this.each(function(index) {
var $original = $(this); // the original select multiple
var $container; // a container that is wrapped around our widget
var $select; // the new select we have created
var $ol; // the list that we are manipulating
var buildingSelect = false; // is the new select being constructed right now?
var ieClick = false; // in IE, has a click event occurred? ignore if not
var ignoreOriginalChangeEvent = false; // originalChangeEvent bypassed when this is true
function init() {
// initialize the alternate select multiple
// this loop ensures uniqueness, in case of existing asmSelects placed by ajax (1.0.3)
while($("#" + options.containerClass + index).size() > 0) index++;
$select = $("<select></select>")
.addClass(options.selectClass)
.attr('name', options.selectClass + index)
.attr('id', options.selectClass + index);
$selectRemoved = $("<select></select>");
$ol = $("<" + options.listType + "></" + options.listType + ">")
.addClass(options.listClass)
.attr('id', options.listClass + index);
$container = $("<div></div>")
.addClass(options.containerClass)
.attr('id', options.containerClass + index);
buildSelect();
$select.change(selectChangeEvent)
.click(selectClickEvent);
$original.change(originalChangeEvent)
.wrap($container).before($select).before($ol);
if(options.sortable) makeSortable();
if($.browser.msie) $ol.css('display', 'inline-block');
}
function makeSortable() {
// make any items in the selected list sortable
// requires jQuery UI sortables, draggables, droppables
$ol.sortable({
items: 'li.' + options.listItemClass,
handle: '.' + options.listItemLabelClass,
axis: 'y',
update: function(e, data) {
var updatedOptionId;
$(this).children("li").each(function(n) {
$option = $('#' + $(this).attr('rel'));
if($(this).is(".ui-sortable-helper")) {
updatedOptionId = $option.attr('id');
return;
}
$original.append($option);
});
if(updatedOptionId) triggerOriginalChange(updatedOptionId, 'sort');
}
}).addClass(options.listSortableClass);
}
function selectChangeEvent(e) {
// an item has been selected on the regular select we created
// check to make sure it's not an IE screwup, and add it to the list
if($.browser.msie && $.browser.version < 7 && !ieClick) return;
var id = $(this).children("option:selected").slice(0,1).attr('rel');
addListItem(id);
ieClick = false;
triggerOriginalChange(id, 'add'); // for use by user-defined callbacks
}
function selectClickEvent() {
// IE6 lets you scroll around in a select without it being pulled down
// making sure a click preceded the change() event reduces the chance
// if unintended items being added. there may be a better solution?
ieClick = true;
}
function originalChangeEvent(e) {
// select or option change event manually triggered
// on the original <select multiple>, so rebuild ours
if(ignoreOriginalChangeEvent) {
ignoreOriginalChangeEvent = false;
return;
}
$select.empty();
$ol.empty();
buildSelect();
// opera has an issue where it needs a force redraw, otherwise
// the items won't appear until something else forces a redraw
if($.browser.opera) $ol.hide().fadeIn("fast");
}
function buildSelect() {
// build or rebuild the new select that the user
// will select items from
buildingSelect = true;
// add a first option to be the home option / default selectLabel
$select.prepend("<option>" + $original.attr('title') + "</option>");
$original.children("option").each(function(n) {
var $t = $(this);
var id;
if(!$t.attr('id')) $t.attr('id', 'asm' + index + 'option' + n);
id = $t.attr('id');
if($t.is(":selected")) {
addListItem(id);
addSelectOption(id, true);
} else {
addSelectOption(id);
}
});
if(!options.debugMode) $original.hide(); // IE6 requires this on every buildSelect()
selectFirstItem();
buildingSelect = false;
}
function addSelectOption(optionId, disabled) {
// add an <option> to the <select>
// used only by buildSelect()
if(disabled == undefined) var disabled = false;
var $O = $('#' + optionId);
var $option = $("<option>" + $O.text() + "</option>")
.val($O.val())
.attr('rel', optionId);
if(disabled) disableSelectOption($option);
$select.append($option);
}
function selectFirstItem() {
// select the firm item from the regular select that we created
$select.children(":eq(0)").attr("selected", true);
}
function disableSelectOption($option) {
// make an option disabled, indicating that it's already been selected
// because safari is the only browser that makes disabled items look 'disabled'
// we apply a class that reproduces the disabled look in other browsers
$option.addClass(options.optionDisabledClass)
.attr("selected", false)
.attr("disabled", true);
if(options.hideWhenAdded) $option.hide();
if($.browser.msie) $select.hide().show(); // this forces IE to update display
}
function enableSelectOption($option) {
// given an already disabled select option, enable it
$option.removeClass(options.optionDisabledClass)
.attr("disabled", false);
if(options.hideWhenAdded) $option.show();
if($.browser.msie) $select.hide().show(); // this forces IE to update display
}
function addListItem(optionId) {
// add a new item to the html list
var $O = $('#' + optionId);
if(!$O) return; // this is the first item, selectLabel
var $removeLink = $("<a></a>")
.attr("href", "#")
.addClass(options.removeClass)
.prepend(options.removeLabel)
.click(function() {
dropListItem($(this).parent('li').attr('rel'));
return false;
});
var $itemLabel = $("<span></span>")
.addClass(options.listItemLabelClass)
.html($O.html());
var $item = $("<li></li>")
.attr('rel', optionId)
.addClass(options.listItemClass)
.append($itemLabel)
.append($removeLink)
.hide();
if(!buildingSelect) {
if($O.is(":selected")) return; // already have it
$O.attr('selected', true);
}
if(options.addItemTarget == 'top' && !buildingSelect) {
$ol.prepend($item);
if(options.sortable) $original.prepend($O);
} else {
$ol.append($item);
if(options.sortable) $original.append($O);
}
addListItemShow($item);
disableSelectOption($("[rel=" + optionId + "]", $select));
if(!buildingSelect) {
setHighlight($item, options.highlightAddedLabel);
selectFirstItem();
if(options.sortable) $ol.sortable("refresh");
}
}
function addListItemShow($item) {
// reveal the currently hidden item with optional animation
// used only by addListItem()
if(options.animate && !buildingSelect) {
$item.animate({
opacity: "show",
height: "show"
}, 100, "swing", function() {
$item.animate({
height: "+=2px"
}, 50, "swing", function() {
$item.animate({
height: "-=2px"
}, 25, "swing");
});
});
} else {
$item.show();
}
}
function dropListItem(optionId, highlightItem) {
// remove an item from the html list
if(highlightItem == undefined) var highlightItem = true;
var $O = $('#' + optionId);
$O.attr('selected', false);
$item = $ol.children("li[rel=" + optionId + "]");
dropListItemHide($item);
enableSelectOption($("[rel=" + optionId + "]", options.removeWhenAdded ? $selectRemoved : $select));
if(highlightItem) setHighlight($item, options.highlightRemovedLabel);
triggerOriginalChange(optionId, 'drop');
}
function dropListItemHide($item) {
// remove the currently visible item with optional animation
// used only by dropListItem()
if(options.animate && !buildingSelect) {
$prevItem = $item.prev("li");
$item.animate({
opacity: "hide",
height: "hide"
}, 100, "linear", function() {
$prevItem.animate({
height: "-=2px"
}, 50, "swing", function() {
$prevItem.animate({
height: "+=2px"
}, 100, "swing");
});
$item.remove();
});
} else {
$item.remove();
}
}
function setHighlight($item, label) {
// set the contents of the highlight area that appears
// directly after the <select> single
// fade it in quickly, then fade it out
if(!options.highlight) return;
$select.next("#" + options.highlightClass + index).remove();
var $highlight = $("<span></span>")
.hide()
.addClass(options.highlightClass)
.attr('id', options.highlightClass + index)
.html(label + $item.children("." + options.listItemLabelClass).slice(0,1).text());
$select.after($highlight);
$highlight.fadeIn("fast", function() {
setTimeout(function() { $highlight.fadeOut("slow"); }, 50);
});
}
function triggerOriginalChange(optionId, type) {
// trigger a change event on the original select multiple
// so that other scripts can pick them up
ignoreOriginalChangeEvent = true;
$option = $("#" + optionId);
$original.trigger('change', [{
'option': $option,
'value': $option.val(),
'id': optionId,
'item': $ol.children("[rel=" + optionId + "]"),
'type': type
}]);
}
init();
});
};
})(jQuery);

View File

@@ -0,0 +1,73 @@
/*
* Chained - jQuery non AJAX(J) chained selects plugin
*
* Copyright (c) 2010-2011 Mika Tuupola
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/mit-license.php
*
*/
(function($) {
$.fn.chained = function(parent_selector, options) {
return this.each(function() {
/* Save this to self because this changes when scope changes. */
var self = this;
var backup = $(self).clone();
/* Handles maximum two parents now. */
$(parent_selector).each(function() {
$(this).bind("change", function() {
$(self).html(backup.html());
/* If multiple parents build classname like foo\bar. */
var selected = "";
$(parent_selector).each(function() {
selected += "\\" + $(":selected", this).val();
});
selected = selected.substr(1);
/* Also check for first parent without subclassing. */
/* TODO: This should be dynamic and check for each parent */
/* without subclassing. */
var first = $(parent_selector).first();
var selected_first = $(":selected", first).val();
$("option", self).each(function() {
/* Remove unneeded items but save the default value. */
if (!$(this).hasClass(selected) &&
!$(this).hasClass(selected_first) && $(this).val() !== "") {
$(this).remove();
}
});
/* If we have only the default value disable select. */
if (1 == $("option", self).size() && $(self).val() === "") {
$(self).attr("disabled", "disabled");
} else {
$(self).removeAttr("disabled");
}
$(self).trigger("change");
});
/* Force IE to see something selected on first page load, */
/* unless something is already selected */
if ( !$("option:selected", this).length ) {
$("option", this).first().attr("selected", "selected");
}
/* Force updating the children. */
$(this).trigger("change");
});
});
};
/* Alias for those who like to use more English like syntax. */
$.fn.chainedTo = $.fn.chained;
})(jQuery);

View File

@@ -0,0 +1,851 @@
/**
* jQuery jEC (jQuery Editable Combobox) 1.3.1
* http://code.google.com/p/jquery-jec
*
* Copyright (c) 2008-2009 Lukasz Rajchel (lukasz@rajchel.pl | http://rajchel.pl)
* Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
* and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
*
* Documentation : http://code.google.com/p/jquery-jec/wiki/Documentation
* Changelog : http://code.google.com/p/jquery-jec/wiki/Changelog
*
* Contributors : Lukasz Rajchel, Artem Orlov
*/
/*jslint white: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true,
regexp: true, strict: true, newcap: true, immed: true, maxerr: 50, indent: 4, maxlen: 120*/
/*global Array, Math, String, clearInterval, document, jQuery, setInterval*/
/*members ':', Handle, Remove, Set, acceptedKeys, addClass, all, append, appendTo, array, attr, before, bind,
blinkingCursor, blinkingCursorInterval, blur, bool, browser, ceil, change, charCode, classes, clearCursor, click, css,
cursorState, data, destroy, disable, each, editable, enable, eq, expr, extend, filter, find, floor, fn, focus,
focusOnNewOption, fromCharCode, get, getId, handleCursor, ignoredKeys, ignoreOptGroups, inArray, init, initJS, integer,
isArray, isPlainObject, jEC, jECTimer, jec, jecKill, jecOff, jecOn, jecPref, jecValue, keyCode, keyDown, keyPress,
keyRange, keyUp, keys, length, max, maxLength, min, msie, object, openedState, optionClasses, optionStyles, parent,
position, pref, prop, push, random, remove, removeAttr, removeClass, removeData, removeProp, safari, setEditableOption,
styles, substring, text, trigger, triggerChangeEvent, unbind, uneditable, useExistingOptions, val, value,
valueIsEditable, which*/
(function ($) {
'use strict';
$.jEC = (function () {
var pluginClass = 'jecEditableOption', cursorClass = 'hasCursor', options = {}, values = {}, lastKeyCode,
defaults, Validators, EventHandlers, Combobox, activeCombobox;
defaults = {
position: 0,
ignoreOptGroups: false,
maxLength: 255,
classes: [],
styles: {},
optionClasses: [],
optionStyles: {},
triggerChangeEvent: false,
focusOnNewOption: false,
useExistingOptions: false,
blinkingCursor: false,
blinkingCursorInterval: 1000,
ignoredKeys: [],
acceptedKeys: [[32, 126], [191, 382]]
};
Validators = (function () {
return {
integer: function (value) {
return typeof value === 'number' && Math.ceil(value) === Math.floor(value);
},
keyRange: function (value) {
var min, max;
if (typeof value === 'object' && !$.isArray(value)) {
min = value.min;
max = value.max;
} else if ($.isArray(value) && value.length === 2) {
min = value[0];
max = value[1];
}
return Validators.integer(min) && Validators.integer(max) && min <= max;
}
};
}());
EventHandlers = (function () {
var getKeyCode;
getKeyCode = function (event) {
var charCode = event.charCode;
if (charCode !== undefined && charCode !== 0) {
return charCode;
} else {
return event.keyCode;
}
};
return {
// focus event handler
// enables blinking cursor
focus: function () {
var opt = options[Combobox.getId($(this))];
if (opt.blinkingCursor && $.jECTimer === undefined) {
activeCombobox = $(this);
$.jECTimer = setInterval($.jEC.handleCursor, opt.blinkingCursorInterval);
}
},
// blur event handler
// disables blinking cursor
blur: function () {
if ($.jECTimer !== undefined) {
clearInterval($.jECTimer);
$.jECTimer = undefined;
activeCombobox = undefined;
Combobox.clearCursor($(this));
}
Combobox.openedState($(this), false);
},
// keydown event handler
// handles keys pressed on select (backspace and delete must be handled
// in keydown event in order to work in IE)
keyDown: function (event) {
var keyCode = getKeyCode(event), option, value;
lastKeyCode = keyCode;
switch (keyCode) {
case 8: // backspace
case 46: // delete
option = $(this).find('option.' + pluginClass);
if (option.val().length >= 1) {
value = option.text().substring(0, option.text().length - 1);
option.val(value).text(value).prop('selected', true);
}
return (keyCode !== 8);
default:
break;
}
},
// keypress event handler
// handles the rest of the keys (keypress event gives more informations
// about pressed keys)
keyPress: function (event) {
var keyCode = getKeyCode(event), opt = options[Combobox.getId($(this))],
option, value, specialKeys, exit = false, text;
Combobox.clearCursor($(this));
if (keyCode !== 9 && keyCode !== 13 && keyCode !== 27) {
// special keys codes
specialKeys = [37, 38, 39, 40, 46];
// handle special keys
$.each(specialKeys, function (i, val) {
if (keyCode === val && keyCode === lastKeyCode) {
exit = true;
}
});
// don't handle ignored keys
if (!exit && $.inArray(keyCode, opt.ignoredKeys) === -1) {
// remove selection from all options
$(this).find('option:selected').removeProp('selected');
if ($.inArray(keyCode, opt.acceptedKeys) !== -1) {
option = $(this).find('option.' + pluginClass);
text = option.text();
if (text.length < opt.maxLength) {
value = text + String.fromCharCode(getKeyCode(event));
option.val(value).text(value);
}
option.prop('selected', true);
}
}
return false;
}
},
keyUp: function () {
var opt = options[Combobox.getId($(this))];
if (opt.triggerChangeEvent) {
$(this).trigger('change');
}
},
// change event handler
// handles editable option changing based on a pre-existing values
change: function () {
var opt = options[Combobox.getId($(this))];
if (opt.useExistingOptions) {
Combobox.setEditableOption($(this));
}
},
click: function () {
if (!$.browser.safari) {
Combobox.openedState($(this), !Combobox.openedState($(this)));
}
}
};
}());
// Combobox
Combobox = (function () {
var Parameters, EditableOption, generateId, setup;
// validates and set combobox parameters
Parameters = (function () {
var Set, Remove, Handle;
Set = (function () {
var parseKeys, Handles;
parseKeys = function (value) {
var keys = [];
if ($.isArray(value)) {
$.each(value, function (i, val) {
var j, min, max;
if (Validators.keyRange(val)) {
if ($.isArray(val)) {
min = val[0];
max = val[1];
} else {
min = val.min;
max = val.max;
}
for (j = min; j <= max; j += 1) {
keys.push(j);
}
} else if (typeof val === 'number' && Validators.integer(val)) {
keys.push(val);
}
});
}
return keys;
};
Handles = (function () {
return {
integer: function (elem, name, value) {
var id = Combobox.getId(elem), opt = options[id];
if (opt !== undefined && Validators.integer(value)) {
opt[name] = value;
return true;
}
return false;
},
bool: function (elem, name, value) {
var id = Combobox.getId(elem), opt = options[id];
if (opt !== undefined && typeof value === 'boolean') {
opt[name] = value;
return true;
}
return false;
},
array: function (elem, name, value) {
if (typeof value === 'string') {
value = [value];
}
var id = Combobox.getId(elem), opt = options[id];
if (opt !== undefined && $.isArray(value)) {
opt[name] = value;
return true;
}
return false;
},
object: function (elem, name, value) {
var id = Combobox.getId(elem), opt = options[id];
if (opt !== undefined && value !== null &&
typeof value === 'object' && !$.isArray(value)) {
opt[name] = value;
}
},
keys: function (elem, name, value) {
var id = Combobox.getId(elem), opt = options[id];
if (opt !== undefined && $.isArray(value)) {
opt[name] = parseKeys(value);
}
}
};
}());
return {
position: function (elem, value) {
if (Handles.integer(elem, 'position', value)) {
var id = Combobox.getId(elem), opt = options[id], optionsCount;
optionsCount =
elem.find('option:not(.' + pluginClass + ')').length;
if (value > optionsCount) {
opt.position = optionsCount;
}
}
},
ignoreOptGroups: function (elem, value) {
Handles.bool(elem, 'ignoreOptGroups', value);
},
maxLength: function (elem, value) {
if (Handles.integer(elem, 'maxLength', value)) {
var id = Combobox.getId(elem), opt = options[id];
if (value < 0 || value > 255) {
opt.maxLength = 255;
}
}
},
classes: function (elem, value) {
Handles.array(elem, 'classes', value);
},
optionClasses: function (elem, value) {
Handles.array(elem, 'optionClasses', value);
},
styles: function (elem, value) {
Handles.object(elem, 'styles', value);
},
optionStyles: function (elem, value) {
Handles.object(elem, 'optionStyles', value);
},
triggerChangeEvent: function (elem, value) {
Handles.bool(elem, 'triggerChangeEvent', value);
},
focusOnNewOption: function (elem, value) {
Handles.bool(elem, 'focusOnNewOption', value);
},
useExistingOptions: function (elem, value) {
Handles.bool(elem, 'useExistingOptions', value);
},
blinkingCursor: function (elem, value) {
Handles.bool(elem, 'blinkingCursor', value);
},
blinkingCursorInterval: function (elem, value) {
Handles.integer(elem, 'blinkingCursorInterval', value);
},
ignoredKeys: function (elem, value) {
Handles.keys(elem, 'ignoredKeys', value);
},
acceptedKeys: function (elem, value) {
Handles.keys(elem, 'acceptedKeys', value);
}
};
}());
Remove = (function () {
var removeClasses, removeStyles;
removeClasses = function (elem, classes) {
$.each(classes, function (i, val) {
elem.removeClass(val);
});
};
removeStyles = function (elem, styles) {
$.each(styles, function (key) {
elem.css(key, '');
});
};
return {
classes: function (elem) {
var id = Combobox.getId(elem), opt = options[id];
if (opt !== undefined) {
removeClasses(elem, opt.classes);
}
},
optionClasses: function (elem) {
var id = Combobox.getId(elem), opt = options[id];
if (opt !== undefined) {
removeClasses(elem.find('option.' + pluginClass),
opt.optionClasses);
}
},
styles: function (elem) {
var id = Combobox.getId(elem), opt = options[id];
if (opt !== undefined) {
removeStyles(elem, opt.styles);
}
},
optionStyles: function (elem) {
var id = Combobox.getId(elem), opt = options[id];
if (opt !== undefined) {
removeStyles(elem.find('option.' + pluginClass),
opt.optionStyles);
}
},
all: function (elem) {
Remove.classes(elem);
Remove.optionClasses(elem);
Remove.styles(elem);
Remove.optionStyles(elem);
}
};
}());
Handle = (function () {
var setClasses, setStyles;
setClasses = function (elem, classes) {
$.each(classes, function (i, val) {
elem.addClass(String(val));
});
};
setStyles = function (elem, styles) {
$.each(styles, function (key, val) {
elem.css(key, val);
});
};
return {
position: function (elem) {
var opt = options[Combobox.getId(elem)], option, uneditableOptions, container;
option = elem.find('option.' + pluginClass);
uneditableOptions = elem.find('option:not(.' + pluginClass + ')');
if (opt.position < uneditableOptions.length) {
container = uneditableOptions.eq(opt.position);
if (!opt.ignoreOptGroups && container.parent('optgroup').length > 0) {
uneditableOptions.eq(opt.position).parent().before(option);
} else {
uneditableOptions.eq(opt.position).before(option);
}
} else {
elem.append(option);
}
},
classes: function (elem) {
var id = Combobox.getId(elem), opt = options[id];
if (opt !== undefined) {
setClasses(elem, opt.classes);
}
},
optionClasses: function (elem) {
var id = Combobox.getId(elem), opt = options[id];
if (opt !== undefined) {
setClasses(elem.find('option.' + pluginClass), opt.optionClasses);
}
},
styles: function (elem) {
var id = Combobox.getId(elem), opt = options[id];
if (opt !== undefined) {
setStyles(elem, opt.styles);
}
},
optionStyles: function (elem) {
var id = Combobox.getId(elem), opt = options[id];
if (opt !== undefined) {
setStyles(elem.find('option.' + pluginClass), opt.optionStyles);
}
},
focusOnNewOption: function (elem) {
var id = Combobox.getId(elem), opt = options[id];
if (opt !== undefined && opt.focusOnNewOption) {
elem.find(':not(option.' + pluginClass + ')').removeProp('selected');
elem.find('option.' + pluginClass).prop('selected', true);
}
},
useExistingOptions: function (elem) {
var id = Combobox.getId(elem), opt = options[id];
if (opt !== undefined && opt.useExistingOptions) {
Combobox.setEditableOption(elem);
}
},
all: function (elem) {
Handle.position(elem);
Handle.classes(elem);
Handle.optionClasses(elem);
Handle.styles(elem);
Handle.optionStyles(elem);
Handle.focusOnNewOption(elem);
Handle.useExistingOptions(elem);
}
};
}());
return {
Set: Set,
Remove: Remove,
Handle: Handle
};
}());
EditableOption = (function () {
return {
init: function (elem) {
if (!elem.find('option.' + pluginClass).length) {
var editableOption = $('<option>');
editableOption.addClass(pluginClass);
elem.append(editableOption);
}
elem.bind('keydown', EventHandlers.keyDown);
elem.bind('keypress', EventHandlers.keyPress);
elem.bind('keyup', EventHandlers.keyUp);
elem.bind('change', EventHandlers.change);
elem.bind('focus', EventHandlers.focus);
elem.bind('blur', EventHandlers.blur);
elem.bind('click', EventHandlers.click);
},
destroy: function (elem) {
elem.find('option.' + pluginClass).remove();
elem.unbind('keydown', EventHandlers.keyDown);
elem.unbind('keypress', EventHandlers.keyPress);
elem.unbind('keyup', EventHandlers.keyUp);
elem.unbind('change', EventHandlers.change);
elem.unbind('focus', EventHandlers.focus);
elem.unbind('blur', EventHandlers.blur);
elem.unbind('click', EventHandlers.click);
}
};
}());
// generates unique identifier
generateId = function () {
while (true) {
var random = Math.floor(Math.random() * 100000);
if (options[random] === undefined) {
return random;
}
}
};
// sets combobox
setup = function (elem) {
EditableOption.init(elem);
Parameters.Handle.all(elem);
};
// Combobox public members
return {
// create editable combobox
init: function (settings) {
return $(this).filter(':uneditable').each(function () {
var id = generateId(), elem = $(this);
elem.data('jecId', id);
// override passed default options
options[id] = $.extend(true, {}, defaults);
// parse keys
Parameters.Set.ignoredKeys(elem, options[id].ignoredKeys);
Parameters.Set.acceptedKeys(elem, options[id].acceptedKeys);
if (typeof settings === 'object' && !$.isArray(settings)) {
$.each(settings, function (key, val) {
if (val !== undefined) {
switch (key) {
case 'position':
Parameters.Set.position(elem, val);
break;
case 'ignoreOptGroups':
Parameters.Set.ignoreOptGroups(elem, val);
break;
case 'maxLength':
Parameters.Set.maxLength(elem, val);
break;
case 'classes':
Parameters.Set.classes(elem, val);
break;
case 'optionClasses':
Parameters.Set.optionClasses(elem, val);
break;
case 'styles':
Parameters.Set.styles(elem, val);
break;
case 'optionStyles':
Parameters.Set.optionStyles(elem, val);
break;
case 'triggerChangeEvent':
Parameters.Set.triggerChangeEvent(elem, val);
break;
case 'focusOnNewOption':
Parameters.Set.focusOnNewOption(elem, val);
break;
case 'useExistingOptions':
Parameters.Set.useExistingOptions(elem, val);
break;
case 'blinkingCursor':
Parameters.Set.blinkingCursor(elem, val);
break;
case 'blinkingCursorInterval':
Parameters.Set.blinkingCursorInterval(elem, val);
break;
case 'ignoredKeys':
Parameters.Set.ignoredKeys(elem, val);
break;
case 'acceptedKeys':
Parameters.Set.acceptedKeys(elem, val);
break;
}
}
});
}
setup($(this));
});
},
// creates editable combobox without using existing select elements
initJS: function (options, settings) {
var select, addOptions;
select = $('<select>');
addOptions = function (elem, options) {
if ($.isArray(options)) {
$.each(options, function (i, val) {
if ($.isPlainObject(val)) {
$.each(val, function (key, value) {
if ($.isArray(value)) {
var og = $('<optgroup>').attr('label', key);
addOptions(og, value);
og.appendTo(select);
} else if (typeof value === 'number' || typeof value === 'string') {
$('<option>').text(value).attr('value', key)
.appendTo(elem);
}
});
} else if (typeof val === 'string' || typeof val === 'number') {
$('<option>').text(val).attr('value', val).appendTo(elem);
}
});
}
};
addOptions(select, options);
return select.jec(settings);
},
// destroys editable combobox
destroy: function () {
return $(this).filter(':editable').each(function () {
$(this).jecOff();
$.removeData($(this).get(0), 'jecId');
$.removeData($(this).get(0), 'jecCursorState');
$.removeData($(this).get(0), 'jecOpenedState');
});
},
// enable editablecombobox
enable: function () {
return $(this).filter(':editable').each(function () {
var id = Combobox.getId($(this)), value = values[id];
setup($(this));
if (value !== undefined) {
$(this).jecValue(value);
}
});
},
// disable editable combobox
disable: function () {
return $(this).filter(':editable').each(function () {
var val = $(this).find('option.' + pluginClass).val();
values[Combobox.getId($(this))] = val;
Parameters.Remove.all($(this));
EditableOption.destroy($(this));
});
},
// gets or sets editable option's value
value: function (value, setFocus) {
if ($(this).filter(':editable').length > 0) {
if (value === null || value === undefined) {
// get value
return $(this).find('option.' + pluginClass).val();
} else if (typeof value === 'string' || typeof value === 'number') {
// set value
return $(this).filter(':editable').each(function () {
var option = $(this).find('option.' + pluginClass);
option.val(value).text(value);
if (typeof setFocus !== 'boolean' || setFocus) {
option.prop('selected', true);
}
});
}
}
},
// gets or sets editable option's preference
pref: function (name, value) {
if ($(this).filter(':editable').length > 0) {
if (typeof name === 'string') {
if (value === null || value === undefined) {
// get preference
return options[Combobox.getId($(this))][name];
} else {
// set preference
return $(this).filter(':editable').each(function () {
switch (name) {
case 'position':
Parameters.Set.position($(this), value);
Parameters.Handle.position($(this));
break;
case 'classes':
Parameters.Remove.classes($(this));
Parameters.Set.classes($(this), value);
Parameters.Handle.position($(this));
break;
case 'optionClasses':
Parameters.Remove.optionClasses($(this));
Parameters.Set.optionClasses($(this), value);
Parameters.Set.optionClasses($(this));
break;
case 'styles':
Parameters.Remove.styles($(this));
Parameters.Set.styles($(this), value);
Parameters.Set.styles($(this));
break;
case 'optionStyles':
Parameters.Remove.optionStyles($(this));
Parameters.Set.optionStyles($(this), value);
Parameters.Handle.optionStyles($(this));
break;
case 'focusOnNewOption':
Parameters.Set.focusOnNewOption($(this), value);
Parameters.Handle.focusOnNewOption($(this));
break;
case 'useExistingOptions':
Parameters.Set.useExistingOptions($(this), value);
Parameters.Handle.useExistingOptions($(this));
break;
case 'blinkingCursor':
Parameters.Set.blinkingCursor($(this), value);
break;
case 'blinkingCursorInterval':
Parameters.Set.blinkingCursorInterval($(this), value);
break;
case 'ignoredKeys':
Parameters.Set.ignoredKeys($(this), value);
break;
case 'acceptedKeys':
Parameters.Set.acceptedKeys($(this), value);
break;
}
});
}
}
}
},
// sets editable option to the value of currently selected option
setEditableOption: function (elem) {
var value = elem.find('option:selected').text();
elem.find('option.' + pluginClass).attr('value', elem.val()).text(value).prop('selected', true);
},
// get combobox id
getId: function (elem) {
return elem.data('jecId');
},
valueIsEditable: function (elem) {
return elem.find('option.' + pluginClass).get(0) === elem.find('option:selected').get(0);
},
clearCursor: function (elem) {
$(elem).find('option.' + cursorClass).each(function () {
var text = $(this).text();
$(this).removeClass(cursorClass).text(text.substring(0, text.length - 1));
});
},
cursorState: function (elem, state) {
return elem.data('jecCursorState', state);
},
openedState: function (elem, state) {
return elem.data('jecOpenedState', state);
},
//handles editable cursor
handleCursor: function () {
if (activeCombobox !== undefined && activeCombobox !== null) {
if ($.browser.msie && Combobox.openedState(activeCombobox)) {
return;
}
var state = Combobox.cursorState(activeCombobox), elem;
if (state) {
Combobox.clearCursor(activeCombobox);
} else if (Combobox.valueIsEditable(activeCombobox)) {
elem = activeCombobox.find('option:selected');
elem.addClass(cursorClass).text(elem.text() + '|');
}
Combobox.cursorState(activeCombobox, !state);
}
}
};
}());
// jEC public members
return {
init: Combobox.init,
enable: Combobox.enable,
disable: Combobox.disable,
destroy: Combobox.destroy,
value: Combobox.value,
pref: Combobox.pref,
initJS: Combobox.initJS,
handleCursor: Combobox.handleCursor
};
}());
// register functions
$.fn.extend({
jec: $.jEC.init,
jecOn: $.jEC.enable,
jecOff: $.jEC.disable,
jecKill: $.jEC.destroy,
jecValue: $.jEC.value,
jecPref: $.jEC.pref
});
$.extend({
jec: $.jEC.initJS
});
// register selectors
$.extend($.expr[':'], {
editable: function (a) {
var data = $(a).data('jecId');
return data !== null && data !== undefined;
},
uneditable: function (a) {
var data = $(a).data('jecId');
return data === null || data === undefined;
}
});
}(jQuery));

View File

@@ -0,0 +1,20 @@
(function($){$.extend({progressBar:new function(){this.defaults={steps:20,stepDuration:20,max:100,showText:true,textFormat:'percentage',width:120,height:12,callback:null,boxImage:'images/progressbar.gif',barImage:{0:'images/progressbg_red.gif',30:'images/progressbg_orange.gif',70:'images/progressbg_green.gif'},running_value:0,value:0,image:null};this.construct=function(arg1,arg2){var argvalue=null;var argconfig=null;if(arg1!=null){if(!isNaN(arg1)){argvalue=arg1;if(arg2!=null){argconfig=arg2;}}else{argconfig=arg1;}}
return this.each(function(child){var pb=this;var config=this.config;if(argvalue!=null&&this.bar!=null&&this.config!=null){this.config.value=parseInt(argvalue)
if(argconfig!=null)
pb.config=$.extend(this.config,argconfig);config=pb.config;}else{var $this=$(this);var config=$.extend({},$.progressBar.defaults,argconfig);config.id=$this.attr('id')?$this.attr('id'):Math.ceil(Math.random()*100000);if(argvalue==null)
argvalue=$this.html().replace("%","")
config.value=parseInt(argvalue);config.running_value=0;config.image=getBarImage(config);var numeric=['steps','stepDuration','max','width','height','running_value','value'];for(var i=0;i<numeric.length;i++)
config[numeric[i]]=parseInt(config[numeric[i]]);$this.html("");var bar=document.createElement('img');var text=document.createElement('span');var $bar=$(bar);var $text=$(text);pb.bar=$bar;$bar.attr('id',config.id+"_pbImage");$text.attr('id',config.id+"_pbText");$text.html(getText(config));$bar.attr('title',getText(config));$bar.attr('alt',getText(config));$bar.attr('src',config.boxImage);$bar.attr('width',config.width);$bar.css("width",config.width+"px");$bar.css("height",config.height+"px");$bar.css("background-image","url("+config.image+")");$bar.css("background-position",((config.width*-1))+'px 50%');$bar.css("padding","0");$bar.css("margin","0");$this.append($bar);$this.append($text);}
function getPercentage(config){return config.running_value*100/config.max;}
function getBarImage(config){var image=config.barImage;if(typeof(config.barImage)=='object'){for(var i in config.barImage){if(config.running_value>=parseInt(i)){image=config.barImage[i];}else{break;}}}
return image;}
function getText(config){if(config.showText){if(config.textFormat=='percentage'){return" "+Math.round(config.running_value)+"%";}else if(config.textFormat=='fraction'){return" "+config.running_value+'/'+config.max;}}}
config.increment=Math.round((config.value-config.running_value)/config.steps);if(config.increment<0)
config.increment*=-1;if(config.increment<1)
config.increment=1;var t=setInterval(function(){var pixels=config.width/100;if(config.running_value>config.value){if(config.running_value-config.increment<config.value){config.running_value=config.value;}else{config.running_value-=config.increment;}}
else if(config.running_value<config.value){if(config.running_value+config.increment>config.value){config.running_value=config.value;}else{config.running_value+=config.increment;}}
if(config.running_value==config.value)
clearInterval(t);var $bar=$("#"+config.id+"_pbImage");var $text=$("#"+config.id+"_pbText");var image=getBarImage(config);if(image!=config.image){$bar.css("background-image","url("+image+")");config.image=image;}
$bar.css("background-position",(((config.width*-1))+(getPercentage(config)*pixels))+'px 50%');$bar.attr('title',getText(config));$text.html(getText(config));if(config.callback!=null&&typeof(config.callback)=='function')
config.callback(config);pb.config=config;},config.stepDuration);});};}});$.fn.extend({progressBar:$.progressBar.construct});})(jQuery);

File diff suppressed because one or more lines are too long

365
public/scripts/main.js Normal file
View File

@@ -0,0 +1,365 @@
/**
* Ripping Cluster Webui
*
* Written by Ben Roberts
* Homepage: https://benroberts.net/projects/ripping-cluster/
* Code: https://github.com/optiz0r/ripping-cluster-webui/
*
* Dependencies:
* - Bootstrap
* - JQuery
* - JQueryUI
* - JQuery Progressbar
*
* Released under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License
* http://creativecommons.org/licenses/by-nc-sa/3.0/
*/
/**
* Ripping Cluster object
*
* Entry point for all ripping cluster webui code
*/
var rc = {
/**
* Initialises the webui code
*/
init: function() {
rc.page.init();
rc.sources.init();
rc.settings.init();
},
/**
* Page module
*
* Configures hooks for updating pages
*/
page: {
/**
* Initialises the module
*/
init: function() {
// Display pretty progress bars
sf.page.addCallback('progress-bars', function(d) {
$(d).find('.progressBar').each(
function() {
$(this).progressBar({
steps: 100,
width: 120,
height: 12,
boxImage: base_uri + 'images/jquery.progressbar/progressbar.gif',
barImage: {
0: base_uri + 'images/jquery.progressbar/progressbg_red.gif',
25: base_uri + 'images/jquery.progressbar/progressbg_orange.gif',
50: base_uri + 'images/jquery.progressbar/progressbg_yellow.gif',
75: base_uri + 'images/jquery.progressbar/progressbg_green.gif',
}
});
}
);
});
// Display highlights on given items when hovered over
sf.page.addCallback('hover-highlights', function(d) {
$(d).find('.hover-highlight').hover(
function() {
$(this).addClass('highlight');
},
function() {
$(this).removeClass('highlight');
}
);
});
// Display popovers
sf.page.addCallback('popovers', function(d) {
$(d).find('a[rel=popover]').popover({
offset: 10,
html: true,
});
});
// Configure select-all checkboxes
sf.page.addCallback('select-all-checkboxes', function(d) {
$(d).find('input[type=checkbox].select_all').click(function() {
$('input[type=checkbox].'+$(this).attr('id')).attr('checked', $(this).attr('checked') == 'checked');
});
});
// Update the content of the page on first load
sf.page.updateEvents($('#page_content'));
}
},
/**
* Sources module
*
* Contains code for interacting with rip sources
*/
sources: {
/**
* Initialises the module
*/
init: function() {
sf.actions.addAction('delete-source-confirm', function(params) {
rc.sources.remove_confirmed(params['plugin'], params['id']);
});
},
/**
* Display a confirmation box for deleting a source
*
* @param plugin Name of the plugin providing the source
* @param source Encoded filename of the source to be deleted
*/
remove: function(plugin, source) {
sf.ajax.get(base_url + "ajax/delete-source/plugin/" + plugin + "/id/" + source);
},
/**
* Permanently delete a source
*
* @param plugin Name of the plugin providing the source
* @param source Encoded filename of the source to be deleted
*/
remove_confirmed: function(plugin, source) {
sf.ajax.get(base_url + "ajax/delete-source/plugin/" + plugin + "/id/" + source + "/confirm/");
},
},
/**
* Settings module
*
* Contains code for handling the admin settings page
*/
settings: {
/**
* Configure actions for handling settings ajax requests.
*/
init: function() {
sf.actions.addAction('add-setting', function(params) {
sf.ajax.post(base_url + 'ajax/admin/add-setting/name/' + $('#'+params.name).val() + '/type/' + $('#'+params.type).val() + '/');
});
sf.actions.addAction('add_setting_row', function(params) {
$("#settings tbody").append(params.content);
});
sf.actions.addAction('rename_setting', function(params) {
sf.ajax.post(base_url + 'ajax/admin/rename-setting/name/' + params.name + '/new-name/' + $('#'+params.new_name_field).val() + '/confirm/');
});
sf.actions.addAction('rename_setting_confirm', function(params) {
$('#setting_'+params.old_id+'_row').replaceWith($(params.content));
});
sf.actions.addAction('remove_setting', function(params) {
sf.ajax.post(base_url + 'ajax/admin/remove-setting/name/' + params.name + '/');
sf.actions.trigger('remove_setting_row', params);
});
sf.actions.addAction('remove_setting_row', function(params) {
$('#setting_' + params.id + '_row').remove();
});
$("#settings_save").click(function() {
rc.settings.save();
});
$("#settings_new").click(function() {
rc.settings.new_setting();
});
},
/**
* Add a new setting to the settings list
*
* Presents a dialog box prompting for the setting details
*
*/
new_setting: function() {
sf.ajax.get(base_url + "ajax/admin/new-setting/");
},
/**
* Rename a setting
*
* Presents a dialog box prompting for the new setting name
*
* @param id DOM ID of the setting to be renamed
* @param name Name of the setting to be renamed
*/
rename_setting: function(id, name) {
sf.ajax.get(base_url + "ajax/admin/rename-setting/name/" + name + "/");
},
/**
* Removes a setting
*
* Presents a dialog to confirm removal
*
* @param id DOM ID of the setting to be removed
* @param name Name of the setting to be removed
*/
remove_setting: function(id, name) {
sf.ui.dialog.prepare({
show: true,
title: 'Remove this setting?',
content: "Do you really want to remove setting '" + name + "'",
buttons: {
type: 'okcancel',
actions: {
ok: [
'remove_setting',
'close-dialog'
],
cancel: 'close-dialog'
},
params: {
id: id,
name: name
}
}
});
},
/**
* Adds a new field to a string list setting
*
* For array(string) setting types, adds UI for a new element in the array
*
* @param id DOM ID of the setting to have a new element added
*/
add_stringlist_field: function(id) {
var container = $('#container_'+id);
var next = $('#settings_'+id+'_next');
var next_value = next.val();
var line = $('<div>');
line.attr('id', 'settings_'+id+'_line'+next.val());
line.append($('<input type="text" name="'+id+'[]" class="setting settings_field_string" />'));
line.append(' ');
var button = $('<button class="btn small settings_field_remove">-</button>');
button.click(function() {
rc.settings.remove_stringlist_field(id, next_value);
});
line.append(button);
// Add the new item
container.append(line);
// Increment the next counter
next.val(parseInt(next_value)+1);
},
/**
* Removes a field from a string list setting
*
* For array(string) setting types, removes the UI for an element in the array
*
* @param id DOM ID of the setting to be modified
* @param line Number of the line to be removed
*/
remove_stringlist_field: function(id, line) {
$("#settings_"+id+"_line"+line).remove();
},
/**
* Add a new Hash setting key
*
* For hash setting types, adds UI for a new key
*
* @param id DOM ID of the setting to be modified
*/
add_hash_field: function(id) {
var container = $('#container_'+id);
var next = $('#settings_'+id+'_next');
var next_value = next.val();
var line = $('<div>');
line.attr('id', 'settings_'+id+'_line'+next.val());
var hash_key = $('<input type="text" value="" class="small setting hash_key" />');
var hash_value = $('<input type="text" id="setting_'+id+'_value'+next_value+'" name="'+id+'[New]" class="xlarge setting hash_value" />');
hash_key.change(function() {
$('#setting_'+id+'_value'+next_value).attr('name', id+'['+$(this).val()+']');
});
line.append(hash_key).append(' ').append(hash_value).append(' ');
var button = $('<button class="btn small settings_field_remove">-</button>');
button.click(function() {
rc.settings.remove_hash_field(id, next_value);
});
line.append(button);
// Add the new item
container.append(line);
// Increment the next counter
next.val(parseInt(next_value)+1);
},
/**
* Removes a hash setting key
*
* For hash setting types, removes the UI for a specific key
*
* @param id DOM ID of the setting to be modified
* @param line Line number of the hash key to be removed
*/
remove_hash_field: function(id, line) {
$("#settings_"+id+"_line"+line).remove();
},
/**
* Saves the current setting values to the database
*
*/
save: function() {
var settings = {};
var fields = $("input.setting").get();
for (var i in fields) {
var setting = fields[i];
var name = setting.name;
var value;
switch(setting.type) {
case 'checkbox':
value = $(setting).is(':checked') ? 1 : 0;
break;
default:
value = setting.value;
}
if (/\[\]$/.test(name)) {
if (! settings[name]) {
settings[name] = [];
}
settings[name].push(value);
} else {
settings[name] = value;
}
}
sf.ajax.post(base_url + "ajax/update-settings/", settings);
}
}
};
$(document).ready(rc.init);

View File

@@ -0,0 +1 @@
../../externals/sihnon-js-lib/public

356
public/styles/3rdparty/bootstrap.min.css vendored Normal file
View File

@@ -0,0 +1,356 @@
html,body{margin:0;padding:0;}
h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,cite,code,del,dfn,em,img,q,s,samp,small,strike,strong,sub,sup,tt,var,dd,dl,dt,li,ol,ul,fieldset,form,label,legend,button,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;font-weight:normal;font-style:normal;font-size:100%;line-height:1;font-family:inherit;}
table{border-collapse:collapse;border-spacing:0;}
ol,ul{list-style:none;}
q:before,q:after,blockquote:before,blockquote:after{content:"";}
html{overflow-y:scroll;font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;}
a:focus{outline:thin dotted;}
a:hover,a:active{outline:0;}
article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;}
audio,canvas,video{display:inline-block;*display:inline;*zoom:1;}
audio:not([controls]){display:none;}
sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;}
sup{top:-0.5em;}
sub{bottom:-0.25em;}
img{border:0;-ms-interpolation-mode:bicubic;}
button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;}
button,input{line-height:normal;*overflow:visible;}
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;}
input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;}
input[type="search"]::-webkit-search-decoration{-webkit-appearance:none;}
textarea{overflow:auto;vertical-align:top;}
body{background-color:#ffffff;margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:18px;color:#404040;}
.container{width:940px;margin-left:auto;margin-right:auto;zoom:1;}.container:before,.container:after{display:table;content:"";zoom:1;}
.container:after{clear:both;}
.container-fluid{position:relative;min-width:940px;padding-left:20px;padding-right:20px;zoom:1;}.container-fluid:before,.container-fluid:after{display:table;content:"";zoom:1;}
.container-fluid:after{clear:both;}
.container-fluid>.sidebar{position:absolute;top:0;left:20px;width:220px;}
.container-fluid>.content{margin-left:240px;}
a{color:#0069d6;text-decoration:none;line-height:inherit;font-weight:inherit;}a:hover{color:#00438a;text-decoration:underline;}
.pull-right{float:right;}
.pull-left{float:left;}
.hide{display:none;}
.show{display:block;}
.row{zoom:1;margin-left:-20px;}.row:before,.row:after{display:table;content:"";zoom:1;}
.row:after{clear:both;}
.row>[class*="span"]{display:inline;float:left;margin-left:20px;}
.span1{width:40px;}
.span2{width:100px;}
.span3{width:160px;}
.span4{width:220px;}
.span5{width:280px;}
.span6{width:340px;}
.span7{width:400px;}
.span8{width:460px;}
.span9{width:520px;}
.span10{width:580px;}
.span11{width:640px;}
.span12{width:700px;}
.span13{width:760px;}
.span14{width:820px;}
.span15{width:880px;}
.span16{width:940px;}
.span17{width:1000px;}
.span18{width:1060px;}
.span19{width:1120px;}
.span20{width:1180px;}
.span21{width:1240px;}
.span22{width:1300px;}
.span23{width:1360px;}
.span24{width:1420px;}
.row>.offset1{margin-left:80px;}
.row>.offset2{margin-left:140px;}
.row>.offset3{margin-left:200px;}
.row>.offset4{margin-left:260px;}
.row>.offset5{margin-left:320px;}
.row>.offset6{margin-left:380px;}
.row>.offset7{margin-left:440px;}
.row>.offset8{margin-left:500px;}
.row>.offset9{margin-left:560px;}
.row>.offset10{margin-left:620px;}
.row>.offset11{margin-left:680px;}
.row>.offset12{margin-left:740px;}
.span-one-third{width:300px;}
.span-two-thirds{width:620px;}
.row>.offset-one-third{margin-left:340px;}
.row>.offset-two-thirds{margin-left:660px;}
p{font-size:13px;font-weight:normal;line-height:18px;margin-bottom:9px;}p small{font-size:11px;color:#bfbfbf;}
h1,h2,h3,h4,h5,h6{font-weight:bold;color:#404040;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{color:#bfbfbf;}
h1{margin-bottom:18px;font-size:30px;line-height:36px;}h1 small{font-size:18px;}
h2{font-size:24px;line-height:36px;}h2 small{font-size:14px;}
h3,h4,h5,h6{line-height:36px;}
h3{font-size:18px;}h3 small{font-size:14px;}
h4{font-size:16px;}h4 small{font-size:12px;}
h5{font-size:14px;}
h6{font-size:13px;color:#bfbfbf;text-transform:uppercase;}
ul,ol{margin:0 0 18px 25px;}
ul ul,ul ol,ol ol,ol ul{margin-bottom:0;}
ul{list-style:disc;}
ol{list-style:decimal;}
li{line-height:18px;color:#808080;}
ul.unstyled{list-style:none;margin-left:0;}
dl{margin-bottom:18px;}dl dt,dl dd{line-height:18px;}
dl dt{font-weight:bold;}
dl dd{margin-left:9px;}
hr{margin:20px 0 19px;border:0;border-bottom:1px solid #eee;}
strong{font-style:inherit;font-weight:bold;}
em{font-style:italic;font-weight:inherit;line-height:inherit;}
.muted{color:#bfbfbf;}
blockquote{margin-bottom:18px;border-left:5px solid #eee;padding-left:15px;}blockquote p{font-size:14px;font-weight:300;line-height:18px;margin-bottom:0;}
blockquote small{display:block;font-size:12px;font-weight:300;line-height:18px;color:#bfbfbf;}blockquote small:before{content:'\2014 \00A0';}
address{display:block;line-height:18px;margin-bottom:18px;}
code,pre{padding:0 3px 2px;font-family:Monaco, Andale Mono, Courier New, monospace;font-size:12px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
code{background-color:#fee9cc;color:rgba(0, 0, 0, 0.75);padding:1px 3px;}
pre{background-color:#f5f5f5;display:block;padding:8.5px;margin:0 0 18px;line-height:18px;font-size:12px;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;white-space:pre;white-space:pre-wrap;word-wrap:break-word;}
form{margin-bottom:18px;}
fieldset{margin-bottom:18px;padding-top:18px;}fieldset legend{display:block;padding-left:150px;font-size:19.5px;line-height:1;color:#404040;*padding:0 0 5px 145px;*line-height:1.5;}
form .clearfix{margin-bottom:18px;zoom:1;}form .clearfix:before,form .clearfix:after{display:table;content:"";zoom:1;}
form .clearfix:after{clear:both;}
label,input,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:normal;}
label{padding-top:6px;font-size:13px;line-height:18px;float:left;width:130px;text-align:right;color:#404040;}
form .input{margin-left:150px;}
input[type=checkbox],input[type=radio]{cursor:pointer;}
input,textarea,select,.uneditable-input{display:inline-block;width:210px;height:18px;padding:4px;font-size:13px;line-height:18px;color:#808080;border:1px solid #ccc;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
select{padding:initial;}
input[type=checkbox],input[type=radio]{width:auto;height:auto;padding:0;margin:3px 0;*margin-top:0;line-height:normal;border:none;}
input[type=file]{background-color:#ffffff;padding:initial;border:initial;line-height:initial;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
input[type=button],input[type=reset],input[type=submit]{width:auto;height:auto;}
select,input[type=file]{height:27px;*height:auto;line-height:27px;*margin-top:4px;}
select[multiple]{height:inherit;background-color:#ffffff;}
textarea{height:auto;}
.uneditable-input{background-color:#ffffff;display:block;border-color:#eee;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);cursor:not-allowed;}
:-moz-placeholder{color:#bfbfbf;}
::-webkit-input-placeholder{color:#bfbfbf;}
input,textarea{-webkit-transition:border linear 0.2s,box-shadow linear 0.2s;-moz-transition:border linear 0.2s,box-shadow linear 0.2s;-ms-transition:border linear 0.2s,box-shadow linear 0.2s;-o-transition:border linear 0.2s,box-shadow linear 0.2s;transition:border linear 0.2s,box-shadow linear 0.2s;-webkit-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1);}
input:focus,textarea:focus{outline:0;border-color:rgba(82, 168, 236, 0.8);-webkit-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1),0 0 8px rgba(82, 168, 236, 0.6);-moz-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1),0 0 8px rgba(82, 168, 236, 0.6);box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1),0 0 8px rgba(82, 168, 236, 0.6);}
input[type=file]:focus,input[type=checkbox]:focus,select:focus{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;outline:1px dotted #666;}
form .clearfix.error>label,form .clearfix.error .help-block,form .clearfix.error .help-inline{color:#b94a48;}
form .clearfix.error input,form .clearfix.error textarea{color:#b94a48;border-color:#ee5f5b;}form .clearfix.error input:focus,form .clearfix.error textarea:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7;}
form .clearfix.error .input-prepend .add-on,form .clearfix.error .input-append .add-on{color:#b94a48;background-color:#fce6e6;border-color:#b94a48;}
form .clearfix.warning>label,form .clearfix.warning .help-block,form .clearfix.warning .help-inline{color:#c09853;}
form .clearfix.warning input,form .clearfix.warning textarea{color:#c09853;border-color:#ccae64;}form .clearfix.warning input:focus,form .clearfix.warning textarea:focus{border-color:#be9a3f;-webkit-box-shadow:0 0 6px #e5d6b1;-moz-box-shadow:0 0 6px #e5d6b1;box-shadow:0 0 6px #e5d6b1;}
form .clearfix.warning .input-prepend .add-on,form .clearfix.warning .input-append .add-on{color:#c09853;background-color:#d2b877;border-color:#c09853;}
form .clearfix.success>label,form .clearfix.success .help-block,form .clearfix.success .help-inline{color:#468847;}
form .clearfix.success input,form .clearfix.success textarea{color:#468847;border-color:#57a957;}form .clearfix.success input:focus,form .clearfix.success textarea:focus{border-color:#458845;-webkit-box-shadow:0 0 6px #9acc9a;-moz-box-shadow:0 0 6px #9acc9a;box-shadow:0 0 6px #9acc9a;}
form .clearfix.success .input-prepend .add-on,form .clearfix.success .input-append .add-on{color:#468847;background-color:#bcddbc;border-color:#468847;}
.input-mini,input.mini,textarea.mini,select.mini{width:60px;}
.input-small,input.small,textarea.small,select.small{width:90px;}
.input-medium,input.medium,textarea.medium,select.medium{width:150px;}
.input-large,input.large,textarea.large,select.large{width:210px;}
.input-xlarge,input.xlarge,textarea.xlarge,select.xlarge{width:270px;}
.input-xxlarge,input.xxlarge,textarea.xxlarge,select.xxlarge{width:530px;}
textarea.xxlarge{overflow-y:auto;}
input.span1,textarea.span1{display:inline-block;float:none;width:30px;margin-left:0;}
input.span2,textarea.span2{display:inline-block;float:none;width:90px;margin-left:0;}
input.span3,textarea.span3{display:inline-block;float:none;width:150px;margin-left:0;}
input.span4,textarea.span4{display:inline-block;float:none;width:210px;margin-left:0;}
input.span5,textarea.span5{display:inline-block;float:none;width:270px;margin-left:0;}
input.span6,textarea.span6{display:inline-block;float:none;width:330px;margin-left:0;}
input.span7,textarea.span7{display:inline-block;float:none;width:390px;margin-left:0;}
input.span8,textarea.span8{display:inline-block;float:none;width:450px;margin-left:0;}
input.span9,textarea.span9{display:inline-block;float:none;width:510px;margin-left:0;}
input.span10,textarea.span10{display:inline-block;float:none;width:570px;margin-left:0;}
input.span11,textarea.span11{display:inline-block;float:none;width:630px;margin-left:0;}
input.span12,textarea.span12{display:inline-block;float:none;width:690px;margin-left:0;}
input.span13,textarea.span13{display:inline-block;float:none;width:750px;margin-left:0;}
input.span14,textarea.span14{display:inline-block;float:none;width:810px;margin-left:0;}
input.span15,textarea.span15{display:inline-block;float:none;width:870px;margin-left:0;}
input.span16,textarea.span16{display:inline-block;float:none;width:930px;margin-left:0;}
input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{background-color:#f5f5f5;border-color:#ddd;cursor:not-allowed;}
.actions{background:#f5f5f5;margin-top:18px;margin-bottom:18px;padding:17px 20px 18px 150px;border-top:1px solid #ddd;-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;}.actions .secondary-action{float:right;}.actions .secondary-action a{line-height:30px;}.actions .secondary-action a:hover{text-decoration:underline;}
.help-inline,.help-block{font-size:13px;line-height:18px;color:#bfbfbf;}
.help-inline{padding-left:5px;*position:relative;*top:-5px;}
.help-block{display:block;max-width:600px;}
.inline-inputs{color:#808080;}.inline-inputs span{padding:0 2px 0 1px;}
.input-prepend input,.input-append input{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
.input-prepend .add-on,.input-append .add-on{position:relative;background:#f5f5f5;border:1px solid #ccc;z-index:2;float:left;display:block;width:auto;min-width:16px;height:18px;padding:4px 4px 4px 5px;margin-right:-1px;font-weight:normal;line-height:18px;color:#bfbfbf;text-align:center;text-shadow:0 1px 0 #ffffff;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
.input-prepend .active,.input-append .active{background:#a9dba9;border-color:#46a546;}
.input-prepend .add-on{*margin-top:1px;}
.input-append input{float:left;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
.input-append .add-on{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;margin-right:0;margin-left:-1px;}
.inputs-list{margin:0 0 5px;width:100%;}.inputs-list li{display:block;padding:0;width:100%;}
.inputs-list label{display:block;float:none;width:auto;padding:0;margin-left:20px;line-height:18px;text-align:left;white-space:normal;}.inputs-list label strong{color:#808080;}
.inputs-list label small{font-size:11px;font-weight:normal;}
.inputs-list .inputs-list{margin-left:25px;margin-bottom:10px;padding-top:0;}
.inputs-list:first-child{padding-top:6px;}
.inputs-list li+li{padding-top:2px;}
.inputs-list input[type=radio],.inputs-list input[type=checkbox]{margin-bottom:0;margin-left:-20px;float:left;}
.form-stacked{padding-left:20px;}.form-stacked fieldset{padding-top:9px;}
.form-stacked legend{padding-left:0;}
.form-stacked label{display:block;float:none;width:auto;font-weight:bold;text-align:left;line-height:20px;padding-top:0;}
.form-stacked .clearfix{margin-bottom:9px;}.form-stacked .clearfix div.input{margin-left:0;}
.form-stacked .inputs-list{margin-bottom:0;}.form-stacked .inputs-list li{padding-top:0;}.form-stacked .inputs-list li label{font-weight:normal;padding-top:0;}
.form-stacked div.clearfix.error{padding-top:10px;padding-bottom:10px;padding-left:10px;margin-top:0;margin-left:-10px;}
.form-stacked .actions{margin-left:-20px;padding-left:20px;}
table{width:100%;margin-bottom:18px;padding:0;font-size:13px;border-collapse:collapse;}table th,table td{padding:10px 10px 9px;line-height:18px;text-align:left;}
table th{padding-top:9px;font-weight:bold;vertical-align:middle;}
table td{vertical-align:top;border-top:1px solid #ddd;}
table tbody th{border-top:1px solid #ddd;vertical-align:top;}
.condensed-table th,.condensed-table td{padding:5px 5px 4px;}
.bordered-table{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.bordered-table th+th,.bordered-table td+td,.bordered-table th+td{border-left:1px solid #ddd;}
.bordered-table thead tr:first-child th:first-child,.bordered-table tbody tr:first-child td:first-child{-webkit-border-radius:4px 0 0 0;-moz-border-radius:4px 0 0 0;border-radius:4px 0 0 0;}
.bordered-table thead tr:first-child th:last-child,.bordered-table tbody tr:first-child td:last-child{-webkit-border-radius:0 4px 0 0;-moz-border-radius:0 4px 0 0;border-radius:0 4px 0 0;}
.bordered-table tbody tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;}
.bordered-table tbody tr:last-child td:last-child{-webkit-border-radius:0 0 4px 0;-moz-border-radius:0 0 4px 0;border-radius:0 0 4px 0;}
table .span1{width:20px;}
table .span2{width:60px;}
table .span3{width:100px;}
table .span4{width:140px;}
table .span5{width:180px;}
table .span6{width:220px;}
table .span7{width:260px;}
table .span8{width:300px;}
table .span9{width:340px;}
table .span10{width:380px;}
table .span11{width:420px;}
table .span12{width:460px;}
table .span13{width:500px;}
table .span14{width:540px;}
table .span15{width:580px;}
table .span16{width:620px;}
.zebra-striped tbody tr:nth-child(odd) td,.zebra-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9;}
.zebra-striped tbody tr:hover td,.zebra-striped tbody tr:hover th{background-color:#f5f5f5;}
table .header{cursor:pointer;}table .header:after{content:"";float:right;margin-top:7px;border-width:0 4px 4px;border-style:solid;border-color:#000 transparent;visibility:hidden;}
table .headerSortUp,table .headerSortDown{background-color:rgba(141, 192, 219, 0.25);text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);}
table .header:hover:after{visibility:visible;}
table .headerSortDown:after,table .headerSortDown:hover:after{visibility:visible;filter:alpha(opacity=60);-khtml-opacity:0.6;-moz-opacity:0.6;opacity:0.6;}
table .headerSortUp:after{border-bottom:none;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid #000;visibility:visible;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;filter:alpha(opacity=60);-khtml-opacity:0.6;-moz-opacity:0.6;opacity:0.6;}
table .blue{color:#049cdb;border-bottom-color:#049cdb;}
table .headerSortUp.blue,table .headerSortDown.blue{background-color:#ade6fe;}
table .green{color:#46a546;border-bottom-color:#46a546;}
table .headerSortUp.green,table .headerSortDown.green{background-color:#cdeacd;}
table .red{color:#9d261d;border-bottom-color:#9d261d;}
table .headerSortUp.red,table .headerSortDown.red{background-color:#f4c8c5;}
table .yellow{color:#ffc40d;border-bottom-color:#ffc40d;}
table .headerSortUp.yellow,table .headerSortDown.yellow{background-color:#fff6d9;}
table .orange{color:#f89406;border-bottom-color:#f89406;}
table .headerSortUp.orange,table .headerSortDown.orange{background-color:#fee9cc;}
table .purple{color:#7a43b6;border-bottom-color:#7a43b6;}
table .headerSortUp.purple,table .headerSortDown.purple{background-color:#e2d5f0;}
.topbar{height:40px;position:fixed;top:0;left:0;right:0;z-index:10000;overflow:visible;}.topbar a{color:#bfbfbf;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);}
.topbar h3 a:hover,.topbar .brand:hover,.topbar ul .active>a{background-color:#333;background-color:rgba(255, 255, 255, 0.05);color:#ffffff;text-decoration:none;}
.topbar h3{position:relative;}
.topbar h3 a,.topbar .brand{float:left;display:block;padding:8px 20px 12px;margin-left:-20px;color:#ffffff;font-size:20px;font-weight:200;line-height:1;}
.topbar p{margin:0;line-height:40px;}.topbar p a:hover{background-color:transparent;color:#ffffff;}
.topbar form{float:left;margin:5px 0 0 0;position:relative;filter:alpha(opacity=100);-khtml-opacity:1;-moz-opacity:1;opacity:1;}
.topbar form.pull-right{float:right;}
.topbar input{background-color:#444;background-color:rgba(255, 255, 255, 0.3);font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:normal;font-weight:13px;line-height:1;padding:4px 9px;color:#ffffff;color:rgba(255, 255, 255, 0.75);border:1px solid #111;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.25);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.25);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.25);-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none;}.topbar input:-moz-placeholder{color:#e6e6e6;}
.topbar input::-webkit-input-placeholder{color:#e6e6e6;}
.topbar input:hover{background-color:#bfbfbf;background-color:rgba(255, 255, 255, 0.5);color:#ffffff;}
.topbar input:focus,.topbar input.focused{outline:0;background-color:#ffffff;color:#404040;text-shadow:0 1px 0 #ffffff;border:0;padding:5px 10px;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);}
.topbar-inner,.topbar .fill{background-color:#222;background-color:#222222;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#333333), to(#222222));background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #333333), color-stop(100%, #222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);}
.topbar div>ul,.nav{display:block;float:left;margin:0 10px 0 0;position:relative;left:0;}.topbar div>ul>li,.nav>li{display:block;float:left;}
.topbar div>ul a,.nav a{display:block;float:none;padding:10px 10px 11px;line-height:19px;text-decoration:none;}.topbar div>ul a:hover,.nav a:hover{color:#ffffff;text-decoration:none;}
.topbar div>ul .active>a,.nav .active>a{background-color:#222;background-color:rgba(0, 0, 0, 0.5);}
.topbar div>ul.secondary-nav,.nav.secondary-nav{float:right;margin-left:10px;margin-right:0;}.topbar div>ul.secondary-nav .menu-dropdown,.nav.secondary-nav .menu-dropdown,.topbar div>ul.secondary-nav .dropdown-menu,.nav.secondary-nav .dropdown-menu{right:0;border:0;}
.topbar div>ul a.menu:hover,.nav a.menu:hover,.topbar div>ul li.open .menu,.nav li.open .menu,.topbar div>ul .dropdown-toggle:hover,.nav .dropdown-toggle:hover,.topbar div>ul .dropdown.open .dropdown-toggle,.nav .dropdown.open .dropdown-toggle{background:#444;background:rgba(255, 255, 255, 0.05);}
.topbar div>ul .menu-dropdown,.nav .menu-dropdown,.topbar div>ul .dropdown-menu,.nav .dropdown-menu{background-color:#333;}.topbar div>ul .menu-dropdown a.menu,.nav .menu-dropdown a.menu,.topbar div>ul .dropdown-menu a.menu,.nav .dropdown-menu a.menu,.topbar div>ul .menu-dropdown .dropdown-toggle,.nav .menu-dropdown .dropdown-toggle,.topbar div>ul .dropdown-menu .dropdown-toggle,.nav .dropdown-menu .dropdown-toggle{color:#ffffff;}.topbar div>ul .menu-dropdown a.menu.open,.nav .menu-dropdown a.menu.open,.topbar div>ul .dropdown-menu a.menu.open,.nav .dropdown-menu a.menu.open,.topbar div>ul .menu-dropdown .dropdown-toggle.open,.nav .menu-dropdown .dropdown-toggle.open,.topbar div>ul .dropdown-menu .dropdown-toggle.open,.nav .dropdown-menu .dropdown-toggle.open{background:#444;background:rgba(255, 255, 255, 0.05);}
.topbar div>ul .menu-dropdown li a,.nav .menu-dropdown li a,.topbar div>ul .dropdown-menu li a,.nav .dropdown-menu li a{color:#999;text-shadow:0 1px 0 rgba(0, 0, 0, 0.5);}.topbar div>ul .menu-dropdown li a:hover,.nav .menu-dropdown li a:hover,.topbar div>ul .dropdown-menu li a:hover,.nav .dropdown-menu li a:hover{background-color:#191919;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#292929), to(#191919));background-image:-moz-linear-gradient(top, #292929, #191919);background-image:-ms-linear-gradient(top, #292929, #191919);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #292929), color-stop(100%, #191919));background-image:-webkit-linear-gradient(top, #292929, #191919);background-image:-o-linear-gradient(top, #292929, #191919);background-image:linear-gradient(top, #292929, #191919);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#292929', endColorstr='#191919', GradientType=0);color:#ffffff;}
.topbar div>ul .menu-dropdown .active a,.nav .menu-dropdown .active a,.topbar div>ul .dropdown-menu .active a,.nav .dropdown-menu .active a{color:#ffffff;}
.topbar div>ul .menu-dropdown .divider,.nav .menu-dropdown .divider,.topbar div>ul .dropdown-menu .divider,.nav .dropdown-menu .divider{background-color:#222;border-color:#444;}
.topbar ul .menu-dropdown li a,.topbar ul .dropdown-menu li a{padding:4px 15px;}
li.menu,.dropdown{position:relative;}
a.menu:after,.dropdown-toggle:after{width:0;height:0;display:inline-block;content:"&darr;";text-indent:-99999px;vertical-align:top;margin-top:8px;margin-left:4px;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid #ffffff;filter:alpha(opacity=50);-khtml-opacity:0.5;-moz-opacity:0.5;opacity:0.5;}
.menu-dropdown,.dropdown-menu{background-color:#ffffff;float:left;display:none;position:absolute;top:40px;z-index:900;min-width:160px;max-width:220px;_width:160px;margin-left:0;margin-right:0;padding:6px 0;zoom:1;border-color:#999;border-color:rgba(0, 0, 0, 0.2);border-style:solid;border-width:0 1px 1px;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:0 2px 4px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 2px 4px rgba(0, 0, 0, 0.2);box-shadow:0 2px 4px rgba(0, 0, 0, 0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.menu-dropdown li,.dropdown-menu li{float:none;display:block;background-color:none;}
.menu-dropdown .divider,.dropdown-menu .divider{height:1px;margin:5px 0;overflow:hidden;background-color:#eee;border-bottom:1px solid #ffffff;}
.topbar .dropdown-menu a,.dropdown-menu a{display:block;padding:4px 15px;clear:both;font-weight:normal;line-height:18px;color:#808080;text-shadow:0 1px 0 #ffffff;}.topbar .dropdown-menu a:hover,.dropdown-menu a:hover,.topbar .dropdown-menu a.hover,.dropdown-menu a.hover{background-color:#dddddd;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#eeeeee), to(#dddddd));background-image:-moz-linear-gradient(top, #eeeeee, #dddddd);background-image:-ms-linear-gradient(top, #eeeeee, #dddddd);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #eeeeee), color-stop(100%, #dddddd));background-image:-webkit-linear-gradient(top, #eeeeee, #dddddd);background-image:-o-linear-gradient(top, #eeeeee, #dddddd);background-image:linear-gradient(top, #eeeeee, #dddddd);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#dddddd', GradientType=0);color:#404040;text-decoration:none;-webkit-box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.025),inset 0 -1px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.025),inset 0 -1px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.025),inset 0 -1px rgba(0, 0, 0, 0.025);}
.open .menu,.dropdown.open .menu,.open .dropdown-toggle,.dropdown.open .dropdown-toggle{color:#ffffff;background:#ccc;background:rgba(0, 0, 0, 0.3);}
.open .menu-dropdown,.dropdown.open .menu-dropdown,.open .dropdown-menu,.dropdown.open .dropdown-menu{display:block;}
.tabs,.pills{margin:0 0 18px;padding:0;list-style:none;zoom:1;}.tabs:before,.pills:before,.tabs:after,.pills:after{display:table;content:"";zoom:1;}
.tabs:after,.pills:after{clear:both;}
.tabs>li,.pills>li{float:left;}.tabs>li>a,.pills>li>a{display:block;}
.tabs{border-color:#ddd;border-style:solid;border-width:0 0 1px;}.tabs>li{position:relative;margin-bottom:-1px;}.tabs>li>a{padding:0 15px;margin-right:2px;line-height:34px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}.tabs>li>a:hover{text-decoration:none;background-color:#eee;border-color:#eee #eee #ddd;}
.tabs .active>a,.tabs .active>a:hover{color:#808080;background-color:#ffffff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default;}
.tabs .menu-dropdown,.tabs .dropdown-menu{top:35px;border-width:1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px;}
.tabs a.menu:after,.tabs .dropdown-toggle:after{border-top-color:#999;margin-top:15px;margin-left:5px;}
.tabs li.open.menu .menu,.tabs .open.dropdown .dropdown-toggle{border-color:#999;}
.tabs li.open a.menu:after,.tabs .dropdown.open .dropdown-toggle:after{border-top-color:#555;}
.pills a{margin:5px 3px 5px 0;padding:0 15px;line-height:30px;text-shadow:0 1px 1px #ffffff;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;}.pills a:hover{color:#ffffff;text-decoration:none;text-shadow:0 1px 1px rgba(0, 0, 0, 0.25);background-color:#00438a;}
.pills .active a{color:#ffffff;text-shadow:0 1px 1px rgba(0, 0, 0, 0.25);background-color:#0069d6;}
.pills-vertical>li{float:none;}
.tab-content>.tab-pane,.pill-content>.pill-pane,.tab-content>div,.pill-content>div{display:none;}
.tab-content>.active,.pill-content>.active{display:block;}
.breadcrumb{padding:7px 14px;margin:0 0 18px;background-color:#f5f5f5;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#ffffff), to(#f5f5f5));background-image:-moz-linear-gradient(top, #ffffff, #f5f5f5);background-image:-ms-linear-gradient(top, #ffffff, #f5f5f5);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #f5f5f5));background-image:-webkit-linear-gradient(top, #ffffff, #f5f5f5);background-image:-o-linear-gradient(top, #ffffff, #f5f5f5);background-image:linear-gradient(top, #ffffff, #f5f5f5);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0);border:1px solid #ddd;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;}.breadcrumb li{display:inline;text-shadow:0 1px 0 #ffffff;}
.breadcrumb .divider{padding:0 5px;color:#bfbfbf;}
.breadcrumb .active a{color:#404040;}
.hero-unit{background-color:#f5f5f5;margin-bottom:30px;padding:60px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;}
.hero-unit p{font-size:18px;font-weight:200;line-height:27px;}
footer{margin-top:17px;padding-top:17px;border-top:1px solid #eee;}
.page-header{margin-bottom:17px;border-bottom:1px solid #ddd;-webkit-box-shadow:0 1px 0 rgba(255, 255, 255, 0.5);-moz-box-shadow:0 1px 0 rgba(255, 255, 255, 0.5);box-shadow:0 1px 0 rgba(255, 255, 255, 0.5);}.page-header h1{margin-bottom:8px;}
.btn.danger,.alert-message.danger,.btn.danger:hover,.alert-message.danger:hover,.btn.error,.alert-message.error,.btn.error:hover,.alert-message.error:hover,.btn.success,.alert-message.success,.btn.success:hover,.alert-message.success:hover,.btn.info,.alert-message.info,.btn.info:hover,.alert-message.info:hover{color:#ffffff;}
.btn .close,.alert-message .close{font-family:Arial,sans-serif;line-height:18px;}
.btn.danger,.alert-message.danger,.btn.error,.alert-message.error{background-color:#c43c35;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35));background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-ms-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(top, #ee5f5b, #c43c35);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#c43c35 #c43c35 #882a25;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);}
.btn.success,.alert-message.success{background-color:#57a957;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957));background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-ms-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(top, #62c462, #57a957);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#57a957 #57a957 #3d773d;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);}
.btn.info,.alert-message.info{background-color:#339bb9;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9));background-image:-moz-linear-gradient(top, #5bc0de, #339bb9);background-image:-ms-linear-gradient(top, #5bc0de, #339bb9);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9));background-image:-webkit-linear-gradient(top, #5bc0de, #339bb9);background-image:-o-linear-gradient(top, #5bc0de, #339bb9);background-image:linear-gradient(top, #5bc0de, #339bb9);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#339bb9 #339bb9 #22697d;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);}
.btn{cursor:pointer;display:inline-block;background-color:#e6e6e6;background-repeat:no-repeat;background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:-moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6);background-image:-ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:-o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);padding:5px 14px 6px;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);color:#333;font-size:13px;line-height:normal;border:1px solid #ccc;border-bottom-color:#bbb;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-webkit-transition:0.1s linear all;-moz-transition:0.1s linear all;-ms-transition:0.1s linear all;-o-transition:0.1s linear all;transition:0.1s linear all;}.btn:hover{background-position:0 -15px;color:#333;text-decoration:none;}
.btn:focus{outline:1px dotted #666;}
.btn.primary{color:#ffffff;background-color:#0064cd;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd));background-image:-moz-linear-gradient(top, #049cdb, #0064cd);background-image:-ms-linear-gradient(top, #049cdb, #0064cd);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd));background-image:-webkit-linear-gradient(top, #049cdb, #0064cd);background-image:-o-linear-gradient(top, #049cdb, #0064cd);background-image:linear-gradient(top, #049cdb, #0064cd);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#049cdb', endColorstr='#0064cd', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#0064cd #0064cd #003f81;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);}
.btn.active,.btn:active{-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.25),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.25),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.25),0 1px 2px rgba(0, 0, 0, 0.05);}
.btn.disabled{cursor:default;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=65);-khtml-opacity:0.65;-moz-opacity:0.65;opacity:0.65;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
.btn[disabled]{cursor:default;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=65);-khtml-opacity:0.65;-moz-opacity:0.65;opacity:0.65;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
.btn.large{font-size:15px;line-height:normal;padding:9px 14px 9px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}
.btn.small{padding:7px 9px 7px;font-size:11px;}
:root .alert-message,:root .btn{border-radius:0 \0;}
button.btn::-moz-focus-inner,input[type=submit].btn::-moz-focus-inner{padding:0;border:0;}
.close{float:right;color:#000000;font-size:20px;font-weight:bold;line-height:13.5px;text-shadow:0 1px 0 #ffffff;filter:alpha(opacity=25);-khtml-opacity:0.25;-moz-opacity:0.25;opacity:0.25;}.close:hover{color:#000000;text-decoration:none;filter:alpha(opacity=40);-khtml-opacity:0.4;-moz-opacity:0.4;opacity:0.4;}
.alert-message{position:relative;padding:7px 15px;margin-bottom:18px;color:#404040;background-color:#eedc94;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#fceec1), to(#eedc94));background-image:-moz-linear-gradient(top, #fceec1, #eedc94);background-image:-ms-linear-gradient(top, #fceec1, #eedc94);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fceec1), color-stop(100%, #eedc94));background-image:-webkit-linear-gradient(top, #fceec1, #eedc94);background-image:-o-linear-gradient(top, #fceec1, #eedc94);background-image:linear-gradient(top, #fceec1, #eedc94);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', endColorstr='#eedc94', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#eedc94 #eedc94 #e4c652;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);border-width:1px;border-style:solid;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.25);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.25);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.25);}.alert-message .close{margin-top:1px;*margin-top:0;}
.alert-message a{font-weight:bold;color:#404040;}
.alert-message.danger p a,.alert-message.error p a,.alert-message.success p a,.alert-message.info p a{color:#ffffff;}
.alert-message h5{line-height:18px;}
.alert-message p{margin-bottom:0;}
.alert-message div{margin-top:5px;margin-bottom:2px;line-height:28px;}
.alert-message .btn{-webkit-box-shadow:0 1px 0 rgba(255, 255, 255, 0.25);-moz-box-shadow:0 1px 0 rgba(255, 255, 255, 0.25);box-shadow:0 1px 0 rgba(255, 255, 255, 0.25);}
.alert-message.block-message{background-image:none;background-color:#fdf5d9;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);padding:14px;border-color:#fceec1;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}.alert-message.block-message ul,.alert-message.block-message p{margin-right:30px;}
.alert-message.block-message ul{margin-bottom:0;}
.alert-message.block-message li{color:#404040;}
.alert-message.block-message .alert-actions{margin-top:5px;}
.alert-message.block-message.error,.alert-message.block-message.success,.alert-message.block-message.info{color:#404040;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);}
.alert-message.block-message.error{background-color:#fddfde;border-color:#fbc7c6;}
.alert-message.block-message.success{background-color:#d1eed1;border-color:#bfe7bf;}
.alert-message.block-message.info{background-color:#ddf4fb;border-color:#c6edf9;}
.alert-message.block-message.danger p a,.alert-message.block-message.error p a,.alert-message.block-message.success p a,.alert-message.block-message.info p a{color:#404040;}
.pagination{height:36px;margin:18px 0;}.pagination ul{float:left;margin:0;border:1px solid #ddd;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);}
.pagination li{display:inline;}
.pagination a{float:left;padding:0 14px;line-height:34px;border-right:1px solid;border-right-color:#ddd;border-right-color:rgba(0, 0, 0, 0.15);*border-right-color:#ddd;text-decoration:none;}
.pagination a:hover,.pagination .active a{background-color:#c7eefe;}
.pagination .disabled a,.pagination .disabled a:hover{background-color:transparent;color:#bfbfbf;}
.pagination .next a{border:0;}
.well{background-color:#f5f5f5;margin-bottom:20px;padding:19px;min-height:20px;border:1px solid #eee;border:1px solid rgba(0, 0, 0, 0.05);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);}.well blockquote{border-color:#ddd;border-color:rgba(0, 0, 0, 0.15);}
.modal-backdrop{background-color:#000000;position:fixed;top:0;left:0;right:0;bottom:0;z-index:10000;}.modal-backdrop.fade{opacity:0;}
.modal-backdrop,.modal-backdrop.fade.in{filter:alpha(opacity=80);-khtml-opacity:0.8;-moz-opacity:0.8;opacity:0.8;}
.modal{position:fixed;top:50%;left:50%;z-index:11000;width:560px;margin:-250px 0 0 -280px;background-color:#ffffff;border:1px solid #999;border:1px solid rgba(0, 0, 0, 0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.modal .close{margin-top:7px;}
.modal.fade{-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-ms-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;}
.modal.fade.in{top:50%;}
.modal-header{border-bottom:1px solid #eee;padding:5px 15px;}
.modal-body{padding:15px;}
.modal-body form{margin-bottom:0;}
.modal-footer{background-color:#f5f5f5;padding:14px 15px 15px;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;zoom:1;margin-bottom:0;}.modal-footer:before,.modal-footer:after{display:table;content:"";zoom:1;}
.modal-footer:after{clear:both;}
.modal-footer .btn{float:right;margin-left:5px;}
.modal .popover,.modal .twipsy{z-index:12000;}
.twipsy{display:block;position:absolute;visibility:visible;padding:5px;font-size:11px;z-index:1000;filter:alpha(opacity=80);-khtml-opacity:0.8;-moz-opacity:0.8;opacity:0.8;}.twipsy.fade.in{filter:alpha(opacity=80);-khtml-opacity:0.8;-moz-opacity:0.8;opacity:0.8;}
.twipsy.above .twipsy-arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
.twipsy.left .twipsy-arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;}
.twipsy.below .twipsy-arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;}
.twipsy.right .twipsy-arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;}
.twipsy-inner{padding:3px 8px;background-color:#000000;color:white;text-align:center;max-width:200px;text-decoration:none;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
.twipsy-arrow{position:absolute;width:0;height:0;}
.popover{position:absolute;top:0;left:0;z-index:1000;padding:5px;display:none;}.popover.above .arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
.popover.right .arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;}
.popover.below .arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;}
.popover.left .arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;}
.popover .arrow{position:absolute;width:0;height:0;}
.popover .inner{background:#000000;background:rgba(0, 0, 0, 0.8);padding:3px;overflow:hidden;width:280px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);}
.popover .title{background-color:#f5f5f5;padding:9px 15px;line-height:1;-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;border-bottom:1px solid #eee;}
.popover .content{background-color:#ffffff;padding:14px;-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.popover .content p,.popover .content ul,.popover .content ol{margin-bottom:0;}
.fade{-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-ms-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear;opacity:0;}.fade.in{opacity:1;}
.label{padding:1px 3px 2px;font-size:9.75px;font-weight:bold;color:#ffffff;text-transform:uppercase;white-space:nowrap;background-color:#bfbfbf;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}.label.important{background-color:#c43c35;}
.label.warning{background-color:#f89406;}
.label.success{background-color:#46a546;}
.label.notice{background-color:#62cffc;}
.media-grid{margin-left:-20px;margin-bottom:0;zoom:1;}.media-grid:before,.media-grid:after{display:table;content:"";zoom:1;}
.media-grid:after{clear:both;}
.media-grid li{display:inline;}
.media-grid a{float:left;padding:4px;margin:0 0 18px 20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);}.media-grid a img{display:block;}
.media-grid a:hover{border-color:#0069d6;-webkit-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);-moz-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);}

View File

@@ -0,0 +1,63 @@
.asmContainer {
/* container that surrounds entire asmSelect widget */
}
.asmSelect {
/* the newly created regular 'select' */
display: inline;
}
.asmOptionDisabled {
/* disabled options in new select */
color: #999;
}
.asmHighlight {
/* the highlight span */
padding: 0;
margin: 0 0 0 1em;
}
.asmList {
/* html list that contains selected items */
margin: 0.25em 0 1em 0;
position: relative;
display: block;
padding-left: 0;
list-style: none;
}
.asmListItem {
/* li item from the html list above */
position: relative;
margin-left: 0;
padding-left: 0;
list-style: none;
background: #ddd;
border: 1px solid #bbb;
width: 100%;
margin: 0 0 -1px 0;
line-height: 1em;
}
.asmListItem:hover {
background-color: #e5e5e5;
}
.asmListItemLabel {
/* this is a span that surrounds the text in the item, except for the remove link */
padding: 5px;
display: block;
}
.asmListSortable .asmListItemLabel {
cursor: move;
}
.asmListItemRemove {
/* the remove link in each list item */
position: absolute;
right: 0;
top: 0;
padding: 5px;
}

76
public/styles/normal.css Normal file
View File

@@ -0,0 +1,76 @@
/**
* StatusBoard normal stylesheet
*
*/
@import url('http://twitter.github.com/bootstrap/1.4.0/bootstrap.min.css');
@CHARSET "UTF-8";
@media all {
body {
margin: 0em;
margin-top: 60px;
padding-top: 40px;
padding: 0em;
font-family: verdana, helvetica, sans-serif;
/* background: #F7F7F7;*/
}
a {
color: gray;
}
label {
margin-left: 1em;
margin-right: 1em;
}
footer {
padding: 2em;
font-size: smaller;
font-style: italic;
color: #333333;
text-align: center;
}
.dialog-footer-buttonset {
display: none;
}
.dialog-footer-buttonset fieldset {
padding-top: 0;
padding-bottom: 0;
margin-bottom: 0;
}
.default {
background: beige;
color: darkgray;
font-style: italic;
}
.icon {
height: 16px;
width: 16px;
}
#quantizer-slider {
width: 10em;
margin: 0.5em;
}
.highlight {
background: #dceaf4;
}
}
@media print {
.no-print {
display: none;
}
body {
margin: 0;
}
}

View File

@@ -30,11 +30,14 @@ class Net_Gearman_Job_HandBrake extends Net_Gearman_Job_Common implements Rippin
$this->job = RippingCluster_Job::fromId($args['rip_options']['id']);
// Substitute a temporary output filename into the rip options
$args['temp_output_filename'] = tempnam($config->get('rips.temp_dir', '/tmp'), 'hbr_');
$handbrake_cmd_raw = array(
'-n', $config->get('rips.nice'),
$config->get('rips.handbrake_binary'),
self::evaluateOption($args['rip_options'], 'input_filename', '-i'),
self::evaluateOption($args['rip_options'], 'output_filename', '-o'),
self::evaluateOption($args, 'temp_output_filename', '-o'),
self::evaluateOption($args['rip_options'], 'title'),
self::evaluateOption($args['rip_options'], 'format', '-f'),
self::evaluateOption($args['rip_options'], 'video_codec', '-e'),
@@ -61,12 +64,34 @@ class Net_Gearman_Job_HandBrake extends Net_Gearman_Job_Common implements Rippin
list($return_val, $stdout, $stderr) = RippingCluster_ForegroundTask::execute($handbrake_cmd, null, null, null, array($this, 'callbackOutput'), array($this, 'callbackOutput'), $this);
if ($return_val) {
$this->fail($return_val);
// Remove any temporary output files
if (file_exists($args['temp_output_filename'])) {
$result = unlink($args['temp_output_filename']);
if (!$result) {
RippingCluster_WorkerLogEntry::warning($log, $this->job->id(), "Failed to remove temporary output file, still exists at '{$args['temp_output_filename']}'.");
}
}
$this->fail("Call to HandBrake failed with return code {$return_val}.");
} else {
$this->job->updateStatus(RippingCluster_JobStatus::COMPLETE);
$this->complete( array(
'id' => $this->job->id()
));
// Copy the temporary output file to the desired destination
$move = copy($args['temp_output_filename'], $args['rip_options']['output_filename']);
if ($move) {
// Remove the temporary output file
$result = unlink($args['temp_output_filename']);
if (!$result) {
RippingCluster_WorkerLogEntry::warning($log, $this->job->id(), "Failed to remove temporary output file, still exists at '{$args['temp_output_filename']}'.");
}
//Report success
$this->job->updateStatus(RippingCluster_JobStatus::COMPLETE);
$this->complete( array(
'id' => $this->job->id()
));
} else {
RippingCluster_WorkerLogEntry::error($log, $this->job->id(), "Failed to copy temporary output file to proper destination. File retained as '{$args['temp_output_filename']}'.");
$this->job->updateStatus(RippingCluster_JobStatus::FAILED);
$this->fail('Encode complete, but output file could not be copied to the correct place.');
}
}
}
@@ -108,7 +133,7 @@ class Net_Gearman_Job_HandBrake extends Net_Gearman_Job_Common implements Rippin
$status = $rip->job->currentStatus();
$status->updateRipProgress($matches[1]);
$this->status($matches[1], 100);
} else {
} else if (!preg_match('/^\s+$/', $line)) {
$log = RippingCluster_Main::instance()->log();
RippingCluster_WorkerLogEntry::debug($log, $rip->job->id(), $line);
}

View File

@@ -183,10 +183,12 @@ class RippingCluster_Job {
$database = RippingCluster_Main::instance()->database();
$database->insert(
'INSERT INTO jobs
(id,name,source,destination,title,format,video_codec,video_width,video_height,quantizer,deinterlace,audio_tracks,audio_codecs,audio_names,subtitle_tracks)
VALUES(NULL,:name,:source,:destination,:title,:format,:video_codec,:video_width,:video_height,:quantizer,:deinterlace,:audio_tracks,:audio_codecs,:audio_names,:subtitle_tracks)',
(id,name,source_plugin,rip_plugin,source,destination,title,format,video_codec,video_width,video_height,quantizer,deinterlace,audio_tracks,audio_codecs,audio_names,subtitle_tracks)
VALUES(NULL,:name,:source_plugin,:rip_plugin,:source,:destination,:title,:format,:video_codec,:video_width,:video_height,:quantizer,:deinterlace,:audio_tracks,:audio_codecs,:audio_names,:subtitle_tracks)',
array(
array('name' => 'name', 'value' => $this->name, 'type' => PDO::PARAM_STR),
array('name' => 'source_plugin', 'value' => $this->source_plugin, 'type' => PDO::PARAM_STR),
array('name' => 'rip_plugin', 'value' => $this->rip_plugin, 'type' => PDO::PARAM_STR),
array('name' => 'source', 'value' => $this->source_filename, 'type' => PDO::PARAM_STR),
array('name' => 'destination', 'value' => $this->destination_filename, 'type' => PDO::PARAM_STR),
array('name' => 'title', 'value' => $this->title, 'type' => PDO::PARAM_INT),
@@ -242,7 +244,10 @@ class RippingCluster_Job {
'subtitle_tracks' => $this->subtitle_tracks,
);
return array('HandBrake', array('rip_options' => $rip_options));
return array(
'method' => 'HandBrake',
'rip_options' => $rip_options
);
}
protected function loadStatuses() {
@@ -271,7 +276,20 @@ class RippingCluster_Job {
return $new_status;
}
public function isFinished() {
$current_status = $this->currentStatus()->status();
return ($current_status == RippingCluster_JobStatus::COMPLETE || $current_status == RippingCluster_JobStatus::FAILED);
}
public function outputFilesize() {
if (file_exists($this->destination_filename)) {
return filesize($this->destination_filename);
}
return null;
}
public function calculateETA() {
$current_status = $this->currentStatus();
if ($current_status->status() != RippingCluster_JobStatus::RUNNING) {
@@ -319,6 +337,14 @@ class RippingCluster_Job {
public function name() {
return $this->name;
}
public function sourcePlugin() {
return $this->source_plugin;
}
public function ripPlugin() {
return $this->rip_plugin;
}
public function sourceFilename() {
return $this->source_filename;
@@ -327,13 +353,22 @@ class RippingCluster_Job {
public function destinationFilename() {
return $this->destination_filename;
}
public function destinationFileBasename() {
return basename($this->destination_filename);
}
public function title() {
return $this->title;
}
public static function runAllJobs() {
RippingCluster_BackgroundTask::run('/usr/bin/php ' . RippingCluster_Main::makeAbsolutePath('run-jobs.php'));
$env = $_ENV;
if (isset($_SERVER['RIPPING_CLUSTER_CONFIG'])) {
$env['RIPPING_CLUSTER_CONFIG'] = $_SERVER['RIPPING_CLUSTER_CONFIG'];
}
RippingCluster_BackgroundTask::run('/usr/bin/php ' . RippingCluster_Main::makeAbsolutePath('../source/webui/run-jobs.php'), null, $env);
}
};

View File

@@ -61,7 +61,7 @@ class RippingCluster_JobStatus {
$statuses = array();
$database = RippingCluster_Main::instance()->database();
foreach ($database->selectList('SELECT * FROM job_status WHERE job_id=:job_id ORDER BY mtime ASC', array(
foreach ($database->selectList('SELECT * FROM job_status WHERE job_id=:job_id ORDER BY id ASC', array(
array('name' => 'job_id', 'value' => $job->id(), 'type' => PDO::PARAM_INT),
)) as $row) {
$statuses[] = RippingCluster_JobStatus::fromDatabaseRow($row);

View File

@@ -4,12 +4,20 @@ class RippingCluster_LogEntry extends SihnonFramework_LogEntry {
protected $job_id;
protected static $types;
public static function initialise() {
self::$types['job_id'] = 'int';
// Copy the list of datatypes from the parent
// We can't modify it in place, else we'll break any logging done inside the SihnonFramework tree
// or other subclass trees.
static::$types = parent::$types;
// Add the new data types for this subclass
static::$types['job_id'] = 'int';
}
protected function __construct($level, $category, $ctime, $pid, $file, $line, $message, $job_id) {
parent::__construct($level, $category, $ctime, $pid, $file, $line, $message);
protected function __construct($level, $category, $ctime, $hostname, $progname, $pid, $file, $line, $message, $job_id) {
parent::__construct($level, $category, $ctime, $hostname, $progname, $pid, $file, $line, $message);
$this->job_id = $job_id;
}
@@ -19,6 +27,8 @@ class RippingCluster_LogEntry extends SihnonFramework_LogEntry {
$row['level'],
$row['category'],
$row['ctime'],
$row['hostname'],
$row['progname'],
$row['pid'],
$row['file'],
$row['line'],
@@ -32,8 +42,8 @@ class RippingCluster_LogEntry extends SihnonFramework_LogEntry {
$this->level,
$this->category,
$this->ctime,
static::$hostname,
static::$progname,
$this->hostname,
$this->progname,
$this->pid,
$this->file,
$this->line,
@@ -48,7 +58,7 @@ class RippingCluster_LogEntry extends SihnonFramework_LogEntry {
protected static function log($logger, $severity, $job_id, $message, $category = SihnonFramework_Log::CATEGORY_DEFAULT) {
$backtrace = debug_backtrace(false);
$entry = new self($severity, $category, time(), getmypid(), $backtrace[1]['file'], $backtrace[1]['line'], $message, $job_id);
$entry = new self($severity, $category, time(), static::$localHostname, static::$localProgname, getmypid(), $backtrace[1]['file'], $backtrace[1]['line'], $message, $job_id);
$logger->log($entry);
}

View File

@@ -4,6 +4,9 @@ require 'smarty/Smarty.class.php';
class RippingCluster_Main extends SihnonFramework_Main {
const TEMPLATE_DIR = '../source/webui/templates/';
const CODE_DIR = '../source/webui/pages/';
protected static $instance;
protected $smarty;
@@ -11,24 +14,38 @@ class RippingCluster_Main extends SihnonFramework_Main {
protected function __construct() {
parent::__construct();
}
protected function init() {
parent::init();
$request_string = isset($_GET['l']) ? $_GET['l'] : '';
$this->request = new RippingCluster_RequestParser($request_string);
if (HBC_File == 'index') {
$this->smarty = new Smarty();
$this->smarty->template_dir = './source/templates';
$this->smarty->compile_dir = './tmp/templates';
$this->smarty->cache_dir = './tmp/cache';
$this->smarty->config_dir = './config';
$this->smarty->registerPlugin('modifier', 'formatDuration', array('RippingCluster_Main', 'formatDuration'));
$this->smarty->assign('version', '0.1');
$this->smarty->assign('messages', array());
$this->request = new RippingCluster_RequestParser($request_string, self::TEMPLATE_DIR, self::CODE_DIR);
$this->smarty->assign('base_uri', $this->base_uri);
switch (HBC_File) {
case 'ajax':
case 'index': {
$smarty_tmp = $this->config->get('templates.tmp_path', '/var/tmp/ripping-cluster');
$this->smarty = new Smarty();
$this->smarty->template_dir = static::makeAbsolutePath(self::TEMPLATE_DIR);
$this->smarty->compile_dir = static::makeAbsolutePath($smarty_tmp . '/templates');
$this->smarty->cache_dir = static::makeAbsolutePath($smarty_tmp . '/cache');
$this->smarty->config_dir = static::makeAbsolutePath($smarty_tmp . '/config');
$this->smarty->addPluginsDir(static::makeAbsolutePath('../source/webui/smarty/plugins'));
$this->smarty->registerPlugin('modifier', 'formatDuration', array('RippingCluster_Main', 'formatDuration'));
$this->smarty->registerPlugin('modifier', 'formatFilesize', array('RippingCluster_Main', 'formatFilesize'));
$this->smarty->assign('version', '0.4.1');
$this->smarty->assign('messages', array());
$this->smarty->assign('base_uri', $this->base_uri);
$this->smarty->assign('base_url', static::absoluteUrl(''));
$this->smarty->assign('title', 'Ripping Cluster WebUI');
} break;
}
}
@@ -37,14 +54,14 @@ class RippingCluster_Main extends SihnonFramework_Main {
}
/**
*
*
* @return RippingCluster_RequestParser
*/
public function request() {
return $this->request;
}
}
?>

View File

@@ -68,6 +68,8 @@ class RippingCluster_Source_Plugin_Bluray extends RippingCluster_PluginBase impl
$source->cache();
}
}
return $source;
}
/**

View File

@@ -0,0 +1,11 @@
<?php
$main = RippingCluster_Main::instance();
$config = $main->config();
$settings = $config->enumerateAll();
asort($settings);
$this->smarty->assign('settings', $settings);
$this->smarty->assign('config', $config);
?>

View File

@@ -0,0 +1,55 @@
<?php
$main = RippingCluster_Main::instance();
$req = $main->request();
$config = $main->config();
$messages = array();
$result = false;
try {
$name = $req->get('name', 'RippingCluster_Exception_InvalidParameters');
$type = $req->get('type', 'RippingCluster_Exception_InvalidParameters');
// Convert the web-friendly type field into the correct internal name
$value = null;
switch($type) {
case 'bool': {
$type = Sihnon_Config::TYPE_BOOL;
$value = false;
} break;
case 'int': {
$type = Sihnon_Config::TYPE_INT;
$value = 0;
} break;
case 'string': {
$type = Sihnon_Config::TYPE_STRING;
$value = '';
} break;
case 'string-list': {
$type = Sihnon_Config::TYPE_STRING_LIST;
$value = array();
} break;
case 'hash': {
$type = Sihnon_Config::TYPE_HASH;
$value = array('' => '');
} break;
}
// Add the new (empty) value. This is because no suitable UI has been presented yet.
// Possible future fix, to insert intermediate dialog to capture the value using the correct UI.
$result = $config->add($name, $type, $value);
$this->smarty->assign('success', $result);
$this->smarty->assign('name', $name);
$this->smarty->assign('id', str_replace('.', '-',$name));
$this->smarty->assign('type', $type);
$this->smarty->assign('value', '');
} catch(RippingCluster_Exception $e) {
$messages[] = get_class($e) . ':' . $e->getMessage();
$this->smarty->assign('messages', $messages);
$this->smarty->assign('success', false);
}
?>

View File

@@ -0,0 +1,23 @@
<?php
$main = RippingCluster_Main::instance();
$req = $main->request();
$config = $main->config();
$messages = array();
$result = false;
try {
$name = $req->get('name', 'RippingCluster_Exception_InvalidParameters');
$result = $config->remove($name);
$this->smarty->assign('success', $result);
} catch(RippingCluster_Exception $e) {
$messages[] = get_class($e) . ':' . $e->getMessage();
$this->smarty->assign('messages', $messages);
$this->smarty->assign('success', false);
}
?>

View File

@@ -0,0 +1,39 @@
<?php
$main = RippingCluster_Main::instance();
$req = $main->request();
$config = $main->config();
$messages = array();
$result = false;
$confirm = $req->exists('confirm');
$this->smarty->assign('confirm', $confirm);
if ($confirm) {
try {
$name = $req->get('name', 'RippingCluster_Exception_InvalidParameters');
$new_name = $req->get('new-name', 'RippingCluster_Exception_InvalidParameters');
$result = $config->rename($name, $new_name);
$this->smarty->assign('success', $result);
$this->smarty->assign('old_name', $name);
$this->smarty->assign('old_id', str_replace('.', '-', $name));
$this->smarty->assign('name', $new_name);
$this->smarty->assign('id', str_replace('.', '-', $new_name));
$this->smarty->assign('type', $config->type($new_name));
$this->smarty->assign('value', $config->get($new_name));
} catch(RippingCluster_Exception $e) {
$messages[] = get_class($e) . ':' . $e->getMessage();
$this->smarty->assign('messages', $messages);
$this->smarty->assign('success', false);
}
} else {
$name = $req->get('name', 'RippingCluster_Exception_InvalidParameters');
$this->smarty->assign('name', $name);
}
?>

View File

@@ -7,22 +7,29 @@ $config = $main->config();
// Grab the name of this source
$encoded_filename = null;
if ($req->exists('confirm')) {
$this->smarty->assign('confirmed', true);
$plugin = $req->get('plugin', 'RippingCluster_Exception_InvalidParameters');
$encoded_filename = $req->get('id', 'RippingCluster_Exception_InvalidParameters');
$source = RippingCluster_Source_PluginFactory::loadEncoded($plugin, $encoded_filename, false);
$source->delete();
// Redirect back to the sources page
RippingCluster_Page::redirect('rips/sources');
// Generate a new list of sources to update the page with
$all_sources = RippingCluster_Source_PluginFactory::enumerateAll();
$this->smarty->assign('all_sources', $all_sources);
} else {
$this->smarty->assign('confirmed', false);
$plugin = $req->get('plugin', 'RippingCluster_Exception_InvalidParameters');
$encoded_filename = $req->get('id', 'RippingCluster_Exception_InvalidParameters');
$source = RippingCluster_Source_PluginFactory::loadEncoded($plugin, $encoded_filename, false);
$this->smarty->assign('source', $source);
$this->smarty->assign('source_plugin', $plugin);
$this->smarty->assign('source_id', $encoded_filename);
}
?>

View File

@@ -0,0 +1,9 @@
<?php
$main = RippingCluster_Main::instance();
$config = $main->config();
$all_sources = RippingCluster_Source_PluginFactory::enumerateAll();
$this->smarty->assign('all_sources', $all_sources);
?>

View File

@@ -0,0 +1,22 @@
<?php
$main = RippingCluster_Main::instance();
$config = $main->config();
$messages = array();
// Iterate over the settings and store each one back to the backend
foreach($_POST as $key => $value) {
// Convert - to . (to work around the PHP register global backwards compatibility that renames input variables)
$key = str_replace("-", ".", $key);
if ($config->exists($key)) {
$config->set($key, $value);
} else {
$messages[] = "Unknown config key '{$key}', value not updated.";
}
}
$this->smarty->assign('messages', $messages);
?>

View File

@@ -44,7 +44,7 @@ if ($req->exists('submit')) {
RippingCluster_Job::runAllJobs();
# Redirect to the job queued page to show the jobs were successfully dispatched
RippingCluster_Page::redirect('rips/setup-rip/queued');
RippingCluster_Page::redirect('rips/setup/queued');
} break;
case 'delete': {

View File

@@ -0,0 +1,45 @@
<?php
$main = RippingCluster_Main::instance();
$req = $main->request();
$log = $main->log();
$config = $main->config();
$job_id = $req->get('id');
$job = RippingCluster_Job::fromId($job_id);
$this->smarty->assign('job', $job);
// Fetch log entries for this job
$log_count = $req->get('logs', $config->get('job.logs.default_display_count'));
$default_log_order = $config->get('job.logs.default_order');
$log_order = $req->get('order', $default_log_order);
if ( ! in_array($log_order, array(SihnonFramework_Log::ORDER_ASC, SihnonFramework_Log::ORDER_DESC))) {
$log_order = $default_log_order;
}
$this->smarty->assign('log_order', $log_order);
$this->smarty->assign('log_order_reverse', ($log_order == SihnonFramework_Log::ORDER_ASC ? SihnonFramework_Log::ORDER_DESC : SihnonFramework_Log::ORDER_ASC));
$client_log_entries = array();
$worker_log_entries = array();
$log_count_display = null;
if ($log_count == 'all') {
$log_count_display = 'all';
$log_count = '18446744073709551615'; // see mysql man page for LIMIT
} else if(!is_int($log_count)) {
$log_count = $config->get('job.logs.default_display_count');
$log_count_display = $log_count;
} else {
$log_count_display = $log_count;
}
$client_log_entries = RippingCluster_LogEntry::recentEntriesByField($log, 'webui', 'job_id', $job_id, 'ctime', $log_order, $log_count);
$worker_log_entries = RippingCluster_LogEntry::recentEntriesByField($log, 'worker', 'job_id', $job_id, 'ctime', $log_order, $log_count);
$this->smarty->assign('log_count_display', $log_count_display);
$this->smarty->assign('client_log_entries', $client_log_entries);
$this->smarty->assign('worker_log_entries', $worker_log_entries);
?>

View File

@@ -0,0 +1,56 @@
<?php
$main = RippingCluster_Main::instance();
$req = $main->request();
$config = $main->config();
// Grab the name of this source
$encoded_filename = null;
if ($req->exists('submit')) {
$encoded_filename = RippingCluster_Main::issetelse($_POST['id'], 'RippingCluster_Exception_InvalidParameters');
// Update the recently used list
$recent_output_directories = $config->get('rips.output_directories.recent');
if (( $key = array_search($_POST['rip-options']['output-directory'], $recent_output_directories, true)) >= 0) {
// Move the entry to the top of the recently used list if necessary
$recent_directory = array_splice($recent_output_directories, $key, 1);
if ($key > 0) {
array_unshift($recent_output_directories, $recent_directory[0]);
$config->set('rips.output_directories.recent', array_slice($recent_output_directories, 0, $config->get('rips.output_directories.recent_limit', 10)));
}
} else {
array_unshift($recent_output_directories, $_POST['rip-options']['output-directory']);
$config->set('rips.output_directories.recent', array_slice($recent_output_directories, 0, $config->get('rips.output_directories.recent_limit', 10)));
}
// Create the jobs from the request
$jobs = RippingCluster_Job::fromPostRequest($_POST['plugin'], $_POST['id'], $_POST['rip-options'], $_POST['rips']);
// Spawn the background client process to run all the jobs
RippingCluster_Job::runAllJobs();
RippingCluster_Page::redirect('rips/setup/queued');
} elseif ($req->exists('queued')) {
$this->smarty->assign('rips_submitted', true);
} else {
$this->smarty->assign('rips_submitted', false);
$encoded_filename = $req->get('id', 'RippingCluster_Exception_InvalidParameters');
$plugin = $req->get('plugin', 'RippingCluster_Exception_InvalidParameters');
$source = RippingCluster_Source_PluginFactory::loadEncoded($plugin, $encoded_filename);
$this->smarty->assign('source', $source);
$this->smarty->assign('titles', $source->titles());
$this->smarty->assign('longest_title', $source->longestTitle());
$this->smarty->assign('default_output_directory', $config->get('rips.default.output_directory'));
$default_output_directories = $config->get('rips.output_directories.default');
$recent_output_directories = $config->get('rips.output_directories.recent');
$this->smarty->assign('default_output_directories', $default_output_directories);
$this->smarty->assign('recent_output_directories', $recent_output_directories);
$this->smarty->assign('next_output_directory_index', count($default_output_directories) + count($recent_output_directories) + 1);
}
?>

View File

@@ -2,12 +2,10 @@
define('HBC_File', 'run-jobs');
require_once '/etc/ripping-cluster/config.php';
require_once(SihnonFramework_Lib . 'SihnonFramework/Main.class.php');
require_once '_inc.php';
require_once 'Net/Gearman/Client.php';
SihnonFramework_Main::registerAutoloadClasses('SihnonFramework', SihnonFramework_Lib,
'RippingCluster', RippingCluster_Lib);
try {
$main = RippingCluster_Main::instance();
@@ -22,15 +20,19 @@ try {
foreach ($jobs as $job) {
// Enqueue the job using gearman
list($method, $rip_options) = $job->queue();
$task = new Net_Gearman_Task($method, $rip_options);
$args = $job->queue();
$task = new Net_Gearman_Task($args['method'], $args);
$task->attachCallback('gearman_complete', Net_Gearman_Task::TASK_COMPLETE);
$task->attachCallback('gearman_fail', Net_Gearman_Task::TASK_FAIL);
$set->addTask($task);
$job->updateStatus(RippingCluster_JobStatus::QUEUED);
RippingCluster_ClientLogEntry::info($log, $args['rip_options']['id'], 'Job queued', 'client');
}
$job_count = count($jobs);
RippingCluster_ClientLogEntry::info($log, null, "Job queue started with {$job_count} jobs.", 'batch');
// Start the job queue
$result = $client->runSet($set);
@@ -58,7 +60,7 @@ function gearman_fail($task) {
$job = RippingCluster_Job::fromId($task->arg['rip_options']['id']);
$job->updateStatus(RippingCluster_JobStatus::FAILED);
RippingCluster_ClientLogEntry::info($log, $job->id(), 'Job failed');
RippingCluster_ClientLogEntry::info($log, $job->id(), "Job failed with message: {$task->result}");
}

View File

@@ -0,0 +1,292 @@
<?php
/**
* Switch statement plugin for smarty.
* This smarty plugin provides php switch statement functionality in smarty tags.
* To install this plugin drop it into your smarty plugins folder. You will also need to manually
* load the plugin sot hat all the hooks are registered properly. Add the following line after
* you load smarty and create an instance of it in your source code.
*
* <code>
* $this->smartyObj->loadPlugin('smarty_compiler_switch');
* </code>
*
* @author Jeremy Pyne <jeremy.pyne@gmail.com>
* - Donations: Accepted via PayPal at the above address.
* - Updated: 02/10/2010 - Version 3.2
* - File: smarty/plugins/compiler.switch.php
* - Licence: CC:BY/NC/SA http://creativecommons.org/licenses/by-nc-sa/3.0/
*
* - Updates
* Version 2:
* Changed the break attribute to cause a break to be printed before the next case, instead of before this
* case. This way makes more sense and simplifies the code. This change in incompatible with code in
* from version one. This is written to support nested switches and will work as expected.
* Version 2.1:
* Added {/case} tag, this is identical to {break}.
* Version 3:
* Updated switch statment to support Smarty 3. This update is NOT backwards compatible but the old version is still maintained.
* Version 3.1:
* Added a prefilter to re-enable the shorthand {switch $myvar} support. To use the shorthand form you will need to add the following line to your code.
* $smarty->loadPlugin('smarty_compiler_switch');
* Version 3.2:
* Fixed a bug when chaining multiple {case} statements without a {break}.
* Version 3.5:
* Updated to work with Smarty 3.0 release. (Tested and working with 3.0.5, no longer compatible with 3.0rcx releases.)
*
* - Bugs/Notes:
*
* @package Smarty
* @subpackage plugins
*
* Sample usage:
* <code>
* {foreach item=$debugItem from=$debugData}
* // Switch on $debugItem.type
* {switch $debugItem.type}
* {case 1}
* {case "invalid_field"}
* // Case checks for string and numbers.
* {/case}
* {case $postError}
* {case $getError|cat:"_ajax"|lower}
* // Case checks can also use variables and modifiers.
* {break}
* {default}
* // Default case is supported.
* {/switch}
* {/foreach}
* </code>
*
* Note in the above example that the break statements work exactly as expected. Also the switch and default
* tags can take the break attribute. If set they will break automatically before the next case is printed.
*
* Both blocks produce the same switch logic:
* <code>
* {case 1 break}
* Code 1
* {case 2}
* Code 2
* {default break}
* Code 3
* </code>
*
* <code>
* {case 1}
* Code 1
* {break}
* {case 2}
* Code 2
* {default}
* Code 3
* {break}
* </code>
*
* Finally, there is an alternate long hand style for the switch statments that you may need to use in some cases.
*
* <code>
* {switch var=$type}
* {case value="box" break}
* {case value="line"}
* {break}
* {default}
* {/switch}
* </code>
*/
//Register the post and pre filters as they are not auto-registered.
$this->registerFilter('post', 'smarty_postfilter_switch');
class Smarty_Compiler_Switch extends Smarty_Internal_CompileBase {
public $required_attributes = array('var');
public $optional_attributes = array();
public $shorttag_order = array('var');
/**
* Start a new switch statement.
* A variable must be passed to switch on.
* Also, the switch can only directly contain {case} and {default} tags.
*
* @param string $tag_arg
* @param Smarty_Compiler $smarty
* @return string
*/
public function compile($args, $compiler){
$this->compiler = $compiler;
$attr = $this->_get_attributes($args);
$_output = '';
$this->_open_tag('switch',array($compiler->tag_nocache));
if (is_array($attr['var'])) {
$_output .= "<?php if (!isset(\$_smarty_tpl->tpl_vars[".$attr['var']['var']."])) \$_smarty_tpl->tpl_vars[".$attr['var']['var']."] = new Smarty_Variable;";
$_output .= "switch (\$_smarty_tpl->tpl_vars[".$attr['var']['var']."]->value = ".$attr['var']['value']."){?>";
} else {
$_output .= '<?php switch (' . $attr['var'] . '){?>';
}
return $_output;
}
}
class Smarty_Compiler_Case extends Smarty_Internal_CompileBase {
public $required_attributes = array('value');
public $optional_attributes = array('break');
public $shorttag_order = array('value', 'break');
/**
* Print out a case line for this switch.
* A condition must be passed to match on.
* This can only go in {switch} tags.
* If break is passed, a {break} will be rendered before the next case.
*
* @param string $tag_arg
* @param Smarty_Compiler $smarty
* @return string
*/
public function compile($args, $compiler){
$this->compiler = $compiler;
$attr = $this->_get_attributes($args);
$_output = '';
list($last_tag, $last_attr) = $this->compiler->_tag_stack[count($this->compiler->_tag_stack) - 1];
if($last_tag == 'case')
{
list($break, $compiler->tag_nocache) = $this->_close_tag(array('case'));
if($last_attr[0])
$_output .= '<?php break;?>';
}
$this->_open_tag('case', array(isset($attr['break']) ? $attr['break'] : false, $compiler->tag_nocache));
if (is_array($attr['value'])) {
$_output .= "<?php if (!isset(\$_smarty_tpl->tpl_vars[".$attr['value']['var']."])) \$_smarty_tpl->tpl_vars[".$attr['value']['var']."] = new Smarty_Variable;";
$_output .= "case \$_smarty_tpl->tpl_vars[".$attr['value']['var']."]->value = ".$attr['value']['value'].":?>";
} else {
$_output .= '<?php case ' . $attr['value'] . ':?>';
}
return $_output;
}
}
class Smarty_Compiler_Default extends Smarty_Internal_CompileBase {
public $required_attributes = array();
public $optional_attributes = array('break');
public $shorttag_order = array('break');
/**
* Print out a default line for this switch.
* This can only go in {switch} tags.
* If break is passed, a {break} will be rendered before the next case.
*
* @param string $tag_arg
* @param Smarty_Compiler $smarty
* @return string
*/
public function compile($args, $compiler){
$this->compiler = $compiler;
$attr = $this->_get_attributes($args);
$_output = '';
list($last_tag, $last_attr) = $this->compiler->_tag_stack[count($this->compiler->_tag_stack) - 1];
if($last_tag == 'case')
{
list($break, $compiler->tag_nocache) = $this->_close_tag(array('case'));
if($last_attr[0])
$_output .= '<?php break;?>';
}
$this->_open_tag('case', array(isset($attr['break']) ? $attr['break'] : false, $compiler->tag_nocache));
$_output .= '<?php default:?>';
return $_output;
}
}
class Smarty_Compiler_Break extends Smarty_Internal_CompileBase {
public $required_attributes = array();
public $optional_attributes = array();
public $shorttag_order = array();
/**
* Print out a break command for the switch.
* This can only go inside of {case} tags.
*
* @param string $tag_arg
* @param Smarty_Compiler $smarty
* @return string
*/
public function compile($args, $compiler){
$this->compiler = $compiler;
$attr = $this->_get_attributes($args);
list($break, $compiler->tag_nocache) = $this->_close_tag(array('case'));
return '<?php break;?>';
}
}
class Smarty_Compiler_Caseclose extends Smarty_Internal_CompileBase {
public $required_attributes = array();
public $optional_attributes = array();
public $shorttag_order = array();
/**
* Print out a break command for the switch.
* This can only go inside of {case} tags.
*
* @param string $tag_arg
* @param Smarty_Compiler $smarty
* @return string
*/
public function compile($args, $compiler){
$this->compiler = $compiler;
$attr = $this->_get_attributes($args);
list($break, $compiler->tag_nocache) = $this->_close_tag(array('case'));
return '<?php break;?>';
}
}
class Smarty_Compiler_Switchclose extends Smarty_Internal_CompileBase {
public $required_attributes = array();
public $optional_attributes = array();
public $shorttag_order = array();
/**
* End a switch statement.
*
* @param string $tag_arg
* @param Smarty_Compiler $smarty
* @return string
*/
public function compile($args, $compiler){
$this->compiler = $compiler;
$attr = $this->_get_attributes($args);
list($last_tag, $last_attr) = $this->compiler->_tag_stack[count($this->compiler->_tag_stack) - 1];
if(($last_tag == 'case' || $last_tag == 'default'))
list($break, $compiler->tag_nocache) = $this->_close_tag(array('case'));
list($compiler->tag_nocache) = $this->_close_tag(array('switch'));
return '<?php }?>';
}
}
/**
* Filter the template after it is generated to fix switch bugs.
* Remove any spaces after the 'switch () {' code and before the first case. Any tabs or spaces
* for layout would cause php errors witch this reged will fix.
*
* @param string $compiled
* @param Smarty_Compiler $smarty
* @return string
*/
function smarty_postfilter_switch($compiled, &$smarty) {
// Remove the extra spaces after the start of the switch tag and before the first case statement.
return preg_replace('/({ ?\?>)\s+(<\?php case)/', "$1\n$2", $compiled);
}
?>

View File

@@ -0,0 +1,25 @@
<h2>Settings</h2>
<table id="settings">
<thead>
<th>Name</th>
<th>Value</th>
</thead>
<tbody>
{foreach from=$settings item=name}
{assign var='value' value=$config->get($name)}
{assign var='type' value=$config->type($name)}
{assign var='id' value=str_replace('.', '-',$name)}
{include file="fragments/admin-setting-row.tpl"}
{/foreach}
</tbody>
<tfoot>
<tr>
<td colspan="2">
<button id="settings_save" class="btn primary" name="save">Save</button>
<button id="settings_new" class="btn" name="new_setting">New Setting</button>
</td>
</tr>
</tfoot>
</table>

View File

@@ -0,0 +1,12 @@
{
{if $messages}
"messages": [
{foreach from=$messages item=message name=messages}
{$message|json_encode}{if ! $smarty.foreach.messages.last},{/if}
{/foreach}
]{if $page_content},{/if}
{/if}
{$page_content}
}

View File

@@ -0,0 +1,11 @@
{if $success}
"actions": {
"add_setting_row": {
{include file="fragments/admin-setting-row.tpl" assign=content}
"content": {$content|json_encode}
}
},
{/if}
"success": {$success|json_encode}

View File

@@ -0,0 +1,30 @@
"page_replacements": {
"dialog-header-title": {
"content": "Add Setting"
},
"dialog-body": {
{include file="fragments/new-setting-dialog.tpl" assign=new_setting_dialog_content}
"content": {$new_setting_dialog_content|json_encode}
}
},
"dialog": {
"show": true,
"buttons": {
"type": "okcancel",
"actions": {
"ok": [
"add-setting",
"close-dialog"
],
"cancel": "close-dialog"
},
"params": {
"name": "settings_add_name",
"type": "settings_add_type"
}
}
}

View File

@@ -0,0 +1 @@
"success": {$success|json_encode}

View File

@@ -0,0 +1,44 @@
{if $confirm}
{if $success}
"actions": {
"rename_setting_confirm": {
"old_name": {$old_name|json_encode},
"old_id": {$old_id|json_encode},
"name": {$name|json_encode},
{include file="fragments/admin-setting-row.tpl" assign="content"}
"content": {$content|json_encode}
}
},
{/if}
"success": {$success|json_encode}
{else}
"page_replacements": {
"dialog-header-title": {
"content": "Rename Setting"
},
"dialog-body": {
{include file="fragments/rename-setting-dialog.tpl" assign="content"}
"content": {$content|json_encode}
}
},
"dialog": {
"show": true,
"buttons": {
"type": "okcancel",
"actions": {
"ok": [
"rename_setting",
"close-dialog"
],
"cancel": "close-dialog"
},
"params": {
"name": {$name|json_encode},
"new_name_field": "settings_rename_name"
}
}
}
{/if}

View File

@@ -0,0 +1,41 @@
"page_replacements": {
{if $confirmed}
"source-list": {
{include file="fragments/source-list.tpl" assign="sources_html"}
"content": {$sources_html|json_encode}
}
{else}
"dialog-header-title": {
"content": "Delete this source?"
},
"dialog-body": {
{include file="fragments/delete-source.tpl" assign="delete_source_html"}
"content": {$delete_source_html|json_encode}
}
{/if}
{if ! $confirmed}
},
"dialog": {
"show": true,
"buttons": {
"type": "yesno",
"actions": {
"yes": [
"delete-source-confirm",
"close-dialog"
],
"no": "close-dialog"
},
"params": {
"plugin": {$source_plugin|json_encode},
"id": {$source_id|json_encode}
}
}
}
{else}
}
{/if}

View File

@@ -0,0 +1,6 @@
"page_replacements": {
"source-list": {
{include file="fragments/source-list.tpl" assign="sources_html"}
"content": {$sources_html|json_encode}
}
}

View File

@@ -0,0 +1,22 @@
"page_replacements": {
"dialog-header-title": {
"content": "Settings updated"
},
"dialog-body": {
{include file="fragments/update-settings-dialog.tpl" assign=dialog_content}
"content": {$dialog_content|json_encode}
}
},
"dialog": {
"show": true,
"buttons": {
"type": "ok",
"actions": {
"ok": "close-dialog"
}
}
}

View File

@@ -0,0 +1,14 @@
<tr id="setting_{$id}_row">
<td>
<p>
<strong>{$name}</strong>
</p>
<p>
<button id="setting_{$id}_rename" class="btn" onclick="rc.settings.rename_setting('{$id}', '{$name}');">Rename</button>
<button id="setting_{$id}_remove" class="btn" onclick="rc.settings.remove_setting('{$id}', '{$name}');">Remove</button>
</p>
</td>
<td>
{include file="fragments/admin-setting-value.tpl"}
</td>
</tr>

View File

@@ -0,0 +1,45 @@
{switch $type}
{case Sihnon_Config::TYPE_BOOL}
<input type="checkbox" id="setting_{$id}" name="{$id}" value="1" {if $value}checked="checked" {/if} class="setting" />
{/case}
{case Sihnon_Config::TYPE_INT}
<input type="text" id="setting_{$id}" name="{$id}" value="{$value}" class="small setting settings_field_numeric" />
{/case}
{case Sihnon_Config::TYPE_STRING}
<input type="text" id="setting_{$id}" name="{$id}" value="{$value}" class="xxlarge setting settings_field_string" />
{/case}
{case Sihnon_Config::TYPE_STRING_LIST}
<div id="container_{$id}">
{foreach from=$value item=line name=settings}
<div id="settings_{$id}_line{$smarty.foreach.settings.iteration}">
<input type="text" name="{$id}[]" value="{$line}" class="xxlarge setting settings_field_string" />
<button class="btn small settings_field_remove" onclick="rc.settings.remove_stringlist_field('{$id}', '{$smarty.foreach.settings.iteration}')">-</button>
</div>
{/foreach}
</div>
<div class="settings_addfieldcontainer">
<input type="hidden" id="settings_{$id}_next" value="{$smarty.foreach.settings.iteration+1}" />
<button class="btn small settings_field_add" onclick="rc.settings.add_stringlist_field('{$id}')">+</button>
</div>
{/case}
{case Sihnon_Config::TYPE_HASH}
<div id="container_{$id}">
{if $value}
{foreach from=$value item=hash_value key=hash_key name=settings}
<div id="settings_{$id}_line{$smarty.foreach.settings.iteration}">
<input type="text" value="{$hash_key}" class="small setting hash_key" />
<input type="text" name="{$id}[{$hash_key}]" value="{$hash_value}" class="xlarge setting hash_value" />
<button class="btn small settings_field_remove" onclick="rc.settings.remove_hash_field('{$id}', '{$smarty.foreach.settings.iteration}')">-</button>
</div>
{/foreach}
{/if}
</div>
<div class="settings_addfieldcontainer">
<input type="hidden" id="settings_{$id}_next" value="{$smarty.foreach.settings.iteration+1}" />
<button class="btn small settings_field_add" onclick="rc.settings.add_hash_field('{$id}')">+</button>
</div>
{/case}
{default}
<em>Unsupported setting type!</em>
{/switch}

View File

@@ -0,0 +1,3 @@
<p>
Are you sure you want to delete {$source->plugin()|escape:"html"}:{$source->filename()|escape:"html"}?
</p>

View File

@@ -0,0 +1,9 @@
<dl>
<dt>Destination Filename</dt>
<dd>{$job->destinationFilename()|escape:html}</dd>
{if $job->isFinished()}
<dt>File size</dt>
<dd>({$job->outputFilesize()|formatFilesize|escape:html})</dd>
{/if}
</dl>

View File

@@ -0,0 +1,15 @@
<dl>
{if $current_status->hasProgressInfo()}
<dt>Started</dt>
<dd>{$current_status->ctime()|date_format:"%D %T"}</dd>
<dt>Progress</dt>
<dd>{$current_status->ripProgress()}%</dd>
<dt>ETA</dt>
<dd>{$job->calculateETA()|formatDuration}</dd>
{/if}
<dt>Last update</dt>
<dd>{$current_status->mtime()|date_format:"%D %T"}</dd>
</dl>

View File

@@ -0,0 +1,7 @@
{if $messages}
<ul>
{foreach from=$messages item=message}
<li>{$message|escape}</li>
{/foreach}
</ul>
{/if}

View File

@@ -0,0 +1,11 @@
<label for="settings_add_name">Name</label>
<input type="text" id="settings_add_name" value="" />
<label for="settings_add_type">Type</label>
<select id="settings_add_type">
<option value="int">Integer</option>
<option value="bool">Boolean</option>
<option value="string">String</option>
<option value="string-list">String List</option>
<option value="hash">Hash</option>
</select>

View File

@@ -0,0 +1,6 @@
<div>
<p>
Enter a new name for setting '{$name|escape}' below.
</p>
<input type="text" id="settings_rename_name" value="{$name|escape:html}" />
</div>

View File

@@ -0,0 +1,25 @@
{foreach from=$all_sources key=type item=sources}
<li>{$type}
{if $sources}
<ul>
{foreach from=$sources item=source}
{assign var='source_plugin' value=$source->plugin()}
{assign var='source_filename' value=$source->filename()}
{assign var='source_filename_encoded' value=$source->filenameEncoded()}
{assign var='source_cached' value=$source->isCached()}
<li class="hover-highlight">
[ <a href="{$base_uri}sources/details/plugin/{$source_plugin}/id/{$source_filename_encoded}" title="Browse source details">Browse</a> |
<a href="{$base_uri}rips/setup/plugin/{$source_plugin}/id/{$source_filename_encoded}" title="Rip this source">Rip</a> |
<a href="javascript:rc.sources.remove('{$source_plugin|escape:'quote'}', '{$source_filename_encoded|escape:'quote'}');" title="Delete this source">Delete</a> ]
{$source_filename|escape:'html'}{if $source_cached} (cached){/if}
</li>
{/foreach}
</ul>
{else}
<p>
<em>There are no {$type} sources available to rip.</em>
</p>
{/if}
</li>
{/foreach}

View File

@@ -0,0 +1,10 @@
<p>
Your changes have been saved.
</p>
{if $messages}
<p>
Some messages were generated during this operation:
{include file="fragments/messages.tpl"}
</p>
{/if}

View File

@@ -4,7 +4,7 @@
{if $running_jobs}
{foreach from=$running_jobs item=job}
<li><a href="{$base_uri}job-details/id/{$job->id()}" title="View job details">{$job->name()} ({$job->currentStatus()->ripProgress()}%)</a></li>
<li><a href="{$base_uri}jobs/details/id/{$job->id()}" title="View job details">{$job->name()}</a> <span class="progressBar" id="job_progress_{$job->id()}">{$job->currentStatus()->ripProgress()}%</span> ({RippingCluster_Main::formatDuration($job->calculateETA(), 1)} remaining)</li>
{/foreach}
{else}
<em>There are no currently running jobs.</em>
@@ -14,7 +14,7 @@
{if $queued_jobs}
{foreach from=$queued_jobs item=job}
<li><a href="{$base_uri}job-details/id/{$job->id()}" title="View job details">{$job->name()}</a></li>
<li><a href="{$base_uri}jobs/details/id/{$job->id()}" title="View job details">{$job->name()}</a></li>
{/foreach}
{else}
<em>There are no currently running jobs.</em>
@@ -25,7 +25,7 @@
{if $completed_jobs}
<ul>
{foreach from=$completed_jobs item=job}
<li><a href="{$base_uri}job-details/id/{$job->id()}" title="View job details">{$job->name()}</a></li>
<li><a href="{$base_uri}jobs/details/id/{$job->id()}" title="View job details">{$job->name()}</a></li>
{/foreach}
</ul>
{else}
@@ -37,7 +37,7 @@
{if $failed_jobs}
<ul>
{foreach from=$failed_jobs item=job}
<li><a href="{$base_uri}job-details/id/{$job->id()}" title="View job details">{$job->name()}</a></li>
<li><a href="{$base_uri}jobs/details/id/{$job->id()}" title="View job details">{$job->name()}</a></li>
{/foreach}
</ul>
{else}

View File

@@ -0,0 +1,131 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>{$title}</title>
<!-- JQuery //-->
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js"></script>
<link type="text/css" href="//ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/smoothness/jquery-ui.css" rel="Stylesheet" />
<!-- JQuery Plugins //-->
<script type="text/javascript" src="{$base_uri}scripts/3rdparty/jquery.chained.js"></script>
<script type="text/javascript" src="{$base_uri}scripts/3rdparty/jquery.jec-1.3.2.js"></script>
<script type="text/javascript" src="{$base_uri}scripts/3rdparty/jquery.asmselect.js"></script>
<link type="text/css" href="{$base_uri}styles/3rdparty/jquery.asmselect.css" rel="Stylesheet" />
<script type="text/javascript" src="{$base_uri}scripts/3rdparty/jquery.progressbar.min.js"></script>
<!-- Less //-->
<script type="text/javascript" src="{$base_uri}scripts/3rdparty/less-1.1.5.min.js"></script>
<!-- Bootstrap //-->
<link rel="stylesheet" href="{$base_uri}scripts/3rdparty/bootstrap.min.css">
<script type="text/javascript" src="{$base_uri}scripts/3rdparty/bootstrap-alerts.js"></script>
<script type="text/javascript" src="{$base_uri}scripts/3rdparty/bootstrap-twipsy.js"></script>
<script type="text/javascript" src="{$base_uri}scripts/3rdparty/bootstrap-popover.js"></script>
<script type="text/javascript" src="{$base_uri}scripts/3rdparty/bootstrap-dropdown.js"></script>
<script type="text/javascript" src="{$base_uri}scripts/3rdparty/bootstrap-tabs.js"></script>
<script type="text/javascript" src="{$base_uri}scripts/3rdparty/bootstrap-modal.js"></script>
<!-- Local //-->
<script type="text/javascript" src="{$base_uri}scripts/sihnon-js-lib/sihnon-framework.js"></script>
<script type="text/javascript" src="{$base_uri}scripts/main.js"></script>
<link rel="stylesheet/less" href="{$base_uri}less/bootstrap.less" media="all" />
<link rel="stylesheet" type="text/css" href="{$base_uri}styles/normal.css" />
<script type="text/javascript">
var base_uri = "{$base_uri|escape:'quote'}";
var base_url = "{$base_url|escape:'quote'}";
</script>
</head>
<body>
<div class="topbar no-print">
<div class="topbar-inner">
<div class="container-fluid">
{$page->include_template('navigation')}
</div><!-- /tobar-inner -->
</div><!-- /container-fliud -->
</div><!-- /topbar -->
<div class="container">
<div class="row">
<div class="span16">
<h1>RippingCluster WebUI</h1>
</div>
</div>
<div class="row">
{if ! $messages}
{$session = RippingCluster_Main::instance()->session()}
{$messages = $session->get('messages')}
{$session->delete('messages')}
{/if}
{if $messages}
<div id="messages">
{foreach from=$messages item=message}
{if is_array($message)}
{$severity=$message['severity']}
<div class="alert-message {$severity}">
{$message['content']|escape:html}
</div>
{else}
<div class="alert-message info">
{$message|escape:html}
</div>
{/if}
{/foreach}
</div><!-- /messages -->
{/if}
<div id="page_content">
{$page_content}
</div>
</div>
<footer class="no-print">
<p>
Powered by RippingCluster WebUI {$version}. Written by
<a xmlns:cc="http://creativecommons.org/ns#" href="http://benroberts.net" property="cc:attributionName" rel="cc:attributionURL">Ben Roberts</a>
</p>
<p>
This work is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/3.0/">Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License</a>.
<br />
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/3.0/">
<img alt="Creative Commons Licence" style="border-width:0" src="http://i.creativecommons.org/l/by-nc-sa/3.0/88x31.png" />
</a>
</p>
</footer>
</div>
<div id="dialog" class="modal hide fade">
<div class="modal-header">
<a href="#" class="close">&times;</a>
<h3 id="dialog-header-title"></h3>
</div>
<div id="dialog-body" class="modal-body"></div>
<div id="dialog-footer" class="modal-footer">
<div id="dialog-footer-ok" class="dialog-footer-buttonset">
<fieldset>
<input type="button" class="btn primary" id="dialog-footer-ok-ok" value="Ok" />
</fieldset>
</div>
<div id="dialog-footer-okcancel" class="dialog-footer-buttonset">
<fieldset>
<input type="button" class="btn primary" id="dialog-footer-okcancel-ok" value="Ok" />
<input type="button" class="btn secondary" id="dialog-footer-okcancel-cancel" value="Cancel" />
</fieldset>
</div>
<div id="dialog-footer-yesno" class="dialog-footer-buttonset">
<fieldset>
<input type="button" class="btn primary" id="dialog-footer-yesno-yes" value="Yes" />
<input type="button" class="btn secondary" id="dialog-footer-yesno-no" value="No" />
</fieldset>
</div>
</div>
</div>
</body>
</html>

View File

@@ -31,39 +31,41 @@
<table>
<thead>
<tr>
<th>
<input id="jobs_select_all" class="select_all" type="checkbox" />
Actions
</th>
<th>Name</th>
<th>Destination</th>
<th>Title</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{foreach from=$jobs item=job}
{assign var=current_status value=$job->currentStatus()}
<tr>
<td><a href="{$base_uri}job-details/id/{$job->id()}" title="View job details">{$job->name()}</a></td>
<td>{$job->destinationFilename()}</td>
<td>{$job->title()}</td>
<td>
{$current_status->statusName()}
{if $current_status->hasProgressInfo()}
<br />
Started: <em>{$current_status->ctime()|date_format:"%D %T"}</em><br />
Progress: {$current_status->ripProgress()}%<br />
Last update: <em>{$current_status->mtime()|date_format:"%D %T"}</em><br />
ETA: <em>{$job->calculateETA()|formatDuration}</em>
{/if}
</td>
<td>
<fieldset>
<input type="checkbox" name="include[]" value="{$job->id()}" />
<input type="checkbox" class="jobs_select_all" name="include[]" value="{$job->id()}" />
<input type="image" class="icon" name="action" id="mark-failed-{$job->id()}" value="mark-failed[{$job->id()}]" src="{$base_uri}images/caution.png" alt="Mark job as failed" />
<input type="image" class="icon" name="action" id="redo-{$job->id()}" value="retry[{$job->id()}]" src="{$base_uri}images/redo.png" alt="Repeat job" />
<input type="image" class="icon" name="action" id="delete-{$job->id()}" value="delete[{$job->id()}]" src="{$base_uri}images/trash.png" alt="Delete job" />
<input type="image" class="icon" name="action" id="fix-broken-timestamps-{$job->id()}" value="fix-broken-timestamps[{$job->id()}]" src="{$base_uri}images/clock.png" alt="Fix broken status timestamps" />
</fieldset>
</td>
<td><a href="{$base_uri}jobs/details/id/{$job->id()}" title="View job details">{$job->name()}</a></td>
<td>
{include file="fragments/job-filename-popover.tpl" assign=popover_content}
<a href="#" rel="popover" data-placement="below" data-title="Destination details" data-content="{$popover_content|escape:html}">{$job->destinationFileBasename()|escape:html}</a>
</td>
<td>
{include file="fragments/job-status-popover.tpl" assign=popover_content}
<a href="#" rel="popover" title="{$current_status->statusName()|escape:html}" data-placement="below" data-content="{$popover_content|escape:html}">{$current_status->statusName()}</a>
{if $current_status->hasProgressInfo()}
<br />
<small>{$job->calculateETA()|formatDuration:1} remaining</small>
{/if}
</td>
</tr>
{/foreach}
</tbody>

View File

@@ -0,0 +1,91 @@
<h2>Job Details</h2>
<h3>Summary</h3>
<dl>
<dt>Source Plugin</dt>
<dd>{$job->sourcePlugin()}</dd>
<dt>Rip Plugin</dt>
<dd>{$job->ripPlugin()}</dd>
<dt>Source Filename</dt>
<dd>{$job->sourceFilename()}</dd>
<dt>Source Title</dt>
<dd>{$job->title()}</dd>
<dt>Status</dt>
<dd>{$job->currentStatus()->statusName()} ({$job->currentStatus()->mtime()|date_format:'%Y-%m-%d %H:%M:%S'})</dd>
<dt>Destination Filename</dt>
<dd>{$job->destinationFilename()}</dd>
{if $job->isFinished()}
<dt>Destination Filesize</dt>
<dd>{$job->outputFilesize()|formatFilesize}</dd>
{/if}
</dl>
<h3>Log messages</h3>
<h4>Options</h4>
<ul>
{if $log_count_display eq 'all'}
<li><a href="{$base_uri}jobs/details/id/{$job->id()}/order/{$log_order}/" title="View recent logs only">View recent messages only</a></li>
{else}
<li><a href="{$base_uri}jobs/details/id/{$job->id()}/logs/all/" title="View all logs">View all messages</a></li>
{/if}
<li><a href="{$base_uri}jobs/details/id/{$job->id()}/logs/{$log_count_display}/order/{$log_order_reverse}/" title="Reverse display order of log messages">Reverse display order</a></li>
</ul>
<h4>Recent Client Logs</h4>
{if $client_log_entries}
<table>
<thead>
<tr>
<th>Level</th>
<th>Time</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{foreach from=$client_log_entries item=log_entry}
<tr>
<td>{$log_entry->level()}</td>
<td>{$log_entry->ctime()|date_format:"%Y-%m-%d %H:%M:%S"}</td>
<td>{$log_entry->message()}</td>
</tr>
{/foreach}
</tbody>
</table>
{else}
<em>There are no client log entries.</em>
{/if}
<h4>Recent Worker Logs</h4>
{if $worker_log_entries}
<table>
<thead>
<tr>
<th>Level</th>
<th>Time</th>
<th>Hostname</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{foreach from=$worker_log_entries item=log_entry}
<tr>
<td>{$log_entry->level()}</td>
<td>{$log_entry->ctime()|date_format:"%Y-%m-%d %H:%M:%S"}</td>
<td>{$log_entry->hostname()}</td>
<td>{$log_entry->message()}</td>
</tr>
{/foreach}
</tbody>
</table>
{else}
<em>There are no worker log entries.</em>
{/if}

Some files were not shown because too many files have changed in this diff Show More