VoiceLinks - Preview

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

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