122 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
efb7db35d8 Fixed typo in _inc.php 2011-06-20 22:33:18 +01:00
841a5b9f92 Revert previous untested change in include statements 2011-06-20 22:04:44 +01:00
506a6e189c Update paths for packaging 2011-06-20 19:34:27 +01:00
3da59727de Bug fix: typo in variable name 2011-06-20 19:29:11 +01:00
c0d8747b21 Update init script to background the worker process 2011-06-19 22:48:45 +01:00
e1bd324e84 Bugfix: error in client log call 2011-06-19 00:13:39 +01:00
8fe8f8ba08 Use defines for library paths 2011-06-18 23:53:25 +01:00
274bc8f3c9 Use defines for library paths 2011-06-18 22:15:36 +01:00
edc2439232 Make worker read configuration from /etc/ripping-cluster 2011-06-18 18:36:33 +01:00
bd74b0f54e Update default config files 2011-06-18 18:35:40 +01:00
cd93f13f41 Add gentoo init script 2011-06-18 18:30:03 +01:00
21bd423f02 Rename worker file 2011-06-18 17:45:20 +01:00
5adf9170c4 Merge branch 'master' of git+ssh://git.sihnon.net/home/git/public/handbrake-cluster-webui 2011-04-25 14:16:43 +01:00
ce94762905 Bug fix: undefined variable when rip progress is 0% 2011-04-25 14:05:33 +01:00
72be6d6e5f Bug fixes
1) Quoted barewords in array construction
2) Added order by clause to all() to provide consistent ordering
2011-04-25 01:52:56 +01:00
5a5c3438c1 Updated client side logging and attempt to include proper job_id 2011-04-25 01:19:18 +01:00
c0702aa98e Bug fix: typoed variable name
Caused broken javascript on rip page
2011-04-25 01:18:41 +01:00
4e58ac7135 Updated code to use new WorkerLogEntry class 2011-04-25 00:54:28 +01:00
180e63c456 Bug fixes
1) full path to run-jobs.php to ensure script is runnable
2) reordering of allWithStatus to ensure consistent results
2011-04-25 00:50:22 +01:00
9be81f92f4 Updated subclassing of Client/Worker LogEntry classes 2011-04-25 00:49:09 +01:00
a459ba283a Updated status information on home page 2011-04-25 00:48:02 +01:00
4e7a5d189a Bug fix in source deletion 2011-04-25 00:47:30 +01:00
bdfa91f9b0 Updated worker to use new logging code 2011-04-24 21:18:22 +01:00
9ec2df1f18 Bug fix: incorrect parameters to log retrieval calls 2011-04-24 11:10:07 +01:00
dbaf4968ab Updated code to use new Logging code in sihnon lib 2011-04-24 10:40:14 +01:00
b896877591 fix call to Gearman_Job::complete() 2011-04-22 00:49:32 +01:00
da263a23b7 Fixed bug with worker plugin scanning 2011-04-22 00:40:38 +01:00
edc717ef68 Bug fixes 2011-04-22 00:26:30 +01:00
d3fe08d40f Massive refactor to use SihnonFramework and PEAR's Net_Gearman 2011-04-21 23:31:21 +01:00
fa7b54b861 Add option to delete sources with support in all three backends 2010-11-19 22:24:12 +00:00
73e42a42b1 Update BackgroundTask to use same cwd for spawned process 2010-11-19 21:12:18 +00:00
7c175bb608 Merge branch 'feature-mkv-plugins'; commit 'HEAD^' 2010-10-12 19:30:27 +01:00
697002dddb Merge branch 'feature-bluray' 2010-10-12 19:26:57 +01:00
d87c924b14 Merge branch 'master', remote branch 'origin' 2010-09-24 20:10:09 +01:00
170 changed files with 5554 additions and 8684 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

