/** 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 status varibales self.isRestarting = ko.observable(false); self.useGlobalOptions = ko.observable(true).extend({ persist: 'useGlobalOptions' }); self.refreshRate = ko.observable(1).extend({ persist: 'pageRefreshRate' }); self.dateFormat = ko.observable('fromNow').extend({ persist: 'pageDateFormat' }); self.displayTabbed = ko.observable().extend({ persist: 'displayTabbed' }); self.displayCompact = ko.observable(false).extend({ persist: 'displayCompact' }); self.displayFullWidth = ko.observable(false).extend({ persist: 'displayFullWidth' }); self.confirmDeleteQueue = ko.observable(true).extend({ persist: 'confirmDeleteQueue' }); self.confirmDeleteHistory = ko.observable(true).extend({ persist: 'confirmDeleteHistory' }); self.extraQueueColumns = ko.observableArray([]).extend({ persist: 'extraColumns' }); self.extraHistoryColumns = ko.observableArray([]).extend({ persist: 'extraHistoryColumns' }); self.showActiveConnections = ko.observable(false).extend({ persist: 'showActiveConnections' }); self.speedMetrics = { K: "KB/s", M: "MB/s", G: "GB/s" }; // Set information varibales self.title = ko.observable(); self.speed = ko.observable(0); self.speedMetric = ko.observable(); self.bandwithLimit = ko.observable(false); self.pauseCustom = ko.observable('').extend({ rateLimit: { timeout: 200, method: "notifyWhenChangesStop" } }); self.speedLimit = ko.observable(100).extend({ rateLimit: { timeout: 200, 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.diskSpaceExceeded1 = ko.observable(false); self.diskSpaceExceeded2 = ko.observable(false); self.quotaLimit = ko.observable(); self.quotaLimitLeft = ko.observable(); self.systemLoad = ko.observable(); self.cacheSize = ko.observable(); self.cacheArticles = ko.observable(); self.nrWarnings = ko.observable(0); self.allWarnings = ko.observableArray([]); self.allMessages = ko.observableArray([]); self.onQueueFinish = ko.observable(''); self.speedHistory = []; // Statusinfo container self.hasStatusInfo = ko.observable(false); self.hasPerformanceInfo = ko.observable(false); self.statusInfo = {}; self.statusInfo.folders = ko.observableArray([]); self.statusInfo.servers = ko.observableArray([]); self.statusInfo.localipv4 = ko.observable(); self.statusInfo.publicipv4 = ko.observable(); self.statusInfo.ipv6 = ko.observable(); self.statusInfo.dnslookup = ko.observable(); self.statusInfo.pystone = ko.observable(); self.statusInfo.cpumodel = ko.observable(); self.statusInfo.loglevel = ko.observable(); self.statusInfo.downloaddir = ko.observable(); self.statusInfo.downloaddirspeed = ko.observable(); self.statusInfo.completedir = ko.observable(); self.statusInfo.completedirspeed = ko.observable(); self.statusInfo.internetbandwidth = ko.observable(); /*** Dynamic functions ***/ // Make the speedlimit tekst self.speedLimitText = ko.pureComputed(function() { // Set? if(!self.bandwithLimit()) return; // The text var bandwithLimitText = self.bandwithLimit().replace(/[^a-zA-Z]+/g, ''); // Only the number var speedLimitNumberFull = (parseFloat(self.bandwithLimit()) * (self.speedLimit() / 100)); // Trick to only get decimal-point when needed var speedLimitNumber = Math.round(speedLimitNumberFull*10)/10; // Fix it for lower than 1MB/s if(bandwithLimitText == 'M' && speedLimitNumber < 1) { bandwithLimitText = 'K'; speedLimitNumber = Math.round(speedLimitNumberFull * 1024); } // Show text return self.speedLimit() + '% (' + speedLimitNumber + ' ' + self.speedMetrics[bandwithLimitText] + ')'; }); // Dynamic speed text function self.speedText = ko.pureComputed(function() { return self.speed() + ' ' + (self.speedMetrics[self.speedMetric()] ? self.speedMetrics[self.speedMetric()] : "B/s"); }); // Dynamic icon self.SABIcon = ko.pureComputed(function() { if(self.downloadsPaused()) { return './staticcfg/ico/faviconpaused.ico?v=1.1.0'; } else { return './staticcfg/ico/favicon.ico?v=1.1.0'; } }) // Dynamic queue length check self.hasQueue = ko.pureComputed(function() { return(self.queue.queueItems().length > 0 || self.queue.searchTerm() || self.queue.isLoading()) }) // Dynamic history length check self.hasHistory = ko.pureComputed(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.history.isLoading()) }) self.hasWarnings = ko.pureComputed(function() { return(self.allWarnings().length > 0) }) // Check for any warnings/messages self.hasMessages = ko.pureComputed(function() { return parseInt(self.nrWarnings()) + self.allMessages().length; }) // Update main queue self.updateQueue = function(response) { // Block in case off dragging 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; } /*** Possible login failure? ***/ if(response.hasOwnProperty('error') && response.error == 'Missing authentication') { // Restart document.location = document.location; } /*** Basic information ***/ // Queue left self.queueDataLeft(response.queue.mbleft > 0 ? response.queue.sizeleft : '') // Paused? self.downloadsPaused(response.queue.paused); // Finish action. Replace null with empty self.onQueueFinish(response.queue.finishaction ? response.queue.finishaction : ''); // Disk sizes self.diskSpaceLeft1(response.queue.diskspace1_norm) // Same sizes? Then it's all 1 disk! if(response.queue.diskspace1 != response.queue.diskspace2) { self.diskSpaceLeft2(response.queue.diskspace2_norm) } else { self.diskSpaceLeft2('') } // Did we exceed the space? self.diskSpaceExceeded1(parseInt(response.queue.mbleft)/1024 > parseFloat(response.queue.diskspace1)) self.diskSpaceExceeded2(parseInt(response.queue.mbleft)/1024 > parseFloat(response.queue.diskspace2)) // Quota self.quotaLimit(response.queue.quota) self.quotaLimitLeft(response.queue.left_quota) // System load self.systemLoad(response.queue.loadavg) // Cache self.cacheSize(response.queue.cache_size) self.cacheArticles(response.queue.cache_art) // 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 var 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.0 : parseFloat(response.queue.speedlimit).toFixed(1); // Trick to only get decimal-point when needed response.queue.speedlimit = Math.round(response.queue.speedlimit*10)/10; 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 ***/ var 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 { var pauseSplit = response.queue.pause_int.split(/:/); var seconds = parseInt(pauseSplit[0]) * 60 + parseInt(pauseSplit[1]); var hours = Math.floor(seconds / 3600); var minutes = Math.floor((seconds -= hours * 3600) / 60); seconds -= minutes * 60; // Add leading zeros if(minutes < 10) minutes = '0' + minutes; if(seconds < 10) seconds = '0' + seconds; // Final formating 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); } // Set new update timer self.setNextUpdate = function() { self.interval = setTimeout(self.refresh, parseInt(self.refreshRate()) * 1000); } // Refresh function self.refresh = function(forceFullHistory) { // Clear previous timeout to prevent double-calls clearTimeout(self.interval); /** Limited refresh **/ // Only update the title when page not visible if(!pageIsVisible) { // Request new title callSpecialAPI('./queue/', { limit: 1, start: 0 }).done(function(data) { // Split title & speed var dataSplit = data.split('|||'); // Maybe the result is actually the login page? if(dataSplit[0].substring(0, 11) === ' -1) // Force the next full update to be full self.history.lastUpdate = 0 }).always(self.setNextUpdate) // Do not continue! return; } /** Full refresh **/ // Do requests for full information // Catch the fail to display message var queueApi = callAPI({ mode: "queue", search: self.queue.searchTerm(), start: self.queue.pagination.currentStart(), limit: parseInt(self.queue.paginationLimit()) }) .done(self.updateQueue) .fail(function(response) { // Catch the failure of authorization error if(response.status == 401) { // Stop refresh and reload clearInterval(self.interval) location.reload(); } // Show screen self.isRestarting(1) }).always(self.setNextUpdate); // Force full history update? if(forceFullHistory) { self.history.lastUpdate = 0 } // History callAPI({ mode: "history", search: self.history.searchTerm(), failed_only: self.history.showFailed()*1, start: self.history.pagination.currentStart(), limit: parseInt(self.history.paginationLimit()), last_history_update: self.history.lastUpdate }).done(self.updateHistory); // We are now done with any loading // But we wait a few ms so Knockout has time to update setTimeout(function() { self.queue.isLoading(false); self.history.isLoading(false); }, 100) // 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(item, event) { callAPI({ mode: 'config', name: 'set_pause', value: $(event.currentTarget).data('time') }).then(self.refresh); self.downloadsPaused(true); }; // Open modal self.openCustomPauseTime = function() { // Was it loaded already? if(!Date.i18n) { jQuery.getScript('./static/javascripts/date.min.js').then(function() { // After loading we start again self.openCustomPauseTime() }) return; } // Show modal $('#modal_custom_pause').modal('show') // Focus on the input field $('#modal_custom_pause').on('shown.bs.modal', function () { $('#customPauseInput').focus() }) // Reset on modal close $('#modal_custom_pause').on('hide.bs.modal', function () { self.pauseCustom(''); }) } // Update on changes self.pauseCustom.subscribe(function(newValue) { // Is it plain numbers? if(newValue.match(/^\s*\d+\s*$/)) { // Treat it as a number of minutes newValue += "minutes"; } // At least 3 charaters if(newValue.length < 3) { $('#customPauseOutput').text('').data('time', 0) $('#modal_custom_pause .btn-default').addClass('disabled') return; } // Fix DateJS bug it has some strange problem with the current day-of-month + 1 // Removing the space makes DateJS work properly newValue = newValue.replace(/\s*h|\s*m|\s*d/g, function(match) { return match.trim() }); // Parse var pauseParsed = Date.parse(newValue); // 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('+' + newValue); } // Calculate difference in minutes and save var pauseDuration = Math.round((pauseParsed - Date.parse('now'))/1000/60); $('#customPauseOutput').html(' ' +glitterTranslate.pauseFor + ' ' + pauseDuration + ' ' + glitterTranslate.minutes) $('#customPauseOutput').data('time', pauseDuration) $('#modal_custom_pause .btn-default').removeClass('disabled') } else if(newValue) { // No.. $('#customPauseOutput').text(glitterTranslate.pausePromptFail) $('#modal_custom_pause .btn-default').addClass('disabled') } }) // Save custom pause self.saveCustomPause = function() { // Get duration var pauseDuration = $('#customPauseOutput').data('time'); // If in the future if(pauseDuration > 0) { callAPI({ mode: 'config', name: 'set_pause', value: pauseDuration }).then(function() { // Refresh and close the modal self.refresh() self.downloadsPaused(true); $('#modal_custom_pause').modal('hide') }); } } // 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) { // Reformat CSS label and date // Replaces spaces by non-breakable spaces and newlines with br's var warningData = { index: index, type: glitterTranslate.status[warning.type].slice(0, -1), text: convertHTMLtoText(warning.text).replace(/ /g, '\u00A0').replace(/(?:\r\n|\r|\n)/g, '
'), timestamp: warning.time, css: (warning.type == "ERROR" ? "danger" : warning.type == "WARNING" ? "warning" : "info"), clear: self.clearWarnings }; self.allWarnings.push(warningData) }) } }); }) // Clear warnings self.clearWarnings = function() { callAPI({ mode: "status", name: "clear" }).done(self.refresh) } // 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 localStorageSetItem(whatToRemove, Date.now()) } // 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() { // Send call to override speedlimit callAPI({ mode: "config", name: "speedlimit", value: 100 }) self.speedLimitInt(100.0) self.speedLimit(100.0) } // Shutdown options self.setOnQueueFinish = function(model, event) { // Ignore updates before the page is done if(!self.hasStatusInfo()) return; // Something changes callAPI({ mode: 'queue', name: 'change_complete_action', value: $(event.target).val() }) // Top stop blinking while the API is calling self.onQueueFinish($(event.target).val()) } // Use global settings or device-specific? self.useGlobalOptions.subscribe(function(newValue) { // Reload in case of enabling global options if(newValue) document.location = document.location; }) // Update refreshrate self.refreshRate.subscribe(function(newValue) { // Refresh now self.refresh(); // Save in config if global-settings if(self.useGlobalOptions()) { callAPI({ mode: "set_config", section: "misc", keyword: "refresh_rate", value: newValue }) } }) // Save the rest in config if global-settings var saveInterfaceSettings = function(newValue) { if(self.useGlobalOptions()) { var interfaceSettings = { "dateFormat": self.dateFormat, "extraQueueColumns": self.extraQueueColumns, "extraHistoryColumns": self.extraHistoryColumns, "displayCompact": self.displayCompact, "displayFullWidth": self.displayFullWidth, "displayTabbed": self.displayTabbed, "confirmDeleteQueue": self.confirmDeleteQueue, "confirmDeleteHistory": self.confirmDeleteHistory }; callAPI({ mode: "set_config", section: "misc", keyword: "interface_settings", value: ko.toJSON(interfaceSettings) }) } } self.dateFormat.subscribe(saveInterfaceSettings); self.extraQueueColumns.subscribe(saveInterfaceSettings); self.extraHistoryColumns.subscribe(saveInterfaceSettings); self.displayCompact.subscribe(saveInterfaceSettings); self.displayFullWidth.subscribe(saveInterfaceSettings); self.displayTabbed.subscribe(saveInterfaceSettings); self.confirmDeleteQueue.subscribe(saveInterfaceSettings); self.confirmDeleteHistory.subscribe(saveInterfaceSettings); /*** 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) } // Add NZB form self.addNZB = function(form) { // Anything? if(!$(form.nzbFile)[0].files[0] && !$(form.nzbURL).val()) { $('.btn-file, input[name="nzbURL"]').attr('style', 'border-color: red !important') setTimeout(function() { $('.btn-file, input[name="nzbURL"]').css('border-color', '') }, 2000) return false; } // Upload file using the method we also use for drag-and-drop if($(form.nzbFile)[0].files[0]) { self.addNZBFromFile($(form.nzbFile)[0].files); // Hide modal, upload will reset the form $("#modal-add-nzb").modal("hide"); } else if($(form.nzbURL).val()) { // Or add URL var theCall = { mode: "addurl", name: $(form.nzbURL).val(), nzbname: $('#nzbname').val(), password: $('#password').val(), script: $('#modal-add-nzb select[name="Post-processing"]').val(), priority: $('#modal-add-nzb select[name="Priority"]').val(), pp: $('#modal-add-nzb select[name="Processing"]').val() } // Optional, otherwise they get mis-labeled if left empty if($('#modal-add-nzb select[name="Category"]').val() != '*') theCall.cat = $('#modal-add-nzb select[name="Category"]').val() if($('#modal-add-nzb select[name="Processing"]').val()) theCall.pp = $('#modal-add-nzb select[name="Category"]').val() // Add callAPI(theCall).then(function(r) { // Hide and reset/refresh self.refresh() $("#modal-add-nzb").modal("hide"); form.reset() $('#nzbname').val('') }); } } // From the upload or filedrop self.addNZBFromFile = function(files, fileindex) { // First file if(fileindex === undefined) { fileindex = 0 } var file = files[fileindex] fileindex++ // Check if it's maybe a folder, we can't handle those if(!file.type && file.size % 4096 == 0) return; // Add notification showNotification('.main-notification-box-uploading', 0, fileindex) // 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("password", $('#password').val()); data.append("script", $('#modal-add-nzb select[name="Post-processing"]').val()) data.append("priority", $('#modal-add-nzb select[name="Priority"]').val()) data.append("apikey", apiKey); // Optional, otherwise they get mis-labeled if left empty if($('#modal-add-nzb select[name="Category"]').val() != '*') data.append("cat", $('#modal-add-nzb select[name="Category"]').val()); if($('#modal-add-nzb select[name="Processing"]').val()) data.append("pp", $('#modal-add-nzb select[name="Processing"]').val()); // Add this one $.ajax({ url: "./api", type: "POST", cache: false, processData: false, contentType: false, data: data }).then(function(r) { // Are we done? if(fileindex < files.length) { // Do the next one self.addNZBFromFile(files, fileindex) } else { // Refresh self.refresh(); // Hide notification hideNotification(true) // Reset the form $('#modal-add-nzb form').trigger('reset'); $('#nzbname').val('') $('.btn-file em').html(glitterTranslate.chooseFile + '…') } }); } // Load status info self.loadStatusInfo = function(item, event) { // Full refresh? Only on click and for the status-screen var statusFullRefresh = (event != undefined) && $('#options-status').hasClass('active'); // Make it spin if the user requested it otherwise we don't, // because browsers use a lot of CPU for the animation if(statusFullRefresh) { self.hasStatusInfo(false) } // Load the custom status info callAPI({ mode: 'fullstatus', skip_dashboard: (!statusFullRefresh)*1 }).then(function(data) { // Update basic self.statusInfo.loglevel(data.status.loglevel) self.statusInfo.folders(data.status.folders) // Update the full set if(statusFullRefresh) { self.statusInfo.pystone(data.status.pystone) self.statusInfo.cpumodel(data.status.cpumodel) self.statusInfo.downloaddir(data.status.downloaddir) self.statusInfo.downloaddirspeed(data.status.downloaddirspeed) self.statusInfo.completedir(data.status.completedir) self.statusInfo.completedirspeed(data.status.completedirspeed) self.statusInfo.internetbandwidth(data.status.internetbandwidth) self.statusInfo.dnslookup(data.status.dnslookup) self.statusInfo.localipv4(data.status.localipv4) self.statusInfo.publicipv4(data.status.publicipv4) self.statusInfo.ipv6(data.status.ipv6 || glitterTranslate.noneText) // Loaded disk info self.hasPerformanceInfo(true) } // Update the servers if(self.statusInfo.servers().length != data.status.servers.length) { // Only now we can subscribe to the log-level-changes! (only at start) if(self.statusInfo.servers().length == 0) { self.statusInfo.loglevel.subscribe(function(newValue) { // Update log-level callSpecialAPI('./status/change_loglevel/', { loglevel: newValue }); }) } // Empty them, in case of update self.statusInfo.servers([]) // Initial add $.each(data.status.servers, function() { self.statusInfo.servers.push({ 'servername': ko.observable(this.servername), 'serveroptional': ko.observable(this.serveroptional), 'serverpriority': ko.observable(this.serverpriority), 'servertotalconn': ko.observable(this.servertotalconn), 'serverssl': ko.observable(this.serverssl), 'serversslinfo': ko.observable(this.serversslinfo), 'serveractiveconn': ko.observable(this.serveractiveconn), 'servererror': ko.observable(this.servererror), 'serveractive': ko.observable(this.serveractive), 'serverconnections': ko.observableArray(this.serverconnections), 'serverbps': ko.observable(this.serverbps) }) }) } else { // Update $.each(data.status.servers, function(index) { var activeServer = self.statusInfo.servers()[index]; activeServer.servername(this.servername), activeServer.serveroptional(this.serveroptional), activeServer.serverpriority(this.serverpriority), activeServer.servertotalconn(this.servertotalconn), activeServer.serverssl(this.serverssl), activeServer.serversslinfo(this.serversslinfo), activeServer.serveractiveconn(this.serveractiveconn), activeServer.servererror(this.servererror), activeServer.serveractive(this.serveractive), activeServer.serverconnections(this.serverconnections), activeServer.serverbps(this.serverbps) }) } // Add tooltips to possible new items if(!isMobile) $('#modal-options [data-tooltip="true"]').tooltip({ trigger: 'hover', container: 'body' }) // Stop it spin self.hasStatusInfo(true) }); } // Do a disk-speedtest self.testDiskSpeed = function(item, event) { self.hasPerformanceInfo(false) // Run it and then display it callSpecialAPI('./status/dashrefresh/').then(function() { self.loadStatusInfo(true, true) }) } // Download a test-NZB self.testDownload = function(data, event) { var nzbSize = $(event.target).data('size') // Maybe it was a click on the icon? if(nzbSize == undefined) { nzbSize = $(event.target.parentElement).data('size') } // Build request var theCall = { mode: "addurl", name: "https://sabnzbd.org/tests/test_download_" + nzbSize + ".nzb", priority: self.queue.priorityName["Force"] } // Add callAPI(theCall).then(function(r) { // Hide and reset/refresh self.refresh() $("#modal-options").modal("hide"); }); } // Unblock server self.unblockServer = function(servername) { callSpecialAPI("./status/unblock_server/", { server: servername }).then(function() { $("#modal-options").modal("hide"); }) } // Refresh connections page var connectionRefresh $('.nav-tabs a[href="#options_connections"]').on('shown.bs.tab', function() { // Check size on open checkSize() // Set the interval connectionRefresh = setInterval(function() { // Start small checkSize() // Check if still visible if(!$('#options_connections').is(':visible') && connectionRefresh) { // Stop refreshing clearInterval(connectionRefresh) return } // Update the server stats (speed/connections) self.loadStatusInfo() }, self.refreshRate() * 1000) }) // On close of the tab $('.nav-tabs a[href="#options_connections"]').on('hidden.bs.tab', function() { checkSize() }) // Function that handles the actual sizing of connections tab function checkSize() { // Any connections? if(self.showActiveConnections() && $('#options_connections').is(':visible') && $('.table-server-connections').height() > 1) { var mainWidth = $('.main-content').width() $('#modal-options .modal-dialog').width(mainWidth*0.85 > 650 ? mainWidth*0.85 : '') } else { // Small again $('#modal-options .modal-dialog').width('') } } // Make sure Connections get refreshed also after open->close->open $('#modal-options').on('show.bs.modal', function () { // Trigger $('.nav-tabs a[href="#options_connections"]').trigger('shown.bs.tab') }) // Orphaned folder processing self.folderProcess = function(folder, htmlElement) { // Hide tooltips (otherwise they stay forever..) $('#options-orphans [data-tooltip="true"]').tooltip('hide') // Show notification on delete if($(htmlElement.currentTarget).data('action') == 'delete') { showNotification('.main-notification-box-removing', 1000) } else { // Adding back to queue showNotification('.main-notification-box-sendback', 2000) } // Activate callSpecialAPI("./status/" + $(htmlElement.currentTarget).data('action'), { name: $("
").html(folder).text() }).then(function() { // Refresh self.loadStatusInfo(true, true) // Hide notification hideNotification(true) }) } // Orphaned folder deletion of all self.removeAllOrphaned = function() { if(!self.confirmDeleteHistory() || confirm(glitterTranslate.clearWarn)) { // Show notification showNotification('.main-notification-box-removing-multiple', 0, self.statusInfo.folders().length) // Delete them all callSpecialAPI("./status/delete_all/").then(function() { // Remove notifcation and update screen hideNotification(true) self.loadStatusInfo(true, true) }) } } // Orphaned folder adding of all self.addAllOrphaned = function() { if(!self.confirmDeleteHistory() || confirm(glitterTranslate.clearWarn)) { // Show notification showNotification('.main-notification-box-sendback') // Delete them all callSpecialAPI("./status/add_all/").then(function() { // Remove notifcation and update screen hideNotification(true) self.loadStatusInfo(true, true) }) } } // Toggle Glitter's compact layout dynamically self.displayCompact.subscribe(function() { $('body').toggleClass('container-compact') }) // Toggle full width self.displayFullWidth.subscribe(function() { $('body').toggleClass('container-full-width') }) // Toggle Glitter's tabbed modus self.displayTabbed.subscribe(function() { $('body').toggleClass('container-tabbed') }) /** SABnzb options **/ // Shutdown self.shutdownSAB = function() { if(confirm(glitterTranslate.shutdown)) { // Show notification and return true to follow the URL showNotification('.main-notification-box-shutdown') return true } } // Restart self.restartSAB = function() { if(!confirm(glitterTranslate.restart)) return; // Call restart function callAPI({ mode: "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) { // Event var theAction = $(event.target).data('mode'); // Show notification if available if(['rss_now', 'watched_now'].indexOf(theAction) > -1) { showNotification('.main-notification-box-' + theAction, 2000) } // Send to the API callAPI({ mode: theAction }) } // Repair queue self.repairQueue = function() { if(!confirm(glitterTranslate.repair)) return; // Hide the modal and show the notifucation $("#modal-options").modal("hide"); showNotification('.main-notification-box-queue-repair') // Call the API callAPI({ mode: "restart_repair" }).then(function() { $("#modal-options").modal("hide"); }) } // Force disconnect self.forceDisconnect = function() { // Show notification showNotification('.main-notification-box-disconnect', 3000) // Call API callAPI({ mode: "disconnect" }).then(function() { $("#modal-options").modal("hide"); }) } /*** Retrieve config information and do startup functions ***/ // Force compact mode as fast as possible if(localStorageGetItem('displayCompact') === 'true') { // Add extra class $('body').addClass('container-compact') } if(localStorageGetItem('displayFullWidth') === 'true') { // Add extra class $('body').addClass('container-full-width') } // Tabbed layout? if(localStorageGetItem('displayTabbed') === 'true') { $('body').addClass('container-tabbed') } // 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()) // Import the rest of the settings if(response.config.misc.interface_settings) { var interfaceSettings = JSON.parse(response.config.misc.interface_settings); self.dateFormat(interfaceSettings['dateFormat']); self.extraQueueColumns(interfaceSettings['extraQueueColumns']); self.extraHistoryColumns(interfaceSettings['extraHistoryColumns']); self.displayCompact(interfaceSettings['displayCompact']); self.displayFullWidth(interfaceSettings['displayFullWidth']); self.displayTabbed(interfaceSettings['displayTabbed']); self.confirmDeleteQueue(interfaceSettings['confirmDeleteQueue']); self.confirmDeleteHistory(interfaceSettings['confirmDeleteHistory']); } } // 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; // Update message if(newRelease) { self.allMessages.push({ index: 'UpdateMsg', type: glitterTranslate.status['INFO'], text: (''+glitterTranslate.updateAvailable+' '+newRelease+' '), css: 'info' }); } // Message about cache - Not for 5 days if user ignored it if(!response.config.misc.cache_limit && localStorageGetItem('CacheMsg')*1+(1000*3600*24*5) < Date.now()) { self.allMessages.push({ index: 'CacheMsg', type: glitterTranslate.status['INFO'], text: (''+glitterTranslate.useCache.replace(/
/g, " ")+'
'), css: 'info', clear: function() { self.clearMessages('CacheMsg')} }); } // Message about tips and tricks, only once if(response.config.misc.notified_new_skin < 2) { self.allMessages.push({ index: 'TipsMsgV110', type: glitterTranslate.status['INFO'], text: glitterTranslate.glitterTips + ' Glitter Tips and Tricks ', css: 'info', clear: function() { // Update the config to not show again callAPI({ mode: 'set_config', section: 'misc', keyword: 'notified_new_skin', value: 2 }) // Remove the actual message self.clearMessages('TipsMsgV110') } }); } }) // Orphaned folder check - Not for 5 days if user ignored it var orphanMsg = localStorageGetItem('OrphanedMsg')*1+(1000*3600*24*5) < Date.now(); // Delay the check if(orphanMsg) { setTimeout(self.loadStatusInfo, 200); } // On any status load we check Orphaned folders self.hasStatusInfo.subscribe(function(finishedLoading) { // Loaded or just starting? if(!finishedLoading) return; // Orphaned folders? If user clicked away we check again in 5 days if(self.statusInfo.folders().length >= 3 && orphanMsg) { // Check if not already there if(!ko.utils.arrayFirst(self.allMessages(), function(item) { return item.index == 'OrphanedMsg' })) { self.allMessages.push({ index: 'OrphanedMsg', type: glitterTranslate.status['INFO'], text: glitterTranslate.orphanedJobsMsg + ' ', css: 'info', clear: function() { self.clearMessages('OrphanedMsg')} }); } } else { // Remove any message, if it was there self.allMessages.remove(function(item) { return item.index == 'OrphanedMsg'; }) } }) // Message about localStorage not being enabled every 20 days if(!hasLocalStorage && localStorageGetItem('LocalStorageMsg')*1+(1000*3600*24*20) < Date.now()) { self.allMessages.push({ index: 'LocalStorageMsg', type: glitterTranslate.status['WARNING'].replace(':', ''), text: glitterTranslate.noLocalStorage, css: 'warning', clear: function() { self.clearMessages('LocalStorageMsg')} }); } /*** Date-stuff ***/ moment.locale(displayLang); // Fill the basic info for date-formats with current date-time $('[name="general-date-format"] option').each(function() { $(this).text(displayDateTime('', $(this).val()), '') }) // Update the date every minute setInterval(function() { $('[data-timestamp]').each(function() { $(this).text(displayDateTime($(this).data('timestamp'), self.dateFormat(), 'X')) }) }, 60*1000) /*** End of main functions, start of the fun! ***/ // Trigger first refresh self.interval = setTimeout(self.refresh, parseInt(self.refreshRate()) * 1000); // And refresh now! self.refresh() // Activate tooltips if(!isMobile) $('[data-tooltip="true"]').tooltip({ trigger: 'hover', container: 'body' }) }