diff --git a/.gitignore b/.gitignore index 84c048a..8dfae2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -/build/ +/build/*.zip diff --git a/build/build_packages.py b/build/build_packages.py new file mode 100644 index 0000000..52e7a61 --- /dev/null +++ b/build/build_packages.py @@ -0,0 +1,41 @@ +"""Creates a package.zip file that contains the extension's files.""" +from pathlib import Path +import zipfile + +ROOT_DIR = Path(__file__).parent.parent +EXTENSION_DIR = ROOT_DIR / 'extension' +MANIFEST_V2_DIR = ROOT_DIR / 'manifest-v2' +OUTPUT_DIR = Path(__file__).parent +EXCLUDED_FILENAMES = ['.DS_Store'] + + +def create_package(filename, manifest_version=3): + package_path = OUTPUT_DIR / filename + + print(f'\nGenerating package: {package_path}') + print(f'Zipped files:') + with zipfile.ZipFile(package_path, 'w') as z: + for path in EXTENSION_DIR.rglob('*'): + if path.name in EXCLUDED_FILENAMES: + continue + + relative_path = path.relative_to(EXTENSION_DIR) + + if ( + manifest_version == 2 + and (MANIFEST_V2_DIR / relative_path).exists() + ): + path = MANIFEST_V2_DIR / relative_path + + z.write(path, relative_path) + print(f' {path.relative_to(ROOT_DIR)}') + + +def main(): + create_package('package-chrome.zip') + create_package('package-firefox.zip', manifest_version=2) + create_package('package-edge.zip', manifest_version=2) + + +if __name__ == '__main__': + main() diff --git a/extension/background.js b/extension/background.js index f3cb6fd..592a014 100644 --- a/extension/background.js +++ b/extension/background.js @@ -1,21 +1,23 @@ -// All AJAX requests are made from this background script to avoid CORB errors. +// All API requests are made through this background script to avoid CORB +// errors and to cache results. let cache = {} let cacheTimes = [] let cacheDuration = 600000 // Default is 10 mins. -chrome.storage.sync.get({cacheDuration: 600000}, function(settings) { - if (settings && settings.cacheDuration !== undefined) { - cacheDuration = settings.cacheDuration - } +chrome.runtime.onInstalled.addListener(() => { + chrome.storage.sync.get({cacheDuration: 600000}, function(settings) { + if (settings && settings.cacheDuration !== undefined) { + cacheDuration = settings.cacheDuration + } + }) }) -chrome.runtime.onMessage.addListener( - function(message, sender, sendResponse) { - if (message.query === 'videoApiRequest') { - +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + switch (message.query) { + case 'videoApiRequest': // Remove expired cache data. - const now = new Date().getTime() + const now = Date.now() let numRemoved = 0 for (const [fetchTime, videoId] of cacheTimes) { if (now - fetchTime > cacheDuration) { @@ -32,38 +34,44 @@ chrome.runtime.onMessage.addListener( if (message.videoId in cache) { // Use cached data if it exists. sendResponse(cache[message.videoId]) + return + } - } else { - // Otherwise, fetch new data and cache it. - fetch('https://returnyoutubedislikeapi.com/Votes?videoId=' + message.videoId) - .then(response => { - if (!response.ok) { - sendResponse(null) - } else { - response.json().then(data => { - const likesData = { - 'likes': data.likes, - 'dislikes': data.dislikes, - } - if (!(message.videoId in cache)) { - cache[message.videoId] = likesData - cacheTimes.push([new Date().getTime(), message.videoId]) - } - sendResponse(likesData) - }) - } - }) + // Otherwise, fetch new data and cache it. + fetch('https://returnyoutubedislikeapi.com/Votes?videoId=' + message.videoId) + .then(response => { + if (!response.ok) { + sendResponse(null) + } else { + response.json().then(data => { + const likesData = { + 'likes': data.likes, + 'dislikes': data.dislikes, + } + if (!(message.videoId in cache)) { + cache[message.videoId] = likesData + cacheTimes.push([Date.now(), message.videoId]) + } + sendResponse(likesData) + }) + } + }) - // Returning `true` signals to the browser that we will send our - // response asynchronously using `sendResponse()`. - return true - } + // Returning `true` signals to the browser that we will send our + // response asynchronously using `sendResponse()`. + return true - } else if (message.query === 'insertCss') { - chrome.tabs.insertCSS(sender.tab.id, {file: message.url}) + case 'insertCss': + chrome.scripting.insertCSS({ + target: { + tabId: sender.tab.id, + }, + files: message.files, + }) + break - } else if (message.query === 'updateSettings') { + case 'updateSettings': cacheDuration = message.cacheDuration - } + break } -) +}) diff --git a/extension/content-script.js b/extension/content-script.js index f214bf3..58dd973 100755 --- a/extension/content-script.js +++ b/extension/content-script.js @@ -137,31 +137,6 @@ function exponentialRatingWidthPercentage(rating) { return 100 * Math.pow(2, 10 * (rating - 1)) } -// function getLikesToViewsPercentage(likes, views) { -// if (likes <= 0) return 0 -// if (likes >= views) return 100 -// -// let ratio = likes / views -// let r = (ratio ** 0.21032389998435974 - 0.4999999701976776) / 0.09012361615896225 -// let v = (Math.log(views) - 12.015865325927734) / 2.8472495079040527 -// -// let m0 = 0.040817804634571075 -// let m1 = -0.27621328830718994 -// let m2 = -0.05106991529464722 -// let m3 = -0.02893015556037426 -// let mean = m0 + m1 * v + m2 * v ** 2 + m3 * v ** 3 -// -// let s0 = -0.09283683449029922 -// let s1 = -0.13813409209251404 -// let s2 = 0.003354990854859352 -// let s3 = 0.004593323916196823 -// let log_std = s0 + s1 * v + s2 * v ** 2 + s3 * v ** 3 -// let std = Math.exp(log_std) -// -// let cdf = jStat.normal.cdf(r, mean, std) -// return cdf * 100 -// } - function getRatingBarHtml(videoData) { let ratingElement if (videoData.rating == null) { @@ -514,50 +489,51 @@ function handleDomMutations() { // An observer for watching changes to the body element. const mutationObserver = new MutationObserver(handleDomMutations) -function insertCss(url) { - chrome.runtime.sendMessage({ - query: 'insertCss', - url: url, - }) -} - chrome.storage.sync.get(DEFAULT_USER_SETTINGS, function(storedSettings) { // In Firefox, `storedSettings` will be undeclared if not previously set. if (storedSettings) { userSettings = storedSettings } + const cssFiles = [] if (userSettings.barHeight !== 0) { - insertCss('css/bar.css') + cssFiles.push('css/bar.css') if (userSettings.barPosition === 'top') { - insertCss('css/bar-top.css') + cssFiles.push('css/bar-top.css') } else { - insertCss('css/bar-bottom.css') + cssFiles.push('css/bar-bottom.css') } if (userSettings.barSeparator) { if (userSettings.barPosition === 'top') { - insertCss('css/bar-top-separator.css') + cssFiles.push('css/bar-top-separator.css') } else { - insertCss('css/bar-bottom-separator.css') + cssFiles.push('css/bar-bottom-separator.css') } } if (userSettings.barTooltip) { - insertCss('css/bar-tooltip.css') + cssFiles.push('css/bar-tooltip.css') if (userSettings.barPosition === 'top') { - insertCss('css/bar-top-tooltip.css') + cssFiles.push('css/bar-top-tooltip.css') } else { - insertCss('css/bar-bottom-tooltip.css') + cssFiles.push('css/bar-bottom-tooltip.css') } } if (userSettings.useOnVideoPage) { - insertCss('css/bar-video-page.css') + cssFiles.push('css/bar-video-page.css') } } + if (cssFiles.length > 0) { + chrome.runtime.sendMessage({ + query: 'insertCss', + files: cssFiles, + }) + } + document.documentElement.style.setProperty('--ytrb-bar-height', userSettings.barHeight + 'px') document.documentElement.style.setProperty('--ytrb-bar-opacity', userSettings.barOpacity / 100) diff --git a/extension/lib/getmdl-select.min.css.map b/extension/lib/getmdl-select.min.css.map new file mode 100644 index 0000000..fe07286 --- /dev/null +++ b/extension/lib/getmdl-select.min.css.map @@ -0,0 +1 @@ +{"version":3,"file":"getmdl-select.min.css","sources":["getmdl-select.scss"],"sourcesContent":[".getmdl-select {\r\n outline: none;\r\n .mdl-textfield__input {\r\n cursor: pointer;\r\n }\r\n .selected {\r\n background-color: #ddd;\r\n }\r\n .mdl-icon-toggle__label {\r\n float: right;\r\n margin-top: -30px;\r\n color: rgba(0, 0, 0, 0.4);\r\n transform: rotate(0);\r\n transition: transform 0.3s;\r\n }\r\n &.is-focused {\r\n .mdl-icon-toggle__label {\r\n color: #3f51b5;\r\n transform: rotate(180deg);\r\n }\r\n }\r\n\r\n .mdl-menu__container {\r\n width: 100% !important;\r\n margin-top: 2px;\r\n .mdl-menu {\r\n width: 100%;\r\n .mdl-menu__item {\r\n font-size: 16px;\r\n }\r\n }\r\n }\r\n}\r\n\r\n.getmdl-select__fix-height {\r\n .mdl-menu__container .mdl-menu {\r\n overflow-y: auto;\r\n max-height: 288px !important;\r\n }\r\n .mdl-menu.mdl-menu--top-left {\r\n bottom: auto;\r\n top: 0;\r\n }\r\n}"],"mappings":"AAAA,AAAA,cAAc,AAAC,CACb,OAAO,CAAE,IAAK,CA+Bf,AAhCD,AAEE,cAFY,CAEZ,qBAAqB,AAAC,CACpB,MAAM,CAAE,OAAQ,CACjB,AAJH,AAKE,cALY,CAKZ,SAAS,AAAC,CACR,gBAAgB,CAAE,IAAK,CACxB,AAPH,AAQE,cARY,CAQZ,uBAAuB,AAAC,CACtB,KAAK,CAAE,KAAM,CACb,UAAU,CAAE,KAAM,CAClB,KAAK,CAAE,eAAI,CACX,SAAS,CAAE,SAAM,CACjB,UAAU,CAAE,cAAe,CAC5B,AAdH,AAgBI,cAhBU,AAeX,WAAW,CACV,uBAAuB,AAAC,CACtB,KAAK,CAAE,OAAQ,CACf,SAAS,CAAE,cAAM,CAClB,AAnBL,AAsBE,cAtBY,CAsBZ,oBAAoB,AAAC,CACnB,KAAK,CAAE,eAAgB,CACvB,UAAU,CAAE,GAAI,CAOjB,AA/BH,AAyBI,cAzBU,CAsBZ,oBAAoB,CAGlB,SAAS,AAAC,CACR,KAAK,CAAE,IAAK,CAIb,AA9BL,AA2BM,cA3BQ,CAsBZ,oBAAoB,CAGlB,SAAS,CAEP,eAAe,AAAC,CACd,SAAS,CAAE,IAAK,CACjB,AAKP,AACuB,0BADG,CACxB,oBAAoB,CAAC,SAAS,AAAC,CAC7B,UAAU,CAAE,IAAK,CACjB,UAAU,CAAE,gBAAiB,CAC9B,AAJH,AAKW,0BALe,CAKxB,SAAS,AAAA,mBAAmB,AAAC,CAC3B,MAAM,CAAE,IAAK,CACb,GAAG,CAAE,CAAE,CACR","names":[]} \ No newline at end of file diff --git a/extension/lib/getmdl-select.min.js.map b/extension/lib/getmdl-select.min.js.map new file mode 100644 index 0000000..d7c6d1b --- /dev/null +++ b/extension/lib/getmdl-select.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["src\\js\\getmdl-select.js"],"names":["whenLoaded","getmdlSelect","init","window","addEventListener","attachEvent","_defaultValue","width","_addEventListeners","dropdown","input","querySelector","hiddenInput","list","querySelectorAll","menu","arrow","label","previousValue","previousDataVal","opened","setSlectedItem","li","value","textContent","trim","forEach","classList","remove","add","MaterialTextfield","change","setTimeout","updateClasses_","dataset","val","document","evt","createEvent","initEvent","hide","dispatchEvent","fireEvent","hideAllMenus","contains","menus","call","event","Event","body","onkeydown","keyCode","onfocus","e","show","focus","onblur","stopPropagation","onclick","onBlur_","onFocus_","selected","selector","dropdowns","componentHandler","upgradeElement"],"mappings":"AAAA,cAII,WACI,QAASA,KACLC,aAAaC,KAAK,kBAGtBC,OAAOC,iBAAmBD,OAAOC,iBAAiB,OAAQJ,GAAY,GAASG,OAAOE,aAAeF,OAAOE,YAAY,SAAUL,KAGtI,IAAIC,eACAK,eACIC,MAAO,KAEXC,mBAAoB,SAA4BC,GAC5C,GAAIC,GAAQD,EAASE,cAAc,SAC/BC,EAAcH,EAASE,cAAc,wBACrCE,EAAOJ,EAASK,iBAAiB,MACjCC,EAAON,EAASE,cAAc,gBAC9BK,EAAQP,EAASE,cAAc,2BAC/BM,EAAQ,GACRC,EAAgB,GAChBC,EAAkB,GAClBC,GAAS,EAETC,EAAiB,SAAwBC,GACzC,GAAIC,GAAQD,EAAGE,YAAYC,MAiB3B,IAhBAf,EAAMa,MAAQA,EACdV,EAAKa,QAAQ,SAAUJ,GACnBA,EAAGK,UAAUC,OAAO,cAExBN,EAAGK,UAAUE,IAAI,YACjBpB,EAASqB,kBAAkBC,OAAOR,GAClCS,WAAW,WACPvB,EAASqB,kBAAkBG,kBAC5B,KAGHrB,EAAYW,MAAQD,EAAGY,QAAQC,KAAO,GAEtCjB,EAAgBR,EAAMa,MACtBJ,EAAkBP,EAAYW,MAE1B,eAAiBa,UAAU,CAC3B,GAAIC,GAAMD,SAASE,YAAY,aAC/BD,GAAIE,UAAU,UAAU,GAAO,GAC/BxB,EAAmB,aAAEyB,OACrB9B,EAAM+B,cAAcJ,OAEpB3B,GAAMgC,UAAU,aAIpBC,EAAe,WACfvB,GAAS,EACTV,EAAMa,MAAQL,EACdN,EAAYW,MAAQJ,EACfV,EAASE,cAAc,wBAAwBgB,UAAUiB,SAAS,eACnEnC,EAASkB,UAAUC,OAAO,aAE9B,IAAIiB,GAAQT,SAAStB,iBAAiB,kCACnCY,QAAQoB,KAAKD,EAAO,SAAU9B,GAC7BA,EAAmB,aAAEyB,QAEzB,IAAIO,GAAQ,GAAIC,OAAM,cACtBjC,GAAK0B,cAAcM,GAEvBX,UAASa,KAAK7C,iBAAiB,QAASuC,GAAc,GAGtDlC,EAASyC,UAAY,SAAUH,GACN,GAAjBA,EAAMI,UACNzC,EAAMa,MAAQL,EACdN,EAAYW,MAAQJ,EACpBJ,EAAmB,aAAEyB,OACrB/B,EAASkB,UAAUC,OAAO,gBAKlClB,EAAM0C,QAAU,SAAUC,GACtBtC,EAAmB,aAAEuC,OACrBvC,EAAKwC,QACLnC,GAAS,GAGbV,EAAM8C,OAAS,SAAUH,GACrBA,EAAEI,mBAIN/C,EAAMgD,QAAU,SAAUL,GACtBA,EAAEI,kBACG1C,EAAKY,UAAUiB,SAAS,eAMzB7B,EAAmB,aAAEyB,OACrBpB,GAAS,IANTL,EAAmB,aAAEuC,OACrBX,IACAlC,EAASkB,UAAUE,IAAI,cACvBT,GAAS,IAOjBV,EAAMwC,UAAY,SAAUH,GACH,IAAjBA,EAAMI,UACNzC,EAAMa,MAAQL,EACdN,EAAYW,MAAQJ,EACpBJ,EAAmB,aAAEyB,OACrB/B,EAASqB,kBAAkB6B,UACb,KAAV1C,IACAR,EAASE,cAAc,yBAAyBa,YAAcP,EAC9DA,EAAQ,MAKpBF,EAAKX,iBAAiB,cAAe,SAAUiD,GAC3C3C,EAAMa,MAAQL,EACdN,EAAYW,MAAQJ,EACpBV,EAASkB,UAAUC,OAAO,cACZ,KAAVX,IACAR,EAASE,cAAc,yBAAyBa,YAAcP,EAC9DA,EAAQ,MAKhBF,EAAKmC,UAAY,SAAUH,GACF,IAAjBA,EAAMI,UACNzC,EAAMa,MAAQL,EACdN,EAAYW,MAAQJ,EACpBV,EAASkB,UAAUC,OAAO,cACZ,KAAVX,IACAR,EAASE,cAAc,yBAAyBa,YAAcP,EAC9DA,EAAQ,MAKhBD,IACAA,EAAM0C,QAAU,SAAUL,GACtBA,EAAEI,kBACErC,GACAL,EAAmB,aAAEyB,OACrBpB,GAAS,EACTX,EAASkB,UAAUC,OAAO,cAC1BnB,EAASqB,kBAAkB6B,UAC3BjD,EAAMa,MAAQL,EACdN,EAAYW,MAAQJ,IAEpBwB,IACAlC,EAASqB,kBAAkB8B,WAC3BlD,EAAM6C,QACNxC,EAAmB,aAAEuC,OACrBlC,GAAS,QAKlBM,QAAQoB,KAAKjC,EAAM,SAAUS,GAC5BA,EAAG8B,QAAU,WACT3C,EAASkB,UAAUE,IAAI,aACvB,IAAIN,GAAQD,EAAGE,YAAYC,MAC3Bf,GAAMa,MAAQA,EACTd,EAASkB,UAAUiB,SAAS,kCAA6C,IAAT3B,IACjEA,EAAQR,EAASE,cAAc,yBAAyBa,YAAYC,OACpEhB,EAASE,cAAc,yBAAyBa,YAAc,KAItEF,EAAGoC,QAAU,WACTrC,EAAeC,IA4BfA,EAAGY,QAAQ2B,UACXxC,EAAeC,MAI3BpB,KAAM,SAAc4D,GAChB,GAAIC,GAAY3B,SAAStB,iBAAiBgD,MACvCpC,QAAQoB,KAAKiB,EAAW,SAAUtD,GACjCR,aAAaO,mBAAmBC,GAChCuD,iBAAiBC,eAAexD,GAChCuD,iBAAiBC,eAAexD,EAASE,cAAc","file":"getmdl-select.min.js","sourceRoot":"E:\\html5\\GitHub\\getmdl-select"} \ No newline at end of file diff --git a/extension/lib/jquery-3.6.0.min.js b/extension/lib/jquery-3.6.0.min.js deleted file mode 100644 index c4c6022..0000000 --- a/extension/lib/jquery-3.6.0.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&v(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!y||!y.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ve(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ye(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ve(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],y=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||y.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||y.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||y.push(".#.+[+~]"),e.querySelectorAll("\\\f"),y.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),y=y.length&&new RegExp(y.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),v=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&v(p,e)?-1:t==C||t.ownerDocument==p&&v(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!y||!y.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),v.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",v.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",v.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),v.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(v.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return B(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=_e(v.pixelPosition,function(e,t){if(t)return t=Be(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return B(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 - + @@ -185,4 +185,4 @@
- \ No newline at end of file + diff --git a/manifest-v2/background.js b/manifest-v2/background.js new file mode 100644 index 0000000..d2c452e --- /dev/null +++ b/manifest-v2/background.js @@ -0,0 +1,72 @@ +// All API requests are made through this background script to avoid CORB +// errors and to cache results. + +let cache = {} +let cacheTimes = [] +let cacheDuration = 600000 // Default is 10 mins. + +chrome.storage.local.get({cacheDuration: 600000}, function(settings) { + if (settings && settings.cacheDuration !== undefined) { + cacheDuration = settings.cacheDuration + } +}) + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + switch (message.query) { + case 'videoApiRequest': + // Remove expired cache data. + const now = Date.now() + let numRemoved = 0 + for (const [fetchTime, videoId] of cacheTimes) { + if (now - fetchTime > cacheDuration) { + delete cache[videoId] + numRemoved++ + } else { + break + } + } + if (numRemoved > 0) { + cacheTimes = cacheTimes.slice(numRemoved) + } + + if (message.videoId in cache) { + // Use cached data if it exists. + sendResponse(cache[message.videoId]) + return + } + + // Otherwise, fetch new data and cache it. + fetch('https://returnyoutubedislikeapi.com/Votes?videoId=' + message.videoId) + .then(response => { + if (!response.ok) { + sendResponse(null) + } else { + response.json().then(data => { + const likesData = { + 'likes': data.likes, + 'dislikes': data.dislikes, + } + if (!(message.videoId in cache)) { + cache[message.videoId] = likesData + cacheTimes.push([Date.now(), message.videoId]) + } + sendResponse(likesData) + }) + } + }) + + // Returning `true` signals to the browser that we will send our + // response asynchronously using `sendResponse()`. + return true + + case 'insertCss': + for (const file of message.files) { + chrome.tabs.insertCSS(sender.tab.id, { file }) + } + break + + case 'updateSettings': + cacheDuration = message.cacheDuration + break + } +}) diff --git a/manifest-v2/content-script.js b/manifest-v2/content-script.js new file mode 100755 index 0000000..0864be7 --- /dev/null +++ b/manifest-v2/content-script.js @@ -0,0 +1,571 @@ +// Variables for throttling handling DOM mutations. +const HANDLE_DOM_MUTATIONS_THROTTLE_MS = 100 +let domMutationsAreThrottled = false +let hasUnseenDomMutations = false + +// Variables for handling what to do when an API request fails. +const API_RETRY_DELAY = 5000 +const MAX_RETRIES_PER_THUMBNAIL = 10 +let isPendingApiRetry = false +let thumbnailsToRetry = [] + +// Enum values for which YouTube theme is currently being viewed. +let curTheme = 0 // No theme set yet. +const THEME_MODERN = 1 // The new Material Design theme. +const THEME_CLASSIC = 2 // The classic theme. +const THEME_GAMING = 3 // The YouTube Gaming theme. +const THEME_MOBILE = 4 // The YouTube mobile theme (m.youtube.com). +const NUM_THEMES = 4 + +// `isDarkTheme` will be true if the appearance setting is in dark theme mode. +const isDarkTheme = getComputedStyle(document.body).getPropertyValue('--yt-spec-general-background-a') === ' #181818' + +// We use these JQuery selectors to find new thumbnails on the page. We use +// :not([data-ytrb-processed]) to make sure these aren't thumbnails that we've +// already added a rating bar to. We need to check all combinations of these +// modes and types: +// Modes: +// * Classic (Can be enabled by add &disable_polymer=true to the URL) +// * Modern (The new Material Design theme) +// * Gaming (YouTube Gaming) +// Types: +// * Search results videos +// * Search results playlist +// * Creator's videos +// * Creator's playlist +// * Sidebar suggested videos +// * Sidebar suggested playlists +// * Playlist page big thumbnail +// * Playlist page small thumbnails +// * Playing playlist small icons +// * Video wall (suggested videos after the video ends) +// +// (Note: the gaming playlist page big thumbnail will be ignored due to +// complications in getting the associated video ID from the thumbnail. +// Also, since the ratings for the videos in the playlist are shown in the +// smaller icons right below the big icon, adding a rating bar to the +// big icon doesn't add much value.) +// +// Listed below are which type of thumbnails that part of selector identifies, +// and where the link tag element is relative to the thumbnail element for +// figuring out the video ID associated with that thumbnail. +const THUMBNAIL_SELECTORS = [] +THUMBNAIL_SELECTORS[THEME_MODERN] = '' + + // All types except the video wall. The URL is on the selected a link. + // The mini-player thumbnail will not have an href attribute, which is why + // we require that it exists. + 'a#thumbnail[href]' + +THUMBNAIL_SELECTORS[THEME_CLASSIC] = '' + + // Search results videos. (url on parent) + // Creator's videos. (url on parent) + // Playlist page small thumbnails. (url on parent) + // Sidebar suggested playlist. (url on grandparent) + // Playing playlist small thumbnails. (url on parent) + '.video-thumb' + + ':not(.yt-thumb-20)' + + ':not(.yt-thumb-27)' + + ':not(.yt-thumb-32)' + + ':not(.yt-thumb-36)' + + ':not(.yt-thumb-48)' + + ':not(.yt-thumb-64), ' + + // (For search results, if a channel is in the results, it's thumbnail will + // be caught by this selector, but won't have an matchable video URL. + // Since this does not cause an error, it should be fine to ignore it.) + + // Sidebar suggested video. (url on first child) + '.thumb-wrapper, ' + + + // Playlist page big thumbnail. (url on second child) + '.pl-header-thumb' + +THUMBNAIL_SELECTORS[THEME_GAMING] = '' + + // Gaming all types except video wall. URL is on the great-grandparent, + // except for search result playlists it is on the grandparent. + 'ytg-thumbnail' + + ':not([avatar])' + + ':not(.avatar)' + + ':not(.ytg-user-avatar)' + + ':not(.ytg-box-art)' + + ':not(.ytg-compact-gaming-event-renderer)' + + ':not(.ytg-playlist-header-renderer)' + +THUMBNAIL_SELECTORS[THEME_MOBILE] = '' + + 'a.media-item-thumbnail-container, ' + + 'a.compact-media-item-image, ' + + 'a.video-card-image' + +// All themes use this selector for video wall videos. +const THUMBNAIL_SELECTOR_VIDEOWALL = '' + + 'a.ytp-videowall-still' + +// The default user settings. `userSettings` is replaced with the stored user's +// settings once they are loaded. +const DEFAULT_USER_SETTINGS = { + barPosition: 'bottom', + barColor: 'blue-gray', + barLikesColor: '#3095e3', + barDislikesColor: '#cfcfcf', + barColorsSeparator: false, + barHeight: 4, + barOpacity: 100, + barSeparator: false, + useExponentialScaling: false, + barTooltip: true, + useOnVideoPage: false, + showPercentage: false, +} +let userSettings = DEFAULT_USER_SETTINGS + +function ratingToPercentage(rating) { + if (rating === 1) { + return '100%' + } + // Note: We use floor instead of round to ensure that anything lower than + // 100% does not display "100.0%". + return (Math.floor(rating * 1000) / 10).toFixed(1) + '%' +} + +function getToolTipText(videoData) { + return videoData.likes.toLocaleString() + ' / ' + + videoData.dislikes.toLocaleString() + '    ' + + ratingToPercentage(videoData.rating) + '    ' + + videoData.total.toLocaleString() + ' total' +} + +function exponentialRatingWidthPercentage(rating) { + return 100 * Math.pow(2, 10 * (rating - 1)) +} + +function getRatingBarHtml(videoData) { + let ratingElement + if (videoData.rating == null) { + ratingElement = '' + } else { + let likesWidthPercentage + if (userSettings.useExponentialScaling) { + likesWidthPercentage = exponentialRatingWidthPercentage(videoData.rating) + } else { + likesWidthPercentage = 100 * videoData.rating + } + ratingElement = '' + + '' + + '' + + '' + } + + return '' + + ratingElement + + (userSettings.barTooltip + ? '
' + getToolTipText(videoData) + '
' + : '' + ) + + '' +} + +function getRatingPercentageHtml(videoData) { + const r = (1 - videoData.rating) * 1275 + let g = videoData.rating * 637.5 - 255 + if (!isDarkTheme) { + g = Math.min(g, 255) * 0.85 + } + const rgb = 'rgb(' + r + ',' + g + ',0)' + + return '' + ratingToPercentage(videoData.rating) + '' +} + +function getNewThumbnails() { + // Returns an array of thumbnails that have not been processed yet, and sets + // the theme if it hasn't been set yet. + let thumbnails = [] + if (curTheme) { + thumbnails = $(THUMBNAIL_SELECTORS[curTheme]) + } else { + for (let i = 1; i <= NUM_THEMES; i++) { + thumbnails = $(THUMBNAIL_SELECTORS[i]) + if (thumbnails.length) { + curTheme = i + break + } + } + } + thumbnails = $.merge(thumbnails, $(THUMBNAIL_SELECTOR_VIDEOWALL)) + return thumbnails +} + +function getThumbnailsAndIds(thumbnails) { + // Finds the video ID associated with each thumbnail and returns an array of + // arrays of [thumbnail element, video ID string]. + const thumbnailsAndVideoIds = [] + $(thumbnails).each(function(_, thumbnail) { + // Find the link tag element of the thumbnail and its URL. + let url + if (curTheme === THEME_MODERN) { + // The URL should be on the current element. + url = $(thumbnail).attr('href') + + } else if (curTheme === THEME_CLASSIC) { + // Check the current element, then the parent, then the grandparent, + // then the first child, then the second child. + url = $(thumbnail).attr('href') + || $(thumbnail).parent().attr('href') + || $(thumbnail).parent().parent().attr('href') + || $(thumbnail).children(':first').attr('href') + || $(thumbnail).children(':first').next().attr('href') + + } else if (curTheme === THEME_GAMING) { + // Check the current element, then the grandparent. + url = $(thumbnail).attr('href') + || $(thumbnail).parent().parent().attr('href') + || $(thumbnail).parent().parent().parent().attr('href') + + // Unless the element is a video wall thumbnail, change the thumbnail + // element to the parent element, so that it will show over the thumbnail + // preview video that plays when you hover over the thumbnail. + if (!$(thumbnail).is('a')) { + thumbnail = $(thumbnail).parent() + } + + } else if (curTheme === THEME_MOBILE) { + // The URL should be on the current element. + url = $(thumbnail).attr('href') + + // On mobile gaming (m.youtube.com/gaming), the thumbnail should be + // reassigned to the child container. + const firstChild = $(thumbnail).children(':first')[0] + if ($(firstChild).is('.video-thumbnail-container-compact')) { + thumbnail = firstChild + } + + } else { + // The theme may not be set if only video-wall thumbnails were found. + url = $(thumbnail).attr('href') + } + + if (!url) { + return true + } + + // Check if this thumbnail was previously found. + const previousUrl = $(thumbnail).attr('data-ytrb-processed') + if (previousUrl) { + // Check if this thumbnail is for the same URL as previously. + if (previousUrl === url) { + // If it is for the same URL, continue the next thumbnail, except on + // mobile where we have to make on additional check. + if (curTheme === THEME_MOBILE) { + // On mobile, we have to check to make sure the bar is still present, + // because thumbnails can sometimes be recreated (such as when they + // are scrolled out of view) which causes the bar to be removed. + if ($(thumbnail).children().last().is('ytrb-bar')) { + return true + } + } else { + return true + } + } else { + // If not, remove the old rating bar and retries count. + $(thumbnail).children('ytrb-bar').remove() + $(thumbnail).removeAttr('data-ytrb-retries') + } + } + // Add an attribute that marks this thumbnail as found, and give it the + // value of the URL the thumbnail is for. + $(thumbnail).attr('data-ytrb-processed', url) + + // Extract the video ID from the URL. + const match = url.match(/.*[?&]v=([^&]+).*/) + if (match) { + const id = match[1] + thumbnailsAndVideoIds.push([thumbnail, id]) + } + }) + return thumbnailsAndVideoIds +} + +function getVideoDataObject(likes, dislikes) { + const total = likes + dislikes + const rating = total ? likes / total : null + return { + likes: likes, + dislikes: dislikes, + total: total, + rating: rating, + } +} + +function retryProcessingThumbnailInTheFuture(thumbnail) { + thumbnailsToRetry.push(thumbnail) + if (!isPendingApiRetry) { + isPendingApiRetry = true + setTimeout(() => { + isPendingApiRetry = false + thumbnailsToRetry.forEach(thumbnail => { + const retriesAttr = $(thumbnail).attr('data-ytrb-retries') + const retriesNum = retriesAttr ? Number.parseInt(retriesAttr, 10) : 0 + if (retriesNum < MAX_RETRIES_PER_THUMBNAIL) { + $(thumbnail).attr('data-ytrb-retries', retriesNum + 1) + $(thumbnail).removeAttr('data-ytrb-processed') + hasUnseenDomMutations = true + } + }) + thumbnailsToRetry = [] + + // Note: `handleDomMutations()` must be called after updating + // `isPendingApiRetry` and `thumbnailsToRetry` above to allow for + // additional retries if needed. + handleDomMutations() + }, API_RETRY_DELAY) + } +} + +function getVideoData(thumbnail, videoId) { + return new Promise(resolve => { + chrome.runtime.sendMessage( + {query: 'videoApiRequest', videoId: videoId}, + (likesData) => { + if (likesData === null) { + // The API request failed, which is usually due to rate limiting, so + // we will retry processing the thumbnail in the future. + retryProcessingThumbnailInTheFuture(thumbnail) + resolve(null) + } else { + resolve(getVideoDataObject(likesData.likes, likesData.dislikes)) + } + } + ) + }) +} + +function addRatingBar(thumbnail, videoData) { + // Add a rating bar to each thumbnail. + $(thumbnail).append(getRatingBarHtml(videoData)) +} + +function addRatingPercentage(thumbnail, videoData) { + // Add the rating text percentage below or next to the thumbnail. + let metadataLine + if (curTheme === THEME_MOBILE) { + metadataLine = $(thumbnail).closest('ytm-media-item').find('ytm-badge-and-byline-renderer').last() + } else { + metadataLine = $(thumbnail).closest( + '.ytd-rich-item-renderer, ' + // Home page. + '.ytd-grid-renderer, ' + // Trending and subscriptions page. + '.ytd-expanded-shelf-contents-renderer, ' + // Subscriptions page. + '.yt-horizontal-list-renderer, ' + // Channel page. + '.ytd-item-section-renderer, ' + // History page. + '.ytd-horizontal-card-list-renderer, ' + // Gaming page. + '.ytd-playlist-video-list-renderer' // Playlist page. + ).find('#metadata-line').last() + } + + if (metadataLine) { + // Remove any previously added percentages. + for (const oldPercentage of metadataLine.children('.ytrb-percentage')) { + oldPercentage.remove() + } + if (curTheme === THEME_MOBILE) { + for (const oldPercentage of metadataLine.children('.ytrb-percentage-separator')) { + oldPercentage.remove() + } + } + + // Add new percentage. + if (videoData.rating != null) { + const ratingPercentageHtml = getRatingPercentageHtml(videoData) + const lastSpan = metadataLine.children('span').last() + if (lastSpan.length) { + lastSpan.after(ratingPercentageHtml) + if (curTheme === THEME_MOBILE) { + // On mobile, we have to add the separator dot manually. + lastSpan.after('') + } + } else { + // This handles metadata lines that are initially empty, which + // occurs on playlist pages. We prepend the rating percentage as well + // as an empty meta block element to add a separating dot before the + // rating percentage. + metadataLine.prepend(ratingPercentageHtml) + metadataLine.prepend('') + } + } + } +} + +function processNewThumbnails() { + const thumbnails = getNewThumbnails() + const thumbnailsAndVideoIds = getThumbnailsAndIds(thumbnails) + + for (const [thumbnail, videoId] of thumbnailsAndVideoIds) { + getVideoData(thumbnail, videoId).then(videoData => { + if (videoData !== null) { + if (userSettings.barHeight !== 0) { + addRatingBar(thumbnail, videoData) + } + if (userSettings.showPercentage) { + addRatingPercentage(thumbnail, videoData) + } + } + }) + } +} + +function getVideoDataFromTooltipText(text) { + let likes = 0 + let dislikes = 0 + let match = text.match(/\s*([0-9,.]+)([^0-9,.]+)([0-9,.]+)/) + if (match && match.length >= 4) { + likes = parseInt(match[1].replaceAll(/[^0-9]/g, ''), 10) + dislikes = parseInt(match[3].replaceAll(/[^0-9]/g, ''), 10) + } + return getVideoDataObject(likes, dislikes) +} + +function updateVideoRatingBar() { + $('.ryd-tooltip').each(function(_, rydTooltip) { + const tooltip = $(rydTooltip).find('#tooltip') + const curText = $(tooltip).text() + + // We add a zero width space to the end of any processed tooltip text to + // prevent it from being reprocessed. + if (!curText.endsWith('\u200b')) { + const videoData = getVideoDataFromTooltipText(curText) + + if (userSettings.barTooltip) { + $(tooltip).text(`${curText} \u00A0\u00A0 ` + + `${videoData.rating == null ? '0%' : ratingToPercentage(videoData.rating)} \u00A0\u00A0 ` + + `${videoData.total.toLocaleString()} total\u200b`) + } else { + $(tooltip).text(`${curText}\u200b`) + } + + if (userSettings.useExponentialScaling && videoData.rating) { + $(rydTooltip).find('#ryd-bar')[0].style.width = exponentialRatingWidthPercentage(videoData.rating) + '%' + } + } + }) +} + +function handleDomMutations() { + // When the DOM is updated, we search for items that should be modified. + // However, we throttle these searches to not over tax the CPU. + if (domMutationsAreThrottled) { + // If updates are currently being throttled, we'll remember to handle + // them later. + hasUnseenDomMutations = true + } else { + // Turn on throttling. + domMutationsAreThrottled = true + + // Run the updates. + if (userSettings.barHeight !== 0 || userSettings.showPercentage) { + processNewThumbnails() + } + if (userSettings.barTooltip || userSettings.useExponentialScaling) { + updateVideoRatingBar() + } + + hasUnseenDomMutations = false + + setTimeout(function() { + // After the timeout, turn off throttling. + domMutationsAreThrottled = false + + // If any mutations occurred while being throttled, handle them now. + if (hasUnseenDomMutations) { + handleDomMutations() + } + + }, HANDLE_DOM_MUTATIONS_THROTTLE_MS) + } +} + +// An observer for watching changes to the body element. +const mutationObserver = new MutationObserver(handleDomMutations) + +chrome.storage.local.get(DEFAULT_USER_SETTINGS, function(storedSettings) { + // In Firefox, `storedSettings` will be undeclared if not previously set. + if (storedSettings) { + userSettings = storedSettings + } + + const cssFiles = [] + if (userSettings.barHeight !== 0) { + cssFiles.push('css/bar.css') + + if (userSettings.barPosition === 'top') { + cssFiles.push('css/bar-top.css') + } else { + cssFiles.push('css/bar-bottom.css') + } + + if (userSettings.barSeparator) { + if (userSettings.barPosition === 'top') { + cssFiles.push('css/bar-top-separator.css') + } else { + cssFiles.push('css/bar-bottom-separator.css') + } + } + + if (userSettings.barTooltip) { + cssFiles.push('css/bar-tooltip.css') + if (userSettings.barPosition === 'top') { + cssFiles.push('css/bar-top-tooltip.css') + } else { + cssFiles.push('css/bar-bottom-tooltip.css') + } + } + + if (userSettings.useOnVideoPage) { + cssFiles.push('css/bar-video-page.css') + } + } + + if (cssFiles.length > 0) { + chrome.runtime.sendMessage({ + query: 'insertCss', + files: cssFiles, + }) + } + + document.documentElement.style.setProperty('--ytrb-bar-height', userSettings.barHeight + 'px') + document.documentElement.style.setProperty('--ytrb-bar-opacity', userSettings.barOpacity / 100) + + if (userSettings.barColor === 'blue-gray') { + document.documentElement.style.setProperty('--ytrb-bar-likes-color', '#3095e3') + document.documentElement.style.setProperty('--ytrb-bar-dislikes-color', '#cfcfcf') + document.documentElement.style.setProperty('--ytrb-bar-likes-shadow', 'none') + document.documentElement.style.setProperty('--ytrb-bar-dislikes-shadow', 'none') + } else if (userSettings.barColor === 'green-red') { + document.documentElement.style.setProperty('--ytrb-bar-likes-color', '#060') + document.documentElement.style.setProperty('--ytrb-bar-dislikes-color', '#c00') + document.documentElement.style.setProperty('--ytrb-bar-likes-shadow', '1px 0 #fff') + document.documentElement.style.setProperty('--ytrb-bar-dislikes-shadow', 'inset 1px 0 #fff') + } else if (userSettings.barColor === 'custom-colors') { + document.documentElement.style.setProperty( + '--ytrb-bar-likes-color', + userSettings.barLikesColor + ) + document.documentElement.style.setProperty( + '--ytrb-bar-dislikes-color', + userSettings.barDislikesColor + ) + document.documentElement.style.setProperty( + '--ytrb-bar-likes-shadow', + userSettings.barColorsSeparator ? '1px 0 #fff' : 'none' + ) + document.documentElement.style.setProperty( + '--ytrb-bar-dislikes-shadow', + userSettings.barColorsSeparator ? 'inset 1px 0 #fff' : 'none' + ) + } + + handleDomMutations() + mutationObserver.observe(document.body, {childList: true, subtree: true}) +}) diff --git a/manifest-v2/manifest.json b/manifest-v2/manifest.json new file mode 100644 index 0000000..f20644a --- /dev/null +++ b/manifest-v2/manifest.json @@ -0,0 +1,57 @@ +{ + "manifest_version": 2, + "name": "Thumbnail Rating Bar for YouTube™", + "version": "1.8.7", + "description": "Displays a rating bar (likes/dislikes) on the bottom of every YouTube™ video thumbnail.", + "author": "Elliot Waite", + "icons": { + "96": "icons/icon96.png", + "128": "icons/icon128.png" + }, + "browser_action": { + "default_icon": "icons/icon96.png", + "default_title": "Settings", + "default_popup": "options.html" + }, + "options_ui": { + "page": "options.html", + "chrome_style": false + }, + "background": { + "scripts": [ + "background.js" + ], + "persistent": false + }, + "content_scripts": [ + { + "matches": [ + "*://*.youtube.com/*" + ], + "js": [ + "lib/jquery-3.6.1.min.js", + "content-script.js" + ], + "run_at": "document_end" + } + ], + "permissions": [ + "*://*.youtube.com/*", + "*://*.returnyoutubedislikeapi.com/*", + "storage" + ], + "web_accessible_resources": [ + "css/bar-blue-gray.css", + "css/bar-blue-gray-video-page.css", + "css/bar-bottom-separator.css", + "css/bar-bottom-tooltip.css", + "css/bar-bottom.css", + "css/bar-green-red.css", + "css/bar-green-red-video-page.css", + "css/bar-tooltip.css", + "css/bar-top-separator.css", + "css/bar-top-tooltip.css", + "css/bar-top.css", + "css/bar.css" + ] +} diff --git a/manifest-v2/options.js b/manifest-v2/options.js new file mode 100644 index 0000000..229f343 --- /dev/null +++ b/manifest-v2/options.js @@ -0,0 +1,278 @@ +const DEFAULT_USER_SETTINGS = { + barPosition: 'bottom', + barColor: 'blue-gray', + barLikesColor: '#3095e3', + barDislikesColor: '#cfcfcf', + barColorsSeparator: false, + barHeight: 4, + barOpacity: 100, + barSeparator: false, + useExponentialScaling: false, + barTooltip: true, + useOnVideoPage: false, + showPercentage: false, + cacheDuration: 600000, +} + +function sanitizeHexColor(str) { + return str.replace(/[^#0-9a-zA-Z]/g, '') +} + +function getCssLink(url) { + return $('', { + rel: 'stylesheet', + type: 'text/css', + class: 'ytrb-css', + href: chrome.runtime.getURL(url) + }) +} + +function updateCss() { + $('head').children('.ytrb-css').remove() + if ($('#bar-height').val() !== '0') { + $('head').append(getCssLink('css/bar.css')) + + if ($('#bar-position-top').prop('checked')) { + $('head').append(getCssLink('css/bar-top.css')) + } else { + $('head').append(getCssLink('css/bar-bottom.css')) + } + + if ($('#bar-separator').prop('checked')) { + if ($('#bar-position-top').prop('checked')) { + $('head').append(getCssLink('css/bar-top-separator.css')) + } else { + $('head').append(getCssLink('css/bar-bottom-separator.css')) + } + } + + if ($('#bar-tooltip').prop('checked')) { + $('head').append(getCssLink('css/bar-tooltip.css')) + if ($('#bar-position-top').prop('checked')) { + $('head').append(getCssLink('css/bar-top-tooltip.css')) + } else { + $('head').append(getCssLink('css/bar-bottom-tooltip.css')) + } + } + + } +} + +// Watch for changes that require updating the CSS. +$('#bar-position-top, #bar-position-bottom, #bar-color-blue-gray, #bar-color-green-red, ' + + '#bar-separator, #bar-tooltip').change(updateCss) + +// Watch bar position. +function updateSeparatorPosition() { + $('#bar-separator-position').text($('#bar-position-top').prop('checked') ? 'below' : 'above') +} +$('#bar-position-top').on('change', updateSeparatorPosition) +$('#bar-position-bottom').on('change', updateSeparatorPosition) + +// Watch bar color selector. +$('#bar-color').on('input change', function(event) { + $('#custom-colors-options').toggle($('[name="bar-color"]').val() === 'custom-colors') + if ($('[name="bar-color"]').val() === 'blue-gray') { + document.documentElement.style.setProperty('--ytrb-bar-likes-color', '#3095e3') + document.documentElement.style.setProperty('--ytrb-bar-dislikes-color', '#cfcfcf') + document.documentElement.style.setProperty('--ytrb-bar-dislikes-shadow', 'none') + } else if ($('[name="bar-color"]').val() === 'green-red') { + document.documentElement.style.setProperty('--ytrb-bar-likes-color', '#060') + document.documentElement.style.setProperty('--ytrb-bar-dislikes-color', '#c00') + document.documentElement.style.setProperty('--ytrb-bar-dislikes-shadow', 'inset 1px 0 #fff') + } else if ($('[name="bar-color"]').val() === 'custom-colors') { + document.documentElement.style.setProperty( + '--ytrb-bar-likes-color', + sanitizeHexColor($('#bar-likes-color').val()) + ) + document.documentElement.style.setProperty( + '--ytrb-bar-dislikes-color', + sanitizeHexColor($('#bar-dislikes-color').val()) + ) + document.documentElement.style.setProperty( + '--ytrb-bar-dislikes-shadow', + $('#bar-colors-separator').prop('checked') ? 'inset 1px 0 #fff' : 'none' + ) + } +}) +$('#bar-likes-color').on('input change', function(event) { + if ($('[name="bar-color"]').val() === 'custom-colors') { + document.documentElement.style.setProperty( + '--ytrb-bar-likes-color', + sanitizeHexColor($('#bar-likes-color').val()) + ) + } +}) +$('#bar-dislikes-color').on('input change', function(event) { + if ($('[name="bar-color"]').val() === 'custom-colors') { + document.documentElement.style.setProperty( + '--ytrb-bar-dislikes-color', + sanitizeHexColor($('#bar-dislikes-color').val()) + ) + } +}) +$('#bar-colors-separator').on('input change', function(event) { + if ($('[name="bar-color"]').val() === 'custom-colors') { + document.documentElement.style.setProperty( + '--ytrb-bar-dislikes-shadow', + $('#bar-colors-separator').prop('checked') ? 'inset 1px 0 #fff' : 'none' + ) + } +}) + +// Watch height slider. +$('#bar-height').on('input change', function(event) { + $('#bar-height-text').text($('#bar-height').val() === '0' ? 'Hidden' : $('#bar-height').val() + ' px') + + // Make the slider bubble move with the handle. + $('#bar-height-text').css('left', ($('#bar-height').val() / 16 * 210) + 'px') + + document.documentElement.style.setProperty('--ytrb-bar-height', $('#bar-height').val() + 'px') +}) + +// Watch opacity slider. +$('#bar-opacity').on('input change', function(event) { + $('#bar-opacity-text').text($('#bar-opacity').val() + '%') + + // Make the slider bubble move with the handle. + $('#bar-opacity-text').css('left', ($('#bar-opacity').val() / 100 * 210) + 'px') + + // Temporarily disable the transition when updating the opacity. + $('#thumbnail-preview ytrb-bar').css('transition', 'none') + document.documentElement.style.setProperty('--ytrb-bar-opacity', $('#bar-opacity').val() / 100) + setTimeout(function () { + $('#thumbnail-preview ytrb-bar').css('transition', 'opacity 0.2s ease-out 0.2s') + }) +}) + +// Save settings. +$('#save-btn').click(function() { + let cacheDuration = parseInt($('[name="cache-duration"]').val()) + chrome.storage.local.set({ + barPosition: $('#bar-position-top').prop('checked') ? 'top' : 'bottom', + barColor: $('[name="bar-color"]').val(), + barLikesColor: sanitizeHexColor($('#bar-likes-color').val()), + barDislikesColor: sanitizeHexColor($('#bar-dislikes-color').val()), + barColorsSeparator: $('#bar-colors-separator').prop('checked'), + barHeight: Number($('#bar-height').val()), + barOpacity: Number($('#bar-opacity').val()), + barSeparator: $('#bar-separator').prop('checked'), + useExponentialScaling: $('#use-exponential-scaling').prop('checked'), + barTooltip: $('#bar-tooltip').prop('checked'), + useOnVideoPage: $('#use-on-video-page').prop('checked'), + showPercentage: $('#show-percentage').prop('checked'), + cacheDuration: cacheDuration, + }, function() { + // Show "Settings saved" message. + document.querySelector('#toast').MaterialSnackbar.showSnackbar({ + message: 'Settings saved. Refresh the page.', + timeout: 2000, + }) + }) + chrome.runtime.sendMessage({ + query: 'updateSettings', + cacheDuration: cacheDuration, + }) +}) + +function applySettings(settings, applyColors=true) { + $('#bar-position-' + settings.barPosition).click() + + // Note: We don't restore the custom colors and colors separator settings so + // that users can easily restore to their custom colors if they want to. + if (applyColors) { + $('#bar-likes-color').val(settings.barLikesColor) + if (settings.barLikesColor.length) { + $('#bar-likes-color-container').removeClass('is-invalid').addClass('is-dirty') + } + $('#bar-dislikes-color').val(settings.barDislikesColor) + if (settings.barDislikesColor.length) { + $('#bar-dislikes-color-container').removeClass('is-invalid').addClass('is-dirty') + } + let barColorsSeparator = $('#bar-colors-separator') + if (barColorsSeparator.prop('checked') !== settings.barColorsSeparator) { + barColorsSeparator.click() + } + } + + // We set the bar color after setting the custom colors and colors + // separator option above so that the update triggers get fired properly. + $('#bar-color-' + settings.barColor).click() + + let barHeightSlider = $('#bar-height')[0].MaterialSlider + if (barHeightSlider) { + barHeightSlider.change(settings.barHeight) + } + $('#bar-height').change() + + let barOpacitySlider = $('#bar-opacity')[0].MaterialSlider + if (barOpacitySlider) { + barOpacitySlider.change(settings.barOpacity) + } + $('#bar-opacity').change() + + if ($('#bar-separator').prop('checked') !== settings.barSeparator) { + $('#bar-separator').click() + } + if ($('#use-exponential-scaling').prop('checked') !== settings.useExponentialScaling) { + $('#use-exponential-scaling').click() + } + if ($('#bar-tooltip').prop('checked') !== settings.barTooltip) { + $('#bar-tooltip').click() + } + if ($('#use-on-video-page').prop('checked') !== settings.useOnVideoPage) { + $('#use-on-video-page').click() + } + if ($('#show-percentage').prop('checked') !== settings.showPercentage) { + $('#show-percentage').click() + } + if (parseInt($('[name="cache-duration"]').val()) !== settings.cacheDuration) { + $('#cache-duration-' + settings.cacheDuration.toString()).click() + } +} + +// Restore defaults. +$('#restore-defaults-btn').click(function() { + // Note: We don't restore the custom colors and colors separator settings so + // that users can easily restore to their custom colors if they want to. + applySettings(DEFAULT_USER_SETTINGS, false) +}) + +// Load saved settings. +function restoreOptions() { + chrome.storage.local.get(DEFAULT_USER_SETTINGS, function(settings) { + // In Firefox, `settings` will be undeclared if not previously set. + if (!settings) { + settings = DEFAULT_USER_SETTINGS + } + + // Set any missing settings to their default values. Some settings may be + // missing if loading in settings that were saved using an older version of + // this extension. + for (const [key, value] of Object.entries(DEFAULT_USER_SETTINGS)) { + if (!(key in settings)) { + settings[key] = value + } + } + + applySettings(settings) + }) +} + +document.addEventListener('DOMContentLoaded', function() { + // We restore option twice in case the first time is before the Material + // Design components are loaded. (This is a temporary workaround until a + // better method is figured out.) + restoreOptions() + setTimeout(function () { + updateCss() + }) + + // We use the delay timeout to give the MDL components time to load. + setTimeout(function () { + restoreOptions() + setTimeout(function () { + updateCss() + }) + }, 250) +}) diff --git a/tools/firefox-notes-to-reviewer.txt b/notes/firefox-notes-to-reviewer.txt similarity index 57% rename from tools/firefox-notes-to-reviewer.txt rename to notes/firefox-notes-to-reviewer.txt index 2e67cd3..9f84641 100644 --- a/tools/firefox-notes-to-reviewer.txt +++ b/notes/firefox-notes-to-reviewer.txt @@ -3,8 +3,3 @@ These minified files: * getmdl-select.min.js were downloaded from the source code zip file here: https://github.com/CreativeIT/getmdl-select/releases/tag/v2.0 - -This minified files: - * jstat.min.js -was downloaded from the source code zip file here: -https://github.com/jstat/jstat/releases/tag/1.9.5 \ No newline at end of file diff --git a/tools/notes.txt b/notes/notes.txt similarity index 100% rename from tools/notes.txt rename to notes/notes.txt diff --git a/tools/build_packages.py b/tools/build_packages.py deleted file mode 100644 index 733979e..0000000 --- a/tools/build_packages.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Builds the extension for multiple browsers.""" -import os -import shutil -import tempfile -import zipfile - -CUR_DIR = os.path.join(os.path.dirname(__file__)) -BUILD_DIR = os.path.abspath(os.path.join(CUR_DIR, '../build/')) -EXTENSION_DIR = os.path.abspath(os.path.join(CUR_DIR, '../extension/')) -VALID_BROWSERS = 'chrome', 'firefox', 'edge', 'opera', 'safari', 'firefox_android' -TARGET_BROWSERS = 'chrome', 'firefox', 'edge' - - -def zip_dir(source_dir, output_path, excluded_filenames=None): - if excluded_filenames is None: - excluded_filenames = ['.DS_Store'] - print(f'Zip source path: {source_dir}') - print(f'Zip output path: {output_path}') - print(f'Zipped files:') - with zipfile.ZipFile(output_path, 'w') as z: - for dir_path, dir_names, filenames in os.walk(source_dir): - for filename in filenames: - if filename in excluded_filenames: - continue - path = os.path.join(dir_path, filename) - path_in_zip = path[len(source_dir) :] - print(f' {path_in_zip}') - z.write(path, path_in_zip) - print() - - -def build_package(browser): - assert browser in VALID_BROWSERS, 'Unsupported browser specified.' - package_path = os.path.join(BUILD_DIR, f'{browser}-package.zip') - - with tempfile.TemporaryDirectory() as tmp_dir: - tmp_extension_dir = os.path.join(tmp_dir, 'extension') - shutil.copytree(EXTENSION_DIR, tmp_extension_dir) - manifest_path = os.path.join(tmp_extension_dir, 'manifest.json') - with open(manifest_path, 'r') as f: - manifest_text = f.read() - - if browser in ('firefox', 'safari', 'firefox_android'): - # Remove the `"persistent": false` line from the manifest.json file - # for browsers that do not support persistent background scripts. - # https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/background - manifest_text = manifest_text.replace(',\n "persistent": false', '') - assert manifest_text.find('persistent') == -1, ( - 'Persistent background scripts are not supported by Firefox, however, ' - 'the word "persistent" was found in the manifest.json file for the ' - 'Firefox package.' - ) - - if browser not in ('chrome', 'opera'): - # Change the "chrome_style" key to "browser_style". They are - # equivalent, but some browsers use the "browser_style" - # key instead. - # https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/user_interface/Browser_styles - manifest_text = manifest_text.replace('"chrome_style": false', '"browser_style": false') - assert manifest_text.find('chrome_style') == -1, ( - 'The "chrome_style" key is not supported by Firefox, however,the ' - 'word "chrome_style" was found in the manifest.json file for the ' - 'Firefox package.' - ) - - with open(manifest_path, 'w') as f: - f.write(manifest_text) - - zip_dir(tmp_extension_dir, package_path) - print(f'Generated package: {package_path}\n') - - -def main(): - os.makedirs(BUILD_DIR, exist_ok=True) - for browser in TARGET_BROWSERS: - build_package(browser) - - -if __name__ == '__main__': - main()