* Streaming Comment Reader chan

It reads comment text on streaming sites by speech synthesis.

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

QingJ © 2025

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