Greasy Fork 支持简体中文。

* Streaming Comment Reader chan

It reads comment text on streaming sites by speech synthesis.

目前為 2019-07-22 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name * Streaming Comment Reader chan
  3. // @name:ja * 配信コメント読み上げちゃん
  4. // @name:zh-CN * 朗读直播评论酱
  5. // @namespace knoa.jp
  6. // @description It reads comment text on streaming sites by speech synthesis.
  7. // @description:ja ライブ配信サイトの新着コメントを音声で読み上げます。
  8. // @description:zh-CN 用声音朗读直播网站的新到来评论。
  9. // @include https://abema.tv/*
  10. // @include https://live.bilibili.com/*
  11. // @include https://www.douyu.com/*
  12. // @include https://live.fc2.com/*
  13. // @include https://www.huajiao.com/l/*
  14. // @include https://www.huya.com/*
  15. // @include http*://www.inke.cn/live*
  16. // @include https://live.line.me/channels/*/broadcast/*
  17. // @include https://live*.nicovideo.jp/watch/*
  18. // @include https://www.openrec.tv/live/*
  19. // @include https://www.pscp.tv/w/*
  20. // @include https://www.showroom-live.com/*
  21. // @include https://twitcasting.tv/*
  22. // @include https://www.twitch.tv/*
  23. // @include https://whowatch.tv/viewer/*
  24. // @include https://www.yizhibo.com/l/*
  25. // @include https://www.youtube.com/live_chat*
  26. // @include https://www.yy.com/*
  27. // @version 1.0.1
  28. // @grant none
  29. // ==/UserScript==
  30.  
  31. (function(){
  32. const SCRIPTID = 'StreamingCommentReader-chan';
  33. const SCRIPTNAME = 'Streaming Comment Reader chan';
  34. const DEBUG = false;/*
  35. [update] 1.0.1
  36. small fixes.
  37.  
  38. [to do]
  39.  
  40. [possible]
  41.  
  42. [research]
  43. ふわっち (デフォであるw)
  44. Flash:
  45. ドキドキ
  46. mirrativ
  47. BIGO
  48. */
  49. if(window === top && console.time) console.time(SCRIPTID);
  50. if(!('speechSynthesis' in window)) return console.log(SCRIPTID, 'speechSynthesis undefined.');
  51. const USERLANGUAGE = 'zh-CN' || window.navigator.language;
  52. const SITELANGUAGE = document.documentElement.lang || USERLANGUAGE;
  53. const _TEXTS = {
  54. en: {
  55. scriptname: () => `${SCRIPTNAME}`,
  56. configs: () => `${SCRIPTNAME} configs`,
  57. test: () => 'Trial',
  58. text: () => 'this is a test ABC',
  59. speech: () => 'Speech',
  60. volume: () => 'volume',
  61. pitch: () => 'pitch',
  62. voice: () => 'voice',
  63. fast: () => 'When comments flow fast',
  64. fastest: () => 'fastest',
  65. buffer: () => 'catch up latest',
  66. bufferNote: () => '* To cut off more than this number of comments for catching up latest ones.',
  67. translators: () => 'Domain specific terms',
  68. translatorsEmpty: () => 'No terms available now.',
  69. dictionary: () => 'Replacement dictionary',
  70. dictionaryNote: () => '[/source(RegExp)/, \'destination\', \'memo(optional)\'],... as Array',
  71. professional: () => '(for professional)',
  72. ng: () => 'NG words',
  73. ngNote: () => 'comma(,) separated list',
  74. reset: () => 'reset',
  75. cancel: () => 'Cancel',
  76. save: () => 'Save',
  77. dictionaryParseError: () => `Replacement dictionary error:\nrequired ${TEXTS.dictionaryNote()},\nor you can reset all preferences.`,
  78. resetConfirmation: () => `All preferences will be reset to defaults. Are you sure?`,
  79. },
  80. ja: {
  81. scriptname: () => `配信コメント読み上げちゃん`,
  82. configs: () => `配信コメント読み上げちゃん 設定`,
  83. test: () => '試し読み',
  84. text: () => 'これはテストです ABC',
  85. speech: () => '読み上げの声',
  86. volume: () => '音量',
  87. pitch: () => '高さ',
  88. voice: () => '種類',
  89. fast: () => 'コメント混雑時',
  90. fastest: () => '速読み',
  91. buffer: () => '追いかけコメント数',
  92. bufferNote: () => '※これ以上古いコメントを切り捨てることで、読み上げがいつまでも追いつかなくなるのを防ぎます。',
  93. translators: () => '専門用語モード',
  94. translatorsEmpty: () => '専門用語が用意されていません。',
  95. dictionary: () => '置換辞書',
  96. dictionaryNote: () => '[/置換元(正規表現)/, \'置換先\', \'メモ(任意)\'],... の配列',
  97. professional: () => '(上級者向け)',
  98. ng: () => 'NGワード',
  99. ngNote: () => 'カンマ(,)区切りのリスト',
  100. reset: () => 'リセット',
  101. cancel: () => 'キャンセル',
  102. save: () => '保存',
  103. dictionaryParseError: () => `置換辞書の形式が正しくありません:\n${TEXTS.dictionaryNote()}にするか、\nまたは全ての設定値をリセットしてください。`,
  104. resetConfirmation: () => 'すべての設定が初期化されます。よろしいですか?',
  105. },
  106. zh: {
  107. scriptname: () => `发布评论朗读`,
  108. configs: () => `发布评论阅读设置`,
  109. test: () => '试读',
  110. text: () => '这是测试ABC',
  111. speech: () => '朗读的声音',
  112. volume: () => '音量',
  113. pitch: () => '高度',
  114. voice: () => '种类',
  115. fast: () => '评论拥挤时',
  116. fastest: () => '速读',
  117. buffer: () => '追随评论数',
  118. bufferNote: () => '※通过舍弃更旧的评论,防止朗读永远跟不上。',
  119. translators: () => '术语模式',
  120. translatorsEmpty: () => '未提供专业术语',
  121. dictionary: () => '替换词典',
  122. dictionaryNote: () => '[/替换自(正则表达式)/, \'替换为\', \'注释(可选)\'],... 的数组。',
  123. professional: () => '(高级)',
  124. ng: () => 'NG字',
  125. ngNote: () => '以逗号(,)分隔的列表',
  126. reset: () => '重置',
  127. cancel: () => '取消',
  128. save: () => '保存',
  129. dictionaryParseError: () => `替换词典的格式不正确: \n${TEXTS.dictionaryNote()},或者\n将所有的设定值复位。`,
  130. resetConfirmation: () => '所有设置都将被初始化。可以吗?',
  131. },
  132. };
  133. const TEXTS = _TEXTS[USERLANGUAGE] || _TEXTS[USERLANGUAGE.substring(0, 2)] || _TEXTS.en;
  134. const _DICTIONARIES = {
  135. /* 置換元, 置換先, 説明(任意) */
  136. en: {
  137. default: [
  138. [/http:\/\/[^\s]+/, 'URL'],
  139. ],
  140. },
  141. ja: {
  142. default: [
  143. [/http:\/\/[^\s]+/, 'URL'],
  144. [/[88]{3,}/, 'パチパチパチ'],
  145. [/[ww]{3,}/, 'ワラワラワラ'],
  146. [/[ww]{2}/, 'ワラワラ'],
  147. [/[ww]$/, 'ワラ', '文末のみ1文字でも'],
  148. [/w/g, 'ワラ', '全角のみ1文字でも'],
  149. [/(.{1})\1{4,}/ug, '$1$1$1$1$1', '1文字の5回以上の繰り返しはカット'],
  150. [/(.{2})\1{3,}/ug, '$1$1$1$1', '2文字の4回以上の繰り返しはカット'],
  151. [/(.{3})\1{2,}/ug, '$1$1', '3文字の3回以上の繰り返しはカット'],
  152. [/(.{4,})\1{1,}/ug, '$1', '4文字以上の繰り返しはカット'],
  153. [/([あ-ん~])[~〜]/g, '$1ー', 'から => 長音'],
  154. [/はよ$/, 'ハヨ'],
  155. [/初見/, 'ショケン'],
  156. [/AbemaTV/, 'アベマティーヴィー'],
  157. [/Abema/, 'アベマ'],
  158. [/ニコ生/, 'ニコナマ'],
  159. ],
  160. niconico: [
  161. [/^(【広告貢献[0-9]位】)?(.+)さんが([0-9]+)ptニコニ広告しました(「(.+)」)?$/, '$1、$2さんが、$3ポイント、ニコニ広告しました。$4。'],
  162. [/^(【ニコニコ新市場】)「(.+)」が貼られました$/, '$1、$2、が貼られました'],
  163. ],
  164. }
  165. };
  166. const DICTIONARIES = _DICTIONARIES[SITELANGUAGE] || _DICTIONARIES[SITELANGUAGE.substring(0, 2)] || _DICTIONARIES.en;
  167. const _TRANSLATORS = {
  168. en: {
  169. },
  170. ja: {
  171. '将棋': (text) => {
  172. const POSITIONS = [
  173. [/[11一]/g, 'イチ'],
  174. [/[22二]/g, 'ニー'],
  175. [/[33三]/g, 'サン'],
  176. [/[44四]/g, 'ヨン'],
  177. [/[55五]/g, 'ゴー'],
  178. [/[66六]/g, 'ロク'],
  179. [/[77七]/g, 'ナナ'],
  180. [/[88八]/g, 'ハチ'],
  181. [/[99九]/g, 'キュー'],
  182. ];
  183. const PIECES = [
  184. [/王/, 'オー'],
  185. [/玉/, 'ギョク'],
  186. [/飛車/, 'ヒシャ'],
  187. [/飛/, 'ヒ'],
  188. [/角/, 'カク'],
  189. [/金/, 'キン'],
  190. [/銀/, 'ギン'],
  191. [/桂馬/, 'ケーマ'],
  192. [/桂/, 'ケー'],
  193. [/香/, 'キョー'],
  194. [/歩/, 'フ'],
  195. [/龍|竜/, 'リュー'],
  196. [/馬/, 'ウマ'],
  197. [/不成/, 'ナラズ'],
  198. [/成(?![ら-ろ])/, 'ナリ'],
  199. [/と/, 'ト'],
  200. [/同/, 'ドウ'],
  201. [/打/, 'ウツ'],
  202. [/右/, 'ミギ'],
  203. [/左/, 'ヒダリ'],
  204. [/上/, 'アガル'],
  205. [/寄/, 'ヨル'],
  206. [/引/, 'ヒク'],
  207. [/直/, 'スグ'],
  208. ];
  209. const MOVES = [{
  210. regexp: /([1-91-9])([1-91-9一二三四五六七八九])([王玉飛車角金銀桂香歩龍竜馬成と同不打右左上寄引直]+).?/g,
  211. replacement: [...POSITIONS, ...PIECES],
  212. }, {
  213. regexp: /([1-91-9])([1-91-9一二三四五六七八九])(?=[あ-ん指取成走入跳突叩攻守]|$)/g,
  214. replacement: [...POSITIONS],
  215. }, {
  216. regexp: /([王玉飛車角金銀桂香歩龍竜馬成と同不打右左上寄引直]{2,}).?/g,
  217. replacement: [...PIECES],
  218. }];
  219. const MODIFICATIONS = [
  220. /* 固有名詞 */
  221. [/大山/, 'オーヤマ'],
  222. [/中原/, 'ナカハラ'],
  223. [/羽生/, 'ハブ'],
  224. [/豊島/, 'トヨシマ'],
  225. [/天彦/, 'アマヒコ'],
  226. [/高見/, 'タカミ'],
  227. [/イトシン(TV|TV)/i, 'イトシンティーヴィー'],
  228. [/朝日杯/, 'アサヒハイ'],
  229. [/NHK杯/, 'エネーチケーハイ'],
  230. [/棋神/, 'キシン'],
  231. [/elmo/, 'エルモ'],
  232. /* 用語 */
  233. [/評価値/, 'ヒョーカチ'],
  234. [/AI/, 'エーアイ'],
  235. [/将棋星人/, 'ショーギセージン'],
  236. [/級位者/, 'キューイシャ'],
  237. [/先手/g, 'センテ'],
  238. [/後手/g, 'ゴテ'],
  239. [/一手/g, 'イッテ'],
  240. [/早指し/, 'ハヤザシ'],
  241. [/早逃げ/, 'ハヤニゲ'],
  242. [/最善手/, 'サイゼンシュ'],
  243. [/筋悪/, 'スジワル'],
  244. [/長手数/, 'チョーテスー'],
  245. [/余詰(め)?/, 'ヨヅメ'],
  246. [/([1-91-9一-九])冠/, '$1カン'],
  247. [/\s対\s/, ' タイ '],
  248. [/vs|vs/, 'ブイエス'],
  249. /* 戦型 */
  250. [/定跡型/, 'ジョーセキケー'],
  251. [/力戦型/, 'リキセンケー'],
  252. [/戦型/, 'センケー'],
  253. [/右玉/, 'ミギギョク'],
  254. [/相居飛車/, 'アイイビシャ'],
  255. [/相(掛|懸)(かり)?/, 'アイガカリ'],
  256. [/横歩取り/, 'ヨコフドリ'],
  257. [/居飛車/, 'イビシャ'],
  258. [/振(り)?飛車/, 'フリビシャ'],
  259. [/中飛車/, 'ナカビシャ'],
  260. [/四間飛車/, 'シケンビシャ'],
  261. [/四間/, 'シケン'],
  262. [/三間飛車/, 'サンケンビシャ'],
  263. [/三間/, 'サンケン'],
  264. [/向(かい)?飛車/, 'ムカイビシャ'],
  265. [/早石田/, 'ハヤイシダ'],
  266. [/角(換|替)わり/, 'カクガワリ'],
  267. [/角交換/, 'カクコーカン'],
  268. [/一手損/, 'イッテゾン'],
  269. /* 囲い */
  270. [/居玉/, 'イギョク'],
  271. [/中住まい/, 'ナカズマイ'],
  272. [/(舟|船)囲い/, 'フナガコイ'],
  273. /* 駒(1文字は特に最後へ) */
  274. [/大駒/, 'オーゴマ'],
  275. [/金駒/, 'カナゴマ'],
  276. [/小駒/, 'コゴマ'],
  277. [/玉頭/, 'ギョクトー'],
  278. [/王手飛車/, 'オーテビシャ'],
  279. [/角頭/, 'カクトー'],
  280. [/桂頭/, 'ケートー'],
  281. [/二歩/, 'ニフ'],
  282. [/と金/, 'とキン'],
  283. [/金底の歩/, 'キンゾコのフ'],
  284. [/玉/g, 'ギョク'],/*(?<!埼)*/
  285. [/角/g, 'カク'],
  286. [/金/g, 'キン'],/*(?<!お)*/
  287. [/桂/g, 'ケー'],
  288. [/香(?![いらりるれろ])/g, 'キョー'],
  289. [/歩(?![いかきくけこ])/g, 'フ'],
  290. ];
  291. /* 棋譜と符号 */
  292. MOVES.forEach(p => {
  293. let tes = text.match(p.regexp);
  294. if(tes !== null) tes.forEach(te => {
  295. let yomi = te;
  296. p.replacement.forEach(p => yomi = yomi.replace(p[0], p[1]));
  297. text = text.replace(te, yomi);
  298. });
  299. });
  300. /* 用語 */
  301. MODIFICATIONS.forEach(m => text = text.replace(m[0], m[1]));
  302. return text;
  303. },
  304. },
  305. };
  306. const TRANSLATORS = _TRANSLATORS[SITELANGUAGE] || _TRANSLATORS[SITELANGUAGE.substring(0, 2)] || _TRANSLATORS.en;
  307. const UNKNOWNPITCHRATIO = .5;/* 不明コメントのピッチ係数 */
  308. const RETRY = 10;
  309. let sites = {
  310. abema: {
  311. id: 'abema',
  312. url: /^https:\/\/abema\.tv/,
  313. reverse: false,
  314. insertBefore: false,
  315. targets: {
  316. board: () => $('.com-a-OnReachTop > div'),
  317. settingAnchor: () => $('.com-tv-TVController__volume'),
  318. },
  319. addedNodes: {
  320. name: (node) => null,
  321. content: (node) => node.querySelector('div > p > span'),
  322. read: [
  323. [1.0, (node) => (node.querySelector('time[datetime]') !== null)],
  324. ],
  325. ignore: [],
  326. }
  327. },
  328. bilibili: {
  329. id: 'bilibili',
  330. url: /^https:\/\/live\.bilibili\.com\/[0-9]+/,
  331. reverse: false,
  332. insertBefore: false,
  333. targets: {
  334. board: () => $('#chat-history-list'),
  335. settingAnchor: () => $('.icon-right-part > *:last-child'),
  336. },
  337. addedNodes: {
  338. name: (node) => node.querySelector('.user-name'),
  339. content: (node) => node.querySelector('.danmaku-content'),
  340. read: [
  341. [1.500, (node) => node.classList.contains('guard-level-3')],
  342. [1.250, (node) => node.classList.contains('guard-level-2')],
  343. [1.125, (node) => node.classList.contains('guard-danmaku')],
  344. [1.000, (node) => node.classList.contains('danmaku-item')],
  345. ],
  346. ignore: [
  347. [0.0, (node) => node.classList.contains('system-msg')],
  348. [0.0, (node) => node.classList.contains('welcome-msg')],
  349. ],
  350. }
  351. },
  352. douyu: {
  353. id: 'douyu',
  354. url: /^https:\/\/www\.douyu\.com\/.+/,
  355. reverse: false,
  356. insertBefore: false,
  357. targets: {
  358. board: () => $('#js-barrage-list'),
  359. settingAnchor: () => $('.ChatToolBar > *:last-child'),
  360. },
  361. addedNodes: {
  362. name: (node) => node.querySelector('.Barrage-nickName'),
  363. content: (node) => node.querySelector('.Barrage-content'),
  364. read: [
  365. [1.25, (node) => (node.querySelector('.Barrage-message') !== null)],
  366. [1.00, (node) => (node.querySelector('.Barrage-notice--normalBarrage') !== null)],
  367. ],
  368. ignore: [
  369. [0.0, (node) => (node.querySelector('.Barrage-userEnter') !== null)],
  370. [0.0, (node) => (node.querySelector('.Barrage-notice') !== null)],
  371. ],
  372. }
  373. },
  374. fc2: {
  375. id: 'fc2',
  376. url: /^https:\/\/live\.fc2\.com\/[0-9]+/,
  377. reverse: false,
  378. insertBefore: true,
  379. targets: {
  380. board: () => $('#js-commentListContainer'),
  381. settingAnchor: () => $('.chat_tab-control > *:first-child'),
  382. },
  383. addedNodes: {
  384. name: (node) => node.querySelector('.js-commentUserName'),
  385. content: (node) => node.querySelector('.js-commentText'),
  386. read: [
  387. [1.0, (node) => node.classList.contains('js-commentLine')],
  388. ],
  389. ignore: [],
  390. }
  391. },
  392. huajiao: {
  393. id: 'huajiao',
  394. url: /^https:\/\/www\.huajiao\.com\/l\/[0-9]+/,
  395. reverse: false,
  396. insertBefore: true,
  397. targets: {
  398. board: () => $('.tt-msg-list'),
  399. settingAnchor: () => $('.tt-type-form'),
  400. },
  401. addedNodes: {
  402. name: (node) => node.querySelector('.tt-msg-nickname'),
  403. content: (node) => node.querySelector('.tt-msg-content-h5'),
  404. read: [
  405. [1.0, (node) => node.classList.contains('.tt-msg-message')],
  406. ],
  407. ignore: [],
  408. }
  409. },
  410. huya: {
  411. id: 'huya',
  412. url: /^https:\/\/www\.huya\.com\/.+/,
  413. reverse: false,
  414. insertBefore: true,
  415. targets: {
  416. board: () => $('#chat-room__list'),
  417. settingAnchor: () => $('.room-chat-tools > *:first-child'),
  418. },
  419. addedNodes: {
  420. name: (node) => node.querySelector('.name'),
  421. content: (node) => node.querySelector('.msg'),
  422. read: [
  423. [1.25, (node) => (node.querySelector('.msg-nobleSpeak') !== null)],
  424. [1.00, (node) => (node.querySelector('.msg') !== null)],
  425. ],
  426. ignore: [
  427. [0.0, (node) => (node.querySelector('.msg-nobleEnter') !== null)],
  428. ],
  429. }
  430. },
  431. inke: {
  432. id: 'inke',
  433. url: /^https?:\/\/www\.inke\.cn\/live.+/,
  434. reverse: false,
  435. insertBefore: true,
  436. targets: {
  437. board: () => $('.comments_list > ul'),
  438. settingAnchor: () => $('.comments_box > input[type="text"]'),
  439. },
  440. addedNodes: {
  441. name: (node) => node.querySelector('li > span'),
  442. content: (node) => node.querySelector('.comments_text') || node.querySelector('.comments_gift'),
  443. read: [
  444. [1.0, (node) => (node.querySelector('img + span + span.comments_text') !== null)],
  445. [1.0, (node) => (node.querySelector('img + span + span.comments_gift') !== null)],
  446. ],
  447. ignore: [],
  448. },
  449. },
  450. line: {
  451. id: 'line',
  452. url: /^https:\/\/live\.line\.me\/channels\/[0-9]+\/broadcast\/[0-9]+/,
  453. reverse: false,
  454. insertBefore: false,
  455. targets: {
  456. board: () => $('[class*="Comment"] > div + div > [class*="Scroll"]'),
  457. settingAnchor: () => $('[class*="Notice"] > [class*="Desc"] > span'),
  458. },
  459. addedNodes: {
  460. name: (node) => node.querySelector('[class*="Head"]'),
  461. content: (node) => node.querySelector('[class*="Heart"]') || node.querySelector('[class*="Desc"]') || node,
  462. read: [
  463. [1.0, (node) => node.className.includes('Label')],
  464. [1.0, (node) => node.className.includes('Chat')],
  465. ],
  466. ignore: [],
  467. }
  468. },
  469. niconico: {
  470. id: 'niconico',
  471. url: /^https:\/\/live[0-9]+\.nicovideo\.jp\/watch\/[a-z]+[0-9]+/,
  472. reverse: false,
  473. insertBefore: false,
  474. targets: {
  475. board: () => $('[class*="_comment-panel_"] [class*="_table_"]'),
  476. settingAnchor: () => $('[class*="_setting-button_"]'),
  477. },
  478. addedNodes: {
  479. name: (node) => node.querySelector('[class*="_comment-author-name_"]'),
  480. content: (node) => node.querySelector('[class*="_comment-text_"]'),
  481. read: [
  482. [1.0, (node) => (node.dataset.commentType === 'nicoad')],
  483. [1.0, (node) => (node.dataset.commentType === 'normal')],
  484. [0.9, (node) => (node.dataset.commentType === 'trialWatch')],
  485. [0.5, (node) => (node.dataset.commentType === 'operator')],
  486. ],
  487. ignore: [],
  488. }
  489. },
  490. openrec: {
  491. id: 'openrec',
  492. url: /^https:\/\/www\.openrec\.tv\/live\/.+/,
  493. reverse: false,
  494. insertBefore: true,
  495. targets: {
  496. board: () => $('.chat-list-content'),
  497. settingAnchor: () => $('[class*="InputArea__ToolbarItem-"]'),
  498. },
  499. addedNodes: {
  500. name: (node) => node.querySelector('[class*="UserName__Name-"]'),
  501. content: (node) => node.querySelector('.chat-content'),
  502. read: [
  503. [1.0, (node) => node.className.includes('ChatList__CellContainer-')],
  504. ],
  505. ignore: [
  506. [0.0, (node) => node.className.includes('system-chat')],
  507. ],
  508. }
  509. },
  510. periscope: {
  511. id: 'periscope',
  512. url: /^https:\/\/www\.pscp\.tv\/w\/.+/,
  513. reverse: false,
  514. insertBefore: false,
  515. targets: {
  516. board: () => $('.Chat > div[style] > div[style]'),
  517. settingAnchor: () => $('.VideoOverlayRedesign-BottomBar-Right > *:last-child'),
  518. },
  519. addedNodes: {
  520. name: (node) => node.querySelector('.CommentMessage-username'),
  521. content: (node) => node.querySelector('.CommentMessage-message'),
  522. read: [
  523. [1.0, (node) => (node.querySelector('.CommentMessage') !== null)],
  524. ],
  525. ignore: [
  526. [0.0, (node) => (node.querySelector('.ParticipantMessage') !== null)],
  527. ],
  528. }
  529. },
  530. showroom: {
  531. id: 'showroom',
  532. url: /^https:\/\/www\.showroom-live\.com\/.+/,
  533. reverse: true,
  534. insertBefore: true,
  535. targets: {
  536. board: () => $('#room-comment-log-list'),
  537. settingAnchor: () => $('#js-room-head-other-select-box').parentNode,
  538. },
  539. addedNodes: {
  540. name: (node) => node.querySelector('.comment-log-name'),
  541. content: (node) => node.querySelector('.comment-log-comment'),
  542. read: [
  543. [1.0, (node) => node.classList.contains('commentlog-row')],
  544. ],
  545. ignore: [],
  546. }
  547. },
  548. twitcasting: {
  549. id: 'twitcasting',
  550. url: /^https:\/\/twitcasting\.tv\/.+/,
  551. reverse: true,
  552. insertBefore: false,
  553. targets: {
  554. board: () => $('.tw-player-comment-list'),
  555. settingAnchor: () => $('#commentnumarea'),
  556. },
  557. addedNodes: {
  558. name: (node) => node.querySelector('.tw-comment-item-name'),
  559. content: (node) => node.querySelector('.tw-comment-item-comment'),
  560. read: [
  561. [1.0, (node) => node.className.includes('tw-comment-item')],
  562. ],
  563. ignore: [],
  564. }
  565. },
  566. twitch: {
  567. id: 'twitch',
  568. url: /^https:\/\/www\.twitch\.tv/,
  569. reverse: false,
  570. insertBefore: true,
  571. targets: {
  572. board: () => $('[role="log"]'),
  573. settingAnchor: () => $('.chat-input__buttons-container [aria-describedby]'),
  574. },
  575. addedNodes: {
  576. name: (node) => node.querySelector('.chat-author__display-name'),
  577. content: (node) => node.querySelector('.text-fragment'),
  578. read: [
  579. [1.0, (node) => node.className.includes('chat-line__message')],
  580. ],
  581. ignore: [],
  582. }
  583. },
  584. whowatch: {
  585. id: 'whowatch',
  586. url: /^https:\/\/whowatch\.tv\/viewer\/[0-9]+/,
  587. reverse: true,
  588. insertBefore: true,
  589. targets: {
  590. board: () => $('.normal-comment-list > div'),
  591. settingAnchor: () => $('.limit'),
  592. },
  593. addedNodes: {
  594. name: (node) => node.querySelector('.user-name'),
  595. content: (node) => node.querySelector('.message'),
  596. read: [
  597. [1.0, (node) => node.classList.contains('comment-box')],
  598. ],
  599. ignore: [],
  600. },
  601. },
  602. yizhibo: {
  603. id: 'yizhibo',
  604. url: /^https:\/\/www\.yizhibo\.com\/l\/.+/,
  605. reverse: false,
  606. insertBefore: true,
  607. targets: {
  608. board: () => $('#J_msglist'),
  609. settingAnchor: () => $('#J_send_danmu'),
  610. },
  611. addedNodes: {
  612. name: (node) => node.querySelector('.nickname'),
  613. content: (node) => node.querySelector('.content'),
  614. read: [
  615. [1.0, (node) => node.classList.contains('msg_1')],
  616. ],
  617. ignore: [
  618. [0.0, (node) => node.classList.contains('msg_2')],
  619. [0.0, (node) => node.classList.contains('msg_3')],
  620. ],
  621. },
  622. },
  623. youtube: {
  624. id: 'youtube',
  625. url: /^https:\/\/www\.youtube\.com\/live_chat/,
  626. reverse: false,
  627. insertBefore: true,
  628. targets: {
  629. board: () => $('#item-offset > #items'),
  630. settingAnchor: () => $('yt-live-chat-header-renderer yt-icon-button'),
  631. },
  632. addedNodes: {
  633. name: (node) => node.querySelector('#author-name'),
  634. content: (node) => node.querySelector('#message'),
  635. read: [
  636. [1.5, (node) => (node.localName === 'yt-live-chat-paid-message-renderer'), 'スパチャ'],
  637. [1.0, (node) => node.classList.contains('yt-live-chat-item-list-renderer')],
  638. ],
  639. ignore: [
  640. [0.0, (node) => (node.localName === 'yt-live-chat-viewer-engagement-message-renderer')],
  641. ],
  642. },
  643. },
  644. yy: {
  645. id: 'yy',
  646. url: /^https:\/\/www\.yy\.com\/[0-9]+\/[0-9]+/,
  647. reverse: false,
  648. insertBefore: false,
  649. targets: {
  650. board: () => $('.chatroom-list'),
  651. settingAnchor: () => $('.chat-room-ft'),
  652. },
  653. addedNodes: {
  654. name: (node) => node.querySelector('.nickname'),
  655. content: (node) => node.querySelector('.nickname + span'),
  656. read: [
  657. [1.0, (node) => node.classList.contains('phizbox')],
  658. ],
  659. ignore: [],
  660. },
  661. },
  662. };
  663. class Configs{
  664. constructor(configs){
  665. Configs.DICTIONARY = [...DICTIONARIES.default, ...(DICTIONARIES[site.id] || [])];
  666. Configs.TRANSLATORS = Object.keys(TRANSLATORS);
  667. Configs.PROPERTIES = {
  668. text: {type: 'string', default: TEXTS.text()},
  669. volume: {type: 'int', default: 25},/* 0-100 => 0.0-1.0 */
  670. pitch: {type: 'int', default: 100},/* 0-200 => 0.0-2.0 */
  671. voice: {type: 'string', default: ''},/* name of voice */
  672. fastest: {type: 'int', default: 150},/* 100-250 => 1.0-2.5 */
  673. buffer: {type: 'int', default: 5},/* 1- 25 */
  674. dictionary: {type: 'array', default: Configs.DICTIONARY},/* replacement pairs */
  675. translators: {type: 'array', default: []},/* name of translators */
  676. ngs: {type: 'array', default: []},/* ng word list */
  677. };
  678. this.data = this.read(configs || {});
  679. }
  680. read(configs){
  681. let newConfigs = {};
  682. Object.keys(Configs.PROPERTIES).forEach(key => {
  683. if(configs[key] === undefined) return newConfigs[key] = Configs.PROPERTIES[key].default;
  684. if(key === 'dictionary') return newConfigs[key] = configs[key].map(entry => {
  685. if(entry[0] instanceof RegExp) return entry;
  686. let parts = entry[0].match(/^\/(.*)\/([a-z]*)$/);
  687. if(parts === null) entry[0] = new RegExp(entry[0]);
  688. else entry[0] = new RegExp(parts[1], parts[2]);
  689. return entry;
  690. });
  691. switch(Configs.PROPERTIES[key].type){
  692. case('bool'): return newConfigs[key] = (configs[key]) ? 1 : 0;
  693. case('int'): return newConfigs[key] = parseInt(configs[key]);
  694. case('float'): return newConfigs[key] = parseFloat(configs[key]);
  695. default: return newConfigs[key] = configs[key];
  696. }
  697. });
  698. return newConfigs;
  699. }
  700. toJSON(){
  701. let json = {};
  702. Object.keys(this.data).forEach(key => {
  703. switch(key){
  704. case('dictionary'):
  705. return json[key] = this.data[key].map(entry => {
  706. if(entry[2] === undefined) return [entry[0].toString(), entry[1]];
  707. else return [entry[0].toString(), entry[1], entry[2]];
  708. });
  709. default:
  710. return json[key] = this.data[key];
  711. }
  712. });
  713. return json;
  714. }
  715. parseDictionaryString(string){
  716. let wrapper = string.trim().match(/^\[([\S\s]+)\]$/);
  717. if(wrapper === null) return false;
  718. let entries = wrapper[1].trim().match(/\[(.+)\]\s*,/g);
  719. if(entries === null) return false;
  720. let lines = wrapper[1].trim().match(/.{3,}(\n|$)/g);
  721. if(lines.length !== entries.length) return false;
  722. let dictionary = [];
  723. for(let i = 0; entries[i]; i++){
  724. let parts = entries[i].trim().match(/\[\s*\/(.*)\/([a-z]*)\s*,\s*'(.*?[^\\])'(?:\s*,\s*'(.*[^\\])')?\s*\]\s*,/);
  725. if(parts === null) return false;
  726. dictionary[i] = [new RegExp(parts[1], parts[2]), parts[3]];
  727. if(parts[4] !== undefined) dictionary[i].push(parts[4]);
  728. }
  729. return dictionary;
  730. }
  731. parseNgsString(string){
  732. if(string.trim() === '') return [];
  733. else return string.trim().split(',');
  734. }
  735. get text(){return this.data.text;}
  736. get volume(){return this.data.volume / 100;}
  737. get pitch(){return this.data.pitch / 100;}
  738. get voice(){return this.data.voice;}
  739. get fastest(){return this.data.fastest / 100;}
  740. get buffer(){return this.data.buffer;}
  741. get dictionary(){return this.data.dictionary;}
  742. get translators(){return this.data.translators;}
  743. get ngs(){return this.data.ngs;}
  744. get dictionaryString(){
  745. let dictionary = this.data.dictionary, string = '';
  746. let quote = (s) => '\'' + s.replace('\'', '\\\'') + '\'';
  747. dictionary.forEach(entry => {
  748. string += ' [';
  749. string += entry[0].toSource();
  750. string += ', ';
  751. string += quote(entry[1]);
  752. if(entry[2] !== undefined){
  753. string += ', ';
  754. string += quote(entry[2]);
  755. }
  756. string += '],\n';
  757. });
  758. return '[\n' + string + ']';
  759. }
  760. get ngsString(){
  761. return this.data.ngs.join(',');
  762. }
  763. }
  764. class Speaker{
  765. constructor(configs){
  766. Speaker.TRANSLATORS = TRANSLATORS;
  767. this.speechSynthesis = speechSynthesis;
  768. this.voices = this.getVoices();
  769. this.configs = configs;
  770. this.queue = [];
  771. this.interval = 250;
  772. }
  773. getVoices(){
  774. let voices = {}, array = this.speechSynthesis.getVoices();
  775. if(array.length) array.forEach(v => voices[v.name] = v);
  776. else this.speechSynthesis.addEventListener('voiceschanged', () => this.voices = this.getVoices());
  777. return voices;
  778. }
  779. request(text, ratio, node){
  780. let utterance = new SpeechSynthesisUtterance(this.modify(text));
  781. utterance.pitch = this.configs.pitch * ratio;
  782. utterance.node = node;
  783. this.queue.push(utterance);
  784. if(this.queue.length === 1){/* 2個以上あるならすでに連続発話が始まっている */
  785. setTimeout(() => this.speak(), 0);/* 一度に複数リクエストを受け取った際に合計数をrateに反映させたい */
  786. }
  787. }
  788. modify(text){
  789. this.configs.dictionary.forEach(d => text = text.replace(d[0], d[1]));
  790. this.configs.translators.forEach(key => text = Speaker.TRANSLATORS[key](text));
  791. return text;
  792. }
  793. speak(){
  794. if(this.queue.length === 0) return;
  795. if(this.configs.ngs.some(ng => this.queue[0].text.includes(ng))) return this.queue.shift(), this.speak();
  796. if(this.queue.length > this.configs.buffer) this.queue = this.queue.slice(-this.configs.buffer);/*古いものは切り捨てる*/
  797. let utterance = this.queue[0];
  798. utterance.volume = this.configs.volume;
  799. utterance.rate = 1 + ((this.queue.length - 1) / ((this.configs.buffer - 1) || 1))*(this.configs.fastest - 1);
  800. utterance.voice = this.voices[this.configs.voice];
  801. utterance.node.dataset.speaking = 'true';
  802. utterance.addEventListener('end', (e) => {
  803. utterance.node.dataset.speaking = 'false';
  804. this.queue.shift();
  805. if(this.queue.length) setTimeout(() => this.speak(), this.interval);
  806. });
  807. log(utterance);
  808. this.speechSynthesis.speak(utterance);
  809. }
  810. cancel(){
  811. this.queue = [];
  812. this.speechSynthesis.cancel();
  813. }
  814. test(text, volume, pitch, voice, rate){
  815. let utterance = new SpeechSynthesisUtterance(this.modify(text));
  816. utterance.volume = volume;
  817. utterance.pitch = pitch;
  818. utterance.voice = this.voices[voice];
  819. utterance.rate = rate;
  820. this.speechSynthesis.speak(utterance);
  821. }
  822. }
  823. let html, elements = {}, timers = {}, site, configs, speaker;
  824. let core = {
  825. initialize: function(){
  826. html = document.documentElement;
  827. html.classList.add(SCRIPTID);
  828. core.site(RETRY);
  829. },
  830. site: function(retry){
  831. site = sites[Object.keys(sites).find(key => sites[key].url.test(location.href))];
  832. if(site === undefined) return log('Doesn\'t match any sites:', location.href);
  833. core.read();
  834. core.observeElements();
  835. core.addStyle();
  836. core.addStyle(site.id);
  837. core.addStyle('stylePanels', window.top.document);
  838. },
  839. observeElements: function(){
  840. /* 開閉する要素に対応。結局インターバルがいちばん負荷が軽い */
  841. setInterval(function(){
  842. new Promise(function(resolve, reject){
  843. if(elements.settingAnchor && elements.settingAnchor.isConnected) return resolve();
  844. elements.settingAnchor = site.targets.settingAnchor();
  845. if(elements.settingAnchor){
  846. core.configs.createButton();
  847. log("Configs button ready.");
  848. return resolve();
  849. }else{
  850. return reject();
  851. }
  852. }).then(() => {
  853. if(elements.board && elements.board.isConnected) return;
  854. elements.board = site.targets.board();
  855. if(elements.board){
  856. core.observeBoard(elements.board);
  857. log("Board ready.");
  858. }
  859. });
  860. }, 1000);
  861. },
  862. read: function(){
  863. panels = new Panels(window.top.document.body.appendChild(createElement(core.html.panels())));
  864. configs = new Configs(Storage.read('configs') || {});
  865. speaker = new Speaker(configs);
  866. },
  867. observeBoard: function(board){
  868. let configButton = elements.configButton;
  869. let isNewer = function(node){
  870. if(site.reverse){
  871. for(let i = 0; board.children[i]; i++){
  872. if(node === board.children[i]) return true;
  873. if(i >= configs.buffer) return false;
  874. }
  875. }else{
  876. for(let i = board.children.length - 1; board.children[i]; i--){
  877. if(node === board.children[i]) return true;
  878. if(board.children.length - i >= configs.buffer) return false;
  879. }
  880. }
  881. };
  882. observe(board, function(records){
  883. //log(records);
  884. if(configButton.classList.contains('active') === false) return;
  885. if(site.reverse) records.reverse();
  886. records.forEach(r => {
  887. r.addedNodes.forEach(n => {
  888. if(isNewer(n) === false) return;/*最後のbuffer個数分でなければ無視してよい*/
  889. let name = site.addedNodes.name(n);
  890. let content = site.addedNodes.content(n);
  891. if(content === null || content.textContent.trim() === '') return;
  892. let read = site.addedNodes.read.find(r => r[1](n));
  893. if(read) return speaker.request(content.textContent, read[0], content);
  894. if(site.addedNodes.ignore.some(i => i[1](n))) return;
  895. speaker.request(content.textContent, UNKNOWNPITCHRATIO, content);
  896. });
  897. });
  898. });
  899. },
  900. configs: {
  901. createButton: function(){
  902. let anchor = elements.settingAnchor, before = site.insertBefore;
  903. let node, configButton = elements.configButton = createElement(core.html.configButton(core.html.configButtonProperties[site.id]));
  904. if(core.html.configButtonWrappers[site.id]){
  905. node = createElement(core.html.configButtonWrappers[site.id]());
  906. node.appendChild(configButton);
  907. }else{
  908. node = configButton;
  909. }
  910. node.className = [node.className, anchor.className].join(' ');
  911. configButton.addEventListener('click', function(e){
  912. configButton.classList.toggle('active');
  913. if(configButton.classList.contains('active') === false) speaker.cancel();
  914. });
  915. configButton.addEventListener('contextmenu', function(e){
  916. e.preventDefault();
  917. panels.toggle('configs');
  918. });
  919. anchor.parentNode.insertBefore(node, (before ? anchor : anchor.nextElementSibling));
  920. core.configs.createPanel();
  921. },
  922. createPanel: function(){
  923. let panel = createElement(core.html.configPanel()), itemElements = panel.querySelectorAll('[name]'), items = {};
  924. Array.from(itemElements).forEach(e => items[e.name] = e);
  925. /* リセット */
  926. panel.querySelector('button.reset').addEventListener('click', function(e){
  927. if(confirm(TEXTS.resetConfirmation())){
  928. panels.hide('configs');
  929. configs = new Configs({});
  930. core.configs.createPanel();
  931. panels.show('configs');
  932. }
  933. });
  934. /* 試し読み */
  935. let normal = panel.querySelector('button.normal'), fast = panel.querySelector('button.fast');
  936. let getValue = (node) => (parseInt(node.value) / 100);
  937. normal.addEventListener('click', function(e){
  938. speaker.test(items.text.value, getValue(items.volume), getValue(items.pitch), items.voice.value, 1);
  939. });
  940. fast.addEventListener('click', function(e){
  941. speaker.test(items.text.value, getValue(items.volume), getValue(items.pitch), items.voice.value, getValue(items.fastest));
  942. });
  943. /* 声 */
  944. let currentVoice = speaker.voices[configs.voice || Object.keys(speaker.voices).find(key => speaker.voices[key].default)], languages = [], voices = [];
  945. Object.keys(speaker.voices).forEach(key => {
  946. if(languages.includes(speaker.voices[key].lang) === false) languages.push(speaker.voices[key].lang);
  947. voices.push(key);
  948. });
  949. languages.sort().forEach(l => {
  950. let option = createElement(core.html.option(l));
  951. if(l === currentVoice.lang) option.selected = true;
  952. items.language.appendChild(option);
  953. });
  954. voices.sort().forEach(v => {
  955. let option = createElement(core.html.option(v));
  956. if(speaker.voices[v].lang !== currentVoice.lang) option.classList.add('hidden');
  957. if(v === currentVoice.name) option.selected = true;
  958. items.voice.appendChild(option);
  959. });
  960. items.language.addEventListener('change', function(e){
  961. Array.from(items.voice.children).reverse().forEach(o => {
  962. if(speaker.voices[o.value].lang === e.target.value){
  963. o.classList.remove('hidden');
  964. o.selected = true;
  965. }
  966. else o.classList.add('hidden');
  967. });
  968. });
  969. /* 専門用語モード */
  970. let translatorTemplate = createElement(core.html.checkbox('translators', 'template')), translatorsEmpty = panel.querySelector('.translatorsEmpty');
  971. items.translators = [];
  972. Object.keys(TRANSLATORS).forEach(key => {
  973. let label = translatorTemplate.cloneNode(true), input = label.querySelector('input[type="checkbox"]');
  974. label.dataset.translator = key;
  975. input.value = key;
  976. input.checked = configs.translators.some(t => (t === key));
  977. translatorsEmpty.parentNode.insertBefore(label, translatorsEmpty.parentNode.firstElementChild);
  978. items.translators.push(input);
  979. });
  980. /* キャンセル */
  981. panel.querySelector('button.cancel').addEventListener('click', function(e){
  982. panels.hide('configs');
  983. core.configs.createPanel();/*クリアしておく*/
  984. });
  985. /* 保存 */
  986. panel.querySelector('button.save').addEventListener('click', function(e){
  987. let dictionary = configs.parseDictionaryString(items.dictionary.value);
  988. if(dictionary === false) return alert(TEXTS.dictionaryParseError());
  989. configs = new Configs({
  990. text: items.text.value,
  991. volume: items.volume.value,
  992. pitch: items.pitch.value,
  993. voice: items.voice.value,
  994. fastest: items.fastest.value,
  995. buffer: items.buffer.value,
  996. translators: Array.from(items.translators).filter(t => t.checked).map(t => t.value),
  997. dictionary: dictionary,
  998. ngs: configs.parseNgsString(items.ngs.value),
  999. });
  1000. speaker.cancel();
  1001. speaker = new Speaker(configs);
  1002. Storage.save('configs', configs.toJSON());
  1003. panels.hide('configs');
  1004. core.configs.createPanel();/*クリアしておく*/
  1005. });
  1006. panels.add('configs', panel);
  1007. },
  1008. },
  1009. addStyle: function(name = 'style', d = document){
  1010. if(core.html[name] === undefined) return;
  1011. let style = createElement(core.html[name]());
  1012. d.head.appendChild(style);
  1013. if(elements[name] && elements[name].isConnected) d.head.removeChild(elements[name]);
  1014. elements[name] = style;
  1015. },
  1016. html: {
  1017. configButtonWrappers: {
  1018. showroom: () => `<li></li>`,
  1019. },
  1020. configButtonProperties: {
  1021. niconico: 'aria-label',
  1022. },
  1023. configButton: (property = 'title') => `
  1024. <button id="${SCRIPTID}-config-button" ${property}="${TEXTS.scriptname()}">
  1025. <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 330 330" xml:space="preserve">
  1026. <g id="XMLID_797_">
  1027. <path id="XMLID_798_" d="M164.998,210c35.887,0,65.085-29.195,65.085-65.12l-0.204-80c0-35.776-29.105-64.88-64.881-64.88
  1028. c-35.773,0-64.877,29.104-64.877,64.843l-0.203,80.076C99.918,180.805,129.112,210,164.998,210z"/>
  1029. <path id="XMLID_799_" d="M280.084,154.96c0-8.285-6.717-15-15-15c-8.284,0-15,6.715-15,15c0,46.732-37.878,84.773-84.546,85.067
  1030. c-0.181-0.007-0.357-0.027-0.54-0.027c-0.184,0-0.359,0.02-0.541,0.027c-46.664-0.293-84.541-38.335-84.541-85.067
  1031. c0-8.285-6.717-15-15-15c-8.284,0-15,6.715-15,15c0,58.372,43.688,106.731,100.082,114.104V300H117c-8.284,0-15,6.716-15,15
  1032. s6.716,15,15,15h96.002c8.283,0,15-6.716,15-15s-6.717-15-15-15h-33.004v-30.936C236.395,261.69,280.084,213.332,280.084,154.96z"/>
  1033. </g>
  1034. </svg>
  1035. </button>
  1036. `,
  1037. configPanel: () => `
  1038. <div class="panel" id="${SCRIPTID}-config-panel" data-order="1">
  1039. <h1>
  1040. <button class="reset" title="${TEXTS.reset()}">
  1041. <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
  1042. <metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
  1043. <g><path d="M500,10v392l196-196L500,10z"/><path d="M500,990C271.3,990,85.1,803.8,85.1,575.1c0-228.7,186.2-414.9,414.9-414.9v91.5c-176.4,0-323.4,143.7-323.4,323.4c0,179.7,143.7,323.4,323.4,323.4c179.7,0,323.4-143.7,323.4-323.4h91.5C914.9,803.8,728.7,990,500,990z"/></g>
  1044. </svg>
  1045. </button>
  1046. ${TEXTS.configs()}
  1047. </h1>
  1048. <fieldset>
  1049. <legend>${TEXTS.test()}</legend>
  1050. <p class="property"><input type="text" name="text" value="${configs.data.text}"><button class="normal">▶</button><button class="fast">▶▶</button></p>
  1051. </fieldset>
  1052. <fieldset>
  1053. <legend>${TEXTS.speech()}</legend>
  1054. <p class="property"><label for="config-volume">${TEXTS.volume()}<small>(0-100%)</small>:</label><input type="number" name="volume" id="config-volume" value="${configs.data.volume}" min="0" max="100" step="5"></p>
  1055. <p class="property"><label for="config-pitch" >${TEXTS.pitch()}<small>(0-200%)</small>: </label><input type="number" name="pitch" id="config-pitch" value="${configs.data.pitch}" min="0" max="200" step="10"></p>
  1056. <p class="property"><label for="config-voice" >${TEXTS.voice()}:</label><select name="language"></select><select name="voice" id="config-voice"></select></p>
  1057. </fieldset>
  1058. <fieldset>
  1059. <legend>${TEXTS.fast()}</legend>
  1060. <p class="property"><label for="config-fastest">${TEXTS.fastest()}<small>(100-250%)</small>: </label><input type="number" name="fastest" id="config-fastest" value="${configs.data.fastest}" min="100" max="250" step="10"></p>
  1061. <p class="property"><label for="config-buffer" title="${TEXTS.bufferNote()}">${TEXTS.buffer()}<sup>※</sup>:</label><input type="number" name="buffer" id="config-buffer" value="${configs.data.buffer}" min="1" max="25" step="1"></p>
  1062. </fieldset>
  1063. <fieldset>
  1064. <legend>${TEXTS.translators()}</legend>
  1065. <p class="property"><span class="translatorsEmpty">${TEXTS.translatorsEmpty()}</span></p>
  1066. </fieldset>
  1067. <fieldset>
  1068. <legend>${TEXTS.dictionary()}<small>${TEXTS.professional()}</small></legend>
  1069. <p class="property"><textarea name="dictionary" id="config-dictionary">${configs.dictionaryString}</textarea></p>
  1070. <p class="note">${TEXTS.dictionaryNote()}</p>
  1071. </fieldset>
  1072. <fieldset>
  1073. <legend>${TEXTS.ng()}</legend>
  1074. <p class="property"><textarea name="ngs" id="config-ngs">${configs.ngsString}</textarea></p>
  1075. <p class="note">${TEXTS.ngNote()}</p>
  1076. </fieldset>
  1077. <p class="buttons"><button class="cancel">${TEXTS.cancel()}</button><button class="save primary">${TEXTS.save()}</button></p>
  1078. </div>
  1079. `,
  1080. option: (value) => `<option value="${value}">${value}</option>`,
  1081. checkbox: (key, value) => `<label data-${key}="${value}"><input type="checkbox" name="${key}"></label>`,
  1082. panels: () => `<div class="panels" id="${SCRIPTID}-panels" data-panels="0"></div>`,
  1083. stylePanels: () => `
  1084. <style type="text/css">
  1085. /* 設定パネル(共通) */
  1086. #${SCRIPTID}-panels *{
  1087. font-size: 14px;
  1088. line-height: 20px;
  1089. padding: 0;
  1090. margin: 0;
  1091. }
  1092. #${SCRIPTID}-panels{
  1093. font-family: Arial, sans-serif;
  1094. position: fixed;
  1095. width: 100%;
  1096. height: 100%;
  1097. top: 0;
  1098. left: 0;
  1099. overflow: hidden;
  1100. pointer-events: none;
  1101. z-index: 99999;
  1102. }
  1103. #${SCRIPTID}-panels div.panel{
  1104. position: absolute;
  1105. max-height: 100%;/*小さなウィンドウに対応*/
  1106. overflow: auto;
  1107. left: 50%;
  1108. bottom: 50%;
  1109. transform: translate(-50%, 50%);
  1110. background: rgba(0,0,0,.75);
  1111. transition: 250ms;
  1112. padding: 5px 0;
  1113. pointer-events: auto;
  1114. }
  1115. #${SCRIPTID}-panels div.panel.hidden{
  1116. bottom: 0;
  1117. transform: translate(-50%, 100%) !important;
  1118. display: block !important;
  1119. }
  1120. #${SCRIPTID}-panels div.panel.hidden *{
  1121. animation: none !important;/*CPU負荷軽減*/
  1122. }
  1123. #${SCRIPTID}-panels h1,
  1124. #${SCRIPTID}-panels h2,
  1125. #${SCRIPTID}-panels h3,
  1126. #${SCRIPTID}-panels h4,
  1127. #${SCRIPTID}-panels legend,
  1128. #${SCRIPTID}-panels ul,
  1129. #${SCRIPTID}-panels ol,
  1130. #${SCRIPTID}-panels dl,
  1131. #${SCRIPTID}-panels p{
  1132. color: white;
  1133. padding: 2px 10px;
  1134. vertical-align: baseline;
  1135. }
  1136. #${SCRIPTID}-panels legend ~ p,
  1137. #${SCRIPTID}-panels legend ~ ul,
  1138. #${SCRIPTID}-panels legend ~ ol,
  1139. #${SCRIPTID}-panels legend ~ dl{
  1140. padding-left: calc(10px + 14px);
  1141. }
  1142. #${SCRIPTID}-panels header{
  1143. display: flex;
  1144. }
  1145. #${SCRIPTID}-panels header h1{
  1146. flex: 1;
  1147. }
  1148. #${SCRIPTID}-panels fieldset{
  1149. border: none;
  1150. }
  1151. #${SCRIPTID}-panels fieldset > p{
  1152. display: flex;
  1153. align-items: center;
  1154. }
  1155. #${SCRIPTID}-panels fieldset > p.property:hover{
  1156. background: rgba(255,255,255,.125);
  1157. }
  1158. #${SCRIPTID}-panels fieldset > p.property > label{
  1159. flex: 1;
  1160. }
  1161. #${SCRIPTID}-panels fieldset > p.property > input,
  1162. #${SCRIPTID}-panels fieldset > p.property > textarea,
  1163. #${SCRIPTID}-panels fieldset > p.property > select{
  1164. color: black;
  1165. background: white;
  1166. padding: 1px 2px;
  1167. }
  1168. #${SCRIPTID}-panels fieldset > p.property > input,
  1169. #${SCRIPTID}-panels fieldset > p.property > button{
  1170. box-sizing: border-box;
  1171. height: 20px;
  1172. }
  1173. #${SCRIPTID}-panels fieldset small{
  1174. font-size: 12px;
  1175. margin: 0 0 0 .25em;
  1176. }
  1177. #${SCRIPTID}-panels fieldset sup,
  1178. #${SCRIPTID}-panels fieldset p.note{
  1179. font-size: 10px;
  1180. line-height: 14px;
  1181. opacity: .75;
  1182. }
  1183. #${SCRIPTID}-panels div.panel > p.buttons{
  1184. text-align: right;
  1185. padding: 5px 10px;
  1186. }
  1187. #${SCRIPTID}-panels div.panel > p.buttons button{
  1188. line-height: 1.4;
  1189. width: 120px;
  1190. padding: 5px 10px;
  1191. margin-left: 10px;
  1192. border-radius: 5px;
  1193. color: rgba(255,255,255,1);
  1194. background: rgba(64,64,64,1);
  1195. border: 1px solid rgba(255,255,255,1);
  1196. cursor: pointer;
  1197. }
  1198. #${SCRIPTID}-panels div.panel > p.buttons button.primary{
  1199. font-weight: bold;
  1200. background: rgba(0,0,0,1);
  1201. }
  1202. #${SCRIPTID}-panels div.panel > p.buttons button:hover,
  1203. #${SCRIPTID}-panels div.panel > p.buttons button:focus{
  1204. background: rgba(128,128,128,1);
  1205. }
  1206. #${SCRIPTID}-panels .template{
  1207. display: none !important;
  1208. }
  1209. /* 設定パネル */
  1210. #${SCRIPTID}-config-panel{
  1211. width: 320px;
  1212. }
  1213. #${SCRIPTID}-config-panel button.reset{
  1214. float: right;
  1215. font-size: 20px;
  1216. color: white;
  1217. background: black;
  1218. border: 1px solid #666;
  1219. border-radius: 5px;
  1220. width: 1em;
  1221. height: 1em;
  1222. cursor: pointer;
  1223. }
  1224. #${SCRIPTID}-config-panel button.reset:hover{
  1225. background: #333;
  1226. }
  1227. #${SCRIPTID}-config-panel button.reset svg{
  1228. fill: white;
  1229. width: 100%;
  1230. height: 100%;
  1231. padding: 2px;
  1232. box-sizing: border-box;
  1233. }
  1234. #${SCRIPTID}-config-panel input[type="number"]{
  1235. width: 4em;
  1236. }
  1237. #${SCRIPTID}-config-panel input[name="text"]{
  1238. border: 1px solid #999;
  1239. border-radius: 5px 0 0 5px;
  1240. height: 24px;
  1241. flex: 1;
  1242. }
  1243. #${SCRIPTID}-config-panel input[name="text"] ~ button{
  1244. font-size: 10px;
  1245. white-space: nowrap;
  1246. color: white;
  1247. background: #000;
  1248. border: 1px solid #666;
  1249. border-left: none;
  1250. width: 4em;
  1251. height: 24px;
  1252. padding: 0 1em;
  1253. cursor: pointer;
  1254. }
  1255. #${SCRIPTID}-config-panel input[name="text"] ~ button.fast{
  1256. border-radius: 0 5px 5px 0;
  1257. }
  1258. #${SCRIPTID}-config-panel input[name="text"] ~ button:hover{
  1259. background: #333;
  1260. }
  1261. #${SCRIPTID}-config-panel option.hidden{
  1262. display: none;
  1263. }
  1264. #${SCRIPTID}-config-panel label[data-translator]{
  1265. background: #333;
  1266. border: 1px solid #666;
  1267. border-radius: 5px;
  1268. padding: 2px 5px;
  1269. flex: 0 !important;
  1270. white-space: nowrap;
  1271. cursor: pointer;
  1272. }
  1273. #${SCRIPTID}-config-panel label[data-translator]:hover{
  1274. background: #444;
  1275. }
  1276. #${SCRIPTID}-config-panel label[data-translator]::after{
  1277. content: attr(data-translator);
  1278. margin-left: 5px;
  1279. }
  1280. #${SCRIPTID}-config-panel label[data-translator] input{
  1281. cursor: pointer;
  1282. }
  1283. #${SCRIPTID}-config-panel .translatorsEmpty{
  1284. opacity: .75;
  1285. }
  1286. #${SCRIPTID}-config-panel label + .translatorsEmpty{
  1287. display: none;
  1288. }
  1289. #${SCRIPTID}-config-panel textarea{
  1290. width: 100%;
  1291. height: 40px;
  1292. font-family: monospace;
  1293. }
  1294. </style>
  1295. `,
  1296. style: () => `
  1297. <style type="text/css">
  1298. /* 設定ボタン */
  1299. button#${SCRIPTID}-config-button{
  1300. background: transparent;
  1301. border: none;
  1302. padding: 0;
  1303. margin: 0;
  1304. cursor: pointer;
  1305. transition: 125ms;
  1306. }
  1307. button#${SCRIPTID}-config-button svg{
  1308. fill: #666;
  1309. }
  1310. button#${SCRIPTID}-config-button:hover svg{
  1311. fill: #999;
  1312. }
  1313. button#${SCRIPTID}-config-button.active svg{
  1314. fill: #f00;
  1315. }
  1316. button#${SCRIPTID}-config-button.active:hover svg{
  1317. fill: #f33;
  1318. }
  1319. /* 読み上げコメント */
  1320. [data-speaking="true"]{
  1321. position: relative !important;
  1322. overflow: visible !important;
  1323. }
  1324. [data-speaking="true"]::after/*公式がbeforeを使っていても干渉しない*/{
  1325. font-family: Arial, sans-serif;
  1326. content: "●";
  1327. color: red;
  1328. font-size: 100%;
  1329. position: absolute;
  1330. left: -.125em;
  1331. top: 50%;
  1332. transform: translate(-100%, -50%);
  1333. animation: ${SCRIPTID}-blink 1000ms ease 0ms infinite alternate forwards;
  1334. }
  1335. @keyframes ${SCRIPTID}-blink{
  1336. 50%{opacity: .5}
  1337. }
  1338. </style>
  1339. `,
  1340. abema: () => `
  1341. <style type="text/css">
  1342. button#${SCRIPTID}-config-button{
  1343. width: 40px;
  1344. height: 40px;
  1345. }
  1346. button#${SCRIPTID}-config-button svg{
  1347. width: 24px;
  1348. height: 24px;
  1349. transform: translateY(7px);
  1350. fill: #ccc;
  1351. }
  1352. button#${SCRIPTID}-config-button:hover svg{
  1353. fill: #fff;
  1354. }
  1355. button#${SCRIPTID}-config-button.active svg{
  1356. fill: #f00;
  1357. }
  1358. </style>
  1359. `,
  1360. bilibili: () => `
  1361. <style type="text/css">
  1362. button#${SCRIPTID}-config-button{
  1363. width: 20px;
  1364. height: 20px;
  1365. transform: translateY(1px);
  1366. vertical-align: middle;
  1367. }
  1368. button#${SCRIPTID}-config-button::before{
  1369. display: none;
  1370. }
  1371. [data-speaking="true"]{
  1372. position: static !important;
  1373. }
  1374. [data-speaking="true"]::after{
  1375. left: .25em;
  1376. }
  1377. </style>
  1378. `,
  1379. douyu: () => `
  1380. <style type="text/css">
  1381. button#${SCRIPTID}-config-button{
  1382. width: 20px;
  1383. height: 20px;
  1384. transform: translate(-5px, calc(-100% - 5px));
  1385. vertical-align: middle;
  1386. }
  1387. [data-speaking="true"]{
  1388. position: static !important;
  1389. }
  1390. [data-speaking="true"]::after{
  1391. left: .625em;
  1392. }
  1393. </style>
  1394. `,
  1395. fc2: () => `
  1396. <style type="text/css">
  1397. button#${SCRIPTID}-config-button{
  1398. width: 42px;
  1399. height: 38px;
  1400. }
  1401. button#${SCRIPTID}-config-button svg{
  1402. width: 24px;
  1403. height: 24px;
  1404. transform: translateY(1px);
  1405. }
  1406. [data-speaking="true"]::after{
  1407. left: .5em;
  1408. }
  1409. .js-commentLine{
  1410. position: relative;
  1411. }
  1412. .js-commentText{
  1413. position: static !important;
  1414. }
  1415. </style>
  1416. `,
  1417. huajiao: () => `
  1418. <style type="text/css">
  1419. button#${SCRIPTID}-config-button{
  1420. width: 30px;
  1421. height: 30px;
  1422. position: absolute;
  1423. left: 100%;
  1424. top: 0;
  1425. transform: translate(-100%,-100%);
  1426. }
  1427. button#${SCRIPTID}-config-button svg{
  1428. width: 24px;
  1429. height: 24px;
  1430. transform: translateY(1px);
  1431. }
  1432. .tt-msg-message{
  1433. position: relative;
  1434. }
  1435. [data-speaking="true"]{
  1436. position: static !important;
  1437. }
  1438. [data-speaking="true"]::after{
  1439. left: 1.25em;
  1440. }
  1441. </style>
  1442. `,
  1443. huya: () => `
  1444. <style type="text/css">
  1445. button#${SCRIPTID}-config-button{
  1446. width: 22px;
  1447. height: 22px;
  1448. transform: translateY(1px);
  1449. vertical-align: middle;
  1450. float: left;
  1451. margin-right: 10px;
  1452. }
  1453. button#${SCRIPTID}-config-button::before{
  1454. display: none;
  1455. }
  1456. .J_msg{
  1457. position: relative;
  1458. }
  1459. [data-speaking="true"]{
  1460. position: static !important;
  1461. }
  1462. [data-speaking="true"]::after{
  1463. left: .625em;
  1464. }
  1465. </style>
  1466. `,
  1467. inke: () => `
  1468. <style type="text/css">
  1469. button#${SCRIPTID}-config-button{
  1470. width: 36px;
  1471. height: 36px;
  1472. position: absolute;
  1473. left: 100%;
  1474. top: 0;
  1475. transform: translate(calc(-100% - 10px), -100%)
  1476. }
  1477. button#${SCRIPTID}-config-button svg{
  1478. width: 24px;
  1479. height: 24px;
  1480. transform: translateY(1px);
  1481. }
  1482. .comments_list li{
  1483. position: relative;
  1484. }
  1485. [data-speaking="true"]{
  1486. position: static !important;
  1487. }
  1488. [data-speaking="true"]::after{
  1489. left: calc(28px + .65em);
  1490. }
  1491. </style>
  1492. `,
  1493. line: () => `
  1494. <style type="text/css">
  1495. button#${SCRIPTID}-config-button{
  1496. width: 40px;
  1497. height: 40px;
  1498. float: right;
  1499. }
  1500. button#${SCRIPTID}-config-button svg{
  1501. width: 24px;
  1502. height: 24px;
  1503. transform: translateY(1px);
  1504. }
  1505. #${SCRIPTID}-config-panel legend{
  1506. position: static;
  1507. width: auto;
  1508. height: auto;
  1509. }
  1510. [class*="Chat"] [data-speaking="true"]{
  1511. position: static !important;
  1512. }
  1513. [class*="Chat"] [data-speaking="true"]::after{
  1514. left: 1em;
  1515. }
  1516. [class*="Label"][data-speaking="true"]::after{
  1517. left: 0em;
  1518. }
  1519. </style>
  1520. `,
  1521. niconico: () => `
  1522. <style type="text/css">
  1523. button#${SCRIPTID}-config-button{
  1524. width: 32px;
  1525. height: 36px;
  1526. }
  1527. button#${SCRIPTID}-config-button svg{
  1528. width: 20px;
  1529. height: 20px;
  1530. transform: translateY(1px);
  1531. }
  1532. </style>
  1533. `,
  1534. openrec: () => `
  1535. <style type="text/css">
  1536. button#${SCRIPTID}-config-button{
  1537. width: 2.2rem;
  1538. height: 2.2rem;
  1539. margin-right: 1rem;
  1540. }
  1541. .chat-content[data-speaking="true"]{
  1542. position: static !important;
  1543. }
  1544. </style>
  1545. `,
  1546. periscope: () => `
  1547. <style type="text/css">
  1548. button#${SCRIPTID}-config-button{
  1549. width: 32px;
  1550. height: 32px;
  1551. margin-left: 10px;
  1552. background-color: rgba(255, 255, 255, 0.2);
  1553. border-radius: 32px;
  1554. }
  1555. button#${SCRIPTID}-config-button svg{
  1556. width: 20px;
  1557. height: 20px;
  1558. }
  1559. .CommentMessage-body,
  1560. [data-speaking="true"]{
  1561. position: static !important;
  1562. }
  1563. </style>
  1564. `,
  1565. showroom: () => `
  1566. <style type="text/css">
  1567. button#${SCRIPTID}-config-button{
  1568. width: 60px;
  1569. height: 50px;
  1570. }
  1571. button#${SCRIPTID}-config-button svg{
  1572. width: 28px;
  1573. height: 28px;
  1574. transform: translateY(2px);
  1575. }
  1576. </style>
  1577. `,
  1578. twitcasting: () => `
  1579. <style type="text/css">
  1580. button#${SCRIPTID}-config-button{
  1581. width: 2em;
  1582. height: 2em;
  1583. margin-left: .5em;
  1584. }
  1585. #${SCRIPTID}-config-panel legend{
  1586. border: none;
  1587. width: auto;
  1588. }
  1589. #${SCRIPTID}-config-panel input,
  1590. #${SCRIPTID}-config-panel select{
  1591. width: auto;
  1592. }
  1593. </style>
  1594. `,
  1595. twitch: () => `
  1596. <style type="text/css">
  1597. button#${SCRIPTID}-config-button{
  1598. width: 3rem;
  1599. height: 3rem;
  1600. padding: .4rem;
  1601. }
  1602. #${SCRIPTID}-config-panel button{
  1603. text-align: center;
  1604. }
  1605. .chat-line__message{
  1606. position: relative;
  1607. }
  1608. .chat-line__message [data-speaking="true"]{
  1609. position: static !important;
  1610. }
  1611. .chat-line__message [data-speaking="true"]::after{
  1612. left: 1em;
  1613. }
  1614. </style>
  1615. `,
  1616. whowatch: () => `
  1617. <style type="text/css">
  1618. button#${SCRIPTID}-config-button{
  1619. width: 36px;
  1620. height: 36px;
  1621. position: absolute;
  1622. left: 0;
  1623. bottom: 0;
  1624. }
  1625. button#${SCRIPTID}-config-button svg{
  1626. width: 32px;
  1627. height: 32px;
  1628. transform: translateY(4px);
  1629. }
  1630. form .row{
  1631. position: relative;
  1632. }
  1633. [data-speaking="true"]{
  1634. position: static !important;
  1635. }
  1636. </style>
  1637. `,
  1638. yizhibo: () => `
  1639. <style type="text/css">
  1640. button#${SCRIPTID}-config-button{
  1641. width: 30px;
  1642. height: 30px;
  1643. position: absolute;
  1644. left: 100%;
  1645. top: 0;
  1646. transform: translate(-100%,-100%);
  1647. }
  1648. button#${SCRIPTID}-config-button svg{
  1649. width: 24px;
  1650. height: 24px;
  1651. transform: translateY(1px);
  1652. }
  1653. .msg_1{
  1654. overflow: visible !important;
  1655. }
  1656. [data-speaking="true"]{
  1657. position: static !important;
  1658. }
  1659. </style>
  1660. `,
  1661. youtube: () => `
  1662. <style type="text/css">
  1663. button#${SCRIPTID}-config-button{
  1664. width: 40px;
  1665. height: 40px;
  1666. }
  1667. button#${SCRIPTID}-config-button svg{
  1668. width: 20px;
  1669. height: 20px;
  1670. transform: translateY(1px);
  1671. }
  1672. yt-live-chat-text-message-renderer #content{
  1673. position: relative !important;
  1674. }
  1675. yt-live-chat-text-message-renderer [data-speaking="true"]{
  1676. position: static !important;
  1677. }
  1678. </style>
  1679. `,
  1680. yy: () => `
  1681. <style type="text/css">
  1682. button#${SCRIPTID}-config-button{
  1683. width: 30px;
  1684. height: 30px;
  1685. position: absolute;
  1686. left: 100%;
  1687. top: 0;
  1688. transform: translate(calc(-100% - 5px), calc(-100% - 5px));
  1689. }
  1690. button#${SCRIPTID}-config-button svg{
  1691. width: 24px;
  1692. height: 24px;
  1693. transform: translateY(1px);
  1694. }
  1695. ul.chatroom-list > li{
  1696. position: relative;
  1697. }
  1698. [data-speaking="true"]{
  1699. position: static !important;
  1700. }
  1701. [data-speaking="true"]::after{
  1702. left: .5em;
  1703. }
  1704. </style>
  1705. `,
  1706. },
  1707. };
  1708. const setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, requestAnimationFrame = window.requestAnimationFrame;
  1709. const alert = window.alert, confirm = window.confirm, getComputedStyle = window.getComputedStyle, fetch = window.fetch, speechSynthesis = window.speechSynthesis;
  1710. if(!('isConnected' in Node.prototype)) Object.defineProperty(Node.prototype, 'isConnected', {get: function(){return document.contains(this)}});
  1711. class Storage{
  1712. static key(key){
  1713. return (SCRIPTID) ? (SCRIPTID + '-' + key) : key;
  1714. }
  1715. static save(key, value, expire = null){
  1716. key = Storage.key(key);
  1717. localStorage[key] = JSON.stringify({
  1718. value: value,
  1719. saved: Date.now(),
  1720. expire: expire,
  1721. });
  1722. }
  1723. static read(key){
  1724. key = Storage.key(key);
  1725. if(localStorage[key] === undefined) return undefined;
  1726. let data = JSON.parse(localStorage[key]);
  1727. if(data.value === undefined) return data;
  1728. if(data.expire === undefined) return data;
  1729. if(data.expire === null) return data.value;
  1730. if(data.expire < Date.now()) return localStorage.removeItem(key);
  1731. return data.value;
  1732. }
  1733. static delete(key){
  1734. key = Storage.key(key);
  1735. delete localStorage.removeItem(key);
  1736. }
  1737. static saved(key){
  1738. key = Storage.key(key);
  1739. if(localStorage[key] === undefined) return undefined;
  1740. let data = JSON.parse(localStorage[key]);
  1741. if(data.saved) return data.saved;
  1742. else return undefined;
  1743. }
  1744. }
  1745. class Panels{
  1746. constructor(parent){
  1747. this.parent = parent;
  1748. this.panels = {};
  1749. this.listen();
  1750. }
  1751. listen(){
  1752. window.addEventListener('keydown', (e) => {
  1753. if(e.key !== 'Escape') return;
  1754. if(['input', 'textarea'].includes(document.activeElement.localName)) return;
  1755. Object.keys(this.panels).forEach(key => this.hide(key));
  1756. }, true);
  1757. }
  1758. add(name, panel){
  1759. this.panels[name] = panel;
  1760. }
  1761. toggle(name){
  1762. let panel = this.panels[name];
  1763. if(panel.isConnected === false || panel.classList.contains('hidden')) this.show(name);
  1764. else this.hide(name);
  1765. }
  1766. show(name){
  1767. let panel = this.panels[name];
  1768. if(panel.isConnected) return;
  1769. panel.classList.add('hidden');
  1770. this.parent.appendChild(panel);
  1771. this.parent.dataset.panels = parseInt(this.parent.dataset.panels) + 1;
  1772. animate(() => panel.classList.remove('hidden'));
  1773. }
  1774. hide(name){
  1775. let panel = this.panels[name];
  1776. if(panel.classList.contains('hidden')) return;
  1777. panel.classList.add('hidden');
  1778. panel.addEventListener('transitionend', (e) => {
  1779. this.parent.removeChild(panel);
  1780. this.panels.dataset.panels = parseInt(this.panels.dataset.panels) - 1;
  1781. }, {once: true});
  1782. }
  1783. }
  1784. const $ = function(s, f){
  1785. let target = document.querySelector(s);
  1786. if(target === null) return null;
  1787. return f ? f(target) : target;
  1788. };
  1789. const $$ = function(s){return document.querySelectorAll(s)};
  1790. const animate = function(callback, ...params){requestAnimationFrame(() => requestAnimationFrame(() => callback(...params)))};
  1791. const createElement = function(html = '<span></span>'){
  1792. let outer = document.createElement('div');
  1793. outer.innerHTML = html;
  1794. return outer.firstElementChild;
  1795. };
  1796. const observe = function(element, callback, options = {childList: true, attributes: false, characterData: false, subtree: false}){
  1797. let observer = new MutationObserver(callback.bind(element));
  1798. observer.observe(element, options);
  1799. return observer;
  1800. };
  1801. const normalize = function(string){
  1802. return string.replace(/[!-~]/g, function(s){
  1803. return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
  1804. }).replace(normalize.RE, function(s){
  1805. return normalize.KANA[s];
  1806. }).replace(/ /g, ' ').replace(/~/g, '〜');
  1807. };
  1808. normalize.KANA = {
  1809. ガ:'ガ', ギ:'ギ', グ:'グ', ゲ:'ゲ', ゴ: 'ゴ',
  1810. ザ:'ザ', ジ:'ジ', ズ:'ズ', ゼ:'ゼ', ゾ: 'ゾ',
  1811. ダ:'ダ', ヂ:'ヂ', ヅ:'ヅ', デ:'デ', ド: 'ド',
  1812. バ:'バ', ビ:'ビ', ブ:'ブ', ベ:'ベ', ボ: 'ボ',
  1813. パ:'パ', ピ:'ピ', プ:'プ', ペ:'ペ', ポ: 'ポ',
  1814. ヷ:'ヷ', ヺ:'ヺ', ヴ:'ヴ',
  1815. ア:'ア', イ:'イ', ウ:'ウ', エ:'エ', オ:'オ',
  1816. カ:'カ', キ:'キ', ク:'ク', ケ:'ケ', コ:'コ',
  1817. サ:'サ', シ:'シ', ス:'ス', セ:'セ', ソ:'ソ',
  1818. タ:'タ', チ:'チ', ツ:'ツ', テ:'テ', ト:'ト',
  1819. ナ:'ナ', ニ:'ニ', ヌ:'ヌ', ネ:'ネ', ノ:'ノ',
  1820. ハ:'ハ', ヒ:'ヒ', フ:'フ', ヘ:'ヘ', ホ:'ホ',
  1821. マ:'マ', ミ:'ミ', ム:'ム', メ:'メ', モ:'モ',
  1822. ヤ:'ヤ', ユ:'ユ', ヨ:'ヨ',
  1823. ラ:'ラ', リ:'リ', ル:'ル', レ:'レ', ロ:'ロ',
  1824. ワ:'ワ', ヲ:'ヲ', ン:'ン',
  1825. ァ:'ァ', ィ:'ィ', ゥ:'ゥ', ェ:'ェ', ォ:'ォ',
  1826. ッ:'ッ', ャ:'ャ', ュ:'ュ', ョ:'ョ',
  1827. "。":'。', "、":'、', "ー":'ー', "「":'「', "」":'」', "・":'・',
  1828. };
  1829. normalize.RE = new RegExp('(' + Object.keys(normalize.KANA).join('|') + ')', 'g');
  1830. const log = function(){
  1831. if(!DEBUG) return;
  1832. let l = log.last = log.now || new Date(), n = log.now = new Date();
  1833. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  1834. //console.log(error.stack);
  1835. console.log(
  1836. (SCRIPTID || '') + ':',
  1837. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  1838. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  1839. /* :00 */ ':' + line,
  1840. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  1841. /* caller */ (callers[1] || '') + '()',
  1842. ...arguments
  1843. );
  1844. };
  1845. log.formats = [{
  1846. name: 'Firefox Scratchpad',
  1847. detector: /MARKER@Scratchpad/,
  1848. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  1849. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  1850. }, {
  1851. name: 'Firefox Console',
  1852. detector: /MARKER@debugger/,
  1853. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  1854. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  1855. }, {
  1856. name: 'Firefox Greasemonkey 3',
  1857. detector: /\/gm_scripts\//,
  1858. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  1859. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  1860. }, {
  1861. name: 'Firefox Greasemonkey 4+',
  1862. detector: /MARKER@user-script:/,
  1863. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  1864. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  1865. }, {
  1866. name: 'Firefox Tampermonkey',
  1867. detector: /MARKER@moz-extension:/,
  1868. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
  1869. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  1870. }, {
  1871. name: 'Chrome Console',
  1872. detector: /at MARKER \(<anonymous>/,
  1873. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  1874. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  1875. }, {
  1876. name: 'Chrome Tampermonkey',
  1877. detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?id=/,
  1878. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 6,
  1879. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  1880. }, {
  1881. name: 'Chrome Extension',
  1882. detector: /at MARKER \(chrome-extension:/,
  1883. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  1884. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  1885. }, {
  1886. name: 'Edge Console',
  1887. detector: /at MARKER \(eval/,
  1888. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  1889. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  1890. }, {
  1891. name: 'Edge Tampermonkey',
  1892. detector: /at MARKER \(Function/,
  1893. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  1894. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  1895. }, {
  1896. name: 'Safari',
  1897. detector: /^MARKER$/m,
  1898. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  1899. getCallers: (e) => e.stack.split('\n'),
  1900. }, {
  1901. name: 'Default',
  1902. detector: /./,
  1903. getLine: (e) => 0,
  1904. getCallers: (e) => [],
  1905. }];
  1906. log.format = log.formats.find(function MARKER(f){
  1907. if(!f.detector.test(new Error().stack)) return false;
  1908. //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack);
  1909. return true;
  1910. });
  1911. const time = function(label){
  1912. if(!DEBUG) return;
  1913. const BAR = '|', TOTAL = 100;
  1914. switch(true){
  1915. case(label === undefined):/* time() to output total */
  1916. let total = 0;
  1917. Object.keys(time.records).forEach((label) => total += time.records[label].total);
  1918. Object.keys(time.records).forEach((label) => {
  1919. console.log(
  1920. BAR.repeat((time.records[label].total / total) * TOTAL),
  1921. label + ':',
  1922. (time.records[label].total).toFixed(3) + 'ms',
  1923. '(' + time.records[label].count + ')',
  1924. );
  1925. });
  1926. time.records = {};
  1927. break;
  1928. case(!time.records[label]):/* time('label') to create and start the record */
  1929. time.records[label] = {count: 0, from: performance.now(), total: 0};
  1930. break;
  1931. case(time.records[label].from === null):/* time('label') to re-start the lap */
  1932. time.records[label].from = performance.now();
  1933. break;
  1934. case(0 < time.records[label].from):/* time('label') to add lap time to the record */
  1935. time.records[label].total += performance.now() - time.records[label].from;
  1936. time.records[label].from = null;
  1937. time.records[label].count += 1;
  1938. break;
  1939. }
  1940. };
  1941. time.records = {};
  1942. core.initialize();
  1943. if(window === top && console.timeEnd) console.timeEnd(SCRIPTID);
  1944. })();