hatebu-counter

displays current page's Hatena Bookmark (http://b.hatena.ne.jp/) count and comments on a corner of the page.

  1. // ==UserScript==
  2. // @name hatebu-counter
  3. // @name:ja はてブカウンター
  4. // @description displays current page's Hatena Bookmark (http://b.hatena.ne.jp/) count and comments on a corner of the page.
  5. // @description:ja 現在閲覧中のページの、はてなブックマーク数およびコメントをページ右下にコンパクトに表示するスクリプトです。
  6. // @namespace http://reppets.hatenablog.com/
  7. // @version 1.1.2
  8. // @compatible firefox
  9. // @compatible chrome
  10.  
  11. // @include *
  12. // @noframes
  13. // @run-at document-end
  14. // @require https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js
  15. // @resource loadingIcon http://cdn-ak.f.st-hatena.com/images/fotolife/r/reppets/20131208/20131208222202.gif
  16. // @resource lockedIcon http://cdn-ak.f.st-hatena.com/images/fotolife/r/reppets/20131223/20131223190146.png
  17. // @resource unlockedIcon http://cdn-ak.f.st-hatena.com/images/fotolife/r/reppets/20131223/20131223181716.png
  18. // @resource closeIcon http://cdn-ak.f.st-hatena.com/images/fotolife/r/reppets/20131223/20131223190145.png
  19. // @resource errorIcon http://cdn-ak.f.st-hatena.com/images/fotolife/r/reppets/20131223/20131223190147.png
  20. // @resource reloadIcon http://cdn-ak.f.st-hatena.com/images/fotolife/r/reppets/20131223/20131223221947.png
  21. // @resource hatebuFavicon http://b.hatena.ne.jp/favicon.ico
  22. // @grant GM_getResourceURL
  23. // @grant GM_xmlhttpRequest
  24. // ==/UserScript==
  25.  
  26. // INFORMATION ABOUT IMAGE RESOURCES:
  27. // the loading icon generated with http://www.ajaxload.info/
  28. // the other icons (except Hatena Bookmark icon) are from http://modernuiicons.com/
  29. // under license of CC BY-ND(3.0) https://github.com/Templarian/WindowsIcons/blob/117f1a468f77248df87677e78e3453d07971ca55/WindowsPhone/license.txt
  30.  
  31. // LICENSE INFORMATION:
  32. /*
  33. * Copyright (c) 2014-2017, reppets <all.you.need.is.word@gmail.com>
  34. * All rights reserved.
  35. *
  36. * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
  37. *
  38. * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  39. *
  40. * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  41. *
  42. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  43. */
  44.  
  45.  
  46. // ====== OPTIONS ==============================================================
  47.  
  48. // use lazy loading or not.
  49. var usesLazyLoad = true;
  50.  
  51. // ====== CONSTANTS ============================================================
  52. const HATENA_FAVICON_URL = GM_getResourceURL('hatebuFavicon');
  53. const LOADING_ICON_URL = GM_getResourceURL('loadingIcon');
  54. const LOCKED_ICON_URL = GM_getResourceURL('lockedIcon');
  55. const UNLOCKED_ICON_URL = GM_getResourceURL('unlockedIcon');
  56. const CLOSE_ICON_URL = GM_getResourceURL('closeIcon');
  57. const ERROR_ICON_URL = GM_getResourceURL('errorIcon');
  58. const RELOAD_ICON_URL = GM_getResourceURL('reloadIcon');
  59.  
  60. const MESSAGE_NO_COMMENT = 'コメントはありません';
  61.  
  62. const INTENTIONAL_RELOAD_DELAY_MSEC = 350;
  63.  
  64. // ====== IFRAME CONTENTS ======================================================
  65.  
  66. var iframeBodyContent = '<div id="wrapper"><!--\
  67. --><ul id="commentlist"></ul><!--\
  68. --><div id="messageline"><!--\
  69. --><span id="message"></span><!--\
  70. --></div><!--\
  71. --><div id="counterline"><!--\
  72. --><img id="close" class="icon action" alt="閉じる" title="閉じる"><!--\
  73. --><img id="lock" class="icon action" alt="ロックする" title="ロックする"><!--\
  74. --><img id="reload" class="icon action" alt="リロード" title="リロード"><!--\
  75. --><a id="hatebuentrylink" target="_blank"><img id="hatebufavicon" class="icon" alt="はてなブックマークエントリーページへ" title="はてなブックマークエントリーページへ"></a><!--\
  76. --><span id="count"></span><!--\
  77. --><img id="countloaderror" class="icon"><!--\
  78. --></div><!--\
  79. --></div>';
  80.  
  81. var iframeHeadContent = '<meta charset="UTF-8" />\
  82. <title></title>\
  83. <style type="text/css">\
  84. * {\
  85. padding: 0;\
  86. border: 0;\
  87. margin: 0;\
  88. font-size: small;\
  89. }\
  90. body {\
  91. background-color: rgba(0,0,0,0.8);\
  92. color: white;\
  93. display: block;\
  94. }\
  95. img {\
  96. display: none;\
  97. }\
  98. img.icon {\
  99. height: 1em;\
  100. width: 1em;\
  101. margin: 3px;\
  102. vertical-align: middle;\
  103. }\
  104. img.action {\
  105. cursor: pointer;\
  106. }\
  107. #commentlist {\
  108. margin: 0;\
  109. padding: 1em 1em 1em 1em;\
  110. list-style: square inside;\
  111. display: none;\
  112. width: 500px;\
  113. height: auto;\
  114. white-space: normal;\
  115. overflow-y: auto;\
  116. }\
  117. #commentlist li {\
  118. margin-bottom: 1.1ex;\
  119. line-height: 110%;\
  120. word-wrap: break-word;\
  121. }\
  122. #messageline {\
  123. margin: 0 0.8em 0 0.8em;\
  124. padding: 0.5em;\
  125. height: auto;\
  126. width: auto;\
  127. overflow: auto;\
  128. display: none;\
  129. }\
  130. #counterline {\
  131. padding: 2px 5px 3px 3px;\
  132. margin: 0;\
  133. border: 0;\
  134. text-align: right;\
  135. width: auto;\
  136. height: auto;\
  137. display: block;\
  138. }\
  139. #count {\
  140. vertical-align:middle;\
  141. }\
  142. #wrapper {\
  143. display: block;\
  144. width: auto;\
  145. height: auto;\
  146. position: absolute;\
  147. right: 0;\
  148. bottom: 0;\
  149. white-space: nowrap;\
  150. }\
  151. </style>';
  152.  
  153.  
  154.  
  155. // ====== STYLE DEFINITIONS ====================================================
  156. // tab style
  157. var tabStyle = {
  158. 'position':'fixed'
  159. ,'z-index':'2147483647'
  160. ,'display':'block'
  161. ,'right':'0px'
  162. ,'bottom':'0px'
  163. ,'visibility':'visible'
  164. ,'overflow':'visible'
  165. ,'border': '0'
  166. ,'padding': '0'
  167. ,'margin': '0'
  168. ,'width':'auto'
  169. ,'height':'auto'
  170. ,'background':'rgba(1,0,0,0.8) none'
  171. };
  172.  
  173. var iframeStyle = {
  174. 'position':'fixed'
  175. ,'z-index':'2147483647'
  176. ,'display':'block'
  177. ,'right':'0px'
  178. ,'bottom':'0px'
  179. ,'width':'auto'
  180. ,'height':'auto'
  181. ,'max-width':'none'
  182. ,'min-width':'none'
  183. ,'max-height':'none'
  184. ,'min-height':'none'
  185. ,'background':'rgba(0,0,0,0) none'
  186. ,'color':'rgba(255,255,255,0.8)'
  187. ,'overflow':'auto'
  188. ,'padding': '0'
  189. ,'border': '0'
  190. ,'margin': '0'
  191. ,'text-align':'left'
  192. ,'vertical-align':'bottom'
  193. };
  194.  
  195. // ====== DOM CREATION =========================================================
  196. var iframe = $('<iframe/>');
  197. iframe.attr('src', 'about:blank');
  198. iframe.css(iframeStyle);
  199. iframe.load(function() {
  200. var idoc = iframe.contents()[0];
  201. iframe.document = idoc;
  202. iframe.body = $('body',idoc);
  203. iframe.body.append(iframeBodyContent);
  204. $('head',idoc).append(iframeHeadContent);
  205. constructIFrame(iframe);
  206. retrieveCount(iframe);
  207. if (!usesLazyLoad) {
  208. retrieveComments(iframe);
  209. }
  210. });
  211. $(document.body).append(iframe);
  212.  
  213. function showInline() {
  214. if (this.isDisplayable) {
  215. this.css('display', 'inline');
  216. }
  217. }
  218.  
  219. function showBlock() {
  220. if (this.isDisplayable) {
  221. this.css('display', 'block');
  222. }
  223. }
  224.  
  225. function hide() {
  226. this.css('display', 'none');
  227. }
  228.  
  229. function constructIcon(element, src, isDisplayable) {
  230. element.attr('src', src);
  231. element.appear = showInline;
  232. element.disappear = hide;
  233. element.isDisplayable = isDisplayable;
  234. return element;
  235. }
  236.  
  237. function constructIFrame(iframe) {
  238. // set iframe attributes
  239. iframe.isLocked = false;
  240.  
  241. // methods
  242. iframe.update = function() {
  243. if (this.isExpanded) {
  244. this.expand();
  245. } else {
  246. this.shrink();
  247. }
  248. };
  249.  
  250. iframe.expand = function() {
  251. this.isExpanded = true;
  252. this.closeIcon.appear();
  253. this.lockIcon.appear();
  254. this.reloadIcon.appear();
  255. this.commentList.appear();
  256. this.messageLine.appear();
  257. this.setIFrameSize();
  258. };
  259.  
  260. iframe.shrink = function() {
  261. if (this.isLocked) {
  262. return;
  263. }
  264. this.isExpanded = false;
  265. this.closeIcon.disappear();
  266. this.lockIcon.disappear();
  267. this.reloadIcon.disappear();
  268. this.commentList.disappear();
  269. this.messageLine.disappear();
  270. this.setIFrameSize();
  271. };
  272.  
  273. iframe.setMessage = function( message) {
  274. this.messageText.text(message);
  275. this.messageLine.isDisplayable = true;
  276. };
  277.  
  278. iframe.setIFrameSize = function() {
  279. var newHeight = $('#wrapper',iframe.body).outerHeight();
  280. var newWidth = $('#wrapper',iframe.body).outerWidth();
  281. iframe.css('height',newHeight+'px');
  282. iframe.css('width',newWidth+'px');
  283. };
  284.  
  285. // set icons
  286. iframe.closeIcon = constructIcon($('#close', iframe.body), CLOSE_ICON_URL, true);
  287. iframe.closeIcon.click(function() {
  288. iframe.remove();
  289. });
  290.  
  291. iframe.lockIcon = constructIcon($('#lock', iframe.body), UNLOCKED_ICON_URL, true);
  292. iframe.lockIcon.click(function() {
  293. if (iframe.isLocked) {
  294. iframe.lockIcon.attr('src', UNLOCKED_ICON_URL);
  295. iframe.isLocked = false;
  296. } else {
  297. iframe.lockIcon.attr('src', LOCKED_ICON_URL);
  298. iframe.isLocked = true;
  299. }
  300. });
  301.  
  302. iframe.reloadIcon = constructIcon($('#reload', iframe.body), RELOAD_ICON_URL, true);
  303. iframe.reloadIcon.click(function() {
  304. iframe.hatebuIcon.attr('src', LOADING_ICON_URL);
  305. iframe.countText.text('-');
  306. iframe.commentList.empty();
  307.  
  308. setTimeout(
  309. function() {
  310. retrieveCount(iframe);
  311. retrieveComments(iframe);
  312. }
  313. , INTENTIONAL_RELOAD_DELAY_MSEC);
  314. });
  315.  
  316. iframe.hatebuIcon = constructIcon($('#hatebufavicon', iframe.body), LOADING_ICON_URL, true);
  317. iframe.hatebuIcon.appear();
  318.  
  319. iframe.errorIcon = constructIcon($('#countloaderror', iframe.body), ERROR_ICON_URL, false);
  320.  
  321.  
  322. // set other elements
  323. iframe.entryLink = $('#hatebuentrylink', iframe.body);
  324. let bookmarkUrl = getCanonicalUrl();
  325. iframe.entryLink.attr('href', 'http://b.hatena.ne.jp/entry/'+ (bookmarkUrl.lastIndexOf('https://',0) < 0 ? bookmarkUrl.replace('http://','') : bookmarkUrl.replace('https://','s/')));
  326.  
  327. iframe.commentList = $('#commentlist', iframe.body);
  328. iframe.commentList.appear = showBlock;
  329. iframe.commentList.disappear = hide;
  330. iframe.commentList.css('max-height',(Math.round(window.innerHeight*0.7)+'px'));
  331. iframe.commentList.isDisplayable = false;
  332.  
  333. iframe.messageText = $('#message', iframe.body);
  334. iframe.messageLine = $('#messageline', iframe.body);
  335. iframe.messageLine.appear = showBlock;
  336. iframe.messageLine.disappear = hide;
  337. iframe.messageLine.isDisplayable = false;
  338.  
  339. iframe.countText = $('#count', iframe.body);
  340. iframe.countText.text('-');
  341.  
  342. iframe.wrapper = $('#wrapper', iframe.body);
  343. iframe.shrink();
  344.  
  345. // event handlers
  346. if (usesLazyLoad) {
  347. var commentLoadHandler = function() {
  348. iframe.hatebuIcon.attr('src', GM_getResourceURL('loadingIcon'));
  349. retrieveComments(iframe);
  350. iframe.wrapper.unbind('mouseenter', commentLoadHandler);
  351. };
  352. iframe.wrapper.mouseenter(commentLoadHandler);
  353. }
  354.  
  355. iframe.wrapper.mouseenter(function() {
  356. iframe.expand();
  357. });
  358.  
  359. iframe.wrapper.mouseleave(function() {
  360. iframe.shrink();
  361. });
  362.  
  363. $(window).resize(function() {
  364. iframe.commentList.css('max-height',(Math.round(window.innerHeight*0.7)+'px'));
  365. });
  366.  
  367. }
  368.  
  369. function retrieveCount(iframe) {
  370. GM_xmlhttpRequest({
  371. method:'GET',
  372. url:'http://api.b.st-hatena.com/entry.count?url='+encodeURIComponent(getCanonicalUrl()),
  373. onload: function(response) {
  374. if (response.status >= 400) {
  375. iframe.errorIcon.isDisplayable = true;
  376. var errorComment = 'ブックマーク数取得エラー: '+response.status+' '+response.statusText;
  377. iframe.errorIcon.attr('alt', errorComment);
  378. iframe.errorIcon.attr('title', errorComment);
  379. iframe.update();
  380. return;
  381. }
  382. iframe.hatebuIcon.attr('src', HATENA_FAVICON_URL);
  383. if (response.responseText) {
  384. iframe.countText.text(response.responseText);
  385. } else {
  386. iframe.countText.text('0');
  387. }
  388. iframe.update();
  389. }
  390. });
  391. }
  392.  
  393. function retrieveComments(iframe) {
  394. GM_xmlhttpRequest({
  395. method:'GET',
  396. url:'http://b.hatena.ne.jp/entry/jsonlite/?url='+encodeURIComponent(getCanonicalUrl()),
  397. onload: function(response) {
  398. if (response.status >= 400) {
  399. iframe.setMessage('コメント取得エラー : '+response.status + ' '+response.statusText);
  400. } else if (response.responseText && response.responseText != 'null') {
  401. var comments = JSON.parse(response.responseText);
  402. var hasComment = false;
  403. for (var i in comments.bookmarks) {
  404. var user = comments.bookmarks[i].user;
  405. var comment = comments.bookmarks[i].comment;
  406. if (comment) {
  407. var item = $('<li/>');
  408. item.text(user+' : '+comment);
  409. iframe.commentList.append(item);
  410. iframe.commentList.isDisplayable = true;
  411. hasComment = true;
  412. }
  413. }
  414. if (!hasComment) {
  415. iframe.setMessage(MESSAGE_NO_COMMENT);
  416. }
  417. } else {
  418. iframe.setMessage(MESSAGE_NO_COMMENT);
  419. }
  420. iframe.hatebuIcon.attr('src', HATENA_FAVICON_URL);
  421.  
  422. iframe.update();
  423. }
  424. });
  425. }
  426.  
  427. function getCanonicalUrl() {
  428. let e = $("link[rel='canonical']", document);
  429. if (e.length===0) {
  430. return document.URL;
  431. } else {
  432. return e.get(0).href;
  433. }
  434. }