* 朗读直播评论酱

用声音朗读直播网站的新到来评论。

当前为 2020-09-02 提交的版本,查看 最新版本

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

QingJ © 2025

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