@@ -0,0 +1,32 @@
#!/sbin/runscript
# Copyright 1999-2010 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
depend() {
need localmount net
use dns logger puppetmaster netmount nfsmount
}
start() {
ebegin "Starting ripping-cluster-worker"
start-stop-daemon --start --quiet \
--background --make-pidfile --pidfile ${PID_FILE} \
--user ${USER} \
--exec /usr/bin/php /usr/lib/ripping-cluster/source/worker/ripping-cluster-worker.php
eend $? "Failed to start ripping-cluster-worker"
}
stop() {
ebegin "Stopping ripping-cluster-worker"
start-stop-daemon --stop --quiet \
--pidfile ${PID_FILE}
local ret=$?
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;

View File

@@ -1,23 +0,0 @@
<?php
/**
* RippingCluster Library path
*
* Specifies the absolute or relative path to the RippingCluster library directory, relative to the webui root directory.
* Path must end with a trailing slash.
*
* @var string
*/
define('RippingCluster_Lib', '../lib/');
/**
* RippingCluster Database Configuration
*
* Specifies the absolute or relative path to the RippingCluster database configuration file.
* This is a standard ini type file, containing the config required to connect to the database.
*
* @var string
*/
define('RippingCluster_DBConfig', '../dbconfig.conf');
?>

View File

@@ -1,5 +0,0 @@
hostname = localhost
username = handbrake
password = handbrake
dbname = handbrake_cluster

1
externals/sihnon-js-lib vendored Submodule

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

View File

@@ -1,17 +0,0 @@
<?php
class RippingCluster_BackgroundTask {
protected function __construct() {
}
public static function run($command) {
$pipes = array();
$pid = proc_open($command . ' &', array(), $pipes);
proc_close($pid);
}
};
?>

View File

@@ -1,62 +0,0 @@
<?php
class RippingCluster_Cache {
protected $config;
protected $cache_dir;
public function __construct(RippingCluster_Config $config) {
$this->config = $config;
$this->cache_dir = $config->get('cache.base_dir');
if (is_dir($this->cache_dir)) {
if ( ! is_writeable($this->cache_dir)) {
throw new RippingCluster_Exception_InvalidCacheDir();
}
} else {
if ( ! RippingCluster_Main::mkdir_recursive($this->cache_dir)) {
throw new RippingCluster_Exception_InvalidCacheDir();
}
}
}
protected function cacheFilename($source_filename) {
return $this->cache_dir . sha1($source_filename);
}
public function exists($source_filename, $ttl = 3600) {
$cache_filename = $this->cacheFilename($source_filename);
// Check to see if the file is cached
if (!file_exists($cache_filename)) {
return false;
}
// Check to see if the cache has expired
if (filemtime($cache_filename) + $ttl < time()) {
// Delete the cached item
unlink($cache_filename);
return false;
}
return true;
}
public function store($source_filename, $content) {
$cache_filename = $this->cacheFilename($source_filename);
return file_put_contents($cache_filename, $content);
}
public function fetch($source_filename, $ttl = 3600) {
$cache_filename = $this->cacheFilename($source_filename);
if (!$this->exists($source_filename)) {
throw new RippingCluster_Exception_CacheObjectNotFound($source_filename);
}
return file_get_contents($cache_filename);
}
};
?>

View File

@@ -1,13 +0,0 @@
<?php
class RippingCluster_ClientLogEntry extends RippingCluster_LogEntry {
public static function initialise() {
parent::$table_name = 'client_log';
}
};
RippingCluster_ClientLogEntry::initialise();
?>

View File

@@ -1,145 +0,0 @@
<?php
class RippingCluster_Config {
/**
* Boolean value type
* @var bool
*/
const TYPE_BOOL = 'bool';
/**
* Integer value type
* @var int
*/
const TYPE_INT = 'int';
/**
* Float value type
* @var float
*/
const TYPE_FLOAT = 'float';
/**
* String value type
* @var string
*/
const TYPE_STRING = 'string';
/**
* String List value type; list of newline separated strings
* @var array(string)
*/
const TYPE_STRING_LIST = 'array(string)';
/**
* Contents of the dbconfig file
* @var string
*/
private $dbconfig;
/**
* Database object created for the lifetime of this script
* @var RippingCluster_Database
*/
private $database;
/**
* Associative array of connection parameters for the database configuration
* @var array(string=>string)
*/
private $databaseConfig = array();
/**
* Associative array of settings loaded from the database
* @var array(string=>array(string=>string))
*/
private $settings = array();
/**
* Constructs a new instance of the Config class
*
* @param string $dbconfig Database configuration file contents
* @return RippingCluster_Config
*/
public function __construct($dbconfig) {
$this->dbconfig = $dbconfig;
$this->parseDatabaseConfig();
}
/**
* Parses the contents of the database configuration file so that individual settings can be retrieved.
*
*/
public function parseDatabaseConfig() {
$this->databaseConfig = parse_ini_file($this->dbconfig);
}
/**
* Returns the value of the named item from the database configuration file
*
* @param string $key Name of the setting to retrieve
*/
public function getDatabase($key) {
if (!isset($this->databaseConfig[$key])) {
throw new RippingCluster_Exception_DatabaseConfigMissing($key);
}
return $this->databaseConfig[$key];
}
/**
* Sets the database instance used by this object
*
* @param RippingCluster_Database $database Database instance
*/
public function setDatabase(RippingCluster_Database $database) {
$this->database = $database;
$this->preload();
}
/**
* Loads the entire list of settings from the database
*
*/
private function preload() {
if (!$this->database) {
throw new RippingCluster_Exception_NoDatabaseConnection();
}
$this->settings = $this->database->selectAssoc('SELECT name,type,value FROM settings', 'name', array('name', 'value', 'type'));
}
/**
* Identifies whether the named setting exists
*
* @param string $key Name of the setting
* @return bool
*/
public function exists($key) {
return isset($this->settings[$key]);
}
/**
* Fetches the value of the named setting
*
* @param string $key Name of the setting
*/
public function get($key) {
if (!isset($this->settings[$key])) {
throw new RippingCluster_Exception_UnknownSetting($key);
}
switch ($this->settings[$key]['type']) {
case self::TYPE_STRING_LIST:
return array_map('trim', explode("\n", $this->settings[$key]['value']));
default:
return $this->settings[$key]['value'];
}
}
};
?>

View File

@@ -1,141 +0,0 @@
<?php
class RippingCluster_Database {
private $config;
private $dbh;
private $hostname;
private $username;
private $password;
private $dbname;
private $prepared_statements = array();
public function __construct(RippingCluster_Config $config) {
$this->config = $config;
$this->hostname = $this->config->getDatabase('hostname');
$this->username = $this->config->getDatabase('username');
$this->password = $this->config->getDatabase('password');
$this->dbname = $this->config->getDatabase('dbname');
try {
$this->dbh = new PDO("mysql:host={$this->hostname};dbname={$this->dbname}", $this->username, $this->password);
} catch (PDOException $e) {
throw new RippingCluster_Exception_DatabaseConnectFailed($e->getMessage());
}
}
public function __destruct() {
$this->dbh = null;
}
public function selectAssoc($sql, $key_col, $value_cols) {
$results = array();
foreach ($this->dbh->query($sql) as $row) {
if (is_array($value_cols)) {
$values = array();
foreach ($value_cols as $value_col) {
$values[$value_col] = $row[$value_col];
}
$results[$row[$key_col]] = $values;
} else {
$results[$row[$key_col]] = $row[$value_col];
}
}
return $results;
}
public function selectList($sql, $bind_params = null) {
if ($bind_params) {
$stmt = $this->dbh->prepare($sql);
foreach ($bind_params as $param) {
$stmt->bindValue(':'.$param['name'], $param['value'], $param['type']);
}
$result = $stmt->execute();
if (!$result) {
list($dummy, $code, $message) = $stmt->errorInfo();
throw new RippingCluster_Exception_DatabaseQueryFailed($message, $code);
}
return $stmt->fetchAll();
} else {
$results = array();
$result = $this->dbh->query($sql);
foreach ($result as $row) {
$results[] = $row;
}
return $results;
}
}
public function selectOne($sql, $bind_params = null) {
$rows = $this->selectList($sql, $bind_params);
if (count($rows) != 1) {
throw new RippingCluster_Exception_ResultCountMismatch(count($rows));
}
return $rows[0];
}
public function insert($sql, $bind_params = null) {
$stmt = $this->dbh->prepare($sql);
if ($bind_params) {
foreach ($bind_params as $param) {
if (isset($param['type'])) {
$stmt->bindValue(':'.$param['name'], $param['value'], $param['type']);
} else {
$stmt->bindValue(':'.$param['name'], $param['value']);
}
}
}
$result = $stmt->execute();
if (!$result) {
list($code, $dummy, $message) = $stmt->errorInfo();
throw new RippingCluster_Exception_DatabaseQueryFailed($message, $code);
}
}
public function update($sql, $bind_params = null) {
$stmt = $this->dbh->prepare($sql);
if ($bind_params) {
foreach ($bind_params as $param) {
if (isset($param['type'])) {
$stmt->bindValue(':'.$param['name'], $param['value'], $param['type']);
} else {
$stmt->bindValue(':'.$param['name'], $param['value']);
}
}
}
$result = $stmt->execute();
if (!$result) {
list($code, $dummy, $message) = $stmt->errorInfo();
throw new RippingCluster_Exception_DatabaseQueryFailed($message, $code);
}
}
public function errorInfo() {
return $this->dbh->errorInfo();
}
public function lastInsertId() {
return $this->dbh->lastInsertId();
}
}
?>

View File

@@ -1,32 +0,0 @@
<?php
class RippingCluster_Exception extends Exception {};
class RippingCluster_Exception_DatabaseException extends RippingCluster_Exception {};
class RippingCluster_Exception_DatabaseConfigMissing extends RippingCluster_Exception_DatabaseException {};
class RippingCluster_Exception_DatabaseConnectFailed extends RippingCluster_Exception_DatabaseException {};
class RippingCluster_Exception_NoDatabaseConnection extends RippingCluster_Exception_DatabaseException {};
class RippingCluster_Exception_DatabaseQueryFailed extends RippingCluster_Exception_DatabaseException {};
class RippingCluster_Exception_ResultCountMismatch extends RippingCluster_Exception_DatabaseException {};
class RippingCluster_Exception_ConfigException extends RippingCluster_Exception {};
class RippingCluster_Exception_UnknownSetting extends RippingCluster_Exception_ConfigException {};
class RippingCluster_Exception_TemplateException extends RippingCluster_Exception {};
class RippingCluster_Exception_AbortEntirePage extends RippingCluster_Exception_TemplateException {};
class RippingCluster_Exception_Unauthorized extends RippingCluster_Exception_TemplateException {};
class RippingCluster_Exception_FileNotFound extends RippingCluster_Exception_TemplateException {};
class RippingCluster_Exception_InvalidParameters extends RippingCluster_Exception_TemplateException {};
class RippingCluster_Exception_InvalidSourceDirectory extends RippingCluster_Exception {};
class RippingCluster_Exception_CacheException extends RippingCluster_Exception {};
class RippingCluster_Exception_InvalidCacheDir extends RippingCluster_Exception_CacheException {};
class RippingCluster_Exception_CacheObjectNotFound extends RippingCluster_Exception_CacheException {};
class RippingCluster_Exception_LogicException extends RippingCluster_Exception {};
class RippingCluster_Exception_JobNotRunning extends RippingCluster_Exception_LogicException {};
class RippingCluster_Exception_InvalidPluginName extends RippingCluster_Exception {};
?>

View File

@@ -1,113 +0,0 @@
<?php
class RippingCluster_ForegroundTask {
const PIPE_STDIN = 0;
const PIPE_STDOUT = 1;
const PIPE_STDERR = 2;
private function __construct() {
}
/**
*
* Code largely taken from user submitted comment on http://php.sihnon.net/manual/en/function.proc-open.php
* @param unknown_type $command
* @param unknown_type $cwd
* @param unknown_type $env
* @param unknown_type $stdin
* @param unknown_type $callback_stdout
* @param unknown_type $callback_stderr
*/
public static function execute($command, $cwd = null, $env = null, $stdin = null, $callback_stdout = null, $callback_stderr = null, $identifier = null) {
$txOff = 0;
$txLen = strlen($stdin);
$stdout = '';
$stdoutDone = FALSE;
$stderr = '';
$stderrDone = FALSE;
$descriptors = array(
self::PIPE_STDIN => array('pipe', 'r'),
self::PIPE_STDOUT => array('pipe', 'w'),
self::PIPE_STDERR => array('pipe', 'w'),
);
$pipes = array();
$process = proc_open($command, $descriptors, $pipes);
stream_set_blocking($pipes[self::PIPE_STDIN], 0); // Make stdin/stdout/stderr non-blocking
stream_set_blocking($pipes[self::PIPE_STDOUT], 0);
stream_set_blocking($pipes[self::PIPE_STDERR], 0);
if ($txLen == 0) {
fclose($pipes[0]);
}
while (true) {
$rx = array(); // The program's stdout/stderr
if (!$stdoutDone) {
$rx[] = $pipes[self::PIPE_STDOUT];
}
if (!$stderrDone) {
$rx[] = $pipes[self::PIPE_STDERR];
}
$tx = array(); // The program's stdin
if ($txOff < $txLen) {
$tx[] = $pipes[self::PIPE_STDIN];
}
$ex = array();
stream_select($rx, $tx, $ex, null, null); // Block til r/w possible
if (!empty($tx)) {
$txRet = fwrite($pipes[self::PIPE_STDIN], substr($stdin, $txOff, 8192));
if ($txRet !== false) {
$txOff += $txRet;
}
if ($txOff >= $txLen) {
fclose($pipes[self::PIPE_STDIN]);
}
}
foreach ($rx as $r) {
if ($r == $pipes[self::PIPE_STDOUT]) {
$chunk = fread($pipes[self::PIPE_STDOUT], 8192);
if (feof($pipes[self::PIPE_STDOUT])) {
fclose($pipes[self::PIPE_STDOUT]); $stdoutDone = true;
}
if ($callback_stdout) {
call_user_func($callback_stdout, $identifier, $chunk);
} else {
$stdout .= $chunk;
}
} else if ($r == $pipes[self::PIPE_STDERR]) {
$chunk = fread($pipes[self::PIPE_STDERR], 8192);
if (feof($pipes[self::PIPE_STDERR])) {
fclose($pipes[self::PIPE_STDERR]); $stderrDone = true;
}
if ($callback_stderr) {
call_user_func($callback_stderr, $identifier, $chunk);
} else {
$stderr .= $chunk;
}
}
}
if (!is_resource($process))
break;
if ($txOff >= $txLen && $stdoutDone && $stderrDone)
break;
}
return array(proc_close($process), $stdout, $stderr);
}
}

View File

@@ -1,11 +0,0 @@
<?php
interface RippingCluster_IPlugin {
public static function init();
public static function name();
}
?>

View File

@@ -1,9 +0,0 @@
<?php
interface RippingCluster_IPluginFactory {
public static function init();
}
?>

View File

@@ -1,61 +0,0 @@
<?php
class RippingCluster_Log {
private static $hostname = '';
private $database;
private $config;
private $table;
public function __construct(RippingCluster_Database $database, RippingCluster_Config $config, $table) {
$this->database = $database;
$this->config = $config;
$this->table = $table;
}
public function log($severity, $message, $job_id = 0) {
$this->database->insert("INSERT INTO {$this->table} (job_id,level,ctime,pid,hostname,progname,line,message) VALUES(:job_id, :level, :ctime, :pid, :hostname, :progname, :line, :message)",
array(
array('name' => 'job_id', 'value' => $job_id, 'type' => PDO::PARAM_INT),
array('name' => 'level', 'value' => $severity, 'type' => PDO::PARAM_STR),
array('name' => 'ctime', 'value' => time(), 'type' => PDO::PARAM_INT),
array('name' => 'pid', 'value' => 0, 'type' => PDO::PARAM_INT),
array('name' => 'hostname', 'value' => self::$hostname, 'type' => PDO::PARAM_STR),
array('name' => 'progname', 'value' => 'webui', 'type' => PDO::PARAM_STR),
array('name' => 'line', 'value' => 0, 'type' => PDO::PARAM_INT),
array('name' => 'message', 'value' => $message, 'type' => PDO::PARAM_STR)
)
);
if (HBC_File == 'worker') {
echo date("r") . ' ' . $message . "\n";
}
}
public function debug($message, $job_id = 0) {
return $this->log('DEBUG', $message, $job_id);
}
public function info($message, $job_id = 0) {
return $this->log('INFO', $message, $job_id);
}
public function warning($message, $job_id = 0) {
return $this->log('WARNING', $message, $job_id);
}
public function error($message, $job_id = 0) {
return $this->log('ERROR', $message, $job_id);
}
public static function initialise() {
self::$hostname = trim(`hostname`);
}
}
RippingCluster_Log::initialise();
?>

View File

@@ -1,122 +0,0 @@
<?php
abstract class RippingCluster_LogEntry {
protected static $table_name = "";
protected $id;
protected $job_id;
protected $level;
protected $ctime;
protected $pid;
protected $hostname;
protected $progname;
protected $line;
protected $message;
protected function __construct($id, $job_id, $level, $ctime, $pid, $hostname, $progname, $line, $message) {
$this->id = $id;
$this->job_id = $job_id;
$this->level = $level;
$this->ctime = $ctime;
$this->pid = $pid;
$this->hostname = $hostname;
$this->progname = $progname;
$this->line = $line;
$this->message = $message;
}
public static function fromDatabaseRow($row) {
return new RippingCluster_ClientLogEntry(
$row['id'],
$row['job_id'],
$row['level'],
$row['ctime'],
$row['pid'],
$row['hostname'],
$row['progname'],
$row['line'],
$row['message']
);
}
public static function fromId($id) {
$database = RippingCluster_Main::instance()->database();
return RippingCluster_ClientLogEntry::fromDatabaseRow(
$database->selectOne('SELECT * FROM '.self::$table_name.' WHERE id=:id', array(
array('name' => 'id', 'value' => $id, 'type' => PDO::PARAM_INT)
)
)
);
}
public static function recent($limit = 100) {
$entries = array();
$database = RippingCluster_Main::instance()->database();
foreach ($database->selectList('SELECT * FROM '.self::$table_name.' ORDER BY ctime DESC LIMIT :limit', array(
array('name' => 'limit', 'value' => $limit, 'type' => PDO::PARAM_INT)
)) as $row) {
$entries[] = self::fromDatabaseRow($row);
}
return $entries;
}
public static function recentForJob($job_id, $limit = 100) {
$entries = array();
$database = RippingCluster_Main::instance()->database();
foreach ($database->selectList('SELECT * FROM '.self::$table_name.' WHERE job_id=:job_id ORDER BY ctime DESC LIMIT :limit', array(
array('name' => 'job_id', 'value' => $job_id, 'type' => PDO::PARAM_INT),
array('name' => 'limit', 'value' => $limit, 'type' => PDO::PARAM_INT)
)) as $row) {
$entries[] = self::fromDatabaseRow($row);
}
return $entries;
}
public static function allForNoJob() {
return self::allForJob(0);
}
public function id() {
return $this->id;
}
public function jobId() {
return $this->job_id;
}
public function level() {
return $this->level;
}
public function ctime() {
return $this->ctime;
}
public function pid() {
return $this->pid;
}
public function hostname() {
return $this->hostname;
}
public function progname() {
return $this->progname;
}
public function line() {
return $this->line;
}
public function message() {
return $this->message;
}
};
?>

View File

@@ -1,215 +0,0 @@
<?php
require 'smarty/Smarty.class.php';
class RippingCluster_Main {
private static $instance;
private $smarty;
private $config;
private $database;
private $log;
private $request;
private $cache;
private $base_uri;
private function __construct() {
$request_string = isset($_GET['l']) ? $_GET['l'] : '';
$log_table = null;
switch(HBC_File) {
case 'index':
case 'run-jobs': {
$log_table = 'client_log';
} break;
case 'worker': {
$log_table = 'worker_log';
}
}
$this->config = new RippingCluster_Config(RippingCluster_DBConfig);
$this->database = new RippingCluster_Database($this->config);
$this->config->setDatabase($this->database);
$this->log = new RippingCluster_Log($this->database, $this->config, $log_table);
$this->request = new RippingCluster_RequestParser($request_string);
$this->cache = new RippingCluster_Cache($this->config);
$this->smarty = new Smarty();
$this->smarty->template_dir = './templates';
$this->smarty->compile_dir = './tmp/templates';
$this->smarty->cache_dir = './tmp/cache';
$this->smarty->config_fir = './config';
$this->smarty->register_modifier('formatDuration', array('RippingCluster_Main', 'formatDuration'));
$this->smarty->assign('version', '0.1');
$this->base_uri = dirname($_SERVER['SCRIPT_NAME']) . '/';
$this->smarty->assign('base_uri', $this->base_uri);
}
/**
*
* @return RippingCluster_Main
*/
public static function instance() {
if (!self::$instance) {
self::$instance = new RippingCluster_Main();
}
return self::$instance;
}
public function smarty() {
return $this->smarty;
}
/**
*
* @return RippingCluster_Config
*/
public function config() {
return $this->config;
}
/**
*
* @return RippingCluster_Database
*/
public function database() {
return $this->database;
}
/**
*
* @return RippingCluster_Log
*/
public function log() {
return $this->log;
}
/**
*
* @return RippingCluster_RequestParser
*/
public function request() {
return $this->request;
}
/**
*
* @return RippingCluster_Cache
*/
public function cache() {
return $this->cache;
}
public function baseUri() {
return $this->base_uri;
}
public function absoluteUrl($relative_url) {
$secure = isset($_SERVER['secure']);
$port = $_SERVER['SERVER_PORT'];
return 'http' . ($secure ? 's' : '') . '://'
. $_SERVER['HTTP_HOST'] . (($port == 80 || ($secure && $port == 443)) ? '' : ':' . $port)
. '/' . $this->base_uri . $relative_url;
}
public static function initialise() {
spl_autoload_register(array('RippingCluster_Main','autoload'));
}
public static function autoload($classname) {
// Ensure the classname contains only valid class name characters
if (!preg_match('/^[A-Z][a-zA-Z0-9_]*$/', $classname)) {
throw new Exception('Illegal characters in classname'); // TODO Subclass this exception
}
// Ensure the class to load begins with our prefix
if (!preg_match('/^RippingCluster_/', $classname)) {
return;
}
// Special case: All exceptions are stored in the same file
if (preg_match('/^RippingCluster_Exception/', $classname)) {
require_once(RippingCluster_Lib . 'RippingCluster/Exceptions.class.php');
return;
}
// Replace any underscores with directory separators
$filename = RippingCluster_Lib . preg_replace('/_/', '/', $classname);
// Tack on the class file suffix
$filename .= '.class.php';
// If this file exists, load it
if (file_exists($filename)) {
require_once $filename;
}
}
public static function mkdir_recursive($directory, $permissions=0777) {
$parts = explode('/', $directory);
$path = '';
for ($i=1,$l=count($parts); $i<=$l; $i++) {
$iPath = $parts;
$path = join('/', array_slice($iPath, 0, $i));
if (empty($path)) continue;
if (!file_exists($path)) {
if (!mkdir($path)) return false;
if (!chmod($path, $permissions)) return false;
}
}
return true;
}
public static function issetelse($var, $default = null) {
if (isset($var)) {
return $var;
}
if (is_string($default) && preg_match('/^RippingCluster_Exception/', $default) && class_exists($default) && is_subclass_of($default, RippingCluster_Exception)) {
throw new $default();
}
return $default;
}
public static function formatDuration($time) {
if (is_null($time)) {
return 'unknown';
}
$labels = array('seconds', 'minutes', 'hours', 'days', 'weeks', 'months', 'years');
$limits = array(1, 60, 3600, 86400, 604800, 2592000, 31556926, PHP_INT_MAX);
$working_time = $time;
$result = "";
$ptr = count($labels) - 1;
while ($ptr >= 0 && $working_time < $limits[$ptr]) {
--$ptr;
}
while ($ptr >= 0) {
$unit_time = floor($working_time / $limits[$ptr]);
$working_time -= $unit_time * $limits[$ptr];
$result = $result . ' ' . $unit_time . ' ' . $labels[$ptr];
--$ptr;
}
return $result;
}
}
RippingCluster_Main::initialise();
?>

View File

@@ -1,74 +0,0 @@
<?php
class RippingCluster_Page {
private $smarty;
private $request;
private $page;
public function __construct(Smarty $smarty, RippingCluster_RequestParser $request) {
$this->smarty = $smarty;
$this->request = $request;
$this->page = $request->page();
}
public function page() {
return $this->page;
}
public function template_filename() {
return $this->page . '.tpl';
}
public function evaluate($template_variables = array()) {
$code_filename = $this->page . '.php';
$template_filename = $this->template_filename();
try {
$this->render($template_filename, $code_filename, $template_variables);
} catch (RippingCluster_Exception_AbortEntirePage $e) {
return false;
} catch (RippingCluster_Exception_FileNotFound $e) {
$this->render('errors/404.tpl', 'errors/404.php');
} catch (RippingCluster_Exception $e) {
$this->render('errors/unhandled-exception.tpl', 'errors/unhandled-exception.php', array(
'exception' => $e,
));
}
return true;
}
protected function render($template_filename, $code_filename = null, $template_variables = array()) {
if ( ! $this->smarty->template_exists($template_filename)) {
throw new RippingCluster_Exception_FileNotFound($template_filename);
}
// Copy all the template variables into the namespace for this function,
// so that they are readily available to the template
foreach ($template_variables as $__k => $__v) {
$$__k = $__v;
}
// Include the template code file, which will do all the work for this page
$real_code_filename = 'pages/' . $code_filename;
if ($code_filename && file_exists($real_code_filename)) {
include $real_code_filename;
}
// Now execute the template itself, which will render the results of the code file
$this->smarty->assign('page_content', $this->smarty->fetch($template_filename));
}
public static function redirect($relative_url) {
$absolute_url = RippingCluster_Main::instance()->absoluteUrl($relative_url);
header("Location: $absolute_url");
throw new RippingCluster_Exception_AbortEntirePage();
}
};
?>

View File

@@ -1,30 +0,0 @@
<?php
/**
* Base class for all plugins, providing default implementations for
* standard plugin methods.
*
* @class RippingCluster_PluginBase
*/
class RippingCluster_PluginBase {
/**
* Provides a basic initialisation function that does nothing.
*
*/
public static function init() {
// Nothing to do
}
/**
* Returns the name of this plugin
*
* @return string
*/
public static function name() {
return static::PLUGIN_NAME;
}
}
?>

View File

@@ -1,67 +0,0 @@
<?php
abstract class RippingCluster_PluginFactory implements RippingCluster_IPluginFactory {
static private $validPlugins = array();
protected static function ensureScanned() {
if (! isset(self::$validPlugins[get_called_class()])) {
static::scan();
}
}
protected static function isValidPlugin($plugin) {
return isset(self::$validPlugins[get_called_class()][$plugin]);
}
public static function getValidPlugins() {
static::ensureScanned();
return array_keys(self::$validPlugins[get_called_class()]);
}
protected static function findPlugins($directory) {
$plugins = array();
$iterator = new RippingCluster_Utility_ClassFilesIterator(new RippingCluster_Utility_VisibleFilesIterator(new DirectoryIterator(RippingCluster_Lib . $directory)));
foreach ($iterator as /** @var SplFileInfo */ $file) {
$plugin = preg_replace('/.class.php$/', '', $file->getFilename());
$plugins[] = $plugin;
}
return $plugins;
}
protected static function loadPlugins($plugins, $prefix, $interface) {
self::$validPlugins[get_called_class()] = array();
foreach ($plugins as $plugin) {
$fullClassname = $prefix . $plugin;
if ( ! class_exists($fullClassname, true)) {
continue;
}
if ( ! in_array($interface, class_implements($fullClassname))) {
continue;
}
// Initialise the plugin
call_user_func(array($fullClassname, 'init'));
self::$validPlugins[get_called_class()][$plugin] = $fullClassname;
}
}
public static function classname($plugin) {
static::ensureScanned();
if ( ! self::isValidPlugin($plugin)) {
throw new RippingCluster_Exception_InvalidPluginName($plugin);
}
return self::$validPlugins[get_called_class()][$plugin];
}
}
?>

View File

@@ -1,97 +0,0 @@
<?php
class RippingCluster_RequestParser {
private $request_string;
private $page = array();
private $vars = array();
public function __construct($request_string) {
$this->request_string = $request_string;
$this->parse();
}
public function parse() {
if (!$this->request_string) {
$this->page = array('home');
return;
}
$components = explode('/', $this->request_string);
if (!$components) {
return;
}
// Read through the components list looking for elements matching known directories and files
// to determine which page this request is for
$base_dir = 'templates';
while (true) {
if ($components && ! $components[0]) {
// Skip over any empty components before we find a page
array_shift($components);
}
if ($components && is_dir($base_dir . '/' . $components[0])) {
$base_dir .= '/' . $components[0];
array_push($this->page, array_shift($components));
} elseif ($components && is_file($base_dir . '/' . $components[0] . '.tpl')) {
// We have found a valid page, so break the loop here,
// leaving the remaining components as key/value pairs
array_push($this->page, array_shift($components));
break;
} else {
// See if we've already seen a component and assumed it referred to a dir when a file of the same name exists
if ($this->page && is_file($base_dir . '.tpl')) {
break;
} elseif ( ! $components && is_file($base_dir . '/index.tpl')) {
// The last component in the path was a valid directory, and a directory index exists
array_push($this->page, 'index');
break;
} else {
// No valid page was found, so display an error page
$this->page = array('404');
return;
}
}
}
// The subsequent tokens are parameters for this page in key value pairs
while ($components) {
// If the url ended with a trailing slash, the last element will be null
$last_element = $components[count($components) - 1];
if ($last_element == "") {
array_pop($components);
}
$this->vars[array_shift($components)] = $components ? array_shift($components) : true;
}
}
public function page() {
return join('/', $this->page);
}
public function exists($key) {
return isset($this->vars[$key]);
}
public function get($key, $default = null) {
if (isset($this->vars[$key])) {
return $this->vars[$key];
}
if (is_string($default) && preg_match('/^RippingCluster_Exception/', $default) && class_exists($default) && is_subclass_of($default, RippingCluster_Exception)) {
throw new $default();
}
return $default;
}
public function request_string() {
return $this->request_string;
}
};
?>

View File

@@ -1,45 +0,0 @@
<?php
class RippingCluster_Worker {
protected $gearman;
public function __construct() {
$this->init();
}
private function init() {
if ($this->gearman) {
return;
}
$config = RippingCluster_Main::instance()->config();
$this->gearman = new GearmanWorker();
$this->gearman->addServers($config->get('rips.job_servers'));
// Load all the plugin classes
RippingCluster_Worker_PluginFactory::scan();
foreach (RippingCluster_Worker_PluginFactory::getValidPlugins() as $plugin) {
$workerFunctions = RippingCluster_Worker_PluginFactory::getPluginWorkerFunctions($plugin);
foreach ($workerFunctions as $function => $callback) {
$this->gearman->addFunction($function, $callback);
}
}
}
public function start() {
while($this->gearman->work()) {
if ($this->gearman->returnCode() != GEARMAN_SUCCESS) {
break;
}
}
return true;
}
}
?>

View File

@@ -1,21 +0,0 @@
<?php
interface RippingCluster_Worker_IPlugin extends RippingCluster_IPlugin {
/**
* Returns the list of functions (and names) implemented by this plugin for registration with Gearman
*
* @return array(string => callback)
*/
public static function workerFunctions();
/**
* Creates an instance of the Worker plugin, and uses it to execute a single job
*
* @param GearmanJob $job Gearman Job object, describing the work to be done
*/
public static function rip(GearmanJob $job);
}
?>

View File

@@ -1,140 +0,0 @@
<?php
class RippingCluster_Worker_Plugin_HandBrake extends RippingCluster_PluginBase implements RippingCluster_Worker_IPlugin {
const PLUGIN_NAME = 'HandBrake';
const DEINTERLACE_ALWAYS = 1;
const DEINTERLACE_SELECTIVELY = 2;
private $output;
private $gearman_job;
private $job;
private $rip_options;
private function __construct(GearmanJob $gearman_job) {
$this->output = '';
$this->gearman_job = $gearman_job;
$this->rip_options = unserialize($this->gearman_job->workload());
if ( ! $this->rip_options['id']) {
throw new RippingCluster_Exception_LogicException("Job ID must not be zero/null");
}
$this->job = RippingCluster_Job::fromId($this->rip_options['id']);
}
/**
* Returns the list of functions (and names) implemented by this plugin for registration with Gearman
*
* @return array(string => callback)
*/
public static function workerFunctions() {
return array(
'handbrake_rip' => array(__CLASS__, 'rip'),
);
}
/**
* Creates an instance of the Worker plugin, and uses it to execute a single job
*
* @param GearmanJob $job Gearman Job object, describing the work to be done
*/
public static function rip(GearmanJob $job) {
$rip = new self($job);
$rip->execute();
}
private function execute() {
$main = RippingCluster_Main::instance();
$config = $main->config();
$log = $main->log();
$handbrake_cmd_raw = array(
'-n', $config->get('rips.nice'),
$config->get('rips.handbrake_binary'),
$this->evaluateOption('input_filename', '-i'),
$this->evaluateOption('output_filename', '-o'),
$this->evaluateOption('title'),
$this->evaluateOption('format', '-f'),
$this->evaluateOption('video_codec', '-e'),
$this->evaluateOption('quantizer', '-q'),
$this->evaluateOption('video_width', '-w'),
$this->evaluateOption('video_height', '-l'),
$this->evaluateOption('deinterlace'),
$this->evaluateOption('audio_tracks', '-a'),
$this->evaluateOption('audio_codec', '-E'),
$this->evaluateOption('audio_names', '-A'),
$this->evaluateOption('subtitle_tracks', '-s'),
);
$handbrake_cmd = array($config->get('rips.nice_binary'));
foreach(new RecursiveIteratorIterator(new RecursiveArrayIterator($handbrake_cmd_raw)) as $value) {
$handbrake_cmd[] = escapeshellarg($value);
}
$handbrake_cmd = join(' ', $handbrake_cmd);
$log->debug($handbrake_cmd, $this->job->id());
// Change the status of this job to running
$log->debug("Setting status to Running", $this->job->id());
$this->job->updateStatus(RippingCluster_JobStatus::RUNNING, 0);
list($return_val, $stdout, $stderr) = RippingCluster_ForegroundTask::execute($handbrake_cmd, null, null, null, array($this, 'callbackOutput'), array($this, 'callbackOutput'), $this);
if ($return_val) {
$this->gearman_job->sendFail($return_val);
} else {
$this->job->updateStatus(RippingCluster_JobStatus::COMPLETE);
}
}
private function evaluateOption($name, $option = null) {
switch($name) {
case 'title': {
if (!$this->rip_options[$name] || (int)$this->rip_options[$name] < 0) {
return array('-L');
} else {
return array('-t', $this->rip_options[$name]);
}
} break;
case 'deinterlace': {
switch ($this->rip_options[$name]) {
case self::DEINTERLACE_ALWAYS:
return array('-d');
case self::DEINTERLACE_SELECTIVELY:
return array('-5');
default:
return array();
}
}
default:
return array(isset($option) ? $option : $name, $this->rip_options[$name]);
}
}
public function callbackOutput($rip, $data) {
$this->output .= $data;
while (count($lines = preg_split('/[\r\n]+/', $this->output, 2)) > 1) {
$line = $lines[0];
$rip->output = $lines[1];
$matches = array();
if (preg_match('/Encoding: task \d+ of \d+, (\d+\.\d+) %/', $line, $matches)) {
$status = $rip->job->currentStatus();
$status->updateRipProgress($matches[1]);
} else {
$log = RippingCluster_Main::instance()->log();
$log->debug($line, $rip->job->id());
}
}
}
}
?>

View File

@@ -1,13 +0,0 @@
<?php
class RippingCluster_WorkerLogEntry extends RippingCluster_LogEntry {
public static function initialise() {
parent::$table_name = 'worker_log';
}
};
RippingCluster_WorkerLogEntry::initialise();
?>

2
private/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
config.php
dbconfig.conf

92
private/config.php.dist Normal file
View File

@@ -0,0 +1,92 @@
<?php
/**
* Sihnon Framework Library path
*
* Specifies the absolute or relative path to the Sihnon Framework library directory, relative to the webui root directory.
* Path must end with a trailing slash.
*
* @var string
*/
define('SihnonFramework_Lib', '/usr/lib/sihnon-php-lib/source/lib/');
/**
* Sihnon Library path
*
* Specifies the absolute or relative path to the Sihnon library directory, relative to the webui root directory.
* Path must end with a trailing slash.
*
* @var string
*/
define('Sihnon_Lib', '/usr/lib/ripping-cluster/source/lib/');
/**
* RippingCluster Library path
*
* Specifies the absolute or relative path to the RippingCluster library directory, relative to the webui root directory.
* Path must end with a trailing slash.
*
* @var string
*/
define('RippingCluster_Lib', '/usr/lib/ripping-cluster/source/lib/');
/**
* Sihnon Database Support
*
* Specifies whether or not to include database support in the Framework
*
* @var bool
*/
define('Sihnon_DatabaseSupport', true);
/**
* Sihnon Database Configuration
*
* Specifies the absolute or relative path to the Sihnon database configuration file, required when Database support is enabled.
* This is a standard ini type file, containing the config required to connect to the database.
*
* @var string
*/
define('Sihnon_DBConfig', '/etc/ripping-cluster/dbconfig.conf');
/**
* Sihnon Config Plugin
*
* Specifies the plugin to use for configuration value storage.
* Options include:
* - Database (Requires Database Support to be enabled, and the Sihnon_ConfigTable option set)
* - FlatFile (Requires the Sihnon_ConfigFile option set)
*
* @var string
*/
define('Sihnon_ConfigPlugin', 'Database');
/**
* Sihnon Config Table
*
* Specifies the name of the database table thats used for storing configuration values
* when the Database Config Plugin is used.
*
* @var string
*/
define('Sihnon_ConfigTable', 'settings');
/**
* Sihnon Config File
*
* Specifies the name of the file used to store configuration values when the FlatFile Config Plugin is used
*
* @var string
*/
define('Sihnon_ConfigFile', '/etc/ripping-cluster/settings.txt');
/**
* Sihnon Development Mode
*
* Specifies whether or not the Framework should operate in debug mode
*
* @var bool
*/
define('Sihnon_Dev', false);
?>

View File

@@ -0,0 +1,5 @@
hostname = localhost
username = ripping
password = ripping
dbname = ripping_cluster

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

@@ -0,0 +1,145 @@
<?php
class Net_Gearman_Job_HandBrake extends Net_Gearman_Job_Common implements RippingCluster_Worker_IPlugin {
const DEINTERLACE_ALWAYS = 1;
const DEINTERLACE_SELECTIVELY = 2;
private $output;
private $job;
public function __construct($conn, $handle) {
parent::__construct($conn, $handle);
$this->output = '';
}
public static function init() {
}
public static function name() {
}
public function run($args) {;
$main = RippingCluster_Main::instance();
$config = $main->config();
$log = $main->log();
$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, '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'),
self::evaluateOption($args['rip_options'], 'quantizer', '-q'),
self::evaluateOption($args['rip_options'], 'video_width', '-w'),
self::evaluateOption($args['rip_options'], 'video_height', '-l'),
self::evaluateOption($args['rip_options'], 'deinterlace'),
self::evaluateOption($args['rip_options'], 'audio_tracks', '-a'),
self::evaluateOption($args['rip_options'], 'audio_codec', '-E'),
self::evaluateOption($args['rip_options'], 'audio_names', '-A'),
self::evaluateOption($args['rip_options'], 'subtitle_tracks', '-s'),
);
$handbrake_cmd = array($config->get('rips.nice_binary'));
foreach(new RecursiveIteratorIterator(new RecursiveArrayIterator($handbrake_cmd_raw)) as $value) {
$handbrake_cmd[] = escapeshellarg($value);
}
$handbrake_cmd = join(' ', $handbrake_cmd);
RippingCluster_WorkerLogEntry::debug($log, $this->job->id(), $handbrake_cmd);
// Change the status of this job to running
RippingCluster_WorkerLogEntry::debug($log, $this->job->id(), "Setting status to Running");
$this->job->updateStatus(RippingCluster_JobStatus::RUNNING, 0);
list($return_val, $stdout, $stderr) = RippingCluster_ForegroundTask::execute($handbrake_cmd, null, null, null, array($this, 'callbackOutput'), array($this, 'callbackOutput'), $this);
if ($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 {
// 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.');
}
}
}
private static function evaluateOption($options, $name, $option = null) {
switch($name) {
case 'title': {
if (!$options[$name] || (int)$options[$name] < 0) {
return array('-L');
} else {
return array('-t', $options[$name]);
}
} break;
case 'deinterlace': {
switch ($options[$name]) {
case self::DEINTERLACE_ALWAYS:
return array('-d');
case self::DEINTERLACE_SELECTIVELY:
return array('-5');
default:
return array();
}
}
default:
return array(isset($option) ? $option : $name, $options[$name]);
}
}
public function callbackOutput($rip, $data) {
$this->output .= $data;
while (count($lines = preg_split('/[\r\n]+/', $this->output, 2)) > 1) {
$line = $lines[0];
$rip->output = $lines[1];
$matches = array();
if (preg_match('/Encoding: task \d+ of \d+, (\d+\.\d+) %/', $line, $matches)) {
$status = $rip->job->currentStatus();
$status->updateRipProgress($matches[1]);
$this->status($matches[1], 100);
} else if (!preg_match('/^\s+$/', $line)) {
$log = RippingCluster_Main::instance()->log();
RippingCluster_WorkerLogEntry::debug($log, $rip->job->id(), $line);
}
}
}
}
?>

