轻小说文库+

轻小说文库全方位体验改善,涵盖阅读、下载、书架、推荐、书评、账号、页面个性化等各种方面,你能想到的这里都有。没有?欢迎提出你的点子。

  1. /* eslint-disable no-multi-spaces */
  2. /* eslint-disable no-useless-call */
  3. /* eslint-disable userscripts/no-invalid-headers */
  4.  
  5. // ==UserScript==
  6. // @name 轻小说文库+
  7. // @namespace Wenku8+
  8. // @version 1.7.5
  9. // @description 轻小说文库全方位体验改善,涵盖阅读、下载、书架、推荐、书评、账号、页面个性化等各种方面,你能想到的这里都有。没有?欢迎提出你的点子。
  10. // @updateinfo <h3>v1.7.5</h3><ul><li>支持wenku8.cc</li><li>修复书评文字缩放行间距错误地随文字缩放放大缩小问题</li></ul>
  11. // @author PY-DNG
  12. // @license GPL-license
  13. // @icon data:image/vnd.microsoft.icon;base64,AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAD/igD//4oA//+KAP//igD//4oA//+KAP//igD//4oA//+KAP//igD//4oA//+KAP//igD//4oA//+KAP//igD//4oA//+KAP//igD//4oA//+KAP//igD//4oA//+KAP//igD//4oA//+KAP//igD//4oA//+KAP//igD//4oA//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw///igD//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//+KAP/858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//4oA//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw///igD//OfD//znw//858P//OfD//znw//858P//rlB//3HaP/858P//OfD//znw//858P//OfD//znw//858P//OfD//zitv/91Y///dWP//3Vj//9y3X//cdo//3HaP/9x2j//cdo//zitv/858P//OfD//znw//858P//OfD//+KAP/858P//OfD//znw//858P//OfD//znw///ogD//6IA//znw//858P//OfD//znw//9x2j//6IA//+iAP//ogD//6IA//+iAP//ogD//6IA//+iAP//ogD//6IA//+iAP//ogD//6cN//znw//858P//OfD//znw//858P//4oA//znw//858P//OfD//3ZnP//ogD//6IA//+iAP//ogD//sJb//3HaP/83qn//OfD//zeqf//pw3//6IA//+iAP//ogD//6IA//+iAP//ogD//6IA//+iAP//ogD//6IA//+rGv/90IL//OfD//znw//858P//OfD//znw///igD//OfD//znw//858P//cdo//+iAP//ogD//6IA//+iAP//ogD//6IA//+iAP//ogD//OfD//znw//858P//OfD//znw//858P//6IA//+iAP/84rb//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//+KAP/858P//OfD//znw//858P//rQ0//60NP//ogD//6IA//+iAP//ogD//6IA//+iAP/858P//OfD//znw//858P//OfD//znw///ogD//6IA//3Vj//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//4oA//znw//858P//OfD//znw//858P//OfD//6wJ///ogD//6sa//3HaP/92Zz//OfD//znw//858P//OfD//znw//858P//OfD//+iAP//ogD//rQ0//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw///igD//OfD//znw//858P//OfD//zitv/+vk7//6IA//+iAP//ogD//6IA//+iAP/+tDT//OfD//znw//858P//OfD//znw//858P//rAn//+iAP//ogD//ct1//3HaP/+tDT//rQ0//6+Tv/858P//OfD//znw//858P//OfD//+KAP/858P//OfD//znw//858P//6IA//+iAP//ogD//6IA//+iAP//ogD//6IA//+iAP//ogD//ct1//7CW///ogD//6IA//+iAP//ogD//6IA//+iAP//ogD//6IA//+iAP//ogD//6IA//znw//858P//OfD//znw//858P//4oA//znw//858P//OfD//znw///ogD//6IA//65Qf//ogD//6IA//3HaP/9x2j//6cN//+iAP//ogD//r5O//+iAP//ogD//6IA//+iAP//ogD//6IA//+iAP//ogD//6IA//60NP/9x2j//OfD//znw//858P//OfD//znw///igD//OfD//znw//858P//OfD//+iAP//ogD//OfD//65Qf//ogD//6cN//znw//858P//6IA//+iAP/92Zz//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//+KAP/858P//OfD//znw//858P//6IA//+iAP/858P//cdo//+iAP//ogD//dWP//3Vj///pw3//6IA//7CW//858P//rQ0//+iAP/92Zz//OfD//3Ldf/+tDT//OK2//znw//858P//N6p//znw//858P//OfD//znw//858P//4oA//znw//858P//OfD//znw///ogD//6IA//+iAP//ogD//6IA//+iAP//ogD//6IA//7CW///ogD//6IA//6+Tv//ogD//6IA//6+Tv/91Y///6IA//+iAP/+vk7//OfD//+iAP//ogD//6IA//znw//858P//OfD//znw///igD//OfD//znw//858P//OfD//+iAP//ogD//6IA//+iAP//ogD//6IA//+iAP//ogD//dWP//+iAP//ogD//6IA//+iAP//ogD//OK2//+rGv//ogD//6IA//3ZnP/858P//6IA//+iAP//qxr//OfD//znw//858P//OfD//+KAP/858P//OfD//znw//858P//sJb//+iAP//pw3//rQ0//+iAP//ogD//OfD//znw//+wlv//6IA//+iAP//ogD//6cN//3ZnP/+vk7//6IA//+iAP/90IL//OfD//60NP//ogD//6IA//znw//858P//OfD//znw//858P//4oA//znw//858P//OfD//znw//84rb//rAn//+iAP//ogD//6IA//+iAP/+sCf//rQ0//+iAP//ogD//6IA//+iAP/9x2j//OfD//65Qf//ogD//rAn//znw//858P//rQ0//+iAP/+uUH//OfD//znw//858P//OfD//znw///igD//OfD//znw//858P//OfD//znw//858P//r5O//60NP//ogD//6IA//+iAP//ogD//6IA//+iAP/9x2j//6IA//+iAP/9x2j//OfD//6+Tv//ogD//6IA//znw//84rb//6IA//+iAP/84rb//OfD//znw//858P//OfD//+KAP/858P//OfD//znw//858P//OfD//znw//858P//OfD//+nDf//ogD//6cN//60NP/+vk7//ct1//znw//+wlv//6IA//+iAP/858P//OK2//+iAP//ogD//dmc//znw///pw3//6IA//6wJ//83qn//OfD//znw//858P//4oA//znw//858P//OfD//znw//858P//OfD//znw//858P//cdo//+iAP//ogD//dmc//znw//858P//OfD//znw///pw3//6IA//+nDf/83qn//dmc//+nDf//ogD//6sa//znw//92Zz//6IA//+iAP/858P//OfD//znw///igD//OfD//znw//858P//OfD//znw//858P//N6p//3HaP/+sCf//6IA//+iAP//ogD//6IA//+iAP//qxr//dmc//3ZnP//pw3//6IA//6+Tv/858P//dmc//+iAP//ogD//OfD//znw//9x2j//sJb//znw//858P//OfD//+KAP/858P//OfD//znw//858P//OfD//znw///ogD//6IA//+iAP//ogD//6IA//+iAP//ogD//6IA//+iAP/+vk7//OfD//znw//91Y///OK2//znw//858P//dWP//3Vj//9x2j//rlB//+nDf//pw3//OfD//znw//858P//4oA//znw//858P//OfD//znw//858P//OfD//3Ldf//ogD//6IA//+iAP//ogD//6IA//60NP/+tDT//cdo//zitv/+tDT//rQ0//+iAP//ogD//6IA//+iAP//ogD//6IA//+iAP//ogD//6IA//+iAP/83qn//OfD//znw///igD//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//dmc//+iAP/+sCf//OfD//znw//858P//dWP//+iAP//ogD//6IA//+iAP//ogD//6IA//+iAP//ogD//6IA//+iAP//ogD//6IA//znw//858P//OfD//+KAP/858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//cdo//zeqf/858P//OfD//znw//858P//rQ0//60NP/+tDT//rQ0//60NP/9x2j//cdo//3HaP/91Y///dWP//zeqf/858P//OfD//znw//858P//4oA//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw///igD//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//+KAP/858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//4oA//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw//858P//OfD//znw///igD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
  14. // @match http*://www.wenku8.net/*
  15. // @match http*://www.wenku8.cc/*
  16. // @connect wenku8.com
  17. // @connect wenku8.net
  18. // @connect greasyfork.org
  19. // @connect image.kieng.cn
  20. // @connect sm.ms
  21. // @connect catbox.moe
  22. // @connect liumingye.cn
  23. // @connect p.sda1.dev
  24. // @connect api.pandaimg.com
  25. // @connect imagelol.com
  26. // @connect pic.jitudisk.com
  27. // @connect cdn.jsdelivr.net
  28. // @connect cdnjs.cloudflare.com
  29. // @connect bowercdn.net
  30. // @connect unpkg.com
  31. // @connect cdn.bootcdn.net
  32. // @connect kit.fontawesome.com
  33. // @grant GM_xmlhttpRequest
  34. // @grant GM_getValue
  35. // @grant GM_setValue
  36. // @grant GM_deleteValue
  37. // @grant GM_listValues
  38. // @grant GM_openInTab
  39. // @grant GM_getResourceText
  40. // @grant GM_info
  41. // @grant unsafeWindow
  42. // @require https://greasyfork.org/scripts/427726-gbk-url-js/code/GBK_URLjs.js?version=953098
  43. // @require https://greasyfork.org/scripts/431490-greasyforkscriptupdate/code/GreasyForkScriptUpdate.js?version=965063
  44. // @require https://cdnjs.cloudflare.com/ajax/libs/AlertifyJS/1.13.1/alertify.min.js
  45. // @require https://unpkg.com/@popperjs/core@2
  46. // @require https://unpkg.com/tippy.js@6
  47. // @resource alertify-css https://cdnjs.cloudflare.com/ajax/libs/AlertifyJS/1.13.1/css/alertify.min.css
  48. // @resource alertify-theme https://cdnjs.cloudflare.com/ajax/libs/AlertifyJS/1.13.1/css/themes/default.min.css
  49. // @noframes
  50. // ==/UserScript==
  51.  
  52. /* 需求记录 [容易(优先级高) ➡️ 困难(优先级低)(我懒,一般而言优先做低难度的)]
  53. ** [已完成]{BK}书评页提供用户书评搜索
  54. ** {BK}图片大小(最大)限制
  55. ** [已完成]{BK}回复区插入@好友
  56. ** [已完成]全卷/分卷下载:文件重命名为书名,而不是书号
  57. ** · [已完成]添加单文件下载重命名
  58. ** {BK}回复区悬浮显示
  59. ** {热忱}[已完成]修复https引用问题
  60. ** [已完成]书评打开最后一页
  61. ** [待完善]书评实时更新
  62. ** · [待完善]新回复直接添加到当前页面
  63. ** · 主动回复内容直接添加到当前页面
  64. ** [待完善]引用回复
  65. ** · [已完成]引用楼层号和回复内容
  66. ** · [已完成]仅引用楼层号
  67. ** [已完成]支持preview版tag搜索
  68. ** [高优先级]备注功能
  69. · [待完善]用户备注
  70. · 小说备注
  71. · [低优先级]阅读随笔(这真的可能实现吗??)
  72. ** [待完善]书评帖子收藏
  73. ** · [已完成]书评页面收藏
  74. ** · [高优先级]收藏的书评页面可以添加编辑备注
  75. ** [已完成]每日自动推书
  76. ** [待完善]{热忱}快速切换账号
  77. ** · [已完成]为每个账号储存单独的配置
  78. ** · [待完善]保存账号信息并快速自动切换
  79. ** [待完善]快速插入图片/表情
  80. ** · [已完成]直接插入本地图片
  81. ** · [持续进行]更多图床
  82. ** · [低优先级]保存常用图片/表情链接
  83. ** [部分完成]{BK}页面美化
  84. ** · [已完成]阅读页去除广告
  85. ** · [已完成]阅读页美化
  86. ** · [已完成]书评页美化
  87. ** · …
  88. ** [高优先级][施工中]脚本储存管理界面
  89. ** [高优先级][待完善]稍后再读(可以的话,请给我提出改进建议)
  90. ** {BK}类似ehunter的阅读模式
  91. ** 改进旧代码:
  92. ** · 每个page-addon内部要按照功能分模块,执行功能靠调用模块,不能直接写功能代码
  93. ** · 共性模块要写进脚本全局作用域,可以的话写成构造函数
  94. ** [低优先级]{RC}书评:@某人时通知他
  95. ** [待完善]{BK}书评:草稿箱功能
  96. ** {热忱}{s1h2}提供带文字和插图的epub整合下载
  97. */
  98. /* API记录
  99. ** 阅读API:http://dl.wenku8.com/pack.php?aid=2478&vid=92914
  100. ** 回帖API:https://www.wenku8.net/modules/article/reviewshow.php?rid=209631&aid=2751
  101. ** 查人API:https://www.wenku8.net/modules/article/reviewslist.php?keyword=136877
  102. ** 读书API:https://www.wenku8.net/modules/article/reader.php?aid=2946
  103. ** 好友API:https://www.wenku8.net/myfriends.php // 好友名称选择器:content.querySelectorAll('tr>td.odd:nth-child(1)')
  104. ** 登录API:https://www.wenku8.net/login.php?do=submit&jumpurl=http%3A%2F%2Fwww.wenku8.net%2Findex.php
  105. ** 最新回复:https://www.wenku8.net/modules/article/reviewslist.php?t=1
  106. ** 检查更新:https://greasyfork.org/zh-CN/scripts/416310/code/script.meta.js
  107. */
  108. /* 账号收藏
  109. ** wenku8高仿号(按照相似度排列):
  110. ** ** https://www.wenku8.net/userpage.php?uid=912148
  111. ** ** https://www.wenku8.net/userpage.php?uid=728810
  112. ** ** https://www.wenku8.net/userpage.php?uid=917768
  113. ** BK高仿号
  114. ** ** https://www.wenku8.net/userpage.php?uid=918609
  115. ** 热忱高仿号
  116. ** ** https://www.wenku8.net/userpage.php?uid=918764
  117. ** 隐身鱼高仿号
  118. ** ** https://www.wenku8.net/userpage.php?uid=918773
  119. */
  120. (function FUNC_MAIN() {
  121. 'use strict';
  122.  
  123. // Polyfills
  124. const script_name = '轻小说文库+';
  125. const script_version = '1.7.4.3';
  126. const NMonkey_Info = {
  127. GM_info: {
  128. script: {
  129. name: script_name,
  130. author: 'PY-DNG',
  131. version: script_version,
  132. }
  133. },
  134. mainFunc: FUNC_MAIN,
  135. name: 'wenku8_plus',
  136. requires: [
  137. // GBK-URL
  138. {
  139. name: 'GBK-URL',
  140. src: 'https://greasyfork.org/scripts/427726-gbk-url-js/code/GBK_URLjs.js?version=953098',
  141. srcset: [
  142. 'https://cdn.jsdelivr.net/gh/PYUDNG/CDN@eed1fcf0e901348bc4e752fd483bcb571ebe0408/js/GBK_URL/GBK.js',
  143. ],
  144. loaded: () => (typeof $URL === 'object'),
  145. execmode: 'function'
  146. },
  147.  
  148. // GreasyForkScriptUpdate
  149. {
  150. name: 'GreasyForkScriptUpdate',
  151. src: 'https://greasyfork.org/scripts/431490-greasyforkscriptupdate/code/GreasyForkScriptUpdate.js?version=965063',
  152. srcset: [
  153. 'https://cdn.jsdelivr.net/gh/PYUDNG/CDN@94fc2bdd313f7bf2af6db5b8699effee8dd0b18d/js/ajax/GreasyForkScriptUpdate.js',
  154. ],
  155. loaded: () => (typeof GreasyForkUpdater === 'function'),
  156. execmode: 'eval'
  157. },
  158.  
  159. // Alertify
  160. {
  161. name: 'Alertify',
  162. src: 'https://cdnjs.cloudflare.com/ajax/libs/AlertifyJS/1.13.1/alertify.min.js',
  163. srcset: [
  164. 'https://cdn.jsdelivr.net/gh/MohammadYounes/AlertifyJS@3151fa0d65909936afcbb2f1665ed4f20767bee5/build/alertify.min.js',
  165. 'https://bowercdn.net/c/alertify-js-1.13.1/build/alertify.min.js',
  166. 'https://cdn.bootcdn.net/ajax/libs/AlertifyJS/1.9.0/alertify.min.js',
  167. ],
  168. loaded: () => (typeof alertify === 'object'),
  169. execmode: 'function'
  170. },
  171.  
  172. // FontAwesome
  173. /*
  174. {
  175. src: 'https://kit.fontawesome.com/1288cd6170.js',
  176. loaded: () => (typeof(FontAwesomeKitConfig) === 'object')
  177. }
  178. */
  179.  
  180. // Tippy.js
  181. {
  182. name: 'Tippy.js-Core',
  183. src: 'https://unpkg.com/@popperjs/core@2',
  184. loaded: () => (typeof tippy === 'function'),
  185. execmode: 'function'
  186. },
  187. {
  188. name: 'Tippy.js',
  189. src: 'https://unpkg.com/tippy.js@6',
  190. loaded: () => (typeof tippy === 'function'),
  191. execmode: 'function'
  192. },
  193. ],
  194. resources: [
  195. // Alertify css
  196. {
  197. src: 'https://cdnjs.cloudflare.com/ajax/libs/AlertifyJS/1.13.1/css/alertify.min.css',
  198. srcset: [
  199. 'https://cdn.jsdelivr.net/gh/MohammadYounes/AlertifyJS@3151fa0d65909936afcbb2f1665ed4f20767bee5/build/css/alertify.min.css',
  200. 'https://bowercdn.net/c/alertify-js-1.13.1/build/css/alertify.min.css',
  201. 'https://cdn.bootcdn.net/ajax/libs/AlertifyJS/1.9.0/css/alertify.min.css',
  202. ],
  203. name: 'alertify-css',
  204. isCss: true
  205. },
  206.  
  207. // Alertify theme
  208. {
  209. src: 'https://cdnjs.cloudflare.com/ajax/libs/AlertifyJS/1.13.1/css/themes/default.min.css',
  210. srcset: [
  211. 'https://cdn.jsdelivr.net/gh/MohammadYounes/AlertifyJS@3151fa0d65909936afcbb2f1665ed4f20767bee5/build/css/themes/default.min.css',
  212. 'https://bowercdn.net/c/alertify-js-1.13.1/build/css/themes/default.min.css',
  213. 'https://cdn.bootcdn.net/ajax/libs/AlertifyJS/1.9.0/css/themes/default.min.css',
  214. ],
  215. name: 'alertify-theme',
  216. isCss: true
  217. },
  218.  
  219. // tooltip
  220. /*
  221. {
  222. src: 'https://cdn.jsdelivr.net/gh/PYUDNG/css-components@main/build/tooltip/tooltip.css',
  223. srcset: [
  224. '',
  225. ],
  226. name: 'css-tooltip',
  227. isCss: true
  228. },
  229. */
  230.  
  231. // FontAwesome
  232. /*
  233. {
  234. src: 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.1.1/css/all.min.css',
  235. srcset: [
  236. 'https://bowercdn.net/c/fontAwesome-6.1.1/css/all.min.css',
  237. 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css',
  238. ],
  239. name: 'css-fontawesome',
  240. isCss: true
  241. }
  242. */
  243. ]
  244. };
  245. const NMonkey_Ready = NMonkey(NMonkey_Info);
  246. if (!NMonkey_Ready) {return false;}
  247. polyfill_replaceAll();
  248.  
  249. // CONSTS
  250. const NUMBER_MAX_XHR = typeof mbrowser === 'object' ? 1 : 10;
  251. const NUMBER_LOGSUCCESS_AFTER = NUMBER_MAX_XHR * 2;
  252. const NUMBER_ELEMENT_LOADING_WAIT_INTERVAL = 500;
  253.  
  254. const KEY_CM = 'Config-Manager';
  255. const KEY_CM_VERSION = 'version';
  256. const VALUE_CM_VERSION = '0.3';
  257.  
  258. const KEY_DRAFT_DRAFTS = 'comment-drafts';
  259. const KEY_DRAFT_VERSION = 'version';
  260. const VALUE_DRAFT_VERSION = '0.2';
  261.  
  262. const KEY_REVIEW_PREFS = 'comment-preferences';
  263. const KEY_REVIEW_VERSION = 'version';
  264. const VALUE_REVIEW_VERSION = '0.9';
  265.  
  266. const KEY_BOOKCASES = 'book-cases';
  267. const KEY_BOOKCASE_VERSION = 'version';
  268. const VALUE_BOOKCASE_VERSION = '0.5';
  269.  
  270. const KEY_ATRCMMDS = 'auto-recommends';
  271. const KEY_ATRCMMDS_VERSION = 'version';
  272. const VALUE_ATRCMMDS_VERSION = '0.2';
  273.  
  274. const KEY_USRDETAIL = 'user-detail';
  275. const KEY_USRDETAIL_VERSION = 'version';
  276. const VALUE_USRDETAIL_VERSION = '0.2';
  277.  
  278. const KEY_BEAUTIFIER = 'beautifier';
  279. const KEY_BEAUTIFIER_VERSION = 'version';
  280. const VALUE_BEAUTIFIER_VERSION = '0.9';
  281.  
  282. const KEY_REMARKS = 'remarks';
  283. const KEY_REMARKS_VERSION = 'version';
  284. const VALUE_REMARKS_VERSION = '0.1';
  285.  
  286. const KEY_USERGLOBAL = 'user-global-config';
  287. const KEY_USERGLOBAL_VERSION = 'version';
  288. const VALUE_USERGLOBAL_VERSION = '0.1';
  289.  
  290. const VALUE_STR_NULL = 'null';
  291.  
  292. const URL_NOVELINDEX = `https://${location.host}/book/{I}.htm`;
  293. const URL_REVIEWSEARCH = `https://${location.host}/modules/article/reviewslist.php?keyword={K}`;
  294. const URL_REVIEWSHOW = `https://${location.host}/modules/article/reviewshow.php?rid={R}&aid={A}&page={P}`;
  295. const URL_REVIEWSHOW_1 = `https://${location.host}/modules/article/reviewshow.php?rid={R}`;
  296. const URL_REVIEWSHOW_2 = `https://${location.host}/modules/article/reviewshow.php?rid={R}&page={P}`;
  297. const URL_REVIEWSHOW_3 = `https://${location.host}/modules/article/reviewshow.php?rid={R}&aid={A}`;
  298. const URL_REVIEWSHOW_4 = `https://${location.host}/modules/article/reviewshow.php?rid={R}&page={P}#{Y}`;
  299. const URL_REVIEWSHOW_5 = `https://${location.host}/modules/article/reviewshow.php?rid={R}&aid={A}&page={P}#{Y}`;
  300. const URL_USERINFO = `https://${location.host}/userinfo.php?id={K}`;
  301. const URL_DOWNLOAD1 = `http://${location.host.replace('www.', 'dl.')}/packtxt.php?aid={A}&vid={V}&charset={C}`;
  302. const URL_DOWNLOAD2 = `http://${location.host.replace('www.', 'dl2.')}/packtxt.php?aid={A}&vid={V}&charset={C}`;
  303. const URL_DOWNLOAD3 = `http://${location.host.replace('www.', 'dl3.')}/packtxt.php?aid={A}&vid={V}&charset={C}`;
  304. const URL_PACKSHOW = `https://${location.host}/modules/article/packshow.php?id={A}&type={T}`;
  305. const URL_BOOKINTRO = `https://${location.host}/book/{A}.htm`;
  306. const URL_ADDBOOKCASE = `https://${location.host}/modules/article/addbookcase.php?bid={A}`;
  307. const URL_RECOMMEND = `https://${location.host}/modules/article/uservote.php?id={B}`;
  308. const URL_TAGSEARCH = `https://${location.host}/modules/article/tags.php?t={TU}`;
  309. const URL_USRDETAIL = `https://${location.host}/userdetail.php`;
  310. const URL_USRFRIEND = `https://${location.host}/myfriends.php`;
  311. const URL_BOOKCASE = `https://${location.host}/modules/article/bookcase.php`;
  312. const URL_USRLOGIN = `https://${location.host}/login.php?do=submit&jumpurl=http%3A%2F%2F${location.host}%2Findex.php`;
  313. const URL_USRLOGOFF = `https://${location.host}/logout.php`;
  314.  
  315. const DATA_XHR_LOGIN = [
  316. "username={U}",
  317. "password={P}",
  318. "usecookie={C}",
  319. "action=login",
  320. "submit=%26%23160%3B%B5%C7%26%23160%3B%26%23160%3B%C2%BC%26%23160%3B" // '&#160;登&#160;&#160;录&#160'
  321. ].join('&');
  322. const DATA_IMAGERS = {
  323. default: 'SDAIDEV',
  324. /* Imager Model
  325. _IMAGER_KEY_: {
  326. available: true,
  327. name: '_IMAGER_DISPLAY_NAME_',
  328. tip: '_IMAGER_DISPLAY_TIP_',
  329. upload: {
  330. request: {
  331. url: '_UPLOAD_URL_',
  332. data: {
  333. '_FORM_NAME_FOR_FILE_': '$file$'
  334. }
  335. },
  336. response: {
  337. checksuccess: (json)=>{return json._SUCCESS_KEY_ === '_SUCCESS_VALUE_';},
  338. geturl: (json)=>{return json._PATH_._SUCCESS_URL_KEY_;},
  339. getname: (json)=>{return json._PATH_ ? json._PATH_._FILENAME_ : null;},
  340. getsize: (json)=>{return json._PATH_._SIZE_},
  341. getpage: (json)=>{return json._PATH_ ? json._PATH_._PAGE_ : null;},
  342. gethash: (json)=>{return json._PATH_ ? json._PATH_._HASH_ : null;},
  343. getdelete: (json)=>{return json._PATH_ ? json._PATH_._DELETE_ : null;}
  344. }
  345. },
  346. isImager: true
  347. },
  348. */
  349. LIUMINGYE: {
  350. available: true,
  351. name: '刘明野-全能图床',
  352. tip: '2021-12-04测试可用</br>理论无上传大小限制,实际测试图片过大会上传失败',
  353. upload: {
  354. request: {
  355. url: 'https://tool.liumingye.cn/tuchuang/update.php',
  356. data: {
  357. 'file': '$file$'
  358. }
  359. },
  360. response: {
  361. checksuccess: (json)=>{return json.code === 0;},
  362. geturl: (json)=>{return json.msg;}
  363. }
  364. },
  365. isImager: true
  366. },
  367. PANDAIMG: {
  368. available: true,
  369. name: '熊猫图床',
  370. tip: '2022-01-16测试可用</br>单张图片最大5MB',
  371. upload: {
  372. request: {
  373. url: 'https://api.pandaimg.com/upload',
  374. data: {
  375. 'file': '$file$',
  376. 'classifications': '',
  377. 'day': '0'
  378. },
  379. headers: {
  380. 'usersOrigin': '5edd88d4dfe5d288518c0454d3ccdd2a'
  381. }
  382. },
  383. response: {
  384. checksuccess: (json)=>{return json.code === '200';},
  385. geturl: (json)=>{return json.data.url;},
  386. getname: (json)=>{return json.data.name;}
  387. }
  388. },
  389. isImager: true
  390. },
  391. SDAIDEV: {
  392. available: true,
  393. name: '流浪图床',
  394. tip: '2022-01-09测试可用</br>单张图片最大5MB',
  395. upload: {
  396. request: {
  397. url: 'https://p.sda1.dev/api/v1/upload_external_noform',
  398. urlargs: {
  399. 'filename': '$filename$',
  400. 'ts': '$time$',
  401. 'rand': '$random$'
  402. }
  403. },
  404. response: {
  405. checksuccess: (json)=>{return json.success;},
  406. geturl: (json)=>{return json.data.url;},
  407. getdelete: (json)=>{return json.data ? json.data.delete_url : null;},
  408. getsize: (json)=>{return json.data ? json.data.size : null;}
  409. }
  410. },
  411. isImager: true
  412. },
  413. JITUDISK: {
  414. available: true,
  415. name: '极兔兔床',
  416. tip: '2022-02-02测试可用',
  417. upload: {
  418. request: {
  419. url: 'https://pic.jitudisk.com/api/upload',
  420. data: {
  421. 'image': '$file$'
  422. }
  423. },
  424. response: {
  425. checksuccess: (json)=>{return json.code === 200;},
  426. geturl: (json)=>{return json.data.url;},
  427. getname: (json)=>{return json.data.name;}
  428. }
  429. },
  430. isImager: true
  431. },
  432. IMAGELOL: {
  433. available: false,
  434. name: '笑果图床',
  435. tip: '2022-01-17测试可用</br>该图床不支持重复上传同一张图片,请注意</br>单张图片最大2MB',
  436. upload: {
  437. request: {
  438. url: 'https://imagelol.com/json',
  439. data: {
  440. 'source': '$file$',
  441. 'type': 'file',
  442. 'action': 'upload',
  443. 'timestamp': '$time$',
  444. 'auth_token': '4f6fb8d04525bae5a455f4f09e2b09aa750e60c3',
  445. 'nsfw': '0'
  446. }
  447. },
  448. response: {
  449. checksuccess: (json)=>{return json.status_code === 200 && json.success && json.success.code === 200;},
  450. geturl: (json)=>{return json.image.url;},
  451. getname: (json)=>{return json.image.original_filename;},
  452. getsize: (json)=>{return json.image.size},
  453. gethash: (json)=>{return json.image.md5;},
  454. }
  455. },
  456. isImager: true
  457. },
  458. /*GEJIBA: {
  459. available: true,
  460. name: '老王图床',
  461. tip: '2022-01-17测试可用</br>单张图片最大10MB</br>PS:此图床审核比较严格',
  462. upload: {
  463. request: {
  464. url: '_UPLOAD_URL_',
  465. data: {
  466. '_FORM_NAME_FOR_FILE_': '$file$'
  467. }
  468. },
  469. response: {
  470. checksuccess: (json)=>{return json._SUCCESS_KEY_ === '_SUCCESS_VALUE_';},
  471. geturl: (json)=>{return json._PATH_._SUCCESS_URL_KEY_;},
  472. getname: (json)=>{return json._PATH_ ? json._PATH_._FILENAME_ : null;},
  473. getsize: (json)=>{return json._PATH_._SIZE_},
  474. getpage: (json)=>{return json._PATH_ ? json._PATH_._PAGE_ : null;},
  475. gethash: (json)=>{return json._PATH_ ? json._PATH_._HASH_ : null;},
  476. getdelete: (json)=>{return json._PATH_ ? json._PATH_._DELETE_ : null;}
  477. }
  478. }
  479. },*/
  480. KIENG_JD: {
  481. available: false,
  482. name: 'KIENG-JD',
  483. tip: '默认图床</br>个人体验良好,推荐使用',
  484. upload: {
  485. request: {
  486. url: 'https://image.kieng.cn/upload.html?type=jd',
  487. data: {
  488. 'image': '$file$'
  489. }
  490. },
  491. response: {
  492. checksuccess: (json)=>{return json.code === 200;},
  493. geturl: (json)=>{return json.data.url;},
  494. getname: (json)=>{return json.data.name;}
  495. }
  496. },
  497. isImager: true
  498. },
  499. KIENG_SG: {
  500. available: false,
  501. name: 'KIENG-SG',
  502. upload: {
  503. request: {
  504. url: 'https://image.kieng.cn/upload.html?type=sg',
  505. data: {
  506. 'image': '$file$'
  507. }
  508. },
  509. response: {
  510. checksuccess: (json)=>{return json.code === 200;},
  511. geturl: (json)=>{return json.data.url;},
  512. getname: (json)=>{return json.data.name;}
  513. }
  514. },
  515. isImager: true
  516. },
  517. KIENG_58: {
  518. available: false,
  519. name: 'KIENG-58',
  520. upload: {
  521. request: {
  522. url: 'https://image.kieng.cn/upload.html?type=c58',
  523. data: {
  524. 'image': '$file$'
  525. }
  526. },
  527. response: {
  528. checksuccess: (json)=>{return json.code === 200;},
  529. geturl: (json)=>{return json.data.url;},
  530. getname: (json)=>{return json.data.name;}
  531. }
  532. },
  533. isImager: true
  534. },
  535. KIENG_WY: {
  536. available: false,
  537. name: 'KIENG-WY',
  538. upload: {
  539. request: {
  540. url: 'https://image.kieng.cn/upload.html?type=wy',
  541. data: {
  542. 'image': '$file$'
  543. }
  544. },
  545. response: {
  546. checksuccess: (json)=>{return json.code === 200;},
  547. geturl: (json)=>{return json.data.url;},
  548. getname: (json)=>{return json.data.name;}
  549. }
  550. },
  551. isImager: true
  552. },
  553. KIENG_QQ: {
  554. available: false,
  555. name: 'KIENG-QQ',
  556. upload: {
  557. request: {
  558. url: 'https://image.kieng.cn/upload.html?type=qq',
  559. data: {
  560. 'image': '$file$'
  561. }
  562. },
  563. response: {
  564. checksuccess: (json)=>{return json.code === 200;},
  565. geturl: (json)=>{return json.data.url;},
  566. getname: (json)=>{return json.data.name;}
  567. }
  568. },
  569. isImager: true
  570. },
  571. KIENG_SN: {
  572. available: false,
  573. name: 'KIENG-SN',
  574. upload: {
  575. request: {
  576. url: 'https://image.kieng.cn/upload.html?type=sn',
  577. data: {
  578. 'image': '$file$'
  579. }
  580. },
  581. response: {
  582. checksuccess: (json)=>{return json.code === 200;},
  583. geturl: (json)=>{return json.data.url;},
  584. getname: (json)=>{return json.data.name;}
  585. }
  586. },
  587. isImager: true
  588. },
  589. KIENG_HL: {
  590. available: false,
  591. name: 'KIENG-HLX',
  592. upload: {
  593. request: {
  594. url: 'https://image.kieng.cn/upload.html?type=hl',
  595. data: {
  596. 'image': '$file$'
  597. }
  598. },
  599. response: {
  600. checksuccess: (json)=>{return json.code === 200;},
  601. geturl: (json)=>{return json.data.url;},
  602. getname: (json)=>{return json.data.name;}
  603. }
  604. },
  605. isImager: true
  606. },
  607. SMMS: {
  608. available: true,
  609. name: 'SM.MS',
  610. tip: '注意:此图床跨域访问较不稳定,且有用户反映其被国内部分服务商屏蔽,请谨慎使用此图床',
  611. warning: '注意:此图床跨域访问较不稳定,且有用户反映其被国内部分服务商屏蔽,请谨慎使用此图床</br>如出现上传错误/图片加载慢/无法加载图片等情况,请更换其他图床',
  612. upload: {
  613. request: {
  614. url: 'https://sm.ms/api/v2/upload?inajax=1',
  615. data: {
  616. 'smfile': '$file$'
  617. }
  618. },
  619. response: {
  620. checksuccess: (json)=>{return json.success === true || /^https?:\/\//.test(json.images);},
  621. geturl: (json)=>{return json.data ? json.data.url : json.images;},
  622. getname: (json)=>{return json.data ? json.data.filename : null;},
  623. getpage: (json)=>{return json.data ? json.data.page : null;},
  624. gethash: (json)=>{return json.data ? json.data.hash : null;},
  625. getdelete: (json)=>{return json.data ? json.data.delete : null;}
  626. }
  627. },
  628. isImager: true
  629. },
  630. CATBOX: {
  631. available: true,
  632. name: 'CatBox',
  633. tip: '注意:此图床访问较不稳定,请谨慎使用此图床',
  634. warning: '注意:此图床访问较不稳定,请谨慎使用此图床</br>如出现上传错误/图片加载慢/无法加载图片等情况,请更换其他图床',
  635. upload: {
  636. request: {
  637. url: 'https://catbox.moe/user/api.php',
  638. responseType: 'text',
  639. data: {
  640. 'fileToUpload': '$file$',
  641. 'reqtype': 'fileupload'
  642. }
  643. },
  644. response: {
  645. checksuccess: (text)=>{return true;},
  646. geturl: (text)=>{return text;}
  647. }
  648. },
  649. isImager: true
  650. }
  651. };
  652.  
  653. const FUNC_LATERBOOK_SORTERS = {
  654. 'addTime_old2new': {
  655. name: '由旧到新',
  656. sorter: (a, b) => (a.addTime - b.addTime),
  657. },
  658. 'addTime_new2old': {
  659. name: '由新到旧',
  660. sorter: (a, b) => (b.addTime - a.addTime),
  661. },
  662. 'sort': {
  663. name: '手动排序',
  664. sorter: (a, b) => (a.sort - b.sort),
  665. }
  666. }
  667.  
  668. const CLASSNAME_BUTTON = 'plus_btn';
  669. const CLASSNAME_TEXT = 'plus_text';
  670. const CLASSNAME_DISABLED = 'plus_disabled';
  671. const CLASSNAME_BOOKCASE_FORM = 'plus_bcform';
  672. const CLASSNAME_LIST = 'plus_list';
  673. const CLASSNAME_LIST_ITEM = 'plus_list_item';
  674. const CLASSNAME_LIST_BUTTON = 'plus_list_input';
  675. const CLASSNAME_MODIFIED = 'plus_modified';
  676.  
  677. const HTML_BOOK_COPY = '<span class="{C}">[复制]</span>'.replace('{C}', CLASSNAME_BUTTON);
  678. const HTML_BOOK_META = '{K}:{V}<span class="{C}">[复制]</span>'.replace('{C}', CLASSNAME_BUTTON);
  679. const HTML_BOOK_TAG = '<a class="{C}" href="{U}" target="_blank">{TN}</span>'.replace('{C}', CLASSNAME_BUTTON).replace('{U}', URL_TAGSEARCH);
  680. const HTML_DOWNLOAD_CONTENER = '<div id="dctn" style=\"margin:0px auto;overflow:hidden;\">\n<fieldset style=\"width:820px;height:35px;margin:0px auto;padding:0px;\">\n<legend><b>《{BOOKNAME}》小说TXT简繁全本下载</b></legend>\n</fieldset>\n</div>';
  681. const HTML_DOWNLOAD_LINKS_OLD = '<div id="txtfull" style="margin:0px auto;overflow:hidden;"><fieldset style="width:820px;height:35px;margin:0px auto;padding:0px;"><legend><b>《{ORIBOOKNAME}》小说TXT全本下载</b></legend><div style="width:16%; float:left; text-align:center;"><a href="http://dl.wenku8.com/down.php?type=txt&amp;id={BOOKID}">G版原始下载</a></div><div style="width:16%; float:left; text-align:center;"><a href="http://dl.wenku8.com/down.php?type=txt&amp;id={BOOKID}&amp;fname={BOOKNAME}.txt">G版自动重命名</a></div><div style="width:16%; float:left; text-align:center;"><a href="http://dl.wenku8.com/down.php?type=utf8&amp;id={BOOKID}">U版原始下载</a></div><div style="width:16%; float:left; text-align:center;"><a href="http://dl.wenku8.com/down.php?type=utf8&amp;id={BOOKID}&amp;fname={BOOKNAME}">U版自动重命名</a></div><div style="width:16%; float:left; text-align:center;"><a href="http://dl.wenku8.com/down.php?type=big5&amp;id={BOOKID}">繁体原始下载</a></div><div style="width:16%; float:left; text-align:center;"><a href="http://dl.wenku8.com/down.php?type=big5&amp;id={BOOKID}&amp;fname={BOOKNAME}">繁体自动重命名</a></div></fieldset></div>'.replaceAll('{C}', CLASSNAME_BUTTON);
  682. const HTML_DOWNLOAD_LINKS = `<div style="margin:0px auto;overflow:hidden;"><fieldset style="width:820px;height:35px;margin:0px auto;padding:0px;"><legend><b>《{ORIBOOKNAME}》小说TXTUMDJAR电子书下载</b></legend><div style="width:210px; float:left; text-align:center;"><a href="https://${location.host}/modules/article/packshow.php?id={BOOKID}&amp;type=txt{CHARSET}">TXT简繁分卷</a></div><div style="width:210px; float:left; text-align:center;"><a href="https://${location.host}/modules/article/packshow.php?id={BOOKID}&amp;type=txtfull{CHARSET}">TXT简繁全本</a></div><div style="width:210px; float:left; text-align:center;"><a href="https://${location.host}/modules/article/packshow.php?id={BOOKID}&amp;type=umd{CHARSET}">UMD分卷下载</a></div><div style="width:190px; float:left; text-align:center;"><a href="https://${location.host}/modules/article/packshow.php?id={BOOKID}&amp;type=jar{CHARSET}">JAR分卷下载</a></div></fieldset></div>`;
  683. const HTML_DOWNLOAD_BOARD = '<span class="{C}">阅读与下载限制已解除</br>此功能仅供学习交流,请支持正版<span style="text-align: right;">——{N}</span></span>'.replace('{N}', GM_info.script.name).replace('{C}', CLASSNAME_TEXT);
  684. const CSS_DOWNLOAD = '.even {display: grid; grid-template-columns: repeat(3, 1fr); text-align: center;} .dlink {text-align: center;}';
  685. const CSS_PAGE_API = 'body>div {display: flex; align-items: center; justify-content: center;}';
  686. const CSS_COLOR_BTN_NORMAL = 'rgb(0, 160, 0)', CSS_COLOR_BTN_HOVER = 'rgb(0, 100, 0)', CSS_COLOR_FLOOR_MODIFIED = '#CCCCFF';
  687. const CSS_COMMON = '.{CT} {color: rgb(30, 100, 220) !important;} .{CB} {color: rgb(0, 160, 0) !important; cursor: pointer !important; user-select: none;} .{CB}:hover {color: rgb(0, 100, 0) !important;} .{CB}:focus {color: rgb(0, 100, 0) !important;} .{CB}.{CD} {color: rgba(150, 150, 150) !important; cursor: not-allowed !important;}'.replaceAll('{CB}', CLASSNAME_BUTTON).replaceAll('{CT}', CLASSNAME_TEXT).replaceAll('{CD}', CLASSNAME_DISABLED)
  688. + '.{CAT}>ul {list-style: none; text-align: center; padding: 0px; margin: 0px;} .{CAT} {position: absolute; zIndex: 999; backgroundColor: #f5f5f5; float: left; clear: both; height: 180px; overflow-y: auto; overflow-x: visible;} .{CLI} {display: block; list-style: outside none none; margin: 0px; border: 1px solid rgb(204, 204, 204);} .{CLB} {border: 0px; width: 100%; height: 100%; cursor: pointer; padding: 0 0.5em;}'.replaceAll('{CAT}', CLASSNAME_LIST).replaceAll('{CLI}', CLASSNAME_LIST_ITEM).replaceAll('{CLB}', CLASSNAME_LIST_BUTTON)
  689. + '.tippy-box[data-theme~="wenku_tip"] {background-color: #f0f7ff;color: black;border: 1px solid #a3bee8;}.tippy-box[data-theme~="wenku_tip"][data-placement^="top"]>.tippy-arrow::before {border-top-color: #a3bee8;}.tippy-box[data-theme~="wenku_tip"][data-placement^="left"]>.tippy-arrow::before {border-left-color: #a3bee8;}.tippy-box[data-theme~="wenku_tip"][data-placement^="right"]>.tippy-arrow::before {border-right-color: #a3bee8;}.tippy-box[data-theme~="wenku_tip"][data-placement^="bottom"]>.tippy-arrow::before {border-bottom-color: #a3bee8;}';
  690. const CSS_COMMONBEAUTIFIER = '.plus_cbty_image {position: fixed;top: 0;left: 0;z-index: -2;}.plus_cbty_cover {position: fixed;top: 0;left: calc((100vw - 960px) / 2);z-Index: -1;background-color: rgba(255,255,255,0.7);width: 960px;height: 100vh;}body {overflow: auto;}body>.main {position: relative;margin-left: 0;margin-right: 0;left: calc((100vw - 960px) / 2);}body.plus_cbty table.grid td, body.plus_cbty .odd, body.plus_cbty .even, body.plus_cbty .blockcontent {background-color: rgba(255,255,255,0) !important;}.textarea, .text {background-color: rgba(255,255,255,0.9);}#headlink{background-color: rgba(255,255,255,0.7);}';
  691. const CSS_REVIEWSHOW ='body {overflow: auto;background-image: url({BGI});}#content > table > tbody > tr > td {background-color: rgba(255,255,255,0.7) !important;overflow: auto;}body.plus_cbty #content > table > tbody > tr > td {background-color: rgba(255,255,255,0) !important;overflow: auto;}#content {height: 100vh;overflow: auto;}.m_top, .m_head, .main.nav, .m_foot {display: none;}.main {margin-top: 0px;}#content table div[style*="width:100%"]{font-size: calc(1em * {S}/ 100);line-height: 100%;}.jieqiQuote, .jieqiCode, .jieqiNote {font-size: inherit;}.{M}{background-color: {C}}'.replace('{M}', CLASSNAME_MODIFIED).replace('{C}', CSS_COLOR_FLOOR_MODIFIED);
  692. const CSS_NOVEL = 'html{background-image: url({BGI});}body {width: 100vw;height: 100vh;overflow: overlay;margin: 0px;background-color: rgba(255,255,255,0.7);}#contentmain {overflow-y: auto;height: calc(100vh - {H});max-width: 100%;min-width: 0px;max-width: 100vw;}#adv1, #adtop, #headlink, #footlink, #adbottom {overflow: overlay;min-width: 0px;max-width: 100vw;}#adv900, #adv5 {max-width: 100vw;}';
  693. const CSS_SIDEPANEL = '#sidepanel-panel {background-color: #00000000;z-index: 4000;}.sidepanel-button {font-size: 1vmin;color: #1E64DC;background-color: #FDFDFD;}.sidepanel-button:hover, .sidepanel-button.low-opacity:hover {opacity: 1;color: #FDFDFD;background-color: #1E64DC;}.sidepanel-button.low-opacity{opacity: 0.4 }.sidepanel-button>i[class^="fa-"] {line-height: 3vmin;width: 3vmin;}.sidepanel-button[class*="tooltip"]:hover::after {font-size: 0.9rem;top: calc((5vmin - 25px) / 2);}.sidepanel-button[class*="tooltip"]:hover::before {top: calc((5vmin - 12px) / 2);}.sidepanel-button.accept-pointer{pointer-events:auto;}';
  694.  
  695. const ARR_GUI_BOOKCASE_WIDTH = ['3%', '19%', '9%', '25%', '20%', '9%', '5%', '10%'];
  696.  
  697. const TEXT_TIP_COPY = '点击复制';
  698. const TEXT_TIP_COPIED = '已复制';
  699. const TEXT_TIP_SERVERCHANGE = '点击切换线路';
  700. const TEXT_TIP_API_PACKSHOW_LOADING = '正在初始化下载页面,请稍候...';
  701. const TEXT_TIP_API_PACKSHOW_LOADED = '初始化下载页面成功';
  702. const TEXT_TIP_INDEX_LATERREADS = '文库首页显示前六本稍后再读书目</br>您可以在书架页面管理稍后阅读书目和调整书籍顺序';
  703. const TEXT_TIP_SEARCH_OPTION_TAG = '有关标签搜索</br></br>未完善-开发中…</br>官方尚未正式开放此功能</br>功能预览由[轻小说文库+]提供';
  704. const TEXT_TIP_REVIEW_BEAUTIFUL = '背景图片可以在"用户面板"中设置</br>您可以从文库首页左侧点击进入用户面板';
  705. const TEXT_TIP_REVIEW_IMG_INSERTURL = '直接插入网络图片的链接地址';
  706. const TEXT_TIP_REVIEW_IMG_SELECTIMG = '选择本地图片上传到第三方图床,然后再插入图床提供的图片链接</br>您也可以直接拖拽图片到输入框,或者Ctrl+V直接粘贴您剪贴板里面的图片</br>您可以在用户面板中切换图床</br></br>上传图片请遵守法律以及图床使用规定</br>请不要上传违规图片';
  707. const TEXT_TIP_IMAGE_FIT = '请选择适合您的屏幕宽高比的图片</br>您选择的图片将会被拉伸以适应屏幕的宽高比,图片宽高比与屏幕宽高比相差过大会导致图片扭曲</br>请避免选择文件大小过大的图片,以防止浏览器卡顿';
  708. const TEXT_TIP_IMAGER_DEFAULT = '</br></br><span class=\'{CT}\'>{N} 默认图床</span>'.replace('{N}', GM_info.script.name).replace('{CT}', CLASSNAME_TEXT);
  709. const TEXT_TIP_DOWNLOAD_BBCODE = 'BBCODE格式:</br>即文库评论的代码格式</br>相当于引用楼层时自动填入回复框的内容</br>保存为此格式可以保留排版及多媒体信息';
  710. const TEXT_TIP_ACCOUNT_NOACCOUNT = '没有储存的账号信息</br>请在登录页面手动登录一次,相关帐号信息就会自动储存</br></br>所有储存的账号信息都自动保存在浏览器的本地存储中';
  711. const TEXT_ALT_SCRIPT_ERROR_AJAX_FA = 'FontAwesome加载失败(自动重试也失败了),可能会影响一部分脚本界面图标和样式的展示,但基本不会影响功能</br>您可以将此消息<a href="https://greasyfork.org/scripts/416310/feedback" class=\'{CB}\'>反馈给开发者</a>以尝试解决问题'.replace('{CB}', CLASSNAME_BUTTON);
  712. const TEXT_ALT_DOWNLOAD_BBCODE_NOCHANGE = '帖子正在下载中,请不要更改此设置!';
  713. const TEXT_ALT_DOWNLOADFINISH_REVIEW = '{T}({I}) 已下载完毕</br>{N} 已保存';
  714. const TEXT_ALT_DOWNLOADIMG_CONFIRM_TITLE = '确认下载';
  715. const TEXT_ALT_DOWNLOADIMG_CONFIRM_MESSAGE = '是否要下载 {N} 的全部插图?';
  716. const TEXT_ALT_DOWNLOADIMG_CONFIRM_OK = '下载';
  717. const TEXT_ALT_DOWNLOADIMG_CONFIRM_CANCEL = '取消';
  718. const TEXT_ALT_DOWNLOADIMG_STATUS_INDEX = '正在获取小说目录...';
  719. const TEXT_ALT_DOWNLOADIMG_STATUS_LOADING = '正在下载: {CCUR}/{CALL}';
  720. const TEXT_ALT_DOWNLOADIMG_STATUS_FINISH = '全部插图下载完毕:)';
  721. const TEXT_ALT_BOOK_AFTERBOOKS_ADDED = '已添加到稍后再读';
  722. const TEXT_ALT_BOOK_AFTERBOOKS_REMOVED = '已将其从稍后再读中移除';
  723. const TEXT_ALT_BOOKCASE_AFTERBOOKS_MISSING = '看起来这本书并不在稍后再读的列表里呢</br>是不是已经在其他的标签页里把它从稍后再读中移除了?';
  724. const TEXT_ALT_BOOKCASE_AFTERBOOKS_V4BUG = '由于历史版本脚本的一个bug,您的<i>稍后再读</i>列表的小说排序被打乱了(非常抱歉)</br>而现在这个bug已经修复,<i>稍后再读</i>列表的小说排序也许需要您重新调整一次</br><span class="{CB}">[我知道了]</span>'.replaceAll('{CB}', CLASSNAME_BUTTON);
  725. const TEXT_ALT_AUTOREFRESH_ON = '页面自动刷新已开启';
  726. const TEXT_ALT_AUTOREFRESH_OFF = '页面自动刷新已关闭';
  727. const TEXT_ALT_AUTOREFRESH_NOTLAST = '请先翻到最后一页再开启页面自动刷新</br><span class="{CB}">[点击这里翻到最后一页]</span>'.replaceAll('{CB}', CLASSNAME_BUTTON);
  728. const TEXT_ALT_AUTOREFRESH_WORKING = '正在获取新的回复...';
  729. const TEXT_ALT_AUTOREFRESH_NOMORE = '木有新的回复';
  730. const TEXT_ALT_AUTOREFRESH_APPLIED = '发现了新的回复,页面已更新~</br>'.replaceAll('{CB}', CLASSNAME_BUTTON);
  731. const TEXT_ALT_AUTOREFRESH_MODIFIED = '发现已有楼层内容变更,已对其进行了颜色标记</br>点击标记区域即可恢复原来的颜色';
  732. const TEXT_ALT_BEAUTIFUL_ON = '页面美化已开启</br>您可能需要刷新页面使其生效';
  733. const TEXT_ALT_BEAUTIFUL_OFF = '页面美化已关闭</br>您可能需要刷新页面使其生效';
  734. const TEXT_ALT_FAVORITE_LAST_ON = '将在点击收藏的帖子时打开最后一页';
  735. const TEXT_ALT_FAVORITE_LAST_OFF = '将在点击收藏的帖子时打开第一页';
  736. const TEXT_ALT_IMAGE_FORMATERROR = '很遗憾,您选择的图片格式无法识别</br>(建议选择jpeg,png)!';
  737. const TEXT_ALT_IMAGE_UPLOAD_WORKING = '正在上传图片…';
  738. const TEXT_ALT_IMAGE_DOWNLOAD_WORKING = '正在下载图片…';
  739. const TEXT_ALT_IMAGE_UPLOAD_SUCCESS = '图片上传成功!</br>文件名: {NAME}</br>URL: {URL}';
  740. const TEXT_ALT_IMAGE_DOWNLOAD_SUCCESS = '图片下载成功!</br>已经将背景图片 {NAME} 保存在本地';
  741. const TEXT_ALT_IMAGE_RESPONSE_NONAME = '空(服务器没有返回文件名)';
  742. const TEXT_ALT_IMAGE_UPLOAD_ERROR = '上传错误!';
  743. const TEXT_ALT_TEXTSCALE_CHANGED = '字体缩放已保存:{S}%';
  744. const TEXT_ALT_CONFIG_EXPORTED = '配置文件已导出</br>文件名:{N}';
  745. const TEXT_ALT_CONFIG_IMPORTED = '配置文件已导入';
  746. const TEXT_ALT_IMAGER_RESET = '由于{O}已失效,您的图床已自动切换到{N}';
  747. const TEXT_ALT_IMAGER_NOAVAILBLE = '{O}已失效';
  748. const TEXT_ALT_META_COPIED = '{M} 已复制';
  749. const TEXT_ALT_ATRCMMDS_SAVED = '已保存:《{B}》</br>每日自动推荐{N}次</br>每日还可推荐{R}次';
  750. const TEXT_ALT_ATRCMMDS_INVALID = '未保存:{N}不是非负整数';
  751. const TEXT_ALT_ATRCMMDS_OVERFLOW = '注意:</br>您的用户信息显示您每天最多推荐{V}票</br>当前您已设置每日推荐合计{C}票</br><span class="{CB}">[单击此处以立即更新您的用户信息]</span>'.replaceAll('{CB}', CLASSNAME_BUTTON);
  752. const TEXT_ALT_ATRCMMDS_AUTO = '已开启自动推书';
  753. const TEXT_ALT_ATRCMMDS_NOAUTO = '已关闭自动推书';
  754. const TEXT_ALT_ATRCMMDS_ALL_START = '{S}:正在自动推书...'.replaceAll('{S}', GM_info.script.name);
  755. const TEXT_ALT_ATRCMMDS_RUNNING = '正在推荐书目:</br>{BN}({BID})';
  756. const TEXT_ALT_ATRCMMDS_DONE = '推荐完成:</br>{BN}({BID})';
  757. const TEXT_ALT_ATRCMMDS_ALL_DONE = '全部书目推荐完成:</br>{R}';
  758. const TEXT_ALT_ATRCMMDS_NOTASK = '木有要推荐的书目╮( ̄▽ ̄)╭';
  759. const TEXT_ALT_ATRCMMDS_NOTASK_OPENBC = '您还没有设置每日自动推荐的书目╮( ̄▽ ̄)╭</br><span class="{CB}">[点击此处打开书架页面进行设置]</span>'.replaceAll('{CB}', CLASSNAME_BUTTON);
  760. const TEXT_ALT_ATRCMMDS_NOTASK_PLSSET = '请在\'自动推书\'一栏设置每日推荐的书目及推荐次数';
  761. const TEXT_ALT_ATRCMMDS_MAXRCMMD = '根据您的头衔,您每日一共可以推荐{V}次';
  762. const TEXT_ALT_USRDTL_REFRESH = '{S}:正在更新用户信息({T})...'.replaceAll('{S}', GM_info.script.name).replaceAll('{T}', getTime());
  763. const TEXT_ALT_USRDTL_REFRESHED = '{S}:用户信息已更新</br><span class="{CB}">[点此查看详细信息]</span>'.replaceAll('{S}', GM_info.script.name).replaceAll('{CB}', CLASSNAME_BUTTON);
  764. const TEXT_ALT_POLYFILL = '<span class="{CT}">提示:正在使用移动端适配模式</span>'.replaceAll('{CT}', CLASSNAME_TEXT);
  765. const TEXT_ALT_LASTPAGE_LOADING = '正在获取最后一页,请稍候...';
  766. const TEXT_ALT_ACCOUNT_SWITCHED = '帐号已切换到 <i>"<span class="{CT}">{N}</span>"</i></br>3s后自动刷新页面</br><span class="{CB}">点击这里取消刷新</span>'.replaceAll('{CT}', CLASSNAME_TEXT).replaceAll('{CB}', CLASSNAME_BUTTON);
  767. const TEXT_ALT_ACCOUNT_WORKING_LOGOFF = '正在退出当前账号...';
  768. const TEXT_ALT_ACCOUNT_WORKING_LOGIN = '正在登录...';
  769. const TEXT_ALT_SCRIPT_UPDATE_CHECKING = '正在检查脚本更新...';
  770. const TEXT_ALT_SCRIPT_UPDATE_GOT = '<div class="{CT}">{SN} 有新版本啦!</br>新版本:{NV}</br>当前版本:{CV}</br><span id="script_update_info" class="{CB}">[点击此处 查看 更新]</span></br><span id="script_update_install" class="{CB}">[点击此处 安装 更新]</span></div>'.replaceAll('{CT}', CLASSNAME_TEXT).replaceAll('{CB}', CLASSNAME_BUTTON);
  771. const TEXT_ALT_SCRIPT_UPDATE_INFO = '更新信息';
  772. const TEXT_ALT_SCRIPT_UPDATE_NOINFO = '没有发现更新日志。。';
  773. const TEXT_ALT_SCRIPT_UPDATE_INSTALL = '安装';
  774. const TEXT_ALT_SCRIPT_UPDATE_CLOSE = '朕知道了';
  775. const TEXT_ALT_SCRIPT_UPDATE_NONE = '当前已是最新版本';
  776. const TEXT_ALT_DETAIL_IMPORTED = '配置导入成功';
  777. const TEXT_ALT_DETAIL_CONFIG_IMPORT_ERROR_SELECT = '您选择的文件不是配置文件,请检查后再试';
  778. const TEXT_ALT_DETAIL_CONFIG_IMPORT_ERROR_READ = '配置文件读取出错,请检查是否粘贴了正确的配置文件,以及配置文件是否损坏';
  779. const TEXT_ALT_DETAIL_MANAGE_NOTFOUND = '该记录已不存在,您是否已经在其他标签页删除它了呢?';
  780. const TEXT_GUI_API_ADDBOOKCASE_TOBOOKCASE = '进入书架';
  781. const TEXT_GUI_API_ADDBOOKCASE_REMOVE = '移出本书';
  782. const TEXT_GUI_API_PACKSHOW_TITLE_LOADING = '初始化下载界面...';
  783. const TEXT_GUI_API_PACKSHOW_TITLE = '{N} 轻小说TXT分卷下载 - 轻小说文库';
  784. const TEXT_GUI_UNKNOWN = '未知';
  785. const TEXT_GUI_DOWNLOAD_THISVOLUME = '下载本卷';
  786. const TEXT_GUI_DOWNLOAD_THISCHAPTER = '下载本章';
  787. const TEXT_GUI_NOVEL_FILLING = '</br><span class="{CT}">[轻小说文库+] 正在获取章节内容...</span>'.replaceAll('{CT}', CLASSNAME_TEXT);
  788. const TEXT_GUI_BOOK_IMAGESDOWNLOAD = '全部插图下载';
  789. const TEXT_GUI_BOOK_READITLATER = '稍后再读';
  790. const TEXT_GUI_BOOK_DONTREADLATER = '移出稍后再读';
  791. const TEXT_GUI_REVIEW_ADDFAVORITE = '收藏本帖:';
  792. const TEXT_GUI_REVIEW_FAVORADDED = '已收藏 {N}';
  793. const TEXT_GUI_REVIEW_FAVORDELED = '已从收藏中移除 {N}';
  794. const TEXT_GUI_REVIEW_BEAUTIFUL = '页面美化:';
  795. const TEXT_GUI_REVEIW_IMG_INSERTURL = '插入网图链接';
  796. const TEXT_GUI_REVEIW_IMG_SELECTIMG = '选择本地图片';
  797. const TEXT_GUI_REVIEW_UNLOCK_WARNING = '<span style="color: red;">仅供测试使用,请勿滥用此功能!</span>';
  798. const TEXT_GUI_DOWNLOAD_REVIEW = '[下载本帖(共A页)]';
  799. const TEXT_GUI_DOWNLOADING_REVIEW = '[下载中...(C/A)]';
  800. const TEXT_GUI_DOWNLOAD_BBCODE = '保存为BBCODE格式:';
  801. const TEXT_GUI_DOWNLOADFINISH_REVIEW = '[下载完毕]';
  802. const TEXT_GUI_DOWNLOADALL = '下载全部分卷,请点击右边的按钮:';
  803. const TEXT_GUI_WAITING = ' 等待中...';
  804. const TEXT_GUI_DOWNLOADING = ' 下载中...';
  805. const TEXT_GUI_DOWNLOADED = ' (下载完毕)';
  806. const TEXT_GUI_NOTHINGHERE = '<span style="color:grey">-Nothing Here-</span>';
  807. const TEXT_GUI_SDOWNLOAD = '地址三(程序重命名)';
  808. const TEXT_GUI_SDOWNLOAD_FILENAME = '{NovelName} {VolumeName}.{Extension}';
  809. const TEXT_GUI_DOWNLOADING_ALL = '下载中...(C/A)';
  810. const TEXT_GUI_DOWNLOADED_ALL = '下载图片(已完成)';
  811. const TEXT_GUI_AUTOREFRESH = '自动更新页面:';
  812. const TEXT_GUI_AUTOREFRESH_PAUSED = '(回复编辑中,暂停刷新)';
  813. const TEXT_GUI_AUTOSAVE = '(您输入的内容已保存到书评草稿中)';
  814. const TEXT_GUI_AUTOSAVE_CLEAR = '(草稿为空)';
  815. const TEXT_GUI_AUTOSAVE_RESTORE = '(已从书评草稿中恢复了您上次编辑的内容)';
  816. const TEXT_GUI_AREAREPLY_AT = '想用@提到谁?';
  817. const TEXT_GUI_INDEX_FAVORITES = '收藏的书评';
  818. const TEXT_GUI_INDEX_STATUS = '{S} 正在运行,版本 {V}。'.replace('{S}', GM_info.script.name).replace('{V}', GM_info.script.version);
  819. const TEXT_GUI_INDEX_LATERBOOKS = '稍后再读';
  820. const TEXT_GUI_BOOKCASE_GETTING = '正在搬运书架...(C/A)';
  821. const TEXT_GUI_BOOKCASE_TOPTITLE = '您的书架可收藏 A 本,已收藏 B 本';
  822. const TEXT_GUI_BOOKCASE_MOVEBOOK = '移动到 [N]';
  823. const TEXT_GUI_BOOKCASE_DBLCLICK = '双击/长按我,给我取一个好听的名字吧~';
  824. const TEXT_GUI_BOOKCASE_WHATNAME = '呜呜呜~会是什么名字呢?';
  825. const TEXT_GUI_BOOKCASE_ATRCMMD = '自动推书';
  826. const TEXT_GUI_BOOKCASE_RCMMDAT = '<span>每日自动推书:</span>';
  827. const TEXT_GUI_BOOKCASE_RCMMDNW = '立即推书';
  828. const TEXT_GUI_BOOKCASE_RCMMDNW_DONE = '今日推书已完成';
  829. const TEXT_GUI_BOOKCASE_RCMMDNW_NOTYET = '今日尚未推书';
  830. const TEXT_GUI_BOOKCASE_RCMMDNW_NOTASK = '您还没有设置自动推书';
  831. const TEXT_GUI_BOOKCASE_RCMMDNW_CONFIRM = '今天已经推过书了,是否要再推一遍?';
  832. const TEXT_GUI_SEARCH_OPTION_TAG = '标签(preview)';
  833. const TEXT_GUI_DETAIL_TITLE_SETTINGS = '脚本设置';
  834. const TEXT_GUI_DETAIL_TITLE_BGI = '页面美化背景图片';
  835. const TEXT_GUI_DETAIL_DEFAULT_BGI = '点击选择图片 / 拖拽图片到此处 / Ctrl+V粘贴剪贴板中的图片';
  836. const TEXT_GUI_DETAIL_BGI = '当前图片:{N}';
  837. const TEXT_GUI_DETAIL_BGI_WORKING = '处理中...';
  838. const TEXT_GUI_DETAIL_BGI_UPLOADING = '正在上传: {NAME}';
  839. const TEXT_GUI_DETAIL_BGI_UPLOADFAILED = '{NAME}(上传失败,已本地保存)';
  840. const TEXT_GUI_DETAIL_BGI_DOWNLOADING = '正在下载: {NAME}';
  841. const TEXT_GUI_DETAIL_BGI_UPLOAD = '上传图片到图床以防止卡顿';
  842. const TEXT_GUI_DETAIL_BGI_LEGAL = '上传图片请遵守法律以及图床使用规定</br>请不要上传违规图片';
  843. const TEXT_GUI_DETAIL_GUI_IMAGER = '图床选择';
  844. const TEXT_GUI_DETAIL_GUI_SCALE = '书评字体缩放';
  845. const TEXT_GUI_DETAIL_BTF_NOVEL = '阅读页面美化';
  846. const TEXT_GUI_DETAIL_BTF_REVIEW = '书评页面美化';
  847. const TEXT_GUI_DETAIL_BTF_COMMON = '其他页面美化';
  848. const TEXT_GUI_DETAIL_FVR_LASTPAGE = '点击收藏的帖子时打开最后一页';
  849. const TEXT_GUI_DETAIL_VERSION_CURVER = '当前版本';
  850. const TEXT_GUI_DETAIL_VERSION_CHECKUPDATE = '检查更新';
  851. const TEXT_GUI_DETAIL_VERSION_CHECK = '点击此处检查更新';
  852. const TEXT_GUI_DETAIL_CONFIG_EXPORT = '导出所有脚本配置到文件(包含账号密码)';
  853. const TEXT_GUI_DETAIL_CONFIG_EXPORT_NOPASS = '导出所有脚本配置到文件(不包含账号密码)';
  854. const TEXT_GUI_DETAIL_EXPORT_CLICK = '点击导出';
  855. const TEXT_GUI_DETAIL_CONFIG_IMPORT = '从文件导入脚本配置';
  856. const TEXT_GUI_DETAIL_IMPORT_CLICK = '点击导入 / 拖拽配置文件到此处 / Ctrl+V粘贴剪贴板中的配置文件,并刷新页面';
  857. const TEXT_GUI_DETAIL_FEEDBACK_TITLE = '提出反馈';
  858. const TEXT_GUI_DETAIL_FEEDBACK = '点击打开反馈页面';
  859. const TEXT_GUI_DETAIL_UPDATEINFO_TITLE = '更新日志';
  860. const TEXT_GUI_DETAIL_UPDATEINFO = '点击去主页查看';
  861. const TEXT_GUI_DETAIL_CONFIG_MANAGE = '管理存储的信息';
  862. const TEXT_GUI_DETAIL_CONFIG_MANAGE_EMPTY = '<span style="color:grey;">没有内容</span>';
  863. const TEXT_GUI_DETAIL_CONFIG_MANAGE_MORE = '<span style="color:grey;">…</span>';
  864. const TEXT_GUI_DETAIL_MANAGE_CLICK = '点击打开管理页面';
  865. const TEXT_GUI_DETAIL_MANAGE_HEADER = '脚本储存管理';
  866. const TEXT_GUI_DETAIL_MANAGE_FAV_NOTE_BTN_OPEN = '打开';
  867. const TEXT_GUI_DETAIL_MANAGE_FAV_NOTE_BTN_NOTE = '备注';
  868. const TEXT_GUI_DETAIL_MANAGE_FAV_NOTE_BTN_DELETE = '删除';
  869. const TEXT_GUI_DETAIL_MANAGE_FAV_NOTE_TIP = '为{TITLE}设置备注: </br>备注将在主页鼠标经过此帖子收藏的链接时悬浮显示';
  870. const TEXT_GUI_DETAIL_MANAGE_FAV_NOTE_TITLE = '编辑备注';
  871. const TEXT_GUI_DETAIL_MANAGE_FAV_DELETE_TIP = '确认将{TITLE}移除收藏?';
  872. const TEXT_GUI_DETAIL_MANAGE_FAV_DELETE_TITLE = '移除收藏';
  873. const TEXT_GUI_DETAIL_MANAGE_FAV_SAVED = '已保存';
  874. const TEXT_GUI_DETAIL_MANAGE_FAV_DELETED = '已删除';
  875. const TEXT_GUI_DETAIL_CONFIG_IMPORT_CONFIRM_SELECT = '是否要将您粘贴的图片({N})中设置为页面美化背景图片?';
  876. const TEXT_GUI_DETAIL_CONFIG_IMPORT_CONFIRM_PASTE = '是否要从您粘贴的配置文件({N})中导入配置?\n建议先备份您当前的配置,再导入新配置';
  877. const TEXT_GUI_BLOCK_TITLE_DEFULT = '操作区域';
  878. const TEXT_GUI_USER_REVIEWSEARCH = '用户书评';
  879. const TEXT_GUI_USER_USERINFO = '详细资料';
  880. const TEXT_GUI_USER_USERREMARKEDIT = '编辑备注';
  881. const TEXT_GUI_USER_USERREMARKSHOW = '用户备注:';
  882. const TEXT_GUI_USER_USERREMARKEMPTY = '假装这里有个备注';
  883. const TEXT_GUI_USER_USERREMARKEDIT_TITLE = '编辑备注';
  884. const TEXT_GUI_USER_USERREMARKEDIT_MSG = '设置 [{N}] 的备注为:';
  885. const TEXT_GUI_LINK_TOLASTPAGE = '[打开尾页]';
  886. const TEXT_GUI_ACCOUNT_SWITCH = '切换账号:';
  887. const TEXT_GUI_ACCOUNT_CONFIRM = '是否要切换到帐号 "{N}"?';
  888. const TEXT_GUI_ACCOUNT_NOACCOUNT = '(帐号列表为空)';
  889. const TEXT_GUI_ACCOUNT_NOTLOGGEDIN = '(没有登录信息)';
  890.  
  891. // Emoji smiles (not used in the script yet)
  892. const SmList =
  893. [{text:"/:O",id:"1",alt:"惊讶"}, {text:"/:~",id:"2",alt:"撇嘴"}, {text:"/:*",id:"3",alt:"色色"},
  894. {text:"/:|",id:"4",alt:"发呆"}, {text:"/8-)",id:"5",alt:"得意"}, {text:"/:LL",id:"6",alt:"流泪"},
  895. {text:"/:$",id:"7",alt:"害羞"}, {text:"/:X",id:"8",alt:"闭嘴"}, {text:"/:Z",id:"9",alt:"睡觉"},
  896. {text:"/:`(",id:"10",alt:"大哭"}, {text:"/:-",id:"11",alt:"尴尬"}, {text:"/:@",id:"12",alt:"发怒"},
  897. {text:"/:P",id:"13",alt:"调皮"}, {text:"/:D",id:"14",alt:"呲牙"}, {text:"/:)",id:"15",alt:"微笑"},
  898. {text:"/:(",id:"16",alt:"难过"}, {text:"/:+",id:"17",alt:"耍酷"}, {text:"/:#",id:"18",alt:"禁言"},
  899. {text:"/:Q",id:"19",alt:"抓狂"}, {text:"/:T",id:"20",alt:"呕吐"}]
  900.  
  901. /* \t
  902. ┌┬┐┌─┐┏┳┓┏━┓╭─╮
  903. ├┼┤│┼│┣╋┫┃╋┃│╳│
  904. └┴┘└─┘┗┻┛┗━┛╰─╯
  905. ╲╱╭╮
  906. ╱╲╰╯
  907. */
  908. /* **output format: Review Name.txt**
  909. ** 轻小说文库-帖子 [ID: reviewid]
  910. ** title
  911. ** 保存自: reviewlink
  912. ** 保存时间: savetime
  913. ** By scriptname Ver. version, author authorname
  914. **
  915. ** ──────────────────────────────
  916. ** [用户: username userid]
  917. ** 用户名: username
  918. ** 用户ID: userid
  919. ** 加入日期: 1970-01-01
  920. ** 用户链接: userlink
  921. ** 最早出现: 1楼
  922. ** ──────────────────────────────
  923. ** ...
  924. ** ──────────────────────────────
  925. ** [#1 2021-04-26 17:53:49] [username userid]
  926. ** ──────────────────────────────
  927. ** content - line 1
  928. ** content - line 2
  929. ** content - line 3
  930. ** ──────────────────────────────
  931. **
  932. ** ──────────────────────────────
  933. ** [#2 2021-04-26 19:28:08] [username userid]
  934. ** ──────────────────────────────
  935. ** content - line 1
  936. ** content - line 2
  937. ** content - line 3
  938. ** ──────────────────────────────
  939. **
  940. ** ...
  941. **
  942. **
  943. ** [THE END]
  944. */
  945. const TEXT_SPLIT_LINE_CHAR = '━'; const TEXT_SPLIT_LINE = TEXT_SPLIT_LINE_CHAR.repeat(20)
  946. const TEXT_OUTPUT_REVIEW_HEAD =
  947. '轻小说文库-帖子 [ID: {RWID}]\n{RWTT}\n保存自: {RWLK}\n保存时间: {SVTM}\nBy {SCNM} Ver. {VRSN}, author {ATNM}'
  948. const TEXT_OUTPUT_REVIEW_USER =
  949. '{LNSPLT}\n[用户: {USERNM} {USERID}]\n用户名: {USERNM}\n用户ID: {USERID}\n加入日期: {USERJT}\n用户链接: {USERLK}\n最早出现: {USERFL}楼\n{LNSPLT}'
  950. const TEXT_OUTPUT_REVIEW_FLOOR =
  951. '{LNSPLT}\n[#{RPNUMB} {RPTIME}] [{USERNM} {USERID}]\n{LNSPLT}\n{RPTEXT}\n{LNSPLT}';
  952. const TEXT_OUTPUT_REVIEW_END = '\n[THE END]';
  953.  
  954. // Arguments: level=LogLevel.Info, logContent, asObject=false
  955. // Needs one call "DoLog();" to get it initialized before using it!
  956. function DoLog() {
  957. // Global log levels set
  958. unsafeWindow.LogLevel = {
  959. None: 0,
  960. Error: 1,
  961. Success: 2,
  962. Warning: 3,
  963. Info: 4,
  964. }
  965. unsafeWindow.LogLevelMap = {};
  966. unsafeWindow.LogLevelMap[LogLevel.None] = {prefix: '' , color: 'color:#ffffff'}
  967. unsafeWindow.LogLevelMap[LogLevel.Error] = {prefix: '[Error]' , color: 'color:#ff0000'}
  968. unsafeWindow.LogLevelMap[LogLevel.Success] = {prefix: '[Success]' , color: 'color:#00aa00'}
  969. unsafeWindow.LogLevelMap[LogLevel.Warning] = {prefix: '[Warning]' , color: 'color:#ffa500'}
  970. unsafeWindow.LogLevelMap[LogLevel.Info] = {prefix: '[Info]' , color: 'color:#888888'}
  971. unsafeWindow.LogLevelMap[LogLevel.Elements] = {prefix: '[Elements]', color: 'color:#000000'}
  972.  
  973. // Current log level
  974. DoLog.logLevel = (unsafeWindow ? unsafeWindow.isPY_DNG : window.isPY_DNG) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
  975.  
  976. // Log counter
  977. DoLog.logCount === undefined && (DoLog.logCount = 0);
  978. if (++DoLog.logCount > 512) {
  979. console.clear();
  980. DoLog.logCount = 0;
  981. }
  982.  
  983. // Get args
  984. let level, logContent, asObject;
  985. switch (arguments.length) {
  986. case 1:
  987. level = LogLevel.Info;
  988. logContent = arguments[0];
  989. asObject = false;
  990. break;
  991. case 2:
  992. level = arguments[0];
  993. logContent = arguments[1];
  994. asObject = false;
  995. break;
  996. case 3:
  997. level = arguments[0];
  998. logContent = arguments[1];
  999. asObject = arguments[2];
  1000. break;
  1001. default:
  1002. level = LogLevel.Info;
  1003. logContent = 'DoLog initialized.';
  1004. asObject = false;
  1005. break;
  1006. }
  1007.  
  1008. // Log when log level permits
  1009. if (level <= DoLog.logLevel) {
  1010. let msg = '%c' + LogLevelMap[level].prefix;
  1011. let subst = LogLevelMap[level].color;
  1012.  
  1013. if (asObject) {
  1014. msg += ' %o';
  1015. } else {
  1016. switch(typeof(logContent)) {
  1017. case 'string': msg += ' %s'; break;
  1018. case 'number': msg += ' %d'; break;
  1019. case 'object': msg += ' %o'; break;
  1020. }
  1021. }
  1022.  
  1023. console.log(msg, subst, logContent);
  1024. }
  1025. }
  1026. DoLog();
  1027.  
  1028. let tipready, CONFIG, TASK, DMode, SPanel, AndAPI
  1029. let API
  1030. main();
  1031.  
  1032. // Main
  1033. function main() {
  1034. // Get tab url api part
  1035. API = window.location.href.replace(/https?:\/\/www\.wenku8\.(net|cc)\//, '').replace(/\?.*/, '').replace(/#.*/, '')
  1036. .replace(/^book\/\d+\.html?/, 'book').replace(/novel\/(\d+\/?)+\.html?$/, 'novel')
  1037. .replace(/^novel[\/\d]+index\.html?$/, 'novelindex');
  1038.  
  1039. // Common actions
  1040. loadinResourceCSS();
  1041. loadinFontAwesome();
  1042. polyfillAlert();
  1043. tipready = tipcheck();
  1044. tipscroll();
  1045. addStyle(CSS_COMMON);
  1046. GMXHRHook(NUMBER_MAX_XHR);
  1047. CONFIG = new configManager();
  1048. TASK = new taskManager();
  1049. AndAPI = new AndroidAPI();
  1050. //DMode = new Darkmode({autoMatchOsTheme: false});
  1051. formSearch();
  1052. linkReview();
  1053. multiAccount();
  1054. commonBeautify(API);
  1055. SPanel = sideFunctions();
  1056. unsafeWindow.alertify = alertify;
  1057. alertify.set('notifier','position', 'top-right');
  1058.  
  1059. if (isAPIPage()) {
  1060. if (!pageAPI(API)) {
  1061. return;
  1062. }
  1063. }
  1064. if (!API) {
  1065. location.href = `https://${location.host}/index.php`;
  1066. return;
  1067. };
  1068. switch (API) {
  1069. // Dwonload page
  1070. case 'modules/article/packshow.php':
  1071. pageDownload();
  1072. break;
  1073. // ReviewList page
  1074. case 'modules/article/reviews.php':
  1075. areaReply();
  1076. break;
  1077. // Review page
  1078. case 'modules/article/reviewshow.php':
  1079. areaReply();
  1080. pageReview();
  1081. break;
  1082. // ReviewEdit page
  1083. case 'modules/article/reviewedit.php':
  1084. areaReply();
  1085. pageReviewedit();
  1086. break;
  1087. // Bookcase page
  1088. case 'modules/article/bookcase.php':
  1089. pageBookcase();
  1090. break;
  1091. // Tags page
  1092. case 'modules/article/tags.php':
  1093. pageTags();
  1094. break;
  1095. // Mylink page
  1096. case 'mylink.php':
  1097. pageMylink();
  1098. break;
  1099. case 'userpage.php':
  1100. pageUser();
  1101. break;
  1102. // Detail page
  1103. case 'userdetail.php':
  1104. pageDetail();
  1105. break;
  1106. // Index page
  1107. case 'index.php':
  1108. pageIndex();
  1109. break;
  1110. // Book page
  1111. // Also: https://www.wenku8.net/modules/article/articleinfo.php?id={ID}&charset=gbk
  1112. case 'modules/article/articleinfo.php':
  1113. case 'book':
  1114. pageBook();
  1115. break;
  1116. // Novel index page
  1117. case 'novelindex':
  1118. pageNovelIndex();
  1119. break;
  1120. // Novel page
  1121. case 'novel':
  1122. pageNovel();
  1123. break;
  1124. // Novel index page & novel page
  1125. case 'modules/article/reader.php':
  1126. chapter_id === '0' ? pageNovelIndex() : pageNovel();
  1127. break;
  1128. // Login page
  1129. case 'login.php':
  1130. pageLogin();
  1131. break;
  1132. // Other pages
  1133. default:
  1134. DoLog(LogLevel.Info, API);
  1135. }
  1136. }
  1137.  
  1138. // Autorun tasks
  1139. // use 'new' keyword
  1140. function taskManager() {
  1141. const TM = this;
  1142.  
  1143. // UserDetail refresh
  1144. TM.UserDetail = {
  1145. // Refresh userDetail storage everyday
  1146. refresh: function() {
  1147. // Time check: whether recommend has done today
  1148. if (getMyUserDetail().lasttime === getTime('-', false)) {return false;};
  1149. refreshMyUserDetail();
  1150. }
  1151. }
  1152.  
  1153. // Auto-recommend
  1154. TM.AutoRecommend = {
  1155.  
  1156. // Check if recommend has done
  1157. checkRcmmd: function() {
  1158. const arConfig = CONFIG.AutoRecommend.getConfig();
  1159. return arConfig.lasttime === getTime('-', false);
  1160. },
  1161.  
  1162. // Auto recommend main function
  1163. run: function(recommendAnyway=false) {
  1164. let i;
  1165.  
  1166. // Get config
  1167. const arConfig = CONFIG.AutoRecommend.getConfig();
  1168.  
  1169. // Time check: whether all recommends has done today
  1170. if (TM.AutoRecommend.checkRcmmd() && !recommendAnyway) {return false;};
  1171.  
  1172. // Config check: whether we need to auto-recommend
  1173. if (!arConfig.auto && !recommendAnyway) {return false;}
  1174.  
  1175. // Config check: whether the recommend list is empty
  1176. if (arConfig.allCount === 0) {
  1177. const altBox = alertify.notify(
  1178. /modules\/article\/bookcase\.php$/.test(location.href) ?
  1179. TEXT_ALT_ATRCMMDS_NOTASK_PLSSET + (getMyUserDetail().userDetail ? '</br>'+TEXT_ALT_ATRCMMDS_MAXRCMMD.replace('{V}', String(getMyUserDetail().userDetail.vote)) : '') :
  1180. TEXT_ALT_ATRCMMDS_NOTASK_OPENBC
  1181. );
  1182. altBox.callback = (isClicked) => {
  1183. isClicked && window.open(URL_BOOKCASE);
  1184. }
  1185. return false;
  1186. };
  1187.  
  1188. // Recommend for each
  1189. let recommended = {}, AM = new AsyncManager();
  1190. AM.onfinish = allFinish;
  1191.  
  1192. alertify.notify(TEXT_ALT_ATRCMMDS_ALL_START);
  1193. for (const strBookID in arConfig.books) {
  1194. // Only when inherited properties exists must we use hasOwnProperty()
  1195. // here we know there is no inherited properties
  1196. const book = arConfig.books[strBookID]
  1197. const number = book.number;
  1198. const bookID = book.id;
  1199. const bookName = book.name;
  1200.  
  1201. // Time check: whether this book's recommend has done today
  1202. if (book.lasttime === getTime('-', false) && !recommendAnyway) {continue;};
  1203.  
  1204. // Soft alert
  1205. //alertify.notify(TEXT_ALT_ATRCMMDS_RUNNING.replaceAll('{BN}', bookName).replaceAll('{BID}', strBookID));
  1206.  
  1207. // Go work
  1208. for (i = 0; i < number; i++) {
  1209. AM.add();
  1210. getDocument(URL_RECOMMEND.replaceAll('{B}', strBookID), bookFinish,[book, strBookID, bookName]);
  1211. }
  1212.  
  1213. // Soft alert
  1214. //alertify.notify(TEXT_ALT_ATRCMMDS_DONE.replaceAll('{BN}', bookName).replaceAll('{BID}', strBookID));
  1215. }
  1216. AM.finishEvent = true;
  1217. return true;
  1218.  
  1219. function bookFinish(oDoc, book, strBookID, bookName) {
  1220. // title: "处理成功"
  1221. const statusText = $(oDoc, '.blocktitle').innerText;
  1222. // success: "我们已经记录了本次推荐,感谢您的参与!\n\n您每天拥有 5 次推荐权利,这是您今天第 1 次推荐。"
  1223. // overflow: "\n错误原因:对不起,您今天已经用完了推荐的权利!\n\n您每天可以推荐 20 次。\n\n请 返 回 并修正"
  1224. const returnText = $(oDoc, '.blockcontent').innerText.replace(/\s*\[.+\]\s*$/, '');
  1225.  
  1226. // Save book
  1227. book.lasttime = getTime('-', false);
  1228. CONFIG.AutoRecommend.saveConfig(arConfig);
  1229.  
  1230. // Log
  1231. DoLog(statusText + '\n' + returnText);
  1232.  
  1233. /*
  1234. // Check status
  1235. const success = /我们已经记录了本次推荐,感谢您的参与!\s*您每天拥有\s*(\d+)\s*次推荐权利,这是您今天第\s*(\d+)\s*次推荐。/;
  1236. const overflow = /\s*错误原因:对不起,您今天已经用完了推荐的权利!\s*您每天可以推荐\s*(\d+)\s*次。\s*请\s*返\s*回\s*并修正/;
  1237. */
  1238. const b = recommended[strBookID] = recommended[strBookID] || {name: bookName, strID: strBookID, count: 0};
  1239. b.count++;
  1240. AM.finish();
  1241. }
  1242.  
  1243. function allFinish() {
  1244. // Save config
  1245. arConfig.lasttime = getTime('-', false);
  1246. CONFIG.AutoRecommend.saveConfig(arConfig);
  1247.  
  1248. // Soft alert
  1249. let text = [];
  1250. for (const strBookID of Object.keys(recommended)) {
  1251. const book = recommended[strBookID];
  1252. text.push('[{BID}]{BN} 推荐了{C}次'.replaceAll('{C}', book.count).replaceAll('{BID}', book.strID).replaceAll('{BN}', book.name));
  1253. }
  1254. alertify.success(TEXT_ALT_ATRCMMDS_ALL_DONE.replaceAll('{R}', text.join('</br>')));
  1255. }
  1256. }
  1257. }
  1258.  
  1259. // Config Maintainer
  1260. TM.Cleaner = {
  1261. cleanPageStatus: function() {
  1262. const config = CONFIG.BkReviewPrefs.getConfig();
  1263. const history = config.history;
  1264. let count = 0;
  1265. for (const [rid, his] of Object.entries(history)) {
  1266. if (!his.time || (new Date()).getTime() - his.time > 30*1000) {
  1267. delete history[rid];
  1268. count++;
  1269. }
  1270. }
  1271. CONFIG.BkReviewPrefs.saveConfig(config);
  1272. DoLog(count > 0 ? LogLevel.Success : LogLevel.Info, 'Review page status cleaned ({C})'.replace('{C}', count.toString()));
  1273. },
  1274.  
  1275. imagerFix: function() {
  1276. const config = CONFIG.UserGlobalCfg.getConfig();
  1277. const curimager = config.imager;
  1278.  
  1279. // If imager does not exist or imager disabled, change it to default
  1280. if (!DATA_IMAGERS[curimager] || !DATA_IMAGERS[curimager].available) {
  1281. DoLog(LogLevel.Warning, 'Current imager unavailable, changing to default.');
  1282. if (curimager !== DATA_IMAGERS.default && DATA_IMAGERS[DATA_IMAGERS.default].available) {
  1283. // Default available
  1284. config.imager = DATA_IMAGERS.default;
  1285. DoLog(LogLevel.Success, 'Changed to default.');
  1286. } else {
  1287. // Default not available
  1288. DoLog(LogLevel.Warning, 'Default imager unavailable, trying to find another imager for use. ')
  1289. for (const [key, imager] of Object.entries(DATA_IMAGERS)) {
  1290. if (imager.available) {
  1291. config.imager = key;
  1292. DoLog(LogLevel.Success, 'Changed to {K}.'.replace('{K}', key));
  1293. break;
  1294. }
  1295. }
  1296.  
  1297. if (config.imager === curimager) {
  1298. // OMG, There's NO IMAGER AVAILABLE!!
  1299. DoLog(LogLevel.Error, 'OMG, There\'s NO IMAGER AVAILABLE!!');
  1300. }
  1301. }
  1302.  
  1303. CONFIG.UserGlobalCfg.saveConfig(config);
  1304. alertify.warning((config.imager !== curimager ? TEXT_ALT_IMAGER_RESET : TEXT_ALT_IMAGER_NOAVAILBLE).replace('{O}', DATA_IMAGERS[curimager].name).replace('{N}', DATA_IMAGERS[config.imager].name));
  1305. }
  1306. },
  1307. }
  1308.  
  1309. // Script
  1310. TM.Script = {
  1311. // Check & Update to latest version of script
  1312. update: function(force=false) {
  1313. // Check for update once a day
  1314. const scriptID = 416310;
  1315. const config = CONFIG.GlobalConfig.getConfig();
  1316. if (!force && config.scriptUpdate.lasttime === getTime('-', false)) {return false;}
  1317.  
  1318. const GFU = new GreasyForkUpdater();
  1319. alertify.notify(TEXT_ALT_SCRIPT_UPDATE_CHECKING);
  1320. GFU.checkUpdate(scriptID, GM_info.script.version, function(update, updateurl, metaData) {
  1321. if (update) {
  1322. const box = alertify.notify(TEXT_ALT_SCRIPT_UPDATE_GOT.replaceAll('{SN}', metaData.name).replaceAll('{NV}', metaData.version).replaceAll('{CV}', GM_info.script.version));
  1323. const btnInfo = $(box.element, '#script_update_info');
  1324. const btnInstall = $(box.element, '#script_update_install');
  1325. btnInfo.addEventListener('click', show);
  1326. btnInstall.addEventListener('click', install);
  1327. } else {
  1328. alertify.message(TEXT_ALT_SCRIPT_UPDATE_NONE);
  1329. }
  1330. config.scriptUpdate.lasttime = getTime('-', false);
  1331. CONFIG.GlobalConfig.saveConfig(config);
  1332.  
  1333. function install(e) {
  1334. location.href = updateurl;
  1335. }
  1336.  
  1337. function show(e) {
  1338. const info = metaData.updateinfo;
  1339. const box = alertify.confirm(info ? info : TEXT_ALT_SCRIPT_UPDATE_NOINFO, install);
  1340. box.setHeader(TEXT_ALT_SCRIPT_UPDATE_INFO);
  1341. box.set('labels', {ok: TEXT_ALT_SCRIPT_UPDATE_INSTALL, cancel: TEXT_ALT_SCRIPT_UPDATE_CLOSE});
  1342. box.set('overflow', true);
  1343. }
  1344. });
  1345.  
  1346. return true;
  1347. }
  1348. }
  1349.  
  1350. TM.Script.update();
  1351. TM.Cleaner.cleanPageStatus();
  1352. TM.Cleaner.imagerFix();
  1353. TM.UserDetail.refresh();
  1354. TM.AutoRecommend.run();
  1355. }
  1356.  
  1357. // Config Manager
  1358. // use 'new' keyword
  1359. function configManager() {
  1360. const CM = this;
  1361. const [getValue, setValue, deleteValue, listValues] = [
  1362. window.getValue ? window.getValue : GM_getValue,
  1363. window.setValue ? window.setValue : GM_setValue,
  1364. window.deleteValue ? window.deleteValue : GM_deleteValue,
  1365. window.listValues ? window.listValues : GM_listValues,
  1366. ]
  1367.  
  1368. CM.GlobalConfig = {
  1369. saveConfig: function(config) {
  1370. config ? config[KEY_CM_VERSION] = VALUE_CM_VERSION : function() {};
  1371. setValue(KEY_CM, config);
  1372. },
  1373.  
  1374. initConfig: function(save=true, func) {
  1375. let config = {
  1376. users: {},
  1377. scriptUpdate: {
  1378. lasttime: ''
  1379. }
  1380. };
  1381.  
  1382. config = func ? func(config) : config;
  1383. save ? CM.GlobalConfig.saveConfig(config) : function() {};
  1384. return config;
  1385. },
  1386.  
  1387. getConfig: function(init) {
  1388. let config = getValue(KEY_CM, null);
  1389. config = config ? config : (init ? CM.GlobalConfig.initConfig(true, init) : CM.GlobalConfig.initConfig());
  1390. return config;
  1391. },
  1392.  
  1393. // Review config upgrade (Uses GM_functions)
  1394. upgradeConfig: function() {
  1395. // Get version
  1396. const default_self = {}; default_self[KEY_CM_VERSION] = '0.1'; // v0.1 has no self object
  1397. const self = GM_getValue(KEY_CM, default_self);
  1398. const version = self[KEY_CM_VERSION];
  1399.  
  1400. // Upgrade by version
  1401. if (self[KEY_CM_VERSION] === VALUE_CM_VERSION) {DoLog(LogLevel.Info, 'Config Manager self config is in latest version. ');};
  1402. switch(version) {
  1403. case '0.1':
  1404. v01_To_v02();
  1405. v02_To_v03();
  1406. logUpgrade();
  1407. break;
  1408. case '0.2':
  1409. v02_To_v03();
  1410. logUpgrade();
  1411. break;
  1412. }
  1413.  
  1414. // Save to global gm_storage
  1415. self[KEY_CM_VERSION] = VALUE_CM_VERSION;
  1416. setValue(KEY_CM, self);
  1417.  
  1418. function logUpgrade() {
  1419. DoLog(LogLevel.Success, 'Config Manager self config successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', version).replaceAll('{V2}', VALUE_CM_VERSION));
  1420. }
  1421.  
  1422. function v01_To_v02() {
  1423. const props = GM_listValues();
  1424. const userStorage = {};
  1425. for (const prop of props) {
  1426. userStorage[prop] = GM_getValue(prop);
  1427. }
  1428. const userID = getUserID();
  1429. userID ? GM_setValue(userID, userStorage) : GM_setValue('temp', userStorage);
  1430. for (const prop of props) {
  1431. GM_deleteValue(prop);
  1432. }
  1433. }
  1434.  
  1435. function v02_To_v03() {
  1436. self.scriptUpdate = self.scriptUpdate ? self.scriptUpdate : {lasttime: ''};
  1437. }
  1438. },
  1439.  
  1440. // Redirect global gm_storage to user's storage area (Uses GM_functions)
  1441. // callback(key)
  1442. redirectToUser: function (callback) {
  1443. // Get userID from cookies
  1444. const userID = getUserID();
  1445.  
  1446. if (userID) {
  1447. // delete temp data if exist
  1448. GM_deleteValue('temp');
  1449.  
  1450. // Save lastUserID
  1451. const config = CM.GlobalConfig.getConfig();
  1452. config.lastUserID = userID;
  1453. CM.GlobalConfig.saveConfig(config);
  1454.  
  1455. // Redirect to user storage area
  1456. redirectGMStorage(userID);
  1457. DoLog(LogLevel.Info, 'GM_storage redirected to ' + String(userID));
  1458. } else {
  1459. // Redirect to temp storage area before request finish
  1460. const lastUserID = CM.GlobalConfig.getConfig().lastUserID;
  1461. redirectTemp(lastUserID);
  1462.  
  1463. // Request userID
  1464. getMyUserDetail((userDetail)=>{
  1465. const key = userDetail.userDetail.userID;
  1466.  
  1467. // Move temp data to user storage area
  1468. redirectGMStorage();
  1469. const tempStorage = GM_getValue('temp');
  1470. GM_setValue(lastUserID ? lastUserID : key, tempStorage);
  1471. GM_deleteValue('temp');
  1472.  
  1473. // Save lastUserID
  1474. const config = CM.GlobalConfig.getConfig();
  1475. config.lastUserID = key;
  1476. CM.GlobalConfig.saveConfig(config);
  1477.  
  1478. // Redirect to user storage area
  1479. redirectGMStorage(key);
  1480. DoLog(LogLevel.Info, 'GM_storage redirected to ' + String(key));
  1481.  
  1482. // callback
  1483. callback ? callback(key) : function() {};
  1484. })
  1485. }
  1486.  
  1487. // When userID request not finished, use 'temp' as gm_storage key
  1488. function redirectTemp(lastUserID) {
  1489. if (lastUserID) {
  1490. // Copy config of the user we use last time to 'temp' storage area
  1491. const lastUser = GM_getValue(lastUserID, {});
  1492. GM_setValue('temp', lastUser);
  1493. }
  1494. redirectGMStorage('temp');
  1495. DoLog(LogLevel.Info, 'GM_storage redirected to temp');
  1496. }
  1497. }
  1498. }
  1499.  
  1500. CM.GlobalConfig.upgradeConfig();
  1501. CM.GlobalConfig.redirectToUser();
  1502.  
  1503. CM.AutoRecommend = {
  1504. saveConfig: function(config) {
  1505. config ? config[KEY_ATRCMMDS_VERSION] = VALUE_ATRCMMDS_VERSION : function() {};
  1506. GM_setValue(KEY_ATRCMMDS, config);
  1507. },
  1508.  
  1509. initConfig: function(save=true, func) {
  1510. let config = {};
  1511. config[KEY_ATRCMMDS_VERSION] = VALUE_ATRCMMDS_VERSION;
  1512. config.allCount = 0;
  1513. config.books = {};
  1514. config.auto = true;
  1515.  
  1516. config = func ? func(config) : config;
  1517. save ? CM.AutoRecommend.saveConfig(config) : function() {};
  1518. return config;
  1519. },
  1520.  
  1521. getConfig: function(init) {
  1522. let config = GM_getValue(KEY_ATRCMMDS, null);
  1523. config = config ? config : (init ? CM.AutoRecommend.initConfig(true, init) : CM.AutoRecommend.initConfig());
  1524. return config;
  1525. },
  1526.  
  1527. // Auto-recommend config upgrade
  1528. upgradeConfig: function() {
  1529. // Get config
  1530. const config = CM.AutoRecommend.getConfig();
  1531.  
  1532. // if not inited
  1533. if (!config) {return;};
  1534.  
  1535. switch (config[KEY_ATRCMMDS_VERSION]) {
  1536. case '0.1':
  1537. config.auto = true;
  1538. logUpgrade();
  1539. break;
  1540. case VALUE_ATRCMMDS_VERSION:
  1541. DoLog(LogLevel.Info, 'Auto-recommend config is in latest version. ');
  1542. break;
  1543. default:
  1544. DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for Auto-recommend. '.replace('{V}', config[KEY_ATRCMMDS_VERSION]));
  1545. }
  1546.  
  1547. // Save to gm_storage
  1548. CM.AutoRecommend.saveConfig(config);
  1549.  
  1550. function logUpgrade() {
  1551. DoLog(LogLevel.Success, 'Auto-recommend config successfully upgraded From v{V1} to {V2}. '.replaceAll('{V1}', config[KEY_ATRCMMDS_VERSION]).replaceAll('{V2}', VALUE_ATRCMMDS_VERSION));
  1552. }
  1553. }
  1554. }
  1555.  
  1556. CM.commentDrafts = {
  1557. saveConfig: function(config) {
  1558. config ? config[KEY_DRAFT_VERSION] = VALUE_DRAFT_VERSION : function() {};
  1559. GM_setValue(KEY_DRAFT_DRAFTS, config);
  1560. },
  1561.  
  1562. initConfig: function(save=true, func) {
  1563. let config = {};
  1564.  
  1565. config = func ? func(config) : config;
  1566. save ? CM.commentDrafts.saveConfig(config) : function() {};
  1567. return config;
  1568. },
  1569.  
  1570. getConfig: function(init) {
  1571. let config = GM_getValue(KEY_DRAFT_DRAFTS, null);
  1572. config = config ? config : (init ? CM.commentDrafts.initConfig(true, init) : CM.commentDrafts.initConfig());
  1573. return config;
  1574. },
  1575.  
  1576. // Comment-drafts config upgrade
  1577. upgradeConfig: function() {
  1578. // Get config
  1579. let config = CM.commentDrafts.getConfig();
  1580.  
  1581. // if not inited
  1582. if (!config) {return;};
  1583.  
  1584. switch (config[KEY_DRAFT_VERSION]) {
  1585. case '0.1':
  1586. case undefined:
  1587. v01_To_v02();
  1588. logUpgrade();
  1589. break;
  1590. case VALUE_DRAFT_VERSION:
  1591. DoLog(LogLevel.Info, 'comment-drafts config is in latest version. ');
  1592. break;
  1593. default:
  1594. DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for comment-drafts. '.replace('{V}', config[KEY_DRAFT_VERSION]));
  1595. }
  1596.  
  1597. // Save to gm_storage
  1598. CM.commentDrafts.saveConfig(config);
  1599.  
  1600. function logUpgrade() {
  1601. DoLog(LogLevel.Success, 'comment-drafts config successfully upgraded From v{V1} to {V2}. '.replaceAll('{V1}', config[KEY_DRAFT_VERSION]).replaceAll('{V2}', VALUE_DRAFT_VERSION));
  1602. }
  1603.  
  1604. function v01_To_v02() {
  1605. // Fix bug caused bookcase's config overwriting comment-drafts' config
  1606. if (config instanceof Array) {
  1607. config = {};
  1608. }
  1609. }
  1610. }
  1611. }
  1612.  
  1613. CM.bookcasePrefs = {
  1614. saveConfig: function(config) {
  1615. config ? config[KEY_BOOKCASE_VERSION] = VALUE_BOOKCASE_VERSION : function() {};
  1616. GM_setValue(KEY_BOOKCASES, config);
  1617. },
  1618.  
  1619. initConfig: function(save=true, func) {
  1620. let config = {
  1621. bookcases: [],
  1622. laterbooks: {
  1623. sortby: 'addTime_old2new',
  1624. books: {}
  1625. }
  1626. };
  1627.  
  1628. config = func ? func(config) : config;
  1629. save ? CM.bookcasePrefs.saveConfig(config) : function() {};
  1630. return config;
  1631. },
  1632.  
  1633. getConfig: function(init) {
  1634. let config = GM_getValue(KEY_BOOKCASES, null);
  1635. config = config ? config : (init ? CM.bookcasePrefs.initConfig(true, init) : CM.bookcasePrefs.initConfig());
  1636. return config;
  1637. },
  1638.  
  1639. // Bookcase config upgrade
  1640. upgradeConfig: function() {
  1641. // Get config
  1642. let config = CM.bookcasePrefs.getConfig();
  1643.  
  1644. // if not inited
  1645. if (!config) {return;};
  1646.  
  1647. // Original version
  1648. let V = config && config[KEY_BOOKCASE_VERSION] ? config[KEY_BOOKCASE_VERSION] : '0';
  1649.  
  1650. switch (V) {
  1651. case '0.1':
  1652. case undefined:
  1653. case '0':
  1654. v01_To_v02();
  1655. v02_To_v03();
  1656. v03_To_v04();
  1657. v04_To_v05();
  1658. logUpgrade();
  1659. break;
  1660. case '0.2':
  1661. v02_To_v03();
  1662. v03_To_v04();
  1663. v04_To_v05();
  1664. logUpgrade();
  1665. break;
  1666. case '0.3':
  1667. v03_To_v04();
  1668. v04_To_v05();
  1669. logUpgrade();
  1670. break;
  1671. case '0.4':
  1672. v04_To_v05();
  1673. logUpgrade();
  1674. break;
  1675. case VALUE_BOOKCASE_VERSION:
  1676. DoLog(LogLevel.Info, 'Bookcase config is in latest version. ');
  1677. break;
  1678. default:
  1679. DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for Bookcase. '.replace('{V}', config[KEY_BOOKCASE_VERSION]));
  1680. }
  1681.  
  1682. // Save to gm_storage
  1683. CM.bookcasePrefs.saveConfig(config);
  1684.  
  1685. function logUpgrade() {
  1686. DoLog(LogLevel.Success, 'Bookcase config successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', V).replaceAll('{V2}', VALUE_BOOKCASE_VERSION));
  1687. }
  1688.  
  1689. function v01_To_v02() {
  1690. // Clear useless key added falsely
  1691. delete config.bbcode;
  1692.  
  1693. // Convert array to an object
  1694. if (Array.isArray(config)) {
  1695. const newConfig = {bookcases: []};
  1696. for (let i = 0; i < config.length; i++) {
  1697. newConfig.bookcases[i] = config[i];
  1698. }
  1699. config = newConfig;
  1700. }
  1701. }
  1702.  
  1703. function v02_To_v03() {
  1704. // Fix bug caused config.bookcases equals to []
  1705. if (config && config.bookcases && config.bookcases.length === 0) {
  1706. config = CM.bookcasePrefs.initConfig();
  1707. }
  1708. }
  1709.  
  1710. function v03_To_v04() {
  1711. if (config.laterbooks) {return false;}
  1712. config.laterbooks = {
  1713. sortby: 'addTime_old2new',
  1714. books: {}
  1715. };
  1716. }
  1717.  
  1718. function v04_To_v05() {
  1719. const books = config.laterbooks.books;
  1720. const sorts = [];
  1721. let err = false;
  1722. for (const book of Object.values(books)) {
  1723. if (sorts.includes(book.sort)) {
  1724. err = true;
  1725. break;
  1726. }
  1727. sorts.push(book.sort);
  1728. }
  1729. Math.max.apply(null, sorts) > books.length && (err = true);
  1730. if (err) {
  1731. let i = 0;
  1732. for (const book of Object.values(books)) {
  1733. book.sort = ++i;
  1734. }
  1735. alertify.notify(TEXT_ALT_BOOKCASE_AFTERBOOKS_V4BUG, '', 0);
  1736. }
  1737. }
  1738. }
  1739. }
  1740.  
  1741. CM.userDtlePrefs = {
  1742. saveConfig: function(config) {
  1743. config ? config[KEY_USRDETAIL_VERSION] = VALUE_USRDETAIL_VERSION : function() {};
  1744. GM_setValue(KEY_USRDETAIL, config);
  1745. },
  1746.  
  1747. initConfig: function(save=true, func) {
  1748. let config = {userDetail: null};
  1749.  
  1750. config = func ? func(config) : config;
  1751. save ? CM.userDtlePrefs.saveConfig(config) : function() {};
  1752. return config;
  1753. },
  1754.  
  1755. getConfig: function(init) {
  1756. let config = GM_getValue(KEY_USRDETAIL, null);
  1757. config = config ? config : (init ? CM.userDtlePrefs.initConfig(true, init) : CM.userDtlePrefs.initConfig());
  1758. return config;
  1759. },
  1760.  
  1761. // userDetail config upgrade
  1762. upgradeConfig: function() {
  1763. // Get config
  1764. const config = CM.userDtlePrefs.getConfig();
  1765.  
  1766. // if not inited
  1767. if (!config) {return;};
  1768.  
  1769. // Original version
  1770. let V = config && config[KEY_BOOKCASE_VERSION] ? config[KEY_BOOKCASE_VERSION] : '0';
  1771.  
  1772. switch (V) {
  1773. case '0.1':
  1774. refreshMyUserDetail(logUpgrade);
  1775. break;
  1776. case VALUE_USRDETAIL_VERSION:
  1777. DoLog(LogLevel.Info, 'User-detail config is in latest version. ');
  1778. break;
  1779. default:
  1780. DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for User-detail. '.replace('{V}', V));
  1781. }
  1782.  
  1783. // Save to gm_storage
  1784. CM.userDtlePrefs.saveConfig(config);
  1785.  
  1786. function logUpgrade() {
  1787. DoLog(LogLevel.Success, 'User-detail config successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', V).replaceAll('{V2}', VALUE_USRDETAIL_VERSION));
  1788. }
  1789. }
  1790. }
  1791.  
  1792. CM.BkReviewPrefs = {
  1793. saveConfig: function(config) {
  1794. config ? config[KEY_REVIEW_VERSION] = VALUE_REVIEW_VERSION : function() {};
  1795. GM_setValue(KEY_REVIEW_PREFS, config);
  1796. },
  1797.  
  1798. initConfig: function(save=true, func) {
  1799. let config = {
  1800. bbcode: false,
  1801. autoRefresh: false,
  1802. beautiful: true,
  1803. backgroundImage: 'https://img12.360buyimg.com/ddimg/jfs/t1/197476/22/6462/3478996/613227a8E03e8ffc3/99970183ddb9f896.jpg',
  1804. favorites: {
  1805. 228884: {
  1806. name: '文库导航姬',
  1807. href: `https://${location.host}/modules/article/reviewshow.php?rid=228884`,
  1808. tiptitle: '梦想成为书评区大水怪的可以来康康'
  1809. }
  1810. },
  1811. favorlast: false,
  1812. history: {}
  1813. };
  1814.  
  1815. config = func ? func(config) : config;
  1816. save ? CM.BkReviewPrefs.saveConfig(config) : function() {};
  1817. return config;
  1818. },
  1819.  
  1820. getConfig: function(init) {
  1821. let config = GM_getValue(KEY_REVIEW_PREFS, null);
  1822. config = config ? config : (init ? CM.BkReviewPrefs.initConfig(true, init) : CM.BkReviewPrefs.initConfig());
  1823. return config;
  1824. },
  1825.  
  1826. // Review config upgrade
  1827. upgradeConfig: function() {
  1828. // Get config
  1829. const config = CM.BkReviewPrefs.getConfig();
  1830.  
  1831. // if not inited
  1832. if (!config) {return;};
  1833.  
  1834. switch (config[KEY_REVIEW_VERSION]) {
  1835. case '0.1':
  1836. v01_To_v02();
  1837. v02_To_v03();
  1838. v03_To_v04();
  1839. v04_To_v05();
  1840. v05_To_v06();
  1841. v06_To_v07();
  1842. v07_To_v08();
  1843. v08_To_v09();
  1844. logUpgrade();
  1845. break;
  1846. case '0.2':
  1847. v02_To_v03();
  1848. v03_To_v04();
  1849. v04_To_v05();
  1850. v05_To_v06();
  1851. v06_To_v07();
  1852. v07_To_v08();
  1853. v08_To_v09();
  1854. logUpgrade();
  1855. break;
  1856. case '0.3':
  1857. v03_To_v04();
  1858. v04_To_v05();
  1859. v05_To_v06();
  1860. v06_To_v07();
  1861. v07_To_v08();
  1862. v08_To_v09();
  1863. logUpgrade();
  1864. break;
  1865. case '0.4':
  1866. v04_To_v05();
  1867. v05_To_v06();
  1868. v06_To_v07();
  1869. v07_To_v08();
  1870. v08_To_v09();
  1871. logUpgrade();
  1872. break;
  1873. case '0.5':
  1874. v05_To_v06();
  1875. v06_To_v07();
  1876. v07_To_v08();
  1877. v08_To_v09();
  1878. logUpgrade();
  1879. break;
  1880. case '0.6':
  1881. v06_To_v07();
  1882. v07_To_v08();
  1883. v08_To_v09();
  1884. logUpgrade();
  1885. break;
  1886. case '0.7':
  1887. v07_To_v08();
  1888. v08_To_v09();
  1889. logUpgrade();
  1890. break;
  1891. case '0.8':
  1892. v08_To_v09();
  1893. logUpgrade();
  1894. break;
  1895. case VALUE_REVIEW_VERSION:
  1896. DoLog(LogLevel.Info, 'Review config is in latest version. ');
  1897. break;
  1898. default:
  1899. DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for Review. '.replace('{V}', config[KEY_REVIEW_VERSION]));
  1900. }
  1901.  
  1902. // Save to gm_storage
  1903. CM.BkReviewPrefs.saveConfig(config);
  1904.  
  1905. function logUpgrade() {
  1906. DoLog(LogLevel.Success, 'Review config successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', config[KEY_REVIEW_VERSION]).replaceAll('{V2}', VALUE_REVIEW_VERSION));
  1907. }
  1908.  
  1909. function v01_To_v02() {
  1910. config.autoRefresh = false;
  1911. delete config.downloading;
  1912. }
  1913.  
  1914. function v02_To_v03() {
  1915. config.favorites = {
  1916. 228884: {
  1917. name: '文库导航姬',
  1918. href: `https://${location.host}/modules/article/reviewshow.php?rid=228884`,
  1919. tiptitle: '梦想成为书评区大水怪的可以来康康'
  1920. }
  1921. }
  1922. }
  1923.  
  1924. function v03_To_v04() {
  1925. if (config.favorites) {return;};
  1926. config.favorites = {
  1927. 228884: {
  1928. name: '文库导航姬',
  1929. href: `https://${location.host}/modules/article/reviewshow.php?rid=228884`,
  1930. tiptitle: '梦想成为书评区大水怪的可以来康康'
  1931. }
  1932. };
  1933. }
  1934.  
  1935. function v04_To_v05() {
  1936. if (config.history) {return;};
  1937. config.history = {};
  1938. }
  1939.  
  1940. function v05_To_v06() {
  1941. if (config.beautiful !== undefined) {return;};
  1942. config.beautiful = true;
  1943. config.backgroundImage = 'https://img12.360buyimg.com/ddimg/jfs/t1/197476/22/6462/3478996/613227a8E03e8ffc3/99970183ddb9f896.jpg';
  1944. }
  1945.  
  1946. function v06_To_v07() {
  1947. // Move CM.BkReviewPrefs.upgradeConfig.beautiful to CM.BeautifierCfg
  1948. if (config.beautiful === undefined) {return;};
  1949. const beautifierConfig = {
  1950. reviewshow: {
  1951. beautiful: config.beautiful,
  1952. backgroundImage: config.backgroundImage
  1953. }
  1954. }
  1955. CM.BeautifierCfg.saveConfig(beautifierConfig);
  1956.  
  1957. delete config.beautiful;
  1958. delete config.backgroundImage;
  1959. }
  1960.  
  1961. function v07_To_v08() {
  1962. // Move CM.BkReviewPrefs.upgradeConfig.beautiful to CM.BeautifierCfg
  1963. if (config.favorlast !== undefined) {return;};
  1964. config.favorlast = false;
  1965. for (const [rid, favorite] of Object.entries(config.favorites)) {
  1966. config.favorites[rid] = {
  1967. name: favorite.name,
  1968. href: favorite.href.replace(/&page=1$/, ''),
  1969. tiptitle: favorite.tiptitle
  1970. };
  1971. }
  1972. }
  1973.  
  1974. function v08_To_v09() {
  1975. // Fill all favorite bookreviews' tiptitle using null for those don't have
  1976. config.favorlast = false;
  1977. for (const [rid, favorite] of Object.entries(config.favorites)) {
  1978. !favorite.tiptitle && (favorite.tiptitle = null);
  1979. }
  1980. }
  1981. }
  1982. }
  1983.  
  1984. CM.BeautifierCfg = {
  1985. saveConfig: function(config) {
  1986. config ? config[KEY_BEAUTIFIER_VERSION] = VALUE_BEAUTIFIER_VERSION : function() {};
  1987. GM_setValue(KEY_BEAUTIFIER, config);
  1988. },
  1989.  
  1990. initConfig: function(save=true, func) {
  1991. let config = {
  1992. upload: false,
  1993. reviewshow: {
  1994. beautiful: true,
  1995. },
  1996. novel: {
  1997. beautiful: true,
  1998. },
  1999. common: {
  2000. beautiful: false,
  2001. },
  2002. backgroundImage: 'https://img12.360buyimg.com/ddimg/jfs/t1/197476/22/6462/3478996/613227a8E03e8ffc3/99970183ddb9f896.jpg',
  2003. bgiName: '默认背景图片 - Pixiv ID: 88913164',
  2004. textScale: 100
  2005. };
  2006.  
  2007. config = func ? func(config) : config;
  2008. save ? CM.BeautifierCfg.saveConfig(config) : function() {};
  2009. return config;
  2010. },
  2011.  
  2012. getConfig: function(init) {
  2013. let config = GM_getValue(KEY_BEAUTIFIER, null);
  2014. config = config ? config : (init ? CM.BeautifierCfg.initConfig(true, init) : CM.BeautifierCfg.initConfig());
  2015. return config;
  2016. },
  2017.  
  2018. // Beautifier config upgrade
  2019. upgradeConfig: function() {
  2020. // Get config
  2021. const config = CM.BeautifierCfg.getConfig();
  2022.  
  2023. // if not inited
  2024. if (!config) {return;};
  2025.  
  2026. switch (config[KEY_BEAUTIFIER_VERSION]) {
  2027. /*case '0.1':
  2028. v01_To_v02();
  2029. break;*/
  2030. case VALUE_BEAUTIFIER_VERSION:
  2031. DoLog(LogLevel.Info, 'Beautifier config is in latest version. ');
  2032. break;
  2033. case '0.1':
  2034. v01_To_v02();
  2035. v02_To_v03();
  2036. v03_To_v04();
  2037. v04_To_v05();
  2038. v05_To_v06();
  2039. v06_To_v07();
  2040. v07_To_v08();
  2041. v08_To_v09();
  2042. logUpgrade();
  2043. break;
  2044. case '0.2':
  2045. v02_To_v03();
  2046. v03_To_v04();
  2047. v04_To_v05();
  2048. v05_To_v06();
  2049. v06_To_v07();
  2050. v07_To_v08();
  2051. v08_To_v09();
  2052. logUpgrade();
  2053. break;
  2054. case '0.3':
  2055. v03_To_v04();
  2056. v04_To_v05();
  2057. v05_To_v06();
  2058. v06_To_v07();
  2059. v07_To_v08();
  2060. v08_To_v09();
  2061. logUpgrade();
  2062. break;
  2063. case '0.4':
  2064. v04_To_v05();
  2065. v05_To_v06();
  2066. v06_To_v07();
  2067. v07_To_v08();
  2068. v08_To_v09();
  2069. logUpgrade();
  2070. break;
  2071. case '0.5':
  2072. v05_To_v06();
  2073. v06_To_v07();
  2074. v07_To_v08();
  2075. v08_To_v09();
  2076. logUpgrade();
  2077. break;
  2078. case '0.6':
  2079. v06_To_v07();
  2080. v07_To_v08();
  2081. v08_To_v09();
  2082. logUpgrade();
  2083. break;
  2084. case '0.7':
  2085. v07_To_v08();
  2086. v08_To_v09();
  2087. logUpgrade();
  2088. break;
  2089. case '0.8':
  2090. v08_To_v09();
  2091. logUpgrade();
  2092. break;
  2093. default:
  2094. DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for Beautifier. '.replace('{V}', config[KEY_BEAUTIFIER_VERSION]));
  2095. }
  2096.  
  2097. // Save to gm_storage
  2098. CM.BeautifierCfg.saveConfig(config);
  2099.  
  2100. function logUpgrade() {
  2101. DoLog(LogLevel.Success, 'Beautifier config successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', config[KEY_BEAUTIFIER_VERSION]).replaceAll('{V2}', VALUE_BEAUTIFIER_VERSION));
  2102. }
  2103.  
  2104. function v01_To_v02() {
  2105. if (config.upload !== undefined) {return false;};
  2106. config.upload = false;
  2107. }
  2108.  
  2109. function v02_To_v03() {
  2110. if (config.reviewshow.bgiName !== undefined) {return false;};
  2111. config.reviewshow.bgiName = 'image.jpeg';
  2112. }
  2113.  
  2114. function v03_To_v04() {
  2115. if (config.textScale !== undefined) {return false;};
  2116. config.textScale = 100;
  2117. }
  2118.  
  2119. function v04_To_v05() {
  2120. if (config.novel !== undefined) {return false;};
  2121. config.novel = {
  2122. beautiful: true
  2123. };
  2124. }
  2125.  
  2126. function v05_To_v06() {
  2127. if (!config.textScale) {config.textScale = 100;};
  2128. if (!config.novel) {config.novel = {beautiful: true};};
  2129. }
  2130.  
  2131. function v06_To_v07() {
  2132. config.backgroundImage = config.reviewshow.backgroundImage;
  2133. config.bgiName = config.reviewshow.bgiName;
  2134. delete config.reviewshow.backgroundImage;
  2135. delete config.reviewshow.bgiName;
  2136. }
  2137.  
  2138. function v07_To_v08() {
  2139. if (config.common) {return false;}
  2140. config.common = {
  2141. beautiful: false
  2142. };
  2143. }
  2144.  
  2145. function v08_To_v09() {
  2146. if (config.common) {return false;}
  2147. config.common = {
  2148. beautiful: false
  2149. };
  2150. }
  2151. }
  2152. }
  2153.  
  2154. CM.RemarksConfig = {
  2155. saveConfig: function(config) {
  2156. config ? config[KEY_REMARKS_VERSION] = VALUE_REMARKS_VERSION : function() {};
  2157. GM_setValue(KEY_REMARKS, config);
  2158. },
  2159.  
  2160. initConfig: function(save=true, func) {
  2161. let config = {
  2162. user: {}
  2163. };
  2164.  
  2165. config = func ? func(config) : config;
  2166. save ? CM.RemarksConfig.saveConfig(config) : function() {};
  2167. return config;
  2168. },
  2169.  
  2170. getConfig: function(init) {
  2171. let config = GM_getValue(KEY_REMARKS, null);
  2172. config = config ? config : (init ? CM.RemarksConfig.initConfig(true, init) : CM.RemarksConfig.initConfig());
  2173. return config;
  2174. },
  2175.  
  2176. // Beautifier config upgrade
  2177. upgradeConfig: function() {
  2178. // Get config
  2179. const config = CM.RemarksConfig.getConfig();
  2180.  
  2181. // if not inited
  2182. if (!config) {return;};
  2183.  
  2184. switch (config[KEY_REMARKS_VERSION]) {
  2185. //case '0.1':
  2186. // v01_To_v02();
  2187. // logUpgrade();
  2188. // break;
  2189. case VALUE_REMARKS_VERSION:
  2190. DoLog(LogLevel.Info, 'RemarksConfig config is in latest version. ');
  2191. break;
  2192. default:
  2193. DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for RemarksConfig. '.replace('{V}', config[KEY_REMARKS_VERSION]));
  2194. }
  2195.  
  2196. // Save to gm_storage
  2197. CM.RemarksConfig.saveConfig(config);
  2198.  
  2199. function logUpgrade() {
  2200. DoLog(LogLevel.Success, 'RemarksConfig config successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', config[KEY_REMARKS_VERSION]).replaceAll('{V2}', VALUE_REMARKS_VERSION));
  2201. }
  2202.  
  2203. //function v#BEFORE_To_v#AFTER() {
  2204. // if (config.#NEWPROP !== undefined) {return false;};
  2205. // config.#NEWPROP = #DEFAULTVALUE;
  2206. //}
  2207. }
  2208. }
  2209.  
  2210. CM.UserGlobalCfg = {
  2211. saveConfig: function(config) {
  2212. config ? config[KEY_USERGLOBAL_VERSION] = VALUE_USERGLOBAL_VERSION : function() {};
  2213. GM_setValue(KEY_USERGLOBAL, config);
  2214. },
  2215.  
  2216. initConfig: function(save=true, func) {
  2217. let config = {
  2218. imager: DATA_IMAGERS.default
  2219. };
  2220.  
  2221. config = func ? func(config) : config;
  2222. save ? CM.UserGlobalCfg.saveConfig(config) : function() {};
  2223. return config;
  2224. },
  2225.  
  2226. getConfig: function(init) {
  2227. let config = GM_getValue(KEY_USERGLOBAL, null);
  2228. config = config ? config : (init ? CM.UserGlobalCfg.initConfig(true, init) : CM.UserGlobalCfg.initConfig());
  2229. return config;
  2230. },
  2231.  
  2232. // Beautifier config upgrade
  2233. upgradeConfig: function() {
  2234. // Get config
  2235. const config = CM.UserGlobalCfg.getConfig();
  2236.  
  2237. // if not inited
  2238. if (!config) {return;};
  2239.  
  2240. switch (config[KEY_USERGLOBAL_VERSION]) {
  2241. //case '0.1':
  2242. // v01_To_v02();
  2243. // logUpgrade();
  2244. // break;
  2245. case VALUE_USERGLOBAL_VERSION:
  2246. DoLog(LogLevel.Info, 'UserGlobal config is in latest version. ');
  2247. break;
  2248. default:
  2249. DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for UserGlobalCfg. '.replace('{V}', config[KEY_USERGLOBAL_VERSION]));
  2250. }
  2251.  
  2252. // Save to gm_storage
  2253. CM.UserGlobalCfg.saveConfig(config);
  2254.  
  2255. function logUpgrade() {
  2256. DoLog(LogLevel.Success, 'UserGlobal config successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', config[KEY_USERGLOBAL_VERSION]).replaceAll('{V2}', VALUE_USERGLOBAL_VERSION));
  2257. }
  2258.  
  2259. //function v#BEFORE_To_v#AFTER() {
  2260. // if (config.#NEWPROP !== undefined) {return false;};
  2261. // config.#NEWPROP = #DEFAULTVALUE;
  2262. //}
  2263. }
  2264. }
  2265.  
  2266. // New Config Item Template
  2267. /*CM.#NEWCONFIGNAME = {
  2268. saveConfig: function(config) {
  2269. config ? config[#KEY_NEWCONFIG_VERSION] = #VALUE_NEWCONFIG_VERSION : function() {};
  2270. GM_setValue(#KEY_NEWCONFIG, config);
  2271. },
  2272.  
  2273. initConfig: function(save=true, func) {
  2274. let config = {
  2275. #key: #value,
  2276. #key: #value
  2277. };
  2278.  
  2279. config = func ? func(config) : config;
  2280. save ? CM.#NEWCONFIGNAME.saveConfig(config) : function() {};
  2281. return config;
  2282. },
  2283.  
  2284. getConfig: function(init) {
  2285. let config = GM_getValue(#KEY_NEWCONFIG, null);
  2286. config = config ? config : (init ? CM.#NEWCONFIGNAME.initConfig(true, init) : CM.#NEWCONFIGNAME.initConfig());
  2287. return config;
  2288. },
  2289.  
  2290. // Beautifier config upgrade
  2291. upgradeConfig: function() {
  2292. // Get config
  2293. const config = CM.#NEWCONFIGNAME.getConfig();
  2294.  
  2295. // if not inited
  2296. if (!config) {return;};
  2297.  
  2298. switch (config[#KEY_NEWCONFIG_VERSION]) {
  2299. //case '0.1':
  2300. // v01_To_v02();
  2301. // logUpgrade();
  2302. // break;
  2303. case #VALUE_NEWCONFIG_VERSION:
  2304. DoLog(LogLevel.Info, '#NEWCONFIGNAME config is in latest version. ');
  2305. break;
  2306. default:
  2307. DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for #NEWCONFIGNAME. '.replace('{V}', config[#KEY_NEWCONFIG_VERSION]));
  2308. }
  2309.  
  2310. // Save to gm_storage
  2311. CM.#NEWCONFIGNAME.saveConfig(config);
  2312.  
  2313. function logUpgrade() {
  2314. DoLog(LogLevel.Success, '#NEWCONFIGNAME config successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', config[#KEY_NEWCONFIG_VERSION]).replaceAll('{V2}', #VALUE_NEWCONFIG_VERSION));
  2315. }
  2316.  
  2317. //function v#BEFORE_To_v#AFTER() {
  2318. // if (config.#NEWPROP !== undefined) {return false;};
  2319. // config.#NEWPROP = #DEFAULTVALUE;
  2320. //}
  2321. }
  2322. }*/
  2323.  
  2324. CM.AutoRecommend.upgradeConfig();
  2325. CM.commentDrafts.upgradeConfig();
  2326. CM.bookcasePrefs.upgradeConfig();
  2327. CM.userDtlePrefs.upgradeConfig();
  2328. CM.BkReviewPrefs.upgradeConfig();
  2329. CM.BeautifierCfg.upgradeConfig();
  2330. CM.RemarksConfig.upgradeConfig();
  2331. CM.UserGlobalCfg.upgradeConfig();
  2332. //CM.#NEWCONFIGNAME.upgradeConfig();
  2333. }
  2334.  
  2335. // Beautifier for all wenku pages
  2336. function commonBeautify(API) {
  2337. // No beautifier on exluded pages
  2338. const excludes = ['novel']
  2339. if (excludes.includes(API)) {return false;}
  2340.  
  2341. // No beatifier if user does not want
  2342. if (!CONFIG.BeautifierCfg.getConfig().common.beautiful) {return false;}
  2343.  
  2344. const img = $CrE('img');
  2345. img.src = CONFIG.BeautifierCfg.getConfig().backgroundImage;
  2346. img.classList.add('plus_cbty_image');
  2347. document.body.appendChild(img);
  2348.  
  2349. const cover = $CrE('div');
  2350. cover.classList.add('plus_cbty_cover');
  2351. document.body.appendChild(cover);
  2352.  
  2353. document.body.classList.add('plus_cbty');
  2354. addStyle(CSS_COMMONBEAUTIFIER, 'plus_commonbeautifier')
  2355. return true;
  2356. }
  2357.  
  2358. // Book page add-on
  2359. function pageBook() {
  2360. // Resource
  2361. const pageResource = {
  2362. elements: {},
  2363. info: {}
  2364. }
  2365. collectPageResources();
  2366. DoLog(LogLevel.Info, pageResource, true)
  2367.  
  2368. // Provide meta info copy
  2369. metaCopy();
  2370.  
  2371. // Provide read-later button
  2372. laterReads();
  2373.  
  2374. // Provide txtfull download for copyright book
  2375. enableDownload();
  2376.  
  2377. // Provide images download
  2378. imagesDownload();
  2379.  
  2380. // Provide tag search
  2381. tagOption();
  2382.  
  2383. // Ctrl+Enter comment submit
  2384. areaReply();
  2385.  
  2386. // Get page resources
  2387. function collectPageResources() {
  2388. collectElements();
  2389. collectInfos();
  2390.  
  2391. function collectElements() {
  2392. const elements = pageResource.elements;
  2393. elements.content = $('#content');
  2394. elements.bookMain = $(elements.content, 'div');
  2395. elements.header = $(elements.content, 'div>table');
  2396. elements.titleContainer = $(elements.header, 'table td>span');
  2397. elements.bookName = $(elements.header, 'b');
  2398. elements.recommend = $(elements.content, `a[href^="https://${location.host}/modules/article/uservote.php"]`);
  2399. elements.metaContainer = $(elements.header, 'tr+tr');
  2400. elements.metas = $All(elements.metaContainer, 'td');
  2401. elements.info = $(elements.bookMain, 'div+table');
  2402. elements.cover = $(elements.info, 'img');
  2403. elements.infoText = $(elements.info, 'td+td');
  2404. elements.notice = $All(elements.infoText, 'span.hottext>b');
  2405. elements.tags = elements.notice.length > 1 ? elements.notice[0] : null;
  2406. elements.notice = elements.notice[elements.notice.length-1];
  2407. elements.introduce = $All(elements.infoText, 'span');
  2408. elements.introduce = elements.introduce[elements.introduce.length-1];
  2409. elements.downloadContainer = $(pageResource.elements.bookMain, 'div>fieldset');
  2410. elements.downloadPanel = elements.downloadContainer ? elements.downloadContainer.parentElement : null;
  2411. }
  2412.  
  2413. function collectInfos() {
  2414. const info = pageResource.info;
  2415. const elements = pageResource.elements;
  2416. info.bookName = elements.bookName.innerText;
  2417. info.BID = Number(getUrlArgv('id') || location.href.match(/book\/(\d+).htm/)[1]);
  2418. info.metas = []; elements.metas.forEach(function(meta){this.push(getKeyValue(meta.innerText));}, info.metas);
  2419. info.notice = elements.notice.innerText;
  2420. info.tags = elements.tags ? getKeyValue(elements.tags.innerText).VALUE.split(' ') : null;
  2421. info.introduce = elements.introduce.innerText;
  2422. info.cover = elements.cover.src;
  2423. info.dlEnabled = $(elements.content, 'legend>b');
  2424. info.dlEnabled = info.dlEnabled ? info.dlEnabled.innerText : false;
  2425. info.dlEnabled = info.dlEnabled ? (info.dlEnabled.indexOf('TXT') !== -1 && info.dlEnabled.indexOf('UMD') !== -1 && info.dlEnabled.indexOf('JAR') !== -1) : false;
  2426. }
  2427. }
  2428.  
  2429. // Copy meta info
  2430. function metaCopy() {
  2431. let tip = TEXT_TIP_COPY;
  2432. for (let i = -1; i < pageResource.elements.metas.length; i++) {
  2433. const meta = i !== -1 ? pageResource.elements.metas[i] : pageResource.elements.bookName;
  2434. const info = i !== -1 ? pageResource.info.metas[i] : pageResource.info.bookName;
  2435. const value = i !== -1 ? info.VALUE : info;
  2436. meta.innerHTML += HTML_BOOK_COPY;
  2437. const copyBtn = $(meta, '.'+CLASSNAME_BUTTON);
  2438. copyBtn.addEventListener('click', function() {
  2439. copyText(value);
  2440. showtip(TEXT_TIP_COPIED);
  2441. alertify.message(TEXT_ALT_META_COPIED.replaceAll('{M}', value));
  2442. });
  2443.  
  2444. settip(copyBtn, TEXT_TIP_COPY);
  2445. }
  2446. }
  2447.  
  2448. // Add to later-reads
  2449. function laterReads() {
  2450. // Make button
  2451. let btn = installBtn(makeBtn(inAfterbooks() ? 'remove' : 'add'));
  2452.  
  2453. // Update book info if in list
  2454. inAfterbooks() && add(false);
  2455.  
  2456. function add(alt=true) {
  2457. // Add to config
  2458. const config = CONFIG.bookcasePrefs.getConfig();
  2459. config.laterbooks.books[pageResource.info.BID] = {
  2460. sort: Object.keys(config.laterbooks.books).length + 1,
  2461. addTime: new Date().getTime(),
  2462. name: pageResource.info.bookName,
  2463. aid: pageResource.info.BID,
  2464. metas: pageResource.info.metas,
  2465. tags: pageResource.info.tags,
  2466. introduce: pageResource.info.introduce,
  2467. cover: pageResource.info.cover
  2468. };
  2469. CONFIG.bookcasePrefs.saveConfig(config);
  2470.  
  2471. // New button
  2472. removeBtn(btn);
  2473. btn = installBtn(makeBtn('remove'));
  2474.  
  2475. // Soft alert
  2476. alt && alertify.success(TEXT_ALT_BOOK_AFTERBOOKS_ADDED);
  2477. }
  2478.  
  2479. function remove() {
  2480. // Remove from config
  2481. const config = CONFIG.bookcasePrefs.getConfig();
  2482. const books = config.laterbooks.books;
  2483. const book = books[pageResource.info.BID];
  2484. if (!book) {return false;}
  2485. delete books[pageResource.info.BID];
  2486. Array.prototype.forEach.call(Object.values(books), (b) => (b.sort > book.sort && b.sort--));
  2487. CONFIG.bookcasePrefs.saveConfig(config);
  2488.  
  2489. // New button
  2490. removeBtn(btn);
  2491. btn = installBtn(makeBtn('add'));
  2492.  
  2493. // Soft alert
  2494. alertify.success(TEXT_ALT_BOOK_AFTERBOOKS_REMOVED);
  2495. }
  2496.  
  2497. function makeBtn(type='add') {
  2498. const btn = $CrE('span');
  2499. btn.classList.add(CLASSNAME_BUTTON);
  2500. switch (type) {
  2501. case 'add':
  2502. btn.innerHTML = TEXT_GUI_BOOK_READITLATER;
  2503. btn.addEventListener('click', add);
  2504. break;
  2505. case 'remove':
  2506. btn.innerHTML = TEXT_GUI_BOOK_DONTREADLATER;
  2507. btn.addEventListener('click', remove);
  2508. break;
  2509. }
  2510. return btn;
  2511. }
  2512.  
  2513. function installBtn(btn) {
  2514. pageResource.elements.recommend.previousElementSibling.insertAdjacentElement('afterend', btn);
  2515. btn.insertAdjacentText('beforebegin', '[');
  2516. btn.insertAdjacentText('afterend', ']');
  2517. return btn;
  2518. }
  2519.  
  2520. function removeBtn(btn) {
  2521. const parent = btn.parentElement;
  2522. for (const node of [btn.previousSibling, btn, btn.nextSibling]) {
  2523. parent.removeChild(node);
  2524. }
  2525. return btn;
  2526. }
  2527.  
  2528. function inAfterbooks() {
  2529. return CONFIG.bookcasePrefs.getConfig().laterbooks.books[pageResource.info.BID] ? true : false;
  2530. }
  2531. }
  2532.  
  2533. // Download copyright book
  2534. function enableDownload() {
  2535. if (pageResource.info.dlEnabled) {return false;};
  2536.  
  2537. // Download panel
  2538. // Create panel
  2539. let div = $CrE('div');
  2540. pageResource.elements.bookMain.appendChild(div);
  2541. div.outerHTML = HTML_DOWNLOAD_LINKS
  2542. .replaceAll('{ORIBOOKNAME}', pageResource.info.bookName)
  2543. .replaceAll('{BOOKID}', String(pageResource.info.BID))
  2544. .replaceAll('{CHARSET}', getUrlArgv('charset') ? '&amp;charset=' + getUrlArgv('charset') : '')
  2545.  
  2546. // Use about:blank instead of direct url; aims to aviod unnecessary web requests
  2547. const container = pageResource.elements.downloadContainer = $(pageResource.elements.bookMain, 'div>fieldset');
  2548. div = pageResource.elements.downloadPanel = container.parentElement;
  2549. for (const a of $All(container, 'div>a')) {
  2550. //a.addEventListener('click', openDlPage);
  2551. }
  2552.  
  2553. // Notice board
  2554. pageResource.elements.notice.innerHTML = HTML_DOWNLOAD_BOARD
  2555. .replaceAll('{ORIBOOKNAME}', pageResource.info.bookName);
  2556.  
  2557. function openDlPage(e) {
  2558. e.preventDefault();
  2559.  
  2560. const url = e.target.href;
  2561. const win = window.open(`https://${location.host}/`);
  2562. win.history.replaceState({...win.history.state}, '', url);
  2563. }
  2564. }
  2565.  
  2566. // All images downloader
  2567. function imagesDownload() {
  2568. const container = pageResource.elements.downloadContainer;
  2569. const divImage = $CrE('div'), a = $CrE('a');
  2570. divImage.setAttribute('style', 'width:164px; float:left; text-align:center');
  2571. a.href = 'javascript:void(0);';
  2572. a.innerHTML = TEXT_GUI_BOOK_IMAGESDOWNLOAD;
  2573. a.addEventListener('click', confirm);
  2574. divImage.appendChild(a);
  2575. container.appendChild(divImage);
  2576. for (const div of $All(container, 'div')) {
  2577. div.style.width = '164px';
  2578. }
  2579.  
  2580. function confirm() {
  2581. const title = TEXT_ALT_DOWNLOADIMG_CONFIRM_TITLE;
  2582. const message = TEXT_ALT_DOWNLOADIMG_CONFIRM_MESSAGE.replace('{N}', pageResource.info.bookName);
  2583. const ok = TEXT_ALT_DOWNLOADIMG_CONFIRM_OK;
  2584. const cancel = TEXT_ALT_DOWNLOADIMG_CONFIRM_CANCEL;
  2585. alertify.confirm(title, message, download, function() {/* oncancel */}).set('labels', {ok: ok, cancel: cancel});
  2586. }
  2587.  
  2588. function download() {
  2589. // GUI
  2590. const delay = alertify.get('notifier','delay');
  2591. alertify.set('notifier','delay', 0);
  2592.  
  2593. let finished = false, CAll, CCur = 0;
  2594. const AM = new AsyncManager();
  2595. AM.onfinish = downloadFinish;
  2596. const box = alertify.message(TEXT_ALT_DOWNLOADIMG_STATUS_INDEX);
  2597. box.ondismiss = function() {return finished;}
  2598.  
  2599. // Start download
  2600. AM.add()
  2601. AndAPI.getNovelIndex({
  2602. aid: pageResource.info.BID,
  2603. lang: 0,
  2604. callback: function(xml) {
  2605. const allChapters = $All(xml, 'chapter');
  2606. const chapters = Array.prototype.filter.call(allChapters, (c) => (c.firstChild.nodeValue.includes('插图')));
  2607. CAll = chapters.length;
  2608. box.setContent(TEXT_ALT_DOWNLOADIMG_STATUS_LOADING.replace('{CCUR}', CCur).replace('{CALL}', CAll));
  2609. for (const chapter of chapters) {
  2610. AM.add();
  2611. getChapter(chapter.getAttribute('cid'), chapter.parentNode);
  2612. }
  2613. AM.finish();
  2614. }
  2615. });
  2616. AM.finishEvent = true;
  2617.  
  2618. function getChapter(cid, volume) {
  2619. AndAPI.getNovelContent({
  2620. aid: pageResource.info.BID,
  2621. cid: cid,
  2622. lang: 0,
  2623. callback: getImgs,
  2624. args: [volume]
  2625. });
  2626.  
  2627. function getImgs(str, volume) {
  2628. const imgs = str.match(/<!--image-->https?:[^<>]+<!--image-->/g);
  2629. const len = imgs.length.toString().length;
  2630. const CAM = new AsyncManager();
  2631. CAM.onfinish = chapterFinish;
  2632.  
  2633. for (let i = 0; i < imgs.length; i++) {
  2634. const img = imgs[i];
  2635. const src = img.match(/<!--image-->(https?:[^<>]+)<!--image-->/)[1];
  2636. const ext = src.match(/\.(\w+)$/) ? src.match(/\.(\w+)$/)[1] : 'jpg';
  2637. const filename = pageResource.info.bookName + '_' + volume.firstChild.nodeValue + ' ' + ['插图', '插圖'][getLang()] + '_' + fillNumber(i+1, len) + '.' + ext;
  2638. CAM.add();
  2639. downloadFile({
  2640. url: src,
  2641. name: filename,
  2642. onload: function() {
  2643. CAM.finish();
  2644. }
  2645. });
  2646. }
  2647. CAM.finishEvent = true;
  2648.  
  2649. function chapterFinish() {
  2650. AM.finish();
  2651. box.setContent(TEXT_ALT_DOWNLOADIMG_STATUS_LOADING.replace('{CCUR}', ++CCur).replace('{CALL}', CAll));
  2652. }
  2653. }
  2654. }
  2655.  
  2656. function downloadFinish() {
  2657. finished = true;
  2658. alertify.set('notifier','delay', delay);
  2659. box.dismiss();
  2660. alertify.success(TEXT_ALT_DOWNLOADIMG_STATUS_FINISH);
  2661. }
  2662. }
  2663. }
  2664.  
  2665. // Download copyright book full txt
  2666. function enableDownload_old() {
  2667. if (pageResource.info.dlEnabled) {return false;};
  2668.  
  2669. let div = $CrE('div');
  2670. pageResource.elements.bookMain.appendChild(div);
  2671. div.outerHTML = HTML_DOWNLOAD_LINKS_OLD
  2672. .replaceAll('{ORIBOOKNAME}', pageResource.info.bookName)
  2673. .replaceAll('{BOOKID}', String(pageResource.info.BID))
  2674. .replaceAll('{BOOKNAME}', encodeURIComponent(pageResource.info.bookName));
  2675. div = $('#txtfull');
  2676. pageResource.elements.txtfull = div;
  2677.  
  2678. pageResource.elements.notice.innerHTML = HTML_DOWNLOAD_BOARD
  2679. .replaceAll('{ORIBOOKNAME}', pageResource.info.bookName);
  2680. }
  2681.  
  2682. // Tag Search
  2683. function tagOption() {
  2684. const tagsEle = pageResource.elements.tags;
  2685. const tags = pageResource.info.tags;
  2686. if (!tags) {return false;}
  2687.  
  2688. let html = getKeyValue(tagsEle.innerText).KEY + ':';
  2689. for (const tag of tags) {
  2690. html += HTML_BOOK_TAG.replace('{TU}', $URL.encode(tag)).replace('{TN}', tag) + ' ';
  2691. }
  2692. tagsEle.innerHTML = html;
  2693. }
  2694. }
  2695.  
  2696. // Reply area add-on
  2697. function areaReply() {
  2698. /* ## Release title area ## */
  2699. if ($('td > input[name="Submit"]') && !$('#ptitle')) {
  2700. const table = $('form>table');
  2701. const titleText = table.innerHTML.match(/<!--[\s\S]+id="ptitle"[\s\S]+-->/)[0];
  2702. const titleHTML = titleText.replace(/^<!--\s*/, '').replace(/\s*-->$/, '');
  2703. const titleEle = $CrE('tr');
  2704. const caption = $(table, 'caption');
  2705. table.insertBefore(titleEle, caption);
  2706. titleEle.outerHTML = titleHTML;
  2707. }
  2708.  
  2709. const commentArea = $('#pcontent'); if (!commentArea) {return false;};
  2710. const commentForm = $(`form[action^="https://${location.host}/modules/article/review"]`);
  2711. const commentSbmt = $('td > input[name="Submit"]');
  2712. const commenttitl = $('#ptitle');
  2713. const commentbttm = commentSbmt.parentElement;
  2714.  
  2715. /* ## Ctrl+Enter comment submit ## */
  2716. let btnSbmtValue = commentSbmt.value;
  2717. if (commentSbmt) {
  2718. commentSbmt.value = '发表书评(Ctrl+Enter)';
  2719. commentSbmt.style.padding = '0.3em 0.4em 0.3em 0.4em';
  2720. commentSbmt.style.height= 'auto';
  2721. commentArea.addEventListener('keydown', hotkeyReply);
  2722. commenttitl.addEventListener('keydown', hotkeyReply);
  2723. }
  2724.  
  2725. // Enable https protocol for inserted url
  2726. fixHTTPS();
  2727.  
  2728. // Provide image upload & insert
  2729. imageplus();
  2730.  
  2731. // At user
  2732. atUser();
  2733.  
  2734. // Comment auto-save
  2735. // GUI
  2736. const asTip = $CrE('span');
  2737. commentbttm.appendChild(asTip);
  2738.  
  2739. // Review-Page: Same rid, same savekey - 'rid123456'
  2740. // Book-Page & Book-Review-List-Page: Same bookid, same savekey - 'bid1234'
  2741. const rid = getUrlArgv({url: commentForm.action, name: 'rid', dealFunc: Number});
  2742. const aid = getUrlArgv({url: commentForm.action, name: 'aid', dealFunc: Number});
  2743. const bid = location.href.match(/\/book\/(\d+).htm/) ? Number(location.href.match(/\/book\/(\d+).htm/)[1]) : 0;
  2744. const key = rid ? 'rid' + String(rid) : 'bid' + String(bid);
  2745. let commentData = CONFIG.commentDrafts.getConfig()[key] || {
  2746. key : key,
  2747. rid : rid,
  2748. aid : aid,
  2749. bid : bid,
  2750. page : getUrlArgv({name: 'rid', dealFunc: Number, defaultValue: 1}),
  2751. time : (new Date()).getTime()
  2752. };
  2753. restoreDraft();
  2754. submitHook();
  2755.  
  2756. const events = ['focus', 'blur', 'mousedown', 'keydown', 'keyup', 'change'];
  2757. const eventEles = [commentArea, commenttitl];
  2758. for (const eventEle of eventEles) {
  2759. for (const event of events) {
  2760. eventEle.addEventListener(event, saveDraft);
  2761. }
  2762. }
  2763.  
  2764. function saveDraft() {
  2765. const content = commentArea.value;
  2766. const title = commenttitl.value;
  2767.  
  2768. if (!content && !title) {
  2769. clearDraft();
  2770. return;
  2771. } else if (commentData.content === content && commentData.title === title) {
  2772. return;
  2773. }
  2774.  
  2775. commentData.content = content;
  2776. commentData.title = title;
  2777.  
  2778. const allCData = CONFIG.commentDrafts.getConfig();
  2779.  
  2780. allCData[commentData.key] = commentData;
  2781. CONFIG.commentDrafts.saveConfig(allCData);
  2782. asTip.innerHTML = TEXT_GUI_AUTOSAVE;
  2783. }
  2784.  
  2785. function restoreDraft() {
  2786. const allCData = CONFIG.commentDrafts.getConfig();
  2787. if (!allCData[commentData.key]) {return false;};
  2788. if (!commenttitl.value && !commentArea.value) {
  2789. commentData = allCData[commentData.key];
  2790. commenttitl.value = commentData.title;
  2791. commentArea.value = commentData.content;
  2792. asTip.innerHTML = TEXT_GUI_AUTOSAVE_RESTORE;
  2793. }
  2794. return true;
  2795. }
  2796.  
  2797. function clearDraft() {
  2798. const allCData = CONFIG.commentDrafts.getConfig();
  2799. if (!allCData[commentData.key]) {return false;};
  2800. delete allCData[commentData.key];
  2801. CONFIG.commentDrafts.saveConfig(allCData);
  2802. asTip.innerHTML = TEXT_GUI_AUTOSAVE_CLEAR;
  2803. return true;
  2804. }
  2805.  
  2806. function hotkeyReply() {
  2807. let keycode = event.keyCode;
  2808. if (keycode === 13 && event.ctrlKey && !event.altKey) {
  2809. // Do not submit directly like this; we need to submit with onsubmit executed
  2810. //commentForm.submit();
  2811. commentSbmt.click();
  2812. }
  2813. }
  2814.  
  2815. function fixHTTPS() {
  2816. if (typeof(UBBEditor) === 'undefined') {
  2817. fixHTTPS.wait = fixHTTPS.wait ? fixHTTPS.wait : 0;
  2818. if (++fixHTTPS.wait > 50) {return false;}
  2819. DoLog('fixHTTPS: UBBEditor not loaded, waiting...');
  2820. setTimeout(fixHTTPS, NUMBER_ELEMENT_LOADING_WAIT_INTERVAL);
  2821. return false;
  2822. }
  2823. const eid = 'pcontent';
  2824.  
  2825. const menuItemInsertUrl = $(commentForm, '#menuItemInsertUrl');
  2826. const menuItemInsertImage = $(commentForm, '#menuItemInsertImage');
  2827.  
  2828. // Wait until menuItemInsertUrl and menuItemInsertImage is loaded
  2829. if (!menuItemInsertUrl || !menuItemInsertImage) {
  2830. DoLog(LogLevel.Info, 'fixHTTPS: element not loaded, waiting...');
  2831. setTimeout(fixHTTPS, NUMBER_ELEMENT_LOADING_WAIT_INTERVAL);
  2832. return false;
  2833. }
  2834.  
  2835. // Wait until original onclick function is set
  2836. if (!menuItemInsertUrl.onclick || !menuItemInsertImage.onclick) {
  2837. DoLog(LogLevel.Info, 'fixHTTPS: defult onclick not loaded, waiting...');
  2838. setTimeout(fixHTTPS, NUMBER_ELEMENT_LOADING_WAIT_INTERVAL);
  2839. return false;
  2840. }
  2841.  
  2842. menuItemInsertUrl.onclick = function () {
  2843. var url = prompt("请输入超链接地址", "http://");
  2844. if (url != null && url.indexOf("http://") < 0 && url.indexOf("https://") < 0) {
  2845. alert("请输入完整的超链接地址!");
  2846. return;
  2847. }
  2848. if (url != null) {
  2849. if ((document.selection && document.selection.type == "Text") ||
  2850. (window.getSelection &&
  2851. document.getElementById(eid).selectionStart > -1 && document.getElementById(eid).selectionEnd >
  2852. document.getElementById(eid).selectionStart)) {UBBEditor.InsertTag(eid, "url", url,'');}
  2853. else {UBBEditor.InsertTag(eid, "url", url, url);}
  2854. }
  2855. };
  2856.  
  2857. menuItemInsertImage.onclick = function () {
  2858. var imgurl = prompt("请输入图片路径", "http://");
  2859. if (imgurl != null && imgurl.indexOf("http://") < 0 && imgurl.indexOf("https://") < 0) {
  2860. alert("请输入完整的图片路径!");
  2861. return;
  2862. }
  2863. if (imgurl != null) {
  2864. UBBEditor.InsertTag(eid, "img", "", imgurl);
  2865. }
  2866. };
  2867.  
  2868. return true;
  2869. }
  2870.  
  2871. function imageplus() {
  2872. if (typeof(UBBEditor) === 'undefined') {
  2873. DoLog('imageplus: UBBEditor not loaded, waiting...');
  2874. setTimeout(imageplus, NUMBER_ELEMENT_LOADING_WAIT_INTERVAL);
  2875. return false;
  2876. }
  2877.  
  2878. // Imager menu
  2879. const menu = $('#UBB_Menu');
  2880. const elmImage = $(commentForm, '#menuItemInsertImage');
  2881. const onclick = elmImage.onclick;
  2882. const imagers = new PlusList({
  2883. id: 'plus_imager',
  2884. list: [
  2885. {value: TEXT_GUI_REVEIW_IMG_INSERTURL, tip: TEXT_TIP_REVIEW_IMG_INSERTURL, onclick: onclick},
  2886. {value: TEXT_GUI_REVEIW_IMG_SELECTIMG, tip: TEXT_TIP_REVIEW_IMG_SELECTIMG, onclick: pickfile}
  2887. ],
  2888. parentElement: menu.parentElement,
  2889. insertBefore: $('#SmileListTable'),
  2890. visible: false,
  2891. onshow: onshow
  2892. });
  2893. elmImage.onclick = (e) => {
  2894. e.stopPropagation();
  2895. imagers.show();
  2896. };
  2897. document.addEventListener('click', imagers.hide);
  2898.  
  2899. // drag-drop & copy-paste
  2900. commentArea.addEventListener('paste', pictureGot);
  2901. commentArea.addEventListener('dragenter', destroyEvent);
  2902. commentArea.addEventListener('dragover', destroyEvent);
  2903. commentArea.addEventListener('drop', pictureGot);
  2904.  
  2905. function onshow() {
  2906. imagers.div.style.left = String(UBBEditor.GetPosition(elmImage).x) + 'px';
  2907. imagers.div.style.top = String(UBBEditor.GetPosition(elmImage).y + 20) + 'px';
  2908. }
  2909.  
  2910. function pickfile() {
  2911. const fileinput = $CrE('input');
  2912. fileinput.type = 'file';
  2913. fileinput.addEventListener('change', pictureGot);
  2914. fileinput.click();
  2915. }
  2916.  
  2917. function pictureGot(e) {
  2918. // Get picture file
  2919. const input = e.dataTransfer || e.clipboardData || window.clipboardData || e.target;
  2920. if (!input.files || input.files.length === 0) {return false;};
  2921. const file = input.files[0];
  2922. const mimetype = file.type;
  2923. const name = file.name;
  2924.  
  2925. // Pasting an unrecognizable file is not a mistake
  2926. // Maybe the user just wants to paste the filename here
  2927. // Otherwise getting an unrecognizable file is a mistake
  2928. if (!mimetype || mimetype.split('/')[0] !== 'image') {
  2929. if (!e.clipboardData && !window.clipboardData) {
  2930. destroyEvent(e);
  2931. alertify.error(TEXT_ALT_IMAGE_FORMATERROR);
  2932. }
  2933. return false;
  2934. } else {
  2935. destroyEvent(e);
  2936. }
  2937.  
  2938. // Insert picture marker
  2939. const marker = '[image_uploading={ID} name={NAME}]'.replace('{ID}', randstr(16, true, commentArea.value)).replace('{NAME}', name);
  2940. insertText(marker);
  2941.  
  2942. // Upload
  2943. alertify.notify(TEXT_ALT_IMAGE_UPLOAD_WORKING);
  2944. uploadImage({
  2945. file: file,
  2946. onerror: (e) => {
  2947. alertify.error(TEXT_ALT_IMAGE_UPLOAD_ERROR);
  2948. DoLog(LogLevel.Error, ['Upload error at imageplus>upload:', e]);
  2949. },
  2950. onload: (json) => {
  2951. const name = json.name;
  2952. const url = json.url;
  2953. commentArea.value = commentArea.value.replace(marker, url);
  2954. alertify.success(TEXT_ALT_IMAGE_UPLOAD_SUCCESS.replaceAll('{NAME}', name).replaceAll('{URL}', url));
  2955. }
  2956. });
  2957.  
  2958. }
  2959. }
  2960.  
  2961. function submitHook() {
  2962. const onsubmit = commentForm.onsubmit;
  2963. commentForm.onsubmit = onsubmitForm;
  2964.  
  2965. function onsubmitForm(e) {
  2966. // Cancel submit while content empty
  2967. if (commentArea.value === '' && commenttitl.value === '') {return false;};
  2968.  
  2969. // Clear Draft
  2970. clearDraft();
  2971.  
  2972. // Restore original submit button value
  2973. if (commentSbmt.value !== btnSbmtValue) {
  2974. commentSbmt.value = btnSbmtValue;
  2975. setTimeout(()=>{commentSbmt.click.call(commentSbmt);}, 0);
  2976. return false;
  2977. }
  2978.  
  2979. // Continue submit
  2980. return onsubmit ? onsubmit() : function() {return true;};
  2981. }
  2982. }
  2983.  
  2984. function atUser() {
  2985. if (typeof(UBBEditor) === 'undefined') {
  2986. DoLog(LogLevel.Info, 'atUser: UBBEditor not loaded, waiting...');
  2987. setTimeout(atUser, NUMBER_ELEMENT_LOADING_WAIT_INTERVAL);
  2988. return false;
  2989. }
  2990.  
  2991. const menu = $('#UBB_Menu');
  2992. const list = new PlusList({
  2993. id: 'plus_AtTable',
  2994. list: [],
  2995. parentElement: menu.parentElement,
  2996. insertBefore: $('#FontSizeTable'),
  2997. visible: false,
  2998. onshow: showlist
  2999. });
  3000. list.onhide = list.clear;
  3001. document.addEventListener('click', list.hide);
  3002.  
  3003. const firstBtn = menu.children[0];
  3004. const atBtn = $CrE('input');
  3005. atBtn.type = 'button';
  3006. atBtn.style.backgroundImage = 'none';
  3007. atBtn.value = '@';
  3008. atBtn.title = TEXT_GUI_AREAREPLY_AT;
  3009. atBtn.id = 'plus_At';
  3010. atBtn.classList.add(CLASSNAME_BUTTON);
  3011. atBtn.classList.add('UBB_MenuItem');
  3012. atBtn.addEventListener('click', (e) => {
  3013. e.stopPropagation();
  3014. list.show();
  3015. });
  3016. menu.insertBefore(atBtn, firstBtn);
  3017.  
  3018. function showlist(shown) {
  3019. if (shown) {return false;};
  3020. if (typeof(ubb_subdiv) === 'string' && typeof(hideeve) === 'function') {
  3021. hideeve(ubb_subdiv);
  3022. ubb_subdiv = 'plus_AtTable';
  3023. }
  3024. makelist();
  3025. list.ul.focus();
  3026. return true;
  3027. }
  3028.  
  3029. function makelist() {
  3030. // Get users
  3031. const allUsers = getAllUsers();
  3032.  
  3033. // Make list
  3034. for (const user of allUsers) {
  3035. const item = list.append({
  3036. value: user.userName,
  3037. tip: ()=>{return 'uid: ' + String(user.userID);},
  3038. onclick: btnClick
  3039. });
  3040. item.li.user = user;
  3041. item.button.user = user;
  3042. }
  3043.  
  3044. // Style
  3045. list.div.style.left = String(UBBEditor.GetPosition(atBtn).x) + 'px';
  3046. list.div.style.top = String(UBBEditor.GetPosition(atBtn).y + 20) + 'px';
  3047.  
  3048. return true;
  3049.  
  3050. function getAllUsers() {
  3051. const pageUsers = $All(`#content table strong>a[href^="https://${location.host}/userpage.php"]`);
  3052. const friends = getMyUserDetail().userFriends;
  3053. if (!friends) {
  3054. refreshMyUserDetail(refreshList);
  3055. return false;
  3056. }
  3057.  
  3058. // concat to one array
  3059. const allUsers = [];
  3060. for (const pageUser of pageUsers) {
  3061. // Valid check
  3062. if (isNaN(Number(pageUser.href.match(/\?uid=(\d+)/)[1]))) {continue;};
  3063. const user = {
  3064. userName: pageUser.innerText,
  3065. userID: Number(pageUser.href.match(/\?uid=(\d+)/)[1]),
  3066. referred: 0
  3067. }
  3068. if (!userExist(allUsers, user)) {
  3069. const userAsFriend = userExist(friends, user);
  3070. allUsers.push(userAsFriend ? userAsFriend : user);
  3071. }
  3072. }
  3073. for (const friend of friends) {
  3074. if (!userExist(allUsers, friend)) {
  3075. allUsers.push(friend);
  3076. }
  3077. }
  3078.  
  3079. // Sort by referred
  3080. allUsers.sort((a,b)=>{return (b.referred?b.referred:0) - (a.referred?a.referred:0);});
  3081.  
  3082. return allUsers;
  3083.  
  3084. // returns the exist user object found in users, or false if not found
  3085. function userExist(users, user) {
  3086. for (const u of users) {
  3087. if (u.userID === user.userID) {return u;};
  3088. }
  3089. return false;
  3090. }
  3091. }
  3092.  
  3093. function btnClick() {
  3094. const btn = this;
  3095. const user = btn.user;
  3096. const name = btn.user.userName;
  3097. const insertValue = '@' + name;
  3098. insertText(insertValue);
  3099.  
  3100. // referred increase
  3101. const userDetail = getMyUserDetail();
  3102. const friends = userDetail.userFriends;
  3103. user.referred = user.referred ? user.referred+1 : 1;
  3104. for (let i = 0; i < friends.length; i++) {
  3105. if (friends[i].userID === user.userID) {
  3106. friends[i] = user;
  3107. break;
  3108. }
  3109. }
  3110. CONFIG.userDtlePrefs.saveConfig(userDetail);
  3111. }
  3112. }
  3113. }
  3114.  
  3115. function insertText(insertValue) {
  3116. const insertPosition = commentArea.selectionEnd;
  3117. const text = commentArea.value;
  3118. const leftText = text.substr(0, insertPosition);
  3119. const rightText = text.substr(insertPosition);
  3120.  
  3121. // if not at the beginning of a line then insert a whitespace before the link
  3122. insertValue = ((leftText.length === 0 || /[ \r\n]$/.test(leftText)) ? '' : ' ') + insertValue;
  3123. // if not at the end of a line then insert a whitespace after the link
  3124. insertValue += (rightText.length === 0 || /^[ \r\n]/.test(rightText)) ? '' : ' ';
  3125.  
  3126. commentArea.value = leftText + insertValue + rightText;
  3127. const position = insertPosition + insertValue.length;
  3128. commentForm.scrollIntoView(); commentArea.focus(); commentArea.setSelectionRange(position, position);
  3129. }
  3130. }
  3131.  
  3132. // Review link add-on
  3133. function linkReview() {
  3134. // Get all review links and apply add-on functions
  3135. const allRLinks = $All(`td>a[href^="https://${location.host}/modules/article/reviewshow.php?"]`);
  3136. for (const RLink of allRLinks) {
  3137. lastPage(RLink);
  3138. }
  3139.  
  3140. // Provide button direct to review last page
  3141.  
  3142. // New version. Uses '&page=last' keyword.
  3143. function lastPage(a) {
  3144. const p = a.parentElement;
  3145. const lastpg = $CrE('a');
  3146. const strrid = getUrlArgv({url: a.href, name: 'rid'});
  3147. lastpg.href = URL_REVIEWSHOW_2.replace('{R}', strrid).replace('{P}', 'last');
  3148. lastpg.classList.add(CLASSNAME_BUTTON);
  3149. lastpg.target = '_blank';
  3150. lastpg.innerText = TEXT_GUI_LINK_TOLASTPAGE;
  3151. p.insertBefore(lastpg, a);
  3152. }
  3153. }
  3154.  
  3155. // Side functions area
  3156. function sideFunctions() {
  3157. const SPanel = new SidePanel();
  3158. SPanel.usercss = CSS_SIDEPANEL;
  3159. SPanel.create();
  3160. SPanel.setPosition('bottom-right');
  3161.  
  3162. commonButtons();
  3163. return SPanel;
  3164.  
  3165. function commonButtons() {
  3166. // Button show/hide-all-buttons
  3167. const btnShowHide = SPanel.add({
  3168. faicon: 'fa-solid fa-down-left-and-up-right-to-center',
  3169. className: 'accept-pointer',
  3170. tip: '隐藏面板',
  3171. onclick: (function() {
  3172. let hidden = false;
  3173. return (e) => {
  3174. hidden = !hidden;
  3175. btnShowHide.faicon.className = 'fa-solid ' + (hidden ? 'fa-up-right-and-down-left-from-center' : 'fa-down-left-and-up-right-to-center');
  3176. btnShowHide.classList[hidden ? 'add' : 'remove']('low-opacity');
  3177. btnShowHide.setAttribute('aria-label', (hidden ? '显示面板' : '隐藏面板'));
  3178. SPanel.elements.panel.style.pointerEvents = hidden ? 'none' : 'auto';
  3179. for (const button of SPanel.elements.buttons) {
  3180. if (button === btnShowHide) {continue;}
  3181. //button.style.display = hidden ? 'none' : 'block';
  3182. button.style.pointerEvents = hidden ? 'none' : 'auto';
  3183. button.style.opacity = hidden ? '0' : '1';
  3184. }
  3185. };
  3186. }) ()
  3187. });
  3188.  
  3189. // Button scroll-to-bottom
  3190. const btnDown = SPanel.add({
  3191. faicon: 'fa-solid fa-angle-down',
  3192. tip: '转到底部',
  3193. onclick: (e) => {
  3194. const elms = [document.body.parentElement, $('#content'), $('#contentmain')];
  3195.  
  3196. for (const elm of elms) {
  3197. elm && elm.scrollTo && elm.scrollTo(elm.scrollLeft, elm.scrollHeight);
  3198. }
  3199. }
  3200. });
  3201.  
  3202. // Button scroll-to-top
  3203. const btnUp = SPanel.add({
  3204. faicon: 'fa-solid fa-angle-up',
  3205. tip: '转到顶部',
  3206. onclick: (e) => {
  3207. const elms = [document.body.parentElement, $('#content'), $('#contentmain')];
  3208.  
  3209. for (const elm of elms) {
  3210. elm && elm.scrollTo && elm.scrollTo(elm.scrollLeft, 0);
  3211. }
  3212. }
  3213. });
  3214.  
  3215. // Darkmode
  3216. /*
  3217. const btnDarkmode = SPanel.add({
  3218. faicon: 'fa-solid ' + (DMode.isActivated() ? 'fa-sun' : 'fa-moon'),
  3219. tip: '明暗切换',
  3220. onclick: (e) => {
  3221. DMode.toggle();
  3222. btnDarkmode.faicon.className = 'fa-solid ' + (DMode.isActivated() ? 'fa-sun' : 'fa-moon');
  3223. }
  3224. });
  3225. */
  3226.  
  3227. // Refresh page
  3228. const btnRefresh = SPanel.add({
  3229. faicon: 'fa-solid fa-rotate-right',
  3230. tip: '刷新页面',
  3231. onclick: (e) => {
  3232. reloadPage();
  3233. }
  3234. });
  3235. }
  3236. }
  3237.  
  3238. // Reviewedit page add-on
  3239. function pageReviewedit() {
  3240. redirectToCorrectPage();
  3241.  
  3242. function redirectToCorrectPage() {
  3243. // Get redirect target rid
  3244. const refreshMeta = $('meta[http-equiv="refresh"]');
  3245. const metaurl = refreshMeta.content.match(/url=(.+)/)[1];
  3246. if (!refreshMeta) {return false;};
  3247. if (getUrlArgv({url: metaurl, name: 'page'})) {return false;};
  3248.  
  3249. // Read correct redirect location
  3250. const rid = Number(getUrlArgv({url: metaurl, name: 'rid'}));
  3251. const config = CONFIG.BkReviewPrefs.getConfig();
  3252. const history = config.history;
  3253. const pageHist = history[rid];
  3254. if (!pageHist) {return false;}
  3255. const url = pageHist.href;
  3256.  
  3257. // Check if time expired (Expire time: 30 seconds)
  3258. if ((new Date()).getTime() - pageHist.time > 30*1000) {
  3259. // Delete expired record
  3260. delete history[rid];
  3261. CONFIG.BkReviewPrefs.saveConfig(config);
  3262. }
  3263.  
  3264. // Redirect link
  3265. $('a').href = url;
  3266.  
  3267. // Redirect
  3268. setTimeout(() => {location.href = url;}, 1500);
  3269. }
  3270. }
  3271.  
  3272. // Review page add-on
  3273. function pageReview() {
  3274. // Elements
  3275. const main = $('#content');
  3276. const headBars = $All(main, 'tr>td[align]');
  3277.  
  3278. // Page Info
  3279. const rid = Number(getUrlArgv('rid'));
  3280. const aid = getUrlArgv('aid') ? Number(getUrlArgv('aid')) : Number($(main, 'td[width]>a').href.match(/(\d+)\.html?$/)[1]);
  3281. const page = Number($('#pagelink strong').innerText);
  3282. const title = $(main, 'th>strong').textContent;
  3283.  
  3284. // URL correction
  3285. correctURL();
  3286.  
  3287. // Enhancements
  3288. pageStatus();
  3289. downloader();
  3290. sideButtons();
  3291. beautifier();
  3292. floorEnhance();
  3293. autoRefresh();
  3294. addFavorite();
  3295. addUnlock();
  3296.  
  3297. function correctURL() {
  3298. (getUrlArgv('page') === 'last' || !getUrlArgv('page')) && setPageUrl(URL_REVIEWSHOW.replace('{A}', aid).replace('{R}', rid).replace('{P}', page));
  3299. }
  3300.  
  3301. function sideButtons() {
  3302. // Last page
  3303. SPanel.add({
  3304. faicon: 'fa-solid fa-angles-right',
  3305. tip: '最后一页',
  3306. onclick: (e) => {findclick('#pagelink>.last');}
  3307. });
  3308.  
  3309. // Next page
  3310. SPanel.add({
  3311. faicon: 'fa-solid fa-angle-right',
  3312. tip: '下一页',
  3313. onclick: (e) => {findclick('#pagelink>.next');}
  3314. });
  3315.  
  3316. // Previous page
  3317. SPanel.add({
  3318. faicon: 'fa-solid fa-angle-left',
  3319. tip: '上一页',
  3320. onclick: (e) => {findclick('#pagelink>.prev');}
  3321. });
  3322.  
  3323. // First page
  3324. SPanel.add({
  3325. faicon: 'fa-solid fa-angles-left',
  3326. tip: '第一页',
  3327. onclick: (e) => {findclick('#pagelink>.first');}
  3328. });
  3329.  
  3330. function findclick(selector) {return $(selector) && $(selector).click();}
  3331. }
  3332.  
  3333. function beautifier() {
  3334. // GUI
  3335. const span = $CrE('span');
  3336. const check = $CrE('input');
  3337. check.type = 'checkbox';
  3338. check.checked = CONFIG.BeautifierCfg.getConfig().reviewshow.beautiful;
  3339. span.innerHTML = TEXT_GUI_REVIEW_BEAUTIFUL;
  3340. span.classList.add(CLASSNAME_BUTTON);
  3341. span.style.marginLeft = '0.5em';
  3342. span.addEventListener('click', toggleBeautiful);
  3343. check.addEventListener('click', toggleBeautiful);
  3344. settip(span, TEXT_TIP_REVIEW_BEAUTIFUL);
  3345. settip(check, TEXT_TIP_REVIEW_BEAUTIFUL);
  3346. headBars[0].appendChild(span);
  3347. headBars[0].appendChild(check);
  3348. CONFIG.BeautifierCfg.getConfig().reviewshow.beautiful && beautiful();
  3349.  
  3350. function toggleBeautiful(e) {
  3351. // stop event
  3352. destroyEvent(e);
  3353.  
  3354. // Togle & save to config
  3355. const config = CONFIG.BeautifierCfg.getConfig();
  3356. config.reviewshow.beautiful = !config.reviewshow.beautiful;
  3357. CONFIG.BeautifierCfg.saveConfig(config);
  3358.  
  3359. setTimeout(() => {check.checked = config.reviewshow.beautiful;}, 0);
  3360. alertify.notify(config.reviewshow.beautiful ? TEXT_ALT_BEAUTIFUL_ON : TEXT_ALT_BEAUTIFUL_OFF);
  3361.  
  3362. // beautifier
  3363. config.reviewshow.beautiful ? beautiful() : recover();
  3364. }
  3365.  
  3366. function beautiful() {
  3367. const config = CONFIG.BeautifierCfg.getConfig();
  3368. addStyle(CSS_REVIEWSHOW
  3369. .replaceAll('{BGI}', config.backgroundImage)
  3370. .replaceAll('{S}', config.textScale)
  3371. , 'beautifier');
  3372. scaleimgs();
  3373. hookPosition();
  3374.  
  3375. function scaleimgs() {
  3376. const imgs = $All('.divimage>img');
  3377. const w = main.clientWidth * 0.8 - 3; // td.width = "80%", .even {padding: 3px;}
  3378. for (const img of imgs) {
  3379. img.width = w;
  3380. }
  3381. }
  3382. }
  3383.  
  3384. function recover() {
  3385. addStyle('', 'beautifier');
  3386. restorePosition();
  3387. }
  3388.  
  3389. function hookPosition() {
  3390. if (!CONFIG.BeautifierCfg.getConfig().reviewshow.beautiful) {return false;};
  3391. if (typeof(UBBEditor) !== 'object') {
  3392. hookPosition.wait = hookPosition.wait ? hookPosition.wait : 0;
  3393. if (++hookPosition.wait > 50) {return false;}
  3394. DoLog('beautiful/hookPosition: UBBEditor not loaded, waiting...');
  3395. setTimeout(hookPosition, NUMBER_ELEMENT_LOADING_WAIT_INTERVAL);
  3396. return false;
  3397. }
  3398. UBBEditor.GetPosition_BK = UBBEditor.GetPosition;
  3399. UBBEditor.GetPosition = function (obj) {
  3400. var r = new Array();
  3401. r.x = obj.offsetLeft;
  3402. r.y = obj.offsetTop;
  3403. while (obj = obj.offsetParent) {
  3404. if (unsafeWindow.$(obj).getStyle('position') == 'absolute' || unsafeWindow.$(obj).getStyle('position') == 'relative') break;
  3405. r.x += obj.offsetLeft;
  3406. r.y += obj.offsetTop;
  3407. }
  3408. r.x -= main.scrollLeft;
  3409. r.y -= main.scrollTop;
  3410. return r;
  3411. }
  3412. }
  3413.  
  3414. function restorePosition() {
  3415. if (typeof(UBBEditor) !== 'object') {return false;};
  3416. if (!UBBEditor.GetPosition_BK) {return false;};
  3417. UBBEditor.GetPosition = UBBEditor.GetPosition_BK;
  3418. }
  3419. }
  3420.  
  3421. function pageStatus() {
  3422. window.addEventListener('load', () => {
  3423. // Recover page status
  3424. applyPageStatus();
  3425. // Record the current page status of current review
  3426. setInterval(recordPage, 1000);
  3427. });
  3428. }
  3429.  
  3430. // Apply page status sored in history record
  3431. function applyPageStatus() {
  3432. const config = CONFIG.BkReviewPrefs.getConfig();
  3433. const history = config.history;
  3434. const pageHist = history[rid];
  3435.  
  3436. // Scroll to the last position
  3437. if (pageHist && pageHist.page === page) {
  3438. // Check if time expired
  3439. if (pageHist.time && (new Date()).getTime() - pageHist.time < 30*1000) {
  3440. // Do not scroll when opening a positioned link(http[s]://.../...#yidxxxxxx)
  3441. if (/#yid\d+$/.test(location.href)) {return;}
  3442. // Scroll
  3443. pageHist.scrollX !== undefined && window.scrollTo(pageHist.scrollX, pageHist.scrollY);
  3444. pageHist.contentsclX !== undefined && main.scrollTo(pageHist.contentsclX, pageHist.contentsclY);
  3445. } else {
  3446. // Delete expired record
  3447. delete history[rid];
  3448. CONFIG.BkReviewPrefs.saveConfig(config);
  3449. }
  3450. }
  3451. }
  3452.  
  3453. function recordPage() {
  3454. const config = CONFIG.BkReviewPrefs.getConfig();
  3455. const history = config.history;
  3456.  
  3457. // Save page history
  3458. config.history[rid] = {
  3459. rid: rid,
  3460. aid: aid,
  3461. page: page,
  3462. href: URL_REVIEWSHOW.replace('{R}', String(rid)).replace('{A}', String(aid)).replace('{P}', String(page)),
  3463. scrollX: window.pageXOffset,
  3464. scrollY: window.pageYOffset,
  3465. contentsclX: main.scrollLeft,
  3466. contentsclY: main.scrollTop,
  3467. time: (new Date()).getTime()
  3468. }
  3469. CONFIG.BkReviewPrefs.saveConfig(config);
  3470. }
  3471.  
  3472. function floorEnhance() {
  3473. const floors = getAllFloors();
  3474. floors.forEach((f)=>(correctFloorLink(f)));
  3475. for (const floor of floors) {
  3476. alinkEdit(floor);
  3477. addQuoteBtn(floor);
  3478. addQueryBtn(floor);
  3479. addRemark(floor);
  3480. alinktofloor(floor.table);
  3481. }
  3482. }
  3483.  
  3484. function alinktofloor(parent=main) {
  3485. const floorLinks = $All(main, `a[name][href^="https://${location.host}/modules/article/reviewshow.php"][href*="#yid"]`);
  3486. for (const a of $All(parent, 'a')) {
  3487. if (!a.href.match(/^https?:\/\/www\.wenku8\.(net|cc)\/modules\/article\/reviewshow\.php\?(&?rid=\d+|&?aid=\d+|&?page=\d+){1,4}#yid\d+$/)) {continue;};
  3488. for (const flink of floorLinks) {
  3489. if (isSameReply(a, flink)) {
  3490. // Set scroll target
  3491. a.targetNode = flink;
  3492. while (a.targetNode.nodeName !== 'TABLE') {
  3493. a.targetNode = a.targetNode.parentElement;
  3494. }
  3495.  
  3496. // Scroll when clicked
  3497. a.addEventListener('click', (e) => {
  3498. destroyEvent(e);
  3499. e.currentTarget.targetNode.scrollIntoView();
  3500. })
  3501. };
  3502. }
  3503. }
  3504.  
  3505. function isSameReply(link1, link2) {
  3506. const url1 = link1.href.toLowerCase().replace('http://', 'https://');
  3507. const url2 = link2.href.toLowerCase().replace('http://', 'https://');
  3508. const rid1 = getUrlArgv({url: url1, name: 'rid', defaultValue: null});
  3509. const yid1 = url1.match(/#yid(\d+)/) ? url1.match(/#yid(\d+)/)[1] : null;
  3510. const rid2 = getUrlArgv({url: url2, name: 'rid', defaultValue: null});
  3511. const yid2 = url2.match(/#yid(\d+)/) ? url2.match(/#yid(\d+)/)[1] : null;
  3512. return rid1 === rid2 && yid1 === yid2;
  3513. }
  3514. }
  3515.  
  3516. function alinkEdit(parent=document) {
  3517. const eLinks = $All(`a[href^="https://${location.host}/modules/article/reviewedit.php?yid="]`);
  3518. for (const eLink of eLinks) {
  3519. eLink.addEventListener('click', (e) => {
  3520. // NO e.stopPropagation() here. Just hooks the open action.
  3521. e.preventDefault();
  3522.  
  3523. // Open editor dialog
  3524. openDialog(e.target.href + '&ajax_gets=jieqi_contents');
  3525.  
  3526. // Show mask if mask not shown
  3527. !document.getElementById("mask") && showMask();
  3528. })
  3529. }
  3530. }
  3531.  
  3532. function autoRefresh() {
  3533. let working=false, interval=0;
  3534. const pagelink = $('#pagelink');
  3535. const tdLink = pagelink.parentElement;
  3536. const trContainer = tdLink.parentElement;
  3537. const tdAutoRefresh = $CrE('td');
  3538. const chkAutoRefresh = $CrE('input');
  3539. const txtAutoRefresh = $CrE('span');
  3540. const txtPaused = $CrE('span');
  3541. const ptitle = $('#ptitle');
  3542. const pcontent = $('#pcontent');
  3543. txtAutoRefresh.innerText = TEXT_GUI_AUTOREFRESH;
  3544. txtAutoRefresh.classList.add(CLASSNAME_BUTTON);
  3545. txtAutoRefresh.addEventListener('click', toggleRefresh);
  3546. chkAutoRefresh.addEventListener('click', toggleRefresh);
  3547. chkAutoRefresh.type = 'checkbox';
  3548. chkAutoRefresh.checked = false;
  3549. txtPaused.innerText = '';
  3550. txtPaused.classList.add(CLASSNAME_TEXT);
  3551. txtPaused.style.marginLeft = '0.5em';
  3552. tdAutoRefresh.style.align = 'left';
  3553. tdAutoRefresh.appendChild(txtAutoRefresh);
  3554. tdAutoRefresh.appendChild(chkAutoRefresh);
  3555. tdAutoRefresh.appendChild(txtPaused);
  3556. trContainer.insertBefore(tdAutoRefresh, tdLink);
  3557.  
  3558. // Apply config
  3559. CONFIG.BkReviewPrefs.getConfig().autoRefresh ? toggleRefresh() : function() {};
  3560.  
  3561. /* No pauses after v1.5.7
  3562. // Show pause
  3563. // Note: Blur event triggers after Focus event was triggered
  3564. for (const editElm of [ptitle, pcontent]) {
  3565. if (!editElm) {continue;};
  3566. editElm.addEventListener('blur', (e) => {
  3567. txtPaused.innerText = '';
  3568. });
  3569. editElm.addEventListener('focus', (e) => {
  3570. txtPaused.innerText = TEXT_GUI_AUTOREFRESH_PAUSED;
  3571. });
  3572. }
  3573. */
  3574.  
  3575. function toggleRefresh(e) {
  3576. // stop event
  3577. destroyEvent(e);
  3578.  
  3579. // Not in last Page, no auto refresh
  3580. if (!isCurLastPage() && !working) {
  3581. const box = alertify.notify(TEXT_ALT_AUTOREFRESH_NOTLAST);
  3582. box.callback = (isClicked) => {isClicked && (location.href = $('#pagelink>a.last').href);};
  3583. return false;
  3584. }
  3585.  
  3586. // toggle
  3587. working = !working;
  3588. working ? interval = setInterval(refresh, 20*1000) : clearInterval(interval);
  3589. working && refresh();
  3590.  
  3591. // Save to config
  3592. const review = CONFIG.BkReviewPrefs.getConfig();
  3593. review.autoRefresh = working;
  3594. CONFIG.BkReviewPrefs.saveConfig(review);
  3595.  
  3596. setTimeout(() => {chkAutoRefresh.checked = working;}, 0);
  3597. alertify.notify(working ? TEXT_ALT_AUTOREFRESH_ON : TEXT_ALT_AUTOREFRESH_OFF);
  3598. }
  3599.  
  3600. function refresh() {
  3601. const box = alertify.notify(TEXT_ALT_AUTOREFRESH_WORKING);
  3602. const url = URL_REVIEWSHOW.replace('{R}', String(rid)).replace('{A}', String(aid)).replace('{P}', 'last');
  3603. getDocument(url, refreshLoaded, url);
  3604.  
  3605.  
  3606. function refreshLoaded(oDoc, pageurl) {
  3607. // Clost alert box
  3608. box.exist ? box.close.apply(box) : function() {};
  3609.  
  3610. // Update all existing floor content (and title)
  3611. const nowfloors = $All('#content>table[class="grid"]');
  3612. const newfloors = $All(oDoc, '#content>table[class="grid"]');
  3613. let i, modified = false;
  3614.  
  3615. for (i = 1; i < Math.min(nowfloors.length, newfloors.length); i++) {
  3616. isFloorTable(nowfloors[i]) && isFloorTable(newfloors[i]) && getFloorNumber(nowfloors[i]) === getFloorNumber(newfloors[i]) && updatefloor(i);
  3617. }
  3618. modified && alertify.notify(TEXT_ALT_AUTOREFRESH_MODIFIED);
  3619.  
  3620. const newtop = getTopFloorNumber(oDoc);
  3621. const nowtop = getTopFloorNumber(document);
  3622. if (unsafeWindow.isPY_DNG && newtop === 9899) {
  3623. sendReviewReply({rid: rid, title: '测试标题', content: '测试内容'});
  3624. }
  3625. if (newtop > nowtop) {
  3626. const newmain = $(oDoc, '#content');
  3627. const eleLastPage = $(oDoc, '#pagelink a.last');
  3628. const urlLastPage = newmain.url = eleLastPage.href;
  3629. const newpage = Number(getUrlArgv({url: urlLastPage, name: 'page'}));
  3630. const newfloors = getAllFloors(newmain);
  3631. const nowfloors = getAllFloors();
  3632. if (newpage === page) {
  3633. // In same page, append floors
  3634. for (let i = nowfloors.length; i < newfloors.length; i++) {
  3635. const floor = newfloors[i];
  3636. appendfloor(floor);
  3637. }
  3638. } else {
  3639. // In New page, remake floors
  3640. let box = alertify.notify(TEXT_ALT_AUTOREFRESH_APPLIED);
  3641.  
  3642. // Remove old floors
  3643. for (const oldfloor of nowfloors) {
  3644. oldfloor.table.parentElement.removeChild(oldfloor.table);
  3645. }
  3646.  
  3647. // Append new floors
  3648. for (const newfloor of newfloors) {
  3649. appendfloor(newfloor);
  3650. }
  3651.  
  3652. // Remake #pagelink
  3653. $(main, '#pagelink').innerHTML = $(newmain, '#pagelink').innerHTML;
  3654.  
  3655. // Reset location.href
  3656. page !== 'last' && setPageUrl(urlLastPage);
  3657.  
  3658. return true;
  3659. }
  3660. } else {
  3661. alertify.message(TEXT_ALT_AUTOREFRESH_NOMORE);
  3662. return false;
  3663. }
  3664.  
  3665. function updatefloor(i) {
  3666. const nowfloor = nowfloors[i];
  3667. const newfloor = newfloors[i];
  3668. const nowTitle = getEleFloorTitle(nowfloor);
  3669. const newTitle = getEleFloorTitle(newfloor);
  3670. const nowContent = getEleFloorContent(nowfloor);
  3671. const newContent = getEleFloorContent(newfloor);
  3672.  
  3673. if (nowTitle.innerHTML !== newTitle.innerHTML) {
  3674. nowTitle.innerHTML = newTitle.innerHTML;
  3675. nowTitle.classList.add(CLASSNAME_MODIFIED);
  3676. nowTitle.addEventListener('click', (e) => {e.currentTarget.classList.remove(CLASSNAME_MODIFIED);});
  3677. modified = true;
  3678. }
  3679. if (getFloorContent(nowContent) !== getFloorContent(newContent)) {
  3680. nowContent.innerHTML = newContent.innerHTML;
  3681. nowContent.classList.add(CLASSNAME_MODIFIED);
  3682. nowContent.addEventListener('click', (e) => {e.currentTarget.classList.remove(CLASSNAME_MODIFIED);});
  3683. modified = true;
  3684. }
  3685. if (modified) {
  3686. alinktofloor(nowfloor);
  3687. }
  3688. }
  3689. }
  3690. }
  3691.  
  3692. function isCurLastPage() {
  3693. return $('#pagelink>strong').innerText === $('#pagelink>a.last').innerText;
  3694. }
  3695.  
  3696. function getTopFloorNumber(oDoc) {
  3697. const tblfloors = $All(oDoc, '#content>table[class="grid"]');
  3698. for (let i = tblfloors.length-1; i >= 0; i--) {
  3699. const tbllast = tblfloors[i];
  3700. if (isFloorTable(tbllast)) {return getFloorNumber(tbllast);}
  3701. }
  3702.  
  3703. return null;
  3704. }
  3705. }
  3706.  
  3707. function correctFloorLink(floor) {
  3708. floor.hrefa.href = floor.href;
  3709. }
  3710.  
  3711. function addFavorite() {
  3712. // Create GUI
  3713. const spliter = $CrE('span');
  3714. const favorBtn = $CrE('span');
  3715. const favorChk = $CrE('input');
  3716. spliter.style.marginLeft = '1em';
  3717. favorBtn.innerText = TEXT_GUI_REVIEW_ADDFAVORITE;
  3718. favorBtn.classList.add(CLASSNAME_BUTTON);
  3719. favorChk.type = 'checkbox';
  3720. favorChk.checked = CONFIG.BkReviewPrefs.getConfig().favorites.hasOwnProperty(rid);
  3721. favorBtn.addEventListener('click', checkChange);
  3722. favorChk.addEventListener('change', checkChange);
  3723.  
  3724. headBars[0].appendChild(spliter);
  3725. headBars[0].appendChild(favorBtn);
  3726. headBars[0].appendChild(favorChk);
  3727.  
  3728. function checkChange(e) {
  3729. if (e && e.target === favorChk) {
  3730. destroyEvent(e);
  3731. }
  3732.  
  3733. let inFavorites;
  3734. const config = CONFIG.BkReviewPrefs.getConfig();
  3735. if (config.favorites.hasOwnProperty(rid)) {
  3736. delete config.favorites[rid];
  3737. inFavorites = false;
  3738. } else {
  3739. config.favorites[rid] = {
  3740. rid: rid,
  3741. name: title,
  3742. href: URL_REVIEWSHOW_3.replace('{R}', rid).replace('{A}', aid),
  3743. time: (new Date()).getTime(), // time added in version 1.6.7
  3744. tiptitle: null
  3745. };
  3746. inFavorites = true;
  3747. }
  3748. CONFIG.BkReviewPrefs.saveConfig(config);
  3749. setTimeout(() => {favorChk.checked = inFavorites;}, 0);
  3750. alertify.notify((inFavorites ? TEXT_GUI_REVIEW_FAVORADDED : TEXT_GUI_REVIEW_FAVORDELED).replace('{N}', title));
  3751. }
  3752.  
  3753. function updateFavorite() {
  3754. const config = CONFIG.BkReviewPrefs.getConfig();
  3755. if (config.favorites.hasOwnProperty(rid)) {
  3756. config.favorites[rid] = {
  3757. rid: rid,
  3758. name: title,
  3759. href: URL_REVIEWSHOW_3.replace('{R}', rid).replace('{A}', aid)
  3760. };
  3761. }
  3762. }
  3763. }
  3764.  
  3765. function addQuoteBtn(floor) {
  3766. const table = floor.table;
  3767. const numberEle = $(table, 'td.even div a');
  3768. const attr = numberEle.parentElement;
  3769. const btn = createQuoteBtn(attr);
  3770. const spliter = document.createTextNode(' | ');
  3771. attr.insertBefore(spliter, numberEle);
  3772. attr.insertBefore(btn, spliter);
  3773.  
  3774. function createQuoteBtn() {
  3775. // Get content textarea
  3776. const pcontent = $('#pcontent');
  3777. const form = $(`form[action^="https://${location.host}/modules/article/review"]`);
  3778.  
  3779. // Create button
  3780. const btn = $CrE('span');
  3781. btn.classList.add(CLASSNAME_BUTTON);
  3782. btn.addEventListener('click', quoteThisFloor);
  3783. btn.innerHTML = '引用';
  3784. const tip_panel = $CrE('div');
  3785. tip_panel.insertAdjacentText('afterbegin', '或者,');
  3786. const btn_qtnum = $CrE('span');
  3787. btn_qtnum.classList.add(CLASSNAME_BUTTON);
  3788. btn_qtnum.addEventListener('click', quoteFloorNum);
  3789. btn_qtnum.innerHTML = '仅引用序号';
  3790. tip_panel.appendChild(btn_qtnum);
  3791. const panel = tippy(btn, {
  3792. content: tip_panel,
  3793. theme: 'wenku_tip',
  3794. placement: 'top',
  3795. interactive: true,
  3796. });
  3797. return btn;
  3798.  
  3799. function quoteThisFloor() {
  3800. // In DOM Events, <this> keyword points to the Event Element.
  3801. const numberEle = $(this.parentElement, 'a[name]');
  3802. const numberText = numberEle.innerText;
  3803. const url = URL_REVIEWSHOW_4.replace('{R}', rid).replace('{P}', page).replace('{Y}', numberEle.name);
  3804. const contentEle = $(this.parentElement.parentElement, 'hr+div');
  3805. const content = getFloorContent(contentEle);
  3806. const insertPosition = pcontent.selectionEnd;
  3807. const text = pcontent.value;
  3808. const leftText = text.substr(0, insertPosition);
  3809. const rightText = text.substr(insertPosition);
  3810.  
  3811. // Create insert value
  3812. let insertValue = '[url=U]N[/url] [quote]Q[/quote]';
  3813. insertValue = insertValue.replace('U', url).replace('N', numberText).replace('Q', content);
  3814. // if not at the beginning of a line then insert a whitespace before the link
  3815. insertValue = ((leftText.length === 0 || /[ \r\n]$/.test(leftText)) ? '' : ' ') + insertValue;
  3816. // if not at the end of a line then insert a whitespace after the link
  3817. insertValue += (rightText.length === 0 || /^[ \r\n]/.test(rightText)) ? '' : ' ';
  3818.  
  3819. pcontent.value = leftText + insertValue + rightText;
  3820. const position = insertPosition + (pcontent.value.length - text.length);
  3821. form.scrollIntoView(); pcontent.focus(); pcontent.setSelectionRange(position, position);
  3822. }
  3823.  
  3824. function quoteFloorNum() {
  3825. // In DOM Events, <this> keyword points to the Event Element.
  3826. const numberEle = $(this.parentElement.parentElement.parentElement.parentElement.parentElement, 'a[name]');
  3827. const numberText = numberEle.innerText;
  3828. const url = URL_REVIEWSHOW_4.replace('{R}', rid).replace('{P}', page).replace('{Y}', numberEle.name);
  3829. const contentEle = $(this.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement, 'hr+div');
  3830. const insertPosition = pcontent.selectionEnd;
  3831. const text = pcontent.value;
  3832. const leftText = text.substr(0, insertPosition);
  3833. const rightText = text.substr(insertPosition);
  3834.  
  3835. // Create insert value
  3836. let insertValue = '[url=U]N[/url]';
  3837. insertValue = insertValue.replace('U', url).replace('N', numberText);
  3838. // if not at the beginning of a line then insert a whitespace before the link
  3839. insertValue = ((leftText.length === 0 || /[ \r\n]$/.test(leftText)) ? '' : ' ') + insertValue;
  3840. // if not at the end of a line then insert a whitespace after the link
  3841. insertValue += (rightText.length === 0 || /^[ \r\n]/.test(rightText)) ? '' : ' ';
  3842.  
  3843. pcontent.value = leftText + insertValue + rightText;
  3844. const position = insertPosition + (pcontent.value.length - text.length);
  3845. form.scrollIntoView(); pcontent.focus(); pcontent.setSelectionRange(position, position);
  3846. }
  3847. }
  3848. }
  3849.  
  3850. function addQueryBtn(floor) {
  3851. // Get container div
  3852. const div = floor.leftdiv;
  3853.  
  3854. // Create buttons
  3855. const qBtn = $CrE('a'); // Button for query reviews
  3856. const iBtn = $CrE('a'); // Button for query userinfo
  3857. const mBtn = $CrE('a'); // Button for edit user remark
  3858.  
  3859. // Get UID
  3860. const user = $(div, 'a');
  3861. const name = user.innerText;
  3862. const UID = Math.floor(user.href.match(/uid=(\d+)/)[1]);
  3863.  
  3864. // Create text spliter
  3865. const spliter = document.createTextNode(' | ');
  3866.  
  3867. // Config buttons
  3868. qBtn.href = URL_REVIEWSEARCH.replaceAll('{K}', String(UID));
  3869. iBtn.href = URL_USERINFO .replaceAll('{K}', String(UID));
  3870. mBtn.href = 'javascript: void(0);'
  3871. qBtn.target = '_blank';
  3872. iBtn.target = '_blank';
  3873. mBtn.addEventListener('click', editUserRemark.bind(null, UID, name, reloadRemarks));
  3874. qBtn.innerText = TEXT_GUI_USER_REVIEWSEARCH;
  3875. iBtn.innerText = TEXT_GUI_USER_USERINFO;
  3876. mBtn.innerText = TEXT_GUI_USER_USERREMARKEDIT;
  3877.  
  3878. // Append to GUI
  3879. div.appendChild($CrE('br'));
  3880. div.appendChild(iBtn);
  3881. div.appendChild(qBtn);
  3882. div.insertBefore(spliter, qBtn);
  3883. div.appendChild($CrE('br'));
  3884. div.appendChild(mBtn);
  3885.  
  3886. function reloadRemarks() {
  3887. const floors = getAllFloors();
  3888. floors.forEach((f) => (addRemark(f)));
  3889. }
  3890. }
  3891.  
  3892. function addRemark(floor) {
  3893. // Get container div
  3894. const div = floor.leftdiv;
  3895. const strong = $(div, 'strong');
  3896.  
  3897. // Get config
  3898. const config = CONFIG.RemarksConfig.getConfig();
  3899. const uid = Math.floor($(div, 'strong>a').href.match(/\?uid=(\d+)/)[1]);
  3900. const user = (config.user[uid] || {});
  3901.  
  3902. if ($(div, '.user-remark')) {
  3903. // Edit remark displayer
  3904. const name = $(div, '.user-remark-remark');
  3905. name.innerText = user.remark || TEXT_GUI_USER_USERREMARKEMPTY;
  3906. name.style.color = user.remark ? 'black' : 'grey';
  3907. } else {
  3908. // Add remark displayer
  3909. const container = $CrE('span');
  3910. const br = $CrE('br');
  3911. const name = $CrE('span');
  3912. container.classList.add('user-remark');
  3913. container.classList.add(CLASSNAME_TEXT);
  3914. container.innerText = TEXT_GUI_USER_USERREMARKSHOW;
  3915. name.innerText = user.remark || TEXT_GUI_USER_USERREMARKEMPTY;
  3916. name.style.color = user.remark ? 'black' : 'grey';
  3917. name.classList.add('user-remark-remark');
  3918. container.appendChild(name);
  3919. strong.insertAdjacentElement('afterend', br);
  3920. br.insertAdjacentElement('afterend', container);
  3921. }
  3922. }
  3923.  
  3924. // Provide a hidden function to reply overtime book-reviews
  3925. function addUnlock() {
  3926. listen();
  3927.  
  3928. function listen() {
  3929. if ($('#pcontent')) {return;}
  3930. const target = $('#content>table>caption+tbody>tr>td:nth-child(2)');
  3931. let count = 0;
  3932. document.addEventListener('click', function hidden_unlocker(e) {
  3933. e.target === target ? count++ : (count = 0);
  3934. count >= 10 && add();
  3935. count >= 10 && document.removeEventListener('click', hidden_unlocker);
  3936. count >= 10 && (target.innerHTML = TEXT_GUI_REVIEW_UNLOCK_WARNING);
  3937. });
  3938. }
  3939.  
  3940. function add() {
  3941. const container = $CrE('div');
  3942. $('#content').appendChild(container);
  3943. makeEditor(container, rid, aid);
  3944. }
  3945. }
  3946.  
  3947. // Reply without refreshing the document
  3948. function hookReply() {
  3949. const form = $('form[name="frmreview"]');
  3950. const onsubmit = form.onsubmit;
  3951. form.onsubmit = function() {
  3952. const title = $(form, '#ptitle').value;
  3953. const content = $(form, '#pcontent').value;
  3954. (onsubmit ? onsubmit() : true) && sendReviewReply({
  3955. rid: rid, title: title, content: content,
  3956. onload: function(oDoc) {
  3957. // Make floor(s)
  3958. },
  3959. onerror: function(e) {
  3960. DoLog(LogLevel.Error, 'pageReview/hookReply: submit onerror.');
  3961. }
  3962. });
  3963. };
  3964. }
  3965.  
  3966. function downloader() {
  3967. // GUI
  3968. const pageCountText = $('#pagelink>.last').href.match(/page=(\d+)/)[1];
  3969. const lefta = $(headBars[0], 'a');
  3970. const lefttext = document.createTextNode('书评回复');
  3971. clearChildnodes(headBars[0]);
  3972. headBars[0].appendChild(lefta);
  3973. headBars[0].appendChild(lefttext);
  3974. headBars[0].width = '45%';
  3975. headBars[1].width = '55%';
  3976.  
  3977. const saveBtn = $CrE('span');
  3978. saveBtn.innerText = TEXT_GUI_DOWNLOAD_REVIEW.replaceAll('A', pageCountText);
  3979. saveBtn.classList.add(CLASSNAME_BUTTON);
  3980. saveBtn.addEventListener('click', downloadWholePost);
  3981. headBars[1].appendChild(saveBtn);
  3982.  
  3983. const spliter = $CrE('span');
  3984. const bbcdTxt = $CrE('span');
  3985. const bbcdChk = $CrE('input');
  3986. spliter.style.marginLeft = '1em';
  3987. bbcdTxt.innerText = TEXT_GUI_DOWNLOAD_BBCODE;
  3988. bbcdChk.type = 'checkbox';
  3989. bbcdChk.checked = CONFIG.BkReviewPrefs.getConfig().bbcode;
  3990. bbcdTxt.addEventListener('click', bbcodeOnclick);
  3991. bbcdChk.addEventListener('click', bbcodeOnclick);
  3992. settip(bbcdTxt, TEXT_TIP_DOWNLOAD_BBCODE);
  3993. settip(bbcdChk, TEXT_TIP_DOWNLOAD_BBCODE);
  3994. bbcdTxt.classList.add(CLASSNAME_BUTTON);
  3995. headBars[1].appendChild(spliter);
  3996. headBars[1].appendChild(bbcdTxt);
  3997. headBars[1].appendChild(bbcdChk);
  3998.  
  3999. function bbcodeOnclick(e) {
  4000. destroyEvent(e);
  4001.  
  4002. if (downloadWholePost.working) {
  4003. alertify.warning(TEXT_ALT_DOWNLOAD_BBCODE_NOCHANGE);
  4004. return false;
  4005. }
  4006. const cmConfig = CONFIG.BkReviewPrefs.getConfig();
  4007. cmConfig.bbcode = !cmConfig.bbcode;
  4008. setTimeout(() => {bbcdChk.checked = cmConfig.bbcode;}, 0);
  4009. CONFIG.BkReviewPrefs.saveConfig(cmConfig);
  4010. }
  4011.  
  4012. // ## Function: Get data from page document or join it into the given data variable ##
  4013. function getDataFromPage(document, data) {
  4014. let i;
  4015. DoLog(LogLevel.Info, document, true);
  4016.  
  4017. // Get Floors; avatars uses for element locating
  4018. const main = $(document, '#content');
  4019. const avatars = $All(main, 'table div img.avatar');
  4020.  
  4021. // init data, floors and users if need
  4022. let floors = {}, users = {};
  4023. if (data) {
  4024. floors = data.floors;
  4025. users = data.users;
  4026. } else {
  4027. data = {};
  4028. initData(data, floors, users);
  4029. }
  4030. for (i = 0; i < avatars.length; i++) {
  4031. const floor = newFloor(floors, avatars, i);
  4032. const elements = getFloorElements(floor);
  4033. const reply = getFloorReply(floor);
  4034. const user = getFloorUser(floor);
  4035. appendFloor(floors, floor);
  4036. }
  4037. return data;
  4038.  
  4039. function initData(data, floors, users) {
  4040. // data vars
  4041. data.floors = floors; floors.data = data;
  4042. data.users = users; users.data = data;
  4043.  
  4044. // review info
  4045. data.link = location.href;
  4046. data.id = getUrlArgv({name: 'rid', dealFunc: Number, defaultValue: 0});
  4047. data.page = getUrlArgv({name: 'page', dealFunc: Number, defaultValue: 1});
  4048. data.title = $(main, 'th strong').innerText;
  4049. return data;
  4050. }
  4051.  
  4052. function newFloor(floors, avatars, i) {
  4053. const floor = {};
  4054. floor.avatar = avatars[i];
  4055. floor.floors = floors;
  4056. return floor;
  4057. }
  4058.  
  4059. function getFloorElements(floor) {
  4060. const elements = {}; floor.elements = elements;
  4061. elements.avatar = floor.avatar;
  4062. elements.table = elements.avatar.parentElement.parentElement.parentElement.parentElement.parentElement;
  4063. elements.tr = $(elements.table, 'tr');
  4064. elements.tdUser = $(elements.table, 'td.odd');
  4065. elements.tdReply = $(elements.table, 'td.even');
  4066. elements.divUser = $(elements.tdUser, 'div');
  4067. elements.aUser = $(elements.divUser, 'a');
  4068. elements.attr = $(elements.tdReply, 'div a').parentElement;
  4069. elements.time = elements.attr.childNodes[0];
  4070. elements.number = $(elements.attr, 'a[name]');
  4071. elements.title = $(elements.tdReply, 'div>strong');
  4072. elements.content = $(elements.tdReply, 'hr+div');
  4073. return elements;
  4074. }
  4075.  
  4076. function getFloorReply(floor) {
  4077. const elements = floor.elements;
  4078. const reply = {}; floor.reply = reply;
  4079. reply.time = elements.time.nodeValue.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/)[0];
  4080. reply.number = Number(elements.number.innerText.match(/\d+/)[0]);
  4081. reply.value = CONFIG.BkReviewPrefs.getConfig().bbcode ? getFloorContent(elements.content, true) : elements.content.innerText;
  4082. reply.title = elements.title.innerText;
  4083. return reply;
  4084. }
  4085.  
  4086. function getFloorUser(floor) {
  4087. const elements = floor.elements;
  4088. const user = {}; floor.user = user;
  4089. user.id = elements.aUser.href.match(/uid=(\d+)/)[1];
  4090. user.name = elements.aUser.innerText;
  4091. user.avatar = elements.avatar.src;
  4092. user.link = elements.aUser.href;
  4093. user.jointime = elements.divUser.innerText.match(/\d{4}-\d{2}-\d{2}/)[0];
  4094.  
  4095. const data = floor.floors.data; const users = data.users;
  4096. if (!users.hasOwnProperty(user.id)) {
  4097. users[user.id] = user;
  4098. user.floors = [floor];
  4099. } else {
  4100. const uFloors = users[user.id].floors;
  4101. uFloors.push(floor);
  4102. sortUserFloors(uFloors);
  4103. }
  4104. return user;
  4105. }
  4106.  
  4107. function sortUserFloors(uFloors) {
  4108. uFloors.sort(function(F1, F2) {
  4109. return F1.reply.number - F2.reply.number;
  4110. })
  4111. }
  4112.  
  4113. function appendFloor(floors, floor) {
  4114. floors[floor.reply.number-1] = floor;
  4115. }
  4116. }
  4117.  
  4118. // ## Function: Get pages and parse each page to a data, returns data ##
  4119. // callback(data, gotcount, finished) is called when xhr and parsing completed
  4120. function getAllPages(callback) {
  4121. let i, data, gotcount = 0;
  4122. const ridMatcher = /rid=(\d+)/, pageMatcher = /page=(\d+)/;
  4123. const lastpageUrl = $('#pagelink>.last').href;
  4124. const rid = Number(lastpageUrl.match(ridMatcher)[1]);
  4125. const pageCount = Number(lastpageUrl.match(pageMatcher)[1]);
  4126. const curPageNum = location.href.match(pageMatcher) ? Number(location.href.match(pageMatcher)[1]) : 1;
  4127.  
  4128. for (i = 1; i <= pageCount; i++) {
  4129. const url = lastpageUrl.replace(pageMatcher, 'page='+String(i));
  4130. getDocument(url, joinPageData, callback);
  4131. }
  4132.  
  4133. function joinPageData(pageDocument, callback) {
  4134. data = getDataFromPage(pageDocument, data);
  4135. gotcount++;
  4136.  
  4137. // log
  4138. const level = gotcount % NUMBER_LOGSUCCESS_AFTER ? LogLevel.Info : LogLevel.Success;
  4139. DoLog(level, 'got ' + String(gotcount) + ' pages.');
  4140. if (gotcount === pageCount) {
  4141. DoLog(LogLevel.Success, 'All pages xhr and parsing completed.');
  4142. DoLog(LogLevel.Success, data, true);
  4143. }
  4144.  
  4145. // callback
  4146. if (callback) {callback(data, gotcount, gotcount === pageCount);};
  4147. }
  4148. }
  4149.  
  4150. // Function output
  4151. function joinTXT(data, noSpliter=true) {
  4152. const floors = data.floors; const users = data.users;
  4153.  
  4154. // HEAD META DATA
  4155. const saveTime = getTime();
  4156. const head = TEXT_OUTPUT_REVIEW_HEAD
  4157. .replaceAll('{RWID}', data.id).replaceAll('{RWTT}', data.title).replaceAll('{RWLK}', data.link)
  4158. .replaceAll('{SVTM}', saveTime).replaceAll('{SCNM}', GM_info.script.name)
  4159. .replaceAll('{VRSN}', GM_info.script.version).replaceAll('{ATNM}', GM_info.script.author);
  4160.  
  4161. // join userinfos
  4162. let userText = '';
  4163. for (const [pname, user] of Object.entries(users)) {
  4164. if (!isNumeric(pname)) {continue;};
  4165. userText += TEXT_OUTPUT_REVIEW_USER
  4166. .replaceAll('{LNSPLT}', noSpliter ? '' : TEXT_SPLIT_LINE).replaceAll('{USERNM}', user.name)
  4167. .replaceAll('{USERID}', user.id).replaceAll('{USERJT}', user.jointime)
  4168. .replaceAll('{USERLK}', user.link).replaceAll('{USERFL}', user.floors[0].reply.number);
  4169. userText += '\n'.repeat(2);
  4170. }
  4171.  
  4172. // join floors
  4173. let floorText = '';
  4174. for (const [pname, floor] of Object.entries(floors)) {
  4175. if (!isNumeric(pname)) {continue;};
  4176. const avatar = floor.avatar; const elements = floor.elements; const user = floor.user; const reply = floor.reply;
  4177. floorText += TEXT_OUTPUT_REVIEW_FLOOR
  4178. .replaceAll('{LNSPLT}', noSpliter ? '' : TEXT_SPLIT_LINE).replaceAll('{RPNUMB}', String(reply.number))
  4179. .replaceAll('{RPTIME}', reply.time).replaceAll('{USERNM}', user.name)
  4180. .replaceAll('{USERID}', user.id).replaceAll('{RPTEXT}', reply.value);
  4181. floorText += '\n'.repeat(2);
  4182. }
  4183.  
  4184. // End
  4185. const foot = TEXT_OUTPUT_REVIEW_END;
  4186.  
  4187. // return
  4188. const txt = head + '\n'.repeat(2) + userText + '\n'.repeat(2) + floorText + '\n'.repeat(2) + foot;
  4189. return txt;
  4190. }
  4191.  
  4192. // ## Function: Download the whole post ##
  4193. function downloadWholePost() {
  4194. // Continues only if not working
  4195. if (downloadWholePost.working) {return;};
  4196. downloadWholePost.working = true;
  4197. bbcdTxt.classList.add(CLASSNAME_DISABLED);
  4198.  
  4199. // GUI
  4200. saveBtn.innerText = TEXT_GUI_DOWNLOADING_REVIEW
  4201. .replaceAll('C', '0').replaceAll('A', pageCountText);
  4202.  
  4203. // go work!
  4204. getAllPages(function(data, gotCount, finished) {
  4205. // GUI
  4206. saveBtn.innerText = TEXT_GUI_DOWNLOADING_REVIEW
  4207. .replaceAll('C', String(gotCount)).replaceAll('A', pageCountText);
  4208.  
  4209. // Stop here if not completed
  4210. if (!finished) {return;};
  4211.  
  4212. // Join text
  4213. const TXT = joinTXT(data);
  4214.  
  4215. // Download
  4216. const blob = new Blob([TXT],{type:"text/plain;charset=utf-8"});
  4217. const url = URL.createObjectURL(blob);
  4218. const name = '文库贴 - ' + data.title + ' - ' + data.id.toString() + '.txt';
  4219.  
  4220. const a = $CrE('a');
  4221. a.href = url;
  4222. a.download = name;
  4223. a.click();
  4224.  
  4225. // GUI
  4226. saveBtn.innerText = TEXT_GUI_DOWNLOADFINISH_REVIEW;
  4227. alertify.success(TEXT_ALT_DOWNLOADFINISH_REVIEW.replaceAll('{T}', data.title).replaceAll('{I}', data.id).replaceAll('{N}', name));
  4228.  
  4229. // Work finish
  4230. downloadWholePost.working = false;
  4231. bbcdTxt.classList.remove(CLASSNAME_DISABLED);
  4232. })
  4233. }
  4234. }
  4235.  
  4236. // Get all floor object
  4237. /* Contains:
  4238. ** floor.table
  4239. ** floor.tbody
  4240. ** floor.tr
  4241. ** floor.lefttd
  4242. ** floor.righttd
  4243. ** floor.leftdiv
  4244. ** floor.titlediv
  4245. ** floor.titlestrong
  4246. ** floor.metadiv
  4247. ** floor.replydiv
  4248. */
  4249. function getAllFloors(parent=main) {
  4250. const avatars = $All(parent, 'table div img.avatar');
  4251. const floors = [];
  4252. for (const avt of avatars) {
  4253. const floor = {};
  4254. floor.leftdiv = avt.parentElement;
  4255. floor.lefttd = floor.leftdiv.parentElement;
  4256. floor.tr = floor.lefttd.parentElement
  4257. floor.righttd = floor.tr.children[1];
  4258. floor.titlediv = floor.righttd.children[0];
  4259. floor.titlestrong = floor.titlediv.children[0];
  4260. floor.metadiv = floor.righttd.children[1];
  4261. floor.replydiv = floor.righttd.children[3];
  4262. floor.hrefa = $(floor.metadiv, 'a[name]');
  4263. floor.tbody = floor.tr.parentElement;
  4264. floor.table = floor.tbody.parentElement;
  4265. floor.rid = Number(getUrlArgv({url: parent.url || location.href, name: 'rid'}));
  4266. floor.aid = Number($(parent, 'td[width]>a').href.match(/(\d+)\.html?$/)[1]);
  4267. floor.page = Number($(avt.ownerDocument, '#pagelink strong').innerText);
  4268. floor.pagehref = URL_REVIEWSHOW.replace('{R}', floor.rid.toString()).replace('{A}', floor.aid.toString()).replace('{P}', floor.page.toString());
  4269. floor.href = URL_REVIEWSHOW_5.replace('{R}', floor.rid.toString()).replace('{A}', floor.aid.toString()).replace('{P}', floor.page.toString()).replace('{Y}', floor.hrefa.name);
  4270. floors.push(floor);
  4271. }
  4272. return floors;
  4273. }
  4274.  
  4275. // Validate a <table> element whether is a floor
  4276. function isFloorTable(tbl) {
  4277. return $(tbl, 'a[href*="#yid"][name^="yid"]') ? true : false;
  4278. }
  4279.  
  4280. // Get floor title element (<strong>)
  4281. // Argv: <table> element of the floor
  4282. function getEleFloorTitle(tblfloor) {
  4283. return $(tblfloor, 'td.even>div:first-child>strong'); // or :nth-child(1)
  4284. }
  4285.  
  4286. // Get floor content element (<div>)
  4287. // Argv: <table> element of the floor
  4288. function getEleFloorContent(tblfloor) {
  4289. return $(tblfloor, 'td.even>hr+div');
  4290. }
  4291.  
  4292. // Get the floor number
  4293. // Argv: <table> element of the floor
  4294. function getFloorNumber(tblfloor) {
  4295. const eleNumber = $(tblfloor, 'a[name^="yid"]');
  4296. return eleNumber ? Number(eleNumber.innerText.match(/\d+/)[0]) : false;
  4297. }
  4298.  
  4299. // Get floor content by BBCode format (content only, no title)
  4300. // Argv: <div> content Element
  4301. function getFloorContent(contentEle, original=false) {
  4302. const subNodes = contentEle.childNodes;
  4303. let content = '';
  4304.  
  4305. for (const node of subNodes) {
  4306. const type = node.nodeName;
  4307. switch (type) {
  4308. case '#text':
  4309. // Prevent 'Quote:' repeat
  4310. content += node.data.replace(/^\s*Quote:\s*$/, ' ');
  4311. break;
  4312. case 'IMG':
  4313. // wenku8 has forbidden [img] tag for secure reason (preventing CSRF)
  4314. //content += '[img]S[/img]'.replace('S', node.src);
  4315. content += original ? '[img]S[/img]'.replace('S', node.src) : ' S '.replace('S', node.src);
  4316. break;
  4317. case 'A':
  4318. content += '[url=U]T[/url]'.replace('U', node.getAttribute('href')).replace('T', getFloorContent(node));
  4319. break;
  4320. case 'BR':
  4321. // no need to add \n, because \n will be preserved in #text nodes
  4322. //content += '\n';
  4323. break;
  4324. case 'DIV':
  4325. if (node.classList.contains('jieqiQuote')) {
  4326. content += getTagedSubcontent('quote', node);
  4327. } else if (node.classList.contains('jieqiCode')) {
  4328. content += getTagedSubcontent('code', node);
  4329. } else if (node.classList.contains('divimage')) {
  4330. content += getFloorContent(node, original);
  4331. } else {
  4332. content += getFloorContent(node, original);
  4333. }
  4334. break;
  4335. case 'CODE': content += getFloorContent(node, original); break; // Just ignore
  4336. case 'PRE': content += getFloorContent(node, original); break; // Just ignore
  4337. case 'SPAN': content += getFontedSubcontent(node); break; // Size and color
  4338. case 'P': content += getFontedSubcontent(node); break; // Text Align
  4339. case 'B': content += getTagedSubcontent('b', node); break;
  4340. case 'I': content += getTagedSubcontent('i', node); break;
  4341. case 'U': content += getTagedSubcontent('u', node); break;
  4342. case 'DEL': content += getTagedSubcontent('d', node); break;
  4343. default: content += getFloorContent(node, original); break;
  4344. /*
  4345. case 'SPAN':
  4346. subContent = getFloorContent(node);
  4347. size = node.style.fontSize.match(/\d+/) ? node.style.fontSize.match(/\d+/)[0] : '';
  4348. color = node.style.color.match(/rgb\((\d+), ?(\d+), ?(\d+)\)/);
  4349. break;
  4350. */
  4351. }
  4352. }
  4353. return content;
  4354.  
  4355. function getTagedSubcontent(tag, node) {
  4356. const subContent = getFloorContent(node, original);
  4357. return '[{T}]{S}[/{T}]'.replaceAll('{T}', tag).replaceAll('{S}', subContent);
  4358. }
  4359.  
  4360. function getFontedSubcontent(node) {
  4361. let tag, value;
  4362.  
  4363. let strSize = node.style.fontSize.match(/\d+/);
  4364. let strColor = node.style.color;
  4365. let strAlign = node.align;
  4366. strSize = strSize ? strSize[0] : null;
  4367. strColor = strColor ? rgbToHex.apply(null, strColor.match(/\d+/g)) : null;
  4368.  
  4369. tag = tag || (strSize ? 'size' : null);
  4370. tag = tag || (strColor ? 'color' : null);
  4371. tag = tag || (strAlign ? 'align' : null);
  4372. value = value || strSize || null;
  4373. value = value || strColor || null;
  4374. value = value || strAlign || null;
  4375.  
  4376. const subContent = getFloorContent(node, original);
  4377. if (tag && value) {
  4378. return '[{T}={V}]{S}[/{T}]'.replaceAll('{T}', tag).replaceAll('{V}', value).replaceAll('{S}', subContent);
  4379. } else {
  4380. return subContent;
  4381. }
  4382. }
  4383. }
  4384.  
  4385. // Append floor to #content
  4386. function appendfloor(floor) {
  4387. // Append
  4388. const table = floor.table;
  4389. const elmafter = $(main, 'table.grid+table[border]');
  4390. main.insertBefore(table, elmafter);
  4391.  
  4392. // Enhances
  4393. correctFloorLink(floor);
  4394. alinkEdit(floor);
  4395. addQuoteBtn(floor);
  4396. addQueryBtn(floor);
  4397. addRemark(floor);
  4398. alinktofloor(floor.table);
  4399. }
  4400. }
  4401.  
  4402. // Bookcase page add-on
  4403. function pageBookcase() {
  4404. // Get auto-recommend config
  4405. let arConfig = CONFIG.AutoRecommend.getConfig();
  4406. // Get bookcase lists
  4407. const bookCaseURL = `https://${location.host}/modules/article/bookcase.php?classid={CID}`;
  4408. const content = $('#content');
  4409. const selector = $('[name="classlist"]');
  4410. const options = selector.children;
  4411. // Current bookcase
  4412. const curForm = $(content, '#checkform');
  4413. const curClassid = Number($('[name="clsssid"]').value);
  4414. // Init bookcase config if need
  4415. initPreferences();
  4416. const bookcases = CONFIG.bookcasePrefs.getConfig().bookcases;
  4417. addTopTitle();
  4418. decorateForm(curForm, bookcases[curClassid]);
  4419.  
  4420. // gowork
  4421. laterReads();
  4422. showBookcases();
  4423. recommendAllGUI();
  4424.  
  4425. function recommendAllGUI() {
  4426. const block = createWenkuBlock({
  4427. type: 'mypage',
  4428. parent: '#left',
  4429. title: TEXT_GUI_BOOKCASE_ATRCMMD,
  4430. items: [
  4431. {innerHTML: arConfig.allCount === 0 ? TEXT_GUI_BOOKCASE_RCMMDNW_NOTASK : (TASK.AutoRecommend.checkRcmmd() ? TEXT_GUI_BOOKCASE_RCMMDNW_DONE : TEXT_GUI_BOOKCASE_RCMMDNW_NOTYET), id: 'arstatus'},
  4432. {innerHTML: TEXT_GUI_BOOKCASE_RCMMDAT, id: 'autorcmmd'},
  4433. {innerHTML: TEXT_GUI_BOOKCASE_RCMMDNW, id: 'rcmmdnow'}
  4434. ]
  4435. });
  4436.  
  4437. // Configure buttons
  4438. const ulitm = $(block, '.ulitem');
  4439. const txtst = $(block, '#arstatus');
  4440. const btnAR = $(block, '#autorcmmd');
  4441. const btnRN = $(block, '#rcmmdnow');
  4442. const txtAR = $(block, 'span');
  4443. const checkbox = $CrE('input');
  4444. txtst.classList.add(CLASSNAME_TEXT);
  4445. btnAR.classList.add(CLASSNAME_BUTTON);
  4446. btnRN.classList.add(CLASSNAME_BUTTON);
  4447. checkbox.type = 'checkbox';
  4448. checkbox.checked = arConfig.auto;
  4449. checkbox.addEventListener('click', onclick);
  4450. btnAR.addEventListener('click', onclick);
  4451. btnAR.appendChild(checkbox);
  4452. btnRN.addEventListener('click', rcmmdnow);
  4453.  
  4454. function onclick(e) {
  4455. destroyEvent(e);
  4456. arConfig.auto = !arConfig.auto;
  4457. setTimeout(function() {checkbox.checked = arConfig.auto;}, 0);
  4458. CONFIG.AutoRecommend.saveConfig(arConfig);
  4459. alertify.notify(arConfig.auto ? TEXT_ALT_ATRCMMDS_AUTO : TEXT_ALT_ATRCMMDS_NOAUTO);
  4460. }
  4461.  
  4462. function rcmmdnow() {
  4463. if (TASK.AutoRecommend.checkRcmmd() && !confirm(TEXT_GUI_BOOKCASE_RCMMDNW_CONFIRM)) {return false;}
  4464. if (arConfig.allCount === 0) {alertify.warning(TEXT_ALT_ATRCMMDS_NOTASK); return false;};
  4465. TASK.AutoRecommend.run(true);
  4466. }
  4467. }
  4468.  
  4469. function initPreferences() {
  4470. const config = CONFIG.bookcasePrefs.getConfig();
  4471. if (config.bookcases.length === 0) {
  4472. for (const option of options) {
  4473. config.bookcases.push({
  4474. classid: Number(option.value),
  4475. url: bookCaseURL.replace('{CID}', String(option.value)),
  4476. name: option.innerText
  4477. });
  4478. }
  4479. CONFIG.bookcasePrefs.saveConfig(config);
  4480. }
  4481. }
  4482.  
  4483. function addTopTitle() {
  4484. // Clone title bar
  4485. const checkform = $('#checkform') ? $('#checkform') : $('.'+CLASSNAME_BOOKCASE_FORM);
  4486. const oriTitle = $(checkform, 'div.gridtop');
  4487. const topTitle = oriTitle.cloneNode(true);
  4488. content.insertBefore(topTitle, checkform);
  4489.  
  4490. // Hide bookcase selector
  4491. const bcSelector = $(topTitle, '[name="classlist"]');
  4492. bcSelector.style.display = 'none';
  4493.  
  4494. // Write title text
  4495. const textNode = topTitle.childNodes[0];
  4496. const numMatch = textNode.nodeValue.match(/\d+/g);
  4497. const text = TEXT_GUI_BOOKCASE_TOPTITLE.replace('A', numMatch[0]).replace('B', numMatch[1]);
  4498. textNode.nodeValue = text;
  4499. }
  4500.  
  4501. function showBookcases() {
  4502. // GUI
  4503. const topTitle = $(content, 'script+div.gridtop');
  4504. const textNode = topTitle.childNodes[0];
  4505. const oriTitleText = textNode.nodeValue;
  4506. const allCount = bookcases.length;
  4507. let finished = 1;
  4508. textNode.nodeValue = TEXT_GUI_BOOKCASE_GETTING.replace('C', String(finished)).replace('A', String(allCount));
  4509.  
  4510. // Get all bookcase pages
  4511. for (const bookcase of bookcases) {
  4512. if (bookcase.classid === curClassid) {continue;};
  4513. getDocument(bookcase.url, appendBookcase, [bookcase]);
  4514. }
  4515.  
  4516. function appendBookcase(mDOM, bookcase) {
  4517. const classid = bookcase.classid;
  4518.  
  4519. // Get bookcase form and modify it
  4520. const form = $(mDOM, '#checkform');
  4521. form.parentElement.removeChild(form);
  4522.  
  4523. // Find the right place to insert it in
  4524. const forms = $All(content, '.'+CLASSNAME_BOOKCASE_FORM);
  4525. for (let i = 0; i < forms.length; i++) {
  4526. const thisForm = forms[i];
  4527. const cid = typeof thisForm.classid === 'number' ? thisForm.classid : curClassid;
  4528. if (cid > classid) {
  4529. content.insertBefore(form, thisForm);
  4530. break;
  4531. }
  4532. }
  4533. if(!form.parentElement) {$('#laterbooks').insertAdjacentElement('beforebegin', form);};
  4534.  
  4535. // Decorate
  4536. decorateForm(form, bookcase);
  4537.  
  4538. // finished increase
  4539. finished++;
  4540. textNode.nodeValue = finished < allCount ?
  4541. TEXT_GUI_BOOKCASE_GETTING.replace('C', String(finished)).replace('A', String(allCount)) :
  4542. oriTitleText;
  4543. }
  4544. }
  4545.  
  4546. function decorateForm(form, bookcase) {
  4547. const classid = bookcase.classid;
  4548. let name = bookcase.name;
  4549.  
  4550. // Provide auto-recommand button
  4551. arBtn();
  4552.  
  4553. // Modify properties
  4554. form.classList.add(CLASSNAME_BOOKCASE_FORM);
  4555. form.id += String(classid);
  4556. form.classid = classid;
  4557. form.onsubmit = my_check_confirm;
  4558.  
  4559. // Hide bookcase selector
  4560. const bcSelector = $(form, '[name="classlist"]');
  4561. bcSelector.style.display = 'none';
  4562.  
  4563. // Dblclick Change title
  4564. const titleBar = bcSelector.parentElement;
  4565. titleBar.childNodes[0].nodeValue = name;
  4566. titleBar.addEventListener('dblclick', editName);
  4567. // Longpress Change title for mobile
  4568. let touchTimer;
  4569. titleBar.addEventListener('touchstart', () => {touchTimer = setTimeout(editName, 500);});
  4570. titleBar.addEventListener('touchmove', () => {clearTimeout(touchTimer);});
  4571. titleBar.addEventListener('touchend', () => {clearTimeout(touchTimer);});
  4572. titleBar.addEventListener('mousedown', () => {touchTimer = setTimeout(editName, 500);});
  4573. titleBar.addEventListener('mouseup', () => {clearTimeout(touchTimer);});
  4574.  
  4575. // Show tips
  4576. let tip = TEXT_GUI_BOOKCASE_DBLCLICK;
  4577. if (tipready) {
  4578. // tipshow and tiphide is coded inside wenku8 itself, its function is to show a text tip besides the mouse
  4579. titleBar.addEventListener('mouseover', function() {tipshow(tip);});
  4580. titleBar.addEventListener('mouseout' , tiphide);
  4581. } else {
  4582. titleBar.title = tip;
  4583. }
  4584.  
  4585. // Change selector names
  4586. renameSelectors(false);
  4587.  
  4588. // Replaces the original check_confirm() function
  4589. function my_check_confirm() {
  4590. const checkform = this;
  4591. let checknum = 0;
  4592. for (let i = 0; i < checkform.elements.length; i++){
  4593. if (checkform.elements[i].name == 'checkid[]' && checkform.elements[i].checked == true) checknum++;
  4594. }
  4595. if (checknum === 0){
  4596. alert('请先选择要操作的书目!');
  4597. return false;
  4598. }
  4599. const newclassid = $(checkform, '#newclassid');
  4600. if(newclassid.value == -1){
  4601. if (confirm('确实要将选中书目移出书架么?')) {return true;} else {return false;};
  4602. } else {
  4603. return true;
  4604. }
  4605. }
  4606.  
  4607. // Selector name refresh
  4608. function renameSelectors(renameAll) {
  4609. if (renameAll) {
  4610. const forms = $All(content, '.'+CLASSNAME_BOOKCASE_FORM);
  4611. for (const form of forms) {
  4612. renameFormSlctr(form);
  4613. }
  4614. } else {
  4615. renameFormSlctr(form);
  4616. }
  4617.  
  4618. function renameFormSlctr(form) {
  4619. const newclassid = $(form, '#newclassid');
  4620. const options = newclassid.children;
  4621. for (let i = 0; i < options.length; i++) {
  4622. const option = options[i];
  4623. const value = Number(option.value);
  4624. const bc = bookcases[value];
  4625. bc ? option.innerText = TEXT_GUI_BOOKCASE_MOVEBOOK.replace('N', bc.name) : function(){};
  4626. }
  4627. }
  4628. }
  4629.  
  4630. // Provide <input> GUI to edit bookcase name
  4631. function editName() {
  4632. const nameInput = $CrE('input');
  4633. const form = this;
  4634. tip = TEXT_GUI_BOOKCASE_WHATNAME;
  4635. tipready ? tipshow(tip) : function(){};
  4636.  
  4637. titleBar.childNodes[0].nodeValue = '';
  4638. titleBar.appendChild(nameInput);
  4639. nameInput.value = name;
  4640. nameInput.addEventListener('blur', onblur);
  4641. nameInput.addEventListener('keydown', onkeydown)
  4642. nameInput.focus();
  4643. nameInput.setSelectionRange(0, name.length);
  4644.  
  4645. function onblur() {
  4646. tip = TEXT_GUI_BOOKCASE_DBLCLICK;
  4647. tipready ? tipobj.innerHTML = tip : function(){};
  4648. const value = nameInput.value.trim();
  4649. if (value) {
  4650. name = value;
  4651. bookcase.name = name;
  4652. CONFIG.bookcasePrefs.saveConfig(bookcases);
  4653. }
  4654. titleBar.childNodes[0].nodeValue = name;
  4655. try {titleBar.removeChild(nameInput)} catch (DOMException) {};
  4656. renameSelectors(true);
  4657. }
  4658.  
  4659. function onkeydown(e) {
  4660. if (e.keyCode === 13) {
  4661. e.preventDefault();
  4662. onblur();
  4663. }
  4664. }
  4665. }
  4666.  
  4667. // Provide auto-recommend option
  4668. function arBtn() {
  4669. const table = $(form, 'table');
  4670. for (const tr of $All(table, 'tr')) {
  4671. $(tr, '.odd') ? decorateRow(tr) : function() {};
  4672. $(tr, 'th') ? decorateHeader(tr) : function() {};
  4673. $(tr, 'td.foot') ? decorateFooter(tr) : function() {};
  4674. }
  4675.  
  4676. // Insert auto-recommend option for given row
  4677. function decorateRow(tr) {
  4678. const eleBookLink = $(tr, 'td:nth-child(2)>a');
  4679. const strBookID = eleBookLink.href.match(/aid=(\d+)/)[1];
  4680. const strBookName = eleBookLink.innerText;
  4681. const newTd = $CrE('td');
  4682. const input = $CrE('input');
  4683. newTd.classList.add('odd');
  4684. input.type = 'number';
  4685. input.inputmode = 'numeric';
  4686. input.style.width = '85%';
  4687. input.value = arConfig.books[strBookID] ? String(arConfig.books[strBookID].number) : '0';
  4688. input.addEventListener('change', onvaluechange);
  4689. input.strBookID = strBookID; input.strBookName = strBookName;
  4690. newTd.appendChild(input); tr.appendChild(newTd);
  4691. }
  4692.  
  4693. // Insert a new row for auto-recommend options
  4694. function decorateHeader(tr) {
  4695. const allTh = $All(tr, 'th');
  4696. const width = ARR_GUI_BOOKCASE_WIDTH;
  4697. const newTh = $CrE('th');
  4698. newTh.innerText = TEXT_GUI_BOOKCASE_ATRCMMD;
  4699. newTh.classList.add(CLASSNAME_TEXT);
  4700. tr.appendChild(newTh);
  4701. for (let i = 0; i < allTh.length; i++) {
  4702. const th = allTh[i];
  4703. th.style.width = width[i];
  4704. }
  4705. }
  4706.  
  4707. // Fit the width
  4708. function decorateFooter(tr) {
  4709. const td = $(tr, 'td.foot');
  4710. td.colSpan = ARR_GUI_BOOKCASE_WIDTH.length;
  4711. }
  4712.  
  4713. // auto-recommend onvaluechange
  4714. function onvaluechange(e) {
  4715. arConfig = CONFIG.AutoRecommend.getConfig();
  4716. const input = e.target;
  4717. const value = input.value;
  4718. const strBookID = input.strBookID;
  4719. const strBookName = input.strBookName;
  4720. const bookID = Number(strBookID);
  4721. const userDetail = getMyUserDetail() ? getMyUserDetail().userDetail : refreshMyUserDetail();
  4722. if (isNumeric(value, true) && Number(value) >= 0) {
  4723. // allCount increase
  4724. const oriNum = arConfig.books[strBookID] ? arConfig.books[strBookID].number : 0;
  4725. const number = Number(value);
  4726. arConfig.allCount += number - oriNum;
  4727.  
  4728. // save to config
  4729. number > 0 ? arConfig.books[strBookID] = {number: number, name: strBookName, id: bookID} : delete arConfig.books[strBookID];
  4730. CONFIG.AutoRecommend.saveConfig(arConfig);
  4731.  
  4732. // alert
  4733. alertify.notify(
  4734. TEXT_ALT_ATRCMMDS_SAVED
  4735. .replaceAll('{B}', strBookName)
  4736. .replaceAll('{N}', value)
  4737. .replaceAll('{R}', userDetail.vote-arConfig.allCount)
  4738. );
  4739. if (userDetail && arConfig.allCount > userDetail.vote) {
  4740. const alertBox = alertify.warning(
  4741. TEXT_ALT_ATRCMMDS_OVERFLOW
  4742. .replace('{V}', String(userDetail.vote))
  4743. .replace('{C}', String(arConfig.allCount))
  4744. );
  4745. alertBox.callback = function(isClicked) {
  4746. isClicked && refreshMyUserDetail();
  4747. }
  4748. };
  4749. } else {
  4750. // invalid input value, alert
  4751. alertify.error(TEXT_ALT_ATRCMMDS_INVALID.replaceAll('{N}', value));
  4752. }
  4753. }
  4754. }
  4755. }
  4756.  
  4757. function laterReads() {
  4758. // Container
  4759. const container = $CrE('div');
  4760. container.id = 'laterbooks';
  4761. content.appendChild(container);
  4762.  
  4763. // Title div
  4764. const titlebar = $CrE('div');
  4765. titlebar.classList.add('gridtop');
  4766. titlebar.style.display = 'grid';
  4767. titlebar.style['grid-template-columns'] = '1fr 1fr 1fr';
  4768. container.appendChild(titlebar);
  4769.  
  4770. const title = $CrE('span');
  4771. title.innerHTML = '稍后再读';
  4772. title.style['grid-column'] = '2/3';
  4773. titlebar.appendChild(title);
  4774.  
  4775. // Sorter select container
  4776. const sortContainer = $CrE('span');
  4777. sortContainer.style['grid-column'] = '3/4';
  4778. sortContainer.style.textAlign = 'right';
  4779. titlebar.appendChild(sortContainer);
  4780.  
  4781. // Sorter select
  4782. const sltsort = $CrE('select');
  4783. sltsort.style.width = 'max-content';
  4784. sltsort.addEventListener('change', function() {
  4785. const config = CONFIG.bookcasePrefs.getConfig();
  4786. config.laterbooks.sortby = sltsort.value;
  4787. CONFIG.bookcasePrefs.saveConfig(config);
  4788. showBooks();
  4789. });
  4790. sortContainer.appendChild(sltsort);
  4791.  
  4792. // Sorter select options
  4793. const sorttypes = Object.keys(FUNC_LATERBOOK_SORTERS);
  4794. for (const type of sorttypes) {
  4795. const sort = FUNC_LATERBOOK_SORTERS[type];
  4796. const option = $CrE('option');
  4797. option.innerHTML = sort.name;
  4798. option.value = type;
  4799. sltsort.appendChild(option);
  4800. }
  4801. sltsort.selectedIndex = sorttypes.indexOf(CONFIG.bookcasePrefs.getConfig().laterbooks.sortby);
  4802.  
  4803. // Body table
  4804. const body = $CrE('table');
  4805. setAttributes(body, {
  4806. 'class': 'grid',
  4807. 'width': '100%',
  4808. 'align': 'center'
  4809. });
  4810. const tbody = $CrE('tbody');
  4811. body.appendChild(tbody);
  4812. container.appendChild(body);
  4813.  
  4814. // Header & Rows
  4815. showBooks();
  4816.  
  4817. function showBooks() {
  4818. const config = CONFIG.bookcasePrefs.getConfig().laterbooks;
  4819. clearChildnodes(body);
  4820.  
  4821. // headers
  4822. const headtr = $CrE('tr');
  4823. headtr.setAttribute('align', 'center');
  4824. const headers = [{
  4825. name: '名称',
  4826. width: '22%'
  4827. },{
  4828. name: '简介',
  4829. width: '60%'
  4830. },{
  4831. name: '操作',
  4832. width: '18%'
  4833. }];
  4834. for (const head of headers) {
  4835. const th = $CrE('th');
  4836. th.innerHTML = head.name;
  4837. th.style.width = head.width;
  4838. headtr.appendChild(th);
  4839. }
  4840. body.appendChild(headtr);
  4841.  
  4842. // Book rows
  4843. const books = sortLaterReads(config.books, config.sortby);
  4844.  
  4845. for (const book of books) {
  4846. makeRow(book);
  4847. }
  4848.  
  4849. function makeRow(book) {
  4850. const config = CONFIG.bookcasePrefs.getConfig().laterbooks;
  4851.  
  4852. // row
  4853. const row = $CrE('tr');
  4854.  
  4855. // cover & name
  4856. const tdName = $CrE('td');
  4857. tdName.classList.add('odd');
  4858. tdName.style.textAlign = 'center';
  4859. const clink = $CrE('a');
  4860. clink.href = URL_NOVELINDEX.replace('{I}', book.aid);
  4861. clink.target = '_blank';
  4862. tdName.appendChild(clink);
  4863. const cover = $CrE('img');
  4864. cover.src = book.cover;
  4865. cover.style.width = '100px';
  4866. clink.appendChild(cover);
  4867. clink.insertAdjacentHTML('beforeend', '</br>');
  4868. clink.insertAdjacentText('beforeend', book.name);
  4869. row.appendChild(tdName);
  4870.  
  4871. // info
  4872. const tdInfo = $CrE('td');
  4873. tdInfo.classList.add('even');
  4874. tdInfo.insertAdjacentHTML('afterbegin', '<span class="hottext">作品Tags:</span></br>');
  4875. for (const tag of book.tags) {
  4876. const a = $CrE('a');
  4877. a.target = '_blank';
  4878. a.href = URL_TAGSEARCH.replace('{TU}', $URL.encode(tag));
  4879. a.classList.add(CLASSNAME_BUTTON);
  4880. a.innerText = tag + ' ';
  4881. tdInfo.appendChild(a);
  4882. }
  4883. tdInfo.insertAdjacentHTML('beforeend', '</br></br><span class="hottext">内容简介:</span></br>');
  4884. tdInfo.insertAdjacentText('beforeend', book.introduce);
  4885. row.appendChild(tdInfo);
  4886.  
  4887. // operator
  4888. const tdOprt = $CrE('td');
  4889. tdOprt.classList.add('odd');
  4890. tdOprt.style.textAlign = 'center';
  4891. const btnDel = makeBtn();
  4892. btnDel.innerHTML = '删除';
  4893. btnDel.addEventListener('click', del);
  4894. tdOprt.appendChild(btnDel);
  4895. const btnAbc = makeBtn('a'); // Abc ==> AddBookCase
  4896. btnAbc.innerHTML = '加入书架';
  4897. btnAbc.href = URL_ADDBOOKCASE.replace('{A}', book.aid);
  4898. btnAbc.target = '_blank';
  4899. tdOprt.appendChild(btnAbc);
  4900. if (config.sortby === 'sort') {
  4901. tdOprt.appendChild($CrE('br'));
  4902. const btnUp = makeBtn();
  4903. btnUp.innerHTML = '上移';
  4904. btnUp.addEventListener('click', function () {
  4905. const config = CONFIG.bookcasePrefs.getConfig();
  4906. const books = Object.values(config.laterbooks.books);
  4907. const cur = books.filter((b) => (b.sort === book.sort));
  4908. const previous = books.filter((b) => (b.sort === book.sort-1));
  4909.  
  4910. if (cur) {
  4911. if (previous.length > 0) {
  4912. previous[0].sort++;
  4913. cur[0].sort--;
  4914. CONFIG.bookcasePrefs.saveConfig(config);
  4915. showBooks();
  4916. }
  4917. } else {
  4918. alertify.warning(TEXT_ALT_BOOKCASE_AFTERBOOKS_MISSING);
  4919. }
  4920. });
  4921. tdOprt.appendChild(btnUp);
  4922. const btnDown = makeBtn();
  4923. btnDown.innerHTML = '下移';
  4924. btnDown.addEventListener('click', function () {
  4925. const config = CONFIG.bookcasePrefs.getConfig();
  4926. const books = Object.values(config.laterbooks.books);
  4927. const cur = books.filter((b) => (b.sort === book.sort));
  4928. const after = books.filter((b) => (b.sort === book.sort+1));
  4929.  
  4930. if (cur) {
  4931. if (after.length > 0) {
  4932. after[0].sort--;
  4933. cur[0].sort++;
  4934. CONFIG.bookcasePrefs.saveConfig(config);
  4935. showBooks();
  4936. }
  4937. } else {
  4938. alertify.warning(TEXT_ALT_BOOKCASE_AFTERBOOKS_MISSING);
  4939. }
  4940. });
  4941. tdOprt.appendChild(btnDown);
  4942. }
  4943. row.appendChild(tdOprt);
  4944.  
  4945. body.appendChild(row);
  4946.  
  4947. function del() {
  4948. const config = CONFIG.bookcasePrefs.getConfig();
  4949. const books = config.laterbooks.books;
  4950. const bk = books[book.aid];
  4951. if (!bk) {
  4952. body.removeChild(row);
  4953. return false;
  4954. }
  4955. delete config.laterbooks.books[book.aid];
  4956. Array.prototype.forEach.call(Object.values(books), (b) => (b.sort > bk.sort && b.sort--));
  4957. CONFIG.bookcasePrefs.saveConfig(config);
  4958. body.removeChild(row);
  4959. }
  4960.  
  4961. function makeBtn(tagName='span') {
  4962. const btn = $CrE(tagName);
  4963. btn.classList.add(CLASSNAME_BUTTON);
  4964. btn.style.margin = '0 1em';
  4965. return btn;
  4966. }
  4967. }
  4968. }
  4969. }
  4970.  
  4971. // Set attributes to an element
  4972. function setAttributes(elm, attributes) {
  4973. for (const [name, attr] of Object.entries(attributes)) {
  4974. elm.setAttribute(name, attr);
  4975. }
  4976. }
  4977. }
  4978.  
  4979. // Novel ads remover
  4980. function removeTopAds() {
  4981. const ads = []; $All('div>script+script+a').forEach(function(a) {ads.push(a.parentElement);});
  4982. for (const ad of ads) {
  4983. ad.parentElement.removeChild(ad);
  4984. }
  4985. }
  4986.  
  4987. // Novel index page add-on
  4988. function pageNovelIndex() {
  4989. removeTopAds();
  4990. //downloader();
  4991.  
  4992. function downloader() {
  4993. AndAPI.getNovelIndex({
  4994. aid: unsafeWindow.article_id,
  4995. lang: 0,
  4996. callback: indexGot
  4997. });
  4998.  
  4999. function indexGot(xml) {
  5000. const volumes = $All(xml, 'volume');
  5001. const vtitles = $All('.vcss');
  5002. if (volumes.length !== vtitles.length) {return false;}
  5003.  
  5004. for (let i = 0; i < volumes.length; i++) {
  5005. const volume = volumes[i];
  5006. const vtitle = vtitles[i];
  5007. const vname = volume.childNodes[0].nodeValue;
  5008.  
  5009. // Title element
  5010. const elmTitle = $CrE('span');
  5011. elmTitle.innerText = vname;
  5012.  
  5013. // Spliter element
  5014. const elmSpliter = $CrE('span');
  5015. elmSpliter.style.margin = '0 0.5em';
  5016.  
  5017. // Download button
  5018. const elmDlBtn = $CrE('span');
  5019. elmDlBtn.classList.add(CLASSNAME_BUTTON);
  5020. elmDlBtn.innerHTML = TEXT_GUI_DOWNLOAD_THISVOLUME;
  5021. elmDlBtn.addEventListener('click', function() {
  5022. // getAttribute returns string rather than number,
  5023. // but downloadVolume accepts both string and number as vid
  5024. downloadVolume(volume.getAttribute('vid'), vname, ['utf-8', 'big5'][getLang()]);
  5025. });
  5026.  
  5027. clearChildnodes(vtitle);
  5028. vtitle.appendChild(elmTitle);
  5029. vtitle.appendChild(elmSpliter);
  5030. vtitle.appendChild(elmDlBtn);
  5031. }
  5032. }
  5033.  
  5034. function downloadVolume(vid, vname, charset='utf-8') {
  5035. const url = URL_DOWNLOAD1.replace('{A}', unsafeWindow.article_id).replace('{V}', vid).replace('{C}', charset);
  5036. downloadFile({
  5037. url: url,
  5038. name: TEXT_GUI_SDOWNLOAD_FILENAME
  5039. .replace('{NovelName}', $('#title').innerText)
  5040. .replace('{VolumeName}', vname)
  5041. .replace('{Extension}', 'txt')
  5042. });
  5043. }
  5044. }
  5045. }
  5046.  
  5047. // Novel page add-on
  5048. function pageNovel() {
  5049. const CSM = new ConfigSetManager();
  5050. CSM.install();
  5051. const pageResource = {elements: {}, infos: {}, download: {}};
  5052. collectPageResources();
  5053.  
  5054. // Remove ads
  5055. removeTopAds();
  5056.  
  5057. // Side-Panel buttons
  5058. sideButtons();
  5059.  
  5060. // Provide download GUI
  5061. downloadGUI();
  5062.  
  5063. // Prevent URL.revokeObjectURL in script 轻小说文库下载
  5064. revokeObjectURLHOOK();
  5065.  
  5066. // Font changer
  5067. fontChanger();
  5068.  
  5069. // More font-sizes
  5070. moreFontSizes();
  5071.  
  5072. // Fill content if need
  5073. fillContent();
  5074.  
  5075. // Beautifier page
  5076. beautifier();
  5077.  
  5078. function collectPageResources() {
  5079. collectElements();
  5080. collectInfos();
  5081. initDownload();
  5082.  
  5083. function collectElements() {
  5084. const elements = pageResource.elements;
  5085. elements.title = $('#title');
  5086. elements.images = $All('.imagecontent');
  5087. elements.rightButtonDiv = $('#linkright');
  5088. elements.rightNodes = elements.rightButtonDiv.childNodes;
  5089. elements.rightBlank = elements.rightNodes[elements.rightNodes.length-1];
  5090. elements.content = $('#content');
  5091. elements.contentmain = $('#contentmain');
  5092. elements.spliterDemo = document.createTextNode(' | ');
  5093. }
  5094. function collectInfos() {
  5095. const elements = pageResource.elements;
  5096. const infos = pageResource.infos;
  5097. infos.title = elements.title.innerText;
  5098. infos.isImagePage = elements.images.length > 0;
  5099. infos.content = infos.isImagePage ? null : elements.content.innerText;
  5100. }
  5101. function initDownload() {
  5102. const elements = pageResource.elements;
  5103. const download = pageResource.download;
  5104. download.running = false;
  5105. download.finished = 0;
  5106. download.all = elements.images.length;
  5107. download.error = 0;
  5108. }
  5109. }
  5110.  
  5111. // Prevent URL.revokeObjectURL in script 轻小说文库下载
  5112. function revokeObjectURLHOOK() {
  5113. const Ori_revokeObjectURL = URL.revokeObjectURL;
  5114. URL.revokeObjectURL = function(arg) {
  5115. if (typeof(arg) === 'string' && arg.substr(0, 5) === 'blob:') {return false;};
  5116. return Ori_revokeObjectURL(arg);
  5117. }
  5118. }
  5119.  
  5120. // Side-Panel buttons
  5121. function sideButtons() {
  5122. // Download
  5123. SPanel.add({
  5124. faicon: 'fa-solid fa-download',
  5125. tip: TEXT_GUI_DOWNLOAD_THISCHAPTER,
  5126. onclick: dlNovel
  5127. });
  5128.  
  5129. // Next page
  5130. SPanel.add({
  5131. faicon: 'fa-solid fa-angle-right',
  5132. tip: '下一页',
  5133. onclick: (e) => {$('#foottext>a:nth-child(4)').click();}
  5134. });
  5135.  
  5136. // Previous page
  5137. SPanel.add({
  5138. faicon: 'fa-solid fa-angle-left',
  5139. tip: '上一页',
  5140. onclick: (e) => {$('#foottext>a:nth-child(3)').click();}
  5141. });
  5142. }
  5143.  
  5144. // Provide download GUI
  5145. function downloadGUI() {
  5146. const elements = pageResource.elements;
  5147. const infos = pageResource.infos;
  5148.  
  5149. // Create donwload button
  5150. const dlBtn = elements.downloadBtn = $CrE('span');
  5151. dlBtn.classList.add(CLASSNAME_BUTTON);
  5152. dlBtn.addEventListener('click', dlNovel);
  5153. dlBtn.innerText = TEXT_GUI_DOWNLOAD_THISCHAPTER;
  5154.  
  5155. // Create spliter
  5156. const spliter = elements.spliterDemo.cloneNode();
  5157.  
  5158. // Append to rightButtonDiv
  5159. elements.rightButtonDiv.style.width = '550px';
  5160. elements.rightButtonDiv.insertBefore(spliter, elements.rightBlank);
  5161. elements.rightButtonDiv.insertBefore(dlBtn, elements.rightBlank);
  5162. }
  5163.  
  5164. // Page beautifier
  5165. function beautifier() {
  5166. CONFIG.BeautifierCfg.getConfig().novel.beautiful && beautiful();
  5167.  
  5168. function beautiful() {
  5169. const config = CONFIG.BeautifierCfg.getConfig();
  5170. const usedHeight = getRestHeight();
  5171.  
  5172. addStyle(CSS_NOVEL
  5173. .replaceAll('{BGI}', config.backgroundImage)
  5174. .replaceAll('{S}', config.textScale)
  5175. .replaceAll('{H}', usedHeight), 'beautifier'
  5176. );
  5177.  
  5178. unsafeWindow.stopScroll = beautiful_stopScroll;
  5179. document.onmousedown = beautiful_stopScroll;
  5180. unsafeWindow.scrolling = beautiful_scrolling;
  5181.  
  5182. // Get rest height without #contentmain
  5183. function getRestHeight() {
  5184. let usedHeight = 0;
  5185. ['adv1', 'adtop', 'headlink', 'footlink', 'adbottom'].forEach((id) => {
  5186. const node = $('#'+id);
  5187. if (node instanceof Element && node.id !== 'contentmain') {
  5188. const cs = getComputedStyle(node);
  5189. ['height', 'marginTop', 'marginBottom', 'paddingTop', 'paddingBottom', 'borderTop', 'borderBottom'].forEach((style) => {
  5190. const reg = cs[style].match(/([\.\d]+)px/);
  5191. reg && (usedHeight += Number(reg[1]));
  5192. });
  5193. };
  5194. });
  5195. usedHeight = usedHeight.toString() + 'px';
  5196. return usedHeight;
  5197. }
  5198.  
  5199. // Mouse dblclick scroll with beautifier applied
  5200. function beautiful_scrolling() {
  5201. let contentmain = pageResource.elements.contentmain;
  5202. let currentpos = contentmain.scrollTop || 0;
  5203. contentmain.scrollTo(0, ++currentpos);
  5204. let nowpos = contentmain.scrollTop || 0;
  5205. pageResource.elements.content.style.userSelect = 'none';
  5206. currentpos != nowpos && beautiful_stopScroll();
  5207. }
  5208.  
  5209. function beautiful_stopScroll() {
  5210. pageResource.elements.content.style.userSelect = '';
  5211. unsafeWindow.clearInterval(timer);
  5212. }
  5213. }
  5214. }
  5215.  
  5216. // Provide font changer
  5217. function fontChanger() {
  5218. // Button
  5219. const bcolor = $('#bcolor');
  5220. const txtfont = $CrE('select');
  5221. txtfont.id = 'txtfont';
  5222. txtfont.addEventListener('change', applyFont);
  5223. bcolor.insertAdjacentElement('afterend', txtfont);
  5224. bcolor.insertAdjacentText('afterend', '\t\t\t 字体选择');
  5225.  
  5226. // Provided fonts
  5227. const FONTS = [{"name":"默认","value":"unset"}, {"name":"微软雅黑","value":"Microsoft YaHei"},{"name":"黑体","value":"SimHei"},{"name":"微软正黑体","value":"Microsoft JhengHei"},{"name":"宋体","value":"SimSun"},{"name":"仿宋","value":"FangSong"},{"name":"新宋体","value":"NSimSun"},{"name":"细明体","value":"MingLiU"},{"name":"新细明体","value":"PMingLiU"},{"name":"楷体","value":"KaiTi"},{"name":"标楷体","value":"DFKai-SB"}]
  5228. for (const font of FONTS) {
  5229. const option = $CrE('option');
  5230. option.innerText = font.name;
  5231. option.value = font.value;
  5232. txtfont.appendChild(option);
  5233. }
  5234.  
  5235. // Function
  5236. CSM.ConfigSets.txtfont = {
  5237. save: () => (setCookies('txtfont', txtfont[txtfont.selectedIndex].value)),
  5238. load: () => {
  5239. const tmpstr = ReadCookies("txtfont");
  5240. if (tmpstr != "") {
  5241. for (let i = 0; i < txtfont.length; i++) {
  5242. if (txtfont.options[i].value == tmpstr) {
  5243. txtfont.selectedIndex = i;
  5244. break;
  5245. }
  5246. }
  5247. }
  5248. applyFont();
  5249. }
  5250. };
  5251.  
  5252. // Load saved font
  5253. CSM.ConfigSets.txtfont.load();
  5254.  
  5255. function applyFont() {
  5256. $('#content').style['font-family'] = txtfont[txtfont.selectedIndex].value;
  5257. }
  5258. }
  5259.  
  5260. // Provide more font-sizes
  5261. function moreFontSizes() {
  5262. const select = $('#fonttype');
  5263. const savebtn = $('#saveset');
  5264. const sizes = [
  5265. {
  5266. name: '更小',
  5267. size: '10px'
  5268. },
  5269. {
  5270. name: '更大',
  5271. size: '28px'
  5272. },
  5273. {
  5274. name: '很大',
  5275. size: '32px'
  5276. },
  5277. {
  5278. name: '超大',
  5279. size: '36px'
  5280. },
  5281. {
  5282. name: '极大',
  5283. size: '40px'
  5284. },
  5285. {
  5286. name: '过大',
  5287. size: '44px'
  5288. },
  5289. ];
  5290.  
  5291. for (const size of sizes) {
  5292. const option = $CrE('option');
  5293. option.innerHTML = size.name;
  5294. option.value = size.size;
  5295.  
  5296. // Insert with sorting
  5297. for (const opt of select.children) {
  5298. const sizeNum1 = getSizeNum(opt.value);
  5299. const sizeNum2 = getSizeNum(option.value);
  5300. if (isNaN(sizeNum1) || isNaN(sizeNum2)) {continue;} // Code shouldn't be here in normal cases
  5301. if (sizeNum1 > sizeNum2) {
  5302. select.insertBefore(option, opt);
  5303. break;
  5304. }
  5305. }
  5306. option.parentElement !== select && select.appendChild(option);
  5307. }
  5308.  
  5309. // Load saved fonttype
  5310. CSM.ConfigSets.fonttype.load();
  5311.  
  5312. function getSizeNum(size) {
  5313. return Number(size.match(/(\d+)px/)[1]);
  5314. }
  5315. }
  5316.  
  5317. // Provide content using AndroidAPI
  5318. function fillContent() {
  5319. // Check whether needs filling
  5320. if ($('#contentmain>span')) {
  5321. if ($('#contentmain>span').innerText.trim() !== 'null') {
  5322. return false;
  5323. }
  5324. } else {return false;}
  5325.  
  5326. // prepare
  5327. const content = pageResource.elements.content;
  5328. content.innerHTML = TEXT_GUI_NOVEL_FILLING;
  5329. const charset = (function() {
  5330. const match = document.cookie.match(/(; *)?jieqiUserCharset=(.+?)( *;|$)/);
  5331. return match && match[2] && match[2].toLowerCase() === 'big5' ? 1 : 0;
  5332. }) ();
  5333.  
  5334. // Get content xml
  5335. AndAPI.getNovelContent({
  5336. aid: unsafeWindow.article_id,
  5337. cid: unsafeWindow.chapter_id,
  5338. lang: charset,
  5339. callback: function(text) {
  5340. const imgModel = '<div class="divimage"><a href="{U}" target="_blank"><img src="{U}" border="0" class="imagecontent"></a></div>';
  5341.  
  5342. // Trim whitespaces
  5343. text = text.trim();
  5344.  
  5345. // Get images like <!--image-->http://pic.wenku8.com/pictures/0/716/24406/11588.jpg<!--image-->
  5346. const imgUrls = text.match(/<!--image-->[^<>]+?<!--image-->/g) || [];
  5347.  
  5348. // Parse <img> for every image url
  5349. let html = '';
  5350. for (const url of imgUrls) {
  5351. const index = text.indexOf(url);
  5352. const src = htmlEncode(url.match(/<!--image-->([^<>]+?)<!--image-->/)[1]);
  5353. html += htmlEncode(text.substring(0, index)).replaceAll('\r\n', '\n').replaceAll('\r', '\n').replaceAll('\n', '</br>');
  5354. html += imgModel.replaceAll('{U}', src);
  5355. text = text.substring(index + url.length);
  5356. }
  5357. html += htmlEncode(text);
  5358.  
  5359. // Set content
  5360. pageResource.elements.content.innerHTML = html;
  5361.  
  5362. // Reset pageResource-image if need
  5363. pageResource.infos.isImagePage = imgUrls.length > 0;
  5364. pageResource.elements.images = $All('.imagecontent');
  5365. pageResource.download.all = pageResource.elements.images.length;
  5366. }
  5367. })
  5368.  
  5369. return true;
  5370. }
  5371.  
  5372. // Download button onclick
  5373. function dlNovel() {
  5374. pageResource.infos.isImagePage ? dlNovelImages() : dlNovelText();
  5375. }
  5376.  
  5377. // Download Images
  5378. function dlNovelImages() {
  5379. const elements = pageResource.elements;
  5380. const infos = pageResource.infos;
  5381. const download = pageResource.download;
  5382.  
  5383. if (download.running) {return false;};
  5384. download.running = true; download.finished = 0; download.error = 0;
  5385. updateDownloadStatus();
  5386.  
  5387. const lenNumber = String(elements.images.length).length;
  5388. for (let i = 0; i < elements.images.length; i++) {
  5389. const img = elements.images[i];
  5390. const name = infos.title + '_' + fillNumber(i+1, lenNumber) + '.jpg';
  5391. GM_xmlhttpRequest({
  5392. url: img.src,
  5393. responseType: 'blob',
  5394. onloadstart: function() {
  5395. DoLog(LogLevel.Info, '[' + String(i) + ']downloading novel image from ' + img.src);
  5396. },
  5397. onload: function(e) {
  5398. DoLog(LogLevel.Info, '[' + String(i) + ']image got: ' + img.src);
  5399.  
  5400. const image = new Image();
  5401. image.onload = function() {
  5402. const url = toImageFormatURL(image, 1);
  5403. DoLog(LogLevel.Info, '[' + String(i) + ']image transformed: ' + img.src);
  5404.  
  5405. const a = $CrE('a');
  5406. a.href = url;
  5407. a.download = name;
  5408. a.click();
  5409.  
  5410. download.finished++;
  5411. updateDownloadStatus();
  5412. // Code below seems can work, but actually it doesn't work well and somtimes some file cannot be saved
  5413. // The reason is still unknown, but from what I know I can tell that mistakes happend in GM_xmlhttpRequest
  5414. // Error stack: GM_xmlhttpRequest.onload ===> image.onload ===> downloadFile ===> GM_xmlhttpRequest =X=> .onload
  5415. // This Error will also stuck the GMXHRHook.ongoingList
  5416. /*downloadFile({
  5417. url: url,
  5418. name: name,
  5419. onload: function() {
  5420. download.finished++;
  5421. DoLog(LogLevel.Info, '[' + String(i) + ']file saved: ' + name);
  5422. alert('[' + String(i) + ']file saved: ' + name);
  5423. updateDownloadStatus();
  5424. },
  5425. onerror: function() {
  5426. alert('downloadfile error! url = ' + String(url) + ', i = ' + String(i));
  5427. }
  5428. })*/
  5429. }
  5430. image.onerror = function() {
  5431. throw new Error('image load error! image.src = ' + String(image.src) + ', i = ' + String(i));
  5432. }
  5433. image.src = URL.createObjectURL(e.response);
  5434. },
  5435. onerror: function(e) {
  5436. // Error dealing need...
  5437. DoLog(LogLevel.Error, '[' + String(i) + ']image fetch error: ' + img.src);
  5438. download.error++;
  5439. }
  5440. })
  5441. }
  5442.  
  5443. function updateDownloadStatus() {
  5444. elements.downloadBtn.innerText = TEXT_GUI_DOWNLOADING_ALL.replaceAll('C', String(download.finished)).replaceAll('A', String(download.all));
  5445. if (download.finished === download.all) {
  5446. DoLog(LogLevel.Success, 'All images got.');
  5447. elements.downloadBtn.innerText = TEXT_GUI_DOWNLOADED_ALL;
  5448. download.running = false;
  5449. }
  5450. }
  5451. }
  5452.  
  5453. // Download Text
  5454. function dlNovelText() {
  5455. const infos = pageResource.infos;
  5456. const name = infos.title + '.txt';
  5457. const text = infos.content.replaceAll(/[\r\n]+/g, '\r\n');
  5458. downloadText(text, name);
  5459. }
  5460.  
  5461. // Image format changing function
  5462. // image: <img> or Image(); format: 1 for jpeg, 2 for png, 3 for webp
  5463. function toImageFormatURL(image, format) {
  5464. if (typeof(format) === 'number') {format = ['image/jpeg', 'image/png', 'image/webp'][format-1]}
  5465. const cvs = $CrE('canvas');
  5466. cvs.width = image.width;
  5467. cvs.height = image.height;
  5468. const ctx = cvs.getContext('2d');
  5469. ctx.drawImage(image, 0, 0);
  5470. return cvs.toDataURL(format);
  5471. }
  5472.  
  5473. function ConfigSetManager() {
  5474. const CSM = this;
  5475. /*const setCookies = unsafeWindow.setCookies,
  5476. ReadCookies = unsafeWindow.ReadCookies,
  5477. bcolor = unsafeWindow.bcolor,
  5478. txtcolor = unsafeWindow.txtcolor,
  5479. fonttype = unsafeWindow.fonttype,
  5480. scrollspeed = unsafeWindow.scrollspeed,
  5481. setSpeed = unsafeWindow.setSpeed,
  5482. contentobj = unsafeWindow.contentobj;*/
  5483.  
  5484. CSM.ConfigSets = {
  5485. 'bcolor': {
  5486. save: () => (setCookies("bcolor", bcolor.options[bcolor.selectedIndex].value)),
  5487. load: () => {
  5488. const tmpstr = ReadCookies("bcolor");
  5489. bcolor.selectedIndex = 0;
  5490. if (tmpstr != "") {
  5491. for (let i = 0; i < bcolor.length; i++) {
  5492. if (bcolor.options[i].value == tmpstr) {
  5493. bcolor.selectedIndex = i;
  5494. break;
  5495. }
  5496. }
  5497. }
  5498. document.bgColor = bcolor.options[bcolor.selectedIndex].value;
  5499. }
  5500. },
  5501. 'txtcolor': {
  5502. save: () => (setCookies("txtcolor", txtcolor.options[txtcolor.selectedIndex].value)),
  5503. load: () => {
  5504. const tmpstr = ReadCookies("txtcolor");
  5505. txtcolor.selectedIndex = 0;
  5506. if (tmpstr != "") {
  5507. for (let i = 0; i < txtcolor.length; i++) {
  5508. if (txtcolor.options[i].value == tmpstr) {
  5509. txtcolor.selectedIndex = i;
  5510. break;
  5511. }
  5512. }
  5513. }
  5514. $('#content').style.color = txtcolor.options[txtcolor.selectedIndex].value;
  5515. }
  5516. },
  5517. 'fonttype': {
  5518. save: () => (setCookies("fonttype", fonttype.options[fonttype.selectedIndex].value)),
  5519. load: () => {
  5520. const tmpstr = ReadCookies("fonttype");
  5521. fonttype.selectedIndex = 2;
  5522. if (tmpstr != "") {
  5523. for (let i = 0; i < fonttype.length; i++) {
  5524. if (fonttype.options[i].value == tmpstr) {
  5525. fonttype.selectedIndex = i;
  5526. break;
  5527. }
  5528. }
  5529. }
  5530. $('#content').style.fontSize = fonttype.options[fonttype.selectedIndex].value;
  5531. }
  5532. },
  5533. 'scrollspeed': {
  5534. save: () => (setCookies("scrollspeed", scrollspeed.value)),
  5535. load: () => {
  5536. const tmpstr = ReadCookies("scrollspeed");
  5537. if (tmpstr == '') {tmpstr = 5;}
  5538. scrollspeed.value = tmpstr;
  5539. setSpeed();
  5540. }
  5541. }
  5542. };
  5543.  
  5544. CSM.saveSet = function() {
  5545. for (const [name, set] of Object.entries(CSM.ConfigSets)) {
  5546. set.save();
  5547. }
  5548. };
  5549.  
  5550. CSM.loadSet = function() {
  5551. for (const [name, set] of Object.entries(CSM.ConfigSets)) {
  5552. set.load();
  5553. }
  5554. };
  5555.  
  5556. CSM.install = function() {
  5557. Object.defineProperty(unsafeWindow, 'saveSet', {
  5558. configurable: false,
  5559. enumerable: true,
  5560. value: CSM.saveSet,
  5561. writable: false
  5562. });
  5563. Object.defineProperty(unsafeWindow, 'loadSet', {
  5564. configurable: false,
  5565. enumerable: true,
  5566. value: CSM.loadSet,
  5567. writable: false
  5568. });
  5569. };
  5570. }
  5571. }
  5572.  
  5573. // Search form add-on
  5574. function formSearch() {
  5575. const searchForm = $('form[name="articlesearch"]');
  5576. if (!searchForm) {return false;};
  5577. const typeSelect = $(searchForm, '#searchtype');
  5578. const searchText = $(searchForm, '#searchkey');
  5579. const searchSbmt = $(searchForm, 'input[class="button"][type="submit"]');
  5580.  
  5581. let optionTags;
  5582. provideTagOption();
  5583. onsubmitHOOK();
  5584.  
  5585. function provideTagOption() {
  5586. optionTags = $CrE('option');
  5587. optionTags.value = VALUE_STR_NULL;
  5588. optionTags.innerText = TEXT_GUI_SEARCH_OPTION_TAG;
  5589. typeSelect.appendChild(optionTags);
  5590.  
  5591. if (tipready) {
  5592. // tipshow and tiphide is coded inside wenku8 itself, its function is to show a text tip besides the mouse
  5593. typeSelect.addEventListener('mouseover', show);
  5594. searchSbmt.addEventListener('mouseover', show);
  5595. typeSelect.addEventListener('mouseout' , tiphide);
  5596. searchSbmt.addEventListener('mouseout' , tiphide);
  5597. } else {
  5598. typeSelect.title = TEXT_TIP_SEARCH_OPTION_TAG;
  5599. searchSbmt.title = TEXT_TIP_SEARCH_OPTION_TAG;
  5600. }
  5601.  
  5602. function show() {
  5603. optionTags.selected ? tipshow(TEXT_TIP_SEARCH_OPTION_TAG) : function() {};
  5604. }
  5605. }
  5606. function onsubmitHOOK() {
  5607. const onsbmt = searchForm.onsubmit;
  5608. searchForm.onsubmit = function() {
  5609. if (optionTags.selected) {
  5610. // DON'T USE window.open()!
  5611. // Wenku8 has no window.open used in its own scripts, so do not use it in userscript either.
  5612. // It might cause security problems.
  5613. //window.open('https://www.wenku8.net/modules/article/tags.php?t=' + $URL.encode(searchText.value));
  5614. if (typeof($URL) === 'undefined' ) {
  5615. $URLError();
  5616. return true;
  5617. } else {
  5618. GM_openInTab(URL_TAGSEARCH.replace('{TU}', $URL.encode(searchText.value)), {
  5619. active: true, insert: true, setParent: true, incognito: false
  5620. });
  5621. return false;
  5622. }
  5623. }
  5624. }
  5625.  
  5626. function $URLError() {
  5627. DoLog(LogLevel.Error, '$URL(from gbk.js) is not loaded.');
  5628. DoLog(LogLevel.Warning, 'Search as plain text instead.');
  5629.  
  5630. // Search as plain text instead
  5631. for (const node of typeSelect.childNodes) {
  5632. node.selected = (node.tagName === 'OPTION' && node.value === 'articlename') ? true : false;
  5633. }
  5634. }
  5635. }
  5636. }
  5637.  
  5638. // Tags page add-on
  5639. function pageTags() {
  5640. }
  5641.  
  5642. // Mylink page add-on
  5643. function pageMylink() {
  5644. // Get elements
  5645. const main = $('#content');
  5646. const tbllink = $('#content>table');
  5647.  
  5648. linkEnhance();
  5649.  
  5650. function fixEdit(link) {
  5651. const aedit = link.aedit;
  5652. aedit.setAttribute('onclick', "editlink({ULID},'{NAME}','{HREF}','{INFO}')".replace('{ULID}', deal(link.ulid)).replace('{NAME}', deal(link.name)).replace('{HREF}', deal(link.href)).replace('{INFO}', deal(link.info)));
  5653.  
  5654. function deal(str) {
  5655. return str.replaceAll("'", "\\'");
  5656. }
  5657. }
  5658.  
  5659. function linkEnhance() {
  5660. const links = getAllLinks();
  5661. for (const link of links) {
  5662. fixEdit(link);
  5663. }
  5664. }
  5665.  
  5666. function getAllLinks() {
  5667. const links = [];
  5668. const trs = $All(tbllink, 'tbody>tr+tr');
  5669. for (const tr of trs) {
  5670. const link = {};
  5671.  
  5672. // All <td>
  5673. link.tdlink = tr.children[0];
  5674. link.tdinfo = tr.children[1];
  5675. link.tdtime = tr.children[2];
  5676. link.tdoprt = tr.children[3];
  5677.  
  5678. // Inside <td>
  5679. link.alink = link.tdlink.children[0];
  5680. link.aedit = link.tdoprt.children[0];
  5681. link.apos = link.tdoprt.children[1];
  5682. link.adel = link.tdoprt.children[2];
  5683.  
  5684. // Infos
  5685. link.href = link.alink.href;
  5686. link.ulid = getUrlArgv({url: link.apos.href, name: 'ulid'});
  5687. link.name = link.alink.innerText;
  5688. link.info = link.tdinfo.innerText;
  5689. link.time = link.tdtime.innerText;
  5690. link.purl = link.apos.href;
  5691.  
  5692. links.push(link);
  5693. }
  5694.  
  5695. return links;
  5696. }
  5697. }
  5698.  
  5699. // User page add-on
  5700. function pageUser() {
  5701. const UID = Number(getUrlArgv('uid'));
  5702.  
  5703. // Provide review search option
  5704. reviewButton();
  5705.  
  5706. // Review search option
  5707. function reviewButton() {
  5708. // clone button and container div
  5709. const oriContainer = $All('.blockcontent .userinfo')[0].parentElement;
  5710. const container = oriContainer.cloneNode(true);
  5711. const button = $(container, 'a');
  5712. button.innerText = TEXT_GUI_USER_REVIEWSEARCH;
  5713. button.href = URL_REVIEWSEARCH.replaceAll('{K}', String(UID));
  5714. oriContainer.parentElement.appendChild(container);
  5715. }
  5716. }
  5717.  
  5718. // Detail page add-on
  5719. function pageDetail() {
  5720. // Get elements
  5721. const content = $('#content');
  5722. const tbody = $(content, 'table>tbody');
  5723.  
  5724. insertSettings();
  5725.  
  5726. // Insert Settings GUI
  5727. function insertSettings() {
  5728. let elements = GUI();
  5729.  
  5730. function GUI() {
  5731. const review = CONFIG.BkReviewPrefs.getConfig();
  5732. const settings = [
  5733. [{html: TEXT_GUI_DETAIL_TITLE_SETTINGS, colSpan: 3, class: 'foot'}],
  5734. [{html: TEXT_GUI_DETAIL_TITLE_BGI}, {colSpan: 2, key: 'bgimage', tiptitle: TEXT_TIP_IMAGE_FIT}],
  5735. [{html: TEXT_GUI_DETAIL_BGI_UPLOAD}, {colSpan: 2, key: 'bgupload'}],
  5736. [{html: TEXT_GUI_DETAIL_GUI_IMAGER}, {colSpan: 2, key: 'imager'}],
  5737. [{html: TEXT_GUI_DETAIL_GUI_SCALE}, {colSpan: 2, key: 'scalectnr'}],
  5738. [{html: TEXT_GUI_DETAIL_BTF_NOVEL}, {colSpan: 2, key: 'btfnvlctnr'}],
  5739. [{html: TEXT_GUI_DETAIL_BTF_REVIEW}, {colSpan: 2, key: 'btfrvwctnr'}],
  5740. [{html: TEXT_GUI_DETAIL_BTF_COMMON}, {colSpan: 2, key: 'btfcmnctnr'}],
  5741. [{html: TEXT_GUI_DETAIL_FVR_LASTPAGE}, {colSpan: 2, key: 'favoropen'}],
  5742. [{html: TEXT_GUI_DETAIL_VERSION_CURVER}, {colSpan: 2, key: 'curversion'}],
  5743. [{html: TEXT_GUI_DETAIL_VERSION_CHECKUPDATE}, {colSpan: 2, key: 'updatecheck'}],
  5744. [{html: TEXT_GUI_DETAIL_FEEDBACK_TITLE, colSpan: 1, key: 'feedbackttle'}, {html: TEXT_GUI_DETAIL_FEEDBACK, colSpan: 2, key: 'feedback'}],
  5745. [{html: TEXT_GUI_DETAIL_UPDATEINFO_TITLE, colSpan: 1, key: 'feedbackttle'}, {html: TEXT_GUI_DETAIL_UPDATEINFO, colSpan: 2, key: 'updateinfo'}],
  5746. [{html: TEXT_GUI_DETAIL_CONFIG_EXPORT}, {html: TEXT_GUI_DETAIL_EXPORT_CLICK, colSpan: 2, key: 'exportcfg'}],
  5747. [{html: TEXT_GUI_DETAIL_CONFIG_EXPORT_NOPASS}, {html: TEXT_GUI_DETAIL_EXPORT_CLICK, colSpan: 2, key: 'exportcfgnp'}],
  5748. [{html: TEXT_GUI_DETAIL_CONFIG_IMPORT, colSpan: 1, key: 'importcfgttle'}, {html: TEXT_GUI_DETAIL_IMPORT_CLICK, colSpan: 2, key: 'importcfg'}],
  5749. [{html: TEXT_GUI_DETAIL_CONFIG_MANAGE, colSpan: 1, key: 'managecfgttle'}, {html: TEXT_GUI_DETAIL_MANAGE_CLICK, colSpan: 2, key: 'managecfg'}],
  5750. //[{html: TEXT_GUI_DETAIL_XXXXXX_XXXXXX, colSpan: 1, key: 'xxxxxxxx'}, {html: TEXT_GUI_DETAIL_XXXXXX_XXXXXX, colSpan: 2, key: 'xxxxxxxx'}],
  5751. ]
  5752. const elements = createTableGUI(settings);
  5753. const tdBgi = elements.bgimage;
  5754. const imageinput = elements.imageinput = $CrE('input');
  5755. const bgioprt = elements.bgioprt = $CrE('span');
  5756. const bgiupld = elements.bgupload;
  5757. const ckbgiup = elements.ckbgiup = $CrE('input');
  5758. ckbgiup.type = 'checkbox';
  5759. ckbgiup.checked = CONFIG.BeautifierCfg.getConfig().upload;
  5760. ckbgiup.addEventListener('change', uploadChange);
  5761. settip(ckbgiup, TEXT_GUI_DETAIL_BGI_LEGAL);
  5762. bgiupld.appendChild(ckbgiup);
  5763. imageinput.type = 'file';
  5764. imageinput.style.display = 'none';
  5765. imageinput.addEventListener('change', pictureGot);
  5766. bgioprt.innerHTML = TEXT_GUI_DETAIL_DEFAULT_BGI + '</br>' + TEXT_GUI_DETAIL_BGI.replace('{N}', CONFIG.BeautifierCfg.getConfig().bgiName);
  5767. bgioprt.style.color = 'grey';
  5768. settip(bgioprt, TEXT_TIP_IMAGE_FIT);
  5769. tdBgi.addEventListener("dragenter", destroyEvent);
  5770. tdBgi.addEventListener("dragover", destroyEvent);
  5771. tdBgi.addEventListener('drop', pictureGot);
  5772. tdBgi.style.textAlign = 'center';
  5773. tdBgi.addEventListener('click', ()=>{elements.imageinput.click();});
  5774. tdBgi.appendChild(imageinput);
  5775. tdBgi.appendChild(bgioprt);
  5776.  
  5777. // Imager
  5778. const curimager = CONFIG.UserGlobalCfg.getConfig().imager;
  5779. elements.imager.style.padding = '0px 0.5em';
  5780. for (const [key, imager] of Object.entries(DATA_IMAGERS)) {
  5781. if (typeof(imager) !== 'object' || !imager.isImager) {continue;}
  5782.  
  5783. const span = $CrE('span');
  5784. const radio = $CrE('input');
  5785. const text = $CrE('span');
  5786. radio.type = 'radio';
  5787. radio.value = '';
  5788. radio.id = 'imager_'+key;
  5789. radio.imagerkey = key;
  5790. radio.name = 'imagerselect';
  5791. radio.style.cursor = 'pointer';
  5792. radio.addEventListener('change', imagerChange);
  5793. radio.disabled = !imager.available;
  5794. text.innerText = imager.name + (imager.available ? '' : '(已失效)');
  5795. text.style.marginRight = '1em';
  5796. text.style.cursor = 'pointer';
  5797. text.addEventListener('click', function() {radio.click();});
  5798. span.style.display = 'inline-block';
  5799. span.appendChild(radio);
  5800. span.appendChild(text);
  5801. if (imager.tip) {
  5802. let tip = imager.tip;
  5803. DATA_IMAGERS.default === key && (tip += TEXT_TIP_IMAGER_DEFAULT);
  5804. !imager.available && (tip = '<del>{T}</del></br>已失效'.replace('{T}', tip));
  5805. settip(radio, tip);
  5806. settip(text, tip);
  5807. //settip(span, imager.tip);
  5808. }
  5809. elements.imager.appendChild(span);
  5810. }
  5811. $(elements.imager, '#imager_'+curimager).checked = true;
  5812.  
  5813. // Text scale
  5814. const textScale = CONFIG.BeautifierCfg.getConfig().textScale;
  5815. const scalectnr = elements.scalectnr;
  5816. const elmscale = elements.scale = $CrE('input');
  5817. elmscale.type = 'number';
  5818. elmscale.id = 'textScale';
  5819. elmscale.value = textScale;
  5820. elmscale.addEventListener('change', scaleChange);
  5821. elmscale.addEventListener('keydown', (e) => {e.keyCode === 13 && scaleChange();});
  5822. scalectnr.appendChild(elmscale);
  5823. scalectnr.appendChild(document.createTextNode('%'));
  5824.  
  5825. // Beautifier
  5826. const btfnvlctnr = elements.btfnvlctnr;
  5827. const btfrvwctnr = elements.btfrvwctnr;
  5828. const btfcmnctnr = elements.btfcmnctnr;
  5829. const ckbtfnvl = elements.ckbtfnvl = $CrE('input');
  5830. const ckbtfrvw = elements.ckbtfrvw = $CrE('input');
  5831. const ckbtfcmn = elements.ckbtfcmn = $CrE('input');
  5832. ckbtfnvl.type = ckbtfrvw.type = ckbtfcmn.type = 'checkbox';
  5833. ckbtfnvl.page = 'novel';
  5834. ckbtfrvw.page = 'reviewshow';
  5835. ckbtfcmn.page = 'common';
  5836. ckbtfnvl.checked = CONFIG.BeautifierCfg.getConfig().novel.beautiful;
  5837. ckbtfrvw.checked = CONFIG.BeautifierCfg.getConfig().reviewshow.beautiful;
  5838. ckbtfcmn.checked = CONFIG.BeautifierCfg.getConfig().common.beautiful;
  5839. ckbtfnvl.addEventListener('change', beautifulChange);
  5840. ckbtfrvw.addEventListener('change', beautifulChange);
  5841. ckbtfcmn.addEventListener('change', beautifulChange);
  5842. btfnvlctnr.appendChild(ckbtfnvl);
  5843. btfrvwctnr.appendChild(ckbtfrvw);
  5844. btfcmnctnr.appendChild(ckbtfcmn);
  5845.  
  5846. // Favorite open
  5847. const favoropen = elements.favoropen;
  5848. const favorlast = elements.favorlast = $CrE('input');
  5849. favorlast.type = 'checkbox';
  5850. favorlast.checked = CONFIG.BkReviewPrefs.getConfig().favorlast;
  5851. favorlast.addEventListener('change', favorlastChange);
  5852. favoropen.appendChild(favorlast);
  5853.  
  5854. // Version control
  5855. const curversion = elements.curversion;
  5856. const updatecheck = elements.updatecheck;
  5857. const versiondisplay = $CrE('span');
  5858. versiondisplay.innerText = 'v' + GM_info.script.version;
  5859. updatecheck.innerText = TEXT_GUI_DETAIL_VERSION_CHECK;
  5860. updatecheck.style.color = 'grey';
  5861. updatecheck.style.textAlign = 'center';
  5862. updatecheck.addEventListener('click', updateOnclick);
  5863. curversion.appendChild(versiondisplay);
  5864.  
  5865. // Feedback
  5866. const feedback = elements.feedback;
  5867. feedback.style.color = 'grey';
  5868. feedback.style.textAlign = 'center';
  5869. feedback.addEventListener('click', function() {
  5870. window.open('https://greasyfork.org/scripts/416310/feedback');
  5871. });
  5872.  
  5873. // Update info
  5874. const updateinfo = elements.updateinfo;
  5875. updateinfo.style.color = 'grey';
  5876. updateinfo.style.textAlign = 'center';
  5877. updateinfo.addEventListener('click', function() {
  5878. window.open('https://greasyfork.org/scripts/416310#updateinfo');
  5879. })
  5880.  
  5881. // Config export/import
  5882. const exportcfg = elements.exportcfg;
  5883. const exportcfgnp = elements.exportcfgnp;
  5884. const importcfg = elements.importcfg;
  5885. const configinput = elements.configinput = $CrE('input');
  5886. configinput.type = 'file';
  5887. configinput.style.display = 'none';
  5888. importcfg.style.color = exportcfgnp.style.color = exportcfg.style.color = 'grey';
  5889. importcfg.style.textAlign = exportcfgnp.style.textAlign = exportcfg.style.textAlign = 'center';
  5890. exportcfg.addEventListener('click', ()=>{exportConfig(false);});
  5891. exportcfgnp.addEventListener('click', ()=>{exportConfig(true);});
  5892. importcfg.addEventListener('click', () => {configinput.click()});
  5893. configinput.addEventListener('change', configfileGot);
  5894. importcfg.addEventListener("dragenter", destroyEvent);
  5895. importcfg.addEventListener("dragover", destroyEvent);
  5896. importcfg.addEventListener('drop', configfileGot);
  5897. //importcfg.appendChild(configinput);
  5898.  
  5899. // Config management
  5900. const managecfg = elements.managecfg;
  5901. managecfg.style.color = 'grey';
  5902. managecfg.style.textAlign = 'center';
  5903. managecfg.addEventListener('click', openManagePanel);
  5904.  
  5905. // Paste event
  5906. window.addEventListener('paste', filePasted);
  5907.  
  5908. return elements;
  5909. }
  5910.  
  5911. function filePasted(e) {
  5912. const input = e.dataTransfer || e.clipboardData || window.clipboardData || e.target;
  5913. if (!input.files || input.files.length === 0) {return false;};
  5914.  
  5915. for (const file of input.files) {
  5916. switch (file.type) {
  5917. case 'image/bmp':
  5918. case 'image/gif':
  5919. case 'image/vnd.microsoft.icon':
  5920. case 'image/jpeg':
  5921. case 'image/png':
  5922. case 'image/svg+xml':
  5923. case 'image/tiff':
  5924. case 'image/webp':
  5925. confirm(TEXT_GUI_DETAIL_CONFIG_IMPORT_CONFIRM_SELECT.replace('{N}', file.name)) && pictureGot(e);
  5926. break;
  5927. case '': {
  5928. const splited = file.name.split('.');
  5929. const ext = splited[splited.length-1];
  5930. switch (ext) {
  5931. case 'wkp':
  5932. confirm(TEXT_GUI_DETAIL_CONFIG_IMPORT_CONFIRM_PASTE.replace('{N}', file.name)) && configfileGot(e);
  5933. }
  5934. }
  5935. }
  5936. }
  5937. }
  5938.  
  5939. function pictureGot(e) {
  5940. e.preventDefault();
  5941.  
  5942. // Get file
  5943. const input = e.dataTransfer || e.clipboardData || window.clipboardData || e.target;
  5944. if (!input.files || input.files.length === 0) {return false;};
  5945. const fileObj = input.files[0];
  5946. const mimetype = fileObj.type;
  5947. const name = fileObj.name;
  5948.  
  5949. // Create a new file input
  5950. elements.bgimage.removeChild(elements.imageinput);
  5951. const imageinput = elements.imageinput = $CrE('input');
  5952. imageinput.type = 'file';
  5953. imageinput.style.display = 'none';
  5954. imageinput.addEventListener('change', pictureGot);
  5955. elements.bgimage.appendChild(imageinput);
  5956.  
  5957. if (!mimetype || mimetype.split('/')[0] !== 'image') {
  5958. alertify.error(TEXT_ALT_IMAGE_FORMATERROR);
  5959. return false;
  5960. }
  5961. elements.bgioprt.innerHTML = TEXT_GUI_DETAIL_BGI_WORKING;
  5962.  
  5963. // Get object url
  5964. const objurl = URL.createObjectURL(fileObj);
  5965.  
  5966. // Get image url(format base64)
  5967. getImageUrl(objurl, true, true, (url) => {
  5968. if (!url) {return false;};
  5969.  
  5970. // Save to config
  5971. const config = CONFIG.BeautifierCfg.getConfig();
  5972. config.backgroundImage = url;
  5973. config.bgiName = name;
  5974. CONFIG.BeautifierCfg.saveConfig(config);
  5975. elements.bgioprt.innerHTML = name;
  5976. URL.revokeObjectURL(objurl);
  5977.  
  5978. // Upload if need
  5979. if (config.upload) {
  5980. alertify.notify(TEXT_ALT_IMAGE_UPLOAD_WORKING);
  5981. elements.bgioprt.innerHTML = TEXT_GUI_DETAIL_BGI_UPLOADING.replace('{NAME}', name);
  5982. const file = dataURLtoFile(url, name);
  5983. uploadImage({
  5984. file: file,
  5985. onerror: (e) => {
  5986. alertify.error(TEXT_ALT_IMAGE_UPLOAD_ERROR);
  5987. DoLog(LogLevel.Error, ['Upload error at pictureGot:', e]);
  5988. elements.bgioprt.innerHTML = TEXT_GUI_DETAIL_BGI_UPLOADFAILED.replace('{NAME}', name);
  5989. const config = CONFIG.BeautifierCfg.getConfig();
  5990. config.upload = elements.ckbgiup.checked = /^https?:\/\//.test(config.reveiwshow.backgroundImage);
  5991. CONFIG.BeautifierCfg.saveConfig(config);
  5992. },
  5993. onload: (json) => {
  5994. const config = CONFIG.BeautifierCfg.getConfig();
  5995. config.backgroundImage = json.url;
  5996. CONFIG.BeautifierCfg.saveConfig(config);
  5997. elements.bgioprt.innerHTML = TEXT_GUI_DETAIL_DEFAULT_BGI + '</br>' + TEXT_GUI_DETAIL_BGI.replace('{N}', CONFIG.BeautifierCfg.getConfig().bgiName);
  5998. alertify.success(TEXT_ALT_IMAGE_UPLOAD_SUCCESS.replace('{NAME}', json.name).replace('{URL}', json.url));
  5999. }
  6000. })
  6001. }
  6002. });
  6003. }
  6004.  
  6005. function uploadChange(e) {
  6006. e.preventDefault();
  6007.  
  6008. const config = CONFIG.BeautifierCfg.getConfig();
  6009. config.upload = !config.upload;
  6010. CONFIG.BeautifierCfg.saveConfig(config);
  6011. const name = config.bgiName ? config.bgiName : 'image.jpeg';
  6012.  
  6013. if (config.upload) {
  6014. // Upload
  6015. const url = config.backgroundImage;
  6016. if (!/^https?:\/\//.test(url)) {
  6017. alertify.notify(TEXT_ALT_IMAGE_UPLOAD_WORKING);
  6018. elements.bgioprt.innerHTML = TEXT_GUI_DETAIL_BGI_UPLOADING.replace('{NAME}', name);
  6019. const file = dataURLtoFile(url, name);
  6020. uploadImage({
  6021. file: file,
  6022. onerror: (e) => {
  6023. alertify.error(TEXT_ALT_IMAGE_UPLOAD_ERROR);
  6024. DoLog(LogLevel.Error, ['Upload error at uploadChange:', e]);
  6025. elements.bgioprt.innerHTML = TEXT_GUI_DETAIL_BGI_UPLOADFAILED.replace('{NAME}', name);
  6026. const config = CONFIG.BeautifierCfg.getConfig();
  6027. config.upload = elements.ckbgiup.checked = /^https?:\/\//.test(config.backgroundImage);
  6028. CONFIG.BeautifierCfg.saveConfig(config);
  6029. },
  6030. onload: (json) => {
  6031. const config = CONFIG.BeautifierCfg.getConfig();
  6032. config.backgroundImage = json.url;
  6033. config.bgiName = elements.bgioprt.innerHTML = json.name;
  6034. CONFIG.BeautifierCfg.saveConfig(config);
  6035. alertify.success(TEXT_ALT_IMAGE_UPLOAD_SUCCESS.replace('{NAME}', json.name).replace('{URL}', json.url));
  6036. }
  6037. });
  6038. }
  6039. } else {
  6040. // Download
  6041. const url = config.backgroundImage;
  6042. if (/^https?:\/\//.test(url)) {
  6043. alertify.notify(TEXT_ALT_IMAGE_DOWNLOAD_WORKING);
  6044. elements.bgioprt.innerHTML = TEXT_GUI_DETAIL_BGI_DOWNLOADING.replace('{NAME}', name);
  6045. getImageUrl(url, true, true, (dataurl) => {
  6046. if (!dataurl) {
  6047. const config = CONFIG.BeautifierCfg.getConfig();
  6048. config.upload = elements.ckbgiup.checked = /^https?:\/\//.test(config.backgroundImage);
  6049. CONFIG.BeautifierCfg.saveConfig(config);
  6050. return false;
  6051. };
  6052.  
  6053. // Save to config
  6054. const config = CONFIG.BeautifierCfg.getConfig();
  6055. config.backgroundImage = dataurl;
  6056. CONFIG.BeautifierCfg.saveConfig(config);
  6057. alertify.success(TEXT_ALT_IMAGE_DOWNLOAD_SUCCESS.replace('{NAME}', name));
  6058. elements.bgioprt.innerHTML = name;
  6059. });
  6060. }
  6061. }
  6062.  
  6063. setTimeout(()=>{elements.ckbgiup.checked = config.upload;}, 0);
  6064. }
  6065.  
  6066. function imagerChange(e) {
  6067. e.stopPropagation();
  6068. const radio = e.target;
  6069. if (radio.checked) {
  6070. const imager = DATA_IMAGERS[radio.imagerkey];
  6071. const config = CONFIG.UserGlobalCfg.getConfig();
  6072. config.imager = radio.imagerkey;
  6073. CONFIG.UserGlobalCfg.saveConfig(config);
  6074. alertify.message('图床已切换到{NAME}'.replace('{NAME}', imager.name));
  6075. imager.warning && alertify.warning(imager.warning);
  6076. }
  6077. }
  6078.  
  6079. function scaleChange(e) {
  6080. e.stopPropagation();
  6081. const config = CONFIG.BeautifierCfg.getConfig();
  6082. config.textScale = e.target.value;
  6083. CONFIG.BeautifierCfg.saveConfig(config);
  6084. alertify.message(TEXT_ALT_TEXTSCALE_CHANGED.replaceAll('{S}', config.textScale));
  6085. }
  6086.  
  6087. function beautifulChange(e) {
  6088. e.stopPropagation();
  6089. const checkbox = e.target;
  6090. const config = CONFIG.BeautifierCfg.getConfig();
  6091. config[checkbox.page].beautiful = checkbox.checked;
  6092. CONFIG.BeautifierCfg.saveConfig(config);
  6093. alertify.message(checkbox.checked ? TEXT_ALT_BEAUTIFUL_ON : TEXT_ALT_BEAUTIFUL_OFF);
  6094. }
  6095.  
  6096. function favorlastChange(e) {
  6097. e.stopPropagation();
  6098. const checkbox = e.target;
  6099. const config = CONFIG.BkReviewPrefs.getConfig();
  6100. config.favorlast = checkbox.checked;
  6101. CONFIG.BkReviewPrefs.saveConfig(config);
  6102. alertify.message(checkbox.checked ? TEXT_ALT_FAVORITE_LAST_ON : TEXT_ALT_FAVORITE_LAST_OFF);
  6103. }
  6104.  
  6105. function updateOnclick(e) {
  6106. TASK.Script.update(true);
  6107. }
  6108.  
  6109. function configfileGot(e) {
  6110. e.preventDefault();
  6111.  
  6112. // Get file
  6113. const input = e.dataTransfer || e.clipboardData || window.clipboardData || e.target;
  6114. if (!input.files || input.files.length === 0) {return false;};
  6115. const fileObj = input.files[0];
  6116. const splitedname = fileObj.name.split('.');
  6117. const ext = splitedname[splitedname.length-1].toLowerCase();
  6118. if (ext !== 'wkp') {
  6119. alertify.error(TEXT_ALT_DETAIL_CONFIG_IMPORT_ERROR_SELECT);
  6120. DoLog(LogLevel.Warning, 'pageDetail.insertSettings.GUI.configfileGot: userinput error.')
  6121. return false;
  6122. }
  6123.  
  6124. // Read config from file
  6125. try {
  6126. const FR = new FileReader();
  6127. FR.onload = fileOnload;
  6128. FR.readAsText(fileObj);
  6129. } catch(e) {
  6130. fileError(e);
  6131. }
  6132.  
  6133. function fileOnload(e) {
  6134. try {
  6135. // Get json
  6136. const json = JSON.parse(e.target.result);
  6137.  
  6138. // Import
  6139. importConfig(json);
  6140.  
  6141. alertify.success(TEXT_ALT_DETAIL_IMPORTED);
  6142. } catch(err) {
  6143. fileError(err);
  6144. }
  6145. }
  6146.  
  6147. function fileError(e) {
  6148. DoLog(LogLevel.Error, ['pageDetail.insertSettings.GUI.configfileGot:', e]);
  6149. alertify.error(TEXT_ALT_DETAIL_CONFIG_IMPORT_ERROR_READ);
  6150. }
  6151. }
  6152.  
  6153. function openManagePanel(e) {
  6154. const settings = {
  6155. id: 'ConfigPanel'
  6156. };
  6157. const SetPanel = new SettingPanel(settings);
  6158. const tblAccount = SetPanel.tables[0];
  6159.  
  6160. account();
  6161. drafts();
  6162. review_favorites();
  6163. pending_tip();
  6164.  
  6165. SetPanel.usercss += '.settingpanel-block.sp-center {text-align: center;} .settingpanel-block{overflow-wrap: anywhere;}';
  6166.  
  6167. function account() {
  6168. const userConfig = CONFIG.GlobalConfig.getConfig();
  6169. const users = userConfig.users ? userConfig.users : {};
  6170.  
  6171. // Create table
  6172. const table = new SetPanel.PanelTable({
  6173. rows: [{
  6174. blocks: [{
  6175. className: 'sp-center',
  6176. innerHTML: '账号管理',
  6177. colSpan: 3
  6178. }]
  6179. },{
  6180. blocks: [{
  6181. className: 'sp-center',
  6182. innerHTML: '用户名'
  6183. },{
  6184. className: 'sp-center',
  6185. innerHTML: '密码'
  6186. },{
  6187. className: 'sp-center',
  6188. innerHTML: '操作'
  6189. }]
  6190. }]
  6191. });
  6192. SetPanel.appendTable(table);
  6193.  
  6194. for (const [name, user] of Object.entries(users)) {
  6195. // Get account
  6196. const username = user.username;
  6197. const password = user.password;
  6198.  
  6199. // Row
  6200. const row = new SetPanel.PanelRow();
  6201. table.appendRow(row);
  6202.  
  6203. // Block username
  6204. const block_username = new SetPanel.PanelBlock({
  6205. className: 'sp-center',
  6206. innerHTML: username
  6207. });
  6208.  
  6209. // Block password
  6210. const spanpswd = $CrE('span');;
  6211. spanpswd.innerHTML = '*'.repeat(password.length);
  6212. const block_password = new SetPanel.PanelBlock({
  6213. className: 'sp-center',
  6214. children: [spanpswd]
  6215. });
  6216.  
  6217. // Block operator
  6218. const btndel = _createBtn('删除', make_del_callback(row, username));
  6219. const elmshow = $CrE('span'); elmshow.innerHTML = '查看';
  6220. const btnshow = _createBtn(elmshow, make_show_callback(elmshow, spanpswd, password));
  6221. const block_operator = new SetPanel.PanelBlock({
  6222. className: 'sp-center',
  6223. children: [btnshow, btndel]
  6224. });
  6225.  
  6226. // Append row to SettingPanel
  6227. row.appendBlock(block_username).appendBlock(block_password).appendBlock(block_operator);
  6228. }
  6229.  
  6230. function make_del_callback(row, username) {
  6231. return function(e) {
  6232. const userConfig = CONFIG.GlobalConfig.getConfig();
  6233. delete userConfig.users[username];
  6234. CONFIG.GlobalConfig.saveConfig(userConfig);
  6235. row.remove();
  6236. }
  6237. }
  6238.  
  6239. function make_show_callback(btn, span, password) {
  6240. let show = false;
  6241. let timeout;
  6242. return function toggle(e) {
  6243. show = !show;
  6244. span.innerHTML = show ? password : '*'.repeat(password.length);
  6245. btn.innerHTML = show ? '隐藏' : '查看';
  6246. }
  6247. }
  6248. }
  6249.  
  6250. function drafts() {
  6251. // Get config
  6252. const allCData = CONFIG.commentDrafts.getConfig();
  6253.  
  6254. // Create table
  6255. const table = new SetPanel.PanelTable({
  6256. rows: [{
  6257. blocks: [{
  6258. className: 'sp-center',
  6259. innerHTML: '书评草稿管理',
  6260. colSpan: 3
  6261. }]
  6262. },{
  6263. blocks: [{
  6264. className: 'sp-center',
  6265. innerHTML: '标题'
  6266. },{
  6267. className: 'sp-center',
  6268. innerHTML: '内容'
  6269. },{
  6270. className: 'sp-center',
  6271. innerHTML: '操作'
  6272. }]
  6273. }]
  6274. });
  6275. SetPanel.appendTable(table);
  6276.  
  6277. // Append rows
  6278. for (const [propkey, commentData] of Object.entries(allCData)) {
  6279. if (propkey === KEY_DRAFT_VERSION) {continue;}
  6280. const title = commentData.title;
  6281. const content = commentData.content;
  6282. const key = commentData.key;
  6283.  
  6284. // Row
  6285. const row = new SetPanel.PanelRow();
  6286. table.appendRow(row);
  6287.  
  6288. // Block title
  6289. const span_title = $CrE('span');
  6290. span_title.innerHTML = _decorate(title);
  6291. const block_title = new SetPanel.PanelBlock({className: 'draft-title sp-center', children: [span_title]});
  6292.  
  6293. // Block content
  6294. const span_content = $CrE('span');
  6295. span_content.innerHTML = _decorate(content);
  6296. const block_content = new SetPanel.PanelBlock({className: 'draft-content', children: [span_content]});
  6297.  
  6298. // Block operator
  6299. const elmshow = $CrE('span'); elmshow.innerHTML = '展开';
  6300. const btnshow = _createBtn(elmshow, make_show_callback(elmshow, key, row, span_title, span_content));
  6301. //const btnedit = _createBtn('编辑', make_edit_callback(key, row));
  6302. const btnopen = _createBtn('打开', make_open_callback(key));
  6303. const btndel = _createBtn('删除', make_del_callback(key, row));
  6304. const block_operator = new SetPanel.PanelBlock({className: 'draft-operator sp-center', children: [btnshow, btnopen, btndel]});
  6305.  
  6306. // Append to row
  6307. row.appendBlock(block_title).appendBlock(block_content).appendBlock(block_operator);
  6308. }
  6309.  
  6310. // Append css
  6311. SetPanel.usercss += '.settingpanel-block.draft-title {width: 20%;} .settingpanel-block.draft-content {width: 50%;} .settingpanel-block.draft-operator {width: 30%}';
  6312.  
  6313. function make_show_callback(btn, key, row, span_title, span_content) {
  6314. let show = false;
  6315. return function() {
  6316. const allCData = CONFIG.commentDrafts.getConfig();
  6317. const data = allCData[key];
  6318. if (!data) {
  6319. alertify.warning(TEXT_ALT_DETAIL_MANAGE_NOTFOUND);
  6320. row.remove();
  6321. return false;
  6322. }
  6323. show = !show;
  6324. btn.innerHTML = show ? '收起' : '展开';
  6325. span_title.innerHTML = _decorate(show ? {text: data.title, length: -1} : data.title);
  6326. span_content.innerHTML = _decorate(show ? {text: data.content, length: -1} : data.content);
  6327. };
  6328. }
  6329.  
  6330. function make_edit_callback(key, row) {
  6331. return function() {
  6332. // Get data
  6333. const allCData = CONFIG.commentDrafts.getConfig();
  6334. const data = allCData[key];
  6335. if (!data) {
  6336. alertify.warning(TEXT_ALT_DETAIL_MANAGE_NOTFOUND);
  6337. row.remove();
  6338. return false;
  6339. }
  6340.  
  6341. // Create box gui
  6342. const box = alertify.alert();
  6343. const container = box.elements.content;
  6344. makeEditor(container, data.rid.toString());
  6345. const form = $(container, 'form');
  6346. const ptitle = $(container, '#ptitle');
  6347. const pcontent = $(container, '#pcontent');
  6348. ptitle.value = data.title;
  6349. pcontent.value = data.content;
  6350. box.setting({
  6351. maximizable: false,
  6352. resizable: true
  6353. });
  6354. box.resizeTo('80%', '60%');
  6355. box.show();
  6356. };
  6357. }
  6358.  
  6359. function make_open_callback(key, row) {
  6360. return function() {
  6361. const allCData = CONFIG.commentDrafts.getConfig();
  6362. const data = allCData[key];
  6363. if (!data) {
  6364. alertify.warning(TEXT_ALT_DETAIL_MANAGE_NOTFOUND);
  6365. row.remove();
  6366. return false;
  6367. }
  6368. const url = data.rid ? URL_REVIEWSHOW_1.replace('{R}', data.rid.toString()) : URL_NOVELINDEX.replace('{I}', data.bid.toString());
  6369. window.open(url);
  6370. }
  6371. }
  6372.  
  6373. function make_del_callback(key, row) {
  6374. return function() {
  6375. const allCData = CONFIG.commentDrafts.getConfig();
  6376. delete allCData[key];
  6377. CONFIG.commentDrafts.saveConfig(allCData);
  6378. row.remove();
  6379. };
  6380. }
  6381. }
  6382.  
  6383. function review_favorites() {
  6384. // Get config
  6385. const config = CONFIG.BkReviewPrefs.getConfig();
  6386. const favs = config.favorites;
  6387.  
  6388. // Create table
  6389. const table = new SetPanel.PanelTable({
  6390. rows: [{
  6391. blocks: [{
  6392. className: 'sp-center',
  6393. innerHTML: '书评收藏管理',
  6394. colSpan: 3
  6395. }]
  6396. },{
  6397. blocks: [{
  6398. className: 'sp-center',
  6399. innerHTML: '主题'
  6400. },{
  6401. className: 'sp-center',
  6402. innerHTML: '备注'
  6403. },{
  6404. className: 'sp-center',
  6405. innerHTML: '操作'
  6406. }]
  6407. }]
  6408. });
  6409. SetPanel.appendTable(table);
  6410.  
  6411. // Append rows
  6412. for (const [rid, fav] of Object.entries(favs)) {
  6413. // Row
  6414. const row = new SetPanel.PanelRow();
  6415. table.appendRow(row);
  6416.  
  6417. // Title block
  6418. const span_title = $CrE('span');
  6419. span_title.innerHTML = _decorate({text: fav.name, length: 0});
  6420. const block_title = new SetPanel.PanelBlock({className: 'fav-title sp-center', children: [span_title]});
  6421.  
  6422. // Note block
  6423. const span_note = $CrE('span');
  6424. span_note.innerHTML = _decorate({text: fav.tiptitle, length: 0});
  6425. const block_note = new SetPanel.PanelBlock({className: 'fav-note sp-center', children: [span_note]});
  6426.  
  6427. // Operator block
  6428. const btn_open = _makeBtn({
  6429. tagName: 'a',
  6430. innerHTML: TEXT_GUI_DETAIL_MANAGE_FAV_NOTE_BTN_OPEN,
  6431. props: {
  6432. href: fav.href + (config.favorlast ? '&page=last' : ''),
  6433. target: '_blank'
  6434. }
  6435. });
  6436. const btn_edit = _makeBtn({
  6437. innerHTML: TEXT_GUI_DETAIL_MANAGE_FAV_NOTE_BTN_NOTE,
  6438. onclick: edit.bind(null, fav, row)
  6439. });
  6440. const btn_delete = _makeBtn({
  6441. innerHTML: TEXT_GUI_DETAIL_MANAGE_FAV_NOTE_BTN_DELETE,
  6442. onclick: del.bind(null, rid, row)
  6443. });
  6444. const block_oprt = new SetPanel.PanelBlock({className: 'fav-operator sp-center', children: [btn_open, btn_edit, btn_delete]});
  6445.  
  6446. // Append to row
  6447. row.appendBlock(block_title).appendBlock(block_note).appendBlock(block_oprt);
  6448. }
  6449.  
  6450. // Append css
  6451. SetPanel.usercss += '.settingpanel-block.fav-title {width: 35%;} .settingpanel-block.fav-note {width: 35%;} .settingpanel-block.fav-operator {width: 30%}';
  6452.  
  6453. function edit(fav, row) {
  6454. alertify.prompt(TEXT_GUI_DETAIL_MANAGE_FAV_NOTE_TITLE, TEXT_GUI_DETAIL_MANAGE_FAV_NOTE_TIP.replace('{TITLE}', fav.name), fav.tiptitle || '', onok, function() {});
  6455.  
  6456. function onok(e, value) {
  6457. // Save empty value as null
  6458. value === value || null;
  6459. fav.tiptitle = value;
  6460. CONFIG.BkReviewPrefs.saveConfig(config);
  6461. row.blocks[1].element.firstChild.innerHTML = _decorate({text: value, length: 0});
  6462. alertify.success(TEXT_GUI_DETAIL_MANAGE_FAV_SAVED);
  6463. }
  6464. }
  6465.  
  6466. function del(rid, row) {
  6467. alertify.confirm(TEXT_GUI_DETAIL_MANAGE_FAV_DELETE_TITLE, TEXT_GUI_DETAIL_MANAGE_FAV_DELETE_TIP.replace('{TITLE}', favs[rid].name), onok, function() {});
  6468.  
  6469. function onok() {
  6470. delete favs[rid];
  6471. CONFIG.BkReviewPrefs.saveConfig(config);
  6472. row.remove();
  6473. alertify.success(TEXT_GUI_DETAIL_MANAGE_FAV_DELETED);
  6474. }
  6475. }
  6476. }
  6477.  
  6478. function pending_tip() {
  6479. const span = $CrE('span');
  6480. span.innerHTML = '*其他管理项尚在开发中,请耐心等待';
  6481. span.classList.add(CLASSNAME_TEXT);
  6482. SetPanel.element.appendChild(span);
  6483. }
  6484.  
  6485. function _createBtn(htmlorbtn, onclick) {
  6486. const innerHTML = typeof htmlorbtn === 'string' ? htmlorbtn : htmlorbtn.innerHTML;
  6487. const btn = htmlorbtn instanceof HTMLElement ? htmlorbtn : $CrE('span');
  6488. !btn.classList.contains(CLASSNAME_BUTTON) && btn.classList.add(CLASSNAME_BUTTON);
  6489. btn.innerHTML = innerHTML;
  6490. btn.style.margin = '0px 0.5em';
  6491. onclick && btn.addEventListener('click', onclick);
  6492. return btn;
  6493. }
  6494.  
  6495. function _makeBtn(details) {
  6496. // Create element
  6497. const elm = $CrE(details.tagName || 'span');
  6498.  
  6499. // Write innerHTML
  6500. copyProp(details, elm, 'innerHTML');
  6501.  
  6502. // Write other properties
  6503. details.props && copyProps(details.props, elm, Object.keys(details.props));
  6504.  
  6505. // Make onclick
  6506. const onclick = details.onclick || (details.onclickMaker ? details.onclickMaker.apply(null, details.onclickArgs || []) : null);
  6507.  
  6508. // Create button
  6509. const btn = _createBtn(elm, onclick);
  6510.  
  6511. // Custom classes
  6512. details.classes && details.classes.forEach(function(c) {!btn.classList.contains(c) && btn.classList.add(c);});
  6513.  
  6514. return btn;
  6515. }
  6516.  
  6517. // details: 'string' or {text: '', length: 16}
  6518. function _decorate(details) {
  6519. // Get Args
  6520. details = !details ? '' : details;
  6521. details = typeof details === 'string' ? {text: details} : details;
  6522. const text = details.text || '';
  6523. const length = typeof details.length === 'number' ? (details.length > 0 ? details.length : Infinity) : 16;
  6524.  
  6525. const len = length > 0 ? length : 9999999999999;
  6526. const overflow = (text.length - len) > length;
  6527. const cut = overflow ? text.substr(0, len) : text;
  6528. const encoded = htmlEncode(cut).replaceAll('\n', '</br>');
  6529. const filled = text.length === 0 ? TEXT_GUI_DETAIL_CONFIG_MANAGE_EMPTY : (overflow ? encoded + TEXT_GUI_DETAIL_CONFIG_MANAGE_MORE : encoded);
  6530. return filled;
  6531. }
  6532. }
  6533. }
  6534.  
  6535. function createTableGUI(lines) {
  6536. const elements = {};
  6537. for (const line of lines) {
  6538. const tr = $CrE('tr');
  6539. for (const item of line) {
  6540. const td = $CrE('td');
  6541. item.html && (td.innerHTML = item.html);
  6542. item.colSpan && (td.colSpan = item.colSpan);
  6543. item.class && (td.className = item.class);
  6544. item.id && (td.id = item.id);
  6545. item.tiptitle && settip(td, item.tiptitle);
  6546. item.key && (elements[item.key] = td);
  6547. td.style.padding = '3px';
  6548. tr.appendChild(td);
  6549. }
  6550. tbody.appendChild(tr);
  6551. }
  6552. return elements;
  6553.  
  6554. function ElementObject(element) {
  6555. const p = new Proxy(element, {
  6556. get: function(elm, id, receiver) {
  6557. return elm[id] || $(elm, '#'+id);
  6558. }
  6559. });
  6560.  
  6561. return p;
  6562. }
  6563. }
  6564. }
  6565.  
  6566. // Index page add-on
  6567. function pageIndex() {
  6568. insertStatus();
  6569. showFavorites();
  6570. showLaterReads();
  6571.  
  6572. // Insert usersript inserted tip
  6573. function insertStatus() {
  6574. const blockcontent = $('#centers>.block:nth-child(1)>.blockcontent');
  6575. blockcontent.appendChild($CrE('br'));
  6576. const textNode = $CrE('span');
  6577. textNode.innerText = TEXT_GUI_INDEX_STATUS;
  6578. textNode.classList.add(CLASSNAME_TEXT);
  6579. blockcontent.appendChild(textNode);
  6580. }
  6581.  
  6582. // Show favorite reviews
  6583. function showFavorites() {
  6584. const links = [];
  6585. const config = CONFIG.BkReviewPrefs.getConfig();
  6586.  
  6587. for (const [rid, favorite] of Object.entries(config.favorites)) {
  6588. const href = favorite.href + (config.favorlast ? '&page=last' : '');
  6589. const tiptitle = favorite.tiptitle ? favorite.tiptitle : href;
  6590. const innerHTML = favorite.name.substr(0, 12) // prevent overflow
  6591. links.push({
  6592. innerHTML: innerHTML,
  6593. tiptitle: tiptitle,
  6594. href: href
  6595. });
  6596. }
  6597.  
  6598. const block = createWenkuBlock({
  6599. type: 'toplist',
  6600. parent: '#left',
  6601. title: TEXT_GUI_INDEX_FAVORITES,
  6602. items: links
  6603. });
  6604. }
  6605.  
  6606. // Show top-6 read-later books
  6607. function showLaterReads() {
  6608. const config = CONFIG.bookcasePrefs.getConfig().laterbooks;
  6609. const books = sortLaterReads(config.books, config.sortby).filter((e,i,a)=>(i<6));
  6610. const items = books.map(function(book, i) {
  6611. return {
  6612. href: URL_NOVELINDEX.replace('{I}', book.aid),
  6613. src: book.cover,
  6614. tiptitle: book.name,
  6615. text: book.name
  6616. }
  6617. });
  6618. const block = createWenkuBlock({
  6619. type: 'imagelist',
  6620. parent: '#centers',
  6621. title: TEXT_GUI_INDEX_LATERBOOKS,
  6622. items: items
  6623. });
  6624. settip($(block, '.blocktitle'), TEXT_TIP_INDEX_LATERREADS);
  6625. }
  6626. }
  6627.  
  6628. // Download page add-on
  6629. function pageDownload() {
  6630. let i;
  6631. let dlCount = 0; // number of active download tasks
  6632. let dlAllRunning = false; // whether there is downloadAll running
  6633.  
  6634. // Get novel info
  6635. const novelInfo = {}; collectNovelInfo();
  6636. const myDlBtns = [];
  6637.  
  6638. // Donwload GUI
  6639. downloadGUI();
  6640.  
  6641. // Server GUI
  6642. serverGUI();
  6643.  
  6644. /* ******************* Code ******************* */
  6645. function collectNovelInfo() {
  6646. novelInfo.novelName = $('html body div.main div#centerm div#content table.grid caption a').innerText;
  6647. novelInfo.displays = getAllNameEles();
  6648. novelInfo.volumeNames = getAllNames();
  6649. novelInfo.type = getUrlArgv('type');
  6650. novelInfo.ext = novelInfo.type !== 'txtfull' ? novelInfo.type : 'txt';
  6651. }
  6652.  
  6653. // Donwload GUI
  6654. function downloadGUI() {
  6655. switch (novelInfo.type) {
  6656. case 'txt':
  6657. downloadGUI_txt();
  6658. break;
  6659. case 'txtfull':
  6660. downloadGUI_txtfull();
  6661. break;
  6662. case 'umd':
  6663. downloadGUI_umd();
  6664. break;
  6665. case 'jar':
  6666. downloadGUI_jar();
  6667. break;
  6668. default:
  6669. DoLog(LogLevel.Warning, 'pageDownload.downloadGUI: Unknown download type');
  6670. }
  6671. }
  6672.  
  6673. // Donwload GUI for txt
  6674. function downloadGUI_txt() {
  6675. // Only txt is really separated by volumes
  6676. if (novelInfo.type !== 'txt') {return false;};
  6677.  
  6678. // define vars
  6679. let i;
  6680.  
  6681. const tbody = $('table>tbody');
  6682. const header = $(tbody, 'th').parentElement;
  6683. const thead = $(header, 'th');
  6684.  
  6685. // Append new th
  6686. const newHead = thead.cloneNode(true);
  6687. newHead.innerText = TEXT_GUI_SDOWNLOAD;
  6688. thead.width = '40%';
  6689. header.appendChild(newHead);
  6690.  
  6691. // Append new td
  6692. const trs = $All(tbody, 'tr');
  6693. for (i = 1; i < trs.length; i++) { /* i = 1 to trs.length-1: skip header */
  6694. const index = i-1;
  6695. const tr = trs[i];
  6696. const newTd = $(tr, 'td.even').cloneNode(true);
  6697. const links = $All(newTd, 'a');
  6698. for (const a of links) {
  6699. a.classList.add(CLASSNAME_BUTTON);
  6700. a.info = {
  6701. description: 'volume download button',
  6702. name: novelInfo.volumeNames[index],
  6703. filename: TEXT_GUI_SDOWNLOAD_FILENAME
  6704. .replace('{NovelName}', novelInfo.novelName)
  6705. .replace('{VolumeName}', novelInfo.volumeNames[index])
  6706. .replace('{Extension}', novelInfo.ext),
  6707. index: index,
  6708. display: novelInfo.displays[index]
  6709. }
  6710. a.onclick = downloadOnclick;
  6711. myDlBtns.push(a);
  6712. }
  6713. tr.appendChild(newTd);
  6714. }
  6715.  
  6716. // Append new tr, provide batch download
  6717. const newTr = trs[trs.length-1].cloneNode(true);
  6718. const newTds = $All(newTr, 'td');
  6719. newTds[0].innerText = TEXT_GUI_DOWNLOADALL;
  6720. //clearChildnodes(newTds[1]); clearChildnodes(newTds[2]);
  6721. newTds[1].innerHTML = newTds[2].innerHTML = TEXT_GUI_NOTHINGHERE;
  6722. tbody.insertBefore(newTr, tbody.children[1]);
  6723.  
  6724. const allBtns = $All(newTds[3], 'a');
  6725. for (i = 0; i < allBtns.length; i++) {
  6726. const a = allBtns[i];
  6727. a.href = 'javascript:void(0);';
  6728. a.info = {
  6729. description: 'download all button',
  6730. index: i
  6731. }
  6732. a.onclick = downloadAllOnclick;
  6733. }
  6734.  
  6735. // Download button onclick
  6736. function downloadOnclick() {
  6737. const a = this;
  6738. a.info.display.innerText = a.info.name + TEXT_GUI_WAITING;
  6739. downloadFile({
  6740. url: a.href,
  6741. name: a.info.filename,
  6742. onloadstart: function(e) {
  6743. a.info.display.innerText = a.info.name + TEXT_GUI_DOWNLOADING;
  6744. },
  6745. onload: function(e) {
  6746. a.info.display.innerText = a.info.name + TEXT_GUI_DOWNLOADED;
  6747. }
  6748. });
  6749. return false;
  6750. }
  6751.  
  6752. // DownloadAll button onclick
  6753. function downloadAllOnclick() {
  6754. const a = this;
  6755. const index = (a.info.index+1)%3;
  6756. for (let i = 0; i < myDlBtns.length; i++) {
  6757. if ((i+1)%3 !== index) {continue;};
  6758. const btn = myDlBtns[i];
  6759. btn.click();
  6760. }
  6761. return false;
  6762. }
  6763. }
  6764.  
  6765. // Donwload GUI for txtfull
  6766. function downloadGUI_txtfull() {
  6767. const container = $('#content>table tr>td:nth-child(3)');
  6768. const links = arrfilter(container.children, (e,i)=>([1,3,5].includes(i)));
  6769. const TEXTS = ['简体(G)', '简体(U)', '繁体(U)'];
  6770. const elms = [];
  6771.  
  6772. elms.push($CrE('br'));
  6773. elms.push(document.createTextNode('程序重命名('));
  6774. for (let i = 0; i < links.length; i++) {
  6775. const a = links[i];
  6776. const btn = $CrE('a');
  6777. btn.href = a.previousElementSibling.href;
  6778. btn.download = novelInfo.novelName + '.txt';
  6779. btn.innerHTML = TEXTS[i];
  6780. btn.classList.add(CLASSNAME_BUTTON);
  6781. btn.addEventListener('click', downloadFromA);
  6782. elms.push(btn);
  6783. i+1 < links.length && elms.push(a.previousSibling.cloneNode());
  6784. }
  6785. elms.push(document.createTextNode(')'));
  6786.  
  6787. for (const elm of elms) {
  6788. container.appendChild(elm);
  6789. }
  6790. }
  6791.  
  6792. // Donwload GUI for umd
  6793. function downloadGUI_umd() {
  6794. const container = $('#content>table tr>td:nth-child(5)');
  6795. const a = container.firstChild;
  6796. const btn = $CrE('a');
  6797. btn.href = a.href;
  6798. btn.download = novelInfo.novelName + '.umd';
  6799. btn.innerHTML = '重命名下载';
  6800. btn.classList.add(CLASSNAME_BUTTON);
  6801. btn.addEventListener('click', downloadFromA);
  6802. a.insertAdjacentElement('afterend', btn);
  6803. a.insertAdjacentElement('afterend', $CrE('br'));
  6804. }
  6805.  
  6806. // Donwload GUI for jar
  6807. function downloadGUI_jar() {
  6808. const container = $('#content>table tr>td:nth-child(5)');
  6809. const links = arrfilter(container.children, ()=>(true));
  6810. const TEXTS = ['重命名JAR', '重命名JAD'];
  6811. const EXTS = ['.jar', '.jad'];
  6812. const elms = [];
  6813.  
  6814. elms.push($CrE('br'));
  6815. for (let i = 0; i < links.length; i++) {
  6816. const a = links[i];
  6817. const btn = $CrE('a');
  6818. btn.href = a.href;
  6819. btn.download = novelInfo.novelName + EXTS[i];
  6820. btn.innerHTML = TEXTS[i];
  6821. btn.classList.add(CLASSNAME_BUTTON);
  6822. btn.addEventListener('click', downloadFromA);
  6823. elms.push(btn);
  6824. i+1 < links.length && elms.push(a.nextSibling.cloneNode());
  6825. }
  6826.  
  6827. for (const elm of elms) {
  6828. container.appendChild(elm);
  6829. }
  6830.  
  6831. $('#content>table tr>th:nth-child(4)').setAttribute('width', '47%');
  6832. $('#content>table tr>th:nth-child(5)').setAttribute('width', '20%');
  6833. }
  6834.  
  6835. function downloadFromA(e) {
  6836. e.preventDefault();
  6837.  
  6838. const btn = e.target;
  6839. const url = btn.href;
  6840.  
  6841. downloadFile({
  6842. url: url,
  6843. name: btn.download
  6844. });
  6845. }
  6846.  
  6847. // Get all name display elements
  6848. function getAllNameEles() {
  6849. return $All('.grid tbody tr .odd');
  6850. }
  6851.  
  6852. // Get all names
  6853. function getAllNames() {
  6854. const all = getAllNameEles()
  6855. const names = [];
  6856. for (let i = 0; i < all.length; i++) {
  6857. names[i] = all[i].innerText;
  6858. }
  6859. return names;
  6860. }
  6861.  
  6862. // Server GUI
  6863. function serverGUI() {
  6864. let servers = $All('#content>b');
  6865. let serverEles = [];
  6866. for (i = 0; i < servers.length; i++) {
  6867. if (servers[i].innerText.includes('wenku8.com')) {
  6868. serverEles.push(servers[i]);
  6869. }
  6870. }
  6871. for (i = 0; i < serverEles.length; i++) {
  6872. serverEles[i].classList.add(CLASSNAME_BUTTON);
  6873. serverEles[i].addEventListener('click', function () {
  6874. changeAllServers(this.innerText);
  6875. });
  6876. settip(serverEles[i], TEXT_TIP_SERVERCHANGE);
  6877. }
  6878. }
  6879.  
  6880. // Change all server elements
  6881. function changeAllServers(server) {
  6882. let i;
  6883. const allA = $All('.even a');
  6884. for (i = 0; i < allA.length; i++) {
  6885. changeServer(server, allA[i]);
  6886. }
  6887. }
  6888.  
  6889. // Change server for an element
  6890. function changeServer(server, element) {
  6891. if (!element.href) {return false;};
  6892. element.href = element.href.replace(/\/\/dl\d?\.wenku8\.com\//g, '//' + server + '/');
  6893. }
  6894.  
  6895. // Array.prototype.filter
  6896. function arrfilter(arr, callback) {
  6897. return Array.prototype.filter.call(arr, callback);
  6898. }
  6899. }
  6900.  
  6901. // Login page add-on
  6902. function pageLogin() {
  6903. const form = $('form[name="frmlogin"]');
  6904. if (!form) {return false;}
  6905. const eleUsername = $(form, 'input.text[name="username"]');
  6906. const elePassword = $(form, 'input.text[name="password"]')
  6907.  
  6908. catchAccount();
  6909.  
  6910. // Save account info
  6911. function catchAccount() {
  6912. form.addEventListener('submit', () => {
  6913. const config = CONFIG.GlobalConfig.getConfig();
  6914. const username = eleUsername.value;
  6915. const password = elePassword.value;
  6916. config.users = config.users ? config.users : {};
  6917. config.users[username] = {
  6918. username: username,
  6919. password: password
  6920. }
  6921. CONFIG.GlobalConfig.saveConfig(config);
  6922. });
  6923. }
  6924. }
  6925.  
  6926. // Account fast switching
  6927. function multiAccount() {
  6928. if (!$('.fl')) {return false;};
  6929. GUI();
  6930.  
  6931. function GUI() {
  6932. // Add switch select
  6933. const eleTopLeft = $('.fl');
  6934. const eletext = $CrE('span');
  6935. const sltSwitch = $CrE('select');
  6936. eletext.innerText = TEXT_GUI_ACCOUNT_SWITCH;
  6937. eletext.classList.add(CLASSNAME_TEXT);
  6938. eletext.style.marginLeft = '0.5em';
  6939. eleTopLeft.appendChild(eletext);
  6940. eleTopLeft.appendChild(sltSwitch);
  6941.  
  6942. // Not logged in, create and select an empty option
  6943. // Select current user's option
  6944. if (!getUserName()) {
  6945. appendOption(TEXT_GUI_ACCOUNT_NOTLOGGEDIN, '').selected = true;
  6946. };
  6947.  
  6948. // Add select options
  6949. const userConfig = CONFIG.GlobalConfig.getConfig();
  6950. const users = userConfig.users ? userConfig.users : {};
  6951. const names = Object.keys(users);
  6952. if (names.length === 0) {
  6953. appendOption(TEXT_GUI_ACCOUNT_NOACCOUNT, '');
  6954. settip(sltSwitch, TEXT_TIP_ACCOUNT_NOACCOUNT);
  6955. }
  6956. for (const username of names) {
  6957. appendOption(username, username)
  6958. }
  6959.  
  6960. // Select current user's option
  6961. if (getUserName()) {selectCurUser();};
  6962.  
  6963. // onchange: switch account
  6964. sltSwitch.addEventListener('change', (e) => {
  6965. const select = e.target;
  6966. if (!select.value || !confirm(TEXT_GUI_ACCOUNT_CONFIRM.replace('{N}', select.value))) {
  6967. selectCurUser();
  6968. destroyEvent(e);
  6969. return;
  6970. }
  6971.  
  6972. switchAccount(select.value);
  6973. });
  6974.  
  6975. function appendOption(text, value) {
  6976. const option = $CrE('option');
  6977. option.innerText = text;
  6978. option.value = value;
  6979. sltSwitch.appendChild(option);
  6980. return option;
  6981. }
  6982.  
  6983. function selectCurUser() {
  6984. for (const option of $All(sltSwitch, 'option')) {
  6985. option.selected = getUserName().toLowerCase() === option.value.toLowerCase();
  6986. }
  6987. }
  6988. }
  6989.  
  6990. function switchAccount(username) {
  6991. // Logout
  6992. alertify.notify(TEXT_ALT_ACCOUNT_WORKING_LOGOFF);
  6993. GM_xmlhttpRequest({
  6994. method: 'GET',
  6995. url: URL_USRLOGOFF,
  6996. onload: function(response) {
  6997. // Login
  6998. alertify.notify(TEXT_ALT_ACCOUNT_WORKING_LOGIN);
  6999. const account = CONFIG.GlobalConfig.getConfig().users[username];
  7000. const data = DATA_XHR_LOGIN
  7001. .replace('{U}', $URL.encode(account.username))
  7002. .replace('{P}', $URL.encode(account.password))
  7003. .replace('{C}', $URL.encode('315360000')) // Expire time: 1 year
  7004. GM_xmlhttpRequest({
  7005. method: 'POST',
  7006. url: URL_USRLOGIN,
  7007. data: data,
  7008. headers: {
  7009. "Content-Type": "application/x-www-form-urlencoded"
  7010. },
  7011. onload: function() {
  7012. let box = alertify.success(TEXT_ALT_ACCOUNT_SWITCHED.replace('{N}', username));
  7013. redirectGMStorage(getUserID());
  7014. DoLog(LogLevel.Info, 'GM_storage redirected to ' + String(getUserID()));
  7015. const timeout = setTimeout(()=>{location.href=location.href;}, 3000);
  7016. box.callback = (isClicked) => {
  7017. isClicked && clearTimeout(timeout);
  7018. };
  7019. }
  7020. })
  7021. }
  7022. })
  7023. }
  7024. }
  7025.  
  7026. // API page and its sub pages add-on
  7027. function pageAPI(API) {
  7028. addStyle(CSS_PAGE_API, 'plus_api_css');
  7029. //logAPI();
  7030.  
  7031. let result;
  7032. switch(API) {
  7033. case 'modules/article/addbookcase.php':
  7034. result = pageAddbookcase();
  7035. break;
  7036. case 'modules/article/packshow.php':
  7037. result = pagePackshow();
  7038. break;
  7039. default:
  7040. result = logAPI();
  7041. }
  7042.  
  7043. return result;
  7044.  
  7045. function logAPI() {
  7046. DoLog('This is wenku API page.');
  7047. DoLog('API is: [' + API + ']');
  7048. DoLog('There is nothing to do. Quiting...');
  7049. }
  7050.  
  7051. function pageAddbookcase() {
  7052.  
  7053. // Append link to bookcase page
  7054. addBottomButton({
  7055. href: `https://${location.host}/modules/article/bookcase.php`,
  7056. innerHTML: TEXT_GUI_API_ADDBOOKCASE_TOBOOKCASE
  7057. });
  7058.  
  7059. // Append link to remove from bookcase (not finished)
  7060. /*addBottomButton({
  7061. href: `https://${location.host}/modules/article/bookcase.php?delid=` + getUrlArgv('bid'),
  7062. innerHTML: TEXT_GUI_API_ADDBOOKCASE_REMOVE,
  7063. onclick: function() {
  7064. confirm('确实要将本书移出书架么?')
  7065. }
  7066. });*/
  7067. }
  7068.  
  7069. function pagePackshow() {
  7070. // Load packshow page
  7071. loadPage();
  7072.  
  7073. // Packshow page loader
  7074. function loadPage() {
  7075. // Data
  7076. const language = getLang();
  7077. const aid = getUrlArgv('id');
  7078. const type = getUrlArgv('type');
  7079.  
  7080. if (!['txt', 'txtfull', 'umd', 'jar'].includes(type)) {
  7081. return false;
  7082. }
  7083.  
  7084. // Hide api box
  7085. const apiBox = $('body>div:nth-child(1)');
  7086. apiBox.style.display = 'none';
  7087.  
  7088. // Disable api css
  7089. addStyle('', 'plus_api_css');
  7090.  
  7091. // AsyncManager
  7092. const resource = {xmlIndex: null, xmlInfo: null, oDoc: null};
  7093. const AM = new AsyncManager();
  7094. AM.onfinish = fetchFinish;
  7095.  
  7096. // Show soft alert
  7097. alertify.message(TEXT_TIP_API_PACKSHOW_LOADING);
  7098.  
  7099. // Set Title
  7100. document.title = TEXT_GUI_API_PACKSHOW_TITLE_LOADING;
  7101.  
  7102. // Load model page
  7103. const bgImage = $('body>.plus_cbty_image');
  7104. AM.add();
  7105. getDocument(URL_PACKSHOW.replace('{A}', "1").replace('{T}', type), function(oDoc) {
  7106. resource.oDoc = oDoc;
  7107.  
  7108. // Insert body elements
  7109. const nodes = Array.prototype.map.call(oDoc.body.childNodes, (elm) => (elm));
  7110. for (const node of nodes) {
  7111. document.body.insertBefore(node, bgImage);
  7112. }
  7113.  
  7114. // Insert css link and scripts
  7115. const links = Array.prototype.map.call($All(oDoc, 'link[rel="stylesheet"][href]'), (elm) => (elm));
  7116. const olinks = Array.prototype.map.call($All('link[rel="stylesheet"][href]'), (elm) => (elm));
  7117. for (const link of links) {
  7118. if (!link.href.startsWith('http')) {continue;}
  7119. for (const olink of Array.prototype.filter.call(olinks, (l) => (l.href === link.href))) {olink.parentElement.removeChild(olink);}
  7120. document.head.appendChild(link);
  7121. }
  7122. const scripts = Array.prototype.map.call($All(oDoc, 'script[src]'), (elm) => (elm));
  7123. for (const script of scripts) {
  7124. if (!script.src.startsWith('http')) {continue;}
  7125. if (Array.prototype.filter.call($All('script[src]'), (s) => (s.src === script.src)).length > 0) {continue;}
  7126. document.head.appendChild(script);
  7127. }
  7128.  
  7129. // Fix all <a>.href
  7130. Array.from($All('a')).forEach((a) => {
  7131. if (/https?:\/\/www\.wenku8\.(net|cc)\/modules\/article\/packshow.php\??/.test(a.href)) {
  7132. a.href = a.href.replace(/([\?&])id=\d+/, '$1id='+aid)
  7133. }
  7134. });
  7135.  
  7136. AM.finish();
  7137. });
  7138.  
  7139. // Load novel index
  7140. AM.add();
  7141. AndAPI.getNovelIndex({
  7142. aid: aid,
  7143. lang: language,
  7144. callback: function(xml) {
  7145. resource.xmlIndex = xml;
  7146. AM.finish();
  7147. }
  7148. });
  7149. AM.add();
  7150. AndAPI.getNovelShortInfo({
  7151. aid: aid,
  7152. lang: language,
  7153. callback: function(xml) {
  7154. resource.xmlInfo = xml;
  7155. AM.finish();
  7156. }
  7157. });
  7158.  
  7159. AM.finishEvent = true;
  7160.  
  7161. function fetchFinish() {
  7162. // Resources
  7163. const xmlIndex = resource.xmlIndex;
  7164. const xmlInfo = resource.xmlInfo;
  7165. const oDoc = resource.oDoc;
  7166.  
  7167. // Elements
  7168. const content = $('#content');
  7169. const table = $(content, 'table');
  7170. const tbody = $(table, 'tbody');
  7171.  
  7172. // Data
  7173. const name = $(xmlInfo, 'data[name="Title"]').childNodes[0].nodeValue;
  7174. const lastupdate = $(xmlInfo, 'data[name="LastUpdate"]').getAttribute('value');
  7175. const aBook = $(table, 'caption>a:first-child');
  7176. const charsets = ['gbk', 'utf-8', 'big5', 'gbk', 'utf-8', 'big5'];
  7177. const innerTexts = ['简体(G)', '简体(U)', '繁体(U)', '简体(G)', '简体(U)', '繁体(U)'];
  7178.  
  7179. // Set Title
  7180. document.title = TEXT_GUI_API_PACKSHOW_TITLE.replace('{N}', name);
  7181.  
  7182. // Set book
  7183. aBook.innerText = name;
  7184. aBook.href = URL_BOOKINTRO.replace('{A}', aid);
  7185.  
  7186. // Load book index
  7187. loadIndex();
  7188.  
  7189. // Soft alert
  7190. alertify.success(TEXT_TIP_API_PACKSHOW_LOADED);
  7191.  
  7192. // Enter common download page enhance
  7193. pageDownload();
  7194.  
  7195. // Book index loader
  7196. function loadIndex() {
  7197. switch (type) {
  7198. case 'txt':
  7199. loadIndex_txt();
  7200. break;
  7201. case 'txtfull':
  7202. loadIndex_txtfull();
  7203. break;
  7204. case 'umd':
  7205. loadIndex_umd();
  7206. break;
  7207. case 'jar':
  7208. loadIndex_jar();
  7209. break;
  7210. }
  7211. }
  7212.  
  7213. // Book index loader for type txt
  7214. function loadIndex_txt() {
  7215. // Clear tbody trs
  7216. for (const tr of $All(table, 'tr+tr')) {
  7217. tbody.removeChild(tr);
  7218. }
  7219.  
  7220. // Make new trs
  7221. for (const volume of $All(xmlIndex, 'volume')) {
  7222. const tr = makeTr(volume);
  7223. tbody.appendChild(tr);
  7224. }
  7225.  
  7226. function makeTr(volume) {
  7227. const tr = $CrE('tr');
  7228. const [tdName, td1, td2] = [$CrE('td'), $CrE('td'), $CrE('td')];
  7229. const a = Array(6);
  7230. const vid = volume.getAttribute('vid');
  7231. const vname = volume.childNodes[0].nodeValue;
  7232.  
  7233. // init tds
  7234. tdName.classList.add('odd');
  7235. td1.classList.add('even');
  7236. td2.classList.add('even');
  7237. td1.align = td2.align = 'center';
  7238.  
  7239. // Set volume name
  7240. tdName.innerText = vname;
  7241.  
  7242. // Make <a> links
  7243. for (let i = 0; i < a.length; i++) {
  7244. a[i] = $CrE('a');
  7245. a[i].target = '_blank';
  7246. a[i].href = 'http://dl.wenku8.com/packtxt.php?aid=' + aid +
  7247. '&vid=' + vid +
  7248. (i >= 3 ? '&aname=' + $URL.encode(name) : '') +
  7249. (i >= 3 ? '&vname=' + $URL.encode(vname) : '') +
  7250. '&charset=' + charsets[i];
  7251. a[i].innerText = innerTexts[i];
  7252. (i < 3 ? td1 : td2).appendChild(a[i]);
  7253. }
  7254.  
  7255. // Insert whitespace textnode
  7256. for (const i of [1, 2, 4, 5]) {
  7257. (i < 3 ? td1 : td2).insertBefore(document.createTextNode('\n'), a[i]);
  7258. }
  7259.  
  7260. tr.appendChild(tdName);
  7261. tr.appendChild(td1);
  7262. tr.appendChild(td2);
  7263.  
  7264. return tr;
  7265. }
  7266. }
  7267.  
  7268. // Book index loader for type txtfull
  7269. function loadIndex_txtfull() {
  7270. const tr = $(tbody, 'tr+tr');
  7271. const tds = Array.prototype.map.call(tr.children, (elm) => (elm));
  7272.  
  7273. tds[0].innerText = lastupdate;
  7274. tds[1].innerText = TEXT_GUI_UNKNOWN;
  7275. for (const a of $All(tds[2], 'a')) {
  7276. a.href = a.href.replace(/id=\d+/, 'id='+aid).replace(/fname=[^&]+/, 'fname='+$URL.encode(name));
  7277. }
  7278. }
  7279.  
  7280. // Book index loader for type umd
  7281. function loadIndex_umd() {
  7282. const tr = $(tbody, 'tr+tr');
  7283. const tds = toArray(tr.children);
  7284.  
  7285. tds[0].innerText = tds[1].innerText = TEXT_GUI_UNKNOWN;
  7286. tds[2].innerText = lastupdate;
  7287. tds[3].innerText = $(xmlIndex, 'volume:first-child').childNodes[0].nodeValue + '—' + $(xmlIndex, 'volume:last-child').childNodes[0].nodeValue;
  7288. const as = [].concat(toArray($All(tds[4], 'a'))).concat(toArray($All(table, 'caption>a+a')));
  7289. for (const a of as) {
  7290. a.href = a.href.replace(/id=\d+/, 'id='+aid);
  7291. }
  7292. }
  7293.  
  7294. // Book index loader for type jar
  7295. function loadIndex_jar() {
  7296. // Currently type jar is the same as type umd
  7297. loadIndex_umd();
  7298. }
  7299.  
  7300. function toArray(_arr) {
  7301. return Array.prototype.map.call(_arr, (elm) => (elm));
  7302. }
  7303. }
  7304. }
  7305. }
  7306.  
  7307. // Add a bottom-styled botton into bottom line, to the first place
  7308. function addBottomButton(details) {
  7309. const aClose = $('a[href="javascript:window.close()"]');
  7310. const bottom = aClose.parentElement;
  7311. const a = $CrE('a');
  7312. const t1 = document.createTextNode('[');
  7313. const t2 = document.createTextNode(']');
  7314. const blank = $CrE('span');
  7315. blank.innerHTML = ' ';
  7316. blank.style.width = '0.5em';
  7317. a.href = details.href;
  7318. a.innerHTML = details.innerHTML;
  7319. a.onclick = details.onclick;
  7320. [blank, t2, a, t1].forEach((elm) => {bottom.insertBefore(elm, bottom.childNodes[0]);});
  7321. }
  7322. }
  7323.  
  7324. // Check if current page is an wenku API page ('处理成功', '出现错误!')
  7325. function isAPIPage() {
  7326. // API page has just one .block div and one close-page button
  7327. const block = $All('.block');
  7328. const close = $All('a[href="javascript:window.close()"]');
  7329. return block.length === 1 && close.length === 1;
  7330. }
  7331.  
  7332. // Basic functions
  7333. // querySelector
  7334. function $() {
  7335. switch(arguments.length) {
  7336. case 2:
  7337. return arguments[0].querySelector(arguments[1]);
  7338. break;
  7339. default:
  7340. return document.querySelector(arguments[0]);
  7341. }
  7342. }
  7343. // querySelectorAll
  7344. function $All() {
  7345. switch(arguments.length) {
  7346. case 2:
  7347. return arguments[0].querySelectorAll(arguments[1]);
  7348. break;
  7349. default:
  7350. return document.querySelectorAll(arguments[0]);
  7351. }
  7352. }
  7353. // createElement
  7354. function $CrE() {
  7355. switch(arguments.length) {
  7356. case 2:
  7357. return arguments[0].createElement(arguments[1]);
  7358. break;
  7359. default:
  7360. return document.createElement(arguments[0]);
  7361. }
  7362. }
  7363. // Object1[prop] ==> Object2[prop]
  7364. function copyProp(obj1, obj2, prop) {obj1[prop] !== undefined && (obj2[prop] = obj1[prop]);}
  7365. function copyProps(obj1, obj2, props) {props.forEach((prop) => (copyProp(obj1, obj2, prop)));}
  7366.  
  7367. // Display an alertify prompt for editing user-remark
  7368. function editUserRemark(uid, name, callback) {
  7369. const config = CONFIG.RemarksConfig.getConfig();
  7370. const user = config.user[uid] || {uid: uid, name: name, remark: ''};
  7371.  
  7372. // Update name
  7373. user.name = name;
  7374. CONFIG.RemarksConfig.saveConfig(config);
  7375.  
  7376. // Display dialog
  7377. alertify.prompt(TEXT_GUI_USER_USERREMARKEDIT_TITLE, TEXT_GUI_USER_USERREMARKEDIT_MSG.replace('{N}', user.name), user.remark, onChange, onCancel);
  7378.  
  7379. function onChange(evt, value) {
  7380. const config = CONFIG.RemarksConfig.getConfig();
  7381. if (value) {
  7382. const user = config.user[uid] || {uid: uid, name: name, remark: ''};
  7383. user.remark = value;
  7384. config.user[uid] = user;
  7385. } else {
  7386. delete config.user[uid]
  7387. }
  7388. CONFIG.RemarksConfig.saveConfig(config);
  7389.  
  7390. callback(value);
  7391. }
  7392.  
  7393. function onCancel() {}
  7394. }
  7395.  
  7396. // Send reply for bookreview
  7397. // Arg: {rid, title, content, onload:(oDoc)=>{}, onerror:()=>{}}
  7398. function sendReviewReply(detail) {
  7399. if (typeof($URL) !== 'object') {
  7400. DoLog(LogLevel.Error, 'sendReviewReply: $URL not found.');
  7401. return false;
  7402. }
  7403. const data = '&ptitle=' + $URL.encode(detail.title) + '&pcontent=' + $URL.encode(detail.content);
  7404. const url = `https://${location.host}/modules/article/reviewshow.php?rid=` + detail.rid.toString();
  7405. GM_xmlhttpRequest({
  7406. method: 'POST',
  7407. url: url,
  7408. headers: {
  7409. 'Content-Type': 'application/x-www-form-urlencoded'
  7410. },
  7411. data: data,
  7412. responseType: 'blob',
  7413. onload: function (response) {
  7414. if (!detail.onload) {return false;}
  7415. parseDocument(response.response, detail.onload);
  7416. },
  7417. onerror: function (e) {
  7418. detail.onerror && detail.onerror(e);
  7419. }
  7420. });
  7421. }
  7422.  
  7423. // Android API set
  7424. function AndroidAPI() {
  7425. const AA = this;
  7426. const DParser = new DOMParser();
  7427.  
  7428. const encode = AA.encode = function(str) {
  7429. return '&appver=1.13&request=' + btoa(str) + '&timetoken=' + (new Date().getTime());
  7430. };
  7431.  
  7432. const request = AA.request = function(details) {
  7433. const url = details.url;
  7434. const type = details.type || 'text';
  7435. const callback = details.callback || function() {};
  7436. const args = details.args || [];
  7437. GM_xmlhttpRequest({
  7438. method: 'POST',
  7439. url: 'http://app.wenku8.com/android.php',
  7440. headers: {
  7441. 'Content-Type': 'application/x-www-form-urlencoded',
  7442. 'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 7.1.2; unknown Build/NZH54D)'
  7443. },
  7444. data: encode(url),
  7445. onload: function(e) {
  7446. let result;
  7447. switch (type) {
  7448. case 'xml':
  7449. result = DParser.parseFromString(e.responseText, 'text/xml');
  7450. break;
  7451. case 'text':
  7452. result = e.responseText;
  7453. break;
  7454. }
  7455. callback.apply(null, [result].concat(args));
  7456. },
  7457. onerror: function(e) {
  7458. DoLog(LogLevel.Error, 'AndroidAPI.request Error while requesting "' + url + '"');
  7459. DoLog(LogLevel.Error, e);
  7460. }
  7461. });
  7462. };
  7463.  
  7464. // aid, lang, callback, args
  7465. AA.getNovelShortInfo = function(details) {
  7466. const aid = details.aid;
  7467. const lang = details.lang;
  7468. const callback = details.callback || function() {};
  7469. const args = details.args || [];
  7470. const url = 'action=book&do=info&aid=' + aid + '&t=' + lang;
  7471. request({
  7472. url: url,
  7473. callback: callback,
  7474. args: args,
  7475. type: 'xml'
  7476. });
  7477. }
  7478.  
  7479. // aid, lang, callback, args
  7480. AA.getNovelIndex = function(details) {
  7481. const aid = details.aid;
  7482. const lang = details.lang;
  7483. const callback = details.callback || function() {};
  7484. const args = details.args || [];
  7485. const url = 'action=book&do=list&aid=' + aid + '&t=' + lang;
  7486. request({
  7487. url: url,
  7488. callback: callback,
  7489. args: args,
  7490. type: 'xml'
  7491. });
  7492. };
  7493.  
  7494. // aid, cid, lang, callback, args
  7495. AA.getNovelContent = function(details) {
  7496. const aid = details.aid;
  7497. const cid = details.cid;
  7498. const lang = details.lang;
  7499. const callback = details.callback || function() {};
  7500. const args = details.args || [];
  7501. const url = 'action=book&do=text&aid=' + aid + '&cid=' + cid + '&t=' + lang;
  7502. request({
  7503. url: url,
  7504. callback: callback,
  7505. args: args,
  7506. type: 'text'
  7507. });
  7508. };
  7509. }
  7510.  
  7511. // Create reply-area with enhanced UBBEditor
  7512. function makeEditor(parent, rid, aid) {
  7513. parent.innerHTML = `<form name="frmreview" method="post" action="https://${location.host}/modules/article/reviewshow.php?rid={RID}"><table class="grid" width="100%" align="center"><tbody><tr><td class="odd" width="25%">标题</td><td class="even"><input type="text" class="text" name="ptitle" id="ptitle" size="60" maxlength="60" value="" /></td></tr></tbody><caption>回复书评:</caption><tbody><tr><td class="odd" width="25%">内容(每帖+1积分)</td><td class="even"><textarea class="textarea" name="pcontent" id="pcontent" cols="60" rows="12"></textarea></td></tr><tr><td class="odd" width="25%">&nbsp;</td><td class="even"><input type="submit" name="Submit" class="button" value="发表书评(Ctrl+Enter)" style="padding: 0.3em 0.4em; height: auto;" /><span></span></td></tr></tbody></table></form>`.replace('{RID}', rid).replace('{AID}', aid);
  7514. const script = $CrE('script');
  7515. script.innerHTML = `loadJs("https://${location.host}/scripts/ubbeditor_gbk.js", function(){UBBEditor.Create("pcontent");});`;
  7516. $(parent, '#pcontent').parentElement.appendChild(script);
  7517. areaReply();
  7518. }
  7519.  
  7520. // getMyUserDetail with soft alerts
  7521. function refreshMyUserDetail(callback, args=[]) {
  7522. alertify.notify(TEXT_ALT_USRDTL_REFRESH);
  7523. getMyUserDetail(function() {
  7524. const alertBox = alertify.success(TEXT_ALT_USRDTL_REFRESHED);
  7525.  
  7526. // rewrite onclick function from copying to showing details
  7527. alertBox.callback = function(isClicked) {
  7528. isClicked && alertify.message(altMyUserDetail()/*JSON.stringify(getMyUserDetail())*/);
  7529. }
  7530.  
  7531. // callback if exist
  7532. callback ? callback.apply(args) : function() {};
  7533. })
  7534. }
  7535.  
  7536. // Get my user info detail
  7537. // if no argument provided, this function will just read userdetail from gm_storage
  7538. // otherwise, the function will make a http request to get the latest userdetail
  7539. // if no argument provided and no gm_storage record, then will just return false
  7540. // if not logged in, return false
  7541. // if callback is not a function, then will just request&store but not callback
  7542. function getMyUserDetail(callback, args=[]) {
  7543. if (getUserID() === null) {
  7544. return false;
  7545. }
  7546. if (callback) {
  7547. requestWeb();
  7548. return true;
  7549. } else {
  7550. const storage = CONFIG.userDtlePrefs.getConfig();
  7551. if (!storage.userDetail && !storage.userFriends) {
  7552. DoLog(LogLevel.Warning, 'Attempt to read userDetail from gm_storage but no record found');
  7553. return false;
  7554. };
  7555. const userDetail = storage;
  7556. return userDetail;
  7557. }
  7558.  
  7559. function requestWeb() {
  7560. const lastStorage = CONFIG ? CONFIG.userDtlePrefs.getConfig() : undefined;
  7561. let restXHR = 2;
  7562. let storage = {};
  7563.  
  7564. // Request userDetail
  7565. getDocument(URL_USRDETAIL, detailLoaded)
  7566.  
  7567. // Request userFriends
  7568. getDocument(URL_USRFRIEND, friendLoaded)
  7569.  
  7570. function detailLoaded(oDoc) {
  7571. const content = $(oDoc, '#content');
  7572. storage.userDetail = {
  7573. userID: Number($(content, 'tr:nth-child(1)>.even').innerText), // '用户ID'
  7574. userLink: $(content, 'tr:nth-child(2)>.even').innerText, // '推广链接'
  7575. userName: $(content, 'tr:nth-child(3)>.even').innerText, // '用户名'
  7576. displayName: $(content, 'tr:nth-child(4)>.even').innerText, // '用户昵称'
  7577. userType: $(content, 'tr:nth-child(5)>.even').innerText, // '等级'
  7578. userGrade: $(content, 'tr:nth-child(6)>.even').innerText, // '头衔'
  7579. gender: $(content, 'tr:nth-child(7)>.even').innerText, // '性别'
  7580. email: $(content, 'tr:nth-child(8)>.even').innerText, // 'Email'
  7581. qq: $(content, 'tr:nth-child(9)>.even').innerText, // 'QQ'
  7582. msn: $(content, 'tr:nth-child(10)>.even').innerText, // 'MSN'
  7583. site: $(content, 'tr:nth-child(11)>.even').innerText, // '网站'
  7584. signupDate: $(content, 'tr:nth-child(13)>.even').innerText, // '注册日期'
  7585. contibute: $(content, 'tr:nth-child(14)>.even').innerText, // '贡献值'
  7586. exp: $(content, 'tr:nth-child(15)>.even').innerText, // '经验值'
  7587. credit: $(content, 'tr:nth-child(16)>.even').innerText, // '现有积分'
  7588. friends: $(content, 'tr:nth-child(17)>.even').innerText, // '最多好友数'
  7589. mailbox: $(content, 'tr:nth-child(18)>.even').innerText, // '信箱最多消息数'
  7590. bookcase: $(content, 'tr:nth-child(19)>.even').innerText, // '书架最大收藏量'
  7591. vote: $(content, 'tr:nth-child(20)>.even').innerText, // '每天允许推荐次数'
  7592. sign: $(content, 'tr:nth-child(22)>.even').innerText, // '用户签名'
  7593. intoduction: $(content, 'tr:nth-child(23)>.even').innerText, // '个人简介'
  7594. userImage: $(content, 'tr>td>img').src // '头像'
  7595. }
  7596. loaded();
  7597. }
  7598.  
  7599. function friendLoaded(oDoc) {
  7600. const content = $(oDoc, '#content');
  7601. const trs = $All(content, 'tr');
  7602. const friends = [];
  7603. const lastFriends = lastStorage ? lastStorage.userFriends : undefined;
  7604.  
  7605. for (let i = 1; i < trs.length; i++) {
  7606. getFriends(trs[i]);
  7607. }
  7608. storage.userFriends = friends;
  7609. loaded();
  7610.  
  7611. function getFriends(tr) {
  7612. // Check if userID exist
  7613. if (isNaN(Number($(tr.children[2], 'a').href.match(/\?uid=(\d+)/)[1]))) {return false;};
  7614.  
  7615. // Collect information
  7616. let friend = {
  7617. userID: Number($(tr.children[2], 'a').href.match(/\?uid=(\d+)/)[1]),
  7618. userName: tr.children[0].innerText,
  7619. signupDate: tr.children[1].innerText
  7620. }
  7621. friend = fillLocalInfo(friend)
  7622. friends.push(friend);
  7623. }
  7624.  
  7625. function fillLocalInfo(friend) {
  7626. if (!lastFriends) {return friend;};
  7627. for (const f of lastFriends) {
  7628. if (f.userID === friend.userID) {
  7629. for (const [key, value] of Object.entries(f)) {
  7630. if (friend.hasOwnProperty(key)) {continue;};
  7631. friend[key] = value;
  7632. }
  7633. break;
  7634. }
  7635. }
  7636. return friend;
  7637. }
  7638. }
  7639.  
  7640. function loaded() {
  7641. restXHR--;
  7642. if (restXHR === 0) {
  7643. // Save to gm_storage
  7644. if (CONFIG) {
  7645. storage.lasttime = getTime('-', false);
  7646. CONFIG.userDtlePrefs.saveConfig(storage);
  7647. }
  7648.  
  7649. // Callback
  7650. typeof(callback) === 'function' ? callback.apply(null, [storage].concat(args)) : function() {};
  7651. }
  7652. }
  7653. }
  7654. }
  7655.  
  7656. // Show userdetail in an alertify alertbox
  7657. function altMyUserDetail() {
  7658. const json = getMyUserDetail();
  7659. alertify.message(JSON.stringify(getMyUserDetail()));
  7660. }
  7661.  
  7662. function exportConfig(noPass=false) {
  7663. // Get config
  7664. const config = {};
  7665. const getValue = window.getValue ? window.getValue : GM_getValue;
  7666. const listValues = window.listValues ? window.listValues : GM_listValues;
  7667. for (const key of listValues()) {
  7668. config[key] = getValue(key);
  7669. }
  7670.  
  7671. // Remove username and password if required
  7672. noPass && (config[KEY_CM].users = {});
  7673.  
  7674. // Download
  7675. const text = JSON.stringify(config);
  7676. const name = '轻小说文库+_配置文件({P})_v{V}_{T}.wkp'.replace('{P}', noPass ? '无账号密码' : '含账号密码').replace('{V}', GM_info.script.version).replace('{T}', getTime());
  7677. downloadText(text, name);
  7678. alertify.success(TEXT_ALT_CONFIG_EXPORTED.replace('{N}', name));
  7679. }
  7680.  
  7681. function importConfig(json) {
  7682. // Redirect
  7683. redirectGMStorage();
  7684.  
  7685. // Preserve users
  7686. const users = GM_getValue('Config-Manager').users;
  7687.  
  7688. // Delete json
  7689. for (const [key, value] of GM_listValues()) {
  7690. GM_deleteValue(key, value);
  7691. }
  7692.  
  7693. // Set json
  7694. for (const [key, value] of Object.entries(json)) {
  7695. GM_setValue(key, value);
  7696. }
  7697.  
  7698. // Preserve users
  7699. const config = GM_getValue('Config-Manager', {});
  7700. if (!config.users) {config.users = {}}
  7701. for (const [name, user] of Object.entries(users)) {
  7702. config.users[name] = user;
  7703. }
  7704. GM_setValue('Config-Manager', config);
  7705.  
  7706. // Reload
  7707. location.reload();
  7708. }
  7709.  
  7710. function sortLaterReads(books, sortby) {
  7711. const sorter = FUNC_LATERBOOK_SORTERS[sortby].sorter;
  7712. return Object.values(books).sort(sorter);
  7713. }
  7714.  
  7715. function getUserID() {
  7716. const match = $URL.decode(document.cookie).match(/jieqiUserId=(\d+)/);
  7717. const id = match && match[1] ? Number(match[1]) : null;
  7718. return isNaN(id) ? null : id;
  7719. }
  7720.  
  7721. function getUserName() {
  7722. const match = $URL.decode(document.cookie).match(/jieqiUserName=([^, ;]+)/);
  7723. const name = match ? match[1] : null;
  7724. return name;
  7725. }
  7726.  
  7727. // Reload page without re-sending form data, and keeps reviewshow-page
  7728. function reloadPage() {
  7729. const url = /^https?:\/\/www\.wenku8\.(net|cc)\/modules\/article\/reviewshow\.php/.test(location.href) ? URL_REVIEWSHOW_2.replace('{R}', getUrlArgv('rid')).replace('{P}', $('#pagelink>strong').innerText) : location.href;
  7730. location.href = url;
  7731. }
  7732.  
  7733. // Check if tipobj is ready, if not, then make it
  7734. function tipcheck() {
  7735. DoLog(LogLevel.Info, 'checking tipobj...');
  7736. if (typeof(tipobj) === 'object' && tipobj !== null) {
  7737. DoLog(LogLevel.Info, 'tipobj ready...');
  7738. return true;
  7739. } else {
  7740. DoLog(LogLevel.Warning, 'tipobj not ready');
  7741. if (typeof(tipinit) === 'function') {
  7742. DoLog(LogLevel.Success, 'tipinit executed');
  7743. tipinit();
  7744. return true;
  7745. } else {
  7746. DoLog(LogLevel.Error, 'tipinit not found');
  7747. return false;
  7748. }
  7749. }
  7750. }
  7751.  
  7752. // New tipobj movement method. Makes sure the tipobj stay close with the mouse.
  7753. function tipscroll() {
  7754. if (!tipready) {return false;}
  7755.  
  7756. DoLog('tipscroll executed. ')
  7757. tipobj.style.position = 'fixed';
  7758. window.addEventListener('mousemove', tipmoveplus)
  7759. return true;
  7760.  
  7761. function tipmoveplus(e) {
  7762. tipobj.style.left = e.clientX + tipx + 'px';
  7763. tipobj.style.top = e.clientY + tipy + 'px';
  7764. }
  7765. }
  7766.  
  7767. // show & hide tip when mouse in & out. accepts tip as a string or a function that returns the tip string
  7768. function settip(elm, tip) {
  7769. typeof(tip) === 'string' && (elm.tiptitle = tip);
  7770. typeof(tip) === 'function' && (elm.tipgetter = tip);
  7771. elm.removeEventListener('mouseover', showtip);
  7772. elm.removeEventListener('mouseout', hidetip);
  7773. elm.addEventListener('mouseover', showtip);
  7774. elm.addEventListener('mouseout', hidetip);
  7775. }
  7776.  
  7777. function showtip(e) {
  7778. if (e && e.currentTarget && (e.currentTarget.tiptitle || e.currentTarget.tipgetter)) {
  7779. const tip = e.currentTarget.tiptitle || e.currentTarget.tipgetter();
  7780. if (tipready) {
  7781. tipshow(tip);
  7782. e.currentTarget.title && e.currentTarget.removeAttribute('title');
  7783. } else {
  7784. e.currentTarget.title = e.currentTarget.tiptitle;
  7785. }
  7786. } else if (typeof(e) === 'string') {
  7787. tipready && tipshow(e);
  7788. }
  7789. }
  7790.  
  7791. function hidetip() {
  7792. tipready && tiphide();
  7793. }
  7794.  
  7795. // Side-located control panel
  7796. // Requirements: FontAwesome, tooltip.css(from https://github.com/felipefialho/css-components/blob/main/build/tooltip/tooltip.css)
  7797. // Use 'new' keyword
  7798. function SidePanel() {
  7799. // Public SP
  7800. const SP = this;
  7801. const elms = SP.elements = {};
  7802.  
  7803. // Private _SP
  7804. // keys start with '_' shouldn't be modified
  7805. const _SP = {
  7806. _id: {
  7807. css: 'sidepanel-style',
  7808. usercss: 'sidepanel-style-user',
  7809. panel: 'sidepanel-panel'
  7810. },
  7811. _class: {
  7812. button: 'sidepanel-button'
  7813. },
  7814. _css: '#sidepanel-panel {position: fixed; background-color: #00000000; padding: 0.5vmin; line-height: 3.5vmin; height: auto; display: flex; transition-duration: 0.3s; z-index: 9999999999;} #sidepanel-panel.right {right: 3vmin;} #sidepanel-panel.bottom {bottom: 3vmin; flex-direction: column-reverse;} #sidepanel-panel.left {left: 3vmin;} #sidepanel-panel.top {top: 3vmin; flex-direction: column;} .sidepanel-button {padding: 1vmin; margin: 0.5vmin; font-size: 3.5vmin; border-radius: 10%; text-align: center; color: #00000088; background-color: #FFFFFF88; box-shadow:3px 3px 2px #00000022; user-select: none; transition-duration: inherit;} .sidepanel-button:hover {color: #FFFFFFDD; background-color: #000000DD;}',
  7815. _directions: ['left', 'right', 'top', 'bottom']
  7816. };
  7817.  
  7818. Object.defineProperty(SP, 'css', {
  7819. configurable: false,
  7820. enumerable: true,
  7821. get: () => (_SP.css),
  7822. set: (css) => {
  7823. _SP.css = css;
  7824. spAddStyle(css, _SP._id.css);
  7825. }
  7826. });
  7827. Object.defineProperty(SP, 'usercss', {
  7828. configurable: false,
  7829. enumerable: true,
  7830. get: () => (_SP.usercss),
  7831. set: (usercss) => {
  7832. _SP.usercss = usercss;
  7833. spAddStyle(usercss, _SP._id.usercss);
  7834. }
  7835. });
  7836. SP.css = _SP._css;
  7837.  
  7838. SP.create = function() {
  7839. // Create panel
  7840. const panel = elms.panel = document.createElement('div');
  7841. panel.id = _SP._id.panel;
  7842. SP.setPosition('bottom-right');
  7843. document.body.appendChild(panel);
  7844.  
  7845. // Prepare buttons
  7846. elms.buttons = [];
  7847. }
  7848.  
  7849. // Insert a button to given index
  7850. // details = {index, text, faicon, id, tip, className, onclick, listeners}, all optional
  7851. // listeners = [..[..args]]. [..args] will be applied as button.addEventListener's args
  7852. // faicon = 'fa-icon-name-classname fa-icon-style-classname', this arg stands for a FontAwesome icon to be inserted inside the botton
  7853. // Returns the button(HTMLDivElement), including button.faicon(HTMLElement/HTMLSpanElement in firefox, <i>) if faicon is set
  7854. SP.insert = function(details) {
  7855. const index = details.index;
  7856. const text = details.text;
  7857. const faicon = details.faicon;
  7858. const id = details.id;
  7859. const tip = details.tip;
  7860. const className = details.className;
  7861. const onclick = details.onclick;
  7862. const listeners = details.listeners || [];
  7863.  
  7864. const button = document.createElement('div');
  7865. text && (button.innerHTML = text);
  7866. id && (button.id = id);
  7867. tip && setTooltip(button, tip); //settip(button, tip);
  7868. className && (button.className = className);
  7869. onclick && (button.onclick = onclick);
  7870. if (faicon) {
  7871. const i = document.createElement('i');
  7872. i.className = faicon;
  7873. button.faicon = i;
  7874. button.appendChild(i);
  7875. }
  7876. for (const listener of listeners) {
  7877. button.addEventListener.apply(button, listener);
  7878. }
  7879. button.classList.add(_SP._class.button);
  7880.  
  7881. elms.buttons = insertItem(elms.buttons, button, index);
  7882. index < elms.buttons.length ? elms.panel.insertBefore(button, elms.panel.children[index]) : elms.panel.appendChild(button);
  7883. return button;
  7884. }
  7885.  
  7886. // Append a button
  7887. SP.add = function(details) {
  7888. details.index = elms.buttons.length;
  7889. return SP.insert(details);
  7890. }
  7891.  
  7892. // Remove a button
  7893. SP.remove = function(arg) {
  7894. let index, elm;
  7895. if (arg instanceof HTMLElement) {
  7896. elm = arg;
  7897. index = elms.buttons.indexOf(elm);
  7898. } else if (typeof(arg) === 'number') {
  7899. index = arg;
  7900. elm = elms.buttons[index];
  7901. } else if (typeof(arg) === 'string') {
  7902. elm = $(elms.panel, arg);
  7903. index = elms.buttons.indexOf(elm);
  7904. }
  7905.  
  7906. elms.buttons = delItem(elms.buttons, index);
  7907. elm.parentElement.removeChild(elm);
  7908. }
  7909.  
  7910. // Sets the display position by texts like 'right-bottom'
  7911. SP.setPosition = function(pos) {
  7912. const poses = _SP.direction = pos.split('-');
  7913. const avails = _SP._directions;
  7914.  
  7915. // Available check
  7916. if (poses.length !== 2) {return false;}
  7917. for (const p of poses) {
  7918. if (!avails.includes(p)) {return false;}
  7919. }
  7920.  
  7921. // remove all others
  7922. for (const p of avails) {
  7923. elms.panel.classList.remove(p);
  7924. }
  7925.  
  7926. // add new pos
  7927. for (const p of poses) {
  7928. elms.panel.classList.add(p);
  7929. }
  7930.  
  7931. // Change tooltips' direction
  7932. elms.buttons && elms.buttons.forEach(function(button) {
  7933. if (button.getAttribute('role') === 'tooltip') {
  7934. setTooltipDirection(button)
  7935. }
  7936. });
  7937. }
  7938.  
  7939. // Gets the current display position
  7940. SP.getPosition = function() {
  7941. return _SP.direction.join('-');
  7942. }
  7943.  
  7944. // Append a style text to document(<head>) with a <style> element
  7945. // Replaces existing id-specificed <style>s
  7946. function spAddStyle(css, id) {
  7947. const style = document.createElement("style");
  7948. id && (style.id = id);
  7949. style.textContent = css;
  7950. for (const elm of $All('#'+id)) {
  7951. elm.parentElement && elm.parentElement.removeChild(elm);
  7952. }
  7953. document.head.appendChild(style);
  7954. }
  7955.  
  7956. // Set a tooltip to the element
  7957. function setTooltip(elm, text, direction='auto') {
  7958. elm.tooltip = tippy(elm, {
  7959. content: text,
  7960. arrow: true,
  7961. hideOnClick: false
  7962. });
  7963.  
  7964. // Old version, uses tooltip.css
  7965. /*
  7966. elm.setAttribute('role', 'tooltip');
  7967. elm.setAttribute('aria-label', text);
  7968. */
  7969.  
  7970. setTooltipDirection(elm, direction);
  7971. }
  7972.  
  7973. function setTooltipDirection(elm, direction='auto') {
  7974. direction === 'auto' && (direction = _SP.direction.includes('left') ? 'right' : 'left');
  7975. if (!_SP._directions.includes(direction)) {throw new Error('setTooltip: invalid direction');}
  7976.  
  7977. // Tippy direction
  7978. if (!elm.tooltip) {
  7979. DoLog(LogLevel.Error, 'SidePanel.setTooltipDirection: Given elm has no tippy instance(elm.tooltip)');
  7980. throw new Error('SidePanel.setTooltipDirection: Given elm has no tippy instance(elm.tooltip)');
  7981. }
  7982. elm.tooltip.setProps({
  7983. placement: direction
  7984. });
  7985.  
  7986. // Old version, uses tooltip.css
  7987. /*
  7988. for (const dirct of _SP._directions) {
  7989. elm.classList.remove('tooltip-'+dirct);
  7990. }
  7991. elm.classList.add('tooltip-'+direction);
  7992. */
  7993. }
  7994.  
  7995. // Del an item from an array using its index. Returns the array but can NOT modify the original array directly!!
  7996. function delItem(arr, index) {
  7997. arr = arr.slice(0, index).concat(arr.slice(index+1));
  7998. return arr;
  7999. }
  8000.  
  8001. // Insert an item into an array using given index. Returns the array but can NOT modify the original array directly!!
  8002. function insertItem(arr, item, index) {
  8003. arr = arr.slice(0, index).concat(item).concat(arr.slice(index));
  8004. return arr;
  8005. }
  8006. }
  8007.  
  8008. // Create a list gui like reviewshow.php##FontSizeTable
  8009. // list = {display: '', id: '', parentElement: <*>, insertBefore: <*>, list: [{value: '', onclick: Function, tip: ''/Function}, ...], visible: bool, onshow: Function(bool shown), onhide: Function(bool hidden)}
  8010. // structure: {div: <div>, ul: <ul>, list: [{li: <li>, button: <input>}, ...], visible: list.visible, show: Function, hide: Function, append: Function({...}), remove: Function(index), clear: Function, onshow: list.onshow, onhide: list.onhide}
  8011. // Use 'new' keyword
  8012. function PlusList(list) {
  8013. const PL = this;
  8014.  
  8015. // Make list
  8016. const div = PL.div = document.createElement('div');
  8017. const ul = PL.ul = document.createElement('ul');
  8018. div.classList.add(CLASSNAME_LIST);
  8019. div.appendChild(ul);
  8020. list.display && (div.style.display = list.display);
  8021. list.id && (div.id = list.id);
  8022. list.parentElement && list.parentElement.insertBefore(div, list.insertBefore ? list.insertBefore : null);
  8023.  
  8024. PL.list = [];
  8025. for (const item of list.list) {
  8026. appendItem(item);
  8027. }
  8028.  
  8029. // Attach properties
  8030. let onshow = list.onshow ? list.onshow : function() {};
  8031. let onhide = list.onhide ? list.onhide : function() {};
  8032. let visible = list.visible;
  8033. PL.create = createItem;
  8034. PL.append = appendItem;
  8035. PL.insert = insertItem;
  8036. PL.remove = removeItem;
  8037. PL.clear = removeAll;
  8038. PL.show = showList;
  8039. PL.hide = hideList;
  8040. Object.defineProperty(PL, 'onshow', {
  8041. get: function() {return onshow;},
  8042. set: function(func) {
  8043. onshow = func ? func : function() {};
  8044. },
  8045. configurable: false,
  8046. enumerable: true
  8047. });
  8048. Object.defineProperty(PL, 'onhide', {
  8049. get: function() {return onhide;},
  8050. set: function(func) {
  8051. onhide = func ? func : function() {};
  8052. },
  8053. configurable: false,
  8054. enumerable: true
  8055. });
  8056. Object.defineProperty(PL, 'visible', {
  8057. get: function() {return visible;},
  8058. set: function(bool) {
  8059. if (typeof(bool) !== 'boolean') {return false;};
  8060. visible = bool;
  8061. bool ? showList() : hideList();
  8062. },
  8063. configurable: false,
  8064. enumerable: true
  8065. });
  8066. Object.defineProperty(PL, 'maxheight', {
  8067. get: function() {return maxheight;},
  8068. set: function(num) {
  8069. if (typeof(num) !== 'number') {return false;};
  8070. maxheight = num;
  8071. },
  8072. configurable: false,
  8073. enumerable: true
  8074. });
  8075.  
  8076. // Apply configurations
  8077. div.style.display = list.visible === true ? '' : 'none';
  8078.  
  8079. // Functions
  8080. function appendItem(item) {
  8081. const listitem = createItem(item);
  8082. ul.appendChild(listitem.li);
  8083. PL.list.push(listitem);
  8084. return listitem;
  8085. }
  8086.  
  8087. function insertItem(item, index, insertByNode=false) {
  8088. const listitem = createItem(item);
  8089. const children = insertByNode ? ul.childNodes : ul.children;
  8090. const elmafter = children[index];
  8091. ul.insertBefore(item.li, elmafter);
  8092. inserttoarr(PL.list, listitem, index);
  8093. }
  8094.  
  8095. function createItem(item) {
  8096. const listitem = {
  8097. remove: () => {removeItem(listitem);},
  8098. li: document.createElement('li'),
  8099. button: document.createElement('input')
  8100. };
  8101. const li = listitem.li;
  8102. const btn = listitem.button;
  8103. btn.type = 'button';
  8104. btn.classList.add(CLASSNAME_LIST_BUTTON);
  8105. li.classList.add(CLASSNAME_LIST_ITEM);
  8106. item.value && (btn.value = item.value);
  8107. item.onclick && btn.addEventListener('click', item.onclick);
  8108. item.tip && settip(li, item.tip);
  8109. item.tip && settip(btn, item.tip);
  8110. li.appendChild(btn);
  8111. return listitem;
  8112. }
  8113.  
  8114. function removeItem(itemorindex) {
  8115. // Get index
  8116. let index;
  8117. if (typeof(itemorindex) === 'number') {
  8118. index = itemorindex;
  8119. } else if (typeof(itemorindex) === 'object') {
  8120. index = PL.list.indexOf(itemorindex);
  8121. } else {
  8122. return false;
  8123. }
  8124. if (index < 0 || index >= PL.list.length) {
  8125. return false;
  8126. }
  8127.  
  8128. // Remove
  8129. const li = PL.list[index];
  8130. ul.removeChild(li.li);
  8131. delfromarr(PL.list, index);
  8132. return li;
  8133. }
  8134.  
  8135. function removeAll() {
  8136. const length = PL.list.length;
  8137. for (let i = 0; i < length; i++) {
  8138. removeItem(0);
  8139. }
  8140. }
  8141.  
  8142. function showList() {
  8143. if (visible) {return false;};
  8144. onshow(false);
  8145. div.style.display = '';
  8146. onshow(true);
  8147. visible = true;
  8148. }
  8149.  
  8150. function hideList() {
  8151. if (!visible) {return false;};
  8152. onhide(false);
  8153. div.style.display = 'none';
  8154. hidetip();
  8155. onhide(true);
  8156. visible = false;
  8157. }
  8158.  
  8159. // Support functions
  8160. // Del an item from an array by provided index, returns the deleted item. MODIFIES the original array directly!!
  8161. function delfromarr(arr, delIndex) {
  8162. if (delIndex < 0 || delIndex > arr.length-1) {
  8163. return false;
  8164. }
  8165. const deleted = arr[delIndex];
  8166. for (let i = delIndex; i < arr.length-1; i++) {
  8167. arr[i] = arr[i+1];
  8168. }
  8169. arr.pop();
  8170. return deleted;
  8171. }
  8172.  
  8173. // Insert an item to an array by its provided index, returns the item itself. MODIFIES the original array directly!!
  8174. function inserttoarr(arr, item, index) {
  8175. if (index < 0 || index > arr.length-1) {
  8176. return false;
  8177. }
  8178. for (let i = arr.length; i > index; i--) {
  8179. arr[i] = arr[i-1];
  8180. }
  8181. arr[index] = item;
  8182. return item;
  8183. }
  8184. }
  8185.  
  8186. // A table-based setting panel using alertify-js
  8187. // Requires: alertify-js
  8188. // Use 'new' keyword
  8189. // Usage:
  8190. /*
  8191. var panel = new SettingPanel({
  8192. className: '',
  8193. id: '',
  8194. name: '',
  8195. tables: [
  8196. {
  8197. className: '',
  8198. id: '',
  8199. name: '',
  8200. rows: [
  8201. {
  8202. className: '',
  8203. id: '',
  8204. name: '',
  8205. blocks: [
  8206. {
  8207. innerHTML / innerText: ''
  8208. colSpan: 1,
  8209. rowSpan: 1,
  8210. className: '',
  8211. id: '',
  8212. name: '',
  8213. children: [HTMLElement, ...]
  8214. },
  8215. ...
  8216. ]
  8217. },
  8218. ...
  8219. ]
  8220. },
  8221. ...
  8222. ]
  8223. });
  8224. */
  8225. function SettingPanel(details={}) {
  8226. const SP = this;
  8227. SP.insertTable = insertTable;
  8228. SP.appendTable = appendTable;
  8229. SP.removeTable = removeTable;
  8230. SP.remove = remove;
  8231. SP.PanelTable = PanelTable;
  8232. SP.PanelRow = PanelRow;
  8233. SP.PanelBlock = PanelBlock;
  8234.  
  8235. // <div> element
  8236. const elm = $C('div');
  8237. copyProps(details, elm, ['id', 'name', 'className']);
  8238. elm.classList.add('settingpanel-container');
  8239.  
  8240. // Configure object
  8241. let css='', usercss='';
  8242. SP.element = elm;
  8243. SP.elements = {};
  8244. SP.children = {};
  8245. SP.tables = [];
  8246. SP.length = 0;
  8247. details.id !== undefined && (SP.elements[details.id] = elm);
  8248. copyProps(details, SP, ['id', 'name']);
  8249. Object.defineProperty(SP, 'css', {
  8250. configurable: false,
  8251. enumerable: true,
  8252. get: function() {
  8253. return css;
  8254. },
  8255. set: function(_css) {
  8256. addStyle(_css, 'settingpanel-css');
  8257. css = _css;
  8258. }
  8259. });
  8260. Object.defineProperty(SP, 'usercss', {
  8261. configurable: false,
  8262. enumerable: true,
  8263. get: function() {
  8264. return usercss;
  8265. },
  8266. set: function(_usercss) {
  8267. addStyle(_usercss, 'settingpanel-usercss');
  8268. usercss = _usercss;
  8269. }
  8270. });
  8271. SP.css = '.settingpanel-table {border-spacing: 0px; border-collapse: collapse; width: 100%; margin: 2em 0;} .settingpanel-block {border: 1px solid; text-align: center; vertical-align: middle; padding: 3px; text-align: left;}'
  8272.  
  8273. // Create tables
  8274. if (details.tables) {
  8275. for (const table of details.tables) {
  8276. if (table instanceof PanelTable) {
  8277. appendTable(table);
  8278. } else {
  8279. appendTable(new PanelTable(table));
  8280. }
  8281. }
  8282. }
  8283.  
  8284. // Make alerity box
  8285. const box = SP.alertifyBox = alertify.alert();
  8286. clearChildNodes(box.elements.content);
  8287. box.elements.content.appendChild(elm);
  8288. box.elements.content.style.overflow = 'auto';
  8289. box.setHeader(TEXT_GUI_DETAIL_MANAGE_HEADER);
  8290. box.setting({
  8291. maximizable: true,
  8292. overflow: true
  8293. });
  8294. box.show();
  8295.  
  8296. // Insert a Panel-Row
  8297. // Returns Panel object
  8298. function insertTable(table, index) {
  8299. // Insert table
  8300. !(table instanceof PanelTable) && (table = new PanelTable(table));
  8301. index < SP.length ? elm.insertBefore(table.element, elm.children[index]) : elm.appendChild(table.element);
  8302. insertItem(SP.tables, table, index);
  8303. table.id !== undefined && (SP.children[table.id] = table);
  8304. SP.length++;
  8305.  
  8306. // Set parent
  8307. table.parent = SP;
  8308.  
  8309. // Inherit elements
  8310. for (const [id, subelm] of Object.entries(table.elements)) {
  8311. SP.elements[id] = subelm;
  8312. }
  8313.  
  8314. // Inherit children
  8315. for (const [id, child] of Object.entries(table.children)) {
  8316. SP.children[id] = child;
  8317. }
  8318. return SP;
  8319. }
  8320.  
  8321. // Append a Panel-Row
  8322. // Returns Panel object
  8323. function appendTable(table) {
  8324. return insertTable(table, SP.length);
  8325. }
  8326.  
  8327. // Remove a Panel-Row
  8328. // Returns Panel object
  8329. function removeTable(index) {
  8330. const table = SP.tables[index];
  8331. SP.element.removeChild(table.element);
  8332. removeItem(SP.rows, index);
  8333. return SP;
  8334. }
  8335.  
  8336. // Remove itself from parentElement
  8337. // Returns Panel object
  8338. function remove() {
  8339. SP.element.parentElement && SP.parentElement.removeChild(SP.element);
  8340. return SP;
  8341. }
  8342.  
  8343. // Panel-Table object
  8344. // Use 'new' keyword
  8345. function PanelTable(details={}) {
  8346. const PT = this;
  8347. PT.insertRow = insertRow;
  8348. PT.appendRow = appendRow;
  8349. PT.removeRow = removeRow;
  8350. PT.remove = remove
  8351.  
  8352. // <table> element
  8353. const elm = $C('table');
  8354. copyProps(details, elm, ['id', 'name', 'className']);
  8355. elm.classList.add('settingpanel-table');
  8356.  
  8357. // Configure
  8358. PT.element = elm;
  8359. PT.elements = {};
  8360. PT.children = {};
  8361. PT.rows = [];
  8362. PT.length = 0;
  8363. details.id !== undefined && (PT.elements[details.id] = elm);
  8364. copyProps(details, PT, ['id', 'name']);
  8365.  
  8366. // Append rows
  8367. if (details.rows) {
  8368. for (const row of details.rows) {
  8369. if (row instanceof PanelRow) {
  8370. insertRow(row);
  8371. } else {
  8372. insertRow(new PanelRow(row));
  8373. }
  8374. }
  8375. }
  8376.  
  8377. // Insert a Panel-Row
  8378. // Returns Panel-Table object
  8379. function insertRow(row, index) {
  8380. // Insert row
  8381. !(row instanceof PanelRow) && (row = new PanelRow(row));
  8382. index < PT.length ? elm.insertBefore(row.element, elm.children[index]) : elm.appendChild(row.element);
  8383. insertItem(PT.rows, row, index);
  8384. row.id !== undefined && (PT.children[row.id] = row);
  8385. PT.length++;
  8386.  
  8387. // Set parent
  8388. row.parent = PT;
  8389.  
  8390. // Inherit elements
  8391. for (const [id, subelm] of Object.entries(row.elements)) {
  8392. PT.elements[id] = subelm;
  8393. }
  8394.  
  8395. // Inherit children
  8396. for (const [id, child] of Object.entries(row.children)) {
  8397. PT.children[id] = child;
  8398. }
  8399. return PT;
  8400. }
  8401.  
  8402. // Append a Panel-Row
  8403. // Returns Panel-Table object
  8404. function appendRow(row) {
  8405. return insertRow(row, PT.length);
  8406. }
  8407.  
  8408. // Remove a Panel-Row
  8409. // Returns Panel-Table object
  8410. function removeRow(index) {
  8411. const row = PT.rows[index];
  8412. PT.element.removeChild(row.element);
  8413. removeItem(PT.rows, index);
  8414. return PT;
  8415. }
  8416.  
  8417. // Remove itself from parentElement
  8418. // Returns Panel-Table object
  8419. function remove() {
  8420. PT.parent instanceof SettingPanel && PT.parent.removeTable(PT.tables.indexOf(PT));
  8421. return PT;
  8422. }
  8423. }
  8424.  
  8425. // Panel-Row object
  8426. // Use 'new' keyword
  8427. function PanelRow(details={}) {
  8428. const PR = this;
  8429. PR.insertBlock = insertBlock;
  8430. PR.appendBlock = appendBlock;
  8431. PR.removeBlock = removeBlock;
  8432. PR.remove = remove;
  8433.  
  8434. // <tr> element
  8435. const elm = $C('tr');
  8436. copyProps(details, elm, ['id', 'name', 'className']);
  8437. elm.classList.add('settingpanel-row');
  8438.  
  8439. // Configure object
  8440. PR.element = elm;
  8441. PR.elements = {};
  8442. PR.children = {};
  8443. PR.blocks = [];
  8444. PR.length = 0;
  8445. details.id !== undefined && (PR.elements[details.id] = elm);
  8446. copyProps(details, PR, ['id', 'name']);
  8447.  
  8448. // Append blocks
  8449. if (details.blocks) {
  8450. for (const block of details.blocks) {
  8451. if (block instanceof PanelBlock) {
  8452. appendBlock(block);
  8453. } else {
  8454. appendBlock(new PanelBlock(block));
  8455. }
  8456. }
  8457. }
  8458.  
  8459. // Insert a Panel-Block
  8460. // Returns Panel-Row object
  8461. function insertBlock(block, index) {
  8462. // Insert block
  8463. !(block instanceof PanelBlock) && (block = new PanelBlock(block));
  8464. index < PR.length ? elm.insertBefore(block.element, elm.children[index]) : elm.appendChild(block.element);
  8465. insertItem(PR.blocks, block, index);
  8466. block.id !== undefined && (PR.children[block.id] = block);
  8467. PR.length++;
  8468.  
  8469. // Set parent
  8470. block.parent = PR;
  8471.  
  8472. // Inherit elements
  8473. for (const [id, subelm] of Object.entries(block.elements)) {
  8474. PR.elements[id] = subelm;
  8475. }
  8476.  
  8477. // Inherit children
  8478. for (const [id, child] of Object.entries(block.children)) {
  8479. PR.children[id] = child;
  8480. }
  8481. return PR;
  8482. };
  8483.  
  8484. // Append a Panel-Block
  8485. // Returns Panel-Row object
  8486. function appendBlock(block) {
  8487. return insertBlock(block, PR.length);
  8488. }
  8489.  
  8490. // Remove a Panel-Block
  8491. // Returns Panel-Row object
  8492. function removeBlock(index) {
  8493. const block = PR.blocks[index];
  8494. PR.element.removeChild(block.element);
  8495. removeItem(PR.blocks, index);
  8496. return PR;
  8497. }
  8498.  
  8499. // Remove itself from parent
  8500. // Returns Panel-Row object
  8501. function remove() {
  8502. PR.parent instanceof PanelTable && PR.parent.removeRow(PR.parent.rows.indexOf(PR));
  8503. return PR;
  8504. }
  8505. }
  8506.  
  8507. // Panel-Block object
  8508. // Use 'new' keyword
  8509. function PanelBlock(details={}) {
  8510. const PB = this;
  8511. PB.remove = remove;
  8512.  
  8513. // <td> element
  8514. const elm = $C('td');
  8515. copyProps(details, elm, ['innerText', 'innerHTML', 'colSpan', 'rowSpan', 'id', 'name', 'className']);
  8516. elm.classList.add('settingpanel-block');
  8517.  
  8518. // Configure object
  8519. PB.element = elm;
  8520. PB.elements = {};
  8521. PB.children = {};
  8522. details.id !== undefined && (PB.elements[details.id] = elm);
  8523. copyProps(details, PB, ['id', 'name']);
  8524.  
  8525. // Append to parent if need
  8526. details.parent instanceof PanelRow && (PB.parent = details.parent.appendBlock(PB));
  8527.  
  8528. // Append child elements if exist
  8529. if (details.children) {
  8530. for (const child of details.children) {
  8531. elm.appendChild(child);
  8532. }
  8533. }
  8534.  
  8535. // Remove itself from parent
  8536. // Returns Panel-Block object
  8537. function remove() {
  8538. PB.parent instanceof PanelRow && PB.parent.removeBlock(PB.parent.blocks.indexOf(PB));
  8539. return PB;
  8540. }
  8541. }
  8542.  
  8543. function $(e) {return document.querySelector(e);}
  8544. function $C(e) {return document.createElement(e);}
  8545. function $R(e) {return $(e) && $(e).parentElement.removeChild($(e));}
  8546. function clearChildNodes(elm) {for (const el of elm.childNodes) {elm.removeChild(el);}}
  8547. function copyProp(obj1, obj2, prop) {obj1[prop] !== undefined && (obj2[prop] = obj1[prop]);}
  8548. function copyProps(obj1, obj2, props) {props.forEach((prop) => (copyProp(obj1, obj2, prop)));}
  8549. function insertItem(arr, item, index) {
  8550. for (let i = arr.length; i > index ; i--) {
  8551. arr[i] = arr[i-1];
  8552. }
  8553. arr[index] = item;
  8554. return arr;
  8555. }
  8556. function removeItem(arr, index) {
  8557. for (let i = index; i < arr.length-1; i++) {
  8558. arr[i] = arr[i+1];
  8559. }
  8560. delete arr[arr.length-1];
  8561. return arr;
  8562. }
  8563. function addStyle(css, id) {
  8564. $R('#'+id);
  8565. const style = $C('style');
  8566. style.innerHTML = css;
  8567. style.id = id;
  8568. document.head.appendChild(style);
  8569. return style
  8570. }
  8571. }
  8572.  
  8573. // Create a left .block operatingArea
  8574. // options = {type: '', ...opts}
  8575. // Supported type: 'mypage', 'toplist'
  8576. function createWenkuBlock(details) {
  8577. // Args
  8578. //title=TEXT_GUI_BLOCK_TITLE_DEFULT, append=false, options
  8579. const title = details.title || TEXT_GUI_BLOCK_TITLE_DEFULT;
  8580. const parent = ({'string': $(details.parent), 'object': details.parent})[typeof details.parent];
  8581. const type = details.type ? details.type.toLowerCase() : null;
  8582. const items = details.items;
  8583. const options = details.options;
  8584.  
  8585. // Standard block
  8586. const stdBlock = makeStandardBlock();
  8587. const block = stdBlock.block;
  8588. const blocktitle = stdBlock.blocktitle;
  8589. const blockcontent = stdBlock.blockcontent;
  8590.  
  8591. blocktitle.innerHTML = title;
  8592. makeContent();
  8593. parent && parent.appendChild(block);
  8594.  
  8595. return block;
  8596.  
  8597. // Create a standard block structure
  8598. function makeStandardBlock() {
  8599. const block = $CrE('div'); block.classList.add('block');
  8600. const blocktitle = $CrE('div'); blocktitle.classList.add('blocktitle');
  8601. const blockcontent = $CrE('div'); blockcontent.classList.add('blockcontent');
  8602. block.appendChild(blocktitle); block.appendChild(blockcontent);
  8603. return {block: block, blocktitle: blocktitle, blockcontent: blockcontent};
  8604. }
  8605.  
  8606. function makeContent() {
  8607. switch (type) {
  8608. case 'mypage': typeMypage(); break;
  8609. case 'toplist': typeToplist(); break;
  8610. case 'imagelist': typeImglist(); break;
  8611. case 'element': typeElement(); break;
  8612. default: DoLog(LogLevel.Error, 'createWenkuBlock: Invalid block type');
  8613. }
  8614. }
  8615.  
  8616. // Links such as https://www.wenku8.net/userdetail.php
  8617. function typeMypage() {
  8618. const ul = $CrE('ul');
  8619. ul.classList.add('ulitem');
  8620. for (const link of details.items) {
  8621. const li = $CrE('li');
  8622. const a = $CrE('a');
  8623. a.href = link.href ? link.href : 'javascript: void(0);';
  8624. link.href && (a.target = '_blank');
  8625. link.tiptitle && settip(a, link.tiptitle);
  8626. a.innerHTML = link.innerHTML;
  8627. a.id = link.id ? link.id : '';
  8628. li.appendChild(a);
  8629. ul.appendChild(li);
  8630. }
  8631. blockcontent.appendChild(ul);
  8632. }
  8633.  
  8634. // Links such as top-books-list inside #right in index page
  8635. // links = [...{href: '', innerHTML: '', tiptitle: '', id: ''}]
  8636. function typeToplist() {
  8637. const ul = $CrE('ul');
  8638. ul.classList.add('ultop');
  8639. for (const link of details.items) {
  8640. const li = $CrE('li');
  8641. const a = $CrE('a');
  8642. a.href = link.href ? link.href : 'javascript: void(0);';
  8643. link.href && (a.target = '_blank');
  8644. link.tiptitle && settip(a, link.tiptitle);
  8645. a.innerHTML = link.innerHTML;
  8646. a.id = link.id ? link.id : '';
  8647. li.appendChild(a);
  8648. ul.appendChild(li);
  8649. }
  8650. blockcontent.appendChild(ul);
  8651. }
  8652.  
  8653. // Links with images like center blocks in index page
  8654. function typeImglist() {
  8655. const container = $CrE('div');
  8656. container.style.height = '155px';
  8657.  
  8658. for (const item of items) {
  8659. const div = $CrE('div');
  8660. div.setAttribute('style', 'float: left;text-align:center;width: 95px; height:155px;overflow:hidden;');
  8661.  
  8662. const a = $CrE('a');
  8663. a.href = item.href;
  8664. a.target = '_blank';
  8665. item.tiptitle && settip(a, item.tiptitle);
  8666.  
  8667. const img = $CrE('img');
  8668. img.src = item.src;
  8669. setAttributes(img, {
  8670. 'border': '0',
  8671. 'width': '90',
  8672. 'height': '127'
  8673. });
  8674. a.appendChild(img);
  8675.  
  8676. const br = $CrE('br');
  8677.  
  8678. const a2 = $CrE('a');
  8679. a2.href = item.href;
  8680. a2.target = '_blank';
  8681. a2.innerHTML = item.text;
  8682.  
  8683. div.appendChild(a);
  8684. div.appendChild(br);
  8685. div.appendChild(a2);
  8686. container.appendChild(div);
  8687. }
  8688.  
  8689. blockcontent.appendChild(container);
  8690. }
  8691.  
  8692. // Just append given elements into block content
  8693. function typeElement() {
  8694. const elms = Array.isArray(items) ? items : [items];
  8695. for (const elm of elms) {
  8696. blockcontent.appendChild(elm);
  8697. }
  8698. }
  8699.  
  8700. // Set attributes to an element
  8701. function setAttributes(elm, attributes) {
  8702. for (const [name, attr] of Object.entries(attributes)) {
  8703. elm.setAttribute(name, attr);
  8704. }
  8705. }
  8706. }
  8707.  
  8708. // Get a review's last page url
  8709. function getLatestReviewPageUrl(rid, callback, args=[]) {
  8710. const reviewUrl = `https://${location.host}/modules/article/reviewshow.php?rid=` + String(rid);
  8711. getDocument(reviewUrl, firstPage, args);
  8712.  
  8713. function firstPage(oDoc, ...args) {
  8714. const url = $(oDoc, '#pagelink>a.last').href;
  8715. args = [url].concat(args);
  8716. callback.apply(null, args);
  8717. };
  8718. };
  8719.  
  8720. // Upload image to KIENG images
  8721. // details: {file: File, onload: Function, onerror: Function, type: 'sm.ms/jd/sg/tt/...'}
  8722. function uploadImage(details) {
  8723. const file = details.file;
  8724. const onload = details.onload ? details.onload : function() {};
  8725. const onerror = details.onerror ? details.onerror : uploadError;
  8726. const type = details.type ? details.type : CONFIG.UserGlobalCfg.getConfig().imager;
  8727. if (!DATA_IMAGERS.hasOwnProperty(type) || !DATA_IMAGERS[type].available) {
  8728. onerror();
  8729. return false;
  8730. }
  8731. const imager = DATA_IMAGERS[type];
  8732. const upload = imager.upload;
  8733. const request = upload.request;
  8734. const response = upload.response;
  8735.  
  8736. // Construct request url
  8737. let url = request.url;
  8738. if (request.urlargs) {
  8739. const args = request.urlargs;
  8740. const makearg = (key, value) => ('{K}={V}'.replace('{K}', key).replace('{V}', value));
  8741. const replacers = {
  8742. '$filename$': () => (encodeURIComponent(file.name)),
  8743. '$random$': () => (Math.random().toString()),
  8744. '$time$': () => ((new Date()).getTime().toString())
  8745. };
  8746. for (let [key, value] of Object.entries(args)) {
  8747. url += url.includes('?') ? '&' : '?';
  8748. for (const [str, replacer] of Object.entries(replacers)) {
  8749. while (value !== null && value.includes(str)) {
  8750. const val = replacer(key);
  8751. value = (val !== null) ? value.replace(str, val) : null;
  8752. }
  8753. }
  8754. (value !== null) && (url += makearg(key, value));
  8755. }
  8756. }
  8757.  
  8758. // Construst request body
  8759. let data;
  8760. if (request.data) {
  8761. data = new FormData();
  8762. const replacers = {
  8763. '$file$': (key) => ((data.append(key, file), null)),
  8764. '$random$': () => (Math.random().toString()),
  8765. '$time$': () => ((new Date()).getTime().toString())
  8766. };
  8767.  
  8768. for (let [key, value] of Object.entries(request.data)) {
  8769. for (const [str, replacer] of Object.entries(replacers)) {
  8770. while (value !== null && value.includes(str)) {
  8771. const val = replacer(key);
  8772. value = (val !== null) ? value.replace(str, val) : null;
  8773. }
  8774. }
  8775. (value !== null) && data.append(key, value);
  8776. }
  8777. } else {
  8778. data = file;
  8779. }
  8780.  
  8781. // headers
  8782. const headers = request.headers || {};
  8783.  
  8784. GM_xmlhttpRequest({
  8785. method: 'POST',
  8786. url: url,
  8787. timeout: 15 * 1000,
  8788. data: data,
  8789. headers: headers,
  8790. responseType: request.responseType ? request.responseType : 'json',
  8791. onerror: onerror,
  8792. ontimeout: onerror,
  8793. onabort: onerror,
  8794. onload: (e) => {
  8795. const json = e.response;
  8796. const success = e.status === 200 && response.checksuccess(json);
  8797. if (success) {
  8798. const url = response.geturl(json);
  8799. const name = response.getname ? (response.getname(json) ? response.getname(json) : TEXT_ALT_IMAGE_RESPONSE_NONAME) : TEXT_ALT_IMAGE_RESPONSE_NONAME
  8800. onload({
  8801. url: url,
  8802. name: name,
  8803. });
  8804. } else {
  8805. onerror(json);
  8806. return;
  8807. }
  8808. }
  8809. })
  8810. /* Common xhr version. Cannot bypass CORS.
  8811. const re = new XMLHttpRequest();
  8812. re.open('POST', request.url, true);
  8813. re.timeout = 15 * 1000;
  8814. re.onerror = re.ontimeout = re.onabort = uploadError;
  8815. re.responseType = request.responseType ? request.responseType : 'json';
  8816. re.onload = (e) => {
  8817. const json = re.response;
  8818. const success = response.checksuccess(json)
  8819. if (success) {
  8820. onload({
  8821. url: response.geturl(json),
  8822. name: response.getname ? response.getname(json) : TEXT_ALT_IMAGE_RESPONSE_NONAME,
  8823. });
  8824. } else {
  8825. uploadError(json);
  8826. return;
  8827. }
  8828. }
  8829. re.send(data);*/
  8830.  
  8831. function uploadError(json) {
  8832. alertify.error(TEXT_ALT_IMAGE_UPLOAD_ERROR);
  8833. DoLog(LogLevel.Error, [TEXT_ALT_IMAGE_UPLOAD_ERROR, json]);
  8834. }
  8835. }
  8836.  
  8837. // Wait until a variable loaded, and call callback
  8838. function waitUntilLoaded(varnames, callback, args=[]) {
  8839. if (!varnames) {callback.apply(null, args)}
  8840. if (!Array.isArray(varnames)) {varnames = [varnames];}
  8841.  
  8842. const AM = new AsyncManager();
  8843. AM.onfinish = function() {
  8844. callback.apply(null, args);
  8845. };
  8846. for (const varname of varnames) {
  8847. AM.add();
  8848. makeWaitFunc(varname, AM)();
  8849. }
  8850. AM.finishEvent = true;
  8851.  
  8852. function makeWaitFunc(varname, AM) {
  8853. return function wait() {
  8854. if (typeof(getvar(varname)) === 'undefined') {
  8855. setTimeout(wait, NUMBER_ELEMENT_LOADING_WAIT_INTERVAL);
  8856. return false;
  8857. }
  8858. AM.finish();
  8859. };
  8860. }
  8861. }
  8862.  
  8863. // Remove all childnodes from an element
  8864. function clearChildnodes(element) {
  8865. const cns = []
  8866. for (const cn of element.childNodes) {
  8867. cns.push(cn);
  8868. }
  8869. for (const cn of cns) {
  8870. element.removeChild(cn);
  8871. }
  8872. }
  8873.  
  8874. // Change location.href without reloading using history.pushState/replaceState
  8875. function setPageUrl(url, push=false) {
  8876. return history[push ? 'pushState' : 'replaceState']({modified: true, ...history.state}, '', url);
  8877. }
  8878.  
  8879. // Just stopPropagation and preventDefault
  8880. function destroyEvent(e) {
  8881. if (!e) {return false;};
  8882. if (!e instanceof Event) {return false;};
  8883. e.stopPropagation();
  8884. e.preventDefault();
  8885. }
  8886.  
  8887. // eval() function with security check that only allows to get variable values, but don't allow executing js.
  8888. function getvar(varname) {
  8889. const unsafe_chars = ['(', ')', '+', '-', '*', '/', '&', '|', '[', ']', '=', '^', '%', '!', '.', '<', '>', '\\', '"', '\''];
  8890. for (const char of unsafe_chars) {
  8891. if (varname.includes(char)) {throw new Error('Function getvar(varname) called with insecure string "{V}"'.replaceAll('V', varname.replaceAll('"', '\\"')))}
  8892. }
  8893.  
  8894. return eval(varname);
  8895. }
  8896.  
  8897. // GM_XHR HOOK: The number of running GM_XHRs in a time must under maxXHR
  8898. // Returns the abort function to stop the request anyway(no matter it's still waiting, or requesting)
  8899. // (If the request is invalid, such as url === '', will return false and will NOT make this request)
  8900. // If the abort function called on a request that is not running(still waiting or finished), there will be NO onabort event
  8901. // Requires: function delItem(){...} & function uniqueIDMaker(){...}
  8902. function GMXHRHook(maxXHR=5) {
  8903. const GM_XHR = GM_xmlhttpRequest;
  8904. const getID = uniqueIDMaker();
  8905. let todoList = [], ongoingList = [];
  8906. GM_xmlhttpRequest = safeGMxhr;
  8907.  
  8908. function safeGMxhr() {
  8909. // Get an id for this request, arrange a request object for it.
  8910. const id = getID();
  8911. const request = {id: id, args: arguments, aborter: null};
  8912.  
  8913. // Deal onload function first
  8914. dealEndingEvents(request);
  8915.  
  8916. /* DO NOT DO THIS! KEEP ITS ORIGINAL PROPERTIES!
  8917. // Stop invalid requests
  8918. if (!validCheck(request)) {
  8919. return false;
  8920. }
  8921. */
  8922.  
  8923. // Judge if we could start the request now or later?
  8924. todoList.push(request);
  8925. checkXHR();
  8926. return makeAbortFunc(id);
  8927.  
  8928. // Decrease activeXHRCount while GM_XHR onload;
  8929. function dealEndingEvents(request) {
  8930. const e = request.args[0];
  8931.  
  8932. // onload event
  8933. const oriOnload = e.onload;
  8934. e.onload = function() {
  8935. reqFinish(request.id);
  8936. checkXHR();
  8937. oriOnload ? oriOnload.apply(null, arguments) : function() {};
  8938. }
  8939.  
  8940. // onerror event
  8941. const oriOnerror = e.onerror;
  8942. e.onerror = function() {
  8943. reqFinish(request.id);
  8944. checkXHR();
  8945. oriOnerror ? oriOnerror.apply(null, arguments) : function() {};
  8946. }
  8947.  
  8948. // ontimeout event
  8949. const oriOntimeout = e.ontimeout;
  8950. e.ontimeout = function() {
  8951. reqFinish(request.id);
  8952. checkXHR();
  8953. oriOntimeout ? oriOntimeout.apply(null, arguments) : function() {};
  8954. }
  8955.  
  8956. // onabort event
  8957. const oriOnabort = e.onabort;
  8958. e.onabort = function() {
  8959. reqFinish(request.id);
  8960. checkXHR();
  8961. oriOnabort ? oriOnabort.apply(null, arguments) : function() {};
  8962. }
  8963. }
  8964.  
  8965. // Check if the request is invalid
  8966. function validCheck(request) {
  8967. const e = request.args[0];
  8968.  
  8969. if (!e.url) {
  8970. return false;
  8971. }
  8972.  
  8973. return true;
  8974. }
  8975.  
  8976. // Call a XHR from todoList and push the request object to ongoingList if called
  8977. function checkXHR() {
  8978. if (ongoingList.length >= maxXHR) {return false;};
  8979. if (todoList.length === 0) {return false;};
  8980. const req = todoList.shift();
  8981. const reqArgs = req.args;
  8982. const aborter = GM_XHR.apply(null, reqArgs);
  8983. req.aborter = aborter;
  8984. ongoingList.push(req);
  8985. return req;
  8986. }
  8987.  
  8988. // Make a function that aborts a certain request
  8989. function makeAbortFunc(id) {
  8990. return function() {
  8991. let i;
  8992.  
  8993. // Check if the request haven't been called
  8994. for (i = 0; i < todoList.length; i++) {
  8995. const req = todoList[i];
  8996. if (req.id === id) {
  8997. // found this request: haven't been called
  8998. delItem(todoList, i);
  8999. return true;
  9000. }
  9001. }
  9002.  
  9003. // Check if the request is running now
  9004. for (i = 0; i < ongoingList.length; i++) {
  9005. const req = todoList[i];
  9006. if (req.id === id) {
  9007. // found this request: running now
  9008. req.aborter();
  9009. reqFinish(id);
  9010. checkXHR();
  9011. }
  9012. }
  9013.  
  9014. // Oh no, this request is already finished...
  9015. return false;
  9016. }
  9017. }
  9018.  
  9019. // Remove a certain request from ongoingList
  9020. function reqFinish(id) {
  9021. let i;
  9022. for (i = 0; i < ongoingList.length; i++) {
  9023. const req = ongoingList[i];
  9024. if (req.id === id) {
  9025. ongoingList = delItem(ongoingList, i);
  9026. return true;
  9027. }
  9028. }
  9029. return false;
  9030. }
  9031. }
  9032. }
  9033.  
  9034. // Redirect GM_storage API
  9035. // Each key points to a different storage area
  9036. // Original GM_functions will be backuped in window object
  9037. // PS: No worry for GM_functions leaking, because Tempermonkey's Sandboxing
  9038. function redirectGMStorage(key) {
  9039. // Recover if redirected before
  9040. GM_setValue = typeof(window.setValue) === 'function' ? window.setValue : GM_setValue;
  9041. GM_getValue = typeof(window.getValue) === 'function' ? window.getValue : GM_getValue;
  9042. GM_listValues = typeof(window.listValues) === 'function' ? window.listValues : GM_listValues;
  9043. GM_deleteValue = typeof(window.deleteValue) === 'function' ? window.deleteValue : GM_deleteValue;
  9044.  
  9045. // Stop if no key
  9046. if (!key) {return;};
  9047.  
  9048. // Save original GM_functions
  9049. window.setValue = typeof(GM_setValue) === 'function' ? GM_setValue : function() {};
  9050. window.getValue = typeof(GM_getValue) === 'function' ? GM_getValue : function() {};
  9051. window.listValues = typeof(GM_listValues) === 'function' ? GM_listValues : function() {};
  9052. window.deleteValue = typeof(GM_deleteValue) === 'function' ? GM_deleteValue : function() {};
  9053.  
  9054. // Redirect GM_functions
  9055. typeof(GM_setValue) === 'function' ? GM_setValue = RD_GM_setValue : function() {};
  9056. typeof(GM_getValue) === 'function' ? GM_getValue = RD_GM_getValue : function() {};
  9057. typeof(GM_listValues) === 'function' ? GM_listValues = RD_GM_listValues : function() {};
  9058. typeof(GM_deleteValue) === 'function' ? GM_deleteValue = RD_GM_deleteValue : function() {};
  9059.  
  9060. // Get global storage
  9061. //const storage = getStorage();
  9062.  
  9063. function getStorage() {
  9064. return window.getValue(key, {});
  9065. }
  9066.  
  9067. function saveStorage(storage) {
  9068. return window.setValue(key, storage);
  9069. }
  9070.  
  9071. function RD_GM_setValue(key, value) {
  9072. const storage = getStorage();
  9073. storage[key] = value;
  9074. saveStorage(storage);
  9075. }
  9076.  
  9077. function RD_GM_getValue(key, defaultValue) {
  9078. const storage = getStorage();
  9079. return storage[key] || defaultValue;
  9080. }
  9081.  
  9082. function RD_GM_listValues() {
  9083. const storage = getStorage();
  9084. return Object.keys(storage);
  9085. }
  9086.  
  9087. function RD_GM_deleteValue(key) {
  9088. const storage = getStorage();
  9089. delete storage[key];
  9090. saveStorage(storage);
  9091. }
  9092. }
  9093.  
  9094. // Aim to separate big data from config, to boost up the speed of config reading.
  9095. // FAILED. NEVER USE THESE CODES. NEVER DO THESE THINGS AGAIN. FUCK MYSELF ME STUPID.
  9096. // NOOOOOOOO!!!!!!! WHY ARE YOU DICKHEAD STILL THINGKING ABOUT THIS SHIT??????? NEVER EVER THINK ABOUT THIS FUCKING UNACHIEVABLE FUNCTION AGAIN!!!!!!
  9097. // See https://www.wenku8.net/modules/article/reviewshow.php?rid=244568&aid=1973&page=202#yid930393 if you still want to try, you'll pay for that.
  9098. function GMBigData(maxsize=1024) {
  9099. const BD = this;
  9100. BD.maxsize = maxsize;
  9101. BD.keyPrefix = 'GM_BIGDATA:' + btoa(encodeURIComponent(GM_info.script.name + (GM_info.script.namespace || '')));
  9102.  
  9103. BD.hook = function() {
  9104. hookget();
  9105. hookset();
  9106. }
  9107.  
  9108. BD.unhook = function() {
  9109. if (!BD.GM_getValue || !BD.GM_setValue) {
  9110. throw TypeError('GMBigData: BD.GM_getValue or BD.GM_setValue missing');
  9111. }
  9112. GM_getValue = BD.GM_getValue;
  9113. GM_setValue = BD.GM_setValue;
  9114. }
  9115.  
  9116. function hookget() {
  9117. const oGet = BD.GM_getValue = GM_getValue;
  9118. GM_getValue = function(name, defaultValue) {
  9119. return decodeValue(oGet(name, defaultValue));
  9120. }
  9121.  
  9122. function decodeValue(value) {
  9123. return (({
  9124. 'string': decodeString,
  9125. 'object': value !== null ? decodeObject : null
  9126. })[typeof value] || ((v) => (v)))(value);
  9127.  
  9128. function decodeString(str) {
  9129. return (isDatakey(str) && keyExists(str)) ? localStorage.getItem(str) : str;
  9130. }
  9131.  
  9132. function decodeObject(obj) {
  9133. return new Proxy(obj, {
  9134. get: function(target, property, receiver) {
  9135. return decodeValue(target[property]);
  9136. }
  9137. });
  9138. }
  9139. }
  9140. }
  9141.  
  9142. function hookset() {
  9143. const oSet = BD.GM_setValue = GM_setValue;
  9144. GM_setValue = function(name, value) {
  9145. const encoded = encodeValue(value);
  9146. clearUnusedBigData(encoded);
  9147. return oSet(name, encoded);
  9148. }
  9149.  
  9150. function encodeValue(value) {
  9151. return (({
  9152. 'string': encodeString,
  9153. 'object': value !== null ? encodeObject : value
  9154. })[typeof value] || ((v) => (v)))(value);
  9155.  
  9156. function encodeString(str) {
  9157. if (getDataSize(str) <= BD.maxsize) {
  9158. return str;
  9159. } else {
  9160. const key = generateKey();
  9161. localStorage.setItem(key, str);
  9162. return key;
  9163. }
  9164. }
  9165.  
  9166. function encodeObject(obj) {
  9167. return new Proxy(obj, {
  9168. get: function(target, property, receiver) {
  9169. return encodeValue(target[property]);
  9170. }
  9171. });
  9172. }
  9173. }
  9174.  
  9175. function clearUnusedBigData(data) {
  9176. const usingKeys = getAllUsingKeys(data);
  9177. for (const key of Object.keys(localStorage)) {
  9178. if (isDatakey(key) && !usingKeys.includes(key)) {
  9179. localStorage.removeItem(key);
  9180. }
  9181. }
  9182.  
  9183. function getAllUsingKeys(data) {
  9184. const usingKeys = [];
  9185. (({
  9186. 'string': checkString,
  9187. 'object': data !== null ? getAllUsingKeys : null
  9188. })[typeof data] || function() {})();
  9189. return usingKeys;
  9190.  
  9191. function checkString(str) {
  9192. isDatakey(str) && keyExists(str) && usingKeys.push(str);
  9193. }
  9194. }
  9195. }
  9196. }
  9197.  
  9198. // Datakey generator
  9199. function generateKey(length=16) {
  9200. let datakey = newKey();
  9201. while (keyExists(datakey)) {
  9202. datakey = newKey();
  9203. }
  9204. return datakey;
  9205.  
  9206. function newKey() {
  9207. return BD.keyPrefix + ',' + randstr(length);
  9208. }
  9209. }
  9210.  
  9211. // Check whether a datakey already exists
  9212. function keyExists(datakey) {
  9213. return Object.keys(localStorage).includes(datakey);
  9214. }
  9215.  
  9216. // Check whether the value is a datakey
  9217. function isDatakey(value) {
  9218. return typeof value === 'string' && value.startsWith(BD.keyPrefix);
  9219. }
  9220.  
  9221. // Get the size of data
  9222. function getDataSize(data) {
  9223. return (new Blob([data])).size;
  9224. }
  9225. }
  9226.  
  9227. // Download and parse a url page into a html document(dom).
  9228. // when xhr onload: callback.apply([dom, args])
  9229. function getDocument(url, callback, args=[]) {
  9230. GM_xmlhttpRequest({
  9231. method : 'GET',
  9232. url : url,
  9233. responseType : 'blob',
  9234. timeout : 15 * 1000,
  9235. onloadstart : function() {
  9236. DoLog(LogLevel.Info, 'getting document, url=\'' + url + '\'');
  9237. },
  9238. onload : function(response) {
  9239. const htmlblob = response.response;
  9240. parseDocument(htmlblob, callback, args);
  9241. },
  9242. onerror : reqerror,
  9243. ontimeout : reqerror
  9244. });
  9245.  
  9246. function reqerror(e) {
  9247. DoLog(LogLevel.Error, 'getDocument: Request Error');
  9248. DoLog(LogLevel.Error, e);
  9249. throw new Error('getDocument: Request Error')
  9250. }
  9251. }
  9252.  
  9253. function parseDocument(htmlblob, callback, args=[]) {
  9254. const reader = new FileReader();
  9255. reader.onload = function(e) {
  9256. const htmlText = reader.result;
  9257. const dom = new DOMParser().parseFromString(htmlText, 'text/html');
  9258. args = [dom].concat(args);
  9259. callback.apply(null, args);
  9260. //callback(dom, htmlText);
  9261. }
  9262. const charset = ['GBK', 'BIG5'][getLang()];
  9263. reader.readAsText(htmlblob, charset);
  9264. }
  9265.  
  9266. // Get a base64-formatted url of an image
  9267. // When image load error occurs, callback will be called without any argument
  9268. function getImageUrl(src, fitx, fity, callback, args=[]) {
  9269. const image = new Image();
  9270. image.setAttribute("crossOrigin",'anonymous');
  9271. image.onload = convert;
  9272. image.onerror = image.onabort = callback;
  9273. image.src = src;
  9274.  
  9275. function convert() {
  9276. const cvs = $CrE('canvas');
  9277. const ctx = cvs.getContext('2d');
  9278.  
  9279. let width, height;
  9280. if (fitx && fity) {
  9281. width = window.innerWidth;
  9282. height = window.innerHeight;
  9283. } else if (fitx) {
  9284. width = window.innerWidth;
  9285. height = (width / image.width) * image.height;
  9286. } else if (fity) {
  9287. height = window.innerHeight;
  9288. width = (height / image.height) * image.width;
  9289. } else {
  9290. width = image.width;
  9291. height = image.height;
  9292. }
  9293. cvs.width = width;
  9294. cvs.height = height;
  9295. ctx.drawImage(image, 0, 0, width, height);
  9296. try {
  9297. callback.apply(null, [cvs.toDataURL()].concat(args));
  9298. } catch (e) {
  9299. DoLog(LogLevel.Error, ['Error at getImageUrl.convert()', e]);
  9300. callback();
  9301. }
  9302. }
  9303. }
  9304.  
  9305. // Convert a 'data:image/jpeg;base64,57af8b....' to a Blob object
  9306. function b64toBlob(dataURI) {
  9307. const mime = dataURI.match(/data:(.+?);/)[1];
  9308. const byteString = atob(dataURI.split(',')[1]);
  9309. const ab = new ArrayBuffer(byteString.length);
  9310. const ia = new Uint8Array(ab);
  9311.  
  9312. for (let i = 0; i < byteString.length; i++) {
  9313. ia[i] = byteString.charCodeAt(i);
  9314. }
  9315. return new Blob([ab], {type: mime});
  9316. }
  9317.  
  9318. //将base64转换为文件
  9319. function dataURLtoFile(dataurl, filename) {
  9320. var arr = dataurl.split(','),
  9321. mime = arr[0].match(/:(.*?);/)[1],
  9322. bstr = atob(arr[1]),
  9323. n = bstr.length,
  9324. u8arr = new Uint8Array(n);
  9325. while (n--) {
  9326. u8arr[n] = bstr.charCodeAt(n);
  9327. }
  9328. return new File([u8arr], filename, {
  9329. type: mime
  9330. });
  9331. }
  9332.  
  9333. // Save dataURL to file
  9334. function saveFile(dataURL, filename) {
  9335. const a = $CrE('a');
  9336. a.href = dataURL;
  9337. a.download = filename;
  9338. a.click();
  9339. }
  9340.  
  9341. // File download function
  9342. // details looks like the detail of GM_xmlhttpRequest
  9343. // onload function will be called after file saved to disk
  9344. function downloadFile(details) {
  9345. if (!details.url || !details.name) {return false;};
  9346.  
  9347. // Configure request object
  9348. const requestObj = {
  9349. url: details.url,
  9350. responseType: 'blob',
  9351. onload: function(e) {
  9352. // Save file
  9353. const url = URL.createObjectURL(e.response);
  9354. saveFile(URL.createObjectURL(e.response), details.name);
  9355. URL.revokeObjectURL(url);
  9356.  
  9357. // onload callback
  9358. details.onload ? details.onload(e) : function() {};
  9359. }
  9360. }
  9361. if (details.onloadstart ) {requestObj.onloadstart = details.onloadstart;};
  9362. if (details.onprogress ) {requestObj.onprogress = details.onprogress;};
  9363. if (details.onerror ) {requestObj.onerror = details.onerror;};
  9364. if (details.onabort ) {requestObj.onabort = details.onabort;};
  9365. if (details.onreadystatechange) {requestObj.onreadystatechange = details.onreadystatechange;};
  9366. if (details.ontimeout ) {requestObj.ontimeout = details.ontimeout;};
  9367.  
  9368. // Send request
  9369. GM_xmlhttpRequest(requestObj);
  9370. }
  9371.  
  9372. // Save text to textfile
  9373. function downloadText(text, name) {
  9374. if (!text || !name) {return false;};
  9375.  
  9376. // Get blob url
  9377. const blob = new Blob([text],{type:"text/plain;charset=utf-8"});
  9378. const url = URL.createObjectURL(blob);
  9379.  
  9380. // Create <a> and download
  9381. const a = $CrE('a');
  9382. a.href = url;
  9383. a.download = name;
  9384. a.click();
  9385. }
  9386.  
  9387. function requestText(url, callback, args=[]) {
  9388. GM_xmlhttpRequest({
  9389. method: 'GET',
  9390. url: url,
  9391. responseType: 'text',
  9392. onload: function(response) {
  9393. const text = response.responseText;
  9394. const argvs = [text].concat(args);
  9395. callback.apply(null, argvs);
  9396. }
  9397. })
  9398. }
  9399.  
  9400. // Get a url argument from lacation.href
  9401. // also recieve a function to deal the matched string
  9402. // returns defaultValue if name not found
  9403. // Args: {url=location.href, name, dealFunc=((a)=>{return a;}), defaultValue=null} or 'name'
  9404. function getUrlArgv(details) {
  9405. typeof(details) === 'string' && (details = {name: details});
  9406. typeof(details) === 'undefined' && (details = {});
  9407. if (!details.name) {return null;};
  9408.  
  9409. const url = details.url ? details.url : location.href;
  9410. const name = details.name ? details.name : '';
  9411. const dealFunc = details.dealFunc ? details.dealFunc : ((a)=>{return a;});
  9412. const defaultValue = details.defaultValue ? details.defaultValue : null;
  9413. const matcher = new RegExp('[\\?&]' + name + '=([^&#]+)');
  9414. const result = url.match(matcher);
  9415. const argv = result ? dealFunc(result[1]) : defaultValue;
  9416.  
  9417. return argv;
  9418. }
  9419.  
  9420. // Get language: 0 for simplyfied chinese and others, 1 for traditional chinese
  9421. function getLang() {
  9422. const match = document.cookie.match(/(; *)?jieqiUserCharset=(.+?)( *;|$)/);
  9423. const nvgtLang = ({'zh-CN': 0, 'zh-TW': 1})[navigator.language] || 0;
  9424. return match && match[2] ? (match[2].toLowerCase() === 'big5' ? 1 : 0) : nvgtLang;
  9425. }
  9426.  
  9427. // Get a time text like 1970-01-01 00:00:00
  9428. // if dateSpliter provided false, there will be no date part. The same for timeSpliter.
  9429. function getTime(dateSpliter='-', timeSpliter=':') {
  9430. const d = new Date();
  9431. let fulltime = ''
  9432. fulltime += dateSpliter ? fillNumber(d.getFullYear(), 4) + dateSpliter + fillNumber((d.getMonth() + 1), 2) + dateSpliter + fillNumber(d.getDate(), 2) : '';
  9433. fulltime += dateSpliter && timeSpliter ? ' ' : '';
  9434. fulltime += timeSpliter ? fillNumber(d.getHours(), 2) + timeSpliter + fillNumber(d.getMinutes(), 2) + timeSpliter + fillNumber(d.getSeconds(), 2) : '';
  9435. return fulltime;
  9436. }
  9437.  
  9438. // Get key-value object from text like 'key: value'/'key:value'/' key : value '
  9439. // returns: {key: value, KEY: key, VALUE: value}
  9440. function getKeyValue(text, delimiters=[':', ':', ',', '︰']) {
  9441. // Modify from https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error#examples
  9442. // Create a new object, that prototypally inherits from the Error constructor.
  9443. function SplitError(message) {
  9444. this.name = 'SplitError';
  9445. this.message = message || 'SplitError Message';
  9446. this.stack = (new Error()).stack;
  9447. }
  9448. SplitError.prototype = Object.create(Error.prototype);
  9449. SplitError.prototype.constructor = SplitError;
  9450.  
  9451. if (!text) {return [];};
  9452.  
  9453. const result = {};
  9454. let key, value;
  9455. for (let i = 0; i < text.length; i++) {
  9456. const char = text.charAt(i);
  9457. for (const delimiter of delimiters) {
  9458. if (delimiter === char) {
  9459. if (!key && !value) {
  9460. key = text.substr(0, i).trim();
  9461. value = text.substr(i+1).trim();
  9462. result[key] = value;
  9463. result.KEY = key;
  9464. result.VALUE = value;
  9465. } else {
  9466. throw new SplitError('Mutiple Delimiter in Text');
  9467. }
  9468. }
  9469. }
  9470. }
  9471.  
  9472. return result;
  9473. }
  9474.  
  9475. function htmlEncode(text) {
  9476. const span = $CrE('div');
  9477. span.innerText = text;
  9478. return span.innerHTML;
  9479. }
  9480.  
  9481. // Convert rgb color(e.g. 51,51,153) to hex color(e.g. '333399')
  9482. function rgbToHex(r, g, b) {return fillNumber(((r << 16) | (g << 8) | b).toString(16), 6);}
  9483.  
  9484. // Fill number text to certain length with '0'
  9485. function fillNumber(number, length) {
  9486. let str = String(number);
  9487. for (let i = str.length; i < length; i++) {
  9488. str = '0' + str;
  9489. }
  9490. return str;
  9491. }
  9492.  
  9493. // Judge whether the str is a number
  9494. function isNumeric(str, disableFloat=false) {
  9495. const result = Number(str);
  9496. return !isNaN(result) && str !== '' && (!disableFloat || result===Math.floor(result));
  9497. }
  9498.  
  9499. // Del a item from an array using its index. Returns the array but can NOT modify the original array directly!!
  9500. function delItem(arr, delIndex) {
  9501. arr = arr.slice(0, delIndex).concat(arr.slice(delIndex+1));
  9502. return arr;
  9503. }
  9504.  
  9505. // Clone(deep) an object variable
  9506. // Returns the new object
  9507. function deepclone(obj) {
  9508. if (obj === null) return null;
  9509. if (typeof(obj) !== 'object') return obj;
  9510. if (obj.constructor === Date) return new Date(obj);
  9511. if (obj.constructor === RegExp) return new RegExp(obj);
  9512. var newObj = new obj.constructor(); //保持继承的原型
  9513. for (let key in obj) {
  9514. if (obj.hasOwnProperty(key)) {
  9515. const val = obj[key];
  9516. newObj[key] = typeof val === 'object' ? deepclone(val) : val;
  9517. }
  9518. }
  9519. return newObj;
  9520. }
  9521.  
  9522. // Makes a function that returns a unique ID number each time
  9523. function uniqueIDMaker() {
  9524. let id = 0;
  9525. return makeID;
  9526. function makeID() {
  9527. id++;
  9528. return id;
  9529. }
  9530. }
  9531.  
  9532. // Returns a random string
  9533. function randstr(length=16, cases=true, aviod=[]) {
  9534. const all = 'abcdefghijklmnopqrstuvwxyz0123456789' + (cases ? 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' : '');
  9535. while (true) {
  9536. let str = '';
  9537. for (let i = 0; i < length; i++) {
  9538. str += all.charAt(randint(0, all.length-1));
  9539. }
  9540. if (!aviod.includes(str)) {return str;};
  9541. }
  9542. }
  9543.  
  9544. function randint(min, max) {
  9545. return Math.floor(Math.random() * (max - min + 1)) + min;
  9546. }
  9547.  
  9548. function AsyncManager() {
  9549. const AM = this;
  9550.  
  9551. // Ongoing xhr count
  9552. this.taskCount = 0;
  9553.  
  9554. // Whether generate finish events
  9555. let finishEvent = false;
  9556. Object.defineProperty(this, 'finishEvent', {
  9557. configurable: true,
  9558. enumerable: true,
  9559. get: () => (finishEvent),
  9560. set: (b) => {
  9561. finishEvent = b;
  9562. b && AM.taskCount === 0 && AM.onfinish && AM.onfinish();
  9563. }
  9564. });
  9565.  
  9566. // Add one task
  9567. this.add = () => (++AM.taskCount);
  9568.  
  9569. // Finish one task
  9570. this.finish = () => ((--AM.taskCount === 0 && AM.finishEvent && AM.onfinish && AM.onfinish(), AM.taskCount));
  9571. }
  9572.  
  9573. function loadinResourceCSS() {
  9574. for (const res of NMonkey_Info.resources) {
  9575. if (res.isCss) {
  9576. const css = GM_getResourceText(res.name);
  9577. css && addStyle(css);
  9578. }
  9579. }
  9580. }
  9581.  
  9582. function loadinFontAwesome() {
  9583. // https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.1.1/css/all.min.css
  9584. const url = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css';
  9585. const alts = [
  9586. 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.1.1/css/all.min.css',
  9587. 'https://bowercdn.net/c/fontAwesome-6.1.1/css/all.min.css',
  9588. ];
  9589. let i = -1;
  9590.  
  9591. const link = $CrE('link');
  9592. link.href = url;
  9593. link.rel = 'stylesheet';
  9594. link.onerror = function() {
  9595. i++;
  9596. if (i < alts.length) {
  9597. link.href = alts[i];
  9598. } else {
  9599. alertify.error(TEXT_ALT_SCRIPT_ERROR_AJAX_FA);
  9600. }
  9601. }
  9602.  
  9603. document.head.appendChild(link);
  9604. }
  9605.  
  9606. // NMonkey By PY-DNG, 2021.07.18 - 2022.02.18, License GPL-3
  9607. // NMonkey: Provides GM_Polyfills and make your userscript compatible with non-script-manager environment
  9608. // Description:
  9609. /*
  9610. Simulates a script-manager environment("NMonkey Environment") for non-script-manager browser, load @require & @resource, provides some GM_functions(listed below), and also compatible with script-manager environment.
  9611. Provides GM_setValue, GM_getValue, GM_deleteValue, GM_listValues, GM_xmlhttpRequest, GM_openInTab, GM_setClipboard, GM_getResourceText, GM_getResourceURL, GM_addStyle, GM_addElement, GM_log, unsafeWindow(object), GM_info(object)
  9612. Also provides an object called GM_POLYFILLED which has the following properties that shows you which GM_functions are actually polyfilled.
  9613. Returns true if polyfilled is environment is ready, false for not. Don't worry, just follow the usage below.
  9614. */
  9615. // Note: DO NOT DEFINE GM-FUNCTION-NAMES IN YOUR CODE. DO NOT DEFINE GM_POLYFILLED AS WELL.
  9616. // Note: NMonkey is an advanced version of GM_PolyFill (and BypassXB), it includes more functions than GM_PolyFill, and provides better stability and compatibility. Do NOT use NMonkey and GM_PolyFill (and BypassXB) together in one script.
  9617. // Usage:
  9618. /*
  9619. // ==UserScript==
  9620. // @name xxx
  9621. // @namespace xxx
  9622. // @version 1.0
  9623. // ...
  9624. // @require https://.../xxx.js
  9625. // @require ...
  9626. // ...
  9627. // @resource https://.../xxx
  9628. // @resource ...
  9629. // ...
  9630. // ==/UserScript==
  9631.  
  9632. // Use a closure to wrap your code. Make sure you have it a name.
  9633. (function YOUR_MAIN_FUNCTION() {
  9634. 'use strict';
  9635. // Strict mode is optional. You can use strict mode or not as you want.
  9636. // Polyfill first. Do NOT do anything before Polyfill.
  9637. var NMonkey_Ready = NMonkey({
  9638. mainFunc: YOUR_MAIN_FUNCTION,
  9639. name: "script-storage-key, aims to separate different scripts' storage area. Use your script's @namespace value if you don't how to fill this field.",
  9640. requires: [
  9641. {
  9642. name: "", // Optional, used to display loading error messages if anything went wrong while loading this item
  9643. src: "https://.../xxx.js",
  9644. loaded: function() {return boolean_value_shows_whether_this_js_has_already_loaded;}
  9645. execmode: "'eval' for eval code in current scope or 'function' for Function(code)() in global scope or 'script' for inserting a <script> element to document.head"
  9646. },
  9647. ...
  9648. ],
  9649. resources: [
  9650. {
  9651. src: "https://.../xxx"
  9652. name: "@resource name. Will try to get it from @resource using this name before fetch it from src",
  9653. },
  9654. ...
  9655. ],
  9656. GM_info: {
  9657. // You can get GM_info object, if you provide this argument(and there is no GM_info provided by the script-manager).
  9658. // You can provide any object here, what you provide will be what you get.
  9659. // Additionally, two property of NMonkey itself will be attached to GM_info if polyfilled:
  9660. // {
  9661. // scriptHandler: "NMonkey"
  9662. // version: "NMonkey's version, it should look like '0.1'"
  9663. // }
  9664. // The following is just an example.
  9665. script: {
  9666. name: 'my first userscript for non-scriptmanager browsers!',
  9667. description: 'this script works well both in my PC and my mobile!',
  9668. version: '1.0',
  9669. released: true,
  9670. version_num: 1,
  9671. authors: ['Johnson', 'Leecy', 'War Mars']
  9672. update_history: {
  9673. '0.9': 'First beta version',
  9674. '1.0': 'Finally released!'
  9675. }
  9676. }
  9677. surprise: 'if you check GM_info.surprise and you will read this!'
  9678. // And property "scriptHandler" & "version" will be attached here
  9679. }
  9680. });
  9681. if (!NMonkey_Ready) {
  9682. // Stop executing of polyfilled environment not ready.
  9683. // Don't worry, during polyfill progress YOUR_MAIN_FUNCTION will be called twice, and on the second call the polyfilled environment will be ready.
  9684. return;
  9685. }
  9686.  
  9687. // Your code here...
  9688. // Make sure your code is written after NMonkey be called
  9689. if
  9690. // ...
  9691.  
  9692. // Just place NMonkey function code here
  9693. function NMonkey(details) {
  9694. ...
  9695. }
  9696. }) ();
  9697.  
  9698. // Oh you want to write something here? Fine. But code you write here cannot get into the simulated script-manager-environment.
  9699. */
  9700. function NMonkey(details) {
  9701. // Constances
  9702. const CONST = {
  9703. Text: {
  9704. Require_Load_Failed: '动态加载依赖js库失败(自动重试也都失败了),请刷新页面后再试:(\n一共尝试了{I}个备用加载源\n加载项目:{N}',
  9705. Resource_Load_Failed: '动态加载依赖resource资源失败(自动重试也都失败了),请刷新页面后再试:(\n一共尝试了{I}个备用加载源\n加载项目:{N}',
  9706. UnkownItem: '未知项目',
  9707. }
  9708. };
  9709.  
  9710. // Init DoLog
  9711. DoLog();
  9712.  
  9713. // Get argument
  9714. const mainFunc = details.mainFunc;
  9715. const name = details.name || 'default';
  9716. const requires = details.requires || [];
  9717. const resources = details.resources || [];
  9718. details.GM_info = details.GM_info || {};
  9719. details.GM_info.scriptHandler = 'NMonkey';
  9720. details.GM_info.version = '1.0';
  9721.  
  9722. // Run in variable-name-polifilled environment
  9723. if (InNPEnvironment()) {
  9724. // Already in polifilled environment === polyfill has alredy done, just return
  9725. return true;
  9726. }
  9727.  
  9728. // Polyfill functions and data
  9729. const GM_POLYFILL_KEY_STORAGE = 'GM_STORAGE_POLYFILL';
  9730. let GM_POLYFILL_storage;
  9731. const Supports = {
  9732. GetStorage: function() {
  9733. let gstorage = localStorage.getItem(GM_POLYFILL_KEY_STORAGE);
  9734. gstorage = gstorage ? JSON.parse(gstorage) : {};
  9735. let storage = gstorage[name] ? gstorage[name] : {};
  9736. return storage;
  9737. },
  9738.  
  9739. SaveStorage: function() {
  9740. let gstorage = localStorage.getItem(GM_POLYFILL_KEY_STORAGE);
  9741. gstorage = gstorage ? JSON.parse(gstorage) : {};
  9742. gstorage[name] = GM_POLYFILL_storage;
  9743. localStorage.setItem(GM_POLYFILL_KEY_STORAGE, JSON.stringify(gstorage));
  9744. },
  9745. };
  9746. const Provides = {
  9747. // GM_setValue
  9748. GM_setValue: function(name, value) {
  9749. GM_POLYFILL_storage = Supports.GetStorage();
  9750. name = String(name);
  9751. GM_POLYFILL_storage[name] = value;
  9752. Supports.SaveStorage();
  9753. },
  9754.  
  9755. // GM_getValue
  9756. GM_getValue: function(name, defaultValue) {
  9757. GM_POLYFILL_storage = Supports.GetStorage();
  9758. name = String(name);
  9759. if (GM_POLYFILL_storage.hasOwnProperty(name)) {
  9760. return GM_POLYFILL_storage[name];
  9761. } else {
  9762. return defaultValue;
  9763. }
  9764. },
  9765.  
  9766. // GM_deleteValue
  9767. GM_deleteValue: function(name) {
  9768. GM_POLYFILL_storage = Supports.GetStorage();
  9769. name = String(name);
  9770. if (GM_POLYFILL_storage.hasOwnProperty(name)) {
  9771. delete GM_POLYFILL_storage[name];
  9772. Supports.SaveStorage();
  9773. }
  9774. },
  9775.  
  9776. // GM_listValues
  9777. GM_listValues: function() {
  9778. GM_POLYFILL_storage = Supports.GetStorage();
  9779. return Object.keys(GM_POLYFILL_storage);
  9780. },
  9781.  
  9782. // unsafeWindow
  9783. unsafeWindow: window,
  9784.  
  9785. // GM_xmlhttpRequest
  9786. // not supported properties of details: synchronous binary nocache revalidate context fetch
  9787. // not supported properties of response(onload arguments[0]): finalUrl
  9788. // ---!IMPORTANT!--- DOES NOT SUPPORT CROSS-ORIGIN REQUESTS!!!!! ---!IMPORTANT!---
  9789. // details.synchronous is not supported as Tampermonkey
  9790. GM_xmlhttpRequest: function(details) {
  9791. const xhr = new XMLHttpRequest();
  9792.  
  9793. // open request
  9794. const openArgs = [details.method, details.url, true];
  9795. if (details.user && details.password) {
  9796. openArgs.push(details.user);
  9797. openArgs.push(details.password);
  9798. }
  9799. xhr.open.apply(xhr, openArgs);
  9800.  
  9801. // set headers
  9802. if (details.headers) {
  9803. for (const key of Object.keys(details.headers)) {
  9804. xhr.setRequestHeader(key, details.headers[key]);
  9805. }
  9806. }
  9807. details.cookie ? xhr.setRequestHeader('cookie', details.cookie) : function () {};
  9808. details.anonymous ? xhr.setRequestHeader('cookie', '') : function () {};
  9809.  
  9810. // properties
  9811. xhr.timeout = details.timeout;
  9812. xhr.responseType = details.responseType;
  9813. details.overrideMimeType ? xhr.overrideMimeType(details.overrideMimeType) : function () {};
  9814.  
  9815. // events
  9816. xhr.onabort = details.onabort;
  9817. xhr.onerror = details.onerror;
  9818. xhr.onloadstart = details.onloadstart;
  9819. xhr.onprogress = details.onprogress;
  9820. xhr.onreadystatechange = details.onreadystatechange;
  9821. xhr.ontimeout = details.ontimeout;
  9822. xhr.onload = function (e) {
  9823. const response = {
  9824. readyState: xhr.readyState,
  9825. status: xhr.status,
  9826. statusText: xhr.statusText,
  9827. responseHeaders: xhr.getAllResponseHeaders(),
  9828. response: xhr.response
  9829. };
  9830. (details.responseType === '' || details.responseType === 'text') ? (response.responseText = xhr.responseText) : function () {};
  9831. (details.responseType === '' || details.responseType === 'document') ? (response.responseXML = xhr.responseXML) : function () {};
  9832. details.onload(response);
  9833. }
  9834.  
  9835. // send request
  9836. details.data ? xhr.send(details.data) : xhr.send();
  9837.  
  9838. return {
  9839. abort: xhr.abort
  9840. };
  9841. },
  9842.  
  9843. // NOTE: options(arg2) is NOT SUPPORTED! if provided, then will just be skipped.
  9844. GM_openInTab: function(url) {
  9845. window.open(url);
  9846. },
  9847.  
  9848. // NOTE: needs to be called in an event handler function, and info(arg2) is NOT SUPPORTED!
  9849. GM_setClipboard: function(text) {
  9850. // Create a new textarea for copying
  9851. const newInput = document.createElement('textarea');
  9852. document.body.appendChild(newInput);
  9853. newInput.value = text;
  9854. newInput.select();
  9855. document.execCommand('copy');
  9856. document.body.removeChild(newInput);
  9857. },
  9858.  
  9859. GM_getResourceText: function(name) {
  9860. const _get = typeof(GM_getResourceText) === 'function' ? GM_getResourceText : () => (null);
  9861. let text = _get(name);
  9862. if (text) {return text;}
  9863. for (const resource of resources) {
  9864. if (resource.name === name) {
  9865. return resource.content ? resource.content : null;
  9866. }
  9867. }
  9868. return null;
  9869. },
  9870.  
  9871. GM_getResourceURL: function(name) {
  9872. const _get = typeof(GM_getResourceURL) === 'function' ? GM_getResourceURL : () => (null);
  9873. let url = _get(name);
  9874. if (url) {return url;}
  9875. for (const resource of resources) {
  9876. if (resource.name === name) {
  9877. return resource.src ? btoa(resource.src) : null;
  9878. }
  9879. }
  9880. return null;
  9881. },
  9882.  
  9883. GM_addStyle: function(css) {
  9884. const style = document.createElement('style');
  9885. style.innerHTML = css;
  9886. document.head.appendChild(style);
  9887. },
  9888.  
  9889. GM_addElement: function() {
  9890. let parent_node, tag_name, attributes;
  9891. const head_elements = ['title', 'base', 'link', 'style', 'meta', 'script', 'noscript'/*, 'template'*/];
  9892. if (arguments.length === 2) {
  9893. tag_name = arguments[0];
  9894. attributes = arguments[1];
  9895. parent_node = head_elements.includes(tag_name.toLowerCase()) ? document.head : document.body;
  9896. } else if (arguments.length === 3) {
  9897. parent_node = arguments[0];
  9898. tag_name = arguments[1];
  9899. attributes = arguments[2];
  9900. }
  9901. const element = document.createElement(tag_name);
  9902. for (const [prop, value] of Object.entries(attributes)) {
  9903. element[prop] = value;
  9904. }
  9905. parent_node.appendChild(element);
  9906. },
  9907.  
  9908. GM_log: function() {
  9909. const args = [];
  9910. for (let i = 0; i < arguments.length; i++) {
  9911. args[i] = arguments[i];
  9912. }
  9913. console.log.apply(null, args);
  9914. },
  9915.  
  9916. GM_info: details.GM_info,
  9917.  
  9918. GM: {info: details.GM_info}
  9919. };
  9920. const _GM_POLYFILLED = Provides.GM_POLYFILLED = {};
  9921. for (const pname of Object.keys(Provides)) {
  9922. _GM_POLYFILLED[pname] = true;
  9923. }
  9924.  
  9925. // Not in polifilled environment, then polyfill functions and create & move into the environment
  9926. // Bypass xbrowser's useless GM_functions
  9927. bypassXB();
  9928.  
  9929. // Create & move into polifilled environment
  9930. ExecInNPEnv();
  9931.  
  9932. return false;
  9933.  
  9934. // Bypass xbrowser's useless GM_functions
  9935. function bypassXB() {
  9936. if (typeof(mbrowser) === 'object' || (typeof(GM_info) === 'object' && GM_info.scriptHandler === 'XMonkey')) {
  9937. // Useless functions in XMonkey 1.0
  9938. const GM_funcs = [
  9939. 'unsafeWindow',
  9940. 'GM_getValue',
  9941. 'GM_setValue',
  9942. 'GM_listValues',
  9943. 'GM_deleteValue',
  9944. //'GM_xmlhttpRequest',
  9945. ];
  9946. for (const GM_func of GM_funcs) {
  9947. window[GM_func] = undefined;
  9948. eval('typeof({F}) === "function" && ({F} = Provides.{F});'.replaceAll('{F}', GM_func));
  9949. }
  9950. // Delete dirty data saved by these stupid functions before
  9951. for (let i = 0; i < localStorage.length; i++) {
  9952. const key = localStorage.key(i);
  9953. const value = localStorage.getItem(key);
  9954. value === '[object Object]' && localStorage.removeItem(key);
  9955. }
  9956. }
  9957. }
  9958.  
  9959. // Check if already in name-predefined environment
  9960. // I think there won't be anyone else wants to use this fxxking variable name...
  9961. function InNPEnvironment() {
  9962. return (typeof(GM_POLYFILLED) === 'object' && GM_POLYFILLED !== null && GM_POLYFILLED !== window.GM_POLYFILLED) ? true : false;
  9963. }
  9964.  
  9965. function ExecInNPEnv() {
  9966. const NG = new NameGenerator();
  9967.  
  9968. // Init names
  9969. const tnames = ['context', 'fapply', 'CDATA', 'uneval', 'define', 'module', 'exports', 'window', 'globalThis', 'console', 'cloneInto', 'exportFunction', 'createObjectIn', 'GM', 'GM_info'];
  9970. const pnames = Object.keys(Provides);
  9971. const fnames = tnames.slice();
  9972. const argvlist = [];
  9973. const argvs = [];
  9974.  
  9975. // Add provides
  9976. for (const pname of pnames) {
  9977. !fnames.includes(pname) && fnames.push(pname);
  9978. }
  9979.  
  9980. // Add grants
  9981. if (typeof(GM_info) === 'object' && GM_info.script && GM_info.script.grant) {
  9982. for (const gname of GM_info.script.grant) {
  9983. !fnames.includes(gname) && fnames.push(gname);
  9984. }
  9985. }
  9986.  
  9987. // Make name code
  9988. for (let i = 0; i < fnames.length; i++) {
  9989. const fname = fnames[i];
  9990. const exist = eval('typeof ' + fname + ' !== "undefined"') && fname !== 'GM_POLYFILLED';
  9991. argvlist[i] = exist ? fname : (Provides.hasOwnProperty(fname) ? 'Provides.'+fname : '');
  9992. argvs[i] = exist ? eval(fname) : (Provides.hasOwnProperty(fname) ? Provides[name] : undefined);
  9993. pnames.includes(fname) && (_GM_POLYFILLED[fname] = !exist);
  9994. }
  9995.  
  9996. // Load all @require and @resource
  9997. loadRequires(requires, resources, function(requires, resources) {
  9998. // Join requirecode
  9999. let requirecode = '';
  10000. for (const require of requires) {
  10001. const mode = require.execmode ? require.execmode : 'eval';
  10002. const content = require.content;
  10003. if (!content) {continue;}
  10004. switch(mode) {
  10005. case 'eval':
  10006. requirecode += content + '\n';
  10007. break;
  10008. case 'function': {
  10009. const func = Function.apply(null, fnames.concat(content));
  10010. func.apply(null, argvs);
  10011. break;
  10012. }
  10013. case 'script': {
  10014. const s = document.createElement('script');
  10015. s.innerHTML = content;
  10016. document.head.appendChild(s);
  10017. break;
  10018. }
  10019. }
  10020. }
  10021.  
  10022. // Make final code & eval
  10023. const varnames = ['NG', 'tnames', 'pnames', 'fnames', 'argvist', 'argvs', 'code', 'finalcode', 'wrapper', 'ExecInNPEnv', 'GM_POLYFILL_KEY_STORAGE', 'GM_POLYFILL_storage', 'InNPEnvironment', 'NameGenerator', 'LocalCDN', 'loadRequires', 'requestText', 'Provides', 'Supports', 'bypassXB', 'details', 'mainFunc', 'name', 'requires', 'resources', '_GM_POLYFILLED', 'CONST', 'NMonkey', 'polyfill_status'];
  10024. const code = requirecode + 'let ' + varnames.join(', ') + ';\n(' + mainFunc.toString() + ') ();';
  10025. const wrapper = Function.apply(null, fnames.concat(code));
  10026. const finalcode = '(' + wrapper.toString() + ').apply(this, [' + argvlist.join(', ') + ']);';
  10027. eval(finalcode);
  10028. });
  10029.  
  10030. function NameGenerator() {
  10031. const NG = this;
  10032. const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  10033. let index = [0];
  10034.  
  10035. NG.generate = function() {
  10036. const chars = [];
  10037. indexIncrease();
  10038. for (let i = 0; i < index.length; i++) {
  10039. chars[i] = letters.charAt(index[i]);
  10040. }
  10041. return chars.join('');
  10042. }
  10043.  
  10044. NG.randtext = function(len=32) {
  10045. const chars = [];
  10046. for (let i = 0; i < len; i++) {
  10047. chars[i] = letters[randint(0, letter.length-1)];
  10048. }
  10049. return chars.join('');
  10050. }
  10051.  
  10052. function indexIncrease(i=0) {
  10053. index[i] === undefined && (index[i] = -1);
  10054. ++index[i] >= letters.length && (index[i] = 0, indexIncrease(i+1));
  10055. }
  10056.  
  10057. function randint(min, max) {
  10058. return Math.floor(Math.random() * (max - min + 1)) + min;
  10059. }
  10060. }
  10061. }
  10062.  
  10063. // Load all @require and @resource for non-GM/TM environments (such as Alook javascript extension)
  10064. // Requirements: function AsyncManager(){...}, function LocalCDN(){...}
  10065. function loadRequires(requires, resoures, callback, args=[]) {
  10066. // LocalCDN
  10067. const LCDN = new LocalCDN();
  10068.  
  10069. // AsyncManager
  10070. const AM = new AsyncManager();
  10071. AM.onfinish = function() {
  10072. callback.apply(null, [requires, resoures].concat(args));
  10073. }
  10074.  
  10075. // Load js
  10076. for (const js of requires) {
  10077. !js.loaded() && loadinJs(js);
  10078. }
  10079.  
  10080. // Load resource
  10081. for (const resource of resoures) {
  10082. loadinResource(resource);
  10083. }
  10084.  
  10085. AM.finishEvent = true;
  10086.  
  10087. function loadinJs(js) {
  10088. AM.add();
  10089.  
  10090. const srclist = js.srcset ? LCDN.sort(js.srcset).srclist : [];
  10091. let i = -1;
  10092. LCDN.get(js.src, onload, [], onfail);
  10093.  
  10094. function onload(content) {
  10095. js.content = content;
  10096. AM.finish();
  10097. }
  10098.  
  10099. function onfail() {
  10100. i++;
  10101. if (i < srclist.length) {
  10102. LCDN.get(srclist[i], onload, [], onfail);
  10103. } else {
  10104. alert(CONST.Text.Require_Load_Failed.replace('{I}', i.toString()).replace('{N}', js.name ? js.name : CONST.Text.UnkownItem));
  10105. }
  10106. }
  10107. }
  10108.  
  10109. function loadinResource(resource) {
  10110. let content;
  10111. if (typeof GM_getResourceText === 'function' && (content = GM_getResourceText(resource.name))) {
  10112. resource.content = content;
  10113. } else {
  10114. AM.add();
  10115.  
  10116. let i = -1;
  10117. LCDN.get(resource.src, onload, [], onfail);
  10118.  
  10119. function onload(content) {
  10120. resource.content = content;
  10121. AM.finish();
  10122. }
  10123.  
  10124. function onfail(content) {
  10125. i++;
  10126. if (resource.srcset && i < resource.srcset.length) {
  10127. LCDN.get(resource.srcset[i], onload, [], onfail);
  10128. } else {
  10129. debugger;
  10130. alert(CONST.Text.Resource_Load_Failed.replace('{I}', i.toString()).replace('{N}', js.name ? js.name : CONST.Text.UnkownItem));
  10131. }
  10132. }
  10133. }
  10134. }
  10135. }
  10136.  
  10137. // Loads web resources and saves them to GM-storage
  10138. // Tries to load web resources from GM-storage in subsequent calls
  10139. // Updates resources every $(this.expire) hours, or use $(this.refresh) function to update all resources instantly
  10140. // Dependencies: GM_getValue(), GM_setValue(), requestText(), AsyncManager(), KEY_LOCALCDN
  10141. function LocalCDN() {
  10142. const LC = this;
  10143. const _GM_getValue = typeof(GM_getValue) === 'function' ? GM_getValue : Provides.GM_getValue;
  10144. const _GM_setValue = typeof(GM_setValue) === 'function' ? GM_setValue : Provides.GM_setValue;
  10145.  
  10146. const KEY_LOCALCDN = 'LOCAL-CDN';
  10147. const KEY_LOCALCDN_VERSION = 'version';
  10148. const VALUE_LOCALCDN_VERSION = '0.3';
  10149.  
  10150. // Default expire time (by hour)
  10151. LC.expire = 72;
  10152.  
  10153. // Try to get resource content from loaclCDN first, if failed/timeout, request from web && save to LocalCDN
  10154. // Accepts callback only: onload & onfail(optional)
  10155. // Returns true if got from LocalCDN, false if got from web
  10156. LC.get = function(url, onload, args=[], onfail=function(){}) {
  10157. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  10158. const resource = CDN[url];
  10159. const time = (new Date()).getTime();
  10160.  
  10161. if (resource && resource.content !== null && !expired(time, resource.time)) {
  10162. onload.apply(null, [resource.content].concat(args));
  10163. return true;
  10164. } else {
  10165. LC.request(url, _onload, [], onfail);
  10166. return false;
  10167. }
  10168.  
  10169. function _onload(content) {
  10170. onload.apply(null, [content].concat(args));
  10171. }
  10172. }
  10173.  
  10174. // Generate resource obj and set to CDN[url]
  10175. // Returns resource obj
  10176. // Provide content means load success, provide null as content means load failed
  10177. LC.set = function(url, content) {
  10178. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  10179. const time = (new Date()).getTime();
  10180. const resource = {
  10181. url: url,
  10182. time: time,
  10183. content: content,
  10184. success: content !== null ? (CDN[url] ? CDN[url].success + 1 : 1) : (CDN[url] ? CDN[url].success : 0),
  10185. fail: content === null ? (CDN[url] ? CDN[url].fail + 1 : 1) : (CDN[url] ? CDN[url].fail : 0),
  10186. };
  10187. CDN[url] = resource;
  10188. _GM_setValue(KEY_LOCALCDN, CDN);
  10189. return resource;
  10190. }
  10191.  
  10192. // Delete one resource from LocalCDN
  10193. LC.delete = function(url) {
  10194. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  10195. if (!CDN[url]) {
  10196. return false;
  10197. } else {
  10198. delete CDN[url];
  10199. _GM_setValue(KEY_LOCALCDN, CDN);
  10200. return true;
  10201. }
  10202. }
  10203.  
  10204. // Delete all resources in LocalCDN
  10205. LC.clear = function() {
  10206. _GM_setValue(KEY_LOCALCDN, {});
  10207. upgradeConfig();
  10208. }
  10209.  
  10210. // List all resource saved in LocalCDN
  10211. LC.list = function() {
  10212. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  10213. const urls = LC.listurls();
  10214. return LC.listurls().map((url) => (CDN[url]));
  10215. }
  10216.  
  10217. // List all resource's url saved in LocalCDN
  10218. LC.listurls = function() {
  10219. return Object.keys(_GM_getValue(KEY_LOCALCDN, {})).filter((url) => (url !== KEY_LOCALCDN_VERSION));
  10220. }
  10221.  
  10222. // Request content from web and save it to CDN[url]
  10223. // Accepts callbacks only: onload & onfail(optional)
  10224. LC.request = function(url, onload, args=[], onfail=function(){}) {
  10225. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  10226. requestText(url, _onload, [], _onfail);
  10227.  
  10228. function _onload(content) {
  10229. LC.set(url, content);
  10230. onload.apply(null, [content].concat(args));
  10231. }
  10232.  
  10233. function _onfail() {
  10234. LC.set(url, null);
  10235. onfail();
  10236. }
  10237. }
  10238.  
  10239. // Re-request all resources in CDN instantly, ignoring LC.expire
  10240. LC.refresh = function(callback, args=[]) {
  10241. const urls = LC.listurls();
  10242.  
  10243. const AM = new AsyncManager();
  10244. AM.onfinish = function() {
  10245. callback.apply(null, [].concat(args))
  10246. };
  10247.  
  10248. for (const url of urls) {
  10249. AM.add();
  10250. LC.request(url, function() {
  10251. AM.finish();
  10252. });
  10253. }
  10254.  
  10255. AM.finishEvent = true;
  10256. }
  10257.  
  10258. // Sort src && srcset, to get a best request sorting
  10259. LC.sort = function(srcset) {
  10260. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  10261. const result = {srclist: [], lists: []};
  10262. const lists = result.lists;
  10263. const srclist = result.srclist;
  10264. const suc_rec = lists[0] = []; // Recent successes take second (not expired yet)
  10265. const suc_old = lists[1] = []; // Old successes take third
  10266. const fails = lists[2] = []; // Fails & unused take the last place
  10267. const time = (new Date()).getTime();
  10268.  
  10269. // Make lists
  10270. for (const s of srcset) {
  10271. const resource = CDN[s];
  10272. if (resource && resource.content !== null) {
  10273. if (!expired(resource.time, time)) {
  10274. suc_rec.push(s);
  10275. } else {
  10276. suc_old.push(s);
  10277. }
  10278. } else {
  10279. fails.push(s);
  10280. }
  10281. }
  10282.  
  10283. // Sort lists
  10284. // Recently successed: Choose most recent ones
  10285. suc_rec.sort((res1, res2) => (res2.time - res1.time));
  10286. // Successed long ago or failed: Sort by success rate & tried time
  10287. [suc_old, fails].forEach((arr) => (arr.sort(sorting)));
  10288.  
  10289. // Push all resources into seclist
  10290. [suc_rec, suc_old, fails].forEach((arr) => (arr.forEach((res) => (srclist.push(res)))));
  10291.  
  10292. DoLog(['LocalCDN: sorted', result]);
  10293. return result;
  10294.  
  10295. function sorting(res1, res2) {
  10296. const sucRate1 = (res1.success+1) / (res1.fail+1);
  10297. const sucRate2 = (res2.success+1) / (res2.fail+1);
  10298.  
  10299. if (sucRate1 !== sucRate2) {
  10300. // Success rate: high to low
  10301. return sucRate2 - sucRate1;
  10302. } else {
  10303. // Tried time: less to more
  10304. // Less tried time means newer added source
  10305. return (res1.success+res1.fail) - (res2.success+res2.fail);
  10306. }
  10307. }
  10308. }
  10309.  
  10310. function upgradeConfig() {
  10311. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  10312. switch(CDN[KEY_LOCALCDN_VERSION]) {
  10313. case undefined:
  10314. init();
  10315. break;
  10316. case '0.1':
  10317. v01_To_v02();
  10318. logUpgrade();
  10319. break;
  10320. case '0.2':
  10321. v01_To_v02();
  10322. v02_To_v03();
  10323. logUpgrade();
  10324. break;
  10325. case VALUE_LOCALCDN_VERSION:
  10326. DoLog('LocalCDN is in latest version.');
  10327. break;
  10328. default:
  10329. DoLog(LogLevel.Error, 'LocalCDN.upgradeConfig: Invalid config version({V}) for LocalCDN. '.replace('{V}', CDN[KEY_LOCALCDN_VERSION]));
  10330. }
  10331. CDN[KEY_LOCALCDN_VERSION] = VALUE_LOCALCDN_VERSION;
  10332. _GM_setValue(KEY_LOCALCDN, CDN);
  10333.  
  10334. function logUpgrade() {
  10335. DoLog(LogLevel.Success, 'LocalCDN successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', CDN[KEY_LOCALCDN_VERSION]).replaceAll('{V2}', VALUE_LOCALCDN_VERSION));
  10336. }
  10337.  
  10338. function init() {
  10339. // Nothing to do here
  10340. }
  10341.  
  10342. function v01_To_v02() {
  10343. const urls = LC.listurls();
  10344. for (const url of urls) {
  10345. if (url === KEY_LOCALCDN_VERSION) {continue;}
  10346. CDN[url] = {
  10347. url: url,
  10348. time: 0,
  10349. content: CDN[url]
  10350. };
  10351. }
  10352. }
  10353.  
  10354. function v02_To_v03() {
  10355. const urls = LC.listurls();
  10356. for (const url of urls) {
  10357. CDN[url].success = CDN[url].fail = 0;
  10358. }
  10359. }
  10360. }
  10361.  
  10362. function clearExpired() {
  10363. const resources = LC.list();
  10364. const time = (new Date()).getTime();
  10365.  
  10366. for (const resource of resources) {
  10367. expired(resource.time, time) && LC.delete(resource.url);
  10368. }
  10369. }
  10370.  
  10371. function expired(t1, t2) {
  10372. return (t1 - t2) > (LC.expire * 60 * 60 * 1000);
  10373. }
  10374.  
  10375. upgradeConfig();
  10376. clearExpired();
  10377. }
  10378.  
  10379. function requestText(url, callback, args=[], onfail=function(){}) {
  10380. const req = typeof(GM_xmlhttpRequest) === 'function' ? GM_xmlhttpRequest : Provides.GM_xmlhttpRequest;
  10381. req({
  10382. method: 'GET',
  10383. url: url,
  10384. responseType: 'text',
  10385. timeout: 45*1000,
  10386. onload: function(response) {
  10387. const text = response.responseText;
  10388. const argvs = [text].concat(args);
  10389. callback.apply(null, argvs);
  10390. },
  10391. onerror: onfail,
  10392. ontimeout: onfail,
  10393. onabort: onfail,
  10394. })
  10395. }
  10396.  
  10397. function AsyncManager() {
  10398. const AM = this;
  10399.  
  10400. // Ongoing xhr count
  10401. this.taskCount = 0;
  10402.  
  10403. // Whether generate finish events
  10404. let finishEvent = false;
  10405. Object.defineProperty(this, 'finishEvent', {
  10406. configurable: true,
  10407. enumerable: true,
  10408. get: () => (finishEvent),
  10409. set: (b) => {
  10410. finishEvent = b;
  10411. b && AM.taskCount === 0 && AM.onfinish && AM.onfinish();
  10412. }
  10413. });
  10414.  
  10415. // Add one task
  10416. this.add = () => (++AM.taskCount);
  10417.  
  10418. // Finish one task
  10419. this.finish = () => ((--AM.taskCount === 0 && AM.finishEvent && AM.onfinish && AM.onfinish(), AM.taskCount));
  10420. }
  10421.  
  10422. // Arguments: level=LogLevel.Info, logContent, asObject=false
  10423. // Needs one call "DoLog();" to get it initialized before using it!
  10424. function DoLog() {
  10425. const win = typeof(unsafeWindow) !== 'undefined' ? unsafeWindow : window;
  10426.  
  10427. // Global log levels set
  10428. win.LogLevel = {
  10429. None: 0,
  10430. Error: 1,
  10431. Success: 2,
  10432. Warning: 3,
  10433. Info: 4,
  10434. }
  10435. win.LogLevelMap = {};
  10436. win.LogLevelMap[LogLevel.None] = {prefix: '' , color: 'color:#ffffff'}
  10437. win.LogLevelMap[LogLevel.Error] = {prefix: '[Error]' , color: 'color:#ff0000'}
  10438. win.LogLevelMap[LogLevel.Success] = {prefix: '[Success]' , color: 'color:#00aa00'}
  10439. win.LogLevelMap[LogLevel.Warning] = {prefix: '[Warning]' , color: 'color:#ffa500'}
  10440. win.LogLevelMap[LogLevel.Info] = {prefix: '[Info]' , color: 'color:#888888'}
  10441. win.LogLevelMap[LogLevel.Elements] = {prefix: '[Elements]', color: 'color:#000000'}
  10442.  
  10443. // Current log level
  10444. DoLog.logLevel = win.isPY_DNG ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
  10445.  
  10446. // Log counter
  10447. DoLog.logCount === undefined && (DoLog.logCount = 0);
  10448. if (++DoLog.logCount > 512) {
  10449. console.clear();
  10450. DoLog.logCount = 0;
  10451. }
  10452.  
  10453. // Get args
  10454. let level, logContent, asObject;
  10455. switch (arguments.length) {
  10456. case 1:
  10457. level = LogLevel.Info;
  10458. logContent = arguments[0];
  10459. asObject = false;
  10460. break;
  10461. case 2:
  10462. level = arguments[0];
  10463. logContent = arguments[1];
  10464. asObject = false;
  10465. break;
  10466. case 3:
  10467. level = arguments[0];
  10468. logContent = arguments[1];
  10469. asObject = arguments[2];
  10470. break;
  10471. default:
  10472. level = LogLevel.Info;
  10473. logContent = 'DoLog initialized.';
  10474. asObject = false;
  10475. break;
  10476. }
  10477.  
  10478. // Log when log level permits
  10479. if (level <= DoLog.logLevel) {
  10480. let msg = '%c' + LogLevelMap[level].prefix;
  10481. let subst = LogLevelMap[level].color;
  10482.  
  10483. if (asObject) {
  10484. msg += ' %o';
  10485. } else {
  10486. switch(typeof(logContent)) {
  10487. case 'string': msg += ' %s'; break;
  10488. case 'number': msg += ' %d'; break;
  10489. case 'object': msg += ' %o'; break;
  10490. }
  10491. }
  10492.  
  10493. console.log(msg, subst, logContent);
  10494. }
  10495. }
  10496. }
  10497.  
  10498. // Polyfill alert
  10499. function polyfillAlert() {
  10500. if (typeof(GM_POLYFILLED) !== 'object') {return false;}
  10501. if (GM_POLYFILLED.GM_setValue) {
  10502. alertify.notify(TEXT_ALT_POLYFILL);
  10503. }
  10504. }
  10505.  
  10506. // Polyfill String.prototype.replaceAll
  10507. // replaceValue does NOT support regexp match groups($1, $2, etc.)
  10508. function polyfill_replaceAll() {
  10509. String.prototype.replaceAll = String.prototype.replaceAll ? String.prototype.replaceAll : PF_replaceAll;
  10510.  
  10511. function PF_replaceAll(searchValue, replaceValue) {
  10512. const str = String(this);
  10513.  
  10514. if (searchValue instanceof RegExp) {
  10515. const global = RegExp(searchValue, 'g');
  10516. if (/\$/.test(replaceValue)) {console.error('Error: Polyfilled String.protopype.replaceAll does support regexp groups');};
  10517. return str.replace(global, replaceValue);
  10518. } else {
  10519. return str.split(searchValue).join(replaceValue);
  10520. }
  10521. }
  10522. }
  10523.  
  10524. // Append a style text to document(<head>) with a <style> element
  10525. function addStyle(css, id) {
  10526. const style = $CrE("style");
  10527. id && (style.id = id);
  10528. style.textContent = css;
  10529. for (const elm of $All('#'+id)) {
  10530. elm.parentElement && elm.parentElement.removeChild(elm);
  10531. }
  10532. document.head.appendChild(style);
  10533. }
  10534.  
  10535. // Copy text to clipboard (needs to be called in an user event)
  10536. function copyText(text) {
  10537. // Create a new textarea for copying
  10538. const newInput = $CrE('textarea');
  10539. document.body.appendChild(newInput);
  10540. newInput.value = text;
  10541. newInput.select();
  10542. document.execCommand('copy');
  10543. document.body.removeChild(newInput);
  10544. }
  10545. })();