You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

751 lines
26 KiB

/**
Model for the whole Queue with all it's items
**/
function QueueListModel(parent) {
// Internal var's
var self = this;
self.parent = parent;
self.dragging = false;
self.rawCatList = [];
self.rawScriptList = [];
// Because SABNZB returns the name
// But when you want to set Priority you need the number..
self.priorityName = [];
self.priorityName["Force"] = 2;
self.priorityName["High"] = 1;
self.priorityName["Normal"] = 0;
self.priorityName["Low"] = -1;
self.priorityName["Stop"] = -4;
self.priorityOptions = ko.observableArray([
{ value: 2, name: glitterTranslate.priority["Force"] },
{ value: 1, name: glitterTranslate.priority["High"] },
{ value: 0, name: glitterTranslate.priority["Normal"] },
{ value: -1, name: glitterTranslate.priority["Low"] },
{ value: -4, name: glitterTranslate.priority["Stop"] }
]);
self.processingOptions = ko.observableArray([
{ value: 0, name: glitterTranslate.pp["Download"] },
{ value: 1, name: glitterTranslate.pp["+Repair"] },
{ value: 2, name: glitterTranslate.pp["+Unpack"] },
{ value: 3, name: glitterTranslate.pp["+Delete"] }
]);
// External var's
self.queueItems = ko.observableArray([]);
self.totalItems = ko.observable(0);
self.isMultiEditing = ko.observable(false).extend({ persist: 'queueIsMultiEditing' });
self.isLoading = ko.observable(false).extend({ rateLimit: 100 });
self.multiEditItems = ko.observableArray([]);
self.categoriesList = ko.observableArray([]);
self.scriptsList = ko.observableArray([]);
self.searchTerm = ko.observable('').extend({ rateLimit: { timeout: 200, method: "notifyWhenChangesStop" } });
self.paginationLimit = ko.observable(20).extend({ persist: 'queuePaginationLimit' });
self.pagination = new paginationModel(self);
// Don't update while dragging
self.shouldUpdate = function() {
return !self.dragging;
}
self.dragStart = function() {
self.dragging = true;
}
self.dragStop = function(event) {
// Remove that extra label
$(event.target).parent().removeClass('table-active-sorting')
// Wait a little before refreshing again (prevents jumping)
setTimeout(function() {
self.dragging = false;
}, 500)
}
// Update slots from API data
self.updateFromData = function(data) {
// Get all ID's
var itemIds = $.map(self.queueItems(), function(i) {
return i.id;
});
// Did the category-list change?
// Otherwise KO will send updates to all <select> for every refresh()
if(self.rawCatList != data.categories.toString()) {
// Reformat categories
self.categoriesList($.map(data.categories, function(cat) {
// Default?
if(cat == '*') return { catValue: '*', catText: glitterTranslate.defaultText };
return { catValue: cat, catText: cat };
}))
// Update
self.rawCatList = data.categories.toString();
}
// Did the script-list change?
if(self.rawScriptList != data.scripts.toString()) {
// Reformat script-list
self.scriptsList($.map(data.scripts, function(script) {
// Default?
if(script == 'None') return glitterTranslate.noneText;
return script;
}))
// Update
self.rawScriptList = data.scripts.toString();
}
// Set limit
self.totalItems(data.noofslots);
// Container for new models
var newItems = [];
// Go over all items
$.each(data.slots, function() {
var item = this;
var existingItem = ko.utils.arrayFirst(self.queueItems(), function(i) {
return i.id == item.nzo_id;
});
if(existingItem) {
existingItem.updateFromData(item);
itemIds.splice(itemIds.indexOf(item.nzo_id), 1);
} else {
// Add new item
newItems.push(new QueueModel(self, item))
}
});
// Remove all items if there's any
if(itemIds.length == self.paginationLimit()) {
// Replace it, so only 1 Knockout DOM-update!
self.queueItems(newItems);
newItems = [];
} else {
// Remove items that don't exist anymore
$.each(itemIds, function() {
var id = this.toString();
self.queueItems.remove(ko.utils.arrayFirst(self.queueItems(), function(i) {
return i.id == id;
}));
});
}
// New items, then add!
if(newItems.length > 0) {
ko.utils.arrayPushAll(self.queueItems, newItems);
self.queueItems.valueHasMutated();
}
// Sort every time (takes just few msec)
self.queueItems.sort(function(a, b) {
return a.index() < b.index() ? -1 : 1;
});
};
// Move in sortable
self.move = function(event) {
var itemMoved = event.item;
// Up or down?
var corTerm = event.targetIndex > event.sourceIndex ? -1 : 1;
// See what the actual index is of the queue-object
// This way we can see how we move up and down independent of pagination
var itemReplaced = self.queueItems()[event.targetIndex+corTerm];
callAPI({
mode: "switch",
value: itemMoved.id,
value2: itemReplaced.index()
}).then(self.parent.refresh);
};
// Move button clicked
self.moveButton = function(event,ui) {
var itemMoved = event;
var targetIndex;
if($(ui.currentTarget).is(".buttonMoveToTop")){
//we want to move to the top
targetIndex = 0;
} else {
// we want to move to the bottom
targetIndex = self.totalItems() - 1;
}
callAPI({
mode: "switch",
value: itemMoved.id,
value2: targetIndex
}).then(self.parent.refresh);
}
// Save pagination state
self.paginationLimit.subscribe(function(newValue) {
// Save in config if global
if(self.parent.useGlobalOptions()) {
callAPI({
mode: "set_config",
section: "misc",
keyword: "queue_limit",
value: newValue
})
}
});
// Do we show search box. So it doesn't dissapear when nothing is found
self.hasQueueSearch = ko.pureComputed(function() {
return (self.pagination.hasPagination() || self.searchTerm() || (self.parent.hasQueue() && self.isMultiEditing()))
})
// Searching in queue (rate-limited in decleration)
self.searchTerm.subscribe(function() {
// Refresh now
self.parent.refresh();
// Go back to page 1
if(self.pagination.currentPage() != 1) {
self.pagination.moveToPage(1);
}
})
// Clear searchterm
self.clearSearchTerm = function(data, event) {
// Was it escape key or click?
if(event.type == 'mousedown' || (event.keyCode && event.keyCode == 27)) {
self.isLoading(true)
self.searchTerm('');
self.parent.refresh()
}
// Was it click and the field is empty? Then we focus on the field
if(event.type == 'mousedown' && self.searchTerm() == '') {
$(event.target).parents('.search-box').find('input[type="text"]').focus()
return;
}
// Need to return true to allow typing
return true;
}
/***
Multi-edit functions
***/
self.queueSorting = function(data, event) {
// What action?
var sort, dir;
switch($(event.currentTarget).data('action')) {
case 'sortAgeAsc':
sort = 'avg_age';
dir = 'desc';
break;
case 'sortAgeDesc':
sort = 'avg_age';
dir = 'asc';
break;
case 'sortNameAsc':
sort = 'name';
dir = 'asc';
break;
case 'sortNameDesc':
sort = 'name';
dir = 'desc';
break;
case 'sortSizeAsc':
sort = 'size';
dir = 'asc';
break;
case 'sortSizeDesc':
sort = 'size';
dir = 'desc';
break;
}
// Show notification
showNotification('.main-notification-box-sorting', 2000)
// Send call
callAPI({
mode: 'queue',
name: 'sort',
sort: sort,
dir: dir
}).then(parent.refresh)
}
// Show the input box
self.showMultiEdit = function() {
// Update value
self.isMultiEditing(!self.isMultiEditing())
// Form
var $form = $('form.multioperations-selector')
// Reset form and remove all checked ones
$form[0].reset();
self.multiEditItems.removeAll();
$('.delete input[name="multiedit"], #multiedit-checkall').prop({'checked': false, 'indeterminate': false})
// Is the multi-edit in view?
if(($form.offset().top + $form.outerHeight(true)) > ($(window).scrollTop()+$(window).height())) {
// Scroll to form
$('html, body').animate({
scrollTop: $form.offset().top + $form.outerHeight(true) - $(window).height() + 'px'
}, 'fast')
}
}
// Add to the list
self.addMultiEdit = function(item, event) {
// Is it a shift-click?
if(event.shiftKey) {
checkShiftRange('.queue-table input[name="multiedit"]');
}
// Add or remove from the list?
if(event.currentTarget.checked) {
// Add item
self.multiEditItems.push(item);
// Update them all
self.doMultiEditUpdate();
} else {
// Go over them all to know which one to remove
self.multiEditItems.remove(function(inList) { return inList.id == item.id; })
}
// Update check-all buton state
setCheckAllState('#multiedit-checkall', '.queue-table input[name="multiedit"]')
return true;
}
// Check all
self.checkAllJobs = function(item, event) {
// Get which ones we care about
var allChecks = $('.queue-table input[name="multiedit"]').filter(':not(:disabled):visible');
// We need to re-evaltuate the state of this check-all
// Otherwise the 'inderterminate' will be overwritten by the click event!
setCheckAllState('#multiedit-checkall', '.queue-table input[name="multiedit"]')
// Now we can check what happend
// For when some are checked, or all are checked (but not partly)
if(event.target.indeterminate || (event.target.checked && !event.target.indeterminate)) {
var allActive = allChecks.filter(":checked")
// First remove the from the list
if(allActive.length == self.multiEditItems().length) {
// Just remove all
self.multiEditItems.removeAll();
// Remove the check
allActive.prop('checked', false)
} else {
// Remove them seperate
allActive.each(function() {
// Go over them all to know which one to remove
var item = ko.dataFor(this)
self.multiEditItems.remove(function(inList) { return inList.id == item.id; })
// Remove the check of this one
this.checked = false;
})
}
} else {
// None are checked, so check and add them all
allChecks.prop('checked', true)
allChecks.each(function() { self.multiEditItems.push(ko.dataFor(this)) })
event.target.checked = true
// Now we fire the update
self.doMultiEditUpdate()
}
// Set state of all the check-all's
setCheckAllState('#multiedit-checkall', '.queue-table input[name="multiedit"]')
return true;
}
// Do the actual multi-update immediatly
self.doMultiEditUpdate = function() {
// Anything selected?
if(self.multiEditItems().length < 1) return;
// Retrieve the current settings
var newCat = $('.multioperations-selector select[name="Category"]').val()
var newScript = $('.multioperations-selector select[name="Post-processing"]').val()
var newPrior = $('.multioperations-selector select[name="Priority"]').val()
var newProc = $('.multioperations-selector select[name="Processing"]').val()
var newStatus = $('.multioperations-selector input[name="multiedit-status"]:checked').val()
// List all the ID's
var strIDs = '';
$.each(self.multiEditItems(), function(index) {
strIDs = strIDs + this.id + ',';
})
// What is changed?
if(newCat != '') {
callAPI({
mode: 'change_cat',
value: strIDs,
value2: newCat
})
}
if(newScript != '') {
callAPI({
mode: 'change_script',
value: strIDs,
value2: newScript
})
}
if(newPrior != '') {
callAPI({
mode: 'queue',
name: 'priority',
value: strIDs,
value2: newPrior
})
}
if(newProc != '') {
callAPI({
mode: 'change_opts',
value: strIDs,
value2: newProc
})
}
if(newStatus) {
callAPI({
mode: 'queue',
name: newStatus,
value: strIDs
})
}
// Wat a little and do the refresh
// Only if anything changed!
if(newStatus || newProc != '' || newPrior != '' || newScript != '' || newCat != '') {
setTimeout(parent.refresh, 100)
}
}
// Selete all selected
self.doMultiDelete = function() {
// Anything selected?
if(self.multiEditItems().length < 1) return;
// Need confirm
if(!self.parent.confirmDeleteQueue() || confirm(glitterTranslate.removeDown)) {
// List all the ID's
var strIDs = '';
$.each(self.multiEditItems(), function(index) {
strIDs = strIDs + this.id + ',';
})
// Show notification
showNotification('.main-notification-box-removing-multiple', 0, self.multiEditItems().length)
// Remove
callAPI({
mode: 'queue',
name: 'delete',
del_files: 1,
value: strIDs
}).then(function(response) {
if(response.status) {
// Make sure the queue doesnt flicker and then fade-out
self.isLoading(true)
self.parent.refresh()
// Empty it
self.multiEditItems.removeAll();
// Hide notification
hideNotification()
}
})
}
}
// On change of page we need to check all those that were in the list!
self.queueItems.subscribe(function() {
// We need to wait until the unit is actually finished rendering
setTimeout(function() {
$.each(self.multiEditItems(), function(index) {
$('#multiedit_' + this.id).prop('checked', true);
})
// Update check-all buton state
setCheckAllState('#multiedit-checkall', '.queue-table input[name="multiedit"]')
}, 100)
}, null, "arrayChange")
}
/**
Model for each Queue item
**/
function QueueModel(parent, data) {
var self = this;
self.parent = parent;
self.rawLabels = []
// Job info
self.id = data.nzo_id;
self.name = ko.observable($.trim(data.filename));
self.password = ko.observable(data.password);
self.index = ko.observable(data.index);
self.status = ko.observable(data.status);
self.labels = ko.observableArray(data.labels);
self.isGrabbing = ko.observable(data.status == 'Grabbing' || data.avg_age == '-')
self.isFetchingBlocks = data.status == 'Fetching' || data.priority == 'Repair' // No need to update
self.totalMB = ko.observable(parseFloat(data.mb));
self.remainingMB = ko.observable(parseFloat(data.mbleft))
self.missingMB = ko.observable(parseFloat(data.mbmissing))
self.percentage = ko.observable(parseInt(data.percentage))
self.avg_age = ko.observable(data.avg_age)
self.direct_unpack = ko.observable(data.direct_unpack)
self.category = ko.observable(data.cat);
self.priority = ko.observable(parent.priorityName[data.priority]);
self.script = ko.observable(data.script);
self.unpackopts = ko.observable(parseInt(data.unpackopts)) // UnpackOpts fails if not parseInt'd!
self.pausedStatus = ko.observable(data.status == 'Paused');
self.timeLeft = ko.observable(data.timeleft);
// Initially empty
self.nameForEdit = ko.observable();
self.editingName = ko.observable(false);
self.hasDropdown = ko.observable(false);
// Color of the progress bar
self.progressColor = ko.computed(function() {
// Checking
if(self.status() == 'Checking') {
return '#58A9FA'
}
// Check for missing data, the value is arbitrary! (2%)
if(self.missingMB()/self.totalMB() > 0.02) {
return '#F8A34E'
}
// Set to grey, only when not Force download
if((self.parent.parent.downloadsPaused() && self.priority() != 2) || self.pausedStatus()) {
return '#B7B7B7'
}
// Nothing
return '';
});
// MB's
self.progressText = ko.pureComputed(function() {
return (self.totalMB() - self.remainingMB()).toFixed(0) + " MB / " + (self.totalMB() * 1).toFixed(0) + " MB";
})
// Texts
self.name_title = ko.pureComputed(function() {
// When hovering over the job
if(self.direct_unpack()) {
return self.name() + ' - ' + glitterTranslate.status['DirectUnpack'] + ': ' + self.direct_unpack()
}
return self.name()
})
self.missingText = ko.pureComputed(function() {
// Check for missing data, can show 0 if article-size is smaller than 500K, but we accept that
if(self.missingMB()) {
return self.missingMB().toFixed(0) + ' MB ' + glitterTranslate.misingArt
}
})
self.statusText = ko.computed(function() {
// Checking
if(self.status() == 'Checking') {
return glitterTranslate.checking
}
// Grabbing
if(self.status() == 'Grabbing') {
return glitterTranslate.fetch
}
// Pausing status
if((self.parent.parent.downloadsPaused() && self.priority() != 2) || self.pausedStatus()) {
return glitterTranslate.paused;
}
// Just the time
return rewriteTime(self.timeLeft());
});
// Icon to better show force-priority
self.queueIcon = ko.computed(function() {
// Force comes first
if(self.priority() == 2) {
return 'glyphicon-forward'
}
if(self.pausedStatus()) {
return 'glyphicon-play'
}
return 'glyphicon-pause'
})
// Extra queue columns
self.showColumn = function(param) {
switch(param) {
case 'category':
// Exception for *
if(self.category() == "*")
return glitterTranslate.defaultText
return self.category();
case 'priority':
// Onload-exception
if(self.priority() == undefined) return;
return ko.utils.arrayFirst(self.parent.priorityOptions(), function(item) { return item.value == self.priority()}).name;
case 'processing':
// Onload-exception
if(self.unpackopts() == undefined) return;
return ko.utils.arrayFirst(self.parent.processingOptions(), function(item) { return item.value == self.unpackopts()}).name;
case 'scripts':
return self.script();
case 'age':
return self.avg_age();
}
return;
};
// Every update
self.updateFromData = function(data) {
// Update job info
self.name($.trim(data.filename));
self.password(data.password);
self.index(data.index);
self.status(data.status)
self.isGrabbing(data.status == 'Grabbing' || data.avg_age == '-')
self.totalMB(parseFloat(data.mb));
self.remainingMB(parseFloat(data.mbleft));
self.missingMB(parseFloat(data.mbmissing))
self.percentage(parseInt(data.percentage))
self.avg_age(data.avg_age)
self.direct_unpack(data.direct_unpack)
self.category(data.cat);
self.priority(parent.priorityName[data.priority]);
self.script(data.script);
self.unpackopts(parseInt(data.unpackopts)) // UnpackOpts fails if not parseInt'd!
self.pausedStatus(data.status == 'Paused');
self.timeLeft(data.timeleft);
// Did the label-list change?
// Otherwise KO will send updates to all texts during refresh()
if(self.rawLabels != data.labels.toString()) {
// Update
self.labels(data.labels);
self.rawLabels = data.labels.toString();
}
};
// Pause individual download
self.pauseToggle = function() {
callAPI({
mode: 'queue',
name: (self.pausedStatus() ? 'resume' : 'pause'),
value: self.id
}).then(self.parent.parent.refresh);
};
// Edit name
self.editName = function(data, event) {
// Not when still grabbing
if(self.isGrabbing()) return false;
// Change status and fill
self.editingName(true)
self.nameForEdit(self.name())
// Select
$(event.target).parents('.name').find('input').select()
}
// Catch the submit action
self.editingNameSubmit = function() {
self.editingName(false)
}
// Do on change
self.nameForEdit.subscribe(function(newName) {
// Anything change or empty?
if(!newName || self.name() == newName) return;
// Rename would abort Direct Unpack, so ask if user is sure
if(self.direct_unpack() && !confirm(glitterTranslate.renameAbort)) return;
// Send rename
callAPI({
mode: 'queue',
name: 'rename',
value: self.id,
value2: newName
}).then(self.parent.parent.refresh)
})
// See items
self.showFiles = function() {
// Not when still grabbing
if(self.isGrabbing()) return false;
// Trigger update
parent.parent.filelist.loadFiles(self)
}
// Toggle calculation of dropdown
// Turns out that the <select> in the dropdown are a hugggeeee slowdown on initial load!
// Only loading on click cuts half the speed (especially on large queues)
self.toggleDropdown = function(item, event) {
self.hasDropdown(true)
// Keep it open!
keepOpen(event.target)
}
// Change of settings
self.changeCat = function(item, event) {
callAPI({
mode: 'change_cat',
value: item.id,
value2: item.category()
}).then(function() {
// Hide all tooltips before we refresh
$('.queue-item-settings li').filter('[data-tooltip="true"]').tooltip('hide')
self.parent.parent.refresh()
})
}
self.changeScript = function(item) {
// Not on empty handlers
if(!item.script() || parent.scriptsList().length <= 1) return;
callAPI({
mode: 'change_script',
value: item.id,
value2: item.script()
})
}
self.changeProcessing = function(item) {
callAPI({
mode: 'change_opts',
value: item.id,
value2: item.unpackopts()
})
}
self.changePriority = function(item, event) {
// Not if we are fetching extra blocks for repair!
if(item.isFetchingBlocks) return
callAPI({
mode: 'queue',
name: 'priority',
value: item.id,
value2: item.priority()
}).then(function() {
// Hide all tooltips before we refresh
$('.queue-item-settings li').filter('[data-tooltip="true"]').tooltip('hide')
self.parent.parent.refresh()
})
}
// Remove 1 download from queue
self.removeDownload = function(item, event) {
// Confirm and remove
if(!self.parent.parent.confirmDeleteQueue() || confirm(glitterTranslate.deleteMsg + ":\n" + item.name() + "\n\n" + glitterTranslate.removeDow1)) {
var itemToDelete = this;
// Show notification
showNotification('.main-notification-box-removing')
callAPI({
mode: 'queue',
name: 'delete',
del_files: 1,
value: item.id
}).then(function(response) {
// Make sure no flickering (if there are more items left) and then remove
self.parent.isLoading(self.parent.totalItems() > 1)
parent.queueItems.remove(itemToDelete);
parent.multiEditItems.remove(function(inList) { return inList.id == itemToDelete.id; })
self.parent.parent.refresh();
// Hide notifcation
hideNotification()
});
}
};
}