View File

@@ -0,0 +1,23 @@
<?php
class RippingCluster_ClientLogEntry extends RippingCluster_LogEntry {
public static function debug($logger, $job_id, $message) {
static::log($logger, SihnonFramework_Log::LEVEL_DEBUG, $job_id, $message, 'client');
}
public static function info($logger, $job_id, $message) {
static::log($logger, SihnonFramework_Log::LEVEL_INFO, $job_id, $message, 'client');
}
public static function warning($logger, $job_id, $message) {
static::log($logger, SihnonFramework_Log::LEVEL_WARNING, $job_id, $message, 'client');
}
public static function error($logger, $job_id, $message) {
static::log($logger, SihnonFramework_Log::LEVEL_ERROR, $job_id, $message, 'client');
}
};
?>

View File

@@ -0,0 +1,11 @@
<?php
class RippingCluster_Exception extends Exception {};
class RippingCluster_Exception_InvalidSourceDirectory extends RippingCluster_Exception {};
class RippingCluster_Exception_LogicException extends RippingCluster_Exception {};
class RippingCluster_Exception_JobNotRunning extends RippingCluster_Exception_LogicException {};
?>

View File

@@ -111,7 +111,7 @@ class RippingCluster_Job {
$jobs = array();
$database = RippingCluster_Main::instance()->database();
foreach ($database->selectList('SELECT * FROM jobs WHERE id > 0') as $row) {
foreach ($database->selectList('SELECT * FROM jobs WHERE id > 0 ORDER BY id DESC') as $row) {
$job = self::fromDatabaseRow($row);
self::$cache[$job->id] = $job;
@@ -135,7 +135,7 @@ class RippingCluster_Job {
$params[] = array('name' => 'limit', 'value' => $limit, 'type' => PDO::PARAM_INT);
}
foreach ($database->selectList("SELECT * FROM jobs WHERE id IN (SELECT job_id FROM job_status_current WHERE id > 0 AND status=:status) ORDER BY id {$limitSql}", $params) as $row) {
foreach ($database->selectList("SELECT * FROM jobs WHERE id IN (SELECT job_id FROM job_status_current WHERE id > 0 AND status=:status) ORDER BY id DESC {$limitSql}", $params) as $row) {
$jobs[] = self::fromDatabaseRow($row);
}
@@ -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),
@@ -212,18 +214,16 @@ class RippingCluster_Job {
$database->update(
'DELETE FROM jobs WHERE id=:job_id LIMIT 1',
array(
array(name => 'job_id', value => $this->id, type => PDO::PARAM_INT),
array('name' => 'job_id', 'value' => $this->id, 'type' => PDO::PARAM_INT),
)
);
$this->id = null;
}
public function queue($gearman) {
public function queue() {
$main = RippingCluster_Main::instance();
$config = $main->config();
$log = $main->log();
$log->info('Starting job', $this->id);
// Construct the rip options
$rip_options = array(
@@ -244,16 +244,10 @@ class RippingCluster_Job {
'subtitle_tracks' => $this->subtitle_tracks,
);
// Enqueue this rip
if ( ! $this->id) {
throw new RippingCluster_Exception_LogicException("Rip cannot be queued without being saved!");
}
$task = $gearman->addTask('handbrake_rip', serialize($rip_options), $config->get('rips.context'), $this->id);
if ($task) {
$this->updateStatus(RippingCluster_JobStatus::QUEUED);
} else {
$this->updateStatus(RippingCluster_JobStatus::FAILED);
}
return array(
'method' => 'HandBrake',
'rip_options' => $rip_options
);
}
protected function loadStatuses() {
@@ -282,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) {
@@ -292,6 +299,7 @@ class RippingCluster_Job {
$running_time = $current_status->mtime() - $current_status->ctime();
$progress = $current_status->ripProgress();
$remaining_time = 0;
if ($progress > 0) {
$remaining_time = round((100 - $progress) * ($running_time / $progress));
}
@@ -329,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;
@@ -337,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 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

@@ -0,0 +1,86 @@
<?php
class RippingCluster_LogEntry extends SihnonFramework_LogEntry {
protected $job_id;
protected static $types;
public static function initialise() {
// 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, $hostname, $progname, $pid, $file, $line, $message, $job_id) {
parent::__construct($level, $category, $ctime, $hostname, $progname, $pid, $file, $line, $message);
$this->job_id = $job_id;
}
public static function fromArray($row) {
return new self(
$row['level'],
$row['category'],
$row['ctime'],
$row['hostname'],
$row['progname'],
$row['pid'],
$row['file'],
$row['line'],
$row['message'],
$row['job_id']
);
}
public function values() {
return array(
$this->level,
$this->category,
$this->ctime,
$this->hostname,
$this->progname,
$this->pid,
$this->file,
$this->line,
$this->message,
$this->job_id,
);
}
public function jobId() {
return $this->job_id;
}
protected static function log($logger, $severity, $job_id, $message, $category = SihnonFramework_Log::CATEGORY_DEFAULT) {
$backtrace = debug_backtrace(false);
$entry = new self($severity, $category, time(), static::$localHostname, static::$localProgname, getmypid(), $backtrace[1]['file'], $backtrace[1]['line'], $message, $job_id);
$logger->log($entry);
}
public static function debug($logger, $job_id, $message, $category = SihnonFramework_Log::CATEGORY_DEFAULT) {
static::log($logger, SihnonFramework_Log::LEVEL_DEBUG, $job_id, $message, $category);
}
public static function info($logger, $job_id, $message, $category = SihnonFramework_Log::CATEGORY_DEFAULT) {
static::log($logger, SihnonFramework_Log::LEVEL_INFO, $job_id, $message, $category);
}
public static function warning($logger, $job_id, $message, $category = SihnonFramework_Log::CATEGORY_DEFAULT) {
static::log($logger, SihnonFramework_Log::LEVEL_WARNING, $job_id, $message, $category);
}
public static function error($logger, $job_id, $message, $category = SihnonFramework_Log::CATEGORY_DEFAULT) {
static::log($logger, SihnonFramework_Log::LEVEL_ERROR, $job_id, $message, $category);
}
}
RippingCluster_LogEntry::initialise();
?>

View File

@@ -0,0 +1,67 @@
<?php
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;
protected $request;
protected function __construct() {
parent::__construct();
}
protected function init() {
parent::init();
$request_string = isset($_GET['l']) ? $_GET['l'] : '';
$this->request = new RippingCluster_RequestParser($request_string, self::TEMPLATE_DIR, self::CODE_DIR);
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;
}
}
public function smarty() {
return $this->smarty;
}
/**
*
* @return RippingCluster_RequestParser
*/
public function request() {
return $this->request;
}
}
?>

View File

@@ -25,7 +25,7 @@ class RippingCluster_Rips_SourceAudioTrack {
}
public function name() {
return $name;
return $this->name;
}
public function setName($name) {

View File

@@ -87,7 +87,7 @@ class RippingCluster_Source {
}
$longest_index = null;
$maximmum_duration = 0;
$maximum_duration = 0;
if ( ! $this->titles) {
return null;
@@ -104,6 +104,14 @@ class RippingCluster_Source {
return $longest_index;
}
/**
* Permanently deletes this source from disk
*
*/
public function delete() {
RippingCluster_Source_PluginFactory::delete($this->plugin, $this->filename);
}
public function filename() {
return $this->filename;

View File

@@ -41,13 +41,21 @@ interface RippingCluster_Source_IPlugin extends RippingCluster_IPlugin {
public static function loadEncoded($encoded_filename, $scan = true, $use_cache = true);
/**
* Determins if a filename is a valid source loadable using this plugin
* Determines if a filename is a valid source loadable using this plugin
*
* @param string $source_filename Filename of the source
* @return bool
*/
public static function isValidSource($source_filename);
/**
* Permanently deletes the given source from disk
*
* @param RippingCluster_Source $source Source object to be deleted
* @return bool
*/
public static function delete($source_filename);
}
?>

View File

@@ -68,6 +68,8 @@ class RippingCluster_Source_Plugin_Bluray extends RippingCluster_PluginBase impl
$source->cache();
}
}
return $source;
}
/**
@@ -117,6 +119,20 @@ class RippingCluster_Source_Plugin_Bluray extends RippingCluster_PluginBase impl
return true;
}
/**
* Permanently deletes the given source from disk
*
* @param RippingCluster_Source $source Source object to be deleted
* @return bool
*/
public static function delete($source_filename) {
if ( ! self::isValidSource($source_filename)) {
return false;
}
return RippingCluster_Main::rmdir_recursive($source_filename);
}
}
?>

View File

@@ -217,6 +217,21 @@ class RippingCluster_Source_Plugin_HandBrake extends RippingCluster_PluginBase i
return false;
}
/**
* Permanently deletes the given source from disk
*
* @param RippingCluster_Source $source Source object to be deleted
* @return bool
*/
public static function delete($source_filename) {
if ( ! self::isValidSource($source_filename)) {
return false;
}
return RippingCluster_Main::rmdir_recursive($source_filename);
}
}
?>

View File

@@ -244,6 +244,20 @@ class RippingCluster_Source_Plugin_MkvInfo extends RippingCluster_PluginBase imp
return true;
}
/**
* Permanently deletes the given source from disk
*
* @param RippingCluster_Source $source Source object to be deleted
* @return bool
*/
public static function delete($source_filename) {
if ( ! self::isValidSource($source_filename)) {
return false;
}
return unlink($source_filename);
}
}
?>

