VoiceLinks-适配本地

Makes RJ codes more useful.(8-bit RJCode supported.)

  1. // ==UserScript==
  2. // @name VoiceLinks-适配本地
  3. // @namespace Sanya
  4. // @description Makes RJ codes more useful.(8-bit RJCode supported.)
  5. // @match *://*/*
  6. // @match file:///*
  7. // @version 4.8.10
  8. // @connect dlsite.com
  9. // @connect media.ci-en.jp
  10. // @connect 127.0.0.1
  11. // @grant GM_setClipboard
  12. // @grant GM_openInTab
  13. // @grant GM_registerMenuCommand
  14. // @grant GM_setValue
  15. // @grant GM_getValue
  16. // @grant GM_addElement
  17. // @grant GM.xmlHttpRequest
  18. // @grant GM_xmlhttpRequest
  19. // @run-at document-start
  20. // @homepage https://sleazyfork.org/zh-CN/scripts/456775-voicelinks
  21. // ==/UserScript==
  22.  
  23. (function () {
  24. 'use strict';
  25.  
  26. const IS_PREVIEW = false;
  27.  
  28. //------持久化设置项------
  29. let settings = {
  30. //语言设置
  31. _s_lang: "zh_CN",
  32. _s_popup_lang: "zh_CN",
  33.  
  34. //常规设置
  35. _s_parse_url: true,
  36. _s_parse_url_in_dl: false,
  37. _s_show_translated_title_in_dl: true,
  38. _s_copy_as_filename_btn: true,
  39. _s_show_compatibility_warning: true,
  40. _s_url_insert_mode: "before_rj",
  41. _s_url_insert_text: "🔗",
  42. _s_sfw_mode: false,
  43. _s_sfw_blur_level: "medium",
  44. _s_sfw_remove_when_hover: true,
  45. _s_sfw_blur_transition: true,
  46.  
  47. //信息显示设置
  48. _s_category_preset: "voice",
  49. _s_voice__info_display_order: [
  50. "voice__dl_count",
  51. "voice__circle_name",
  52. "voice__translator_name",
  53. "voice__release_date",
  54. "voice__update_date",
  55. "voice__age_rating",
  56. "voice__scenario",
  57. "voice__illustration",
  58. "voice__voice_actor",
  59. "voice__music",
  60. "voice__genre",
  61. "voice__file_size"
  62. ],
  63. _s_voice__dl_count: false,
  64. _s_voice__circle_name: true,
  65. _s_voice__translator_name: true,
  66. _s_voice__release_date: true,
  67. _s_voice__update_date: true,
  68. _s_voice__age_rating: true,
  69. _s_voice__scenario: false,
  70. _s_voice__illustration: false,
  71. _s_voice__voice_actor: true,
  72. _s_voice__music: true,
  73. _s_voice__genre: true,
  74. _s_voice__file_size: true,
  75. _s_game__info_display_order: [
  76. "game__dl_count",
  77. "game__circle_name",
  78. "game__translator_name",
  79. "game__release_date",
  80. "game__update_date",
  81. "game__age_rating",
  82. "game__scenario",
  83. "game__illustration",
  84. "game__voice_actor",
  85. "game__music",
  86. "game__genre",
  87. "game__file_size"
  88. ],
  89. _s_game__dl_count: false,
  90. _s_game__circle_name: true,
  91. _s_game__translator_name: true,
  92. _s_game__release_date: true,
  93. _s_game__update_date: true,
  94. _s_game__age_rating: true,
  95. _s_game__scenario: true,
  96. _s_game__illustration: true,
  97. _s_game__voice_actor: true,
  98. _s_game__music: true,
  99. _s_game__genre: true,
  100. _s_game__file_size: true,
  101. _s_manga__info_display_order:[
  102. "manga__dl_count",
  103. "manga__circle_name",
  104. "manga__translator_name",
  105. "manga__release_date",
  106. "manga__update_date",
  107. "manga__age_rating",
  108. "manga__scenario",
  109. "manga__illustration",
  110. "manga__voice_actor",
  111. "manga__music",
  112. "manga__genre",
  113. "manga__file_size"
  114. ],
  115. _s_manga__dl_count: false,
  116. _s_manga__circle_name: true,
  117. _s_manga__translator_name: true,
  118. _s_manga__release_date: true,
  119. _s_manga__update_date: true,
  120. _s_manga__age_rating: true,
  121. _s_manga__scenario: true,
  122. _s_manga__illustration: true,
  123. _s_manga__voice_actor: true, //音声漫画
  124. _s_manga__music: true,
  125. _s_manga__genre: true,
  126. _s_manga__file_size: true,
  127. _s_video__info_display_order: [
  128. "video__dl_count",
  129. "video__circle_name",
  130. "video__translator_name",
  131. "video__release_date",
  132. "video__update_date",
  133. "video__age_rating",
  134. "video__scenario",
  135. "video__illustration",
  136. "video__voice_actor",
  137. "video__music",
  138. "video__genre",
  139. "video__file_size"
  140. ],
  141. _s_video__dl_count: false,
  142. _s_video__circle_name: true,
  143. _s_video__translator_name: true,
  144. _s_video__release_date: true,
  145. _s_video__update_date: true,
  146. _s_video__age_rating: true,
  147. _s_video__scenario: true,
  148. _s_video__illustration: true,
  149. _s_video__voice_actor: true,
  150. _s_video__music: true,
  151. _s_video__genre: true,
  152. _s_video__file_size: true,
  153. _s_novel__info_display_order: [
  154. "novel__dl_count",
  155. "novel__circle_name",
  156. "novel__translator_name",
  157. "novel__release_date",
  158. "novel__update_date",
  159. "novel__age_rating",
  160. "novel__scenario",
  161. "novel__illustration",
  162. "novel__voice_actor",
  163. "novel__music",
  164. "novel__genre",
  165. "novel__file_size"
  166. ],
  167. _s_novel__dl_count: false,
  168. _s_novel__circle_name: true,
  169. _s_novel__translator_name: true,
  170. _s_novel__release_date: true,
  171. _s_novel__update_date: true,
  172. _s_novel__age_rating: true,
  173. _s_novel__scenario: true,
  174. _s_novel__illustration: true,
  175. _s_novel__voice_actor: false,
  176. _s_novel__music: false,
  177. _s_novel__genre: true,
  178. _s_novel__file_size: true,
  179. _s_other__info_display_order: [
  180. "other__dl_count",
  181. "other__circle_name",
  182. "other__translator_name",
  183. "other__release_date",
  184. "other__update_date",
  185. "other__age_rating",
  186. "other__scenario",
  187. "other__illustration",
  188. "other__voice_actor",
  189. "other__music",
  190. "other__genre",
  191. "other__file_size"
  192. ],
  193. _s_other__dl_count: false,
  194. _s_other__circle_name: true,
  195. _s_other__translator_name: true,
  196. _s_other__release_date: true,
  197. _s_other__update_date: true,
  198. _s_other__age_rating: true,
  199. _s_other__scenario: true,
  200. _s_other__illustration: true,
  201. _s_other__voice_actor: true,
  202. _s_other__music: true,
  203. _s_other__genre: true,
  204. _s_other__file_size: true,
  205.  
  206. //标签显示设置
  207. _s_tag_main_switch: true,
  208. _s_tag_display_order: [
  209. "tag_no_longer_available",
  210. "tag_rate",
  211. "tag_work_type",
  212. "tag_translatable",
  213. "tag_not_translatable",
  214. "tag_translated",
  215. "tag_bonus_work",
  216. "tag_has_bonus",
  217. "tag_language_support",
  218. "tag_file_format",
  219. "tag_ai",
  220. ],
  221. _s_tag_rate: true,
  222. _s_tag_work_type: true,
  223. _s_tag_translatable: true,
  224. _s_tag_not_translatable: true,
  225. _s_tag_translated: true,
  226. _s_tag_language_support: true,
  227. _s_tag_bonus_work: true,
  228. _s_tag_has_bonus: true,
  229. _s_tag_file_format: false,
  230. _s_tag_no_longer_available: true,
  231. _s_tag_ai: true,
  232.  
  233. _s_show_rate_count: false,
  234.  
  235. _s_tag_translation_request: true,
  236. _s_tag_translation_request_display_order: [
  237. "tag_translation_request_simplified_chinese",
  238. "tag_translation_request_traditional_chinese",
  239. "tag_translation_request_english",
  240. "tag_translation_request_korean",
  241. "tag_translation_request_spanish",
  242. "tag_translation_request_german",
  243. "tag_translation_request_french",
  244. "tag_translation_request_indonesian",
  245. "tag_translation_request_italian",
  246. "tag_translation_request_portuguese",
  247. "tag_translation_request_swedish",
  248. "tag_translation_request_thai",
  249. "tag_translation_request_vietnamese",
  250. ],
  251. _s_tag_translation_request_english: false,
  252. _s_tag_translation_request_simplified_chinese: true,
  253. _s_tag_translation_request_traditional_chinese: true,
  254. _s_tag_translation_request_korean: false,
  255. _s_tag_translation_request_spanish: false,
  256. _s_tag_translation_request_german: false,
  257. _s_tag_translation_request_french: false,
  258. _s_tag_translation_request_indonesian: false,
  259. _s_tag_translation_request_italian: false,
  260. _s_tag_translation_request_portuguese: false,
  261. _s_tag_translation_request_swedish: false,
  262. _s_tag_translation_request_thai: false,
  263. _s_tag_translation_request_vietnamese: false,
  264.  
  265. backup: function() {
  266. let backup = {};
  267. for (let key in this) {
  268. if(!key.startsWith("_s_")) continue;
  269. //如果类型为列表,则需要将其拷贝出来
  270. if(this[key] && Array.isArray(this[key])){
  271. backup[key] = [...this[key]];
  272. }else{
  273. backup[key] = this[key];
  274. }
  275. }
  276. this.default_backup = backup;
  277. },
  278.  
  279. //备份默认值
  280. default_backup: {},
  281.  
  282. //暂存已修改值,不更新到设置
  283. temp_edited: {},
  284.  
  285. load: function(){
  286. let need_reorder = false;
  287. for(let key in this){
  288. if(!key.startsWith("_s_")) continue;
  289. let val = GM_getValue(key.substring(3), this[key]);
  290. if(typeof val !== typeof this[key]){
  291. val = this[key];
  292. }
  293. if(Array.isArray(val) && val.length !== this[key].length){
  294. val = this[key];
  295. need_reorder = true;
  296. }
  297. this[key] = val !== undefined ? val : this[key];
  298. }
  299.  
  300. if(need_reorder) {
  301. window.addEventListener("load", () => {
  302. window.alert("VoiceLinks: " + localize(localizationMap.need_reorder));
  303. });
  304. this.save();
  305. }
  306. },
  307.  
  308. save: function () {
  309. //将暂存修改应用至Settings
  310. for (let key in this.temp_edited) {
  311. if(!key.startsWith("_s_")) continue;
  312. if(this[key] === undefined || this.temp_edited[key] === undefined) continue;
  313. this[key] = this.temp_edited[key];
  314. this.temp_edited[key] = undefined;
  315. }
  316.  
  317. //将修改保存至GM
  318. for(let key in this){
  319. if(!key.startsWith("_s_")) continue;
  320. GM_setValue(key.substring(3), this[key]);
  321. }
  322. },
  323.  
  324. //保存临时修改
  325. saveTemp: function (key, value){
  326. if(!key.startsWith("_s_")) key = "_s_" + key;
  327. this.temp_edited[key] = value;
  328. },
  329.  
  330. clearTemp: function (){
  331. this.temp_edited = {};
  332. },
  333.  
  334. reset: function () {
  335. if(!this.default_backup) return;
  336. for(let key in this.default_backup){
  337. if(!key.startsWith("_s_")) continue;
  338. GM_setValue(key.substring(3), this.default_backup[key]);
  339. }
  340. },
  341.  
  342. hasEdited: function (key) {
  343. if(!key.startsWith("_s_")) key = "_s_" + key;
  344. if(this[key] === undefined) return false;
  345. return this[key] !== this.default_backup[key];
  346. },
  347.  
  348. getDefaultValue: function (key) {
  349. if(!key.startsWith("_s_")) key = "_s_" + key;
  350. return this.default_backup[key];
  351. }
  352. }
  353. settings.backup();
  354. settings.load();
  355. //----------------------
  356.  
  357. //------本地化-----------
  358. const localizationMap = {
  359. notice_update: {
  360. zh_CN: "VoiceLinks公告更新,可能包含重要的新功能说明,是否跳转至说明页面?",
  361. zh_TW: "VoiceLinks公告更新,可能包含重要的新功能説明,是否跳轉至説明頁面?",
  362. en_US: "VoiceLinks Notice Update, may contain important new features, do you want to jump to the notice page?"
  363. },
  364.  
  365. title_settings: {
  366. zh_CN: "VoiceLinks 设置",
  367. zh_TW: "VoiceLinks 設定",
  368. en_US: "VoiceLinks Settings"
  369. },
  370.  
  371. title_language_settings: {
  372. zh_CN: "语言设置",
  373. zh_TW: "語言設定",
  374. en_US: "Language Settings"
  375. },
  376.  
  377. display_language: {
  378. zh_CN: "显示语言",
  379. zh_TW: "顯示語言",
  380. en_US: "Language"
  381. },
  382.  
  383. popup_language: {
  384. zh_CN: "弹窗语言",
  385. zh_TW: "彈窗語言",
  386. en_US: "Popup Language"
  387. },
  388.  
  389. popup_language_tooltip: {
  390. zh_CN: "仅修改标题和标签显示语言,信息本身的语言以DLSite网页设置的语言为准。",
  391. zh_TW: "只修改標題和標籤顯示語言,資訊本身的語言以DLSite網頁設定的語言為準。",
  392. en_US: "Only modify the title and tag display language, the language of the information itself is determined by the language of the DLSite page settings."
  393. },
  394.  
  395. title_general_settings: {
  396. zh_CN: "常规",
  397. zh_TW: "常規",
  398. en_US: "General"
  399. },
  400.  
  401. parse_url: {
  402. zh_CN: "解析URL",
  403. zh_TW: "解析URL",
  404. en_US: "Parse URL"
  405. },
  406.  
  407. parse_url_tooltip: {
  408. zh_CN: "鼠标悬停导指向DLSite作品页面的URL时,同样显示作品信息",
  409. zh_TW: "鼠標懸停導向DLSite作品頁面的URL時,同樣顯示作品資訊",
  410. en_US: "Show work info when hovering over DLSite work URL"
  411. },
  412.  
  413. parse_url_in_dl: {
  414. zh_CN: "在DLSite上解析URL",
  415. zh_TW: "在DLSite上解析URL",
  416. en_US: "Parse URL in DLSite"
  417. },
  418.  
  419. parse_url_in_dl_tooltip: {
  420. zh_CN: "URL较多可能影响正常阅读",
  421. zh_TW: "URL較多可能影響正常閱讀",
  422. en_US: "URL is more likely to affect normal reading"
  423. },
  424.  
  425. show_translated_title_in_dl: {
  426. zh_CN: "在DLSite显示对应语言的翻译标题",
  427. zh_TW: "在DLSite顯示對應語言的翻譯標題",
  428. en_US: "Show translated title in DLSite"
  429. },
  430.  
  431. show_translated_title_in_dl_tooltip: {
  432. zh_CN: "作品信息页面的标题将会被修改为与翻译语言对应的标题,避免简中看繁中作品标题为日文的问题",
  433. zh_TW: "作品資訊頁面的標題將會被修改為與翻譯語言對應的標題,避免繁中看簡中作品標題為日文的問題",
  434. en_US: "The title of the work info page will be modified to match the corresponding translation language, to avoid viewing the title as Japanese when viewing a work in non-English language."
  435. },
  436.  
  437. copy_as_filename_btn: {
  438. zh_CN: "“复制为有效文件名”按钮",
  439. zh_TW: "“複製為有效檔案名”按鈕",
  440. en_US: '"Copy as filename" button'
  441. },
  442.  
  443. copy_as_filename_btn_tooltip: {
  444. zh_CN: "鼠标悬停至DLSite作品标题部分将会出现该按钮,点击即可将标题复制为有效文件名,有效文件名指的是会将标题中的非法部分用相似的符号代替",
  445. zh_TW: "鼠標懸停至DLSite作品標題部分將會出現按鈕,點擊即可將標題複製為有效檔案名,有效檔案名指的是會將標題中的非法部分用相似的符號代替",
  446. en_US: "Show button when hovering over DLSite work title. Clicking it will copy the title to a valid filename, which will replace the illegal part of the title with similar symbols."
  447. },
  448.  
  449. show_compatibility_warning: {
  450. zh_CN: "显示兼容性警告",
  451. zh_TW: "顯示兼容性警告",
  452. en_US: "Show compatibility warning"
  453. },
  454.  
  455. show_compatibility_warning_tooltip: {
  456. zh_CN: "如果脚本中,修改DLSite页面元素的功能覆盖了其它脚本的修改,则会触发该弹窗警告",
  457. zh_TW: "如果腳本中,修改DLSite頁面元素的功能覆蓋了其它腳本的修改,则會觸發該彈窗警告",
  458. en_US: "If the script modifies the functionality of DLSite elements that are covered by other scripts, the warning will be triggered"
  459. },
  460.  
  461. url_insert_mode: {
  462. zh_CN: "导向文本的插入方式",
  463. zh_TW: "導向文本的插入方式",
  464. en_US: "Type of the insertion"
  465. },
  466.  
  467. url_insert_mode_tooltip: {
  468. zh_CN: "如果某段链接中的RJ号被解析成功,为了保证原链接不被完全覆盖,会根据需要,在URL的文本前/后插入特定导向文本",
  469. zh_TW: "如果某段連結中的RJ號被解析成功,為了保證原連結不被完全覆蓋,會根據需要,在URL的文本前/後插入特定導向文本",
  470. en_US: "If the RJ number in a link is parsed successfully, it is necessary to insert a specific text in the URL before/after the link when the link is almost completely covered by the script"
  471. },
  472.  
  473. url_insert_mode_none: {
  474. zh_CN: "不插入",
  475. zh_TW: "不插入",
  476. en_US: "None"
  477. },
  478.  
  479. url_insert_mode_prefix: {
  480. zh_CN: "前缀插入代替原链接",
  481. zh_TW: "前綴插入代替原連結",
  482. en_US: "Insert before the link as original link."
  483. },
  484.  
  485. url_insert_mode_before_rj: {
  486. zh_CN: "插入到RJ号前代替RJ链接",
  487. zh_TW: "插入到RJ號前代替RJ連結",
  488. en_US: "Insert before the RJ link as the RJ link."
  489. },
  490.  
  491. url_insert_text: {
  492. zh_CN: "导向文本",
  493. zh_TW: "導向文本",
  494. en_US: "Text to insert"
  495. },
  496.  
  497. sfw_mode: {
  498. zh_CN: "SFW 模式",
  499. zh_TW: "SFW 模式",
  500. en_US: "SFW Mode"
  501. },
  502.  
  503. sfw_mode_tooltip: {
  504. zh_CN: "启用后,所有作品封面均会模糊处理(固定窗口时将鼠标移动到图片上可临时去除模糊)",
  505. zh_TW: "啟用後,所有作品封面均會模糊處理(固定視窗時將滑鼠移動到圖片上可暫時去除模糊)",
  506. en_US: "Turn on to blur the cover of all works (temporarily remove the blur by moving the mouse over the image when the window is fixed)."
  507. },
  508.  
  509. sfw_blur_level: {
  510. zh_CN: "模糊程度",
  511. zh_TW: "模糊程度",
  512. en_US: "Blur level"
  513. },
  514.  
  515. sfw_remove_when_hover: {
  516. zh_CN: "鼠标移到图片上时移除模糊",
  517. zh_TW: "滑鼠移到圖片上時移除模糊",
  518. en_US: "Remove the blur when the mouse moves over the image"
  519. },
  520.  
  521. sfw_blur_transition: {
  522. zh_CN: "模糊动画(卡顿请关闭)",
  523. zh_TW: "模糊動畫(卡頓請關閉)",
  524. en_US: "Blur animation"
  525. },
  526.  
  527. low: {
  528. zh_CN: "低 - 仅模糊细节",
  529. zh_TW: "低 - 僅模糊細節",
  530. en_US: "Low - Only blur details"
  531. },
  532.  
  533. medium: {
  534. zh_CN: "中 - 依稀可见",
  535. zh_TW: "中 - 依稀可見",
  536. en_US: "Medium - Hard to see"
  537. },
  538.  
  539. high: {
  540. zh_CN: "高 - 完全无法辨认",
  541. zh_TW: "高 - 完全無法辨識",
  542. en_US: "High - Unrecognizable"
  543. },
  544.  
  545. title_info_settings: {
  546. zh_CN: "信息显示",
  547. zh_TW: "信息顯示",
  548. en_US: "Info Display"
  549. },
  550.  
  551. category_preset: {
  552. zh_CN: "类别预设",
  553. zh_TW: "類別預設",
  554. en_US: "Category Preset"
  555. },
  556.  
  557. category_preset_tooltip: {
  558. zh_CN: "使不同类别的作品根据需要显示不同的信息<br/><br/>注意:即使勾选了显示,若作品中不存在该信息则也会隐藏。",
  559. zh_TW: "使不同類別的作品根據需要顯示不同的信息<br/><br/>注意:即使勾選了顯示,若作品中不存在該信息則也會隱藏。",
  560. en_US: "Show the information of different categories of works. <br/><br/>Note: even if checked, the information of a work that does not exist will be hidden."
  561. },
  562.  
  563. rate: {
  564. zh_CN: "评分",
  565. zh_TW: "評分",
  566. en_US: "Rate"
  567. },
  568.  
  569. rate_tooltip: {
  570. zh_CN: "星数★ (评分人数 (设置开启))",
  571. zh_TW: "星數★ (評分人數 (設置開啟))",
  572. en_US: "Star★ (number of ratings (enable in settings))"
  573. },
  574.  
  575. dl_count: {
  576. zh_CN: "销量",
  577. zh_TW: "銷量",
  578. en_US: "Sales"
  579. },
  580.  
  581. circle_name: {
  582. zh_CN: "社团名",
  583. zh_TW: "社團名",
  584. en_US: "Circle Name"
  585. },
  586.  
  587. translator_name: {
  588. zh_CN: "翻译者",
  589. zh_TW: "翻譯者",
  590. en_US: "Translator"
  591. },
  592.  
  593. release_date: {
  594. zh_CN: "发售日",
  595. zh_TW: "發售日",
  596. en_US: "Release Date"
  597. },
  598.  
  599. update_date: {
  600. zh_CN: "更新日",
  601. zh_TW: "更新日",
  602. en_US: "Update Date"
  603. },
  604.  
  605. age_rating: {
  606. zh_CN: "年龄指定",
  607. zh_TW: "年齡指定",
  608. en_US: "Age Rating"
  609. },
  610.  
  611. scenario: {
  612. zh_CN: "剧情",
  613. zh_TW: "劇情",
  614. en_US: "Scenario"
  615. },
  616.  
  617. illustration: {
  618. zh_CN: "插画",
  619. zh_TW: "插圖",
  620. en_US: "Illustration"
  621. },
  622.  
  623. voice_actor: {
  624. zh_CN: "声优",
  625. zh_TW: "聲優",
  626. en_US: "Voice Actor"
  627. },
  628.  
  629. music: {
  630. zh_CN: "音乐",
  631. zh_TW: "音樂",
  632. en_US: "Music"
  633. },
  634.  
  635. genre: {
  636. zh_CN: "分类",
  637. zh_TW: "分類",
  638. en_US: "Genre"
  639. },
  640.  
  641. file_size: {
  642. zh_CN: "文件容量",
  643. zh_TW: "檔案容量",
  644. en_US: "File Size"
  645. },
  646.  
  647. title_tag_settings: {
  648. zh_CN: "标签显示",
  649. zh_TW: "標籤顯示",
  650. en_US: "Tag Display"
  651. },
  652.  
  653. tag_main_switch: {
  654. zh_CN: "标签总开关",
  655. zh_TW: "標籤總開關",
  656. en_US: "Tag Main Switch"
  657. },
  658.  
  659. tag_main_switch_tooltip: {
  660. zh_CN: "关闭则所有标签均不显示",
  661. zh_TW: "關閉則所有標籤都不顯示",
  662. en_US: "If turned off, all tags will not be displayed"
  663. },
  664.  
  665. tag_work_type: {
  666. zh_CN: "作品类型",
  667. zh_TW: "作品類型",
  668. en_US: "Work Type"
  669. },
  670.  
  671. work_type_game: {
  672. zh_CN: "游戏",
  673. zh_TW: "遊戲",
  674. en_US: "Game"
  675. },
  676.  
  677. work_type_comic: {
  678. zh_CN: "漫画",
  679. zh_TW: "漫畫",
  680. en_US: "Manga"
  681. },
  682.  
  683. work_type_illustration: {
  684. zh_CN: "CG・插画",
  685. zh_TW: "CG・插畫",
  686. en_US: "CG + Illustrations"
  687. },
  688.  
  689. work_type_novel: {
  690. zh_CN: "小说",
  691. zh_TW: "小說",
  692. en_US: "Novel"
  693. },
  694.  
  695. work_type_video: {
  696. zh_CN: "视频",
  697. zh_TW: "影片",
  698. en_US: "Video"
  699. },
  700.  
  701. work_type_voice: {
  702. zh_CN: "音声・ASMR",
  703. zh_TW: "聲音作品・ASMR",
  704. en_US: "Voice / ASMR"
  705. },
  706.  
  707. work_type_music: {
  708. zh_CN: "音乐",
  709. zh_TW: "音樂",
  710. en_US: "Music"
  711. },
  712.  
  713. work_type_tool: {
  714. zh_CN: "工具/装饰",
  715. zh_TW: "工具/配件",
  716. en_US: "Tools / Accessories"
  717. },
  718.  
  719. work_type_voice_comic: {
  720. zh_CN: "音声漫画",
  721. zh_TW: "有聲漫畫",
  722. en_US: "Voiced Comics"
  723. },
  724.  
  725. work_type_other: {
  726. zh_CN: "其他",
  727. zh_TW: "其他",
  728. en_US: "Miscellaneous"
  729. },
  730.  
  731. tag_translatable: {
  732. zh_CN: "可翻译",
  733. zh_TW: "可翻譯",
  734. en_US: "Translatable"
  735. },
  736.  
  737. tag_translatable_tooltip: {
  738. zh_CN: "大家一起来翻译 授权作品",
  739. zh_TW: "大家一起翻譯 授权作品",
  740. en_US: "Translators Unite translation permitted work"
  741. },
  742.  
  743. tag_not_translatable: {
  744. zh_CN: "不可翻译",
  745. zh_TW: "不可翻譯",
  746. en_US: "Not Translatable"
  747. },
  748.  
  749. tag_not_translatable_tooltip: {
  750. zh_CN: "未授权 大家一起来翻译",
  751. zh_TW: "未授權 大家一起來翻譯",
  752. en_US: "Not Translators Unite translation permitted work"
  753. },
  754.  
  755. tag_translated: {
  756. zh_CN: "翻译作品",
  757. zh_TW: "翻譯作品",
  758. en_US: "Translated"
  759. },
  760.  
  761. tag_translated_tooltip: {
  762. zh_CN: "当前作品为 大家一起来翻译 作品",
  763. zh_TW: "當前作品為 大家一起來翻譯 作品",
  764. en_US: "Current work is Translators Unite translation work"
  765. },
  766.  
  767. tag_language_support: {
  768. zh_CN: "语言支持",
  769. zh_TW: "語言支援",
  770. en_US: "Language Support"
  771. },
  772.  
  773. language_japanese: {
  774. zh_CN: "日文",
  775. zh_TW: "日文",
  776. en_US: "Japanese"
  777. },
  778.  
  779. language_english: {
  780. zh_CN: "英文",
  781. zh_TW: "英文",
  782. en_US: "English"
  783. },
  784.  
  785. language_korean: {
  786. zh_CN: "韩语",
  787. zh_TW: "韓語",
  788. en_US: "Korean"
  789. },
  790.  
  791. language_simplified_chinese: {
  792. zh_CN: "简体中文",
  793. zh_TW: "簡體中文",
  794. en_US: "Simplified Chinese"
  795. },
  796.  
  797. language_traditional_chinese: {
  798. zh_CN: "繁体中文",
  799. zh_TW: "繁體中文",
  800. en_US: "Traditional Chinese"
  801. },
  802.  
  803. language_german: {
  804. zh_CN: "德语",
  805. zh_TW: "德語",
  806. en_US: "German"
  807. },
  808.  
  809. language_french: {
  810. zh_CN: "法语",
  811. zh_TW: "法語",
  812. en_US: "French"
  813. },
  814.  
  815. language_russian: {
  816. zh_CN: "俄语",
  817. zh_TW: "俄語",
  818. en_US: "Russian"
  819. },
  820.  
  821. language_spanish: {
  822. zh_CN: "西班牙语",
  823. zh_TW: "西班牙語",
  824. en_US: "Spanish"
  825. },
  826.  
  827. language_indonesian: {
  828. zh_CN: "印尼文",
  829. zh_TW: "印尼文",
  830. en_US: "Indonesian"
  831. },
  832.  
  833. language_italian: {
  834. zh_CN: "意大利语",
  835. zh_TW: "義大利語",
  836. en_US: "Italian"
  837. },
  838.  
  839. language_arabic: {
  840. zh_CN: "阿拉伯语",
  841. zh_TW: "阿拉伯語",
  842. en_US: "Arabic"
  843. },
  844.  
  845. language_portuguese: {
  846. zh_CN: "葡萄牙语",
  847. zh_TW: "葡萄牙語",
  848. en_US: "Portuguese"
  849. },
  850.  
  851. language_finnish: {
  852. zh_CN: "芬兰语",
  853. zh_TW: "芬蘭語",
  854. en_US: "Finnish"
  855. },
  856.  
  857. language_polish: {
  858. zh_CN: "波兰语",
  859. zh_TW: "波蘭語",
  860. en_US: "Polish"
  861. },
  862.  
  863. language_swedish: {
  864. zh_CN: "瑞典文",
  865. zh_TW: "瑞典文",
  866. en_US: "Swedish"
  867. },
  868.  
  869. language_thai: {
  870. zh_CN: "泰语",
  871. zh_TW: "泰語",
  872. en_US: "Thai"
  873. },
  874.  
  875. language_vietnamese: {
  876. zh_CN: "越南语",
  877. zh_TW: "越南語",
  878. en_US: "Vietnamese"
  879. },
  880.  
  881. language_japanese_abbr: {
  882. zh_CN: "日",
  883. zh_TW: "日",
  884. en_US: "JP"
  885. },
  886.  
  887. language_english_abbr: {
  888. zh_CN: "英",
  889. zh_TW: "英",
  890. en_US: "EN"
  891. },
  892.  
  893. language_korean_abbr: {
  894. zh_CN: "韩",
  895. zh_TW: "韩",
  896. en_US: "KO"
  897. },
  898.  
  899. language_simplified_chinese_abbr: {
  900. zh_CN: "简中",
  901. zh_TW: "簡中",
  902. en_US: "ZH"
  903. },
  904.  
  905. language_traditional_chinese_abbr: {
  906. zh_CN: "繁中",
  907. zh_TW: "繁中",
  908. en_US: "TW"
  909. },
  910.  
  911. language_german_abbr: {
  912. zh_CN: "德",
  913. zh_TW: "德",
  914. en_US: "DE"
  915. },
  916.  
  917. language_french_abbr: {
  918. zh_CN: "法",
  919. zh_TW: "法",
  920. en_US: "FR"
  921. },
  922.  
  923. language_spanish_abbr: {
  924. zh_CN: "西",
  925. zh_TW: "西",
  926. en_US: "ES"
  927. },
  928.  
  929. language_indonesian_abbr: {
  930. zh_CN: "印",
  931. zh_TW: "印",
  932. en_US: "ID"
  933. },
  934.  
  935. language_italian_abbr: {
  936. zh_CN: "意",
  937. zh_TW: "意",
  938. en_US: "IT"
  939. },
  940.  
  941. language_portuguese_abbr: {
  942. zh_CN: "葡",
  943. zh_TW: "葡",
  944. en_US: "PT"
  945. },
  946.  
  947. language_swedish_abbr: {
  948. zh_CN: "瑞典",
  949. zh_TW: "瑞典",
  950. en_US: "SV"
  951. },
  952.  
  953. language_thai_abbr: {
  954. zh_CN: "泰",
  955. zh_TW: "泰",
  956. en_US: "TH"
  957. },
  958.  
  959. language_vietnamese_abbr: {
  960. zh_CN: "越",
  961. zh_TW: "越",
  962. en_US: "VN"
  963. },
  964.  
  965. show_rate_count: {
  966. zh_CN: "显示评分人数",
  967. zh_TW: "顯示評分人數",
  968. en_US: "Show Rate Count"
  969. },
  970.  
  971. tag_translation_request: {
  972. zh_CN: "翻译申请情况",
  973. zh_TW: "翻譯申請情况",
  974. en_US: "Translation Request"
  975. },
  976.  
  977. tag_translation_request_tooltip: {
  978. zh_CN: "当前作品目前的翻译申请情况,格式为:语言简写 申请数-发售数",
  979. zh_TW: "當前作品目前的翻譯申請情況,格式為:语言簡稱 申請數-發售數",
  980. en_US: "Current work's translation request. Format: Language_Abbr Number_of_Requests - Number_of_Sales"
  981. },
  982.  
  983. tag_bonus_work: {
  984. zh_CN: "特典",
  985. zh_TW: "特典",
  986. en_US: "Bonus"
  987. },
  988.  
  989. tag_bonus_work_tooltip: {
  990. zh_CN: "当前作品是某部作品的特典",
  991. zh_TW: "當前作品是某部作品的特典",
  992. en_US: "Current work is a bonus work"
  993. },
  994.  
  995. tag_has_bonus: {
  996. zh_CN: "有特典",
  997. zh_TW: "有特典",
  998. en_US: "Has Bonus"
  999. },
  1000.  
  1001. tag_has_bonus_tooltip: {
  1002. zh_CN: "当前作品目前附赠特典,若特典已下架则不会显示该标签",
  1003. zh_TW: "當前作品目前附赠特典,若特典已下架則不會顯示該標籤",
  1004. en_US: "Current work has bonus. If bonus is not available, the tag will not be displayed."
  1005. },
  1006.  
  1007. tag_file_format: {
  1008. zh_CN: "文件格式",
  1009. zh_TW: "檔案形式",
  1010. en_US: "File Format"
  1011. },
  1012.  
  1013. tag_file_format_tooltip: {
  1014. zh_CN: "WAV、EXE、MP3等",
  1015. zh_TW: "WAV、EXE、MP3等",
  1016. en_US: "WAV, EXE, MP3, etc."
  1017. },
  1018.  
  1019. tag_no_longer_available: {
  1020. zh_CN: "已下架",
  1021. zh_TW: "已下架",
  1022. en_US: "Unavailable"
  1023. },
  1024.  
  1025. tag_announce: {
  1026. zh_CN: "预告",
  1027. zh_TW: "預告",
  1028. en_US: "Announce"
  1029. },
  1030.  
  1031. tag_ai: {
  1032. zh_CN: "AI & 部分AI",
  1033. zh_TW: "AI & 部分AI",
  1034. en_US: "AI & Partial AI"
  1035. },
  1036.  
  1037. tag_aig: {
  1038. zh_CN: "AI生成",
  1039. zh_TW: "AI生成",
  1040. en_US: "AI Gen",
  1041. },
  1042.  
  1043. tag_aip: {
  1044. zh_CN: "AI部分使用",
  1045. zh_TW: "AI部分使用",
  1046. en_US: "AI Partial",
  1047. },
  1048.  
  1049. tag_ai_tooltip: {
  1050. zh_CN: "全部或部分使用AI的作品",
  1051. zh_TW: "全部或部分使用AI的作品",
  1052. en_US: "Full or partial use of AI",
  1053. },
  1054.  
  1055. button_save: {
  1056. zh_CN: "保存设置",
  1057. zh_TW: "保存設置",
  1058. en_US: "Save",
  1059. },
  1060.  
  1061. button_cancel: {
  1062. zh_CN: "取消设置",
  1063. zh_TW: "取消設置",
  1064. en_US: "Cancel",
  1065. },
  1066.  
  1067. button_reset: {
  1068. zh_CN: "重置设置",
  1069. zh_TW: "重置設置",
  1070. en_US: "Reset",
  1071. },
  1072.  
  1073. need_reorder: {
  1074. zh_CN: "检测到设置更新,可能添加了新的信息位,请重新设置对应设置项的排列",
  1075. zh_TW: "檢查到設置更新,可能添加了新的信息位,请重新設置對應設置項的排列",
  1076. en_US: "There is a new setting item added. Please reorder the corresponding setting item",
  1077. },
  1078.  
  1079. save_complete: {
  1080. zh_CN: "设置已保存,部分设置需要刷新对应页面以生效",
  1081. zh_TW: "設置已保存,部分設置需要刷新對應頁面以生效",
  1082. en_US: "Settings saved, some settings need to refresh the corresponding page to take effect",
  1083. },
  1084.  
  1085. save_failed: {
  1086. zh_CN: "设置保存失败",
  1087. zh_TW: "設置保存失敗",
  1088. en_US: "Settings save failed",
  1089. },
  1090.  
  1091. reset_confirm: {
  1092. zh_CN: "确定要将设置重置到最初始的状态吗?(重置后,需要再点击保存才会生效)",
  1093. zh_TW: "確定要將設置重置到最初始的狀態嗎?(重置後,需要再點擊保存才會生效)",
  1094. en_US: "Are you sure you want to reset the settings to the initial state? (After resetting, you need to click Save to take effect)",
  1095. },
  1096.  
  1097. reset_complete: {
  1098. zh_CN: "设置已重置",
  1099. zh_TW: "設置已重置",
  1100. en_US: "Settings reset",
  1101. },
  1102.  
  1103. reset_failed: {
  1104. zh_CN: "设置重置失败",
  1105. zh_TW: "設置重置失敗",
  1106. en_US: "Settings reset failed",
  1107. },
  1108.  
  1109. reset_order: {
  1110. zh_CN: "重置顺序",
  1111. zh_TW: "重置順序",
  1112. en_US: "Reset Order",
  1113. },
  1114.  
  1115. reset_order_confirm: {
  1116. zh_CN: "确定要将元素顺序重置到最初始的状态吗?",
  1117. zh_TW: "確定要將元素順序重置到最初始的狀態嗎?",
  1118. en_US: "Are you sure you want to reset the element order to the initial state?",
  1119. },
  1120.  
  1121. reset_order_and_setting: {
  1122. zh_CN: "重置元素顺序和各自的设置值",
  1123. zh_TW: "重置元素順序和各自的設置值",
  1124. en_US: "Reset element order and their settings",
  1125. },
  1126.  
  1127. hint_pin: {
  1128. zh_CN: "按住CTRL以固定弹框,固定时可复制信息",
  1129. zh_TW: "按住CTRL以固定彈窗,固定時可複製資訊",
  1130. en_US: "Hold CTRL to pin the popup, info can be copied.",
  1131. },
  1132.  
  1133. hint_unpin: {
  1134. zh_CN: "抬起CTRL以关闭弹框 & 查看其它作品RJ信息",
  1135. zh_TW: "抬起CTRL以關閉彈窗 & 查看其它作品RJ信息",
  1136. en_US: "Release CTRL to close the popup & view other works.",
  1137. },
  1138.  
  1139. hint_copy: {
  1140. zh_CN: "左键单击以复制信息",
  1141. zh_TW: "左鍵單擊以複製資訊",
  1142. en_US: "Left click to copy info.",
  1143. },
  1144.  
  1145. hint_copy_all: {
  1146. zh_CN: "左键单击以复制内部所有信息",
  1147. zh_TW: "左鍵單擊以複製內部所有資訊",
  1148. en_US: "Left click to copy all contained info.",
  1149. },
  1150.  
  1151. hint_copy_work_title: {
  1152. zh_CN: "单击复制标题,Alt+单击复制为有效文件名",
  1153. zh_TW: "單擊複製標題,Alt+單擊複製為有效檔名",
  1154. en_US: "Click to copy title, Alt+click to copy as valid filename.",
  1155. },
  1156.  
  1157. get: function (key, langKey = "_s_lang") {
  1158. return typeof key === "string" ? localizationMap[key][settings[langKey]] : key[settings[langKey]];
  1159. }
  1160. }
  1161.  
  1162. function localize(key) {
  1163. return localizationMap.get(key);
  1164. }
  1165.  
  1166. function localizePopup(key) {
  1167. return localizationMap.get(key, "_s_popup_lang");
  1168. }
  1169. //----------------------
  1170.  
  1171.  
  1172. const RJ_REGEX = new RegExp("(R[JE][0-9]{8})|(R[JE][0-9]{6})|([VB]J[0-9]{8})|([VB]J[0-9]{6})", "gi");
  1173. const URL_REGEX = new RegExp("dlsite.com/.*/product_id/((R[JE][0-9]{8})|(R[JE][0-9]{6})|([VB]J[0-9]{8})|([VB]J[0-9]{6}))", "g");
  1174. const VOICELINK_CLASS = 'voicelink-' + Math.random().toString(36).slice(2);
  1175. const VOICELINK_IGNORED_CLASS = `${VOICELINK_CLASS}_ignored`;
  1176. const RJCODE_ATTRIBUTE = 'rjcode';
  1177. const POPUP_CSS = `
  1178. .${VOICELINK_CLASS}_voicepopup {
  1179. min-width: 630px !important;
  1180. z-index: 2147483646 !important;
  1181. max-width: 80% !important;
  1182. position: fixed !important;
  1183. line-height: normal !important; /*原1.4em !important;*/
  1184. font-size:1.1em!important;
  1185. margin-bottom: 10px !important;
  1186. box-shadow: 0 0 .125em 0 rgba(0,0,0,.5) !important;
  1187. border-radius: 0.5em !important;
  1188. background-color:#8080C0 !important;
  1189. color:#F6F6F6 !important;
  1190. text-align: left !important;
  1191. padding: 10px !important;
  1192. pointer-events: none !important;
  1193. }
  1194. .${VOICELINK_CLASS}_voicepopup[pin][mouse-in] *[copy-text] {
  1195. text-decoration: underline !important;
  1196. cursor: pointer !important;
  1197. }
  1198. .${VOICELINK_CLASS}_voicepopup[pin][mouse-in] *[copy-text]:active {
  1199. opacity: 0.5 !important;
  1200. }
  1201. #${VOICELINK_CLASS}_info-container {
  1202. font-size: 1em !important;
  1203. }
  1204. #${VOICELINK_CLASS}_info-container > div {
  1205. margin-bottom: 3px !important;
  1206. font-size: 1em !important;
  1207. }
  1208. #${VOICELINK_CLASS}_info-container > div > a {
  1209. display: inline;
  1210. }
  1211. #${VOICELINK_CLASS}_info-container > div > .info-title {
  1212. margin-right: 5px !important;
  1213. display: inline-block;
  1214. }
  1215. #${VOICELINK_CLASS}_info-container > div > .info-title::after {
  1216. content: ":" !important;
  1217. text-decoration: none !important;
  1218. display: inline-block !important;
  1219. }
  1220. #${VOICELINK_CLASS}_info-container .${VOICELINK_CLASS}_tags {
  1221. margin-top: 12px !important;
  1222. margin-bottom: 0 !important;
  1223. font-size: 0.909091em !important;
  1224. }
  1225. .${VOICELINK_CLASS}_loader {
  1226. display: flex !important;
  1227. justify-content: center !important;
  1228. align-items: center !important;
  1229. position: absolute !important;
  1230. top: 50% !important;
  1231. left: 50% !important;
  1232. transform: translate(-50%, -50%) !important;
  1233. width: 100% !important;
  1234. height: 100% !important;
  1235. min-width: 300px !important;
  1236. min-height: 30px !important;
  1237. z-index: -1 !important;
  1238. }
  1239. .${VOICELINK_CLASS}_dot {
  1240. width: 20px !important;
  1241. height: 20px !important;
  1242. margin: 0 8px !important;
  1243. background-color: #fbfbfb !important;
  1244. border-radius: 50% !important;
  1245. animation: ${VOICELINK_CLASS}_scale 1s infinite !important;
  1246. }
  1247. .${VOICELINK_CLASS}_dot:nth-child(1) {
  1248. animation-delay: 0s !important;
  1249. }
  1250. .${VOICELINK_CLASS}_dot:nth-child(2) {
  1251. animation-delay: 0.2s !important;
  1252. }
  1253. .${VOICELINK_CLASS}_dot:nth-child(3) {
  1254. animation-delay: 0.4s !important;
  1255. }
  1256. @keyframes ${VOICELINK_CLASS}_scale {
  1257. 0%, 100% {
  1258. transform: scale(1);
  1259. }
  1260. 50% {
  1261. transform: scale(1.5);
  1262. }
  1263. }
  1264. .${VOICELINK_CLASS}_voicepopup-maniax{
  1265. background-color:#8080C0 !important;
  1266. }
  1267. .${VOICELINK_CLASS}_voicepopup-girls{
  1268. background-color:#B33761 !important;
  1269. }
  1270. .${VOICELINK_CLASS}_voicepopup .${VOICELINK_CLASS}_left_panel{
  1271. display: flex !important;
  1272. flex-direction: column !important;
  1273. justify-content: space-between !important;
  1274. margin: 0 16px 0 0 !important;
  1275. width: 310px !important;
  1276. flex-shrink: 0 !important;
  1277. }
  1278. .${VOICELINK_CLASS}_voicepopup .${VOICELINK_CLASS}_img_container{
  1279. width: 100% !important;
  1280. padding: 3px !important;
  1281. position: relative;
  1282. }
  1283.  
  1284. .${VOICELINK_CLASS}_img_container img {
  1285. width: 100% !important;
  1286. height: auto !important;
  1287. }
  1288. #${VOICELINK_CLASS}_hint {
  1289. font-size: 0.8em !important;
  1290. opacity: 0.5 !important;
  1291. max-width: 300px !important;
  1292. margin-top: 5px !important;
  1293. }
  1294. .${VOICELINK_CLASS}_voicepopup a {
  1295. text-decoration: none !important;
  1296. color: pink !important;
  1297. }
  1298. .${VOICELINK_CLASS}_voicepopup .${VOICELINK_CLASS}_age-18{
  1299. color: hsl(300deg 76% 77%) !important;
  1300. }
  1301. .${VOICELINK_CLASS}_voicepopup .${VOICELINK_CLASS}_age-all{
  1302. color: hsl(157deg 82% 52%) !important;
  1303. }
  1304.  
  1305. .${VOICELINK_CLASS}_voice-title {
  1306. font-size: 1.363636em !important; /*原1.4em*/
  1307. font-weight: bold !important;
  1308. text-align: center !important;
  1309. margin: 5px 10px 0 0 !important;
  1310. display: block !important;
  1311. }
  1312.  
  1313. .${VOICELINK_CLASS}_rjcode {
  1314. text-align: center !important;
  1315. margin: 5px 0 !important;
  1316. font-size: 1.2012987em !important; /*原1.2em !important;*/
  1317. font-style: italic !important;
  1318. opacity: 0.3 !important;
  1319. }
  1320.  
  1321. .${VOICELINK_CLASS}_error {
  1322. height: 210px !important;
  1323. line-height: 210px !important;
  1324. text-align: center !important;
  1325. }
  1326.  
  1327. .${VOICELINK_CLASS}_discord-dark {
  1328. background-color: #36393f !important;
  1329. color: #dcddde !important;
  1330. font-size: 0.9375rem !important;
  1331. }
  1332. .${VOICELINK_CLASS}_work_title:hover #${VOICELINK_CLASS}_copy_btn {
  1333. opacity: 1 !important;
  1334. }
  1335. #${VOICELINK_CLASS}_copy_btn {
  1336. background: transparent !important;
  1337. border-color: transparent !important;
  1338. cursor: pointer !important;
  1339. transition: all 0.3s !important;
  1340. opacity: 0 !important;
  1341. font-size: 0.75em !important;
  1342. user-select: none !important;
  1343. position: absolute !important;
  1344. }
  1345. #${VOICELINK_CLASS}_copy_btn:hover {
  1346. scale: 1.2 !important;
  1347. }
  1348. #${VOICELINK_CLASS}_copy_btn:active {
  1349. scale: 1.1 !important;
  1350. }
  1351. `
  1352. const SETTINGS_CSS = `
  1353. #${VOICELINK_CLASS}_settings-container {
  1354. font-family: Arial, sans-serif !important;
  1355. background-color: #f4f4f9 !important;
  1356. margin: auto !important;
  1357. padding: 20px 30px !important;
  1358. line-height: unset !important;
  1359. position: fixed !important;
  1360. overflow-y: auto !important;
  1361. overflow-x: hidden !important;
  1362. top: 20px !important;
  1363. bottom: 20px !important;
  1364. left: 50% !important;
  1365. transform: translateX(-50%) !important;
  1366. box-sizing: border-box !important;
  1367. max-width: 800px !important;
  1368. width: 100% !important;
  1369. height: calc(100% - 40px) !important;
  1370. z-index: 2147483647 !important;
  1371. border-radius: 20px !important;
  1372. box-shadow: darkgray 0px 0px 17px 2px !important;
  1373. /*scrollbar-width: none;*/
  1374. /*-ms-overflow-style: none;*/
  1375. }
  1376. #${VOICELINK_CLASS}_settings-container::-webkit-scrollbar {
  1377. width: 5px !important;
  1378. height: 5px !important;
  1379. }
  1380. #${VOICELINK_CLASS}_settings-container::-webkit-scrollbar-track {
  1381. background-color: #f4f4f9 !important;
  1382. border-radius: 5px !important;
  1383. }
  1384. #${VOICELINK_CLASS}_settings-container::-webkit-scrollbar-thumb {
  1385. background-color: #888 !important;
  1386. border-radius: 5px !important;
  1387. }
  1388. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_container {
  1389. max-width: 800px !important;
  1390. margin: auto !important;
  1391. background: #fff !important;
  1392. padding: 20px !important;
  1393. border-radius: 10px !important;
  1394. box-shadow: 0 0 10px rgba(0, 0, 0, 0.1) !important;
  1395. }
  1396. #${VOICELINK_CLASS}_settings-container h1 {
  1397. display: block !important;
  1398. text-align: center !important;
  1399. color: #333 !important;
  1400. font-size: 32px !important;
  1401. margin: 21.44px 0 !important;
  1402. font-weight: bold !important;
  1403. line-height: normal !important;
  1404. }
  1405. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_section-container {
  1406. margin: 20px 0 !important;
  1407. }
  1408. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_section-container h2 {
  1409. display: block !important;
  1410. color: #007bff !important;
  1411. font-size: 24px !important;
  1412. margin: 22px 0 14px 0 !important;
  1413. font-weight: bold !important;
  1414. line-height: normal !important;
  1415. }
  1416. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting {
  1417. /*display: flex;*/
  1418. /*align-items: center;*/
  1419. /*justify-content: space-between;*/
  1420. margin: 10px 0 !important;
  1421. }
  1422. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting .${VOICELINK_CLASS}_row-title {
  1423. margin: 0 0 0 10px !important;
  1424. color: #555 !important;
  1425. font-size: 18px !important;
  1426. font-weight: normal !important;
  1427. /*flex-grow: 1;*/
  1428. }
  1429. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting input[type="text"],
  1430. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting input[type="password"],
  1431. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting input[type="number"],
  1432. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting input[type="email"],
  1433. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting select {
  1434. width: 100% !important;
  1435. padding: 10px !important;
  1436. border: 1px solid #ddd !important;
  1437. border-radius: 5px !important;
  1438. background: #fafafa !important;
  1439. box-sizing: border-box !important;
  1440. color: #666666FF !important;
  1441. font-size: 13.3333px !important;
  1442. height: unset !important;
  1443. max-height: unset !important;
  1444. max-width: unset !important;
  1445. /*margin-bottom: 10px;*/
  1446. }
  1447. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting input[type="checkbox"] {
  1448. display: none !important;
  1449. }
  1450. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_toggle-container {
  1451. display: flex !important;
  1452. flex-direction: row !important;
  1453. align-items: center !important;
  1454. justify-content: flex-end !important;
  1455. }
  1456. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting .${VOICELINK_CLASS}_toggle {
  1457. display: inline-block !important;
  1458. margin: 0 !important;
  1459. width: 60px !important;
  1460. height: 30px !important;
  1461. padding: 0 !important;
  1462. background: #ccc !important;
  1463. border-radius: 15px !important;
  1464. position: relative !important;
  1465. cursor: pointer !important;
  1466. transition: background 0.3s !important;
  1467. }
  1468. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_toggle:before {
  1469. content: "" !important;
  1470. display: block !important;
  1471. width: 24px !important;
  1472. height: 24px !important;
  1473. background: #fff !important;
  1474. border-radius: 50% !important;
  1475. position: absolute !important;
  1476. top: 3px !important;
  1477. left: 3px !important;
  1478. transition: transform 0.3s !important;
  1479. }
  1480. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting input[type="checkbox"]:checked + label {
  1481. background: #007bff !important;
  1482. }
  1483. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting input[type="checkbox"]:checked + label:before {
  1484. transform: translateX(30px) !important;
  1485. }
  1486. #${VOICELINK_CLASS}_button-close{
  1487. position: absolute !important;
  1488. top: 20px !important;
  1489. right: 20px !important;
  1490. font-size: 24px !important;
  1491. cursor: pointer !important;
  1492. background: rgba(0, 0, 0, 0.05) !important;
  1493. border: none !important;
  1494. width: 42px !important;
  1495. height: 42px !important;
  1496. border-radius: 50% !important;
  1497. }
  1498. #${VOICELINK_CLASS}_button-save,
  1499. #${VOICELINK_CLASS}_button-cancel,
  1500. #${VOICELINK_CLASS}_button-reset{
  1501. display: block !important;
  1502. width: 100% !important;
  1503. padding: 10px !important;
  1504. border: none !important;
  1505. border-radius: 5px !important;
  1506. background: #007bff !important;
  1507. color: #fff !important;
  1508. font-size: 16px !important;
  1509. cursor: pointer !important;
  1510. margin-top: 10px !important;
  1511.  
  1512. transition: background 0.3s, filter 0.3s !important;
  1513. }
  1514. #${VOICELINK_CLASS}_button-reset{
  1515. background: #999 !important;
  1516. }
  1517. #${VOICELINK_CLASS}_button-save:hover,
  1518. #${VOICELINK_CLASS}_button-cancel:hover,
  1519. #${VOICELINK_CLASS}_button-reset:hover{
  1520. filter: brightness(1.3) !important;
  1521. }
  1522. #${VOICELINK_CLASS}_button-save:active,
  1523. #${VOICELINK_CLASS}_button-cancel:active,
  1524. #${VOICELINK_CLASS}_button-reset:active{
  1525. filter: brightness(0.9) !important;
  1526. }
  1527.  
  1528. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_tooltip {
  1529. position: relative !important;
  1530. }
  1531. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_tooltip .${VOICELINK_CLASS}_tooltip-text {
  1532. visibility: hidden !important;
  1533. min-width: 200px !important;
  1534. max-width: 100% !important;
  1535. background-color: #555 !important;
  1536. color: #fff !important;
  1537. font-size: 14px !important;
  1538. text-align: center !important;
  1539. border-radius: 5px !important;
  1540. padding: 8px 10px !important;
  1541. position: absolute !important;
  1542. z-index: 1 !important;
  1543. bottom: 125% !important;
  1544. left: 0 !important;
  1545. /*margin-left: -100px;*/
  1546. opacity: 0 !important;
  1547. filter: brightness(1.0) !important;
  1548. transition: opacity 0.3s !important;
  1549. }
  1550. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_tooltip:hover .${VOICELINK_CLASS}_tooltip-text {
  1551. visibility: visible !important;
  1552. opacity: 1 !important;
  1553. }
  1554. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_sortable {
  1555. cursor: move !important;
  1556. }
  1557. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_sortable span{
  1558. cursor: default !important;
  1559. }
  1560. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_dragging{
  1561. background-color: #1e82ff38 !important;
  1562. user-select: none !important;
  1563. transition: background-color 0.3s !important;
  1564. }
  1565. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_sortable .${VOICELINK_CLASS}_setting {
  1566. cursor: move !important;
  1567. }
  1568. #${VOICELINK_CLASS}_settings-container table {
  1569. width: 100% !important;
  1570. margin-bottom: 20px !important;
  1571. border-collapse: collapse !important;
  1572. font-size: unset !important;
  1573. }
  1574. #${VOICELINK_CLASS}_settings-container table,
  1575. #${VOICELINK_CLASS}_settings-container th,
  1576. #${VOICELINK_CLASS}_settings-container td {
  1577. border: 0 solid #ddd !important;
  1578. }
  1579. #${VOICELINK_CLASS}_settings-container th,
  1580. #${VOICELINK_CLASS}_settings-container td {
  1581. border-bottom: 1px dashed rgba(221, 221, 221, 0.64) !important;
  1582. /*border-top: 1px solid #ddd;*/
  1583. padding: 8px 10px !important;
  1584. text-align: left !important;
  1585. vertical-align: middle !important;
  1586. }
  1587.  
  1588. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_hidden{
  1589. display: none !important;
  1590. }
  1591. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_input-cell{
  1592. text-align: right !important;
  1593. padding-right: 20px !important;
  1594. }
  1595. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_indent-1 > td {
  1596. padding: 8px 24px !important;
  1597. }
  1598. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_indent-1 .${VOICELINK_CLASS}_input-cell {
  1599. padding: 8px 20px !important;
  1600. }
  1601.  
  1602. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_tags {
  1603. font-size: 14px;
  1604. }
  1605. .${VOICELINK_CLASS}_tags {
  1606. display: flex !important;
  1607. flex-wrap: wrap !important;
  1608. justify-content: left !important;
  1609. align-items: stretch !important;
  1610. }
  1611. .${VOICELINK_CLASS}_tags > label,
  1612. .${VOICELINK_CLASS}_tags > span {
  1613. border-radius: 5px !important;
  1614. font-size: 1em !important;
  1615. margin-right: 8px !important;
  1616. margin-bottom: 8px !important;
  1617. padding: 5px 8px !important;
  1618. display: flex !important;
  1619. justify-content: center !important;
  1620. align-items: center !important;
  1621.  
  1622. transition: color 0.3s, background-color 0.3s !important;
  1623. }
  1624. .${VOICELINK_CLASS}_tags > label.${VOICELINK_CLASS}_tag_tight,
  1625. .${VOICELINK_CLASS}_tags > span.${VOICELINK_CLASS}_tag_tight{
  1626. padding: 2px 7px !important;
  1627. }
  1628. .${VOICELINK_CLASS}_tags > label.${VOICELINK_CLASS}_tag_small,
  1629. .${VOICELINK_CLASS}_tags > span.${VOICELINK_CLASS}_tag_small{
  1630. padding: 2px 7px !important;
  1631. font-size: 0.857143em !important;
  1632. }
  1633.  
  1634. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_tag-off{
  1635. background-color: #ffffff !important;
  1636. color: #aaaaaa !important;
  1637. }
  1638.  
  1639. .${VOICELINK_CLASS}_tag-purple{
  1640. background-color: #EED9F2 !important;
  1641. color: #7B1FA2 !important;
  1642. }
  1643.  
  1644. .${VOICELINK_CLASS}_tag-blue{
  1645. background-color: #d9eefc !important;
  1646. color: #4285F4 !important;
  1647. }
  1648.  
  1649. .${VOICELINK_CLASS}_tag-red{
  1650. background-color: #ffd6da !important;
  1651. color: #EA4335 !important;
  1652. }
  1653.  
  1654. .${VOICELINK_CLASS}_tag-yellow{
  1655. background-color: #FFF8E1 !important;
  1656. color: #F57F17 !important;
  1657. }
  1658.  
  1659. .${VOICELINK_CLASS}_tag-green{
  1660. background-color: #dcf5e4 !important;
  1661. color: #34A853 !important;
  1662. }
  1663.  
  1664. .${VOICELINK_CLASS}_tag-teal{
  1665. background-color: #d8eced !important;
  1666. color: #0097A7 !important;
  1667. }
  1668.  
  1669. .${VOICELINK_CLASS}_tag-gray{
  1670. background-color: #E0E0E0 !important;
  1671. color: #424242 !important;
  1672. }
  1673.  
  1674. .${VOICELINK_CLASS}_tag-pink{
  1675. background-color: #ffd9e7 !important;
  1676. color: #f032a7 !important;
  1677. }
  1678.  
  1679. .${VOICELINK_CLASS}_tag-orange{
  1680. background-color: #ffebcc !important;
  1681. color: #f04000 !important;
  1682. }
  1683.  
  1684. .${VOICELINK_CLASS}_tag-darkblue{
  1685. background-color: #d2e7fa !important;
  1686. color: #0D47A1 !important;
  1687. }
  1688.  
  1689. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_reset-btn-small {
  1690. position: relative !important;
  1691. display: inline-block !important;
  1692. width: 16px !important;
  1693. height: 16px !important;
  1694. margin-right: 4px !important;
  1695. padding: 0 !important;
  1696. color: transparent !important;
  1697. background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAA6xJREFUeF7tmlFy2jAQhn+ZHIScJOEixYQ++BYJt/BDSU0vUnKSuvcoVkeq3RpXsndlScYAMzwwyLL20+rXarUCN/4RN24/7gDuHnDjBO5LYEoHSF+ytHjPiynHMJkHrLfZVwApgOKwzzdTQZgEQMv4xu7JIEQHYDB+UghRAfQYDyGwKr7kx9hLIRqASzRewY4C4FKNjwKgz3gAZesLIfGhBhVzawzqAQPG9y13DUZIHFDhWBS5+h3kEwxA+jl7lhLfPYxaGX8UJ+xCgAgHIM2WVYK1EHjzAEF1UUqJ4tt7vvPUn+4mGADVeeofggYhTlj58oagAAgQSiGwwS+UeMASFZZS4KkOkXs1wpc3BAdAgmCYUeU9coHXPhhS4m3skogCwBVC85xcQB2cnk0uMRZCNAAECMfDPl+ZjBzQEr2MXMPoqAB6IJSHff44pO6fXrJXy67iLIzRARggkIxv4KgkihR6SXQ/rH6ahycB0IbgImI2TxASG24YPRmAIXcf+t8Cge0FswVQb5Mq1F62YXG9YLYA9DIy6wHLC+YN4E+w9F+MwMkuzRqA8gKTFnCCo9kDqLXgR0c0rUFVV1xnD0AZtN5mSgzPQuXDPifZRmo0tCVN/b8JgDjhkXJkvhYAzS3T37mgCuFVADAJoVcASmiQdI6jgZOVnGVligeoARHJA0wJTs5WwzHGpa3RA4jngqsAYEq/exXBsXuty6xyngkOoN5rZWdQrJibYxC37XqbqUDo7FDkPQ4w7rUT3eh2Aa23WXdy4B3A2JibO6vU9pYTIbnggiSCTQZHLuAcc1MN4rYbEwWqd5EB2GJuasDBNYzS3pYfpLo/G4CPBATFMGobk/hx4xOWB1i2Q3BfSjWwr50tMcqZfbYHaC2wpKGExI6bkXUFYbt6p4a/7feyPKAlhqarqlE3NFQYNi9Ut8aUy5Xue9gANAR78YPzDQ0FQF/RhasYOwFQg+27pvJ1dd2G0ms88eBjguwMYACCFsakwoGSlembfUKRBTn/5x0AYXDOZS2EvpU9o4x32gW6FIkD/Vf1laC0XWW3+lIJTmM9QPN+X1vvqCXQhtGjCTYPb5e+nZ3khgTRVfC8LwGTNwyVtQwZN/C/KpfbjNWVUXEAxYB6r7aWtVD66LQJFmN4WwImo+qokVL1ZXpcC2gC/AwZYQYF0Fils8oPWFYVnoTQmZv2t9ECLZRSokwSfLjW/HC9KwoA7qBitr8DiEn7Et9194BLnJWYY7p5D/gNXP0HX03p5E0AAAAASUVORK5CYII=") !important;
  1698. background-position: center !important;
  1699. background-size: contain !important;
  1700. background-color: transparent !important;
  1701. border-radius: 3px !important;
  1702. border: none !important;
  1703. opacity: 0.5 !important;
  1704. }
  1705. #${VOICELINK_CLASS}_settings-container button.${VOICELINK_CLASS}_reset-btn-small:hover {
  1706. opacity: 1 !important;
  1707. }
  1708.  
  1709. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_button-flat {
  1710. background-color: transparent !important;
  1711. border: none !important;
  1712. color: #aaa !important;
  1713. cursor: pointer !important;
  1714. border-radius: 5px !important;
  1715. padding: 5px 5px !important;
  1716. margin-bottom: 6px !important;
  1717. margin-right: 6px !important;
  1718.  
  1719. display: inline-flex !important;
  1720. align-items: center !important;
  1721. justify-content: center !important;
  1722.  
  1723. transition: background-color 0.3s !important;
  1724. }
  1725. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_button-flat:hover {
  1726. background-color: rgba(0, 0, 0, 0.1) !important;
  1727. }
  1728. #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_button-flat span{
  1729. display: inline-block !important;
  1730. }
  1731. `
  1732.  
  1733. /**
  1734. * Work promise cache
  1735. * @type {{info:{}, api:{}, api2: {}, circle: {}}}
  1736. */
  1737. const work_promise = {};
  1738.  
  1739. function getAdditionalPopupClasses() {
  1740. const hostname = document.location.hostname;
  1741. switch (hostname) {
  1742. case "boards.4chan.org": return "post reply";
  1743. case "discordapp.com": return `${VOICELINK_CLASS}_discord-dark`;
  1744. default: return null;
  1745. }
  1746. }
  1747.  
  1748. function getOS() {
  1749. const userAgent = navigator.userAgent;
  1750. if (userAgent.indexOf("Windows NT 10.0") !== -1) return "Windows 10";
  1751. if (userAgent.indexOf("Windows NT 6.2") !== -1) return "Windows 8";
  1752. if (userAgent.indexOf("Windows NT 6.1") !== -1) return "Windows 7";
  1753. if (userAgent.indexOf("Windows NT 6.0") !== -1) return "Windows Vista";
  1754. if (userAgent.indexOf("Windows NT 5.1") !== -1) return "Windows XP";
  1755. if (userAgent.indexOf("Windows NT 5.0") !== -1) return "Windows 2000";
  1756. if (userAgent.indexOf("Mac") !== -1) return "Mac";
  1757. if (userAgent.indexOf("X11") !== -1) return "UNIX";
  1758. if (userAgent.indexOf("Linux") !== -1) return "Linux";
  1759. return "Other";
  1760. }
  1761.  
  1762. function getVoiceLinkTarget(target){
  1763. while (target && !target.classList.contains(VOICELINK_CLASS)){
  1764. target = target.parentElement;
  1765. }
  1766. return target;
  1767. }
  1768.  
  1769. function isInDLSite(){
  1770. return document.location.hostname.endsWith("dlsite.com");
  1771. }
  1772.  
  1773. /**
  1774. * Convert to valid file name.
  1775. * @param {String} original
  1776. */
  1777. function convertToValidFileName(original){
  1778. const charMapRegs = {
  1779. "\\/": "/",
  1780. "\\\\": "\",
  1781. "\\:": ":",
  1782. "\\*": "*",
  1783. "\\?": "?",
  1784. "\"": """,
  1785. "\\<": "<",
  1786. "\\>": ">",
  1787. "\\|": "|"
  1788. }
  1789.  
  1790. let fileName = original;
  1791. for (let key in charMapRegs){
  1792. fileName = fileName.replaceAll(new RegExp(key, "g"), charMapRegs[key]);
  1793. }
  1794. return fileName;
  1795. }
  1796.  
  1797. function setUserSelectTitle(){
  1798. // Make title selectable
  1799. const hostname = document.location.hostname;
  1800. if(!hostname.endsWith("dlsite.com")){
  1801. return;
  1802. }
  1803. const rjList = document.URL.match(RJ_REGEX)
  1804. const rj = rjList[rjList.length - 1]
  1805.  
  1806. const title = document.getElementById("work_name");
  1807. if(!title){
  1808. return;
  1809. }
  1810. let titleStr = title.innerText;
  1811. let titleHtml = title.innerHTML;
  1812.  
  1813. const button = document.createElement("button");
  1814. button.id = `${VOICELINK_CLASS}_copy_btn`;
  1815. button.innerText = "📃";
  1816. button.addEventListener("mouseenter", function(){
  1817. button.innerText = "📃 复制为有效文件名";
  1818. });
  1819. button.addEventListener("mouseleave", function(){
  1820. button.innerText = "📃";
  1821. });
  1822. button.addEventListener("click", function(){
  1823. const fileName = convertToValidFileName(titleStr);
  1824. // const promise = navigator.clipboard.writeText(fileName);
  1825. const promise = GM_setClipboard(fileName, "text");
  1826. promise?.then(() => {
  1827. button.innerText = "✔ 复制成功";
  1828. });
  1829. promise?.catch(e => {
  1830. window.prompt("复制失败,请手动复制", fileName);
  1831. button.innerText = "📃";
  1832. });
  1833. });
  1834.  
  1835. title.style.setProperty("user-select", "text", "important"); //userSelect = "text !important";
  1836. title.classList.add(`${VOICELINK_CLASS}_work_title`);
  1837.  
  1838. if(settings._s_show_translated_title_in_dl){
  1839. //将Title替换成大家翻对应的语言翻译版本
  1840. WorkPromise.getTranslationInfo(rj).then(info => {
  1841. if(info.is_original) {
  1842. return null;
  1843. }
  1844. else{
  1845. return WorkPromise.getWorkTitle(rj);
  1846. }
  1847. }).then(t => {
  1848. if(!t){
  1849. if(settings._s_copy_as_filename_btn) title.appendChild(button);
  1850. return;
  1851. }
  1852. compatibilityCheck(title, titleHtml);
  1853. titleStr = t
  1854. title.innerText = t
  1855. if(settings._s_copy_as_filename_btn) title.appendChild(button);
  1856. })
  1857. }else{
  1858. if(settings._s_copy_as_filename_btn) title.appendChild(button);
  1859. }
  1860. }
  1861.  
  1862. function compatibilityCheck(titleElement, titleHtml){
  1863. if(!settings._s_show_compatibility_warning) return;
  1864.  
  1865. if(titleElement.innerHTML.trim() === titleHtml.trim()){
  1866. return;
  1867. }
  1868.  
  1869. //其它脚本修改了标题内部,进行警告
  1870. window.alert("警告:\n" +
  1871. "VoiceLinks检测到DL作品标题元素发生变化,该变化可能是脚本与其它插件冲突导致的。\n" +
  1872. "可以关闭本脚本中的 “在DLSite显示对应语言的翻译标题” 设置项,以尝试解决冲突。(也可根据情况酌情关闭 “在DL作品标题旁添加复制为文件名按钮” 选项)\n\n" +
  1873. "本脚本的设置方法:点击Tampermonkey等扩展程序的按钮,在弹出的脚本列表中找到当前脚本,点击下方的Settings按钮即可打开设置页面。\n\n" +
  1874. "注意:如果不想看到该警告,可以同时关闭“显示兼容性警告”设置项。")
  1875. }
  1876.  
  1877. function getXmlHttpRequest() {
  1878. return (typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : GM_xmlhttpRequest);
  1879. }
  1880.  
  1881. const Parser = {
  1882. walkNodes: function (elem) {
  1883. const rjNodeTreeWalker = document.createTreeWalker(
  1884. elem,
  1885. NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
  1886. {
  1887. acceptNode: function (node) {
  1888. if(node.nodeName === "SCRIPT" || node.parentElement && node.parentElement.nodeName === "SCRIPT"){
  1889. return NodeFilter.FILTER_REJECT;
  1890. }
  1891.  
  1892. if(node.parentElement.isContentEditable){
  1893. return NodeFilter.FILTER_SKIP;
  1894. }
  1895.  
  1896. if(settings._s_parse_url && node.nodeName === "A"){
  1897. if(!settings._s_parse_url_in_dl && document.location.hostname.endsWith("dlsite.com")){
  1898. return NodeFilter.FILTER_SKIP;
  1899. }
  1900.  
  1901. let href = node.href;
  1902. if(href.match(URL_REGEX) && !node.classList.contains(VOICELINK_IGNORED_CLASS)){
  1903. return NodeFilter.FILTER_ACCEPT;
  1904. }
  1905. }
  1906.  
  1907. if (node.nodeName !== "#text") return NodeFilter.FILTER_SKIP;
  1908. if(node.parentElement.classList.contains(VOICELINK_IGNORED_CLASS)
  1909. || node.parentElement.hasAttribute(RJCODE_ATTRIBUTE)){
  1910. return NodeFilter.FILTER_SKIP;
  1911. }
  1912.  
  1913. if (node.parentElement.classList.contains(VOICELINK_CLASS))
  1914. return NodeFilter.FILTER_ACCEPT;
  1915. if (node.nodeValue.match(RJ_REGEX))
  1916. return NodeFilter.FILTER_ACCEPT;
  1917.  
  1918. return NodeFilter.FILTER_SKIP;
  1919. }
  1920. },
  1921. false,
  1922. );
  1923. while (rjNodeTreeWalker.nextNode()) {
  1924. const node = rjNodeTreeWalker.currentNode;
  1925.  
  1926. //Ignore Element which let user input (textarea), input can be ignored because it's not a text node.
  1927. if(node.parentElement.nodeName === "TEXTAREA"){
  1928. continue;
  1929. }
  1930.  
  1931. if (node.parentElement.classList.contains(VOICELINK_CLASS)) {
  1932. Parser.rebindEvents(node.parentElement);
  1933. }else if(node.nodeName === "A") {
  1934. // alert("准备解析链接:" + node.nodeValue)
  1935. Parser.linkifyURL(node);
  1936. }else{
  1937. // alert("准备解析文本:" + node.nodeValue)
  1938. Parser.linkify(node);
  1939. }
  1940. }
  1941. },
  1942.  
  1943. wrapPlaceholder: function (content) {
  1944. let e;
  1945. e = document.createElement("span");
  1946. e.classList = VOICELINK_CLASS;
  1947. e.innerText = content;
  1948. e.classList.add(VOICELINK_IGNORED_CLASS);
  1949. e.setAttribute(RJCODE_ATTRIBUTE, "");
  1950. return e;
  1951. },
  1952.  
  1953. wrapRJCode: function (rjCode) {
  1954. let e;
  1955. e = document.createElement("a");
  1956. e.classList = VOICELINK_CLASS;
  1957. e.href = `https://www.dlsite.com/maniax/work/=/product_id/${rjCode.toUpperCase()}.html`
  1958. e.innerText = rjCode;
  1959. e.target = "_blank";
  1960. e.rel = "noreferrer";
  1961. e.classList.add(VOICELINK_IGNORED_CLASS);
  1962. e.style.setProperty("display", "inline", "important"); //display = "inline !important";
  1963.  
  1964. e.setAttribute(RJCODE_ATTRIBUTE, rjCode.toUpperCase());
  1965. e.setAttribute("voicelink-linkified", "true");
  1966. e.addEventListener("mouseover", Popup.over);
  1967. e.addEventListener("mouseout", Popup.out);
  1968. e.addEventListener("mousemove", Popup.move);
  1969. e.addEventListener("keydown", Popup.keydown);
  1970. //e.addEventListener("keyup", Popup.keyup);
  1971. getXmlHttpRequest()({
  1972. method: "GET",
  1973. url: "http://127.0.0.1:21962/local/" + rjCode,
  1974. responseType: "json",// 指定响应类型为 JSON
  1975. onload: function (response) {
  1976. if (response.status === 200) {
  1977. // 处理响应数据(JSON 对象)
  1978. e.style.textDecoration = 'underline 3px green'
  1979. if ( response.response['media']==1) {
  1980. e.innerText='媒体 '+e.innerText
  1981.  
  1982. }
  1983. if (response.response['CHIT']==1) {
  1984. e.style.textDecoration = 'underline 3px violet'
  1985. e.innerText='繁中 '+e.innerText
  1986. }
  1987. if (response.response['CHIS']==1) {
  1988. e.style.textDecoration = 'underline 3px violet'
  1989. e.innerText='简中 '+e.innerText
  1990. }
  1991. if (response.response['CHIS']!=1 && response.response['CHIT']!=1 && response.response['subtitles']==1) {
  1992. e.style.textDecoration = 'underline 3px red'
  1993. e.innerText='字幕 '+e.innerText
  1994. }
  1995.  
  1996. } else {
  1997. console.log("Request failed with status:", response.status);
  1998. }
  1999. },
  2000. onerror: function (error) {
  2001. console.log("Request failed:", error);
  2002. }
  2003. });
  2004. return e;
  2005. },
  2006.  
  2007. calculateCoverage: function(text){
  2008. const matches = text.match(RJ_REGEX);
  2009. if (!matches) return 0;
  2010. //覆盖大小 = 所有匹配项的长度总和
  2011. const coverSize = matches.reduce((total, current) => total + current.length, 0);
  2012. return (coverSize / text.length) * 100;
  2013. },
  2014.  
  2015. /***
  2016. * 处理直链
  2017. * @param {Node} node
  2018. ***/
  2019. linkifyURL: function(node) {
  2020. const e = node;
  2021. const href = e.href;
  2022. const rjs = href.match(RJ_REGEX);
  2023. const rj = rjs[rjs.length - 1];
  2024. if(!rj) return;
  2025.  
  2026. // alert(`解析链接:${e.nodeValue}`)
  2027.  
  2028. e.classList.add(VOICELINK_CLASS);
  2029. e.setAttribute(RJCODE_ATTRIBUTE, rj.toUpperCase());
  2030. e.addEventListener("mouseover", Popup.over);
  2031. e.addEventListener("mouseout", Popup.out);
  2032. e.addEventListener("mousemove", Popup.move);
  2033. e.addEventListener("keydown", Popup.keydown);
  2034. //e.addEventListener("keyup", Popup.keyup);
  2035. },
  2036.  
  2037. linkify: function (textNode) {
  2038. const nodeOriginalText = textNode.nodeValue;
  2039. const matches = [];
  2040.  
  2041. let insert = settings._s_url_insert_mode;
  2042. let tagA = textNode.parentElement.closest("a");
  2043. let tagB = textNode.parentElement.closest("button");
  2044. let tag = tagA ? tagA : tagB;
  2045. if((!tagA && !tagB) || insert.trim() !== "none" && this.calculateCoverage(tag.innerText) < 71){
  2046. insert = "none";
  2047. }
  2048.  
  2049. let match;
  2050. while (match = RJ_REGEX.exec(nodeOriginalText)) {
  2051. matches.push({
  2052. index: match.index,
  2053. value: match[0],
  2054. });
  2055. }
  2056. if(matches.length === 0) return;
  2057.  
  2058. // alert(`解析文本:${textNode.nodeValue}`)
  2059.  
  2060. // Keep text in text node until first RJ code
  2061. textNode.nodeValue = nodeOriginalText.substring(0, matches[0].index);
  2062. if(insert.startsWith("prefix")){
  2063. //加前缀
  2064. textNode.nodeValue = `${settings._s_url_insert_text}${textNode.nodeValue}`
  2065. }
  2066.  
  2067. // Insert rest of text while linkifying RJ codes
  2068. let prevNode = null;
  2069. for (let i = 0; i < matches.length; ++i) {
  2070. // Insert linkified RJ code
  2071. let code = matches[i].value
  2072. let rjLinkNode = Parser.wrapRJCode(code);
  2073. //保证后续游走时忽略当前节点
  2074. if(insert.startsWith("before_rj")){
  2075. //用导向文本替代RJ号链接,RJ号保留到后面的文本里不变
  2076. rjLinkNode.innerText = settings._s_url_insert_text;
  2077. textNode.parentNode.insertBefore(
  2078. rjLinkNode,
  2079. prevNode ? prevNode.nextSibling : textNode.nextSibling,
  2080. );
  2081. prevNode = rjLinkNode;
  2082. rjLinkNode = Parser.wrapPlaceholder(code);
  2083. }
  2084. textNode.parentNode.insertBefore(
  2085. rjLinkNode,
  2086. prevNode ? prevNode.nextSibling : textNode.nextSibling,
  2087. );
  2088.  
  2089. // Insert text after if there is any
  2090. //找到当前RJ和下一个RJ之间的字符串
  2091. let nextRJ = undefined;
  2092. if (i < matches.length - 1) {
  2093. nextRJ = matches[i + 1].index;
  2094. }
  2095. let substring = nodeOriginalText.substring(matches[i].index + matches[i].value.length, nextRJ);
  2096.  
  2097. if (substring) {
  2098. const subtextNode = document.createTextNode(substring);
  2099. textNode.parentNode.insertBefore(
  2100. subtextNode,
  2101. rjLinkNode.nextElementSibling,
  2102. );
  2103. prevNode = subtextNode;
  2104. }
  2105. else {
  2106. prevNode = rjLinkNode;
  2107. }
  2108. }
  2109. },
  2110.  
  2111. rebindEvents: function (elem) {
  2112. if (elem.nodeName === "A") {
  2113. elem.addEventListener("mouseover", Popup.over);
  2114. elem.addEventListener("mouseout", Popup.out);
  2115. elem.addEventListener("mousemove", Popup.move);
  2116. elem.addEventListener("keydown", Popup.keydown);
  2117. //elem.addEventListener("keyup", Popup.keyup);
  2118. }
  2119. else {
  2120. const voicelinks = elem.querySelectorAll("." + VOICELINK_CLASS);
  2121. for (let i = 0, j = voicelinks.length; i < j; i++) {
  2122. const voicelink = voicelinks[i];
  2123. voicelink.addEventListener("mouseover", Popup.over);
  2124. voicelink.addEventListener("mouseout", Popup.out);
  2125. voicelink.addEventListener("mousemove", Popup.move);
  2126. voicelink.addEventListener("keydown", Popup.keydown);
  2127. //voicelink.addEventListener("keyup", Popup.keyup);
  2128. }
  2129. }
  2130. },
  2131.  
  2132. }
  2133.  
  2134. const DateParser = {
  2135. parseDateStr: function(dateStr, lang){
  2136. dateStr = dateStr.trim().replace(/ /g, "");
  2137. lang = lang.trim().toLowerCase().replace(/_/g, "-");
  2138. let nums = this.parseNumbers(dateStr);
  2139. if(!nums || nums.length < 3 && lang !== "en-us" || nums.length < 2 && lang === "en-us"){
  2140. //数字不够,无法解析
  2141. return null;
  2142. }
  2143.  
  2144. let parsers = [
  2145. this.parseAsiaDateStr,
  2146. this.parseEnglishDateStr,
  2147. this.parseEuropeanDateStr,
  2148. this.parseSpanishDateStr
  2149. ]
  2150. let date = null;
  2151. for (let i = 0; i < parsers.length; i++){
  2152. date = parsers[i](dateStr, nums, lang);
  2153. if(date){
  2154. break;
  2155. }
  2156. }
  2157.  
  2158. return date;
  2159. },
  2160. parseNumbers: function (dateStr){
  2161. let nums = dateStr.match(/\d+/g);
  2162. if(!nums) return null;
  2163.  
  2164. for (let i = 0; i < nums.length; i++) {
  2165. nums[i] = Number(nums[i]);
  2166. }
  2167. return nums;
  2168. },
  2169. parseAsiaDateStr: function(dateStr, nums, lang){
  2170. //2024年10月05日
  2171. //2024년 10월 05일(已去除空格)
  2172. if (!dateStr.match(/\d{4}年\d{1,2}月\d{1,2}日/)
  2173. && !dateStr.match(/\d{4}년\d{1,2}월\d{1,2}일/)) {
  2174. return null;
  2175. }
  2176. return new Date(nums[0], nums[1] - 1, nums[2]);
  2177. },
  2178. parseEnglishDateStr: function(dateStr, nums, lang){
  2179. //Oct/05/2024
  2180. if(!dateStr.match(/[a-zA-Z]{3}\/\d{1,2}\/\d{4}/)){
  2181. return null;
  2182. }
  2183. const monthMap = {
  2184. "Jan": 0, "Feb": 1, "Mar": 2,
  2185. "Apr": 3, "May": 4, "Jun": 5,
  2186. "Jul": 6, "Aug": 7, "Sep": 8,
  2187. "Oct": 9, "Nov": 10, "Dec": 11
  2188. }
  2189. let monthStr = dateStr.substring(0, dateStr.indexOf("/")).toLowerCase();
  2190. monthStr = monthStr[0].toUpperCase() + monthStr.substring(1);
  2191. return new Date(nums[1], monthMap[monthStr], nums[0])
  2192. },
  2193. parseSpanishDateStr: function (dateStr, nums, lang) {
  2194. //10/05/2024
  2195. if(lang !== "es-es" || !dateStr.match(/\d{1,2}\/\d{1,2}\/\d{4}/)){
  2196. return null;
  2197. }
  2198. return new Date(nums[2], nums[0] - 1, nums[1]);
  2199. },
  2200. parseEuropeanDateStr: function (dateStr, nums, lang) {
  2201. //05/10/2024
  2202. if(lang === "es-es" || !dateStr.match(/\d{1,2}\/\d{1,2}\/\d{4}/)){
  2203. return null;
  2204. }
  2205. return new Date(nums[2], nums[1] - 1, nums[0]);
  2206. },
  2207. /***
  2208. 获得带倒计时的文本HTML
  2209. @param date {Date}
  2210. ***/
  2211. getCountDownDateElement: function(date){
  2212. if(!date) return "";
  2213.  
  2214. const today = new Date();
  2215. today.setHours(0);
  2216. today.setMinutes(0);
  2217. today.setSeconds(0);
  2218. today.setMilliseconds(0);
  2219. date.setHours(0);
  2220. date.setMinutes(0);
  2221. date.setSeconds(0);
  2222. date.setMilliseconds(0);
  2223.  
  2224. if(date.getTime() < today.getTime()) return "";
  2225. let days = (date.getTime() - today.getTime()) / (1000 * 60 * 60 * 24);
  2226. let element = document.createElement("span");
  2227. element.innerText = `(Coming in ${days} day${(days > 1 ? "s" : "")})`;
  2228. element.style.setProperty("color", "#ffeb3b", "important");
  2229. element.style.setProperty("font-style", "italic", "important");
  2230. return element;
  2231. //return `<span style="color:#ffeb3b !important; font-size: 16px !important; font-style: italic !important; margin-left: 16px !important"></span>`
  2232. },
  2233. }
  2234.  
  2235. const Popup = {
  2236. popupElement: {
  2237. popup: null,
  2238. not_found: null,
  2239. left_panel: null,
  2240. img: {container: null},
  2241. right_panel: null,
  2242. title: null,
  2243. rj_code: null,
  2244. info_container: null,
  2245. loader: null,
  2246. flag: null,
  2247. tags: null,
  2248. dl_count: null,
  2249. circle_name: null,
  2250. debug: null,
  2251. translator_name: null,
  2252. release_date: null,
  2253. update_date: null,
  2254. age_rating: null,
  2255. scenario: null,
  2256. illustration: null,
  2257. voice_actor: null,
  2258. music: null,
  2259. genre: null,
  2260. file_size: null,
  2261.  
  2262. _state: {
  2263. mouseX: 0,
  2264. mouseY: 0
  2265. }
  2266. },
  2267.  
  2268. makePopup: function (display) {
  2269. const popup = document.createElement("div");
  2270. const ele = Popup.popupElement;
  2271. ele.popup = popup;
  2272.  
  2273. popup.className = `${VOICELINK_CLASS}_voicepopup ${VOICELINK_CLASS}_voicepopup-maniax ` + (getAdditionalPopupClasses() || '');
  2274. popup.id = `${VOICELINK_CLASS}-voice-popup`; // + rjCode;
  2275. popup.style.setProperty("display", display === false ? "none" : "flex", "important"); //display = display === false ? "none" : "flex";
  2276. document.body.appendChild(popup);
  2277.  
  2278. popup.addEventListener("mouseenter", () => {
  2279. popup.setAttribute("mouse-in", "");
  2280. });
  2281. popup.addEventListener("mouseleave", () => {
  2282. popup.removeAttribute("mouse-in");
  2283. })
  2284.  
  2285. const notFoundElement = document.createElement("div");
  2286. ele.not_found = notFoundElement;
  2287. //占满整个popup
  2288. //"display: none; width: 100%; height: 100%";
  2289. notFoundElement.style.setProperty("display", "none", "important");
  2290. notFoundElement.style.setProperty("width", "100%", "important");
  2291. notFoundElement.style.setProperty("height", "100%", "important");
  2292. notFoundElement.innerText = "Work Not Found.";
  2293. popup.appendChild(notFoundElement);
  2294.  
  2295. const leftPanel = document.createElement("div");
  2296. leftPanel.classList.add(`${VOICELINK_CLASS}_left_panel`);
  2297. popup.appendChild(leftPanel);
  2298. ele.left_panel = leftPanel;
  2299.  
  2300. const imgContainer = document.createElement("div")
  2301. imgContainer.classList.add(`${VOICELINK_CLASS}_img_container`);
  2302. ele.img.container = imgContainer;
  2303. leftPanel.appendChild(imgContainer);
  2304.  
  2305. //左下角提示状态栏
  2306. ele.hint = document.createElement("div");
  2307. leftPanel.appendChild(ele.hint);
  2308. ele.hint.id = `${VOICELINK_CLASS}_hint`;
  2309.  
  2310. const rightPanel = document.createElement("div");
  2311. ele.right_panel = rightPanel;
  2312.  
  2313. const titleElement = Popup.createCopyTag("div", "", false, localizePopup(localizationMap.hint_copy_work_title));
  2314. ele.title = titleElement;
  2315. titleElement.classList.add(`${VOICELINK_CLASS}_voice-title`);
  2316. rightPanel.appendChild(titleElement);
  2317.  
  2318. const rjCodeElement = document.createElement("div");
  2319. ele.rj_code = rjCodeElement;
  2320. rjCodeElement.classList.add(`${VOICELINK_CLASS}_rjcode`);
  2321. rightPanel.appendChild(rjCodeElement);
  2322.  
  2323. const infoContainer = document.createElement("div");
  2324. ele.info_container = infoContainer;
  2325. infoContainer.id = `${VOICELINK_CLASS}_info-container`;
  2326. infoContainer.style.setProperty("position", "relative", "important"); //position = "relative !important";
  2327. infoContainer.style.setProperty("min-height", "70px", "important"); //minHeight = "70px !important";
  2328. rightPanel.appendChild(infoContainer);
  2329.  
  2330. const loader = document.createElement("div");
  2331. loader.className = `${VOICELINK_CLASS}_loader`;
  2332. loader.innerHTML = Csp.createHTML(`
  2333. <div class="${VOICELINK_CLASS}_dot"></div>
  2334. <div class="${VOICELINK_CLASS}_dot"></div>
  2335. <div class="${VOICELINK_CLASS}_dot"></div>
  2336. `);
  2337. ele.loader = loader;
  2338. infoContainer.appendChild(loader);
  2339.  
  2340. ele.tags = document.createElement("div");
  2341. infoContainer.appendChild(ele.tags);
  2342.  
  2343. ele.dl_count = document.createElement("div");
  2344. ele.circle_name = document.createElement("div");
  2345. ele.debug = document.createElement("div");
  2346. ele.translator_name = document.createElement("div");
  2347. ele.release_date = document.createElement("div");
  2348. ele.update_date = document.createElement("div");
  2349. ele.age_rating = document.createElement("div");
  2350. ele.scenario = document.createElement("div");
  2351. ele.illustration = document.createElement("div");
  2352. ele.voice_actor = document.createElement("div");
  2353. ele.music = document.createElement("div");
  2354. ele.genre = document.createElement("div");
  2355. ele.file_size = document.createElement("div");
  2356.  
  2357. rightPanel.style.setProperty("padding-bottom", "3px", "important"); //paddingBottom = "3px !important";
  2358. rightPanel.style.setProperty("flex-grow", "1", "important"); //flexGrow = "1 !important";
  2359. popup.appendChild(rightPanel);
  2360. popup.insertBefore(leftPanel, popup.childNodes[0]);
  2361. },
  2362.  
  2363. updatePopup: function(e, rjCode, isParent=false) {
  2364. const ele = Popup.popupElement;
  2365. const popup = ele.popup;
  2366. popup.className = `${VOICELINK_CLASS}_voicepopup ${VOICELINK_CLASS}_voicepopup-maniax ` + (getAdditionalPopupClasses() || '');
  2367. // popup.id = "voice-" + rjCode;
  2368. popup.style.setProperty("display", "flex", "important"); //= "display: flex";
  2369. popup.setAttribute(RJCODE_ATTRIBUTE, rjCode);
  2370.  
  2371. //------检查作品存在情况------
  2372. let workFound = true;
  2373. Popup.setFoundState(true);
  2374. WorkPromise.getFound(rjCode).then(async found => {
  2375. if(rjCode !== popup.getAttribute(RJCODE_ATTRIBUTE)) return;
  2376.  
  2377. if(found){
  2378. //找到则直接返回交给下一级处理
  2379. return {found: true, parentRJ: rjCode};
  2380. }
  2381.  
  2382. //没找到则尝试找到父作品的RJ号,填补子作品信息的缺失
  2383. let parentRJ = await WorkPromise.getParentRJ(rjCode);
  2384. if(parentRJ === rjCode || !parentRJ) {
  2385. return {found: false, parentRJ: rjCode};
  2386. }
  2387. found = await WorkPromise.getFound(parentRJ);
  2388. return {found: found, parentRJ: parentRJ};
  2389.  
  2390. }).then((state) => {
  2391. if(rjCode !== popup.getAttribute(RJCODE_ATTRIBUTE)) return;
  2392.  
  2393. const found = state.found;
  2394. const rj = state.parentRJ;
  2395. if(found && rj !== rjCode){
  2396. //如果找到了父作品的信息但子作品找不到,就重新update
  2397. Popup.updatePopup(e, rj, true);
  2398. return;
  2399. }
  2400.  
  2401. ele.not_found.style.setProperty("display", found ? "none" : "block", "important"); //display = found ? "none" : "block";
  2402. Popup.setFoundState(found);
  2403. workFound = found;
  2404. });
  2405.  
  2406. //------检查是否为女性向------
  2407. WorkPromise.getGirls(rjCode).then(isGirls => {
  2408. if(rjCode !== popup.getAttribute(RJCODE_ATTRIBUTE)) return;
  2409. if(isGirls) popup.className += (` ${VOICELINK_CLASS}_voicepopup-girls`)
  2410. }).catch(e => {});
  2411.  
  2412. //------获取作品封面------
  2413. const imgContainer = ele.img.container;
  2414.  
  2415. //NSFW模糊等级
  2416. const blur_map = {
  2417. low: "6px",
  2418. medium: "12px",
  2419. high: "24px"
  2420. };
  2421.  
  2422. //先对Container内的所有img进行隐藏
  2423. for (let i = 0; i < imgContainer.childNodes.length; ++i) {
  2424. imgContainer.childNodes[i].style.setProperty("display", "none", "important"); //display = "none !important";
  2425. }
  2426.  
  2427. //NOTE: 注意这里可能因为快速的多次获取导致同时加载两个图片,可使用占位符来预先占用图片位置
  2428. new Promise((resolve, reject) => {
  2429. let img = ele.img[rjCode];
  2430. if(img) resolve(img);
  2431. else throw Error("首次加载图片");
  2432. }).catch(_ => {
  2433. //首次加载图片,对图片添加占位
  2434. ele.img[rjCode] = 1;
  2435. return WorkPromise.getImgLink(rjCode);
  2436. }).then(link => {
  2437. if(typeof link !== "string"){
  2438. //图片已经加载过,传递的是img,不通过当前then
  2439. return link; //实际上是img
  2440. }
  2441.  
  2442. if(rjCode !== popup.getAttribute(RJCODE_ATTRIBUTE)) {
  2443. //清除占位
  2444. ele.img[rjCode] = null;
  2445. return null;
  2446. }
  2447. let img;
  2448. try{
  2449. img = GM_addElement("img", {
  2450. src: link,
  2451. });
  2452. if(!img) { // noinspection ExceptionCaughtLocallyJS
  2453. throw new Error("API调用生成失败");
  2454. }
  2455. }catch (e) {
  2456. img = document.createElement("img");
  2457. img.src = link;
  2458. }
  2459.  
  2460. imgContainer.appendChild(img);
  2461. console.warn("添加封面!")
  2462. ele.img[rjCode] = img;
  2463.  
  2464. //开启动画
  2465. if(settings._s_sfw_blur_transition){
  2466. img.style.setProperty("transition", "all 0.3s", "important");
  2467. }
  2468.  
  2469. //鼠标移动上去解除模糊
  2470. img.addEventListener("mouseenter", e => {
  2471. if(!settings._s_sfw_remove_when_hover){
  2472. return;
  2473. }
  2474. img.style.setProperty("filter", "inherit", "important");
  2475. });
  2476. img.addEventListener("mouseleave", e => {
  2477. if(settings._s_sfw_mode){
  2478. img.style.setProperty("filter", `blur(${blur_map[settings._s_sfw_blur_level]})`, "important");
  2479. }else{
  2480. img.style.setProperty("filter", "inherit", "important");
  2481. }
  2482. });
  2483.  
  2484. return img;
  2485. }).then(img => {
  2486. if(!(img instanceof HTMLElement)) return;
  2487. img.style.setProperty("display", "block", "important"); //display = "block"
  2488.  
  2489. //设置NSFW模糊
  2490. if(settings._s_sfw_mode){
  2491. img.style.setProperty("filter", `blur(${blur_map[settings._s_sfw_blur_level]})`, "important");
  2492. }else{
  2493. img.style.setProperty("filter", "inherit", "important");
  2494. }
  2495. }).catch(e => {
  2496. //清理并在下次重试
  2497. if(ele.img[rjCode] instanceof HTMLElement) img.remove();
  2498. ele.img[rjCode] = null;
  2499. console.error(e)
  2500. });
  2501.  
  2502. //------设置hint可见------
  2503. ele.hint.style.setProperty("display", "block", "important");
  2504.  
  2505. //------设置标题------
  2506. const titleElement = ele.title;
  2507. titleElement.innerText = "Loading...";
  2508. titleElement.setHint(localizePopup(localizationMap.hint_copy_work_title));
  2509. titleElement.setCopyText(null);
  2510. titleElement.setSecondaryCopyText(null);
  2511. WorkPromise.getWorkTitle(rjCode).then(title => {
  2512. if(rjCode !== popup.getAttribute(RJCODE_ATTRIBUTE)) return;
  2513. titleElement.innerText = title;
  2514. titleElement.setCopyText(title);
  2515. titleElement.setSecondaryCopyText(convertToValidFileName(title));
  2516. }).catch(_ => {
  2517. if(rjCode !== popup.getAttribute(RJCODE_ATTRIBUTE)) return;
  2518. titleElement.innerHTML = Csp.createHTML("");
  2519. })
  2520.  
  2521. //------设置RJ号------
  2522. const rjCodeElement = ele.rj_code;
  2523. rjCodeElement.innerHTML = Csp.createHTML(`[ ${isParent ? " ↑ " : ""}<span class="${VOICELINK_IGNORED_CLASS}" style="font-weight: bold !important;text-decoration-line: underline !important;">${rjCode}</span> ]`);
  2524. WorkPromise.getRJChain(rjCode).then(chain => {
  2525. if(rjCode !== popup.getAttribute(RJCODE_ATTRIBUTE)) return;
  2526. rjCodeElement.innerText = "[ ";
  2527. //构造chain
  2528. for (let i = 0; i < chain.length; i++) {
  2529. const rj = chain[i];
  2530. let e = Popup.createCopyTag("span", rj);
  2531. e.innerText = rj;
  2532. e.classList.add(VOICELINK_IGNORED_CLASS);
  2533. if(i === 0) {
  2534. //第一个元素,也就是当前RJ号
  2535. e.style.setProperty("font-weight", "bold", "important");
  2536. e.style.setProperty("text-decoration", "underline", "important");
  2537. }else{
  2538. rjCodeElement.appendChild(document.createTextNode(" → "));
  2539. }
  2540. rjCodeElement.appendChild(e);
  2541. }
  2542. rjCodeElement.appendChild(document.createTextNode(" ]"));
  2543. });
  2544.  
  2545. //清除原有信息并展示加载界面
  2546. for(let child of [...this.popupElement.info_container.children]){
  2547. if(child === this.popupElement.loader) continue;
  2548. child.remove();
  2549. }
  2550. ele.loader.style.setProperty("display", "flex", "important"); //display = "flex !important";
  2551. WorkPromise.getWorkCategory(rjCode).then(category => {
  2552. if(rjCode !== popup.getAttribute(RJCODE_ATTRIBUTE)) return;
  2553. this.set_info_container(rjCode, category);
  2554. }).catch(e => {
  2555. if (rjCode !== popup.getAttribute(RJCODE_ATTRIBUTE)) return;
  2556. //默认other
  2557. this.set_info_container(rjCode, "other");
  2558. });
  2559.  
  2560. Popup.move(e);
  2561. },
  2562.  
  2563. setFoundState(found){
  2564. const ele = Popup.popupElement;
  2565.  
  2566. ele.not_found.style.setProperty("display", found ? "none" : "block", "important");
  2567. //ele.img.container.style.setProperty("display", found && !Popup.hideImg ? "block" : "none", "important");
  2568. ele.left_panel.style.setProperty("display", found ? "flex" : "none", "important");
  2569. ele.right_panel.style.setProperty("display", found ? "block" : "none", "important");
  2570. ele.title.style.setProperty("display", found ? "block" : "none", "important");
  2571. ele.rj_code.style.setProperty("display", found ? "block" : "none", "important");
  2572. ele.info_container.style.setProperty("display", found ? "block" : "none", "important");
  2573. ele.hint.style.setProperty("display", found ? "block" : "none", "important");
  2574. /*ele.dl_count.style.setProperty("display", found ? "block" : "none", "important");
  2575. ele.circle_name.style.setProperty("display", found ? "block" : "none", "important");
  2576. ele.debug.style.setProperty("display", found ? "block" : "none", "important");
  2577. ele.translator_name.style.setProperty("display", found ? "block" : "none", "important");
  2578. ele.release_date.style.setProperty("display", found ? "block" : "none", "important");
  2579. ele.update_date.style.setProperty("display", found ? "block" : "none", "important");
  2580. ele.age_rating.style.setProperty("display", found ? "block" : "none", "important");
  2581. ele.voice_actor.style.setProperty("display", found ? "block" : "none", "important");
  2582. ele.music.style.setProperty("display", found ? "block" : "none", "important");
  2583. ele.genre.style.setProperty("display", found ? "block" : "none", "important");
  2584. ele.file_size.style.setProperty("display", found ? "block" : "none", "important");*/
  2585. },
  2586.  
  2587. /**
  2588. * 创建可复制标签
  2589. * @param tag {string|HTMLElement} 标签名或标签对象(如果为标签对象,则会将对象原地转化成可复制标签)
  2590. * @param copyText {string} 需要复制的文本
  2591. * @param isTitle {boolean} 是否为标题元素(使用特殊class)
  2592. * @param hint {string} 提示栏显示的提示文本
  2593. * @returns {HTMLElement} 创建/转换后的标签
  2594. */
  2595. createCopyTag: function (tag, copyText, isTitle = false, hint = isTitle ? localizePopup(localizationMap.hint_copy_all) : localizePopup(localizationMap.hint_copy)) {
  2596. tag = (typeof tag === "string") ? document.createElement(tag) : tag;
  2597. if(isTitle) tag.classList.add("info-title");
  2598.  
  2599. //添加自定义方法
  2600. tag.getCopyText = () => tag.getAttribute("copy-text");
  2601. tag.setCopyText = text => {
  2602. if(!text){
  2603. tag.removeAttribute("copy-text");
  2604. return;
  2605. }
  2606. tag.setAttribute("copy-text", text);
  2607. };
  2608.  
  2609. tag.getSecondaryCopyText = () => tag.getAttribute("sec-copy-text");
  2610. tag.setSecondaryCopyText = text => {
  2611. if(!text){
  2612. tag.removeAttribute("sec-copy-text");
  2613. return;
  2614. }
  2615. tag.setAttribute("sec-copy-text", text);
  2616. };
  2617.  
  2618. tag.getHint = () => tag.getAttribute("hint");
  2619. tag.setHint = hint => {
  2620. tag.setAttribute("hint", hint);
  2621. };
  2622.  
  2623. tag.setCopyText(copyText);
  2624. tag.setHint(hint);
  2625. tag.addEventListener("click", e => {
  2626. const attr = e.altKey ? "sec-copy-text" : "copy-text";
  2627. if(!tag.hasAttribute(attr)) return;
  2628. GM_setClipboard(tag.getAttribute(attr), "text")?.finally();
  2629. // navigator.clipboard.writeText(tag.getAttribute(attr)).finally();
  2630. });
  2631. tag.addEventListener("mouseenter", e => {
  2632. let hint = tag.getHint();
  2633. Popup.popupElement.hint.innerText = hint ? hint : Popup.popupElement.hint.innerText;
  2634. })
  2635. return tag;
  2636. },
  2637.  
  2638. /**
  2639. * 显示某行的信息
  2640. * @param rjCode {string} 信息对应的RJ号
  2641. * @param id {string} 信息对应的ID
  2642. * @param rowElement {HTMLElement} 行对应的Element元素
  2643. * @param title {string} 行信息标题
  2644. * @param contentProvider {Promise<any|Array|HTMLElement>} 无参内容Provider,返回字符串/字符串列表等用于生成文本
  2645. * @param suffixProvider {Promise<HTMLElement>} 无参后缀Provider,返回一个Element用于放在Content后面
  2646. * @param contentSeperator {string, HTMLElement} Content如果是列表,则该内容为作为分隔符
  2647. * @param contentSeperatorText {string} 如果采用HTMLElement的分隔符,则在复制的时候需要有一个文本表示
  2648. */
  2649. set_info_row: function (rjCode, id, rowElement, title, contentProvider, suffixProvider, contentSeperator = " ", contentSeperatorText = undefined ){
  2650. const settingId = `_s_${id}`;
  2651. //如果设置了不展示信息,或信息没在设置中定义,则不显示
  2652. if(!settings[settingId]) return;
  2653.  
  2654. const ele = Popup.popupElement;
  2655. const popup = ele.popup;
  2656. const titleElement = Popup.createCopyTag("span", "", true);
  2657. titleElement.innerText = title;
  2658. const contentElement = Popup.createCopyTag("span", null);
  2659. contentElement.innerText = "Loading...";
  2660.  
  2661. rowElement.innerHTML = Csp.createHTML("");
  2662. rowElement.appendChild(titleElement);
  2663. rowElement.appendChild(contentElement);
  2664.  
  2665. contentProvider.then(contents => {
  2666. if(rjCode !== popup.getAttribute(RJCODE_ATTRIBUTE)) return;
  2667. if(!Array.isArray(contents)){
  2668. //单个结果转化成列表
  2669. contents = [contents];
  2670. }
  2671.  
  2672. //处理结果列表
  2673. contentElement.setCopyText(null);
  2674. contentElement.innerText = "";
  2675. //以指定的分隔符文本为准,不指定分隔符文本再使用分隔符内的文本
  2676. let sepText = contentSeperatorText ? contentSeperatorText : contentSeperator.toString();
  2677. let sep;
  2678. if(typeof contentSeperator === "string"){
  2679. sep = document.createElement("span");
  2680. sep.innerText = contentSeperator;
  2681. }else{
  2682. sep = contentSeperator;
  2683. }
  2684.  
  2685. let contentsText = [];
  2686. for (let i = 0; i < contents.length; i++) {
  2687. let c = contents[i];
  2688. if(i > 0) {
  2689. //如果Seperator是Element,则直接复制一个出来添加,否则创建一个span然后把内容转换成文本放进去。
  2690. contentElement.appendChild(sep.cloneNode(true));
  2691. }
  2692.  
  2693. let element;
  2694. if(c instanceof HTMLElement){
  2695. element = c;
  2696. }else{
  2697. element = Popup.createCopyTag("a", c);
  2698. element.innerText = c;
  2699. }
  2700.  
  2701. //将复制文本加入复制列表
  2702. const copyText = element.getAttribute("copy-text");
  2703. if(copyText) contentsText.push(copyText);
  2704.  
  2705. contentElement.appendChild(element);
  2706. }
  2707.  
  2708. //为标题添加复制文本
  2709. titleElement.setCopyText(contentsText.join(sepText));
  2710. }).catch(e => {
  2711. if(rjCode !== popup.getAttribute(RJCODE_ATTRIBUTE)) return;
  2712. rowElement.innerHTML = Csp.createHTML("");
  2713. //console.error(e);
  2714. }).finally(() => {
  2715. Popup.adjustPopup(ele._state.mouseX, ele._state.mouseY, true);
  2716. });
  2717.  
  2718. if(suffixProvider){
  2719. suffixProvider.then((element) => {
  2720. if(rjCode !== popup.getAttribute(RJCODE_ATTRIBUTE)) return;
  2721. rowElement.appendChild(element);
  2722. }).catch(_ => {});
  2723. }
  2724.  
  2725. return rowElement;
  2726. },
  2727. set_dl_count: function (rjCode, category){
  2728. const id = `${category}__dl_count`;
  2729. const element = Popup.set_info_row(rjCode, id, Popup.popupElement.dl_count, localizePopup(localizationMap.dl_count),
  2730. WorkPromise.getDLCount(rjCode));
  2731. if(element) Popup.popupElement.info_container.appendChild(element);
  2732. },
  2733. set_circle_name: function (rjCode, category){
  2734. const id = `${category}__circle_name`;
  2735. const element = Popup.set_info_row(rjCode, id, Popup.popupElement.circle_name, localizePopup(localizationMap.circle_name),
  2736. WorkPromise.getCircle(rjCode), null);
  2737. if(element) Popup.popupElement.info_container.appendChild(element);
  2738. },
  2739. set_translator_name: function (rjCode, category){
  2740. const id = `${category}__translator_name`;
  2741. const element = Popup.set_info_row(rjCode, id, Popup.popupElement.translator_name,
  2742. localizePopup(localizationMap.translator_name), WorkPromise.getTranslatorName(rjCode), null);
  2743. if(element) Popup.popupElement.info_container.appendChild(element);
  2744. },
  2745. set_release_date: function (rjCode, category){
  2746. const id = `${category}__release_date`;
  2747. const element = Popup.set_info_row(rjCode, id, Popup.popupElement.release_date, localizePopup(localizationMap.release_date),
  2748. WorkPromise.getReleaseDate(rjCode).then(async (date) => {
  2749. const [dateStr, isAnnounce] = date;
  2750. const e = Popup.createCopyTag("a", dateStr);
  2751. e.innerText = dateStr;
  2752. if(isAnnounce) e.style.setProperty("color", "gold", "important");
  2753. return e;
  2754. }), WorkPromise.getReleaseCountDownElement(rjCode).then(element => {
  2755. element.style.setProperty("margin-left", "16px", "important");
  2756. return element;
  2757. }));
  2758.  
  2759. if(element) Popup.popupElement.info_container.appendChild(element);
  2760. },
  2761. set_update_date: function (rjCode, category){
  2762. const id = `${category}__update_date`;
  2763. const element = Popup.set_info_row(rjCode, id, Popup.popupElement.update_date, localizePopup(localizationMap.update_date),
  2764. WorkPromise.getUpdateDate(rjCode));
  2765. if(element) Popup.popupElement.info_container.appendChild(element);
  2766. },
  2767. set_age_rating: function (rjCode, category){
  2768. const id = `${category}__age_rating`;
  2769. const element = Popup.set_info_row(rjCode, id, Popup.popupElement.age_rating, localizePopup(localizationMap.age_rating),
  2770. WorkPromise.getAgeRating(rjCode).then(rating => {
  2771. let ratingClass = `${VOICELINK_CLASS}_age-all`;
  2772. if(rating.includes("18")){
  2773. ratingClass = `${VOICELINK_CLASS}_age-18`;
  2774. }
  2775. let e = Popup.createCopyTag("a", rating);
  2776. e.innerText = rating;
  2777. e.classList.add(ratingClass);
  2778. return e;
  2779. }));
  2780. if(element) Popup.popupElement.info_container.appendChild(element);
  2781. },
  2782. set_scenario: function (rjCode, category){
  2783. const id = `${category}__scenario`;
  2784. const element = Popup.set_info_row(rjCode, id, Popup.popupElement.scenario, localizePopup(localizationMap.scenario),
  2785. WorkPromise.getScenario(rjCode), null, " / ");
  2786. if(element) Popup.popupElement.info_container.appendChild(element);
  2787. },
  2788. set_illustration: function (rjCode, category){
  2789. const id = `${category}__illustration`;
  2790. const element = Popup.set_info_row(rjCode, id, Popup.popupElement.illustration, localizePopup(localizationMap.illustration),
  2791. WorkPromise.getIllustrator(rjCode), null, " / ");
  2792. if(element) Popup.popupElement.info_container.appendChild(element);
  2793. },
  2794. set_voice_actor: function (rjCode, category){
  2795. const id = `${category}__voice_actor`;
  2796. const element = Popup.set_info_row(rjCode, id, Popup.popupElement.voice_actor, localizePopup(localizationMap.voice_actor),
  2797. WorkPromise.getCV(rjCode), null, " / ");
  2798. if(element) Popup.popupElement.info_container.appendChild(element);
  2799. },
  2800. set_music: function (rjCode, category) {
  2801. const id = `${category}__music`;
  2802. const element = Popup.set_info_row(rjCode, id, Popup.popupElement.music, localizePopup(localizationMap.music),
  2803. WorkPromise.getMusic(rjCode), null, " / ");
  2804. if(element) Popup.popupElement.info_container.appendChild(element);
  2805. },
  2806. set_genre: function (rjCode, category){
  2807. const id = `${category}__genre`;
  2808. const element = Popup.set_info_row(rjCode, id, Popup.popupElement.genre, localizePopup(localizationMap.genre),
  2809. WorkPromise.getTags(rjCode), null, "\u3000");
  2810. if(element) Popup.popupElement.info_container.appendChild(element);
  2811. },
  2812. set_file_size: function (rjCode, category){
  2813. const id = `${category}__file_size`;
  2814. const element = Popup.set_info_row(rjCode, id, Popup.popupElement.file_size, localizePopup(localizationMap.file_size),
  2815. WorkPromise.getFileSize(rjCode));
  2816. if(element) Popup.popupElement.info_container.appendChild(element);
  2817. },
  2818.  
  2819. get_tag: function (text, tagClass) {
  2820. if(!tagClass.startsWith(`${VOICELINK_CLASS}_`)){
  2821. tagClass = `${VOICELINK_CLASS}_${tagClass}`
  2822. }
  2823. const tag = document.createElement("span");
  2824. tag.classList.add(`${VOICELINK_CLASS}_tag_tight`);
  2825. tag.classList.add(tagClass);
  2826. tag.innerText = text;
  2827. return tag;
  2828. },
  2829. get_tag_rate: async function (rjCode) {
  2830. let rate = await WorkPromise.getRateAvg(rjCode);
  2831. let cot = await WorkPromise.getRateCount(rjCode);
  2832. return Popup.get_tag(`${rate.toFixed(2)}★` + (settings._s_show_rate_count ? ` (${cot})` : ""), "tag-yellow");
  2833. },
  2834. get_tag_no_longer_available: async function (rjCode) {
  2835. let sale = await WorkPromise.getSale(rjCode);
  2836. if(sale) return;
  2837. return Popup.get_tag(localizePopup(localizationMap.tag_no_longer_available),
  2838. "tag-gray");
  2839. },
  2840. get_tag_work_type: async function (rjCode) {
  2841. let type = await WorkPromise.getWorkTypeText(rjCode);
  2842. let tagClass = "tag-gray";
  2843. switch (type) {
  2844. case localizePopup(localizationMap.work_type_game):
  2845. tagClass = "tag-purple";
  2846. break;
  2847. case localizePopup(localizationMap.work_type_comic):
  2848. tagClass = "tag-green";
  2849. break;
  2850. case localizePopup(localizationMap.work_type_illustration):
  2851. tagClass = "tag-teal";
  2852. break;
  2853. case localizePopup(localizationMap.work_type_novel):
  2854. tagClass = "tag-gray";
  2855. break;
  2856. case localizePopup(localizationMap.work_type_video):
  2857. tagClass = "tag-darkblue";
  2858. break;
  2859. case localizePopup(localizationMap.work_type_voice):
  2860. tagClass = "tag-orange";
  2861. break;
  2862. case localizePopup(localizationMap.work_type_music):
  2863. tagClass = "tag-yellow";
  2864. break;
  2865. case localizePopup(localizationMap.work_type_tool):
  2866. tagClass = "tag-gray";
  2867. break;
  2868. case localizePopup(localizationMap.work_type_voice_comic):
  2869. tagClass = "tag-blue";
  2870. break;
  2871. case localizePopup(localizationMap.work_type_other):
  2872. tagClass = "tag-gray";
  2873. break;
  2874. default:
  2875. tagClass = "tag-gray";
  2876. break;
  2877. }
  2878. return Popup.get_tag(type, tagClass);
  2879. },
  2880. get_tag_translatable: async function (rjCode) {
  2881. let able = await WorkPromise.getTranslatable(rjCode);
  2882. if(!able) return;
  2883. return Popup.get_tag(localizePopup(localizationMap.tag_translatable),
  2884. "tag-green");
  2885. },
  2886. get_tag_not_translatable: async function (rjCode) {
  2887. let able = await WorkPromise.getTranslatable(rjCode);
  2888. let translated = await WorkPromise.getTranslated(rjCode);
  2889. if(able || translated) return;
  2890. return Popup.get_tag(localizePopup(localizationMap.tag_not_translatable),
  2891. "tag-red");
  2892. },
  2893. get_tag_translated: async function (rjCode) {
  2894. let translated = await WorkPromise.getTranslated(rjCode);
  2895. if(!translated) return;
  2896. return Popup.get_tag(localizePopup(localizationMap.tag_translated), "tag-teal");
  2897. },
  2898. get_tag_bonus_work: async function (rjCode) {
  2899. let bonus = await WorkPromise.getBonus(rjCode);
  2900. if(!bonus) return;
  2901. return Popup.get_tag(localizePopup(localizationMap.tag_bonus_work),
  2902. "tag-yellow");
  2903. },
  2904. get_tag_has_bonus: async function (rjCode) {
  2905. let has = await WorkPromise.getHasBonus(rjCode);
  2906. if(!has) return;
  2907. return Popup.get_tag(localizePopup(localizationMap.tag_has_bonus),
  2908. "tag-orange");
  2909. },
  2910. get_tag_language_support: async function (rjCode) {
  2911. const lang = await WorkPromise.getLanguages(rjCode);
  2912. if(!lang || lang.length <= 0){
  2913. return;
  2914. }
  2915. let txt = "";
  2916. lang.forEach(l => {
  2917. txt += ` | ${l}`;
  2918. });
  2919. txt = txt.substring(3);
  2920. return Popup.get_tag(txt, "tag-pink");
  2921. },
  2922. get_tag_file_format: async function (rjCode) {
  2923. const format = await WorkPromise.getFileFormats(rjCode);
  2924. if(!format || format.length <= 0){
  2925. return;
  2926. }
  2927. let txt = "";
  2928. format.forEach(f => {
  2929. txt += ` | ${f}`;
  2930. });
  2931. txt = txt.substring(3);
  2932. return Popup.get_tag(txt, "tag-darkblue");
  2933. },
  2934. get_tag_ai: async function (rjCode) {
  2935. const ai = await WorkPromise.getAIUsedText(rjCode);
  2936. if(!ai) return;
  2937. return Popup.get_tag(ai, "tag-purple");
  2938. },
  2939. get_translatable_tag: async function (rjCode, tag_id) {
  2940. if(settings[`_s_${tag_id}`] !== true) return;
  2941.  
  2942. if(tag_id.startsWith("tag_")) tag_id = tag_id.substring(4);
  2943. const t = await WorkPromise.getWorkPromise(rjCode).translatable;
  2944. const stat = t[tag_id];
  2945.  
  2946. const hasRequest = stat.request > 0;
  2947. const hasSale = stat.sale > 0;
  2948. const displayCount = stat.agree || hasRequest || hasSale;
  2949. const lang = tag_id.substring("translation_request_".length);
  2950. const tag = Popup.get_tag(`${localizePopup(localizationMap[`language_${lang}_abbr`])}${stat.agree ? "" : (stat.agree === false ? " ✘" : " ?")} ${displayCount ? ` ${stat.request}-${stat.sale}` : ""}`,
  2951. hasSale ? "tag-green" : (hasRequest ? "tag-orange" : "tag-gray"));
  2952. tag.classList.add(`${VOICELINK_CLASS}_tag_small`);
  2953. return tag;
  2954. },
  2955.  
  2956. get_tag_container: function (rjCode, tag_list) {
  2957. const container = document.createElement("div");
  2958. container.classList.add(`${VOICELINK_CLASS}_tags`);
  2959. for (const tag_id of tag_list) {
  2960. if(settings[`_s_${tag_id}`] !== true) continue;
  2961.  
  2962. let shadowTag = document.createElement("span");
  2963. shadowTag.style.setProperty("display", "none", "important"); //display = "none !important";
  2964. shadowTag.setAttribute("data-id", tag_id);
  2965. container.appendChild(shadowTag);
  2966.  
  2967. let tag_get = this[`get_${tag_id}`];
  2968. tag_get(rjCode).then(tag => {
  2969. if(tag){
  2970. container.insertBefore(tag, shadowTag);
  2971. shadowTag.remove();
  2972. }
  2973. });
  2974. }
  2975. return container;
  2976. },
  2977. get_translatable_tag_container: function (rjCode, tag_list) {
  2978. const container = document.createElement("div");
  2979. container.classList.add(`${VOICELINK_CLASS}_tags`);
  2980. container.style.setProperty("margin-top", "0", "important"); //marginTop = "0 !important";
  2981. for (const tag_id of tag_list) {
  2982. let shadowTag = document.createElement("span");
  2983. shadowTag.style.setProperty("display", "none", "important"); //display = "none !important";
  2984. shadowTag.setAttribute("data-id", tag_id);
  2985. container.appendChild(shadowTag);
  2986.  
  2987. Popup.get_translatable_tag(rjCode, tag_id).then(tag => {
  2988. if(tag){
  2989. container.insertBefore(tag, shadowTag);
  2990. shadowTag.remove();
  2991. }
  2992. }).catch(e => {});
  2993. }
  2994. return container;
  2995. },
  2996.  
  2997. //整合顺序
  2998. set_info_container: function (rjCode, category) {
  2999. //清除上次的信息
  3000. for(let child of [...this.popupElement.info_container.children]){
  3001. if(child === this.popupElement.loader) {
  3002. child.style.setProperty("display", "none", "important"); //display = "none !important";
  3003. continue;
  3004. }
  3005. child.remove();
  3006. }
  3007.  
  3008. //TAG部分
  3009. const infoContainer = this.popupElement.info_container;
  3010. let tagContainer = null;
  3011. if(settings._s_tag_main_switch === true){
  3012. const container = this.get_tag_container(rjCode,
  3013. settings[`_s_tag_display_order`]);
  3014. tagContainer = container;
  3015. infoContainer.appendChild(container);
  3016. }
  3017.  
  3018. //翻译申请情况
  3019. const shadowContainer = document.createElement("div");
  3020. shadowContainer.style.setProperty("display", "none", "important"); //display = "none !important";
  3021. infoContainer.appendChild(shadowContainer);
  3022. WorkPromise.getTranslatable(rjCode).then(able => {
  3023. if(rjCode !== Popup.popupElement.popup.getAttribute(RJCODE_ATTRIBUTE)) return;
  3024. if(able && settings._s_tag_translation_request === true){
  3025. const translatableContainer = this.get_translatable_tag_container(rjCode,
  3026. settings._s_tag_translation_request_display_order);
  3027. infoContainer.insertBefore(translatableContainer, shadowContainer);
  3028. shadowContainer.remove();
  3029. }
  3030. }).catch(e => {});
  3031.  
  3032. //信息部分
  3033. const order = settings[`_s_${category}__info_display_order`];
  3034. order.forEach(id => {
  3035. try{
  3036. id = id.substring(id.indexOf("__") + 2);
  3037. this["set_" + id](rjCode, category);
  3038. }catch (e) {
  3039. console.error(e);
  3040. }
  3041. });
  3042.  
  3043. const debugElement = document.createElement("div");
  3044. this.popupElement.info_container.appendChild(debugElement);
  3045. WorkPromise.getDebug(rjCode).then(t => {
  3046. debugElement.innerHTML = Csp.createHTML(t);
  3047. });
  3048. },
  3049.  
  3050. //调整弹框位置
  3051. adjustPopup: function (mouseX, mouseY, force = false){
  3052. // console.log("定位修正")
  3053.  
  3054. //定位修正
  3055. const popup = Popup.popupElement.popup;
  3056. const ele = Popup.popupElement;
  3057. if(!Popup.pinRJ || force){
  3058. if (popup.offsetWidth + mouseX + 10 < window.innerWidth - 10) {
  3059. popup.style.setProperty("left", (mouseX + 10) + "px", "important");
  3060. }
  3061. else {
  3062. popup.style.setProperty("left", (window.innerWidth - popup.offsetWidth - 10) + "px", "important");
  3063. }
  3064. }
  3065.  
  3066. let rect = popup.getBoundingClientRect();
  3067. if(!Popup.pinRJ || force || rect.top < 0 || rect.bottom > window.innerHeight){
  3068. if (mouseY > window.innerHeight / 2) {
  3069. let top = Math.max(mouseY - popup.offsetHeight - 8, 0);
  3070. popup.style.setProperty("top", top + "px", "important");
  3071. }
  3072. else {
  3073. let top = Math.min(mouseY + 20, window.innerHeight - popup.offsetHeight);
  3074. popup.style.setProperty("top", top + "px", "important");
  3075. }
  3076. }
  3077.  
  3078. //大小修正
  3079. let currentFontSize = popup.computedStyleMap().get("font-size").toString();
  3080. currentFontSize = parseFloat(currentFontSize.substring(0, Math.max(currentFontSize.indexOf("px"), 1)));
  3081. const sizeLevel = [15, 14.5, 14, 13.5, 13, 12.5, 12];
  3082. let size = sizeLevel[sizeLevel.length - 1];
  3083. if(popup.offsetHeight > window.innerHeight){
  3084. //计算popup的高度与window高度的比值,找到离它最相近且更大的当前字体大小和sizeLevel的比值
  3085. for (const s of sizeLevel) {
  3086. if(popup.offsetHeight / window.innerHeight < currentFontSize / s){
  3087. size = s;
  3088. break;
  3089. }
  3090. }
  3091. popup.style.setProperty("font-size", size + "px", "important");
  3092. }
  3093.  
  3094. //封面图位置修正
  3095. ele.img.container.style.top = `${Math.max(0, -popup.offsetTop)}px`;
  3096. },
  3097.  
  3098. pinRJ: undefined,
  3099. setPinState: function (rjCode, pin, close = true){
  3100. const ele = Popup.popupElement;
  3101. const popup = ele.popup;
  3102. if(!pin){
  3103. //关闭弹框
  3104. popup.style.setProperty("pointer-events", "none", "important");
  3105. Popup.pinRJ = undefined;
  3106. popup.removeAttribute("pin");
  3107.  
  3108. if(close) popup.style.setProperty("display", "none", "important");
  3109.  
  3110. //取消注册自动关闭监听
  3111. document.removeEventListener("keyup", Popup.keyup);
  3112. document.removeEventListener("mousemove", Popup.domMove);
  3113. return
  3114. }
  3115.  
  3116. popup.style.setProperty("pointer-events", "auto", "important");
  3117. Popup.pinRJ = rjCode;
  3118. popup.setAttribute("pin", "");
  3119.  
  3120. //添加监听器
  3121. document.addEventListener("keyup", Popup.keyup);
  3122. document.addEventListener("mousemove", Popup.domMove);
  3123. },
  3124. hasPinned: function (){
  3125. return Popup.popupElement.popup.hasAttribute("pin");
  3126. },
  3127. /**
  3128. * @param e {KeyboardEvent}
  3129. */
  3130. isHoldPinKey: function(e){
  3131. if(getOS() === "Mac"){
  3132. return e.metaKey;
  3133. }
  3134. return e.ctrlKey;
  3135. },
  3136. /**
  3137. * @param e {KeyboardEvent}
  3138. */
  3139. isPinKeyDown: function (e) {
  3140. if(getOS() === "Mac"){
  3141. return e.key === "Meta";
  3142. }
  3143. return e.key === "Control";
  3144. },
  3145.  
  3146. /**
  3147. * 鼠标离开固定弹窗时,如果没有按住pin键则消失
  3148. * @param e {MouseEvent}
  3149. */
  3150. /*pinLeave: function (e) {
  3151. if(Popup.isHoldPinKey(e)){
  3152. return;
  3153. }
  3154. Popup.setPinState(null, false, true);
  3155. },*/
  3156. /**
  3157. * 监听网页内的鼠标移动事件,来保证弹框正常移除
  3158. * @param e {MouseEvent}
  3159. */
  3160. domMove: function (e) {
  3161. if(!Popup.hasPinned() || Popup.isHoldPinKey(e)){
  3162. return;
  3163. }
  3164. Popup.setPinState(null, false);
  3165. },
  3166. /**
  3167. * 鼠标移动到链接上触发
  3168. * @param e {MouseEvent}
  3169. */
  3170. over: function (e) {
  3171. const target = isInDLSite() ? e.target : getVoiceLinkTarget(e.target);
  3172. if(!target || !target.classList.contains(VOICELINK_CLASS)) return;
  3173.  
  3174. const rjCode = target.getAttribute(RJCODE_ATTRIBUTE);
  3175. if(rjCode === null) return;
  3176.  
  3177. //记录鼠标位置
  3178. let ele = Popup.popupElement;
  3179. ele._state.mouseX = e.clientX;
  3180. ele._state.mouseY = e.clientY;
  3181.  
  3182. //如果用户固定了弹框,则提示用户必须ctrl关闭弹框才能解析
  3183. if(Popup.isHoldPinKey(e) && Popup.pinRJ){
  3184. ele.hint.innerText = localizePopup(localizationMap.hint_unpin);
  3185. return;
  3186. }else{
  3187. //没有固定弹框的话清理pinRJ,因为有时候pinRJ没办法被keyup清理(如keyup未触发)
  3188. Popup.pinRJ = undefined
  3189. ele.hint.innerText = localizePopup(localizationMap.hint_pin);
  3190. }
  3191.  
  3192. //修正链接
  3193. if(target.hasAttribute("voicelink-linkified")){
  3194. WorkPromise.getWorkPromise(rjCode).info.then(info => {
  3195. if(info.is_announce === true){
  3196. target.href = `https://www.dlsite.com/maniax/announce/=/product_id/${rjCode}.html`;
  3197. }
  3198. });
  3199. }
  3200.  
  3201. let popup = document.querySelector(`div#${VOICELINK_CLASS}-voice-popup`); // + rjCode);
  3202. if (popup) {
  3203. popup.style.setProperty("display", "flex", "important"); //display = "flex !important";
  3204. //先将字体大小变回原样
  3205. popup.style.setProperty("font-size", "15.4px", "important");
  3206. }
  3207. else {
  3208. Popup.makePopup();
  3209. popup = ele.popup;
  3210. }
  3211. Popup.updatePopup(e, rjCode);
  3212.  
  3213. //如果按住了CTRL,则将popup可被点击,否则设置穿透
  3214. //并设置Copy显示情况
  3215. if(Popup.isHoldPinKey(e)){
  3216. Popup.setPinState(rjCode, true)
  3217. ele.hint.innerText = localizePopup(localizationMap.hint_unpin);
  3218. }else{
  3219. Popup.setPinState(rjCode, false, false)
  3220. }
  3221.  
  3222. //设置焦点至链接上
  3223. target.focus();
  3224. target.style.setProperty("outline", "none", "important");
  3225.  
  3226. },
  3227.  
  3228. /**
  3229. * 鼠标离开时触发
  3230. * @param e {MouseEvent}
  3231. */
  3232. out: function (e) {
  3233. //如果固定则禁止关闭
  3234. if(Popup.isHoldPinKey(e)) {
  3235. return
  3236. }
  3237.  
  3238. const target = isInDLSite() ? e.target : getVoiceLinkTarget(e.target);
  3239. if(!target || !target.classList.contains(VOICELINK_CLASS)) return;
  3240.  
  3241. const rjCode = target.getAttribute(RJCODE_ATTRIBUTE);
  3242. if(rjCode === null) return;
  3243.  
  3244. //取消固定并关闭
  3245. Popup.setPinState(rjCode, false)
  3246.  
  3247. //取消focus
  3248. target.blur();
  3249. target.style.setProperty("outline", null);
  3250. },
  3251.  
  3252. /**
  3253. * 鼠标移动时触发
  3254. * @param e {MouseEvent}
  3255. */
  3256. move: function (e) {
  3257. const target = isInDLSite() ? e.target : getVoiceLinkTarget(e.target);
  3258. if(!target || !target.classList.contains(VOICELINK_CLASS)) return;
  3259.  
  3260. const popup = document.querySelector(`div#${VOICELINK_CLASS}-voice-popup`); // + rjCode);
  3261. if(!popup) return;
  3262.  
  3263. const rjCode = e.target.getAttribute(RJCODE_ATTRIBUTE);
  3264. if(rjCode === null) return;
  3265.  
  3266. let ele = Popup.popupElement;
  3267. ele._state.mouseX = e.clientX;
  3268. ele._state.mouseY = e.clientY;
  3269.  
  3270. //焦点不在浏览器上的时候无法触发keydown,因此需要用move来辅助激活pin
  3271. if(Popup.isHoldPinKey(e) && !Popup.pinRJ){
  3272. //按下pin键但没激活pin模式的时候手动激活
  3273. Popup.setPinState(rjCode, true);
  3274. }
  3275.  
  3276. //如果弹框已固定且固定的并非当前所选链接RJ号,则不进行定位修正
  3277. if(Popup.pinRJ && rjCode !== Popup.pinRJ){
  3278. return;
  3279. }
  3280.  
  3281. Popup.adjustPopup(e.clientX, e.clientY);
  3282.  
  3283. },
  3284.  
  3285. /**
  3286. * 按键按下时触发
  3287. * @param e {KeyboardEvent}
  3288. */
  3289. keydown: function (e) {
  3290. const target = isInDLSite() ? e.target : getVoiceLinkTarget(e.target);
  3291. if(!target || !target.classList.contains(VOICELINK_CLASS)) return;
  3292.  
  3293. const rjCode = target.getAttribute(RJCODE_ATTRIBUTE);
  3294. if(rjCode === null) return;
  3295.  
  3296. let popup = Popup.popupElement.popup;
  3297. if(popup.style.display !== "none" && Popup.isPinKeyDown(e)){
  3298. //按住CTRL以固定显示弹框
  3299. Popup.setPinState(rjCode, true);
  3300. }
  3301. },
  3302.  
  3303. /**
  3304. * 按键抬起时触发
  3305. * @param e {KeyboardEvent}
  3306. */
  3307. keyup: function (e) {
  3308. let popup = Popup.popupElement.popup;
  3309. if(popup && Popup.isPinKeyDown(e)){
  3310. Popup.setPinState(null, false);
  3311. }
  3312. }
  3313. }
  3314.  
  3315. const WorkPromise = {
  3316. /**
  3317. * 标题、社团、发行日期、更新日期、年龄指定
  3318. * CV、标签、文件大小、封面地址
  3319. */
  3320.  
  3321. checkNotNull: function (obj){
  3322. if(obj === null || obj === undefined) throw new Error();
  3323. return obj;
  3324. },
  3325.  
  3326. getWorkPromise: function (rjCode){
  3327. if(work_promise[rjCode]){
  3328. return work_promise[rjCode];
  3329. }
  3330. work_promise[rjCode] = DLsite.getWorkRequestPromise(rjCode);
  3331. return work_promise[rjCode];
  3332. },
  3333.  
  3334. getFound: async function(rjCode){
  3335. try{
  3336. const data = await WorkPromise.getWorkPromise(rjCode).api2;
  3337. if(data && data.product_id !== undefined) return true;
  3338.  
  3339. //否则再次检查api1
  3340. const api = await WorkPromise.getWorkPromise(rjCode).api;
  3341. return api && api.is_sale !== undefined;
  3342. }catch (e){
  3343. //说明是网络问题,删除缓存并返回true
  3344. delete work_promise[rjCode];
  3345. return true;
  3346. }
  3347. },
  3348.  
  3349. getTranslationInfo: async function(rjCode){
  3350. const p = WorkPromise.getWorkPromise(rjCode);
  3351. let data = await p.api2;
  3352. if(data.translation_info) return data.translation_info;
  3353.  
  3354. data = await p.api;
  3355. return data.translation_info ? data.translation_info : {};
  3356. },
  3357.  
  3358. getRJChain: async function(rjCode) {
  3359. //RJxxx → RJxxx → RJxxx,这样从子级指向父级
  3360. const trans = await WorkPromise.getTranslationInfo(rjCode);
  3361. let chain = [rjCode];
  3362. if(trans.is_child){
  3363. chain.push(trans.parent_workno, trans.original_workno);
  3364. }else if(trans.is_parent){
  3365. chain.push(trans.original_workno);
  3366. }
  3367. return chain;
  3368. },
  3369.  
  3370. getParentRJ: async function(rjCode){
  3371. try{
  3372. const p = WorkPromise.getWorkPromise(rjCode);
  3373. let trans = await WorkPromise.getTranslationInfo(rjCode);
  3374. if(trans.is_original || trans.is_parent) return rjCode;
  3375. if(trans.parent_workno) return trans.parent_workno;
  3376.  
  3377. let data = await p.info;
  3378. return data.parentWork;
  3379. }catch (e){
  3380. return null;
  3381. }
  3382. },
  3383.  
  3384. getGirls: async function(rjCode){
  3385. const p = WorkPromise.getWorkPromise(rjCode);
  3386. let data = await p.api2;
  3387. if(data.sex_category && data.sex_category === 2) return true;
  3388. if(data.site_id === "girls") return true;
  3389.  
  3390. //否则再次检查api1
  3391. data = await WorkPromise.getWorkPromise(rjCode).api;
  3392. WorkPromise.checkNotNull(data.is_girls)
  3393. return data.is_girls;
  3394. },
  3395.  
  3396. getAnnounce: async function(rjCode) {
  3397. const p = WorkPromise.getWorkPromise(rjCode);
  3398. const info = await p.info;
  3399. return info.is_announce;
  3400. },
  3401.  
  3402. getSale: async function(rjCode, checkAnnounce = true){
  3403. const p = WorkPromise.getWorkPromise(rjCode);
  3404. let data = await p.api;
  3405. if(!checkAnnounce){
  3406. return data.is_sale;
  3407. }
  3408. return data.is_sale || await WorkPromise.getAnnounce(rjCode);
  3409. },
  3410.  
  3411. getDLCount: async function (rjCode) {
  3412. const p = WorkPromise.getWorkPromise(rjCode);
  3413. let data = await p.api;
  3414. WorkPromise.checkNotNull(data.dl_count);
  3415. return data.dl_count;
  3416. },
  3417.  
  3418. getRateAvg: async function (rjCode) {
  3419. const p = WorkPromise.getWorkPromise(rjCode);
  3420. let data = await p.api;
  3421. if(data.rate_average_2dp) return data.rate_average_2dp;
  3422.  
  3423. //还可以累加api2的结果获得
  3424. data = await p.api2;
  3425. this.checkNotNull(data.rate_count_detail);
  3426. let sum = 0;
  3427. let count = 0;
  3428. for (const key in data.rate_count_detail) {
  3429. let rate = parseInt(key);
  3430. let cot = parseInt(data.rate_count_detail[key]);
  3431. count += cot
  3432. sum += rate * cot;
  3433. }
  3434. return sum / count;
  3435. },
  3436.  
  3437. getRateCount: async function (rjCode) {
  3438. const p = WorkPromise.getWorkPromise(rjCode);
  3439. let data = await p.api;
  3440. if(data.rate_count) return data.rate_count;
  3441.  
  3442. //还可以累加api2的结果获得
  3443. data = await p.api2;
  3444. this.checkNotNull(data.rate_count_detail);
  3445. let count = 0;
  3446. for (const key in data.rate_count_detail) {
  3447. count += parseInt(data.rate_count_detail[key]);
  3448. }
  3449. return count;
  3450. },
  3451.  
  3452. getWishlistCount: async function (rjCode) {
  3453. const p = WorkPromise.getWorkPromise(rjCode);
  3454. let data = await p.api;
  3455. this.checkNotNull(data.wishlist_count);
  3456. return data.wishlist_count;
  3457. },
  3458.  
  3459. getPriceText: async function (rjCode) {
  3460. const p = WorkPromise.getWorkPromise(rjCode);
  3461. //TODO: 价格以后再加,还要考虑汇率和添加设置项
  3462. },
  3463.  
  3464. getBonus: async function(rjCode) {
  3465. const p = WorkPromise.getWorkPromise(rjCode);
  3466. let data = await p.api;
  3467. return !data.is_sale && data.is_free && data.is_oly && data.wishlist_count === 0;
  3468. // return data.is_bonus;
  3469. },
  3470.  
  3471. getHasBonus: async function(rjCode) {
  3472. const p = WorkPromise.getWorkPromise(rjCode);
  3473. let data = await p.api;
  3474. return data.bonuses && data.bonuses.length > 0;
  3475. },
  3476.  
  3477. getTranslatable: async function(rjCode) {
  3478. const trans = await WorkPromise.getTranslationInfo(rjCode);
  3479. return trans.is_translation_agree === true;
  3480. },
  3481.  
  3482. getTranslated: async function(rjCode) {
  3483. const trans = await WorkPromise.getTranslationInfo(rjCode);
  3484. return trans.is_parent === true || trans.is_child === true;
  3485. },
  3486.  
  3487. getLanguages: async function(rjCode){
  3488. //返回字符串数组,根据popup设置的语言返回支持的语言列表
  3489. const map = {
  3490. JPN: localizePopup(localizationMap.language_japanese),
  3491. ENG: localizePopup(localizationMap.language_english),
  3492. CHI_HANS: localizePopup(localizationMap.language_simplified_chinese),
  3493. CHI_HANT: localizePopup(localizationMap.language_traditional_chinese),
  3494. KO_KR: localizePopup(localizationMap.language_korean),
  3495. SPA: localizePopup(localizationMap.language_spanish),
  3496. FRE: localizePopup(localizationMap.language_french),
  3497. RUS: localizePopup(localizationMap.language_russian),
  3498. THA: localizePopup(localizationMap.language_thai),
  3499. GER: localizePopup(localizationMap.language_german),
  3500. FIN: localizePopup(localizationMap.language_finnish),
  3501. POR: localizePopup(localizationMap.language_portuguese),
  3502. VIE: localizePopup(localizationMap.language_vietnamese),
  3503. ITA: localizePopup(localizationMap.language_italian),
  3504. ARA: localizePopup(localizationMap.language_arabic),
  3505. POL: localizePopup(localizationMap.language_polish),
  3506. }
  3507. const p = WorkPromise.getWorkPromise(rjCode);
  3508. let api = await p.api2;
  3509. api = api.options ? api : await p.api;
  3510. const options = api.options?.split("#");
  3511. const result = [];
  3512. for (const key in map) {
  3513. const lang = map[key];
  3514. if(options?.includes(key)) result.push(lang);
  3515. }
  3516. return result;
  3517. },
  3518.  
  3519. getFileFormats: async function(rjCode){
  3520. //返回字符串数组,返回文件格式列表
  3521. const result = [];
  3522. const p = WorkPromise.getWorkPromise(rjCode);
  3523. let api = await p.api2;
  3524. if(api.file_type === "EXE"){
  3525. result.push("EXE");
  3526. }else if(api.file_type_string){
  3527. result.push(api.file_type_string);
  3528. }
  3529. if(api.file_type_special) result.push(api.file_type_special);
  3530.  
  3531. if(!api.options) api = await p.api;
  3532. if(api.options && api.options.includes("WPD")){
  3533. result.push("PDF");
  3534. }
  3535. if(api.options && api.options.includes("WAP")){
  3536. result.push("APK");
  3537. }
  3538.  
  3539. return result;
  3540. },
  3541.  
  3542. getAIUsedText: async function(rjCode) {
  3543. //返回是否使用或部分使用AI,根据popup语言返回字符串。
  3544. const p = WorkPromise.getWorkPromise(rjCode);
  3545. let api = await p.api2;
  3546. api = api.options ? api : await p.api;
  3547. const options = api.options ? api.options : "";
  3548. if(options.includes("AIG")){
  3549. return localizePopup(localizationMap.tag_aig);
  3550. }else if(options.includes("AIP")){
  3551. return localizePopup(localizationMap.tag_aip);
  3552. }
  3553. return null;
  3554. },
  3555.  
  3556. getDebug: async function(rjCode){
  3557. return "";
  3558. const work = WorkPromise.getWorkPromise(rjCode);
  3559. const api2 = await work.api2;
  3560. const api = await work.api;
  3561. const info = await work.info;
  3562. const circle = work.circle;
  3563.  
  3564. return `is_ana_api2: ${api2.is_ana}<br/>
  3565. is_ana_api: ${api.is_ana}`;
  3566. },
  3567.  
  3568. getWorkCategory: async function(rjCode){
  3569. const type = await WorkPromise.getWorkType(rjCode);
  3570. /* voice: 音声
  3571. * game: 游戏
  3572. * manga: 漫画/插画/音声漫画
  3573. * video: 视频
  3574. * novel: 小说
  3575. * other: 其它
  3576. */
  3577. switch (type) {
  3578. case 0:
  3579. return "voice";
  3580. case 1:
  3581. return "game";
  3582. case 2 || 3 || 8:
  3583. return "manga";
  3584. case 5:
  3585. return "video";
  3586. case 4:
  3587. return "novel";
  3588. default:
  3589. return "other";
  3590. }
  3591. },
  3592.  
  3593. getWorkTypeText: async function(rjCode) {
  3594. const mapping = [
  3595. localizePopup(localizationMap.work_type_voice),
  3596. localizePopup(localizationMap.work_type_game),
  3597. localizePopup(localizationMap.work_type_comic),
  3598. localizePopup(localizationMap.work_type_illustration),
  3599. localizePopup(localizationMap.work_type_novel),
  3600. localizePopup(localizationMap.work_type_video),
  3601. localizePopup(localizationMap.work_type_music),
  3602. localizePopup(localizationMap.work_type_tool),
  3603. localizePopup(localizationMap.work_type_voice_comic),
  3604. localizePopup(localizationMap.work_type_other),
  3605. ];
  3606. return mapping[await WorkPromise.getWorkType(rjCode)];
  3607. },
  3608.  
  3609. getWorkType: async function(rjCode) {
  3610. const p = WorkPromise.getWorkPromise(rjCode);
  3611. const api2 = await p.api2;
  3612. let workType = api2.work_type;
  3613. if(!workType) workType = (await p.api).work_type;
  3614.  
  3615. switch (workType) {
  3616. case "SOU":
  3617. return 0;
  3618. case (["ACN", "QIZ", "ADV", "RPG", "TBL", "DNV", "SLN", "TYP", "STG", "PZL", "ETC"]
  3619. .includes(workType) ? workType : "ERR"):
  3620. return 1;
  3621. case (["MNG", "SCM", "WBT"]
  3622. .includes(workType) ? workType : "ERR"):
  3623. return 2;
  3624. case "ICG":
  3625. return 3;
  3626. case (["NRE", "KSV"].includes(workType) ? workType : "ERR"):
  3627. return 4;
  3628. case "MOV":
  3629. return 5;
  3630. case "MUS":
  3631. return 6;
  3632. case (["TOL", "IMT", "AMT"]
  3633. .includes(workType) ? workType : "ERR"):
  3634. return 7;
  3635. case "VCM":
  3636. return 8;
  3637. case "ET3":
  3638. return 9;
  3639. default:
  3640. throw new Error("无法获取作品类型/未知作品类型:" + workType);
  3641. }
  3642. },
  3643.  
  3644. getImgLink: async function(rjCode){
  3645. let link = undefined;
  3646. const p = WorkPromise.getWorkPromise(rjCode);
  3647.  
  3648. try {
  3649. let data = await p.api2;
  3650. if (data.image_main && data.image_main.url) link = "https:" + data.image_main.url;
  3651. } catch (e) {}
  3652.  
  3653. if(link && !link.includes("no_img_main.gif")){
  3654. return link;
  3655. }
  3656.  
  3657. try{
  3658. const info = await p.info;
  3659. WorkPromise.checkNotNull(info.img);
  3660. return info.img;
  3661. }catch (e) {
  3662. }
  3663.  
  3664. try{
  3665. const apiData = await WorkPromise.getWorkPromise(rjCode).api;
  3666. if(apiData.work_image) return "https:" + apiData.work_image;
  3667. }catch (e){}
  3668.  
  3669. throw new Error("无法获取图片链接");
  3670. },
  3671.  
  3672. getWorkTitle: async function(rjCode){
  3673. return await WorkPromise.getWorkPromise(rjCode).translated_title;
  3674. },
  3675.  
  3676. getAgeRating: async function(rjCode){
  3677. let p = WorkPromise.getWorkPromise(rjCode);
  3678. let api = await p.api2;
  3679. if(!api.age_category) api = await p.api;
  3680. switch (api.age_category){
  3681. case 1:
  3682. return "All";
  3683. case 2:
  3684. return "R15";
  3685. case 3:
  3686. return "R18";
  3687. }
  3688.  
  3689. const info = await p.info;
  3690. WorkPromise.checkNotNull(info.rating);
  3691. return info.rating;
  3692. },
  3693.  
  3694. getCircle: async function(rjCode, findOriginal = true){
  3695. let trans = await WorkPromise.getTranslationInfo(rjCode);
  3696. if(!trans.is_original && findOriginal){
  3697. //使用原作RJ号开始寻找,如果找不到翻译信息就没办法了
  3698. rjCode = trans.original_workno ? trans.original_workno : rjCode;
  3699. }
  3700.  
  3701. let work = WorkPromise.getWorkPromise(rjCode);
  3702. let api2 = await work.api2;
  3703. if(api2.maker_name) return api2.maker_name;
  3704.  
  3705. /**
  3706. * 接下来有两种搜索方式:
  3707. * 1. api1 + circle接口
  3708. * 2. info搜索
  3709. * 前者成功率更高(下架后还能获取到api1,社团没解散就能获得社团信息),两个加载速度不确定谁快谁慢,所以把1放在前面
  3710. */
  3711.  
  3712. const circleInfo = await work.circle;
  3713. if(circleInfo && circleInfo.name) return circleInfo.name;
  3714.  
  3715. let info = await work.info;
  3716. if(info.circle) return info.circle.trim();
  3717.  
  3718. throw new Error("无法获取社团信息");
  3719. },
  3720.  
  3721. getTranslatorName: async function(rjCode){
  3722. let trans = await WorkPromise.getTranslationInfo(rjCode);
  3723. if(!trans.is_child) throw new Error("非翻译作品RJ号");
  3724. return await WorkPromise.getCircle(rjCode, false);
  3725. },
  3726.  
  3727. getReleaseDate: async function(rjCode){
  3728. const p = WorkPromise.getWorkPromise(rjCode);
  3729. const info = await p.info;
  3730. if(info && !info.is_announce && info.date) return [info.date.trim(), false];
  3731. if(info && info.is_announce && info.dateAnnounce) return [info.dateAnnounce.trim(), true];
  3732.  
  3733. //从api中查找发售时间
  3734. let api = await p.api2;
  3735. api = api.regist_date ? api : await p.api;
  3736. WorkPromise.checkNotNull(api.regist_date)
  3737.  
  3738. return [api.regist_date, api.is_announce];
  3739. },
  3740.  
  3741. getReleaseCountDownElement: async function(rjCode) {
  3742. const p = WorkPromise.getWorkPromise(rjCode);
  3743. const info = await p.info;
  3744. if(info && info.is_announce && info.dateAnnounce) {
  3745. return DateParser.getCountDownDateElement(DateParser.parseDateStr(info.dateAnnounce, info.lang));
  3746. }
  3747. return null;
  3748. },
  3749.  
  3750. getUpdateDate: async function(rjCode) {
  3751. const p = WorkPromise.getWorkPromise(rjCode);
  3752. const info = await p.info;
  3753. if(info["update"]) return info["update"].trim();
  3754.  
  3755. throw new Error();
  3756. },
  3757.  
  3758. getScenario: async function(rjCode) {
  3759. const p = WorkPromise.getWorkPromise(rjCode);
  3760. const api2 = await p.api2;
  3761. if(api2.creaters && api2.creaters.scenario_by && api2.creaters.scenario_by.length > 0){
  3762. return api2.creaters.scenario_by.map(v => v.name);
  3763. }
  3764.  
  3765. //无法获取api2则直接通过html获取
  3766. const info = await WorkPromise.getWorkPromise(rjCode).info;
  3767. WorkPromise.checkNotNull(info.scenario);
  3768. return info.scenario;
  3769. },
  3770.  
  3771. getIllustrator: async function(rjCode) {
  3772. const p = WorkPromise.getWorkPromise(rjCode);
  3773. const api2 = await p.api2;
  3774. if(api2.creaters && api2.creaters.illust_by && api2.creaters.illust_by.length > 0){
  3775. return api2.creaters.illust_by.map(v => v.name);
  3776. }
  3777.  
  3778. //无法获取api2则直接通过html获取
  3779. const info = await WorkPromise.getWorkPromise(rjCode).info;
  3780. WorkPromise.checkNotNull(info.illustration);
  3781. return info.illustration;
  3782. },
  3783.  
  3784. getCV: async function(rjCode){
  3785. const p = WorkPromise.getWorkPromise(rjCode);
  3786. const api2 = await p.api2;
  3787. if(api2.creaters && api2.creaters.voice_by && api2.creaters.voice_by.length > 0){
  3788. return api2.creaters.voice_by.map(v => v.name);
  3789. }
  3790.  
  3791. //无法获取api2则直接通过html获取
  3792. const info = await WorkPromise.getWorkPromise(rjCode).info;
  3793. WorkPromise.checkNotNull(info.cv);
  3794. return info.cv;
  3795. },
  3796.  
  3797. getMusic: async function(rjCode) {
  3798. const p = WorkPromise.getWorkPromise(rjCode);
  3799. const api2 = await p.api2;
  3800. if(api2.creaters && api2.creaters.music_by && api2.creaters.music_by.length > 0){
  3801. return api2.creaters.music_by.map(v => v.name);
  3802. }
  3803.  
  3804. //无法获取api2则直接通过html获取
  3805. const info = await WorkPromise.getWorkPromise(rjCode).info;
  3806. WorkPromise.checkNotNull(info.music);
  3807. return info.music;
  3808. },
  3809.  
  3810. getTags: async function(rjCode) {
  3811. //注意该方法返回字符串数组而不是纯字符串
  3812. const p = WorkPromise.getWorkPromise(rjCode);
  3813. const api2 = await p.api2;
  3814. if(api2.genres && api2.genres.length > 0){
  3815. return api2.genres.map(genre => genre.name);
  3816. }
  3817.  
  3818. //无法获取api2时通过html获取
  3819. const info = await p.info;
  3820. WorkPromise.checkNotNull(info.tags);
  3821. return info.tags;
  3822. },
  3823.  
  3824. getFileSizeStr: function(byteCount = 0){
  3825. const units = ["B", "KB", "MB", "GB", "TB"];
  3826. let unit = "B";
  3827. for (let i = 1; byteCount >= 1024; i++){
  3828. byteCount /= 1024;
  3829. unit = units[i];
  3830. }
  3831. return `${Math.round(byteCount * 100) / 100}${unit}`;
  3832. },
  3833.  
  3834. getFileSize: async function(rjCode) {
  3835. const trans = await WorkPromise.getTranslationInfo(rjCode);
  3836. if(trans.is_parent){
  3837. //翻译版本的父级没有内容信息,自然无法显示文件大小,所以需要获得原作品的大小信息
  3838. //Child和Original都有各自的大小信息,正常获取计算即可
  3839. rjCode = trans.original_workno ? trans.original_workno : rjCode;
  3840. }
  3841.  
  3842. const p = WorkPromise.getWorkPromise(rjCode);
  3843. let api2 = await p.api2;
  3844. if(api2.contents_file_size && api2.contents_file_size > 0){
  3845. return WorkPromise.getFileSizeStr(api2.contents_file_size);
  3846. }
  3847.  
  3848. //通过html获取
  3849. let info = trans.is_child && trans.original_workno ? await WorkPromise.getWorkPromise(trans.original_workno).info : await p.info;
  3850. if(info.filesize) return info.filesize;
  3851.  
  3852. throw new Error("无法获取文件大小信息");
  3853. },
  3854. }
  3855.  
  3856. const DLsite = {
  3857. parseWorkDOM: function (dom, rj) {
  3858. // workInfo: {
  3859. // rj: any;
  3860. // img: string;
  3861. // title: any;
  3862. // circle: any;
  3863. // date: any;
  3864. // rating: any;
  3865. // tags: any[];
  3866. // cv: any;
  3867. // filesize: any;
  3868. // dateAnnounce: any;
  3869. // }
  3870. const workInfo = {};
  3871. workInfo.rj = rj;
  3872.  
  3873. let metaList = dom.getElementsByTagName("meta")
  3874. for (let i = 0; i < metaList.length; i++){
  3875. let meta = metaList[i];
  3876. if(meta.getAttribute("property") === 'og:image'){
  3877. workInfo.img = meta.content;
  3878. break;
  3879. }
  3880. }
  3881.  
  3882. workInfo.lang = dom.querySelector("html").getAttribute("lang");
  3883. workInfo.title = dom.getElementById("work_name").innerText;
  3884. workInfo.circle = dom.querySelector("span.maker_name").innerText;
  3885. workInfo.circleId = dom.querySelector("#work_maker a").href;
  3886. workInfo.circleId = workInfo.circleId.substring(workInfo.circleId.lastIndexOf("/") + 1, workInfo.circleId.lastIndexOf(".")).trim();
  3887.  
  3888. const table_outline = dom.querySelector("table#work_outline");
  3889. for (let i = 0, ii = table_outline.rows.length; i < ii; i++) {
  3890. const row = table_outline.rows[i];
  3891. const row_header = row.cells[0].innerText.trim();
  3892. const row_data = row.cells[1];
  3893. const lambda = text => row_header === text;
  3894. switch (true) {
  3895. case (["販売日", "贩卖日", "販賣日", "Release date", "판매일", "Lanzamiento", "Veröffentlicht",
  3896. "Date de sortie", "Tanggal rilis", "Data di rilascio", "Lançamento", "Utgivningsdatum",
  3897. "วันที่ขาย", "Ngày phát hành"].some(lambda)):
  3898. workInfo.date = row_data.innerText.trim();
  3899. break;
  3900. case (["更新情報", "更新信息", "更新資訊", "Update information", "갱신 정보", "Actualizar información",
  3901. "Aktualisierungen", "Mise à jour des informations", "Perbarui informasi", "Aggiorna informazioni",
  3902. "Atualizar informações", "Uppdatera information", "ข้อมูลอัปเดต", "Thông tin cập nhật"].some(lambda)):
  3903. workInfo.update = row_data.firstChild.data.trim();
  3904. break;
  3905. case (["年齢指定", "年龄指定", "年齡指定", "Age", "연령 지정", "Edad", "Altersfreigabe", "Âge", "Batas usia",
  3906. "Età", "Idade", "Ålder", "การกำหนดอายุ", "Độ tuổi chỉ định"].some(lambda)):
  3907. workInfo.rating = row_data.innerText.trim();
  3908. break;
  3909. case (["ジャンル", "分类", "分類", "Genre", "장르", "Género", "Genre", "Genre", "Genre", "Genere", "Gênero",
  3910. "Genre", "ประเภท", "Thể loại"].some(lambda)):
  3911. const tag_nodes = row_data.querySelectorAll("a");
  3912. workInfo.tags = [...tag_nodes].map(a => { return a.innerText.trim() });
  3913. break;
  3914. case (["シナリオ", "Scenario", "剧情", "劇本", "시나리오", "Guión", "Szenario", "Scénario", "Skenario",
  3915. "Scenario", "Cenário", "Scenario", "บทละคร", "Kịch bản"].some(lambda)):
  3916. workInfo.scenario = row_data.innerText.trim();
  3917. break;
  3918. case (["イラスト", "Illustration", "插画", "插畫", "일러스트", "Ilustración", "AbbilDung", "Illustration",
  3919. "Ilustrasi", "Illustrazione", "Ilustração", "Illustration", "ภาพประกอบ", "Tranh minh họa"].some(lambda)):
  3920. workInfo.illustration = row_data.innerText.trim();
  3921. break;
  3922. case (["声優", "声优", "聲優", "Voice Actor", "성우", "Doblador", "Synchronsprecher", "Doubleur",
  3923. "Pengisi suara", "Doppiatore/Doppiatrice", "Ator de voz", "Röstskådespelare", "นักพากย์",
  3924. "Diễn viên lồng tiếng"].some(lambda)):
  3925. workInfo.cv = row_data.innerText.trim();
  3926. break;
  3927. case (["音楽", "Music", "音乐", "音樂", "음악", "Música", "Musik", "Musique", "Musik", "Musica.",
  3928. "Música", "musik", "ดนตรี", "Âm nhạc"].some(lambda)):
  3929. workInfo.music = row_data.innerText.trim();
  3930. break;
  3931. case (["ファイル容量", "文件容量", "檔案容量", "File size", "파일 용량", "Tamaño del Archivo", "Dateigröße",
  3932. "Taille du fichier", "Ukuran file", "Dimensione del file", "Tamanho do arquivo", "Filstorlek",
  3933. "ขนาดไฟล์", "Dung lượng tệp"].some(lambda)):
  3934. workInfo.filesize = row_data.innerText.trim();
  3935. break;
  3936. default:
  3937. break;
  3938. }
  3939. }
  3940.  
  3941. //获取发售预告时间
  3942. const work_date_ana = dom.querySelector("strong.work_date_ana");
  3943. if (work_date_ana) {
  3944. workInfo.dateAnnounce = work_date_ana.innerText;
  3945. //workInfo.img = "https://img.dlsite.jp/modpub/images2/ana/doujin/" + rj_group + "/" + rj + "_ana_img_main.jpg"
  3946. }
  3947.  
  3948. return workInfo;
  3949. },
  3950.  
  3951. // Get language code for DLSite API
  3952. getLangCode: function (lang) {
  3953. if(!lang) return "ja-JP";
  3954.  
  3955. switch (lang.toUpperCase()) {
  3956. case "JPN":
  3957. return "ja-JP";
  3958. case "ENG":
  3959. return "en-US";
  3960. case "KO_KR":
  3961. return "ko-KR";
  3962. case "CHI_HANS":
  3963. return "zh-CN";
  3964. case "CHI_HANT":
  3965. return "zh-TW";
  3966. default:
  3967. return "ja-JP"
  3968. }
  3969. },
  3970.  
  3971. parseApiData: function (rjCode, data){
  3972. if(!data) data = {};
  3973. let apiData = data;
  3974. apiData.is_bonus = !data.is_sale && data.is_free && data.is_oly && data.wishlist_count === false;
  3975. apiData.is_girls = (data.options && data.options.indexOf("OTM") >= 0) || (data.site_id === "girls");
  3976.  
  3977. if(data.regist_date){
  3978. let reg_date = data.regist_date.replace(/-/g, '/');
  3979. let releaseDate = new Date(reg_date);
  3980. apiData.regist_timestamp = releaseDate.getTime();
  3981. apiData.regist_date = `${releaseDate.getFullYear()} / ${releaseDate.getMonth() + 1} / ${releaseDate.getDate()}`;
  3982. if(apiData.regist_timestamp > Date.now()){
  3983. apiData.is_announce = true;
  3984. }
  3985. }
  3986. return apiData;
  3987. },
  3988.  
  3989. parseApi2Data: function (rjCode, data) {
  3990. const translation_info = data.translation_info ? data.translation_info : {};
  3991. data.lang = DLsite.getLangCode(translation_info.lang);
  3992.  
  3993. if(data.regist_date){
  3994. let reg_date = data.regist_date.replace(/-/g, '/');
  3995. let releaseDate = new Date(reg_date);
  3996. data.regist_timestamp = releaseDate.getTime();
  3997. data.regist_date = `${releaseDate.getFullYear()} / ${releaseDate.getMonth() + 1} / ${releaseDate.getDate()}`;
  3998. if(data.regist_timestamp > Date.now()){
  3999. data.is_announce = true;
  4000. }
  4001. }
  4002.  
  4003. return data;
  4004. },
  4005.  
  4006. getHttpAsync: async function (url, anonymous = false){
  4007. return new Promise((resolve, reject) => {
  4008. getXmlHttpRequest()({
  4009. method: "GET",
  4010. url,
  4011. headers: {
  4012. "Accept": "text/xml",
  4013. "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:67.0)",
  4014. "Cache-Control": "no-cache"
  4015. },
  4016. onload: resolve,
  4017. onerror: reject,
  4018. anonymous: anonymous
  4019. });
  4020. })
  4021. },
  4022.  
  4023. getAnnouncePromise: async function (rjCode, parentRJ) {
  4024. const url = `https://www.dlsite.com/maniax/announce/=/product_id/${rjCode}.html`;
  4025. let resp = await DLsite.getHttpAsync(url);
  4026. if (resp.readyState === 4 && resp.status === 200) {
  4027. const dom = new DOMParser().parseFromString(Csp.createHTML(resp.responseText), "text/html");
  4028. const workInfo = DLsite.parseWorkDOM(dom, rjCode);
  4029. workInfo.parentWork = parentRJ === rjCode ? null : parentRJ;
  4030. workInfo.is_announce = true;
  4031. return workInfo;
  4032. }
  4033. else if (resp.readyState === 4 && resp.status === 404) {
  4034. return {
  4035. parentWork: parentRJ === rjCode ? null : parentRJ,
  4036. is_announce: false
  4037. };
  4038. }
  4039.  
  4040. },
  4041.  
  4042. getHtmlPromise: async function (rjCode) {
  4043. const url = `https://www.dlsite.com/maniax/work/=/product_id/${rjCode}.html`;
  4044. let resp = await DLsite.getHttpAsync(url);
  4045. if (resp.readyState === 4 && resp.status === 200) {
  4046. const dom = new DOMParser().parseFromString(Csp.createHTML(resp.responseText), "text/html");
  4047. const workInfo = DLsite.parseWorkDOM(dom, rjCode);
  4048. workInfo.parentWork = DLsite.getParentWorkRjCode(resp.finalUrl);
  4049. workInfo.parentWork = workInfo.parentWork === rjCode ? null : workInfo.parentWork;
  4050. workInfo.is_announce = false;
  4051. return workInfo;
  4052. }
  4053. else if (resp.readyState === 4 && resp.status === 404) {
  4054. return await DLsite.getAnnouncePromise(rjCode, DLsite.getParentWorkRjCode(resp.finalUrl));
  4055. }
  4056. },
  4057.  
  4058. getApi2Promise: async function (rjCode, locale = undefined) {
  4059. let url = `https://www.dlsite.com/maniax/api/=/product.json?workno=${rjCode}` + (locale ? `&locale=${locale}` : "");
  4060. let resp = await DLsite.getHttpAsync(url);
  4061. let data;
  4062. if (resp.readyState === 4 && resp.status === 200) {
  4063. data = JSON.parse(resp.responseText);
  4064. data = data ? data[0] : {};
  4065. data = data ? data : {}
  4066. }
  4067. else if (resp.readyState === 4 && resp.status === 404) {
  4068. return {};
  4069. }
  4070. else {
  4071. throw new Error(`无法通过API2获取${rjCode}的信息:${resp.status} ${resp.statusText}`);
  4072. }
  4073.  
  4074. return DLsite.parseApi2Data(rjCode, data);
  4075. },
  4076.  
  4077. getApiPromise: async function (rjCode, locale = undefined) {
  4078. //获取对应语言下的实际信息
  4079. let url = `https://www.dlsite.com/maniax/product/info/ajax?product_id=${rjCode}&cdn_cache_min=1` + (locale ? `&locale=${locale}` : "");
  4080. let resp = await DLsite.getHttpAsync(url);
  4081. let data;
  4082. if (resp.readyState === 4 && resp.status === 200) {
  4083. data = JSON.parse(resp.responseText);
  4084. data = data ? data[rjCode] : {};
  4085. data = data ? data : {};
  4086. }
  4087. else if(resp.readyState === 4 && resp.status === 404){
  4088. return {};
  4089. }
  4090. else {
  4091. throw new Error(`无法通过API获取${rjCode}的信息:${resp.status} ${resp.statusText}`);
  4092. }
  4093.  
  4094. const translation_info = data.translation_info ? data.translation_info : {};
  4095. data.lang = DLsite.getLangCode(translation_info.lang);
  4096.  
  4097. return DLsite.parseApiData(rjCode, data);
  4098. },
  4099.  
  4100. getCirclePromise: async function (rjCode, apiPromise){
  4101. let apiData = await apiPromise;
  4102. if(!apiData.maker_id) return null;
  4103. const maker_id = apiData.maker_id;
  4104.  
  4105. let url;
  4106. let resp;
  4107. let data;
  4108. try {
  4109. url = `https://media.ci-en.jp/dlsite/lookup/${maker_id}.json`;
  4110. resp = await DLsite.getHttpAsync(url);
  4111. data = undefined;
  4112. if (resp.readyState === 4 && resp.status === 200) {
  4113. data = JSON.parse(resp.responseText);
  4114. data = data ? data[0] : {};
  4115. data = data ? data : {};
  4116. data.maker_id = maker_id;
  4117. }
  4118. }catch (e){}
  4119.  
  4120. if(!data || !data.name){
  4121. //未获取到社团名称则使用html解析获取
  4122. url = `https://www.dlsite.com/maniax/circle/profile/=/maker_id/${maker_id}.html`;
  4123. resp = await DLsite.getHttpAsync(url);
  4124. data = data ? data : {};
  4125. if(resp.readyState === 4 && resp.status === 200){
  4126. let doc = new DOMParser().parseFromString(Csp.createHTML(resp.responseText), "text/html");
  4127. let name = doc.querySelector("strong.prof_maker_name");
  4128. name = name ? name.innerText : null;
  4129. data.name = name;
  4130. }
  4131. }
  4132.  
  4133. return data;
  4134. },
  4135.  
  4136. getTranslatablePromise: async function (rjCode, site = "maniax") {
  4137. rjCode = rjCode.toUpperCase();
  4138. const result = {
  4139. translation_request_english: {
  4140. agree: undefined,
  4141. request: undefined,
  4142. sale: undefined
  4143. },
  4144. translation_request_simplified_chinese:{
  4145. agree: undefined,
  4146. request: undefined,
  4147. sale: undefined
  4148. },
  4149. translation_request_traditional_chinese:{
  4150. agree: undefined,
  4151. request: undefined,
  4152. sale: undefined
  4153. },
  4154. translation_request_korean: {
  4155. agree: undefined,
  4156. request: undefined,
  4157. sale: undefined
  4158. },
  4159. translation_request_spanish: {
  4160. agree: undefined,
  4161. request: undefined,
  4162. sale: undefined
  4163. },
  4164. translation_request_german: {
  4165. agree: undefined,
  4166. request: undefined,
  4167. sale: undefined
  4168. },
  4169. translation_request_french: {
  4170. agree: undefined,
  4171. request: undefined,
  4172. sale: undefined
  4173. },
  4174. translation_request_indonesian: {
  4175. agree: undefined,
  4176. request: undefined,
  4177. sale: undefined
  4178. },
  4179. translation_request_italian: {
  4180. agree: undefined,
  4181. request: undefined,
  4182. sale: undefined
  4183. },
  4184. translation_request_portuguese: {
  4185. agree: undefined,
  4186. request: undefined,
  4187. sale: undefined
  4188. },
  4189. translation_request_swedish: {
  4190. agree: undefined,
  4191. request: undefined,
  4192. sale: undefined
  4193. },
  4194. translation_request_thai: {
  4195. agree: undefined,
  4196. request: undefined,
  4197. sale: undefined
  4198. },
  4199. translation_request_vietnamese: {
  4200. agree: undefined,
  4201. request: undefined,
  4202. sale: undefined
  4203. },
  4204. };
  4205. const data = await DLsite.getTranslatableApiPromise(rjCode, site);
  4206. if(!data.translationStatusForTranslator){
  4207. return result;
  4208. }
  4209.  
  4210. const map = {
  4211. translation_request_english: "ENG",
  4212. translation_request_simplified_chinese: "CHI_HANS",
  4213. translation_request_traditional_chinese: "CHI_HANT",
  4214. translation_request_korean: "KO_KR",
  4215. translation_request_spanish: "SPA",
  4216. translation_request_german: "GER",
  4217. translation_request_french: "FRE",
  4218. translation_request_indonesian: "IND",
  4219. translation_request_italian: "ITA",
  4220. translation_request_portuguese: "POR",
  4221. translation_request_swedish: "SWE",
  4222. translation_request_thai: "THA",
  4223. translation_request_vietnamese: "VIE",
  4224. };
  4225. for (let key in map) {
  4226. let lang = map[key];
  4227. let status = data.translationStatusForTranslator[lang];
  4228. if(!status){
  4229. //状况未知
  4230. continue;
  4231. }
  4232. result[key].agree = status.available;
  4233. result[key].request = status.count;
  4234. result[key].sale = status.on_sale_count;
  4235. }
  4236.  
  4237. return result;
  4238. },
  4239.  
  4240. getTranslatableApiPromise: async function (rjCode, site = "maniax") {
  4241. //新的可用api,用于搜索作品翻译情况,但也可以获得其它信息。
  4242. rjCode = rjCode.toUpperCase();
  4243. let url = `https://www.dlsite.com/${site}/api/=/translatableProducts.json?keyword=${rjCode}`; //可以使用locale参数指定语言,但这里不需要
  4244. let resp = await DLsite.getHttpAsync(url, true);
  4245. let data;
  4246. if (resp.readyState === 4 && resp.status === 200) {
  4247. data = JSON.parse(resp.responseText);
  4248. }
  4249. else {
  4250. throw new Error(`无法通过API获取${rjCode}的翻译信息:${resp.status} ${resp.statusText}`);
  4251. }
  4252.  
  4253. //从结果中找到对应RJ号,由于关键字是RJ号的话结果一般都在第一页,所以就放弃翻页寻找了
  4254. if(data.meta && data.meta.code !== 200){
  4255. throw new Error(`无法通过API查询${rjCode}的翻译信息:${data.meta.code} - ${data.meta.errorType} - ${data.meta.errorMessage}`);
  4256. }
  4257. if(!data.data || !Array.isArray(data.data.products)){
  4258. throw new Error(`无法通过API查询${rjCode}的翻译信息:未预料到的响应格式。`);
  4259. }
  4260.  
  4261. for (const work of data.data.products) {
  4262. if(work.id === rjCode){
  4263. return work;
  4264. }
  4265. }
  4266.  
  4267. //未找到则返回空对象
  4268. return {};
  4269.  
  4270. },
  4271.  
  4272. getWorkRequestPromise: function (rjCode) {
  4273. return {
  4274. _info: undefined,
  4275. _api: undefined,
  4276. _api2: undefined,
  4277. _circle: undefined,
  4278. _translatable: undefined,
  4279. _translated_title: undefined,
  4280. get info(){
  4281. return this._info ? this._info : this._info = DLsite.getHtmlPromise(rjCode);
  4282. },
  4283. get api() {
  4284. return this._api ? this._api : this._api = DLsite.getApiPromise(rjCode);
  4285. },
  4286. get api2() {
  4287. return this._api2 ? this._api2 : this._api2 = DLsite.getApi2Promise(rjCode);
  4288. },
  4289. get circle(){
  4290. return this._circle ? this._circle : this._circle = DLsite.getCirclePromise(rjCode, this.api);
  4291. },
  4292. get translatable() {
  4293. async function getter(t){
  4294. let api = await t.api2;
  4295. if(!api.site_id) api = await t.api;
  4296.  
  4297. return t._translatable ? t._translatable : t._translatable = DLsite.getTranslatablePromise(rjCode,
  4298. api.site_id ? api.site_id : "maniax");
  4299. }
  4300. return getter(this);
  4301. },
  4302. get translated_title(){
  4303. async function getter(t){
  4304. if(t._translated_title) return t._translated_title;
  4305.  
  4306. let api = await t.api2;
  4307. if(api.translation_info){
  4308. //api2有效
  4309. if(!api.translation_info.is_original) {
  4310. //通过再次查询获得翻译标题
  4311. api = await DLsite.getApi2Promise(rjCode, api.lang);
  4312. }
  4313. t._translated_title = api.work_name;
  4314. return t._translated_title;
  4315. }
  4316.  
  4317. //api2无效,通过api查询
  4318. api = await t.api;
  4319. if(!api.translation_info){
  4320. //api无效则无法获取标题(网页获取希望渺茫)
  4321. t._translated_title = null;
  4322. return null;
  4323. }
  4324.  
  4325. if(!api.translation_info.is_original) {
  4326. //非原作则再次查询
  4327. api = await DLsite.getApiPromise(rjCode, api.lang);
  4328. }
  4329. t._translated_title = api.work_name;
  4330. return t._translated_title;
  4331. }
  4332.  
  4333. return getter(this);
  4334. }
  4335. }
  4336. },
  4337.  
  4338. getParentWorkRjCode: function (redirectUrl){
  4339. const reg = new RegExp("(?<=product_id/)((R[JE][0-9]{8})|(R[JE][0-9]{6})|([VB]J[0-9]{8})|([VB]J[0-9]{6}))")
  4340. return redirectUrl.match(reg)[0];
  4341. }
  4342. }
  4343.  
  4344. function getSettingsUi() {
  4345. return {
  4346. //这一层是设置界面最顶层,编辑大标题信息
  4347. title: localize(localizationMap.title_settings),
  4348. items: [
  4349. {
  4350. //这一层是设置界面的大分类
  4351. title: localize(localizationMap.title_language_settings),
  4352. items: [
  4353. {
  4354. //这一层是设置项列表集合(使用表格呈现设置项)"
  4355. items: [
  4356. {
  4357. //这一层是设置项
  4358. type: "dropdown",
  4359. title: localize(localizationMap.display_language),
  4360. id: "lang",
  4361. ignore_reset: true,
  4362. options: [
  4363. {
  4364. title: "简体中文",
  4365. value: "zh_CN"
  4366. },
  4367. {
  4368. title: "繁體中文",
  4369. value: "zh_TW"
  4370. },
  4371. {
  4372. title: "English",
  4373. value: "en_US"
  4374. }
  4375. ]
  4376. },
  4377. {
  4378. //这一层是设置项
  4379. type: "dropdown",
  4380. title: localize(localizationMap.popup_language),
  4381. id: "popup_lang",
  4382. ignore_reset: true,
  4383. tooltip: localize(localizationMap.popup_language_tooltip),
  4384. options: [
  4385. {
  4386. title: "简体中文",
  4387. value: "zh_CN"
  4388. },
  4389. {
  4390. title: "繁體中文",
  4391. value: "zh_TW"
  4392. },
  4393. {
  4394. title: "English",
  4395. value: "en_US"
  4396. }
  4397. ]
  4398. }
  4399. ]
  4400. }
  4401. ]
  4402. },
  4403.  
  4404. {
  4405. //这一层是设置界面的大分类
  4406. title: localize(localizationMap.title_general_settings),
  4407. items: [
  4408. {
  4409. //这一层是设置项列表集合(使用表格呈现设置项)
  4410. items: [
  4411. {
  4412. //解析URL
  4413. type: "checkbox",
  4414. title: localize(localizationMap.parse_url),
  4415. id: "parse_url",
  4416. tooltip: localize(localizationMap.parse_url_tooltip)
  4417. },
  4418. {
  4419. //DL上解析URL
  4420. binding: {
  4421. target: "parse_url",
  4422. value: true
  4423. },
  4424.  
  4425. type: "checkbox",
  4426. title: localize(localizationMap.parse_url_in_dl),
  4427. id: "parse_url_in_dl",
  4428. indent: 1, //设置项缩进
  4429. tooltip: localize(localizationMap.parse_url_in_dl_tooltip)
  4430. },
  4431. {
  4432. //DL显示翻译标题
  4433. type: "checkbox",
  4434. title: localize(localizationMap.show_translated_title_in_dl),
  4435. id: "show_translated_title_in_dl",
  4436. tooltip: localize(localizationMap.show_translated_title_in_dl_tooltip)
  4437. },
  4438. {
  4439. //“复制为有效文件名”按钮
  4440. type: "checkbox",
  4441. title: localize(localizationMap.copy_as_filename_btn),
  4442. id: "copy_as_filename_btn",
  4443. tooltip: localize(localizationMap.copy_as_filename_btn_tooltip)
  4444. },
  4445. {
  4446. //**显示兼容性警告**
  4447. type: "checkbox",
  4448. title: `<strong>**${localize(localizationMap.show_compatibility_warning)}**</strong>`,
  4449. id: "show_compatibility_warning",
  4450. tooltip: localize(localizationMap.show_compatibility_warning_tooltip)
  4451. },
  4452. {
  4453. //导向文本插入方式
  4454. type: "dropdown",
  4455. title: localize(localizationMap.url_insert_mode),
  4456. id: "url_insert_mode",
  4457. tooltip: localize(localizationMap.url_insert_mode_tooltip),
  4458. options: [
  4459. {
  4460. title: localize(localizationMap.url_insert_mode_none),
  4461. value: "none"
  4462. },
  4463. {
  4464. title: localize(localizationMap.url_insert_mode_prefix),
  4465. value: "prefix"
  4466. },
  4467. {
  4468. title: localize(localizationMap.url_insert_mode_before_rj),
  4469. value: "before_rj"
  4470. }
  4471. ]
  4472. },
  4473. {
  4474. //导向文本
  4475. type: "input",
  4476. title: localize(localizationMap.url_insert_text),
  4477. id: "url_insert_text",
  4478. indent: 1
  4479. },
  4480.  
  4481. {
  4482. //NSFW模式
  4483. type: "checkbox",
  4484. title: localize(localizationMap.sfw_mode),
  4485. id: "sfw_mode",
  4486. tooltip: localize(localizationMap.sfw_mode_tooltip)
  4487. },
  4488. {
  4489. //模糊程度
  4490. binding: {
  4491. target: "sfw_mode",
  4492. value: true
  4493. },
  4494.  
  4495. type: "dropdown",
  4496. title: localize(localizationMap.sfw_blur_level),
  4497. id: "sfw_blur_level",
  4498. indent: 1,
  4499. options: [
  4500. {
  4501. title: localize(localizationMap.low),
  4502. value: "low"
  4503. },
  4504. {
  4505. title: localize(localizationMap.medium),
  4506. value: "medium"
  4507. },
  4508. {
  4509. title: localize(localizationMap.high),
  4510. value: "high"
  4511. }
  4512. ]
  4513. },
  4514. {
  4515. //鼠标移至图片上方移除模糊
  4516. binding: {
  4517. target: "sfw_mode",
  4518. value: true
  4519. },
  4520.  
  4521. type: "checkbox",
  4522. title: localize(localizationMap.sfw_remove_when_hover),
  4523. id: "sfw_remove_when_hover",
  4524. indent: 1,
  4525. },
  4526. {
  4527. //是否开启模糊动画
  4528. binding: {
  4529. target: "sfw_mode",
  4530. value: true
  4531. },
  4532.  
  4533. type: "checkbox",
  4534. title: localize(localizationMap.sfw_blur_transition),
  4535. id: "sfw_blur_transition",
  4536. indent: 1,
  4537. }
  4538. ]
  4539. }
  4540. ]
  4541. },
  4542. {
  4543. //分类:信息显示
  4544. title: localize(localizationMap.title_info_settings),
  4545. items: [
  4546. {
  4547. //预设表格
  4548. items: [
  4549. {
  4550. type: "dropdown",
  4551. title: localize(localizationMap.category_preset),
  4552. id: "category_preset",
  4553. tooltip: localize(localizationMap.category_preset_tooltip),
  4554. ignore_reset: true, //不显示重置按钮
  4555. options: [
  4556. {
  4557. title: localize(localizationMap.work_type_voice),
  4558. value: "voice"
  4559. },
  4560. {
  4561. title: localize(localizationMap.work_type_game),
  4562. value: "game"
  4563. },
  4564. {
  4565. title: `${localize(localizationMap.work_type_comic)} / ${localize(localizationMap.work_type_illustration)} / ${localize(localizationMap.work_type_voice_comic)}`,
  4566. value: "manga"
  4567. },
  4568. {
  4569. title: localize(localizationMap.work_type_video),
  4570. value: "video"
  4571. },
  4572. {
  4573. title: localize(localizationMap.work_type_novel),
  4574. value: "novel"
  4575. },
  4576. {
  4577. title: localize(localizationMap.work_type_other),
  4578. value: "other"
  4579. }
  4580. ]
  4581. }
  4582. ]
  4583. },
  4584. {
  4585. //音声Preset对应的表格,注意使用Binding来决定表格是否显示
  4586. binding: {
  4587. target: "category_preset",
  4588. value: "voice"
  4589. },
  4590. sortable: true, //为true则代表该表格内的行可以排序
  4591. sort_id: "voice__info_display_order", //若排序则一定要指定id,该id将会存储在设置项中,作为列表记录每个元素的顺序
  4592. items: [
  4593. {
  4594. //销量
  4595. type: "checkbox",
  4596. title: localize(localizationMap.dl_count),
  4597. id: "voice__dl_count", //注意这里不同的preset要改成不同的值
  4598. },
  4599. {
  4600. //社团名
  4601. type: "checkbox",
  4602. title: localize(localizationMap.circle_name),
  4603. id: "voice__circle_name", //注意这里不同的preset要改成不同的值
  4604. },
  4605. {
  4606. //翻译者
  4607. type: "checkbox",
  4608. title: localize(localizationMap.translator_name),
  4609. id: "voice__translator_name",
  4610. },
  4611. {
  4612. //发售日
  4613. type: "checkbox",
  4614. title: localize(localizationMap.release_date),
  4615. id: "voice__release_date",
  4616. },
  4617. {
  4618. //更新日
  4619. type: "checkbox",
  4620. title: localize(localizationMap.update_date),
  4621. id: "voice__update_date",
  4622. },
  4623. {
  4624. //年龄指定
  4625. type: "checkbox",
  4626. title: localize(localizationMap.age_rating),
  4627. id: "voice__age_rating",
  4628. },
  4629. {
  4630. //剧情作者
  4631. type: "checkbox",
  4632. title: localize(localizationMap.scenario),
  4633. id: "voice__scenario",
  4634. },
  4635. {
  4636. //插画作者
  4637. type: "checkbox",
  4638. title: localize(localizationMap.illustration),
  4639. id: "voice__illustration",
  4640. },
  4641. {
  4642. //配音者
  4643. type: "checkbox",
  4644. title: localize(localizationMap.voice_actor),
  4645. id: "voice__voice_actor",
  4646. },
  4647. {
  4648. //音乐作者
  4649. type: "checkbox",
  4650. title: localize(localizationMap.music),
  4651. id: "voice__music",
  4652. },
  4653. {
  4654. //作品标签/分类
  4655. type: "checkbox",
  4656. title: localize(localizationMap.genre),
  4657. id: "voice__genre",
  4658. },
  4659. {
  4660. //文件大小
  4661. type: "checkbox",
  4662. title: localize(localizationMap.file_size),
  4663. id: "voice__file_size",
  4664. }
  4665. ]
  4666. },
  4667. {
  4668. //游戏Preset对应的表格
  4669. binding: {
  4670. target: "category_preset",
  4671. value: "game"
  4672. },
  4673. sortable: true, //为true则代表该表格内的行可以排序
  4674. sort_id: "game__info_display_order",
  4675. items: [
  4676. {
  4677. //销量
  4678. type: "checkbox",
  4679. title: localize(localizationMap.dl_count),
  4680. id: "game__dl_count",
  4681. },
  4682. {
  4683. //社团名
  4684. type: "checkbox",
  4685. title: localize(localizationMap.circle_name),
  4686. id: "game__circle_name",
  4687. },
  4688. {
  4689. //翻译者
  4690. type: "checkbox",
  4691. title: localize(localizationMap.translator_name),
  4692. id: "game__translator_name",
  4693. },
  4694. {
  4695. //发售日
  4696. type: "checkbox",
  4697. title: localize(localizationMap.release_date),
  4698. id: "game__release_date",
  4699. },
  4700. {
  4701. //更新日
  4702. type: "checkbox",
  4703. title: localize(localizationMap.update_date),
  4704. id: "game__update_date",
  4705. },
  4706. {
  4707. //年龄指定
  4708. type: "checkbox",
  4709. title: localize(localizationMap.age_rating),
  4710. id: "game__age_rating",
  4711. },
  4712. {
  4713. //剧情作者
  4714. type: "checkbox",
  4715. title: localize(localizationMap.scenario),
  4716. id: "game__scenario",
  4717. },
  4718. {
  4719. //插画作者
  4720. type: "checkbox",
  4721. title: localize(localizationMap.illustration),
  4722. id: "game__illustration",
  4723. },
  4724. {
  4725. //配音者
  4726. type: "checkbox",
  4727. title: localize(localizationMap.voice_actor),
  4728. id: "game__voice_actor",
  4729. },
  4730. {
  4731. //音乐作者
  4732. type: "checkbox",
  4733. title: localize(localizationMap.music),
  4734. id: "game__music",
  4735. },
  4736. {
  4737. //作品标签/分类
  4738. type: "checkbox",
  4739. title: localize(localizationMap.genre),
  4740. id: "game__genre",
  4741. },
  4742. {
  4743. //文件大小
  4744. type: "checkbox",
  4745. title: localize(localizationMap.file_size),
  4746. id: "game__file_size",
  4747. }
  4748. ]
  4749. },
  4750. {
  4751. //漫画对应preset
  4752. binding: {
  4753. target: "category_preset",
  4754. value: "manga"
  4755. },
  4756. sortable: true, //为true则代表该表格内的行可以排序
  4757. sort_id: "manga__info_display_order",
  4758. items: [
  4759. {
  4760. //销量
  4761. type: "checkbox",
  4762. title: localize(localizationMap.dl_count),
  4763. id: "manga__dl_count",
  4764. },
  4765. {
  4766. //社团名
  4767. type: "checkbox",
  4768. title: localize(localizationMap.circle_name),
  4769. id: "manga__circle_name",
  4770. },
  4771. {
  4772. //翻译者
  4773. type: "checkbox",
  4774. title: localize(localizationMap.translator_name),
  4775. id: "manga__translator_name",
  4776. },
  4777. {
  4778. //发售日
  4779. type: "checkbox",
  4780. title: localize(localizationMap.release_date),
  4781. id: "manga__release_date",
  4782. },
  4783. {
  4784. //更新日
  4785. type: "checkbox",
  4786. title: localize(localizationMap.update_date),
  4787. id: "manga__update_date",
  4788. },
  4789. {
  4790. //年龄指定
  4791. type: "checkbox",
  4792. title: localize(localizationMap.age_rating),
  4793. id: "manga__age_rating",
  4794. },
  4795. {
  4796. //剧情作者
  4797. type: "checkbox",
  4798. title: localize(localizationMap.scenario),
  4799. id: "manga__scenario",
  4800. },
  4801. {
  4802. //插画作者
  4803. type: "checkbox",
  4804. title: localize(localizationMap.illustration),
  4805. id: "manga__illustration",
  4806. },
  4807. {
  4808. //配音者
  4809. type: "checkbox",
  4810. title: localize(localizationMap.voice_actor),
  4811. id: "manga__voice_actor",
  4812. tooltip: localize(localizationMap.work_type_voice_comic)
  4813. },
  4814. {
  4815. //音乐作者
  4816. type: "checkbox",
  4817. title: localize(localizationMap.music),
  4818. id: "manga__music",
  4819. },
  4820. {
  4821. //作品标签/分类
  4822. type: "checkbox",
  4823. title: localize(localizationMap.genre),
  4824. id: "manga__genre",
  4825. },
  4826. {
  4827. //文件大小
  4828. type: "checkbox",
  4829. title: localize(localizationMap.file_size),
  4830. id: "manga__file_size",
  4831. }
  4832. ]
  4833. },
  4834. {
  4835. //视频对应preset
  4836. binding: {
  4837. target: "category_preset",
  4838. value: "video"
  4839. },
  4840. sortable: true, //为true则代表该表格内的行可以排序
  4841. sort_id: "video__info_display_order",
  4842. items: [
  4843. {
  4844. //销量
  4845. type: "checkbox",
  4846. title: localize(localizationMap.dl_count),
  4847. id: "video__dl_count",
  4848. },
  4849. {
  4850. //社团名
  4851. type: "checkbox",
  4852. title: localize(localizationMap.circle_name),
  4853. id: "video__circle_name",
  4854. },
  4855. {
  4856. //翻译者
  4857. type: "checkbox",
  4858. title: localize(localizationMap.translator_name),
  4859. id: "video__translator_name",
  4860. },
  4861. {
  4862. //发售日
  4863. type: "checkbox",
  4864. title: localize(localizationMap.release_date),
  4865. id: "video__release_date",
  4866. },
  4867. {
  4868. //更新日
  4869. type: "checkbox",
  4870. title: localize(localizationMap.update_date),
  4871. id: "video__update_date",
  4872. },
  4873. {
  4874. //年龄指定
  4875. type: "checkbox",
  4876. title: localize(localizationMap.age_rating),
  4877. id: "video__age_rating",
  4878. },
  4879. {
  4880. //剧情作者
  4881. type: "checkbox",
  4882. title: localize(localizationMap.scenario),
  4883. id: "video__scenario",
  4884. },
  4885. {
  4886. //插画作者
  4887. type: "checkbox",
  4888. title: localize(localizationMap.illustration),
  4889. id: "video__illustration",
  4890. },
  4891. {
  4892. //配音者
  4893. type: "checkbox",
  4894. title: localize(localizationMap.voice_actor),
  4895. id: "video__voice_actor",
  4896. },
  4897. {
  4898. //音乐作者
  4899. type: "checkbox",
  4900. title: localize(localizationMap.music),
  4901. id: "video__music",
  4902. },
  4903. {
  4904. //作品标签/分类
  4905. type: "checkbox",
  4906. title: localize(localizationMap.genre),
  4907. id: "video__genre",
  4908. },
  4909. {
  4910. //文件大小
  4911. type: "checkbox",
  4912. title: localize(localizationMap.file_size),
  4913. id: "video__file_size",
  4914. }
  4915. ]
  4916. },
  4917. {
  4918. //小说对应Preset
  4919. binding: {
  4920. target: "category_preset",
  4921. value: "novel"
  4922. },
  4923. sortable: true, //为true则代表该表格内的行可以排序
  4924. sort_id: "novel__info_display_order",
  4925. items: [
  4926. {
  4927. //销量
  4928. type: "checkbox",
  4929. title: localize(localizationMap.dl_count),
  4930. id: "novel__dl_count",
  4931. },
  4932. {
  4933. //社团名
  4934. type: "checkbox",
  4935. title: localize(localizationMap.circle_name),
  4936. id: "novel__circle_name",
  4937. },
  4938. {
  4939. //翻译者
  4940. type: "checkbox",
  4941. title: localize(localizationMap.translator_name),
  4942. id: "novel__translator_name",
  4943. },
  4944. {
  4945. //发售日
  4946. type: "checkbox",
  4947. title: localize(localizationMap.release_date),
  4948. id: "novel__release_date",
  4949. },
  4950. {
  4951. //更新日
  4952. type: "checkbox",
  4953. title: localize(localizationMap.update_date),
  4954. id: "novel__update_date",
  4955. },
  4956. {
  4957. //年龄指定
  4958. type: "checkbox",
  4959. title: localize(localizationMap.age_rating),
  4960. id: "novel__age_rating",
  4961. },
  4962. {
  4963. //剧情作者
  4964. type: "checkbox",
  4965. title: localize(localizationMap.scenario),
  4966. id: "novel__scenario",
  4967. },
  4968. {
  4969. //插画作者
  4970. type: "checkbox",
  4971. title: localize(localizationMap.illustration),
  4972. id: "novel__illustration",
  4973. },
  4974. {
  4975. //配音者
  4976. type: "checkbox",
  4977. title: localize(localizationMap.voice_actor),
  4978. id: "novel__voice_actor",
  4979. },
  4980. {
  4981. //音乐作者
  4982. type: "checkbox",
  4983. title: localize(localizationMap.music),
  4984. id: "novel__music",
  4985. },
  4986. {
  4987. //作品标签/分类
  4988. type: "checkbox",
  4989. title: localize(localizationMap.genre),
  4990. id: "novel__genre",
  4991. },
  4992. {
  4993. //文件大小
  4994. type: "checkbox",
  4995. title: localize(localizationMap.file_size),
  4996. id: "novel__file_size",
  4997. }
  4998. ]
  4999. },
  5000. {
  5001. //其他对应Preset
  5002. binding: {
  5003. target: "category_preset",
  5004. value: "other"
  5005. },
  5006. sortable: true, //为true则代表该表格内的行可以排序
  5007. sort_id: "other__info_display_order",
  5008. items: [
  5009. {
  5010. //销量
  5011. type: "checkbox",
  5012. title: localize(localizationMap.dl_count),
  5013. id: "other__dl_count",
  5014. },
  5015. {
  5016. //社团名
  5017. type: "checkbox",
  5018. title: localize(localizationMap.circle_name),
  5019. id: "other__circle_name",
  5020. },
  5021. {
  5022. //翻译者
  5023. type: "checkbox",
  5024. title: localize(localizationMap.translator_name),
  5025. id: "other__translator_name",
  5026. },
  5027. {
  5028. //发售日
  5029. type: "checkbox",
  5030. title: localize(localizationMap.release_date),
  5031. id: "other__release_date",
  5032. },
  5033. {
  5034. //更新日
  5035. type: "checkbox",
  5036. title: localize(localizationMap.update_date),
  5037. id: "other__update_date",
  5038. },
  5039. {
  5040. //年龄指定
  5041. type: "checkbox",
  5042. title: localize(localizationMap.age_rating),
  5043. id: "other__age_rating",
  5044. },
  5045. {
  5046. //剧情作者
  5047. type: "checkbox",
  5048. title: localize(localizationMap.scenario),
  5049. id: "other__scenario",
  5050. },
  5051. {
  5052. //插画作者
  5053. type: "checkbox",
  5054. title: localize(localizationMap.illustration),
  5055. id: "other__illustration",
  5056. },
  5057. {
  5058. //配音者
  5059. type: "checkbox",
  5060. title: localize(localizationMap.voice_actor),
  5061. id: "other__voice_actor",
  5062. },
  5063. {
  5064. //音乐作者
  5065. type: "checkbox",
  5066. title: localize(localizationMap.music),
  5067. id: "other__music",
  5068. },
  5069. {
  5070. //作品标签/分类
  5071. type: "checkbox",
  5072. title: localize(localizationMap.genre),
  5073. id: "other__genre",
  5074. },
  5075. {
  5076. //文件大小
  5077. type: "checkbox",
  5078. title: localize(localizationMap.file_size),
  5079. id: "other__file_size",
  5080. }
  5081. ]
  5082. }
  5083. ]
  5084. },
  5085. {
  5086. //分类:标签显示
  5087. title: localize(localizationMap.title_tag_settings),
  5088. items: [
  5089. {
  5090. //总开关表格
  5091. items: [
  5092. {
  5093. type: "checkbox",
  5094. title: localize(localizationMap.tag_main_switch),
  5095. tooltip: localize(localizationMap.tag_main_switch_tooltip),
  5096. id: "tag_main_switch"
  5097. },
  5098. {
  5099. //显示评分人数
  5100. type: "checkbox",
  5101. title: localize(localizationMap.show_rate_count),
  5102. id: "show_rate_count",
  5103. indent: 1
  5104. }
  5105. ]
  5106. },
  5107. {
  5108. //标签开关表格
  5109. binding: {
  5110. target: "tag_main_switch",
  5111. value: true
  5112. },
  5113. items: [
  5114. {
  5115. //所有的标签开关集合
  5116. type: "tag_switch",
  5117. //标签之间可以排序
  5118. sortable: true,
  5119. sort_id: "tag_display_order",
  5120. items: [
  5121. {
  5122. //销量
  5123. title: localize(localizationMap.rate),
  5124. id: "tag_rate",
  5125. class: "tag-yellow",
  5126. tooltip: localize(localizationMap.rate_tooltip)
  5127. },
  5128. {
  5129. //作品类型
  5130. title: localize(localizationMap.tag_work_type),
  5131. id: "tag_work_type",
  5132. class: "tag-darkblue",
  5133. tooltip: `
  5134. <div class="${VOICELINK_CLASS}_tags">
  5135. <span class="${VOICELINK_CLASS}_tag-purple">${localize(localizationMap.work_type_game)}</span>
  5136. <span class="${VOICELINK_CLASS}_tag-green">${localize(localizationMap.work_type_comic)}</span>
  5137. <span class="${VOICELINK_CLASS}_tag-teal">${localize(localizationMap.work_type_illustration)}</span>
  5138. <span class="${VOICELINK_CLASS}_tag-gray">${localize(localizationMap.work_type_novel)}</span>
  5139. <span class="${VOICELINK_CLASS}_tag-darkblue">${localize(localizationMap.work_type_video)}</span>
  5140. <span class="${VOICELINK_CLASS}_tag-orange">${localize(localizationMap.work_type_voice)}</span>
  5141. <span class="${VOICELINK_CLASS}_tag-yellow">${localize(localizationMap.work_type_music)}</span>
  5142. <span class="${VOICELINK_CLASS}_tag-gray">${localize(localizationMap.work_type_tool)}</span>
  5143. <span class="${VOICELINK_CLASS}_tag-blue">${localize(localizationMap.work_type_voice_comic)}</span>
  5144. <span class="${VOICELINK_CLASS}_tag-gray">${localize(localizationMap.work_type_other)}</span>
  5145. </div>`,
  5146. },
  5147. {
  5148. //可翻译
  5149. title: localize(localizationMap.tag_translatable),
  5150. id: "tag_translatable",
  5151. class: "tag-green",
  5152. tooltip: localize(localizationMap.tag_translatable_tooltip)
  5153. },
  5154. {
  5155. //不可翻译
  5156. title: localize(localizationMap.tag_not_translatable),
  5157. id: "tag_not_translatable",
  5158. class: "tag-red",
  5159. tooltip: localize(localizationMap.tag_not_translatable_tooltip)
  5160. },
  5161. {
  5162. //翻译作品
  5163. title: localize(localizationMap.tag_translated),
  5164. id: "tag_translated",
  5165. class: "tag-teal",
  5166. tooltip: localize(localizationMap.tag_translated_tooltip)
  5167. },
  5168. {
  5169. //支持语言
  5170. title: localize(localizationMap.tag_language_support),
  5171. id: "tag_language_support",
  5172. class: "tag-pink",
  5173. tooltip: `
  5174. <div class="${VOICELINK_CLASS}_tags">
  5175. <span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_japanese)}</span>
  5176. <span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_simplified_chinese)}</span>
  5177. <span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_traditional_chinese)}</span>
  5178. <span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_english)}</span>
  5179. <span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_korean)}</span>
  5180. <span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_german)}</span>
  5181. <span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_french)}</span>
  5182. <span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_indonesian)}</span>
  5183. <span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_italian)}</span>
  5184. <span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_portuguese)}</span>
  5185. <span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_swedish)}</span>
  5186. <span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_thai)}</span>
  5187. <span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_vietnamese)}</span>
  5188. </div>
  5189. `
  5190. },
  5191. {
  5192. //特典作品
  5193. title: localize(localizationMap.tag_bonus_work),
  5194. id: "tag_bonus_work",
  5195. class: "tag-yellow",
  5196. tooltip: localize(localizationMap.tag_bonus_work_tooltip)
  5197. },
  5198. {
  5199. //含特典
  5200. title: localize(localizationMap.tag_has_bonus),
  5201. id: "tag_has_bonus",
  5202. class: "tag-orange",
  5203. tooltip: localize(localizationMap.tag_has_bonus_tooltip)
  5204. },
  5205. {
  5206. //文件格式
  5207. title: localize(localizationMap.tag_file_format),
  5208. id: "tag_file_format",
  5209. class: "tag-darkblue",
  5210. tooltip: localize(localizationMap.tag_file_format_tooltip)
  5211. },
  5212. {
  5213. //已下架
  5214. title: localize(localizationMap.tag_no_longer_available),
  5215. id: "tag_no_longer_available",
  5216. class: "tag-gray",
  5217. },
  5218. {
  5219. //AI & 部分AI
  5220. title: localize(localizationMap.tag_ai),
  5221. id: "tag_ai",
  5222. class: "tag-purple",
  5223. tooltip: localize(localizationMap.tag_ai_tooltip)
  5224. }
  5225. ]
  5226. },
  5227. ]
  5228. },
  5229. {
  5230. //翻译情况显示表格
  5231. items: [
  5232. {
  5233. //翻译情况显示开关
  5234. type: "checkbox",
  5235. title: localize(localizationMap.tag_translation_request),
  5236. id: "tag_translation_request",
  5237. tooltip: localize(localizationMap.tag_translation_request_tooltip)
  5238. },
  5239. ]
  5240. },
  5241. {
  5242. //翻译情况标签显示表格
  5243. binding: {
  5244. target: "tag_translation_request",
  5245. value: true
  5246. },
  5247. items: [
  5248. {
  5249. //各种翻译情况显示
  5250. type: "tag_switch",
  5251. sortable: true,
  5252. sort_id: "tag_translation_request_display_order",
  5253. items: [
  5254. {
  5255. //英语
  5256. title: `${localize(localizationMap.language_english_abbr)} 1-1`,
  5257. id: "tag_translation_request_english",
  5258. class: "tag-orange",
  5259. tooltip: localize(localizationMap.language_english)
  5260. },
  5261. {
  5262. //简体中文
  5263. title: `${localize(localizationMap.language_simplified_chinese_abbr)} 1-1`,
  5264. id: "tag_translation_request_simplified_chinese",
  5265. class: "tag-orange",
  5266. tooltip: localize(localizationMap.language_simplified_chinese)
  5267. },
  5268. {
  5269. //繁体中文
  5270. title: `${localize(localizationMap.language_traditional_chinese_abbr)} 1-1`,
  5271. id: "tag_translation_request_traditional_chinese",
  5272. class: "tag-orange",
  5273. tooltip: localize(localizationMap.language_traditional_chinese)
  5274. },
  5275. {
  5276. //韩语
  5277. title: `${localize(localizationMap.language_korean_abbr)} 1-1`,
  5278. id: "tag_translation_request_korean",
  5279. class: "tag-orange",
  5280. tooltip: localize(localizationMap.language_korean)
  5281. },
  5282. {
  5283. //西班牙语
  5284. title: `${localize(localizationMap.language_spanish_abbr)} 1-1`,
  5285. id: "tag_translation_request_spanish",
  5286. class: "tag-orange",
  5287. tooltip: localize(localizationMap.language_spanish)
  5288. },
  5289. {
  5290. //德语
  5291. title: `${localize(localizationMap.language_german_abbr)} 1-1`,
  5292. id: "tag_translation_request_german",
  5293. class: "tag-orange",
  5294. tooltip: localize(localizationMap.language_german)
  5295. },
  5296. {
  5297. //法语
  5298. title: `${localize(localizationMap.language_french_abbr)} 1-1`,
  5299. id: "tag_translation_request_french",
  5300. class: "tag-orange",
  5301. tooltip: localize(localizationMap.language_french)
  5302. },
  5303. {
  5304. //印尼语
  5305. title: `${localize(localizationMap.language_indonesian_abbr)} 1-1`,
  5306. id: "tag_translation_request_indonesian",
  5307. class: "tag-orange",
  5308. tooltip: localize(localizationMap.language_indonesian)
  5309. },
  5310. {
  5311. //意大利语
  5312. title: `${localize(localizationMap.language_italian_abbr)} 1-1`,
  5313. id: "tag_translation_request_italian",
  5314. class: "tag-orange",
  5315. tooltip: localize(localizationMap.language_italian)
  5316. },
  5317. {
  5318. //葡萄牙语
  5319. title: `${localize(localizationMap.language_portuguese_abbr)} 1-1`,
  5320. id: "tag_translation_request_portuguese",
  5321. class: "tag-orange",
  5322. tooltip: localize(localizationMap.language_portuguese)
  5323. },
  5324. {
  5325. //瑞典语
  5326. title: `${localize(localizationMap.language_swedish_abbr)} 1-1`,
  5327. id: "tag_translation_request_swedish",
  5328. class: "tag-orange",
  5329. tooltip: localize(localizationMap.language_swedish)
  5330. },
  5331. {
  5332. //泰语
  5333. title: `${localize(localizationMap.language_thai_abbr)} 1-1`,
  5334. id: "tag_translation_request_thai",
  5335. class: "tag-orange",
  5336. tooltip: localize(localizationMap.language_thai)
  5337. },
  5338. {
  5339. //越南语
  5340. title: `${localize(localizationMap.language_vietnamese_abbr)} 1-1`,
  5341. id: "tag_translation_request_vietnamese",
  5342. class: "tag-orange",
  5343. tooltip: localize(localizationMap.language_vietnamese)
  5344. }
  5345. ]
  5346. }
  5347. ]
  5348. }
  5349. ]
  5350. }
  5351. ]
  5352. };
  5353. }
  5354. class SettingPageBuilder {
  5355. constructor(structure, settings) {
  5356. this.structure = structure;
  5357. this.settings = settings;
  5358. this.container = null;
  5359. }
  5360.  
  5361. getClass(name) {
  5362. if(!VOICELINK_CLASS || VOICELINK_CLASS === "") return name;
  5363. return `${VOICELINK_CLASS}_${name}`;
  5364. }
  5365.  
  5366. build(useTemp = false) {
  5367. const f = this.structure;
  5368. let tempSettings = this.settings;
  5369.  
  5370. //若未要求则清空设置暂存
  5371. if(useTemp){
  5372. tempSettings = {};
  5373. for(let key in this.settings.temp_edited){
  5374. if(!key.startsWith("_s_")) continue;
  5375. tempSettings[key] = this.settings.temp_edited[key];
  5376. }
  5377. }else{
  5378. this.settings.clearTemp();
  5379. }
  5380.  
  5381. //创建container
  5382. const container = document.createElement("div");
  5383. container.className = this.getClass("container");
  5384. container.id = this.getClass("settings-container");
  5385. this.container = container;
  5386.  
  5387. //创建关闭按钮
  5388. const closeButton = document.createElement("button");
  5389. closeButton.id = this.getClass("button-close");
  5390. closeButton.innerText = "X";
  5391. closeButton.onclick = () => {
  5392. container.remove();
  5393. };
  5394. container.appendChild(closeButton);
  5395.  
  5396. //创建标题
  5397. const title = document.createElement("h1");
  5398. title.innerText = f.title;
  5399. container.appendChild(title);
  5400.  
  5401. //遍历构建Section
  5402. for(const section of f.items){
  5403. container.appendChild(this.buildSection(section));
  5404. }
  5405.  
  5406. //添加保存、重置按钮
  5407. const buttonContainer = document.createElement("div");
  5408. buttonContainer.className = this.getClass("button-container");
  5409. const saveButton = document.createElement("button");
  5410. saveButton.id = this.getClass("button-save");
  5411. saveButton.innerText = localize(localizationMap.button_save);
  5412. saveButton.onclick = () => {
  5413. try{
  5414. this.settings.save();
  5415. window.alert(localize(localizationMap.save_complete))
  5416. container.remove();
  5417. }catch (e){
  5418. window.alert(e);
  5419. }
  5420. };
  5421. const resetButton = document.createElement("button");
  5422. resetButton.id = this.getClass("button-reset");
  5423. resetButton.innerText = localize(localizationMap.button_reset);
  5424. resetButton.onclick = () => {
  5425. if(!window.confirm(localize(localizationMap.reset_confirm))){
  5426. return;
  5427. }
  5428. try{
  5429. // this.settings.reset();
  5430. window.alert(localize(localizationMap.reset_complete))
  5431. }catch (e) {
  5432. window.alert(e);
  5433. }
  5434. this.refreshSettings(this.settings.default_backup);
  5435. };
  5436. buttonContainer.appendChild(saveButton);
  5437. buttonContainer.appendChild(resetButton);
  5438. container.appendChild(buttonContainer);
  5439.  
  5440. this.initSortableElement();
  5441. this.refreshSettings(tempSettings);
  5442. return container;
  5443. };
  5444.  
  5445. buildSection(section){
  5446. //创建容器
  5447. const container = document.createElement("div");
  5448. container.className = this.getClass("section-container");
  5449.  
  5450. //创建标题
  5451. const title = document.createElement("h2");
  5452. title.innerText = section.title;
  5453. container.appendChild(title);
  5454.  
  5455. //遍历构建Table
  5456. for(const item of section.items){
  5457. container.appendChild(this.buildTable(item, container));
  5458. }
  5459.  
  5460. return container;
  5461. };
  5462.  
  5463. buildTable(table, section){
  5464. //创建table
  5465. const tableElement = document.createElement("table");
  5466.  
  5467. //创建可能存在的preset绑定
  5468. this.createBinding(table, tableElement, section);
  5469.  
  5470. //遍历构建Row(先把row缓存在列表里,经过排序后构建)
  5471. let rowList = [];
  5472. const nodesCache = document.createElement("div");
  5473. for(const row of table.items){
  5474. let rowElement = this.buildRow(row, nodesCache);
  5475. if(table.sortable){
  5476. this.setSortable(rowElement);
  5477. }
  5478. rowList.push(rowElement);
  5479.  
  5480. //存入缓存用于绑定
  5481. nodesCache.appendChild(rowElement);
  5482. }
  5483.  
  5484. //重置按钮行
  5485. let resetRow;
  5486. if(table.sortable){
  5487. tableElement.setAttribute("data-sort-id", table.sort_id);
  5488. this.sortSortable(rowList, table.sort_id);
  5489.  
  5490. //若可排序则还要添加重置按钮
  5491. if(table.ignore_reset !== true){
  5492. resetRow = document.createElement("tr");
  5493. const resetCell = document.createElement("td");
  5494. resetCell.classList.add(this.getClass("input-cell"))
  5495. resetCell.colSpan = 2;
  5496. resetCell.style.setProperty("text-align", "right", "important"); //textAlign = "right !important";
  5497. const resetButton = document.createElement("button");
  5498. resetButton.classList.add(this.getClass("button-flat"));
  5499. resetButton.title = localize(localizationMap.reset_order);
  5500. resetButton.style.setProperty("margin-right", "0", "important"); //marginRight = "0 !important";
  5501. resetButton.style.setProperty("margin-bottom", "0", "important"); //marginBottom = "0 !important";
  5502. const icon = document.createElement("span");
  5503. icon.classList.add(this.getClass("reset-btn-small"));
  5504. resetButton.appendChild(icon);
  5505. const title = document.createElement("span");
  5506. title.innerText = localize(localizationMap.reset_order);
  5507. resetButton.appendChild(title);
  5508. resetButton.onclick = () => {
  5509. if(!window.confirm(localize(localizationMap.reset_order_confirm))){
  5510. return;
  5511. }
  5512. this.reorderSortable(table.sort_id, this.settings.getDefaultValue(table.sort_id));
  5513. // tableElement.insertBefore(resetRow, tableElement.firstChild);
  5514. };
  5515. resetCell.appendChild(resetButton);
  5516. resetRow.appendChild(resetCell);
  5517. // tableElement.appendChild(resetRow);
  5518. }
  5519. }
  5520.  
  5521. rowList.forEach((row) => {
  5522. tableElement.appendChild(row);
  5523. });
  5524. if(resetRow){
  5525. tableElement.appendChild(resetRow);
  5526. }
  5527.  
  5528. return tableElement;
  5529. };
  5530.  
  5531. buildRow(row, table){
  5532. //创建row
  5533. let rowElement = document.createElement("tr");
  5534.  
  5535. //根据类型创建内容
  5536. switch (row.type) {
  5537. case "checkbox":
  5538. rowElement = this.createToggleRow(row);
  5539. break;
  5540. case "dropdown":
  5541. rowElement = this.createDropdownRow(row);
  5542. break;
  5543. case "input":
  5544. rowElement = this.createInputRow(row);
  5545. break;
  5546. case "tag_switch":
  5547. rowElement = this.createTagSwitchRow(row);
  5548. break;
  5549. default:
  5550. console.error(`Unknown row type: ${row.type}`);
  5551. break;
  5552. }
  5553.  
  5554. //设置Binding
  5555. if(row.binding){
  5556. this.createBinding(row, rowElement, table)
  5557. }
  5558.  
  5559. rowElement.classList.add(this.getClass("setting"));
  5560. if(row.id){
  5561. rowElement.setAttribute("data-id", row.id);
  5562. }
  5563. return rowElement;
  5564. };
  5565.  
  5566. createToggleRow(row){
  5567. //创建Row
  5568. const rowElement = document.createElement("tr");
  5569. if(row.indent) {
  5570. rowElement.classList.add(this.getClass(`indent-${row.indent}`));
  5571. }
  5572.  
  5573. //创建设置项标题
  5574. const titleCell = document.createElement("td");
  5575. titleCell.className = this.getClass("tooltip");
  5576. const title = document.createElement("span");
  5577. title.className = this.getClass("row-title") + " " + this.getClass("ignore-drag");
  5578. title.innerHTML = Csp.createHTML(row.title);
  5579. titleCell.appendChild(title);
  5580.  
  5581. if(row.tooltip) {
  5582. const tooltip = document.createElement("span");
  5583. tooltip.className = this.getClass("tooltip-text");
  5584. tooltip.innerHTML = Csp.createHTML(row.tooltip);
  5585. titleCell.appendChild(tooltip);
  5586. }
  5587.  
  5588. //创建开关和重置按钮
  5589. const settingId = `_s_${row.id}`;
  5590. const inputCell = document.createElement("td");
  5591. inputCell.classList.add(this.getClass("input-cell"));
  5592. const inputContainer = document.createElement("div");
  5593. inputContainer.classList.add(this.getClass("toggle-container"));
  5594. inputCell.appendChild(inputContainer);
  5595.  
  5596. //创建开关
  5597. const input = document.createElement("input");
  5598. input.type = "checkbox";
  5599. input.id = this.getClass(row.id);
  5600. input.name = input.id;
  5601. // inputCell.appendChild(input);
  5602. inputContainer.appendChild(input);
  5603.  
  5604. const label = document.createElement("label");
  5605. label.classList.add(this.getClass("toggle"));
  5606. label.setAttribute("for", input.id);
  5607. const hidden = document.createElement("span");
  5608. hidden.classList.add(this.getClass("hidden"));
  5609. hidden.innerHTML = Csp.createHTML(row.title);
  5610. label.appendChild(hidden);
  5611. // inputCell.appendChild(label);
  5612. inputContainer.appendChild(label);
  5613.  
  5614. //创建重置按钮
  5615. if(row.ignore_reset !== true){
  5616. const defaultValue = this.settings.getDefaultValue(settingId);
  5617. const resetButton = document.createElement("button");
  5618. resetButton.className = this.getClass("reset-btn-small") + " " + this.getClass("ignore-drag");
  5619. resetButton.title = localize(localizationMap.button_reset);
  5620. resetButton.onclick = () => {
  5621. input.checked = defaultValue === true;
  5622. input.dispatchEvent(new Event("change"));
  5623. };
  5624. // inputCell.insertBefore(resetButton, inputCell.firstChild);
  5625. inputContainer.insertBefore(resetButton, inputContainer.firstChild);
  5626.  
  5627. input.addEventListener("change", () => {
  5628. //决定是否显示重置按钮
  5629. resetButton.style.setProperty("display", input.checked === defaultValue ? "none" : "inline-block", "important"); //display = input.checked === defaultValue ? "none" : "inline-block";
  5630. });
  5631. }
  5632.  
  5633. //监听器都创建好了再设置初始值
  5634. input.addEventListener("change", () => {
  5635. //更新到暂存设置
  5636. this.settings.saveTemp(settingId, input.checked);
  5637. })
  5638. input.checked = this.settings[settingId] === true;
  5639. input.dispatchEvent(new Event("change"));
  5640.  
  5641. rowElement.appendChild(titleCell);
  5642. rowElement.appendChild(inputCell);
  5643. return rowElement;
  5644. };
  5645.  
  5646. createDropdownRow(row){
  5647. const rowElement = document.createElement("tr");
  5648. if(row.indent) {
  5649. rowElement.classList.add(this.getClass(`indent-${row.indent}`));
  5650. }
  5651.  
  5652. const titleCell = document.createElement("td");
  5653. titleCell.className = this.getClass("tooltip");
  5654. const label = document.createElement("label");
  5655. label.className = this.getClass("row-title") + " " + this.getClass("ignore-drag");
  5656. label.setAttribute("for", this.getClass(row.id));
  5657. label.innerHTML = Csp.createHTML(row.title);
  5658. titleCell.appendChild(label);
  5659.  
  5660. if(row.tooltip) {
  5661. const tooltip = document.createElement("span");
  5662. tooltip.className = this.getClass("tooltip-text");
  5663. tooltip.innerHTML = Csp.createHTML(row.tooltip);
  5664. titleCell.appendChild(tooltip);
  5665. }
  5666.  
  5667. const inputCell = document.createElement("td");
  5668. inputCell.classList.add(this.getClass("input-cell"));
  5669. const select = document.createElement("select");
  5670. select.className = this.getClass("ignore-drag");
  5671. select.id = this.getClass(row.id);
  5672. select.name = select.id;
  5673. for(const item of row.options){
  5674. const option = document.createElement("option");
  5675. option.value = item.value;
  5676. option.innerText = item.title;
  5677. select.appendChild(option);
  5678. }
  5679. inputCell.appendChild(select);
  5680.  
  5681. //创建重置按钮
  5682. const settingId = `_s_${row.id}`;
  5683. if(row.ignore_reset !== true){
  5684. const defaultValue = this.settings.getDefaultValue(settingId);
  5685. const resetButton = document.createElement("button");
  5686. resetButton.className = this.getClass("reset-btn-small") + " " + this.getClass("ignore-drag");
  5687. resetButton.title = localize(localizationMap.button_reset);
  5688. resetButton.onclick = () => {
  5689. select.value = defaultValue;
  5690. select.dispatchEvent(new Event("change"));
  5691. };
  5692. inputCell.insertBefore(resetButton, inputCell.firstChild);
  5693.  
  5694. select.addEventListener("change", () => {
  5695. //决定是否显示重置按钮
  5696. resetButton.style.setProperty("display", select.value === defaultValue ? "none" : "inline-block", "important"); //display = select.value === defaultValue ? "none" : "inline-block";
  5697. });
  5698. }
  5699.  
  5700. //监听器都创建好了再设置初始值
  5701. select.addEventListener("change", () => {
  5702. //更新到暂存设置
  5703. this.settings.saveTemp(settingId, select.value);
  5704. })
  5705. select.value = this.settings[settingId];
  5706. select.dispatchEvent(new Event("change"));
  5707.  
  5708. rowElement.appendChild(titleCell);
  5709. rowElement.appendChild(inputCell);
  5710. return rowElement;
  5711. };
  5712.  
  5713. createInputRow(row){
  5714. const rowElement = document.createElement("tr");
  5715. if(row.indent) {
  5716. rowElement.classList.add(this.getClass(`indent-${row.indent}`));
  5717. }
  5718.  
  5719. const titleCell = document.createElement("td");
  5720. titleCell.className = this.getClass("tooltip");
  5721. const label = document.createElement("label");
  5722. label.className = this.getClass("row-title") + " " + this.getClass("ignore-drag");
  5723. label.setAttribute("for", this.getClass(row.id));
  5724. label.innerHTML = Csp.createHTML(row.title);
  5725. titleCell.appendChild(label);
  5726.  
  5727. if(row.tooltip) {
  5728. const tooltip = document.createElement("span");
  5729. tooltip.className = this.getClass("tooltip-text");
  5730. tooltip.innerHTML = Csp.createHTML(row.tooltip);
  5731. titleCell.appendChild(tooltip);
  5732. }
  5733.  
  5734. const inputCell = document.createElement("td");
  5735. inputCell.classList.add(this.getClass("input-cell"));
  5736. const input = document.createElement("input");
  5737. input.type = "text";
  5738. input.id = this.getClass(row.id);
  5739. input.name = input.id;
  5740. inputCell.appendChild(input);
  5741.  
  5742. //创建重置按钮
  5743. const settingId = `_s_${row.id}`;
  5744. if(row.ignore_reset !== true){
  5745. const defaultValue = this.settings.getDefaultValue(settingId);
  5746. const resetButton = document.createElement("button");
  5747. resetButton.className = this.getClass("reset-btn-small") + " " + this.getClass("ignore-drag");
  5748. resetButton.title = localize(localizationMap.button_reset);
  5749. resetButton.onclick = () => {
  5750. input.value = defaultValue;
  5751. input.dispatchEvent(new Event("change"));
  5752. };
  5753. inputCell.insertBefore(resetButton, inputCell.firstChild);
  5754.  
  5755. input.addEventListener("change", () => {
  5756. //决定是否显示重置按钮
  5757. resetButton.style.setProperty("display", input.value === defaultValue ? "none" : "inline-block", "important"); //display = input.value === defaultValue ? "none" : "inline-block";
  5758. });
  5759. }
  5760.  
  5761. //监听器都创建好了再设置初始值
  5762. input.addEventListener("change", () => {
  5763. //更新到暂存设置
  5764. this.settings.saveTemp(settingId, input.value);
  5765. })
  5766. input.value = this.settings[settingId];
  5767. input.dispatchEvent(new Event("change"));
  5768.  
  5769. rowElement.appendChild(titleCell);
  5770. rowElement.appendChild(inputCell);
  5771. return rowElement;
  5772. };
  5773.  
  5774. createTagSwitchRow(row){
  5775. const rowElement = document.createElement("tr");
  5776. if(row.indent) {
  5777. rowElement.classList.add(this.getClass(`indent-${row.indent}`));
  5778. }
  5779.  
  5780. const tagCell = document.createElement("td");
  5781. tagCell.colSpan = 2;
  5782. //用内部容器再次包裹标签,保证重置按钮的位置
  5783. const tagContainer = document.createElement("div");
  5784. tagContainer.className = this.getClass("tags");
  5785. tagCell.appendChild(tagContainer);
  5786.  
  5787. const tagList = [];
  5788. for(const tag of row.items){
  5789. const tagSpan = document.createElement("label");
  5790. tagSpan.classList.add(this.getClass(tag["class"]));
  5791. tagSpan.innerText = tag.title;
  5792. tagSpan.setAttribute("for", this.getClass(tag.id));
  5793. tagSpan.setAttribute("data-id", tag.id);
  5794. this.setSortable(tagSpan);
  5795.  
  5796. //添加switch
  5797. const settingId = `_s_${tag.id}`;
  5798. const switchInput = document.createElement("input");
  5799. switchInput.style.setProperty("display", "none", "important"); //display = "none !important";
  5800. switchInput.type = "checkbox";
  5801. switchInput.id = this.getClass(tag.id);
  5802. switchInput.name = switchInput.id;
  5803. switchInput.addEventListener("change", (event) => {
  5804. if(event.target.checked) {
  5805. tagSpan.classList.remove(this.getClass("tag-off"));
  5806. }else if(!tagSpan.classList.contains(this.getClass("tag-off"))) {
  5807. tagSpan.classList.add(this.getClass("tag-off"));
  5808. }
  5809.  
  5810. //更新到暂存设置
  5811. this.settings.saveTemp(settingId, switchInput.checked);
  5812. })
  5813. switchInput.checked = this.settings[`_s_${tag.id}`] === true;
  5814. switchInput.dispatchEvent(new Event("change"));
  5815.  
  5816. tagSpan.classList.toggle(this.getClass("tag-off"), !switchInput.checked);
  5817. tagSpan.appendChild(switchInput);
  5818.  
  5819. if(tag.tooltip) {
  5820. tagSpan.classList.add(this.getClass("tooltip"));
  5821. const tooltip = document.createElement("span");
  5822. tooltip.className = this.getClass("tooltip-text");
  5823. tooltip.innerHTML = Csp.createHTML(tag.tooltip);
  5824. tagSpan.appendChild(tooltip);
  5825. }
  5826.  
  5827. tagList.push(tagSpan);
  5828. }
  5829.  
  5830. if(row.sortable){
  5831. tagContainer.setAttribute("data-sort-id", row.sort_id);
  5832. this.sortSortable(tagList, row.sort_id);
  5833.  
  5834. //添加排列重置按钮
  5835. if(row.ignore_reset !== true){
  5836. const resetOrderButton = document.createElement("button");
  5837. resetOrderButton.classList.add(this.getClass("button-flat"));
  5838. resetOrderButton.title = localize(localizationMap.button_reset);
  5839. resetOrderButton.onclick = () => {
  5840. if(!window.confirm(localize(localizationMap.reset_order_confirm))){
  5841. return;
  5842. }
  5843. this.reorderSortable(row.sort_id, this.settings.getDefaultValue(row.sort_id));
  5844. };
  5845.  
  5846. const icon = document.createElement("span");
  5847. icon.classList.add(this.getClass("reset-btn-small"));
  5848. resetOrderButton.appendChild(icon);
  5849.  
  5850. const title = document.createElement("span");
  5851. title.innerText = localize(localizationMap.reset_order);
  5852. resetOrderButton.appendChild(title);
  5853.  
  5854. tagCell.appendChild(resetOrderButton);
  5855. // tagCell.insertBefore(resetOrderButton, tagCell.firstChild);
  5856. }
  5857. }
  5858.  
  5859. //添加设置重置按钮
  5860. if(row.ignore_reset !== true){
  5861. const resetSettingsButton = document.createElement("button");
  5862. resetSettingsButton.classList.add(this.getClass("button-flat"));
  5863. resetSettingsButton.title = localize(localizationMap.button_reset);
  5864. resetSettingsButton.onclick = () => {
  5865. if(!window.confirm(localize(localizationMap.reset_confirm))){
  5866. return;
  5867. }
  5868. for (const item of row.items) {
  5869. let tag = tagContainer.querySelector("#" + this.getClass(item.id));
  5870. if(!tag) continue;
  5871. tag.checked = this.settings.getDefaultValue(item.id);
  5872. tag.dispatchEvent(new Event("change"));
  5873. }
  5874. };
  5875.  
  5876. const icon = document.createElement("span");
  5877. icon.classList.add(this.getClass("reset-btn-small"));
  5878. resetSettingsButton.appendChild(icon);
  5879.  
  5880. const title = document.createElement("span");
  5881. title.innerText = localize(localizationMap.button_reset);
  5882. resetSettingsButton.appendChild(title);
  5883.  
  5884. tagCell.appendChild(resetSettingsButton);
  5885. // tagCell.insertBefore(resetOrderButton, tagCell.firstChild);
  5886. }
  5887.  
  5888.  
  5889. tagList.forEach((tag) => {
  5890. tagContainer.appendChild(tag);
  5891. });
  5892. rowElement.appendChild(tagCell);
  5893. return rowElement;
  5894. };
  5895.  
  5896. createBinding(data, element, section){
  5897. if(!data.binding || !this.container) return;
  5898.  
  5899. const binding = data.binding;
  5900. const target = section.querySelector("#" + this.getClass(binding.target));
  5901. if(!target) return;
  5902.  
  5903. let showState = "unset";
  5904. switch (element.nodeName){
  5905. case "TABLE":
  5906. showState = "table";
  5907. break;
  5908. case "TR":
  5909. showState = "table-row";
  5910. break;
  5911. }
  5912.  
  5913. target.addEventListener("change", (event) => {
  5914. const value = event.target.value;
  5915. const checked = event.target.checked;
  5916. element.style.setProperty("display", value === binding.value || checked === binding.value ? showState : "none", "important"); //display = value === binding.value || checked === binding.value ? showState : "none";
  5917. });
  5918.  
  5919. element.style.setProperty("display", target.value === binding.value || target.checked === binding.value ? showState : "none", "important"); //display = target.value === binding.value || target.checked === binding.value ? showState : "none";
  5920. };
  5921.  
  5922. sortSortable(sortList, sortId, orderList){
  5923. orderList = orderList ? orderList : this.settings[`_s_${sortId}`];
  5924. if(!orderList) return;
  5925.  
  5926. for (let i = 0; i < orderList.length; ++i) {
  5927. for (let j = 0; j < sortList.length; ++j){
  5928. if(orderList[i] === sortList[j].getAttribute("data-id")) {
  5929. let tmp = sortList[i];
  5930. sortList[i] = sortList[j];
  5931. sortList[j] = tmp;
  5932. break;
  5933. }
  5934. }
  5935. }
  5936. };
  5937.  
  5938. setSortable(ele){
  5939. ele.classList.add(this.getClass("sortable"));
  5940. };
  5941.  
  5942. refreshSettings(settings) {
  5943. //刷新设置项值的展示情况(保持input值和设置中的实际值一致)
  5944. //清空设置暂存
  5945. this.settings.clearTemp();
  5946. settings = settings ? settings : this.settings;
  5947.  
  5948. //刷新设置项
  5949. for(const key in settings){
  5950. if(!key.startsWith("_s_")) continue;
  5951. const targetId = this.getClass(key.substring(3));
  5952.  
  5953. if(settings[key] && Array.isArray(settings[key])){
  5954. //可能是排序对象,先搜索排序目标,搜不到再按默认值处理
  5955. if(this.reorderSortable(key.substring(3), settings[key])) continue;
  5956. }
  5957.  
  5958. //非排序对象
  5959. const target = this.container.querySelector("#" + targetId);
  5960. if(!target) continue;
  5961.  
  5962. if(target.tagName === "INPUT") {
  5963. if(target.type === "checkbox") {
  5964. target.checked = settings[key] === true;
  5965. }else{
  5966. target.value = settings[key];
  5967. }
  5968. }else if(target.tagName === "SELECT") {
  5969. target.value = settings[key];
  5970. }
  5971.  
  5972. if(key === "_s_lang") continue;
  5973. target.dispatchEvent(new Event("change"));
  5974. }
  5975. };
  5976.  
  5977. reorderSortable(sort_id, orderList) {
  5978. const target = this.container.querySelector(`*[data-sort-id="${sort_id}"]`);
  5979. if(target) {
  5980. let list = [...target.children];
  5981. this.sortSortable(list, sort_id, orderList);
  5982.  
  5983. for(let i = 0; i < list.length; ++i) {
  5984. target.removeChild(list[i]);
  5985. }
  5986.  
  5987. for(let i = 0; i < list.length; ++i) {
  5988. target.appendChild(list[i]);
  5989. }
  5990.  
  5991. this.settings.saveTemp(sort_id, list.map(ele => ele.getAttribute("data-id")).filter(val => val && val !== ""));
  5992. return true;
  5993. }
  5994. return false;
  5995. }
  5996.  
  5997. initSortableElement() {
  5998. //初始化排序组件
  5999. const sortableGroups = this.container.querySelectorAll(`.${this.getClass('sortable')}`);
  6000. sortableGroups.forEach(group => {
  6001. new Sortable(group, group.parentElement.getAttribute("data-sort-id"), this.settings);
  6002. });
  6003. };
  6004.  
  6005. }
  6006. class Sortable {
  6007. constructor(element, sort_id, settings, options) {
  6008. this.element = element;
  6009. this.options = options;
  6010. this.sortId = sort_id;
  6011. this.settings = settings;
  6012. this.dragging = null;
  6013.  
  6014. if(!sort_id) {
  6015. throw new Error("sort_id is required");
  6016. }
  6017.  
  6018. this.init();
  6019. }
  6020.  
  6021. getClass(name) {
  6022. if(!VOICELINK_CLASS || VOICELINK_CLASS === "") return name;
  6023. return `${VOICELINK_CLASS}_${name}`;
  6024. }
  6025.  
  6026. init() {
  6027. this.element.addEventListener('mousedown', this.onMouseDown.bind(this));
  6028. document.addEventListener('mousemove', this.onMouseMove.bind(this));
  6029. document.addEventListener('mouseup', this.onMouseUp.bind(this));
  6030. }
  6031.  
  6032. onMouseDown(event) {
  6033. if(event.target.nodeName === "INPUT" || event.target.classList.contains(this.getClass("toggle")) || event.target.classList.contains(this.getClass("ignore-drag"))) return;
  6034. if (event.target.closest(`.${this.getClass('sortable')}`)) {
  6035. this.dragging = event.target.closest(`.${this.getClass('sortable')}`);
  6036. this.dragging.classList.add(this.getClass('dragging'));
  6037. }
  6038. }
  6039.  
  6040. onMouseMove(event) {
  6041. if (this.dragging) {
  6042. event.preventDefault();
  6043.  
  6044. try{
  6045. let sibling = document.elementFromPoint(event.clientX, event.clientY).closest(`.${this.getClass('sortable')}`);
  6046. if (sibling && sibling !== this.dragging) {
  6047. this.element.parentNode.insertBefore(this.dragging, sibling.nextSibling === this.dragging ? sibling : sibling.nextSibling);
  6048. //暂存当前顺序
  6049. const order = [...this.element.parentNode.children].map(item => item.getAttribute("data-id")).filter(item => item || item === "");
  6050. this.settings.saveTemp(this.sortId, order);
  6051. }
  6052. }catch (e) {
  6053. console.error(e)
  6054. }
  6055.  
  6056. }
  6057. }
  6058.  
  6059. onMouseUp(event) {
  6060. if (this.dragging) {
  6061. this.dragging.classList.remove(this.getClass('dragging'));
  6062. this.dragging = null;
  6063. }
  6064. }
  6065. }
  6066. const SettingsPopup = {
  6067. showPopup(useTemp = false) {
  6068. let uiBuilder = new SettingPageBuilder(getSettingsUi(), settings);
  6069. let ui = uiBuilder.build(useTemp);
  6070. const displayLangElement = ui.querySelector(`#${VOICELINK_CLASS}_lang`);
  6071. displayLangElement.addEventListener("change", e => {
  6072. settings._s_lang = displayLangElement.value;
  6073. GM_setValue("lang", settings._s_lang);
  6074. ui.remove();
  6075. SettingsPopup.showPopup(true);
  6076. });
  6077. document.body.appendChild(ui);
  6078. }
  6079. };
  6080.  
  6081. let isInit = false;
  6082. let observing = false;
  6083. function init () {
  6084. if(!isInit) {
  6085. const style = document.createElement("style");
  6086. style.innerHTML = Csp.createHTML(POPUP_CSS + SETTINGS_CSS);
  6087. document.head.appendChild(style);
  6088. // SettingsPopup.getPopup()
  6089. GM_registerMenuCommand("Settings - 设置", () => SettingsPopup.showPopup())
  6090. GM_registerMenuCommand("Notice - 公告", () => showUpdateNotice(true));
  6091. GM_registerMenuCommand("Home / Update - 主页 / 更新", () =>
  6092. GM_openInTab(IS_PREVIEW ? "https://sleazyfork.org/zh-CN/scripts/521128-voicelinks-preview" : "https://sleazyfork.org/zh-CN/scripts/456775-voicelinks", {active: true}));
  6093.  
  6094. document.addEventListener("securitypolicyviolation", function (e) {
  6095. if (e.blockedURI.includes("img.dlsite.jp")) {
  6096. const img = document.querySelector(`img[src="${e.blockedURI}"]`);
  6097. img.remove();
  6098.  
  6099. const imgContainer = Popup.popupElement.img.container;
  6100. if(imgContainer){
  6101. imgContainer.style.setProperty("display", "none", "important"); //display = "none !important";
  6102. Popup.hideImg = true;
  6103. }
  6104. }
  6105. });
  6106.  
  6107. isInit = true;
  6108. }
  6109.  
  6110. if(!document.body || observing){
  6111. return;
  6112. }
  6113.  
  6114. Parser.walkNodes(document.body);
  6115. if(!document.getElementById(`${VOICELINK_CLASS}-voice-popup`)) Popup.makePopup(false);
  6116.  
  6117. const observer = new MutationObserver(function (m) {
  6118. for (let i = 0; i < m.length; ++i) {
  6119. let addedNodes = m[i].addedNodes;
  6120. let removedNodes = m[i].removedNodes;
  6121.  
  6122. for (let j = 0; j < removedNodes.length; ++j){
  6123. let node = removedNodes[j];
  6124. }
  6125.  
  6126. for (let j = 0; j < addedNodes.length; ++j) {
  6127. Parser.walkNodes(addedNodes[j]);
  6128. }
  6129. }
  6130. });
  6131.  
  6132. observer.observe(document.body, { childList: true, subtree: true})
  6133. setUserSelectTitle();
  6134.  
  6135. //显示重要通知
  6136. showUpdateNotice();
  6137.  
  6138. observing = true;
  6139. }
  6140.  
  6141. document.addEventListener("DOMContentLoaded", init);
  6142.  
  6143. function showUpdateNotice(force = false) {
  6144. const firstTimeToken = 105;
  6145. if(GM_getValue("first_token", undefined) === firstTimeToken && !force){
  6146. return;
  6147. }
  6148. GM_setValue("first_token", firstTimeToken);
  6149.  
  6150. if(!force && window.confirm(localize(localizationMap.notice_update)) === false) {
  6151. return;
  6152. }
  6153.  
  6154. GM_openInTab(
  6155. IS_PREVIEW ? `https://github.com/IceFoxy062/VoiceLinks-Extend/blob/dev/docs/major_updates/v4.8.6/v4.8.6-${settings._s_lang}.md` : `https://github.com/IceFoxy062/VoiceLinks-Extend/blob/dev/docs/major_updates/v4.8.6/v4.8.6-${settings._s_lang}.md`,
  6156. {active: true});
  6157. }
  6158.  
  6159. //Deal with Trusted Types
  6160. let Csp = {
  6161. createHTML: (str) => str
  6162. };
  6163. if(window.isSecureContext === true && trustedTypes){
  6164. Csp = trustedTypes.createPolicy(
  6165. trustedTypes.defaultPolicy ? "VoiceLinkTrustedTypes" : "VoiceLinkTrustedTypes",
  6166. Csp);
  6167. }
  6168.  
  6169. init();
  6170. })();