Greasy Fork 还支持 简体中文。

Hibernate Idle Tabs

If a tab is unused for a long time, it switches to a light holding page until the tab is focused again. This helps the browser to recover memory, and can speed up the re-opening of large sessions.

目前為 2015-01-24 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Hibernate Idle Tabs
  3. // @namespace HIT
  4. // @description If a tab is unused for a long time, it switches to a light holding page until the tab is focused again. This helps the browser to recover memory, and can speed up the re-opening of large sessions.
  5. // @version 1.1.2
  6. // @downstreamURL http://userscripts.org/scripts/source/123252.user.js
  7. // @include *
  8. // ==/UserScript==
  9.  
  10.  
  11. /* +++ Config +++ */
  12.  
  13. var hibernateIfIdleForMoreThan = 4*60*60; // 4 hours
  14. var restoreTime = 0.5; // in seconds
  15.  
  16. // We need an always-available basically blank HTML page we can navigate to for
  17. // hibernation. The userscript will run there and await re-activation.
  18. // Userscripts do not run on about:blank in Firefox 6.0 or Chromium 2011, but a
  19. // file:///... URL might work.
  20.  
  21. // I got desperate and aimed for a 404 on Google:
  22. // This is not really satisfactory, since it provides an image and unneeded CSS!
  23. var holdingPage = "http://neuralyte.org/~joey/hibernated_tab.html";
  24.  
  25. // If you want to use another holding page, put the old one here to keep your
  26. // current/saved browser sessions working.
  27. var oldHoldingPage = "http://www.google.com/hibernated_tab";
  28.  
  29. var passFaviconToHoldingPage = true;
  30. var fadeHibernatedFavicons = true;
  31.  
  32. var forceHibernateWhenRunTwice = true;
  33.  
  34.  
  35.  
  36. /* +++ Documentation +++ */
  37. //
  38. // On a normal page, checks to see if the user goes idle. (Mouse movements,
  39. // key actions and page focus reset the idle timer.) If the page is left idle
  40. // past the timeout, then the window navigates to a lighter holding page,
  41. // hopefully freeing up memory in the browser.
  42. //
  43. // On a holding page, if the user focuses the window, the window navigates back
  44. // to the original page. (This can be cancelled in Chrome by clicking on
  45. // another tab, but not by paging to another tab with the keyboard!)
  46. //
  47. // I think single-click is also a cancellation now.
  48. //
  49. // In order for the tab of the holding page to present the same favicon as the
  50. // original page, we must capture this image before leaving the original page,
  51. // and pass it to the holding page as a CGI parameter.
  52. //
  53. // (A simpler alternative might be to aim for a 404 on the same domain and use
  54. // that as the holding page.)
  55.  
  56.  
  57.  
  58. // BUG: Sometimes when un-hibernating, the webserver of the page we return to
  59. // complains that the referrer URL header is too long!
  60.  
  61. // TODO: Some users may want the hibernated page to restore immediately when the tab is *refocused*, rather than waiting for a mouseover.
  62.  
  63. // TESTING: Expose a function to allow a bookmarklet to force-hibernate the current tab?
  64.  
  65. // CONSIDER: If we forget about fading the favicon, couldn't we simplify things by just sending the favicon URL rather than its image data? I think I tested this, and although I could load the favicon into the document, I was not successful at getting it into the browser's title tab by adding a new <link rel="icon">.
  66.  
  67.  
  68.  
  69. /* +++ Main +++ */
  70.  
  71. var onHoldingPage = document.location.href.match(holdingPage+"?") != null;
  72.  
  73. // If you change holding page, this keeps the old one working for a while, for
  74. // the sake of running browsers or saved sessions.
  75. if (!onHoldingPage && oldHoldingPage) {
  76. onHoldingPage = document.location.href.match(oldHoldingPage+"?") != null;
  77. }
  78.  
  79. function handleNormalPage() {
  80.  
  81. whenIdleDo(hibernateIfIdleForMoreThan,hibernatePage);
  82.  
  83. function hibernatePage() {
  84.  
  85. var params = {
  86. title: document.title,
  87. url: document.location.href
  88. };
  89.  
  90. function processFavicon(canvas) {
  91. document.body.appendChild(canvas);
  92. if (canvas) {
  93. try {
  94. if (fadeHibernatedFavicons) {
  95. makeCanvasMoreTransparent(canvas);
  96. }
  97. var faviconDataURL = canvas.toDataURL("image/png");
  98. params.favicon_data = faviconDataURL;
  99. } catch (e) {
  100. var extra = ( window != top ? " (running in frame or iframe)" : "" );
  101. console.error("[HIT] Got error"+extra+": "+e+" doc.loc="+document.location.href);
  102. // We get "Error: SECURITY_ERR: DOM Exception 18" (Chrome) if
  103. // the favicon is from a different host.
  104. }
  105. }
  106. reallyHibernatePage();
  107. }
  108.  
  109. function reallyHibernatePage() {
  110. var queryString = buildQueryParameterString(params);
  111. document.location = holdingPage + "?" + queryString;
  112. }
  113.  
  114. if (passFaviconToHoldingPage) {
  115. // I don't know how to grab the contents of the current favicon, so we
  116. // try to directly load a copy for ourselves.
  117. var url = document.location.href;
  118. var targetHost = url.replace(/.*:\/\//,'').replace(/\/.*/,'');
  119. loadFaviconIntoCanvas(targetHost,processFavicon);
  120. } else {
  121. reallyHibernatePage();
  122. }
  123.  
  124. }
  125.  
  126. function makeCanvasMoreTransparent(canvas) {
  127. var wid = canvas.width;
  128. var hei = canvas.height;
  129. var ctx = canvas.getContext("2d");
  130. var img = ctx.getImageData(0,0,wid,hei);
  131. var data = img.data;
  132. var len = 4*wid*hei;
  133. for (var ptr=0;ptr<len;ptr+=4) {
  134. data[ptr+3] /= 4; // alpha channel
  135. }
  136. // May or may not be needed:
  137. ctx.putImageData(img,0,0);
  138. }
  139.  
  140.  
  141.  
  142. if (forceHibernateWhenRunTwice) {
  143. if (window.hibernate_idle_tabs_loaded) {
  144. hibernatePage();
  145. }
  146. window.hibernate_idle_tabs_loaded = true;
  147. }
  148.  
  149.  
  150. }
  151.  
  152. function handleHoldingPage() {
  153.  
  154. var params = getQueryParameters();
  155.  
  156. // setHibernateStatus("Holding page for " + params.title + "\n with URL: "+params.url);
  157. // var titleReport = params.title + " (Holding Page)";
  158. var titleReport = "(" + (params.title || params.url) + " :: Hibernated)";
  159. setWindowTitle(titleReport);
  160.  
  161. var mainReport = titleReport;
  162. if (params.title) {
  163. /*
  164. statusElement.appendChild(document.createElement("P"));
  165. var div = document.createElement("tt");
  166. div.style.fontSize = "0.8em";
  167. div.appendChild(document.createTextNode(params.url));
  168. statusElement.appendChild(div);
  169. */
  170. mainReport += "\n" + params.url;
  171. }
  172.  
  173. setHibernateStatus(mainReport);
  174.  
  175. try {
  176. var faviconDataURL = params.favicon_data;
  177. if (!faviconDataURL) {
  178. // If we do not have a favicon, it is preferable to present an empty/transparent favicon, rather than let the browser show the favicon of the holding page site!
  179. faviconDataURL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAEklEQVQ4jWNgGAWjYBSMAggAAAQQAAF/TXiOAAAAAElFTkSuQmCC";
  180. }
  181. writeFaviconFromDataString(faviconDataURL);
  182. } catch (e) {
  183. console.error(""+e);
  184. }
  185.  
  186. function restoreTab(evt) {
  187. var url = decodeURIComponent(params.url);
  188. setHibernateStatus("Returning to: "+url);
  189. document.location.replace(url);
  190. /*
  191. // Alternative; preserves "forward"
  192. window.history.back(); // TESTING! With the fallback below, this seemed to work 90% of the time?
  193. // Sometimes it doesn't work. So we fallback to old method:
  194. setTimeout(function(){
  195. setHibernateStatus("window.history.back() FAILED - setting document.location");
  196. setTimeout(function(){
  197. document.location.replace(url); // I once saw this put ':'s when it should have put '%35's or whatever. (That broke 'Up' bookmarklet.)
  198. },1000);
  199. },2500);
  200. */
  201. evt.preventDefault(); // Accept responsibility for the double-click.
  202. return false; // Prevent browser from doing anything else with it (e.g. selecting the word under the cursor).
  203. }
  204.  
  205. checkForActivity();
  206.  
  207. function checkForActivity() {
  208.  
  209. var countdownTimer = null;
  210.  
  211. // listen(document.body,'mousemove',startCountdown); // In Firefox this ignore mousemove on empty space (outside the document content), so trying window...
  212. listen(window,'mousemove',startCountdown); // Likewise for click below!
  213. // listen(document.body,'blur',clearCountdown); // Does not fire in Chrome?
  214. listen(window,'blur',clearCountdown); // For Chrome
  215. //listen(window,'mouseout',clearCountdown); // Firefox appears to be firing this when my mouse is still over the window, preventing navigation! Let's just rely on 'blur' instead.
  216. // listen(document.body,'click',clearCountdown);
  217. listen(window,'click',clearCountdown);
  218. listen(window,'dblclick',restoreTab);
  219.  
  220. function startCountdown(e) {
  221. if (countdownTimer != null) {
  222. // There is already a countdown running - do not start.
  223. return;
  224. }
  225. var togo = restoreTime*1000;
  226. function countDown() {
  227. setHibernateStatus(mainReport +
  228. '\n' + "Tab will restore in "+(togo/1000).toFixed(1)+" seconds." +
  229. ' ' + "Click or defocus to pause." +
  230. ' ' + "Or double click to restore now!"
  231. );
  232. if (togo <= 0) {
  233. restoreTab();
  234. } else {
  235. togo -= 1000;
  236. if (countdownTimer)
  237. clearTimeout(countdownTimer);
  238. countdownTimer = setTimeout(countDown,1000);
  239. }
  240. }
  241. countDown();
  242. }
  243.  
  244. function clearCountdown(ev) {
  245. if (countdownTimer) {
  246. clearTimeout(countdownTimer);
  247. }
  248. countdownTimer = null;
  249. var evReport = "";
  250. if (ev) {
  251. evReport = " by "+ev.type+" on "+this;
  252. }
  253. var report = mainReport + '\n' + "Paused" + evReport + "";
  254. setHibernateStatus(report);
  255. }
  256.  
  257. }
  258.  
  259. }
  260.  
  261. if (onHoldingPage) {
  262. handleHoldingPage();
  263. } else {
  264. handleNormalPage();
  265. }
  266.  
  267.  
  268.  
  269. /* +++ Library Functions +++ */
  270.  
  271. function listen(target,eventType,handler,capture) {
  272. target.addEventListener(eventType,handler,capture);
  273. }
  274.  
  275. function ignore(target,eventType,handler,capture) {
  276. target.removeEventListener(eventType,handler,capture);
  277. }
  278.  
  279. // Given an object, encode its properties and values into a URI-ready CGI query string.
  280. function buildQueryParameterString(params) {
  281. return Object.keys(params).map( function(key) { return key+"="+encodeURIComponent(params[key]); } ).join("&");
  282. }
  283.  
  284. // Returns an object whose keys and values match those of the CGI query string of the current document.
  285. function getQueryParameters() {
  286. var queryString = document.location.search;
  287. var params = {};
  288. queryString.replace(/^\?/,'').split("&").map( function(s) {
  289. var part = s.split("=");
  290. var key = part[0];
  291. var value = decodeURIComponent(part[1]);
  292. params[key] = value;
  293. });
  294. return params;
  295. }
  296.  
  297. function whenIdleDo(idleTimeoutSecs,activateIdleEvent) {
  298.  
  299. var timer = null;
  300. var pageLastUsed = new Date().getTime();
  301.  
  302. function setNotIdle() {
  303. pageLastUsed = new Date().getTime();
  304. }
  305.  
  306. function checkForIdle() {
  307. var msSinceLastUsed = new Date().getTime() - pageLastUsed;
  308. if (msSinceLastUsed > idleTimeoutSecs * 1000) {
  309. activateIdleEvent();
  310. }
  311. setTimeout(checkForIdle,idleTimeoutSecs/5*1000);
  312. }
  313.  
  314. setTimeout(checkForIdle,idleTimeoutSecs*1000);
  315.  
  316. listen(document.body,'mousemove',setNotIdle);
  317. listen(document.body,'focus',setNotIdle);
  318. listen(document.body,'keydown',setNotIdle);
  319.  
  320. }
  321.  
  322.  
  323.  
  324. /* +++ Local Convenience Functions +++ */
  325.  
  326. var statusElement = null;
  327. function checkStatusElement() {
  328. if (!statusElement) {
  329. while (document.body.firstChild) {
  330. document.body.removeChild(document.body.firstChild);
  331. }
  332. statusElement = document.createElement("div");
  333. document.body.insertBefore(statusElement,document.body.firstChild);
  334. statusElement.style.textAlign = "center";
  335. }
  336. }
  337.  
  338. function setWindowTitle(msg) {
  339. msg = ""+msg;
  340. document.title = msg;
  341. }
  342.  
  343. function setHibernateStatus(msg) {
  344. msg = ""+msg;
  345. checkStatusElement();
  346. statusElement.textContent = msg;
  347. statusElement.innerText = msg; // IE
  348. // Currently '\n' works in Chrome, but not in Firefox.
  349. }
  350.  
  351.  
  352.  
  353.  
  354.  
  355.  
  356. /* +++ Favicon, Canvas and DataURL Magic +++ */
  357.  
  358. function loadFaviconForHost(targetHost,callback) {
  359.  
  360. // Try to load a favicon image for the given host, and pass it to callback.
  361. // Except: If there is a link with rel="icon" in the page, with host
  362. // matching the current page location, load that image file instead of
  363. // guessing the extension!
  364.  
  365. var favicon = document.createElement('img');
  366. favicon.addEventListener('load',function() {
  367. callback(favicon);
  368. });
  369.  
  370. var targetProtocol = document.location.protocol || "http:";
  371.  
  372. // If there is a <link rel="icon" ...> in the current page, then I think that overrides the site-global favicon.
  373. // NOTE: This is not appropriate if a third party targetHost was requested, only if they really wanted the favicon for the current page!
  374. var foundLink = null;
  375. var linkElems = document.getElementsByTagName("link");
  376. for (var i=0;i<linkElems.length;i++) {
  377. var link = linkElems[i];
  378. if (link.rel === "icon" || link.rel === "shortcut icon") {
  379. // Since we can't read the image data of images from third-party hosts, we skip them and keep searching.
  380. if (link.host == document.location.host) {
  381. foundLink = link;
  382. break;
  383. }
  384. }
  385. }
  386. if (foundLink) {
  387. favicon.addEventListener('error',function(){ callback(favicon); });
  388. favicon.src = foundLink.href; // Won't favicon.onload cause an additional callback to the one below?
  389. // NOTE: If we made the callback interface pass favicon as 'this' rather than an argument, then we wouldn't need to wrap it here (the argument may be evt).
  390. favicon = foundLink;
  391. callback(favicon);
  392. return;
  393. }
  394.  
  395. var extsToTry = ["jpg","gif","png","ico"]; // iterated in reverse order
  396. function tryNextExtension() {
  397. var ext = extsToTry.pop();
  398. if (ext == null) {
  399. console.log("Ran out of extensions to try for "+targetHost+"/favicon.???");
  400. // We run the callback anyway!
  401. callback(null);
  402. } else {
  403. favicon.src = targetProtocol+"//"+targetHost+"/favicon."+ext;
  404. }
  405. }
  406. favicon.addEventListener('error',tryNextExtension);
  407. tryNextExtension();
  408. // When the favicon is working we can remove the canvas, but until then we may as well keep it visible!
  409. }
  410.  
  411. function writeFaviconFromCanvas(canvas) {
  412. var faviconDataURL = canvas.toDataURL("image/png");
  413. // var faviconDataURL = canvas.toDataURL("image/x-icon;base64");
  414. // console.log("Got data URL: "+faviconDataURL.substring(0,10+"... (length "+faviconDataURL.length+")");
  415. writeFaviconFromDataString(faviconDataURL);
  416. }
  417.  
  418. function writeFaviconFromDataString(faviconDataURL) {
  419.  
  420. var d = document, h = document.getElementsByTagName('head')[0];
  421.  
  422. // Create this favicon
  423. var ss = d.createElement('link');
  424. ss.rel = 'shortcut icon';
  425. ss.type = 'image/x-icon';
  426. ss.href = faviconDataURL;
  427. /*
  428. // Remove any existing favicons
  429. var links = h.getElementsByTagName('link');
  430. for (var i=0; i<links.length; i++) {
  431. if (links[i].href == ss.href) return;
  432. if (links[i].rel == "shortcut icon" || links[i].rel=="icon")
  433. h.removeChild(links[i]);
  434. }
  435. */
  436. // Add this favicon to the head
  437. h.appendChild(ss);
  438.  
  439. // Force browser to acknowledge
  440. // I saw this trick somewhere. I don't know what browser requires it. But I just left it in!
  441. var shim = document.createElement('iframe');
  442. shim.width = shim.height = 0;
  443. document.body.appendChild(shim);
  444. shim.src = "icon";
  445. document.body.removeChild(shim);
  446.  
  447. }
  448.  
  449. function loadFaviconIntoCanvas(targetHost,callback) {
  450.  
  451. // console.log("Getting favicon for: "+targetHost);
  452.  
  453. var canvas = document.createElement('canvas');
  454. var ctx = canvas.getContext('2d');
  455.  
  456. loadFaviconForHost(targetHost,gotFavicon);
  457.  
  458. function gotFavicon(favicon) {
  459. if (favicon) {
  460. // console.log("Got favicon from: "+favicon.src);
  461. canvas.width = favicon.width;
  462. canvas.height = favicon.height;
  463. ctx.drawImage( favicon, 0, 0 );
  464. }
  465. callback(canvas);
  466. }
  467.  
  468. }
  469.  
  470. /* This throws a security error from canvas.toDataURL(), I think because we are
  471. trying to read something from a different domain than the script!
  472. In Chrome: "SECURITY_ERR: DOM Exception 18"
  473. */
  474. // loadFaviconIntoCanvas(document.location.host,writeFaviconFromCanvas);
  475.