* 朗读直播评论酱

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

当前为 2019-07-21 提交的版本,查看 最新版本

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