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.
2283 lines
82 KiB
2283 lines
82 KiB
/******
|
|
|
|
Glitter V1
|
|
By Safihre (2015) - safihre@sabnzbd.org
|
|
|
|
Code extended from Shiny-template
|
|
Code examples used from Knockstrap-template
|
|
|
|
********/
|
|
/**
|
|
FIX for IE8 and below not having IndexOf for array's
|
|
**/
|
|
if(!Array.prototype.indexOf) {
|
|
Array.prototype.indexOf = function(elt /*, from*/ ) {
|
|
var len = this.length >>> 0;
|
|
var from = Number(arguments[1]) || 0;
|
|
from = (from < 0) ? Math.ceil(from) : Math.floor(from);
|
|
if(from < 0) from += len;
|
|
for(; from < len; from++) {
|
|
if(from in this && this[from] === elt) return from;
|
|
}
|
|
return -1;
|
|
};
|
|
}
|
|
|
|
/**
|
|
Base variables and functions
|
|
**/
|
|
var fadeOnDeleteDuration = 400; // ms after deleting a row
|
|
var isMobile = (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase()));
|
|
|
|
/**
|
|
GLITTER CODE
|
|
**/
|
|
$(function() {
|
|
|
|
// Basic API-call
|
|
function callAPI(data) {
|
|
// Fill basis var's
|
|
data.output = "json";
|
|
data.apikey = apiKey;
|
|
var ajaxQuery = $.ajax({
|
|
url: "tapi",
|
|
type: "GET",
|
|
cache: false,
|
|
data: data,
|
|
timeout: 6000 // Wait a little longer on mobile connections
|
|
});
|
|
|
|
return $.when(ajaxQuery);
|
|
}
|
|
|
|
// Special API call
|
|
function callSpecialAPI(url, data) {
|
|
// Did we get input?
|
|
if(data == undefined) data = {};
|
|
// Fill basis var's
|
|
data.output = "json";
|
|
data.apikey = apiKey;
|
|
var ajaxQuery = $.ajax({
|
|
url: url,
|
|
type: "GET",
|
|
cache: false,
|
|
data: data
|
|
});
|
|
|
|
return $.when(ajaxQuery);
|
|
}
|
|
|
|
/**
|
|
Handle visibility changes so we
|
|
do only incremental update when not visible
|
|
**/
|
|
var pageIsVisible = true;
|
|
// Set the name of the hidden property and the change event for visibility
|
|
var hidden, visibilityChange;
|
|
if(typeof document.hidden !== "undefined") { // Opera 12.10 and Firefox 18 and later support
|
|
hidden = "hidden";
|
|
visibilityChange = "visibilitychange";
|
|
} else if(typeof document.mozHidden !== "undefined") {
|
|
hidden = "mozHidden";
|
|
visibilityChange = "mozvisibilitychange";
|
|
} else if(typeof document.msHidden !== "undefined") {
|
|
hidden = "msHidden";
|
|
visibilityChange = "msvisibilitychange";
|
|
} else if(typeof document.webkitHidden !== "undefined") {
|
|
hidden = "webkitHidden";
|
|
visibilityChange = "webkitvisibilitychange";
|
|
}
|
|
|
|
// Set the global visibility
|
|
function handleVisibilityChange() {
|
|
if(document[hidden]) {
|
|
pageIsVisible = false;
|
|
} else {
|
|
pageIsVisible = true;
|
|
}
|
|
}
|
|
|
|
// Add event listener only for supported browsers
|
|
if(typeof document.addEventListener !== "undefined" && typeof document[hidden] !== "undefined") {
|
|
// Handle page visibility change
|
|
document.addEventListener(visibilityChange, handleVisibilityChange, false);
|
|
}
|
|
|
|
/**
|
|
Define main view model
|
|
**/
|
|
function ViewModel() {
|
|
// Initialize models
|
|
var self = this;
|
|
self.queue = new QueueListModel(this);
|
|
self.history = new HistoryListModel(this);
|
|
self.filelist = new Fileslisting(this);
|
|
|
|
// Set information varibales
|
|
self.title = ko.observable()
|
|
self.isRestarting = ko.observable(false);
|
|
self.useGlobalOptions = ko.observable(localStorage.getItem('useGlobalOptions') == 'false' ? false : true)
|
|
self.refreshRate = ko.observable(localStorage.getItem('pageRefreshRate') ? localStorage.getItem('pageRefreshRate') : 1)
|
|
self.dateFormat = ko.observable(localStorage.getItem('pageDateFormat') ? localStorage.getItem('pageDateFormat') : 'dd-MM-yy')
|
|
self.extraColumn = ko.observable(localStorage.getItem('pageExtraColumn') ? localStorage.getItem('pageExtraColumn') : '')
|
|
self.hasStatusInfo = ko.observable(false); // True when we load it
|
|
self.showActiveConnections = ko.observable(false);
|
|
self.speed = ko.observable(0);
|
|
self.speedMetric = ko.observable();
|
|
self.speedMetrics = { K: "KB/s", M: "MB/s", G: "GB/s" };
|
|
self.bandwithLimit = ko.observable(false);
|
|
self.speedLimit = ko.observable(100).extend({ rateLimit: { timeout: 400, method: "notifyWhenChangesStop" } });
|
|
self.speedLimitInt = ko.observable(false); // We need the 'internal' counter so we don't trigger the API all the time
|
|
self.downloadsPaused = ko.observable(false);
|
|
self.timeLeft = ko.observable("0:00");
|
|
self.diskSpaceLeft1 = ko.observable();
|
|
self.diskSpaceLeft2 = ko.observable();
|
|
self.queueDataLeft = ko.observable();
|
|
self.queueDataLeftMB = ko.observable(); // To check if we have enough diskspace left
|
|
self.quotaLimit = ko.observable();
|
|
self.quotaLimitLeft = ko.observable();
|
|
self.nrWarnings = ko.observable(0);
|
|
self.allWarnings = ko.observableArray([]);
|
|
self.allMessages = ko.observableArray([]);
|
|
self.onQueueFinish = ko.observable('');
|
|
self.speedHistory = [];
|
|
|
|
/***
|
|
Dynamic functions
|
|
***/
|
|
|
|
// Make the speedlimit tekst
|
|
self.speedLimitText = ko.computed(function() {
|
|
// Set?
|
|
if(!self.bandwithLimit()) return;
|
|
|
|
// Only the number
|
|
bandwithLimitNumber = parseInt(self.bandwithLimit());
|
|
bandwithLimitNumber = (bandwithLimitNumber * (self.speedLimit() / 100)).toFixed(1);
|
|
|
|
// The text
|
|
bandwithLimitText = self.bandwithLimit().replace(/[^a-zA-Z]+/g, '');
|
|
|
|
// Fix it for lower than 1MB/s
|
|
if(bandwithLimitText == 'M' && bandwithLimitNumber < 1.025) {
|
|
bandwithLimitText = 'K';
|
|
bandwithLimitNumber = Math.round(bandwithLimitNumber * 1024);
|
|
}
|
|
// Show text
|
|
return bandwithLimitNumber + ' ' + (self.speedMetrics[bandwithLimitText] ? self.speedMetrics[bandwithLimitText] : "KB/s");
|
|
});
|
|
|
|
// Dynamic speed text function
|
|
self.speedText = ko.computed(function() {
|
|
return self.speed() + ' ' + (self.speedMetrics[self.speedMetric()] ? self.speedMetrics[self.speedMetric()] : "KB/s");
|
|
});
|
|
|
|
// Dynamic icon
|
|
self.SABIcon = ko.computed(function() {
|
|
if(self.downloadsPaused()) {
|
|
return './staticcfg/ico/faviconpaused.ico';
|
|
} else {
|
|
return './staticcfg/ico/favicon.ico';
|
|
}
|
|
})
|
|
|
|
// Dynamic queue length check
|
|
self.hasQueue = ko.computed(function() {
|
|
return(self.queue.queueItems().length > 0 || self.queue.searchTerm())
|
|
})
|
|
|
|
// Dynamic history length check
|
|
self.hasHistory = ko.computed(function() {
|
|
// We also 'have history' if we can't find any results of the search or there are no failed ones
|
|
return (self.history.historyItems().length > 0 || self.history.searchTerm() || self.history.showFailed())
|
|
});
|
|
|
|
self.hasWarnings = ko.computed(function() {
|
|
return(self.allWarnings().length > 0)
|
|
})
|
|
|
|
// Check for any warnings/messages
|
|
self.hasMessages = ko.computed(function() {
|
|
return self.nrWarnings() > 0 || self.allMessages().length > 0;
|
|
})
|
|
|
|
// Update main queue
|
|
self.updateQueue = function(response) {
|
|
if(!self.queue.shouldUpdate()) return;
|
|
|
|
// Make sure we are displaying the interface
|
|
if(self.isRestarting() >= 1) {
|
|
// Decrease the counter by 1
|
|
// In case of restart (which takes time to fire) we count down
|
|
// In case of re-connect after failure it counts from 1 so emmediate continuation
|
|
self.isRestarting(self.isRestarting() - 1);
|
|
return;
|
|
}
|
|
|
|
/***
|
|
Basic information
|
|
***/
|
|
// Queue left
|
|
self.queueDataLeft(response.queue.mbleft > 0 ? response.queue.sizeleft : '')
|
|
self.queueDataLeftMB(response.queue.mbleft > 0 ? response.queue.mbleft : 0)
|
|
|
|
// Paused?
|
|
self.downloadsPaused(response.queue.paused);
|
|
|
|
// Finish action. Replace null with empty
|
|
self.onQueueFinish(response.queue.finishaction ? response.queue.finishaction : '');
|
|
|
|
// Disk sizes
|
|
self.diskSpaceLeft1(parseFloat(response.queue.diskspace1).toFixed(1))
|
|
|
|
// Same sizes? Then it's all 1 disk!
|
|
if(response.queue.diskspace1 != response.queue.diskspace2) {
|
|
self.diskSpaceLeft2(parseFloat(response.queue.diskspace2).toFixed(1))
|
|
}
|
|
|
|
// Quota
|
|
self.quotaLimit(response.queue.quota)
|
|
self.quotaLimitLeft(response.queue.left_quota)
|
|
|
|
// Warnings (new warnings will trigger an update of allMessages)
|
|
self.nrWarnings(response.queue.have_warnings)
|
|
|
|
/***
|
|
Spark line
|
|
***/
|
|
// Break the speed if empty queue
|
|
if(response.queue.sizeleft == '0 B') {
|
|
response.queue.kbpersec = 0;
|
|
response.queue.speed = '0';
|
|
}
|
|
|
|
// Re-format the speed
|
|
speedSplit = response.queue.speed.split(/\s/);
|
|
self.speed(parseFloat(speedSplit[0]));
|
|
self.speedMetric(speedSplit[1]);
|
|
|
|
// Update sparkline data
|
|
if(self.speedHistory.length >= 275) {
|
|
// Remove first one
|
|
self.speedHistory.shift();
|
|
}
|
|
// Add
|
|
self.speedHistory.push(parseInt(response.queue.kbpersec));
|
|
|
|
// Is sparkline visible? Not on small mobile devices..
|
|
if($('.sparkline-container').css('display') != 'none') {
|
|
// Make sparkline
|
|
if(self.speedHistory.length == 1) {
|
|
// We only use speedhistory from SAB if we use global settings
|
|
// Otherwise SAB doesn't know the refresh rate
|
|
if(!self.useGlobalOptions()) {
|
|
sabSpeedHistory = [];
|
|
} else {
|
|
// Update internally
|
|
self.speedHistory = sabSpeedHistory;
|
|
}
|
|
|
|
// Create
|
|
$('.sparkline').peity("line", {
|
|
width: 275,
|
|
height: 32,
|
|
fill: '#9DDB72',
|
|
stroke: '#AAFFAA',
|
|
values: sabSpeedHistory
|
|
})
|
|
|
|
} else {
|
|
// Update
|
|
$('.sparkline').text(self.speedHistory.join(",")).change()
|
|
}
|
|
}
|
|
|
|
/***
|
|
Speedlimit
|
|
***/
|
|
// Nothing = 100%
|
|
response.queue.speedlimit = response.queue.speedlimit == '' ? 100 : response.queue.speedlimit;
|
|
self.speedLimitInt(response.queue.speedlimit)
|
|
|
|
// Only update from external source when user isn't doing input
|
|
if(!$('.speedlimit-dropdown .btn-group .btn-group').is('.open')) {
|
|
self.speedLimit(response.queue.speedlimit)
|
|
}
|
|
|
|
/***
|
|
Download timing and pausing
|
|
***/
|
|
timeString = response.queue.timeleft;
|
|
if(timeString === '') {
|
|
timeString = '0:00';
|
|
} else {
|
|
timeString = rewriteTime(response.queue.timeleft)
|
|
}
|
|
|
|
// Paused main queue
|
|
if(self.downloadsPaused()) {
|
|
if(response.queue.pause_int == '0') {
|
|
timeString = glitterTranslate.paused;
|
|
} else {
|
|
pauseSplit = response.queue.pause_int.split(/:/);
|
|
seconds = parseInt(pauseSplit[0]) * 60 + parseInt(pauseSplit[1]);
|
|
hours = Math.floor(seconds / 3600);
|
|
minutes = Math.floor((seconds -= hours * 3600) / 60);
|
|
seconds -= minutes * 60;
|
|
timeString = glitterTranslate.paused + ' (' + rewriteTime(hours + ":" + minutes + ":" + seconds) + ')';
|
|
}
|
|
|
|
// Add info about amount of download (if actually downloading)
|
|
if(response.queue.noofslots > 0 && parseInt(self.queueDataLeft()) > 0) {
|
|
self.title(timeString + ' - ' + self.queueDataLeft() + ' ' + glitterTranslate.left + ' - SABnzbd')
|
|
} else {
|
|
// Set title with pause information
|
|
self.title(timeString + ' - SABnzbd')
|
|
}
|
|
} else if(response.queue.noofslots > 0 && parseInt(self.queueDataLeft()) > 0) {
|
|
// Set title only if we are actually downloading something..
|
|
self.title(self.speedText() + ' - ' + self.queueDataLeft() + ' ' + glitterTranslate.left + ' - SABnzbd')
|
|
} else {
|
|
// Empty title
|
|
self.title('SABnzbd')
|
|
}
|
|
|
|
// Save for timing box
|
|
self.timeLeft(timeString);
|
|
|
|
// Update queue rows
|
|
self.queue.updateFromData(response.queue);
|
|
}
|
|
|
|
// Update history items
|
|
self.updateHistory = function(response) {
|
|
if(!response) return;
|
|
self.history.updateFromData(response.history);
|
|
}
|
|
|
|
// Refresh function
|
|
self.refresh = function() {
|
|
/**
|
|
Limited refresh
|
|
**/
|
|
// Only update the title when page not visible
|
|
if(!pageIsVisible) {
|
|
// Request new title
|
|
callSpecialAPI('queue/', {}).then(function(data) {
|
|
// Split title & speed
|
|
dataSplit = data.split('|||');
|
|
|
|
// Set title
|
|
self.title(dataSplit[0]);
|
|
|
|
// Update sparkline data
|
|
if(self.speedHistory.length >= 50) {
|
|
// Remove first one
|
|
self.speedHistory.shift();
|
|
}
|
|
// Add
|
|
self.speedHistory.push(dataSplit[1]);
|
|
|
|
// Does it contain 'Paused'? Update icon!
|
|
self.downloadsPaused(data.indexOf(glitterTranslate.paused) > -1)
|
|
})
|
|
// Do not continue!
|
|
return;
|
|
}
|
|
|
|
/**
|
|
Full refresh
|
|
**/
|
|
// Do requests for full information
|
|
// Catch the fail to display message
|
|
queueApi = callAPI({
|
|
mode: "queue",
|
|
search: self.queue.searchTerm(),
|
|
start: self.queue.pagination.currentStart(),
|
|
limit: parseInt(self.queue.paginationLimit())
|
|
}).then(
|
|
self.updateQueue,
|
|
function() {
|
|
self.isRestarting(1)
|
|
}
|
|
);
|
|
callAPI({
|
|
mode: "history",
|
|
search: self.history.searchTerm(),
|
|
failed_only: self.history.showFailed()*1,
|
|
start: self.history.pagination.currentStart(),
|
|
limit: parseInt(self.history.paginationLimit())
|
|
}).then(self.updateHistory);
|
|
|
|
// Return for .then() functionality
|
|
return queueApi;
|
|
};
|
|
|
|
// Set pause action on click of toggle
|
|
self.pauseToggle = function() {
|
|
callAPI({
|
|
mode: (self.downloadsPaused() ? "resume" : "pause")
|
|
}).then(self.refresh);
|
|
self.downloadsPaused(!self.downloadsPaused());
|
|
}
|
|
|
|
// Set pause timer
|
|
self.pauseTime = function(e, b) {
|
|
pauseDuration = $(b.currentTarget).data('time');
|
|
callAPI({
|
|
mode: 'config',
|
|
name: 'set_pause',
|
|
value: pauseDuration
|
|
});
|
|
self.downloadsPaused(true);
|
|
};
|
|
|
|
// Custom pause-timer
|
|
self.customPauseTime = function() {
|
|
// Was it loaded already?
|
|
if(!Date.i18n) {
|
|
jQuery.getScript('./static/javascripts/date.min.js').then(function() {
|
|
// After loading we start again
|
|
self.customPauseTime()
|
|
})
|
|
return;
|
|
}
|
|
|
|
// Pop the question
|
|
var pausePrompt = prompt(glitterTranslate.pausePrompt);
|
|
var pauseParsed = Date.parse(pausePrompt);
|
|
|
|
// Did we get it?
|
|
if(pauseParsed) {
|
|
// Is it just now?
|
|
if(pauseParsed <= Date.parse('now')) {
|
|
// Try again with the '+' in front, the parser doesn't get 100min
|
|
pauseParsed = Date.parse('+' + pausePrompt);
|
|
}
|
|
|
|
// Calculate difference in minutes
|
|
var pauseDuration = Math.round((pauseParsed - Date.parse('now'))/1000/60);
|
|
|
|
// If in the future
|
|
if(pauseDuration > 0) {
|
|
callAPI({
|
|
mode: 'config',
|
|
name: 'set_pause',
|
|
value: pauseDuration
|
|
});
|
|
self.downloadsPaused(true);
|
|
}
|
|
} else if(pausePrompt) {
|
|
// No.. And user did not press cancel
|
|
alert(glitterTranslate.pausePromptFail)
|
|
self.customPauseTime();
|
|
}
|
|
}
|
|
|
|
// Update the warnings
|
|
self.nrWarnings.subscribe(function(newValue) {
|
|
// Really any change?
|
|
if(newValue == self.allWarnings().length) return;
|
|
|
|
// Get all warnings
|
|
callAPI({
|
|
mode: 'warnings'
|
|
}).then(function(response) {
|
|
// Reset it all
|
|
self.allWarnings.removeAll();
|
|
if(response) {
|
|
// Newest first
|
|
response.warnings.reverse()
|
|
|
|
// Go over all warnings and add
|
|
$.each(response.warnings, function(index, warning) {
|
|
// Split warning into parts
|
|
var warningSplit = warning.split(/\n/);
|
|
|
|
// Reformat CSS label and date
|
|
var warningData = {
|
|
index: index,
|
|
type: glitterTranslate.status[warningSplit[1]].slice(0, -1),
|
|
text: warningSplit.slice(2).join('<br/>'), // Recombine if multiple lines
|
|
date: $.format.date(warningSplit[0], self.dateFormat() + ' HH:mm'),
|
|
css: (warningSplit[1] == "ERROR" ? "danger" : warningSplit[1] == "WARNING" ? "warning" : "info"),
|
|
clear: self.clearWarnings
|
|
};
|
|
|
|
self.allWarnings.push(warningData)
|
|
})
|
|
}
|
|
});
|
|
})
|
|
|
|
// Clear warnings through this weird URL..
|
|
self.clearWarnings = function() {
|
|
if(!confirm(glitterTranslate.clearWarn))
|
|
return;
|
|
// Activate
|
|
callSpecialAPI("status/clearwarnings")
|
|
}
|
|
|
|
// Clear messages
|
|
self.clearMessages = function(whatToRemove) {
|
|
// Remove specifc type of messages
|
|
self.allMessages.remove(function(item) { return item.index == whatToRemove });
|
|
// Now so we don't show again today
|
|
localStorage.setItem(whatToRemove, 'false')
|
|
}
|
|
|
|
// Update on speed-limit change
|
|
self.speedLimit.subscribe(function(newValue) {
|
|
// Only on new load
|
|
if(!self.speedLimitInt()) return;
|
|
|
|
// Update
|
|
if(self.speedLimitInt() != newValue) {
|
|
callAPI({
|
|
mode: "config",
|
|
name: "speedlimit",
|
|
value: newValue
|
|
})
|
|
}
|
|
});
|
|
|
|
// Clear speedlimit
|
|
self.clearSpeedLimit = function() {
|
|
self.speedLimit(100);
|
|
}
|
|
|
|
// Shutdown options
|
|
self.onQueueFinish.subscribe(function(newValue) {
|
|
// Something changes
|
|
callAPI({
|
|
mode: 'queue',
|
|
name: 'change_complete_action',
|
|
value: newValue
|
|
})
|
|
})
|
|
|
|
// Use global settings or device-specific?
|
|
self.useGlobalOptions.subscribe(function(newValue) {
|
|
localStorage.setItem('useGlobalOptions', newValue)
|
|
|
|
// Reload in case of enabling global options
|
|
if(newValue) document.location = document.location;
|
|
})
|
|
|
|
// Update refreshrate
|
|
self.refreshRate.subscribe(function(newValue) {
|
|
// Set in javascript
|
|
clearInterval(self.interval)
|
|
self.interval = setInterval(self.refresh, parseInt(newValue) * 1000);
|
|
localStorage.setItem('pageRefreshRate', newValue);
|
|
|
|
// Save in config if global-settings
|
|
if(self.useGlobalOptions()) {
|
|
callAPI({
|
|
mode: "set_config",
|
|
section: "misc",
|
|
keyword: "refresh_rate",
|
|
value: newValue
|
|
})
|
|
}
|
|
|
|
})
|
|
|
|
// Update dateformat
|
|
self.dateFormat.subscribe(function(newValue) {
|
|
localStorage.setItem('pageDateFormat', newValue)
|
|
})
|
|
|
|
// Update extraColumn
|
|
self.extraColumn.subscribe(function(newValue) {
|
|
localStorage.setItem('pageExtraColumn', newValue)
|
|
})
|
|
|
|
/***
|
|
Add NZB's
|
|
***/
|
|
// Updating the label
|
|
self.updateBrowseLabel = function(data, event) {
|
|
// Get filename
|
|
var fileName = $(event.target).val().replace(/\\/g, '/').replace(/.*\//, '');
|
|
// Set label
|
|
if(fileName) $('.btn-file em').text(fileName)
|
|
}
|
|
|
|
// NOTE: Adjusted from Knockstrap template
|
|
self.addNZBFromFileForm = function(form) {
|
|
self.addNZBFromFile($(form.nzbFile)[0].files[0]);
|
|
|
|
// After that, hide and reset
|
|
$("#modal_add_nzb").modal("hide");
|
|
form.reset()
|
|
$('#nzbname').val('')
|
|
$('.btn-file em').html(glitterTranslate.chooseFile + '…')
|
|
}
|
|
self.addNZBFromURL = function(form) {
|
|
// Add
|
|
callAPI({
|
|
mode: "addurl",
|
|
name: $(form.nzbURL).val(),
|
|
nzbname: $('#nzbname').val(),
|
|
cat: $('#modal_add_nzb select[name="Category"]').val() == '' ? 'Default' : $('#modal_add_nzb select[name="Category"]').val(),
|
|
script: $('#modal_add_nzb select[name="Post-processing"]').val() == '' ? 'Default' : $('#modal_add_nzb select[name="Post-processing"]').val(),
|
|
priority: $('#modal_add_nzb select[name="Priority"]').val() == '' ? -100 : $('#modal_add_nzb select[name="Priority"]').val(),
|
|
pp: $('#modal_add_nzb select[name="Processing"]').val() == '' ? -1 : $('#modal_add_nzb select[name="Processing"]').val()
|
|
}).then(function(r) {
|
|
// Hide and reset/refresh
|
|
self.refresh()
|
|
$("#modal_add_nzb").modal("hide");
|
|
form.reset()
|
|
$('#nzbname').val('')
|
|
});
|
|
|
|
}
|
|
self.addNZBFromFile = function(file) {
|
|
// Adding a file happens through this special function
|
|
var data = new FormData();
|
|
data.append("name", file);
|
|
data.append("mode", "addfile");
|
|
data.append("nzbname", $('#nzbname').val());
|
|
data.append("cat", $('#modal_add_nzb select[name="Category"]').val() == '' ? 'Default' : $('#modal_add_nzb select[name="Category"]').val()); // Default category
|
|
data.append("script", $('#modal_add_nzb select[name="Post-processing"]').val() == '' ? 'Default' : $('#modal_add_nzb select[name="Post-processing"]').val()); // Default script
|
|
data.append("priority", $('#modal_add_nzb select[name="Priority"]').val() == '' ? -100 : $('#modal_add_nzb select[name="Priority"]').val()); // Default priority
|
|
data.append("pp", $('#modal_add_nzb select[name="Processing"]').val() == '' ? -1 : $('#modal_add_nzb select[name="Processing"]').val()); // Default post-processing options
|
|
data.append("apikey", apiKey);
|
|
// Add
|
|
$.ajax({
|
|
url: "tapi",
|
|
type: "POST",
|
|
cache: false,
|
|
processData: false,
|
|
contentType: false,
|
|
data: data
|
|
}).then(function(r) {
|
|
// Refresh
|
|
self.refresh();
|
|
});
|
|
|
|
}
|
|
|
|
// Load status info
|
|
self.loadStatusInfo = function() {
|
|
// Reset
|
|
self.hasStatusInfo(false)
|
|
|
|
// Load the custom status info
|
|
callSpecialAPI('status/').then(function(data) {
|
|
// Already exists?
|
|
if(self.hasStatusInfo()) {
|
|
ko.mapping.fromJS(ko.utils.parseJson(data), self.statusInfo);
|
|
} else {
|
|
// Making the new object
|
|
self.statusInfo = ko.mapping.fromJS(ko.utils.parseJson(data));
|
|
|
|
// Only now we can subscribe to the log-level-changes!
|
|
self.statusInfo.status.loglevel.subscribe(function(newValue) {
|
|
// Update log-level
|
|
callSpecialAPI('status/change_loglevel', {
|
|
loglevel: newValue
|
|
});
|
|
})
|
|
}
|
|
// Show again
|
|
self.hasStatusInfo(true)
|
|
|
|
// Add tooltips again
|
|
if(!isMobile) $('#modal_options [data-toggle="tooltip"]').tooltip({ trigger: 'hover', container: 'body' })
|
|
});
|
|
}
|
|
|
|
// Do a disk-speedtest
|
|
self.testDiskSpeed = function() {
|
|
// Hide tooltips (otherwise they stay forever..)
|
|
$('#options_status [data-toggle="tooltip"]').tooltip('hide')
|
|
// Hide before running the test
|
|
self.hasStatusInfo(false)
|
|
// Run it and then display it
|
|
callSpecialAPI('status/dashrefresh').then(function() {
|
|
self.loadStatusInfo()
|
|
})
|
|
}
|
|
|
|
// Unblock server
|
|
self.unblockServer = function(servername) {
|
|
callSpecialAPI("status/unblock_server", {
|
|
server: servername
|
|
}).then(function() {
|
|
$("#modal_options").modal("hide");
|
|
})
|
|
}
|
|
|
|
// Orphaned folder processing
|
|
self.folderProcess = function(e, b) {
|
|
// Activate
|
|
callSpecialAPI("status/" + $(b.currentTarget).data('action'), {
|
|
name: $(b.currentTarget).data('folder')
|
|
}).then(function() {
|
|
// Remove item and load status data
|
|
$(b.currentTarget).parent().parent().fadeOut(fadeOnDeleteDuration)
|
|
// Pop from list
|
|
self.statusInfo.status.folders.remove(function(item) {
|
|
return item.folder() == $(b.currentTarget).data('folder')
|
|
})
|
|
})
|
|
// Remove message if now less than 3
|
|
if(self.statusInfo.status.folders().length < 4) {
|
|
self.clearMessages('lastOrphanedMsg')
|
|
}
|
|
}
|
|
|
|
// Orphaned folder deletion of all
|
|
self.removeAllOrphaned = function() {
|
|
if(!confirm(glitterTranslate.clearWarn))
|
|
return;
|
|
|
|
// Do them all
|
|
ko.utils.arrayForEach(self.statusInfo.status.folders(), function(folder) {
|
|
callSpecialAPI("status/delete", {
|
|
name: folder.folder()
|
|
})
|
|
});
|
|
// Refresh
|
|
self.loadStatusInfo()
|
|
// Remove message
|
|
self.clearMessages('lastOrphanedMsg')
|
|
}
|
|
|
|
/**
|
|
SABnzb options
|
|
**/
|
|
// Shutdown
|
|
self.shutdownSAB = function() {
|
|
return confirm(glitterTranslate.shutdown);
|
|
}
|
|
// Restart
|
|
self.restartSAB = function() {
|
|
if(!confirm(glitterTranslate.restart)) return;
|
|
// Call restart function
|
|
callSpecialAPI("config/restart")
|
|
|
|
// Set counter, we need at least 15 seconds
|
|
self.isRestarting(Math.max(1, Math.floor(15 / self.refreshRate())));
|
|
// Force refresh in case of very long refresh-times
|
|
if(self.refreshRate() > 30) {
|
|
setTimeout(self.refresh, 30 * 1000)
|
|
}
|
|
}
|
|
// Queue actions
|
|
self.doQueueAction = function(data, event) {
|
|
// Send to the API
|
|
callAPI({ mode: $(event.target).data('mode') })
|
|
}
|
|
// Repair queue
|
|
self.repairQueue = function() {
|
|
if(!confirm(glitterTranslate.repair)) return;
|
|
callSpecialAPI("config/repair").then(function() {
|
|
$("#modal_options").modal("hide");
|
|
})
|
|
}
|
|
// Force disconnect
|
|
self.forceDisconnect = function() {
|
|
callSpecialAPI("status/disconnect").then(function() {
|
|
$("#modal_options").modal("hide");
|
|
})
|
|
}
|
|
|
|
/***
|
|
Retrieve config information and do startup functions
|
|
***/
|
|
// Get the speed-limit, refresh rate and server names
|
|
callAPI({
|
|
mode: 'get_config'
|
|
}).then(function(response) {
|
|
// Do we use global, or local settings?
|
|
if(self.useGlobalOptions()) {
|
|
// Set refreshrate (defaults to 1/s)
|
|
if(!response.config.misc.refresh_rate) response.config.misc.refresh_rate = 1;
|
|
self.refreshRate(response.config.misc.refresh_rate.toString());
|
|
|
|
// Set history limit
|
|
self.history.paginationLimit(response.config.misc.history_limit.toString())
|
|
|
|
// Set queue limit
|
|
self.queue.paginationLimit(response.config.misc.queue_limit.toString())
|
|
}
|
|
|
|
// Set bandwidth limit
|
|
if(!response.config.misc.bandwidth_max) response.config.misc.bandwidth_max = false;
|
|
self.bandwithLimit(response.config.misc.bandwidth_max);
|
|
|
|
// Save servers (for reporting functionality of OZnzb)
|
|
self.servers = response.config.servers;
|
|
})
|
|
|
|
// Check for Orphaned folders every day
|
|
if(localStorage.getItem('lastOrphanedCheck')*1 + (1000*3600*72) < Date.now()) {
|
|
// Update status-info
|
|
self.loadStatusInfo();
|
|
// Set check so we don't do it every page load
|
|
localStorage.setItem('lastOrphanedCheck', Date.now())
|
|
}
|
|
|
|
// We don't know exactly when it will finish, so we will wait 4 sec
|
|
setTimeout(function() {
|
|
// Done?
|
|
if(self.hasStatusInfo()) {
|
|
// Orphaned folders?
|
|
if(self.statusInfo.status.folders().length >= 3) {
|
|
localStorage.setItem('lastOrphanedMsg', 'true')
|
|
}
|
|
}
|
|
|
|
// Show message (maybe it was there from before!)
|
|
if(localStorage.getItem('lastOrphanedMsg') == 'true') {
|
|
console.log('asdas')
|
|
self.allMessages.push({
|
|
index: 'lastOrphanedMsg',
|
|
type: 'INFO',
|
|
text: glitterTranslate.orphanedJobsMsg + ' <a href="#" onclick="$(\'a[href=#modal_options]\').click().parent().click()"><span class="glyphicon glyphicon-wrench"></span></a>',
|
|
css: 'info',
|
|
clear: function() { self.clearMessages('lastOrphanedMsg')}
|
|
});
|
|
}
|
|
// Timeout only when we don't already know there's a message
|
|
}, (localStorage.getItem('lastOrphanedMsg') != 'true') ? 4000 : 1)
|
|
|
|
// Update message
|
|
if(localStorage.getItem('lastUpdateMsg') != 'false' && newRelease) {
|
|
self.allMessages.push({
|
|
index: 'lastUpdateMsg',
|
|
type: 'INFO',
|
|
text: ('<a class="queue-update-sab" href="'+newReleaseUrl+'" target="_blank">'+glitterTranslate.updateAvailable+' '+newRelease+' <span class="glyphicon glyphicon-save"></span></a>'),
|
|
css: 'info'
|
|
});
|
|
}
|
|
|
|
/***
|
|
End of main functions, start of the fun!
|
|
***/
|
|
// Set interval for refreshing queue
|
|
self.interval = setInterval(self.refresh, parseInt(self.refreshRate()) * 1000);
|
|
|
|
// And refresh now!
|
|
self.refresh()
|
|
|
|
// Activate tooltips
|
|
if(!isMobile) $('[data-toggle="tooltip"]').tooltip({ trigger: 'hover', container: 'body' })
|
|
}
|
|
|
|
/**
|
|
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.multiEditItems = [];
|
|
|
|
// 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);
|
|
self.categoriesList = ko.observableArray([]);
|
|
self.scriptsList = ko.observableArray([]);
|
|
self.searchTerm = ko.observable('').extend({ rateLimit: { timeout: 200, method: "notifyWhenChangesStop" } });
|
|
self.paginationLimit = ko.observable(localStorage.getItem('queuePaginationLimit') ? localStorage.getItem('queuePaginationLimit') : 20)
|
|
self.pagination = new paginationModel(self);
|
|
|
|
// Don't update while dragging
|
|
self.shouldUpdate = function() {
|
|
return !self.dragging;
|
|
}
|
|
self.dragStart = function(e) {
|
|
self.dragging = true;
|
|
}
|
|
self.dragStop = function(e) {
|
|
self.dragging = false;
|
|
$(e.target).parent().removeClass('table-active-sorting')
|
|
}
|
|
|
|
// Update slots from API data
|
|
self.updateFromData = function(data) {
|
|
// Get all ID's'
|
|
var itemIds = $.map(self.queueItems(), function(i) {
|
|
return i.id;
|
|
});
|
|
|
|
// Reformat categories
|
|
self.categoriesList($.map(data.categories, function(cat) {
|
|
// Default?
|
|
if(cat == '*') return { catValue: '*', catText: glitterTranslate.defaultText };
|
|
return { catValue: cat, catText: cat };
|
|
}))
|
|
|
|
// Set categories and scripts and limit
|
|
self.scriptsList(data.scripts)
|
|
self.totalItems(data.noofslots);
|
|
|
|
// 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
|
|
self.queueItems.push(new QueueModel(self, item));
|
|
|
|
// Only now sort
|
|
self.queueItems.sort(function(a, b) {
|
|
return a.index() < b.index() ? -1 : 1;
|
|
});
|
|
}
|
|
});
|
|
|
|
// 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;
|
|
}));
|
|
});
|
|
};
|
|
|
|
// Move in sortable
|
|
self.move = function(e) {
|
|
var itemMoved = e.item;
|
|
var itemReplaced = ko.utils.arrayFirst(self.queueItems(), function(i) {
|
|
return i.index() == e.targetIndex;
|
|
});
|
|
|
|
itemMoved.index(e.targetIndex);
|
|
itemReplaced.index(e.sourceIndex);
|
|
|
|
callAPI({
|
|
mode: "switch",
|
|
value: itemMoved.id,
|
|
value2: e.targetIndex
|
|
}).then(function(r) {
|
|
if(r.position != e.targetIndex) {
|
|
itemMoved.index(e.sourceIndex);
|
|
itemReplaced.index(e.targetIndex);
|
|
}
|
|
});
|
|
};
|
|
|
|
// Save pagination state
|
|
self.paginationLimit.subscribe(function(newValue) {
|
|
// Save in local storage
|
|
localStorage.setItem('queuePaginationLimit', newValue)
|
|
|
|
// Save in config if global
|
|
if(self.parent.useGlobalOptions()) {
|
|
callAPI({
|
|
mode: "set_config",
|
|
section: "misc",
|
|
keyword: "queue_limit",
|
|
value: newValue
|
|
})
|
|
}
|
|
|
|
self.parent.refresh();
|
|
});
|
|
|
|
// Do we show search box. So it doesn't dissapear when nothing is found
|
|
self.hasQueueSearch = ko.computed(function() {
|
|
return (self.pagination.hasPagination() || self.searchTerm())
|
|
})
|
|
|
|
// Searching in queue (rate-limited in decleration)
|
|
self.searchTerm.subscribe(function() {
|
|
// If the refresh-rate is high we do a forced refresh
|
|
if(parseInt(self.parent.refreshRate()) >2 ) {
|
|
self.parent.refresh();
|
|
}
|
|
})
|
|
|
|
// Clear searchterm
|
|
self.clearSearchTerm = function() {
|
|
self.searchTerm('');
|
|
self.parent.refresh();
|
|
}
|
|
|
|
/***
|
|
Multi-edit functions
|
|
***/
|
|
self.queueSorting = function(data, event) {
|
|
// What action?
|
|
var sort, dir;
|
|
switch($(event.currentTarget).data('action')) {
|
|
case 'sortAgeAsc':
|
|
sort = 'avg_age';
|
|
dir = 'asc';
|
|
break;
|
|
case 'sortAgeDesc':
|
|
sort = 'avg_age';
|
|
dir = 'desc';
|
|
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;
|
|
}
|
|
// Send call
|
|
callAPI({
|
|
mode: 'queue',
|
|
name: 'sort',
|
|
sort: sort,
|
|
dir: dir
|
|
}).then(function() {
|
|
// Force a refresh and then re-sort
|
|
parent.refresh().then(function() {
|
|
self.queueItems.sort(function(a, b) {
|
|
return a.index() < b.index() ? -1 : 1;
|
|
});
|
|
})
|
|
})
|
|
}
|
|
|
|
self.showMultiEdit = function() {
|
|
// Update value
|
|
self.isMultiEditing(!self.isMultiEditing())
|
|
// Form
|
|
$form = $('form.multioperations-selector')
|
|
|
|
// Reset form
|
|
$form[0].reset();
|
|
|
|
// 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')
|
|
}
|
|
|
|
|
|
// Do update on close, to make sure it's all updated
|
|
if(!self.isMultiEditing()) {
|
|
self.parent.refresh()
|
|
}
|
|
}
|
|
|
|
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
|
|
$.each(self.multiEditItems, function(index) {
|
|
// Is this the one removed?
|
|
if(item.id == this.id) {
|
|
self.multiEditItems.splice(index, 1)
|
|
}
|
|
});
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Do the actual multi-update immediatly
|
|
self.doMultiEditUpdate = function() {
|
|
// Anything selected?
|
|
if(self.multiEditItems.length < 1) return;
|
|
// Retrieve the current settings
|
|
newCat = $('.multioperations-selector select[name="Category"]').val()
|
|
newScript = $('.multioperations-selector select[name="Post-processing"]').val()
|
|
newPrior = $('.multioperations-selector select[name="Priority"]').val()
|
|
newProc = $('.multioperations-selector select[name="Processing"]').val()
|
|
newStatus = $('.multioperations-selector input[name="multiedit-status"]:checked').val()
|
|
|
|
// List all the ID's
|
|
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
|
|
setTimeout(parent.refresh, 100)
|
|
|
|
}
|
|
|
|
// Selete all selected
|
|
self.doMultiDelete = function() {
|
|
if(!confirm(glitterTranslate.removeDown)) return;
|
|
|
|
// List all the ID's
|
|
strIDs = '';
|
|
$.each(self.multiEditItems, function(index) {
|
|
strIDs = strIDs + this.id + ',';
|
|
})
|
|
|
|
// Remove
|
|
callAPI({
|
|
mode: 'queue',
|
|
name: 'delete',
|
|
del_files: 1,
|
|
value: strIDs
|
|
}).then(function(response) {
|
|
if(response.status) {
|
|
$('.delete input:checked').parents('tr').fadeOut(fadeOnDeleteDuration, function() {
|
|
self.parent.refresh();
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// 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);
|
|
})
|
|
}, 100)
|
|
}, null, "arrayChange")
|
|
}
|
|
|
|
/**
|
|
Model for each Queue item
|
|
**/
|
|
function QueueModel(parent, data) {
|
|
var self = this;
|
|
self.parent = parent;
|
|
|
|
// Define all knockout variables
|
|
self.id;
|
|
self.index = ko.observable();
|
|
self.name = ko.observable();
|
|
self.status = ko.observable();
|
|
self.isGrabbing = ko.observable(false);
|
|
self.totalMB = ko.observable(0);
|
|
self.remainingMB = ko.observable(0);
|
|
self.avg_age = ko.observable(0);
|
|
self.timeLeft = ko.observable();
|
|
self.progressColor = ko.observable();
|
|
self.missingText = ko.observable();
|
|
self.category = ko.observable();
|
|
self.script = ko.observable();
|
|
self.priority = ko.observable();
|
|
self.unpackopts = ko.observable();
|
|
self.editingName = ko.observable(false);
|
|
self.nameForEdit = ko.observable();
|
|
self.pausedStatus = ko.observable();
|
|
self.rating_avg_video = ko.observable(false);
|
|
self.rating_avg_audio = ko.observable(false);
|
|
|
|
// Functional vars
|
|
self.downloadedMB = ko.computed(function() {
|
|
return(self.totalMB() - self.remainingMB()).toFixed(0);
|
|
}, this);
|
|
|
|
self.percentage = ko.computed(function() {
|
|
return((self.downloadedMB() / self.totalMB()) * 100).toFixed(2);
|
|
}, this);
|
|
|
|
self.percentageRounded = ko.computed(function() {
|
|
return fixPercentages(self.percentage())
|
|
}, this);
|
|
|
|
self.progressText = ko.computed(function() {
|
|
return self.downloadedMB() + " MB / " + (self.totalMB() * 1).toFixed(0) + " MB";
|
|
}, this);
|
|
|
|
self.extraText = ko.computed(function() {
|
|
// Picked anything?
|
|
switch(self.parent.parent.extraColumn()) {
|
|
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();
|
|
}
|
|
})
|
|
|
|
// Every update
|
|
self.updateFromData = function(data) {
|
|
// Things that need to be set
|
|
self.id = data.nzo_id;
|
|
self.name($.trim(data.filename));
|
|
self.index(data.index);
|
|
|
|
// General status
|
|
if(data.status == 'Grabbing') {
|
|
self.isGrabbing(true)
|
|
return; // Important! Otherwise cat/script/priority get magically changed!
|
|
} else if(self.isGrabbing()) {
|
|
// Reset after the grabbing is done!
|
|
self.isGrabbing(false)
|
|
}
|
|
|
|
// Set stats
|
|
self.progressColor(''); // Reset
|
|
self.status(data.status)
|
|
self.totalMB(parseFloat(data.mb));
|
|
self.remainingMB(parseFloat(data.mbleft));
|
|
self.avg_age(data.avg_age)
|
|
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');
|
|
|
|
// If exists, otherwise false
|
|
if(data.rating_avg_video !== undefined) {
|
|
self.rating_avg_video(data.rating_avg_video === 0 ? '-' : data.rating_avg_video);
|
|
self.rating_avg_audio(data.rating_avg_audio === 0 ? '-' : data.rating_avg_audio);
|
|
}
|
|
|
|
// Checking
|
|
if(data.status == 'Checking') {
|
|
self.progressColor('#58A9FA')
|
|
self.timeLeft(glitterTranslate.checking);
|
|
}
|
|
|
|
// Check for missing data, the value is arbitrary!
|
|
if(data.missing > 50) {
|
|
self.progressColor('#F8A34E');
|
|
self.missingText(data.missing + ' ' + glitterTranslate.misingArt)
|
|
}
|
|
|
|
// Set color
|
|
if((self.parent.parent.downloadsPaused() && data.priority != 'Force') || self.pausedStatus()) {
|
|
self.timeLeft(glitterTranslate.paused);
|
|
self.progressColor('#B7B7B7');
|
|
} else if(data.status != 'Checking') {
|
|
self.timeLeft(rewriteTime(data.timeleft));
|
|
}
|
|
};
|
|
|
|
// 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() {
|
|
// Change status and fill
|
|
self.editingName(true)
|
|
self.nameForEdit(self.name())
|
|
}
|
|
|
|
// Catch the submit action
|
|
self.editingNameSubmit = function() {
|
|
self.editingName(false)
|
|
}
|
|
|
|
// Do on change
|
|
self.nameForEdit.subscribe(function(newName) {
|
|
// Change?
|
|
if(newName != self.name() && newName != "") {
|
|
callAPI({
|
|
mode: 'queue',
|
|
name: "rename",
|
|
value: self.id,
|
|
value2: newName
|
|
})
|
|
.then(function(response) {
|
|
// Succes?
|
|
if(response.status) {
|
|
self.name(newName)
|
|
self.parent.parent.refresh;
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
// See items
|
|
self.showFiles = function() {
|
|
// Trigger update
|
|
parent.parent.filelist.loadFiles(self)
|
|
}
|
|
|
|
// Change of settings
|
|
self.changeCat = function(itemObj) {
|
|
callAPI({
|
|
mode: 'change_cat',
|
|
value: itemObj.id,
|
|
value2: itemObj.category()
|
|
})
|
|
}
|
|
self.changeScript = function(itemObj) {
|
|
// Not on empty handlers
|
|
if(!itemObj.script()) return;
|
|
callAPI({
|
|
mode: 'change_script',
|
|
value: itemObj.id,
|
|
value2: itemObj.script()
|
|
})
|
|
}
|
|
self.changeProcessing = function(itemObj) {
|
|
callAPI({
|
|
mode: 'change_opts',
|
|
value: itemObj.id,
|
|
value2: itemObj.unpackopts()
|
|
})
|
|
}
|
|
self.changePriority = function(itemObj) {
|
|
// Not if we are fetching extra blocks for repair!
|
|
if(itemObj.status() == 'Fetching') return
|
|
callAPI({
|
|
mode: 'queue',
|
|
name: 'priority',
|
|
value: itemObj.id,
|
|
value2: itemObj.priority()
|
|
})
|
|
}
|
|
|
|
// Remove
|
|
self.removeDownload = function(data, event) {
|
|
if(!confirm(glitterTranslate.removeDow1)) return;
|
|
var itemToDelete = this;
|
|
callAPI({
|
|
mode: 'queue',
|
|
name: 'delete',
|
|
del_files: 1,
|
|
value: this.id
|
|
}).then(function(response) {
|
|
if(response.status) {
|
|
// Fade and remove
|
|
$(event.currentTarget).parent().parent().fadeOut(fadeOnDeleteDuration, function() {
|
|
parent.queueItems.remove(itemToDelete);
|
|
self.parent.parent.refresh();
|
|
})
|
|
|
|
}
|
|
});
|
|
};
|
|
|
|
// Update
|
|
self.updateFromData(data);
|
|
}
|
|
|
|
/**
|
|
Model for the whole History with all it's items
|
|
**/
|
|
function HistoryListModel(parent) {
|
|
var self = this;
|
|
self.parent = parent;
|
|
|
|
// Variables
|
|
self.historyItems = ko.observableArray([]);
|
|
self.showFailed = ko.observable(false);
|
|
self.searchTerm = ko.observable('').extend({ rateLimit: { timeout: 200, method: "notifyWhenChangesStop" } });
|
|
self.paginationLimit = ko.observable(localStorage.getItem('historyPaginationLimit') ? localStorage.getItem('historyPaginationLimit') : 10);
|
|
self.totalItems = ko.observable(0);
|
|
self.pagination = new paginationModel(self);
|
|
|
|
// Download history info
|
|
self.downloadedToday = ko.observable();
|
|
self.downloadedWeek = ko.observable();
|
|
self.downloadedMonth = ko.observable();
|
|
self.downloadedTotal = ko.observable();
|
|
|
|
// Update function for history list
|
|
self.updateFromData = function(data) {
|
|
/***
|
|
History list functions per item
|
|
***/
|
|
var itemIds = $.map(self.historyItems(), function(i) {
|
|
return i.historyStatus.nzo_id();
|
|
});
|
|
|
|
$.each(data.slots, function(index, slot) {
|
|
var existingItem = ko.utils.arrayFirst(self.historyItems(), function(i) {
|
|
return i.historyStatus.nzo_id() == slot.nzo_id;
|
|
});
|
|
// Update or add?
|
|
if(existingItem) {
|
|
existingItem.updateFromData(slot);
|
|
itemIds.splice(itemIds.indexOf(slot.nzo_id), 1);
|
|
} else {
|
|
// Add history item
|
|
self.historyItems.push(new HistoryModel(self, slot));
|
|
|
|
// Only now sort so newest on top. completed is updated every time while download is waiting
|
|
// so doing the sorting every time would cause it to bounce around
|
|
self.historyItems.sort(function(a, b) {
|
|
return a.historyStatus.completed() > b.historyStatus.completed() ? -1 : 1;
|
|
});
|
|
}
|
|
});
|
|
|
|
// Remove the un-used ones
|
|
$.each(itemIds, function() {
|
|
var id = this.toString();
|
|
self.historyItems.remove(ko.utils.arrayFirst(self.historyItems(), function(i) {
|
|
return i.historyStatus.nzo_id() == id;
|
|
}));
|
|
});
|
|
|
|
/***
|
|
History information
|
|
***/
|
|
self.totalItems(data.noofslots);
|
|
self.downloadedToday(data.day_size);
|
|
self.downloadedWeek(data.week_size);
|
|
self.downloadedMonth(data.month_size);
|
|
self.downloadedTotal(data.total_size);
|
|
};
|
|
|
|
// Save pagination state
|
|
self.paginationLimit.subscribe(function(newValue) {
|
|
// Save in localstorage
|
|
localStorage.setItem('historyPaginationLimit', newValue)
|
|
|
|
// Save in config if global config
|
|
if(self.parent.useGlobalOptions()) {
|
|
callAPI({
|
|
mode: "set_config",
|
|
section: "misc",
|
|
keyword: "history_limit",
|
|
value: newValue
|
|
})
|
|
}
|
|
self.parent.refresh();
|
|
});
|
|
|
|
// Retry a job
|
|
self.retryJob = function(form) {
|
|
// Adding a extra retry file happens through this special function
|
|
var data = new FormData();
|
|
data.append("nzbfile", $(form.nzbFile)[0].files[0]);
|
|
data.append("job", $('#modal_retry_job input[name="retry_job_id"]').val());
|
|
data.append("password", $('#retry_job_password').val());
|
|
data.append("session", apiKey);
|
|
|
|
// Add
|
|
$.ajax({
|
|
url: "history/retry_pp",
|
|
type: "POST",
|
|
cache: false,
|
|
processData: false,
|
|
contentType: false,
|
|
data: data
|
|
}).then(function(r) {
|
|
self.parent.refresh()
|
|
});
|
|
|
|
$("#modal_retry_job").modal("hide");
|
|
form.reset()
|
|
}
|
|
|
|
// Searching in history (rate-limited in decleration)
|
|
self.searchTerm.subscribe(function() {
|
|
// If the refresh-rate is high we do a forced refresh
|
|
if(parseInt(self.parent.refreshRate()) >2 ) {
|
|
self.parent.refresh();
|
|
}
|
|
})
|
|
|
|
// Clear searchterm
|
|
self.clearSearchTerm = function() {
|
|
self.searchTerm('');
|
|
self.parent.refresh();
|
|
}
|
|
|
|
// Toggle showing failed
|
|
self.toggleShowFailed = function() {
|
|
self.showFailed(!self.showFailed())
|
|
// Force refresh
|
|
self.parent.refresh()
|
|
}
|
|
|
|
// Empty history options
|
|
self.emptyHistory = function() {
|
|
$("#modal_purge_history").modal('show');
|
|
|
|
// After click
|
|
$('#modal_purge_history .modal-body .btn').on('click', function(event) {
|
|
// Only remove failed
|
|
if(this.id == 'history_purge_failed') {
|
|
del_files = 0;
|
|
value = 'failed';
|
|
}
|
|
// Also remove files
|
|
if(this.id == 'history_purgeremove_failed') {
|
|
del_files = 1;
|
|
value = 'failed';
|
|
}
|
|
// Remove completed
|
|
if(this.id == 'history_purge_completed') {
|
|
del_files = 0;
|
|
value = 'completed';
|
|
}
|
|
|
|
// Call API and close the window
|
|
callAPI({
|
|
mode: 'history',
|
|
name: 'delete',
|
|
value: value,
|
|
del_files: del_files
|
|
}).then(function(response) {
|
|
if(response.status) {
|
|
self.parent.refresh();
|
|
$("#modal_purge_history").modal('hide');
|
|
}
|
|
});
|
|
});
|
|
};
|
|
}
|
|
|
|
/**
|
|
Model for each History item
|
|
**/
|
|
function HistoryModel(parent, data) {
|
|
var self = this;
|
|
self.parent = parent;
|
|
|
|
// We only update the whole set of information on first add
|
|
// If we update the full set every time it uses lot of CPU
|
|
// The Status/Actionline/scriptline/completed we do update every time
|
|
// When clicked on the more-info button we load the rest again
|
|
self.nzo_id = '';
|
|
self.updateAllHistory = false;
|
|
self.historyStatus = ko.mapping.fromJS(data);
|
|
self.status = ko.observable();
|
|
self.action_line = ko.observable();
|
|
self.script_line = ko.observable();
|
|
self.fail_message = ko.observable();
|
|
self.completed = ko.observable();
|
|
self.canRetry = ko.observable();
|
|
|
|
self.updateFromData = function(data) {
|
|
// Fill all the basic info
|
|
self.nzo_id = data.nzo_id;
|
|
self.status(data.status)
|
|
self.action_line(data.action_line)
|
|
self.script_line(data.script_line)
|
|
self.fail_message(data.fail_message)
|
|
self.completed(data.completed)
|
|
self.canRetry(data.retry)
|
|
|
|
// Update all ONCE?
|
|
if(self.updateAllHistory) {
|
|
ko.mapping.fromJS(data, {}, self.historyStatus);
|
|
self.updateAllHistory = false;
|
|
}
|
|
};
|
|
|
|
// True/false if failed or not
|
|
self.failed = ko.computed(function() {
|
|
return self.status() === 'Failed';
|
|
});
|
|
|
|
// Waiting?
|
|
self.processingWaiting = ko.computed(function() {
|
|
return(self.status() == 'Queued')
|
|
})
|
|
|
|
// Processing or done?
|
|
self.processingDownload = ko.computed(function() {
|
|
var status = self.status();
|
|
return(status === 'Extracting' || status === 'Moving' || status === 'Verifying' || status === 'Running' || status == 'Repairing')
|
|
})
|
|
|
|
// Format status text
|
|
self.statusText = ko.computed(function() {
|
|
if(self.action_line() !== '')
|
|
return self.action_line();
|
|
if(self.status() === 'Failed') // Failed
|
|
return self.fail_message();
|
|
if(self.status() === 'Queued')
|
|
return glitterTranslate.status['Queued'];
|
|
if(self.script_line() === '') // No script line
|
|
return glitterTranslate.status['Completed']
|
|
|
|
return self.script_line();
|
|
});
|
|
|
|
// Format completion time
|
|
self.completedOn = ko.computed(function() {
|
|
return $.format.date(parseInt(self.completed()) * 1000, parent.parent.dateFormat() + ' HH:mm')
|
|
});
|
|
|
|
// Re-try button
|
|
self.retry = function() {
|
|
// Set JOB-id
|
|
$('#modal_retry_job input[name="retry_job_id"]').val(self.nzo_id)
|
|
// Open modal
|
|
$('#modal_retry_job').modal("show")
|
|
};
|
|
|
|
// Update information only on click
|
|
self.updateAllHistoryInfo = function(data, event) {
|
|
// Update all info
|
|
self.updateAllHistory = true;
|
|
parent.parent.refresh()
|
|
|
|
// Update link
|
|
setTimeout(function() {
|
|
// Update it after the update of the info! Othwerwise it gets overwritten
|
|
$(event.currentTarget).parent().find('.history-status-modallink a').click(function() {
|
|
// Info in modal
|
|
$('#history_script_log .modal-body').load($(event.currentTarget).parent().find('.history-status-modallink a').attr('href'), function(result) {
|
|
// Set title and then remove it
|
|
$('#history_script_log .modal-title').text($(this).find("h3").text())
|
|
$(this).find("h3, title").remove()
|
|
$('#history_script_log').modal({
|
|
show: true
|
|
});
|
|
});
|
|
return false;
|
|
})
|
|
}, 250)
|
|
|
|
// Try to keep open
|
|
keepOpen(event.target)
|
|
}
|
|
|
|
// Delete button
|
|
self.deleteSlot = function(item, event) {
|
|
if(!confirm(glitterTranslate.removeDow1))
|
|
return;
|
|
|
|
callAPI({
|
|
mode: 'history',
|
|
name: 'delete',
|
|
del_files: 1,
|
|
value: self.nzo_id
|
|
}).then(function(response) {
|
|
if(response.status) {
|
|
// Fade and remove
|
|
$(event.currentTarget).parent().parent().fadeOut(fadeOnDeleteDuration, function() {
|
|
self.parent.historyItems.remove(self);
|
|
self.parent.parent.refresh();
|
|
})
|
|
}
|
|
});
|
|
};
|
|
|
|
// User voting
|
|
self.setUserVote = function(item, event) {
|
|
// Send vote
|
|
callAPI({
|
|
mode: 'queue',
|
|
name: 'rating',
|
|
type: 'vote',
|
|
setting: $(event.target).val(),
|
|
value: self.nzo_id
|
|
}).then(function(response) {
|
|
// Update all info
|
|
self.updateAllHistory = true;
|
|
self.parent.parent.refresh()
|
|
})
|
|
}
|
|
|
|
// User rating
|
|
self.setUserRating = function(item, event) {
|
|
// Audio or video
|
|
var changeWhat = 'audio';
|
|
if($(event.target).attr('name') == 'ratings-video') {
|
|
changeWhat = 'video';
|
|
}
|
|
|
|
// Only on user-event, not the auto-fired ones
|
|
if(!event.originalEvent) return;
|
|
|
|
// Send vote
|
|
callAPI({
|
|
mode: 'queue',
|
|
name: 'rating',
|
|
type: changeWhat,
|
|
setting: $(event.target).val(),
|
|
value: self.nzo_id
|
|
}).then(function(response) {
|
|
// Update all info
|
|
self.updateAllHistory = true;
|
|
})
|
|
}
|
|
|
|
// User comment
|
|
self.setUserReport = function(form) {
|
|
// What are we reporting?
|
|
var userReport = $(form).find('input[name="rating_flag"]:checked').val();
|
|
var userDetail = '';
|
|
|
|
// Anything selected?
|
|
if(!userReport) {
|
|
alert(glitterTranslate.noSelect)
|
|
return;
|
|
}
|
|
|
|
// Extra info?
|
|
if(userReport == 'comment') userDetail = $(form).find('input[name="ratings-report-comment"]').val();
|
|
if(userReport == 'other') userDetail = $(form).find('input[name="ratings-report-other"]').val();
|
|
|
|
// Exception for servers
|
|
if(userReport == 'expired') {
|
|
// Which server?
|
|
userDetail = $(form).find('select[name="ratings-report-expired-server"]').val();
|
|
|
|
// All?
|
|
if(userDetail == "") {
|
|
// Loop over all servers
|
|
$.each(parent.parent.servers, function(index, server) {
|
|
// Set timeout because simultanious requests don't work (yet)
|
|
setTimeout(function() {
|
|
submitUserReport(server.name)
|
|
}, index * 1500)
|
|
})
|
|
|
|
} else {
|
|
// Just the one server
|
|
submitUserReport(userDetail)
|
|
}
|
|
} else {
|
|
submitUserReport(userDetail)
|
|
}
|
|
|
|
// After all, close it
|
|
form.reset();
|
|
$(form).parent().parent().dropdown('toggle');
|
|
alert(glitterTranslate.sendThanks)
|
|
|
|
function submitUserReport(theDetail) {
|
|
// Send note
|
|
return callAPI({
|
|
mode: 'queue',
|
|
name: 'rating',
|
|
type: 'flag',
|
|
setting: userReport,
|
|
detail: theDetail,
|
|
value: self.nzo_id
|
|
})
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Update now
|
|
self.updateFromData(data);
|
|
}
|
|
|
|
// For the file-list
|
|
function Fileslisting(parent) {
|
|
var self = this;
|
|
self.parent = parent;
|
|
self.fileItems = ko.observableArray([]);
|
|
self.modalNZBId = ko.observable();
|
|
self.modalTitle = ko.observable();
|
|
self.modalPassword = ko.observable();
|
|
|
|
// Load the function and reset everything
|
|
self.loadFiles = function(queue_item) {
|
|
// Update
|
|
self.currentItem = queue_item;
|
|
self.fileItems.removeAll()
|
|
self.triggerUpdate()
|
|
|
|
// Get pasword self.currentItem title
|
|
passwordSplit = self.currentItem.name().split(" / ")
|
|
|
|
// Has SAB already detected its encrypted? Then there will be 3x /
|
|
passwordSplitExtra = 0;
|
|
if(passwordSplit.length == 3 || passwordSplit[0] == 'ENCRYPTED') {
|
|
passwordSplitExtra = 1;
|
|
}
|
|
|
|
// Set files & title
|
|
self.modalNZBId(self.currentItem.id)
|
|
self.modalTitle(passwordSplit[0 + passwordSplitExtra])
|
|
self.modalPassword(passwordSplit[1 + passwordSplitExtra])
|
|
|
|
// Hide ok button and reset
|
|
$('#modal_item_filelist .glyphicon-floppy-saved').hide()
|
|
$('#modal_item_filelist .glyphicon-lock').show()
|
|
$('#modal_item_files input[type="checkbox"]').prop('checked', false)
|
|
|
|
// Show
|
|
$('#modal_item_files').modal('show');
|
|
|
|
// Stop updating on closing of the modal
|
|
$('#modal_item_files').on('hidden.bs.modal', function() {
|
|
self.removeUpdate();
|
|
})
|
|
}
|
|
|
|
// Trigger update
|
|
self.triggerUpdate = function() {
|
|
// Call API
|
|
callAPI({
|
|
mode: 'get_files',
|
|
value: self.currentItem.id,
|
|
limit: 5
|
|
}).then(function(response) {
|
|
// When there's no files left we close the modal and the update will be stopped
|
|
// For example when the job has finished downloading
|
|
if(response.files.length === 0) {
|
|
$('#modal_item_files').modal('hide');
|
|
return;
|
|
}
|
|
|
|
// ID's
|
|
var itemIds = $.map(self.fileItems(), function(i) {
|
|
return i.filename();
|
|
});
|
|
var newItems = [];
|
|
|
|
// Go over them all
|
|
$.each(response.files, function(index, slot) {
|
|
var existingItem = ko.utils.arrayFirst(self.fileItems(), function(i) {
|
|
return i.filename() == slot.filename;
|
|
});
|
|
|
|
if(existingItem) {
|
|
existingItem.updateFromData(slot);
|
|
itemIds.splice(itemIds.indexOf(slot.filename), 1);
|
|
} else {
|
|
// Add files item
|
|
newItems.push(new FileslistingModel(self, slot));
|
|
}
|
|
})
|
|
|
|
// Add new ones in 1 time instead of every single push
|
|
if(newItems.length > 0) {
|
|
ko.utils.arrayPushAll(self.fileItems(), newItems);
|
|
self.fileItems.valueHasMutated();
|
|
}
|
|
|
|
// Check if we show/hide completed
|
|
if(localStorage.getItem('showCompletedFiles') == 'No') {
|
|
$('.item-files-table tr:not(.files-sortable)').hide();
|
|
$('#filelist-showcompleted').removeClass('hoverbutton')
|
|
}
|
|
|
|
// Refresh with same as rest
|
|
self.setUpdate()
|
|
})
|
|
}
|
|
|
|
// Set update
|
|
self.setUpdate = function() {
|
|
self.updateTimeout = setTimeout(function() {
|
|
self.triggerUpdate()
|
|
}, parent.refreshRate() * 1000)
|
|
}
|
|
|
|
// Remove the update
|
|
self.removeUpdate = function() {
|
|
clearTimeout(self.updateTimeout)
|
|
}
|
|
|
|
// Move in sortable
|
|
self.move = function(e) {
|
|
// How much did we move?
|
|
var nrMoves = e.sourceIndex - e.targetIndex;
|
|
var direction = (nrMoves > 0 ? 'Up' : 'Down')
|
|
|
|
// We have to create the data-structure before, to be able to use the name as a key
|
|
var dataToSend = {};
|
|
dataToSend[e.item.nzf_id()] = 'on';
|
|
dataToSend['session'] = apiKey;
|
|
dataToSend['action_key'] = direction;
|
|
dataToSend['action_size'] = Math.abs(nrMoves);
|
|
|
|
// Activate with this weird URL "API"
|
|
callSpecialAPI("nzb/" + self.currentItem.id + "/bulk_operation", dataToSend)
|
|
};
|
|
|
|
// Remove selected files
|
|
self.removeSelectedFiles = function() {
|
|
// We have to create the data-structure before, to be able to use the name as a key
|
|
var dataToSend = {};
|
|
dataToSend['session'] = apiKey;
|
|
dataToSend['action_key'] = 'Delete';
|
|
|
|
// Get all selected ones
|
|
$('.item-files-table input:checked:not(:disabled)').each(function() {
|
|
// Add this item
|
|
dataToSend[$(this).prop('name')] = 'on';
|
|
})
|
|
|
|
// Activate with this weird URL "API"
|
|
callSpecialAPI("nzb/" + self.currentItem.id + "/bulk_operation", dataToSend).then(function() {
|
|
$('.item-files-table input:checked:not(:disabled)').parents('tr').fadeOut(fadeOnDeleteDuration)
|
|
})
|
|
|
|
}
|
|
|
|
// For changing the passwords
|
|
self.setNzbPassword = function() {
|
|
// Activate with this weird URL "API"
|
|
callSpecialAPI("nzb/" + self.currentItem.id + "/save", {
|
|
name: self.modalTitle(),
|
|
password: $('#nzb_password').val()
|
|
}).then(function() {
|
|
$('#modal_item_filelist .glyphicon-floppy-saved').show()
|
|
$('#modal_item_filelist .glyphicon-lock').hide()
|
|
})
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Indiviual file models
|
|
function FileslistingModel(parent, data) {
|
|
var self = this;
|
|
// Define veriables
|
|
self.filename = ko.observable();
|
|
self.nzf_id = ko.observable();
|
|
self.file_age = ko.observable();
|
|
self.mb = ko.observable();
|
|
self.percentage = ko.observable();
|
|
self.canChange = ko.computed(function() {
|
|
return self.nzf_id() != undefined;
|
|
})
|
|
|
|
// For selecting range
|
|
self.checkSelectRange = function(data, event) {
|
|
if(event.shiftKey) {
|
|
checkShiftRange('.item-files-table input:not(:disabled)')
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Update internally
|
|
self.updateFromData = function(data) {
|
|
self.filename(data.filename)
|
|
self.nzf_id(data.nzf_id)
|
|
self.file_age(data.age)
|
|
self.mb(data.mb)
|
|
self.percentage(fixPercentages((100 - (data.mbleft / data.mb * 100)).toFixed(0)));
|
|
}
|
|
|
|
// Update now
|
|
self.updateFromData(data);
|
|
}
|
|
|
|
// Model for pagination, since we use it multiple times
|
|
function paginationModel(parent) {
|
|
var self = this;
|
|
|
|
// Var's
|
|
self.nrPages = ko.observable(0);
|
|
self.currentPage = ko.observable(1);
|
|
self.currentStart = ko.observable(0);
|
|
self.allpages = ko.observableArray([]).extend({
|
|
rateLimit: 50
|
|
});
|
|
|
|
// Has pagination
|
|
self.hasPagination = ko.computed(function() {
|
|
return self.nrPages() > 1;
|
|
})
|
|
|
|
// Subscribe to number of items
|
|
parent.totalItems.subscribe(function() {
|
|
// Update
|
|
self.updatePages();
|
|
})
|
|
|
|
// Subscribe to changes of pagination limit
|
|
parent.paginationLimit.subscribe(function(newValue) {
|
|
self.updatePages();
|
|
self.moveToPage(self.currentPage());
|
|
})
|
|
|
|
// Easy handler for adding a page-link
|
|
self.addPaginationPageLink = function(pageNr) {
|
|
// Return object for adding
|
|
return {
|
|
page: pageNr,
|
|
isCurrent: pageNr == self.currentPage(),
|
|
isDots: false,
|
|
onclick: function(data) {
|
|
self.moveToPage(data.page);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Easy handler to add dots
|
|
self.addDots = function() {
|
|
return {
|
|
page: '...',
|
|
isCurrent: false,
|
|
isDots: true,
|
|
onclick: function() {}
|
|
}
|
|
}
|
|
|
|
self.updatePages = function() {
|
|
// Empty it
|
|
self.allpages.removeAll();
|
|
|
|
// How many pages do we need?
|
|
if(parent.totalItems() <= parent.paginationLimit()) {
|
|
// Empty it
|
|
self.nrPages(1)
|
|
|
|
// Reset all to make sure we see something
|
|
self.currentPage(1);
|
|
self.currentStart(0);
|
|
} else {
|
|
// Calculate number of pages needed
|
|
newNrPages = Math.ceil(parent.totalItems() / parent.paginationLimit())
|
|
|
|
// Make sure the current page still exists
|
|
if(self.currentPage() > newNrPages) {
|
|
self.moveToPage(newNrPages);
|
|
return;
|
|
}
|
|
|
|
// All the cases
|
|
if(newNrPages > 7) {
|
|
// Do we show the first ones
|
|
if(self.currentPage() < 5) {
|
|
// Just add the first 4
|
|
$.each(new Array(5), function(index) {
|
|
self.allpages.push(self.addPaginationPageLink(index + 1))
|
|
})
|
|
// Dots
|
|
self.allpages.push(self.addDots())
|
|
// Last one
|
|
self.allpages.push(self.addPaginationPageLink(newNrPages))
|
|
} else {
|
|
// Always add the first
|
|
self.allpages.push(self.addPaginationPageLink(1))
|
|
// Dots
|
|
self.allpages.push(self.addDots())
|
|
|
|
// Are we near the end?
|
|
if((newNrPages - self.currentPage()) < 4) {
|
|
// We add the last ones
|
|
$.each(new Array(5), function(index) {
|
|
self.allpages.push(self.addPaginationPageLink((index - 4) + (newNrPages)))
|
|
})
|
|
} else {
|
|
// We are in the center so display the center 3
|
|
$.each(new Array(3), function(index) {
|
|
self.allpages.push(self.addPaginationPageLink(self.currentPage() + (index - 1)))
|
|
})
|
|
|
|
// Dots
|
|
self.allpages.push(self.addDots())
|
|
// Last one
|
|
self.allpages.push(self.addPaginationPageLink(newNrPages))
|
|
}
|
|
}
|
|
} else {
|
|
// Just add them
|
|
$.each(new Array(newNrPages), function(index) {
|
|
self.allpages.push(self.addPaginationPageLink(index + 1))
|
|
})
|
|
}
|
|
|
|
// Change of number of pages?
|
|
if(newNrPages != self.nrPages()) {
|
|
// Update
|
|
self.nrPages(newNrPages);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update on click
|
|
self.moveToPage = function(page) {
|
|
// Update page and start
|
|
self.currentPage(page)
|
|
self.currentStart((page - 1) * parent.paginationLimit())
|
|
// Re-paginate
|
|
self.updatePages();
|
|
// Force full update
|
|
parent.parent.refresh();
|
|
}
|
|
}
|
|
|
|
// GO!!!
|
|
ko.applyBindings(new ViewModel(), document.getElementById("sabnzbd"));
|
|
});
|
|
|
|
/***
|
|
GENERAL FUNCTIONS
|
|
***/
|
|
// Function to fix percentages
|
|
function fixPercentages(intPercent) {
|
|
// Skip NaN's
|
|
if(isNaN(intPercent))
|
|
intPercent = 0;
|
|
return Math.floor(intPercent || 0) + '%';
|
|
}
|
|
|
|
// Function to re-write 0:09:21 to 9:21
|
|
function rewriteTime(timeString) {
|
|
var timeSplit = timeString.split(/:/);
|
|
var hours = parseInt(timeSplit[0]);
|
|
var minutes = parseInt(timeSplit[1]);
|
|
var seconds = parseInt(timeSplit[2]);
|
|
|
|
// Fix seconds
|
|
if(seconds < 10) seconds = "0" + seconds;
|
|
|
|
// With or without leading 0?
|
|
if(hours == 0) {
|
|
// Output
|
|
return minutes + ":" + seconds
|
|
}
|
|
|
|
// Fix minutes if more than 1 hour
|
|
if(minutes < 10) minutes = "0" + minutes;
|
|
|
|
// Regular
|
|
return hours + ':' + minutes + ':' + seconds;
|
|
}
|
|
|
|
// Keep dropdowns open
|
|
function keepOpen(thisItem) {
|
|
// Onlick so it works for the dynamic items!
|
|
$(thisItem).siblings('.dropdown-menu').children().click(function(e) {
|
|
// Not for links
|
|
if(!$(e.target).is('a')) {
|
|
e.stopPropagation();
|
|
}
|
|
});
|
|
// Add possible tooltips
|
|
if(!isMobile) $(thisItem).siblings('.dropdown-menu').children('[data-toggle="tooltip"]').tooltip({ trigger: 'hover', container: 'body' })
|
|
}
|
|
|
|
// Check all functionality
|
|
function checkAllFiles(objCheck) {
|
|
// Check for main-page or file-list modal?
|
|
if($(objCheck).prop('name') == 'multieditCheckAll') {
|
|
// Is checked himself?
|
|
if($(objCheck).prop('checked')) {
|
|
// (Un)check all in Queue by simulating click, this also fires the knockout-trigger!
|
|
$('.queue-table input[name="multiedit"]').trigger("click")
|
|
} else {
|
|
// Uncheck all checked ones and fires event
|
|
$('.queue-table input[name="multiedit"]:checked').trigger("click")
|
|
}
|
|
|
|
} else {
|
|
// (Un)check all in file-list
|
|
$('#modal_item_files input:checkbox:not(:disabled)').prop('checked', $(objCheck).prop('checked'))
|
|
}
|
|
}
|
|
|
|
// SHift-range functionality for checkboxes
|
|
function checkShiftRange(strCheckboxes) {
|
|
// Get them all
|
|
var arrAllChecks = $(strCheckboxes);
|
|
// Get index of the first and last
|
|
var startCheck = arrAllChecks.index($(strCheckboxes + ':checked:first'));
|
|
var endCheck = arrAllChecks.index($(strCheckboxes + ':checked:last'));
|
|
// Everything in between click it to trigger addMultiEdit
|
|
arrAllChecks.slice(startCheck, endCheck).filter(':not(:checked)').trigger('click')
|
|
}
|
|
|
|
// Hide completed files in files-modal
|
|
function hideCompletedFiles() {
|
|
if($('#filelist-showcompleted').hasClass('hoverbutton')) {
|
|
// Hide all
|
|
$('.item-files-table tr:not(.files-sortable)').hide();
|
|
$('#filelist-showcompleted').removeClass('hoverbutton')
|
|
// Set storage
|
|
localStorage.setItem('showCompletedFiles', 'No')
|
|
} else {
|
|
// show all
|
|
$('.item-files-table tr:not(.files-sortable)').show();
|
|
$('#filelist-showcompleted').addClass('hoverbutton')
|
|
// Set storage
|
|
localStorage.setItem('showCompletedFiles', 'Yes')
|
|
}
|
|
}
|