巴哈姆特动画疯 ASS格式弹幕下载器 Modified

以ASS字幕格式保存巴哈姆特动画疯的弹幕

  1. // ==UserScript==
  2. // @name bahamut ASS Danmaku Downloader Modified
  3. // @name:zh-TW 動畫瘋 ASS格式彈幕下載器 Modified
  4. // @name:zh-CN 巴哈姆特动画疯 ASS格式弹幕下载器 Modified
  5. // @namespace https://github.com/tiansh, https://github.com/zhuzemin
  6. // @description http://ani.gamer.com.tw download danmaku as ".ass"
  7. // @description:zh-TW 以ASS字幕格式保存巴哈姆特動畫瘋的彈幕
  8. // @description:zh-cn 以ASS字幕格式保存巴哈姆特动画疯的弹幕
  9. // @include https://ani.gamer.com.tw/*
  10. // @version 1.07.3
  11. // @grant GM_addStyle
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant GM_registerMenuCommand
  15. // @connect ani.gamer.com.tw
  16. // @run-at document-start
  17. // @author 田生, Modified by zhuzemin, az689
  18. // @copyright 2014+, 田生
  19. // @license Mozilla Public License 2.0; http://www.mozilla.org/MPL/2.0/
  20. // @license CC Attribution-ShareAlike 4.0 International; http://creativecommons.org/licenses/by-sa/4.0/
  21. // ==/UserScript==
  22. // 设置项
  23. var defconfig = {
  24. 'playResX': 960, // 屏幕分辨率宽(像素)
  25. 'playResY': 540, // 屏幕分辨率高(像素)
  26. 'fontlist': [ // 字形(会自动选择最前面一个可用的)
  27. 'Microsoft YaHei UI', 'Microsoft YaHei', '文泉驿正黑', 'STHeitiSC', '黑体',
  28. ],
  29. 'font_size': 24, // 字号(像素)
  30. 'exlinespace': 1, // 额外行距(字号的比例)
  31. 'r2ltime': 8, // 右到左弹幕持续时间(秒)
  32. 'fixtime': 4, // 固定弹幕持续时间(秒)
  33. 'opacity': 0.6, // 不透明度(比例)
  34. 'space': 0, // 弹幕间隔的最小水平距离(像素)
  35. 'max_delay': 6, // 最多允许延迟几秒出现弹幕
  36. 'bottom': 0, // 底端给字幕保留的空间(像素)
  37. 'use_canvas': false, // 是否使用canvas计算文本宽度(布尔值,Linux下的火狐默认否,其他默认是,Firefox bug #561361)
  38. 'debug': true, // 打印调试信息
  39. 'language': "zh_tw"
  40. };
  41.  
  42. //读取设置
  43. var config = GM_getValue("config", null);
  44. if (config === null) {
  45. GM_setValue("config", defconfig);
  46. config = defconfig;
  47. console.log("設定初始化 Configuration has been initialized.");
  48. };
  49. var debug = config.debug ? console.log.bind(console) : function() {};
  50.  
  51. //html模板
  52. var settingPanelHTMLtemplate = `
  53. <div class="assdanmakusetting-window">
  54. <div class="assdanmakusetting-title" style="border-bottom: 1px solid black; height: 54px;">
  55. <button class="assdanmakusetting-exitbutton" title="{{exit}}" onclick="document.querySelector('.assdanmakusetting-window').remove()">{{exit}}</button>
  56. <button class="assdanmakusetting-savebutton" title="{{save}}">{{save}}</button>
  57. <p>{{title}}</p>
  58. </div>
  59. <form class="assdanmakusetting-container">
  60. </form>
  61. </div>
  62. `;
  63. var settingItemHTMLtemplate = `
  64. <div class="assdanmakusetting-item">{{content}}</div>
  65. `;
  66. var settingInputHTMLtemplate = `
  67. <lable for="{{key}}" title="{{key}}">{{description}}</lable>
  68. <button type="button" act="setval" acp="def" title="{{butsetdeftitle}}">{{butsetdef}}</button>
  69. <button type="button" act="setval" acp="cur" title="{{butsetcurtitle}}">{{butsetcur}}</button>
  70. <{{htmltag}} name="{{key}}" {{attr}}>{{content}}</{{htmltag}}>
  71. `;
  72. var last3ButtonHTMLtemplate = `
  73. {{title}}
  74. <button type="button" act="executecode">{{executecode}}</button>
  75. <button type="button" act="editconfig">{{editconfig}}</button>
  76. <button type="button" act="resetconfig">{{resetconfig}}</button>
  77. `;
  78. var settingOptionsHTMLtemplate = `
  79. <option value="{{value}}">{{text}}</option>
  80. `;
  81. var injectedCSS = `
  82. .assdanmakusetting-window {
  83. position: fixed;
  84. left: 50%;
  85. top: 50%;
  86. transform: translateX(-50%) translateY(-50%);
  87. width: 40%;
  88. height: 60%;
  89. min-width: 480px;
  90. z-index: 114514;
  91. overflow: hidden;
  92. background: whitesmoke;
  93. border: 1px solid black;
  94. box-shadow: 2px 2px 2px 2px rgb(119, 119, 119);
  95. border-radius: 4px;
  96. }
  97. .assdanmakusetting-title button {
  98. color: black;
  99. font-size: 24px;
  100. float: right;
  101. top: 2.5%;
  102. border: 2px solid black;
  103. margin: 8px 8px 8px 0px;
  104. background: gainsboro;
  105. font-family: "Arial, sans-serif";
  106. border-radius: 6px;
  107. }
  108. .assdanmakusetting-title p{
  109. font-size: 24px;
  110. float: left;
  111. margin: 16px;
  112. line-height: 1em;
  113. z-index: -1;
  114. }
  115. .assdanmakusetting-container {
  116. overflow: auto;
  117. height: calc(100% - 58px);
  118. width: 100%;
  119. }
  120. .assdanmakusetting-item {
  121. padding: 8px;
  122. font-size: 16px;
  123. word-wrap: break-word;
  124. width: 100%;
  125. border-bottom: gainsboro solid 1px;
  126. overflow: hidden;
  127. }
  128. .assdanmakusetting-item input,.assdanmakusetting-item button,.assdanmakusetting-item select{
  129. float: right;
  130. margin: 4px;
  131. }
  132. .assdanmakusetting-savebutton:hover {
  133. color: white;
  134. background: #37e;
  135. }
  136. .assdanmakusetting-savebutton:active {
  137. background: #6ce;
  138. }
  139. .assdanmakusetting-exitbutton:hover {
  140. color: white;
  141. background: #e44;
  142. }
  143. .assdanmakusetting-exitbutton:active {
  144. background: #b33;
  145. }
  146. .listgetassdanmaku-button {
  147. padding: 4px 4px;
  148. border-radius: 5px;
  149. background-color: var(--baha-primary-color);
  150. z-index: 4;
  151. color: #EEE;
  152. white-space: nowrap;
  153. }
  154. .listdownloadbutton {
  155. border: 1px solid var(--btn-favorite-video);
  156. border-radius: 4px;
  157. color: var(--btn-favorite-video);
  158. cursor: pointer;
  159. padding: 5px 0px;
  160. font-size: 1em;
  161. background: transparent;
  162. margin-right: 5px;
  163. width: 30px;
  164. text-align: center;
  165. }
  166. .listdownloadbutton:hover {
  167. background-color: var(--btn-favorite-video);
  168. color: rgba(var(--anime-white-rgb), 0.95);
  169. }
  170. .ahveuiw:after {
  171. content: "" !important;
  172. }
  173. `;
  174.  
  175. //多语言
  176. var lang = new Object();
  177.  
  178. var l10n = {
  179. 'name': "正體中文",
  180. 'text': {
  181. 'getdanmaku': "獲取ASS彈幕",
  182. 'episodelistgetdanmaku': "下載ASS彈幕:"
  183. },
  184. 'message': {
  185. 'xhrfailed': "獲取失敗",
  186. 'invalidsnid': "無效的SN/Url",
  187. 'invalidsnidlog': "字串中未找到SN號:",
  188. 'getdanmakuformsn': "請輸入SN號或動畫頁面的URL:",
  189. 'gotdanmaku': "獲取了 %d 個彈幕, 從 %s",
  190. 'confirmresetconfig':"您確定要重設設定嗎? 這將重新整理頁面",
  191. 'editconfig': "確定後將立即儲存, 如果再點擊儲存將導致設定被覆蓋, 部分選項仍需要重新整理頁面才能生效",
  192. 'configsaved': "設定已儲存"
  193. },
  194. 'ui':{
  195. 'setting': "設定",
  196. 'getdanmakuformsn': "從SN號獲取彈幕",
  197. 'settingpaneltitle': "ASS格式彈幕下載器設定",
  198. 'save': "儲存",
  199. 'exit': "退出",
  200. 'defaultvalue': "預設值",
  201. 'currenttvalue': "当前值",
  202. 'resetconfig': "重設設定",
  203. 'editconfig': "直接編輯設定(匯入/匯出)",
  204. 'executecode': "執行程式碼",
  205. 'misc': "其他選項",
  206. 'true': "是",
  207. 'false': "否",
  208. 'butsetdeftitle': "設為預設值",
  209. 'butsetcurtitle': "設為目前值"
  210. },
  211. 'config':{
  212. 'description': {
  213. 'playResX': "畫布水平解析度(畫素, ASS裡的同名參數)",
  214. 'playResY': "畫布垂直解析度(畫素, ASS裡的同名參數)",
  215. 'fontlist': "字形(會自動選擇最前面一個可用的)",
  216. 'font_size': "字號(畫素)",
  217. 'exlinespace': "額外行距(字號的比例)",
  218. 'r2ltime': "右到左彈幕持續時間(秒)",
  219. 'fixtime': "固定彈幕持續時間(秒)",
  220. 'opacity': "不透明度(比例)",
  221. 'space': "彈幕間隔的最小水平距離(畫素)",
  222. 'max_delay': "最多允許延遲幾秒出現彈幕",
  223. 'bottom': "底端給字幕保留的空間(畫素)",
  224. 'use_canvas': "是否使用canvas計算文字寬度",
  225. 'debug': "列印除錯資訊",
  226. 'language': "介面語言 (重新整理生效)"
  227. },
  228. 'errormsg': {
  229. 'fontlist': ''
  230. }
  231. }
  232. };
  233.  
  234. lang.zh_tw = { 'name': "正體中文" };
  235.  
  236. lang.en_us = {
  237. 'name': "English",
  238. 'text': {
  239. 'getdanmaku': "Get ASS Danmaku",
  240. 'episodelistgetdanmaku': "Download ASS Danmaku:"
  241. },
  242. 'message': {
  243. 'xhrfailed': "Failed to obtain ",
  244. 'invalidsnid': "Invalid SN/Url",
  245. 'invalidsnidlog': "SNID not found in string:",
  246. 'getdanmakuformsn': "Please enter the SNID or the URL of the anime page:",
  247. 'gotdanmaku': "got %d danmaku form %s",
  248. 'confirmresetconfig': "Are you sure you want to reset the configuration? This will refresh the page.",
  249. 'editconfig': "Changes will be saved immediately. Clicking 'Save' again will overwrite the settings. Some options still need to be refreshed to take effect.",
  250. 'configsaved': "Configuration saved."
  251. },
  252. 'ui':{
  253. 'setting': "Setting",
  254. 'getdanmakuformsn': "Get danmaku form SN",
  255. 'settingpaneltitle': "Danmaku downloader setting",
  256. 'save': "Save",
  257. 'exit': "Exit",
  258. 'defaultvalue': "DefVal",
  259. 'currenttvalue': "CurVal",
  260. 'resetconfig': "Reset Config",
  261. 'editconfig': "Edit Raw Config (Im/Export)",
  262. 'executecode': "Execute Code",
  263. 'misc': "Miscellany",
  264. 'true': "yes",
  265. 'false': "no",
  266. 'butsetdeftitle': "Set to default value",
  267. 'butsetcurtitle': "Set to current value"
  268. },
  269. 'config':{
  270. 'description': {
  271. 'playResX': "Canvas resolution width (pixels, same name parameter in ASS)",
  272. 'playResY': "Canvas resolution heigh (pixels, same name parameter in ASS)",
  273. 'fontlist': "Font (the first available one will be automatically selected)",
  274. 'font_size': "Font size (pixels)",
  275. 'exlinespace': "Extra line spacing (scale of fontsize)",
  276. 'r2ltime': "Right to left danmaku duration (seconds)",
  277. 'fixtime': "Fixed danmauku duration (seconds)",
  278. 'opacity': "opacity (scale)",
  279. 'space': "Minimum horizontal distance between danmaku (pixels)",
  280. 'max_delay': "The maximum delay for danmaku to appear (seconds)",
  281. 'bottom': "The space reserved for subtitles at the bottom (pixels)",
  282. 'use_canvas': "Whether to use canvas to calculate text width",
  283. 'debug': "Print debugging information",
  284. 'language': "UI Language (Refresh to take effect)"
  285. },
  286. 'errormsg': {
  287. 'fontlist': ''
  288. }
  289. }
  290. };
  291.  
  292. //设置面板项
  293. var settingitems = {
  294. 'playResX': {
  295. 'htmltag': "input",
  296. 'type': "number",
  297. 'exattr': [["min", 0]]
  298. },
  299. 'playResY': {
  300. 'htmltag': "input",
  301. 'type': "number",
  302. 'exattr': [["min", 0]]
  303. },
  304. 'fontlist': {
  305. 'htmltag': "input",
  306. 'type': "text",
  307. 'exattr': [], //[["pattern", /^\[(\s*(\x22[^\x22]\x22)|(\x27[^\x27]\x27)|(\x60[^\x60]\x60)\s*,)*\s*(\x22[^\x22]\x22)|(\x27[^\x27]\x27)|(\x60[^\x60]\x60)\s*\]$/ ]] //匹配字符串数组
  308. 'datatype': "array"
  309. },
  310. 'font_size': {
  311. 'htmltag': "input",
  312. 'type': "number",
  313. 'exattr': [["step", 0.5], ["min", 0]]
  314. },
  315. 'exlinespace': {
  316. 'htmltag': "input",
  317. 'type': "number",
  318. 'exattr': [["step", 0.01]]
  319. },
  320. 'r2ltime': {
  321. 'htmltag': "input",
  322. 'type': "number",
  323. 'exattr': [["step", 0.1], ["min", 0]]
  324. },
  325. 'fixtime': {
  326. 'htmltag': "input",
  327. 'type': "number",
  328. 'exattr': [["step", 0.1], ["min", 0]]
  329. },
  330. 'opacity': {
  331. 'htmltag': "input",
  332. 'type': "number",
  333. 'exattr': [["step", 0.01], ["min", 0]]
  334. },
  335. 'space': {
  336. 'htmltag': "input",
  337. 'type': "number",
  338. 'exattr': []
  339. },
  340. 'max_delay': {
  341. 'htmltag': "input",
  342. 'type': "number",
  343. 'exattr': [["step", 0.1], ["min", 0]]
  344. },
  345. 'bottom': {
  346. 'htmltag': "input",
  347. 'type': "number",
  348. 'exattr': [["min", 0]]
  349. },
  350. 'use_canvas': {
  351. 'htmltag': "select",
  352. 'type': "boolean",
  353. 'exattr': [],
  354. get options() {
  355. return [["true", l10n.ui.true], ["false", l10n.ui.false]];
  356. }
  357. },
  358. 'debug': {
  359. 'htmltag': "select",
  360. 'type': "boolean",
  361. 'exattr': [],
  362. get options() {
  363. return [["true", l10n.ui.true], ["false", l10n.ui.false]];
  364. }
  365. },
  366. 'language': {
  367. 'htmltag': "select",
  368. 'type': "string",
  369. 'exattr': [],
  370. get options() {
  371. var out = new Array();
  372. for (let key in lang) {
  373. out.push([key, lang[key].name]);
  374. };
  375. return out;
  376. }
  377. }
  378. };
  379.  
  380. // 将字典中的值填入字符串
  381. var fillStr = function(str) {
  382. var dict = Array.apply(Array, arguments);
  383. return str.replace(/{{([^}]+)}}/g, function(r, o) {
  384. var ret;
  385. dict.some(function(i) {
  386. return ret = i[o];
  387. });
  388. return ret || '';
  389. });
  390. };
  391. // 将颜色的数值化为十六进制字符串表示
  392. var RRGGBB = function(color) {
  393. var t = Number(color).toString(16).toUpperCase();
  394. return (Array(7).join('0') + t).slice(-6);
  395. };
  396. // 将可见度转换为透明度
  397. var hexAlpha = function(opacity) {
  398. var alpha = Math.round(255 * (1 - opacity)).toString(16).toUpperCase();
  399. return Array(3 - alpha.length).join('0') + alpha;
  400. };
  401. // 字符串
  402. var funStr = function(fun) {
  403. return fun.toString().split(/\r\n|\n|\r/).slice(1, -1).join('\n');
  404. };
  405. // 平方和开根
  406. var hypot = Math.hypot ? Math.hypot.bind(Math) : function() {
  407. return Math.sqrt([0].concat(Array.apply(Array, arguments)).reduce(function(x, y) {
  408. return x + y * y;
  409. }));
  410. };
  411. // 创建下载
  412. var startDownload = function(data, filename) {
  413. var blob = new Blob([data], {
  414. type: 'application/octet-stream'
  415. });
  416. var url = window.URL.createObjectURL(blob);
  417. var saveas = document.createElement('a');
  418. saveas.href = url;
  419. saveas.style.display = 'none';
  420. document.body.appendChild(saveas);
  421. saveas.download = filename;
  422. saveas.click();
  423. setTimeout(function() {
  424. saveas.parentNode.removeChild(saveas);
  425. }, 1000)
  426. document.addEventListener('unload', function() {
  427. window.URL.revokeObjectURL(url);
  428. });
  429. };
  430. // 计算文字宽度
  431. var calcWidth = (function() {
  432. // 使用Canvas计算
  433. var calcWidthCanvas = function() {
  434. var canvas = document.createElement('canvas');
  435. var context = canvas.getContext('2d');
  436. return function(fontname, text, fontsize) {
  437. context.font = 'bold ' + fontsize + 'px ' + fontname;
  438. return Math.ceil(context.measureText(text).width + config.space);
  439. };
  440. }; // 使用Div计算
  441. var calcWidthDiv = function() {
  442. var d = document.createElement('div');
  443. d.setAttribute('style', ['all: unset', 'top: -10000px', 'left: -10000px', 'width: auto', 'height: auto', 'position: absolute', '', ].join(' !important; '));
  444. var ld = function() {
  445. document.body.parentNode.appendChild(d);
  446. }
  447. if (!document.body) document.addEventListener('DOMContentLoaded', ld);
  448. else ld();
  449. return function(fontname, text, fontsize) {
  450. d.textContent = text;
  451. d.style.font = 'bold ' + fontsize + 'px ' + fontname;
  452. return d.clientWidth + config.space;
  453. };
  454. };
  455. // 检查使用哪个测量文字宽度的方法
  456. if (config.use_canvas === null) {
  457. if (navigator.platform.match(/linux/i) && !navigator.userAgent.match(/chrome/i)) config.use_canvas = false;
  458. }
  459. debug('use canvas: %o', config.use_canvas !== false);
  460. if (config.use_canvas === false) return calcWidthDiv();
  461. return calcWidthCanvas();
  462. }());
  463. // 选择合适的字体
  464. var choseFont = function(fontlist) {
  465. // 检查这个字串的宽度来检查字体是否存在
  466. var sampleText = 'The quick brown fox jumps over the lazy dog' + '7531902468' + ',.!-' + ',。:!' + '天地玄黄' + '则近道矣';
  467. // 和这些字体进行比较
  468. var sampleFont = ['monospace', 'sans-serif', 'sans', 'Symbol', 'Arial', 'Comic Sans MS', 'Fixed', 'Terminal', 'Times', 'Times New Roman', '宋体', '黑体', '文泉驿正黑', 'Microsoft YaHei'];
  469. // 如果被检查的字体和基准字体可以渲染出不同的宽度
  470. // 那么说明被检查的字体总是存在的
  471. var diffFont = function(base, test) {
  472. var baseSize = calcWidth(base, sampleText, 72);
  473. var testSize = calcWidth(test + ',' + base, sampleText, 72);
  474. return baseSize !== testSize;
  475. };
  476. var validFont = function(test) {
  477. var valid = sampleFont.some(function(base) {
  478. return diffFont(base, test);
  479. });
  480. debug('font %s: %o', test, valid);
  481. return valid;
  482. };
  483. // 找一个能用的字体
  484. var f = fontlist[fontlist.length - 1];
  485. fontlist = fontlist.filter(validFont);
  486. debug('fontlist: %o', fontlist);
  487. return fontlist[0] || f;
  488. };
  489. // 从备选的字体中选择一个机器上提供了的字体
  490. var initFont = (function() {
  491. var done = false;
  492. return function() {
  493. if (done) return;
  494. done = true;
  495. calcWidth = calcWidth.bind(window, config.font = choseFont(config.fontlist));
  496. };
  497. }());
  498. var generateASS = function(danmaku, info) {
  499. var assHeader = fillStr(funStr(function() { /*! ASS弹幕文件文件头
  500. [Script Info]
  501. Title: {{title}}
  502. Original Script: 根据 {{ori}} 的弹幕信息,由 https://github.com/tiansh/us-danmaku 于 {{time}} 生成
  503. ScriptType: v4.00+
  504. Collisions: Normal
  505. PlayResX: {{playResX}}
  506. PlayResY: {{playResY}}
  507. Timer: 10.0000
  508.  
  509. [V4+ Styles]
  510. Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
  511. Style: Fix,{{font}},{{size}},&H{{alpha}}FFFFFF,&H{{alpha}}FFFFFF,&H{{alpha}}000000,&H{{alpha}}000000,1,0,0,0,100,100,0,0,1,2,0,2,20,20,2,0
  512. Style: R2L,{{font}},{{size}},&H{{alpha}}FFFFFF,&H{{alpha}}FFFFFF,&H{{alpha}}000000,&H{{alpha}}000000,1,0,0,0,100,100,0,0,1,2,0,2,20,20,2,0
  513.  
  514. [Events]
  515. Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
  516. */
  517.  
  518.  
  519. }), config, info, {
  520. 'alpha': hexAlpha(config.opacity),
  521. 'size': config.font_size,
  522. 'time': (new Date()).toString()
  523. });
  524. // 补齐数字开头的0
  525. var paddingNum = function(num, len) {
  526. num = '' + num;
  527. while (num.length < len) num = '0' + num;
  528. return num;
  529. };
  530. // 格式化时间
  531. var formatTime = function(time) {
  532. time = 100 * time ^ 0;
  533. var l = [
  534. [100,
  535. 2
  536. ],
  537. [
  538. 60,
  539. 2
  540. ],
  541. [
  542. 60,
  543. 2
  544. ],
  545. [
  546. Infinity,
  547. 0
  548. ]
  549. ].map(function(c) {
  550. var r = time % c[0];
  551. time = (time - r) / c[0];
  552. return paddingNum(r, c[1]);
  553. }).reverse();
  554. return l.slice(0, -1).join(':') + '.' + l[3];
  555. };
  556. // 格式化特效
  557. var format = (function() {
  558. // 适用于所有弹幕
  559. var common = function(line) {
  560. var s = '';
  561. var rgb = line.color.split(/(..)/).filter(function(x) {
  562. return x;
  563. }).map(function(x) {
  564. return parseInt(x, 16);
  565. });
  566. // 如果不是白色,要指定弹幕特殊的颜色
  567. if (line.color !== 'FFFFFF') // line.color 是 RRGGBB 格式
  568. s += '\\c&H' + line.color.split(/(..)/).reverse().join('');
  569. // 如果弹幕颜色比较深,用白色的外边框
  570. var dark = rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114 < 48;
  571. if (dark) s += '\\3c&HFFFFFF';
  572. if (line.size !== 1) s += '\\fs' + Math.round(line.size * config.font_size);
  573. return s;
  574. };
  575. // 适用于从右到左弹幕
  576. var r2l = function(line) {
  577. return '\\move(' + [
  578. line.poss.x,
  579. line.poss.y,
  580. line.posd.x,
  581. line.posd.y
  582. ].join(',') + ')';
  583. };
  584. // 适用于固定位置弹幕
  585. var fix = function(line) {
  586. return '\\pos(' + [
  587. line.poss.x,
  588. line.poss.y
  589. ].join(',') + ')';
  590. };
  591. var withCommon = function(f) {
  592. return function(line) {
  593. return f(line) + common(line);
  594. };
  595. };
  596. return {
  597. 'R2L': withCommon(r2l),
  598. 'Fix': withCommon(fix),
  599. };
  600. }());
  601. // 转义一些字符
  602. var escapeAssText = function(s) {
  603. // "{"、"}"字符libass可以转义,但是VSFilter不可以,所以直接用全角补上
  604. return s.replace(/{/g, '{').replace(/}/g, '}').replace(/\r|\n/g, '');
  605. };
  606. // 将一行转换为ASS的事件
  607. var convert2Ass = function(line) {
  608. return 'Dialogue: ' + [
  609. 0,
  610. formatTime(line.stime),
  611. formatTime(line.dtime),
  612. line.type, ',20,20,2,,',
  613. ].join(',') + '{' + format[line.type](line) + '}' + escapeAssText(line.text);
  614. };
  615. return assHeader + danmaku.map(convert2Ass).filter(function(x) {
  616. return x;
  617. }).join('\n');
  618. };
  619. /*
  620.  
  621. 下文字母含义:
  622. 0 ||----------------------x---------------------->
  623. _____________________c_____________________
  624. = / wc \ 0
  625. | | |--v--| wv | |--v--|
  626. | d |--v--| d f |--v--|
  627. y |--v--| l f | s _ p
  628. | | VIDEO |--v--| |--v--| _ m
  629. v | AREA (x ^ y) |
  630.  
  631. v: 弹幕
  632. c: 屏幕
  633.  
  634. 0: 弹幕发送
  635. a: 可行方案
  636.  
  637. s: 开始出现
  638. f: 出现完全
  639. l: 开始消失
  640. d: 消失完全
  641.  
  642. p: 上边缘(含)
  643. m: 下边缘(不含)
  644.  
  645. w: 宽度
  646. h: 高度
  647. b: 底端保留
  648.  
  649. t: 时间点
  650. u: 时间段
  651. r: 延迟
  652.  
  653. 并规定
  654. ts := t0s + r
  655. tf := wv / (wc + ws) * p + ts
  656. tl := ws / (wc + ws) * p + ts
  657. td := p + ts
  658.  
  659. */
  660. // 滚动弹幕
  661. var normalDanmaku = (function(wc, hc, b, u, maxr) {
  662. return function() {
  663. // 初始化屏幕外面是不可用的
  664. var used = [{
  665. 'p': -Infinity,
  666. 'm': 0,
  667. 'tf': Infinity,
  668. 'td': Infinity,
  669. 'b': false
  670. },
  671. {
  672. 'p': hc,
  673. 'm': Infinity,
  674. 'tf': Infinity,
  675. 'td': Infinity,
  676. 'b': false
  677. },
  678. {
  679. 'p': hc - b,
  680. 'm': hc,
  681. 'tf': Infinity,
  682. 'td': Infinity,
  683. 'b': true
  684. },
  685. ];
  686. // 检查一些可用的位置
  687. var available = function(hv, t0s, t0l, b) {
  688. var suggestion = [];
  689. // 这些上边缘总在别的块的下边缘
  690. used.forEach(function(i) {
  691. if (i.m > hc) return;
  692. var p = i.m;
  693. var m = p + hv;
  694. var tas = t0s;
  695. var tal = t0l;
  696. // 这些块的左边缘总是这个区域里面最大的边缘
  697. used.forEach(function(j) {
  698. if (j.p >= m) return;
  699. if (j.m <= p) return;
  700. if (j.b && b) return;
  701. tas = Math.max(tas, j.tf);
  702. tal = Math.max(tal, j.td);
  703. });
  704. // 最后作为一种备选留下来
  705. suggestion.push({
  706. 'p': p,
  707. 'r': Math.max(tas - t0s, tal - t0l),
  708. });
  709. });
  710. // 根据高度排序
  711. suggestion.sort(function(x, y) {
  712. return x.p - y.p;
  713. });
  714. var mr = maxr;
  715. // 又靠右又靠下的选择可以忽略,剩下的返回
  716. suggestion = suggestion.filter(function(i) {
  717. if (i.r >= mr) return false;
  718. mr = i.r;
  719. return true;
  720. });
  721. return suggestion;
  722. };
  723. // 添加一个被使用的
  724. var use = function(p, m, tf, td) {
  725. used.push({
  726. 'p': p,
  727. 'm': m,
  728. 'tf': tf,
  729. 'td': td,
  730. 'b': false
  731. });
  732. };
  733. // 根据时间同步掉无用的
  734. var syn = function(t0s, t0l) {
  735. used = used.filter(function(i) {
  736. return i.tf > t0s || i.td > t0l;
  737. });
  738. };
  739. // 给所有可能的位置打分,分数是[0, 1)的
  740. var score = function(i) {
  741. if (i.r > maxr) return -Infinity;
  742. return 1 - hypot(i.r / maxr, i.p / hc) * Math.SQRT1_2;
  743. };
  744. // 添加一条
  745. return function(t0s, wv, hv, b) {
  746. var t0l = wc / (wv + wc) * u + t0s;
  747. syn(t0s, t0l);
  748. var al = available(hv, t0s, t0l, b);
  749. if (!al.length) return null;
  750. var scored = al.map(function(i) {
  751. return [score(i),
  752. i
  753. ];
  754. });
  755. var best = scored.reduce(function(x, y) {
  756. return x[0] > y[0] ? x : y;
  757. })[1];
  758. var ts = t0s + best.r;
  759. var tf = wv / (wv + wc) * u + ts;
  760. var td = u + ts;
  761. use(best.p, best.p + hv, tf, td);
  762. return {
  763. 'top': best.p,
  764. 'time': ts,
  765. };
  766. };
  767. };
  768. }(config.playResX, config.playResY, config.bottom, config.r2ltime, config.max_delay));
  769. // 顶部、底部弹幕
  770. var sideDanmaku = (function(hc, b, u, maxr) {
  771. return function() {
  772. var used = [{
  773. 'p': -Infinity,
  774. 'm': 0,
  775. 'td': Infinity,
  776. 'b': false
  777. },
  778. {
  779. 'p': hc,
  780. 'm': Infinity,
  781. 'td': Infinity,
  782. 'b': false
  783. },
  784. {
  785. 'p': hc - b,
  786. 'm': hc,
  787. 'td': Infinity,
  788. 'b': true
  789. },
  790. ];
  791. // 查找可用的位置
  792. var fr = function(p, m, t0s, b) {
  793. var tas = t0s;
  794. used.forEach(function(j) {
  795. if (j.p >= m) return;
  796. if (j.m <= p) return;
  797. if (j.b && b) return;
  798. tas = Math.max(tas, j.td);
  799. });
  800. return {
  801. 'r': tas - t0s,
  802. 'p': p,
  803. 'm': m
  804. };
  805. };
  806. // 顶部
  807. var top = function(hv, t0s, b) {
  808. var suggestion = [];
  809. used.forEach(function(i) {
  810. if (i.m > hc) return;
  811. suggestion.push(fr(i.m, i.m + hv, t0s, b));
  812. });
  813. return suggestion;
  814. };
  815. // 底部
  816. var bottom = function(hv, t0s, b) {
  817. var suggestion = [];
  818. used.forEach(function(i) {
  819. if (i.p < 0) return;
  820. suggestion.push(fr(i.p - hv, i.p, t0s, b));
  821. });
  822. return suggestion;
  823. };
  824. var use = function(p, m, td) {
  825. used.push({
  826. 'p': p,
  827. 'm': m,
  828. 'td': td,
  829. 'b': false
  830. });
  831. };
  832. var syn = function(t0s) {
  833. used = used.filter(function(i) {
  834. return i.td > t0s;
  835. });
  836. };
  837. // 挑选最好的方案:延迟小的优先,位置不重要
  838. var score = function(i, is_top) {
  839. if (i.r > maxr) return -Infinity;
  840. var f = function(p) {
  841. return is_top ? p : (hc - p);
  842. };
  843. return 1 - (i.r / maxr * (31 / 32) + f(i.p) / hc * (1 / 32));
  844. };
  845. return function(t0s, hv, is_top, b) {
  846. syn(t0s);
  847. var al = (is_top ? top : bottom)(hv, t0s, b);
  848. if (!al.length) return null;
  849. var scored = al.map(function(i) {
  850. return [score(i, is_top),
  851. i
  852. ];
  853. });
  854. var best = scored.reduce(function(x, y) {
  855. return x[0] > y[0] ? x : y;
  856. })[1];
  857. use(best.p, best.m, best.r + t0s + u)
  858. return {
  859. 'top': best.p,
  860. 'time': best.r + t0s
  861. };
  862. };
  863. };
  864. }(config.playResY, config.bottom, config.fixtime, config.max_delay));
  865. // 为每条弹幕安置位置
  866. var setPosition = function(danmaku) {
  867. var normal = normalDanmaku(),
  868. side = sideDanmaku();
  869. return danmaku.sort(function(x, y) {
  870. return x.time - y.time;
  871. }).map(function(line) {
  872. var font_size = Math.round(line.size * config.font_size * config.exlinespace);
  873. var width = calcWidth(line.text, Math.round(line.size * config.font_size));
  874. switch (line.mode) {
  875. case 'R2L':
  876. return (function() {
  877. var pos = normal(line.time, width, font_size, line.bottom);
  878. if (!pos) return null;
  879. line.type = 'R2L';
  880. line.stime = pos.time;
  881. line.poss = {
  882. 'x': config.playResX + width / 2,
  883. 'y': pos.top + font_size,
  884. };
  885. line.posd = {
  886. 'x': -width / 2,
  887. 'y': pos.top + font_size,
  888. };
  889. line.dtime = config.r2ltime + line.stime;
  890. return line;
  891. }());
  892. case 'TOP':
  893. case 'BOTTOM':
  894. return (function(isTop) {
  895. var pos = side(line.time, font_size, isTop, line.bottom);
  896. if (!pos) return null;
  897. line.type = 'Fix';
  898. line.stime = pos.time;
  899. line.posd = line.poss = {
  900. 'x': Math.round(config.playResX / 2),
  901. 'y': pos.top + font_size,
  902. };
  903. line.dtime = config.fixtime + line.stime;
  904. return line;
  905. }(line.mode === 'TOP'));
  906. default:
  907. return null;
  908. };
  909. }).filter(function(l) {
  910. return l;
  911. }).sort(function(x, y) {
  912. return x.stime - y.stime;
  913. });
  914. };
  915.  
  916. /*
  917. * 设置面板部分
  918. */
  919. //保存设置
  920. var stringToNumberOrBoolean = function(input) {
  921. var output = parseFloat(input);
  922. if (!isNaN(output)) return output;
  923. output = input.trim().toLowerCase();
  924. if (output === "true") return true;
  925. if (output === "false") return false;
  926. return input;
  927. };
  928. var saveSetting = function(e) {
  929. e.preventDefault();
  930. if (document.querySelector(".assdanmakusetting-container").reportValidity()) {
  931. for (let item of document.querySelector(".assdanmakusetting-container").querySelectorAll("input, select")) {
  932. var parser = stringToNumberOrBoolean;
  933. if ((item.tagName == "INPUT") && (item.type == "text")) {
  934. switch (settingitems?.[item.name]?.datatype) {
  935. case "array":
  936. parser = (input) => input.split(',');
  937. break;
  938. default:
  939. };
  940. };
  941. config[item.name] = parser(item.value);
  942. };
  943. GM_setValue("config", config);
  944. debug = config.debug ? console.log.bind(console) : function() {};
  945. initFont();
  946. alert(l10n.message.configsaved);
  947. };
  948. };
  949. //创建设置面板
  950. var openSettingPanel = function() {
  951. if (document.querySelector(".assdanmakusetting-window") == null) {
  952. document.body.insertAdjacentHTML("beforeend", fillStr(settingPanelHTMLtemplate, {
  953. 'title': l10n.ui.settingpaneltitle,
  954. 'save': l10n.ui.save,
  955. 'exit': l10n.ui.exit
  956. }));
  957. document.querySelector(".assdanmakusetting-savebutton").addEventListener('click', saveSetting);
  958. var container = document.querySelector(".assdanmakusetting-container");
  959. //添加设置项
  960. for (let key in settingitems) {
  961. var item = settingitems[key];
  962. var content = "";
  963. var attr = "";
  964. switch (item.htmltag) {
  965. case "input":
  966. for (let i of item.exattr) {
  967. attr = attr + i[0] + '="' + i[1] +'" ';
  968. };
  969. attr = attr + `type="${item.type}"`;
  970. attr = attr + `value="${config[key]}"`;//设置为当前值, 注意toString后值的变化
  971. content = fillStr(settingInputHTMLtemplate, {
  972. 'key': key,
  973. 'butsetdef': l10n.ui.defaultvalue,
  974. 'butsetcur': l10n.ui.currenttvalue,
  975. 'butsetdeftitle': l10n.ui.butsetdeftitle,
  976. 'butsetcurtitle': l10n.ui.butsetcurtitle,
  977. 'htmltag': "input",
  978. 'attr': attr,
  979. 'content': "",
  980. 'description': l10n.config.description[key]
  981. });
  982. container.insertAdjacentHTML("beforeend", fillStr(settingItemHTMLtemplate, {content: content}));
  983. break;
  984. case "select":
  985. var options = "";
  986. for (let option of item.options) {
  987. options = options + fillStr(settingOptionsHTMLtemplate, {
  988. 'value': option[0],
  989. 'text': option[1],
  990. });
  991. };
  992. for (let i of item.exattr) {
  993. attr = attr + i[0] + '="' + i[1] +'" ';
  994. };
  995. content = fillStr(settingInputHTMLtemplate, {
  996. 'key': key,
  997. 'butsetdef': l10n.ui.defaultvalue,
  998. 'butsetcur': l10n.ui.currenttvalue,
  999. 'butsetdeftitle': l10n.ui.butsetdeftitle,
  1000. 'butsetcurtitle': l10n.ui.butsetcurtitle,
  1001. 'htmltag': "select",
  1002. 'attr': attr,
  1003. 'content': options,
  1004. 'description': l10n.config.description[key]
  1005. });
  1006. container.insertAdjacentHTML("beforeend", fillStr(settingItemHTMLtemplate, {content: content}));
  1007. container.lastElementChild.querySelector("select").value = config[key];//设置为当前值, 注意toString后值的变化
  1008. break;
  1009. default:
  1010. };
  1011. };
  1012. //最后几个按钮
  1013. container.insertAdjacentHTML("beforeend", fillStr(settingItemHTMLtemplate, {
  1014. 'content': fillStr(last3ButtonHTMLtemplate, {
  1015. 'executecode': l10n.ui.executecode,
  1016. 'editconfig': l10n.ui.editconfig,
  1017. 'resetconfig': l10n.ui.resetconfig,
  1018. 'title': l10n.ui.misc
  1019. })
  1020. }));
  1021. //添加事件监听器
  1022. for (let node of container.getElementsByTagName("button")) {
  1023. node.addEventListener("click", settingPanelButtonListener);
  1024. };
  1025. for (let node of container.getElementsByTagName("select")) {
  1026. node.addEventListener("wheel", settingPanelSelectScrollListener);
  1027. };
  1028. for (let node of container.querySelectorAll(`input[type="text"]`)) {
  1029. node.addEventListener("input", settingPanelTextCheckerListener);
  1030. };
  1031. };
  1032. };
  1033. //设置项按钮处理
  1034. var settingPanelButtonListener = function(e) {
  1035. e.preventDefault();
  1036. switch (e.currentTarget.getAttribute("act")) {
  1037. //旁边的两个用于把值设置为默认值/当前值的按钮, 注意值toString后的变化
  1038. case "setval":
  1039. var inputelement = e.currentTarget.parentElement.querySelectorAll("input, select")[0];
  1040. switch (e.currentTarget.getAttribute("acp")) {
  1041. case "def":
  1042. inputelement.value = defconfig?.[inputelement.name];
  1043. break;
  1044. case "cur":
  1045. inputelement.value = config?.[inputelement.name];
  1046. break;
  1047. default:
  1048. };
  1049. break;
  1050. //最下面几个按钮
  1051. case "resetconfig":
  1052. if (confirm(l10n.message.confirmresetconfig)) {
  1053. GM_setValue("config", null);
  1054. location.reload();
  1055. };
  1056. break;
  1057. case "editconfig":
  1058. var input = prompt(l10n.message.editconfig, JSON.stringify(config));
  1059. if (input !== null) {
  1060. config = JSON.parse(input);
  1061. GM_setValue("config", config);
  1062. debug = config.debug ? console.log.bind(console) : function() {};
  1063. initFont();
  1064. alert(l10n.message.configsaved);
  1065. };
  1066. break;
  1067. case "executecode":
  1068. eval(prompt());
  1069. break;
  1070. default:
  1071. };
  1072. };
  1073. //设置项select滚轮处理
  1074. var settingPanelSelectScrollListener = function(e) {
  1075. e.preventDefault();
  1076. if (e.deltaY > 0) {
  1077. if (e.currentTarget.selectedIndex < (e.currentTarget.options.length - 1)) {
  1078. e.currentTarget.selectedIndex++;
  1079. } else {
  1080. e.currentTarget.selectedIndex = 0;
  1081. };
  1082. } else if (e.deltaY < 0) {
  1083. if (e.currentTarget.selectedIndex > 0) {
  1084. e.currentTarget.selectedIndex--;
  1085. } else {
  1086. e.currentTarget.selectedIndex = e.currentTarget.options.length - 1;
  1087. };
  1088. };
  1089. };
  1090. //设置项text验证处理
  1091. var settingPanelTextCheckerListener = function(e) {
  1092. e.currentTarget.reportValidity();
  1093. };
  1094. /*
  1095. * 下载部分
  1096. */
  1097. //发送请求
  1098. var sendXHR = function(url, method, data, callback) {
  1099. var xhr = new XMLHttpRequest();
  1100. xhr.onreadystatechange = () => {
  1101. if (xhr.readyState === 4) {
  1102. if (xhr.status === 200) callback(xhr.response);
  1103. else alert(l10n.message.xhrfailed + xhr.status);
  1104. }
  1105. };
  1106. xhr.open(method, url);
  1107. xhr.setRequestHeader("Cookie", document.cookie);
  1108. xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
  1109. xhr.send(data);
  1110. };
  1111. //将弹幕转换为ASS并保存
  1112. var nmina = function(dm, filename, exinfo) {
  1113. var danmaku = dm.map(function(line) {
  1114. return {
  1115. 'text': line.text,
  1116. 'time': line.time / 10,
  1117. 'color': line.color.substr(1),
  1118. 'mode': ['R2L', 'TOP', 'BOTTOM'][line.position],
  1119. 'size': Math.pow(1.5, (line.size - 1)),//line.size * 0.5 +0.5,
  1120. 'bottom': false,
  1121. 'sender': line.userid,
  1122. // 'create': new Date(Number(info[5]) * 1000),
  1123. // 'danmakuid': info[6], // format as uuid
  1124. };
  1125. });
  1126. debug(l10n.message.gotdanmaku, danmaku.length, exinfo.ori);
  1127. var ass = generateASS(setPosition(danmaku), exinfo);
  1128. startDownload('' + ass, filename + '.ass');
  1129. };
  1130. //从SN号获取弹幕
  1131. var downloadDanmakuformSN = function(inputstr, filename, exinfo) {
  1132. var snid = /^\d+$/.test(inputstr) ? inputstr : inputstr.match(/((?<=\?sn=)\d+)|((?<=sn)\d+)/)?.at(0);
  1133. if (snid == null) { alert(l10n.message.invalidsnid); debug(l10n.message.invalidsnidlog + inputstr); return; };
  1134. var fname = null;
  1135. var info1 = { 'ori': "https://ani.gamer.com.tw/animeVideo.php?sn=" + snid };
  1136. var gettitle = function(resp) {
  1137. try {
  1138. info1.title = (new DOMParser()).parseFromString(resp, "text/html").title;
  1139. }
  1140. catch (e) { debug(e) };
  1141. sendXHR("https://ani.gamer.com.tw/ajax/danmuGet.php", "POST", "sn=" + snid.toString(10), getdanmu);
  1142. };
  1143. var getdanmu = function(resp) {
  1144. Object.assign(info1,exinfo);
  1145. try {
  1146. fname = info1.title.replace("線上看 - 巴哈姆特動畫瘋", "[Baha]");
  1147. }
  1148. catch (e) { debug(e) };
  1149. nmina(JSON.parse(resp), (filename == null || filename === "") ? fname : filename, info1);
  1150. };
  1151. //如果没有标题则获取标题
  1152. if (exinfo?.hasOwnProperty("title")) sendXHR("https://ani.gamer.com.tw/ajax/danmuGet.php", "POST", "sn=" + snid.toString(10), getdanmu);
  1153. else sendXHR("https://ani.gamer.com.tw/animeVideo.php?sn=" + snid, "GET", null, gettitle);
  1154. };
  1155. /*
  1156. * 页面部分
  1157. */
  1158. //下载按钮事件监听器
  1159. var DLButtonListener = function(e) {
  1160. e.preventDefault();
  1161. downloadDanmakuformSN(e.currentTarget.getAttribute("snid"));
  1162. };
  1163. //从列表生成下载按钮
  1164. var genDLButtonformList = function(snlist) {
  1165. var bs = document.createElement("div");
  1166. bs.insertAdjacentHTML("afterbegin", fillStr(`<p style="font-size:1.2em;">{{text}}</p>`, { 'text': l10n.text.episodelistgetdanmaku }));
  1167. for (let node of snlist) {
  1168. if (node.sn === "---") {
  1169. var p = document.createElement("p");
  1170. p.textContent = node.text;
  1171. bs.appendChild(p);
  1172. } else {
  1173. var b = document.createElement("button");
  1174. b.className = "listdownloadbutton";
  1175. b.textContent = node.text;
  1176. b.setAttribute("snid", node.sn);
  1177. b.addEventListener('click', DLButtonListener);
  1178. bs.appendChild(b);
  1179. };
  1180. };
  1181. return bs;
  1182. };
  1183. //添加播放页面的按钮
  1184. var initNewButton = function() {
  1185. //当前集数的按钮
  1186. const b = document.createElement("button");
  1187. b.textContent = l10n.text.getdanmaku;
  1188. b.addEventListener('click', e => {
  1189. e.preventDefault();
  1190. var fname = null;
  1191. try {
  1192. fname = document.title.replace("線上看 - 巴哈姆特動畫瘋", "[Baha]");
  1193. }
  1194. catch (e) {};
  1195. downloadDanmakuformSN(location.href, fname, {
  1196. 'title': document.title,
  1197. 'ori': location.href
  1198. });
  1199. });
  1200. b.className = "ahveuiw";
  1201. document.querySelector(".anime_name").appendChild(b);
  1202. //剧集列表的按钮
  1203. var episodelist = new Array();
  1204. for (let node1 of document.querySelector(".season").children) {
  1205. if (node1?.tagName.toLowerCase() == "p") {
  1206. episodelist.push({
  1207. 'sn': "---",
  1208. 'text': node1.textContent
  1209. });
  1210. };
  1211. if (node1?.tagName.toLowerCase() == "ul") {
  1212. for (let node2 of node1.children) {
  1213. episodelist.push({
  1214. 'sn': node2.querySelector("a").getAttribute("href"),
  1215. 'text': node2.querySelector("a").textContent
  1216. });
  1217. };
  1218. };
  1219. };
  1220. document.querySelector(".season").appendChild(genDLButtonformList(episodelist));
  1221. };
  1222. //添加新番列表的按钮
  1223. var initNewanimelistButton = function() {
  1224. for (let node of document.querySelector(".newanime-block").children) {
  1225. try {
  1226. if (node.querySelector(".anime-card-block") == null) continue;
  1227. var b = document.createElement("div");
  1228. //debug(node);
  1229. b.className = "listgetassdanmaku-button";
  1230. b.textContent = l10n.text.getdanmaku;
  1231. b.setAttribute("snid", node.querySelector(".anime-card-block").getAttribute("href"));
  1232. b.addEventListener('click', DLButtonListener);
  1233. var ae = node.querySelector(".anime-episode");
  1234. if (ae == null) { ae = document.createElement("div"); ae.className = "anime-episode"; node.querySelector("anime-detail-info-block").appendChild(ae); }
  1235. ae.insertAdjacentHTML("beforeend", "<p>&nbsp;</p>");
  1236. ae.appendChild(b);
  1237. }
  1238. catch (e) { debug(e) };
  1239. };
  1240. };
  1241. /*
  1242. * Common
  1243. */
  1244. // 初始化
  1245. var init = function() {
  1246. Object.assign(l10n, lang?.[config.language]);
  1247. GM_registerMenuCommand(l10n.ui.getdanmakuformsn, function(){downloadDanmakuformSN(prompt(l10n.message.getdanmakuformsn));});
  1248. GM_registerMenuCommand(l10n.ui.setting, openSettingPanel);
  1249. GM_addStyle(injectedCSS);
  1250. initFont();
  1251. if (document.querySelector(".anime-detail-info-block") != null) initNewanimelistButton();
  1252. if (document.querySelector(".container-player") != null) initNewButton();
  1253. };
  1254. if (document.body) init();
  1255. else window.addEventListener('DOMContentLoaded', init);