Sleazy Fork is available in English.

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