diff --git a/deluge/ui/webui/templates/ajax/TODO b/deluge/ui/webui/templates/ajax/TODO
new file mode 100644
index 000000000..378a06ad4
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/TODO
@@ -0,0 +1,8 @@
+[features]
+ Improve torrent adding to include options, file upload
+ Implement the Options Menu
+ Implement the connection manager
+
+[bugs]
+ If a torrent is removed from the core it continues to try and get details
+ if it was the selected torrent.
diff --git a/deluge/ui/webui/templates/ajax/index.html b/deluge/ui/webui/templates/ajax/index.html
new file mode 100644
index 000000000..219c094ed
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/index.html
@@ -0,0 +1,51 @@
+
+
+
+ Deluge: AJAX UI (alpha)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/deluge/ui/webui/templates/ajax/meta.cfg b/deluge/ui/webui/templates/ajax/meta.cfg
new file mode 100644
index 000000000..3f5cc7a20
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/meta.cfg
@@ -0,0 +1,9 @@
+{
+ 'authors':['Damien Churchill '],
+ 'inherits':['classic'],
+ 'comment':"""Advanced AJAX enabled template.
+ Making use of the new json/rpc api to update
+ the data on the fly.
+ """
+}
+
diff --git a/deluge/ui/webui/templates/ajax/render/html/preferences_bandwidth.html b/deluge/ui/webui/templates/ajax/render/html/preferences_bandwidth.html
new file mode 100644
index 000000000..a333d2d89
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/render/html/preferences_bandwidth.html
@@ -0,0 +1,16 @@
+
diff --git a/deluge/ui/webui/templates/ajax/render/html/preferences_daemon.html b/deluge/ui/webui/templates/ajax/render/html/preferences_daemon.html
new file mode 100644
index 000000000..45a2b4094
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/render/html/preferences_daemon.html
@@ -0,0 +1,14 @@
+
diff --git a/deluge/ui/webui/templates/ajax/render/html/preferences_download.html b/deluge/ui/webui/templates/ajax/render/html/preferences_download.html
new file mode 100644
index 000000000..05a914e41
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/render/html/preferences_download.html
@@ -0,0 +1,24 @@
+
diff --git a/deluge/ui/webui/templates/ajax/render/html/preferences_network.html b/deluge/ui/webui/templates/ajax/render/html/preferences_network.html
new file mode 100644
index 000000000..a6b86047b
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/render/html/preferences_network.html
@@ -0,0 +1,40 @@
+
diff --git a/deluge/ui/webui/templates/ajax/render/html/preferences_queue.html b/deluge/ui/webui/templates/ajax/render/html/preferences_queue.html
new file mode 100644
index 000000000..be67fd78b
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/render/html/preferences_queue.html
@@ -0,0 +1,21 @@
+
diff --git a/deluge/ui/webui/templates/ajax/render/html/preferences_webui.html b/deluge/ui/webui/templates/ajax/render/html/preferences_webui.html
new file mode 100644
index 000000000..10edb27dd
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/render/html/preferences_webui.html
@@ -0,0 +1,31 @@
+
diff --git a/deluge/ui/webui/templates/ajax/render/html/tab_details.html b/deluge/ui/webui/templates/ajax/render/html/tab_details.html
new file mode 100644
index 000000000..b9f62b43e
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/render/html/tab_details.html
@@ -0,0 +1,10 @@
+
+ $_('Name'):
+ $_('Hash'):
+ $_('Path'):
+ $_('Total Size'):
+ $_('# of files'):
+ $_('Status'):
+ $_('Tracker'):
+
+
diff --git a/deluge/ui/webui/templates/ajax/render/html/tab_files.html b/deluge/ui/webui/templates/ajax/render/html/tab_files.html
new file mode 100644
index 000000000..d412c2ba9
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/render/html/tab_files.html
@@ -0,0 +1,13 @@
+
diff --git a/deluge/ui/webui/templates/ajax/render/html/tab_options.html b/deluge/ui/webui/templates/ajax/render/html/tab_options.html
new file mode 100644
index 000000000..f4a14c0c4
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/render/html/tab_options.html
@@ -0,0 +1,26 @@
+
+
diff --git a/deluge/ui/webui/templates/ajax/render/html/tab_peers.html b/deluge/ui/webui/templates/ajax/render/html/tab_peers.html
new file mode 100644
index 000000000..f800793cd
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/render/html/tab_peers.html
@@ -0,0 +1,14 @@
+
diff --git a/deluge/ui/webui/templates/ajax/render/html/tab_statistics.html b/deluge/ui/webui/templates/ajax/render/html/tab_statistics.html
new file mode 100644
index 000000000..9a6871738
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/render/html/tab_statistics.html
@@ -0,0 +1,25 @@
+
+ $_('Downloaded'):
+ $_('Uploaded'):
+ $_('Share Ratio'):
+ $_('Next Announce'):
+ $_('Tracker Status'):
+
+
+ $_('Speed'):
+ $_('Speed'):
+ $_('ETA'):
+ $_('Pieces'):
+
+
+ $_('Seeders'):
+ $_('Peers'):
+ $_('Availability'):
+ $_('Auto Managed'):
+
+
+ $_('Active Time'):
+ $_('Seeding Time'):
+ $_('Seed Rank'):
+
+
diff --git a/deluge/ui/webui/templates/ajax/render/html/window_add_torrent.html b/deluge/ui/webui/templates/ajax/render/html/window_add_torrent.html
new file mode 100644
index 000000000..c35d77604
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/render/html/window_add_torrent.html
@@ -0,0 +1,15 @@
+
+
+
diff --git a/deluge/ui/webui/templates/ajax/render/html/window_preferences.html b/deluge/ui/webui/templates/ajax/render/html/window_preferences.html
new file mode 100644
index 000000000..986ea6d3c
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/render/html/window_preferences.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ $_('Reset') $_('Apply')
+
+
diff --git a/deluge/ui/webui/templates/ajax/static/css/Roar.css b/deluge/ui/webui/templates/ajax/static/css/Roar.css
new file mode 100644
index 000000000..efa6f539d
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/static/css/Roar.css
@@ -0,0 +1,55 @@
+.roar-body
+{
+ position: absolute;
+ font: 12px/14px "Lucida Grande", Arial, Helvetica, Verdana, sans-serif;
+ color: #fff;
+ text-align: left;
+ z-index: 999;
+}
+
+.roar
+{
+ position: absolute;
+ width: 300px;
+ cursor: pointer;
+}
+.roar-bg
+{
+ position: absolute;
+ z-index: 1000;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+ background-color: #000;
+ -moz-border-radius: 10px;
+ -webkit-border-radius: 5px;
+ -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
+}
+.roar-body-ugly .roar
+{
+ background-color: #333;
+}
+.roar-body-ugly .roar-bg
+{
+ display: none;
+}
+.roar h3
+{
+ position: relative;
+ padding: 15px 10px 0;
+ margin: 0;
+ border: 0;
+ font-size: 13px;
+ color: #fff;
+ z-index: 1002;
+}
+.roar p
+{
+ position: relative;
+ padding: 10px 10px 15px;
+ margin: 0;
+ font-size: 12px;
+ color: #fff;
+ z-index: 1002;
+}
\ No newline at end of file
diff --git a/deluge/ui/webui/templates/ajax/static/css/mooui.css b/deluge/ui/webui/templates/ajax/static/css/mooui.css
new file mode 100644
index 000000000..4a4878c70
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/static/css/mooui.css
@@ -0,0 +1,156 @@
+/*
+ * Stylesheet: Widgets.PopupMenu.css
+ *
+ * Depends: []
+ */
+
+ul.mooui-menu {
+ position: absolute;
+ z-index: 1000;
+ padding: 0;
+}
+
+ul.mooui-menu li {
+ position: relative;
+ list-style: none;
+ margin: 1px;
+ padding-left: 2px;
+ padding-right: 10px;
+ line-height: 20px;
+ height: 20px;
+ background: no-repeat 2px;
+}
+
+ul.mooui-menu li.mooui-menu-icon {
+ padding-left: 20px;
+}
+
+ul.mooui-menu li.mooui-menu-sep {
+ height: 1px;
+}
+
+ul.mooui-menu li.mooui-menu-toggle input {
+ cursor: pointer;
+}
+
+ul.mooui-menu li.mooui-menu-toggle span span {
+ position: relative;
+ top: -3px;
+}
+
+ul.mooui-menu li > ul {
+ display: none;
+ position: relative;
+ top: -20px;
+ width: 400px;
+}
+
+ul.mooui-menu li:hover {
+ cursor: pointer;
+}
+
+ul.mooui-menu li:hover > ul {
+ display: block;
+}
+
+ul.mooui-menu li.mooui-menu-sep:hover {
+ cursor: default;
+}
+
+/*
+ * Stylesheet: Widgets.ProgressBar.css
+ *
+ * Depends: []
+ */
+
+.mooui-progressbar span {
+ text-align: center;
+ float: left;
+}
+
+/*
+ * Stylesheet: Widgets.SplitPane.css
+ *
+ * Depends: []
+ */
+
+.mooui-pane {
+ position: absolute;
+}
+
+.mooui-splitter {
+ position: absolute;
+ background: Gray;
+}
+
+.mooui-splitter-vertical {
+ cursor: e-resize;
+}
+
+.mooui-splitter-horizontal {
+ cursor: n-resize;
+}
+
+/*
+ * Stylesheet: Widgets.SplitPane.css
+ *
+ * Depends: []
+ */
+
+.mooui-tabs ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.mooui-tabs li {
+ float: left;
+ padding: 4px;
+ cursor: pointer;
+ height: 14px;
+ -moz-border-radius: 5px 5px 0 0;
+}
+
+.mooui-tabs div {
+ clear: left;
+}
+
+.mooui-tabpage {
+ display: none;
+}
+
+.mooui-tabpage-active {
+ display: block;
+}
+
+/*
+ * Stylesheet: Widgets.Window.css
+ *
+ * Depends: []
+ */
+
+.mooui-window {
+ position: fixed;
+ top: 0px;
+ left: 200px;
+ z-index: 100;
+}
+
+.mooui-window-title {
+ margin: 10px;
+ cursor: pointer;
+ float: left;
+}
+
+.mooui-window-close {
+ float: right;
+ width: 16px;
+ height: 16px;
+ margin: 5px;
+ cursor: pointer;
+}
+
+.mooui-window-content {
+ clear: both;
+ padding: 5px;
+}
\ No newline at end of file
diff --git a/deluge/ui/webui/templates/ajax/static/icons/16/gtk-edit.png b/deluge/ui/webui/templates/ajax/static/icons/16/gtk-edit.png
new file mode 100644
index 000000000..5200e14f5
Binary files /dev/null and b/deluge/ui/webui/templates/ajax/static/icons/16/gtk-edit.png differ
diff --git a/deluge/ui/webui/templates/ajax/static/icons/16/gtk-yes.png b/deluge/ui/webui/templates/ajax/static/icons/16/gtk-yes.png
new file mode 100644
index 000000000..f04fc1612
Binary files /dev/null and b/deluge/ui/webui/templates/ajax/static/icons/16/gtk-yes.png differ
diff --git a/deluge/ui/webui/templates/ajax/static/icons/16/network-idle.png b/deluge/ui/webui/templates/ajax/static/icons/16/network-idle.png
new file mode 100644
index 000000000..0efee57e5
Binary files /dev/null and b/deluge/ui/webui/templates/ajax/static/icons/16/network-idle.png differ
diff --git a/deluge/ui/webui/templates/ajax/static/icons/16/view-refresh.png b/deluge/ui/webui/templates/ajax/static/icons/16/view-refresh.png
new file mode 100644
index 000000000..3fd71d6e5
Binary files /dev/null and b/deluge/ui/webui/templates/ajax/static/icons/16/view-refresh.png differ
diff --git a/deluge/ui/webui/templates/ajax/static/icons/16/view-sort-ascending.png b/deluge/ui/webui/templates/ajax/static/icons/16/view-sort-ascending.png
new file mode 100644
index 000000000..beba44948
Binary files /dev/null and b/deluge/ui/webui/templates/ajax/static/icons/16/view-sort-ascending.png differ
diff --git a/deluge/ui/webui/templates/ajax/static/icons/16/view-sort-descending.png b/deluge/ui/webui/templates/ajax/static/icons/16/view-sort-descending.png
new file mode 100644
index 000000000..ea4abc4d6
Binary files /dev/null and b/deluge/ui/webui/templates/ajax/static/icons/16/view-sort-descending.png differ
diff --git a/deluge/ui/webui/templates/ajax/static/icons/32/add.png b/deluge/ui/webui/templates/ajax/static/icons/32/add.png
new file mode 100644
index 000000000..2acdd8f51
Binary files /dev/null and b/deluge/ui/webui/templates/ajax/static/icons/32/add.png differ
diff --git a/deluge/ui/webui/templates/ajax/static/icons/32/connections.png b/deluge/ui/webui/templates/ajax/static/icons/32/connections.png
new file mode 100644
index 000000000..4137b3c3b
Binary files /dev/null and b/deluge/ui/webui/templates/ajax/static/icons/32/connections.png differ
diff --git a/deluge/ui/webui/templates/ajax/static/icons/32/deluge.png b/deluge/ui/webui/templates/ajax/static/icons/32/deluge.png
new file mode 100644
index 000000000..2d272f2ea
Binary files /dev/null and b/deluge/ui/webui/templates/ajax/static/icons/32/deluge.png differ
diff --git a/deluge/ui/webui/templates/ajax/static/icons/32/down.png b/deluge/ui/webui/templates/ajax/static/icons/32/down.png
new file mode 100644
index 000000000..dce3f15ef
Binary files /dev/null and b/deluge/ui/webui/templates/ajax/static/icons/32/down.png differ
diff --git a/deluge/ui/webui/templates/ajax/static/icons/32/options.png b/deluge/ui/webui/templates/ajax/static/icons/32/options.png
new file mode 100644
index 000000000..3ec71a357
Binary files /dev/null and b/deluge/ui/webui/templates/ajax/static/icons/32/options.png differ
diff --git a/deluge/ui/webui/templates/ajax/static/icons/32/pause.png b/deluge/ui/webui/templates/ajax/static/icons/32/pause.png
new file mode 100644
index 000000000..1e9f4d535
Binary files /dev/null and b/deluge/ui/webui/templates/ajax/static/icons/32/pause.png differ
diff --git a/deluge/ui/webui/templates/ajax/static/icons/32/remove.png b/deluge/ui/webui/templates/ajax/static/icons/32/remove.png
new file mode 100644
index 000000000..c5524f728
Binary files /dev/null and b/deluge/ui/webui/templates/ajax/static/icons/32/remove.png differ
diff --git a/deluge/ui/webui/templates/ajax/static/icons/32/resume.png b/deluge/ui/webui/templates/ajax/static/icons/32/resume.png
new file mode 100644
index 000000000..66f32d89b
Binary files /dev/null and b/deluge/ui/webui/templates/ajax/static/icons/32/resume.png differ
diff --git a/deluge/ui/webui/templates/ajax/static/icons/32/up.png b/deluge/ui/webui/templates/ajax/static/icons/32/up.png
new file mode 100644
index 000000000..afb307b18
Binary files /dev/null and b/deluge/ui/webui/templates/ajax/static/icons/32/up.png differ
diff --git a/deluge/ui/webui/templates/ajax/static/images/spacer.gif b/deluge/ui/webui/templates/ajax/static/images/spacer.gif
new file mode 100644
index 000000000..91ee09790
Binary files /dev/null and b/deluge/ui/webui/templates/ajax/static/images/spacer.gif differ
diff --git a/deluge/ui/webui/templates/ajax/static/js/Roar.js b/deluge/ui/webui/templates/ajax/static/js/Roar.js
new file mode 100644
index 000000000..78580f8c0
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/static/js/Roar.js
@@ -0,0 +1,166 @@
+/**
+ * Roar - Notifications
+ *
+ * Inspired by Growl
+ *
+ * @version 1.0.1
+ *
+ * @license MIT-style license
+ * @author Harald Kirschner
+ * @copyright Author
+ */
+
+var Roar = new Class({
+
+ Implements: [Options, Events, Chain],
+
+ options: {
+ duration: 3000,
+ position: 'upperLeft',
+ container: null,
+ bodyFx: null,
+ itemFx: null,
+ margin: {x: 10, y: 10},
+ offset: 10,
+ className: 'roar',
+ onShow: $empty,
+ onHide: $empty,
+ onRender: $empty
+ },
+
+ initialize: function(options) {
+ this.setOptions(options);
+ this.items = [];
+ this.container = $(this.options.container) || document;
+ },
+
+ alert: function(title, message, options) {
+ var params = Array.link(arguments, {title: String.type, message: String.type, options: Object.type});
+ var items = [new Element('h3', {'html': $pick(params.title, '')})];
+ if (params.message) items.push(new Element('p', {'html': params.message}));
+ return this.inject(items, params.options);
+ },
+
+ inject: function(elements, options) {
+ if (!this.body) this.render();
+ options = options || {};
+
+ var offset = [-this.options.offset, 0];
+ var last = this.items.getLast();
+ if (last) {
+ offset[0] = last.retrieve('roar:offset');
+ offset[1] = offset[0] + last.offsetHeight + this.options.offset;
+ }
+ var to = {'opacity': 1};
+ to[this.align.y] = offset;
+
+ var item = new Element('div', {
+ 'class': this.options.className,
+ 'opacity': 0
+ }).adopt(
+ new Element('div', {
+ 'class': 'roar-bg',
+ 'opacity': 0.7
+ }),
+ elements
+ );
+
+ item.setStyle(this.align.x, 0).store('roar:offset', offset[1]).set('morph', $merge({
+ unit: 'px',
+ link: 'cancel',
+ onStart: Chain.prototype.clearChain,
+ transition: Fx.Transitions.Back.easeOut
+ }, this.options.itemFx));
+
+ var remove = this.remove.create({
+ bind: this,
+ arguments: [item],
+ delay: 10
+ });
+ this.items.push(item.addEvent('click', remove));
+
+ if (this.options.duration) {
+ var over = false;
+ var trigger = (function() {
+ trigger = null;
+ if (!over) remove();
+ }).delay(this.options.duration);
+ item.addEvents({
+ mouseover: function() {
+ over = true;
+ },
+ mouseout: function() {
+ over = false;
+ if (!trigger) remove();
+ }
+ });
+ }
+ item.inject(this.body).morph(to);
+ return this.fireEvent('onShow', [item, this.items.length]);
+ },
+
+ remove: function(item) {
+ var index = this.items.indexOf(item);
+ if (index == -1) return this;
+ this.items.splice(index, 1);
+ item.removeEvents();
+ var to = {opacity: 0};
+ to[this.align.y] = item.getStyle(this.align.y).toInt() - item.offsetHeight - this.options.offset;
+ item.morph(to).get('morph').chain(item.destroy.bind(item));
+ return this.fireEvent('onHide', [item, this.items.length]).callChain(item);
+ },
+
+ empty: function() {
+ while (this.items.length) this.remove(this.items[0]);
+ return this;
+ },
+
+ render: function() {
+ this.position = this.options.position;
+ if ($type(this.position) == 'string') {
+ var position = {x: 'center', y: 'center'};
+ this.align = {x: 'left', y: 'top'};
+ if ((/left|west/i).test(this.position)) position.x = 'left';
+ else if ((/right|east/i).test(this.position)) this.align.x = position.x = 'right';
+ if ((/upper|top|north/i).test(this.position)) position.y = 'top';
+ else if ((/bottom|lower|south/i).test(this.position)) this.align.y = position.y = 'bottom';
+ this.position = position;
+ }
+ this.body = new Element('div', {'class': 'roar-body'}).inject(document.body);
+ if (Browser.Engine.trident4) this.body.addClass('roar-body-ugly');
+ this.moveTo = this.body.setStyles.bind(this.body);
+ this.reposition();
+ if (this.options.bodyFx) {
+ var morph = new Fx.Morph(this.body, $merge({
+ unit: 'px',
+ chain: 'cancel',
+ transition: Fx.Transitions.Circ.easeOut
+ }, this.options.bodyFx));
+ this.moveTo = morph.start.bind(morph);
+ }
+ var repos = this.reposition.bind(this);
+ window.addEvents({
+ scroll: repos,
+ resize: repos
+ });
+ this.fireEvent('onRender', this.body);
+ },
+
+ reposition: function() {
+ var max = document.getCoordinates(), scroll = document.getScroll(), margin = this.options.margin;
+ max.left += scroll.x;
+ max.right += scroll.x;
+ max.top += scroll.y;
+ max.bottom += scroll.y;
+ var rel = ($type(this.container) == 'element') ? this.container.getCoordinates() : max;
+ this.moveTo({
+ left: (this.position.x == 'right')
+ ? (Math.min(rel.right, max.right) - margin.x)
+ : (Math.max(rel.left, max.left) + margin.x),
+ top: (this.position.y == 'bottom')
+ ? (Math.min(rel.bottom, max.bottom) - margin.y)
+ : (Math.max(rel.top, max.top) + margin.y)
+ });
+ }
+
+});
\ No newline at end of file
diff --git a/deluge/ui/webui/templates/ajax/static/js/Rpc.js b/deluge/ui/webui/templates/ajax/static/js/Rpc.js
new file mode 100644
index 000000000..2d068da19
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/static/js/Rpc.js
@@ -0,0 +1,83 @@
+/*
+ * Script: Rpc.js
+ * A JSON-RPC proxy
+ *
+ * Copyright:
+ * Damien Churchill (c) 2008
+ */
+
+/*
+ * A class that creates a proxy for sending remote procedure calls over JSON
+ */
+JSON.RPC = new Class({
+ Implements: Options,
+
+ options: {
+ async: true,
+ methods: []
+ },
+
+ initialize: function(url, options) {
+ this.setOptions(options)
+ this.url = url
+ if (this.options.methods.length == 0) {
+ this._execute('system.listMethods', {async: false}).each(function(method) {
+ this[method] = function() {
+ var options = this._parseargs(arguments)
+ return this._execute(method, options)
+ }.bind(this)
+ }, this)
+ }
+ },
+
+ _parseargs: function(args) {
+ var params = $A(args), options = params.getLast()
+ if ($type(options) == 'object') {
+ var option_keys = ['async', 'onRequest', 'onComplete',
+ 'onSuccess', 'onFailure', 'onException', 'onCancel'], keys =
+ new Hash(options).getKeys(), is_option = false
+
+ option_keys.each(function(key) {
+ if (keys.contains(key)) {
+ is_option = true
+ }
+ })
+
+ if (is_option) {
+ params.erase(options)
+ } else {
+ options = {}
+ }
+ } else { options = {} }
+ options.params = params
+ return options
+ },
+
+ _execute: function(method, options) {
+ options = $pick(options, {})
+ options.params = $pick(options.params, [])
+ options.async = $pick(options.async, this.options.async)
+
+ data = JSON.encode({
+ method: method,
+ params: options.params,
+ id: 1
+ })
+
+ var request = new Request.JSON({
+ url: this.url,
+ async: options.async,
+ onRequest: options.onRequest,
+ onComplete: options.onComplete,
+ onSuccess: function(response) {
+ if (options.onSuccess) {options.onSuccess(response.result)}
+ },
+ onFailure: options.onFailure,
+ onException: options.onException,
+ onCancel: options.onCancel
+ }).send(data)
+ if (!options.async) {
+ return request.response.json.result
+ }
+ }
+})
diff --git a/deluge/ui/webui/templates/ajax/static/js/deluge.js b/deluge/ui/webui/templates/ajax/static/js/deluge.js
new file mode 100644
index 000000000..2af41cf90
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/static/js/deluge.js
@@ -0,0 +1,302 @@
+Deluge.UI = {
+ initialize: function() {
+ this.torrents = {};
+ this.torrentIds = [];
+ Deluge.Client = new JSON.RPC('/json/rpc');
+
+ var theme = Cookie.read('theme');
+ if (theme) this.setTheme(theme);
+ else this.setTheme('classic');
+
+ this.bound = {
+ updated: this.updated.bindWithEvent(this),
+ resized: this.resized.bindWithEvent(this),
+ toolbar_click: this.toolbar_click.bindWithEvent(this),
+ file_priorities: this.file_priorities.bindWithEvent(this),
+ labelsChanged: this.labelsChanged.bindWithEvent(this)
+ };
+ this.loadUi.delay(100, this);
+ },
+
+ loadUi: function() {
+ this.vbox = new Widgets.VBox('page', {expand: true});
+
+ this.toolbar = new Deluge.Widgets.Toolbar();
+ this.addWindow = new Deluge.Widgets.AddWindow();
+ this.prefsWindow = new Deluge.Widgets.PreferencesWindow();
+
+ this.statusbar = new Deluge.Widgets.StatusBar();
+ this.labels = new Deluge.Widgets.Labels()
+ this.details = new Deluge.Widgets.Details()
+
+ this.initialize_grid()
+
+ this.split_horz = new Widgets.SplitPane('top', this.labels, this.grid, {
+ pane1: {min: 150},
+ pane2: {min: 100, expand: true}
+ });
+ var details = $W('details')
+ this.split_vert = new Widgets.SplitPane('main', this.split_horz, details, {
+ direction: 'vertical',
+ pane1: {min: 100, expand: true},
+ pane2: {min: 200}
+ });
+
+ this.vbox.addBox(this.toolbar, {fixed: true});
+ this.vbox.addBox(this.split_vert);
+ this.vbox.addBox(this.statusbar, {fixed: true});
+ this.vbox.calculatePositions();
+ this.details.expand()
+
+ this.toolbar.addEvent('button_click', this.bound.toolbar_click);
+ this.details.addEvent('filesAction', this.bound.file_priorities)
+ this.labels.addEvent('stateChanged', this.bound.labelsChanged)
+ details.addEvent('resize', function(e) {
+ this.details.expand()
+ }.bindWithEvent(this))
+
+ window.addEvent('resize', this.bound.resized);
+ Deluge.UI.update();
+ },
+
+ initialize_grid: function() {
+ this.grid = new Deluge.Widgets.TorrentGrid('torrents')
+
+ var menu = new Widgets.PopupMenu()
+ menu.add([
+ {type:'text',action:'pause',text:'Pause',icon:'/static/images/tango/pause.png'},
+ {type:'text',action:'resume',text:'Resume',icon:'/static/images/tango/start.png'},
+ {type:'seperator'},
+ {type:'submenu',text:'Options',icon:'/static/images/tango/preferences-system.png',items: [
+ {type:'submenu',text:'D/L Speed Limit',icon:'/pixmaps/downloading16.png',items: [
+ {type:'text',action:'max_download_speed',value:5,text:'5 KiB/s'},
+ {type:'text',action:'max_download_speed',value:10,text:'10 KiB/s'},
+ {type:'text',action:'max_download_speed',value:30,text:'30 KiB/s'},
+ {type:'text',action:'max_download_speed',value:80,text:'80 KiB/s'},
+ {type:'text',action:'max_download_speed',value:300,text:'300 KiB/s'},
+ {type:'text',action:'max_download_speed',value:-1,text:'Unlimited'}
+ ]},
+ {type:'submenu',text:'U/L Speed Limit',icon:'/pixmaps/seeding16.png',items: [
+ {type:'text',action:'max_upload_speed',value:5,text:'5 KiB/s'},
+ {type:'text',action:'max_upload_speed',value:10,text:'10 KiB/s'},
+ {type:'text',action:'max_upload_speed',value:30,text:'30 KiB/s'},
+ {type:'text',action:'max_upload_speed',value:80,text:'80 KiB/s'},
+ {type:'text',action:'max_upload_speed',value:300,text:'300 KiB/s'},
+ {type:'text',action:'max_upload_speed',value:-1,text:'Unlimited'}
+ ]},
+ {type:'submenu',text:'Connection Limit',icon:'/static/images/tango/connections.png',items: [
+ {type:'text',action:'max_connections',value:50,text:'50'},
+ {type:'text',action:'max_connections',value:100,text:'100'},
+ {type:'text',action:'max_connections',value:200,text:'200'},
+ {type:'text',action:'max_connections',value:300,text:'300'},
+ {type:'text',action:'max_connections',value:500,text:'500'},
+ {type:'text',action:'max_connections',value:-1,text:'Unlimited'}
+ ]},
+ {type:'submenu',text:'Upload Slot Limit',icon:'/template/static/icons/16/view-sort-ascending.png',items: [
+ {type:'text',action:'max_upload_slots',value:0,text:'0'},
+ {type:'text',action:'max_upload_slots',value:1,text:'1'},
+ {type:'text',action:'max_upload_slots',value:2,text:'2'},
+ {type:'text',action:'max_upload_slots',value:3,text:'3'},
+ {type:'text',action:'max_upload_slots',value:5,text:'5'},
+ {type:'text',action:'max_upload_slots',value:-1,text:'Unlimited'}
+ ]},
+ {type:'toggle',action:'auto_managed',value:false,text:'Auto Managed'}
+ ]},
+ {type:'seperator'},
+ {type:'submenu',text:'Queue',icon:'/template/static/icons/16/view-sort-descending.png',items:[
+ {type:'text',action:'top',text:'Top',icon:'/static/images/tango/go-top.png'},
+ {type:'text',action:'up',text:'Up',icon:'/static/images/tango/queue-up.png'},
+ {type:'text',action:'down',text:'Down',icon:'/static/images/tango/queue-down.png'},
+ {type:'text',action:'bottom',text:'Bottom',icon:'/static/images/tango/go-bottom.png'}
+ ]},
+ {type: 'seperator'},
+ {type:'text',action:'update_tracker',text:'Update Tracker',icon:'/template/static/icons/16/view-refresh.png'},
+ {type:'text',action:'edit_trackers',text:'Edit Trackers',icon:'/template/static/icons/16/gtk-edit.png'},
+ {type:'seperator'},
+ {type:'submenu',action:'remove',value:0,text:'Remove Torrent',icon:'/static/images/tango/list-remove.png', items:[
+ {type:'text',action:'remove',value:0,text:'From Session'},
+ {type:'text',action:'remove',value:1,text:'... and delete Torrent file'},
+ {type:'text',action:'remove',value:2,text:'... and delete Downloaded files'},
+ {type:'text',action:'remove',value:3,text:'... and delete All files'}
+ ]},
+ {type:'seperator'},
+ {type:'text',action:'force_recheck',text:'Force Recheck',icon:'/static/images/tango/edit-redo.png'},
+ {type:'text',action:'move_storage',text:'Move Storage',icon:'/static/images/tango/move.png'}
+ ]);
+
+ menu.addEvent('action', function(e) {
+ this.torrent_action(e.action, e.value)
+ }.bind(this))
+
+ this.grid.addEvent('row_menu', function(e) {
+ e.stop()
+ var value = this.grid.selectedRow.torrent.is_auto_managed;
+ menu.items[3].items[4].set(value)
+ menu.torrent_id = e.row_id
+ menu.show(e)
+ }.bindWithEvent(this))
+
+ this.grid.addEvent('selectedchanged', function(e) {
+ if ($chk(this.grid.selectedRow))
+ this.details.update(this.grid.selectedRow.id);
+ else
+ this.details.update(null);
+ }.bindWithEvent(this))
+ },
+
+ setTheme: function(name, fn) {
+ this.theme = name;
+ if (this.themecss) this.themecss.destroy();
+ this.themecss = new Asset.css('/template/static/themes/' + name + '/style.css');
+ Cookie.write('theme', name);
+ if (this.vbox) this.vbox.refresh();
+ },
+
+ run: function() {
+ if (!this.running) {
+ this.running = this.update.periodical(2000, this);
+ }
+ },
+
+ stop: function() {
+ if (this.running) {
+ $clear(this.running);
+ this.running = false;
+ }
+ },
+
+ update: function() {
+ filter = {}
+ if (this.labels.state != 'All') filter.state = this.labels.state
+ Deluge.Client.update_ui(Deluge.Keys.Grid, filter, {
+ onSuccess: this.bound.updated
+ })
+ },
+
+ updated: function(data) {
+ this.torrents = new Hash(data.torrents);
+ this.stats = data.stats;
+ this.filters = data.filters
+ this.torrents.each(function(torrent, torrent_id) {
+ torrent.id = torrent_id;
+ })
+ this.grid.update_torrents(this.torrents);
+ this.statusbar.update(this.stats);
+
+ if ($chk(this.grid.selectedRow))
+ this.details.update(this.grid.selectedRow.id);
+ else
+ this.details.update(null);
+
+ this.labels.update(this.filters)
+ },
+
+ file_priorities: function(event) {
+ Deluge.Client.get_torrent_status(event.torrentId, ['file_priorities'], {
+ onSuccess: function(result) {
+ var priorities = result.file_priorities
+ priorities.each(function(priority, index) {
+ if (event.files.contains(index)) priorities[index] = event.action
+ })
+ Deluge.Client.set_torrent_file_priorities(event.torrentId, priorities, {
+ onSuccess: function(response) {
+ this.details.update(event.torrentId)
+ }.bindWithEvent(this)
+ })
+ }.bindWithEvent(this)
+ })
+ },
+
+ resized: function(event) {
+ this.vbox.calculatePositions();
+ },
+
+ toolbar_click: function(event) {
+ this.torrent_action(event.action);
+ },
+
+ labelsChanged: function(event) {
+ this.update()
+ },
+
+ torrent_action: function(action, value) {
+ var torrentIds = this.grid.get_selected_torrents()
+ switch (action) {
+ case 'resume':
+ Deluge.Client.resume_torrent(torrentIds)
+ break;
+ case 'pause':
+ Deluge.Client.pause_torrent(torrentIds)
+ break;
+ case 'top':
+ Deluge.Client.queue_top(torrentIds)
+ break;
+ case 'up':
+ Deluge.Client.queue_up(torrentIds)
+ break;
+ case 'down':
+ Deluge.Client.queue_down(torrentIds)
+ break;
+ case 'bottom':
+ Deluge.Client.queue_bottom(torrentIds)
+ break;
+ case 'force_recheck':
+ Deluge.Client.force_recheck(torrentIds)
+ break;
+ case 'update_tracker':
+ Deluge.Client.force_reannounce(torrentIds)
+ break;
+ case 'max_download_speed':
+ torrentIds.each(function(torrentId) {
+ Deluge.Client.set_torrent_max_download_speed(torrentId, value.toInt())
+ })
+ break;
+ case 'max_upload_speed':
+ torrentIds.each(function(torrentId) {
+ Deluge.Client.set_torrent_max_upload_speed(torrentId, value.toInt())
+ })
+ break;
+ case 'max_connections':
+ torrentIds.each(function(torrentId) {
+ Deluge.Client.set_torrent_max_connections(torrentId, value.toInt())
+ })
+ break;
+ case 'max_upload_slots':
+ torrentIds.each(function(torrentId) {
+ Deluge.Client.set_torrent_max_upload_slots(torrentId, value.toInt())
+ })
+ break;
+ case 'auto_managed':
+ torrentIds.each(function(torrentId) {
+ Deluge.Client.set_torrent_auto_managed(torrentId, value)
+ })
+ break;
+ case 'add':
+ this.addWindow.show()
+ break;
+ case 'remove':
+ var removeTorrent = false, removeFiles = false;
+ if (value == 1) removeTorrent = true;
+ else if (value == 2) removeFiles = true;
+ else if (value > 3) {
+ removeTorrent = true;
+ removeFiles = true;
+ }
+ Deluge.Client.remove_torrent(torrentIds, removeTorrent, removeFiles);
+ break;
+ case 'preferences':
+ this.prefsWindow.show()
+ break;
+ default:
+ break;
+ }
+ this.update()
+ }
+};
+
+window.addEvent('domready', function(e) {
+ Deluge.UI.initialize();
+ Deluge.UI.run();
+});
+
+
diff --git a/deluge/ui/webui/templates/ajax/static/js/deluge.widgets.js b/deluge/ui/webui/templates/ajax/static/js/deluge.widgets.js
new file mode 100644
index 000000000..ba8a21233
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/static/js/deluge.widgets.js
@@ -0,0 +1,909 @@
+/*
+ * Script: deluge.widgets.js
+ * Collection of widgets for use in the deluge web-ui
+ *
+ * Copyright:
+ * Damien Churchill (c) 2008
+ */
+
+var Deluge = $empty;
+
+Deluge.Keys = {
+ Grid: ['queue', 'name', 'total_size', 'state', 'progress',
+ 'num_seeds', 'total_seeds', 'num_peers', 'total_peers',
+ 'download_payload_rate', 'upload_payload_rate', 'eta', 'ratio',
+ 'distributed_copies' ,'is_auto_managed'],
+
+ Statistics: ['total_done', 'total_payload_download', 'total_uploaded',
+ 'total_payload_upload', 'next_announce', 'tracker_status',
+ 'num_pieces', 'piece_length', 'is_auto_managed', 'active_time',
+ 'seeding_time', 'seed_rank'],
+
+ Files: ['files', 'file_progress', 'file_priorities'],
+
+ Peers: ['peers', 'seeds'],
+
+ Details: ['name', 'save_path', 'total_size', 'num_files', 'tracker_status', 'tracker'],
+
+ Options: ['max_download_speed', 'max_upload_speed', 'max_connections',
+ 'max_upload_slots','is_auto_managed', 'stop_at_ratio',
+ 'stop_ratio', 'remove_at_ratio', 'private', 'prioritize_first_last']
+}
+
+Deluge.Keys.Statistics.extend(Deluge.Keys.Grid)
+
+Deluge.Widgets = $empty;
+
+Deluge.Widgets.Details = new Class({
+ Extends: Widgets.Tabs,
+
+ initialize: function() {
+ this.parent($$('#details .mooui-tabs')[0])
+
+ this.statistics = new Deluge.Widgets.StatisticsPage()
+ this.details = new Deluge.Widgets.DetailsPage()
+ this.files = new Deluge.Widgets.FilesPage()
+ this.peers = new Deluge.Widgets.PeersPage()
+ this.options = new Deluge.Widgets.OptionsPage()
+
+ this.addPage(this.statistics)
+ this.addPage(this.details)
+ this.addPage(this.files)
+ this.addPage(this.peers)
+ this.addPage(this.options)
+ this.addEvent('pageChanged', function(e) {
+ this.update(this.torrentId);
+ }.bindWithEvent(this));
+ this.addEvent('resize', this.resized.bindWithEvent(this))
+
+ this.files.addEvent('menuAction', function(e) {
+ files = []
+ this.files.grid.get_selected().each(function(file) {
+ files.push(file.fileIndex)
+ })
+ e.files = files
+ this.fireEvent('filesAction', e)
+ }.bindWithEvent(this))
+ },
+
+ keys: {
+ 0: Deluge.Keys.Statistics,
+ 1: Deluge.Keys.Details,
+ 2: Deluge.Keys.Files,
+ 3: Deluge.Keys.Peers,
+ 4: Deluge.Keys.Options
+ },
+
+ update: function(torrentId) {
+ this.torrentId = torrentId
+ if (!this.torrentId) return
+ var keys = this.keys[this.currentPage], page = this.pages[this.currentPage];
+ Deluge.Client.get_torrent_status(torrentId, keys, {
+ onSuccess: function(torrent) {
+ torrent.id = torrentId
+ if (page.update) page.update(torrent)
+ }.bindWithEvent(this)
+ })
+ },
+
+ resized: function(event) {
+ this.pages.each(function(page) {
+ page.getSizeModifiers()
+ page.sets({
+ width: event.width - page.element.modifiers.x,
+ height: event.height - page.element.modifiers.y - 28
+ })
+ })
+ }
+});
+
+Deluge.Widgets.StatisticsPage = new Class({
+ Extends: Widgets.TabPage,
+
+ options: {
+ url: '/template/render/html/tab_statistics.html'
+ },
+
+ initialize: function() {
+ this.parent('Statistics')
+ },
+
+ update: function(torrent) {
+ var data = {
+ downloaded: torrent.total_done.toBytes()+' ('+torrent.total_payload_download.toBytes()+')',
+ uploaded: torrent.total_uploaded.toBytes()+' ('+torrent.total_payload_upload.toBytes()+')',
+ share: torrent.ratio.toFixed(3),
+ announce: torrent.next_announce.toTime(),
+ tracker_status: torrent.tracker_status,
+ downspeed: torrent.download_payload_rate.toSpeed(),
+ upspeed: torrent.upload_payload_rate.toSpeed(),
+ eta: torrent.eta.toTime(),
+ pieces: torrent.num_pieces + ' (' + torrent.piece_length.toBytes() + ')',
+ seeders: torrent.num_seeds + ' (' + torrent.total_seeds + ')',
+ peers: torrent.num_peers + ' (' + torrent.total_peers + ')',
+ avail: torrent.distributed_copies.toFixed(3),
+ active_time: torrent.active_time.toTime(),
+ seeding_time: torrent.seeding_time.toTime(),
+ seed_rank: torrent.seed_rank
+ }
+
+ if (torrent.is_auto_managed) {data.auto_managed = 'True'}
+ else {data.auto_managed = 'False'}
+
+ this.element.getElements('dd').each(function(item) {
+ item.set('text', data[item.getProperty('class')])
+ }, this)
+ }
+})
+
+Deluge.Widgets.DetailsPage = new Class({
+ Extends: Widgets.TabPage,
+
+ options: {
+ url: '/template/render/html/tab_details.html'
+ },
+
+ initialize: function() {
+ this.parent('Details')
+ },
+
+ update: function(torrent) {
+ var data = {
+ torrent_name: torrent.name,
+ hash: torrent.id,
+ path: torrent.save_path,
+ size: torrent.total_size.toBytes(),
+ files: torrent.num_files,
+ status: torrent.tracker_status,
+ tracker: torrent.tracker
+ }
+ this.element.getElements('dd').each(function(item) {
+ item.set('text', data[item.getProperty('class')])
+ }, this)
+ }
+})
+
+Deluge.Widgets.FilesGrid = new Class({
+ Extends: Widgets.DataGrid,
+
+ options: {
+ columns: [
+ {name: 'filename',text: 'Filename',type:'text',width: 350},
+ {name: 'size',text: 'Size',type:'bytes',width: 80},
+ {name: 'progress',text: 'Progress',type:'progress',width: 180},
+ {name: 'priority',text: 'Priority',type:'icon',width: 150}
+ ]
+ },
+
+ priority_texts: {
+ 0: 'Do Not Download',
+ 1: 'Normal Priority',
+ 2: 'High Priority',
+ 5: 'Highest Priority'
+ },
+
+ priority_icons: {
+ 0: '/static/images/tango/process-stop.png',
+ 1: '/template/static/icons/16/gtk-yes.png',
+ 2: '/static/images/tango/queue-down.png',
+ 5: '/static/images/tango/go-bottom.png'
+ },
+
+ initialize: function(element, options) {
+ this.parent(element, options)
+ var menu = new Widgets.PopupMenu()
+ $A([0,1,2,5]).each(function(index) {
+ menu.add({
+ type:'text',
+ action: index,
+ text: this.priority_texts[index],
+ icon: this.priority_icons[index]
+ })
+ }, this)
+
+ menu.addEvent('action', function(e) {
+ e = {
+ action: e.action,
+ torrentId: menu.row.torrentId
+ }
+ this.fireEvent('menuAction', e)
+ }.bind(this))
+
+ this.addEvent('row_menu', function(e) {
+ e.stop()
+ menu.row = e.row
+ menu.show(e)
+ })
+ },
+
+ clear: function() {
+ this.rows.empty()
+ this.body.empty()
+ this.render()
+ },
+
+ update_files: function(torrent) {
+ torrent.files.each(function(file) {
+ var p = torrent.file_priorities[file.index]
+ var priority = {text:this.priority_texts[p], icon:this.priority_icons[p]}
+
+ var percent = torrent.file_progress[file.index]*100.0;
+ row = {
+ id: torrent.id + '-' + file.index,
+ data: {
+ filename: file.path,
+ size: file.size,
+ progress: {percent: percent, text: percent.toFixed(2) + '%'},
+ priority: priority
+ },
+ fileIndex: file.index,
+ torrentId: torrent.id
+
+ }
+ if (this.has(row.id)) {
+ this.updateRow(row, true)
+ } else {
+ this.addRow(row, true)
+ }
+ }, this)
+ this.render()
+ }
+});
+
+Deluge.Widgets.FilesPage = new Class({
+ Extends: Widgets.TabPage,
+
+ options: {
+ url: '/template/render/html/tab_files.html'
+ },
+
+ initialize: function(el) {
+ this.parent('Files')
+ this.torrentId = -1
+ this.addEvent('loaded', this.loaded.bindWithEvent(this))
+ this.addEvent('resize', this.resized.bindWithEvent(this))
+ },
+
+ loaded: function(event) {
+ this.grid = new Deluge.Widgets.FilesGrid('files')
+ this.grid.addEvent('menuAction', this.menu_action.bindWithEvent(this))
+
+ if (this.been_resized) {
+ this.resized(this.been_resized)
+ delete this.been_resized
+ }
+ },
+
+ resize_check: function(event) {
+
+ },
+
+ resized: function(e) {
+ if (!this.grid) {
+ this.been_resized = e;
+ return
+ }
+
+ this.element.getPadding()
+ this.grid.sets({
+ width: e.width - this.element.padding.x,
+ height: e.height - this.element.padding.y
+ })
+ },
+
+ menu_action: function(e) {
+ this.fireEvent('menuAction', e)
+ },
+
+ update: function(torrent) {
+ if (this.torrentId != torrent.id) {
+ this.torrentId = torrent.id
+ this.grid.rows.empty()
+ this.grid.body.empty()
+ }
+ this.grid.update_files(torrent)
+ }
+})
+
+Deluge.Widgets.PeersPage = new Class({
+ Extends: Widgets.TabPage,
+
+ options: {
+ url: '/template/render/html/tab_peers.html'
+ },
+
+ initialize: function(el) {
+ this.parent('Peers')
+ this.addEvent('resize', this.resized.bindWithEvent(this))
+ this.addEvent('loaded', this.loaded.bindWithEvent(this))
+ },
+
+ loaded: function(event) {
+ this.grid = new Widgets.DataGrid($('peers'), {
+ columns: [
+ {name: 'country',type:'image',width: 20},
+ {name: 'address',text: 'Address',type:'text',width: 80},
+ {name: 'client',text: 'Client',type:'text',width: 180},
+ {name: 'downspeed',text: 'Down Speed',type:'speed',width: 100},
+ {name: 'upspeed',text: 'Up Speed',type:'speed',width: 100},
+ ]})
+ this.torrentId = -1
+ if (this.been_resized) {
+ this.resized(this.been_resized)
+ delete this.been_resized
+ }
+ },
+
+ resized: function(e) {
+ if (!this.grid) {
+ this.been_resized = e;
+ return
+ }
+
+ this.element.getPadding()
+ this.grid.sets({
+ width: e.width - this.element.padding.x,
+ height: e.height - this.element.padding.y
+ })
+ },
+
+ update: function(torrent) {
+ if (this.torrentId != torrent.id) {
+ this.torrentId = torrent.id
+ this.grid.rows.empty()
+ this.grid.body.empty()
+ }
+ var peers = []
+ torrent.peers.each(function(peer) {
+ if (peer.country.strip() != '') {
+ peer.country = '/pixmaps/flags/' + peer.country.toLowerCase() + '.png'
+ } else {
+ peer.country = '/templates/static/images/spacer.gif'
+ }
+ row = {
+ id: peer.ip,
+ data: {
+ country: peer.country,
+ address: peer.ip,
+ client: peer.client,
+ downspeed: peer.down_speed,
+ upspeed: peer.up_speed
+ }
+ }
+ if (this.grid.has(row.id)) {
+ this.grid.updateRow(row, true)
+ } else {
+ this.grid.addRow(row, true)
+ }
+ peers.include(peer.ip)
+ }, this)
+
+ this.grid.rows.each(function(row) {
+ if (!peers.contains(row.id)) {
+ row.element.destroy()
+ this.grid.rows.erase(row)
+ }
+ }, this)
+ this.grid.render()
+ }
+});
+
+Deluge.Widgets.OptionsPage = new Class({
+ Extends: Widgets.TabPage,
+
+ options: {
+ url: '/template/render/html/tab_options.html'
+ },
+
+ initialize: function() {
+ if (!this.element)
+ this.parent('Options');
+ this.addEvent('loaded', function(event) {
+ this.loaded(event)
+ }.bindWithEvent(this))
+ },
+
+ loaded: function(event) {
+ this.bound = {
+ apply: this.apply.bindWithEvent(this),
+ reset: this.reset.bindWithEvent(this)
+ }
+ this.form = this.element.getElement('form');
+ this.changed = new Hash()
+ this.form.getElements('input').each(function(el) {
+ if (el.type == 'button') return;
+ el.focused = false
+ el.addEvent('change', function(e) {
+ if (!this.changed[this.torrentId])
+ this.changed[this.torrentId] = {}
+ if (el.type == 'checkbox')
+ this.changed[this.torrentId][el.name] = el.checked;
+ else
+ this.changed[this.torrentId][el.name] = el.value;
+ }.bindWithEvent(this));
+ el.addEvent('focus', function(e) {
+ el.focused = true;
+ });
+ el.addEvent('blur', function(e) {
+ el.focused = false;
+ });
+ }, this);
+
+ this.form.apply.addEvent('click', this.bound.apply);
+ this.form.reset.addEvent('click', this.bound.reset);
+ },
+
+ apply: function(event) {
+ if (!this.torrentId) return
+ var changed = this.changed[this.torrentId]
+ if ($defined(changed['is_auto_managed'])) {
+ changed['auto_managed'] = changed['is_auto_managed']
+ delete changed['is_auto_managed']
+ }
+ Deluge.Client.set_torrent_options(this.torrentId, changed, {
+ onSuccess: function(event) {
+ delete this.changed[this.torrentId]
+ }.bindWithEvent(this)
+ })
+ },
+
+ reset: function(event) {
+ if (this.torrentId) {
+ delete this.changed[this.torrentId]
+ }
+ Deluge.Client.get_torrent_status(this.torrentId, Deluge.Keys.Options, {
+ onSuccess: function(torrent) {
+ torrent.id = this.torrentId
+ this.update(torrent)
+ }.bindWithEvent(this)
+ })
+ },
+
+ update: function(torrent) {
+ this.torrentId = torrent.id;
+ $each(torrent, function(value, key) {
+ var changed = this.changed[this.torrentId]
+ if (changed && $defined(changed[key])) return;
+ var type = $type(value);
+ if (type == 'boolean') {
+ this.form[key].checked = value;
+ } else {
+ if (!this.form[key].focused)
+ this.form[key].value = value;
+ };
+ if (key == 'private' && value == 0) {
+ this.form[key].disabled = true
+ this.form[key].getParent().addClass('opt-disabled')
+ }
+ }, this);
+ }
+});
+
+Deluge.Widgets.Toolbar = new Class({
+ Implements: Events,
+ Extends: Widgets.Base,
+
+ initialize: function() {
+ this.parent($('toolbar'))
+ this.buttons = this.element.getFirst()
+ this.info = this.element.getLast()
+ this.buttons.getElements('li').each(function(el) {
+ el.addEvent('click', function(e) {
+ e.action = el.id
+ this.fireEvent('button_click', e)
+ }.bind(this))
+ }, this)
+ }
+});
+
+Deluge.Widgets.StatusBar = new Class({
+ Extends: Widgets.Base,
+
+ initialize: function() {
+ this.parent($('status'));
+ this.element.getElements('li').each(function(el) {
+ this[el.id] = el;
+ }, this);
+ },
+
+ update: function(stats) {
+ this.connections.set('text', stats.num_connections);
+ this.downspeed.set('text', stats.download_rate.toSpeed());
+ this.upspeed.set('text', stats.upload_rate.toSpeed());
+ this.dht.set('text', stats.dht_nodes);
+ }
+});
+
+Deluge.Widgets.TorrentGrid = new Class({
+ Extends: Widgets.DataGrid,
+
+ options: {
+ columns: [
+ {name: 'number',text: '#',type:'number',width: 20},
+ {name: 'name',text: 'Name',type:'icon',width: 350},
+ {name: 'size',text: 'Size',type:'bytes',width: 80},
+ {name: 'progress',text: 'Progress',type:'progress',width: 180},
+ {name: 'seeders',text: 'Seeders',type:'text',width: 80},
+ {name: 'peers',text: 'Peers',type:'text',width: 80},
+ {name: 'down',text: 'Down Speed',type:'speed',width: 100},
+ {name: 'up',text: 'Up Speed',type:'speed',width: 100},
+ {name: 'eta',text: 'ETA',type:'time',width: 80},
+ {name: 'ratio',text: 'Ratio',type:'number',width: 60},
+ {name: 'avail',text: 'Avail.',type:'number',width: 60}
+ ]
+ },
+
+ icons: {
+ 'Downloading': '/pixmaps/downloading16.png',
+ 'Seeding': '/pixmaps/seeding16.png',
+ 'Queued': '/pixmaps/queued16.png',
+ 'Paused': '/pixmaps/inactive16.png',
+ 'Error': '/pixmaps/alert16.png',
+ 'Checking': '/pixmaps/inactive16.png'
+ },
+
+ get_selected_torrents: function() {
+ var torrentIds = [];
+ this.get_selected().each(function(row) {
+ torrentIds.include(row.id);
+ });
+ return torrentIds;
+ },
+
+ set_torrent_filter: function(state) {
+ state = state.replace(' ', '');
+ this.filterer = function (r) {
+ if (r.torrent.state == state) { return true } else { return false };
+ };
+ this.render();
+ },
+
+ update_torrents: function(torrents) {
+ torrents.getKeys().each(function(torrentId) {
+ var torrent = torrents[torrentId]
+ var torrentIds = torrents.getKeys()
+ if (torrent.queue == -1) {var queue = ''}
+ else {var queue = torrent.queue + 1}
+ var icon = this.icons[torrent.state]
+ row = {
+ id: torrentId,
+ data: {
+ number: queue,
+ name: {text: torrent.name, icon: icon},
+ size: torrent.total_size,
+ progress: {percent: torrent.progress, text:torrent.state + ' ' + torrent.progress.toFixed(2) + '%'},
+ seeders: torrent.num_seeds + ' (' + torrent.total_seeds + ')',
+ peers: torrent.num_peers + ' (' + torrent.total_peers + ')',
+ down: torrent.download_payload_rate,
+ up: torrent.upload_payload_rate,
+ eta: torrent.eta,
+ ratio: torrent.ratio.toFixed(3),
+ avail: torrent.distributed_copies.toFixed(3)
+ },
+ torrent: torrent
+ }
+ if (this.has(row.id)) {
+ this.updateRow(row, true)
+ } else {
+ this.addRow(row, true)
+ }
+
+ this.rows.each(function(row) {
+ if (!torrentIds.contains(row.id)) {
+ row.element.destroy()
+ this.rows.erase(row.id)
+ }
+ }, this)
+ }, this)
+ this.render()
+ }
+})
+
+Deluge.Widgets.Labels = new Class({
+
+ Extends: Widgets.Base,
+
+ regex: /([\w]+)\s\((\d)\)/,
+
+ initialize: function() {
+ this.parent($('labels'))
+ this.bound = {
+ resized: this.resized.bindWithEvent(this),
+ clickedState: this.clickedState.bindWithEvent(this)
+ }
+
+ this.list = new Element('ul')
+ this.element.grab(this.list)
+ this.addStates()
+ this.state = 'All'
+ this.islabels = false;
+ this.addEvent('resize', this.resized)
+ },
+
+ addStates: function() {
+ this.list.grab(new Element('li').set('text', 'All').addClass('all').addClass('activestate'))
+ this.list.grab(new Element('li').set('text', 'Downloading').addClass('downloading'))
+ this.list.grab(new Element('li').set('text', 'Seeding').addClass('seeding'))
+ this.list.grab(new Element('li').set('text', 'Queued').addClass('queued'))
+ this.list.grab(new Element('li').set('text', 'Paused').addClass('paused'))
+ this.list.grab(new Element('li').set('text', 'Error').addClass('error'))
+ this.list.grab(new Element('li').set('text', 'Checking').addClass('checking'))
+ this.list.grab(new Element('hr'))
+ },
+
+ addLabel: function(name) {
+
+ },
+
+ clickedState: function(e) {
+ if (this.islabels) {
+ var old = this.list.getElement('.' + this.state.toLowerCase())
+ old.removeClass('activestate')
+ this.state = e.target.get('text').match(/^(\w+)/)[1]
+ e.target.addClass('activestate')
+ this.fireEvent('stateChanged', this.state)
+ } else {
+
+ }
+ },
+
+ update: function(filters) {
+ if (filters.state.length == 1)
+ this.updateNoLabels(filters);
+ else
+ this.updateLabels(filters)
+ },
+
+ updateNoLabels: function(filters) {
+ this.islabels = false;
+ },
+
+ updateLabels: function(filters) {
+ this.islabels = true;
+ $each(filters.state, function(state) {
+ var el = this.list.getElement('.' + state[0].toLowerCase())
+ if (!el) return
+
+ el.set('text', state[0] + ' (' + state[1] + ')')
+ el.removeEvent('click', this.bound.clickedState)
+ el.addEvent('click', this.bound.clickedState)
+ }, this)
+ },
+
+ resized: function(event) {
+ var height = this.element.getInnerSize().y;
+ this.list.getSizeModifiers();
+ height -= this.list.modifiers.y;
+ this.list.setStyle('height', height)
+ }
+});
+
+Deluge.Widgets.AddWindow = new Class({
+ Extends: Widgets.Window,
+ options: {
+ width: 400,
+ height: 200,
+ title: 'Add Torrent',
+ url: '/template/render/html/window_add_torrent.html'
+ },
+
+ initialize: function() {
+ this.parent()
+ this.addEvent('loaded', this.loaded.bindWithEvent(this))
+ },
+
+ loaded: function(event) {
+ this.formfile = this.content.getChildren()[0];
+ this.formurl = this.content.getChildren()[1];
+ this.formurl.addEvent('submit', function(e) {
+ e.stop();
+ Deluge.Client.add_torrent_url(this.formurl.url.value, '', {})
+ this.hide()
+ }.bindWithEvent(this))
+ }
+});
+
+Deluge.Widgets.PreferencesCategory = new Class({
+ Extends: Widgets.TabPage,
+});
+
+Deluge.Widgets.PluginPreferencesCategory = new Class({
+ Extends: Deluge.Widgets.PreferencesCategory
+});
+
+Deluge.Widgets.GenericPreferences = new Class({
+ Extends: Deluge.Widgets.PreferencesCategory,
+
+ initialize: function(name, options) {
+ this.parent(name, options)
+ this.core = true;
+ this.addEvent('loaded', function(e) {
+ this.form = this.element.getElement('form');
+ }.bindWithEvent(this));
+ },
+
+ update: function(config) {
+ this.fireEvent('beforeUpdate');
+ this.original = {};
+ this.changed = {};
+ this.inputs = this.form.getElements('input, select');
+ this.inputs.each(function(input) {
+ if (!input.name) return;
+ if (!$defined(config[input.name])) return;
+ if (input.tagName.toLowerCase() == 'select') {
+ var value = config[input.name].toString();
+ input.getElements('option').each(function(option) {
+ if (option.value == value) option.selected = true;
+ });
+ } else if (input.type == 'text') {
+ input.value = config[input.name];
+ } else if (input.type == 'checkbox') {
+ input.checked = config[input.name];
+ } else if (input.type == 'radio') {
+ var value = config[input.name].toString()
+ if (input.value == value) input.checked = true;
+ }
+ this.original[input.name] = input.value;
+ input.addEvent('change', function(el) {
+ if (this.original[input.name] == input.value) {
+ if (this.changed[input.name])
+ delete this.changed[input.name];
+ } else {
+ this.changed[input.name] = input.value;
+ }
+ }.bindWithEvent(this))
+ }, this);
+ this.fireEvent('update');
+ }
+});
+
+Deluge.Widgets.WebUIPreferences = new Class({
+ Extends: Deluge.Widgets.GenericPreferences,
+
+ options: {
+ url: '/template/render/html/preferences_webui.html'
+ },
+
+ initialize: function() {
+ this.parent('Web UI');
+ this.core = false;
+ this.addEvent('beforeUpdate', this.beforeUpdate.bindWithEvent(this));
+ this.addEvent('update', this.updated.bindWithEvent(this));
+ },
+
+ beforeUpdate: function(event) {
+ var templates = Deluge.Client.get_webui_templates({async: false});
+ templates.each(function(template) {
+ var option = new Element('option');
+ option.set('text', template);
+ this.form.template.grab(option);
+ }, this);
+ },
+
+ updated: function(event) {
+ if (this.form.template.value != 'ajax')
+ this.form.theme.disabled = true;
+ else
+ this.form.theme.disabled = false;
+
+ this.form.template.addEvent('change', function(e) {
+ if (this.form.template.value != 'ajax') {
+ this.form.theme.disabled = true;
+ this.form.theme.addClass('disabled')
+ this.form.getElementById('lbl_theme').addClass('disabled')
+ } else {
+ this.form.theme.disabled = false;
+ this.form.getElementById('lbl_theme').removeClass('disabled')
+ this.form.theme.removeClass('disabled')
+ }
+ }.bindWithEvent(this));
+ },
+
+ apply: function() {
+ Deluge.UI.setTheme(this.form.theme.value);
+ Deluge.Client.set_webui_config(this.changed, {
+ onSuccess: function(e) {
+ if (this.changed['template']) location.reload(true);
+ }.bindWithEvent(this)
+ });
+ }
+});
+
+Deluge.Widgets.PreferencesWindow = new Class({
+ Extends: Widgets.Window,
+ options: {
+ width: 500,
+ height: 430,
+ title: 'Preferences',
+ url: '/template/render/html/window_preferences.html'
+ },
+
+ initialize: function() {
+ this.parent();
+ this.categories = [];
+ this.currentPage = -1;
+ this.addEvent('loaded', this.loaded.bindWithEvent(this));
+ this.addEvent('show', this.shown.bindWithEvent(this));
+ },
+
+ loaded: function(event) {
+ this.catlist = this.content.getElement('.categories ul');
+ this.pages = this.content.getElement('.pref_pages');
+ this.title = this.pages.getElement('h3');
+
+ this.reset = this.content.getElement('.buttons .reset');
+ this.apply = this.content.getElement('.buttons .apply');
+ this.apply.addEvent('click', this.applied.bindWithEvent(this));
+
+ this.webui = new Deluge.Widgets.WebUIPreferences();
+
+ this.download = new Deluge.Widgets.GenericPreferences('Download', {
+ url: '/template/render/html/preferences_download.html'
+ });
+ this.network = new Deluge.Widgets.GenericPreferences('Network', {
+ url: '/template/render/html/preferences_network.html'
+ });
+ this.bandwidth = new Deluge.Widgets.GenericPreferences('Bandwidth', {
+ url: '/template/render/html/preferences_bandwidth.html'
+ });
+ this.daemon = new Deluge.Widgets.GenericPreferences('Daemon', {
+ url: '/template/render/html/preferences_daemon.html'
+ });
+ this.queue = new Deluge.Widgets.GenericPreferences('Queue', {
+ url: '/template/render/html/preferences_queue.html'
+ });
+
+ this.addCategory(this.webui);
+ this.addCategory(this.download);
+ this.addCategory(this.network);
+ this.addCategory(this.bandwidth);
+ this.addCategory(this.daemon);
+ this.addCategory(this.queue);
+ },
+
+ addCategory: function(category) {
+ this.categories.include(category);
+ var categoryIndex = this.categories.indexOf(category);
+
+ var tab = new Element('li');
+ tab.set('text', category.name);
+ tab.addEvent('click', function(e) {
+ this.select(categoryIndex);
+ }.bindWithEvent(this));
+ category.tab = tab;
+
+ this.catlist.grab(tab);
+ this.pages.grab(category.addClass('deluge-prefs-page'));
+
+
+ if (this.currentPage < 0) {
+ this.currentPage = categoryIndex;
+ this.select(categoryIndex);
+ };
+ },
+
+ select: function(id) {
+ this.categories[this.currentPage].removeClass('deluge-prefs-page-active');
+ this.categories[this.currentPage].tab.removeClass('deluge-prefs-active');
+ this.categories[id].addClass('deluge-prefs-page-active');
+ this.categories[id].tab.addClass('deluge-prefs-active');
+ this.title.set('text', this.categories[id].name);
+ this.currentPage = id;
+ this.fireEvent('pageChanged');
+ },
+
+ applied: function(event) {
+ this.webui.apply();
+ },
+
+ shown: function(event) {
+ // we want this to be blocking
+ var config = Deluge.Client.get_config({async: false});
+ this.categories.each(function(category) {
+ if (category.update && category.core) category.update(config);
+ });
+
+ var webconfig = Deluge.Client.get_webui_config({async: false});
+ this.webui.update(webconfig);
+ }
+});
diff --git a/deluge/ui/webui/templates/ajax/static/js/mootools-1.2-core-nc.js b/deluge/ui/webui/templates/ajax/static/js/mootools-1.2-core-nc.js
new file mode 100644
index 000000000..e88d4ac8c
--- /dev/null
+++ b/deluge/ui/webui/templates/ajax/static/js/mootools-1.2-core-nc.js
@@ -0,0 +1,3816 @@
+/*
+Script: Core.js
+ MooTools - My Object Oriented JavaScript Tools.
+
+License:
+ MIT-style license.
+
+Copyright:
+ Copyright (c) 2006-2007 [Valerio Proietti](http://mad4milk.net/).
+
+Code & Documentation:
+ [The MooTools production team](http://mootools.net/developers/).
+
+Inspiration:
+ - Class implementation inspired by [Base.js](http://dean.edwards.name/weblog/2006/03/base/) Copyright (c) 2006 Dean Edwards, [GNU Lesser General Public License](http://opensource.org/licenses/lgpl-license.php)
+ - Some functionality inspired by [Prototype.js](http://prototypejs.org) Copyright (c) 2005-2007 Sam Stephenson, [MIT License](http://opensource.org/licenses/mit-license.php)
+*/
+
+var MooTools = {
+ 'version': '1.2.0',
+ 'build': ''
+};
+
+var Native = function(options){
+ options = options || {};
+
+ var afterImplement = options.afterImplement || function(){};
+ var generics = options.generics;
+ generics = (generics !== false);
+ var legacy = options.legacy;
+ var initialize = options.initialize;
+ var protect = options.protect;
+ var name = options.name;
+
+ var object = initialize || legacy;
+
+ object.constructor = Native;
+ object.$family = {name: 'native'};
+ if (legacy && initialize) object.prototype = legacy.prototype;
+ object.prototype.constructor = object;
+
+ if (name){
+ var family = name.toLowerCase();
+ object.prototype.$family = {name: family};
+ Native.typize(object, family);
+ }
+
+ var add = function(obj, name, method, force){
+ if (!protect || force || !obj.prototype[name]) obj.prototype[name] = method;
+ if (generics) Native.genericize(obj, name, protect);
+ afterImplement.call(obj, name, method);
+ return obj;
+ };
+
+ object.implement = function(a1, a2, a3){
+ if (typeof a1 == 'string') return add(this, a1, a2, a3);
+ for (var p in a1) add(this, p, a1[p], a2);
+ return this;
+ };
+
+ object.alias = function(a1, a2, a3){
+ if (typeof a1 == 'string'){
+ a1 = this.prototype[a1];
+ if (a1) add(this, a2, a1, a3);
+ } else {
+ for (var a in a1) this.alias(a, a1[a], a2);
+ }
+ return this;
+ };
+
+ return object;
+};
+
+Native.implement = function(objects, properties){
+ for (var i = 0, l = objects.length; i < l; i++) objects[i].implement(properties);
+};
+
+Native.genericize = function(object, property, check){
+ if ((!check || !object[property]) && typeof object.prototype[property] == 'function') object[property] = function(){
+ var args = Array.prototype.slice.call(arguments);
+ return object.prototype[property].apply(args.shift(), args);
+ };
+};
+
+Native.typize = function(object, family){
+ if (!object.type) object.type = function(item){
+ return ($type(item) === family);
+ };
+};
+
+Native.alias = function(objects, a1, a2, a3){
+ for (var i = 0, j = objects.length; i < j; i++) objects[i].alias(a1, a2, a3);
+};
+
+(function(objects){
+ for (var name in objects) Native.typize(objects[name], name);
+})({'boolean': Boolean, 'native': Native, 'object': Object});
+
+(function(objects){
+ for (var name in objects) new Native({name: name, initialize: objects[name], protect: true});
+})({'String': String, 'Function': Function, 'Number': Number, 'Array': Array, 'RegExp': RegExp, 'Date': Date});
+
+(function(object, methods){
+ for (var i = methods.length; i--; i) Native.genericize(object, methods[i], true);
+ return arguments.callee;
+})
+(Array, ['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift', 'concat', 'join', 'slice', 'toString', 'valueOf', 'indexOf', 'lastIndexOf'])
+(String, ['charAt', 'charCodeAt', 'concat', 'indexOf', 'lastIndexOf', 'match', 'replace', 'search', 'slice', 'split', 'substr', 'substring', 'toLowerCase', 'toUpperCase', 'valueOf']);
+
+function $chk(obj){
+ return !!(obj || obj === 0);
+};
+
+function $clear(timer){
+ clearTimeout(timer);
+ clearInterval(timer);
+ return null;
+};
+
+function $defined(obj){
+ return (obj != undefined);
+};
+
+function $empty(){};
+
+function $arguments(i){
+ return function(){
+ return arguments[i];
+ };
+};
+
+function $lambda(value){
+ return (typeof value == 'function') ? value : function(){
+ return value;
+ };
+};
+
+function $extend(original, extended){
+ for (var key in (extended || {})) original[key] = extended[key];
+ return original;
+};
+
+function $unlink(object){
+ var unlinked;
+
+ switch ($type(object)){
+ case 'object':
+ unlinked = {};
+ for (var p in object) unlinked[p] = $unlink(object[p]);
+ break;
+ case 'hash':
+ unlinked = $unlink(object.getClean());
+ break;
+ case 'array':
+ unlinked = [];
+ for (var i = 0, l = object.length; i < l; i++) unlinked[i] = $unlink(object[i]);
+ break;
+ default: return object;
+ }
+
+ return unlinked;
+};
+
+function $merge(){
+ var mix = {};
+ for (var i = 0, l = arguments.length; i < l; i++){
+ var object = arguments[i];
+ if ($type(object) != 'object') continue;
+ for (var key in object){
+ var op = object[key], mp = mix[key];
+ mix[key] = (mp && $type(op) == 'object' && $type(mp) == 'object') ? $merge(mp, op) : $unlink(op);
+ }
+ }
+ return mix;
+};
+
+function $pick(){
+ for (var i = 0, l = arguments.length; i < l; i++){
+ if (arguments[i] != undefined) return arguments[i];
+ }
+ return null;
+};
+
+function $random(min, max){
+ return Math.floor(Math.random() * (max - min + 1) + min);
+};
+
+function $splat(obj){
+ var type = $type(obj);
+ return (type) ? ((type != 'array' && type != 'arguments') ? [obj] : obj) : [];
+};
+
+var $time = Date.now || function(){
+ return new Date().getTime();
+};
+
+function $try(){
+ for (var i = 0, l = arguments.length; i < l; i++){
+ try {
+ return arguments[i]();
+ } catch(e){}
+ }
+ return null;
+};
+
+function $type(obj){
+ if (obj == undefined) return false;
+ if (obj.$family) return (obj.$family.name == 'number' && !isFinite(obj)) ? false : obj.$family.name;
+ if (obj.nodeName){
+ switch (obj.nodeType){
+ case 1: return 'element';
+ case 3: return (/\S/).test(obj.nodeValue) ? 'textnode' : 'whitespace';
+ }
+ } else if (typeof obj.length == 'number'){
+ if (obj.callee) return 'arguments';
+ else if (obj.item) return 'collection';
+ }
+ return typeof obj;
+};
+
+var Hash = new Native({
+
+ name: 'Hash',
+
+ initialize: function(object){
+ if ($type(object) == 'hash') object = $unlink(object.getClean());
+ for (var key in object) this[key] = object[key];
+ return this;
+ }
+
+});
+
+Hash.implement({
+
+ getLength: function(){
+ var length = 0;
+ for (var key in this){
+ if (this.hasOwnProperty(key)) length++;
+ }
+ return length;
+ },
+
+ forEach: function(fn, bind){
+ for (var key in this){
+ if (this.hasOwnProperty(key)) fn.call(bind, this[key], key, this);
+ }
+ },
+
+ getClean: function(){
+ var clean = {};
+ for (var key in this){
+ if (this.hasOwnProperty(key)) clean[key] = this[key];
+ }
+ return clean;
+ }
+
+});
+
+Hash.alias('forEach', 'each');
+
+function $H(object){
+ return new Hash(object);
+};
+
+Array.implement({
+
+ forEach: function(fn, bind){
+ for (var i = 0, l = this.length; i < l; i++) fn.call(bind, this[i], i, this);
+ }
+
+});
+
+Array.alias('forEach', 'each');
+
+function $A(iterable){
+ if (iterable.item){
+ var array = [];
+ for (var i = 0, l = iterable.length; i < l; i++) array[i] = iterable[i];
+ return array;
+ }
+ return Array.prototype.slice.call(iterable);
+};
+
+function $each(iterable, fn, bind){
+ var type = $type(iterable);
+ ((type == 'arguments' || type == 'collection' || type == 'array') ? Array : Hash).each(iterable, fn, bind);
+};
+
+
+/*
+Script: Browser.js
+ The Browser Core. Contains Browser initialization, Window and Document, and the Browser Hash.
+
+License:
+ MIT-style license.
+*/
+
+var Browser = new Hash({
+ Engine: {name: 'unknown', version: ''},
+ Platform: {name: (navigator.platform.match(/mac|win|linux/i) || ['other'])[0].toLowerCase()},
+ Features: {xpath: !!(document.evaluate), air: !!(window.runtime)},
+ Plugins: {}
+});
+
+if (window.opera) Browser.Engine = {name: 'presto', version: (document.getElementsByClassName) ? 950 : 925};
+else if (window.ActiveXObject) Browser.Engine = {name: 'trident', version: (window.XMLHttpRequest) ? 5 : 4};
+else if (!navigator.taintEnabled) Browser.Engine = {name: 'webkit', version: (Browser.Features.xpath) ? 420 : 419};
+else if (document.getBoxObjectFor != null) Browser.Engine = {name: 'gecko', version: (document.getElementsByClassName) ? 19 : 18};
+Browser.Engine[Browser.Engine.name] = Browser.Engine[Browser.Engine.name + Browser.Engine.version] = true;
+
+if (window.orientation != undefined) Browser.Platform.name = 'ipod';
+
+Browser.Platform[Browser.Platform.name] = true;
+
+Browser.Request = function(){
+ return $try(function(){
+ return new XMLHttpRequest();
+ }, function(){
+ return new ActiveXObject('MSXML2.XMLHTTP');
+ });
+};
+
+Browser.Features.xhr = !!(Browser.Request());
+
+Browser.Plugins.Flash = (function(){
+ var version = ($try(function(){
+ return navigator.plugins['Shockwave Flash'].description;
+ }, function(){
+ return new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version');
+ }) || '0 r0').match(/\d+/g);
+ return {version: parseInt(version[0] || 0 + '.' + version[1] || 0), build: parseInt(version[2] || 0)};
+})();
+
+function $exec(text){
+ if (!text) return text;
+ if (window.execScript){
+ window.execScript(text);
+ } else {
+ var script = document.createElement('script');
+ script.setAttribute('type', 'text/javascript');
+ script.text = text;
+ document.head.appendChild(script);
+ document.head.removeChild(script);
+ }
+ return text;
+};
+
+Native.UID = 1;
+
+var $uid = (Browser.Engine.trident) ? function(item){
+ return (item.uid || (item.uid = [Native.UID++]))[0];
+} : function(item){
+ return item.uid || (item.uid = Native.UID++);
+};
+
+var Window = new Native({
+
+ name: 'Window',
+
+ legacy: (Browser.Engine.trident) ? null: window.Window,
+
+ initialize: function(win){
+ $uid(win);
+ if (!win.Element){
+ win.Element = $empty;
+ if (Browser.Engine.webkit) win.document.createElement("iframe"); //fixes safari 2
+ win.Element.prototype = (Browser.Engine.webkit) ? window["[[DOMElement.prototype]]"] : {};
+ }
+ return $extend(win, Window.Prototype);
+ },
+
+ afterImplement: function(property, value){
+ window[property] = Window.Prototype[property] = value;
+ }
+
+});
+
+Window.Prototype = {$family: {name: 'window'}};
+
+new Window(window);
+
+var Document = new Native({
+
+ name: 'Document',
+
+ legacy: (Browser.Engine.trident) ? null: window.Document,
+
+ initialize: function(doc){
+ $uid(doc);
+ doc.head = doc.getElementsByTagName('head')[0];
+ doc.html = doc.getElementsByTagName('html')[0];
+ doc.window = doc.defaultView || doc.parentWindow;
+ if (Browser.Engine.trident4) $try(function(){
+ doc.execCommand("BackgroundImageCache", false, true);
+ });
+ return $extend(doc, Document.Prototype);
+ },
+
+ afterImplement: function(property, value){
+ document[property] = Document.Prototype[property] = value;
+ }
+
+});
+
+Document.Prototype = {$family: {name: 'document'}};
+
+new Document(document);
+
+/*
+Script: Array.js
+ Contains Array Prototypes like copy, each, contains, and remove.
+
+License:
+ MIT-style license.
+*/
+
+Array.implement({
+
+ every: function(fn, bind){
+ for (var i = 0, l = this.length; i < l; i++){
+ if (!fn.call(bind, this[i], i, this)) return false;
+ }
+ return true;
+ },
+
+ filter: function(fn, bind){
+ var results = [];
+ for (var i = 0, l = this.length; i < l; i++){
+ if (fn.call(bind, this[i], i, this)) results.push(this[i]);
+ }
+ return results;
+ },
+
+ clean: function() {
+ return this.filter($defined);
+ },
+
+ indexOf: function(item, from){
+ var len = this.length;
+ for (var i = (from < 0) ? Math.max(0, len + from) : from || 0; i < len; i++){
+ if (this[i] === item) return i;
+ }
+ return -1;
+ },
+
+ map: function(fn, bind){
+ var results = [];
+ for (var i = 0, l = this.length; i < l; i++) results[i] = fn.call(bind, this[i], i, this);
+ return results;
+ },
+
+ some: function(fn, bind){
+ for (var i = 0, l = this.length; i < l; i++){
+ if (fn.call(bind, this[i], i, this)) return true;
+ }
+ return false;
+ },
+
+ associate: function(keys){
+ var obj = {}, length = Math.min(this.length, keys.length);
+ for (var i = 0; i < length; i++) obj[keys[i]] = this[i];
+ return obj;
+ },
+
+ link: function(object){
+ var result = {};
+ for (var i = 0, l = this.length; i < l; i++){
+ for (var key in object){
+ if (object[key](this[i])){
+ result[key] = this[i];
+ delete object[key];
+ break;
+ }
+ }
+ }
+ return result;
+ },
+
+ contains: function(item, from){
+ return this.indexOf(item, from) != -1;
+ },
+
+ extend: function(array){
+ for (var i = 0, j = array.length; i < j; i++) this.push(array[i]);
+ return this;
+ },
+
+ getLast: function(){
+ return (this.length) ? this[this.length - 1] : null;
+ },
+
+ getRandom: function(){
+ return (this.length) ? this[$random(0, this.length - 1)] : null;
+ },
+
+ include: function(item){
+ if (!this.contains(item)) this.push(item);
+ return this;
+ },
+
+ combine: function(array){
+ for (var i = 0, l = array.length; i < l; i++) this.include(array[i]);
+ return this;
+ },
+
+ erase: function(item){
+ for (var i = this.length; i--; i){
+ if (this[i] === item) this.splice(i, 1);
+ }
+ return this;
+ },
+
+ empty: function(){
+ this.length = 0;
+ return this;
+ },
+
+ flatten: function(){
+ var array = [];
+ for (var i = 0, l = this.length; i < l; i++){
+ var type = $type(this[i]);
+ if (!type) continue;
+ array = array.concat((type == 'array' || type == 'collection' || type == 'arguments') ? Array.flatten(this[i]) : this[i]);
+ }
+ return array;
+ },
+
+ hexToRgb: function(array){
+ if (this.length != 3) return null;
+ var rgb = this.map(function(value){
+ if (value.length == 1) value += value;
+ return value.toInt(16);
+ });
+ return (array) ? rgb : 'rgb(' + rgb + ')';
+ },
+
+ rgbToHex: function(array){
+ if (this.length < 3) return null;
+ if (this.length == 4 && this[3] == 0 && !array) return 'transparent';
+ var hex = [];
+ for (var i = 0; i < 3; i++){
+ var bit = (this[i] - 0).toString(16);
+ hex.push((bit.length == 1) ? '0' + bit : bit);
+ }
+ return (array) ? hex : '#' + hex.join('');
+ }
+
+});
+
+/*
+Script: Function.js
+ Contains Function Prototypes like create, bind, pass, and delay.
+
+License:
+ MIT-style license.
+*/
+
+Function.implement({
+
+ extend: function(properties){
+ for (var property in properties) this[property] = properties[property];
+ return this;
+ },
+
+ create: function(options){
+ var self = this;
+ options = options || {};
+ return function(event){
+ var args = options.arguments;
+ args = (args != undefined) ? $splat(args) : Array.slice(arguments, (options.event) ? 1 : 0);
+ if (options.event) args = [event || window.event].extend(args);
+ var returns = function(){
+ return self.apply(options.bind || null, args);
+ };
+ if (options.delay) return setTimeout(returns, options.delay);
+ if (options.periodical) return setInterval(returns, options.periodical);
+ if (options.attempt) return $try(returns);
+ return returns();
+ };
+ },
+
+ pass: function(args, bind){
+ return this.create({arguments: args, bind: bind});
+ },
+
+ attempt: function(args, bind){
+ return this.create({arguments: args, bind: bind, attempt: true})();
+ },
+
+ bind: function(bind, args){
+ return this.create({bind: bind, arguments: args});
+ },
+
+ bindWithEvent: function(bind, args){
+ return this.create({bind: bind, event: true, arguments: args});
+ },
+
+ delay: function(delay, bind, args){
+ return this.create({delay: delay, bind: bind, arguments: args})();
+ },
+
+ periodical: function(interval, bind, args){
+ return this.create({periodical: interval, bind: bind, arguments: args})();
+ },
+
+ run: function(args, bind){
+ return this.apply(bind, $splat(args));
+ }
+
+});
+
+/*
+Script: Number.js
+ Contains Number Prototypes like limit, round, times, and ceil.
+
+License:
+ MIT-style license.
+*/
+
+Number.implement({
+
+ limit: function(min, max){
+ return Math.min(max, Math.max(min, this));
+ },
+
+ round: function(precision){
+ precision = Math.pow(10, precision || 0);
+ return Math.round(this * precision) / precision;
+ },
+
+ times: function(fn, bind){
+ for (var i = 0; i < this; i++) fn.call(bind, i, this);
+ },
+
+ toFloat: function(){
+ return parseFloat(this);
+ },
+
+ toInt: function(base){
+ return parseInt(this, base || 10);
+ }
+
+});
+
+Number.alias('times', 'each');
+
+(function(math){
+ var methods = {};
+ math.each(function(name){
+ if (!Number[name]) methods[name] = function(){
+ return Math[name].apply(null, [this].concat($A(arguments)));
+ };
+ });
+ Number.implement(methods);
+})(['abs', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'exp', 'floor', 'log', 'max', 'min', 'pow', 'sin', 'sqrt', 'tan']);
+
+/*
+Script: String.js
+ Contains String Prototypes like camelCase, capitalize, test, and toInt.
+
+License:
+ MIT-style license.
+*/
+
+String.implement({
+
+ test: function(regex, params){
+ return ((typeof regex == 'string') ? new RegExp(regex, params) : regex).test(this);
+ },
+
+ contains: function(string, separator){
+ return (separator) ? (separator + this + separator).indexOf(separator + string + separator) > -1 : this.indexOf(string) > -1;
+ },
+
+ trim: function(){
+ return this.replace(/^\s+|\s+$/g, '');
+ },
+
+ clean: function(){
+ return this.replace(/\s+/g, ' ').trim();
+ },
+
+ camelCase: function(){
+ return this.replace(/-\D/g, function(match){
+ return match.charAt(1).toUpperCase();
+ });
+ },
+
+ hyphenate: function(){
+ return this.replace(/[A-Z]/g, function(match){
+ return ('-' + match.charAt(0).toLowerCase());
+ });
+ },
+
+ capitalize: function(){
+ return this.replace(/\b[a-z]/g, function(match){
+ return match.toUpperCase();
+ });
+ },
+
+ escapeRegExp: function(){
+ return this.replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1');
+ },
+
+ toInt: function(base){
+ return parseInt(this, base || 10);
+ },
+
+ toFloat: function(){
+ return parseFloat(this);
+ },
+
+ hexToRgb: function(array){
+ var hex = this.match(/^#?(\w{1,2})(\w{1,2})(\w{1,2})$/);
+ return (hex) ? hex.slice(1).hexToRgb(array) : null;
+ },
+
+ rgbToHex: function(array){
+ var rgb = this.match(/\d{1,3}/g);
+ return (rgb) ? rgb.rgbToHex(array) : null;
+ },
+
+ stripScripts: function(option){
+ var scripts = '';
+ var text = this.replace(/