├── icons.woff ├── img ├── error80.png ├── icon128.png ├── icon16.png ├── icon19.png ├── icon38.png ├── icon48.png ├── refresh24.png ├── square80.png ├── nzbget-arrow.svg └── nzbget-icon.svg ├── screens ├── screen1.png └── screen2.png ├── sites ├── newzleech.css ├── spotweb.css ├── newzleech.js ├── common.css ├── nzbindex.js ├── nzbclub.js ├── spotweb.js ├── feedly.js ├── binsearch.js ├── newsnab.js ├── freshrss.js ├── ttrss.js ├── common.js ├── dognzb.js └── tests │ └── binsearch.html ├── elements ├── ng-element.js ├── ng-checkbox.css ├── ng-checkbox.js ├── ng-download-item.css └── ng-download-item.js ├── js ├── framework.js ├── options.js ├── utilities.js ├── popup.js └── nzbget.js ├── manifest.json ├── popup.html ├── options.html ├── README.md ├── options.css └── popup.css /icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frame/nzbget-chrome/master/icons.woff -------------------------------------------------------------------------------- /img/error80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frame/nzbget-chrome/master/img/error80.png -------------------------------------------------------------------------------- /img/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frame/nzbget-chrome/master/img/icon128.png -------------------------------------------------------------------------------- /img/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frame/nzbget-chrome/master/img/icon16.png -------------------------------------------------------------------------------- /img/icon19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frame/nzbget-chrome/master/img/icon19.png -------------------------------------------------------------------------------- /img/icon38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frame/nzbget-chrome/master/img/icon38.png -------------------------------------------------------------------------------- /img/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frame/nzbget-chrome/master/img/icon48.png -------------------------------------------------------------------------------- /img/refresh24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frame/nzbget-chrome/master/img/refresh24.png -------------------------------------------------------------------------------- /img/square80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frame/nzbget-chrome/master/img/square80.png -------------------------------------------------------------------------------- /screens/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frame/nzbget-chrome/master/screens/screen1.png -------------------------------------------------------------------------------- /screens/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frame/nzbget-chrome/master/screens/screen2.png -------------------------------------------------------------------------------- /sites/newzleech.css: -------------------------------------------------------------------------------- 1 | table.contentt td.get { 2 | width: 36px; 3 | } 4 | .get img.nzbgc_download { 5 | padding-right: 6px; 6 | } -------------------------------------------------------------------------------- /sites/spotweb.css: -------------------------------------------------------------------------------- 1 | table.spots a.nzb { 2 | display: inline; 3 | } 4 | 5 | div.spots table.spots td.nzb, 6 | div.spots table.spots th.nzb { 7 | width: 45px; 8 | } 9 | 10 | .nzbgc_download { 11 | vertical-align: middle; 12 | } 13 | -------------------------------------------------------------------------------- /img/nzbget-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /sites/newzleech.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Inject nzbgc-markup to newzleech.com 3 | */ 4 | (function() { 5 | 'use strict'; 6 | var dllinks = document.querySelectorAll('a[href*="dl=1"]'); 7 | 8 | for(var i = 0; i < dllinks.length; i++) { 9 | var dlitem = dllinks.item(i), 10 | lid = ''; 11 | 12 | // Skip if already processed 13 | if(dlitem.nzbGetProcessed) { 14 | continue; 15 | } 16 | dlitem.nzbGetProcessed = true; 17 | 18 | lid = 'ngi' + dlitem.href.match(/post=([0-9]+)/)[1]; 19 | 20 | var newitem = createNgIcon( 21 | lid + '_nzbgc', 22 | dlitem.href 23 | ); 24 | 25 | dlitem.parentElement.insertBefore(newitem, dlitem); 26 | } 27 | })(); 28 | -------------------------------------------------------------------------------- /img/nzbget-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /elements/ng-element.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class NgElement { 4 | constructor(ob) { 5 | this._elements = { 6 | root: ob 7 | }; 8 | } 9 | 10 | $element(tag, id, children, params) { 11 | if(!id) { 12 | id = tag; 13 | } 14 | 15 | if(!Array.isArray(children)) { 16 | params = children; 17 | children = []; 18 | } 19 | this._elements[id] = document.createElement(tag); 20 | this._elements[id].className = params && params.class || id; 21 | for(var child in children) { 22 | this._elements[id].appendChild(children[child]); 23 | } 24 | if(params && params.text) { 25 | this._elements[id].appendChild( 26 | document.createTextNode(params.text)); 27 | } 28 | return this._elements[id]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sites/common.css: -------------------------------------------------------------------------------- 1 | @keyframes spin { 2 | from { 3 | transform: rotate(0deg); 4 | } 5 | to { 6 | transform: rotate(360deg); 7 | } 8 | } 9 | 10 | .nzbgc_download { 11 | filter: grayscale(0.8); 12 | transition: all 250ms ease-in-out; 13 | cursor: pointer; 14 | display: inline-block; 15 | } 16 | 17 | .nzbgc_download img { 18 | width: 16px; 19 | } 20 | 21 | .nzbgc_download.nzbgc_added_ok{ 22 | filter: grayscale(0); 23 | } 24 | 25 | .nzbgc_download.nzbgc_added_failed{ 26 | opacity:0.5; 27 | } 28 | 29 | .nzbgc_download.nzbgc_adding{ 30 | animation-name: spin; 31 | animation-duration: 500ms; 32 | animation-iteration-count: infinite; 33 | animation-timing-function: linear; 34 | } 35 | 36 | .nzbgc_download:hover { 37 | filter: grayscale(0.5); 38 | } 39 | 40 | a.headerInfo-expanded-img .nzbgc_download { 41 | padding: 3px; 42 | } 43 | -------------------------------------------------------------------------------- /sites/nzbindex.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Inject nzbgc-markup to nzbindex.nl 3 | */ 4 | (function() { 5 | 'use strict'; 6 | var dllinks = document.querySelectorAll('a[href*="/download/"]'); 7 | for(var i = 0; i < dllinks.length; i++) { 8 | var dlitem = dllinks.item(i); 9 | var category = '', lid = ''; 10 | 11 | var trParent = findParentOfType(dlitem, 'TR'); 12 | if(trParent) { 13 | var chb = trParent.querySelector('TD INPUT[type=checkbox]'); 14 | if(chb && chb.value) { 15 | lid = chb.value; 16 | } 17 | } 18 | 19 | var newitem = createNgIcon( 20 | lid + '_nzbgc', 21 | dlitem.href, 22 | category 23 | ); 24 | 25 | newitem.style.verticalAlign = 'middle'; 26 | newitem.style.paddingRight = '4px'; 27 | 28 | var dlparent = dlitem.parentElement; 29 | dlparent.insertBefore(newitem, dlitem); 30 | } 31 | })(); 32 | -------------------------------------------------------------------------------- /sites/nzbclub.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Inject nzbgc-markup to nzbclub.com 3 | */ 4 | (function() { 5 | 'use strict'; 6 | var containers = document.querySelectorAll( 7 | '.row .project-action[collectionid]:first-child'); 8 | for(var i = 0; i < containers.length; i++) { 9 | var container = containers.item(i); 10 | var lid = container.getAttribute('collectionid'); 11 | var resultaction = container.querySelector('div'); 12 | var newdiv = document.createElement('button'); 13 | 14 | newdiv.className = 'btn btn-xs icon_nzbgc'; 15 | //newdiv.style.padding = '0 5px'; 16 | 17 | var newitem = createNgIcon( 18 | lid + '_nzbgc', 19 | window.location.protocol + 20 | '//' + window.location.host + '/nzb_get/' + lid 21 | ); 22 | newitem.style.marginTop = '0'; 23 | newitem.style.marginBottom = '0'; 24 | newitem.style.width = 'auto'; 25 | newitem.style.height = '11px'; 26 | newitem.style.borderRadius = '0'; 27 | 28 | newdiv.appendChild(newitem); 29 | 30 | resultaction.appendChild(newdiv); 31 | } 32 | })(); 33 | -------------------------------------------------------------------------------- /elements/ng-checkbox.css: -------------------------------------------------------------------------------- 1 | ng-checkbox { 2 | display: block; 3 | font-size: 1.1rem; 4 | margin-bottom: 5px; 5 | padding: 0.6rem 0 0.3rem 0; 6 | } 7 | 8 | ng-checkbox:focus { 9 | border-color: #e91e63; 10 | border-style: solid; 11 | border-width: 0 0 2px 0; 12 | margin-bottom: 3px; 13 | outline: none; 14 | } 15 | 16 | ng-checkbox .checkboxContainer { 17 | cursor: pointer; 18 | display: inline-block; 19 | height: 18px; 20 | position: relative; 21 | width: 18px; 22 | } 23 | 24 | ng-checkbox .checkbox { 25 | border: solid 2px #5a5a5a; 26 | box-sizing: border-box; 27 | height: 18px; 28 | left: 0; 29 | pointer-events: none; 30 | position: absolute; 31 | top: 0; 32 | transition: all 100ms linear; 33 | width: 18px; 34 | } 35 | 36 | ng-checkbox[checked] .checkbox { 37 | border-color: #0f9d58; 38 | border-width: 0 2px 2px 0; 39 | height: 21px; 40 | left: 6px; 41 | top: -4px; 42 | transform: rotate(45deg); 43 | width: 10px; 44 | } 45 | 46 | ng-checkbox label { 47 | display: flex; 48 | font-weight: 400; 49 | padding: 0; 50 | } 51 | 52 | ng-checkbox .textContainer { 53 | display: flex; 54 | flex-direction: column; 55 | margin-top: -5px; 56 | padding-left: 14px; 57 | } 58 | 59 | ng-checkbox description { 60 | font-size: 0.6rem; 61 | } 62 | -------------------------------------------------------------------------------- /sites/spotweb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run when a site matching spotweb-markup is found 3 | */ 4 | (function(){ 5 | 'use strict'; 6 | var timeout = null; 7 | var leap = 0; 8 | /** 9 | * DOMModificationCallback 10 | * Called after the last DOMSubtreeModified event in a chain. 11 | * @return {void} 12 | */ 13 | function domModificationCallback() { 14 | var dllinks = document.querySelectorAll('a[title*="Download NZB"'); 15 | for(var i = 0; i < dllinks.length; i++) { 16 | var dlitem = dllinks.item(i); 17 | 18 | // Skip if already processed 19 | if(dlitem.nzbGetProcessed) { 20 | continue; 21 | } 22 | dlitem.nzbGetProcessed = true; 23 | 24 | var eParent = dlitem.parentElement; 25 | 26 | var newitem = createNgIcon( 27 | leap++ + '_nzbgc', 28 | dlitem.href, 29 | '' 30 | ); 31 | 32 | eParent.insertBefore(newitem, dlitem); 33 | } 34 | } 35 | 36 | function domModificationHandler(){ 37 | if(timeout) { 38 | clearTimeout(timeout); 39 | } 40 | timeout = setTimeout(domModificationCallback, 200); 41 | } 42 | 43 | window.addEventListener( 44 | 'DOMSubtreeModified', 45 | domModificationHandler, 46 | false); 47 | domModificationCallback(); 48 | })(); 49 | -------------------------------------------------------------------------------- /js/framework.js: -------------------------------------------------------------------------------- 1 | function $E(params) { 2 | 'use strict'; 3 | var tmp = document.createElement(params.tag); 4 | if(params.className) { 5 | tmp.className = params.className; 6 | } 7 | if(params.text) { 8 | tmp.appendChild(document.createTextNode(params.text)); 9 | } 10 | if(params.styles) { 11 | for(var k in params.styles) { 12 | tmp.style[k] = params.styles[k]; 13 | } 14 | } 15 | if(params.rel) { 16 | tmp.setAttribute('rel', params.rel); 17 | } 18 | return tmp; 19 | } 20 | 21 | function modalDialog(header, body, buttons) { 22 | 'use strict'; 23 | const 24 | shroud = document.querySelector('.shroud'), 25 | btnbar = shroud.querySelector('.btnbar'); 26 | 27 | shroud.querySelector('h2').innerHTML = header; 28 | shroud.querySelector('p').innerHTML = body; 29 | btnbar.innerHTML = ''; 30 | 31 | const clickFunc = function() { 32 | if(this.clickfunc) { 33 | this.clickfunc(); 34 | } 35 | shroud.classList.remove('active'); 36 | }; 37 | 38 | for(let button of buttons) { 39 | let btnElm = $E({ 40 | tag: 'a', 41 | text: button.title}); 42 | if(button.href) { 43 | btnElm.href = button.href; 44 | btnElm.target = '_blank'; 45 | } else { 46 | btnElm.href = '#'; 47 | } 48 | btnElm.clickfunc = button.onClick; 49 | 50 | btnElm.addEventListener('click', clickFunc); 51 | btnbar.appendChild(btnElm); 52 | } 53 | 54 | shroud.classList.add('active'); 55 | } 56 | -------------------------------------------------------------------------------- /sites/feedly.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 'use strict'; 3 | var timeout = null; 4 | 5 | /** 6 | * DOMModificationCallback 7 | * Called after the last DOMSubtreeModified event in a chain. 8 | * 9 | * @return {void} 10 | */ 11 | function DOMModificationCallback() { 12 | var dllinks = document.querySelectorAll('#timeline A.entryTitle'); 13 | for(var i = 0; i < dllinks.length; i++) { 14 | var dlitem = dllinks.item(i); 15 | 16 | // Skip if already processed or if href="" doesn't contain '.nzb' 17 | if(dlitem.nzbGetProcessed || !dlitem.href.match(/\.nzb/)) { 18 | continue; 19 | } 20 | dlitem.nzbGetProcessed = true; 21 | 22 | var eRoot = dlitem.parentElement.parentElement.parentElement, 23 | eParent = eRoot.querySelector('.shareHolder .left'), 24 | eBody = eRoot.querySelector('.entryBody'); 25 | 26 | var newitem = createNgIcon( 27 | eRoot.id.replace('_entryHolder', ' _nzbgc'), 28 | dlitem.href, 29 | eBody.innerText.match(/Category[\s-:]*(.+)/)[1] 30 | ); 31 | newitem.classList.add('headerInfo-expanded-img'); 32 | 33 | eParent.insertBefore(newitem, eParent.firstChild); 34 | } 35 | } 36 | 37 | function DOMModificationHandler(){ 38 | if(timeout) { 39 | clearTimeout(timeout); 40 | } 41 | timeout = setTimeout(DOMModificationCallback, 200); 42 | } 43 | 44 | window.addEventListener( 45 | 'DOMSubtreeModified', 46 | DOMModificationHandler, 47 | false); 48 | })(); 49 | -------------------------------------------------------------------------------- /sites/binsearch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Inject nzbgc-markup to binsearch.info 3 | */ 4 | (function() { 5 | 'use strict'; 6 | var dllinks = document.querySelectorAll('input[type=checkbox]'), 7 | linkref = window.location.href.replace(/^.+\?/, '').split('&'), 8 | baselink = 'http://www.binsearch.info/?action=nzb', 9 | get = {}; 10 | 11 | for(var i in linkref) { 12 | var part = linkref[i].split('='); 13 | get[part[0]] = part.length > 1 ? part[1] : ''; 14 | } 15 | if(get.b && get.g) { 16 | baselink += '&b=' + get.b + '&g=' + get.g; 17 | } 18 | 19 | for(var j = 0; j < dllinks.length; j++) { 20 | var dlitem = dllinks.item(j), 21 | lid = dlitem.name, 22 | newSpan = document.createElement('span'), 23 | trParent = findParentOfType(dlitem, 'TR'), 24 | nameTag = trParent.querySelector('span.s'), 25 | name = ''; 26 | 27 | if(!nameTag) { 28 | nameTag = trParent.querySelectorAll('td')[1]; 29 | } 30 | 31 | if(nameTag) { 32 | name = nameTag.childNodes[0].textContent 33 | .replace(/\[[0-9]+\/[0-9]+\]\s*-\s*/g, '') 34 | .replace(/\s*\([0-9]+\/[0-9]+\)/g, '') 35 | .replace(/"/g, '') 36 | .replace(/\s*yEnc\s*/, '') 37 | .replace(/\//g, ''); 38 | } 39 | 40 | newSpan.className = 'icon_nzbgc'; 41 | newSpan.style.padding = '0 5px'; 42 | 43 | var newitem = createNgIcon( 44 | lid + '_nzbgc', 45 | baselink + '&' + dlitem.name + '=1', 46 | '', 47 | name 48 | ); 49 | 50 | newSpan.appendChild(newitem); 51 | dlitem.parentElement.insertBefore(newSpan, dlitem); 52 | } 53 | })(); 54 | -------------------------------------------------------------------------------- /sites/newsnab.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run when a site matching newznab-markup is found 3 | */ 4 | 5 | (function(){ 6 | 'use strict'; 7 | var dllinks = document.querySelectorAll('div.icon_nzb a[href*="/getnzb"]'); 8 | for(var i = 0; i < dllinks.length; i++) { 9 | var dlitem = dllinks.item(i), 10 | category = '', 11 | lid = '', 12 | newdiv = document.createElement('div'), 13 | tabParent = document.getElementById('details'), 14 | trParent = findParentOfType(dlitem, 'TR'), 15 | dlparent = dlitem.parentElement, 16 | tdCat = ''; 17 | 18 | // Skip if already processed 19 | if(dlitem.nzbGetProcessed) { 20 | continue; 21 | } 22 | dlitem.nzbGetProcessed = true; 23 | 24 | newdiv.className = 'icon icon_nzbgc'; 25 | 26 | // Try to find category and an unique id 27 | if(!tabParent) { 28 | tabParent = document.getElementById('detailstable'); 29 | } 30 | 31 | if(trParent.id) { 32 | lid = trParent.id; 33 | } 34 | 35 | if(tabParent) { // Details page 36 | tdCat = tabParent.querySelector('td a[href*="/browse?t"]'); 37 | if(tdCat) { 38 | category = tdCat.innerText; 39 | } 40 | } 41 | else { // Assume listing page 42 | tdCat = trParent.querySelector('td:nth-child(3)'); 43 | if(tdCat) { 44 | category = tdCat.innerText; 45 | } 46 | } 47 | 48 | var newitem = createNgIcon( 49 | lid + '_nzbgc', 50 | dlitem.href, 51 | category 52 | ); 53 | 54 | newdiv.appendChild(newitem); 55 | dlparent.parentElement.insertBefore(newdiv, dlparent); 56 | } 57 | })(); 58 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": { 3 | "scripts": [ "js/nzbget.js", "js/utilities.js" ] 4 | }, 5 | "description": "Interact with and send NZB-files to NZBGet", 6 | "icons": { 7 | "16": "img/icon16.png" 8 | ,"19": "img/icon19.png" 9 | ,"38": "img/icon38.png" 10 | ,"48": "img/icon48.png" 11 | }, 12 | "manifest_version": 2, 13 | "web_accessible_resources": [ 14 | "img/icon16.png" 15 | ,"img/nzbget-arrow.svg" 16 | ], 17 | "name": "nzbget-chrome-phalanx", 18 | "short_name": "nzbget-chrome-phalanx", 19 | "options_ui": { 20 | "page": "options.html", 21 | "chrome_style": false 22 | }, 23 | "permissions": [ "*://*/", "contextMenus", "tabs", "notifications", "storage" ], 24 | "version": "1.6", 25 | "browser_action": { 26 | "default_icon": "img/icon38.png", 27 | "default_popup": "popup.html" 28 | }, 29 | "content_scripts": [ 30 | { 31 | "matches": [ "*://*.feedly.com/*" ] 32 | ,"js": [ "sites/common.js", "sites/feedly.js" ] 33 | ,"css": [ "sites/common.css" ] 34 | },{ 35 | "matches": [ "*://*.nzbclub.com/*" ] 36 | ,"js": [ "sites/common.js", "sites/nzbclub.js" ] 37 | ,"css": [ "sites/common.css" ] 38 | },{ 39 | "matches": [ "*://*.nzbindex.nl/*", "*://*.nzbindex.com/*" ] 40 | ,"js": [ "sites/common.js", "sites/nzbindex.js" ] 41 | ,"css": [ "sites/common.css" ] 42 | },{ 43 | "matches": [ "*://*.binsearch.info/*", "*://*.binsearch.net/*" ] 44 | ,"js": [ "sites/common.js", "sites/binsearch.js" ] 45 | ,"css": [ "sites/common.css" ] 46 | },{ 47 | "matches": [ "*://*.dognzb.cr/*" ] 48 | ,"js": [ "sites/common.js", "sites/dognzb.js" ] 49 | ,"css": [ "sites/common.css" ] 50 | },{ 51 | "matches": [ "*://*.newzleech.com/*" ] 52 | ,"js": [ "sites/common.js", "sites/newzleech.js" ] 53 | ,"css": [ "sites/common.css", "sites/newzleech.css" ] 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /elements/ng-checkbox.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class NgCheckBox extends NgElement { 4 | 5 | constructor(ob) { 6 | super(ob); 7 | this._setup(); 8 | } 9 | 10 | get checked() { 11 | return this._elements.root.getAttribute('checked') !== null; 12 | } 13 | 14 | set checked(value) { 15 | if(value) { 16 | this._elements.root.setAttribute('checked', ''); 17 | } 18 | else { 19 | this._elements.root.removeAttribute('checked'); 20 | } 21 | } 22 | 23 | _sync() { 24 | this._elements.heading.innerText = 25 | this._elements.root.getAttribute('label'); 26 | this._elements.root.label = 27 | this._elements.root.getAttribute('label'); 28 | this._elements.description.innerText = 29 | this._elements.root.getAttribute('description'); 30 | this._elements.root.checked = this.checked; 31 | } 32 | 33 | _setup() { 34 | Object.defineProperty(this._elements.root, 'value', { 35 | get: () => { 36 | return this.checked; 37 | }, 38 | set: (value) => { 39 | this.checked = value; 40 | } 41 | }); 42 | var observer = new MutationObserver(() => { 43 | this._sync(); 44 | }); 45 | observer.observe(this._elements.root, {attributes: true}); 46 | this._elements.root.addEventListener('click', () => { 47 | this.checked = !this.checked; 48 | }); 49 | 50 | this._elements.root.appendChild( 51 | this.$element('label', 'label', [ 52 | this.$element('div', 'checkboxContainer', [ 53 | this.$element('div', 'checkbox') 54 | ]), 55 | this.$element('div', 'textContainer', [ 56 | this.$element('header', 'heading'), 57 | this.$element('description') 58 | ]) 59 | ]) 60 | ); 61 | this._sync(); 62 | } 63 | } 64 | 65 | var list = document.querySelectorAll('ng-checkbox'); 66 | list.forEach(function(ob) { 67 | new NgCheckBox(ob); 68 | }); 69 | -------------------------------------------------------------------------------- /sites/freshrss.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 'use strict'; 3 | var timeout = null; 4 | 5 | /** 6 | * DOMModificationCallback 7 | * Called after the last DOMSubtreeModified event in a chain. 8 | * 9 | * @return {void} 10 | */ 11 | function DOMModificationCallback() { 12 | var dllinks = document.querySelectorAll('div.flux li.item.title a'); 13 | for(var i = 0; i < dllinks.length; i++) { 14 | var dlitem = dllinks.item(i); 15 | 16 | // Skip if already processed or if href="" doesn't contain '.nzb' 17 | if(dlitem.nzbGetProcessed || !dlitem.href.match(/\.nzb/)) { 18 | continue; 19 | } 20 | dlitem.nzbGetProcessed = true; 21 | 22 | var eParent = dlitem.parentElement; 23 | var eRoot = findParentOfType(eParent, 'DIV'); 24 | if(eRoot.querySelector('.nzbgc_download')) { 25 | return; 26 | } 27 | var eContent = eRoot.querySelector('DIV.flux_content'); 28 | var eTarget = eContent.querySelector('H1'); 29 | 30 | var eBody = eTarget.nextElementSibling; 31 | var catMatch = eBody.innerHTML.replace(/<(?:.|\n)*?>/gm, '\n') 32 | .match(/Category[\s-:]*([^\n]+)/); 33 | var category = catMatch && catMatch.length ? catMatch[1] : ''; 34 | 35 | var newItem = createNgIcon( 36 | eRoot.id + ' _nzbgc', 37 | dlitem.href, 38 | category 39 | ); 40 | newItem.style.marginRight = '10px'; 41 | newItem.style.marginTop = '10px'; 42 | newItem.firstChild.style.width = '22px'; 43 | newItem.style.float = 'left'; 44 | newItem.style.lineHeight = '1'; 45 | newItem.style.display = 'inline-block'; 46 | eTarget.insertBefore(newItem, eTarget.firstChild); 47 | } 48 | } 49 | 50 | function DOMModificationHandler(){ 51 | if(timeout) { 52 | clearTimeout(timeout); 53 | } 54 | timeout = setTimeout(DOMModificationCallback, 200); 55 | } 56 | 57 | window.addEventListener( 58 | 'DOMSubtreeModified', 59 | DOMModificationHandler, 60 | false); 61 | })(); 62 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 |
18 |
0 KB/s
19 |
0 MiB
20 |
0 MiB
21 |
22 |
23 | pause 24 | 25 |
26 |
27 |
28 |
29 | Downloads 30 |
31 |
32 | History 33 |
34 |
35 |
36 |
37 |
38 |
No active downloads.
39 |
40 |
41 |
42 |
43 |
44 | search 45 | 49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |

57 |

58 |
59 |
60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Options - nzbget-chrome 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 |

Settings

15 |
16 |
17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 42 | 43 | 44 | 45 |
46 | History items shown in popup or 0 to disable history. 47 |
48 | 50 | 51 |
52 |
53 | 54 | 55 |
56 |
57 |
58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /sites/ttrss.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 'use strict'; 3 | var timeout = null; 4 | 5 | /** 6 | * DOMModificationCallback 7 | * Called after the last DOMSubtreeModified event in a chain. 8 | * 9 | * @return {void} 10 | */ 11 | function DOMModificationCallback() { 12 | var noCdm = document.querySelector('.postReply .postTitle A'); 13 | if(noCdm) { 14 | // Skip if already processed or if href="" doesn't contain '.nzb' 15 | if(noCdm.nzbGetProcessed || !noCdm.href.match(/\.nzb/)) { 16 | return; 17 | } 18 | noCdm.nzbGetProcessed = true; 19 | var sRoot = noCdm.parentElement.parentElement.parentElement, 20 | sBody = sRoot.querySelector('.postContent'), 21 | sParent = sRoot.querySelector('.postTags'), 22 | sMatch = sBody.innerText.match(/Category[\s-:]*(.+)/), 23 | scat = sMatch && sMatch.length ? sMatch[1] : ''; 24 | var sItem = createNgIcon( 25 | sRoot.id + ' _nzbgc', 26 | noCdm.href, 27 | scat 28 | ); 29 | sItem.style.display = 'inline-block'; 30 | sParent.insertBefore(sItem, sParent.firstChild); 31 | return; 32 | } 33 | var dllinks = document.querySelectorAll('.cdmHeader A.title'); 34 | for(var i = 0; i < dllinks.length; i++) { 35 | var dlitem = dllinks.item(i); 36 | 37 | // Skip if already processed or if href="" doesn't contain '.nzb' 38 | if(dlitem.nzbGetProcessed || !dlitem.href.match(/\.nzb/)) { 39 | continue; 40 | } 41 | dlitem.nzbGetProcessed = true; 42 | 43 | var eRoot = dlitem.parentElement.parentElement.parentElement, 44 | eSibling = eRoot.querySelector('img.tagsPic'), 45 | eParent = eSibling.parentElement, 46 | eBody = eRoot.querySelector('.cdmContent .cdmContentInner'), 47 | catMatch = eBody.innerText.match(/Category[\s-:]*(.+)/), 48 | category = catMatch && catMatch.length ? catMatch[1] : ''; 49 | var newitem = createNgIcon( 50 | eRoot.id + ' _nzbgc', 51 | dlitem.href, 52 | category 53 | ); 54 | newitem.classList.add('headerInfo-expanded-img'); 55 | 56 | eParent.insertBefore(newitem, eParent.firstChild); 57 | } 58 | } 59 | 60 | function DOMModificationHandler(){ 61 | if(timeout) { 62 | clearTimeout(timeout); 63 | } 64 | timeout = setTimeout(DOMModificationCallback, 200); 65 | } 66 | 67 | window.addEventListener( 68 | 'DOMSubtreeModified', 69 | DOMModificationHandler, 70 | false); 71 | })(); 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | nzbget-chrome 2 | ============= 3 | 4 | Google chrome extension to interact with nzbget 5 | 6 | ### Version history 7 | #### 1.6 - (current dev) 8 | 9 | --- 10 | 11 | #### 1.5 - 2017-09-12 12 | * Category support 13 | * New site scripts for ttRSS, FreshRSS 14 | * Bug fixes and polish 15 | --- 16 | 17 | #### 1.4 - 2016-01-19 18 | * New tabbed popup appearance 19 | * History search 20 | * Chromium 49 compatibility fixes 21 | 22 | --- 23 | 24 | 25 | #### 1.3 - 2015-06-05 26 | * Make popup notifications optional and use new rich notification API. 27 | * Try to lookup status in notified downloads 28 | * Use category Detected from site markup when no category is present in header. 29 | * Support one-click integration for spotweb, binsearch, nzbindex.com 30 | * Fixes and polish 31 | 32 | --- 33 | 34 | #### 1.2 - 2014-10-13 35 | * Better handling of connection failures. 36 | * support one-click integration on nzbindex.nl 37 | * support one-click integration on fanzub.com 38 | * support one-click integration on DOGnzb (thanks @GrimSerious) 39 | * Minor fixes to feedly support due to layout changes 40 | * Updated appearance 41 | * New feature: [optional] Persistent download status on download icons. 42 | * Progress bars now show estimated remaining download time 43 | 44 | --- 45 | 46 | #### 1.1 - 2014-05-31 47 | * Protocol selector (Thanks dakky) 48 | * Less strict nzb header check (Thanks dakky) 49 | * One-click site integration for newznab, feedly and nzbclub 50 | * Provides category to NZBGet if available 51 | * Show paused state on badge label color (Thanks pdf) 52 | * Show nzb health in popup on low quality nzbs 53 | * Use webp-icon in notifications, since rich notifications lost support for SVG 54 | * Additional polish and bugfixes 55 | 56 | --- 57 | 58 | #### 1.0 - 2013-12-31 59 | * Initial release 60 | 61 | ### Current main features 62 | * Browser action popup that displays active downloads, history and stats 63 | * Browser action icons badge indicating number of current active downloads 64 | * Adds a context menu item to links to download with NZBGet 65 | * Notification when downloads complete 66 | * Uses no toolkits or frameworks, just Javascript, CSS3 and chrome.*-apis 67 | * Drag-n-drop sorting of download queue 68 | * Flow control (pause, resume, delete) on individual items or whole queue. 69 | * One click site intergarion on newznab sites and feedly.com 70 | 71 | ### Installation from chrome webstore 72 | https://chrome.google.com/webstore/detail/nzbget-chrome/pbhceneiekgjjeblaghpkdkaomlloghm 73 | 74 | ### Installation instructions (as unpacked extension) 75 | *Note: Newer versions of chrome for windows will show a yellow icon backgorund for unpacked extensions* 76 | 77 | 1. Download a release or checkout repo with git 78 | 2. Open chrome://extensions/ in Chrome / Chromium 79 | 3. Make sure the "Developer mode" checkbox is checked. 80 | 4. Click "Load unpacked extension..." and choose the path you used in step 1. 81 | -------------------------------------------------------------------------------- /sites/common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Loop through an elements parents until we find one that matches 3 | * or return null 4 | * 5 | * @param {element} el Element 6 | * @param {string} type Type expected of parent element 7 | * @return {element} Found parent or null 8 | */ 9 | function findParentOfType(el, type){ 10 | 'use strict'; 11 | var par = el; 12 | while(par !== null) { 13 | if(par.tagName === type) { 14 | return par; 15 | } 16 | par = par.parentElement; 17 | } 18 | return null; 19 | } 20 | 21 | /** 22 | * Create an IMG-element with properties and events 23 | * ready to inject into a sites markup. 24 | * 25 | * @param {string} id Unique identifier 26 | * @param {string} href URL 27 | * @param {string} cat Category 28 | * @param {string} nameOverride Override NZB-name provided in header 29 | * @return {element} Ready constructed element 30 | */ 31 | function createNgIcon(id, href, cat, nameOverride){ 32 | 'use strict'; 33 | var eNgIcon = document.createElement('img'), 34 | eNgContainer = document.createElement('a'); 35 | eNgIcon.src = chrome.extension.getURL('img/nzbget-arrow.svg'); 36 | eNgContainer.title = 'Click to download with NZBGet.'; 37 | eNgContainer.className = 'nzbgc_download'; 38 | 39 | eNgContainer.href = href; 40 | eNgContainer.id = id; 41 | eNgContainer.nameOverride = nameOverride; 42 | eNgContainer.category = cat; 43 | 44 | eNgContainer.addEventListener('click', function(e) { 45 | e.preventDefault(); 46 | 47 | chrome.runtime.sendMessage({ 48 | message: 'addURL', 49 | href: this.href, 50 | id: this.id, 51 | category: this.category, 52 | nameOverride: this.nameOverride 53 | }); 54 | 55 | this.classList.add('nzbgc_adding'); 56 | 57 | return false; 58 | }); 59 | 60 | // Check for match in stored URLs 61 | chrome.runtime.sendMessage( 62 | {message: 'checkCachedURL', url: href}, 63 | function(response) { 64 | if(response) { 65 | document.getElementById(id).classList.add('nzbgc_added_ok'); 66 | } 67 | } 68 | ); 69 | eNgContainer.appendChild(eNgIcon); 70 | return eNgContainer; 71 | } 72 | 73 | /** 74 | * Listen to events sent to the tab. Update icon class to reflect add status 75 | * 76 | * @TODO: Implement fail status 77 | * @param {object} m Message object 78 | * @return {bool} success 79 | */ 80 | chrome.runtime.onMessage.addListener(function(m) { 81 | 'use strict'; 82 | switch(m.message) { 83 | case 'addedurl': 84 | var eInject = document.getElementById(m.id); 85 | if(!eInject) { 86 | return false; 87 | } 88 | eInject.classList.remove('nzbgc_adding'); 89 | if(m.status) { 90 | eInject.classList.add('nzbgc_added_ok'); 91 | } else { 92 | eInject.classList.add('nzbgc_added_failed'); 93 | } 94 | break; 95 | } 96 | return true; 97 | }); 98 | -------------------------------------------------------------------------------- /sites/dognzb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Inject nzbgc-markup to dognzb.cr 3 | */ 4 | (function() { 5 | 'use strict'; 6 | function injectBrowsingMode() { 7 | var dllinks = document.querySelectorAll('div.dog-icon-download'); 8 | for(var i = 0; i < dllinks.length; i++) { 9 | var dlitem = dllinks.item(i); 10 | var category = '', lid = ''; 11 | 12 | // create the element we want to insert into the html 13 | var newtd = document.createElement('td'); 14 | newtd.className = 'icon_nzbgc'; 15 | newtd.style.padding = '0 2px'; 16 | var trParent = findParentOfType(dlitem, 'TR'); 17 | if(trParent && trParent.id) { 18 | lid = trParent.id; 19 | } 20 | 21 | // read the nzb id from the onclick attribute 22 | var nzbid = dlitem.getAttribute('onclick'); 23 | nzbid = nzbid.split('\'')[1]; 24 | 25 | // we need the personal token to assemble the download link below 26 | var rssToken = document.getElementsByName('rsstoken')[0].value; 27 | 28 | // create the nzbget icon and assemble the download link 29 | var newitem = createNgIcon( 30 | lid + '_nzbgc', 31 | 'https://dognzb.cr' + '/fetch/' + nzbid + '/' + rssToken, 32 | category 33 | ); 34 | 35 | newtd.appendChild(newitem); 36 | 37 | var dlparent = dlitem.parentElement; 38 | dlparent.parentElement.insertBefore(newtd, dlparent); 39 | } 40 | 41 | var warnings = document.querySelectorAll('div.dog-icon-warning'); 42 | for (var j = 0; j < warnings.length; j++) { 43 | var warningitem = warnings.item(j); 44 | 45 | // we add an empty td to preserve the layout 46 | var tdParent = document.createElement('td'); 47 | var warningparent = warningitem.parentElement; 48 | warningparent.parentElement.insertBefore(tdParent, warningparent); 49 | } 50 | } 51 | function injectDetailsMode() { 52 | var dlitem = document.querySelector('i.icon-download'); 53 | var category = '', lid = 'details'; 54 | // read the nzb id from the onclick attribute 55 | var nzbid = dlitem.parentNode.getAttribute('onclick').split("'")[1]; 56 | 57 | // we need the personal token to assemble the download link below 58 | var rssToken = document.getElementsByName('rsstoken')[0].value; 59 | 60 | // create the nzbget icon and assemble the download link 61 | var newitem = createNgIcon( 62 | lid + '_nzbgc', 63 | 'https://dognzb.cr' + '/fetch/' + nzbid + '/' + rssToken, 64 | category 65 | ); 66 | newitem.querySelector('img').style.paddingRight='5px'; 67 | var container = document.querySelector('#details strong'); 68 | container.insertBefore(newitem, container.childNodes[0]); 69 | } 70 | 71 | if(document.querySelector('title').text.match(/Browse/)) { 72 | injectBrowsingMode(); 73 | } else { 74 | injectDetailsMode(); 75 | } 76 | })(); 77 | -------------------------------------------------------------------------------- /js/options.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 'use strict'; 3 | function parseComponentsFromURL(value) { 4 | var x = new URL(value); 5 | 6 | document.querySelector('#opt_protocol').value = x.protocol.replace( 7 | ':', ''); 8 | document.querySelector('#opt_host').value = x.hostname; 9 | document.querySelector('#opt_port').value = x.port ? x.port : 80; 10 | 11 | var keys = ['username', 'password', 'pathname']; 12 | for(var i in keys) { 13 | var key = 'opt_' + keys[i], 14 | el = document.getElementById(key); 15 | el.value = x[keys[i]]; 16 | } 17 | return x; 18 | } 19 | 20 | document.addEventListener('DOMContentLoaded', function() { 21 | var elConnectionTest = document.querySelector('#connection_test'); 22 | window.ngAPI = chrome.extension.getBackgroundPage().ngAPI; 23 | var opts = window.ngAPI.Options; 24 | 25 | document.querySelector('#opt_protocol') 26 | .addEventListener('change', function(evt) { 27 | var port = document.querySelector('#opt_port'); 28 | if(port.value === '6789' || port.value === '6791') { 29 | port.value = evt.target.value === 'http' ? '6789' : '6791'; 30 | } 31 | }); 32 | 33 | var inputs = document.querySelectorAll( 34 | 'input[type=text],input[type=password],ng-checkbox,select' 35 | ); 36 | 37 | for(var i = 0; i < inputs.length; i++) { 38 | inputs[i].value = opts.get(inputs[i].id); 39 | } 40 | 41 | document.querySelector('#btn_save') 42 | .addEventListener('click', function(){ 43 | for(var input in inputs) { 44 | opts.set(inputs[input].id, inputs[input].value); 45 | } 46 | chrome.runtime.sendMessage({message: 'optionsUpdated'}); 47 | 48 | elConnectionTest.innerText = 'Settings saved!'; 49 | elConnectionTest.className = 'success'; 50 | window.close(); 51 | }); 52 | 53 | 54 | document.querySelector('#btn_test') 55 | .addEventListener('click', function(){ 56 | elConnectionTest.className = 'working'; 57 | elConnectionTest.innerText = 'Trying to connect...'; 58 | elConnectionTest.style.animationName = 'flip'; 59 | 60 | var opOb = {get: function(v) {return this[v]; }}; 61 | for(var input in inputs) { 62 | opOb[inputs[input].id] = (inputs[input].id, 63 | inputs[input].value); 64 | } 65 | 66 | window.ngAPI.version(function(r){ 67 | elConnectionTest.innerText = 'Successfully connected ' + 68 | 'to NZBGet v' + r.result; 69 | elConnectionTest.style.animationName = 'pulse'; 70 | elConnectionTest.className = 'success'; 71 | }, function(reason){ 72 | elConnectionTest.className = 'error'; 73 | elConnectionTest.style.animationName = 'shake'; 74 | elConnectionTest.innerHTML = '' + 75 | 'Connection failed!' + 76 | ' ' + reason; 77 | }, opOb); 78 | }); 79 | elConnectionTest.addEventListener('webkitAnimationEnd', function(){ 80 | this.style.webkitAnimationName = ''; 81 | }, false); 82 | elConnectionTest.addEventListener('click', function(){ 83 | this.innerHTML = ''; 84 | this.className = ''; 85 | }); 86 | /* Parse text in host field and try to place URI-parts 87 | in their right form fields. */ 88 | document.querySelector('#opt_host') 89 | .addEventListener('blur', function() { 90 | var prot = this.value.match(/^([a-z]+):\/\//); 91 | 92 | if(prot) { 93 | parseComponentsFromURL(this.value); 94 | } 95 | }); 96 | }); 97 | })(); 98 | -------------------------------------------------------------------------------- /js/utilities.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 'use strict'; 3 | var MAX32 = 4294967296; 4 | window.ngAPI.parse = { 5 | /** 6 | * Returns a string a history entrys status 7 | * @param {object} hist history item object 8 | * @return {void} 9 | */ 10 | historyStatus: function(hist) { 11 | if(hist.Status) { 12 | return hist.Status 13 | .toLowerCase() 14 | .split('/'); 15 | } 16 | 17 | // < v13 compat 18 | if (hist.Kind === 'NZB') { 19 | switch(true) { 20 | case hist.ParStatus === 'FAILURE': 21 | case hist.UnpackStatus === 'FAILURE': 22 | case hist.MoveStatus === 'FAILURE': 23 | case hist.ScriptStatus === 'FAILURE': 24 | return ['failure']; 25 | case hist.ParStatus === 'MANUAL': 26 | return ['damaged']; 27 | case hist.ScriptStatus === 'UNKNOWN': 28 | return ['unknown']; 29 | case hist.ScriptStatus === 'SUCCESS': 30 | case hist.UnpackStatus === 'SUCCESS': 31 | case hist.ParStatus === 'SUCCESS': 32 | return ['success']; 33 | case hist.ParStatus === 'REPAIR_POSSIBLE': 34 | return ['repairable']; 35 | case hist.ParStatus === 'NONE': 36 | return ['unknown']; 37 | } 38 | } else if (hist.Kind === 'URL') { 39 | switch (hist.UrlStatus) { 40 | case 'SUCCESS': return ['success']; 41 | case 'FAILURE': return ['failure']; 42 | case 'UNKNOWN': return ['unknown']; 43 | } 44 | } 45 | }, 46 | 47 | /** 48 | * function detectGroupStatus() 49 | * Returns a string representing current download status 50 | * 51 | * @param {object} group Group object 52 | * @return {string} Group status 53 | */ 54 | groupStatus: function(group) { 55 | if(group.Status !== 'undefined') { 56 | return group.Status.toLowerCase(); 57 | } 58 | 59 | // < v13 compat 60 | switch(true) { 61 | case typeof group.post !== 'undefined': 62 | return 'postprocess'; 63 | case group.ActiveDownloads > 0: 64 | return 'downloading'; 65 | case group.PausedSizeLo !== 0 && 66 | group.RemainingSizeLo === group.PausedSizeLo: 67 | return 'paused'; 68 | } 69 | return 'queued'; 70 | }, 71 | 72 | /** 73 | * bigNumber 74 | * Combines two 32-bit integers to a 64-bit Double 75 | * (may lose data with extreme sizes) 76 | * 77 | * @param {integer} hi high-end int 78 | * @param {integer} lo low-end int 79 | * @return {number} Larger number 80 | */ 81 | bigNumber: function(hi, lo) { 82 | return Number(hi * MAX32 + lo); 83 | }, 84 | 85 | /** 86 | * function formatHRSize() 87 | * Formats an integer of seconds to a human readable string 88 | * 89 | * @param {integer} bytes size in bytes 90 | * @return {string} Human readable representation of bytes 91 | */ 92 | toHRDataSize: function(bytes) { 93 | var sizes = { 94 | 1: ['KiB', 0], 95 | 2: ['MiB', 1], 96 | 3: ['GiB', 2], 97 | 4: ['TiB', 2] 98 | }, 99 | output = null; 100 | Object.keys(sizes).reverse().forEach( function(i) { 101 | if(!output && this >= Math.pow(1024, i)) { 102 | var nmr = this / Math.pow(1024, i); 103 | output = nmr.toFixed(nmr < 100 ? sizes[i][1] : 0) + 104 | ' ' + sizes[i][0]; 105 | } 106 | }.bind(bytes)); 107 | return output !== null ? output : bytes + 'B'; 108 | } 109 | }; 110 | })(); 111 | -------------------------------------------------------------------------------- /options.css: -------------------------------------------------------------------------------- 1 | /* 2 | Animatioms are copied and modified from animate.css 3 | http://daneden.me/animate 4 | */ 5 | @keyframes flip { 6 | 0% { 7 | transform: rotateY(-360deg); 8 | animation-timing-function: ease-out; 9 | } 10 | 11 | 50% { 12 | transform: rotateY(0deg); 13 | animation-timing-function: ease-in; 14 | } 15 | } 16 | 17 | @keyframes shake { 18 | 0%, 100% { 19 | transform: translateX(0); 20 | } 21 | 22 | 10%, 30%, 50%, 70%, 90% { 23 | transform: translateX(-10px); 24 | } 25 | 26 | 20%, 40%, 60%, 80% { 27 | transform: translateX(10px); 28 | } 29 | } 30 | 31 | @keyframes pulse { 32 | 0% { 33 | transform: scale(1); 34 | } 35 | 36 | 50% { 37 | transform: scale(1.1); 38 | } 39 | 40 | 100% { 41 | transform: scale(1); 42 | } 43 | } 44 | 45 | * { 46 | font-family: 47 | 'Roboto Condensed', 48 | Helvetica, 49 | Arial, 50 | sans-serif; 51 | } 52 | 53 | body { 54 | text-align: center; 55 | } 56 | 57 | .header { 58 | background-color: #e91e63; 59 | margin: -44px; 60 | margin-bottom: 16px; 61 | height: 96px; 62 | box-shadow: 63 | 0 2px 6px rgba(0,0,0,.16), 64 | 0 2px 6px rgba(0,0,0,.23); 65 | color: #fff; 66 | border-radius: 2px 2px 0 0; 67 | } 68 | 69 | .header h1 { 70 | font-size: 36px; 71 | font-weight: 500; 72 | line-height: 1.1; 73 | padding: 0; 74 | margin: 0; 75 | padding-top: 24px; 76 | } 77 | 78 | .header span { 79 | font-size: 16px; 80 | } 81 | 82 | .header img#logo { 83 | float: left; 84 | margin: 16px 8px 16px 16px; 85 | width: 64px; 86 | height: 64px; 87 | filter: grayscale(100%) invert(100%) brightness(150%); 88 | } 89 | 90 | b, strong { 91 | font-weight: 700; 92 | } 93 | 94 | div.card { 95 | text-align: left; 96 | display: inline-block; 97 | background: #fff; 98 | border-radius: 2px; 99 | padding: 36px; 100 | margin: 0; 101 | width: 350px; 102 | color: #333; 103 | font-size: 14px; 104 | line-height: 1.42857143; 105 | } 106 | 107 | fieldset { 108 | border: none; 109 | padding: 0; 110 | margin: 0; 111 | } 112 | 113 | label { 114 | display: block; 115 | padding: 0.6rem 0 0.3rem 0; 116 | font-weight: 700; 117 | } 118 | 119 | input[type=text], input[type=password], select { 120 | display: block; 121 | padding: 0.3rem 0.6rem; 122 | width: 100%; 123 | font-size: 18px; 124 | line-height: 1.33; 125 | color: #555; 126 | outline: 0; 127 | border: 0; 128 | border-bottom: 1px solid #7e7e7e; 129 | outline-width: 2px; 130 | padding-left: 0; 131 | margin-bottom: 1px; 132 | background: transparent; 133 | } 134 | 135 | input[type=text]:focus, input[type=password]:focus, select:focus { 136 | border-color: #e91e63; 137 | border-width: 2px; 138 | margin-bottom: 0; 139 | } 140 | 141 | input[type=button] { 142 | background-color: #4285f4; 143 | border-color: #285e8e; 144 | padding: 8px 30px; 145 | border: 0; 146 | margin: 10px 1px 0 1px; 147 | cursor: pointer; 148 | border-radius: 4px; 149 | text-transform: uppercase; 150 | transition: box-shadow .28s cubic-bezier(0.4,0,.2,1); 151 | outline: 0!important; 152 | font-size: 14px; 153 | font-weight: 400; 154 | line-height: 1.42857143; 155 | white-space: nowrap; 156 | vertical-align: middle; 157 | color: rgba(255,255,255,.84); 158 | } 159 | 160 | input[type=button]:hover { 161 | box-shadow: 162 | 0 3px 6px rgba(0,0,0,.2), 163 | 0 3px 6px rgba(0,0,0,.28); 164 | } 165 | 166 | input[type=button]:active { 167 | box-shadow: 168 | 0 10px 20px rgba(0,0,0,.19), 169 | 0 6px 6px rgba(0,0,0,.23); 170 | } 171 | 172 | input[type=button]#btn_test { 173 | color: rgba(0,0,0,.84); 174 | background-color: transparent; 175 | } 176 | 177 | input.small[type=text], input.small[type=password] { 178 | width: 3rem; 179 | } 180 | 181 | div.small, span.small { 182 | font-size: 0.6rem; 183 | font-weight: 400; 184 | } 185 | 186 | #connection_test { 187 | display: block; 188 | animation-timing-function: linear; 189 | } 190 | 191 | #connection_test.working { 192 | animation-duration: 2s; 193 | animation-iteration-count: infinite; 194 | background-color: #03a9f4; 195 | color: #fff; 196 | padding: 10px; 197 | margin: 10px 0; 198 | } 199 | 200 | #connection_test.error { 201 | color: #fff; 202 | background-color: #f44336; 203 | animation-duration: 500ms; 204 | animation-iteration-count: 1; 205 | padding: 10px; 206 | margin: 10px 0; 207 | } 208 | 209 | #connection_test.success { 210 | color: #fff; 211 | animation-duration: 250ms; 212 | animation-iteration-count: 1; 213 | background-color: #0f9d58; 214 | padding: 10px; 215 | margin: 10px 0; 216 | } 217 | 218 | #btn_grp { 219 | clear: both; 220 | text-align: right; 221 | } 222 | -------------------------------------------------------------------------------- /elements/ng-download-item.css: -------------------------------------------------------------------------------- 1 | ng-download-item { 2 | background: #fff; 3 | border-bottom: 1px solid #dadada; 4 | display: flex; 5 | padding: 15px; 6 | position: relative; 7 | width: 430px; 8 | box-sizing: border-box; 9 | } 10 | 11 | ng-download-item .info { 12 | flex: 1; 13 | padding: 0 10px; 14 | position: relative; 15 | } 16 | 17 | ng-download-item .title { 18 | font-size: 16px; 19 | font-weight: 400; 20 | padding-bottom: 5px; 21 | word-wrap: break-word; 22 | } 23 | 24 | ng-download-item .postinfo { 25 | color: #999; 26 | font-size: 12px; 27 | word-wrap: break-word; 28 | } 29 | 30 | ng-download-item .info .progress_piece { 31 | background-color: #c0c0c0; 32 | height: 8px; 33 | transition: width 0.6s ease; 34 | width: 0; 35 | } 36 | 37 | ng-download-item .progress_bar { 38 | background-color: #e0e0e0; 39 | display: flex; 40 | } 41 | 42 | ng-download-item .progress_piece.paused { 43 | opacity: 0.2; 44 | } 45 | 46 | ng-download-item .progress_bar.paused { 47 | background-color: #fff59d; 48 | } 49 | 50 | ng-download-item .progress_bar.paused .progress_piece { 51 | background-color: #fdd835; 52 | } 53 | 54 | ng-download-item .progress_bar.postprocess { 55 | background-color: #d7ccc8; 56 | } 57 | 58 | ng-download-item .progress_bar.postprocess .progress_piece { 59 | background-color: #795548; 60 | } 61 | 62 | ng-download-item .progress_bar.downloading { 63 | background-color: rgba(76, 175, 80, 0.2); 64 | } 65 | 66 | ng-download-item .progress_bar.downloading .progress_piece { 67 | background-color: #59b75c; 68 | } 69 | 70 | ng-download-item .text { 71 | flex: 1; 72 | padding: 5px 0; 73 | } 74 | 75 | ng-download-item .healthWarning { 76 | background-color: #ff5722; 77 | border-radius: 2px; 78 | box-shadow: 79 | 0 3px 1px -2px rgba(0, 0, 0, 0.2), 80 | 0 2px 2px 0 rgba(0, 0, 0, 0.14), 81 | 0 1px 5px 0 rgba(0, 0, 0, 0.12); 82 | color: #fff; 83 | font-size: 80%; 84 | margin: 3px; 85 | max-height: 16px; 86 | opacity: 0; 87 | padding: 4px; 88 | transform: scale(0); 89 | transition: 90 | opacity 0.2s cubic-bezier(0.4, 0.0, 1, 1), 91 | transform 0.2s cubic-bezier(0.4, 0.0, 1, 1); 92 | } 93 | 94 | ng-download-item .categoryBadge { 95 | align-self: center; 96 | border-radius: 2px; 97 | cursor: pointer; 98 | font-size: 80%; 99 | margin: 0 0 0 0; 100 | padding: 4px; 101 | } 102 | 103 | ng-download-item .categoryBadge.add { 104 | font-size: 80%; 105 | font-style: italic; 106 | opacity: 0.8; 107 | padding: 4px; 108 | } 109 | 110 | ng-download-item .healthWarning.active { 111 | opacity: 1; 112 | transform: scale(1); 113 | } 114 | 115 | ng-download-item .menu { 116 | border-radius: 50%; 117 | cursor: pointer; 118 | position: relative; 119 | } 120 | 121 | ng-download-item .contextmenu { 122 | background: #fafafa; 123 | box-shadow: 124 | 0 2px 2px 0 rgba(0, 0, 0, 0.14), 125 | 0 1px 5px 0 rgba(0, 0, 0, 0.12), 126 | 0 3px 1px -2px rgba(0, 0, 0, 0.2); 127 | display: block; 128 | min-width: 150px; 129 | opacity: 0; 130 | padding: 0; 131 | position: absolute; 132 | right: 15px; 133 | transform: scale(0, 0); 134 | transform-origin: top right; 135 | transition: 136 | z-index 0s linear 0.2s, 137 | transform 0.1s linear, 138 | opacity 0.2s linear; 139 | z-index: -1; 140 | } 141 | 142 | ng-download-item .contextmenu.show { 143 | opacity: 1; 144 | transform: scale(1, 1); 145 | transition-delay: 0s; 146 | z-index: 2; 147 | } 148 | 149 | ng-download-item .contextmenu ul { 150 | list-style-type: none; 151 | margin: 0; 152 | padding: 0; 153 | } 154 | 155 | ng-download-item .contextmenu ul li { 156 | cursor: pointer; 157 | font-size: 16px; 158 | font-weight: 400; 159 | line-height: 24px; 160 | padding: 7px 15px; 161 | } 162 | 163 | ng-download-item .contextmenu ul li:hover { 164 | background: #e5e5e5; 165 | } 166 | 167 | ng-download-item .contextmenu i { 168 | color: rgba(0, 0, 0, 0.54); 169 | padding-right: 10px; 170 | vertical-align: top; 171 | z-index: 1; 172 | } 173 | 174 | ng-download-item .icons { 175 | display: inline-block; 176 | font-size: 24px; 177 | font-style: normal; 178 | font-weight: 400; 179 | height: 1em; 180 | letter-spacing: normal; 181 | line-height: 1; 182 | text-rendering: optimizeLegibility; 183 | text-transform: none; 184 | width: 1em; 185 | word-wrap: normal; 186 | } 187 | 188 | ng-download-item .bottom_box { 189 | display: flex; 190 | } 191 | 192 | /* Highlight on click */ 193 | 194 | ng-download-item .ripple { 195 | --ripple-left: 0; 196 | --ripple-top: 0; 197 | overflow: hidden; 198 | position: relative; 199 | transform: translate3d(0, 0, 0); 200 | transition: 201 | box-shadow .3s ease-in, 202 | background-color .3s ease-in; 203 | } 204 | 205 | .ripple:hover { 206 | background-color: rgba(0,0,0,0.05); 207 | box-shadow: 208 | 0 3px 1px -2px rgba(0, 0, 0, 0.2), 209 | 0 2px 2px 0 rgba(0, 0, 0, 0.14), 210 | 0 1px 5px 0 rgba(0, 0, 0, 0.12); 211 | } 212 | 213 | .ripple:after { 214 | background: rgba(0, 0, 0, 1.2); 215 | border-radius: 50%; 216 | content: ''; 217 | display: block; 218 | height: 10%; 219 | left: var(--ripple-left); 220 | opacity: 0; 221 | pointer-events: none; 222 | position: absolute; 223 | top: var(--ripple-top); 224 | transform: scale(0); 225 | width: 10%; 226 | } 227 | 228 | .ripple.animate:after { 229 | animation: ripple-in 325ms ease-in-out; 230 | animation-fill-mode: forwards; 231 | } 232 | 233 | @keyframes ripple-in { 234 | 100% { 235 | opacity: .2; 236 | transform: scale(27); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /sites/tests/binsearch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 100 | 101 | 102 | 103 | 104 | 108 | 109 | 110 | 111 | 112 |
  7 | 8 | 9 |
10 |

binsearch.info

11 |
12 | basic search - advanced search - F.A.Q. - watchlist - disclaimer - browse newsgroups - RSS 13 |   14 |
15 |
  16 |   17 |
  18 |   19 |

Search!

20 | 21 |
22 |
23 | 24 |
Results per page: 30 |
Maximum age of post: 31 | 33 |
34 | [change default settings]

35 | search in the most popular groups search in the other groups 36 | 37 | 38 |

39 | 40 |

41 | 42 | 43 |

44 |

Results

45 |
46 | 47 | 48 | 49 |

50 | 51 | 52 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
 SubjectPosterGroupAge 55 |
1. Windows 8.1 Ubuntu Edition (x64).rar (1/265)Yenc-PP-A&Aa.b.warez.ibm-pc.german7d
2. Windows 8.1 Ubuntu Edition (x64) 2015.rar (1/265)Yenc-PP-A&Aa.b.warez.ibm-pc.german7d
3. VirtualBox - Ubuntu 14.04 LTS amd64 ALPHA1 Desktop VirtualBox VDI Virtual Computer.rar (1/265)Yenc-PP-A&Aa.b.warez.ibm-pc.german7d
4. Ubuntu Studio 15.04 (x86.x64).rar (1/265)Yenc-PP-A&Aa.b.warez.ibm-pc.german7d
86 | 87 | 88 | 89 | 91 | 92 | 93 |
<< < 100+ records >
94 | 95 |

96 |
97 |
98 |
99 | Generated in 0.03 seconds.
 
  105 |

Copyright © 2006-2011 binsearch - disclaimer

107 |
113 | 114 | 115 | -------------------------------------------------------------------------------- /popup.css: -------------------------------------------------------------------------------- 1 | /* Global styles */ 2 | 3 | body { 4 | background: #f5f5f5; 5 | box-sizing: border-box; 6 | color: #212121; 7 | display: flex; 8 | flex-direction: column; 9 | font-family: 10 | 'Roboto', 11 | Helvetica, 12 | Arial, 13 | sans-serif; 14 | font-size: 14px; 15 | font-weight: 300; 16 | height: 580px; 17 | line-height: 1.42857143; 18 | margin: 0; 19 | overflow: hidden; 20 | width: 430px; 21 | } 22 | 23 | div.inactive { 24 | background: #eee; 25 | border-radius: 50%; 26 | font-size: 0; 27 | height: 0; 28 | line-height: 0; 29 | margin: 0 auto; 30 | opacity: 0; 31 | padding: 0; 32 | text-align: center; 33 | transform: translateZ(0); 34 | transition: all 0.2s cubic-bezier(0.4, 0.0, 1, 1); 35 | width: 200px; 36 | } 37 | 38 | div.inactive.shown { 39 | font-size: 16px; 40 | height: 200px; 41 | line-height: 200px; 42 | margin: 50px auto; 43 | opacity: 1; 44 | padding: 50px; 45 | } 46 | 47 | .searchcontainer { 48 | background: #fff; 49 | border-radius: 3px; 50 | box-shadow: 51 | 0 2px 2px 0 rgba(0, 0, 0, 0.14), 52 | 0 1px 5px 0 rgba(0, 0, 0, 0.12), 53 | 0 3px 1px -2px rgba(0, 0, 0, 0.2); 54 | display: flex; 55 | margin: 4px 7px; 56 | min-height: 0; 57 | } 58 | 59 | .searchcontainer i { 60 | color: #666; 61 | margin: 10px; 62 | } 63 | 64 | input.search { 65 | background: transparent; 66 | border: 0; 67 | font-size: 18px; 68 | padding: 7px 5px 7px 10px; 69 | width: 100%; 70 | } 71 | 72 | input.search:focus { 73 | outline: none; 74 | } 75 | 76 | 77 | /* Infobox styles */ 78 | 79 | .header { 80 | background-color: #e91e63; 81 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); 82 | color: #fff; 83 | } 84 | 85 | .infobox { 86 | display: flex; 87 | padding: 15px; 88 | } 89 | 90 | .infobox img { 91 | cursor: pointer; 92 | filter: grayscale(100%) invert(100%) brightness(150%); 93 | height: 64px; 94 | padding-top: 13px; 95 | width: 64px; 96 | } 97 | 98 | .tabs { 99 | display: flex; 100 | } 101 | 102 | .tab { 103 | cursor: pointer; 104 | flex: 1; 105 | font-weight: 500; 106 | overflow: hidden; 107 | padding: 12px; 108 | text-align: center; 109 | text-transform: uppercase; 110 | } 111 | 112 | .tab.active { 113 | border-bottom: 2px solid #ffff8d; 114 | font-weight: 700; 115 | } 116 | 117 | dl { 118 | flex: 2; 119 | margin: 13px 20px; 120 | } 121 | 122 | dt { 123 | clear: both; 124 | float: left; 125 | font-weight: 400; 126 | } 127 | 128 | dd { 129 | font-weight: 500; 130 | text-align: right; 131 | } 132 | 133 | #download_container, 134 | #history_container { 135 | display: flex; 136 | flex-direction: column; 137 | height: 100%; 138 | opacity: 0; 139 | padding: 10px 0; 140 | position: absolute; 141 | top: 0; 142 | transform: translateZ(0); 143 | transition: opacity 0.1s cubic-bezier(0.4, 0.0, 1, 1); 144 | width: 100%; 145 | z-index: 0; 146 | } 147 | 148 | #history_list { 149 | flex: 1; 150 | height: 100%; 151 | overflow: auto; 152 | } 153 | 154 | .body { 155 | flex: 1; 156 | position: relative; 157 | } 158 | 159 | #download_container.active, 160 | #history_container.active { 161 | opacity: 1; 162 | z-index: 2; 163 | } 164 | 165 | 166 | /* Post styles */ 167 | 168 | span.health-warning { 169 | background: #ff9706; 170 | border-radius: 1px; 171 | color: #fff; 172 | padding: 1px 5px; 173 | position: absolute; 174 | right: 0; 175 | } 176 | 177 | span.health-warning.critical { 178 | background: #e55b59; 179 | } 180 | 181 | div.post { 182 | background: #fff; 183 | border-bottom: 1px solid #dadada; 184 | display: flex; 185 | padding: 15px; 186 | } 187 | 188 | div.placeholder { 189 | background: rgba(0, 0, 0, 0.1); 190 | box-shadow: 0 0 1px rgba(0, 0, 0, 0.9); 191 | } 192 | 193 | div.post .tag { 194 | display: flex; 195 | margin: 0; 196 | max-width: 10px; 197 | transition: max-width 200ms ease-in-out; 198 | border-radius: 2px; 199 | font-weight: 400; 200 | } 201 | 202 | .tag small { 203 | font-size: 60%; 204 | display: block; 205 | font-weight: 300; 206 | } 207 | 208 | div.post .info { 209 | flex: 1; 210 | overflow: hidden; 211 | padding: 0 5px; 212 | position: relative; 213 | } 214 | 215 | div.post .tag:hover { 216 | max-width: 100px; 217 | } 218 | 219 | div.post .tag:hover span { 220 | opacity: 1; 221 | } 222 | 223 | div.post .tag span { 224 | align-self: center; 225 | color: white; 226 | display: inline-block; 227 | flex: 1 0 auto; 228 | opacity: 0; 229 | padding: 5px; 230 | text-transform: capitalize; 231 | transition: opacity 250ms ease-in-out; 232 | } 233 | 234 | div.post .title { 235 | font-weight: 400; 236 | overflow: hidden; 237 | padding: 0 3px 3px 15px; 238 | text-overflow: ellipsis; 239 | white-space: nowrap; 240 | } 241 | 242 | .details { 243 | color: #727272; 244 | display: flex; 245 | } 246 | 247 | .details .left { 248 | flex: 1 0 auto; 249 | padding: 0 0 0 15px; 250 | } 251 | 252 | .details .right { 253 | flex: 1 0 auto; 254 | padding: 0 6px 0 0; 255 | text-align: right; 256 | } 257 | 258 | 259 | /* Tag colors */ 260 | 261 | .tag { 262 | background: #bfbfbf; 263 | } 264 | 265 | .tag.deleted { 266 | background: #333; 267 | } 268 | 269 | .tag.postprocess { 270 | background: #468847; 271 | } 272 | 273 | .tag.unknown { 274 | background: #3a87ad; 275 | } 276 | 277 | .tag.success, 278 | .tag.downloading { 279 | background: #56a858; 280 | } 281 | 282 | .tag.warning { 283 | background: #c67605; 284 | } 285 | 286 | .tag.failure { 287 | background: #b94a48; 288 | } 289 | 290 | .tag.paused { 291 | background: #f89406; 292 | } 293 | 294 | #setup_needed { 295 | position: absolute; 296 | } 297 | 298 | .shroud { 299 | background: rgba(30, 30, 30, 0.7); 300 | bottom: 0; 301 | left: 0; 302 | opacity: 0; 303 | position: absolute; 304 | right: 0; 305 | top: 0; 306 | transition: opacity 0.2s cubic-bezier(0.4, 0.0, 1, 1); 307 | z-index: -1; 308 | } 309 | 310 | .shroud.active { 311 | opacity: 1; 312 | visibility: visible; 313 | z-index: 3; 314 | } 315 | 316 | .shroud.active .messagebox { 317 | display: block; 318 | z-index: 3; 319 | } 320 | 321 | .shroud .messagebox { 322 | background: #fff; 323 | border-radius: 3px; 324 | box-shadow: 325 | 0 16px 24px 2px rgba(0, 0, 0, 0.14), 326 | 0 6px 30px 5px rgba(0, 0, 0, 0.12), 327 | 0 8px 10px -5px rgba(0, 0, 0, 0.4); 328 | display: none; 329 | margin: 30% 10%; 330 | padding: 30px; 331 | z-index: 0; 332 | } 333 | 334 | .messagebox h2 { 335 | font-size: 24px; 336 | margin-top: 0; 337 | } 338 | 339 | .btnbar { 340 | margin-right: -2rem; 341 | padding-top: 20px; 342 | text-align: right; 343 | } 344 | 345 | .btnbar a { 346 | background-color: transparent; 347 | color: #e91e63; 348 | font-size: 14px; 349 | font-weight: 400; 350 | margin: 10px 1px 0 1px; 351 | padding: 8px 30px; 352 | text-decoration: none; 353 | text-transform: uppercase; 354 | transition: box-shadow .28s cubic-bezier(0.4, 0, .2, 1); 355 | vertical-align: middle; 356 | } 357 | 358 | .btnbar a:hover { 359 | box-shadow: 360 | 0 3px 6px rgba(0, 0, 0, .2), 361 | 0 3px 6px rgba(0, 0, 0, .28); 362 | } 363 | 364 | .btnbar a:active { 365 | box-shadow: 366 | 0 10px 20px rgba(0, 0, 0, .19), 367 | 0 6px 6px rgba(0, 0, 0, .23); 368 | } 369 | 370 | div.control { 371 | padding: 10px 0; 372 | } 373 | 374 | div.control i { 375 | border-radius: 50%; 376 | cursor: pointer; 377 | font-size: 32px; 378 | transition: all 250ms cubic-bezier(0.4, 0, .2, 1); 379 | } 380 | 381 | div.control i:active { 382 | background-color: rgba(255, 255, 255, 0.2); 383 | box-shadow: 1px 1px 5px rgba(0, 0, 0, .23); 384 | } 385 | 386 | div.post i { 387 | border-radius: 50%; 388 | cursor: pointer; 389 | font-size: 16px; 390 | height: 16px; 391 | transition: all 250ms cubic-bezier(0.4, 0, .2, 1); 392 | width: 16px; 393 | } 394 | 395 | div.post i:active { 396 | background-color: rgba(255, 255, 255, 0.2); 397 | box-shadow: 1px 1px 5px rgba(0, 0, 0, .23); 398 | } 399 | 400 | #download_list { 401 | height: 100%; 402 | overflow: auto; 403 | } 404 | 405 | @font-face { 406 | font-family: 'icons'; 407 | src: url('icons.woff') format('woff'); 408 | } 409 | 410 | .icons { 411 | -webkit-font-feature-settings: 'liga'; 412 | -webkit-font-smoothing: antialiased; 413 | direction: ltr; 414 | display: inline-block; 415 | font-family: 'icons'; 416 | font-feature-settings: 'liga'; 417 | font-size: 24px; 418 | font-style: normal; 419 | font-variant-ligatures: discretionary-ligatures; 420 | font-variant: normal; 421 | font-weight: 400; 422 | letter-spacing: normal; 423 | line-height: 1; 424 | text-transform: none; 425 | white-space: nowrap; 426 | word-wrap: normal; 427 | } 428 | -------------------------------------------------------------------------------- /elements/ng-download-item.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class NgDownloadItem extends NgElement { 4 | 5 | constructor(ob) { 6 | super(ob); 7 | this._setup(); 8 | } 9 | 10 | _createMarkup() { 11 | this._elements.root.appendChild( 12 | this.$element('div', 'info', [ 13 | this.$element('div', 'title'), 14 | this.$element('div', 'postinfo'), 15 | this.$element('div', 'progress_bar', [ 16 | this.$element('div', 'barpct', { 17 | class: 'progress_piece'}), 18 | this.$element('div', 'barPaused', { 19 | class: 'progress_piece paused'}) 20 | ]), 21 | this.$element('div', 'bottom_box', [ 22 | this.$element('div', 'text'), 23 | this.$element('span', 'healthWarning'), 24 | this.$element('span', 'categoryBadge', { 25 | class:'categoryBadge ripple'}), 26 | this.$element('div', 'categoryContext', [ 27 | this.$element('ul') 28 | ], {class: 'contextmenu'}) 29 | ]) 30 | ]) 31 | ); 32 | 33 | this._elements.root.appendChild( 34 | this.$element('i', 'moreVert', { 35 | class: 'icons menu ripple', text: 'more_vert'}) 36 | ); 37 | 38 | this._elements.root.appendChild( 39 | this.$element('div', 'contextmenu', [ 40 | this.$element('ul', 'ccont', [ 41 | this.$element('li', 'btn_pause', [ 42 | this.$element('i', 'pauseI', { 43 | class: 'icons', text: 'pause'}), 44 | this.$element('span', 'pauseSpan') 45 | ]), 46 | this.$element('li', 'btn_delete', [ 47 | this.$element('i', 'deleteI', { 48 | class: 'icons', text: 'delete'}), 49 | this.$element('span', 'deleteSpan', { 50 | text: 'Delete' 51 | }) 52 | ]), 53 | this.$element('li', 'btn_move_top', [ 54 | this.$element('i', 'moveTopI', { 55 | class: 'icons', 56 | text: 'vertical_align_top'}), 57 | this.$element('span', 'moveTopSpan', { 58 | text: 'Move to top' 59 | }) 60 | ]), 61 | this.$element('li', 'btn_move_bottom', [ 62 | this.$element('i', 'moveBottomI', { 63 | class: 'icons', 64 | text: 'vertical_align_bottom'}), 65 | this.$element('span', 'moveBottomSpan', { 66 | text: 'Move to bottom' 67 | }) 68 | ]) 69 | ]) 70 | ]) 71 | ); 72 | } 73 | 74 | _stringRound(input, precision) { 75 | var multiplier = Math.pow(10, precision), 76 | roundedInput = Math.round(input * multiplier) / multiplier, 77 | stringParts = String(roundedInput).split('.'); 78 | 79 | if(stringParts.length < 2) { 80 | stringParts[1] = ''; 81 | } 82 | 83 | if(stringParts[1].length !== precision) { 84 | stringParts[1] += '0'.repeat(precision - stringParts[1].length); 85 | } 86 | 87 | return stringParts.join('.'); 88 | } 89 | 90 | _setupRipple(element) { 91 | element.addEventListener('mousedown', (e) => { 92 | const 93 | bounds = element.getBoundingClientRect(), 94 | x = e.pageX - bounds.left, 95 | y = e.pageY - bounds.top; 96 | 97 | element.style.setProperty('--ripple-left', x); 98 | element.style.setProperty('--ripple-top', y); 99 | element.classList.add('animate'); 100 | element.mouseDown = true; 101 | element.isAnimating = true; 102 | }); 103 | element.addEventListener('mouseup', () => { 104 | if(!element.isAnimating) { 105 | element.classList.remove('animate'); 106 | } 107 | element.mouseDown = false; 108 | }); 109 | 110 | element.addEventListener('mouseout', () => { 111 | element.mouseDown = false; 112 | element.isAnimating = false; 113 | element.classList.remove('animate'); 114 | }); 115 | 116 | element.addEventListener('animationend', () => { 117 | element.isAnimating = false; 118 | if(!element.mouseDown) { 119 | element.classList.remove('animate'); 120 | } 121 | }); 122 | } 123 | 124 | _editQueue(method) { 125 | window.ngAPI.sendMessage( 126 | 'editqueue', 127 | [method, 0, '', [this._item.NZBID]], 128 | function() {}); 129 | } 130 | 131 | _setupContextEvents() { 132 | this._elements.btn_pause.addEventListener('mousedown', () => { 133 | const method = this._item.Status === 'PAUSED' ? 134 | 'GroupResume' : 135 | 'GroupPause'; 136 | this._editQueue(method); 137 | }); 138 | 139 | this._elements.btn_delete.addEventListener('mousedown', () => { 140 | modalDialog( 141 | 'Are you sure?', 142 | 'Do you want to delete
' + 143 | this._elements.title.innerHTML + '', 144 | [ 145 | { 146 | title: 'cancel' 147 | }, 148 | { 149 | title: 'delete', 150 | onClick: () => {this._editQueue('GroupDelete')} 151 | }, 152 | ] 153 | ); 154 | }); 155 | 156 | this._elements.btn_move_top.addEventListener( 157 | 'mousedown', () => this._editQueue('GroupMoveTop') 158 | ); 159 | 160 | this._elements.btn_move_bottom.addEventListener( 161 | 'mousedown', () => this._editQueue('GroupMoveBottom') 162 | ); 163 | 164 | this._elements.moreVert.addEventListener('click', () => { 165 | const label = 166 | this._item.Status === 'PAUSED' ? 167 | 'Resume' : 168 | 'Pause'; 169 | this._elements.contextmenu.classList.toggle('show'); 170 | this._elements.pauseSpan.innerText = label; 171 | }); 172 | } 173 | 174 | _createCategoryItem(itemText, id) { 175 | const item = this.$element('li', id, {text: itemText}); 176 | 177 | item.setAttribute('data-category', id); 178 | item.addEventListener('mousedown', (e) => { 179 | window.ngAPI.setGroupCategory( 180 | this._item.NZBID, 181 | e.currentTarget.getAttribute('data-category')); 182 | }); 183 | return item; 184 | } 185 | 186 | _setupCategoryMenu() { 187 | const categories = JSON.parse(ngAPI.Options.get('opt_categories')); 188 | const menu = this._elements.categoryContext.querySelector('ul'); 189 | 190 | menu.appendChild(this._createCategoryItem( 191 | 'No category', '' 192 | )); 193 | for(var category in categories) { 194 | menu.appendChild(this._createCategoryItem( 195 | categories[category], 196 | categories[category] 197 | )); 198 | } 199 | 200 | this._elements.categoryBadge.addEventListener('click', () => { 201 | this._elements.categoryContext.classList.toggle('show'); 202 | }); 203 | } 204 | 205 | _setProgress() { 206 | const progressChanged = 207 | this._updateProp('FileSizeHi') | 208 | this._updateProp('FileSizeLo') | 209 | this._updateProp('RemainingSizeHi') | 210 | this._updateProp('RemainingSizeLo') | 211 | this._updateProp('PausedSizeHi') | 212 | this._updateProp('PausedSizeLo') | 213 | this._updateProp('Status') | 214 | this._updateProp('ActiveDownloads'); 215 | 216 | const postChanged = 217 | this._updateProp('PostStageProgress') | 218 | this._updateProp('PostInfoText'); 219 | 220 | if(!progressChanged && !postChanged) { 221 | return; 222 | } 223 | 224 | let 225 | fileSize = window.ngAPI.parse.bigNumber( 226 | this._item.FileSizeHi, 227 | this._item.FileSizeLo), 228 | 229 | remainingSize = window.ngAPI.parse.bigNumber( 230 | this._item.RemainingSizeHi, 231 | this._item.RemainingSizeLo), 232 | 233 | pausedSize = window.ngAPI.parse.bigNumber( 234 | this._item.PausedSizeHi, 235 | this._item.PausedSizeLo), 236 | 237 | progressPercent = (fileSize - remainingSize) / fileSize * 100, 238 | pausedPercent = pausedSize / fileSize * 100, 239 | isPostProcessing = this._item.PostInfoText !== 'NONE'; 240 | 241 | if(isPostProcessing && postChanged) { 242 | /* Post process info */ 243 | progressPercent = this._item.PostStageProgress / 10; 244 | pausedPercent = 0; 245 | 246 | const postInfo = this._item.PostInfoText 247 | .replace(/_/g, ' ') 248 | .replace(/([.\/\\-])/g, '$1'); 249 | this._elements.postinfo.innerHTML = postInfo; 250 | } 251 | 252 | let statusText = 253 | this._stringRound(progressPercent, 1) + 254 | '% of ' + 255 | window.ngAPI.parse.toHRDataSize(fileSize); 256 | 257 | let statusTag = 'none'; 258 | 259 | if(this._item.Status !== 'DOWNLOADING') { 260 | statusText += ', ' + this._item.Status.toLowerCase(); 261 | } 262 | if(this._item.estRem && !isPostProcessing) { 263 | statusText += ' (~' + this._item.estRem + ' left)'; 264 | } 265 | 266 | if(['DOWNLOADING', 'PAUSED'].indexOf(this._item.Status) > -1) { 267 | statusTag = this._item.Status.toLowerCase(); 268 | } 269 | if(isPostProcessing) { 270 | statusTag = 'postprocess'; 271 | if(window.ngAPI.status.PostPaused) { 272 | statusTag = 'paused'; 273 | } 274 | } 275 | 276 | this._elements.barpct.style.width = 277 | progressPercent + '%'; 278 | this._elements.barPaused.style.width = 279 | pausedPercent + '%'; 280 | this._elements.progress_bar.className = 281 | 'progress_bar ' + statusTag; 282 | 283 | this._elements.text.innerText = statusText; 284 | } 285 | 286 | _updateProp(prop) { 287 | if(this._props[prop] === this._item[prop]) { 288 | return false; 289 | } 290 | this._props[prop] = this._item[prop]; 291 | return true; 292 | } 293 | 294 | _setName() { 295 | if(!this._updateProp('NZBName')) return; 296 | 297 | // this._elements.title.style.animationName = 'pulse'; 298 | // this._elements.title.classList.add('animate'); 299 | this._elements.title.innerHTML = this._item.NZBName 300 | .replace(/_/g, ' ') 301 | .replace(/([.\/\\-])/g, '$1'); 302 | } 303 | 304 | _setCategory() { 305 | if(!this._updateProp('Category')) return; 306 | 307 | if(this._item.Category) { 308 | this._elements.categoryBadge.innerText = this._item.Category; 309 | this._elements.categoryBadge.classList.remove('add'); 310 | } else { 311 | this._elements.categoryBadge.innerText = 'No category'; 312 | this._elements.categoryBadge.classList.add('add'); 313 | } 314 | } 315 | 316 | _setHealth() { 317 | if(!this._updateProp('Health')) return; 318 | 319 | let health = 100; 320 | if(this._item.Health < 1000) { 321 | health = Math.floor(this._item.Health / 10); 322 | } 323 | 324 | if(health < 100) { 325 | this._elements.healthWarning.innerText = health + '%' + ' health'; 326 | this._elements.healthWarning.classList.add('active'); 327 | } else { 328 | this._elements.healthWarning.innerText = ''; 329 | this._elements.healthWarning.classList.remove('active'); 330 | } 331 | } 332 | 333 | _refresh() { 334 | this._setName(); 335 | this._setCategory(); 336 | this._setHealth(); 337 | this._setProgress(); 338 | } 339 | 340 | _setup() { 341 | this._createMarkup(); 342 | 343 | this._setupRipple(this._elements.moreVert); 344 | this._setupRipple(this._elements.categoryBadge); 345 | 346 | this._setupContextEvents(); 347 | this._setupCategoryMenu(); 348 | 349 | this._elements.root.cls = this; 350 | this._props = {}; 351 | this._item = this._elements.root.item; 352 | 353 | this._elements.root.refresh = () => this._refresh(); 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /js/popup.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 'use strict'; 3 | var dragging = null, 4 | totalMBToDownload = 0, 5 | api = chrome.extension.getBackgroundPage().ngAPI, 6 | parse = api.parse; 7 | 8 | /** 9 | * function index() 10 | * Get elements "position" in relation to it's siblings 11 | * @return {integer} Number in stack 12 | */ 13 | Element.prototype.index = function() { 14 | if(!this.parentNode) { 15 | return -1; 16 | } 17 | return [].indexOf.call(this.parentNode.children, this); 18 | }; 19 | 20 | /** 21 | * function toHRTimeDiff() 22 | * Compares Date object to current Date and outputs human readable time diff 23 | * @param {Date} inputDate Date object 24 | * @return {string} Human readable time diff 25 | */ 26 | function toHRTimeDiff(inputDate) { 27 | var diff = ((new Date()).getTime() - inputDate.getTime()) / 1000, 28 | dayDiff = Math.floor(diff / 86400); 29 | 30 | if(isNaN(dayDiff) || dayDiff < 0) { 31 | return 0; 32 | } 33 | 34 | return dayDiff === 0 && ( 35 | diff < 60 && 'just now' || 36 | diff < 120 && '1 min ago' || 37 | diff < 3600 && Math.floor(diff / 60) + ' mins ago' || 38 | diff < 7200 && '1 hour ago' || 39 | diff < 86400 && Math.floor(diff / 3600) + ' hours ago') || 40 | dayDiff === 1 && 'Yesterday ' + inputDate.toLocaleTimeString() || 41 | inputDate.toLocaleString(); 42 | } 43 | 44 | /** 45 | * function zeroPad() 46 | * adds a zero before an integer if needed to make it a string 47 | * at least two characters wide 48 | * 49 | * @param {integer} inputNumber number to pad 50 | * @return {string} zero-padded input 51 | */ 52 | function zeroPad(inputNumber) { 53 | return (Number(inputNumber) < 10 ? '0' : '') + String(inputNumber); 54 | } 55 | 56 | /** 57 | * function toHRTimeLeft() 58 | * Formats an integer of seconds to a human readable string 59 | * 60 | * @param {integer} inputStamp timestamp 61 | * @return {string} Human readable time diff 62 | */ 63 | function toHRTimeLeft(inputStamp) { 64 | var days = Math.floor(inputStamp / 86400); 65 | if(days > 10) { 66 | return days + 'd'; 67 | } 68 | 69 | var hours = Math.floor(inputStamp % 86400 / 3600); 70 | if(days > 0) { 71 | return days + 'd ' + hours + 'h'; 72 | } 73 | 74 | var minutes = Math.floor(inputStamp / 60 % 60); 75 | if (hours > 0) { 76 | return hours + 'h ' + zeroPad(minutes) + 'm'; 77 | } 78 | 79 | var seconds = Math.floor(inputStamp % 60); 80 | if (minutes > 0) { 81 | return minutes + 'm ' + zeroPad(seconds) + 's'; 82 | } 83 | 84 | return seconds + 's'; 85 | } 86 | 87 | /** 88 | * function setupDraggable() 89 | * Setup drag'n'drop on an element with all associated event handlers 90 | * also adds a placeholder element that gets moved around while dragging 91 | * 92 | * @param {Element} post element to drag 93 | * @return {void} 94 | */ 95 | function setupDraggable(post) { 96 | post.setAttribute('draggable', true); 97 | post.placeholder = $E({tag: 'div', className: 'placeholder'}); 98 | post.indexBefore = 0; 99 | 100 | var dover = function (ev) { 101 | ev.preventDefault(); 102 | ev.dataTransfer.dropEffect = 'move'; 103 | 104 | if(this.tagName === 'NG-DOWNLOAD-ITEM') { 105 | if(dragging.offsetHeight) { 106 | dragging.storedHeight = dragging.offsetHeight; 107 | } 108 | if(dragging.storedHeight) { 109 | this.placeholder.style.height = dragging.storedHeight + 110 | 'px'; 111 | } 112 | dragging.style.display = 'none'; 113 | var pholders = this.parentElement.querySelectorAll( 114 | 'div.placeholder'); 115 | for(var i = 0; i < pholders.length; i++) { 116 | if(pholders[i] !== this.placeholder) { 117 | pholders[i].parentNode.removeChild(pholders[i]); 118 | } 119 | } 120 | 121 | this.parentElement.insertBefore( 122 | this.placeholder, 123 | this.placeholder.index() < this.index() ? 124 | this.nextSibling : 125 | this); 126 | } 127 | return false; 128 | }; 129 | var drop = function (ev) { 130 | ev.preventDefault(); 131 | this.parentElement.insertBefore( 132 | dragging, 133 | this.parentElement.querySelector('.placeholder')); 134 | if(dragging.indexBefore !== dragging.index()) { 135 | dragging.dispatchEvent(new CustomEvent('sortupdate', {detail: { 136 | oldIndex: dragging.indexBefore, 137 | newIndex: dragging.index(), 138 | nzbID: dragging.getAttribute('rel') 139 | } 140 | })); 141 | } 142 | dragging.dispatchEvent(new Event('dragend')); 143 | return false; 144 | }; 145 | post.addEventListener('dragstart', function(e){ 146 | dragging = this; 147 | dragging.indexBefore = this.index(); 148 | var dt = e.dataTransfer; 149 | dt.effectAllowed = 'move'; 150 | dt.setData('text/html', this.innerHTML); 151 | }); 152 | post.addEventListener('dragend', function(){ 153 | if (!dragging) { 154 | return; 155 | } 156 | dragging.style.display = 'flex'; 157 | var pholders = document.querySelectorAll('div.placeholder'); 158 | for(var i = 0; i < pholders.length; i++) { 159 | pholders[i].parentNode.removeChild(pholders[i]); 160 | } 161 | dragging = null; 162 | }); 163 | 164 | post.addEventListener('dragover', dover); 165 | post.addEventListener('dragenter', dover); 166 | post.addEventListener('drop', drop); 167 | post.addEventListener('sortupdate', function(e) { 168 | var oldI = e.detail.oldIndex, 169 | newI = e.detail.newIndex, 170 | diff = newI - oldI, 171 | fileId = window.ngAPI.groups[e.detail.nzbID].LastID; 172 | 173 | window.ngAPI.sendMessage( 174 | 'editqueue', [ 175 | 'GroupMoveOffset', 176 | diff, 177 | '', 178 | [fileId] 179 | ], function() {}); 180 | }); 181 | 182 | post.placeholder.addEventListener('dragover', dover); 183 | post.placeholder.addEventListener('dragenter', dover); 184 | post.placeholder.addEventListener('drop', drop); 185 | } 186 | 187 | /** 188 | * Add or update a download entry 189 | * 190 | * @param {object} item Download item 191 | * @return {void} 192 | */ 193 | function downloadPost(item) { 194 | var elm = document.querySelector('ng-download-item[rel="' + 195 | item.NZBID + '"]'), 196 | remainingMB = item.RemainingSizeMB - item.PausedSizeMB; 197 | 198 | if(!elm) { 199 | elm = $E({tag: 'ng-download-item', rel: item.NZBID}); 200 | elm.item = item; 201 | new NgDownloadItem(elm); 202 | setupDraggable(elm); 203 | document.querySelector('#download_list').appendChild(elm); 204 | } 205 | elm.refresh(); 206 | 207 | item.estRem = api.status.DownloadRate ? 208 | toHRTimeLeft((totalMBToDownload + remainingMB) * 209 | 1024 / (api.status.DownloadRate / 1024)) 210 | : ''; 211 | totalMBToDownload += remainingMB; 212 | } 213 | 214 | /** 215 | * function cleanupList() 216 | * Remove unneeded elements and resort the list if needed 217 | * 218 | * @param {array} dataObj Array of objects containing at least sortorder 219 | * @param {element} contEl container element for the list 220 | * @return {void} 221 | */ 222 | function cleanupList(dataObj, contEl) { 223 | var i = 0, 224 | sortNeeded = false, 225 | trElements = contEl.querySelectorAll('ng-download-item,div.post'); 226 | 227 | for(var k = 0; k < trElements.length; k++) { 228 | var id = trElements[k].item.NZBID; 229 | if(dataObj[id]) { 230 | if(i++ !== Number(dataObj[id].sortorder)) { 231 | sortNeeded = true; 232 | } 233 | } 234 | else { 235 | contEl.removeChild(trElements[k]); 236 | } 237 | } 238 | 239 | if(sortNeeded) { 240 | // Sort order changed externally 241 | var order = Object.keys(dataObj).sort(function(a, b) { 242 | return parseInt(dataObj[a].sortorder) - 243 | parseInt(dataObj[b].sortorder); 244 | }); 245 | for(var j in order) { 246 | var el = contEl.querySelector( 247 | 'ng-download-item[rel="' + order[j] + '"]'); 248 | if(el) { 249 | contEl.appendChild(contEl.removeChild(el)); 250 | } 251 | } 252 | 253 | } 254 | } 255 | 256 | /** 257 | * function onStatusUpdated() 258 | * 259 | * Triggered whenever status is updated from server. 260 | * Sets speed, remaining and diskspace labels and resets 261 | * pause/resume button. 262 | * 263 | * @return {void} 264 | */ 265 | function onStatusUpdated(){ 266 | if(!window.ngAPI.status) { 267 | return; 268 | } 269 | var downloadPaused = window.ngAPI.status.Download2Paused; 270 | 271 | document.querySelector('#tgl_pause').innerText = downloadPaused ? 272 | 'play_arrow' : 273 | 'pause'; 274 | 275 | // Set "global" labels 276 | var speedLabel = ''; 277 | if(window.ngAPI.status.DownloadRate) { 278 | speedLabel = parse.toHRDataSize( 279 | Number(window.ngAPI.status.DownloadRate)) + '/s'; 280 | } 281 | else { 282 | speedLabel = downloadPaused ? '- PAUSED -' : ''; 283 | } 284 | 285 | var remainingLbl = parse.bigNumber( 286 | window.ngAPI.status.RemainingSizeHi, 287 | window.ngAPI.status.RemainingSizeLo); 288 | 289 | document.querySelector('#lbl_speed').innerText = speedLabel; 290 | document.querySelector('#lbl_remainingmb').innerText = 291 | remainingLbl === 0 ? '' : parse.toHRDataSize(remainingLbl); 292 | document.querySelector('#lbl_remainingdisk').innerText = 293 | parse.toHRDataSize( 294 | parse.bigNumber( 295 | window.ngAPI.status.FreeDiskSpaceHi, 296 | window.ngAPI.status.FreeDiskSpaceLo)) + 297 | ' free'; 298 | } 299 | 300 | /** 301 | * function onGroupsUpdated() 302 | * Triggered when groups are updated from server. 303 | * 304 | * @return {void} 305 | */ 306 | function onGroupsUpdated(){ 307 | var sortable = []; 308 | for (var i in window.ngAPI.groups) 309 | sortable.push(window.ngAPI.groups[i]); 310 | 311 | sortable.sort(function(a, b) { 312 | return parseInt(a.sortorder) - parseInt(b.sortorder); 313 | }); 314 | 315 | 316 | // Build or update active download list 317 | totalMBToDownload = 0; 318 | for(var k in sortable) { 319 | downloadPost(sortable[k]); 320 | } 321 | 322 | cleanupList( 323 | window.ngAPI.groups, 324 | document.querySelector('#download_list')); 325 | 326 | var inactiveContainer = 327 | document.querySelector('#download_container .inactive'); 328 | if(!Object.keys(window.ngAPI.groups).length) { 329 | inactiveContainer.classList.add('shown'); 330 | } 331 | else { 332 | inactiveContainer.classList.remove('shown'); 333 | } 334 | } 335 | 336 | /** 337 | * Add or update a history entry 338 | * 339 | * @param {object} item Download item 340 | * @return {void} 341 | */ 342 | function historyPost(item) { 343 | var parsed = window.ngAPI.parse.historyStatus(item), 344 | post = null; 345 | item.status = parsed[0]; 346 | 347 | post = document.querySelector( 348 | '#history_list [rel="' + item.NZBID + '"]'); 349 | var update = post !== null; 350 | 351 | if(update) { 352 | post.querySelector('.left').innerText = toHRTimeDiff( 353 | new Date(item.HistoryTime * 1000)); 354 | } 355 | else { 356 | post = $E({tag: 'div', className: 'post', rel: item.NZBID}); 357 | post.item = item; 358 | 359 | let [status, reason] = item.Status.toLowerCase().split('/') || 360 | [item.status, '']; 361 | 362 | // Tag 363 | post.appendChild($E({tag: 'div', className: 'tag ' + item.status})) 364 | .appendChild($E({tag: 'span', text: status})) 365 | .appendChild($E({ 366 | tag: 'small', 367 | text: status === 'success' ? '' : reason 368 | })); 369 | 370 | // Info 371 | var info = post.appendChild($E({tag: 'div', className: 'info'})); 372 | info.appendChild($E({ 373 | tag: 'div', 374 | text: item.Name, 375 | className: 'title'})); 376 | var details = info.appendChild($E({ 377 | tag: 'div', 378 | className: 'details'})); 379 | details.appendChild($E({ 380 | tag: 'div', 381 | text: toHRTimeDiff( 382 | new Date(item.HistoryTime * 1000)), 383 | className: 'left'})); 384 | details.appendChild($E({ 385 | tag: 'div', 386 | text: parse.toHRDataSize( 387 | parse.bigNumber(item.FileSizeHi, 388 | item.FileSizeLo)), 389 | className: 'right'})); 390 | 391 | document.querySelector('#history_list').appendChild(post); 392 | } 393 | 394 | return post; 395 | } 396 | 397 | function searchHistory(historyList) { 398 | var srchElement = document.querySelector('.search'), 399 | filteredList = []; 400 | if(!srchElement.value) { 401 | return false; 402 | } 403 | for(var i = 0; i < historyList.length; i++) { 404 | if(!historyList[i].Name 405 | .toLowerCase() 406 | .replace(/[^0-9a-z]+/g, ' ') 407 | .match(srchElement.value 408 | .toLowerCase() 409 | .replace(/[^0-9a-z]+/g, ' '))) { 410 | continue; 411 | } 412 | filteredList.push(historyList[i]); 413 | } 414 | return filteredList; 415 | } 416 | 417 | /** 418 | * function onHistoryUpdated() 419 | * Triggered when history is updated from server. 420 | * 421 | * @return {void} 422 | */ 423 | function onHistoryUpdated() { 424 | var history = []; 425 | window.ngAPI.history(function(j) { 426 | var historyList = j.result, 427 | filteredList = searchHistory(historyList); 428 | if(filteredList !== false) { 429 | historyList = filteredList; 430 | } 431 | for(var i = 0; 432 | i < window.ngAPI.Options.get('opt_historyitems') && 433 | i < historyList.length; 434 | i++) { 435 | history[historyList[i].NZBID] = historyList[i]; 436 | history[historyList[i].NZBID].sortorder = i; 437 | historyPost(historyList[i]); 438 | } 439 | }); 440 | 441 | cleanupList(history, document.querySelector('#history_list')); 442 | } 443 | 444 | function resetTabs() { 445 | var tabs = document.querySelectorAll('.tab'); 446 | for(let tab of tabs) { 447 | tab.classList.remove('active'); 448 | let container = document.getElementById( 449 | tab.getAttribute('data-container')); 450 | container.classList.remove('active'); 451 | } 452 | } 453 | 454 | function switchTab(event) { 455 | resetTabs(); 456 | event.target.classList.add('active'); 457 | let container = document.getElementById( 458 | event.target.getAttribute('data-container')); 459 | if(container.id === 'history_container') { 460 | onHistoryUpdated(); 461 | } 462 | container.classList.add('active'); 463 | } 464 | 465 | document.addEventListener('DOMContentLoaded', function() { 466 | chrome.runtime.connect(); 467 | window.ngAPI = chrome.extension.getBackgroundPage().ngAPI; 468 | var tabs = document.querySelectorAll('.tab'); 469 | for(var tabIndex = 0; tabIndex < tabs.length; tabIndex++) { 470 | tabs[tabIndex].addEventListener('click', switchTab); 471 | } 472 | 473 | if(!window.ngAPI.isInitialized || !window.ngAPI.connectionStatus) { 474 | modalDialog( 475 | 'Connection failure!', 476 | 'Could not connect to NZBGet.
' + 477 | 'Please ensure that the server is running and ' + 478 | 'check your connection settings.', 479 | [{title: 'options page', href: 'options.html'}] 480 | ); 481 | } 482 | 483 | chrome.runtime.onMessage.addListener( 484 | function(request) { 485 | switch(request.statusUpdated) { 486 | case 'groups': 487 | onGroupsUpdated(); 488 | break; 489 | case 'history': 490 | if(window.ngAPI.Options.get('opt_historyitems') > 0) { 491 | setTimeout(onHistoryUpdated, 1500); 492 | } 493 | break; 494 | case 'status': 495 | onStatusUpdated(); 496 | break; 497 | } 498 | } 499 | ); 500 | onGroupsUpdated(); 501 | 502 | document.querySelector('.search') 503 | .addEventListener('search', function() { 504 | onHistoryUpdated(); 505 | }); 506 | 507 | onStatusUpdated(); 508 | document.body.addEventListener('mousedown', function() { 509 | var menus = document.querySelectorAll('.contextmenu'); 510 | for(var i=0; i < menus.length; i++) { 511 | menus[i].classList.remove('show'); 512 | } 513 | }); 514 | 515 | document.querySelector('#tgl_pause') 516 | .addEventListener('click', function() { 517 | var method = !window.ngAPI.status.Download2Paused ? 518 | 'pausedownload2' : 519 | 'resumedownload2'; 520 | window.ngAPI.sendMessage(method, [], function() { 521 | window.ngAPI.updateStatus(); 522 | window.ngAPI.updateGroups(); 523 | }); 524 | }); 525 | 526 | document.querySelector('#logo') 527 | .addEventListener('click', function() { 528 | window.ngAPI.switchToNzbGetTab(); 529 | }); 530 | }); 531 | })(); 532 | -------------------------------------------------------------------------------- /js/nzbget.js: -------------------------------------------------------------------------------- 1 | /** 2 | * nzbGetAPI - Base API Class 3 | */ 4 | (function(){ 5 | 'use strict'; 6 | window.ngAPI = { 7 | groupTimer: null, 8 | statusTimer: null, 9 | groups: {}, 10 | connectionStatus: true, 11 | isInitialized: false, 12 | appVersion: 0, 13 | appName: '', 14 | activeNotifications: {}, 15 | /** 16 | * Setup version information 17 | * 18 | * @param {function} successFunc Callback function on success 19 | * @param {function} failFunc Callback function on failure 20 | * @param {object} tmpOptions Temporary option object 21 | * @return {void} 22 | */ 23 | version: function(successFunc, failFunc, tmpOptions) { 24 | return this.sendMessage( 25 | 'version', {}, successFunc, failFunc, tmpOptions); 26 | }, 27 | /** 28 | * Set group category 29 | * 30 | * @param {int} groupId ID 31 | * @param {string} categoryName Name of new category 32 | * @param {function} successFunc Callback function on success 33 | * @param {function} failFunc Callback function on failure 34 | * @return {void} 35 | */ 36 | setGroupCategory: function(groupId, categoryName, 37 | successFunc, failFunc) { 38 | if(!successFunc) { 39 | successFunc = ngAPI.updateGroups; 40 | } 41 | return ngAPI.sendMessage( 42 | 'editqueue', [ 43 | 'GroupApplyCategory', 44 | 0, 45 | categoryName, 46 | [groupId] 47 | ], successFunc, failFunc); 48 | }, 49 | /** 50 | * Set group name 51 | * 52 | * @param {int} groupId ID 53 | * @param {string} groupName New name 54 | * @param {function} successFunc Callback function on success 55 | * @param {function} failFunc Callback function on failure 56 | * @return {void} 57 | */ 58 | setGroupName: function(groupId, groupName, 59 | successFunc, failFunc) { 60 | if(!successFunc) { 61 | successFunc = ngAPI.updateGroups; 62 | } 63 | return ngAPI.sendMessage( 64 | 'editqueue', [ 65 | 'GroupSetName', 66 | 0, 67 | groupName, 68 | [groupId] 69 | ], successFunc, failFunc); 70 | }, 71 | updateCategories: function() { 72 | ngAPI.sendMessage('config', {}, this.parseCategories.bind(this)); 73 | }, 74 | parseCategories: function(data) { 75 | var result = data.result, 76 | categories = []; 77 | 78 | if(!result || !result.length) { 79 | return false; 80 | } 81 | 82 | for(var i in result) { 83 | var match = result[i].Name.match('Category([0-9]+)\.Name'); 84 | if(match) { 85 | categories.push(result[i].Value); 86 | } 87 | } 88 | this.Options.set('opt_categories', JSON.stringify(categories)); 89 | }, 90 | /** 91 | * Request history from JSON-RPC 92 | * 93 | * @param {function} successFunc Callback function on success 94 | * @return {void} 95 | */ 96 | history: function(successFunc) { 97 | return this.sendMessage('history', {}, successFunc); 98 | }, 99 | /** 100 | * Setup context menu item(s) 101 | * 102 | * @return {void} 103 | */ 104 | loadMenu: function(){ 105 | chrome.contextMenus.removeAll(); 106 | 107 | chrome.contextMenus.create({ 108 | contexts: ['link'], 109 | id: 'root', 110 | title: 'Send to NZBGet', 111 | onclick: window.ngAPI.addLink.bind(this) 112 | }); 113 | 114 | var categories = JSON.parse(this.Options.get('opt_categories')); 115 | if(categories && categories.length) { 116 | chrome.contextMenus.create({ 117 | contexts: ['link'], 118 | parentId: 'root', 119 | title: 'No category', 120 | id: 'cat:', 121 | onclick: window.ngAPI.addLink.bind(this) 122 | }); 123 | chrome.contextMenus.create({ 124 | contexts: ['link'], 125 | parentId: 'root', 126 | type: 'separator' 127 | }); 128 | for(var i in categories) { 129 | chrome.contextMenus.create({ 130 | contexts: ['link'], 131 | parentId: 'root', 132 | title: categories[i], 133 | id: 'cat:' + i, 134 | onclick: window.ngAPI.addLink.bind(this) 135 | }); 136 | } 137 | } 138 | }, 139 | /** 140 | * Build a URL-object from options 141 | * 142 | * @param {object} altOptions Optional alternate options 143 | * @param {string} pathAdd Path to append 144 | * @return {void} 145 | */ 146 | buildServerURI: function(altOptions, pathAdd) { 147 | var opt = altOptions && altOptions.get ? 148 | altOptions : 149 | window.ngAPI.Options; 150 | var x = new URL('http://localhost'); 151 | 152 | var keys = [ 153 | 'protocol', 154 | 'port', 155 | 'pathname', 156 | 'host']; 157 | 158 | for(var i in keys) { 159 | x[keys[i]] = opt.get('opt_' + keys[i]); 160 | } 161 | 162 | // Ensure pathname always does not end with a slash 163 | x.pathname = x.pathname.replace(/\/$/, '') + 164 | (pathAdd ? pathAdd : ''); 165 | 166 | return x.href; 167 | }, 168 | 169 | /** 170 | * Send XHR Request to NZBGet via JSON-RPC 171 | * 172 | * @param {string} method "method" to call 173 | * @param {array} params parameters to send 174 | * @param {function} successFunc function to execute on success. 175 | * @param {function} failFunc function to execute on failure 176 | * @param {object} altOptions use connection options 177 | * from provided object 178 | * @return {void} 179 | */ 180 | sendMessage: function(method, params, successFunc, 181 | failFunc, altOptions) { 182 | var opt = typeof altOptions === 'object' ? 183 | altOptions : 184 | this.Options; 185 | var username = opt.get('opt_username'), 186 | password = opt.get('opt_password'), 187 | query = { 188 | version: '1.1', 189 | method: method, 190 | params: params 191 | }; 192 | 193 | var xhr = new XMLHttpRequest(); 194 | xhr.timeout = 5000; 195 | xhr.ontimeout = function(){ 196 | this.setSuccess(false); 197 | if(typeof failFunc === 'function'){ 198 | failFunc('Timed out after 5 secs.'); 199 | } 200 | }.bind(this); 201 | 202 | xhr.onreadystatechange = function(r){ 203 | if (xhr.readyState === 4) { 204 | if(xhr.status === 200) { 205 | if(typeof altOptions !== 'object') { 206 | this.setSuccess(true); 207 | } 208 | if(typeof successFunc === 'function') { 209 | successFunc( 210 | r.target.responseText ? 211 | JSON.parse(r.target.responseText) : 212 | ''); 213 | } 214 | } else { 215 | if(typeof altOptions !== 'object') { 216 | this.setSuccess(false); 217 | } 218 | if(typeof failFunc === 'function'){ 219 | failFunc( 220 | r.target.statusText ? r.target.statusText : ''); 221 | } 222 | } 223 | } 224 | }.bind(this); 225 | 226 | xhr.open( 227 | 'POST', 228 | window.ngAPI.buildServerURI(opt, '/jsonrpc')); 229 | 230 | xhr.setRequestHeader('Content-Type', 'text/json'); 231 | xhr.setRequestHeader( 232 | // Use Authorization header instead of passing user/pass. 233 | // Otherwise request fails on larger nzb-files!? 234 | 'Authorization', 235 | 'Basic ' + window.btoa(username + ':' + password)); 236 | try { 237 | xhr.send(JSON.stringify(query)); 238 | } 239 | catch (e){ 240 | if(typeof altOptions !== 'object') { 241 | this.setSuccess(false); 242 | } 243 | if(typeof failFunc === 'function'){ 244 | failFunc(e.name ? e.name : ''); 245 | } 246 | } 247 | 248 | if(!successFunc) { 249 | return JSON.parse(xhr.responseText); 250 | } 251 | }, 252 | setSuccess: function(boo) { 253 | if(boo) { 254 | this.connectionStatus = true; 255 | } 256 | else { 257 | chrome.browserAction.setBadgeBackgroundColor( 258 | {color: '#ff0000'}); 259 | chrome.browserAction.setBadgeText( 260 | {text: 'ERR'}); 261 | this.connectionStatus = false; 262 | } 263 | }, 264 | addURL: function(url, tab, ident, category, nameOverride) { 265 | var nzbFileName = url.match(/\/([^\/]+)$/)[1]; 266 | var xhr = new XMLHttpRequest(); 267 | 268 | if(!category) { 269 | category = ''; 270 | } 271 | xhr.onreadystatechange = function(){ 272 | if(xhr.readyState !== 4) { 273 | return; 274 | } 275 | if(xhr.status === 200) { 276 | if(xhr.getResponseHeader('Content-Type') 277 | .indexOf('application/x-nzb') > -1) { 278 | if(xhr.getResponseHeader('X-DNZB-Category')) { 279 | category = xhr.getResponseHeader( 280 | 'X-DNZB-Category'); 281 | } 282 | 283 | /* Replace NZB file name with the one specified in 284 | response header if available. */ 285 | var disposition = xhr.getResponseHeader( 286 | 'Content-Disposition'); 287 | if(disposition) { 288 | var rawName = disposition.replace( 289 | /.+filename=["]?([^";]+).*$/i, 290 | '$1'); 291 | if(rawName) { 292 | // Remove potential path from filename and 293 | // strip .nzb-extension if present 294 | nzbFileName = rawName.replace( 295 | /(.+[\/\\]{1})?(.+?)(\.nzb)?$/i, 296 | '$2'); 297 | } 298 | } 299 | window.ngAPI.sendMessage( 300 | 'append', 301 | [ 302 | nameOverride ? 303 | nameOverride : 304 | nzbFileName + '.nzb', 305 | category, 306 | 0, 307 | false, 308 | window.btoa(xhr.responseText) 309 | ], 310 | function() { 311 | window.ngAPI.updateGroups(); 312 | if(!tab) { 313 | return; 314 | } 315 | if(ident) { 316 | chrome.tabs.sendMessage( 317 | tab, { 318 | message: 'addedurl', 319 | url: url, 320 | status: true, 321 | id: ident 322 | } 323 | ); 324 | } 325 | window.ngAPI.cacheDb.addURLObj(url); 326 | }, 327 | function() { 328 | window.ngAPI.notify({ 329 | title: 'Error', 330 | message: 'Could not download link.', 331 | iconUrl: 'img/error80.png', 332 | contextMessage: url 333 | }, url, tab); 334 | if(!tab) { 335 | return; 336 | } 337 | if(ident) { 338 | chrome.tabs.sendMessage( 339 | tab, 340 | { 341 | message: 'addedurl', 342 | url: url, 343 | status: false, 344 | id: ident 345 | } 346 | ); 347 | } 348 | } 349 | ); 350 | } else { 351 | window.ngAPI.notify({ 352 | title: 'Error', 353 | message: 'Received invalid NZB-file.', 354 | iconUrl: 'img/error80.png', 355 | contextMessage: url 356 | }); 357 | } 358 | } else { 359 | window.ngAPI.notify({ 360 | title: 'Error', 361 | message: 'Download failed.', 362 | contextMessage: url 363 | }, 364 | url, 365 | tab 366 | ); 367 | if(tab && ident) { 368 | chrome.tabs.sendMessage( 369 | tab, 370 | { 371 | message: 'addedurl', 372 | url: url, 373 | status: false, 374 | id: ident 375 | } 376 | ); 377 | } 378 | } 379 | }; 380 | xhr.open('GET', url); 381 | xhr.send(); 382 | }, 383 | 384 | /** 385 | * Download a file and try to send it to NZBGet 386 | * 387 | * @param {object} info chrome OnClickData-object 388 | * @param {object} tab chrome tabs.Tab-object 389 | * @return {void} 390 | */ 391 | addLink: function(info, tab) { 392 | var category = ''; 393 | if(info.menuItemId && info.menuItemId !== 'root') { 394 | var id = info.menuItemId.split(':'), 395 | categories = JSON.parse( 396 | this.Options.get('opt_categories')); 397 | if(id[1]) { 398 | category = categories[id[1]]; 399 | } 400 | } 401 | this.addURL(info.linkUrl, tab, null, category); 402 | }, 403 | /** 404 | * Locate existing NZBGet-tab or open a new one 405 | * @return {void} 406 | */ 407 | switchToNzbGetTab: function() { 408 | chrome.tabs.query({ 409 | url: window.ngAPI.buildServerURI(null, '/*') 410 | }, function(tabs) { 411 | if(tabs.length) { 412 | chrome.tabs.update(tabs[0].id, {selected: true}); 413 | } else { 414 | chrome.tabs.create( 415 | {url: window.ngAPI.buildServerURI()}); 416 | } 417 | }); 418 | }, 419 | /** 420 | * Display notification for 5 sec 421 | * 422 | * @param {object} opt Notification option object 423 | * @param {string} url Optional URL for retry purposes 424 | * @param {integer} tab Optional tab id for addURL 425 | * @return {void} 426 | */ 427 | notify: function(opt, url, tab) { 428 | if(window.ngAPI.Options.get('opt_notifications') === false || 429 | typeof opt !== 'object' || !chrome.notifications) { 430 | return; 431 | } 432 | 433 | if(typeof opt.iconUrl === 'undefined') { 434 | opt.iconUrl = 'img/square80.png'; 435 | } 436 | if(!opt.type) { 437 | opt.type = 'basic'; 438 | } 439 | if(url && !opt.buttons) { 440 | opt.buttons = [ 441 | {title: 'Try again', iconUrl: 'img/refresh24.png'} 442 | ]; 443 | } 444 | 445 | chrome.notifications.create('', opt, function(nId){ 446 | if(url) { 447 | window.ngAPI.activeNotifications[nId] = { 448 | url: url, 449 | tab: tab 450 | }; 451 | } 452 | }); 453 | }, 454 | /** 455 | * Request new status information via NZBGET JSON-RPC. 456 | * 457 | * @return {void} 458 | */ 459 | updateStatus: function() { 460 | this.sendMessage('status', {}, function(j){ 461 | this.status = j.result; 462 | chrome.runtime.sendMessage({statusUpdated: 'status'}); 463 | if(this.status.Download2Paused === true || 464 | this.status.DownloadPaused === true) { 465 | chrome.browserAction.setBadgeBackgroundColor({ 466 | color: '#f09229'}); 467 | } else { 468 | chrome.browserAction.setBadgeBackgroundColor({ 469 | color: '#468847'}); 470 | } 471 | }.bind(this)); 472 | }, 473 | /** 474 | * Tries to show appropriate notification based on status in history. 475 | * 476 | * @param {object} post NZBGet group object 477 | * @return {void} 478 | */ 479 | notifyDownloadStatus: function(post) { 480 | window.ngAPI.history(function(r) { 481 | for(var i in r.result) { 482 | if(r.result[i].NZBID === post.NZBID) { 483 | var status = 484 | window.ngAPI.parse.historyStatus(r.result[i]); 485 | 486 | var nob = { 487 | title: 'Download complete!', 488 | message: post.NZBName, 489 | iconUrl: 'img/square80.png' 490 | }; 491 | 492 | switch(status[0]) { 493 | case 'success': 494 | break; 495 | case 'warning': 496 | nob.title = 'Download finished with warnings!'; 497 | break; 498 | case 'deleted': 499 | if(status[1] && status[1] === 'manual') { 500 | return; 501 | } 502 | if(status[1] && status[1] === 'dupe') { 503 | nob.title = 'Duplicate download removed'; 504 | nob.iconUrl = 'img/error80.png'; 505 | } 506 | break; 507 | default: 508 | nob.title = 'Download failed!'; 509 | nob.contextMessage = status[1] ? 510 | 'Reason: ' + status[1] : 511 | ''; 512 | nob.iconUrl = 'img/error80.png'; 513 | } 514 | window.ngAPI.notify(nob); 515 | break; 516 | } 517 | } 518 | }); 519 | }, 520 | /** 521 | * Request new group information via NZBGET JSON-RPC. 522 | * 523 | * Notifies on complete downloads 524 | * Updates badge on active downloads. 525 | * 526 | * @return {void} 527 | */ 528 | updateGroups: function() { 529 | ngAPI.sendMessage('listgroups', [], function(j) { 530 | var newIDs = []; 531 | for(var i in j.result) { 532 | var id = j.result[i].NZBID; 533 | newIDs[id] = true; 534 | if(ngAPI.groups[id]) { 535 | for(var attr in j.result[i]) { 536 | ngAPI.groups[id][attr] = j.result[i][attr]; 537 | } 538 | } 539 | else { 540 | ngAPI.groups[id] = j.result[i]; 541 | } 542 | ngAPI.groups[id].sortorder = i; 543 | } 544 | if(ngAPI.groups) { 545 | for(i in ngAPI.groups) { 546 | if(!newIDs[i]) { 547 | window.ngAPI.notifyDownloadStatus(ngAPI.groups[i]); 548 | delete ngAPI.groups[i]; 549 | chrome.runtime.sendMessage( 550 | {statusUpdated: 'history'}); 551 | } 552 | } 553 | } 554 | chrome.browserAction.setBadgeText({ 555 | text: j.result.length ? 556 | j.result.length.toString() : 557 | ''}); 558 | chrome.runtime.sendMessage({statusUpdated: 'groups'}); 559 | }); 560 | }, 561 | /** 562 | * Setup polling timers and stuff 563 | * 564 | * @return {bool} success 565 | */ 566 | initialize: function(){ 567 | if(this.groupTimer) { 568 | clearInterval(this.groupTimer); 569 | } 570 | if(this.statusTimer) { 571 | clearInterval(this.statusTimer); 572 | } 573 | if(window.ngAPI.Options.get('opt_host').length === 0) { 574 | this.isInitialized = false; 575 | return; 576 | } 577 | 578 | chrome.browserAction.setBadgeText({text: ''}); 579 | 580 | var manifest = chrome.runtime.getManifest(); 581 | this.appName = manifest.name; 582 | this.appVersion = manifest.version; 583 | 584 | chrome.browserAction.setTitle({ 585 | title: this.appName + ' v' + this.appVersion}); 586 | 587 | this.status = { 588 | DownloadRate: 0, 589 | RemainingSizeMB: 0, 590 | RemainingSizeLo: 0, 591 | Download2Paused: false, 592 | DownloadPaused: false 593 | }; 594 | this.updateCategories(); 595 | this.updateGroups(); 596 | this.updateStatus(); 597 | this.loadMenu(); 598 | 599 | this.groupTimer = setInterval(this.updateGroups.bind(this), 5000); 600 | this.statusTimer = setInterval(this.updateStatus.bind(this), 5000); 601 | 602 | window.ngAPI.cacheDb.open(); 603 | 604 | this.isInitialized = true; 605 | return; 606 | }, 607 | /** 608 | * IndexedDB abstraction storing info from URLS to recognize previously 609 | * added values 610 | */ 611 | cacheDb: { 612 | dbRes: null, 613 | aEl: document.createElement('a'), 614 | open: function() { 615 | if(!('indexedDB' in window)) { 616 | return; 617 | } 618 | var req = indexedDB.open('nzbgc_cache', 1); 619 | var cdb = this; 620 | req.onsuccess = function() { 621 | cdb.dbRes = this.result; 622 | }; 623 | req.onerror = function () { 624 | }; 625 | req.onupgradeneeded = function(e) { 626 | var thisDB = e.currentTarget.result; 627 | 628 | if(!thisDB.objectStoreNames.contains('urls')) { 629 | var store = thisDB.createObjectStore('urls', { 630 | autoIncrement: true 631 | }); 632 | store.createIndex( 633 | 'main', 634 | ['domain', 'id'], 635 | {unique: true}); 636 | } 637 | }; 638 | }, 639 | addURLObj: function(url){ 640 | if(!window.ngAPI.Options.get('opt_rememberurls')) { 641 | return; 642 | } 643 | var store = this.getObjectStore('urls', 'readwrite'), 644 | obj = this.objFromURL(url); 645 | obj.time_added = new Date().valueOf(); 646 | var req = store.add(obj); 647 | req.onerror = function() { 648 | }; 649 | }, 650 | checkURLObj: function(url, callback) { 651 | if(!window.ngAPI.Options.get('opt_rememberurls')) { 652 | return; 653 | } 654 | var store = this.getObjectStore('urls', 'readonly'), 655 | index = store.index('main'), 656 | obj = this.objFromURL(url), 657 | request = index.get(IDBKeyRange.only([obj.domain, obj.id])); 658 | 659 | request.onsuccess = function(e) { 660 | var result = e.target.result; 661 | if(typeof callback !== 'undefined') { 662 | callback(typeof result !== 'undefined'); 663 | } 664 | }; 665 | }, 666 | getObjectStore: function(storeName, mode) { 667 | var tx = this.dbRes.transaction(storeName, mode); 668 | return tx.objectStore(storeName); 669 | }, 670 | objFromURL: function(url) { 671 | // Workaround for broken searchstrings 672 | var atPos = url.indexOf('&'); 673 | if(url.indexOf('?') === -1 && atPos > -1) { 674 | url = url.substring(0, atPos) + 675 | '?' + 676 | url.substring(atPos + 1); 677 | } 678 | /* Use A-element instead of URL() because A can handle 679 | relative URLs */ 680 | this.aEl.href = url; 681 | var osObj = { 682 | domain: this.aEl.host, 683 | id: this.aEl.pathname + this.aEl.search 684 | }; 685 | 686 | // Try to shorten URL based on a simple regex pattern 687 | var match = this.aEl.pathname.match( 688 | /\/[0-9a-z_]+\/([0-9a-z]+)/); 689 | if(match) { 690 | osObj.id = match[1]; 691 | } 692 | return osObj; 693 | } 694 | 695 | }, 696 | /** 697 | * Option abstraction object. Handles everyting option related. 698 | */ 699 | Options: { 700 | defaults: { 701 | opt_port: 6789, 702 | opt_username: 'nzbget', 703 | opt_password: 'tegbzn6789', 704 | opt_pathname: '/', 705 | opt_historyitems: 30, 706 | opt_protocol: 'http', 707 | opt_rememberurls: false, 708 | opt_notifications: true, 709 | opt_categories: '[]' 710 | }, 711 | load: function() { 712 | Array.each( 713 | document.querySelectorAll( 714 | 'input[type=text],input[type=password],select'), 715 | function(o){ 716 | o.value = this.get(o.id); 717 | }, this); 718 | }, 719 | save: function() { 720 | Array.each( 721 | document.querySelectorAll( 722 | 'input[type=text],input[type=password],select'), 723 | function(o){ 724 | localStorage[o.id] = o.value; 725 | }, this); 726 | }, 727 | get: function(opt) { 728 | if(typeof localStorage[opt] !== 'undefined') { 729 | if(['true', 'false'].indexOf(localStorage[opt]) > -1){ 730 | return localStorage[opt] === 'true'; 731 | } 732 | return localStorage[opt]; 733 | } 734 | else if(typeof this.defaults[opt] !== 'undefined') { 735 | return this.defaults[opt]; 736 | } 737 | return ''; 738 | }, 739 | set: function(opt, value) { 740 | localStorage[opt] = value; 741 | } 742 | } 743 | }; 744 | /** 745 | * Setup notifications 746 | * 747 | * @return {void} 748 | */ 749 | function prepareNotifications() { 750 | if(!chrome.notifications) { 751 | return; // No notification support 752 | } 753 | 754 | chrome.notifications.onButtonClicked.addListener(function(nId){ 755 | var not = window.ngAPI.activeNotifications[nId]; 756 | 757 | window.ngAPI.addURL(not.url, not.tab); 758 | chrome.notifications.clear(nId, function() {}); 759 | 760 | delete window.ngAPI.activeNotifications[nId]; 761 | }); 762 | 763 | chrome.notifications.onClicked.addListener(function(nId) { 764 | window.ngAPI.switchToNzbGetTab(); 765 | if(window.ngAPI.activeNotifications[nId]) { 766 | delete window.ngAPI.activeNotifications[nId]; 767 | } 768 | }); 769 | 770 | chrome.notifications.onClosed.addListener(function(nId) { 771 | if(window.ngAPI.activeNotifications[nId]) { 772 | delete window.ngAPI.activeNotifications[nId]; 773 | } 774 | }); 775 | } 776 | 777 | document.addEventListener('DOMContentLoaded', function() { 778 | // Chrome <35 compatibility 779 | if(!window.URL && window.webkitURL) { 780 | window.URL = window.webkitURL; 781 | } 782 | 783 | window.ngAPI.initialize(); 784 | 785 | prepareNotifications(); 786 | 787 | chrome.runtime.onMessage.addListener(function(m, sender, respCallback) { 788 | if(m.message === 'optionsUpdated') { 789 | window.ngAPI.initialize(); 790 | } else if(m.message === 'addURL') { 791 | window.ngAPI.addURL( 792 | m.href, 793 | sender.tab.id, 794 | m.id, 795 | m.category ? m.category : null, 796 | m.nameOverride ? m.nameOverride : null 797 | ); 798 | } else if(m.message === 'checkCachedURL') { 799 | window.ngAPI.cacheDb.checkURLObj(m.url, respCallback); 800 | return true; 801 | } 802 | }); 803 | chrome.runtime.onConnect.addListener(function(port) { 804 | port.onDisconnect.addListener(function(){ 805 | clearInterval(window.ngAPI.groupTimer); 806 | clearInterval(window.ngAPI.statusTimer); 807 | window.ngAPI.groupTimer = setInterval( 808 | window.ngAPI.updateGroups.bind(window.ngAPI), 809 | 5000); 810 | window.ngAPI.statusTimer = setInterval( 811 | window.ngAPI.updateStatus.bind(window.ngAPI), 812 | 5000); 813 | }); 814 | clearInterval(window.ngAPI.groupTimer); 815 | clearInterval(window.ngAPI.statusTimer); 816 | window.ngAPI.groupTimer = setInterval( 817 | window.ngAPI.updateGroups.bind(window.ngAPI), 818 | 500); 819 | window.ngAPI.statusTimer = setInterval( 820 | window.ngAPI.updateStatus.bind(window.ngAPI), 821 | 500); 822 | }); 823 | 824 | chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab){ 825 | if(changeInfo.status === 'loading' && 826 | ['http:', 'https:'].indexOf(new URL(tab.url).protocol) > -1) { 827 | chrome.tabs.executeScript(tabId, { 828 | code: '({' + 829 | ' isSpotweb: document.querySelector(' + 830 | " 'meta[name=generator][content*=SpotWeb]')" + 831 | ' != null,' + 832 | ' isNewznab: document.querySelector(' + 833 | " 'div.icon_nzb a[href*=\"/getnzb\"]')" + 834 | ' != null,' + 835 | ' isTtRSS: document.querySelector(' + 836 | " '#ttrssMain') != null," + 837 | ' isFreshRSS: document.querySelector(' + 838 | " 'meta[name=apple-mobile-web-app-title][" + 839 | " content=FreshRSS]') != null" + 840 | '});' 841 | }, function(r) { 842 | if(!chrome.runtime.lastError && r && 843 | typeof r[0] === 'object') { 844 | chrome.tabs.executeScript( 845 | tabId, 846 | {file: 'sites/common.js'}); 847 | chrome.tabs.insertCSS( 848 | tabId, 849 | {file: 'sites/common.css'}); 850 | if(r[0].isNewznab) { 851 | chrome.tabs.executeScript( 852 | tabId, 853 | {file: 'sites/newsnab.js'}); 854 | } 855 | else if(r[0].isSpotweb) { 856 | chrome.tabs.executeScript( 857 | tabId, 858 | {file: 'sites/spotweb.js'}); 859 | chrome.tabs.insertCSS( 860 | tabId, 861 | {file: 'sites/spotweb.css'}); 862 | } 863 | else if(r[0].isTtRSS) { 864 | chrome.tabs.executeScript( 865 | tabId, 866 | {file: 'sites/ttrss.js'}); 867 | } 868 | else if(r[0].isFreshRSS) { 869 | chrome.tabs.executeScript( 870 | tabId, 871 | {file: 'sites/freshrss.js'}); 872 | } 873 | } 874 | }); 875 | } 876 | }); 877 | }); 878 | })(); 879 | --------------------------------------------------------------------------------