NicoNico Tsuu

ニコニコライフを快適に。

目前为 2019-06-16 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name NicoNico Tsuu
  3. // @namespace knoa.jp
  4. // @description ニコニコライフを快適に。
  5. // @include https://www.nicovideo.jp/watch/*
  6. // @include https://live2.nicovideo.jp/watch/*
  7. // @version 0.8.0
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function(){
  12. const SCRIPTNAME = 'NicoNicoTsuu';
  13. const DEBUG = false;/*
  14. [update] 0.8.0
  15. タイムシフトや追っかけ視聴中に、その時点の時計の時刻を表示。リアルタイム視聴中に[←]で10秒巻き戻しが機能しなくなっていたので修正。
  16.  
  17. [bug]
  18. モニタサイズフルスクリーンからの復帰で崩れる
  19. シアターとの競合が原因。無理やりfullscreen属性を削除して回る手もあるが。
  20.  
  21. [to do]
  22. URLスイッチ方式だわな
  23. 上下で音量もボタンハイライトを維持してあげつつ
  24. 全画面シアターStylishの廃止と統一
  25. ウィンドウリサイズの不要処理を撤去
  26. cssで5秒くらいで消える案内を出す?
  27. いっそ拡張化のタイミングで。
  28. 動画も投稿?動画と生の2種類?しんどい!
  29. # 共通
  30. 設定/Help
  31. 流れるコメント
  32. 縁取り太さ?
  33. マウスオーバー時の透過率比率
  34. フォント指定
  35. コメント一覧
  36. 文字サイズ
  37. NGスコア表示
  38. 当該ユーザーの発言一覧
  39. 連投規制されそうなとき、投稿ボタンにびっくりマーク
  40. ログインボタン目立たないとか勝手にログアウトするとか
  41. # 生放送
  42. 経過時間に当時の時刻を付与(タイムシフト時)
  43. 番組内容にタイムスケジュールがあればリンク化
  44. タイムシフト視聴するだのなんだのUI
  45. リアルタイム視聴でだんだんリアルタイムから遅れていく現象
  46. video.currentTime を監視してplaybackRateの調整で追いつきたい。
  47. 低遅延なら自動で追いつく?一瞬で追いつくので途中切れるが。
  48. TSの30秒移動でいちいちつっかえるのは改善できないかな?
  49. # 動画
  50. 100%レイアウト([自動]時のみでよい)
  51. 右に[動画説明文/コメント一覧]しかないのか?
  52. 倍速再生にアクセスしやすく
  53. 先頭と次の動画のボタンいらない気が
  54. NiconicoMyTheater継承のコメント・ショートカットキーUX
  55. ヒートマップ
  56. 映像中央の再生マークに白い影
  57.  
  58. [to research]
  59. コメント非表示ボタンからマウス操作でも透明度変えたいな
  60. TSでシークバーにフォーカス時、一度だけ[↑][↓]のpreventDefaultが間に合わない
  61. シークバーのマウスアップでデフォーカスすれば回避できるかも
  62. 一覧コメントマウスオーバー時のたまにスクロール止まらない件
  63. 一覧コメント右クリックでテキスト選択中は配慮してあげたい
  64. TSで1.5倍速とかDLが追いつかない
  65. NGスコアの可視化
  66. 生放送TSヒートマップ
  67. 横型番組表
  68.  
  69. (コメントJSONの例)
  70. chat: {
  71. thread : 1649308673,
  72. vpos : 4256100,
  73. date : 1556109561,
  74. date_usec : 880193,
  75. mail : "184",
  76. content : "/hb ifseetno 686",
  77. premium : 3,
  78. user_id : "SaMpLe",
  79. anonymity : 1,
  80. yourpost : 1,
  81. locale : "ja-jp",
  82. }
  83. */
  84. if(window === top && console.time) console.time(SCRIPTNAME);
  85. const API = {
  86. LIVEMSG: 'wss://msg.live2.nicovideo.jp/',
  87. };
  88. const PREMIUM = {
  89. USER: 1,/*プレミアム会員*/
  90. USERAD: 2,/*広告*/
  91. OPERATOR: 3,/*運営・システム*/
  92. GUEST: 7,/*公式ゲストコメント(?)*/
  93. CHANNEL8: 8,/*チャンネル会員(?)*/
  94. CHANNEL9: 9,/*チャンネル会員(?)*/
  95. CHANNEL24: 24,/*未入会(?)*/
  96. CHANNEL25: 25,/*未入会(?)*/
  97. };
  98. const STROKEALPHA = '1.0';/*流れるコメントの縁取りのアルファ値(公式: 0.4)*/
  99. const EASING = 'cubic-bezier(0,.75,.5,1)';/*主にナビゲーションのアニメーション用*/
  100. const RETRY = 10;
  101. let site = {
  102. video: {
  103. targets: {
  104. // videoTitle: () => $('.HeaderContainer-videoTitle'),
  105. // searchBox: () => $('.HeaderContainer-searchBox'),
  106. videoDescription: () => $('.VideoDescription'),
  107. videoDescriptionExpanderSwitch: () => $('.VideoDescriptionExpander-switch'),
  108. commentRenderer: () => $('#CommentRenderer'),
  109. },
  110. get: {
  111. videoDescriptionHtml: (videoDescription) => $('.VideoDescription-html'),
  112. },
  113. },
  114. live: {
  115. targets: {
  116. leoPlayer: () => $('[class*="_leo-player_"]'),/*出没するplayer-statusの親*/
  117. playerDisplayHeader: () => $('[class*="_player-display-header_"]'),/*運営コメント*/
  118. playerDisplayScreen: () => $('[class*="_player-display-screen_"]'),
  119. interactionLayerContent: () => $('[class*="_interaction-layer_"] > [data-content-visibility]'),/*アンケート*/
  120. commentLayer: () => $('[class*="_comment-layer_"]'),
  121. telopLayer: () => $('[class*="_telop-layer_"]'),
  122. seekInformation: () => $('[class*="_seek-information_"]'),
  123. playButton: () => $('[class*="_play-button_"]'),
  124. muteButton: () => $('[class*="_mute-button_"]'),
  125. timeStatusArea: () => $('[class*="_time-status-area_"]'),
  126. commentVisibilityButton: () => $('[class*="_comment-button_"]'),
  127. fullscreenButton: () => $('[class*="_fullscreen-button_"]'),
  128. reloadButton: () => $('[class*="_reload-button_"]'),
  129. commentTextBox: () => $('[class*="_comment-text-box_"]'),
  130. commentsTable: () => $('[class*="_comment-panel_"] [class*="_table_"]'),
  131. embeddedData: () => $('#embedded-data'),
  132. },
  133. get: {
  134. video: () => $('[class*="_video-layer_"] video[src]'),
  135. liveButton: () => $('[class*="_live-button_"]'),
  136. announcement: (playerDisplayHeader) => playerDisplayHeader.querySelector('[class*="_announcement-renderer_"]'),
  137. seekInformationTime: (seekInformation) => seekInformation.querySelector('span'),
  138. elapsedTime: (timeStatusArea) => timeStatusArea.querySelector('span[class*="_elapsed-time_"] > span:first-child'),
  139. content: (comment) => comment.querySelector('[class*="_comment-text_"]'),
  140. time: (comment) => comment.querySelector('[class*="_comment-time_"]'),
  141. props: (embeddedData) => JSON.parse(embeddedData.dataset.props),
  142. },
  143. addedNodes: {
  144. comment: (node) => (node.dataset.commentType) ? node : null,
  145. },
  146. is: {
  147. realtime: () => site.live.get.liveButton() ? true : false,
  148. timeshift: () => site.live.get.liveButton() ? false : true,
  149. },
  150. },
  151. };
  152. let html, elements = {}, storages = {}, timers = {}, configs = {}
  153. let props, chats = [], users = {}/*id検索用テーブル*/;
  154. let core = {
  155. initialize: function(){
  156. html = document.documentElement;
  157. html.classList.add(SCRIPTNAME);
  158. core.listenWebSockets();
  159. core.ready();
  160. //core.panel.createPanels();
  161. },
  162. ready: function(){
  163. core.getTargets(site.video.targets, RETRY).then(() => {
  164. log("I'm ready for video.");
  165. core.addStyle('styleVideo');
  166. });
  167. core.getTargets(site.live.targets, RETRY).then(() => {
  168. log("I'm ready for live.");
  169. core.addStyle('styleLive');
  170. core.getProps();
  171. core.listenUserActions();
  172. core.listenCanvas();
  173. core.listenEnquete();
  174. core.observePlayerDisplayHeader();
  175. core.appendLocalTime();
  176. core.observeCommentTable();
  177. core.appendIndicator();
  178. core.indicateCommentOpacity(elements.commentLayer.dataset.opacity = Storage.read('opacity') || '1');
  179. });
  180. },
  181. getProps: function(){
  182. props = site.live.get.props(elements.embeddedData);
  183. log(props);
  184. },
  185. appendIndicator: function(e){
  186. elements.indicator = createElement(core.html.indicator());
  187. elements.playerDisplayScreen.appendChild(elements.indicator);
  188. },
  189. indicate: function(indication, duration = 1000){
  190. let indicator = elements.indicator;
  191. if(typeof indication !== 'object') indication = document.createTextNode(indication);
  192. while(indicator.firstChild) indicator.removeChild(indicator.firstChild);
  193. indicator.appendChild(indication);
  194. indicator.classList.add('active');
  195. clearTimeout(timers.indicator);
  196. timers.indicator = setTimeout(function(){
  197. indicator.classList.remove('active');
  198. }, duration);
  199. },
  200. indicateCommentOpacity: function(key){
  201. let button = elements.commentVisibilityButton, indicator = elements.commentOpacityIndicator || createElement(core.html.opacity(key));
  202. if(indicator.isConnected) button.replaceChild(elements.commentOpacityIndicator = createElement(core.html.opacity(key)), indicator);
  203. else button.appendChild(elements.commentOpacityIndicator = indicator);
  204. },
  205. listenUserActions: function(){
  206. /* プレイヤーをアクティブに */
  207. const activatePlayer = function(){
  208. document.activeElement.blur();
  209. elements.playerDisplayScreen.click();
  210. };
  211. /* キーボード */
  212. window.addEventListener('keydown', function(e){
  213. let activeElement = document.activeElement;
  214. /* テキスト入力中は反応しない */
  215. if(['input', 'textarea'].includes(activeElement.localName) && activeElement.type !== 'range'){
  216. if(e.key === 'Escape'){/*Escapeは必ずアンフォーカス*/
  217. activeElement.blur();
  218. e.stopPropagation();
  219. return;
  220. }
  221. if(document.activeElement.value !== '') return;/*テキスト入力中*/
  222. else if([/*テキスト空欄なら以下のキーは有効*/
  223. 'ArrowLeft',
  224. ].includes(e.key) === false) return;
  225. }
  226. switch(true){
  227. case(e.key === 'ArrowLeft' && !e.altKey && e.shiftKey === true && !e.ctrlKey && !e.metaKey):
  228. case(e.key === 'ArrowRight' && !e.altKey && e.shiftKey === true && !e.ctrlKey && !e.metaKey):
  229. if(site.live.is.realtime()) return;
  230. else{
  231. let video = site.live.get.video();
  232. video.currentTime += (e.key === 'ArrowLeft') ? -10 : +10;
  233. e.stopPropagation();
  234. e.preventDefault();/*ブラウザによるテキスト選択を回避*/
  235. }
  236. return;
  237. /* 以下Alt/Shift/Ctrl/Metaキーが押されていたら反応しない */
  238. case(e.altKey || e.shiftKey || e.ctrlKey || e.metaKey):
  239. return;
  240. case(e.key === ' '):
  241. if(site.live.is.realtime()){
  242. elements.commentTextBox.focus();
  243. e.preventDefault();/*コメント欄にフォーカスさせるだけ*/
  244. }else activatePlayer();
  245. return;
  246. case(e.key === 'ArrowLeft'):
  247. if(site.live.is.timeshift()){
  248. /* バッファ範囲内なら公式の重たい処理を回避する */
  249. let video = site.live.get.video();
  250. if(video.currentTime - video.buffered.start(0) >= 30){
  251. video.currentTime -= 30;
  252. e.stopPropagation();
  253. }else activatePlayer();/*プレイヤーにフォーカスさせて公式の30秒巻き戻しを実行させる*/
  254. }else{
  255. const REWIND = 10, CATCHUP = 1.5;
  256. let video = site.live.get.video(), rewinded = false, duration = 1000;
  257. if(!video.paused && !video.rewinded && video.currentTime > REWIND/*少しだけ戻すこともできるが通信が安定しなくなるので*/){
  258. duration = (REWIND / (CATCHUP - 1))*1000;
  259. video.rewinded = rewinded = true;
  260. video.currentTime = video.currentTime - REWIND;
  261. video.playbackRate = CATCHUP;
  262. elements.playerDisplayScreen.dataset.rewinded = 'true';
  263. setTimeout(function(){
  264. video.rewinded = false;
  265. video.playbackRate = 1;
  266. delete elements.playerDisplayScreen.dataset.rewinded;
  267. }, duration);
  268. }
  269. core.indicate(createElement(core.html.rewind(rewinded)), duration);
  270. e.stopPropagation();
  271. }
  272. return;
  273. case(e.key === 'ArrowRight'):
  274. if(site.live.is.timeshift()){
  275. /* バッファ範囲内なら公式の重たい処理を回避する */
  276. let video = site.live.get.video();
  277. if(video.buffered.end(0) - video.currentTime >= 30){
  278. video.currentTime += 30;
  279. e.stopPropagation();
  280. }else activatePlayer();/*プレイヤーにフォーカスさせて公式の30秒巻き戻しを実行させる*/
  281. }
  282. return;
  283. case(e.key === 'ArrowUp'):
  284. case(e.key === 'ArrowDown'):
  285. activatePlayer();/*プレイヤーにフォーカスさせて公式の音量調整を実行させる*/
  286. site.live.get.video().addEventListener('volumechange', function(e){
  287. core.indicate(parseInt(e.target.volume * 100));
  288. }, {once: true});
  289. e.preventDefault();
  290. return;
  291. case(e.key === '1'):
  292. case(e.key === '2'):
  293. case(e.key === '3'):
  294. case(e.key === '4'):
  295. case(e.key === '5'):
  296. case(e.key === '6'):
  297. case(e.key === '7'):
  298. case(e.key === '8'):
  299. case(e.key === '9'):
  300. case(e.key === '0'):
  301. elements.commentLayer.dataset.opacity = e.key;
  302. Storage.save('opacity', e.key);
  303. core.indicate(e.key);
  304. core.indicateCommentOpacity(e.key);
  305. return;
  306. case(e.key === 'm'):
  307. elements.muteButton.click();
  308. site.live.get.video().addEventListener('volumechange', function(e){
  309. if(e.target.muted) core.indicate('mute');
  310. else core.indicate(parseInt(e.target.volume * 100));
  311. }, {once: true});
  312. return;
  313. case(e.key === 'f'):
  314. elements.fullscreenButton.click();
  315. return;
  316. case(e.key === 'r'):
  317. elements.reloadButton.click();
  318. return;
  319. }
  320. }, {capture: true});
  321. /* 再生・一時停止 */
  322. /* 勝手に再生を再開してしまうので保留 */
  323. //elements.playButton.addEventListener('click', function(e){
  324. // /* 公式プレイヤによる再読み込みを回避して軽快に動作させる */
  325. // let video = site.live.get.video();
  326. // if(video.paused) video.play();
  327. // else video.pause();
  328. // e.stopPropagation();
  329. //}, true);
  330. /* 出没するplayer-statusを監視 */
  331. observe(elements.leoPlayer, function(records){
  332. let commentsTable = elements.commentsTable = site.live.targets.commentsTable();
  333. if(commentsTable === null) return;
  334. commentsTable.dataset.selector = 'commentsTable';
  335. core.observeCommentTable();/*commentsTableが復活するのでもう一度監視する*/
  336. }, {childList: true});
  337. /* フルスクリーン状態の変化 */
  338. observe(html, function(records){
  339. if(html.dataset.browserFullscreen) return;/*フルスクリーン化したときは何もしない*/
  340. animate(window.scrollTo.bind(window, 0, 0));/*スクロール位置がずれるのを即補正*/
  341. }, {attributes: true});
  342. /* ウィンドウリサイズ */
  343. window.addEventListener('resize', function(e){
  344. /* ニコ生コメント一覧付き全画面シアターとの連携(なめらかスクロールをこちらで引き受ければ本来不要な処理のはず) */
  345. clearTimeout(window.resizing), window.resizing = setTimeout(function(){
  346. if(document.fullscreenElement) return;/*モニタフルスクリーン時は何もしない*/
  347. elements.fullscreenButton.click();
  348. elements.fullscreenButton.click();
  349. window.resizing = null;
  350. }, 250);/*リサイズ中の連続起動を避ける*/
  351. });
  352. },
  353. listenWebSockets: function(){
  354. /* 公式の通信内容を取得 */
  355. window.WebSocket = new Proxy(WebSocket, {
  356. construct(target, arguments){
  357. const ws = new target(...arguments);
  358. log(ws, arguments);
  359. if(ws.url.startsWith(API.LIVEMSG)) ws.addEventListener('message', function(e){
  360. let json = JSON.parse(e.data);
  361. if(json.chat === undefined) return;
  362. //if(json.chat.premium === 3) log(json.chat);
  363. if(![1,2,3,undefined].includes(json.chat.premium)) log(json.chat);/*ユーザーと広告と運営以外のコメントログ*/
  364. chats.push(json.chat);
  365. /* ユーザー別コメント一覧 */
  366. //if(users[json.chat.user_id] === undefined) users[json.chat.user_id] = [];
  367. //users[json.chat.user_id].push(json.chat);
  368. });
  369. return ws;
  370. }
  371. });
  372. },
  373. listenCanvas: function(){
  374. /* 公式のキャンバスコンテキストメソッドを書き換えて縁取りを見やすく */
  375. let strokeText = CanvasRenderingContext2D.prototype.strokeText;
  376. CanvasRenderingContext2D.prototype.strokeText = function(text, x, y, maxWidth){
  377. //log(text, this.strokeStyle);
  378. this.strokeStyle = this.strokeStyle.replace(/rgba\(([0-9]+),\s?([0-9]+),\s?([0-9]+),\s?([0-9.]+)\)/, `rgba($1,$2,$3,${STROKEALPHA})`);
  379. return strokeText.call(this, text, x, y, maxWidth);
  380. };
  381. },
  382. listenEnquete: function(){
  383. /* アンケートの表示を捉える */
  384. Notification.requestPermission();
  385. let notification, title = props.program.title;
  386. observe(elements.interactionLayerContent, function(records){
  387. if(notification) notification.close();/*古い通知が出たままなら閉じる*/
  388. if(elements.interactionLayerContent.dataset.contentVisibility === 'false') return;/*閉じたときは何もしない*/
  389. notification = new Notification(title, {body: site.live.get.announcement(elements.playerDisplayHeader).textContent});
  390. notification.addEventListener('click', function(e){
  391. notification.close();
  392. });
  393. }, {attributes: true});
  394. },
  395. observePlayerDisplayHeader: function(){
  396. let playerDisplayHeader = elements.playerDisplayHeader, commentLayer = elements.commentLayer;
  397. observe(playerDisplayHeader, function(records){
  398. //log(records);
  399. if(playerDisplayHeader.children.length === 0){
  400. delete playerDisplayHeader.dataset.extraLayout;
  401. delete commentLayer.dataset.extraLayout;
  402. }else{
  403. let announcement = site.live.get.announcement(playerDisplayHeader);
  404. if(announcement) setTimeout(function(){
  405. playerDisplayHeader.dataset.fresh = 'true';
  406. observe(announcement, function(rs){
  407. playerDisplayHeader.dataset.fresh = announcement.dataset.fresh;/*同期させる*/
  408. }, {attributes: true});
  409. }, 250);/*フルスクリーン切り替えなどでラグが発生するので*/
  410. playerDisplayHeader.dataset.extraLayout = 'showOperatorComment';
  411. commentLayer.dataset.extraLayout = 'showOperatorComment';
  412. }
  413. });
  414. },
  415. appendLocalTime: function(){
  416. /* seek */
  417. let seekInformation = elements.seekInformation, seekInformationTime = site.live.get.seekInformationTime(seekInformation);
  418. let localTime = createElement(core.html.localTime()), beginTime = props.program.beginTime;
  419. localTime.textContent = seekInformationTime.textContent;
  420. seekInformationTime.parentNode.insertBefore(localTime, seekInformationTime);
  421. observe(seekInformationTime, function(records){
  422. //log(records);
  423. localTime.textContent = (new Date((beginTime + timeToSeconds(seekInformationTime.textContent))*1000)).toLocaleTimeString();
  424. }, {characterData: true, subtree: true});
  425. /* elapsed */
  426. let timeStatusArea = elements.timeStatusArea, elapsedTime = site.live.get.elapsedTime(timeStatusArea);
  427. let currentLocalTime = createElement(core.html.localTime());
  428. currentLocalTime.textContent = elapsedTime.textContent;
  429. elapsedTime.parentNode.insertBefore(currentLocalTime, elapsedTime);
  430. observe(elapsedTime, function(records){
  431. //log(records);
  432. currentLocalTime.textContent = (new Date((beginTime + timeToSeconds(elapsedTime.textContent))*1000)).toLocaleTimeString();
  433. }, {characterData: true, subtree: true});
  434. },
  435. observeCommentTable: function(){
  436. let commentsTable = elements.commentsTable, isTimeshift = site.live.is.timeshift();
  437. if(commentsTable.observing) return;/*起こりえないけど重複を避ける*/
  438. commentsTable.observing = true;
  439. core.listenMouseOnCommentsTable();
  440. /* 初期コメントに適用しつつ、追加コメントを監視する */
  441. Array.from(commentsTable.children).forEach(c => core.modifyComment(c));
  442. observe(commentsTable, function(records){
  443. //log(records);
  444. let removedComments = [], newComments = [];
  445. for(let i = 0, record; record = records[i]; i++){/*あらかじめ削除要素を収集しておく*/
  446. if(record.removedNodes.length) removedComments.push(record.removedNodes[0]);
  447. }
  448. for(let i = records.length - 1, record; record = records[i]; i--){/*chatとのマッチングを逆順に行うのでこちらも逆順で*/
  449. if(record.addedNodes.length === 0) continue;
  450. if(site.live.addedNodes.comment(record.addedNodes[0]) === null) continue;
  451. let comment = record.addedNodes[0];
  452. core.modifyComment(comment);
  453. /* タイムシフトではユーザーコメント以外は毎回置換されるので(バグ?)、置換要素は新着コメント扱いしない */
  454. if(isTimeshift){
  455. if(['normal', 'trialWatch'].includes(comment.dataset.commentType) === false) continue;
  456. if(removedComments.find(c => comment.textContent === c.textContent)) continue;
  457. /*偶然一致するとnewCommentsから抜けてしまう!!*/
  458. }
  459. newComments.push(comment);
  460. }
  461. if(newComments.length) core.slideUpNewComments(newComments);
  462. });
  463. },
  464. listenMouseOnCommentsTable: function(){
  465. /* マウス操作中はスクロールでフォーカスが不意に外れてしまうのを抑制する */
  466. /* (マウスオーバーで新着スクロールを止めて、マウスアウトで一気に復帰させる) */
  467. let commentsTable = elements.commentsTable, parent = commentsTable.parentNode, scroll = 0;
  468. commentsTable.addEventListener('mouseenter', function(e){
  469. scroll = atMost(Math.round(parseFloat(getComputedStyle(commentsTable.lastElementChild).height)), parent.scrollTop);/*最初に少しスクロールさせると公式も空気を読んで新着コメントが来てもスクロールしなくなる*/
  470. commentsTable.dataset.mouseenter = 'true';
  471. parent.scrollTop -= scroll;
  472. commentsTable.style.transform = `translateY(-${scroll}px)`;
  473. });
  474. commentsTable.addEventListener('mouseleave', function(e){
  475. delete commentsTable.dataset.mouseenter;
  476. let scrollTopMax = parent.scrollHeight - parent.clientHeight, distance = scrollTopMax - parent.scrollTop - scroll;
  477. parent.scrollTop = scrollTopMax;
  478. animate(function(){parent.scrollTop = scrollTopMax + scroll});/*スクロールによって移動した分をさらに調整*/
  479. commentsTable.style.transform = ``;
  480. parent.animate([
  481. {transform: `translateY(${distance}px)`},
  482. {transform: `translateY(0)`},
  483. ], {duration: 125, easing: EASING});
  484. commentsTable.lastElementChild.dataset.new = 'true';/*すでに追加されている1件分*/
  485. });
  486. },
  487. modifyComment: function(commentNode){
  488. const additionalVpos = (props.program.beginTime - props.program.openTime) * 100;
  489. const toVpos = function(time){
  490. let sign = (time[0] === '-') ? -1 : +1;
  491. let p = time.split(':').map(d => parseFloat(d)), s = 100, m = 60*s, h = 60*m;
  492. if(p[2] !== undefined) return additionalVpos + sign * (sign*p[0]*h + p[1]*m + p[2]*s);
  493. if(p[1] !== undefined) return additionalVpos + sign * (sign*p[0]*m + p[1]*s);
  494. if(p[0] !== undefined) return additionalVpos + sign * (sign*p[0]*s);
  495. };
  496. let contentNode = site.live.get.content(commentNode), timeNode = site.live.get.time(commentNode);
  497. let commentType = commentNode.dataset.commentType, content = contentNode.textContent, vpos = toVpos(timeNode.textContent);
  498. /* コメントに追加情報を与える */
  499. for(let i = chats.length - 1, chat; chat = chats[i]; i--){
  500. /* 時刻の一致を検証 */
  501. if(chat.vpos < vpos - 60*100) break;/*60秒以上古いログは追わずにあきらめる(TSではchatsの時系列がかなりばらけている)*/
  502. //if(!(vpos <= chat.vpos && chat.vpos <= vpos + 100)) continue;/*timeNodeの表示時刻とvposは必ずしも一致しない*/
  503. /* 既存の一致を検証 */
  504. if(chat.commentNode && chat.commentNode.isConnected) continue;
  505. /* 内容の一致を検証 */
  506. switch(commentType){
  507. case('normal'):/*通常コメント*/
  508. case('trialWatch'):/*有料番組のお試し視聴*/
  509. if(chat.content !== content) continue;
  510. break;
  511. case('operator'):/*運営コメント*/
  512. let operator = content.split(/\s/);
  513. if(!operator.every(o => chat.content.includes(o))) continue;
  514. break;
  515. case('nicoad'):/*ニコニ広告*/
  516. let nicoad = content.match(/(?:【.+?】)?(.+)さんが([0-9]+)pt/) || content.match(/(?:提供:)?(.+)さん(([0-9]+)pt)/);
  517. if(nicoad === null) log('Unknown nicoad format:', content);
  518. else if(!chat.content.includes(nicoad[1]) || !chat.content.includes(nicoad[2])) continue;/*厳密ではないけど十分*/
  519. break;
  520. case('programExtend'):/*放送枠の延長*/
  521. case('ranking'):/*ランキング入り通知*/
  522. case('cruise'):/*クルーズのお知らせ*/
  523. case('quote'):/*クルーズさんのコメント*/
  524. case('spi'):/*ニコニコ新市場*/
  525. if(content.includes(chat.content)) continue;
  526. if(chat.content.includes(content)) continue;
  527. break;
  528. default:
  529. log('Unknown commentType found:', commentType, chats[i]);
  530. continue;/*複数吐かれる時間内のログから当該chatを見つける*/
  531. }
  532. /* 晴れてペアとなるchatを見つけられたので */
  533. chats[i].commentNode = commentNode;
  534. switch(commentType){
  535. case('normal'):
  536. case('trialWatch'):
  537. linkify(contentNode);/*URLをリンク化*/
  538. commentNode.dataset.score = chat.score || 0;
  539. commentNode.dataset.premium = chat.premium || 0;
  540. commentNode.dataset.user_id = chat.user_id || '';
  541. timeNode.parentNode.insertBefore(createElement(core.html.score(commentNode.dataset.score)), timeNode);/*NGスコア付与*/
  542. //commentNode.addEventListener('click', core.showUserHistory.bind(commentNode), {capture: true});
  543. break;
  544. case('operator'):
  545. let link = chat.content.match(/<a href="([^"]+)"/);
  546. if(link === null) linkify(contentNode);/*URLをリンク化*/
  547. else contentNode.innerHTML = `<a href="${link[1]}">${content}</a>`;
  548. break;
  549. case('nicoad'):
  550. linkify(contentNode);/*URLをリンク化*/
  551. break;
  552. case('programExtend'):
  553. case('ranking'):
  554. case('cruise'):
  555. case('quote'):
  556. case('spi'):
  557. break;
  558. default:
  559. break;
  560. }
  561. break;
  562. }
  563. },
  564. slideUpNewComments: function(newComments){
  565. if(elements.commentsTable.dataset.mouseenter) return;/*マウスオーバー中は処理しない*/
  566. //連続起動しうるけど125ms以内には起こらないだろう
  567. const DURATION = '125ms', EASING = 'ease';
  568. let commentsTable = elements.commentsTable, parent = commentsTable.parentNode;
  569. let scrollTopMax = parent.scrollHeight - parent.clientHeight;
  570. let height = parseFloat(getComputedStyle(newComments[0]).height) * newComments.length;/*高さは共通のはずなので*/
  571. for(let i = 0, comment; comment = newComments[i]; i++){
  572. comment.dataset.new = 'true';
  573. }
  574. if(scrollTopMax === 0) return;/*放送開始時などコメントが少なくてスクロール不要*/
  575. parent.scrollTop = scrollTopMax - 2;/* 本来は1でよいが、ブラウザのズーム倍率に対する保険 */
  576. commentsTable.style.transform = `translateY(${height - 2}px)`;
  577. animate(function(){
  578. commentsTable.style.transition = `transform ${DURATION} ${EASING}`;
  579. commentsTable.style.transform = `translateY(0)`;
  580. commentsTable.addEventListener('transitionend', function(e){
  581. commentsTable.style.transition = 'none';
  582. animate(function(){parent.scrollTop = scrollTopMax + 1});
  583. }, {once: true});
  584. });
  585. },
  586. showUserHistory: function(e){
  587. let commentNode = this, user_id = commentNode.dataset.user_id;
  588. log(this, user_id, users[user_id]);
  589. },
  590. getTargets: function(targets, retry = 0){
  591. const get = function(resolve, reject, retry){
  592. for(let i = 0, keys = Object.keys(targets), key; key = keys[i]; i++){
  593. let selected = targets[key]();
  594. if(selected){
  595. if(selected.length) selected.forEach((s) => s.dataset.selector = key);
  596. else selected.dataset.selector = key;
  597. elements[key] = selected;
  598. }else{
  599. if(--retry < 0) return reject(log(`Not found: ${key}, I give up.`));
  600. log(`Not found: ${key}, retrying... (left ${retry})`);
  601. return setTimeout(get, 1000, resolve, reject, retry);
  602. }
  603. }
  604. resolve();
  605. };
  606. return new Promise(function(resolve, reject){
  607. get(resolve, reject, retry);
  608. });
  609. },
  610. addStyle: function(name = 'style'){
  611. let style = createElement(core.html[name]());
  612. document.head.appendChild(style);
  613. if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]);
  614. elements[name] = style;
  615. },
  616. html: {
  617. indicator: () => `<div id="${SCRIPTNAME}-indicator"></div>`,
  618. rewind: (rewinded) => `
  619. <svg id="rewind" ${rewinded ? 'class ="rewinded"' : ''} viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.4" class="PlayerSeekBackwardButton-icon">
  620. <path d="M18.3 29A38 38 0 1 1 23 76.7a4 4 0 0 0-5.7 0l-2.8 2.8a4 4 0 0 0 0 5.7A50 50 0 1 0 8 22.8l-2-1.2a4 4 0 0 0-6 3.5v18.2a4 4 0 0 0 6 3.5L21.7 38a4 4 0 0 0 .2-7L18.3 29zM42 66a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2V40h-2a2 2 0 0 1-2-2v-4c0-1.1.9-2 2-2h8a2 2 0 0 1 2 2v32zm32 0a2 2 0 0 1-2 2H52a2 2 0 0 1-2-2V34c0-1.1.9-2 2-2h20a2 2 0 0 1 2 2v32zm-8-26h-8v20h8V40z"></path>
  621. </svg>
  622. `,
  623. localTime: () => `<span class="${SCRIPTNAME}-localTime"></span>`,
  624. opacity: (key) => `<span id="comment-opacity-indicator" data-opacity="${key}">${key}</span>`,
  625. score: (score) => `<span class="___comment-score___${SCRIPTNAME}">${score}</span>`,
  626. styleVideo: () => `
  627. <style type="text/css">
  628.  
  629. </style>
  630. `,
  631. styleLive: () => `
  632. <style type="text/css">
  633. /* nicoHighlightColor: ${configs.nicoHighlightColor = 'rgba(0,128,255,1)'} */
  634. /* panel_zIndex: ${configs.panel_zIndex = 101} */
  635. /* 流れるコメント透明度 */
  636. [data-selector="commentLayer"],
  637. [data-selector="telopLayer"]{
  638. transition: opacity 125ms;
  639. }
  640. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="1"]{opacity: ${9/36 + ((9*(9+1))/2)/60}}/*比例25:75三角数(max:45)の重みがベスト*/
  641. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="2"]{opacity: ${8/36 + ((8*(8+1))/2)/60}}
  642. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="3"]{opacity: ${7/36 + ((7*(7+1))/2)/60}}
  643. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="4"]{opacity: ${6/36 + ((6*(6+1))/2)/60}}
  644. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="5"]{opacity: ${5/36 + ((5*(5+1))/2)/60}}
  645. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="6"]{opacity: ${4/36 + ((4*(4+1))/2)/60}}
  646. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="7"]{opacity: ${3/36 + ((3*(3+1))/2)/60}}
  647. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="8"]{opacity: ${2/36 + ((2*(2+1))/2)/60}}
  648. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="9"]{opacity: ${1/36 + ((1*(1+1))/2)/60}}
  649. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="0"]{opacity: ${0/36 + ((0*(0+1))/2)/60}}
  650. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="1"]{opacity: ${(9/36 + ((9*(9+1))/2)/60)/4}}
  651. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="2"]{opacity: ${(8/36 + ((8*(8+1))/2)/60)/4}}
  652. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="3"]{opacity: ${(7/36 + ((7*(7+1))/2)/60)/4}}
  653. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="4"]{opacity: ${(6/36 + ((6*(6+1))/2)/60)/4}}
  654. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="5"]{opacity: ${(5/36 + ((5*(5+1))/2)/60)/4}}
  655. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="6"]{opacity: ${(4/36 + ((4*(4+1))/2)/60)/4}}
  656. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="7"]{opacity: ${(3/36 + ((3*(3+1))/2)/60)/4}}
  657. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="8"]{opacity: ${(2/36 + ((2*(2+1))/2)/60)/4}}
  658. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="9"]{opacity: ${(1/36 + ((1*(1+1))/2)/60)/4}}
  659. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="0"]{opacity: ${(0/36 + ((0*(0+1))/2)/60)/4}}
  660. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="1"] ~ [data-selector="telopLayer"]{opacity: ${9/36 + ((9*(9+1))/2)/60}}
  661. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="2"] ~ [data-selector="telopLayer"]{opacity: ${8/36 + ((8*(8+1))/2)/60}}
  662. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="3"] ~ [data-selector="telopLayer"]{opacity: ${7/36 + ((7*(7+1))/2)/60}}
  663. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="4"] ~ [data-selector="telopLayer"]{opacity: ${6/36 + ((6*(6+1))/2)/60}}
  664. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="5"] ~ [data-selector="telopLayer"]{opacity: ${5/36 + ((5*(5+1))/2)/60}}
  665. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="6"] ~ [data-selector="telopLayer"]{opacity: ${4/36 + ((4*(4+1))/2)/60}}
  666. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="7"] ~ [data-selector="telopLayer"]{opacity: ${3/36 + ((3*(3+1))/2)/60}}
  667. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="8"] ~ [data-selector="telopLayer"]{opacity: ${2/36 + ((2*(2+1))/2)/60}}
  668. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="9"] ~ [data-selector="telopLayer"]{opacity: ${1/36 + ((1*(1+1))/2)/60}}
  669. [data-selector="playerDisplayScreen"] [data-selector="commentLayer"][data-opacity="0"] ~ [data-selector="telopLayer"]{opacity: ${0/36 + ((0*(0+1))/2)/60}}
  670. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="1"] ~ [data-selector="telopLayer"]{opacity: ${(9/36 + ((9*(9+1))/2)/60)/4}}
  671. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="2"] ~ [data-selector="telopLayer"]{opacity: ${(8/36 + ((8*(8+1))/2)/60)/4}}
  672. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="3"] ~ [data-selector="telopLayer"]{opacity: ${(7/36 + ((7*(7+1))/2)/60)/4}}
  673. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="4"] ~ [data-selector="telopLayer"]{opacity: ${(6/36 + ((6*(6+1))/2)/60)/4}}
  674. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="5"] ~ [data-selector="telopLayer"]{opacity: ${(5/36 + ((5*(5+1))/2)/60)/4}}
  675. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="6"] ~ [data-selector="telopLayer"]{opacity: ${(4/36 + ((4*(4+1))/2)/60)/4}}
  676. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="7"] ~ [data-selector="telopLayer"]{opacity: ${(3/36 + ((3*(3+1))/2)/60)/4}}
  677. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="8"] ~ [data-selector="telopLayer"]{opacity: ${(2/36 + ((2*(2+1))/2)/60)/4}}
  678. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="9"] ~ [data-selector="telopLayer"]{opacity: ${(1/36 + ((1*(1+1))/2)/60)/4}}
  679. [data-selector="playerDisplayScreen"]:hover [data-selector="commentLayer"][data-opacity="0"] ~ [data-selector="telopLayer"]{opacity: ${(0/36 + ((0*(0+1))/2)/60)/4}}
  680. /* 10秒戻り中の流れるコメント非表示 */
  681. [data-selector="playerDisplayScreen"] #comment-layer-container{
  682. transition: opacity 1000ms;
  683. opacity: 1;
  684. }
  685. [data-selector="playerDisplayScreen"][data-rewinded="true"] #comment-layer-container{
  686. opacity: 0;
  687. }
  688. /* インジケータ */
  689. #${SCRIPTNAME}-indicator{
  690. position: absolute;
  691. bottom: 0;
  692. right: 0;
  693. padding: 1vh 1vw;
  694. font-size: 25vh;
  695. color: ${configs.nicoHighlightColor};
  696. filter: drop-shadow(0 0 2.5px rgba(0,0,0,.75));
  697. opacity: 0;
  698. z-index: ${configs.panel_zIndex};
  699. pointer-events: none;
  700. transition: opacity 250ms;
  701. }
  702. #${SCRIPTNAME}-indicator.active{
  703. opacity: .75;
  704. }
  705. #${SCRIPTNAME}-indicator #rewind{
  706. fill: rgba(195,195,195,.5);
  707. width: 25vh;
  708. height: 25vh;
  709. }
  710. #${SCRIPTNAME}-indicator #rewind.rewinded{
  711. fill: ${configs.nicoHighlightColor};
  712. }
  713. #${SCRIPTNAME}-indicator.active #rewind{
  714. animation: ${SCRIPTNAME}-blink 2s step-end infinite;
  715. }
  716. @keyframes ${SCRIPTNAME}-blink{
  717. 50%{opacity: 0}
  718. }
  719. /* コメント透明度インジケータ */
  720. [data-selector="commentVisibilityButton"] #comment-opacity-indicator{
  721. position: absolute;
  722. top: 0;
  723. left: 0;
  724. width: 100%;
  725. height: 100%;
  726. line-height: 32px;/*職人的調整*/
  727. font-weight: bold;
  728. color: black;
  729. z-index: 1;
  730. }
  731. [data-selector="commentVisibilityButton"] #comment-opacity-indicator[data-opacity="1"]{opacity: ${9/9}}/*視認性を重視*/
  732. [data-selector="commentVisibilityButton"] #comment-opacity-indicator[data-opacity="2"]{opacity: ${8/9}}
  733. [data-selector="commentVisibilityButton"] #comment-opacity-indicator[data-opacity="3"]{opacity: ${7/9}}
  734. [data-selector="commentVisibilityButton"] #comment-opacity-indicator[data-opacity="4"]{opacity: ${6/9}}
  735. [data-selector="commentVisibilityButton"] #comment-opacity-indicator[data-opacity="5"]{opacity: ${5/9}}
  736. [data-selector="commentVisibilityButton"] #comment-opacity-indicator[data-opacity="6"]{opacity: ${4/9}}
  737. [data-selector="commentVisibilityButton"] #comment-opacity-indicator[data-opacity="7"]{opacity: ${3/9}}
  738. [data-selector="commentVisibilityButton"] #comment-opacity-indicator[data-opacity="8"]{opacity: ${2/9}}
  739. [data-selector="commentVisibilityButton"] #comment-opacity-indicator[data-opacity="9"]{opacity: ${1/9}}
  740. [data-selector="commentVisibilityButton"] #comment-opacity-indicator[data-opacity="0"]{opacity: ${0/9}}
  741. [data-selector="commentVisibilityButton"][data-toggle-state="false"] #comment-opacity-indicator{visibility: hidden}
  742. /* 当日時刻の追加 */
  743. [data-selector="seekInformation"]{
  744. display: flex;
  745. flex-direction: column;
  746. }
  747. [data-selector="seekInformation"] .${SCRIPTNAME}-localTime{
  748. color: #808080;
  749. font-size: 12px;
  750. margin-bottom: .25em;
  751. }
  752. [data-selector="timeStatusArea"] button[data-live-status="live"] ~ div .${SCRIPTNAME}-localTime{
  753. display: none;
  754. }
  755. [data-selector="timeStatusArea"] .${SCRIPTNAME}-localTime{
  756. color: #808080;
  757. font-size: 12px !important;
  758. margin: 0 .5em;
  759. }
  760. [data-selector="timeStatusArea"] .${SCRIPTNAME}-localTime + span{
  761. font-size: 16px;/*少し大きく(公式12px)*/
  762. line-height: 24px;
  763. }
  764. [data-browser-fullscreen] [data-selector="timeStatusArea"] .${SCRIPTNAME}-localTime + span{
  765. font-size: 18px;/*少し大きく(公式12px)*/
  766. }
  767. [data-selector="timeStatusArea"] .${SCRIPTNAME}-localTime + span + span/*時刻の区切り*/{
  768. margin: 0 .5em;/*少し間隔を広げて見やすく*/
  769. }
  770. /* 新着コメント停止状態 */
  771. [class*="_comment-panel_"]:hover::after{
  772. content: " ";
  773. position: absolute;
  774. bottom: 0;
  775. width: 100%;
  776. height: 4px;
  777. animation: ${SCRIPTNAME}-stop 1s linear infinite alternate;
  778. }
  779. @keyframes ${SCRIPTNAME}-stop{
  780. 0%{
  781. background: ${configs.nicoHighlightColor};
  782. }
  783. 100%{
  784. background: transparent;
  785. }
  786. }
  787. /* ユーザー発言一覧 */
  788. dummy [class*="_comment-panel_"] [class*="_table-row_"][data-comment-type="normal"]{
  789. cursor: pointer;
  790. }
  791. /* CSSによる簡易なめらかスクロールの打ち消し */
  792. [class*="_comment-panel_"] [class*="_table-row_"]:first-child{
  793. margin-bottom: 0 !important;
  794. opacity: 1 !important;
  795. pointer-events: auto !important;
  796. }
  797. /* 新着コメントのハイライト */
  798. [class*="_comment-panel_"] [class*="_table-row_"][data-new="true"]{
  799. animation: ${SCRIPTNAME}-new 6s linear 1;
  800. }
  801. @keyframes ${SCRIPTNAME}-new{
  802. 0%{
  803. background: rgba(255,255,255,.250);
  804. }
  805. 100%{
  806. background: rgba(255,255,255,.000);
  807. }
  808. }
  809. /* NGスコア */
  810. [class*="_comment-panel_"] [class*="_table-row_"] [class="___comment-score___${SCRIPTNAME}"]{
  811. visibility: hidden;
  812. margin: 0 .25em;
  813. }
  814. [class*="_comment-panel_"] [class*="_table-row_"]:hover [class="___comment-score___${SCRIPTNAME}"]{
  815. visibility: visible;
  816. color: #808080;
  817. }
  818. </style>
  819. `,
  820. },
  821. };
  822. const setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, requestAnimationFrame = window.requestAnimationFrame;
  823. const getComputedStyle = window.getComputedStyle, fetch = window.fetch;
  824. if(!('isConnected' in Node.prototype)) Object.defineProperty(Node.prototype, 'isConnected', {get: function(){return document.contains(this)}});
  825. if(!('fullscreenElement' in document)) Object.defineProperty(document, 'fullscreenElement', {get: function(){return document.mozFullScreenElement}});
  826. class Storage{
  827. static key(key){
  828. return (SCRIPTNAME) ? (SCRIPTNAME + '-' + key) : key;
  829. }
  830. static save(key, value, expire = null){
  831. key = Storage.key(key);
  832. localStorage[key] = JSON.stringify({
  833. value: value,
  834. saved: Date.now(),
  835. expire: expire,
  836. });
  837. }
  838. static read(key){
  839. key = Storage.key(key);
  840. if(localStorage[key] === undefined) return undefined;
  841. let data = JSON.parse(localStorage[key]);
  842. if(data.value === undefined) return data;
  843. if(data.expire === undefined) return data;
  844. if(data.expire === null) return data.value;
  845. if(data.expire < Date.now()) return localStorage.removeItem(key);
  846. return data.value;
  847. }
  848. static delete(key){
  849. key = Storage.key(key);
  850. delete localStorage.removeItem(key);
  851. }
  852. static saved(key){
  853. key = Storage.key(key);
  854. if(localStorage[key] === undefined) return undefined;
  855. let data = JSON.parse(localStorage[key]);
  856. if(data.saved) return data.saved;
  857. else return undefined;
  858. }
  859. }
  860. const $ = function(s){return document.querySelector(s)};
  861. const $$ = function(s){return document.querySelectorAll(s)};
  862. const animate = function(callback, ...params){requestAnimationFrame(() => requestAnimationFrame(() => callback(...params)))};
  863. const wait = function(ms){return new Promise((resolve) => setTimeout(resolve, ms))};
  864. const createElement = function(html = '<span></span>'){
  865. let outer = document.createElement('div');
  866. outer.innerHTML = html;
  867. return outer.firstElementChild;
  868. };
  869. const observe = function(element, callback, options = {childList: true, attributes: false, characterData: false, subtree: false}){
  870. let observer = new MutationObserver(callback.bind(element));
  871. observer.observe(element, options);
  872. return observer;
  873. };
  874. const linkify = function(node){
  875. split(node);
  876. function split(n){
  877. if(['style', 'script', 'a'].includes(n.localName)) return;
  878. if(n.nodeType === Node.TEXT_NODE){
  879. let pos = n.data.search(linkify.RE);
  880. if(0 <= pos){
  881. let target = n.splitText(pos);/*pos直前までのnとpos以降のtargetに分割*/
  882. let rest = target.splitText(RegExp.lastMatch.length);/*targetと続くrestに分割*/
  883. /* この時点でn(処理済み),target(リンクテキスト),rest(次に処理)の3つに分割されている */
  884. let a = document.createElement('a');
  885. let match = target.data.match(linkify.RE);
  886. switch(true){
  887. case(match[1] !== undefined): a.href = (match[1][0] == 'h') ? match[1] : 'h' + match[1]; break;
  888. case(match[2] !== undefined): a.href = 'http://' + match[2]; break;
  889. case(match[3] !== undefined): a.href = 'mailto:' + match[4] + '@' + match[5]; break;
  890. }
  891. a.appendChild(target);/*textContent*/
  892. rest.parentNode.insertBefore(a, rest);
  893. }
  894. }else{
  895. for(let i = 0; n.childNodes[i]; i++) split(n.childNodes[i]);/*回しながらchildNodesは増えていく*/
  896. }
  897. }
  898. };
  899. linkify.RE = new RegExp([
  900. '(h?ttps?://[-\\w_./~*%$@:;,!?&=+#]+[-\\w_/~*%$@:;&=+#])',/*通常のURL*/
  901. '((?:\\w+\\.)+\\w+/[-\\w_./~*%$@:;,!?&=+#]*)',/*http://の省略形*/
  902. '((\\w[-\\w_.]+)(?:@|@)(\\w[-\\w_.]+\\w))',/*メールアドレス*/
  903. ].join('|'));
  904. const secondsToTime = function(seconds){
  905. let floor = Math.floor, zero = (s) => s.toString().padStart(2, '0');
  906. let h = floor(seconds/3600), m = floor(seconds/60)%60, s = floor(seconds%60);
  907. if(h) return h + '時間' + zero(m) + '分' + zero(s) + '秒';
  908. if(m) return m + '分' + zero(s) + '秒';
  909. if(s) return s + '秒';
  910. };
  911. const timeToSeconds = function(time){
  912. let parts = time.split(':').map(p => parseFloat(p));
  913. switch(parts.length){
  914. case(1): return parts[0];
  915. case(2): return parts[0]*60 + parts[1];
  916. case(3): return parts[0]*60*60 + parts[1]*60 + parts[2];
  917. default: return 0;
  918. }
  919. };
  920. const atLeast = function(min, b){
  921. return Math.max(min, b);
  922. };
  923. const atMost = function(a, max){
  924. return Math.min(a, max);
  925. };
  926. const between = function(min, b, max){
  927. return Math.min(Math.max(min, b), max);
  928. };
  929. const log = function(){
  930. if(!DEBUG) return;
  931. let l = log.last = log.now || new Date(), n = log.now = new Date();
  932. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  933. //console.log(error.stack);
  934. console.log(
  935. SCRIPTNAME + ':',
  936. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  937. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  938. /* :00 */ ':' + line,
  939. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  940. /* caller */ (callers[1] || '') + '()',
  941. ...arguments
  942. );
  943. };
  944. log.formats = [{
  945. name: 'Firefox Scratchpad',
  946. detector: /MARKER@Scratchpad/,
  947. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  948. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  949. }, {
  950. name: 'Firefox Console',
  951. detector: /MARKER@debugger/,
  952. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  953. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  954. }, {
  955. name: 'Firefox Greasemonkey 3',
  956. detector: /\/gm_scripts\//,
  957. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  958. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  959. }, {
  960. name: 'Firefox Greasemonkey 4+',
  961. detector: /MARKER@user-script:/,
  962. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  963. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  964. }, {
  965. name: 'Firefox Tampermonkey',
  966. detector: /MARKER@moz-extension:/,
  967. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
  968. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  969. }, {
  970. name: 'Chrome Console',
  971. detector: /at MARKER \(<anonymous>/,
  972. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  973. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  974. }, {
  975. name: 'Chrome Tampermonkey',
  976. detector: /at MARKER \((userscript\.html|chrome-extension:)/,
  977. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 6,
  978. getCallers: (e) => e.stack.match(/[^ ]+(?= \((userscript\.html|chrome-extension:))/gm),
  979. }, {
  980. name: 'Edge Console',
  981. detector: /at MARKER \(eval/,
  982. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  983. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  984. }, {
  985. name: 'Edge Tampermonkey',
  986. detector: /at MARKER \(Function/,
  987. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  988. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  989. }, {
  990. name: 'Safari',
  991. detector: /^MARKER$/m,
  992. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  993. getCallers: (e) => e.stack.split('\n'),
  994. }, {
  995. name: 'Default',
  996. detector: /./,
  997. getLine: (e) => 0,
  998. getCallers: (e) => [],
  999. }];
  1000. log.format = log.formats.find(function MARKER(f){
  1001. if(!f.detector.test(new Error().stack)) return false;
  1002. //console.log('////', f.name, 'wants', 85, '\n' + new Error().stack);
  1003. return true;
  1004. });
  1005. const time = function(label){
  1006. if(!DEBUG) return;
  1007. const BAR = '|', TOTAL = 100;
  1008. switch(true){
  1009. case(label === undefined):/* time() to output total */
  1010. let total = 0;
  1011. log('Total:');
  1012. Object.keys(time.records).forEach((label) => total += time.records[label].total);
  1013. Object.keys(time.records).forEach((label) => {
  1014. console.log(
  1015. BAR.repeat((time.records[label].total / total) * TOTAL),
  1016. label + ':',
  1017. (time.records[label].total).toFixed(3) + 'ms',
  1018. '(' + time.records[label].count + ')',
  1019. );
  1020. });
  1021. time.records = {};
  1022. break;
  1023. case(!time.records[label]):/* time('label') to create and start the record */
  1024. time.records[label] = {count: 0, from: performance.now(), total: 0};
  1025. break;
  1026. case(time.records[label].from === null):/* time('label') to re-start the lap */
  1027. time.records[label].from = performance.now();
  1028. break;
  1029. case(0 < time.records[label].from):/* time('label') to add lap time to the record */
  1030. time.records[label].total += performance.now() - time.records[label].from;
  1031. time.records[label].from = null;
  1032. time.records[label].count += 1;
  1033. break;
  1034. }
  1035. };
  1036. time.records = {};
  1037. core.initialize();
  1038. if(window === top && console.timeEnd) console.timeEnd(SCRIPTNAME);
  1039. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址