futaba auto reloader

赤福Firefox版で自動更新しちゃう(実況モードもあるよ!)

当前为 2020-06-20 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name futaba auto reloader
  3. // @namespace https://github.com/himuro-majika
  4. // @description 赤福Firefox版で自動更新しちゃう(実況モードもあるよ!)
  5. // @author himuro_majika
  6. // @include http://*.2chan.net/*/res/*
  7. // @include https://*.2chan.net/*/res/*
  8. // @include http://board.futakuro.com/*/res/*
  9. // @require http://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js
  10. // @version 1.8.0
  11. // @grant GM_addStyle
  12. // @grant GM_xmlhttpRequest
  13. // @license MIT
  14. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAAPUExURYv4i2PQYy2aLUe0R////zorx9oAAAAFdFJOU/////8A+7YOUwAAAElJREFUeNqUj1EOwDAIQoHn/c88bX+2fq0kRsAoUXVAfwzCttWsDWzw0kNVWd2tZ5K9gqmMZB8libt4pSg6YlO3RnTzyxePAAMAzqMDgTX8hYYAAAAASUVORK5CYII=
  15. // ==/UserScript==
  16. this.$ = this.jQuery = jQuery.noConflict(true);
  17.  
  18. (function ($) {
  19. /*
  20. * 設定
  21. */
  22. var USE_SOUDANE = true; //そうだねをハイライト表示する
  23. var USE_CLEAR_BUTTON = true; //フォームにクリアボタンを表示する
  24. var USE_TITLE_NAME = true; //新着レス数・スレ消滅状態をタブに表示する
  25. var RELOAD_INTERVAL_NORMAL = 60000; //リロード間隔[ミリ秒](通常時)
  26. var RELOAD_INTERVAL_LIVE = 5000; //リロード間隔[ミリ秒](実況モード時)
  27. var LIVE_SCROLL_INTERVAL = 12; //実況モードスクロール間隔[ミリ秒]
  28. var LIVE_SCROLL_SPEED = 2; //実況モードスクロール幅[px]
  29. var LIVE_TOGGLE_KEY = "76"; //実況モードON・OFF切り替えキーコード(With Alt)
  30. var SHOW_NORMAL_BUTTON = true; //通常モードボタンを表示する
  31. var USE_NOTIFICATION_DEFAULT = false; // 新着レスの通知をデフォルトで有効にする
  32. var USE_SAVE_MHT = false; // スレ消滅時にMHTで保存する
  33.  
  34. var res = 0; //新着レス数
  35. var timerNormal, timerLiveReload, timerLiveScroll, timerSoudane;
  36. var url = location.href;
  37. var script_name = "futaba_auto_reloader";
  38. var isWindowActive = true; // タブのアクティブ状態
  39. var isNotificationEnable = USE_NOTIFICATION_DEFAULT; // 通知の有効フラグ
  40. var normal_flag = true; //通常モード有効フラグ
  41. var live_flag = false; //実況モード有効フラグ
  42.  
  43. if(!isFileNotFound()){
  44. setNormalReload();
  45. }
  46. soudane();
  47. makeFormClearButton();
  48. reset_title();
  49. make_live_button();
  50. addCss();
  51. setWindowFocusEvent();
  52. observeInserted();
  53. showFindNextThread();
  54.  
  55. //通常リロード開始
  56. function setNormalReload() {
  57. timerNormal = setInterval(rel, RELOAD_INTERVAL_NORMAL);
  58. console.log(script_name + ": Start auto reloading @" + url);
  59. }
  60. //通常リロード停止
  61. function clearNormalReload() {
  62. clearInterval(timerNormal);
  63. console.log(script_name + ": Stop auto reloading @" + url);
  64. }
  65. /*
  66. * 404チェック
  67. */
  68. function isFileNotFound() {
  69. if(document.title == "404 File Not Found") {
  70. return true;
  71. }
  72. else {
  73. return false;
  74. }
  75. }
  76.  
  77. /*
  78. * ボタン作成
  79. */
  80. function make_live_button() {
  81. //通常モードボタン
  82. var $normalButton = $("<a>", {
  83. id: "GM_FAR_relButton_normal",
  84. class: "GM_FAR_relButton",
  85. text: "[通常]",
  86. title: (RELOAD_INTERVAL_NORMAL / 1000) + "秒毎のリロード",
  87. css: {
  88. cursor: "pointer",
  89. "background-color": "#ea8",
  90. },
  91. click: function() {
  92. toggleNormalMode();
  93. }
  94. });
  95.  
  96. //実況モードボタン
  97. var $liveButton = $("<a>", {
  98. id: "GM_FAR_relButton_live",
  99. class: "GM_FAR_relButton",
  100. text: "[実況(Alt+" + String.fromCharCode(LIVE_TOGGLE_KEY) + ")]",
  101. title: (RELOAD_INTERVAL_LIVE / 1000) + "秒毎のリロード + スクロール",
  102. css: {
  103. cursor: "pointer",
  104. },
  105. click: function() {
  106. liveMode();
  107. }
  108. });
  109. // 通知ボタン
  110. var $notificationButton = $("<a>", {
  111. id: "GM_FAR_notificationButton",
  112. text: "[通知]",
  113. title: "新着レスのポップアップ通知",
  114. css: {
  115. cursor: "pointer",
  116. },
  117. click: function() {
  118. toggleNotification();
  119. }
  120. });
  121. if (isNotificationEnable) {
  122. $notificationButton.css("background-color", "#a9d8ff");
  123. }
  124.  
  125. var $input = $("input[value$='信する']");
  126. $input.after($notificationButton);
  127. $input.after($liveButton);
  128. if(SHOW_NORMAL_BUTTON){
  129. $input.after($normalButton);
  130. }
  131.  
  132. //実況モードトグルショートカットキー
  133. window.addEventListener("keydown",function(e) {
  134. if ( e.altKey && e.keyCode == LIVE_TOGGLE_KEY ) {
  135. liveMode();
  136. }
  137. }, false);
  138.  
  139. /*
  140. * 通常モード切り替え
  141. */
  142. function toggleNormalMode() {
  143. if(normal_flag) {
  144. clearNormalReload();
  145. $normalButton.css("background" , "none");
  146. normal_flag = false;
  147. } else {
  148. setNormalReload();
  149. $normalButton.css("background-color" , "#ea8");
  150. normal_flag = true;
  151. }
  152. }
  153.  
  154. /*
  155. * 通知切り替え
  156. */
  157. function toggleNotification() {
  158. if(isNotificationEnable) {
  159. $notificationButton.css("background" , "none");
  160. isNotificationEnable = false;
  161. } else {
  162. Notification.requestPermission(function(result) {
  163. if (result == "denied") {
  164. $notificationButton.attr("title",
  165. "通知はFirefoxの設定でブロックされています\n" +
  166. "ロケーションバー(URL)の左のアイコンをクリックして\n" +
  167. "「サイトからの通知の表示」を「許可」に設定してください");
  168. return;
  169. } else if (result == "default") {
  170. console.log("default");
  171. return;
  172. }
  173. $notificationButton.attr("title", "新着レスのポップアップ通知");
  174. $notificationButton.css("background-color" , "#a9d8ff");
  175. isNotificationEnable = true;
  176. });
  177. }
  178. }
  179. }
  180.  
  181. /*
  182. * 実況モード
  183. * 呼出ごとにON/OFFトグル
  184. */
  185. function liveMode() {
  186. var live_button = $("#GM_FAR_relButton_live");
  187. if (!live_flag) {
  188. //実況モード時リロード
  189. timerLiveReload = setInterval(rel_scroll, RELOAD_INTERVAL_LIVE);
  190. //自動スクロール
  191. timerLiveScroll = setInterval(live_scroll, LIVE_SCROLL_INTERVAL);
  192. live_button.css("backgroundColor", "#ffa5f0");
  193. startspin();
  194. console.log(script_name + ": Start live mode @" + url);
  195. live_flag = true;
  196. } else {
  197. clearInterval(timerLiveReload);
  198. clearInterval(timerLiveScroll);
  199. live_button.css("background", "none");
  200. stopspin();
  201. console.log(script_name + ": Stop live mode @" + url);
  202. live_flag = false;
  203. }
  204.  
  205. //リロード+新着スクロール
  206. function rel_scroll() {
  207. $('html, body').animate(
  208. {scrollTop:window.scrollMaxY},"fast"
  209. );
  210. rel();
  211. }
  212.  
  213. function live_scroll() {
  214. window.scrollBy( 0, LIVE_SCROLL_SPEED );
  215. }
  216. function startspin() {
  217. $("#akahuku_throp_menu_opener").css(
  218. "animation", "spin 2s infinite steps(8)"
  219. );
  220. }
  221. function stopspin() {
  222. $("#akahuku_throp_menu_opener").css(
  223. "animation", "none"
  224. );
  225. }
  226. }
  227.  
  228. /*
  229. * 新着レスをリセット
  230. */
  231. function reset_title() {
  232. //ページ末尾でホイールダウンした時
  233. window.addEventListener("DOMMouseScroll",function scroll(event) {
  234. var window_y = Math.ceil(window.scrollY);
  235. var window_ymax = window.scrollMaxY;
  236. if (event.detail > 0 && window_y >= window_ymax) {
  237. reset_titlename();
  238. }
  239. return;
  240. } ,false);
  241. //F5キー押された時
  242. window.addEventListener("keydown",function(e) {
  243. if ( e.keyCode == "116" ) {
  244. reset_titlename();
  245. }
  246. }, false);
  247.  
  248. function reset_titlename() {
  249. res = 0;
  250. var title_char = title_name();
  251. document.title = title_char;
  252. }
  253. }
  254.  
  255. /**
  256. * 赤福の続きを読むボタンをクリック
  257. */
  258. function rel() {
  259. if(isAkahukuNotFound()) {
  260. return;
  261. }
  262. var relbutton = $("#akahuku_reload_button").get(0);
  263. if(relbutton){
  264. var e = document.createEvent("MouseEvents");
  265. e.initEvent("click", false, true);
  266. relbutton.dispatchEvent(e);
  267. }
  268. setTimeout(function(){
  269. soudane();
  270.  
  271. if (!isWindowActive && isNotificationEnable) {
  272. getNewResContent();
  273. }
  274. if(isAkahukuNotFound()) {
  275. //404時
  276. if (live_flag) {
  277. liveMode();
  278. }
  279.  
  280. changeTitleWhenExpired();
  281. clearNormalReload();
  282. if (USE_SAVE_MHT) {
  283. saveMHT();
  284. }
  285. findNextThread();
  286.  
  287. console.log(script_name + ": Page not found, Stop auto reloading @" + url);
  288. }
  289. }, 1000);
  290. }
  291. /**
  292. * MHTで保存
  293. */
  294. function saveMHT() {
  295. var saveMHTButton = $("#akahuku_throp_savemht_button").get(0);
  296. if (saveMHTButton) {
  297. var e = document.createEvent("MouseEvents");
  298. e.initEvent("click", false, true);
  299. saveMHTButton.dispatchEvent(e);
  300. }
  301. }
  302. /*
  303. * そうだねの数に応じてレスを着色
  304. */
  305. function soudane() {
  306. if ( !USE_SOUDANE ) return;
  307.  
  308. clearTimeout(timerSoudane);
  309. timerSoudane = setTimeout(function() {
  310. var coloredNode = $(".rtd[style]");
  311. coloredNode.each(function() {
  312. $(this).removeAttr("style");
  313. });
  314.  
  315. $("td > .sod").each(function(){
  316. var sodnum = $(this).text().match(/\d+/);
  317. if (sodnum){
  318. var col = "rgb(180, 240," + (Math.round(10 * sodnum + 180)) + ")";
  319. $(this).parent().css("background-color", col);
  320. }
  321. });
  322. }, 100);
  323. }
  324. // 続きを読むで挿入される要素を監視
  325. function observeInserted() {
  326. var target = $(".thre").length ?
  327. $(".thre").get(0) :
  328. $("html > body > form[action]:not([enctype])").get(0);
  329. var observer = new MutationObserver(function(mutations) {
  330. soudane();
  331.  
  332. mutations.forEach(function(mutation) {
  333. var $nodes = $(mutation.addedNodes);
  334. replaceNodeInserted($nodes);
  335. });
  336. });
  337. observer.observe(target, { childList: true });
  338. }
  339. // 挿入されたレス
  340. function replaceNodeInserted($nodes) {
  341. var insertedRes = $nodes.find(".rtd");
  342. if( insertedRes.length ) {
  343. changetitle();
  344. }
  345. }
  346.  
  347. /*
  348. * タブタイトルに新着レス数・スレ消滅状態を表示
  349. */
  350. function changetitle() {
  351. if ( !USE_TITLE_NAME ) return;
  352. var title_char = title_name();
  353. if (isAkahukuNotFound()) return;
  354. res++;
  355. document.title = "(" + res + ")" + title_char;
  356. }
  357.  
  358. function changeTitleWhenExpired() {
  359. if (!isAkahukuNotFound()) return;
  360. if(document.title.substr(0,1) !== "#"){
  361. document.title = "#" + document.title;
  362. }
  363. }
  364.  
  365. // 新着レスの内容を取得
  366. function getNewResContent() {
  367. var $newrestable = $("#akahuku_new_reply_header ~ table:not([id])");
  368. if ($newrestable.length) {
  369. var restexts = [];
  370. $newrestable.each(function() {
  371. var texts = [];
  372. $(this).find("blockquote").contents().each(function() {
  373. if ($(this).text() !== "") {
  374. texts.push($(this).text());
  375. }
  376. });
  377. restexts.push(texts.join("\r\n"));
  378. });
  379. var popupText = restexts.join("\r\n===============\r\n");
  380. showNotification(popupText);
  381. }
  382. }
  383. /*
  384. * 赤福のステータスからスレ消滅状態をチェック
  385. */
  386. function isAkahukuNotFound() {
  387. var statustext = $("#akahuku_reload_status").text();
  388. if (statustext.match(/(No Future)|((M|N)ot Found)/)) {
  389. return true;
  390. }
  391. else {
  392. return false;
  393. }
  394. }
  395.  
  396. function title_name() {
  397. var title = document.title;
  398. var title_num = title.match(/^(#|\(\d+\))/);
  399. var title_num_length;
  400. if(!title_num){
  401. title_num_length = 0;
  402. }
  403. else {
  404. title_num_length = title_num[0].length;
  405. }
  406. var act_title_name = title.substr(title_num_length);
  407. return act_title_name;
  408. }
  409.  
  410. function makeFormClearButton() {
  411. if ( USE_CLEAR_BUTTON ) {
  412. var $formClearButton = $("<div>", {
  413. id: "formClearButton",
  414. text: "[クリア]",
  415. css: {
  416. cursor: "pointer",
  417. margin: "0 5px"
  418. },
  419. click: function() {
  420. clearForm();
  421. }
  422. });
  423. var $comeTd = $(".ftdc b:contains('コメント')");
  424. $comeTd.after($formClearButton);
  425. }
  426.  
  427. function clearForm() {
  428. $("#ftxa").val("");
  429. }
  430. }
  431. function addCss() {
  432. GM_addStyle(
  433. "@keyframes spin {" +
  434. " 0% { transform: rotate(0deg); }" +
  435. " 100% { transform: rotate(359deg); }" +
  436. "}"
  437. );
  438. }
  439. // タブのアクティブ状態を取得
  440. function setWindowFocusEvent() {
  441. $(window).bind("focus", function() {
  442. // タブアクティブ時
  443. isWindowActive = true;
  444. }).bind("blur", function() {
  445. // タブ非アクティブ時
  446. isWindowActive = false;
  447. });
  448. }
  449. // 新着レスをポップアップでデスクトップ通知する
  450. function showNotification(body) {
  451. Notification.requestPermission();
  452. var icon = $("#akahuku_thumbnail").attr("src");
  453. var instance = new Notification(
  454. document.title, {
  455. body: body,
  456. icon: icon,
  457. }
  458. );
  459. }
  460.  
  461. /**
  462. * 次スレ候補検索ボタン表示
  463. */
  464. function showFindNextThread() {
  465. $("body").append(
  466. $("<div>", {
  467. id: "GM_FAR_next_thread_area",
  468. class: "GM_FAR"
  469. }).append(
  470. $("<div>").append(
  471. $("<a>", {
  472. id: "GM_FAR_find_next_thread",
  473. class: "GM_FAR_Button",
  474. text: "[次スレ候補検索]",
  475. css: {
  476. cursor: "pointer",
  477. "font-size": "9pt"
  478. },
  479. click: function() {
  480. findNextThread();
  481. }
  482. }),
  483. $("<span>", {
  484. id: "GM_FAR_next_thread_search_result",
  485. css: {
  486. "display": "none",
  487. "font-size": "9pt"
  488. }
  489. }).append(
  490. $("<span>", {
  491. text: "検索結果:",
  492. }),
  493. $("<span>", {
  494. id: "GM_FAR_next_thread_search_result_count",
  495. text: "0"
  496. })
  497. )
  498. ),
  499. $("<ul>", {
  500. "id": "GM_FAR_next_thread_found"
  501. }).append(
  502. $("<span>", {
  503. id: "GM_FAR_next_thread_search_status",
  504. text: "次スレ候補検索中...",
  505. css: {
  506. "display": "none"
  507. }
  508. })
  509. )
  510. )
  511. )
  512. }
  513.  
  514. /**
  515. * 次スレ候補検索
  516. */
  517. function findNextThread() {
  518. var foundList = $("#GM_FAR_next_thread_found");
  519. foundList.empty()
  520. var statusMessage = $("#GM_FAR_next_thread_search_status")
  521. statusMessage.show();
  522. var dir = location.href.substr(0, location.href.lastIndexOf('/') - 3);
  523. var threadTitle = $("#akahuku_thread_text").text().substr(0, 4);
  524. var catalogURL = dir + "futaba.php?mode=cat&sort=1"
  525. var resultCount = 0;
  526. GM_xmlhttpRequest({
  527. method: "GET",
  528. url: catalogURL,
  529. onload: function(res) {
  530. statusMessage.hide();
  531. var catalog = $($.parseHTML(res.response));
  532. var cattable = catalog.filter("#cattable");
  533. var td = cattable.find("td small");
  534. td.each(function() {
  535. var tdText = $(this).text()
  536. if( tdText.substr(0, 4) == threadTitle) {
  537. resultCount++;
  538. var foundThread = $(this).parent().find("a");
  539. var foundThreadResCount = $(this).parent().find("font").text();
  540. var href = foundThread.attr("href");
  541. foundThread.attr("href", dir + href);
  542. foundList.append(
  543. $("<li>").append(
  544. $(this),
  545. $("<span>", {
  546. text: foundThreadResCount + "レス",
  547. css: {
  548. "margin-left": "2em"
  549. }
  550. }),
  551. foundThread
  552. )
  553. );
  554. }
  555. });
  556. $("#GM_FAR_next_thread_search_result_count").text(resultCount);
  557. $("#GM_FAR_next_thread_search_result").show();
  558. }
  559. });
  560. }
  561. })(jQuery);