1 /*
  2 Script: Deluge.Add.js
  3     Contains the Add Torrent window.
  4 
  5 Copyright:
  6 	(C) Damien Churchill 2009 <damoxc@gmail.com>
  7 	This program is free software; you can redistribute it and/or modify
  8 	it under the terms of the GNU General Public License as published by
  9 	the Free Software Foundation; either version 3, or (at your option)
 10 	any later version.
 11 
 12 	This program is distributed in the hope that it will be useful,
 13 	but WITHOUT ANY WARRANTY; without even the implied warranty of
 14 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15 	GNU General Public License for more details.
 16 
 17 	You should have received a copy of the GNU General Public License
 18 	along with this program.  If not, write to:
 19 		The Free Software Foundation, Inc.,
 20 		51 Franklin Street, Fifth Floor
 21 		Boston, MA  02110-1301, USA.
 22 
 23     In addition, as a special exception, the copyright holders give
 24     permission to link the code of portions of this program with the OpenSSL
 25     library.
 26     You must obey the GNU General Public License in all respects for all of
 27     the code used other than OpenSSL. If you modify file(s) with this
 28     exception, you may extend this exception to your version of the file(s),
 29     but you are not obligated to do so. If you do not wish to do so, delete
 30     this exception statement from your version. If you delete this exception
 31     statement from all source files in the program, then also delete it here.
 32 
 33 */
 34 
 35 Ext.namespace('Ext.deluge.add');
 36 Ext.deluge.add.OptionsPanel = Ext.extend(Ext.TabPanel, {
 37 	
 38 	torrents: {},
 39 
 40 	constructor: function(config) {
 41 		config = Ext.apply({
 42 			region: 'south',
 43 			margins: '5 5 5 5',
 44 			activeTab: 0,
 45 			height: 220
 46 		}, config);
 47 		Ext.deluge.add.OptionsPanel.superclass.constructor.call(this, config);
 48 	},
 49 	
 50 	initComponent: function() {
 51 		Ext.deluge.add.OptionsPanel.superclass.initComponent.call(this);
 52 		this.files = this.add(new Ext.tree.ColumnTree({
 53 			layout: 'fit',
 54 			title: _('Files'),
 55 			rootVisible: false,
 56 			autoScroll: true,
 57 			height: 170,
 58 			border: false,
 59 			animate: false,
 60 			
 61 			columns: [{
 62 				header: _('Filename'),
 63 				width: 275,
 64 				dataIndex: 'filename'
 65 			},{
 66 				header: _('Size'),
 67 				width: 80,
 68 				dataIndex: 'size'
 69 			}],
 70 			
 71 			root: new Ext.tree.AsyncTreeNode({
 72 				text: 'Files'
 73 			})
 74 		}));
 75 		new Ext.tree.TreeSorter(this.files, {
 76 			folderSort: true
 77 		});
 78 		
 79 		this.optionsManager = new Deluge.OptionsManager({
 80 			defaults: {
 81 				'add_paused': false,
 82 				'compact_allocation': false,
 83 				'download_location': '',
 84 				'max_connections_per_torrent': -1,
 85 				'max_download_speed_per_torrent': -1,
 86 				'max_upload_slots_per_torrent': -1,
 87 				'max_upload_speed_per_torrent': -1,
 88 				'prioritize_first_last_pieces': false,
 89 				'file_priorities': []
 90 			}
 91 		});
 92 		
 93 		this.form = this.add({
 94 			xtype: 'form',
 95 			labelWidth: 1,
 96 			title: _('Options'),
 97 			bodyStyle: 'padding: 5px;',
 98 			border: false,
 99 			height: 170
100 		});
101 		
102 		var fieldset = this.form.add({
103 			xtype: 'fieldset',
104 			title: _('Download Location'),
105 			border: false,
106 			autoHeight: true,
107 			defaultType: 'textfield',
108 			labelWidth: 1,
109 			fieldLabel: ''
110 		});
111 		this.optionsManager.bind('download_location', fieldset.add({
112 			fieldLabel: '',
113 			name: 'download_location',
114 			width: 400,
115 			labelSeparator: ''
116 		}));
117 		
118 		var panel = this.form.add({
119 			border: false,
120 			layout: 'column',
121 			defaultType: 'fieldset'
122 		});
123 		fieldset = panel.add({
124 			title: _('Allocation'),
125 			border: false,
126 			autoHeight: true,
127 			defaultType: 'radio',
128 			width: 100
129 		});
130 
131 		this.optionsManager.bind('compact_allocation', fieldset.add({
132 			xtype: 'radiogroup',
133 			columns: 1,
134 			vertical: true,
135 			labelSeparator: '',
136 			items: [{
137 				name: 'compact_allocation',
138 				value: false,
139 				inputValue: false,
140 				boxLabel: _('Full'),
141 				fieldLabel: '',
142 				labelSeparator: ''
143 			}, {
144 				name: 'compact_allocation',
145 				value: true,
146 				inputValue: true,
147 				boxLabel: _('Compact'),
148 				fieldLabel: '',
149 				labelSeparator: '',
150 			}]
151 		}));
152 		
153 		fieldset = panel.add({
154 			title: _('Bandwidth'),
155 			border: false,
156 			autoHeight: true,
157 			labelWidth: 100,
158 			width: 200,
159 			defaultType: 'uxspinner'
160 		});
161 		this.optionsManager.bind('max_download_speed_per_torrent', fieldset.add({
162 			fieldLabel: _('Max Down Speed'),
163 			/*labelStyle: 'margin-left: 10px',*/
164 			name: 'max_download_speed_per_torrent',
165 			width: 60
166 		}));
167 		this.optionsManager.bind('max_upload_speed_per_torrent', fieldset.add({
168 			fieldLabel: _('Max Up Speed'),
169 			/*labelStyle: 'margin-left: 10px',*/
170 			name: 'max_upload_speed_per_torrent',
171 			width: 60
172 		}));
173 		this.optionsManager.bind('max_connections_per_torrent', fieldset.add({
174 			fieldLabel: _('Max Connections'),
175 			/*labelStyle: 'margin-left: 10px',*/
176 			name: 'max_connections_per_torrent',
177 			width: 60
178 		}));
179 		this.optionsManager.bind('max_upload_slots_per_torrent', fieldset.add({
180 			fieldLabel: _('Max Upload Slots'),
181 			/*labelStyle: 'margin-left: 10px',*/
182 			name: 'max_upload_slots_per_torrent',
183 			width: 60
184 		}));
185 		
186 		fieldset = panel.add({
187 			title: _('General'),
188 			border: false,
189 			autoHeight: true,
190 			defaultType: 'checkbox'
191 		});
192 		this.optionsManager.bind('add_paused', fieldset.add({
193 			name: 'add_paused',
194 			boxLabel: _('Add In Paused State'),
195 			fieldLabel: '',
196 			labelSeparator: '',
197 		}));
198 		this.optionsManager.bind('prioritize_first_last_pieces', fieldset.add({
199 			name: 'prioritize_first_last_pieces',
200 			boxLabel: _('Prioritize First/Last Pieces'),
201 			fieldLabel: '',
202 			labelSeparator: '',
203 		}));
204 		
205 		this.form.on('render', this.onFormRender, this);
206 		this.form.disable();
207 	},
208 	
209 	onFormRender: function(form) {
210 		form.layout = new Ext.layout.FormLayout();
211 		form.layout.setContainer(form);
212 		form.doLayout();
213 		
214 		this.optionsManager.changeId(null);
215 	},
216 	
217 	addTorrent: function(torrent) {
218 		this.torrents[torrent['info_hash']] = torrent;
219 		var fileIndexes = {};
220 		this.walkFileTree(torrent['files_tree'], function(filename, type, entry, parent) {
221 			if (type != 'file') return;
222 			fileIndexes[entry[0]] = entry[2];
223 		}, this);
224 		
225 		var priorities = [];
226 		Ext.each(Ext.keys(fileIndexes), function(index) {
227 			priorities[index] = fileIndexes[index];
228 		});
229 		this.optionsManager.set(torrent['info_hash'], 'file_priorities', priorities);
230 	},
231 	
232 	clear: function() {
233 		this.clearFiles();
234 	},
235 	
236 	clearFiles: function() {
237 		var root = this.files.getRootNode();
238 		if (!root.hasChildNodes()) return;
239 		root.cascade(function(node) {
240 			if (!node.parentNode || !node.getOwnerTree()) return;
241 			node.remove();
242 		});
243 	},
244 	
245 	getDefaults: function() {
246 		var keys = ['add_paused','compact_allocation','download_location',
247 		'max_connections_per_torrent','max_download_speed_per_torrent',
248 		'max_upload_slots_per_torrent','max_upload_speed_per_torrent',
249 		'prioritize_first_last_pieces'];
250 		
251 		Deluge.Client.core.get_config_values(keys, {
252 			success: function(config) {
253 				config['file_priorities'] = [];
254 				this.optionsManager.defaults = config;
255 			},
256 			scope: this
257 		});
258 	},
259 	
260 	getFilename: function(torrentId) {
261 		return this.torrents[torrentId]['filename'];
262 	},
263 	
264 	getOptions: function(torrentId) {
265 		var options = this.optionsManager.get(torrentId);
266 		Ext.each(options['file_priorities'], function(priority, index) {
267 			options['file_priorities'][index] = (priority) ? 1 : 0;
268 		});
269 		return options;
270 	},
271 	
272 	setTorrent: function(torrentId) {
273 		this.torrentId = torrentId;
274 		this.optionsManager.changeId(torrentId);
275 		
276 		this.clearFiles();
277 		var root = this.files.getRootNode();
278 		var priorities = this.optionsManager.get(this.torrentId, 'file_priorities');
279 		
280 		this.walkFileTree(this.torrents[torrentId]['files_tree'], function(filename, type, entry, parent) {
281 			if (type == 'dir') {
282 				var folder = new Ext.tree.TreeNode({
283 					text: filename,
284 					checked: true
285 				});
286 				folder.on('checkchange', this.onFolderCheck, this);
287 				parent.appendChild(folder);
288 				return folder;
289 			} else {
290 				var node = new Ext.tree.TreeNode({
291 					filename: filename,
292 					fileindex: entry[0],
293 					text: filename, // this needs to be here for sorting reasons
294 					size: fsize(entry[1]),
295 					leaf: true,
296 					checked: priorities[entry[0]],
297 					iconCls: 'x-deluge-file',
298 					uiProvider: Ext.tree.ColumnNodeUI
299 				});
300 				node.on('checkchange', this.onNodeCheck, this);
301 				parent.appendChild(node);
302 			}
303 		}, this, root);
304 		root.firstChild.expand();
305 	},
306 	
307 	walkFileTree: function(files, callback, scope, parent) {
308 		for (var filename in files) {
309 			var entry = files[filename];
310 			var type = (Ext.type(entry) == 'object') ? 'dir' : 'file';
311 			
312 			if (scope) {
313 				var ret = callback.apply(scope, [filename, type, entry, parent]);
314 			} else {
315 				var ret = callback(filename, type, entry, parent);
316 			}
317 			
318 			if (type == 'dir') this.walkFileTree(entry, callback, scope, ret);
319 		}
320 	},
321 	
322 	onFolderCheck: function(node, checked) {
323 		var priorities = this.optionsManager.get(this.torrentId, 'file_priorities');
324 		node.cascade(function(child) {
325 			if (!child.ui.checkbox) {
326 				child.attributes.checked = checked;
327 			} else {
328 				child.ui.checkbox.checked = checked;
329 			}
330 			priorities[child.attributes.fileindex] = checked;
331 		}, this);
332 		this.optionsManager.update(this.torrentId, 'file_priorities', priorities);
333 	},
334 	
335 	onNodeCheck: function(node, checked) {
336 		var priorities = this.optionsManager.get(this.torrentId, 'file_priorities');
337 		priorities[node.attributes.fileindex] = checked;
338 		this.optionsManager.update(this.torrentId, 'file_priorities', priorities);
339 	}
340 });
341 
342 Ext.deluge.add.Window = Ext.extend(Ext.Window, {
343 	initComponent: function() {
344 		Ext.deluge.add.Window.superclass.initComponent.call(this);
345 		this.addEvents(
346 			'beforeadd',
347 			'add'
348 		);
349 	},
350 	
351 	createTorrentId: function() {
352 		return new Date().getTime();
353 	}
354 });
355 
356 Ext.deluge.add.AddWindow = Ext.extend(Ext.deluge.add.Window, {
357 	
358 	constructor: function(config) {
359 		config = Ext.apply({
360 			title: _('Add Torrents'),
361 			layout: 'border',
362 			width: 470,
363 			height: 450,
364 			bodyStyle: 'padding: 10px 5px;',
365 			buttonAlign: 'right',
366 			closeAction: 'hide',
367 			closable: true,
368 			plain: true,
369 			iconCls: 'x-deluge-add-window-icon'
370 		}, config);
371 		Ext.deluge.add.AddWindow.superclass.constructor.call(this, config);
372 	},
373 	
374 	initComponent: function() {
375 		Ext.deluge.add.AddWindow.superclass.initComponent.call(this);
376 		
377 		this.addButton(_('Cancel'), this.onCancel, this);
378 		this.addButton(_('Add'), this.onAdd, this);
379 		
380 		function torrentRenderer(value, p, r) {
381 			if (r.data['info_hash']) {
382 				return String.format('<div class="x-deluge-add-torrent-name">{0}</div>', value);
383 			} else {
384 				return String.format('<div class="x-deluge-add-torrent-name-loading">{0}</div>', value);
385 			}
386 		}
387 		
388 		this.grid = this.add({
389 			xtype: 'grid',
390 			region: 'center',
391 			store: new Ext.data.SimpleStore({
392 				fields: [
393 					{name: 'info_hash', mapping: 1},
394 					{name: 'text', mapping: 2}
395 				],
396 				id: 0
397 			}),
398 			columns: [{
399 					id: 'torrent',
400 					width: 150,
401 					sortable: true,
402 					renderer: torrentRenderer,
403 					dataIndex: 'text'
404 			}],	
405 			stripeRows: true,
406 			selModel: new Ext.grid.RowSelectionModel({
407 				singleSelect: true,
408 				listeners: {
409 					'rowselect': {
410 						fn: this.onSelect,
411 						scope: this
412 					}
413 				}
414 			}),
415 			hideHeaders: true,
416 			autoExpandColumn: 'torrent',
417 			deferredRender: false,
418 			autoScroll: true,
419 			margins: '5 5 5 5',
420 			bbar: new Ext.Toolbar({
421 				items: [{
422 					id: 'file',
423 					cls: 'x-btn-text-icon',
424 					iconCls: 'x-deluge-add-file',
425 					text: _('File'),
426 					handler: this.onFile,
427 					scope: this
428 				}, {
429 					id: 'url',
430 					cls: 'x-btn-text-icon',
431 					text: _('Url'),
432 					icon: '/icons/add_url.png',
433 					handler: this.onUrl,
434 					scope: this
435 				}, {
436 					id: 'infohash',
437 					cls: 'x-btn-text-icon',
438 					text: _('Infohash'),
439 					icon: '/icons/add_magnet.png',
440 					disabled: true
441 				}, '->', {
442 					id: 'remove',
443 					cls: 'x-btn-text-icon',
444 					text: _('Remove'),
445 					icon: '/icons/remove.png',
446 					handler: this.onRemove,
447 					scope: this
448 				}]
449 			})
450 		});
451 		
452 		this.optionsPanel = this.add(new Ext.deluge.add.OptionsPanel());
453 		this.on('show', this.onShow, this);
454 	},
455 	
456 	clear: function() {
457 		this.torrents = {};
458 		this.grid.getStore().removeAll();
459 	},
460 	
461 	onAdd: function() {
462 		var torrents = [];
463 		this.grid.getStore().each(function(r) {
464 			var id = r.get('info_hash');
465 			torrents.push({
466 				path: this.optionsPanel.getFilename(id),
467 				options: this.optionsPanel.getOptions(id)
468 			});
469 		}, this);
470 
471 		Deluge.Client.web.add_torrents(torrents, {
472 			success: function(result) {
473 			}
474 		})
475 		this.clear();
476 		this.hide();
477 	},
478 	
479 	onCancel: function() {
480 		this.clear();
481 		this.hide();
482 	},
483 	
484 	onFile: function() {
485 		this.file.show();
486 	},
487 	
488 	onRemove: function() {
489 		var selection = this.grid.getSelectionModel();
490 		if (!selection.hasSelection()) return;
491 		var torrent = selection.getSelected();
492 		
493 		delete this.torrents[torrent.id];
494 		this.grid.getStore().remove(torrent);
495 		this.options.clear();
496 	},
497 	
498 	onSelect: function(selModel, rowIndex, record) {
499 		this.optionsPanel.setTorrent(record.get('info_hash'));
500 	},
501 	
502 	onShow: function() {
503 		if (!this.url) {
504 			this.url = new Ext.deluge.add.UrlWindow();
505 			this.url.on('beforeadd', this.onTorrentBeforeAdd, this);
506 			this.url.on('add', this.onTorrentAdd, this);
507 		}
508 		
509 		if (!this.file) {
510 			this.file = new Ext.deluge.add.FileWindow();
511 			this.file.on('beforeadd', this.onTorrentBeforeAdd, this);
512 			this.file.on('add', this.onTorrentAdd, this);
513 		}
514 		
515 		this.optionsPanel.getDefaults();
516 	},
517 	
518 	onTorrentBeforeAdd: function(torrentId, text) {
519 		var store = this.grid.getStore();
520 		store.loadData([[torrentId, null, text]], true);
521 	},
522 	
523 	onTorrentAdd: function(torrentId, info) {
524 		if (!info) {
525 			Ext.MessageBox.show({
526 				title: _('Error'),
527 				msg: _('Not a valid torrent'),
528 				buttons: Ext.MessageBox.OK,
529 				modal: false,
530 				icon: Ext.MessageBox.ERROR,
531 				iconCls: 'x-deluge-icon-error'
532 			});
533 			return;
534 		}
535 		
536 		var r = this.grid.getStore().getById(torrentId);
537 		r.set('info_hash', info['info_hash']);
538 		r.set('text', info['name']);
539 		this.grid.getStore().commitChanges();
540 		this.optionsPanel.addTorrent(info);
541 	},
542 	
543 	onUrl: function(button, event) {
544 		this.url.show();
545 	}
546 });
547 Deluge.Add = new Ext.deluge.add.AddWindow();
548