View File

@@ -2,20 +2,17 @@
class RippingCluster_Source_PluginFactory extends RippingCluster_PluginFactory {
const PLUGIN_DIR = 'RippingCluster/Source/Plugin/';
const PLUGIN_PREFIX = 'RippingCluster_Source_Plugin_';
const PLUGIN_INTERFACE = 'RippingCluster_Source_IPlugin';
protected static $plugin_prefix = 'RippingCluster_Source_Plugin_';
protected static $plugin_interface = 'RippingCluster_Source_IPlugin';
protected static $plugin_dir = array(
RippingCluster_Lib => 'RippingCluster/Source/Plugin/',
);
public static function init() {
}
public static function scan() {
$candidatePlugins = parent::findPlugins(self::PLUGIN_DIR);
self::loadPlugins($candidatePlugins, self::PLUGIN_PREFIX, self::PLUGIN_INTERFACE);
}
public static function enumerate($plugin) {
self::ensureScanned();
@@ -57,7 +54,7 @@ class RippingCluster_Source_PluginFactory extends RippingCluster_PluginFactory {
return call_user_func(array(self::classname($plugin), 'loadEncoded'), $encoded_filename, $scan, $use_cache);
}
public static function isValidSource($plugin, $source_filename) {
/*public static function isValidSource($plugin, $source_filename) {
self::ensureScanned();
if ( ! self::isValidPlugin($plugin)) {
@@ -65,6 +62,22 @@ class RippingCluster_Source_PluginFactory extends RippingCluster_PluginFactory {
}
return call_user_func(array(self::classname($plugin), 'isValidSource'), source_filename);
}*/
/**
* Permanently deletes the given source from disk
*
* @param string $plugin Name of the plugin used to load the source
* @param string $source_filename Filename of the source to be deleted
*/
public static function delete($plugin, $source_filename) {
self::ensureScanned();
if ( ! self::isValidPlugin($plugin)) {
return null;
}
return call_user_func(array(self::classname($plugin), 'delete'), $source_filename);
}
}

View File

@@ -0,0 +1,42 @@
<?php
class RippingCluster_Worker {
protected $gearman;
public function __construct() {
$this->init();
}
private function init() {
if ($this->gearman) {
return;
}
$config = RippingCluster_Main::instance()->config();
$this->gearman = new Net_Gearman_Worker($config->get('rips.job_servers'));
// Load all the plugin classes
RippingCluster_Worker_PluginFactory::scan();
$plugins = RippingCluster_Worker_PluginFactory::getValidPlugins();
foreach ($plugins as $plugin) {
$this->gearman->addAbility($plugin);
}
}
public function start() {
try {
$this->gearman->beginWork();
} catch (Net_Gearman_Exception $e) {
RippingCluster_WorkerLogEntry::error(SihnonFramework_Main::instance()->log(), 0, $e->toText());
return false;
}
return true;
}
}
?>

View File

@@ -0,0 +1,16 @@
<?php
interface RippingCluster_Worker_IPlugin extends RippingCluster_IPlugin {
/**
* Returns the list of functions (and names) implemented by this plugin for registration with Gearman
*
* @return array(string => callback)
*/
//public static function workerFunctions();
//public static function run($args);
}
?>

View File

@@ -51,6 +51,11 @@ class RippingCluster_Worker_Bluray extends RippingCluster_PluginBase implements
$this->job = RippingCluster_Job::fromId($this->rip_options['id']);
}
public static function init() {
// Nothing to do
}
/**
* Returns the list of functions (and names) implemented by this plugin for registration with Gearman
*
@@ -62,14 +67,9 @@ class RippingCluster_Worker_Bluray extends RippingCluster_PluginBase implements
);
}
/**
* Creates an instance of the Worker plugin, and uses it to execute a single job
*
* @param GearmanJob $job Gearman Job object, describing the work to be done
*/
public static function rip(GearmanJob $job) {
$rip = new self($job);
$rip->execute();
public static function run($args) {
//$rip = new self($job);
//$rip->execute();
}
/**

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