JavDB/MissAV 双站增强工具箱:收藏管理、状态标签、屏蔽过滤、跨站同步、热播/Top250榜单、新作品检测、预告片、字幕搜索、WebDav/Git云备份、演员信息、清单管理、截图墙、磁链聚合、以图识图等功能插件
// ==UserScript== // @name JavDB Power Tools // @namespace zerobiubiu.top // @version 1.46.2 // @author zerobiubiu // @description JavDB/MissAV 双站增强工具箱:收藏管理、状态标签、屏蔽过滤、跨站同步、热播/Top250榜单、新作品检测、预告片、字幕搜索、WebDav/Git云备份、演员信息、清单管理、截图墙、磁链聚合、以图识图等功能插件 // @license MIT // @copyright 2024-2026 zerobiubiu (https://github.com/zerobiubiu) — 基于 JAV-JHS 3.3.6 (MIT) 二次开发 // @icon https://cdn.jsdelivr.net/gh/zerobiubiu/Figure-bed/b507c13a-9f4e-41b5-bbbb-129d0a21f97d.png // @homepageURL https://github.com/zerobiubiu/monkey-jhs-disassemble // @match https://javdb.com/* // @match https://missav.ws/* // @match https://missav.live/* // @match https://missav.ai/* // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/js/tabulator.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/layer.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/js/md5.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/src/toastify.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/localforage.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/viewer.min.js // @connect xunlei.com // @connect missav.live // @connect javdb.com // @connect supjav.com // @connect 127.0.0.1 // @connect javstore.net // @connect u9a9.com // @connect u3c3.com // @connect sukebei.nyaa.si // @connect btsow.lol // @connect imgur.com // @connect api.imgur.com // @connect lens.google.com // @connect yandex.ru // @connect translate-pa.googleapis.com // @connect 123av.com // @connect fc2ppvdb.com // @connect adult.contents.fc2.com // @connect * // @grant GM_addStyle // @grant GM_addValueChangeListener // @grant GM_getValue // @grant GM_info // @grant GM_openInTab // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // @grant window.close // @run-at document-idle // ==/UserScript== (function(jquery, layui_layer, tabulator_tables, toastify_js, localforage$1, viewerjs, blueimp_md5) { "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); jquery = __toESM(jquery); layui_layer = __toESM(layui_layer); tabulator_tables = __toESM(tabulator_tables); toastify_js = __toESM(toastify_js); localforage$1 = __toESM(localforage$1); viewerjs = __toESM(viewerjs); blueimp_md5 = __toESM(blueimp_md5); var s = new Set(); var _css = async (t) => { if (s.has(t)) return; s.add(t); ((c) => { if (typeof GM_addStyle === "function") GM_addStyle(c); else (document.head || document.documentElement).appendChild(document.createElement("style")).append(c); })(t); }; var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports); window.jQuery = jquery.default; window.$ = jquery.default; _css("html #layuicss-skinlayercss{width:1989px;display:none;position:absolute}.layui-layer-shade,.layui-layer{_position:absolute;pointer-events:auto;position:fixed}.layui-layer-shade{width:100%;height:100%;top:0;left:0}.layui-layer{-webkit-overflow-scrolling:touch;background-color:#fff;-webkit-background-clip:content;margin:0;padding:0;top:150px;left:0;box-shadow:1px 1px 50px #0000004d}.layui-layer-close{position:absolute}.layui-layer-content{position:relative}.layui-layer-border{border:1px solid #0000001a;box-shadow:1px 1px 5px #0003}.layui-layer-load{background:#eee url(data:image/gif;base64,R0lGODlhJQAlAJECAL3L2AYrTv///wAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFCgACACwAAAAAJQAlAAACi5SPqcvtDyGYIFpF690i8xUw3qJBwUlSadmcLqYmGQu6KDIeM13beGzYWWy3DlB4IYaMk+Dso2RWkFCfLPcRvFbZxFLUDTt21BW56TyjRep1e20+i+eYMR145W2eefj+6VFmgTQi+ECVY8iGxcg35phGo/iDFwlTyXWphwlm1imGRdcnuqhHeop6UAAAIfkEBQoAAgAsEAACAAQACwAAAgWMj6nLXAAh+QQFCgACACwVAAUACgALAAACFZQvgRi92dyJcVJlLobUdi8x4bIhBQAh+QQFCgACACwXABEADAADAAACBYyPqcsFACH5BAUKAAIALBUAFQAKAAsAAAITlGKZwWoMHYxqtmplxlNT7ixGAQAh+QQFCgACACwQABgABAALAAACBYyPqctcACH5BAUKAAIALAUAFQAKAAsAAAIVlC+BGL3Z3IlxUmUuhtR2LzHhsiEFACH5BAUKAAIALAEAEQAMAAMAAAIFjI+pywUAIfkEBQoAAgAsBQAFAAoACwAAAhOUYJnAagwdjGq2amXGU1PuLEYBACH5BAUKAAIALBAAAgAEAAsAAAIFhI+py1wAIfkEBQoAAgAsFQAFAAoACwAAAhWUL4AIvdnciXFSZS6G1HYvMeGyIQUAIfkEBQoAAgAsFwARAAwAAwAAAgWEj6nLBQAh+QQFCgACACwVABUACgALAAACE5RgmcBqDB2MarZqZcZTU+4sRgEAIfkEBQoAAgAsEAAYAAQACwAAAgWEj6nLXAAh+QQFCgACACwFABUACgALAAACFZQvgAi92dyJcVJlLobUdi8x4bIhBQAh+QQFCgACACwBABEADAADAAACBYSPqcsFADs=) 50% no-repeat}.layui-layer-ico{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANIAAAA9CAYAAADYizcVAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNui8sowAACAASURBVHic7Z15fFvVmfe/92qzJMtrvCQ2jp09TuwsQCAkISwJDVAoW0hombaQhgKlnU5pmc60A+3M0FK6dxhoGghLC0PCEjJAIW8gISQhwWSzHcfO4tix41heZNmSLFnS1T3vH7IUy1qdmPfTd8a/z8cfS/ee5/zuvbq/e855znOeKwkhBEkg3FYC1j0Eug6i2upQHU0ITycAkjEfOaMMOXcWmrz5aAoXIZkKk1WZEs563ezstVLV10WNy8bJfgedPg8A+XojU8wZVKbnsiAzj6VZhUwwmEaFV7Xb8NXV4D95DKWliYC1DbW3FwA5KwtNYRHakjJ0U6ajn1WJnJ07KrzCd5aAYyeqqwrVXYM6cBLhH7zOunzktCnIpkrk9AVoMpYi6SeMCq/D4aCpqYmWlhasVis2mw2n0wmAxWIhNzeXwsJCSkpKKCsrIyMjY1R41Z5ufLWH8R8/ir+5kUB7G2pvDwByVg6a8UXoSiejm1aOvmIucs64UeG1+vvY42rkQH8zRz1nOeXtplMJnm++1sIkwzjKjRO42FzKovTJFOoyk9YpJRKSam9AaXoLpfld1J6jKR2knFOOtvRGtGW3IGfPSPHUIlHXb2dTRxObO5updfakZFNhyeHW/FLuLChjljn7vHiV1tMM7N2Jt2oPyummlGy0E8swLFhE2sKlaC+aeF68qrsOxbaJQM9m1P7alGxkcwWanFvR5t6JbJp1XrwdHR3U1NRQV1dHe3t7Sjbjx49n1qxZVFZWUlBQcF68SksTA7u2M7D3Y5TmxpRstKWTSVt4JWlLrkFbUnZevA0DVt6yH+Sd3hqOes6mZFNunMAXsyq5JXs+M9LiNxBxhCTwNzyPv/4lVFstctZUNIWLkPPno8kuR7KUIBmyEV47wtlCwH4UtfMgAese1N4TAMi5FehmfhXdjHsAKaWDFsAfzzSwvq2eQw4bM8xZLM0pZEFGPhXp2ZQZLeRoDfQoXpo8TmpddqocnezssdLQH2wx5mXksrZoJvcXz0iRFRACz7Z38XzwV/xNJ9EWlaArr0A3ZQbaiWVo8guQ0zNQXQ4CnR0op5vwn2zAf7QWpa0FAF3ZFIzLbsC4/EaQUmUWKB1/xN+xHtV1CNk0AzljKZr0BcimCqS0MiRtDkLpQQw0obprCbiqUB07Ud0NweucPg9dwVq0Bfenfp2FoKqqik8//ZS2tjby8/MpKyujpKSEwsJCsrOzMZlMuN1u7HY7VquVlpYWmpqa6OwMtpBFRUVcdtllLFiwACnV8xUC93tv4dn6Nv7G42gvmoh+9jx002aiLZ2EpmACssWC6nQS6DiL0nwK//F6fEcOobSeDl7nydMwfuEmTNffkvJ1Fgie797DS92fUOM+w9S0AhalT2G+uZRZxvGU6HPJ1pqwK25afDbqPO0c7G9mj+skJwY6AKg0FfPVcVdwz7hFSDGuc5SQhKcTX+1/4K95GjlvLrpJt6It+xJS+kXJD9jTiXJyE/5Tm1G7DgdPvPJB9BXfRjLmJ7S1+jz88nQtv2mu4ZLMPFYVTGJlQRkT09KT8lp9Hv7SfpKNHafY39cFwPdKK/nBxAoK9caEtmqvnf7/3oT77TfQTZ5G2hVXYVh4JZq8xMcbsh34+EMGPvkIf+NxAEw33Y755juRsxK3isJvxX/2l/jbfoNsuQRt7iq0uSuRDMlbNeG3onT9BcW2EdW5HwBd0ffQTfgBki5xt9rpdLJz50527dpFcXExc+bMoaKiguzs5K240+nk0KFDVFdXc+bMGQCWLFnC0qVLsVgsCW1Vew/9b75C/+ZX0U2dSdqSa0hbfDWa/OTDANXeg2fHVgZ2bcd/oh4A862rMd/2ZeTsnIS2nX4Hf+j8kKc7djDXVMJt2fP5UvZcLtIntgvZburZz5v2gxx2Bx+YDxZczXfyryVfF9m91fzkJz/5SeiL8HTiO/AE/rpn0c28F8MlP0I78UYkffI+IoCkM6MpWICm6CpQ/ajdh1A7PgO/C03ePCSdOaad1efhscYDPNVSxwMXzeTxyZdwS/5EsrT6lHjTNTquyCpgeU4RPqGy39HN3t4OnAE/l2bmka7RxbRTe+24Nr2E570tmL5wE+l33YNhwRXI5tjHGXW+aUZ008vRV86HgIK/8Tj+4/WIAQ+6ydOR0mKLWPit+FofQ2l/Ct34B9Bf9DjanFuQtFmp8WrS0ViuQJu1HPChuvajOvciVCea9EuRNLEfPk6nk23btvHJJ5+wcOFCVqxYwaxZszAaEz9sQjAYDEycOJGpU6eiqipnzpyhpaUFr9dLcXExBoMhpp1q78H18nO433kD0w23YvnqWtIWXolsTv6QBJCMRvQzKzDMuxQUBf+JBvwNRxAeN7qpM5HiHH+n38HP29/j2a6PuTdvMT+e8EVuzKokU5Pa+Zo1Bhakl3FVxnT8IsAhdwuf9TfjVL3MN5Vg1pw73yFCEvgO/QLl+CvoL34Ew6WPIhnzUiKMOnFDFtqS5aDRoXYdINC5HySBtuhqhnc/BPCTU4d4oe04j06+mCemXkpBklYkHrJ1Bm4YV4JO1lDV18Xevk6EJHFdblF0YywErtdeYmDH/yF95d2kf3kNcub5ja1kczqGeQuQtFr8J4/hP34UEBgqL47R/RD4z/yEQNcL6C96FH3JE0i68xtrSNpsNNk3IMk61P4qVMdekASarOuIus5CsG3bNvbv38+yZctYsWIF6emp3cjDYTQamT59OhqNhtbWVlpaWhBCMHXq1OhunhC4XtmA54O/YrnrXixfvx85K3lrEAtyugXDJQuRtLqgg6LhCCCCApOG31eCJ6zv8YrtUx4Zfz2PFd1Eni5xqxkPWRoTyzPL0UlaDvSfZn9/E6okuDpjRribFxaSv+F5fJ/9DP3Fj6Cf8z1S7W/Hh4SmcCFIEDi7C7XjMyRTHppx8yJK/fFMAz8++RmPTr6YH5XNGQVWuDK7EIHE9p6z7O3toMBg4tKMSI+PZ9u7uP7rBdJX3o35ti+PYFwTj1hCP7MCEPiOHMZ/vB5NVg66ydMiiikdf8R3+sfoL3oUXdGPGJXrnHElIAj0bUd17kXWFyCnXxpRqqqqiq1bt7Js2TKuueaa1Mc18VglibKy4KC/sbGRlpYWMjIyKC4ujijnfu8tXH9ej+WuezGv+uroXOfZc5AE+GoO4G84gpydg27qzIhiz3fv4fGz7/LI+Ot5uPC6mOOaEdEicUX6ZASwy3mcz/qbyddlMM9UAgwKSbU34Nv/ONrSL2K49FGS/biuZ3PxHXwS38En0U6+HSkt/hNGU3g5wtON2n0I0W9FU3g5kjF4U9f12/lx435uzS/liamXXvAtNRRLsgvp9HvY7+jmrK+fxdmF5A+2dErrafpffYG0yxaR/uU1ET+u0taC0tqcUt8dVcV7sArZaAp3L/QzZiMcffgbj6PabehmzEbODHbZVHcdvtYfo829FX3JEyS6zsJ3FqVzA0rX86h9HyB87UhpU5Dk2N0nTcYSUDpRXfsR/rNoLIuRdMFxXkdHB1u3bmX27NmsWLEioYjcbjdbtmzh/fffp6GhAbPZTG5ufPd+aWkp/f39nDlzBofDQWlpabilU1qacP15PWkLr8Ty9fsvXERDoJ9VidrXi/9EA2qPLTgNMdijaBiw8vjZd7gxq5LHim66YBENxcL0SXQrLg65W7D6+7g8fTLjtOnIAErTWyBr0JXfy4U/IYdDQlf5EHLeXFRbbZBrEJs6mtBIMg8Wl48Ka31/L58OOhsk4AcTK7kkM49DDhubOs65swf27gRZxviFmyJ+XLXXjvvtN+h/4xUC7WeS8nmrD9D/2p/xbH+fQHfn4OlKmG5eiW7yNPxNJ4Ncg1Bsm0DSoC14kETXOdCzGc/hcnxN30Wx/gn/2T/gPbkGz+FyAn3b41hJ6Cb8ANlyCarrUJBrEDU1NUiSxMKFCxOKqKWlhccff5yOjg5mzJiBTqfjxRdf5NVXX41rI0kSV155JcXFxbS1tVFTUxPeN7BrO8gaTDfeGlNEgY52+t94hYGd2xCKP2q/6ujD/e5m3O9uRnU5hxNjvv3L6KbOxN94PMg1iLfsB5GRWTNuyaiKCIIt07cLrmWuqYQa9xnesh8EQBZuK0rzu+gm3YqcPTNJNecH2TIR3aRbAVCa30W4rZz1utnc2cyqgknMTj+/sclQ1Lrs3Fb9AbdWb2NfX/CmLjNaWFUwCYDNnc2c9bpR7Ta8VXtIu+IqtBeVRtThqz2E58P38NUewvHc0wQ6rXH5fEcO43pxHf7G4/Rv/i8Gtm8N79PkF5J2xVUAeKv2oNptwcnWns1oc1chm2bHrVftP8xAw+0IpS+4QdaDHHSWCO8ZBupXoHrqY9pKhjK0uauAoBiF7ywOh4O6ujrmzJmTdN5nw4YNXHXVVXzrW9/ihhtu4O677+ZHP/oRtbW11NfH5gTIyclhzpw5ANTV1eFwOFB7uhnY+3Fw3mfipBgnqjKwcxvu/36N/rc24a8/ElXEe2Af/Vs24v7v1/Dt3xu1X1MwnrQl1wAwsPdj1J5urP4+3umt4bbs+cw0jo+y2Ww/xEXVPyDn4HeYWP2PbO2ri9ivCJVvn36FcQf/npyD32H5sd/QrbgiykzU53Jb9nwA3umtwervQw5Y94DqR1v2pbgXajSgnXInctZU1J6jBKx72Nlrxa+qrCw4v8m1oTjstLGy5kMa+ntp97r59rFzF/3u8VOYYc6i1tnDzl4rvroaUAIYFl4ZfYxlUzBcthgAX/V+nM8+FVNMvrpqnM89hdLWgqTTY7zmevRzL4kok3bltWiLSlBON+GrqyHg2AnCjzZ3ZcJz8bX+M0EXjIy+9BeY5jdhmn8KXfEjgASqH3/rv8S11+bdjWyagdpfS8Cxk6amJgKBABUVFQl5jx8/TiAQYPny5RHbTSYTc+bMYdu2bQnt582bR35+Pu3t7TQ1NeGrPQwBhbTFV8c+z5oDuP7reQK2Lvwn6nG99CfU/nM3rNLajOvP6wmcPYPS1oLzz+tRWpuj6jFe/QW0F01EaW7EV3uYPa5G/CLAl7LnRnOKAOu6dtIf8ALgDHh4qvPDiDINA+28bNuHSnBW6EB/M+/1RYv8zpxLmJpWwFHPWfa4GpEDXQeDYT0pzBNdCCRjPprCRQAEug5S1dfF0pzClOaJEuGw08ZdtTs4Njgha9Ho+M5F52b6C/VGluYExztVfV34Tx5DV14Rc55IW1xCxpqHSBsUmfdQVVBMHedm/X1HDuN87j9RzrQg6Q2YbriF9LvXoJsW2ZrLWdnoyoM3r//kMVRXFXLG0sTzRKoX1fUZAJrsZegmPIKkn4CkL0Zf8gtkY/ChE3Dui1uFpCtEzlgarM5VRUtLC2VlZUnnibq7uzHHcfsXFxfjdrsT2lsslrDzoaWlBf/xo+hnz4s71gx0dyH8/iHfOxFOR/i72tdHoKvz3Hd7D6o9OspFzs5BPzvowPIfP8qB/mYWpU+JOU8kI6GXNBHb9JI24rt22H4AXYxt+boMFqVPAYJik1VbHXL+/JgnO9oI8ai2OmpcNhZkxJ70PODo5pi7L2l9Na4e7q77KBzVkK7R8Uz5Yv5u/JSIciGeGpcNpaUJ3ZT4oUtydg6Wex4g7fIlwKCYnn8Gpa0V39EanBueRmltRtLpMd14G+Y7/w5JH9sBEOJRWppQ3TVo0hckPB+h9CB83QBRghNKD0KxJ7QPIcSjumuwWq2UlJSkZHehCPFYrVb8zY1RD5eh0M+ei27S1PB3w+VL0OSd63pqJ5aStnDJuf0XX4Z2mAc0hBCPv7mRo56zzDeXxiynlWT+vmA5BYOxc0X6bB4u/EJEmelpBfx94bKweJZnzmJFZuwQrBDPUc9ZtKqjCU12edwTHk2EeFRHEyctDiqGjY0EsNNu5a7a7YzTG3ijchnTTLEng2tcPdx95CPqnMGby6LRsW7WEu4qiO6Ph3hO9jsIWNvQTkzcnZSzc7Hc8wBCCLyf7sZ7YB+qsw/hHQiKSKvFdMMtmG+/K66IgDBPwNqGOnAS2ZS4eyVpMzFMeRZEANk8J7xddVXhbbwX4Q+eqyZzWeLjH+RRB05is9koLBydIOJkCPHYbDYC7W1oS2OMjQahKZxA1g//De+hKiSTmbTLFoPm3JNftmSS8cDDGC6+HKGqpC1cEncCN8QTaG/jlDebWTHGRiFcmzGD96d/l5MDXcw0FjJBFzkJLiHx2ISbuSlzDv2qnwXmiRjk2BP6IZ5T3m60wtOJZPl/88QK8QhPJ50+D2XGyAkyu9/LQ8f2YPW6sXrdrKrdzqsV1zB9mJhqXD3cVbODo/3BGytDq+NP5VeyKs54K8TT6fOg9vaiyU8+ASrnjCPj3gdxpRnx7NyG/3hwsC2nWzCuuBnzzSuRDGkJ6wjxqL29CH8nUlqS8aBsQpu/JmKTv/23+JoehsE+u2ycgn7ikwmrCfEIfydOpzNpt+73v/89TqcTt9vNr3/966j9Ho8nvO+aa65h3rx5MWohzON0OlF7e9AUJI5O10woxjShOO5+OTsn6FlNghCP2ttDp+KkRJ84Gn+iPpeJScrMNycP1QrxdCpOtACS4cK9ZqlgOE+ONvJpbtJoebC4nEeOf0p/QOGww8aq2u28Mvtqys3BJ8dhp41VNds5Ptj1y9TqWT9rCSvz49+kw3nk9NSWAcg54zBcuhDP7u0QCATPwZxO2uVLkIzJl2wM55G0I5vRVz3H8DV9L/xdm/dl9GVPIWkT/17DeUymxMfa3t7OqlWrkh7Pzp07qa+vjyuk4Txykvi70cJwnmzt6CynSYahPLJkzEd4U+t7XyhCPJIxn3y9kR7FG7E/TdbwYPFMfjfjCkya4CCw2mHj7iM7OOVxUt/fy+raHWERZWh1rCtPLCIgzJOvNyJnZaG6HAnLh+Crq6Z/86thEQGoPTZcr/2ZgDV5GH6IR87KQtLlI5TUloSEILzN4c/agq9jmPpyUhEBYR5Jl4/FYknqKJAkiXnz5oX/AoEAW7Zs4eDBgxHbkwWmhngsFgtyVg6q05mw/GghxCNn5ZCvtWBXEp/vaCHEk6+1oJUzyhDOFqS00VmclgjCGYyglTPKmGLOoMnjZJwuunv0jQnTCAiVf2jYi0cNcMhh47aaD3ArCicGRWTR6Hhm5uK43bmhaPIEL/QUcwaawiICnR3IlsSBuL4jh3E+O+ji1hswLrsB1dnHwK7teD/dA34/lnu/haYgfn880BkMwdcUFiGnTUEMNCGlj2BxmiSHP2qyvpCgYCTEQHDyWU6bQm5uLna7Pa5HLhb27dtHTk4OJ06cSP1YAbs9+KDMzc1FM76IQMdZ5IzUAp4h2DVTXU5QFNDpkDMyk/5OAIGO4ENNM76ISYZxtPhs5GpTP9/zRYvPBsAkwzi0cu4sAvajyHmxm+tYSP+G7byIA/bg4kA5dxaV6bnUuuxcmhE7MPabRTOQkfjusb24AwrVjnOcGVodfyyP7ViIhVpX8AeuTM9FW1KGcropKgZuKHx11Tif/Q+UtlaQ5aBj4bYvIwY84PczsG8X3kOfIZ79DzK+8e24YgotDtSWlCGbfKju2qgYuIQQyrnPgdSf7qo7uDhQNlVSWFiI1WqNioFLhOuuu44NGzYwffr0lG0g6K2DoNNBhwel+VRUDFzUsTr7GNizE1/1AZRTJ1B7exBCIMky8ri84JKLhUvQz78MSRt70K80nwJAVzqZcuME6jzt4Ri4oWgYsPKh4yhr85ZGucHjoS/g4Xcd21honsJ1mZFOuTpPcFqk3DgBWZM3H7XzYEqVXihCPJq8+SzIzKPK0Zmw/Nqi6fx6+uVR29eNQERAmGdBZh66KdPxn2yIW9bfcATn+kERaTSYb7oD821fRjIaw65xw2WLQAh81QdwrP9DxDxTRF2DPLop05HTFxBwVaV8zACy+WLSyt8nbdZWNJmpt0ghHjl9ASUlJbS0tIyId9q0aVRWVnLbbbeNyC7EU1JSgm5aedhBEwuqy4l7yyZs3/0GjqeeZGDXhwRsXaDXB1sxnY6A9Syebe9i/9cfYv/JD/DVHopZV4hHN62ci82lHOxvjlnursZ1/MuZt7jz5B855E58TVwBL6/2fMaKY7/l99YP+H7rJmxKf0SZEM/F5lK0msJFKI2bEa7WlCdlXc+e6waaVn6KnDklQekghKuVgHUPck45msJFLNVksNHayOkBV8JJ2fuLZmD1evhp4wEA/lS+hNUjENHpARc7e6xUWHJYmlWIflYmA5/sJNDVGTUp66urxrHu98E4O40W8xdvw7zy7gjvnJwzjow1D+GUNQzs/RhfzUEcT/8a47UrSLvynFs60NWJ/2gt2oll6GdVIpmno9g2IrynU1q8B8ExjvA2IwJONJnLkxsAwnsa1bEzuBQ9Yyll+nSqq6ux2+1xvXdCiKh4OlmWee+99yK2Wa3W8KTrcNjtdpqamhg/fjxlZWXolaKgODqtUZOy/lMncP7p9/iODC7+nDkbw/zL0JdXIueOQzKkIbwDBLo68NUewrt3F77D+7E3HCH97rWYbznnGAl0WvEdOYS2dDL6irksStex2X6AVl9P1KTsV3Ov4FfWrXzsPMbyYye4KWsOV1umM8M4nkyNEa+qcNrXQ427lb/21VI/uBx9nNbCA/lXYdGcuw9afT3scZ2k3DiBRemT0UqmQuTc2ShNW9BVPJToN7ogKE1bUHtPoJ//AyRTIROAOZZcXuto4vsTE8+v/GTSPNJkDTk6A2uLRtbdeK2jiYb+Xh6dPD+YHMVgQls6Ce/ejzHdfEdEWeHpR+3uDM4T3bwS8x1fQdJFLy6Us3Ox3PstkKSgKDvOojoiHRjevR+jtLVgXnl3ODmKbJqDYnsN3YTvp3Tsgb4P8DbeD4Cky0Ob97WkNortNVR3A7qLHkXSTyBDH8yzUFtby5VXRodFAVx77bXYbMm765MnT+b666+Pua+2tpbOzk6WLVsWTo6iLZvCwO4dmG+7K6KscDrw1deiGV9E+sq/w3DZ4nCE/FBoiydimLcA0/Iv0v/GK7i3vYPa0x1RZmD3DpTW06TfdQ9yzjgKgdnGIrbYD/NQwTURZf+hcDmLLVN5sv19PnQcZYv9EFvsh9BIMmmyDkWoeNVz0RZZWhM3Z83lOwXLmGSIHNtusR/mxEAHj4xfQaEuM+j+1pbdgq/6d2js9Z9L4Kpqr8d/ajNybgXaslvC2+8sKOOJ5mqOuOxJA1d/WFo5Yt4jLjsbO04xLyOXO4c4JdIWLqX/rVdRWpsjAlf1FRdjWfttVFs3puu/FFNEIchZ2aR/ZQ1ybh6GOfPRzTgXiKq0NjPwyUfoyqaQtnBpeLs29078Z59AdR9JGLga5kibipRWAoF+ZFN07NhwqO4jKLaNyOnz0ObeGd5eWVnJRx99REdHR8zA1eHxdSNFR0cH1dXVFBUVUVl57ndKW3IN/a/9BeX0qYjAVf2sOeT89FfIWTloSycnrV8zvgjLfd/BuOz6iHqU06cY2LU9mCJgyTnR3JI9n99Zt1HvaY8KXL3UXMprU+5nj6uRrX1H2O6op83Xi08oaCWZLF0ms4wTuDZjJsszypmSFh19U+9p5037QSpNxdwyGLyKGISv/jkxsPv7QghVjC5UMbD7+8K5Pkf46p+L2vt0a714oH7358AqxAP1uwX/Z714urU+ar9769vCsf4PQqijzKyqwrH+D8J6x3Lh3vp21G6/9WnhPfWASPk6B9xCVfpSIRbeUw8I1x6E3/p01N59+/aJzZs3C3WUz1dVVbF582bxyCOPiH379kXt73/3TdH3n7/6XK5z33/+SrTfuEj0v/tm1O7nunaJh1s2CjXJdVaFKuxKv2ga6BZnvHYxoPqTln+4ZaPIPvBt8VzXrvD2sH9VN+Me0KUR6BjZgDgZfNW/xV+/AV3lg4MZhSJxf/EMjBode3o7RpX3Z03VPNNaz/dKK7m/ODq2zrj8RtDrB5eFjx76N/8X7q1vY7rp9iDHMGgL7gfZSMC5J7UKZSOSJvkEsr/tZ/jbn0FX9L3BjEKRWLBgAVqtdsSOh2TYsWMHe/fuZcmSJSxYEB1LaLr+FiSDAV9DdAT1haB/059x/3Uz5ltXBzMKDcM94xaRJuuociVOqyYhkaUxUWrIpUifhWFYEOtw/Ma6jQ1du3mw4GruGbfo3I4ItXl7hepzjeS5kFC73sO/Fq4XisXArn8Qqrsjbkm73yucim+UWIX491OHRfqHL4j7ju4S7V53/LIup1A98fePjFgVrjdeFh133yz61v1OBOw98Yv67UJVnKPDK1ThO/Pvov/TdDHQeJ9Qfe1xS7rdbuH1ekeHVVXFhx9+KH784x+LN954QzgcjrhlA6N9nV99UVhvXyb6/uNJEeixxS3aq7iFKzAwOrRCFb9q3yqKDn1ffPf0q6LDF9lLSJggkoAXNPGDMuNBtdfjP7oh3BKlko5rKLxqAIOcmp9/KI647Dx95mi4JUolHddQCL8fSRd7riIRlNZmPFvfDrdEqaTjioDqhThLyBOauY+gdDwdbolSSccVcdyKglab+AkcCx0dHezduzfcEqWSjmsozvs6nz4VXDE72BKlko5rKLxCSdrixEK9p53nuneFW6JY6bjiC0lV8DdsgIAv9bx2rlaUpi34T20GNTDiBJEQXKH49JkGfGog5bx2pwdcvNbRxMaOUwSEOvIEkQCBAO6tb4OipJzXLtDViXfvxwx88hGo6nkkiASEgr/jaVB9qee1855Gsb2GYtsIIjDiBJEAqqqyd+/e8KK/VPLa2e12amtrqa6uRggx8gSRELzO725GKP6U89oFOq0M7N4RXE6uBkacIBKC99Vz3bvwq4GU89q1+nrYYj/Mm/aDqKgjSxA5FKGUxartCJKxIKVMq8i6UUtZXO20UWgwppRpVSfLo5ayWGk+FVyYl0KmVbSaUUtZrLqrFPi7jAAADFNJREFUkXSFKWVaRdKNWsri9vZ20tPTU8q0qtFoRi1lsdJ0Ejk7N6VMq2i0o5ay+IinjXxtRkqZVnWS5kJSFkdiLIn+WBL9sST6iZGSkMYwhjEkxshHXmMYQ2zEHLAkGz+pqroUWAyUAIVAAOiSJKkJ2C1J0u5RPs7PhVsaa5DG8HkilpCEEHOFEA8At0qSlDAvthCiDdgMPCPL8gVP+n1e3GNCGsOoI14rJIQoEUI8IEnSD8+nXiHEYwRv6q7zsP1cuceENIZRR5xWaJkQ4jVJkiKiU7u6uqitraW3txeXy4UkSZjNZnJycqisrCQnJ2d4Pe3AalmWP071eOJxd/R5+bRFwTYgc6ZPBWDaOJk8k+DyEh2mNN3weuJyjwlpDKOO4UJSVfVrkiS9MHRbTU0NH3zwQfjFZfEwYcIErrvuOmbMiJxKEULcLMvy28mOJRb3p80DPLvfT1VLdJrkEEw6iWum6vnGJTrKxkUGL8fiHhPSGEYdQ4WkquoXJEl6P/Td6XTy8ssv09zcPKI6p06dyl133RWRYEUIsTRRyzScu6df4dFtHnadii+g4dBp4OuXGLn/cgNazbml/8O5x4Q0hlFHSEiqqpYBtZIkmQHa2tp48cUXcThSSz4zHLm5uaxZsybc3RNCnJYkaY4kSVHZRAe5qyVJsgA0dfv4zn97aOkNDC+aEhaV6vj1F00Y9ZqY3HJC6zGM4cLwQEhEDofjgkQEwcSTzz33XDhbkSRJE4EHEnBbAGz9Ab61xX3eIgLY0+znh+95CKgiJveYkMbwuUAIcbEkST8Iff/LX/5yQSIKwWazsWnTpqGbfq6qakSug+HcP3zPTdugM+FC8FGjj2erBmJyX/CE7B0b2/5ZkliSvOQ5CMGuN1YX/wxg2pRL/lkgjcheQuw6duKzn4W/D+mT37HxTOwMGTHw+qriqNRJ06ZekrL9seOfpZ566X8ZBl3NABw+fDjmOqgJEyZw9mz8/IB6vR6TyUTvYHhWCA0NDRw/fpxp08KZoB4AHo7Fve24L6ZTYXqehmNd8Vsok04iM02i3RkpwOeqBrh1toH89HAb9ADwsLayYrEEPFJTu/sXcWsFKisW/yPwZE3t7ohBlSSxUpZ4WBWklKNLlshV4ddAUAgSK0E8rKoiNXtZymWo/TBIEsnXZCeEdIH2YwCQJCmce/nDDz+M2j937lxWr15NY2MjL7zwAn5/5M1eWFjImjVrMJlM/PSnP8Xn80Xs37ZtW1hIg1xhIQ3lfmqPJ4r7hhl6fn5DOlUtfh56y4VXifQTTB2n4Y+3W8hMk7nyaTtu/7n9XkWwbp+Hf1lmjuDWAo8Aj1dWLJZranf/PNZFqaxY/E/Avw1+jRKcKvx1r68qi1rieturZ8fJcqA2sqwyFyL98xqNqDtx4kCUfVlZ5TitVl87rOxcNTDabxUcw2hCVdVwEojm5ma6uqLnMBsbG3E4HEyePJmvf/3rEWLKz89n7dq1mM1mTpw4ESUigNbWVqxWayhxf6aqqgtkWa4ayl3drtBsj251DrUpdPerLCjR8dQt6RFimpSrYf3KDLKNEntP+yNEFML7x3w8cpUJg1Y6x01QGP8C/KyyYvFPhxsNbvvZYJmErdZwvLl6Qvfrd140XlY0V0tC6peEFJ0qJgGammq6T5zYP14I+WoJqV8iub0QzE30l8w+oKpzE/2N5Pj/F2Nx6EO8bK1Op5N169ZFiEmn05Gfn883v/lNzGYzp06d4qWXXopLMqzuxcP+s+90bDd3u1Nl7evOCDEZtBKTcjVsuDMoov1nFP5+iyumvcsrONoRIdDF2pra3QA/r6xYnAY8WlmxmJra3Y9BWESPAv8ar7VKhttfPTtTyIF3VJVvSDKvMEJv+6RJl86UUd8RSN8A8Uqy8q+vKq5OtH/lpsTvhm1sPJDQftrUEWRK/d+LcM60WK1RCDabjXXr1vHNb36TyZMns2bNGvLy8sIiev7556O6fEMxrO7pw/7TaIs/BjplC7D2dSfr77CwoETHutstlOZowiJ68E1nVJdvKBq6FOYVhV0M08Ofamp3P1ZZsRiCYgqllAyJ6LG4NSbAqk2nywME3lFV1ryx+qIdd2xqHZH99EmXlKuyeEdI0poTJz7bMXXqJTHLpeJgiOVYCCEVB8PxE/vHHAspQpKk8II0lyv2Uz0Em83G+vXrWbt2LaWlpQApiShG3YXDuW39iT11Q8UUEkUqIgKwuyP2F0Z47QbF1M+5Ltw/1tTuTvwynjhY+XrLrIAqvS0GRQQgVLE41dXB06dfPEsN8DaDIgLQKPJiNNFPmTEHw//fkCQpwvMqy6MzKxM4j1gDzXkOvy/4iAf0hiWv31ka5SgQqrx0qIgA3lhd0hhVDu2ShoZoR4OqykuHigigoakqyn4Mf3sQQoTfYJ2enjjnRl5eHmvXrsVisdDc3IzT6aS0tJQ1a9agS5IgZVjd1uHc48yJb+/SbA3r77AwziyHHRDzirSsu90SciTERbYpYr81okUaMib6x8FNv6isWGxO1LUzeL33r3ytrYghYw9VVatev7P46YRHMghJBO6fOuWSoshtVB0/8VlK9mP4m8Sx0Ie8vPhLfvLy8rjvvvuwWCzh7lxWVhb33XdfWEwbNmyI6bWLUfexYf+ZlBM/E1Vptobn7gyKKNSdK8qUw928dbdbePBNZ0yvXch+KHdYssMcC08Odun+leCYKcqbF4Ik8RWEqEewD8E+wCNL0nVxzyCqAvEVCaleIO8TyPskJI+QR2A/hr9FhFeWTp06NWaBkGCGisjv99PV1cWf/vSncMv0ta99LW5Xb1jdu4f9Z+HE2C3aeIscJSKvIsJjplDL9PsvpSPHaJgMWon5RRFt0G65smJxaJ4oyrEw+Dkkpn8adEZEQaBseW1V8cbXVhVvVFV1Z8xCCSBr1S0nT1ZtPHmyaqOKGLH9GP62IEnS9tDnkpKSqDVFAEVFRVgsFhobG6McCyExORwOSktLY+beKyoqGvqS6T5ZlquGc8+ZoKUoM1qEMwu0jDPLVLX4oxwLITF1ulTmFelIi9HFu2GGfmjXr0+W5SotwW7cvwH/HMvFPeiAGBgsozLCuaS/NaQyl5QIY3NJySFJUkBV1b2SJC2UZZlly5YNj4+jrq6O3/zmN3HXI3V1dfHLX/4So9EYs2s3NPG/EOKFmNwSPLTIxD/9NdJzuP2kj1tf7ONUHPf4KVuAL27oIzNNiura6TRw74Jzr3cJcWuBJwEShQjV1O7+eWXFYjVUdjgCssHwdxuaDAADGlnHYIRsaNtQuGSDQaMOd0tqDUVFiw0AsvDoxKAHJ7QtEn5DMEfF+SHZPFMyJJtnGkMYDwOfQDAcaM+ePbS1tUUUSLaoz+/3x3SBT5o0afhCv2fica+Yruflg1qOWJWIAvFEFIJXEXS6osdHX5mXRklWxPjoGQDtYOxc0lYmgdDOatXAzgHzYPOrCoTE6wADZm3Uq/G0agAkaWt4g5DOBpTATpMxeGIimIzmdQCTcSDKPqAAEluHbx/D3xZkWd47tFVavXo1zzzzTNIXQydDRkYGq1evDn8XQjwuy/KxoWUiuCX47c3p3PWyg+4k80rJsKBEx0OLIhYWhrn/RyzsW7mpbU6yMolaosmTL05qH6sl+p9w7T4PDFnYNweokiRJD8GFfS+88EI4+eRIkZWVxb333kt+fjCdtBDCBsyRZblteNlB7k8lSTIAHO1Q+PZbrvMW07wiLb+92UK2UYrJ/T9CSGP428Kwpea3S5L0euh7b28vL730UsLlE7FQWlrKV77ylYhk/UKIFbIsx+2dDOfu7lf5/jsuDrUp8Uxi4o5KAz+82oxuSI9uOPeYkMYw6kgl+clnn33Gjh076OnpSVhXXl4ey5cvj3gTIIAQYpUsy5vimCXk3n7Sx/pPBzjakVhQV03W8+AVRqbnRc5HxeIeE9IYRh2x0nGpqnoDsEWSIt+r0tbWxtGjR3E6nXR2diLLMuPGjSMzM5NZs2YNdXEDIITolSRppSRJH6R6PPG4z/YpfHI6QH3nOUEZdRKzCzRcVqIl2xQloLjcY0Iaw6gjXoJIVVXLCeZSOK+3fgshnpAk6RlJkkb82sHPm3tMSGMYdaSQ73shwSXad0iSlOxNcH1CiNcHb+IDF3psnxf3mJDGMOoYyYvHBle0Lia4DKJckqSAEOIY0AbslmV5e8IKLgCjyf1/AfzC3EiJBPQWAAAAAElFTkSuQmCC) no-repeat}.layui-layer-dialog .layui-layer-ico,.layui-layer-setwin a,.layui-layer-btn a{vertical-align:top}.layui-layer-move{cursor:move;opacity:0;width:100%;height:100%;filter:alpha(opacity=0);z-index:2147483647;background-color:#fff;top:0;left:0}.layui-layer-resize{cursor:se-resize;width:15px;height:15px;position:absolute;bottom:0;right:0}.layui-layer{border-radius:2px;animation-duration:.3s;animation-fill-mode:both}@keyframes bounceIn{0%{opacity:0;transform:scale(.5)}to{opacity:1;transform:scale(1)}}.layer-anim{animation-name:bounceIn}@keyframes zoomInDown{0%{opacity:0;animation-timing-function:ease-in-out;transform:scale(.1)translateY(-2000px)}60%{opacity:1;animation-timing-function:ease-out;transform:scale(.475)translateY(60px)}}.layer-anim-01{animation-name:zoomInDown}@keyframes fadeInUpBig{0%{opacity:0;transform:translateY(2000px)}to{opacity:1;transform:translateY(0)}}.layer-anim-02{animation-name:fadeInUpBig}@keyframes zoomInLeft{0%{opacity:0;animation-timing-function:ease-in-out;transform:scale(.1)translate(-2000px)}60%{opacity:1;animation-timing-function:ease-out;transform:scale(.475)translate(48px)}}.layer-anim-03{animation-name:zoomInLeft}@keyframes rollIn{0%{opacity:0;transform:translate(-100%)rotate(-120deg)}to{opacity:1;transform:translate(0)rotate(0)}}.layer-anim-04{animation-name:rollIn}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.layer-anim-05{animation-name:fadeIn}@keyframes shake{0%,to{transform:translate(0)}10%,30%,50%,70%,90%{transform:translate(-10px)}20%,40%,60%,80%{transform:translate(10px)}}.layer-anim-06{animation-name:shake}@keyframes bounceOut{to{opacity:0;transform:scale(.7)}30%{transform:scale(1.05)}0%{transform:scale(1)}}.layer-anim-close{animation-name:bounceOut;animation-duration:.2s}.layui-layer-title{color:#333;text-overflow:ellipsis;white-space:nowrap;background-color:#f8f8f8;border-bottom:1px solid #eee;border-radius:2px 2px 0 0;height:42px;padding:0 80px 0 20px;font-size:14px;line-height:42px;overflow:hidden}.layui-layer-setwin{font-size:0;line-height:initial;top:15px}.layui-layer-setwin a{_overflow:hidden;width:16px;height:16px;margin-left:10px;font-size:12px;position:relative}.layui-layer-setwin .layui-layer-min cite{cursor:pointer;_overflow:hidden;background-color:#2e2d3c;width:14px;height:2px;margin-top:-1px;position:absolute;top:50%;left:0}.layui-layer-setwin .layui-layer-min:hover cite{background-color:#2d93ca}.layui-layer-setwin .layui-layer-max{background-position:-32px -40px}.layui-layer-setwin .layui-layer-max:hover{background-position:-16px -40px}.layui-layer-setwin .layui-layer-maxmin{background-position:-65px -40px}.layui-layer-setwin .layui-layer-maxmin:hover{background-position:-49px -40px}.layui-layer-setwin .layui-layer-close1{cursor:pointer;background-position:0 -40px}.layui-layer-setwin .layui-layer-close1:hover{opacity:.7}.layui-layer-setwin .layui-layer-close2{_display:none}.layui-layer-setwin .layui-layer-close2:hover{background-position:-180px -31px}.layui-layer-btn{text-align:right;pointer-events:auto;-webkit-user-select:none;user-select:none;padding:0 10px 12px}.layui-layer-btn a{color:#333;cursor:pointer;background-color:#f1f1f1;border:1px solid #dedede;border-radius:2px;height:28px;margin:0 6px;padding:0 15px;font-weight:400;line-height:28px;text-decoration:none}.layui-layer-btn a:hover{opacity:.9;text-decoration:none}.layui-layer-btn a:active{opacity:.8}.layui-layer-btn .layui-layer-btn0{color:#fff;background-color:#2e8ded;border-color:#4898d5}.layui-layer-btn-l{text-align:left}.layui-layer-btn-c{text-align:center}.layui-layer-dialog{min-width:260px}.layui-layer-dialog .layui-layer-content{word-break:break-all;padding:20px;font-size:14px;line-height:24px;position:relative;overflow:hidden auto}.layui-layer-dialog .layui-layer-content .layui-layer-ico{width:30px;height:30px;position:absolute;top:16px;left:15px}.layui-layer-ico1{background-position:-30px 0}.layui-layer-ico2{background-position:-60px 0}.layui-layer-ico3{background-position:-90px 0}.layui-layer-ico4{background-position:-120px 0}.layui-layer-ico5{background-position:-150px 0}.layui-layer-ico6{background-position:-180px 0}.layui-layer-rim{box-shadow:none;border:6px solid #0000004d;border-radius:5px}.layui-layer-msg{min-width:180px;box-shadow:none;border:1px solid #d3d4d3}.layui-layer-hui{min-width:100px;filter:alpha(opacity=60);color:#fff;background-color:#0009;border:none}.layui-layer-hui .layui-layer-content{text-align:center;padding:12px 25px}.layui-layer-dialog .layui-layer-padding{text-align:left;padding:20px 20px 20px 55px}.layui-layer-page .layui-layer-content{position:relative;overflow:auto}.layui-layer-page .layui-layer-btn,.layui-layer-iframe .layui-layer-btn{padding-top:10px}.layui-layer-nobg{background:0 0}.layui-layer-iframe iframe{width:100%;display:block}.layui-layer-loading{box-shadow:none;background:0 0;border:none;border-radius:100%}.layui-layer-loading .layui-layer-content{background:url(data:image/gif;base64,R0lGODlhPAAYALMPAPPJp/HYxfSwkf50Df5TD/+HAPe5bvqHR/twOviZavyrMu/m3f9EAP9mAP+ZAO7u7iH/C05FVFNDQVBFMi4wAwEAAAAh/wtYTVAgRGF0YVhNUDw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wUmlnaHRzPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvcmlnaHRzLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcFJpZ2h0czpNYXJrZWQ9IkZhbHNlIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6QUNDRERBRjMxRTIwNjgxMTg4QzZERjVGQ0I0MDI1NDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NTlDMEI5MzU5RTY2MTFFMTkxRDY5NkVCMDBGOTBFRUYiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NTlDMEI5MzQ5RTY2MTFFMTkxRDY5NkVCMDBGOTBFRUYiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpENzU2N0YwMTMyMjA2ODExODhDNkRGNUZDQjQwMjU0OSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpBQ0NEREFGMzFFMjA2ODExODhDNkRGNUZDQjQwMjU0OSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PgH//v38+/r5+Pf29fTz8vHw7+7t7Ovq6ejn5uXk4+Lh4N/e3dzb2tnY19bV1NPS0dDPzs3My8rJyMfGxcTDwsHAv769vLu6ubi3trW0s7KxsK+urayrqqmop6alpKOioaCfnp2cm5qZmJeWlZSTkpGQj46NjIuKiYiHhoWEg4KBgH9+fXx7enl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmFgX15dXFtaWVhXVlVUU1JRUE9OTUxLSklIR0ZFRENCQUA/Pj08Ozo5ODc2NTQzMjEwLy4tLCsqKSgnJiUkIyIhIB8eHRwbGhkYFxYVFBMSERAPDg0MCwoJCAcGBQQDAgEAACH5BAkFAA8ALAAAAAA8ABgAAATy8MlJq7046827/2AWCEdpCkGodkHSvHCcLGttLUOsw0NqW4EDYog4+CyInbJxwAQMiqjCcMQkGNgsNmFxLZXcisFBLpMNGYF2zRBQAt9vFWCuOwCXAJt9FMSXbhILdnY0FQd7a00SB39Kiw9jhGZoFQSJWgQTjksTCpNmChaYa5ucOxOgdaOkWaanMamqZRaXrZoSObAvA56zDqKHrQyQjbtME5KqlW/DRwbHDcwBv1UTV5hhEnDHVcqEzF2J2te75BLflBsCCFoIgRU4pwOGFQCfZQp4PxUBSX8IrPELscDLkhkDB7bQxQthwocQI0rMEAEAIfkECQUADwAsAAAAADwAGAAABOjwyUmrvTjrzbv/ILcAiWCUQKh+y9G88DsIa40JcQ4ji30JCIZQeAhcAgOdMvEYKZ5PQA8UHFoZtEpSuVQ4vmCHwuhJXK+EqcTA5YbfYrUmcD4jKNt2rAB/GzoHdWdTAXo6fXByGASCV1k4hjADiG8pG41XTA9skS98lGB/l5hDmgmdnqChHIykWBKQnZOqDpYaVaRkAKgvtA6KFwCudxO8DZ+UCh64grYPp7yqwBgLzFYEWRNIvAlecGMhAq1DCGQVu7JNBn7THQDvAObBeVwJ7T4gLW2i+P0PAQkOCBQoxZ/BgwgTYogAACH5BAkFAA8ALAAAAAA8ABgAAATV8MlJq7046827/2AojmMgHE2aIslCckAcb8Kg3umQvFhCMEAgYXcR4HCFpEJhCPAkiKAUiLAEbMdGwcHtcg28w3R8qKCyA6/aASABxvC2JJBNrdcuURQ+rUqMWVt3XmAhC3xweWdHg2oKIm+IU3KLOI1qkJKTEpU3l14iAZpSTg+dKp9cjyKjQRMGdYKXhSFio2VzdQ2ppSEBP5IEvQ9YR7J3tCICmgIVALrHhE+/cMIXCboDxwpy0z5BQ8POxUcD3U88CwnkKQni6E8yMXnw9fb3+BURACH5BAkFAA8ALAAAAAA8ABgAAATe8MlJq7046827/2AojqQUCEeaJkC5CUgcJ0G2JE2u60jrVgICY0gcIhaXQ25QaDp1vt8jUKwaLbhGwcHtcgeNQe23EFqrAkogt/W6cwlp4mwlICW4gXvvyI1LZnRFaRJgfHtghCRUglUIJjmHbgUNBy4AjY4SAGGSXpSWJZiZRI8PnHqeXKAujKQMpmsNqquVP4GZioa0iT8Hrwx/Sqmefj8BuHRxE5xanmChPwKZRxVKznxgDX8/AAh00RQL18S1YYpSDzffpVEXAjvxB9zpiwnaOQfo9fz9/v8ApUQAACH5BAkFAA8ALAAAAAA8ABgAAATO8MlJq7046827/2AojmS1AIkhJEBQasFBMDRDHC62JE3v97iXJVEr1hKThUHBdBQGv99gIZwIjFiG4AFweL/eQtSHqD4Cs2yR0AW7xeOGoXpQYwvuPHQ8FabtNA15g3ENWyUBgEUDg3mFBy8AijV4jWB7UZAlkpMMlZZemD+aJImdjKBej0KdDIKpDoWHJXWdn42iPzmIrQagcGOkL0SKSG15wFF9ZgJ/WLMBvpeFCLtmMVhBOjxxB1RmJgDiAN8bAQkH6Qcs4O3u7/Dx4BEAIfkECQUADwAsAAAAADwAGAAABMrwyUmrvTjrzbv/YCiOV5AgKCosZIsFCCPPc5KZQ6M3QxK4lgWBRuMVFEgDS5LYOXc24CRGlBUc2CwW8BA8vw2BtFuVDbRoByAHdg6WrmG1kU63v9EWoMy41rVsdzoDQAllBH9oBYJOPy2GVYiJWYuMOlyPh5OUlpcuApqbDpWWjiQBfH6TgYxSVER0m50NeS2gZap1BrOmLpBVuVpcTYy1QLdzuQamAqxuYmMTMGUJcBYmeL3REgAJ3gkC2hgLAOUA1tvp6uvs7R8RACH5BAkFAA8ALAAAAAA8ABgAAATs8MlJq7046827/2AojpmQICgqBGSLCQgjz3OyZEFy7LvgXoDZoEAsDBoyxGWRaDifzgHgNwkQGA2HdqstyBKWA3TsZFEPDAJ37RjIzBIBmTy4uQKyAnuNPEwWR3NjPi4CWHtrXgQTcoJjAz8xA4hrbxJijmNwIzF6lFtXUw+YmU+iJJ2foAyipKUNp5wMk6oOV2aupZsihlmqioyvUT94DJ6USEoSAcINYJGHlF6sFE2lA7sjxQ3HXNN+FAu5ZIRUAlcEtF1IDAh2FQaC2FQUATE0NOVACYENAyv0LMCgQeBAtoAIEypcyLDhhAgAIfkECQUADwAsAAAAADwAGAAABODwyUmrvTjrzbv/YCiOWiAgTJoiSUC+V0KodEokZWIIugtPAYNCUSgMZjWV4BI4NJ7QxsH3Mjiu2OsgqaJKFoOoeLB4AbJoR4PLQFQQ4rh7tEin2QyqIM5fiqx2WVtcfg9hfGMjCoFZBWxzC4h8Xh6MaI8SAJJxACKWWZgPmptRnSGLn45ccwGkUWUhgJaDSTgSh6QDIwGfDkhJeq4Ntn+zbAQUYLmwI7Joa2ymQLhxA5QiAKhXBdBcBxcLcHEHzD+Zv0kEhRcACQfvCdLmFAEytdfzZgD7AOX5/wADCrwQAQAh+QQJBQAPACwAAAAAPAAYAAAE6fDJSau9OOvNu/8gFiQkKSxh+gkE474uEqj0tSBw/gq1twCKYBCAOuiOhJllkRg0no0BDxRQOK5YhyJx7BIsAKj4ifgsrNlsodtNUBbOsdjgMaTvA7buOznI5UoaC3eELXowSgF/cm4bAIR3DYcwUwKLYwMcdpBZkpMujQaXY5qcnZ+gEgmjYhyPpleen42WrFEcg7AOqC4AEoq2Bx1onGu8FAi2vriwvAzCE2Gj0B1VhApGqEkVAnFyByhmm1gGKDifUxULfmN0PerZbATLGD8A9+8ZLF0y+f4PAhAIFJgg0L+DCBMqrBABACH5BAkFAA8ALAAAAAA8ABgAAATz8MlJq7046827/2AWCEhpCkGodkHCvHCcpGtdBUSswwRt3waFUGHwVRC7JAOBaR2eh9nG4KhaqwaLS5lMaBvgMNiLAVzPDgAlwOUaBeJ4Q3BZoNGLiaCtpEsWcnJGElR3V1kSSHw7TBIJgXFkFAqGVwoTi0oTA5BiAxaVZ5iZOxOdcaChVqOkMaanYRaUqpcSOa0vBJuwDZ8VhaGID4q4jQ+PsJITAaoOPlu4kgG8g4TBa7gvRsiQyr+GwhMHuAcWBpDhFwCzVQpqFgu3iwR5FgAHYgfvP2vEXAj7+K1YMI7LgXoCf7SQx4CAlIQQI0qcmCECACH5BAkFAA8ALAAAAAA8ABgAAATe8MlJq7046827/2AXACQZnuCSEEzrEkmAzpjAunhLCDRmKI5g0LC4BHJIl+wRSByeh1hKSBUWKYtbMkdQNb7gb+IDrFIVlcR2iwi7GzwOwGwGTI7r5Pu91NDpEwJ5SAR7bmMaC39mVweDOYZuAxsBi1VLCI84kW6UllSYmi6cYZ6fQaGiDKRfkxunQRNqqoWkiBoGpwaBqi2sfRmKn1cPWb21hreUZWYKwA+zogkGhrsguVVEF5maCBIAB2EHdicLAecBxBYL3HkI6j0zgmvK8T1NWgwwz/b9/v8AMUQAACH5BAkFAA8ALAAAAAA8ABgAAATY8MlJq7046827/+AXACQQhCi4CATjvoSwpFww2nMWtG/vEieaZQFwGI8ORbCy4Pl6hMZhmlimFMiswypBPKGNsDicoBm02pxE8IWN3w0BaoHWGijOLxzO5RTrWRMBbT97b2UgWIBIQQmEDIZvAyGKi0aNj5FvlJZHmISaYyFnnVtrmaENkyCknRMAj1GhiB90nXcTeU+pfRytgGoPjoSyhrQqlWgATLo+xW+4NH9ZCsGCzT5vB8tCEkQG4Aa9EwtehAjW3TQLCdgJ6erdAgn0CXLx+Pn6+xwRACH5BAkFAA8ALAAAAAA8ABgAAATM8MlJq7046827/2AojuSzCAeiqsJSaoChzIoBaALB7DyfvJaAwkEsEhWBS6LHIDQaTt0PKFkMjViFiwLoOZ/gJ0FAfVyx2Ypu9w2HB9QAeu5ISgQ9t75BfhnoaAYTCDxte08HQIBzE2sMh25wL4toE3mQYYqURpY8mJmTm0WdO59gQH+ighKEbKaJLwCiDjd3l5h9L2eAChQBjoZ7di9ylMMSS4WHBFNUVnRaSkx6DM1lDwC7CgBbFwCOTAy51yQLCeDM3eTr7O3u7yMRACH5BAkFAA8ALAAAAAA8ABgAAATO8MlJq7046827/2AojiQVCEmaCEGpLYbizI5iLFlwMHzPHy1XJSCjGRVBU6JBYDZ8PEJSuCgaj7jJssHtMqGIrKt6LdeyAq/660sIHwaz2fBYrNdQxnRkldMUD1t3XQRQAi4BfmYBA4NqYIiKZQGOjz4IkZJGlJVdkCULmkYLjZ1Pl0KiNIGmhT6HLnGidJyVeXsioaJZgneuPZhviZJicIO/UcVjfVgWAAeWl7hjskY3GQsIeTxhbxcLAeEByhjPCOcIBwDe7O3u7/DxEhEAIfkECQUADwAsAAAAADwAGAAABMHwyUmrvTjrzbv/YCiOWGACgbcALEpygeHMtGNoAsLsPCG8GEBt6FAAEofkIRE48J68A7ASIBILjay2QYB6f9PHQmElDrbarveZmsrKRLR2/USE4dazXE1ntElVeENYcg19O2CAgoOFhocJQIGLM4RyhwyQL5KTlWiXiSSTNHpofHR/JG+TjZcEYQuinWmfYQ9CgrJZpmtStQ+bRAYAB1sHAAKHoLUxVjcZOV4+vhYxBtYACx0CCdwJytPg4eLj5B8RACH5BAkFAA8ALAAAAAA8ABgAAATI8MlJq7046827/2AojtgCGKgBLGSbLYojz7LClgnC7AyS3C7KgkacJQKWAIHHZBCQQQmsSG0khMsmkwB0GajUQkMg1WmbiO5oCKYOGsjE+Xz1tt3WR3a+DX7vRGIDAXxnUCQxgDRiDQCFWgAuiYoyjI6PTJEtk5SChJg8hyMAlDNvV3uPBEFspQ1wD3KgdZKlYnULqXNcUa2ADQYUn3xPURIBnFQHmkJmWmnGE1NUNi+yTD/RFQEGCt4KBqIvAOQr2ufo6errIhEAIfkECQUADwAsAAAAADwAGAAABLTwyUmrvTjrzbv/YCiO2QIYSqoYxkK+GODMdG1owoHsyCHAF0NtSLtZAgiGcqlEBIATGXEKqCwIzCyD4IIqplNFJanNIqBSMLEqAZTL7NdXTRRLDm/tAUgHT7B5TAR8fUQTgVqEhTWHiEyKizN/jkqDMHORdg9kjmcwaYtxApQMPzALkTNdEpx5nkBCi0YTAYgEcbCFsxQLrUwIq1APAXS4Fjm/AsHCDwsoQy3M0tPU1dbXGBEAIfkECQUADwAsAAAAADwAGAAABNbwyUmrvTjrzbv/4BWMZGiCC+CsrKMYyyljQWuvSqAFQA/os0kgcSjejoqYRYBgOJ8EQDDRqDYKR6Ql8ew+EzKBtTHIZqUTgHfNQKPGDXNWQWmyuwgTdSzPAgN3a0AfA2NYfTcGEgKBXgIhcIeILYoPXI1fkIaTNpWXmAxgIIVWZZwsaIygDI8ge1Wmpw5/qwyDHgFwsisUBKAEJ69XspUSapitJgabk0kVB40HQQAHsJy3ab5eUUEWCwpyzhkCCeUJbt0VCwZZMOnvxgbyBujw9vf4+RcRACH5BAUFAA8ALAAAAAA8ABgAAATi8MlJq7046827/2AWjGRogovirKyjAMspY0Zrr0owe0Fy/IeE7gG4GRWxS4LAaDIIiaQn0ahaq0KjVmEJMJ1gwpAjuJob2jSgggC7GYjO4nwdpLcUwfst4FDpVQV3WlJfe04EHAOAVYNaaw8Bh29jGIyNjjcGEgCTbpAZl2iZNptEnmCgGIuMpKUSkqhNlUqXgq4roIaTiRsBtrgrUgmyCR1/dHa4XBMLu28EUhsGgAYqrrReh2IgAAdXB2sLrqYWS24H0juR13flFwsA8jDr8DVq9fkB9ywKBur5AgocSPBCBAA7) no-repeat;width:60px;height:24px}.layui-layer-loading .layui-layer-loading1{background:url(data:image/gif;base64,R0lGODlhJQAlAJECAL3L2AYrTv///wAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFCgACACwAAAAAJQAlAAACi5SPqcvtDyGYIFpF690i8xUw3qJBwUlSadmcLqYmGQu6KDIeM13beGzYWWy3DlB4IYaMk+Dso2RWkFCfLPcRvFbZxFLUDTt21BW56TyjRep1e20+i+eYMR145W2eefj+6VFmgTQi+ECVY8iGxcg35phGo/iDFwlTyXWphwlm1imGRdcnuqhHeop6UAAAIfkEBQoAAgAsEAACAAQACwAAAgWMj6nLXAAh+QQFCgACACwVAAUACgALAAACFZQvgRi92dyJcVJlLobUdi8x4bIhBQAh+QQFCgACACwXABEADAADAAACBYyPqcsFACH5BAUKAAIALBUAFQAKAAsAAAITlGKZwWoMHYxqtmplxlNT7ixGAQAh+QQFCgACACwQABgABAALAAACBYyPqctcACH5BAUKAAIALAUAFQAKAAsAAAIVlC+BGL3Z3IlxUmUuhtR2LzHhsiEFACH5BAUKAAIALAEAEQAMAAMAAAIFjI+pywUAIfkEBQoAAgAsBQAFAAoACwAAAhOUYJnAagwdjGq2amXGU1PuLEYBACH5BAUKAAIALBAAAgAEAAsAAAIFhI+py1wAIfkEBQoAAgAsFQAFAAoACwAAAhWUL4AIvdnciXFSZS6G1HYvMeGyIQUAIfkEBQoAAgAsFwARAAwAAwAAAgWEj6nLBQAh+QQFCgACACwVABUACgALAAACE5RgmcBqDB2MarZqZcZTU+4sRgEAIfkEBQoAAgAsEAAYAAQACwAAAgWEj6nLXAAh+QQFCgACACwFABUACgALAAACFZQvgAi92dyJcVJlLobUdi8x4bIhBQAh+QQFCgACACwBABEADAADAAACBYSPqcsFADs=) no-repeat;width:37px;height:37px}.layui-layer-loading .layui-layer-loading2,.layui-layer-ico16{background:url(data:image/gif;base64,R0lGODlhIAAgALMAAP///7Ozs/v7+9bW1uHh4fLy8rq6uoGBgTQ0NAEBARsbG8TExJeXl/39/VRUVAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFBQAAACwAAAAAIAAgAAAE5xDISSlLrOrNp0pKNRCdFhxVolJLEJQUoSgOpSYT4RowNSsvyW1icA16k8MMMRkCBjskBTFDAZyuAEkqCfxIQ2hgQRFvAQEEIjNxVDW6XNE4YagRjuBCwe60smQUDnd4Rz1ZAQZnFAGDd0hihh12CEE9kjAEVlycXIg7BAsMB6SlnJ87paqbSKiKoqusnbMdmDC2tXQlkUhziYtyWTxIfy6BE8WJt5YEvpJivxNaGmLHT0VnOgGYf0dZXS7APdpB309RnHOG5gDqXGLDaC457D1zZ/V/nmOM82XiHQjYKhKP1oZmADdEAAAh+QQFBQAAACwAAAAAGAAXAAAEchDISasKNeuJFKoHs4mUYlJIkmjIV54Soypsa0wmLSnqoTEtBw52mG0AjhYpBxioEqRNy8V0qFzNw+GGwlJki4lBqx1IBgjMkRIghwjrzcDti2/Gh7D9qN774wQGAYOEfwCChIV/gYmDho+QkZKTR3p7EQAh+QQFBQAAACwBAAAAHQAOAAAEchDISWdANesNHHJZwE2DUSEo5SjKKB2HOKGYFLD1CB/DnEoIlkti2PlyuKGEATMBaAACSyGbEDYD4zN1YIEmh0SCQQgYehNmTNNaKsQJXmBuuEYPi9ECAU/UFnNzeUp9VBQEBoFOLmFxWHNoQw6RWEocEQAh+QQFBQAAACwHAAAAGQARAAAEaRDICdZZNOvNDsvfBhBDdpwZgohBgE3nQaki0AYEjEqOGmqDlkEnAzBUjhrA0CoBYhLVSkm4SaAAWkahCFAWTU0A4RxzFWJnzXFWJJWb9pTihRu5dvghl+/7NQmBggo/fYKHCX8AiAmEEQAh+QQFBQAAACwOAAAAEgAYAAAEZXCwAaq9ODAMDOUAI17McYDhWA3mCYpb1RooXBktmsbt944BU6zCQCBQiwPB4jAihiCK86irTB20qvWp7Xq/FYV4TNWNz4oqWoEIgL0HX/eQSLi69boCikTkE2VVDAp5d1p0CW4RACH5BAUFAAAALA4AAAASAB4AAASAkBgCqr3YBIMXvkEIMsxXhcFFpiZqBaTXisBClibgAnd+ijYGq2I4HAamwXBgNHJ8BEbzgPNNjz7LwpnFDLvgLGJMdnw/5DRCrHaE3xbKm6FQwOt1xDnpwCvcJgcJMgEIeCYOCQlrF4YmBIoJVV2CCXZvCooHbwGRcAiKcmFUJhEAIfkEBQUAAAAsDwABABEAHwAABHsQyAkGoRivELInnOFlBjeM1BCiFBdcbMUtKQdTN0CUJru5NJQrYMh5VIFTTKJcOj2HqJQRhEqvqGuU+uw6AwgEwxkOO55lxIihoDjKY8pBoThPxmpAYi+hKzoeewkTdHkZghMIdCOIhIuHfBMOjxiNLR4KCW1ODAlxSxEAIfkEBQUAAAAsCAAOABgAEgAABGwQyEkrCDgbYvvMoOF5ILaNaIoGKroch9hacD3MFMHUBzMHiBtgwJMBFolDB4GoGGBCACKRcAAUWAmzOWJQExysQsJgWj0KqvKalTiYPhp1LBFTtp10Is6mT5gdVFx1bRN8FTsVCAqDOB9+KhEAIfkEBQUAAAAsAgASAB0ADgAABHgQyEmrBePS4bQdQZBdR5IcHmWEgUFQgWKaKbWwwSIhc4LonsXhBSCsQoOSScGQDJiWwOHQnAxWBIYJNXEoFCiEWDI9jCzESey7GwMM5doEwW4jJoypQQ743u1WcTV0CgFzbhJ5XClfHYd/EwZnHoYVDgiOfHKQNREAIfkEBQUAAAAsAAAPABkAEQAABGeQqUQruDjrW3vaYCZ5X2ie6EkcKaooTAsi7ytnTq046BBsNcTvItz4AotMwKZBIC6H6CVAJaCcT0CUBTgaTg5nTCu9GKiDEMPJg5YBBOpwlnVzLwtqyKnZagZWahoMB2M3GgsHSRsRACH5BAUFAAAALAEACAARABgAAARcMKR0gL34npkUyyCAcAmyhBijkGi2UW02VHFt33iu7yiDIDaD4/erEYGDlu/nuBAOJ9Dvc2EcDgFAYIuaXS3bbOh6MIC5IAP5Eh5fk2exC4tpgwZyiyFgvhEMBBEAIfkEBQUAAAAsAAACAA4AHQAABHMQyAnYoViSlFDGXBJ808Ep5KRwV8qEg+pRCOeoioKMwJK0Ekcu54h9AoghKgXIMZgAApQZcCCu2Ax2O6NUud2pmJcyHA4L0uDM/ljYDCnGfGakJQE5YH0wUBYBAUYfBIFkHwaBgxkDgX5lgXpHAXcpBIsRADs=) no-repeat;width:32px;height:32px}.layui-layer-tips{box-shadow:none;background:0 0;border:none}.layui-layer-tips .layui-layer-content{_float:left;color:#fff;background-color:#000;border-radius:2px;min-width:12px;padding:5px 10px;font-size:12px;line-height:22px;position:relative;box-shadow:1px 1px 3px #0003}.layui-layer-tips .layui-layer-close{top:-1px;right:-2px}.layui-layer-tips i.layui-layer-TipsT,.layui-layer-tips i.layui-layer-TipsB{border-right-style:solid;border-right-color:#000;left:5px}.layui-layer-tips i.layui-layer-TipsT{bottom:-8px}.layui-layer-tips i.layui-layer-TipsB{top:-8px}.layui-layer-tips i.layui-layer-TipsR,.layui-layer-tips i.layui-layer-TipsL{border-bottom-style:solid;border-bottom-color:#000;top:1px}.layui-layer-tips i.layui-layer-TipsR{left:-8px}.layui-layer-tips i.layui-layer-TipsL{right:-8px}.layui-layer-lan[type=dialog]{min-width:280px}.layui-layer-lan .layui-layer-title{color:#fff;background:#4476a7;border:none}.layui-layer-lan .layui-layer-btn{text-align:right;border-top:1px solid #e9e7e7;padding:10px}.layui-layer-lan .layui-layer-btn a{background:#bbb5b5;border:none}.layui-layer-lan .layui-layer-btn .layui-layer-btn1{background:#c9c5c5}.layui-layer-molv .layui-layer-title{color:#fff;background:#009f95;border:none}.layui-layer-molv .layui-layer-btn a{background:#009f95}.layui-layer-molv .layui-layer-btn .layui-layer-btn1{background:#92b8b1}.layui-layer-iconext{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALQAAABkCAYAAAAv8xodAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAK6wAACusBgosNWgAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNS8xMS8xNNdPc0oAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAWb0lEQVR4nO2de2wU172Av5l92t4Hti/YQHmVmFcSE7+WR0h0uRRVVC2pKldpCFJQEJAiFwg4diikgLEghEhpVLVQNblBSFEUVYoi+hc0FQoE87B52JQAiQnBJDY2wfix633O7P1jdwbbxe9d7yx3PgnJc87iM2M+js+cx+8nhMNhdHQeFcRE34COTizRhdZ5pNCF1nmk0IXWeaQwPqxQEIRYtpEKOABL9NoLtAKhWDbSDRF4EsgAbEBHtL3LAIN9CV6+fHlSvS0fOXLkof9oj8pzDJZ499A2YCxgWbJkiSUnJ8cApADZgCEO7ZmAImAyYFuxYsV4Iv+ZpgIFcWhPR2PEU2gbkAlQU1Mz67PPPvvvq1ev/s/f/va3LCIyp8e4PROQD4wrLi7Oam9vP/Dhhx/+9f79+3/Kz8+3AxPi0KaOxoiX0KrM169ff6KgoGAKgMFgEF944YWZ0c9Y+vrLw8BEpAceV1xcnHX48OE3HQ7HRIAxY8ZM2bdv39Lo5zJi2KaOBomH0D1knjFjxsTulX6/Pxj9Uo5Re4rMY1esWDH+8OHDb6akpGR2/4Db7fZEv4zXuF1HI8Ra6H5lDgQC0ttvv/1l9NIfg/Z6yPzee+/t6S1zY2PjpdWrV/8retkagzZ1NEwshR5Q5j179pzfu3dvOxBg5HINKHNTU1Ody+WqbG1tDQENQOcI29TROA+dthsGGYAd+pd5165d94nIfHeE7Q1K5qKioorvv/8+ANwGakfYZlwIh8MEg0F1OtFoNGIwxGMCKL4Eg0G6urqQJAlRFElNTcVsNo/6fcSih84A7BaLRRiCzCMZy5qAQoYm86URtBc3FJnNZjMzZswgKyuLQCCAJEmJvrUhEQwGcbvdZGdn88ILL5CTk0NnZ2dCnmOkPbQqc11d3eMPk3n37t01lZWVbcRW5v966aWXJh44cKAyWWWWZZlgMIjD4aCoqIj09HRkWeb48eO0tbUlTS+tyDxz5kz2799Peno6LS0trFq1Cr/fT2pq6qjez0iE1mUeJorM6enpFBUVkZaWRigUIhgMEgwGB/4GGiEQCOB2u5kzZw779u0jPT0yze/xeJAkKdYrzoNiuELrMg8TSZIIBoNkZWVRWFiI2WwmHA7j8/k4d+4cnZ2dCRl7DhWlZ37iiSd46623cDgcANy5c4c33ngDj8eD0+kc9fsajtC6zMMkGAwiSRKTJk0iLy9PHVZ4PB7Onj1LR0cHZrMZUdT2njGlZ87Ly2P//v1YrVYAmpqaKC0tpaGhAbvdnpBh01CFHm2ZzURmMx4ZmadNm0Zubi6iKCIIAm1tbZw5cwav14vJZNK8zH6/H7fbTWFhIXv27Okh85YtW7h9+zY2mw2TyZSQ+xuK0KrMly9ffiInJ2dC98pEyNzY2HjJ5XJVJoPMoVCI2bNnM2vWLCCyo/Hu3bucPXuWUCiE2WxOyJhzKCgyL1y4kMrKSiyWyO6Fb7/9li1bttDS0pJQmWHwQidU5oMHD+6xWq09NhYlg8zKtJwsy+Tm5vLYY48RDocRRZGmpiaqq6uRZRmTyaR5mX0+Hx6Ph2eeeYbdu3er0jY0NFBaWqoJmWFwQg8kc2j37t3ndZl70l3mvLw8pk6dSjgcxmAw8O2333LhwgVEUUyKF0C/36/KXFFRoUpbX19PaWkp9+7dw263YzTGap1u+Ax0B7rMwyAcDhMIBDAajbhcLsaPH48sy4iiyNdff01tbS1GozHhvdlgUHrmpUuXsmPHDvU3ybVr1ygtLaW9vV0zMkP/QusyDwNljtlsNlNUVMS4ceNUma9evcrVq1eTSma3282yZcvYvn27KvP169cpKyujo6NDUzJD30Kn8UDmJ3NycsZ3r4yDzABzeURkTklJweVykZGRQTgcRhAEamtr+eabbzCZTJoSoC+UnvnnP/85W7duVWWuq6ujtLQUn8+XsKm5/ujrJ2sDuHDhwpxRktkJZC9evDg92WV2OBy4XC5sNhuyLCPLMpcuXaKhoQGj0ZgUMnu9XtxuN7/61a947bXX1PLa2lrKy8s1KzP0LbR18uTJ4uzZs3vMM0uSJMdBZohuOy0rK1vUW+bm5uZ/a11mgFAohM1mY8GCBaSmpiJJEj6fj9raWpqbmzGZTJoUoDfBYBCPx/MfMl+8eJGtW7fS1dWlWZmhn912zc3N4WCvjQUGg0H85S9/mR29NEf/xAIvQGNj43/skU5PT//xypUrp0YvHUQO2WoOWZZxOp04HA4kScJgMNDS0sKdO3cQRVGzAvQmGAwyZswY1q5dq5bduHGD9evX4/F4cDgcmn6WvoTu8vv94YMHD16WJKnHUamCgoIpNTU1s6KXY4mEKRgprQAlJSWn6+vrP+9eYTabUysqKirKy8tnEBmaFKFBqUVR5M6dO9y+fRuj0YgkSUyePJk5c+aoG4+SAYPBgMfj4cqVK2rZhAkT+NnPfoYsywQCgQTe3cD0JXQrENq4ceMPf/7zny+OgtR+4JbX65Vzc3PfSUapTSYTsixz7tw5Ghoa1F5s5syZ5Ofnq2NsraM8x86dO6mpqQEgJSWF7du3s3z5ctxuN16vN8F32Td9CS0BzYyu1HUksdSCIKiLJDU1Ndy4cQODwUA4HGbatGkUFhYC9DidokVEUcThcODz+Xj99deprq4GIs9XXl5OcXExbrebrq6uBN/pw+lvJ0wIXeoho2wwunTpEteuXUMURWRZZtKkSbhcLkRR1LzUBoMBu91OIBDg97//PefOnVPrNm/ezPPPP4/H49Gk1ANt7dKlHgbKXPOVK1e4fPkygiAgyzLZ2dksXLgQq9WaFFI7HA78fj9btmzhxIkTat2mTZt48cUXNSn1YPYq6lIPA5PJhMlk4uuvv+bChQuqvJmZmcyfP5/U1FQCgQCyHKvwJLFHGX4IgsCOHTt6SF1SUsKqVavweDx4PJ5+vsvoMtjNt7rUw0BZ4r516xbV1dWEQiHC4TBOp5OFCxfidDrVDUxaRZFalmXeeOMNjh8/rtatW7eOl19+ma6uLs1IPZTd5LrUw8BgMGAymWhsbOTs2bP4/ZH4OmlpaSxcuJCxY8eqm/+1iiiK2O12ALZv386xY8fUujVr1rBu3To8Hg9utztRt6gy1OMRutTDwGAwYDabaWlpoaqqSh13WiwWXC4X2dnZSSO10Whk9+7dHD16VK1btWoVJSUlmuiph3PeR5d6GCh7n9vb2zl16hSdnZ0IgoDRaKSoqIgpU6aoJ1u0ijL8EEWRHTt2cOTIEbVu5cqVbNiwIeE99XAPsOlSDwNRFDGZTHg8Hqqqqrh37x6CIGAwGMjPz2f69OlIkqRpqQVBwG63Y7Va2bt3L59++qlat2LFCkpLS+nq6kqY1CM5kalLPQyUntrn81FVVUVzc7N6MDY3N1cNOKNllOGHxWJh3759fPLJJ2pdcXExr776Kj6fLyHL5CM9YqxLPQwEQVCXmM+cOUNDQwMQ2bap5d65O0pPrUj90UcfqXW/+MUvyMjISMizxOLMvC71MFCkFgSB6upqTp06xcmTJ+no6EiKPdPwQOqUlBTeffddDh48yHfffcfHH39MW1tbQk7lxCoIxHCkHmkE/6FKnTvC9uKCsgBz7949fD5fUgSa6Y4gCDgcDiwWC4cOHWLdunUcOHAAs9mc1ELDIKU+fvz4tOilPQZtDkrq4uLiLGAcGk1JocxVJ0Ogmb5wOp2MGzcOg8FAdnY2NpstIfcR65/egFIXFRVNjn4ZqxwrA0q9fv36Z6KXmhRaJ3bEozvoV+qOjg5l5j2Wm4P7lbq+vv776JeJX8rSiSvxevtQpM7auHHjD62trTWrV6+e5vP5gqWlpV9HPxOLHCvdqQPwer1TcnNz3zly5MjN3Nxc16VLl86sXbv2dLQ9PcfKI47wsC2MMQxLZSTyAtj77KEXaIlVI73IBaY8pPwk0KZnktU2I80kG+/5oRDQRORwq5VIKjcf8f3VX0fkdHg2kTFza/Qe2uLYpo5GiHcPrTm0vKleZ+T010ObiaQStkavfUR6uViPfbujJJ13EOnFlaTzo74W/Kj8qn5UnmOw9DXLYQbGA9a8vDxTXl6eiYjY2TwQPNYUEEky74gmnbcRSUJfRCSKv47OgPQl9BiAo0ePTjl//vzi8+fPL/7nP/+pLIhkEXup04EJ+fn59vv37//pww8//Gt7e/uBbgsi+ehS6wyCvoROcTqdwpIlS2YIUX7yk5/M+Pzzz6dH62MtdQbAvn37lo4ZM2YKgMPhmHj48OE3dal1hkKfCys+nw9Jknpsl3r22Wcfi5PUIeiRZB6AlJSUzF5SF6BLrdMP/YYC++CDD/4d7jUt8Oyzzz526tSpx6KXsZK6FWD16tX/amxs7BGMUZE6Oq4eiy61Tj/0FwpMeuWVV+6+//77tb2lXrhw4fQYS90JNLS2toZcLldlU1NTXffKlJSUzPfee2+PLrXOQAwUCkxas2ZN8/vvv1/be5NRHKSuBW5///33gaKioopklFqSJPx+P36/n0AgkLRz3oFAgLa2NlpbW2lvb0+KmHwK/W1OCtJN6g8++KBuEFKPdBP9JZJUakmSCAQCZGVlkZOTg8lk0nx0pIchSRJut5ucnBx+85vfkJWVhdvtThqpB9ptFwTuMHipxzH6UheiAaklSSIzM5Onn36auXPnsmjRIjU+nNbPCHbH7/fjdDrZu3cvJSUlHDhwgJycnKSRerChwAaUuqqqKid6OWpSv/TSSxOB/0IjUgeDQfx+P6FQCKfTyfz580lPT9d8dKTuCIKAJElq7JDMzEz279/PrFmz6OzsTNr40L0ZUOoFCxb8eLSlPnDgQKVWpDYajbjdbk6fPo3f7yccDpOamsqCBQsYN24cgUBA04FkFMxmM263mz/84Q+0tEQ2RGZmZvL222/z+OOPa76nHmooMF3qPlBibrS2tlJVVaVGEDKbzcybN48f/ehHBINBTcsAD0Lp3rx5k82bN9Pc3AxEjli9++67PPXUU5ruqYcTCiyhUj9snlprUnd0dPDFF1/Q0dGhBpIpKChg6tSpSJKkealNJhM2m41bt26xefNm7ty5A4DVauXNN98kPz8ft9utxunTEsMNBZYwqV0uV+UgpU5IzmFFap/PxxdffMEPP/yAIAgIgsBTTz3FzJkzkyLniiJ1Q0MDJSUl3Lp1CwCbzcZbb73FvHnz8Hg8mpN6JKHAtC51AQmSWom5EQwGOX36tJoJC2DOnDk8+eSThEIhzc9VK1I3Nzfz2muvcfv2bQA1DNjTTz+N2+3G5/Ml+E4fMNJQYANKfe7cuVGV+uDBg3u0IrXZbEaWZU6fPq0mEgqHw+Tk5JCXl6cmEtK61Ha7naamJjZu3Mg333wDRN4NKioqWLRokaZ66liEAutX6qKiolGV2mq1pmtFaniQc6Wmpob6+no1PcW0adOYN28eoihqvqc2Go3Y7Xbu3r3Lpk2buH79OhAJB7x3716WLFmimZ46VqHAdKn7Qcki2z2RUDgcZuLEicybNw+z2az5BRhF6ra2NsrKyvjqq6+AyDvDrl27+OlPf4rH40m41LEMBTZUqUcaaGaoUs8dYXsjQomMdPXqVerq6hAEgXA4TFZWFvPnz1cTCSWD1Pfv3+d3v/sdly9fBiLDq+3bt7Ns2bKESx3rUGADSt0tFJgjBm32kPq777670L1SkXrx4sXpRI6POWPQ5rBRktffuHGDCxcuIEkSsiyTkZHBokWLcDgcmpdamaf2er2UlZVRVxdZGhBFkW3btiU8OWc8QoH1kLr3B+bNm6fEzIhVKDBV6vnz5+9pbm7+d/dKq9WaXlZWtih6mRmjNoeNkkiooaGB6upqvF4vsixjt9uZP38+NptN8yF1Fak9Hg/l5eXU1tYCkZ66rKyM5557Do/Hk5CpyXiEAnMABovFIixZsiS7d6XX61Veh2O1DpwSbZOVK1dOTU9P/3HvDzQ2NioRkzSR09dgMCCKIk1NTbS0tGAwGJAkCafTidPp1HQPraDkMXS73fz2t7/lxo0bQETqdevWqRm+RptYC50B2C0Wi3D16tXcadOm9RA6EAgEKysrlazosRhopRA5Fe4sLy+fUVFRUWE2m3vEnq6vr/+8pKTkdPRSE6HAlFwqc+bMYfLkyUiShNFo5Pbt2zQ1NSVNBFLlRXbZsmVMmDBBLf/yyy/p6upS852PJrGMnDSgzK+//nrNO++800Ektsf9EbY3KJlzc3Pf8Xq9MnCL+MYUGRTKGDk/P5+pU6ciyzIGg4GGhgY1QWci4ioPFa/Xi8fj4bnnnqOsrEwNTlRTU8POnTuRZTkhzxEroYcq890Rtjccmf9jPD+ahMNhdWxcWFjIpEmTVJlv3LhBXV2dmn9F6yjp237961+zefNmtby6upqtW7cSCATUbFmjTSyEzgRsQ5R5JOPnpJQ5GAxiMBgoLCxk/PjxyLKMKIpcu3aNK1euqC+LWkeR+fnnn2fTpk1q+blz59i2bRuBQAC73Z6Q4QaMXOhMwOZ0OoWLFy/qMj8ERWar1UpRUREZGRnIsowgCFy+fJmvvvoq6WR+8cUXKSkpUctPnDjBtm3b1PQUiXwHGInQuswDoOzVSEtLw+VyMWbMGMLhMOFwmIsXL3Lr1i1MJlNSJAnyeDx0dXWxatUq1q1bp5afOHGCHTt2aEJmGL7QuswDoMjsdDpxuVykpaWp4+jz58/T2NioLolrHUXml19+mTVr1qjlx48fZ+fOnQCakBmGJ7Qu8wAo2WDHjh1LYWEhFktkDcnv91NTU0NLS0vSyOx2u+nq6uKVV15h1apVavmxY8fYtWuXuhyuBZlh6ELrMg+AciJl/PjxFBQUqGPjrq4uzp49S1tbW9KkblN65pKSElauXKmWHz16lMrKSs3JDEMTWpd5AEKhEKFQiClTpjB37lxEUUQQBDo6Ojhz5gwejydpZFZ65g0bNrBixQq1/MiRI+zZsweLxYLD4dBccPzBCq3LPAChUAhJkpg+fTq5uQ9yfN67d0/ds5EseQgVmUtLSykuLlbLP/30U/bt24fVasVms2lOZhic0LrMg0CWZdLT01WZlb0a1dXVSJKE2WzWpAC9CQQC+P1+Xn311R4yf/LJJ+zfvx+LxYLdbtfsswwktC7zEAiFQni9XlJSUmhoaOD8+fM9cnonA6FQiIyMDJYvX66WffTRR/zxj38kJSVF0zJD/0LrMg8Bo9FIZ2cnJ0+eJC0tTd1FlwwLJt0xmUy0tbXx8ccfs3TpUv7xj39w6NChpJAZ+hY6jdGVGSL5BZNSZugZvsDn8yXNtFxvlJM1f/nLX/j73/9Oa2ur+gKYDPQltA2gqqpqzijJnAGMKy4uzkpGmRVEUUyKl76BsNlsavL5sWPHJvhuhkZfP32r0+kUZs+ePbF7YZxkhmiOlfXr1z+TrDLraIO+hA62t7eHOzo6upSCOMoM0cyy3ZLME73WZdYZEn0J7QcoLy+/ePPmzeabN2/e2bBhw9k4yQyRkyT+tWvXnj527Nj/trS0fPnZZ58d0mXWGSp9pUYWiIQa6J1mwgf8QGxlVhgDPPOQ8pjKrCev1zbxSl4fJpKOwsGD09l+oGMkjQ1AG3CSSAZbJen8HUZ+VEvn/xEP7aF1dJKV5J9j0tHphi60ziOFLrTOI4UutM4jhS60ziPF/wHgvbfirVNMHAAAAABJRU5ErkJggg==) no-repeat}.layui-layer-prompt .layui-layer-input{color:#333;border:1px solid #ccc;width:220px;height:30px;margin:0 auto;padding:0 5px;line-height:30px;display:block;box-shadow:inset 1px 1px 5px #0000001a}.layui-layer-prompt textarea.layui-layer-input{width:300px;height:100px;line-height:20px}.layui-layer-prompt .layui-layer-content{padding:20px}.layui-layer-prompt .layui-layer-btn{padding-top:0}.layui-layer-tab{box-shadow:1px 1px 50px #0006}.layui-layer-tab .layui-layer-title{background-color:#eee;border-bottom:1px solid #ccc;padding-left:0;overflow:visible}.layui-layer-tab .layui-layer-title span{float:left;text-align:center;cursor:default;text-overflow:ellipsis;white-space:nowrap;min-width:80px;max-width:260px;padding:0 20px;position:relative;overflow:hidden}.layui-layer-tab .layui-layer-title span.layui-layer-tabnow{z-index:10;background-color:#fff;border-left:1px solid #ccc;border-right:1px solid #ccc;height:43px}.layui-layer-tab .layui-layer-title span:first-child{border-left:none}.layui-layer-tabmain{clear:both;line-height:24px}.layui-layer-tabmain .layui-layer-tabli{display:none}.layui-layer-tabmain .layui-layer-tabli.xubox_tab_layer{display:block}.xubox_tabclose{cursor:pointer;position:absolute;top:5px;right:10px}.layui-layer-photos{animation-duration:1s}.layui-layer-photos .layui-layer-content{text-align:center;overflow:hidden}.layui-layer-photos .layui-layer-phimg img{vertical-align:top}.layui-layer-imguide,.layui-layer-imgbar{display:none}.layui-layer-imgprev,.layui-layer-imgnext{_width:44px;blr:expression(this.onFocus=this.blur());outline:none;width:27px;height:44px;margin-top:-22px;position:absolute;top:50%}.layui-layer-imgprev{background-position:-5px -5px;left:10px}.layui-layer-imgprev:hover{background-position:-33px -5px}.layui-layer-imgnext{background-position:-5px -50px;right:10px}.layui-layer-imgnext:hover{background-position:-33px -50px}.layui-layer-imgbar{background-color:#000c;background-color:#000 \\9 ;width:100%;height:32px;filter:Alpha(opacity=80);color:#fff;text-overflow:ellipsis;white-space:nowrap;font-size:0;line-height:32px;position:absolute;bottom:0;left:0;overflow:hidden}.layui-layer-imgtit *{vertical-align:top;font-size:12px}.layui-layer-imgtit a{text-overflow:ellipsis;white-space:nowrap;color:#fff;max-width:65%;overflow:hidden}.layui-layer-imgtit a:hover{color:#fff;text-decoration:underline}.layui-layer-imgtit em{padding-left:10px;font-style:normal}@media screen and (width<=1100px){.layui-layer-iframe{-webkit-overflow-scrolling:touch;overflow-y:auto}}"); _css("/*!\n * Toastify js 1.12.0\n * https://github.com/apvarun/toastify-js\n * @license MIT licensed\n *\n * Copyright (C) 2018 Varun A P\n */\n.toastify{color:#fff;opacity:0;cursor:pointer;z-index:2147483647;background:linear-gradient(135deg,#73a5ff,#5477f5);border-radius:2px;max-width:calc(50% - 20px);padding:12px 20px;text-decoration:none;transition:all .4s cubic-bezier(.215,.61,.355,1);display:inline-block;position:fixed;box-shadow:0 3px 6px -1px #0000001f,0 10px 36px -4px #4d60e84d}.toastify.on{opacity:1}.toast-close{color:#fff;cursor:pointer;opacity:.4;background:0 0;border:0;padding:0 5px;font-family:inherit;font-size:1em}.toastify-right{right:15px}.toastify-left{left:15px}.toastify-top{top:-150px}.toastify-bottom{bottom:-150px}.toastify-rounded{border-radius:25px}.toastify-avatar{border-radius:2px;width:1.5em;height:1.5em;margin:-7px 5px}.toastify-center{max-width:-moz-fit-content;margin-left:auto;margin-right:auto;left:0;right:0}@media only screen and (width<=360px){.toastify-right,.toastify-left{max-width:fit-content;margin-left:auto;margin-right:auto;left:0;right:0}}"); _css("/*!\n * Viewer.js v1.11.7\n * https://fengyuanchen.github.io/viewerjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2024-11-24T04:32:14.526Z\n */\n.viewer-close:before,.viewer-flip-horizontal:before,.viewer-flip-vertical:before,.viewer-fullscreen-exit:before,.viewer-fullscreen:before,.viewer-next:before,.viewer-one-to-one:before,.viewer-play:before,.viewer-prev:before,.viewer-reset:before,.viewer-rotate-left:before,.viewer-rotate-right:before,.viewer-zoom-in:before,.viewer-zoom-out:before{color:#0000;background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 560 40'%3E%3Cpath fill='%23fff' d='M49.6 17.9h20.2v3.9H49.6zm123.1 2 10.9-11 2.7 2.8-8.2 8.2 8.2 8.2-2.7 2.7-10.9-10.9zm94 0-10.8-11-2.7 2.8 8.1 8.2-8.1 8.2 2.7 2.7 10.8-10.9zM212 9.3l20.1 10.6L212 30.5V9.3zm161.5 4.6-7.2 6 7.2 5.9v-4h12.4v4l7.3-5.9-7.3-6v4h-12.4v-4zm40.2 12.3 5.9 7.2 5.9-7.2h-4V13.6h4l-5.9-7.3-5.9 7.3h4v12.6h-4zm35.9-16.5h6.3v2h-4.3V16h-2V9.7Zm14 0h6.2V16h-2v-4.3h-4.2v-2Zm6.2 14V30h-6.2v-2h4.2v-4.3h2Zm-14 6.3h-6.2v-6.3h2v4.4h4.3v2Zm-438 .1v-8.3H9.6v-3.9h8.2V9.7h3.9v8.2h8.1v3.9h-8.1v8.3h-3.9zM93.6 9.7h-5.8v3.9h2V30h3.8V9.7zm16.1 0h-5.8v3.9h1.9V30h3.9V9.7zm-11.9 4.1h3.9v3.9h-3.9zm0 8.2h3.9v3.9h-3.9zm244.6-11.7 7.2 5.9-7.2 6v-3.6c-5.4-.4-7.8.8-8.7 2.8-.8 1.7-1.8 4.9 2.8 8.2-6.3-2-7.5-6.9-6-11.3 1.6-4.4 8-5 11.9-4.9v-3.1Zm147.2 13.4h6.3V30h-2v-4.3h-4.3v-2zm14 6.3v-6.3h6.2v2h-4.3V30h-1.9zm6.2-14h-6.2V9.7h1.9V14h4.3v2zm-13.9 0h-6.3v-2h4.3V9.7h2V16zm33.3 12.5 8.6-8.6-8.6-8.7 1.9-1.9 8.6 8.7 8.6-8.7 1.9 1.9-8.6 8.7 8.6 8.6-1.9 2-8.6-8.7-8.6 8.7-1.9-2zM297 10.3l-7.1 5.9 7.2 6v-3.6c5.3-.4 7.7.8 8.7 2.8.8 1.7 1.7 4.9-2.9 8.2 6.3-2 7.5-6.9 6-11.3-1.6-4.4-7.9-5-11.8-4.9v-3.1Zm-157.3-.6c2.3 0 4.4.7 6 2l2.5-3 1.9 9.2h-9.3l2.6-3.1a6.2 6.2 0 0 0-9.9 5.1c0 3.4 2.8 6.3 6.2 6.3 2.8 0 5.1-1.9 6-4.4h4c-1 4.7-5 8.3-10 8.3a10 10 0 0 1-10-10.2 10 10 0 0 1 10-10.2Z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-size:280px;width:20px;height:20px;font-size:0;line-height:0;display:block}.viewer-zoom-in:before{content:\"Zoom In\";background-position:0 0}.viewer-zoom-out:before{content:\"Zoom Out\";background-position:-20px 0}.viewer-one-to-one:before{content:\"One to One\";background-position:-40px 0}.viewer-reset:before{content:\"Reset\";background-position:-60px 0}.viewer-prev:before{content:\"Previous\";background-position:-80px 0}.viewer-play:before{content:\"Play\";background-position:-100px 0}.viewer-next:before{content:\"Next\";background-position:-120px 0}.viewer-rotate-left:before{content:\"Rotate Left\";background-position:-140px 0}.viewer-rotate-right:before{content:\"Rotate Right\";background-position:-160px 0}.viewer-flip-horizontal:before{content:\"Flip Horizontal\";background-position:-180px 0}.viewer-flip-vertical:before{content:\"Flip Vertical\";background-position:-200px 0}.viewer-fullscreen:before{content:\"Enter Full Screen\";background-position:-220px 0}.viewer-fullscreen-exit:before{content:\"Exit Full Screen\";background-position:-240px 0}.viewer-close:before{content:\"Close\";background-position:-260px 0}.viewer-container{-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;user-select:none;direction:ltr;font-size:0;line-height:0;position:absolute;inset:0;overflow:hidden}.viewer-container ::-moz-selection{background-color:#0000}.viewer-container::-moz-selection{background-color:#0000}.viewer-container ::selection{background-color:#0000}.viewer-container::selection{background-color:#0000}.viewer-container:focus{outline:0}.viewer-container img{width:100%;height:auto;display:block;min-width:0!important;max-width:none!important;min-height:0!important;max-height:none!important}.viewer-canvas{position:absolute;inset:0;overflow:hidden}.viewer-canvas>img{width:auto;height:auto;margin:15px auto;max-width:90%!important}.viewer-footer{text-align:center;position:absolute;bottom:0;left:0;right:0;overflow:hidden}.viewer-navbar{background-color:#00000080;overflow:hidden}.viewer-list{box-sizing:content-box;height:50px;margin:0;padding:1px 0;overflow:hidden}.viewer-list>li{color:#0000;cursor:pointer;float:left;opacity:.5;width:30px;height:50px;font-size:0;line-height:0;transition:opacity .15s;overflow:hidden}.viewer-list>li:focus,.viewer-list>li:hover{opacity:.75}.viewer-list>li:focus{outline:0}.viewer-list>li+li{margin-left:1px}.viewer-list>.viewer-loading{position:relative}.viewer-list>.viewer-loading:after{border-width:2px;width:20px;height:20px;margin-top:-10px;margin-left:-10px}.viewer-list>.viewer-active,.viewer-list>.viewer-active:focus,.viewer-list>.viewer-active:hover{opacity:1}.viewer-player{cursor:none;z-index:1;background-color:#000;display:none;bottom:0;right:0}.viewer-player,.viewer-player>img{position:absolute;top:0;left:0}.viewer-toolbar>ul{margin:0 auto 5px;padding:6px 3px;display:inline-block;overflow:hidden}.viewer-toolbar>ul>li{cursor:pointer;float:left;background-color:#00000080;border-radius:50%;width:24px;height:24px;transition:background-color .15s;overflow:hidden}.viewer-toolbar>ul>li:focus,.viewer-toolbar>ul>li:hover{background-color:#000c}.viewer-toolbar>ul>li:focus{z-index:1;outline:0;position:relative;box-shadow:0 0 3px #fff}.viewer-toolbar>ul>li:before{margin:2px}.viewer-toolbar>ul>li+li{margin-left:1px}.viewer-toolbar>ul>.viewer-small{width:18px;height:18px;margin-top:3px;margin-bottom:3px}.viewer-toolbar>ul>.viewer-small:before{margin:-1px}.viewer-toolbar>ul>.viewer-large{width:30px;height:30px;margin-top:-3px;margin-bottom:-3px}.viewer-toolbar>ul>.viewer-large:before{margin:5px}.viewer-tooltip{color:#fff;text-align:center;background-color:#000c;border-radius:10px;width:50px;height:20px;margin-top:-10px;margin-left:-25px;font-size:12px;line-height:20px;display:none;position:absolute;top:50%;left:50%}.viewer-title{color:#ccc;opacity:.8;text-overflow:ellipsis;white-space:nowrap;max-width:90%;min-height:14px;margin:5px 5%;font-size:12px;line-height:1.2;transition:opacity .15s;display:inline-block;overflow:hidden}.viewer-title:hover{opacity:1}.viewer-button{-webkit-app-region:no-drag;cursor:pointer;background-color:#00000080;border-radius:50%;width:80px;height:80px;transition:background-color .15s;position:absolute;top:-40px;right:-40px;overflow:hidden}.viewer-button:focus,.viewer-button:hover{background-color:#000c}.viewer-button:focus{outline:0;box-shadow:0 0 3px #fff}.viewer-button:before{position:absolute;bottom:15px;left:15px}.viewer-fixed{position:fixed}.viewer-open{overflow:hidden}.viewer-show{display:block}.viewer-hide{display:none}.viewer-backdrop{background-color:#00000080}.viewer-invisible{visibility:hidden}.viewer-move{cursor:move;cursor:grab}.viewer-fade{opacity:0}.viewer-in{opacity:1}.viewer-transition{transition:all .3s}@keyframes viewer-spinner{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.viewer-loading:after{content:\"\";z-index:1;border:4px solid #ffffff1a;border-left-color:#ffffff80;border-radius:50%;width:40px;height:40px;margin-top:-20px;margin-left:-20px;animation:1s linear infinite viewer-spinner;display:inline-block;position:absolute;top:50%;left:50%}@media (width<=767px){.viewer-hide-xs-down{display:none}}@media (width<=991px){.viewer-hide-sm-down{display:none}}@media (width<=1199px){.viewer-hide-md-down{display:none}}"); _css(".tabulator{text-align:left;background-color:#fff;border:1px solid #999;font-size:14px;position:relative;overflow:hidden;transform:translateZ(0)}.tabulator[tabulator-layout=fitDataFill] .tabulator-tableholder .tabulator-table{min-width:100%}.tabulator[tabulator-layout=fitDataTable]{display:inline-block}.tabulator.tabulator-block-select,.tabulator.tabulator-ranges .tabulator-cell:not(.tabulator-editing){-webkit-user-select:none;user-select:none}.tabulator .tabulator-header{box-sizing:border-box;-webkit-user-select:none;-moz-user-select:none;-khtml-user-select:none;-o-user-select:none;white-space:nowrap;border-bottom:1px solid #999;outline:none;width:100%;position:relative;overflow:hidden}.tabulator .tabulator-header.tabulator-header-hidden{display:none}.tabulator .tabulator-header .tabulator-header-contents{position:relative;overflow:hidden}.tabulator .tabulator-header .tabulator-header-contents .tabulator-headers{display:inline-block}.tabulator .tabulator-header .tabulator-col{box-sizing:border-box;text-align:left;vertical-align:bottom;background:#f9fafb;border-right:1px solid #ddd;flex-direction:column;justify-content:flex-start;display:inline-flex;position:relative;overflow:hidden}.tabulator .tabulator-header .tabulator-col.tabulator-moving{pointer-events:none;background:#dae1e7;border:1px solid #999;position:absolute}.tabulator .tabulator-header .tabulator-col.tabulator-range-highlight{color:#000;background-color:#d6d6d6}.tabulator .tabulator-header .tabulator-col.tabulator-range-selected{color:#fff;background-color:#3876ca}.tabulator .tabulator-header .tabulator-col .tabulator-col-content{box-sizing:border-box;padding:4px;position:relative}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-header-popup-button{padding:0 8px}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-header-popup-button:hover{cursor:pointer;opacity:.6}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title-holder{position:relative}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title{box-sizing:border-box;text-overflow:ellipsis;vertical-align:bottom;white-space:nowrap;width:100%;overflow:hidden}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title.tabulator-col-title-wrap{text-overflow:clip;white-space:normal}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title .tabulator-title-editor{box-sizing:border-box;background:#fff;border:1px solid #999;width:100%;padding:1px}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title .tabulator-header-popup-button+.tabulator-title-editor{width:calc(100% - 22px)}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter{align-items:center;display:flex;position:absolute;top:0;bottom:0;right:4px}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-bottom:6px solid #bbb;border-left:6px solid #0000;border-right:6px solid #0000;width:0;height:0}.tabulator .tabulator-header .tabulator-col.tabulator-col-group .tabulator-col-group-cols{border-top:1px solid #ddd;margin-right:-1px;display:flex;position:relative;overflow:hidden}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter{box-sizing:border-box;text-align:center;width:100%;margin-top:2px;position:relative}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter textarea{height:auto!important}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter svg{margin-top:3px}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter input::-ms-clear{width:0;height:0}.tabulator .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-title{padding-right:25px}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable.tabulator-col-sorter-element:hover{cursor:pointer;background-color:#dae1e7}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter{color:#bbb}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{cursor:pointer;border-bottom:6px solid #555}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-top:none;border-bottom:6px solid #bbb}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter{color:#666}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{cursor:pointer;border-bottom:6px solid #555}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-top:none;border-bottom:6px solid #666}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter{color:#666}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{cursor:pointer;border-top:6px solid #555}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{color:#666;border-top:6px solid #666;border-bottom:none}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical .tabulator-col-content .tabulator-col-title{text-orientation:mixed;writing-mode:vertical-rl;justify-content:center;align-items:center;display:flex}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-col-vertical-flip .tabulator-col-title{transform:rotate(180deg)}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable .tabulator-col-title{padding-top:20px;padding-right:0}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable.tabulator-col-vertical-flip .tabulator-col-title{padding-bottom:20px;padding-right:0}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable .tabulator-col-sorter{justify-content:center;inset:4px 0 auto}.tabulator .tabulator-header .tabulator-frozen{z-index:11;position:sticky;left:0}.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-left{border-right:2px solid #ddd}.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-right{border-left:2px solid #ddd}.tabulator .tabulator-header .tabulator-calcs-holder{box-sizing:border-box;border-top:1px solid #ddd;border-bottom:1px solid #ddd;display:inline-block;background:#fff!important}.tabulator .tabulator-header .tabulator-calcs-holder .tabulator-row{background:#fff!important}.tabulator .tabulator-header .tabulator-calcs-holder .tabulator-row .tabulator-col-resize-handle{display:none}.tabulator .tabulator-header .tabulator-frozen-rows-holder{padding-top:1em;display:inline-block}.tabulator .tabulator-header .tabulator-frozen-rows-holder:empty{display:none}.tabulator .tabulator-tableholder{-webkit-overflow-scrolling:touch;white-space:nowrap;width:100%;position:relative;overflow:auto}.tabulator .tabulator-tableholder:focus{outline:none}.tabulator .tabulator-tableholder .tabulator-placeholder{box-sizing:border-box;justify-content:center;align-items:center;width:100%;min-width:100%;display:flex}.tabulator .tabulator-tableholder .tabulator-placeholder[tabulator-render-mode=virtual]{min-height:100%}.tabulator .tabulator-tableholder .tabulator-placeholder .tabulator-placeholder-contents{color:#ccc;text-align:center;white-space:normal;padding:10px;font-size:20px;font-weight:700;display:inline-block}.tabulator .tabulator-tableholder .tabulator-table{color:#333;white-space:nowrap;background-color:#fff;display:inline-block;position:relative;overflow:visible}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs{font-weight:700;background:#e2e2e2!important}.tabulator .tabulator-tableholder .tabulator-range-overlay{pointer-events:none;z-index:10;position:absolute;inset:0}.tabulator .tabulator-tableholder .tabulator-range-overlay .tabulator-range{box-sizing:border-box;border:1px solid #2975dd;position:absolute}.tabulator .tabulator-tableholder .tabulator-range-overlay .tabulator-range.tabulator-range-active:after{content:\"\";background-color:#2975dd;border-radius:999px;width:6px;height:6px;position:absolute;bottom:-3px;right:-3px}.tabulator .tabulator-tableholder .tabulator-range-overlay .tabulator-range-cell-active{box-sizing:border-box;border:2px solid #2975dd;position:absolute}.tabulator .tabulator-footer{color:#555;-webkit-user-select:none;user-select:none;-khtml-user-select:none;-o-user-select:none;white-space:nowrap;background-color:#fff;border-top:1px solid #999;font-weight:700}.tabulator .tabulator-footer .tabulator-footer-contents{flex-direction:row;justify-content:space-between;align-items:center;padding:5px 10px;display:flex}.tabulator .tabulator-footer .tabulator-footer-contents:empty{display:none}.tabulator .tabulator-footer .tabulator-spreadsheet-tabs{margin-top:-5px;overflow-x:auto}.tabulator .tabulator-footer .tabulator-spreadsheet-tabs .tabulator-spreadsheet-tab{border:1px solid #999;border-top:none;border-bottom-right-radius:5px;border-bottom-left-radius:5px;padding:5px;font-size:.9em;display:inline-block}.tabulator .tabulator-footer .tabulator-spreadsheet-tabs .tabulator-spreadsheet-tab:hover{cursor:pointer;opacity:.7}.tabulator .tabulator-footer .tabulator-spreadsheet-tabs .tabulator-spreadsheet-tab.tabulator-spreadsheet-tab-active{background:#fff}.tabulator .tabulator-footer .tabulator-calcs-holder{box-sizing:border-box;text-align:left;border-top:1px solid #ddd;border-bottom:1px solid #ddd;width:100%;overflow:hidden}.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row{display:inline-block}.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row .tabulator-col-resize-handle{display:none}.tabulator .tabulator-footer .tabulator-calcs-holder:only-child{margin-bottom:-5px}.tabulator .tabulator-footer>*+.tabulator-page-counter{margin-left:10px}.tabulator .tabulator-footer .tabulator-page-counter{font-weight:400}.tabulator .tabulator-footer .tabulator-paginator{color:#555;font-family:inherit;font-size:inherit;font-weight:inherit;text-align:right;flex:1}.tabulator .tabulator-footer .tabulator-page-size{border:1px solid #aaa;border-radius:3px;margin:0 5px;padding:2px 5px;display:inline-block}.tabulator .tabulator-footer .tabulator-pages{margin:0 7px}.tabulator .tabulator-footer .tabulator-page{background:#fff3;border:1px solid #aaa;border-radius:3px;margin:0 2px;padding:2px 5px;display:inline-block}.tabulator .tabulator-footer .tabulator-page.active{color:#d00}.tabulator .tabulator-footer .tabulator-page:disabled{opacity:.5}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-footer .tabulator-page:not(disabled):hover{color:#fff;cursor:pointer;background:#0003}}.tabulator .tabulator-col-resize-handle{vertical-align:middle;z-index:11;width:6px;margin-left:-3px;margin-right:-3px;display:inline-block;position:relative}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-col-resize-handle:hover{cursor:ew-resize}}.tabulator .tabulator-col-resize-handle:last-of-type{width:3px;margin-right:0}.tabulator .tabulator-col-resize-guide{opacity:.5;background-color:#999;width:4px;height:100%;margin-left:-.5px;position:absolute;top:0}.tabulator .tabulator-row-resize-guide{opacity:.5;background-color:#999;width:100%;height:4px;margin-top:-.5px;position:absolute;left:0}.tabulator .tabulator-alert{text-align:center;z-index:100;background:#0006;align-items:center;width:100%;height:100%;display:flex;position:absolute;top:0;left:0}.tabulator .tabulator-alert .tabulator-alert-msg{background:#fff;border-radius:10px;margin:0 auto;padding:10px 20px;font-size:16px;font-weight:700;display:inline-block}.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-msg{color:#000;border:4px solid #333}.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-error{color:#590000;border:4px solid #d00}.tabulator-row{box-sizing:border-box;background-color:#fff;min-height:22px;position:relative}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-selectable:hover{cursor:pointer;background-color:#bbb}}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-selected:hover{cursor:pointer;background-color:#769bcc}}.tabulator-row.tabulator-row-moving{background:#fff;border:1px solid #000}.tabulator-row.tabulator-moving{pointer-events:none;z-index:15;border-top:1px solid #ddd;border-bottom:1px solid #ddd;position:absolute}.tabulator-row.tabulator-range-highlight .tabulator-cell.tabulator-range-row-header{color:#000;background-color:#d6d6d6}.tabulator-row.tabulator-range-highlight.tabulator-range-selected .tabulator-cell.tabulator-range-row-header,.tabulator-row.tabulator-range-selected .tabulator-cell.tabulator-range-row-header{color:#fff;background-color:#3876ca}.tabulator-row .tabulator-row-resize-handle{height:5px;position:absolute;bottom:0;left:0;right:0}.tabulator-row .tabulator-row-resize-handle.prev{top:0;bottom:auto}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-row-resize-handle:hover{cursor:ns-resize}}.tabulator-row .tabulator-responsive-collapse{box-sizing:border-box;border-top:1px solid #ddd;border-bottom:1px solid #ddd;padding:5px}.tabulator-row .tabulator-responsive-collapse:empty{display:none}.tabulator-row .tabulator-responsive-collapse table{font-size:14px}.tabulator-row .tabulator-responsive-collapse table tr td{position:relative}.tabulator-row .tabulator-responsive-collapse table tr td:first-of-type{padding-right:10px}.tabulator-row .tabulator-cell{box-sizing:border-box;text-overflow:ellipsis;white-space:nowrap;border-right:1px solid #ddd;outline:none;padding:4px;display:inline-block;position:relative;overflow:hidden}.tabulator-row .tabulator-cell .tabulator-tick{fill:#2dc214}.tabulator-row .tabulator-cell .tabulator-cross{fill:#ce1515}.tabulator-row .tabulator-cell.tabulator-row-header{background:#f9fafb;border-bottom:1px solid #ddd;border-right:1px solid #999}.tabulator-row .tabulator-cell.tabulator-frozen{background-color:inherit;z-index:11;display:inline-block;position:sticky;left:0}.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left{border-right:2px solid #ddd}.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-right{border-left:2px solid #ddd}.tabulator-row .tabulator-cell.tabulator-editing{border:1px solid #1d68cd;outline:none;padding:0}.tabulator-row .tabulator-cell.tabulator-editing input,.tabulator-row .tabulator-cell.tabulator-editing select{background:0 0;border:1px;outline:none}.tabulator-row .tabulator-cell.tabulator-validation-fail{border:1px solid #db2828}.tabulator-row .tabulator-cell.tabulator-validation-fail input,.tabulator-row .tabulator-cell.tabulator-validation-fail select{color:#db2828;background:0 0;border:1px}.tabulator-row .tabulator-cell.tabulator-row-handle{-webkit-user-select:none;-moz-user-select:none;-khtml-user-select:none;-o-user-select:none;justify-content:center;align-items:center;display:inline-flex}.tabulator-row .tabulator-cell.tabulator-row-handle .tabulator-row-handle-box{width:80%}.tabulator-row .tabulator-cell.tabulator-row-handle .tabulator-row-handle-box .tabulator-row-handle-bar{background:#666;width:100%;height:3px;margin-top:2px}.tabulator-row .tabulator-cell.tabulator-range-selected:not(.tabulator-range-only-cell-selected):not(.tabulator-range-row-header){background-color:#9abcea}.tabulator-row .tabulator-cell .tabulator-data-tree-branch-empty{width:7px;display:inline-block}.tabulator-row .tabulator-cell .tabulator-data-tree-branch{vertical-align:middle;border-bottom:2px solid #ddd;border-left:2px solid #ddd;border-bottom-left-radius:1px;width:7px;height:9px;margin-top:-9px;margin-right:5px;display:inline-block}.tabulator-row .tabulator-cell .tabulator-data-tree-control{vertical-align:middle;background:#0000001a;border:1px solid #333;border-radius:2px;justify-content:center;align-items:center;width:11px;height:11px;margin-right:5px;display:inline-flex;overflow:hidden}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-cell .tabulator-data-tree-control:hover{cursor:pointer;background:#0003}}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-collapse{background:0 0;width:1px;height:7px;display:inline-block;position:relative}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-collapse:after{content:\"\";background:#333;width:7px;height:1px;position:absolute;top:3px;left:-3px}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-expand{background:#333;width:1px;height:7px;display:inline-block;position:relative}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-expand:after{content:\"\";background:#333;width:7px;height:1px;position:absolute;top:3px;left:-3px}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle{-webkit-user-select:none;-moz-user-select:none;-khtml-user-select:none;-o-user-select:none;background:#666;border-radius:20px;justify-content:center;align-items:center;width:15px;height:15px;font-size:1.1em;font-weight:700;display:inline-flex}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle:hover{cursor:pointer;opacity:.7}}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle.open .tabulator-responsive-collapse-toggle-close{display:initial}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle.open .tabulator-responsive-collapse-toggle-open{display:none}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle svg{stroke:#fff}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle .tabulator-responsive-collapse-toggle-close{display:none}.tabulator-row .tabulator-cell .tabulator-traffic-light{border-radius:14px;width:14px;height:14px;display:inline-block}.tabulator-row.tabulator-group{box-sizing:border-box;background:#ccc;border-top:1px solid #999;border-bottom:1px solid #999;border-right:1px solid #ddd;min-width:100%;padding:5px 5px 5px 10px;font-weight:700}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-group:hover{cursor:pointer;background-color:#0000001a}}.tabulator-row.tabulator-group.tabulator-group-visible .tabulator-arrow{border:6px solid #0000;border-top-color:#666;border-bottom:0;margin-right:10px}.tabulator-row.tabulator-group.tabulator-group-level-1{padding-left:30px}.tabulator-row.tabulator-group.tabulator-group-level-2{padding-left:50px}.tabulator-row.tabulator-group.tabulator-group-level-3{padding-left:70px}.tabulator-row.tabulator-group.tabulator-group-level-4{padding-left:90px}.tabulator-row.tabulator-group.tabulator-group-level-5{padding-left:110px}.tabulator-row.tabulator-group .tabulator-group-toggle{display:inline-block}.tabulator-row.tabulator-group .tabulator-arrow{vertical-align:middle;border:6px solid #0000;border-left-color:#666;border-right:0;width:0;height:0;margin-right:16px;display:inline-block}.tabulator-row.tabulator-group span{color:#d00;margin-left:10px}.tabulator-toggle{box-sizing:border-box;background:#dcdcdc;border:1px solid #ccc;flex-direction:row;display:flex}.tabulator-toggle.tabulator-toggle-on{background:#1c6cc2}.tabulator-toggle .tabulator-toggle-switch{box-sizing:border-box;background:#fff;border:1px solid #ccc}.tabulator-popup-container{-webkit-overflow-scrolling:touch;box-sizing:border-box;z-index:10000;background:#fff;border:1px solid #ddd;font-size:14px;display:inline-block;position:absolute;overflow-y:auto;box-shadow:0 0 5px #0003}.tabulator-popup{border-radius:3px;padding:5px}.tabulator-tooltip{box-shadow:none;pointer-events:none;border-radius:2px;max-width:min(500px,100%);padding:3px 5px;font-size:12px}.tabulator-menu .tabulator-menu-item{box-sizing:border-box;-webkit-user-select:none;user-select:none;padding:5px 10px;position:relative}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-disabled{opacity:.5}@media (hover:hover) and (pointer:fine){.tabulator-menu .tabulator-menu-item:not(.tabulator-menu-item-disabled):hover{cursor:pointer;background:#efefef}}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-submenu{padding-right:25px}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-submenu:after{content:\"\";vertical-align:top;border:1px solid #ddd;border-width:1px 1px 0 0;width:7px;height:7px;display:inline-block;position:absolute;top:calc(5px + .4em);right:10px;transform:rotate(45deg)}.tabulator-menu .tabulator-menu-separator{border-top:1px solid #ddd}.tabulator-edit-list{-webkit-overflow-scrolling:touch;max-height:200px;font-size:14px;overflow-y:auto}.tabulator-edit-list .tabulator-edit-list-item{color:#333;outline:none;padding:4px}.tabulator-edit-list .tabulator-edit-list-item.active{color:#fff;background:#1d68cd}.tabulator-edit-list .tabulator-edit-list-item.active.focused{outline:1px solid #ffffff80}.tabulator-edit-list .tabulator-edit-list-item.focused{outline:1px solid #1d68cd}@media (hover:hover) and (pointer:fine){.tabulator-edit-list .tabulator-edit-list-item:hover{color:#fff;cursor:pointer;background:#1d68cd}}.tabulator-edit-list .tabulator-edit-list-placeholder{color:#333;text-align:center;padding:4px}.tabulator-edit-list .tabulator-edit-list-group{color:#333;border-bottom:1px solid #ddd;padding:6px 4px 4px;font-weight:700}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-2,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-2{padding-left:12px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-3,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-3{padding-left:20px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-4,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-4{padding-left:28px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-5,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-5{padding-left:36px}.tabulator.tabulator-ltr{direction:ltr}.tabulator.tabulator-rtl{text-align:initial;direction:rtl}.tabulator.tabulator-rtl .tabulator-header .tabulator-col{border-left:1px solid #ddd;border-right:initial;text-align:initial}.tabulator.tabulator-rtl .tabulator-header .tabulator-col.tabulator-col-group .tabulator-col-group-cols{margin-left:-1px;margin-right:0}.tabulator.tabulator-rtl .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-title{padding-left:25px;padding-right:0}.tabulator.tabulator-rtl .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter{left:8px;right:auto}.tabulator.tabulator-rtl .tabulator-tableholder .tabulator-range-overlay .tabulator-range.tabulator-range-active:after{content:\"\";background-color:#2975dd;border-radius:999px;width:6px;height:6px;position:absolute;bottom:-3px;left:-3px;right:auto}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell{border-left:1px solid #ddd;border-right:initial}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell .tabulator-data-tree-branch{border-left:initial;border-right:2px solid #ddd;border-bottom-right-radius:1px;border-bottom-left-radius:0;margin-left:5px;margin-right:0}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell .tabulator-data-tree-control{margin-left:5px;margin-right:0}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left{border-left:2px solid #ddd}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-right{border-right:2px solid #ddd}.tabulator.tabulator-rtl .tabulator-row .tabulator-col-resize-handle:last-of-type{width:3px;margin-left:0;margin-right:-3px}.tabulator.tabulator-rtl .tabulator-footer .tabulator-calcs-holder{text-align:initial}.tabulator-print-fullscreen{z-index:10000;position:absolute;inset:0}body.tabulator-print-fullscreen-hide>:not(.tabulator-print-fullscreen){display:none!important}.tabulator-print-table{border-collapse:collapse}.tabulator-print-table .tabulator-data-tree-branch{vertical-align:middle;border-bottom:2px solid #ddd;border-left:2px solid #ddd;border-bottom-left-radius:1px;width:7px;height:9px;margin-top:-9px;margin-right:5px;display:inline-block}.tabulator-print-table .tabulator-print-table-group{box-sizing:border-box;background:#ccc;border-top:1px solid #999;border-bottom:1px solid #999;border-right:1px solid #ddd;min-width:100%;padding:5px 5px 5px 10px;font-weight:700}@media (hover:hover) and (pointer:fine){.tabulator-print-table .tabulator-print-table-group:hover{cursor:pointer;background-color:#0000001a}}.tabulator-print-table .tabulator-print-table-group.tabulator-group-visible .tabulator-arrow{border:6px solid #0000;border-top-color:#666;border-bottom:0;margin-right:10px}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-1 td{padding-left:30px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-2 td{padding-left:50px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-3 td{padding-left:70px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-4 td{padding-left:90px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-5 td{padding-left:110px!important}.tabulator-print-table .tabulator-print-table-group .tabulator-group-toggle{display:inline-block}.tabulator-print-table .tabulator-print-table-group .tabulator-arrow{vertical-align:middle;border:6px solid #0000;border-left-color:#666;border-right:0;width:0;height:0;margin-right:16px;display:inline-block}.tabulator-print-table .tabulator-print-table-group span{color:#d00;margin-left:10px}.tabulator-print-table .tabulator-data-tree-control{vertical-align:middle;background:#0000001a;border:1px solid #333;border-radius:2px;justify-content:center;align-items:center;width:11px;height:11px;margin-right:5px;display:inline-flex;overflow:hidden}@media (hover:hover) and (pointer:fine){.tabulator-print-table .tabulator-data-tree-control:hover{cursor:pointer;background:#0003}}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-collapse{background:0 0;width:1px;height:7px;display:inline-block;position:relative}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-collapse:after{content:\"\";background:#333;width:7px;height:1px;position:absolute;top:3px;left:-3px}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-expand{background:#333;width:1px;height:7px;display:inline-block;position:relative}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-expand:after{content:\"\";background:#333;width:7px;height:1px;position:absolute;top:3px;left:-3px}.tabulator{box-shadow:none;color:#000000de;border:1px solid #22242626;border-radius:.285714rem;width:100%;margin:1em 0}.tabulator .tabulator-header{box-shadow:none;color:#000000de;font-style:none;text-transform:none;border-bottom:1px solid #2224261a;font-weight:700}.tabulator .tabulator-header,.tabulator .tabulator-header .tabulator-col{background-color:#f9fafb;border-right:none}.tabulator .tabulator-header .tabulator-col .tabulator-col-content{padding:.928571em .785714em}.tabulator .tabulator-tableholder .tabulator-table{background-color:#0000}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs{background:#f2f2f2!important}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs.tabulator-calcs-top{border-bottom:2px solid #ddd}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs.tabulator-calcs-bottom{border-top:2px solid #ddd}.tabulator .tabulator-footer{box-shadow:none;color:#000000de;text-align:right;text-transform:none;background:#f9fafb;border-top:1px solid #22242626;padding:.785714em;font-style:normal;font-weight:400}.tabulator .tabulator-footer .tabulator-calcs-holder{margin:-.785714em -.785714em .785714em;background:#fff!important}.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row{background:#fff!important}.tabulator .tabulator-footer .tabulator-calcs-holder:only-child{border-bottom:none;margin-bottom:-.785714em}.tabulator .tabulator-footer .tabulator-spreadsheet-tabs{margin-top:calc(-.78571em - 5px)}.tabulator .tabulator-footer .tabulator-spreadsheet-tabs .tabulator-spreadsheet-tab.tabulator-spreadsheet-tab-active{color:#d00}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell.positive,.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.positive{box-shadow:inset 0 0 #a3c293;color:#21ba45!important;background:#fcfff5!important}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell.positive:hover,.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.positive:hover{color:#13ae38!important;background:#f7ffe6!important}}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell.negative,.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.negative{box-shadow:inset 0 0 #e0b4b4;color:#db2828!important;background:#fff6f6!important}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell.negative:hover,.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.negative:hover{color:#d41616!important;background:#ffe7e7!important}}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell.error,.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.error{box-shadow:inset 0 0 #e0b4b4;color:#db2828!important;background:#fff6f6!important}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell.error:hover,.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.error:hover{color:#d12323!important;background:#ffe7e7!important}}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell.warning,.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.warning{box-shadow:inset 0 0 #c9ba9b;color:#f2c037!important;background:#fffaf3!important}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell.warning:hover,.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.warning:hover{color:#f1bb29!important;background:#fff4e4!important}}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell.active,.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.active{box-shadow:inset 0 0 #000000de;color:#000000de!important;background:#e0e0e0!important}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell.active:hover,.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.active:hover{color:#13ae38!important;background:#f7ffe6!important}}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell.active,.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.active{color:#0003;pointer-events:none}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.disabled:hover{color:#0003;pointer-events:none}}.tabulator.inverted{color:#ffffffe6;background:#333;border:none}.tabulator.inverted .tabulator-header{color:#ffffffe6;background-color:#00000026}.tabulator.inverted .tabulator-header,.tabulator.inverted .tabulator-header .tabulator-col{border-color:#ffffff1a!important}.tabulator.inverted .tabulator-tableholder .tabulator-table .tabulator-row{color:#ffffffe6;border:none}.tabulator.inverted .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell{border-color:#ffffff1a!important}.tabulator.inverted .tabulator-footer{background:#fff}.tabulator.striped .tabulator-row:nth-child(2n){background-color:#f2f2f2}.tabulator.celled{border:1px solid #22242626}.tabulator.celled .tabulator-header .tabulator-col,.tabulator.celled .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell{border-right:1px solid #2224261a}.tabulator[class*=single\\ line] .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell{border-right:none}.tabulator.red{border-top:.2em solid #db2828}.tabulator.inverted.red{color:#fff!important;background-color:#db2828!important}.tabulator.orange{border-top:.2em solid #f2711c}.tabulator.inverted.orange{color:#fff!important;background-color:#f2711c!important}.tabulator.yellow{border-top:.2em solid #fbbd08}.tabulator.inverted.yellow{color:#fff!important;background-color:#fbbd08!important}.tabulator.olive{border-top:.2em solid #b5cc18}.tabulator.inverted.olive{color:#fff!important;background-color:#b5cc18!important}.tabulator.green{border-top:.2em solid #21ba45}.tabulator.inverted.green{color:#fff!important;background-color:#21ba45!important}.tabulator.teal{border-top:.2em solid #00b5ad}.tabulator.inverted.teal{color:#fff!important;background-color:#00b5ad!important}.tabulator.blue{border-top:.2em solid #2185d0}.tabulator.inverted.blue{color:#fff!important;background-color:#2185d0!important}.tabulator.violet{border-top:.2em solid #6435c9}.tabulator.inverted.violet{color:#fff!important;background-color:#6435c9!important}.tabulator.purple{border-top:.2em solid #a333c8}.tabulator.inverted.purple{color:#fff!important;background-color:#a333c8!important}.tabulator.pink{border-top:.2em solid #e03997}.tabulator.inverted.pink{color:#fff!important;background-color:#e03997!important}.tabulator.brown{border-top:.2em solid #a5673f}.tabulator.inverted.brown{color:#fff!important;background-color:#a5673f!important}.tabulator.grey{border-top:.2em solid #767676}.tabulator.inverted.grey{color:#fff!important;background-color:#767676!important}.tabulator.black{border-top:.2em solid #1b1c1d}.tabulator.inverted.black{color:#fff!important;background-color:#1b1c1d!important}.tabulator.padded .tabulator-header .tabulator-col .tabulator-col-content{padding:1em}.tabulator.padded .tabulator-header .tabulator-col .tabulator-col-content .tabulator-arrow{top:20px}.tabulator.padded .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell{padding:1em}.tabulator.padded.very .tabulator-header .tabulator-col .tabulator-col-content{padding:1.5em}.tabulator.padded.very .tabulator-header .tabulator-col .tabulator-col-content .tabulator-arrow{top:26px}.tabulator.padded.very .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell{padding:1.5em}.tabulator.compact .tabulator-header .tabulator-col .tabulator-col-content{padding:.5em .7em}.tabulator.compact .tabulator-header .tabulator-col .tabulator-col-content .tabulator-arrow{top:12px}.tabulator.compact .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell{padding:.5em .7em}.tabulator.compact.very .tabulator-header .tabulator-col .tabulator-col-content{padding:.4em .6em}.tabulator.compact.very .tabulator-header .tabulator-col .tabulator-col-content .tabulator-arrow{top:10px}.tabulator.compact.very .tabulator-tableholder .tabulator-table .tabulator-row .tabulator-cell{padding:.4em .6em}.tabulator-row{border-bottom:1px solid #2224261a}.tabulator-row.tabulator-row-even{background-color:#fff}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-selectable:hover{box-shadow:inset 0 0 #000000de;color:#000000de!important;background:#e0e0e0!important}}.tabulator-row.tabulator-selected{background-color:#9abcea!important}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-selected:hover{cursor:pointer;background-color:#769bcc!important}}.tabulator-row.tabulator-moving{pointer-events:none!important}.tabulator-row .tabulator-cell{vertical-align:middle;border-right:none;padding:.785714em}.tabulator-row .tabulator-cell:last-of-type{border-right:none}.tabulator-row .tabulator-cell.tabulator-row-header{border-bottom:none}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle{color:#fff}.tabulator-row.tabulator-group{background:#fafafa}.tabulator-row.tabulator-group span{color:#666}.tabulator-menu{background:#fff}@media (hover:hover) and (pointer:fine){.tabulator-menu .tabulator-menu-item:not(.tabulator-menu-item-disabled):hover{background:#f9fafb}}.tabulator-edit-select-list{background:#fff}.tabulator-edit-select-list .tabulator-edit-select-list-item.active{color:#fff}.tabulator-edit-select-list .tabulator-edit-select-list-item.active.focused{outline:1px solid #ffffff80}@media (hover:hover) and (pointer:fine){.tabulator-edit-select-list .tabulator-edit-select-list-item:hover{color:#fff}}.tabulator-edit-select-list .tabulator-edit-select-list-notice{color:inherit}.tabulator-print-table .tabulator-print-table-group{background:#fafafa}.tabulator-print-table .tabulator-print-table-group span{color:#666}"); window.layer = layui_layer.default; window.Tabulator = tabulator_tables.default; window.Toastify = toastify_js.default; window.localforage = localforage$1.default; window.Viewer = viewerjs.default; window.md5 = blueimp_md5.default; var currentHref = window.location.href; var isJavdbSite = currentHref.includes("javdb"); var isMissavSite = /missav\.(ws|live|com|ai)/.test(currentHref); var isSearchOrUserPage = currentHref.includes("/search?q") || currentHref.includes("/search/") || currentHref.includes("/users/"); var JAVDB = "javdb"; var ACTOR = "actor"; var ACTRESS = "actress"; var CENSORED = "censored"; var UNCENSORED = "uncensored"; function injectCss(css) { if (css) { if (css.includes("<style>")) document.head.insertAdjacentHTML("beforeend", css); else { const el = document.createElement("style"); el.textContent = css; document.head.appendChild(el); } } } var GFRIENDS_SOURCES = [{ name: "jsDelivr (全球CDN)", json: "https://cdn.jsdelivr.net/gh/gfriends/gfriends/Filetree.json", base: "https://cdn.jsdelivr.net/gh/gfriends/gfriends/Content/" }, { name: "GitHub Raw (备用)", json: "https://raw.githubusercontent.com/gfriends/gfriends/master/Filetree.json", base: "https://raw.githubusercontent.com/gfriends/gfriends/master/Content/" }]; var GFRIENDS_CDN_INDEX_KEY = "jhs_img_cdn_index"; var GFRIENDS_DB_NAME = "GfriendsAvatarDB"; var FILETREE_STORE = "filetreeStore"; var FILETREE_DATA_KEY = "filetree_data"; async function withLoading(fn) { const loadingHandle = loading(); try { return await fn(); } finally { loadingHandle.close(); } } var FiletreeDb = class { db = null; async open() { if (this.db) return this.db; return new Promise((resolve, reject) => { const request = indexedDB.open(GFRIENDS_DB_NAME, 1); request.onupgradeneeded = () => { const database = request.result; this.db = database; if (!database.objectStoreNames.contains("filetreeStore")) database.createObjectStore(FILETREE_STORE); }; request.onsuccess = () => { const database = request.result; this.db = database; resolve(database); }; request.onerror = () => { console.error("IndexedDB open error:", request.error); reject(new Error("Failed to open IndexedDB")); }; }); } async get(key) { const db = await this.open(); return new Promise((resolve) => { const request = db.transaction([FILETREE_STORE], "readonly").objectStore(FILETREE_STORE).get(key); request.onsuccess = () => resolve(request.result ?? null); request.onerror = () => resolve(null); }); } async set(key, value) { const db = await this.open(); return new Promise((resolve, reject) => { const request = db.transaction([FILETREE_STORE], "readwrite").objectStore(FILETREE_STORE).put(value, key); request.onsuccess = () => resolve(); request.onerror = () => { console.error("IndexedDB set error:", request.error); reject(new Error("Failed to write to IndexedDB")); }; }); } }; var filetreeDb = new FiletreeDb(); var cachedFiletreeRaw = null; var cachedFiletreeParsed = null; function clearCache() { cachedFiletreeRaw = null; cachedFiletreeParsed = null; } function getCurrentCdnSource() { const rawIndex = parseInt(localStorage.getItem("jhs_img_cdn_index") || "0", 10); const index = rawIndex >= 0 && rawIndex < GFRIENDS_SOURCES.length ? rawIndex : 0; const source = GFRIENDS_SOURCES[index]; return { json: source.json, base: source.base, index }; } function parseFiletree(data) { if (!data || !data.Content) return null; const result = {}; const content = data.Content; const baseUrl = getCurrentCdnSource().base; for (const folderKey in content) { const encodedFolder = encodeURIComponent(folderKey); const folder = content[folderKey]; for (const fileName in folder) { let namePart = fileName.replace(/\.jpg$/i, "").split("-")[0]; if (namePart.startsWith("AI-Fix-")) namePart = namePart.substring(7); const normalizedName = namePart.toLowerCase().trim(); if (normalizedName.length > 0) { const urlPath = folder[fileName]; const queryIndex = urlPath.indexOf("?"); let encodedPath; let queryString = ""; if (queryIndex > -1) { encodedPath = encodeURIComponent(urlPath.substring(0, queryIndex)); queryString = urlPath.substring(queryIndex); } else encodedPath = encodeURIComponent(urlPath); const fullUrl = `${baseUrl}${encodedFolder}/${encodedPath}${queryString}`; if (!result[normalizedName]) result[normalizedName] = []; if (!result[normalizedName].includes(fullUrl)) result[normalizedName].push(fullUrl); } } } return result; } async function ensureFiletreeLoaded() { if (cachedFiletreeRaw && cachedFiletreeParsed) return; let idbData = null; try { idbData = await filetreeDb.get(FILETREE_DATA_KEY); } catch (error) { console.error("读取 IndexedDB 失败:", error); } if (idbData && idbData.Content) { cachedFiletreeRaw = idbData; cachedFiletreeParsed = parseFiletree(idbData); if (cachedFiletreeParsed) return; } show.info("正在载入头像数据源..."); const response = await fetch(getCurrentCdnSource().json); if (!response.ok) throw new Error(`请求头像源失败: ${response.status}`); const filetree = await response.json(); if (filetree && filetree.Content) { cachedFiletreeRaw = filetree; cachedFiletreeParsed = parseFiletree(filetree); try { await filetreeDb.set(FILETREE_DATA_KEY, filetree); clog.debug("载入头像数据源并写入缓存成功!"); } catch (error) { clog.error(error); show.error("头像数据源写入缓存失败,可能磁盘已满或其他权限问题。"); } return; } console.log(filetree); throw new Error("解析头像数据源失败"); } async function loadGfriends(actressNames) { try { await withLoading(() => ensureFiletreeLoaded()); } catch (error) { show.error(error); return []; } const filetreeIndex = cachedFiletreeParsed; if (!filetreeIndex) return []; const urlSet = new Set(); const normalizedNames = actressNames.map((name) => name.toLowerCase().trim()).filter((name) => name.length > 0); if (normalizedNames.length === 0) return []; for (const name of normalizedNames) { const urls = filetreeIndex[name]; if (urls) urls.forEach((url) => urlSet.add(url)); } return Array.from(urlSet); } var featureFlags = { upgradeSignature300s: true, caseInsensitiveCarNum: true, autoPageReplaceState: true, wantWatchBatchImport: true, movieShowTypeVisibility: true, storageCacheDeepCopy: true, webdavIdempotentMkdir: true, westernCarFormat: true, actressUserSelectAll: true, navBarNoPaste: false, translatePlugin: true, screenShotPlugin: true, javDbApiAggregate: true, magnetHubPlugin: true, imageRecognitionPlugin: true, fc2By123AvPlugin: true }; (() => { try { const stored = JSON.parse(localStorage.getItem("jhs_upgrade_flags") || "{}"); Object.assign(featureFlags, stored); } catch {} })(); var WebDavClient = class { davUrl; username; password; folderName; constructor(davUrl, username, password) { this.davUrl = davUrl.endsWith("/") ? davUrl : davUrl + "/"; this.username = username; this.password = password; this.folderName = null; } _getAuthHeaders() { return { Authorization: `Basic ${btoa(`${this.username}:${this.password}`)}`, Depth: "1" }; } _sendRequest(method, path, headers = {}, body) { return new Promise((resolve, reject) => { const url = this.davUrl + path; const requestHeaders = { ...this._getAuthHeaders(), ...headers }; GM_xmlhttpRequest({ method, url, headers: requestHeaders, data: body, onload: (response) => { if (response.status >= 200 && response.status < 300) resolve(response); else { console.error(response); reject(new Error(`请求失败 ${response.status}: ${response.statusText}`)); } }, onerror: (error) => { console.error("请求WebDav发生错误:", error); reject(new Error("请求WebDav失败, 请检查服务是否启动, 凭证是否正确")); } }); }); } async checkFolderExists(path) { try { await this._sendRequest("PROPFIND", path, { Depth: "0" }); return true; } catch (error) { const statusMatch = String(error?.message || "").match(/请求失败 (\d+):/); if ((statusMatch ? parseInt(statusMatch[1], 10) : 0) === 404) return false; throw error; } } async createFolder(folderName) { if (!featureFlags.webdavIdempotentMkdir) { await this._sendRequest("MKCOL", folderName); return; } try { if (!await this.checkFolderExists(folderName)) { clog.log(`目录 ${folderName} 不存在,正在创建...`); await this._sendRequest("MKCOL", folderName, { Depth: "0" }); clog.log(`目录 ${folderName} 创建成功。`); } } catch (error) { clog.error(`创建目录 ${folderName} 时发生错误:`, error); throw error; } } async backup(folder, filename, content) { await this.createFolder(folder); const path = folder + "/" + filename; await this._sendRequest("PUT", path, { "Content-Type": "text/plain" }, content); } async getFileList(folder) { const responseText = (await this._sendRequest("PROPFIND", folder, { "Content-Type": "application/xml" }, "<?xml version=\"1.0\"?>\n <d:propfind xmlns:d=\"DAV:\">\n <d:prop>\n <d:displayname />\n <d:getcontentlength />\n <d:creationdate />\n <d:getlastmodified />\n <d:iscollection />\n </d:prop>\n </d:propfind>\n ")).responseText; const responses = new DOMParser().parseFromString(responseText, "text/xml").getElementsByTagNameNS("DAV:", "response"); const items = []; for (let index = 0; index < responses.length; index++) { if (index === 0) continue; const responseElement = responses[index]; console.log(responseElement); const displayName = responseElement.getElementsByTagNameNS("DAV:", "displayname")[0]?.textContent || ""; const contentLength = responseElement.getElementsByTagNameNS("DAV:", "getcontentlength")[0]?.textContent || "0"; const createTime = responseElement.getElementsByTagNameNS("DAV:", "creationdate")[0]?.textContent || responseElement.getElementsByTagNameNS("DAV:", "getlastmodified")[0]?.textContent || ""; if (contentLength !== "0") items.push({ fileId: displayName, name: displayName, size: Number(contentLength), createTime }); } items.reverse(); return items; } async deleteFile(filename) { const path = this.folderName + "/" + encodeURI(filename); await this._sendRequest("DELETE", path, { "Cache-Control": "no-cache" }); } async getBackupList(folder) { this.folderName = folder; await this.createFolder(folder); return this.getFileList(folder); } async getFileContent(filename) { const path = this.folderName + "/" + filename; return (await this._sendRequest("GET", path, { Accept: "application/octet-stream" })).responseText; } }; function createLoading() { const container = document.createElement("div"); container.className = "loading-container"; const animation = document.createElement("div"); animation.className = "loading-animation"; container.appendChild(animation); document.body.appendChild(container); return { close: () => { if (container && container.parentNode) container.parentNode.removeChild(container); } }; } var createToast = (message, type, position, duration, onClick) => { let userOptions; if (typeof position == "object") userOptions = position; else { userOptions = typeof duration == "object" ? duration : onClick || {}; userOptions.gravity = position || "top"; userOptions.position = typeof duration == "string" ? duration : "center"; } if (!userOptions.gravity || userOptions.gravity === "center") userOptions.offset = { y: "calc(50vh - 150px)" }; const infoFrom = "#60A5FA"; const infoTo = "#93C5FD"; const successFrom = "#10B981"; const successTo = "#6EE7B7"; const errorFrom = "#EF4444"; const errorTo = "#FCA5A5"; const baseStyle = { borderRadius: "12px", color: "white", padding: "12px 16px", boxShadow: "0 4px 6px rgba(0,0,0,0.1)", minWidth: "150px", textAlign: "center", zIndex: 999999999 }; const toastOptions = { text: message, duration: 1e3, close: false, gravity: "top", position: "center", style: { info: { ...baseStyle, background: `linear-gradient(to right, ${infoFrom}, ${infoTo})` }, success: { ...baseStyle, background: `linear-gradient(to right, ${successFrom}, ${successTo})` }, error: { ...baseStyle, background: `linear-gradient(to right, ${errorFrom}, ${errorTo})` } }[type], stopOnFocus: true, oldestFirst: false, ...userOptions }; if (toastOptions.duration === -1) toastOptions.close = true; const toast = Toastify(toastOptions); toast.showToast(); toast.closeShow = () => { toast.toastElement.remove(); }; return toast; }; var show$1 = { ok: (message, position = "center", duration, onClick) => createToast(message, "success", position, duration, onClick), error: (message, position = "center", duration, onClick) => createToast(message, "error", position, duration, onClick), info: (message, position = "center", duration, onClick) => createToast(message, "info", position, duration, onClick) }; function failWithToast(msg) { show$1.error(msg); throw new Error(msg); } var VIEWER_CONFIG = (ctx) => ({ zIndex: 999999990, navbar: false, zoomOnWheel: false, zoomRatio: .1, toggleOnDblclick: false, toolbar: { zoomIn: 1, zoomOut: 1, reset: 1, rotateLeft: 0, rotateRight: 0, flipHorizontal: 0, flipVertical: 0 }, title: false, keyboard: false, viewed() { const viewer = ctx.viewerRef.current; viewer.zoomTo(1.4); const offsetX = (viewer.viewerData.width - viewer.imageData.width) / 2; viewer.moveTo(offsetX, 0); }, shown() { const viewer = ctx.viewerRef.current; if (ctx.isTemporary) ctx.container.remove(); document.documentElement.style.overflow = "hidden"; document.body.style.overflow = "hidden"; viewer.handleKeydown = function(event) { if (event.key === "Escape" || event.key === " ") { event.preventDefault(); event.stopPropagation(); viewer.destroy(); document.removeEventListener("keydown", viewer.handleKeydown); document.documentElement.style.overflow = ""; document.body.style.overflow = ""; ctx.resetOverflow(); } }; document.addEventListener("keydown", viewer.handleKeydown); }, hidden() { const viewer = ctx.viewerRef.current; if (viewer && viewer.handleKeydown) document.removeEventListener("keydown", viewer.handleKeydown); viewer.destroy(); document.documentElement.style.overflow = ""; document.body.style.overflow = ""; ctx.resetOverflow(); } }); var VOID_TAGS = new Set([ "img", "input", "br", "hr", "meta", "link", "area", "base", "col", "embed", "param", "source", "track", "wbr" ]); var REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"); function isReactElement(node) { return node != null && typeof node === "object" && "type" in node && "props" in node && typeof node.props === "object" && node.props !== null; } function escapeText(s) { return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); } function escapeAttr(s) { return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); } function sanitizeUrl(url) { const trimmed = url.trim().toLowerCase(); if (trimmed.startsWith("javascript:") || trimmed.startsWith("data:") || trimmed.startsWith("vbscript:")) return "#"; return url; } function camelToKebab(s) { return s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`); } function styleToCss(style) { return Object.entries(style).filter(([, v]) => v != null && v !== false).map(([k, v]) => `${camelToKebab(k)}:${v}`).join(";"); } function renderAttrs(props) { const parts = []; for (const [key, value] of Object.entries(props)) { if (value == null || value === false) continue; if (key === "children") continue; if (key === "dangerouslySetInnerHTML") continue; if (key.startsWith("on")) continue; if (key === "className") { if (value !== "") parts.push(`class="${escapeAttr(String(value))}"`); continue; } if (key === "htmlFor") { parts.push(`for="${escapeAttr(String(value))}"`); continue; } if (key === "style") { if (typeof value === "string") parts.push(`style="${escapeAttr(value)}"`); else if (typeof value === "object" && value !== null) { const css = styleToCss(value); if (css) parts.push(`style="${escapeAttr(css)}"`); } continue; } if (value === true) { parts.push(key); continue; } const attrValue = key === "href" || key === "src" ? sanitizeUrl(String(value)) : String(value); parts.push(`${key}="${escapeAttr(attrValue)}"`); } return parts.length ? " " + parts.join(" ") : ""; } function renderChildren(children) { if (children == null || typeof children === "boolean") return ""; if (typeof children === "string") return escapeText(children); if (typeof children === "number" || typeof children === "bigint") return String(children); if (Array.isArray(children)) return children.map((c) => renderChildren(c)).join(""); if (isReactElement(children)) return jsxToString(children); return ""; } function jsxToString(node) { if (node == null || typeof node === "boolean") return ""; if (typeof node === "string") return escapeText(node); if (typeof node === "number" || typeof node === "bigint") return String(node); if (Array.isArray(node)) return node.map(jsxToString).join(""); if (!isReactElement(node)) return ""; const { type, props } = node; if (type === REACT_FRAGMENT_TYPE) return renderChildren(props.children); if (typeof type === "function") return jsxToString(type(props)); if (typeof type === "string") { const tag = type; const attrs = renderAttrs(props); if (VOID_TAGS.has(tag)) return `<${tag}${attrs} />`; const dsi = props.dangerouslySetInnerHTML; if (dsi && typeof dsi.__html === "string") return `<${tag}${attrs}>${dsi.__html}</${tag}>`; return `<${tag}${attrs}>${renderChildren(props.children)}</${tag}>`; } return ""; } var require_react_jsx_runtime_production = __commonJSMin(((exports) => { var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"); var REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"); function jsxProd(type, config, maybeKey) { var key = null; void 0 !== maybeKey && (key = "" + maybeKey); void 0 !== config.key && (key = "" + config.key); if ("key" in config) { maybeKey = {}; for (var propName in config) "key" !== propName && (maybeKey[propName] = config[propName]); } else maybeKey = config; config = maybeKey.ref; return { $$typeof: REACT_ELEMENT_TYPE, type, key, ref: void 0 !== config ? config : null, props: maybeKey }; } exports.Fragment = REACT_FRAGMENT_TYPE; exports.jsx = jsxProd; exports.jsxs = jsxProd; })); var import_jsx_runtime = __commonJSMin(((exports, module) => { module.exports = require_react_jsx_runtime_production(); }))(); var INDENT_BEFORE = "\n "; var INDENT_BETWEEN = "\n "; var INDENT_AFTER = "\n "; var URL_PATTERN = /(?:https?|ftp):\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|]/gi; function linkifySafe(text) { const nodes = []; let lastIndex = 0; let match; URL_PATTERN.lastIndex = 0; while ((match = URL_PATTERN.exec(text)) !== null) { if (match.index > lastIndex) nodes.push(text.slice(lastIndex, match.index)); const url = match[0]; nodes.push((0, import_jsx_runtime.jsx)("a", { href: sanitizeUrl(url), target: "_blank", rel: "noopener noreferrer", children: url }, match.index)); lastIndex = match.index + url.length; } if (lastIndex < text.length) nodes.push(text.slice(lastIndex)); return nodes; } function LoggerLogEntry({ timeStr, messageType, message }) { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ INDENT_BEFORE, (0, import_jsx_runtime.jsxs)("span", { className: "console-logger-timestamp", children: [ "[", timeStr, "]" ] }), INDENT_BETWEEN, (0, import_jsx_runtime.jsx)("span", { className: "console-logger-message", "data-type": messageType, children: linkifySafe(message) }), INDENT_AFTER ] }); } var LOG_TYPE_STYLES = { base: { label: "信息", background: "#e8f4fd", borderLeftColor: "#3498db" }, warn: { label: "警告", background: "#fef9e7", borderLeftColor: "#f39c12" }, error: { label: "错误", background: "#fdedec", borderLeftColor: "#e74c3c" }, debug: { label: "调试", background: "#f4f6f6", borderLeftColor: "#95a5a6" } }; var FILTER_TYPE_MAP = { base: [ "base", "warn", "error" ], warn: ["warn"], error: ["error"], debug: [ "base", "warn", "error", "debug" ] }; var MAXIMIZE_KEY = "jhs_clog_maximize"; var EXPAND_KEY = "jhs_clog_expand"; var FILTER_KEY = "jhs_clog_filter"; function createContainer(logger) { logger.container = document.createElement("div"); logger.container.className = "console-logger-container"; logger.container.style.display = "none"; logger.toggleBtn = document.createElement("div"); logger.toggleBtn.className = "console-logger-toggle collapsed"; logger.container.appendChild(logger.toggleBtn); logger.window = document.createElement("div"); logger.window.className = "console-logger-window collapsed"; const header = document.createElement("div"); header.className = "console-logger-header"; const title = document.createElement("div"); title.className = "console-logger-title"; title.textContent = "JHS V3.3.2"; const controls = document.createElement("div"); controls.className = "console-logger-controls"; logger.maximizeBtn = document.createElement("button"); logger.maximizeBtn.textContent = ""; logger.maximizeBtn.classList.add("console-logger-maximize-toggle"); controls.appendChild(logger.maximizeBtn); const clearBtn = document.createElement("button"); clearBtn.textContent = "清空"; clearBtn.addEventListener("click", () => logger.clear()); controls.appendChild(clearBtn); header.appendChild(title); header.appendChild(controls); logger.filtersContainer = document.createElement("div"); logger.filtersContainer.className = "console-logger-filters"; logger.filterButtonGroup = document.createElement("div"); logger.filterButtonGroup.className = "console-logger-filter-group"; logger.filtersContainer.appendChild(logger.filterButtonGroup); logger.scrollToBottomBtn = document.createElement("button"); logger.scrollToBottomBtn.className = "console-logger-scroll-to-bottom"; logger.scrollToBottomBtn.textContent = "到底部"; logger.filtersContainer.appendChild(logger.scrollToBottomBtn); logger.content = document.createElement("div"); logger.content.className = "console-logger-content jhs-scrollbar"; logger.window.appendChild(header); logger.window.appendChild(logger.filtersContainer); logger.window.appendChild(logger.content); logger.container.appendChild(logger.window); document.body.appendChild(logger.container); Object.keys(LOG_TYPE_STYLES).forEach((typeName) => { const filterBtn = document.createElement("div"); filterBtn.className = "console-logger-filter"; if (typeName === logger.currentFilter) filterBtn.classList.add("active"); filterBtn.textContent = LOG_TYPE_STYLES[typeName].label; filterBtn.dataset.type = typeName; filterBtn.addEventListener("click", () => logger.setFilter(typeName)); logger.filterButtonGroup.appendChild(filterBtn); }); } function bindEvents(logger) { logger.toggleBtn.addEventListener("click", () => { toggleExpandCollapsed(logger); }); logger.maximizeBtn.addEventListener("click", () => toggleMaximize(logger)); logger.scrollToBottomBtn.addEventListener("click", () => { logger.content.scrollTop = logger.content.scrollHeight; logger.userScrolledUp = false; }); logger.content.addEventListener("scroll", () => { logger.userScrolledUp = !(logger.content.scrollHeight - logger.content.clientHeight <= logger.content.scrollTop + 5); }); logger.content.addEventListener("wheel", (evt) => { const atTop = logger.content.scrollTop === 0; const atBottom = logger.content.scrollHeight - logger.content.clientHeight <= logger.content.scrollTop + 1; if (atTop && evt.deltaY < 0 || atBottom && evt.deltaY > 0) { evt.preventDefault(); evt.stopPropagation(); } }, { passive: false }); } function toggleExpandCollapsed(logger) { const isCollapsed = logger.window.classList.toggle("collapsed"); logger.toggleBtn.classList.toggle("collapsed"); if (isCollapsed) localStorage.setItem(EXPAND_KEY, "no"); else { localStorage.setItem(EXPAND_KEY, "yes"); reRenderAllLogs(logger); } } function checkInitialCollapseState(logger) { const stored = localStorage.getItem(EXPAND_KEY); if (stored && stored !== "no") { logger.window.classList.toggle("collapsed"); logger.toggleBtn.classList.toggle("collapsed"); setTimeout(() => { logger.content.scrollTop = logger.content.scrollHeight; }, 0); } else { logger.window.classList.add("collapsed"); logger.toggleBtn.classList.add("collapsed"); } } function checkInitialMaximizeState(logger) { if (localStorage.getItem(MAXIMIZE_KEY) === "maximized") { logger.window.classList.add("maximized"); logger.maximizeBtn.classList.add("active"); } } function toggleMaximize(logger) { const isMaximized = logger.window.classList.toggle("maximized"); logger.maximizeBtn.classList.toggle("active", isMaximized); if (isMaximized) localStorage.setItem(MAXIMIZE_KEY, "maximized"); else localStorage.setItem(MAXIMIZE_KEY, "minimized"); if (!logger.window.classList.contains("collapsed")) logger.content.scrollTop = logger.content.scrollHeight; } function renderLog(logger, entry) { if (logger.container.style.display === "none") return; if (logger.window.classList.contains("collapsed")) return; if (!(FILTER_TYPE_MAP[logger.currentFilter] || []).includes(entry.type)) return; const el = _createLogElement(entry); logger.content.appendChild(el); if (!logger.window.classList.contains("collapsed") && !logger.userScrolledUp) logger.content.scrollTop = logger.content.scrollHeight; } function reRenderAllLogs(logger) { if (logger.container.style.display !== "none") { if (!logger.window.classList.contains("collapsed")) setTimeout(() => { logger.content.innerHTML = ""; if (logger.logs.length === 0) return; const filterTypes = FILTER_TYPE_MAP[logger.currentFilter] || []; const fragment = document.createDocumentFragment(); logger.logs.forEach((entry) => { if (filterTypes.includes(entry.type)) { const el = _createLogElement(entry); fragment.appendChild(el); } }); logger.content.appendChild(fragment); logger.content.scrollTop = logger.content.scrollHeight; }, 0); } } function _createLogElement(entry) { const el = document.createElement("div"); el.className = "console-logger-entry"; el.dataset.type = entry.type; el.dataset.id = String(entry.id); const style = LOG_TYPE_STYLES[entry.type] || LOG_TYPE_STYLES.base; el.style.borderLeft = "3px solid " + style.borderLeftColor; el.style.background = style.background; const timeStr = (entry.timestamp instanceof Date ? entry.timestamp : new Date(entry.timestamp)).toTimeString().split(" ")[0]; el.innerHTML = jsxToString((0, import_jsx_runtime.jsx)(LoggerLogEntry, { timeStr, messageType: entry.messageType, message: entry.message })); return el; } function setFilter(logger, filterType) { if (logger.currentFilter === filterType) return; logger.currentFilter = filterType; localStorage.setItem(FILTER_KEY, filterType); logger.filterButtonGroup.querySelectorAll(".console-logger-filter").forEach((btn) => { if (btn.dataset.type === filterType) btn.classList.add("active"); else btn.classList.remove("active"); }); reRenderAllLogs(logger); } function clear(logger) { logger.logs = []; logger.content.innerHTML = ""; } var Logger = class { currentFilter; logs = []; isInitialized = false; userScrolledUp = false; container; toggleBtn; window; maximizeBtn; filtersContainer; filterButtonGroup; scrollToBottomBtn; content; maxLogCount = 2e3; maxLogCountInitialized = false; constructor() { const stored = localStorage.getItem(FILTER_KEY); this.currentFilter = stored && LOG_TYPE_STYLES[stored] ? stored : "base"; } tryInitialize() { return document.readyState !== "loading" && (this.isInitialized || (this.init(), this.isInitialized = true, true)); } init() { createContainer(this); bindEvents(this); checkInitialMaximizeState(this); checkInitialCollapseState(this); } addLog(message, typeArg = "base", ...extraArgs) { if (!this.maxLogCountInitialized) { this.maxLogCountInitialized = true; storageManager.getSetting("clogMsgCount", 2e3).then((v) => { this.maxLogCount = v; }).catch(() => {}); } const initialized = this.tryInitialize(); let resolvedType; let resolvedExtra; if (LOG_TYPE_STYLES[typeArg]) { resolvedType = typeArg; resolvedExtra = extraArgs; } else { resolvedType = "base"; resolvedExtra = [typeArg, ...extraArgs]; } resolvedType = LOG_TYPE_STYLES[resolvedType] ? resolvedType : "base"; const allParts = [message, ...resolvedExtra]; let messageType = "msg"; const parts = []; allParts.forEach((part) => { if (Object.prototype.toString.call(part) === "[object Error]") parts.push(String(part)); else if (typeof part === "object" && part !== null) try { parts.push("\n" + JSON.stringify(part, null, 2)); messageType = "json"; } catch { parts.push(String(part)); messageType = "msg"; } else parts.push(String(part)); }); const entry = { message: parts.join(" "), messageType, type: resolvedType, timestamp: new Date(), id: Date.now() + Math.random() }; this.logs.push(entry); if (this.logs.length > this.maxLogCount) { const firstEntry = this.logs[0]; if (initialized) { const oldEl = this.content.querySelector(`.console-logger-entry[data-id="${firstEntry.id}"]`); if (oldEl) { this.logs.shift(); this.content.removeChild(oldEl); } } } if (initialized) renderLog(this, entry); } log(...args) { const [first, ...rest] = args; setTimeout(() => { this.addLog(first, "base", ...rest); }, 0); } error(...args) { const [first, ...rest] = args; console.error(...args); setTimeout(() => { this.addLog(first, "error", ...rest); }, 0); } warn(...args) { const [first, ...rest] = args; setTimeout(() => { this.addLog(first, "warn", ...rest); }, 0); } debug(...args) { const [first, ...rest] = args; setTimeout(() => { this.addLog(first, "debug", ...rest); }, 0); } reRenderAllLogs() { reRenderAllLogs(this); } setFilter(filterType) { setFilter(this, filterType); } clear() { clear(this); } show() { if (this.isInitialized && this.container || this.tryInitialize() && this.container) { this.container.style.display = ""; this.reRenderAllLogs(); } } hide() { if (this.isInitialized && this.container) this.container.style.display = "none"; } lowZIndex() { if (this.isInitialized && this.container) this.container.style.zIndex = "12345678"; } highZIndex() { if (this.isInitialized && this.container) this.container.style.zIndex = "999999999"; } }; var REVISION_CHANNEL = "jhs-storage-revision"; var StorageRevision = class { revision = 0; channel = null; senderId = Math.random().toString(36).slice(2, 10); callbacks = []; get current() { return this.revision; } init() { if (this.channel) return; try { this.channel = new BroadcastChannel(REVISION_CHANNEL); this.channel.addEventListener("message", (event) => { const msg = event.data; if (msg && typeof msg.revision === "number" && msg.senderId !== this.senderId) { if (msg.revision > this.revision) this.revision = msg.revision; for (const cb of this.callbacks) try { cb(msg.revision); } catch (err) { console.error("[StorageRevision] 回调执行失败", err); } } }); } catch {} } increment() { this.revision++; try { this.channel?.postMessage({ revision: this.revision, senderId: this.senderId }); } catch {} return this.revision; } onRemoteChange(callback) { this.callbacks.push(callback); } destroy() { this.channel?.close(); this.channel = null; this.callbacks.length = 0; } }; var storageRevision = new StorageRevision(); var FILTER_ACTION = "filter"; var FAVORITE_ACTION = "favorite"; var HAS_WATCH_ACTION = "hasWatch"; var HAS_DOWN_ACTION = "hasDown"; var BLOCK_TEXT = "🚫 屏蔽"; var BLOCKED_TEXT = "🚫 已屏蔽"; var BLOCK_COLOR = "#de3333"; var FAVORITE_TEXT = "⭐ 收藏"; var FAVORITED_TEXT = "⭐ 已收藏"; var FAVORITE_COLOR = "#25b1dc"; var WATCHED_TEXT = "🔍 已观看"; var WATCHED_COLOR = "#d7a80c"; var HAS_DOWN_TEXT$1 = "📥️ 已下载"; var HAS_DOWN_COLOR = "#7bc73b"; var HAS_DOWN_STATUS$1 = "hasDown"; function saveSingleCar(input, list) { const { carNum, actionType, publishTime, starId, score } = input; let { url, names } = input; if (!carNum) failWithToast("番号为空!"); if (!url) failWithToast("url为空!"); if (!url.includes("http")) url = window.location.origin + url; if (names) names = names.trim(); let car = featureFlags.caseInsensitiveCarNum ? list.find((item) => item.carNum.toLowerCase() === carNum.toLowerCase()) : list.find((item) => item.carNum === carNum); if (car) { if (names) car.names = names; if (url) car.url = url; if (publishTime) car.publishTime = publishTime; if (score !== void 0) car.score = score; car.updateDate = utils.getNowStr(); } else { const now = utils.getNowStr(); car = { carNum, url, names, status: "", createDate: now, updateDate: now, publishTime }; if (starId) car.starId = starId; if (score !== void 0) car.score = score; list.push(car); } switch (actionType) { case FILTER_ACTION: if (car.status === "filter") failWithToast(`${carNum} 已在屏蔽列表中`); car.status = FILTER_ACTION; break; case FAVORITE_ACTION: if (car.status === "favorite") failWithToast(`${carNum} 已在收藏列表中`); car.status = FAVORITE_ACTION; break; case HAS_DOWN_STATUS$1: car.status = HAS_DOWN_STATUS$1; break; case HAS_WATCH_ACTION: car.status = HAS_WATCH_ACTION; break; default: failWithToast("actionType错误, 请联系作者更正: " + actionType); } } async function getCarList(sm) { if (featureFlags.storageCacheDeepCopy) { if (!sm.cacheCarList) sm.cacheCarList = await sm.forage.getItem(sm.car_list_key) || []; return utils.copyObj(sm.cacheCarList ?? []); } return await sm.forage.getItem(sm.car_list_key) || []; } async function getCar(sm, carNum) { const list = await getCarList(sm); if (featureFlags.caseInsensitiveCarNum) { const lower = carNum.toLowerCase(); return list.find((car) => car.carNum.toLowerCase() === lower); } return list.find((car) => car.carNum === carNum); } async function saveCar(sm, input) { const list = await sm.forage.getItem(sm.car_list_key) || []; saveSingleCar(input, list); await sm.forage.setItem(sm.car_list_key, list); storageRevision.increment(); sm.cacheCarList = list; await sm.removeNewVideoList([input.carNum]); const car = featureFlags.caseInsensitiveCarNum ? list.find((c) => c.carNum.toLowerCase() === input.carNum.toLowerCase()) : list.find((c) => c.carNum === input.carNum); if (car) sm.carListChangeCallback?.({ action: "upsert", upserts: [car] }); } async function updateCarInfo(sm, input) { const { carNum, url, actionType, remark } = input; let { names } = input; if (!carNum) failWithToast("番号为空!"); if (!url) failWithToast("url为空!"); if (names) names = names.trim(); const list = await sm.forage.getItem(sm.car_list_key) || []; const car = list.find((item) => item.carNum === carNum); if (!car) failWithToast("数据不存在: " + carNum); car.names = names; car.url = url; car.remark = remark; car.updateDate = utils.getNowStr(); switch (actionType) { case FAVORITE_ACTION: car.status = FAVORITE_ACTION; break; case HAS_WATCH_ACTION: car.status = HAS_WATCH_ACTION; break; default: failWithToast("actionType错误, 请联系作者更正: " + actionType); } await sm.forage.setItem(sm.car_list_key, list); storageRevision.increment(); sm.cacheCarList = list; sm.carListChangeCallback?.({ action: "upsert", upserts: [car] }); } async function saveCarList(sm, inputs) { if (!inputs || !Array.isArray(inputs) || inputs.length === 0) failWithToast("记录列表为空!"); const list = await sm.forage.getItem(sm.car_list_key) || []; for (const item of inputs) saveSingleCar(item, list); await sm.forage.setItem(sm.car_list_key, list); storageRevision.increment(); sm.cacheCarList = list; const numSet = featureFlags.caseInsensitiveCarNum ? new Set(inputs.map((i) => i.carNum.toLowerCase())) : new Set(inputs.map((i) => i.carNum)); const upserts = featureFlags.caseInsensitiveCarNum ? list.filter((c) => numSet.has(c.carNum.toLowerCase())) : list.filter((c) => numSet.has(c.carNum)); if (upserts.length > 0) sm.carListChangeCallback?.({ action: "upsert", upserts }); } async function removeCar(sm, carNum) { const list = await getCarList(sm); const beforeLen = list.length; const filtered = list.filter((item) => item.carNum !== carNum); if (filtered.length === beforeLen) { show.error(`${carNum} 不存在`); return false; } else { await sm.forage.setItem(sm.car_list_key, filtered); storageRevision.increment(); sm.cacheCarList = filtered; sm.carListChangeCallback?.({ action: "delete", deletes: [carNum] }); return true; } } async function batchRemoveCars(sm, carNums) { const list = await getCarList(sm); const beforeLen = list.length; const numSet = new Set(carNums); const filtered = list.filter((item) => !numSet.has(item.carNum)); const removed = beforeLen - filtered.length; if (removed === 0) return false; await sm.forage.setItem(sm.car_list_key, filtered); storageRevision.increment(); sm.cacheCarList = filtered; sm.carListChangeCallback?.({ action: "delete", deletes: carNums }); return removed; } async function getBlacklist(sm) { return await sm.forage.getItem(sm.blacklist_key) || []; } async function addBlacklistItem(sm, item) { const { starId, name, allName, role, movieType, url } = item; if (!starId) throw new Error("缺失starId"); if (!name) throw new Error("缺失name"); if (!role) throw new Error("缺失role"); const list = await getBlacklist(sm); const existing = list.find((entry) => entry.starId === starId); if (existing) { existing.url = url; existing.role = role; existing.movieType = movieType; clog.log("更新黑名单演员信息", existing); } else { const created = { starId, name, allName: allName || [name], createTime: utils.getNowStr(), role, movieType, url }; list.push(created); clog.log("增加黑名单演员信息", created); } await sm.forage.setItem(sm.blacklist_key, list); storageRevision.increment(); } async function updateBlacklistItem(sm, item) { if (!item || !item.starId) throw new Error("参数不全"); const list = await getBlacklist(sm); const existing = list.find((entry) => entry.starId === item.starId); if (!existing) throw new Error(`未找到黑名单演员信息:${item.name} ${item.starId}`); if (item.checkTime) existing.checkTime = item.checkTime; if (item.lastPublishTime) existing.lastPublishTime = item.lastPublishTime; await sm.forage.setItem(sm.blacklist_key, list); storageRevision.increment(); } async function deleteBlacklistItem(sm, starId) { const list = await getBlacklist(sm); const filtered = list.filter((entry) => entry.starId !== starId); if (list.length !== filtered.length) { await sm.forage.setItem(sm.blacklist_key, filtered); storageRevision.increment(); } } function ColoredTextCell({ text, color }) { return (0, import_jsx_runtime.jsx)("span", { style: { color }, children: text }); } function logColoredHtml(text) { return jsxToString((0, import_jsx_runtime.jsx)(ColoredTextCell, { text, color: "#f40" })); } async function getBlacklistCarList(sm) { const cached = sm.cache_filter_actor_actress_car_list; if (cached && cached.length > 0) { if (featureFlags.storageCacheDeepCopy) return utils.deepFreeze(utils.copyObj(cached)); return cached; } const fresh = await sm.forage.getItem(sm.blacklist_car_list_key) || []; sm.cache_filter_actor_actress_car_list = fresh; if (featureFlags.storageCacheDeepCopy) return utils.deepFreeze(utils.copyObj(fresh)); return fresh; } async function batchSaveBlacklistCarList(sm, inputs) { const list = await getBlacklistCarList(sm); const cloned = JSON.parse(JSON.stringify(list)); let changed = false; const newCarNums = []; for (const item of inputs) if (!cloned.find((entry) => entry.carNum === item.carNum)) { saveSingleCar(item, cloned); clog.log("屏蔽演员番号: " + logColoredHtml(`${item.names} ${item.carNum}`)); changed = true; newCarNums.push(item.carNum); } if (changed) { await sm.forage.setItem(sm.blacklist_car_list_key, cloned); storageRevision.increment(); await sm.removeNewVideoList(newCarNums); window.cleanCache_filter_actor_actress_car_list(); } } async function removeBlacklistCarList(sm, starId) { const list = await getBlacklistCarList(sm); const filtered = list.filter((item) => item.starId !== starId); if (filtered.length !== list.length) { await sm.forage.setItem(sm.blacklist_car_list_key, filtered); storageRevision.increment(); window.cleanCache_filter_actor_actress_car_list(); } } var UNCENSORED_TAG = "(無碼)"; async function getFavoriteActressList(sm) { if (featureFlags.storageCacheDeepCopy) { if (!sm.cacheFavoriteActressList) sm.cacheFavoriteActressList = await sm.forage.getItem(sm.favorite_actresses_key) || []; return utils.copyObj(sm.cacheFavoriteActressList ?? []); } return await sm.forage.getItem(sm.favorite_actresses_key) || []; } async function addFavoriteActressList(sm, inputs) { const list = await getFavoriteActressList(sm); let count = 0; for (const item of inputs) { const { starId, avatar, lastCheckTime, lastPublishTime } = item; let { name, allName, actressType } = item; if (!starId) throw new Error("缺失starId"); if (!name) throw new Error("缺失name"); if (!allName) allName = [name]; if (!actressType) actressType = name.includes(UNCENSORED_TAG) || allName.some((n) => n.includes(UNCENSORED_TAG)) ? UNCENSORED : CENSORED; name = name.replace(UNCENSORED_TAG, ""); allName = allName.map((n) => n.replace(UNCENSORED_TAG, "")); const existing = list.find((entry) => entry.starId === starId); if (existing) { if (!existing.avatar || !existing.avatar.includes("https")) { if (avatar) { clog.log(avatar); existing.avatar = avatar; clog.log(logColoredHtml(`补全女优头像: ${name}`)); count++; } } if (!existing.actressType && actressType) { existing.actressType = actressType; clog.log(logColoredHtml(`补全女优类别: ${name} ${actressType}`)); count++; } if (existing.name.includes(UNCENSORED_TAG)) { existing.name = name; existing.allName = allName; clog.log(logColoredHtml(`更正女优名字: ${name} ${allName}`)); count++; } continue; } const now = utils.getNowStr(); list.push({ starId, name, allName, avatar, lastCheckTime, lastPublishTime, createDate: now, updateDate: now, actressType }); clog.log(logColoredHtml(`同步JavDB已收藏的演员: ${name}`)); count++; } if (count > 0) { await sm.forage.setItem(sm.favorite_actresses_key, list); storageRevision.increment(); } else clog.log("信息已记录, 无需要进行同步收藏的演员"); return count; } async function removeFavoriteActress(sm, starId) { const list = await getFavoriteActressList(sm); const beforeLen = list.length; const filtered = list.filter((entry) => entry.starId !== starId); if (filtered.length === beforeLen) { clog.error(`移除演员失败, ${starId} 不存在`); return false; } else { await sm.forage.setItem(sm.favorite_actresses_key, filtered); storageRevision.increment(); sm.cacheFavoriteActressList = filtered; return true; } } async function updateFavoriteActress(sm, item) { const list = await getFavoriteActressList(sm); const { starId, name, allName, avatar, lastCheckTime, newVideoList, lastPublishTime, actressType, remark } = item; if (!starId) throw new Error("缺失starId"); const existing = list.find((entry) => entry.starId === starId); if (!existing) { clog.error("未找到演员信息", starId, name); return false; } if (name) existing.name = name; if (allName) existing.allName = allName; if (avatar) existing.avatar = avatar; if (actressType != null) existing.actressType = actressType; if (lastCheckTime) existing.lastCheckTime = lastCheckTime; if (newVideoList) existing.newVideoList = newVideoList; if (lastPublishTime) existing.lastPublishTime = lastPublishTime; if (remark) existing.remark = remark; existing.updateDate = utils.getNowStr(); await sm.forage.setItem(sm.favorite_actresses_key, list); storageRevision.increment(); } async function removeNewVideoList(sm, carNums) { const actressList = await getFavoriteActressList(sm); let changed = false; const updated = actressList.map((actress) => { if (!actress.newVideoList || !Array.isArray(actress.newVideoList)) return actress; const filtered = actress.newVideoList.filter((carNum) => { const matched = carNums.includes(carNum); if (matched) { clog.log("移除关联女优新作品", actress.name, carNum); changed = true; } return !matched; }); return { ...actress, newVideoList: filtered }; }); if (changed) await sm.forage.setItem(sm.favorite_actresses_key, updated); } async function saveKeyword(sm, keyword, key, label) { let list; if (Array.isArray(keyword)) list = [...keyword]; else { list = await sm.forage.getItem(key) || []; if (list.includes(keyword)) failWithToast(`${keyword} ${label}已存在`); list.push(keyword); } await sm.forage.setItem(key, list); return list; } async function saveTitleFilterKeyword(sm, keyword) { await saveKeyword(sm, keyword, sm.filter_keyword_title_key, "标题关键词"); storageRevision.increment(); if (Array.isArray(keyword)) return null; const actressList = await getFavoriteActressList(sm); let changed = false; const updated = actressList.map((actress) => { if (!actress.newVideoList || !Array.isArray(actress.newVideoList)) return actress; const filtered = actress.newVideoList.filter((carNum) => { const matched = carNum.startsWith(keyword); if (matched) { clog.log("移除关联女优新作品", actress.name, carNum); changed = true; } return !matched; }); return { ...actress, newVideoList: filtered }; }); if (changed) await sm.forage.setItem(sm.favorite_actresses_key, updated); } async function saveReviewFilterKeyword(sm, keyword) { return saveKeyword(sm, keyword, sm.filter_keyword_review_key, "评论关键词"); } async function getTitleFilterKeyword(sm) { return await sm.forage.getItem(sm.filter_keyword_title_key) || []; } async function getReviewFilterKeywordList(sm) { return await sm.forage.getItem(sm.filter_keyword_review_key) || []; } var SCHEMA_VERSION_KEY = "__jhs_schema_version"; async function runMigrations(sm) { const run = async () => { let version = await sm.forage.getItem("__jhs_schema_version") ?? 0; if (version >= 6) return; const steps = [ { target: 1, fn: () => merge_table_name(sm) }, { target: 2, fn: () => clean_no_url_blacklist(sm) }, { target: 3, fn: () => async_merge_other(sm) }, { target: 4, fn: () => merge_blacklist(sm) }, { target: 5, fn: () => merge_favoriteActress(sm) }, { target: 6, fn: () => merge_tow_car_list_table(sm) } ]; for (const step of steps) { if (version >= step.target) continue; try { await step.fn(); await sm.forage.setItem(SCHEMA_VERSION_KEY, step.target); version = step.target; } catch (err) { clog.error(`数据迁移 ${version}→${step.target} 失败,下次启动重试`, err); break; } } sm.cacheCarList = null; sm.cacheFavoriteActressList = null; sm.cache_filter_actor_actress_car_list = null; sm.cacheSettingObj = null; }; if (typeof navigator !== "undefined" && navigator.locks) await navigator.locks.request("jhs-storage-migration", { mode: "exclusive" }, run); else await run(); } async function merge_table_name(sm) { let oldKey = "filter_actor_actress_info_list"; let items = await sm.forage.getItem(oldKey) || []; if (items && items.length > 0) { clog.debug("更正", oldKey); await sm.forage.setItem(sm.blacklist_key, items); } await sm.forage.removeItem(oldKey); oldKey = "favorite_actresses_info_list"; items = await sm.forage.getItem(oldKey) || []; if (items && items.length > 0) { clog.debug("更正", oldKey); await sm.forage.setItem(sm.favorite_actresses_key, items); } await sm.forage.removeItem(oldKey); oldKey = "car_list_filter_actor_actress"; items = await sm.forage.getItem(oldKey) || []; if (items && items.length > 0) { clog.debug("更正", oldKey); await sm.forage.setItem(sm.blacklist_car_list_key, items); } await sm.forage.removeItem(oldKey); oldKey = "title_filter_keyword"; items = await sm.forage.getItem(oldKey) || []; if (items && items.length > 0) { clog.debug("更正", oldKey); await sm.forage.setItem(sm.filter_keyword_title_key, items); } await sm.forage.removeItem(oldKey); oldKey = "review_filter_keyword"; items = await sm.forage.getItem(oldKey) || []; if (items && items.length > 0) { clog.debug("更正", oldKey); await sm.forage.setItem(sm.filter_keyword_review_key, items); } await sm.forage.removeItem(oldKey); oldKey = "highlightedTags"; items = await sm.forage.getItem(oldKey) || []; if (items && items.length > 0) { clog.debug("更正", oldKey); await sm.forage.setItem(sm.highlighted_tags_key, items); } await sm.forage.removeItem(oldKey); } async function clean_no_url_blacklist(sm) { const [blacklistCars, blacklist] = await Promise.all([sm.forage.getItem(sm.blacklist_car_list_key) || [], sm.forage.getItem(sm.blacklist_key) || []]); if (!blacklistCars.length && !blacklist.length) return; const nameSet = new Set(blacklist.map((item) => item.name)); const filteredCars = blacklistCars.filter((item) => !item.actress || nameSet.has(item.actress)); if (blacklistCars.length !== filteredCars.length) { clog.debug("清理 blacklistCarList 前", blacklistCars.length); clog.debug("清理 blacklistCarList 后", filteredCars.length); await sm.forage.setItem(sm.blacklist_car_list_key, filteredCars); sm.cache_filter_actor_actress_car_list = null; } const actressSet = new Set(filteredCars.map((item) => item.actress)); let cleanedBlacklist = blacklist.filter((item) => actressSet.has(item.name)); cleanedBlacklist = cleanedBlacklist.map((item) => { const cleaned = { ...item }; delete cleaned.key; delete cleaned.recordTime; if (item.recordTime !== void 0) cleaned.createTime = item.recordTime; return cleaned; }); if (blacklist.length !== cleanedBlacklist.length || blacklist.some((item) => "key" in item || "recordTime" in item)) { clog.debug("清理 Blacklist 前", blacklist.length); clog.debug("清理 Blacklist 后", cleanedBlacklist.length); await sm.forage.setItem(sm.blacklist_key, cleanedBlacklist); } } async function async_merge_other(sm) { const setting = await sm.getSetting(); let changed = false; for (const key of [ "enableCheckFilterActorActress", "checkIntervalTime_filterActorActress", "checkIntervalTime_ruleTime", "checkIntervalTime_newVideo", "checkIntervalTime_favoriteActress", "checkFilterTime", "checkFilterConcurrencyCount", "checkFilterSleep", "enableCheckBlacklist", "checkBlacklist_intervalTime", "checkBlacklist_ruleTime", "enableCheckFavoriteActress", "checkFavoriteActress_IntervalTime", "enableCheckNewVideo", "checkNewVideo_intervalTime", "checkNewVideo_ruleTime", "checkConcurrencyCount", "checkRequestSleep" ]) if (Object.prototype.hasOwnProperty.call(setting, key)) { delete setting[key]; changed = true; } if (setting.downPath115) { delete setting.downPath115; changed = true; } if (changed) { await sm.saveSetting(setting); clog.debug("配置数据已更正"); } } async function merge_blacklist(sm) { const list = await sm.forage.getItem(sm.blacklist_key) || []; if (!list.length) return; let changed = false; const merged = list.map((item) => { let itemChanged = false; if (Object.prototype.hasOwnProperty.call(item, "isActor") && !item.role) { item.role = item.isActor ? ACTOR : ACTRESS; delete item.isActor; itemChanged = true; } if (!item.starId && item.url) try { const extracted = new URL(item.url).pathname.split("/").filter((seg) => seg.trim() !== "").pop(); if (item.starId !== extracted) { item.starId = extracted; itemChanged = true; } } catch (err) { clog.error("提取url-starId发生错误", item.url, err); } if (!item.allName) { item.allName = item.name ? [item.name] : []; itemChanged = true; } if (!item.movieType) { item.movieType = CENSORED; itemChanged = true; } if (item.url && item.url.includes("sort_type")) try { const urlObj = new URL(item.url); urlObj.searchParams.delete("sort_type"); item.url = urlObj.toString(); itemChanged = true; clog.debug("去除黑名单地址sort_type参数"); } catch (err) { clog.error("去除黑名单地址sort_type参数失败", item.url, err); } if (itemChanged) changed = true; return item; }); if (changed) { clog.debug("更正 Blacklist 数据结构"); await sm.forage.setItem(sm.blacklist_key, merged); } const carList = await sm.forage.getItem(sm.blacklist_car_list_key) || []; changed = false; const mergedCars = carList.map((item) => { if (!item.starId) { const match = list.find((entry) => entry.name === item.actress); if (match) item.starId = match.starId; changed = true; } if (item.type) { delete item.type; changed = true; } return item; }); if (changed) { clog.debug("更正 blacklistCarList 数据结构"); await sm.forage.setItem(sm.blacklist_car_list_key, mergedCars); sm.cache_filter_actor_actress_car_list = null; } } async function merge_favoriteActress(sm) { const list = await sm.forage.getItem(sm.favorite_actresses_key) || []; if (!list.length) return; let changed = false; const merged = list.map((item) => { if (item.dbId) { item.starId = item.dbId; delete item.dbId; changed = true; } return item; }); if (changed) { clog.debug("更正 favoriteActressesInfoList 数据结构"); await sm.forage.setItem(sm.favorite_actresses_key, merged); sm.cacheFavoriteActressList = null; } } async function merge_tow_car_list_table(sm) { const blacklistCars = await sm.forage.getItem(sm.blacklist_car_list_key) || []; const carList = await sm.forage.getItem(sm.car_list_key) || []; let changed = false; const mergedBlacklistCars = blacklistCars.map((item) => { if (item.actress !== void 0) { item.names = item.actress; delete item.actress; changed = true; } return item; }); if (changed) { clog.debug("更正 blacklistCarList 数据结构 actress->names"); await sm.forage.setItem(sm.blacklist_car_list_key, mergedBlacklistCars); sm.cache_filter_actor_actress_car_list = null; } changed = false; const mergedCars = carList.map((item) => { if (item.actress !== void 0) { item.names = item.actress; delete item.actress; changed = true; } return item; }); if (changed) { clog.debug("更正 carList 数据结构 actress->names"); await sm.forage.setItem(sm.car_list_key, mergedCars); sm.cacheCarList = null; } } var StorageManager = class StorageManager { static instance = null; car_list_key = "car_list"; filter_keyword_title_key = "filter_keyword_title"; filter_keyword_review_key = "filter_keyword_review"; setting_key = "setting"; blacklist_key = "blacklist"; blacklist_car_list_key = "blacklist_car_list"; favorite_actresses_key = "favorite_actresses"; highlighted_tags_key = "highlighted_tags"; forage; cacheCarList = null; cacheFavoriteActressList = null; cache_filter_actor_actress_car_list = null; cacheSettingObj = null; carListChangeCallback; constructor(forageInstance) { if (StorageManager.instance) throw new Error("StorageManager已被实例化过了!"); StorageManager.instance = this; this.forage = forageInstance ?? localforage.createInstance({ driver: localforage.INDEXEDDB, name: "JAV-JHS", version: 1, storeName: "appData" }); } static __resetForTesting() { StorageManager.instance = null; } setCarListChangeCallback(cb) { this.carListChangeCallback = cb; } clearFilterActorActressCarListCache() { this.cache_filter_actor_actress_car_list = null; } clearSettingCache() { this.cacheSettingObj = null; } clearCarListCache() { this.cacheCarList = null; } clearFavoriteActressListCache() { this.cacheFavoriteActressList = null; } async getCarList() { return getCarList(this); } async getCar(carNum) { return getCar(this, carNum); } _saveSingleCar(input, list) { saveSingleCar(input, list); } async saveCar(input) { return saveCar(this, input); } async updateCarInfo(input) { return updateCarInfo(this, input); } async saveCarList(inputs) { return saveCarList(this, inputs); } async removeCar(carNum) { return removeCar(this, carNum); } async batchRemoveCars(carNums) { return batchRemoveCars(this, carNums); } async getBlacklist() { return getBlacklist(this); } async addBlacklistItem(item) { return addBlacklistItem(this, item); } async updateBlacklistItem(item) { return updateBlacklistItem(this, item); } async deleteBlacklistItem(starId) { return deleteBlacklistItem(this, starId); } async getBlacklistCarList() { return getBlacklistCarList(this); } async batchSaveBlacklistCarList(inputs) { return batchSaveBlacklistCarList(this, inputs); } async removeBlacklistCarList(starId) { return removeBlacklistCarList(this, starId); } async getFavoriteActressList() { return getFavoriteActressList(this); } async addFavoriteActressList(inputs) { return addFavoriteActressList(this, inputs); } async removeFavoriteActress(starId) { return removeFavoriteActress(this, starId); } async updateFavoriteActress(item) { return updateFavoriteActress(this, item); } async removeNewVideoList(carNums) { return removeNewVideoList(this, carNums); } async getHighlightedTags() { return await this.forage.getItem(this.highlighted_tags_key) || []; } async setHighlightedTags(tags) { return await this.forage.setItem(this.highlighted_tags_key, tags); } async saveTitleFilterKeyword(keyword) { return saveTitleFilterKeyword(this, keyword); } async saveReviewFilterKeyword(keyword) { return saveReviewFilterKeyword(this, keyword); } async getTitleFilterKeyword() { return getTitleFilterKeyword(this); } async getReviewFilterKeywordList() { return getReviewFilterKeywordList(this); } async getSetting(key = null, defaultValue) { if (!this.cacheSettingObj) this.cacheSettingObj = await this.forage.getItem(this.setting_key) || {}; const setting = featureFlags.storageCacheDeepCopy ? utils.copyObj(this.cacheSettingObj) : this.cacheSettingObj; if (key === null) return setting; const val = setting[key]; if (val) { if (val === "true" || val === "false") return val.toLowerCase() === "true"; else if (typeof val != "string" || val.trim() === "" || isNaN(Number(val))) return val; else return Number(val); } else return defaultValue; } async saveSetting(setting) { if (setting) { await this.forage.setItem(this.setting_key, setting); storageRevision.increment(); window.clean_cacheSettingObj(); } else show.error("设置对象为空"); } async saveSettingItem(key, value) { if (!key) { show.error("key 不能为空"); return; } const setting = await this.getSetting(); setting[key] = value; await this.saveSetting(setting); window.clean_cacheSettingObj(); } async importData(data) { if (typeof data !== "object" || data === null || Array.isArray(data)) throw new Error("备份数据格式非法:必须是键值对对象"); const tempInstance = localforage.createInstance({ driver: localforage.INDEXEDDB, name: "JAV-JHS", storeName: "appData_import_staging" }); try { const tasks = []; for (const key in data) tasks.push(tempInstance.setItem(key, data[key])); await Promise.all(tasks); await tempInstance.iterate((value, key) => { return this.forage.setItem(key, value); }); storageRevision.increment(); await tempInstance.clear(); await tempInstance.dropInstance(); } catch (err) { try { await tempInstance.clear(); await tempInstance.dropInstance(); } catch {} throw err; } this.cacheCarList = null; this.cacheFavoriteActressList = null; this.cache_filter_actor_actress_car_list = null; this.cacheSettingObj = null; await this.runMigrations(); } async exportData() { const data = {}; await this.forage.iterate((value, key) => { data[key] = value; }); if (Object.keys(data).length === 0) throw new Error("没有可导出的数据"); return data; } async runMigrations() { return runMigrations(this); } async clean_no_url_blacklist() { return clean_no_url_blacklist(this); } async async_merge_other() { return async_merge_other(this); } }; var MIME_TYPES = { txt: "text/plain", html: "text/html", css: "text/css", csv: "text/csv", json: "application/json", xml: "application/xml", jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp", svg: "image/svg+xml", pdf: "application/pdf", doc: "application/msword", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", xls: "application/vnd.ms-excel", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ppt: "application/vnd.ms-powerpoint", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation", zip: "application/zip", rar: "application/x-rar-compressed", "7z": "application/x-7z-compressed", mp3: "audio/mpeg", wav: "audio/wav", mp4: "video/mp4", webm: "video/webm", ogg: "audio/ogg" }; function download(data, filename) { show.info("开始请求下载..."); const mimeType = MIME_TYPES[filename.split(".").pop().toLowerCase()] || "application/octet-stream"; let blob; if (data instanceof Blob) blob = data; else if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) blob = new Blob([data], { type: mimeType }); else if (typeof data === "string" && data.startsWith("data:")) { const raw = atob(data.split(",")[1]); const buf = new ArrayBuffer(raw.length); const bytes = new Uint8Array(buf); for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i); blob = new Blob([bytes], { type: mimeType }); } else blob = new Blob([data], { type: mimeType }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = filename; document.body.appendChild(anchor); anchor.click(); setTimeout(() => { document.body.removeChild(anchor); URL.revokeObjectURL(url); }, 100); } function StyleBlock({ id, css }) { return (0, import_jsx_runtime.jsx)("style", { id, dangerouslySetInnerHTML: { __html: css } }); } function styleBlockHtml(css, id) { return jsxToString((0, import_jsx_runtime.jsx)(StyleBlock, { id, css })); } function insertStyle(css) { if (css) { if (css.indexOf("<style>") === -1) css = styleBlockHtml(css); $("head").append(css); } } function importResource(url) { let el; if (url.indexOf("css") >= 0) { const link = document.createElement("link"); link.setAttribute("rel", "stylesheet"); link.href = url; el = link; } else { const script = document.createElement("script"); script.setAttribute("type", "text/javascript"); script.src = url; el = script; } document.documentElement.appendChild(el); } function smoothScrollToTop(duration = 500) { return new Promise((resolve) => { const startTime = performance.now(); const startY = window.pageYOffset; window.requestAnimationFrame(function step(now) { const elapsed = now - startTime; const progress = Math.min(elapsed / duration, 1); const eased = progress < .5 ? progress * 4 * progress * progress : 1 - Math.pow(progress * -2 + 2, 3) / 2; window.scrollTo(0, startY * (1 - eased)); if (progress < 1) window.requestAnimationFrame(step); else resolve(); }); }); } function loopDetector(container, checkFn, onSuccess, intervalMs = 20, timeoutMs = 1e4, callOnTimeout = true) { const token = String(Math.random()); const startTime = new Date().getTime(); const finish = (triggered) => { clearInterval(container[token]); if (triggered && onSuccess) onSuccess(); delete container[token]; }; container[token] = setInterval(() => { const elapsed = new Date().getTime() - startTime; if (checkFn()) finish(true); else if (elapsed >= timeoutMs) finish(callOnTimeout); }, intervalMs); } function rightClick(container, targetSelector, handler) { let el = null; if (typeof container === "string") el = document.querySelector(container); else if (container instanceof HTMLElement) el = container; if (!el) { clog.warn("rightClick(), 容器无效或未提供,将使用 document.body 进行全局委托。"); el = document.body; } if (typeof targetSelector === "string" && targetSelector.trim() !== "") el.addEventListener("contextmenu", (event) => { const target = event.target.closest(targetSelector); if (target) handler(event, target); }); else clog.error("rightClick(), 必须提供有效的 targetSelector。"); } function htmlTo$dom(html) { const parser = new DOMParser(); return $(parser.parseFromString(html, "text/html")); } function isHidden(el) { const node = el.jquery ? el[0] : el; return !node || node.offsetWidth <= 0 && node.offsetHeight <= 0 || window.getComputedStyle(node).display === "none"; } function getNowStr(dateSep = "-", timeSep = ":", timestamp = null) { const date = timestamp ? new Date(timestamp) : new Date(); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0"); const seconds = String(date.getSeconds()).padStart(2, "0"); return `${[ year, month, day ].join(dateSep)} ${[ hours, minutes, seconds ].join(timeSep)}`; } function formatDate(input, dateSep = "-", timeSep = ":") { let date; if (input instanceof Date) date = input; else { if (typeof input !== "string") throw new Error("Invalid date input: must be Date object or date string"); date = new Date(input); if (isNaN(date.getTime())) throw new Error("Invalid date string"); } const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0"); const seconds = String(date.getSeconds()).padStart(2, "0"); return `${[ year, month, day ].join(dateSep)} ${[ hours, minutes, seconds ].join(timeSep)}`; } function getHourDifference(start, end) { const startMs = start.getTime(); const endMs = end.getTime(); const hours = Math.abs(endMs - startMs) / 36e5; return Math.floor(hours); } function isUnnecessaryCheck(timeStr, checkIntervalTime) { if (!checkIntervalTime) throw new Error("未传入checkIntervalTime"); const hours = parseInt(String(checkIntervalTime), 10); if (isNaN(hours)) return false; return getHourDifference(new Date(timeStr), new Date()) < hours; } function q(event, content, onConfirm, onCancel) { let left; let top; if (event) { left = event.clientX - 130; top = event.clientY - 120; } else { left = window.innerWidth / 2 - 120; top = window.innerHeight / 2 - 120; } const layerIdx = layer.confirm(content, { offset: [String(top), String(left)], title: "提示", btn: ["确定", "取消"], shade: 0, zIndex: 999999991 }, function() { if (onConfirm) onConfirm(); layer.close(layerIdx); }, function() { if (onCancel) onCancel(); }); } function getDefaultArea() { return ["85%", "90%"]; } function getResponsiveArea(area) { const width = window.innerWidth; if (width >= 1200) return area || getDefaultArea(); else if (width >= 768) return ["70%", "90%"]; else return ["95%", "95%"]; } function closePage() { storageManager.getSetting("needClosePage", "yes").then((setting) => { if (setting !== "yes") return; parent.document.documentElement.style.overflow = "auto"; [ ".layui-layer-shade", ".layui-layer-move", ".layui-layer" ].forEach((selector) => { const matches = parent.document.querySelectorAll(selector); if (matches.length > 0) { const el = matches.length > 1 ? matches[matches.length - 1] : matches[0]; el.parentNode?.removeChild(el); } }); window.close(); }).catch((err) => clog.error("needClosePage 设置读取失败", err)); } async function retry(fn, maxRetries = 3) { let attempt = 0; while (attempt < maxRetries) try { const result = await fn(); if (attempt > 0) clog.debug(`[重试] 请求成功,共发起 ${attempt + 1} 次。`); return result; } catch (err) { const msg = String(err); if (msg.includes("Just a moment") || msg.includes("重定向") || msg.toLowerCase().includes("404 not found")) throw err; attempt++; if (attempt === maxRetries) { clog.debug(`[重试] 达到最大重试次数 (${maxRetries}),最终失败:`, err); throw err; } clog.debug(`[重试] 请求失败,准备第 ${attempt + 1} 次重试, 错误信息: ${msg}`); } } function copyToClipboard$1(label, text) { navigator.clipboard.writeText(text).then(() => show.info(`${label}已复制到剪切板, ${text}`)).catch((err) => clog.error("复制失败: ", err)); } function addCookie(cookieStr, options = {}) { const { maxAge = 604800, path = "/", domain = "", secure = false, sameSite = "Lax" } = options; cookieStr.split(";").forEach((pair) => { const trimmed = pair.trim(); if (trimmed) { const parts = trimmed.split("="); if (parts.length >= 2 && parts[0].trim()) { const segments = [`${parts[0].trim()}=${parts.slice(1).join("=")}`]; if (maxAge > 0) segments.push(`max-age=${maxAge}`); segments.push(`path=${path}`); if (domain) segments.push(`domain=${domain}`); if (secure) segments.push("Secure"); if (sameSite) segments.push(`SameSite=${sameSite}`); document.cookie = segments.join("; "); } } }); } function genericSort(data, sortConfigs, nullsFirst = true) { if (!Array.isArray(data) || data.length === 0) return []; if (!Array.isArray(sortConfigs) || sortConfigs.length === 0) return [...data]; const items = [...data]; const coerceDate = (value) => { if (value instanceof Date) return value; if (typeof value === "string") { const parsed = new Date(value); if (!isNaN(parsed.getTime())) return parsed; } return value; }; return items.sort((itemA, itemB) => { for (const config of sortConfigs) { const { key, order = "asc" } = config; let valA = itemA; let valB = itemB; if (key != null) { if (typeof key === "function") { valA = key(itemA); valB = key(itemB); } else { valA = itemA && typeof itemA === "object" ? itemA[key] : void 0; valB = itemB && typeof itemB === "object" ? itemB[key] : void 0; } } const coercedA = coerceDate(valA); const coercedB = coerceDate(valB); let cmp; const aIsNull = valA == null; const bIsNull = valB == null; if (aIsNull && bIsNull) return 0; if (aIsNull) { if (nullsFirst) return 1; else return -1; } if (bIsNull) { if (nullsFirst) return 1; else return -1; } cmp = coercedA instanceof Date && coercedB instanceof Date ? coercedA.getTime() - coercedB.getTime() : typeof valA === "number" && typeof valB === "number" ? valA - valB : typeof valA === "string" && typeof valB === "string" ? valA.localeCompare(valB) : String(valA).localeCompare(String(valB)); if (order === "desc") cmp *= -1; if (cmp !== 0) return cmp; } return 0; }); } function isUrl(str) { try { new URL(str); return true; } catch { return false; } } function setHrefParam(key, value) { const url = new URL(window.location.href); url.searchParams.set(key, value); window.history.pushState({}, "", url.toString()); } function getUrlParam(url, param) { const query = url.split("?")[1]; if (!query) return null; const regex = new RegExp(`(?:^|&)${param}=([^&]*)`); const match = query.match(regex); let value = ""; if (match && match[1]) value = decodeURIComponent(match[1].replace(/\+/g, " ")); if (value) { if (value === "true" || value === "false") return value.toLowerCase() === "true"; else if (typeof value !== "string" || value.trim() === "" || isNaN(Number(value))) return value; else return Number(value); } else return value; } function isMobile() { const ua = navigator.userAgent.toLowerCase(); return [ "iphone", "ipod", "ipad", "android", "blackberry", "windows phone", "nokia", "webos", "opera mini", "mobile", "mobi", "tablet" ].some((keyword) => ua.includes(keyword)); } function simpleId() { return crypto.randomUUID().replace("-", ""); } function sleep$1(ms = 1e3) { return new Promise((resolve) => setTimeout(resolve, ms)); } function copyObj(data) { return JSON.parse(JSON.stringify(data)); } function deepFreeze(obj) { if (obj === null || typeof obj !== "object" || Object.isFrozen(obj)) return obj; const propNames = Object.getOwnPropertyNames(obj); for (const name of propNames) { const value = obj[name]; if (value && typeof value === "object") deepFreeze(value); } return Object.freeze(obj); } function reBuildSignature$1() { const tsKey = "jhs_review_ts"; const signKey = "jhs_review_sign"; const nowSec = Math.floor(Date.now() / 1e3); if (nowSec - (Number(localStorage.getItem(tsKey)) || 0) <= 20) return localStorage.getItem(signKey); const sign = `${nowSec}.lpw6vgqzsp.${md5(`${nowSec}71cf27bb3c0bcdf207b64abecddc970098c7421ee7203b9cdae54478478a199e7d5a6e1a57691123c1a931c057842fb73ba3b3c83bcd69c17ccf174081e3d8aa`)}`; localStorage.setItem(tsKey, String(nowSec)); localStorage.setItem(signKey, sign); return sign; } function time(timers, label = "default", unit = "s", precision = 2) { if (timers.has(label)) { const entry = timers.get(label); const elapsed = performance.now() - entry.startTime; let formatted; let unitLabel; if (entry.unit === "s") { formatted = (elapsed / 1e3).toFixed(entry.precision); unitLabel = "秒"; } else { formatted = elapsed.toFixed(entry.precision); unitLabel = "毫秒"; } timers.delete(label); return `${label}: ${formatted}${unitLabel}`; } timers.set(label, { startTime: performance.now(), unit, precision }); } var escLayerGate = (() => { let w; try { w = window.top; w && w.location; } catch { w = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; } if (!w.__jhsEscGate) w.__jhsEscGate = { locked: false, tryEnter() { if (this.locked) return false; this.locked = true; return true; }, release() { this.locked = false; } }; return w.__jhsEscGate; })(); function getTopLayerEl() { const nodes = document.querySelectorAll(".layui-layer[times]"); let topEl = null; let topZ = -Infinity; for (let i = 0; i < nodes.length; i++) { const el = nodes[i]; const type = el.getAttribute("type") || ""; if (type === "loading" || type === "tips") continue; if (el.getAttribute("data-jhs-esc-closing") === "1") continue; const cs = window.getComputedStyle(el); if (cs.display === "none" || cs.visibility === "hidden") continue; const z = parseInt(cs.zIndex, 10); const zVal = Number.isFinite(z) ? z : i; const times = Number(el.getAttribute("times")); if (!Number.isFinite(times)) continue; if (zVal >= topZ) { topZ = zVal; topEl = el; } } return topEl; } function syncLayerIndexStack(layerIndexStack) { return layerIndexStack.filter((idx) => document.getElementById(`layui-layer${idx}`) != null); } function _handleGlobalEscKey(event) { if (event.key !== "Escape" && event.keyCode !== 27) return; if ((event.originalEvent ?? event).repeat) { event.preventDefault(); event.stopImmediatePropagation(); return; } if (!escLayerGate.tryEnter()) { event.preventDefault(); event.stopImmediatePropagation(); return; } const topEl = getTopLayerEl(); if (!topEl) { escLayerGate.release(); return; } const layerIdx = Number(topEl.getAttribute("times")); if (!Number.isFinite(layerIdx)) { escLayerGate.release(); return; } let hasViewer = $(topEl).find(".viewer-container").length > 0; let hasInnerLayer = false; if (!hasViewer) { const iframeEl = topEl.querySelector(`#layui-layer-iframe${layerIdx}`); if (iframeEl?.contentDocument) try { if (iframeEl.contentDocument.querySelector(".viewer-container")) hasViewer = true; if (!hasViewer) { const iframeLayers = iframeEl.contentDocument.querySelectorAll(".layui-layer[times]"); for (const il of Array.from(iframeLayers)) { const ilType = il.getAttribute("type") || ""; if (ilType === "loading" || ilType === "tips") continue; if (il.getAttribute("data-jhs-esc-closing") === "1") continue; const ilCs = window.getComputedStyle(il); if (ilCs.display === "none" || ilCs.visibility === "hidden") continue; hasInnerLayer = true; break; } } } catch { clog.warn("无法检查跨域 iframe 内的 .viewer-container"); } } if (hasViewer) { escLayerGate.release(); return; } if (hasInnerLayer) { const iframeEl = topEl.querySelector(`#layui-layer-iframe${layerIdx}`); if (iframeEl?.contentDocument) try { escLayerGate.release(); const innerEvt = new KeyboardEvent("keydown", { key: "Escape", keyCode: 27, which: 27, bubbles: true, cancelable: true, composed: true }); iframeEl.contentDocument.dispatchEvent(innerEvt); window.setTimeout(() => escLayerGate.release(), 100); return; } catch { escLayerGate.release(); return; } escLayerGate.release(); return; } event.preventDefault(); event.stopImmediatePropagation(); topEl.setAttribute("data-jhs-esc-closing", "1"); topEl.style.display = "none"; const shade = document.getElementById(`layui-layer-shade${layerIdx}`); if (shade) { shade.setAttribute("data-jhs-esc-closing", "1"); shade.style.display = "none"; } layer.close(layerIdx); window.setTimeout(() => escLayerGate.release(), 800); } function _handleGlobalEscKeyUp(event) { if (event.key === "Escape" || event.keyCode === 27) escLayerGate.release(); } function setupEscClose(util, layerIdx) { if (!util._boundHandler) { util._boundHandler = util._handleGlobalEscKey.bind(util); util._boundEscKeyupHandler = util._handleGlobalEscKeyUp.bind(util); $(document).off("keydown.globalLayerEsc keyup.globalLayerEsc"); document.removeEventListener("keydown", util._boundHandler, true); document.removeEventListener("keyup", util._boundEscKeyupHandler, true); document.addEventListener("keydown", util._boundHandler, true); document.addEventListener("keyup", util._boundEscKeyupHandler, true); } const numIdx = Number(layerIdx); if (Number.isFinite(numIdx) && util.layerIndexStack.indexOf(numIdx) === -1) util.layerIndexStack.push(numIdx); const iframeEl = document.getElementById(`layui-layer-iframe${layerIdx}`); if (iframeEl && iframeEl.getAttribute("data-esc-bound") !== "yes") { const bindIframe = () => { try { const doc = iframeEl.contentDocument; if (!doc || !util._boundHandler || !util._boundEscKeyupHandler) return; doc.removeEventListener("keydown", util._boundHandler, true); doc.removeEventListener("keyup", util._boundEscKeyupHandler, true); doc.addEventListener("keydown", util._boundHandler, true); doc.addEventListener("keyup", util._boundEscKeyupHandler, true); iframeEl.setAttribute("data-esc-bound", "yes"); } catch (err) { clog.error("iframe监听失败 (跨域或未加载完毕):", err); } }; if (iframeEl.contentDocument?.readyState === "complete") bindIframe(); else iframeEl.addEventListener("load", bindIframe, { once: true }); } } var CommonUtil = class CommonUtil { static instance; intervalContainer = {}; mimeTypes = MIME_TYPES; timers = new Map(); layerIndexStack = []; _boundHandler = null; _boundEscKeyupHandler = null; constructor() { CommonUtil.instance ||= this; return CommonUtil.instance; } insertStyle = (css) => insertStyle(css); importResource(url) { importResource(url); } smoothScrollToTop(duration = 500) { return smoothScrollToTop(duration); } loopDetector(checkFn, onSuccess, intervalMs = 20, timeoutMs = 1e4, callOnTimeout = true) { loopDetector(this.intervalContainer, checkFn, onSuccess, intervalMs, timeoutMs, callOnTimeout); } rightClick(container, targetSelector, handler) { rightClick(container, targetSelector, handler); } htmlTo$dom(html) { return htmlTo$dom(html); } isHidden(el) { return isHidden(el); } q(event, content, onConfirm, onCancel) { q(event, content, onConfirm, onCancel); } getDefaultArea() { return getDefaultArea(); } getResponsiveArea(area) { return getResponsiveArea(area); } closePage() { closePage(); } openPage(url, title, shadeClose, event) { shadeClose = shadeClose ?? true; if (event && (event.ctrlKey || event.metaKey)) { GM_openInTab(url.includes("http") ? url : window.location.origin + url, { insert: false }); return; } let content = url; if (!url.includes("/actors/") && !url.includes("/star/")) content = url.includes("?") ? `${url}&hideNav=1` : `${url}?hideNav=1`; layer.open({ type: 2, title, content, scrollbar: false, shadeClose, area: getResponsiveArea(["85%", "90%"]), isOutAnim: false, anim: -1, success: (_layerEl, layerIdx) => { this.setupEscClose(layerIdx); } }); } getTopLayerEl() { return getTopLayerEl(); } syncLayerIndexStack() { this.layerIndexStack = syncLayerIndexStack(this.layerIndexStack); } _handleGlobalEscKey(event) { _handleGlobalEscKey(event); } _handleGlobalEscKeyUp(event) { _handleGlobalEscKeyUp(event); } setupEscClose(layerIdx) { setupEscClose(this, layerIdx); } getNowStr(dateSep = "-", timeSep = ":", timestamp = null) { return getNowStr(dateSep, timeSep, timestamp); } formatDate(input, dateSep = "-", timeSep = ":") { return formatDate(input, dateSep, timeSep); } getHourDifference(start, end) { return getHourDifference(start, end); } isUnnecessaryCheck(timeStr, checkIntervalTime) { return isUnnecessaryCheck(timeStr, checkIntervalTime); } download(data, filename) { download(data, filename); } async retry(fn, maxRetries = 3) { return retry(fn, maxRetries); } copyToClipboard(label, text) { copyToClipboard$1(label, text); } addCookie(cookieStr, options = {}) { addCookie(cookieStr, options); } genericSort(data, sortConfigs, nullsFirst = true) { return genericSort(data, sortConfigs, nullsFirst); } isUrl(str) { return isUrl(str); } setHrefParam(key, value) { setHrefParam(key, value); } getUrlParam(url, param) { return getUrlParam(url, param); } isMobile() { return isMobile(); } simpleId() { return simpleId(); } sleep(ms = 1e3) { return sleep$1(ms); } copyObj(data) { return copyObj(data); } deepFreeze(obj) { return deepFreeze(obj); } reBuildSignature() { return reBuildSignature$1(); } time(label = "default", unit = "s", precision = 2) { return time(this.timers, label, unit, precision); } }; var GmHttp = class { async get(url, queryParams = {}, headers = {}, followRedirect) { return this.gmRequest("GET", url, null, queryParams, headers, followRedirect); } post(url, body = {}, headers = {}) { const requestHeaders = { "Content-Type": "application/json", ...headers }; const bodyString = JSON.stringify(body); return this.gmRequest("POST", url, bodyString, null, requestHeaders); } postForm(url, formData = {}, headers = {}) { const requestHeaders = headers || {}; requestHeaders["Content-Type"] ||= "application/x-www-form-urlencoded"; let bodyString = ""; if (formData && Object.keys(formData).length > 0) bodyString = Object.entries(formData).map(([key, value]) => `${key}=${String(value)}`).join("&"); return this.gmRequest("POST", url, bodyString, null, requestHeaders); } postFileFormData(url, formFields = {}, headers = {}) { const requestHeaders = headers || {}; const boundary = `----WebKitFormBoundary${Math.random().toString(36).substring(2)}`; requestHeaders["Content-Type"] = `multipart/form-data; boundary=${boundary}`; let body = ""; if (formFields && Object.keys(formFields).length > 0) body = Object.entries(formFields).map(([key, value]) => `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${String(value)}\r\n`).join(""); body += `--${boundary}--`; return this.gmRequest("POST", url, body, null, requestHeaders); } async downloadFileInChunks(url, headers = {}, filename, transform) { if (!filename) throw new Error("请提供文件名 (filename) 用于保存。"); const timeout = await storageManager.getSetting("httpTimeout", 5e3); const retryCount = await storageManager.getSetting("httpRetryCount", 3); let fileSize; let mimeType; clog.log(`[${filename}] 正在获取文件大小...`); try { const sizeResponse = await utils.retry(() => new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url, headers: { ...headers, Range: "bytes=0-0" }, timeout, onload: resolve, onerror: () => reject(new Error("网络错误:无法获取文件大小")), ontimeout: () => reject(new Error("超时:获取文件大小")) }); }), retryCount); if (sizeResponse.status !== 206 && sizeResponse.status !== 200) throw new Error(`请求文件大小失败,状态码: ${sizeResponse.status}`); { const contentRangeMatch = sizeResponse.responseHeaders.match(/content-range:\s*bytes\s*\d+-\d+\/(\d+)/i); const contentTypeMatch = sizeResponse.responseHeaders.match(/content-type:\s*([^\s;]+)/i); if (contentRangeMatch && contentRangeMatch[1]) fileSize = parseInt(contentRangeMatch[1], 10); else { const contentLengthMatch = sizeResponse.responseHeaders.match(/content-length:\s*(\d+)/i); if (!contentLengthMatch || sizeResponse.status !== 200) throw new Error("无法从响应头中获取文件总大小,服务器可能不支持 Range 请求。"); fileSize = parseInt(contentLengthMatch[1], 10); clog.warn(`[${filename}] 服务器返回 200 状态码,可能不支持 Range 请求。将尝试完整下载。`); } if (contentTypeMatch && contentTypeMatch[1]) mimeType = contentTypeMatch[1]; clog.log(`[${filename}] 文件总大小:${(fileSize / 1024 / 1024).toFixed(2)} MB, MIME 类型: ${mimeType || "未知"}`); } } catch (err) { clog.error(`[${filename}] 获取文件大小失败:`, err instanceof Error ? err.message : String(err)); throw err; } if (!fileSize || fileSize <= 0) throw new Error("获取到的文件大小无效或服务器拒绝提供大小信息。"); const chunkSize = 1048576; const chunkCount = Math.ceil(fileSize / chunkSize); const promises = []; const chunks = new Array(chunkCount); clog.log(`[${filename}] 文件将被分为 ${chunkCount} 块进行下载 (每块约 ${1 .toFixed(2)} MB)`); for (let index = 0; index < chunkCount; index++) { const start = index * chunkSize; const rangeHeader = `bytes=${start}-${Math.min(start + chunkSize - 1, fileSize - 1)}`; await utils.retry(() => new Promise((resolve, reject) => { const requestHeaders = { ...headers, Range: rangeHeader, Accept: "application/octet-stream" }; GM_xmlhttpRequest({ method: "GET", url, headers: requestHeaders, timeout, responseType: "arraybuffer", onload: (response) => { if (response.status === 206 || response.status === 200) { const chunkData = response.response; if (chunkData instanceof ArrayBuffer) { chunks[index] = chunkData; clog.log(`[${filename}] 成功下载第 ${index + 1}/${chunkCount} 块 (${rangeHeader})`); resolve(); } else reject(new Error(`第 ${index + 1} 块响应不是 ArrayBuffer。`)); } else reject(new Error(`第 ${index + 1} 块请求失败,状态码: ${response.status}`)); }, onerror: (errorEvent) => reject(new Error(`第 ${index + 1} 块网络错误: ${errorEvent.error}`)), ontimeout: () => reject(new Error(`第 ${index + 1} 块超时。`)) }); }), retryCount); promises.push(Promise.resolve()); } try { await Promise.all(promises); clog.log(`[${filename}] 所有分块下载完成,开始合并...`); } catch (err) { clog.error(`[${filename}] 分块下载过程中发生错误:`, err instanceof Error ? err.message : String(err)); throw err; } const blob = new Blob(chunks); if (blob.size !== fileSize) clog.warn(`[${filename}] 警告:合并后的 Blob 大小 (${blob.size}) 与预期文件大小 (${fileSize}) 不匹配!`); const text = await blob.text(); const content = transform ? transform(text) : text; utils.download(content, filename); clog.log(`[${filename}] 文件合并完成,已触发浏览器下载。`); } async gmRequest(method, url, data = {}, queryParams = {}, headers = {}, followRedirect = false) { let requestUrl = url; if (queryParams && Object.keys(queryParams).length) { const queryString = new URLSearchParams(queryParams).toString(); requestUrl += (requestUrl.includes("?") ? "&" : "?") + queryString; } const timeout = await storageManager.getSetting("httpTimeout", 5e3); const retryCount = await storageManager.getSetting("httpRetryCount", 3); const requestData = data || void 0; return await utils.retry(() => new Promise((resolve, reject) => { GM_xmlhttpRequest({ method, url: requestUrl, headers, timeout, data: requestData, onload: (response) => { try { if (followRedirect && response.finalUrl && response.finalUrl !== requestUrl) { reject("请求被重定向了,URL是:" + response.finalUrl); return; } if (response.status >= 200 && response.status < 300) { if (response.responseText) try { resolve(JSON.parse(response.responseText)); } catch { resolve(response.responseText); } else resolve(response.responseText || response); } else { clog.error("请求失败,状态码:", response.status, requestUrl); if (response.responseText) try { reject(JSON.parse(response.responseText)); } catch { reject(new Error(response.responseText || `请求发生错误 ${response.status}`)); } else reject(new Error(`请求发生错误 ${response.status}`)); } } catch (err) { reject(err); } }, onerror: (errorEvent) => { clog.error("网络错误:", requestUrl); reject(new Error(errorEvent.error || "网络错误")); }, ontimeout: () => { reject(new Error("请求超时: " + requestUrl)); } }); }), retryCount); } }; var layer_fix_default = "/**\n * 修复 layui-layer 关闭按钮不可见 + IE hack 被 lightningcss 误删的属性。\n *\n * 背景:layer.css 含 `*right` / `_display` 等 IE 前缀 hack,vite lightningcss\n * (errorRecovery)解析时连带丢掉相邻标准属性,导致:\n * - `.layui-layer-setwin` 丢失 `position:absolute; right:15px` → 关闭钮不在右上角\n * - `.layui-layer-close2` 几乎只剩无效 `_display:none` → 无标题层关闭钮失效\n *\n * 本文件用无 hack 的标准 CSS 覆盖还原。\n */\n\n.layui-layer-setwin {\n position: absolute !important;\n right: 15px !important;\n top: 15px !important;\n font-size: 0;\n line-height: initial;\n z-index: 10;\n}\n\n.layui-layer-setwin a {\n position: relative;\n display: inline-block;\n width: 16px;\n height: 16px;\n margin-left: 10px;\n font-size: 12px;\n vertical-align: top;\n}\n\n.layui-layer-setwin .layui-layer-close1 {\n cursor: pointer;\n background-position: 0 -40px;\n}\n\n.layui-layer-setwin .layui-layer-close1:hover {\n opacity: 0.7;\n}\n\n.layui-layer-setwin .layui-layer-close2 {\n position: absolute !important;\n right: -28px !important;\n top: -28px !important;\n width: 30px !important;\n height: 30px !important;\n margin-left: 0 !important;\n background-position: -149px -31px !important;\n display: inline-block !important;\n cursor: pointer;\n}\n\n.layui-layer-setwin .layui-layer-close2:hover {\n background-position: -180px -31px !important;\n}\n"; function resetOverflowByShadeCount(delay = 10) { setTimeout(() => { const shadeCount = document.querySelectorAll(".layui-layer-shade").length; document.documentElement.style.overflow = shadeCount > 0 ? "hidden" : ""; }, delay); } function removeFromEscStack(index) { try { const stack = window.utils?.layerIndexStack; if (!stack || index == null) return; const num = Number(index); for (let i = stack.length - 1; i >= 0; i--) if (stack[i] === num) stack.splice(i, 1); } catch {} } function setupLayerWrapper() { injectCss(layer_fix_default); const originalClose = layer.close; layer.close = function(index) { removeFromEscStack(index); const result = originalClose.call(this, index); resetOverflowByShadeCount(); return result; }; const originalCloseAll = layer.closeAll; if (typeof originalCloseAll === "function") layer.closeAll = function(type) { try { const stack = window.utils?.layerIndexStack; if (stack) stack.length = 0; } catch {} const result = originalCloseAll.call(this, type); resetOverflowByShadeCount(); return result; }; const originalOpen = layer.open; layer.open = function(options) { const opts = options || {}; if (opts.closeBtn === void 0 || opts.closeBtn === null) opts.closeBtn = 1; if (opts.shadeClose === void 0 && (opts.type === 1 || opts.type === 2)) opts.shadeClose = true; const originalSuccess = opts.success; opts.success = function(el, index) { if (typeof originalSuccess == "function") originalSuccess.call(this, el, index); utils.setupEscClose(index); }; return originalOpen.call(this, opts); }; } var tooltip_default = ".js-tooltip {\n /* 通用样式 */\n position: fixed;\n padding: 8px 12px;\n border-radius: 6px;\n white-space: normal;\n max-width: 600px;\n pointer-events: none;\n font-size: 14px;\n line-height: 1.5;\n z-index: var(--jhs-z-top);\n background: #f0fdf4;\n color: #166534;\n border: none;\n box-shadow: 0 6px 20px rgb(0, 0, 0, 0.3);\n display: none;\n}\n\n.js-tooltip.is-active {\n display: block !important;\n}\n"; var TOOLTIP_SELECTOR = "[data-tip-top], [data-tip-bottom], [data-tip-left], [data-tip-right], [data-tip]"; function createTooltipElement(content) { const wrapper = document.createElement("div"); wrapper.classList.add("js-tooltip"); const inner = document.createElement("div"); inner.textContent = content; wrapper.appendChild(inner); document.body.appendChild(wrapper); return wrapper; } function positionTooltip(element, content, direction) { const tooltip = createTooltipElement(content); tooltip.style.display = "block"; const elementRect = element.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect(); tooltip.style.display = "none"; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; let left; let top; let actualDirection = direction; const fitsVertically = (y) => y >= 8 && y + tooltipRect.height <= viewportHeight - 8; const fitsHorizontally = (x) => x >= 8 && x + tooltipRect.width <= viewportWidth - 8; const centerX = elementRect.left + elementRect.width / 2 - tooltipRect.width / 2; const centerY = elementRect.top + elementRect.height / 2 - tooltipRect.height / 2; switch (direction) { case "top": top = elementRect.top - tooltipRect.height - 0; if (top < 8 && fitsVertically(elementRect.bottom + 0)) { top = elementRect.bottom + 0; actualDirection = "bottom"; } break; case "bottom": top = elementRect.bottom + 0; if (top + tooltipRect.height > viewportHeight - 8 && fitsVertically(elementRect.top - tooltipRect.height - 0)) { top = elementRect.top - tooltipRect.height - 0; actualDirection = "top"; } break; case "left": left = elementRect.left - tooltipRect.width - 0; if (left < 8 && fitsHorizontally(elementRect.right + 0)) { left = elementRect.right + 0; actualDirection = "right"; } break; case "right": left = elementRect.right + 0; if (left + tooltipRect.width > viewportWidth - 8 && fitsHorizontally(elementRect.left - tooltipRect.width - 0)) { left = elementRect.left - tooltipRect.width - 0; actualDirection = "left"; } } const isHorizontal = actualDirection === "left" || actualDirection === "right"; if (actualDirection === "top" || actualDirection === "bottom") { left = centerX; if (left < 8) left = 8; else if (left + tooltipRect.width > viewportWidth - 8) left = viewportWidth - tooltipRect.width - 8; } else if (isHorizontal) { top = centerY; if (top < 8) top = 8; else if (top + tooltipRect.height > viewportHeight - 8) top = viewportHeight - tooltipRect.height - 8; } tooltip.style.left = `${left}px`; tooltip.style.top = `${top}px`; tooltip.classList.add("is-active"); element.tooltipElement = tooltip; } function setupTooltip() { injectCss(tooltip_default); document.addEventListener("mouseover", (event) => { const target = event.target.closest(TOOLTIP_SELECTOR); if (target && !target.tooltipElement) { let content = ""; let direction = "top"; if (target.hasAttribute("data-tip-bottom")) { content = target.getAttribute("data-tip-bottom") ?? ""; direction = "bottom"; } else if (target.hasAttribute("data-tip-left")) { content = target.getAttribute("data-tip-left") ?? ""; direction = "left"; } else if (target.hasAttribute("data-tip-right")) { content = target.getAttribute("data-tip-right") ?? ""; direction = "right"; } else if (target.hasAttribute("data-tip-top")) { content = target.getAttribute("data-tip-top") ?? ""; direction = "top"; } else if (target.hasAttribute("data-tip")) { content = target.getAttribute("data-tip") ?? ""; direction = "top"; } if (!content) return; target.hoverTimeout = window.setTimeout(() => { if (target.matches(":hover") && !target.tooltipElement) positionTooltip(target, content, direction); }, 50); } }); document.addEventListener("mouseout", (event) => { const target = event.target.closest(TOOLTIP_SELECTOR); if (!target) return; if (target.hoverTimeout) { clearTimeout(target.hoverTimeout); target.hoverTimeout = null; } if (!target.contains(event.relatedTarget)) { if (target.tooltipElement) { target.tooltipElement.remove(); target.tooltipElement = null; } } }); } var WEBDAV_SALT = "x7k9p3"; function encryptCredential(value) { return (WEBDAV_SALT + value + WEBDAV_SALT).split("").map((ch) => { const code = ch.codePointAt(0); return String.fromCodePoint(code + 5); }).join(""); } function decryptCredential(value) { return value.split("").map((ch) => { const code = ch.codePointAt(0); return String.fromCodePoint(code - 5); }).join("").slice(6, -6); } var BADGE_BASE = { display: "inline-block", padding: "1px 6px", borderRadius: "3px", fontSize: "11px", lineHeight: "16px", margin: "1px", whiteSpace: "nowrap" }; var BADGE_COLORS = { ok: { background: "#dcfce7", color: "#166534" }, fail: { background: "#fee2e2", color: "#991b1b" }, skip: { background: "#f1f5f9", color: "#64748b" }, page: { background: "#e0e7ff", color: "#3730a3" }, site: { background: "#fef3c7", color: "#92400e" }, flag: { background: "#fce7f3", color: "#9d174d" } }; var TIME_COLORS = { fast: "#16a34a", medium: "#ca8a04", slow: "#dc2626" }; function summarize(plugins) { return { total: plugins.length, cssOk: plugins.filter((p) => p.cssStatus === "fulfilled").length, handleOk: plugins.filter((p) => p.handleStatus === "fulfilled").length, failed: plugins.filter((p) => p.cssStatus === "rejected" || p.handleStatus === "rejected").length, skipped: plugins.filter((p) => p.cssStatus === "skipped" && p.handleStatus === "skipped").length, flagged: plugins.filter((p) => p.isFeatureFlagged).length, totalMs: plugins.reduce((sum, p) => sum + (p.totalDurationMs ?? 0), 0) }; } function sortRows(plugins) { const isFailed = (p) => p.cssStatus === "rejected" || p.handleStatus === "rejected"; return [...plugins].sort((a, b) => { const failDiff = Number(isFailed(b)) - Number(isFailed(a)); if (failDiff !== 0) return failDiff; return (b.totalDurationMs ?? 0) - (a.totalDurationMs ?? 0); }); } function StatusBadge({ status, durationMs }) { const tone = status === "fulfilled" ? "ok" : status === "rejected" ? "fail" : "skip"; const icon = status === "fulfilled" ? "✅" : status === "rejected" ? "❌" : "⏭"; return (0, import_jsx_runtime.jsxs)("span", { style: { ...BADGE_BASE, ...BADGE_COLORS[tone] }, children: [ icon, " ", durationMs != null ? `${durationMs}ms` : status === "skipped" ? "跳过" : "-" ] }); } function DurationCell({ ms }) { if (ms == null) return (0, import_jsx_runtime.jsx)("span", { style: { color: "#94a3b8", fontSize: "11px" }, children: "-" }); const color = ms < 100 ? TIME_COLORS.fast : ms <= 500 ? TIME_COLORS.medium : TIME_COLORS.slow; return (0, import_jsx_runtime.jsxs)("span", { style: { fontFamily: "monospace", fontSize: "11px", color }, children: [ms, "ms"] }); } function BadgeListCell({ values, tone }) { if (values.length === 0) return (0, import_jsx_runtime.jsx)("span", { style: { color: "#94a3b8", fontSize: "11px" }, children: "全部" }); return (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: values.map((value) => (0, import_jsx_runtime.jsx)("span", { style: { ...BADGE_BASE, ...BADGE_COLORS[tone] }, children: value }, value)) }); } function DiagnosticsTable({ data }) { const summary = summarize(data); const rows = sortRows(data); const th = { padding: "8px 6px", textAlign: "left", fontWeight: 600, borderBottom: "2px solid #e2e8f0", background: "#f1f5f9", whiteSpace: "nowrap" }; const td = { padding: "6px", borderBottom: "1px solid #f1f5f9", verticalAlign: "top" }; return (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: "13px" }, children: [(0, import_jsx_runtime.jsxs)("div", { className: "jhs-diag-summary", style: { background: "#f8fafc", border: "1px solid #e2e8f0", borderRadius: "6px", padding: "10px 12px", marginBottom: "12px", lineHeight: 1.8 }, children: [ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 700, fontSize: "14px", marginBottom: "4px" }, children: "📊 插件诊断报告" }), (0, import_jsx_runtime.jsxs)("div", { children: [ "总计: ", (0, import_jsx_runtime.jsx)("b", { children: summary.total }), " 个插件 | ✅ CSS: ", summary.cssOk, "/", summary.total, " | ✅ Handle:", " ", summary.handleOk, "/", summary.total, " | ⏱ 总耗时: ", (0, import_jsx_runtime.jsxs)("b", { children: [summary.totalMs, "ms"] }) ] }), (0, import_jsx_runtime.jsxs)("div", { children: [ (0, import_jsx_runtime.jsxs)("span", { style: { color: summary.failed > 0 ? "#dc2626" : "#64748b" }, children: [ "❌ 失败: ", summary.failed, " 个插件" ] }), " ", "| ⏭ 跳过: ", summary.skipped, " 个插件 | 🚩 Feature Flag: ", summary.flagged, " 个" ] }) ] }), (0, import_jsx_runtime.jsxs)("table", { className: "jhs-diag-table", style: { width: "100%", borderCollapse: "collapse", fontSize: "12px" }, children: [(0, import_jsx_runtime.jsx)("thead", { children: (0, import_jsx_runtime.jsxs)("tr", { children: [ (0, import_jsx_runtime.jsx)("th", { style: th, children: "#" }), (0, import_jsx_runtime.jsx)("th", { style: th, children: "插件名" }), (0, import_jsx_runtime.jsx)("th", { style: th, children: "页面类型" }), (0, import_jsx_runtime.jsx)("th", { style: th, children: "站点" }), (0, import_jsx_runtime.jsx)("th", { style: th, children: "CSS" }), (0, import_jsx_runtime.jsx)("th", { style: th, children: "Handle" }), (0, import_jsx_runtime.jsx)("th", { style: th, children: "耗时" }), (0, import_jsx_runtime.jsx)("th", { style: th, children: "错误信息" }), (0, import_jsx_runtime.jsx)("th", { style: th, children: "Flag" }) ] }) }), (0, import_jsx_runtime.jsx)("tbody", { children: rows.map((p, index) => { const failed = p.cssStatus === "rejected" || p.handleStatus === "rejected"; const error = p.cssError ?? p.handleError; return (0, import_jsx_runtime.jsxs)("tr", { className: failed ? "jhs-diag-failed" : void 0, style: failed ? { background: "rgba(239,68,68,0.04)" } : void 0, children: [ (0, import_jsx_runtime.jsx)("td", { style: { ...td, color: "#94a3b8" }, children: index + 1 }), (0, import_jsx_runtime.jsx)("td", { style: { ...td, fontWeight: failed ? 600 : 400 }, children: p.name }), (0, import_jsx_runtime.jsx)("td", { style: td, children: (0, import_jsx_runtime.jsx)(BadgeListCell, { values: p.pageTypes, tone: "page" }) }), (0, import_jsx_runtime.jsx)("td", { style: td, children: (0, import_jsx_runtime.jsx)(BadgeListCell, { values: p.sites, tone: "site" }) }), (0, import_jsx_runtime.jsx)("td", { style: td, children: (0, import_jsx_runtime.jsx)(StatusBadge, { status: p.cssStatus, durationMs: p.cssDurationMs }) }), (0, import_jsx_runtime.jsx)("td", { style: td, children: (0, import_jsx_runtime.jsx)(StatusBadge, { status: p.handleStatus, durationMs: p.handleDurationMs }) }), (0, import_jsx_runtime.jsx)("td", { style: td, children: (0, import_jsx_runtime.jsx)(DurationCell, { ms: p.totalDurationMs }) }), (0, import_jsx_runtime.jsx)("td", { style: td, children: error ? (0, import_jsx_runtime.jsx)("span", { className: "jhs-diag-error", title: error, style: { color: "#dc2626", fontSize: "11px", display: "inline-block", maxWidth: "200px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", verticalAlign: "bottom" }, children: error.length > 60 ? `${error.slice(0, 60)}…` : error }) : (0, import_jsx_runtime.jsx)("span", { style: { color: "#94a3b8", fontSize: "11px" }, children: "-" }) }), (0, import_jsx_runtime.jsx)("td", { style: td, children: p.isFeatureFlagged ? (0, import_jsx_runtime.jsxs)("span", { style: { ...BADGE_BASE, ...BADGE_COLORS.flag }, children: ["🚩 ", p.featureFlagName] }) : null }) ] }, p.name); }) })] })] }); } function DiagnosticsWrapper({ html }) { return (0, import_jsx_runtime.jsx)("div", { style: { padding: "15px" }, dangerouslySetInnerHTML: { __html: html } }); } function renderDiagnosticsHtml(pluginManager) { return jsxToString((0, import_jsx_runtime.jsx)(DiagnosticsTable, { data: pluginManager.getDiagnostics().plugins })); } function renderDiagnosticsText(pluginManager) { const diag = pluginManager.getDiagnostics(); const plugins = diag.plugins; const cssOk = plugins.filter((p) => p.cssStatus === "fulfilled").length; const handleOk = plugins.filter((p) => p.handleStatus === "fulfilled").length; const failed = plugins.filter((p) => p.cssStatus === "rejected" || p.handleStatus === "rejected"); const skipped = plugins.filter((p) => p.cssStatus === "skipped" && p.handleStatus === "skipped"); const flagged = plugins.filter((p) => p.isFeatureFlagged); const totalMs = plugins.reduce((sum, p) => sum + (p.totalDurationMs ?? 0), 0); const lines = plugins.map((p) => { const parts = [ `${p.cssStatus === "rejected" || p.handleStatus === "rejected" ? "❌" : p.cssStatus === "skipped" && p.handleStatus === "skipped" ? "⏭️" : "✅"} ${p.name}`, `CSS:${p.cssStatus}${p.cssDurationMs != null ? `(${p.cssDurationMs}ms)` : ""}`, `Handle:${p.handleStatus}${p.handleDurationMs != null ? `(${p.handleDurationMs}ms)` : ""}`, `总耗时:${p.totalDurationMs != null ? `${p.totalDurationMs}ms` : "-"}` ]; if (p.pageTypes.length) parts.push(`页面:[${p.pageTypes.join(",")}]`); if (p.sites.length) parts.push(`站点:[${p.sites.join(",")}]`); if (p.isFeatureFlagged) parts.push(`Flag:${p.featureFlagName}`); if (p.cssError) parts.push(`CSS错误:${p.cssError}`); if (p.handleError) parts.push(`Handle错误:${p.handleError}`); return parts.join(" | "); }); return [ "JavDB Power Tools 插件诊断报告", `总计 ${diag.total} 个插件 | CSS ${cssOk}/${diag.total} | Handle ${handleOk}/${diag.total} | 总耗时 ${totalMs}ms`, `失败 ${failed.length} 个 | 跳过 ${skipped.length} 个 | Feature Flag ${flagged.length} 个`, "", ...lines ].join("\n"); } function registerDiagnosticsMenu(pluginManager) { GM_registerMenuCommand("📊 插件诊断", () => { const diag = pluginManager.getDiagnostics(); const failed = diag.handle.filter((p) => p.status === "rejected"); layer.open({ type: 1, title: `插件诊断 — 共 ${diag.total} 个插件,${failed.length} 个失败`, area: ["600px", "70%"], content: jsxToString((0, import_jsx_runtime.jsx)(DiagnosticsWrapper, { html: renderDiagnosticsHtml(pluginManager) })) }); }); } function detectPageContext() { const href = window.location.href; const isDetailPage = href.split("?")[0].includes("/v/"); const isListPage = typeof $ !== "undefined" && ($(".movie-list").length > 0 || href.includes("advanced_search")); const isFc2Page = href.includes("advanced_search?type=3") || href.includes("advanced_search?type=100"); return { site: isJavdbSite ? "javdb" : isMissavSite ? "missav" : "unknown", pageType: isDetailPage ? "detail" : isFc2Page ? "fc2" : isListPage ? "list" : "unknown", isDetailPage, isListPage, isFc2Page, isJavdbSite, isMissavSite }; } var pageContext = { site: "unknown", pageType: "unknown", isDetailPage: false, isListPage: false, isFc2Page: false, isJavdbSite: false, isMissavSite: false }; function initPageContext() { pageContext = detectPageContext(); return pageContext; } function TemporaryImageContainer({ src, alt = "" }) { return (0, import_jsx_runtime.jsx)("div", { className: "temporary-container", style: { display: "none" }, children: (0, import_jsx_runtime.jsx)("img", { src, alt }) }); } var PluginManager = class { plugins = new Map(); registrationComplete = false; lastCssResults = []; lastHandleResults = []; featureFlags = new Map(); matchesPage(plugin) { const types = plugin.pageTypes; if (!types || types.length === 0) return true; return types.includes(pageContext.pageType); } register(pluginClass, featureFlag) { if (typeof pluginClass !== "function") throw new Error("插件必须是一个类"); const instance = new pluginClass(); instance.pluginManager = this; const pluginName = instance.getName(); if (this.plugins.has(pluginName)) throw new Error(`插件"${pluginName}"已注册`); if (featureFlag) this.featureFlags.set(pluginName, featureFlag); this.plugins.set(pluginName, instance); } getBean(name) { const plugin = this.plugins.get(name); if (!plugin && this.registrationComplete) clog.warn(`[PluginManager] getBean("${name}") 未找到已注册插件`); return plugin; } async processCss() { this.registrationComplete = true; const results = await Promise.all(Array.from(this.plugins).map(async ([name, plugin]) => { if (!this.matchesPage(plugin)) return { name, status: "skipped" }; const start = performance.now(); try { if (typeof plugin.initCss === "function") { const css = await plugin.initCss(); if (css) utils.insertStyle(css); return { name, status: "fulfilled", durationMs: Math.round(performance.now() - start) }; } return { name, status: "skipped" }; } catch (err) { clog.error(`插件 ${name} 加载 CSS 失败`, err); return { name, status: "rejected", error: err, durationMs: Math.round(performance.now() - start) }; } })); this.lastCssResults = results; const failed = results.filter((result) => result.status === "rejected"); if (failed.length) clog.error("以下插件的 CSS 加载失败:", failed.map((result) => result.name)); return results; } async processPlugins() { const results = await Promise.all(Array.from(this.plugins).map(async ([name, plugin]) => { if (!this.matchesPage(plugin)) return { name, status: "skipped" }; const start = performance.now(); try { if (typeof plugin.handle === "function") { await plugin.handle(); return { name, status: "fulfilled", durationMs: Math.round(performance.now() - start) }; } return { name, status: "skipped" }; } catch (err) { clog.error(`插件 ${name} 执行失败`, err); return { name, status: "rejected", error: err, durationMs: Math.round(performance.now() - start) }; } })); this.lastHandleResults = results; const failed = results.filter((result) => result.status === "rejected"); if (failed.length) clog.error("以下插件执行失败:", failed.map((result) => result.name)); return results; } getDiagnostics() { const cssByName = new Map(this.lastCssResults.map((result) => [result.name, result])); const handleByName = new Map(this.lastHandleResults.map((result) => [result.name, result])); const plugins = Array.from(this.plugins).map(([name, plugin]) => { const css = cssByName.get(name); const handle = handleByName.get(name); const cssDurationMs = css?.durationMs ?? null; const handleDurationMs = handle?.durationMs ?? null; const featureFlagName = this.featureFlags.get(name) ?? null; return { name, cssStatus: css?.status ?? "skipped", handleStatus: handle?.status ?? "skipped", cssDurationMs, handleDurationMs, totalDurationMs: cssDurationMs == null && handleDurationMs == null ? null : (cssDurationMs ?? 0) + (handleDurationMs ?? 0), cssError: css?.status === "rejected" ? css.error instanceof Error ? css.error.message : String(css.error) : null, handleError: handle?.status === "rejected" ? handle.error instanceof Error ? handle.error.message : String(handle.error) : null, pageTypes: [...plugin.pageTypes], sites: [...plugin.sites], isFeatureFlagged: featureFlagName != null, featureFlagName }; }); return { total: this.plugins.size, css: this.lastCssResults, handle: this.lastHandleResults, plugins }; } destroyAll() { for (const [name, plugin] of this.plugins) try { plugin.destroy(); } catch (err) { clog.error(`插件 ${name} 销毁失败`, err); } } }; var EDIT_SVG = "<svg t=\"1760920692801\" class=\"jhs-icon icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" p-id=\"3545\" width=\"200\" height=\"200\"><path d=\"M1013.929675 128.26571a143.759824 143.759824 0 0 1 10.44409 53.858738 84.576649 84.576649 0 0 1-5.836403 30.308339 92.870485 92.870485 0 0 1-18.635533 29.284408 1314.726599 1314.726599 0 0 1-24.983901 24.574329c-7.372299 7.06512-13.82306 13.311095-19.249891 18.737926-6.143582 6.143582-12.082378 11.672806-17.406817 16.382886L720.266444 82.598415c9.317766-8.601015 20.478607-18.942712 33.277737-31.02509s23.448006-21.604931 31.946628-28.67005a102.085858 102.085858 0 0 1 68.193763-22.731255c11.263234 0.307179 22.116896 2.047861 32.560985 5.222045 10.546483 3.071791 19.659463 6.655547 27.441334 10.546483 16.280493 8.601015 34.301667 23.550399 54.063524 45.052936 19.864249 21.502538 35.120812 43.82422 46.076867 67.272226z m-907.20231 570.943576l32.560986-33.38013c17.099637-17.509209 38.397389-39.216533 64.098041-64.917186l84.986221-85.395793 94.303987-94.815953 250.350976-251.477299L850.817567 389.163169 600.46659 640.640468l-93.177663 94.815953c-31.02509 30.410732-58.978389 58.364031-83.859898 83.655111-24.779115 25.29108-45.360116 46.17926-61.743001 62.562146a504.797674 504.797674 0 0 1-55.804206 50.274981c-10.239304 7.884264-20.581 14.130239-31.537055 18.737926a507.152714 507.152714 0 0 1-47.715156 19.86425 1609.311367 1609.311367 0 0 1-131.063087 42.185931c-20.478607 5.426831-35.837563 8.908194-45.974474 10.546483-20.88818 2.35504-34.813633-0.819144-41.981145-9.42016-6.860333-8.601015-8.805801-22.93604-5.73401-43.312254a396.261054 396.261054 0 0 1 11.058448-47.305584c5.836403-20.683394 12.082378-42.185931 18.635532-64.40522 6.553154-22.219289 13.003916-42.697897 19.249891-61.435822 6.143582-18.635533 11.263234-31.537055 15.15417-38.602176 4.607687-10.853662 9.829732-20.785787 15.666135-29.796373a192.49891 192.49891 0 0 1 25.086294-29.796374z\" fill=\"#FF9500\" p-id=\"3546\"></path></svg>"; var DELETE_SVG = "<svg t=\"1760921450746\" class=\"jhs-icon icon\" viewBox=\"0 0 1194 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" p-id=\"4530\" width=\"200\" height=\"200\"><path d=\"M761.086847 36.028779s309.754321-147.538628 424.952209 231.50509c2.047962 6.570546 71.337359 253.862013-220.838618 415.139055-12.970429 7.167869-267.515096 145.746661-370.339877 341.327076 0 0-90.963666-205.649563-393.379455-351.566888-6.399883-3.071944-304.549083-156.583796-163.751664-487.2444 3.669266-8.533177 163.666333-336.20717 466.423449-99.411511l24.575549 27.391498L387.931021 324.279495l237.648977 159.570408-109.139333 145.746661L625.579998 849.069874l-30.719437-205.820227 166.226286-169.81022-216.486698-168.103585L761.086847 36.028779z\" fill=\"#F4382E\" p-id=\"4531\"></path></svg>"; var REFRESH_SVG = "<svg t=\"1760926993643\" class=\"jhs-icon icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" p-id=\"5942\" width=\"200\" height=\"200\"><path d=\"M511.966722 0a511.966722 511.966722 0 1 0 179.828311 32.445891l-22.46254 59.964102A447.970882 447.970882 0 1 1 511.966722 63.99584a31.99792 31.99792 0 0 0 0-63.99584z\" fill=\"#333333\" p-id=\"5943\"></path><path d=\"M649.2378 9.151405A30.909991 30.909991 0 0 1 671.316364 0h193.267438a31.99792 31.99792 0 0 1 31.357962 31.99792c0 17.662852-13.759106 31.99792-31.357962 31.99792H703.954243v160.629559a31.99792 31.99792 0 0 1-31.99792 31.357962 31.485953 31.485953 0 0 1-31.99792-31.357962V31.357962c0-8.511447 3.647763-16.318939 9.343392-21.950573z\" fill=\"#333333\" p-id=\"5944\"></path></svg>"; var BasePlugin = class { pluginManager; get sites() { return []; } get pageTypes() { return []; } destroy() {} editSvg = EDIT_SVG; deleteSvg = DELETE_SVG; refreshSvg = REFRESH_SVG; getName() { throw new Error(`${this.constructor.name} 未显示getName()`); } getBean(name) { return this.pluginManager.getBean(name); } async initCss() { return ""; } async handle() {} getPageInfo() { let carNum = null; let url = null; let actress = null; let actors = null; let publishTime = null; const href = window.location.href; if (isJavdbSite) { carNum = $("a[title=\"複製番號\"]").attr("data-clipboard-text") ?? null; url = href.split("?")[0].split("#")[0]; actress = $(".female").prev().map((_index, el) => $(el).text()).get().join(" "); actors = $(".male").prev().map((_index, el) => $(el).text()).get().join(" "); publishTime = $("strong:contains(\"日期:\")").parent(".panel-block").find(".value").text().trim(); } return { carNum, url, actress, actors, publishTime }; } getActressId() { const match = currentHref.match(/\/actors\/([^/?]+)/); if (match && match.length > 1) return match[1]; else return null; } getActressPageInfo() { const href = window.location.href; if (!href.includes("/actors/") && !href.includes("/star/")) throw new Error("接口调用错误, 非演员详情页"); const allName = []; const nameEl = $(".actor-section-name"); if (nameEl.length) nameEl.text().trim().split(",").forEach((part) => { allName.push(part.trim()); }); const metaEl = $(".section-meta:not(:contains('影片'))"); if (metaEl.length) metaEl.text().trim().split(",").forEach((part) => { allName.push(part.trim()); }); const role = $(".section-meta:contains('男優')").length > 0 ? ACTOR : ACTRESS; let movieType = CENSORED; if (allName.some((part) => part.includes("無碼"))) movieType = UNCENSORED; if (href.includes("uncensored")) movieType = UNCENSORED; let blacklistUrl = null; let starId = null; const urlObj = new URL(href); if (isJavdbSite) { starId = urlObj.pathname.split("/").filter((seg) => seg.trim() !== "").pop() ?? null; const searchParams = urlObj.searchParams; searchParams.delete("sort_type"); searchParams.delete("page"); blacklistUrl = urlObj.toString(); } return { starId, name: allName[0], allName, role, movieType, blacklistUrl }; } getSelector(siteType) { const type = siteType || "javdb"; const selectors = { javdb: { boxSelector: ".movie-list", itemSelector: ".movie-list .item", coverImgSelector: ".cover img", requestDomItemSelector: ".movie-list .item", nextPageSelector: ".pagination-next" } }; if (!type || !selectors[type]) throw new Error("类型错误: 无法确定选择器类型 (JavDb)"); return selectors[type]; } parseMovieId(url) { return url.split("/").pop().split(/[?#]/)[0]; } getBoxCarInfo(boxEl) { let titleAttr; let imgTitleAttr; let dataTitleAttr; const linkEl = boxEl.find("a"); const url = linkEl.attr("href"); let carNum = null; let title = null; let publishTime = null; const videoTitleEl = boxEl.find(".video-title"); if (videoTitleEl.length > 0) { const strongEl = videoTitleEl.find("strong"); if (strongEl.length > 0) carNum = strongEl.text().trim(); title = (titleAttr = linkEl.attr("title")) == null ? void 0 : titleAttr.trim(); if (!title) { const text = videoTitleEl.text().trim(); title = carNum && text.includes(carNum) ? text.replace(carNum, "").trim() : text; } publishTime = boxEl.find(".meta").text().trim(); } if (!carNum) { const imgEl = boxEl.find("img"); if (imgEl.length > 0) title = ((imgTitleAttr = imgEl.attr("title")) == null ? void 0 : imgTitleAttr.trim()) || ((dataTitleAttr = imgEl.attr("data-title")) == null ? void 0 : dataTitleAttr.trim()); const dates = boxEl.find("date").map((_index, el) => $(el).text().trim()).get(); const isDate = (val) => /^\d{4}-\d{1,2}-\d{1,2}$/.test(val); publishTime = dates.find(isDate) || null; carNum = dates.find((val) => !isDate(val)) || null; } if (!carNum) { const msg = "提取番号信息失败: carNum 为空"; clog.error("Error in getBoxCarInfo:", msg, "Box Element:", boxEl.get(0)); failWithToast(msg); } if (featureFlags.westernCarFormat) carNum = this._formatWesternCar(carNum, publishTime || ""); return { carNum, url: url || "", title: title || "", publishTime: publishTime || "" }; } _formatWesternCar(carNum, rawDate) { if (!carNum || !rawDate) return carNum; if (!/^[a-zA-Z\s]+$/.test(carNum)) return carNum; const dateMatch = rawDate.match(/\d{2}(\d{2})-(\d{2})-(\d{2})/); if (dateMatch) return `${carNum.replace(/\s+/g, "")}.${dateMatch[1]}.${dateMatch[2]}.${dateMatch[3]}`; return carNum; } getBoxCarInfoList(boxEls = null) { boxEls ||= $(this.getSelector().itemSelector); if (boxEls.length === 0) { clog.error("获取当前列表页所有item的番号信息失败!"); return []; } const list = []; boxEls.each((index, element) => { const itemEl = $(element); try { const info = this.getBoxCarInfo(itemEl); list.push(info); } catch (err) { clog.error("[getBoxCarInfoList] 提取单个 boxCar 信息失败:", err instanceof Error ? err.message : String(err), "元素索引:", index); } }); return list; } checkDuplicateCarNumbers(list, other) { if (!list || list.length === 0 || !other || other.length === 0) return false; const existing = new Set(list.map((item) => item.carNum).filter(Boolean)); if (existing.size === 0) return false; let duplicateCount = 0; for (const item of other) if (item && item.carNum && existing.has(item.carNum)) duplicateCount++; if (duplicateCount / other.length >= .5) { clog.warn("警告: 检测到大量番号信息重复, 该类别可能已被限制页码。"); return true; } return false; } }; var DetailPagePlugin = class extends BasePlugin { getName() { return "DetailPagePlugin"; } get pageTypes() { return ["detail"]; } constructor() { super(); } async handle() { if (window.isDetailPage) { $(".video-meta-panel a").each((_index, element) => { const href = $(element).attr("href"); if (href && (href.startsWith("http://") || href.startsWith("https://") || href.startsWith("/"))) $(element).attr("target", "_blank"); }); this.handleFancyBox(); } } handleFancyBox() { document.addEventListener("click", (event) => { if (event.target.closest(".fancybox-button--thumbs")) { const thumbsVisible = !$(".fancybox-thumbs").is(":hidden"); localStorage.setItem("jhs_fancyboxThumbs", thumbsVisible.toString()); unsafeWindow.$.fancybox.defaults.thumbs.autoStart = thumbsVisible; } }); if (unsafeWindow.$.fancybox !== void 0) { const storedValue = localStorage.getItem("jhs_fancyboxThumbs"); unsafeWindow.$.fancybox.defaults.thumbs.autoStart = storedValue === "true"; } } }; var HighlightMagnetPlugin = class extends BasePlugin { getName() { return "HighlightMagnetPlugin"; } get pageTypes() { return ["detail"]; } doFilterMagnet() { this.handleDb(); } handleDb() { if (!isJavdbSite) return; const nameEls = $("#magnets-content .name"); if (nameEls.length === 0) return; const highQualityMarkers = [ "4k", "-c", "-u", "-uc" ]; let hasHighQuality = false; nameEls.each((_index, element) => { const nameEl = $(element); const nameText = nameEl.text().toLowerCase(); const isHighQuality = highQualityMarkers.some((marker) => nameText.includes(marker)); nameEl.parent().parent().parent().addClass("magnet-row"); if (nameText.includes("4k")) nameEl.css("color", "#f40"); if (isHighQuality) { hasHighQuality = true; nameEl.parent().parent().parent().addClass("high-quality"); } }); if (hasHighQuality) $("#magnets-content .magnet-row").not(".high-quality").hide(); else $("#enable-magnets-filter").addClass("do-hide"); } showAll() { if (isJavdbSite) $("#magnets-content .item").toArray().forEach((element) => $(element).show()); } }; function FoldCategorySectionButton() { return (0, import_jsx_runtime.jsx)("div", { id: "foldCategoryBtn", children: (0, import_jsx_runtime.jsxs)("a", { className: "jhs-toolbar-menu-btn", style: { backgroundColor: "#d23e60 !important", marginLeft: "20px", borderBottom: "none !important", borderRadius: "3px" }, children: [(0, import_jsx_runtime.jsx)("span", {}), (0, import_jsx_runtime.jsx)("i", { style: { marginLeft: "10px" } })] }) }); } function FoldCategoryToolbar({ selectedTagsText }) { return (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", flexGrow: 1, justifyContent: "flex-end" }, children: [(0, import_jsx_runtime.jsxs)("div", { children: ["已选分类: ", (0, import_jsx_runtime.jsx)("span", { id: "jhs-check-tag", children: selectedTagsText })] }), (0, import_jsx_runtime.jsxs)("a", { className: "jhs-toolbar-menu-btn main-tab-btn", id: "foldCategoryBtn", style: { backgroundColor: "#d23e60 !important" }, children: [(0, import_jsx_runtime.jsx)("span", {}), (0, import_jsx_runtime.jsx)("i", { style: { marginLeft: "10px" } })] })] }); } function HighlightButton() { return (0, import_jsx_runtime.jsx)("button", { className: "highlight-btn", title: "高亮显示", children: "★" }); } var fold_category_plugin_default = "#tags a.tag,\n.tags a.tag {\n position: relative;\n}\n\n.highlight-btn {\n position: absolute;\n top: -10px;\n right: -10px;\n background-color: #4caf50;\n color: white;\n border: none;\n border-radius: 50%;\n width: 24px;\n height: 24px;\n font-size: 14px;\n line-height: 24px;\n text-align: center;\n cursor: pointer;\n box-shadow: 0 2px 4px rgb(0, 0, 0, 0.2);\n display: none;\n z-index: 999;\n}\n\n/* 当父元素被高亮时,按钮变为其他颜色 */\n.highlighted .highlight-btn {\n background-color: #ff5722;\n}\n\n/* 高亮状态下的标签样式 */\n.highlighted {\n /* 浅黄色 */\n border: __HIGHLIGHTED_TAG_NUMBER__px solid __HIGHLIGHTED_TAG_COLOR__;\n}\n"; var FoldCategoryPlugin = class extends BasePlugin { getName() { return "FoldCategoryPlugin"; } get pageTypes() { return ["list"]; } async initCss() { const settings = await storageManager.getSetting(); return fold_category_plugin_default.replace("__HIGHLIGHTED_TAG_NUMBER__", String(settings.highlightedTagNumber || 1)).replace("__HIGHLIGHTED_TAG_COLOR__", settings.highlightedTagColor || "#ce2222"); } async handle() { if (window.isListPage) { if (!currentHref.includes("advanced_search")) { this.highlightTag(); utils.loopDetector(() => currentHref.includes("/actors/") || $("#waitCheckBtn").length > 0, () => { this.createFoldBtn(); }, 1, 1e4, true); $("#tags .tag-category .tag-expand").each((_index, expandEl) => { if ($(expandEl).parent().hasClass("collapse")) expandEl.click(); }); } } } highlightTag() { (async () => { const highlightedTags = await storageManager.getHighlightedTags(); if (highlightedTags) highlightedTags.forEach((tagName) => { $(`#tags a.tag:contains(${tagName})`).addClass("highlighted"); $(`.tags a.tag:contains(${tagName})`).addClass("highlighted"); }); })().then(); $("#tags a.tag, .tags a.tag").hover(function() { const tagEl = $(this); const btnEl = $(jsxToString((0, import_jsx_runtime.jsx)(HighlightButton, {}))); tagEl.append(btnEl); btnEl.fadeIn(0); }, function() { $(this).find(".highlight-btn").fadeOut(0, function() { $(this).remove(); }); }); $(document).on("click", ".highlight-btn", async function(event) { event.stopPropagation(); event.preventDefault(); const tagEl = $(this).closest("a.tag"); const cloneEl = tagEl.clone(); cloneEl.find(".highlight-btn").remove(); const tagName = cloneEl.text().trim().replace(/\s*\(\d+\)$/, ""); let highlightedTags = await storageManager.getHighlightedTags(); if (highlightedTags.includes(tagName)) { highlightedTags = highlightedTags.filter((item) => item !== tagName); tagEl.removeClass("highlighted"); } else { highlightedTags.push(tagName); tagEl.addClass("highlighted"); } await storageManager.setHighlightedTags(highlightedTags); }); } async createFoldBtn() { let tagsEl = $("#tags"); const selectedTagsText = $("#tags dl div.tag.is-info").map(function() { return $(this).text().replace(/\n/g, "").replace(/ /g, ""); }).get().join(" "); if (!selectedTagsText) return; $(".tabs").append(jsxToString((0, import_jsx_runtime.jsx)(FoldCategoryToolbar, { selectedTagsText }))); const sectionTitleEl = $("h2.section-title"); if (sectionTitleEl.length > 0) { sectionTitleEl.append(jsxToString((0, import_jsx_runtime.jsx)(FoldCategorySectionButton, {}))); tagsEl = $("section > div > div.box"); } if (!tagsEl) return; const foldBtnEl = $("#foldCategoryBtn"); let isFolded = localStorage.getItem("jhs_foldCategory") === "yes"; const [label, iconClass] = isFolded ? ["展开", "icon-angle-double-down"] : ["折叠", "icon-angle-double-up"]; foldBtnEl.find("span").text(label).end().find("i").attr("class", iconClass); if (!window.location.href.includes("noFold=1")) tagsEl[isFolded ? "hide" : "show"](); foldBtnEl.on("click", async (event) => { event.preventDefault(); isFolded = !isFolded; localStorage.setItem("jhs_foldCategory", isFolded ? "yes" : "no"); const [nextLabel, nextIconClass] = isFolded ? ["展开", "icon-angle-double-down"] : ["折叠", "icon-angle-double-up"]; foldBtnEl.find("span").text(nextLabel).end().find("i").attr("class", nextIconClass); tagsEl[isFolded ? "hide" : "show"](); }); } }; function ActressInfoDetailSegment({ actressName, info, wikiApiUrl }) { const nameNode = (0, import_jsx_runtime.jsxs)("strong", { style: { marginRight: "5px" }, children: [(0, import_jsx_runtime.jsx)("span", { style: { userSelect: "all" }, children: actressName }), ":"] }); if (info) return (0, import_jsx_runtime.jsxs)("div", { className: "panel-block actress-info", children: [nameNode, (0, import_jsx_runtime.jsxs)("a", { href: info.url, style: { marginLeft: "5px" }, target: "_blank", children: [ (0, import_jsx_runtime.jsxs)("span", { className: "info-tag", children: [ info.birthday, " ", info.age ] }), (0, import_jsx_runtime.jsxs)("span", { className: "info-tag", children: [ info.height, " ", info.weight ] }), (0, import_jsx_runtime.jsxs)("span", { className: "info-tag", children: [ info.threeSizeText, " ", info.braSize ] }) ] })] }); return (0, import_jsx_runtime.jsxs)("div", { className: "panel-block actress-info", children: [(0, import_jsx_runtime.jsx)("a", { href: wikiApiUrl + actressName, target: "_blank", children: nameNode }), (0, import_jsx_runtime.jsx)("span", { style: { marginLeft: "5px", color: "#999" }, children: "暂无此演员信息" })] }); } function ActressInfoStarPageHtml({ info }) { if (info) return (0, import_jsx_runtime.jsx)("a", { className: "actress-info", href: info.url, target: "_blank", children: (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: "17px", fontWeight: "normal", marginTop: "5px" }, children: [(0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", marginBottom: "10px" }, children: [ (0, import_jsx_runtime.jsxs)("span", { style: { width: "300px" }, children: ["出生日期: ", info.birthday] }), (0, import_jsx_runtime.jsxs)("span", { style: { width: "200px" }, children: ["年龄: ", info.age] }), (0, import_jsx_runtime.jsxs)("span", { style: { width: "200px" }, children: ["身高: ", info.height] }) ] }), (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", marginBottom: "10px" }, children: [ (0, import_jsx_runtime.jsxs)("span", { style: { width: "300px" }, children: ["体重: ", info.weight] }), (0, import_jsx_runtime.jsxs)("span", { style: { width: "200px" }, children: ["三围: ", info.threeSizeText] }), (0, import_jsx_runtime.jsxs)("span", { style: { width: "200px" }, children: ["罩杯: ", info.braSize] }) ] })] }) }); return (0, import_jsx_runtime.jsx)("div", { className: "actress-info", style: { fontSize: "17px", fontWeight: "normal", marginTop: "5px" }, children: "无此相关演员信息" }); } var actress_info_plugin_default = ".info-tag {\n background-color: #ecf5ff;\n display: inline-block;\n height: 32px;\n padding: 0 10px;\n line-height: 30px;\n font-size: 12px;\n color: #409eff;\n border: 1px solid #d9ecff;\n border-radius: 4px;\n box-sizing: border-box;\n white-space: nowrap;\n}\n"; var ActressInfoPlugin = class extends BasePlugin { apiUrl = "https://ja.wikipedia.org/wiki/"; getName() { return "ActressInfoPlugin"; } async handle() { if (await storageManager.getSetting("enableLoadActressInfo", "yes") === "yes") this.loadActressInfo(); } loadActressInfo() { this.handleDetailPage().then(); this.handleStarPage().then(); } async initCss() { return actress_info_plugin_default; } async handleDetailPage() { if ($(".actress-info").length > 0) return; const actressNames = $(".female").prev().map((_index, element) => $(element).text().trim()).get(); if (!actressNames.length) return; const storageKey = "jhs_actress_info"; const rawCache = localStorage.getItem(storageKey); let cache = {}; try { if (rawCache) cache = JSON.parse(rawCache); } catch {} let info; let html = ""; for (const actressName of actressNames) { info = cache[actressName]; if (!info) try { info = await this.searchInfo(actressName); if (info) cache[actressName] = info; } catch { clog.error("该名称查询失败,尝试其它名称"); } const segment = jsxToString((0, import_jsx_runtime.jsx)(ActressInfoDetailSegment, { actressName, info, wikiApiUrl: this.apiUrl })); html += segment; } $("strong:contains(\"演員\")").parent().after(html); localStorage.setItem(storageKey, JSON.stringify(cache)); } async handleStarPage() { if ($(".actress-info").length > 0) return; const names = []; const nameSectionEl = $(".actor-section-name"); if (nameSectionEl.length) nameSectionEl.text().trim().split(",").forEach((name) => { names.push(name.trim()); }); const metaEl = $(".section-meta:not(:contains('影片'))"); if (metaEl.length) metaEl.text().trim().split(",").forEach((name) => { names.push(name.trim()); }); if (!names.length) return; const storageKey = "jhs_actress_info"; const rawCache = localStorage.getItem(storageKey); let cache = {}; try { if (rawCache) cache = JSON.parse(rawCache); } catch {} let info = null; for (const actressName of names) { info = cache[actressName]; if (info) break; try { info = await this.searchInfo(actressName); } catch { clog.error("该名称查询失败,尝试其它名称"); } if (info) break; } if (info) names.forEach((actressName) => { cache[actressName] = info; }); const html = jsxToString((0, import_jsx_runtime.jsx)(ActressInfoStarPageHtml, { info })); nameSectionEl.parent().append(html); localStorage.setItem(storageKey, JSON.stringify(cache)); } async searchInfo(actressName) { if (actressName === "三上悠亞") actressName = "三上悠亜"; const url = this.apiUrl + actressName; const responseHtml = String(await gmHttp.get(url)); const domParser = new DOMParser(); const $parsed = $(domParser.parseFromString(responseHtml, "text/html")); const birthday = $parsed.find("a[title=\"誕生日\"]").parent().parent().find("td").text().trim(); const age = $parsed.find("th:contains('現年齢')").parent().find("td").text().trim() ? parseInt($parsed.find("th:contains('現年齢')").parent().find("td").text().trim()) + "岁" : ""; const height = $parsed.find("tr:has(a[title=\"身長\"]) td").text().trim().split(" ")[0] + "cm"; let weight = $parsed.find("tr:has(a[title=\"体重\"]) td").text().trim().split("/")[1].trim(); if (weight === "― kg") weight = ""; return { birthday, age, height, weight, threeSizeText: $parsed.find("a[title=\"スリーサイズ\"]").closest("tr").find("td").text().replace("cm", "").trim(), braSize: $parsed.find("th:contains(\"ブラサイズ\")").next("td").contents().first().text().trim(), url }; } }; var API_BASE = "https://jdforrepam.com/api"; var SIGNATURE_TIME_KEY = "jhs_review_ts"; var SIGNATURE_VALUE_KEY = "jhs_review_sign"; var SIGNATURE_SINGLE_KEY = "jhs_jdsignature"; var SIGNATURE_TTL_SECONDS = 20; var SIGNATURE_TTL_SECONDS_V2 = 300; var SIGNATURE_NAMESPACE = "lpw6vgqzsp"; var SIGNATURE_SALT = "71cf27bb3c0bcdf207b64abecddc970098c7421ee7203b9cdae54478478a199e7d5a6e1a57691123c1a931c057842fb73ba3b3c83bcd69c17ccf174081e3d8aa"; function reBuildSignature() { const now = Math.floor(Date.now() / 1e3); if (featureFlags.upgradeSignature300s) { const stored = localStorage.getItem(SIGNATURE_SINGLE_KEY); if (stored) { const parts = stored.split("."); if (parts.length === 3 && now - parseInt(parts[0], 10) <= SIGNATURE_TTL_SECONDS_V2) return stored; } const sig = `${now}.${SIGNATURE_NAMESPACE}.${md5(`${now}${SIGNATURE_SALT}`)}`; localStorage.setItem(SIGNATURE_SINGLE_KEY, sig); return sig; } if (now - Number(localStorage.getItem(SIGNATURE_TIME_KEY) || 0) <= SIGNATURE_TTL_SECONDS) return localStorage.getItem(SIGNATURE_VALUE_KEY); const sig = `${now}.${SIGNATURE_NAMESPACE}.${md5(`${now}${SIGNATURE_SALT}`)}`; localStorage.setItem(SIGNATURE_TIME_KEY, String(now)); localStorage.setItem(SIGNATURE_VALUE_KEY, sig); return sig; } function _updateImgServer(str) { return str.replace(/https:\/\/.*?\/rhe951l4q/g, "https://c0.jdbstatic.com"); } async function fetchMovieReviews(movieId, page = 1, limit = 20) { const url = `${API_BASE}/v1/movies/${encodeURIComponent(movieId)}/reviews`; const headers = { jdSignature: await reBuildSignature() }; return (await gmHttp.get(url, { page, sort_by: "hotly", limit }, headers)).data.reviews; } async function fetchMovieDetail(movieId) { const url = `${API_BASE}/v4/movies/${encodeURIComponent(movieId)}`; const headers = { jdSignature: await reBuildSignature() }; const resp = await gmHttp.get(url, void 0, headers); if (!resp.data) failWithToast("获取视频详情失败: " + resp.message); const movie = resp.data.movie; const imgList = []; movie.preview_images.forEach((img) => { imgList.push(_updateImgServer(img.large_url)); }); return { movieId: movie.id, actors: movie.actors, duration: movie.duration, title: movie.origin_title, carNum: movie.number, score: movie.score, releaseDate: movie.release_date, watchedCount: movie.watched_count, imgList }; } async function fetchRelatedCollections(movieId, page = 1, limit = 20) { const url = `${API_BASE}/v1/lists/related?movie_id=${encodeURIComponent(movieId)}&page=${page}&limit=${limit}`; const headers = { jdSignature: await reBuildSignature() }; const resp = await gmHttp.get(url, void 0, headers); const list = []; resp.data.lists.forEach((item) => { list.push({ relatedId: item.id, name: item.name, movieCount: item.movies_count, collectionCount: item.collections_count, viewCount: item.views_count, createTime: utils.formatDate(item.created_at) }); }); return list; } async function fetchPlaybackRanking(period = "daily", filterBy = "high_score") { const url = `${API_BASE}/v1/rankings/playback?period=${period}&filter_by=${filterBy}`; const headers = { jdSignature: await reBuildSignature() }; return (await gmHttp.get(url, void 0, headers)).data.movies; } var APP_AUTH_KEY$1 = "jhs_appAuthorization"; async function DEFAULT_REQUEST_HEADERS() { return { "user-agent": "Dart/3.5 (dart:io)", "accept-language": "zh-TW", host: "jdforrepam.com", authorization: "Bearer " + (localStorage.getItem(APP_AUTH_KEY$1) || ""), jdsignature: await reBuildSignature() }; } async function fetchTopMovies(type = "all", typeValue = "", page = 1, limit = 40) { const url = `${API_BASE}/v1/movies/top?start_rank=1&type=${type}&type_value=${typeValue}&ignore_watched=false&page=${page}&limit=${limit}`; const headers = await DEFAULT_REQUEST_HEADERS(); return await gmHttp.get(url, void 0, headers); } function HitShowMovieItem({ movie }) { return (0, import_jsx_runtime.jsx)("div", { className: "item", id: String(movie.id), children: (0, import_jsx_runtime.jsxs)("a", { href: `/v/${movie.id}`, className: "box", title: movie.origin_title, children: [ (0, import_jsx_runtime.jsx)("div", { className: "cover ", children: (0, import_jsx_runtime.jsx)("img", { loading: "lazy", src: _updateImgServer(movie.cover_url || ""), alt: "" }) }), (0, import_jsx_runtime.jsxs)("div", { className: "video-title", children: [ (0, import_jsx_runtime.jsx)("strong", { children: movie.number }), " ", movie.origin_title ] }), (0, import_jsx_runtime.jsx)("div", { className: "score", id: `score_${movie.id}` }), (0, import_jsx_runtime.jsx)("div", { className: "meta", children: movie.release_date }), (0, import_jsx_runtime.jsxs)("div", { className: "tags has-addons", children: [ movie.has_cnsub ? (0, import_jsx_runtime.jsx)("span", { className: "tag is-warning", children: "含中字磁鏈" }) : movie.magnets_count > 0 ? (0, import_jsx_runtime.jsx)("span", { className: "tag is-success", children: "含磁鏈" }) : (0, import_jsx_runtime.jsx)("span", { className: "tag is-info", children: "无磁鏈" }), " ", movie.new_magnets ? (0, import_jsx_runtime.jsx)("span", { className: "tag is-info", children: "今日新種" }) : null ] }) ] }) }); } function getStarRating(score) { const stars = []; const fullStars = Math.floor(score); for (let i = 0; i < fullStars; i++) stars.push((0, import_jsx_runtime.jsx)("i", { className: "icon-star" })); for (let i = 0; i < 5 - fullStars; i++) stars.push((0, import_jsx_runtime.jsx)("i", { className: "icon-star gray" })); return stars; } function HitShowScore({ score, watchedCount }) { return (0, import_jsx_runtime.jsxs)("span", { className: "value", children: [ (0, import_jsx_runtime.jsx)("span", { className: "score-stars", children: getStarRating(score) }), " \xA0 ", score, "分,由", watchedCount, "人評價" ] }); } function HitShowToolBar({ period }) { return (0, import_jsx_runtime.jsx)("div", { className: "button-group", style: { marginTop: "18px" }, children: (0, import_jsx_runtime.jsxs)("div", { className: "buttons has-addons", id: "conditionBox", children: [ (0, import_jsx_runtime.jsx)("a", { style: { padding: "18px 18px !important" }, className: `button is-small ${period === "daily" ? "is-info" : ""}`, href: "/advanced_search?handlePlayback=1&period=daily", children: "日榜" }), " ", (0, import_jsx_runtime.jsx)("a", { style: { padding: "18px 18px !important" }, className: `button is-small ${period === "weekly" ? "is-info" : ""}`, href: "/advanced_search?handlePlayback=1&period=weekly", children: "周榜" }), " ", (0, import_jsx_runtime.jsx)("a", { style: { padding: "18px 18px !important" }, className: `button is-small ${period === "monthly" ? "is-info" : ""}`, href: "/advanced_search?handlePlayback=1&period=monthly", children: "月榜" }) ] }) }); } function RankingContainers() { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [(0, import_jsx_runtime.jsx)("div", { className: "tool-box", style: { marginTop: "10px" } }), (0, import_jsx_runtime.jsx)("div", { className: "movie-list h cols-4 vcols-8", style: { marginTop: "10px" } })] }); } var HitShowPlugin = class extends BasePlugin { contentBox = $(".section .container"); getName() { return "HitShowPlugin"; } async handle() { $("a[href*=\"rankings/playback\"]").on("click", (event) => { event.preventDefault(); event.stopPropagation(); window.location.href = "/advanced_search?handlePlayback=1&period=daily"; }); this.handlePlayback().then(); } hookPage() { const titleEl = $("h2.section-title"); titleEl.contents().first().replaceWith("热播"); titleEl.css("marginBottom", "0"); $(".empty-message").remove(); $(".section .container .box").remove(); $("#sort-toggle-btn").remove(); this.contentBox.append(jsxToString((0, import_jsx_runtime.jsx)(RankingContainers, {}))); } async handlePlayback() { if (!window.location.href.includes("handlePlayback=1")) return; const period = new URLSearchParams(window.location.search).get("period"); this.toolBar(period); this.hookPage(); const movieListEl = $(".movie-list"); movieListEl.html(""); await withLoading(async () => { let success = false; for (let attempt = 1; attempt <= 3 && !success; attempt++) try { const movies = await fetchPlaybackRanking(period ?? "daily"); const html = this.markDataListHtml(movies); movieListEl.html(html); this.loadScore(movies); success = true; } catch (error) { if (attempt < 3) { clog.error(`获取热播数据失败 (第 ${attempt} 次重试)`, error); await new Promise((resolve) => setTimeout(resolve, 1e3)); } else clog.error("所有重试尝试均失败,无法获取数据。", error); } }); } toolBar(period) { this.contentBox.append(jsxToString((0, import_jsx_runtime.jsx)(HitShowToolBar, { period }))); } loadScore(movies) { if (movies.length === 0) return; (async () => { const storageKey = "jhs_score_info"; for (const movie of movies) try { const movieId = movie.id; if (!$(`#score_${movieId}`).length) return; if ($(`#${movieId}`).is(":hidden")) continue; const rawCache = localStorage.getItem(storageKey); const cache = rawCache ? JSON.parse(rawCache) : {}; const cachedHtml = cache[movieId]; if (cachedHtml) { this.appendScoreHtml(movieId, cachedHtml); continue; } while (!document.hasFocus()) await new Promise((resolve) => setTimeout(resolve, 500)); const detail = await fetchMovieDetail(movieId); const score = detail.score; const watchedCount = detail.watchedCount; const scoreHtml = jsxToString((0, import_jsx_runtime.jsx)(HitShowScore, { score, watchedCount })); this.appendScoreHtml(movieId, scoreHtml); cache[movieId] = scoreHtml; localStorage.setItem(storageKey, JSON.stringify(cache)); await new Promise((resolve) => setTimeout(resolve, 500)); } catch (error) { clog.error(`🚨 解析评分数据失败 | 编号: ${movie.number}\n`, `错误详情: ${error instanceof Error ? error.message : String(error)}\n`, error instanceof Error && error.stack ? `调用栈:\n${error.stack}` : ""); } })(); } appendScoreHtml(movieId, html) { const scoreEl = $(`#score_${movieId}`); if (scoreEl.length && scoreEl.html().trim() === "") scoreEl.slideUp(0, function() { $(this).html(html).slideDown(500); }); } markDataListHtml(movies) { let html = ""; movies.forEach((movie) => { html += jsxToString((0, import_jsx_runtime.jsx)(HitShowMovieItem, { movie })); }); return html; } }; function LoginDialog() { return (0, import_jsx_runtime.jsxs)("div", { style: { padding: "30px", fontFamily: "'Helvetica Neue', Arial, sans-serif" }, children: [ (0, import_jsx_runtime.jsx)("div", { style: { marginBottom: "25px" }, children: (0, import_jsx_runtime.jsx)("input", { type: "text", id: "username", name: "username", style: { width: "100%", padding: "12px 15px", border: "1px solid #e0e0e0", borderRadius: "4px", boxSizing: "border-box", transition: "all 0.3s", fontSize: "14px", background: "#f9f9f9", color: "#333" }, placeholder: "用户名 | 邮箱" }) }), (0, import_jsx_runtime.jsx)("div", { style: { marginBottom: "15px" }, children: (0, import_jsx_runtime.jsx)("input", { type: "password", id: "password", name: "password", style: { width: "100%", padding: "12px 15px", border: "1px solid #e0e0e0", borderRadius: "4px", boxSizing: "border-box", transition: "all 0.3s", fontSize: "14px", background: "#f9f9f9", color: "#333" }, placeholder: "密码" }) }), (0, import_jsx_runtime.jsx)("button", { id: "loginBtn", style: { width: "100%", padding: "12px", background: "#4a8bfc", color: "white", border: "none", borderRadius: "4px", fontSize: "15px", cursor: "pointer", transition: "background 0.3s" }, children: "登录" }) ] }); } function Top250ErrorMessage({ message }) { return (0, import_jsx_runtime.jsx)("h3", { children: message }); } function Top250LoadError() { return (0, import_jsx_runtime.jsx)("h3", { children: "无法加载数据,请稍后再试。" }); } function Top250NavLink() { return (0, import_jsx_runtime.jsx)("a", { href: "/rankings/top", children: (0, import_jsx_runtime.jsx)("span", { children: "Top250" }) }); } function Top250Pagination({ page }) { const hasMore = page >= 5; return (0, import_jsx_runtime.jsxs)("nav", { className: "pagination", children: [ (0, import_jsx_runtime.jsx)("a", { className: `pagination-previous ${page <= 1 ? "do-hide" : ""}`, "data-page": page - 1, children: "上一頁" }), (0, import_jsx_runtime.jsx)("a", { className: `pagination-next ${hasMore ? "do-hide" : ""}`, "data-page": page + 1, children: "下一頁" }), (0, import_jsx_runtime.jsx)("ul", { className: "pagination-list", children: Array.from({ length: 5 }, (_, i) => i + 1).map((i) => (0, import_jsx_runtime.jsx)("li", { children: (0, import_jsx_runtime.jsx)("a", { className: `pagination-link ${page === i ? "is-current" : ""}`, "data-page": i, children: i }) }, i)) }) ] }); } function Top250ToolBar({ handleType, typeValue, hasCnsub, yearButtonsHtml }) { return (0, import_jsx_runtime.jsxs)("div", { className: "button-group", children: [(0, import_jsx_runtime.jsxs)("div", { className: "buttons has-addons", id: "conditionBox", style: { marginBottom: "0!important" }, children: [ (0, import_jsx_runtime.jsx)("a", { style: { padding: "18px 18px !important" }, className: `button is-small ${handleType === "all" ? "is-info" : ""}`, href: `/advanced_search?handleTop=1&handleType=all&type_value=&has_cnsub=${hasCnsub}`, children: "全部" }), " ", (0, import_jsx_runtime.jsx)("a", { style: { padding: "18px 18px !important" }, className: `button is-small ${typeValue === "0" ? "is-info" : ""}`, href: `/advanced_search?handleTop=1&handleType=video_type&type_value=0&has_cnsub=${hasCnsub}`, children: "有码" }), " ", (0, import_jsx_runtime.jsx)("a", { style: { padding: "18px 18px !important" }, className: `button is-small ${typeValue === "1" ? "is-info" : ""}`, href: `/advanced_search?handleTop=1&handleType=video_type&type_value=1&has_cnsub=${hasCnsub}`, children: "无码" }), " ", (0, import_jsx_runtime.jsx)("a", { style: { padding: "18px 18px !important" }, className: `button is-small ${typeValue === "2" ? "is-info" : ""}`, href: `/advanced_search?handleTop=1&handleType=video_type&type_value=2&has_cnsub=${hasCnsub}`, children: "欧美" }), " ", (0, import_jsx_runtime.jsx)("a", { style: { padding: "18px 18px !important" }, className: `button is-small ${typeValue === "3" ? "is-info" : ""}`, href: `/advanced_search?handleTop=1&handleType=video_type&type_value=3&has_cnsub=${hasCnsub}`, children: "Fc2" }), " ", (0, import_jsx_runtime.jsx)("a", { style: { padding: "18px 18px !important", marginLeft: "50px" }, className: `button is-small ${hasCnsub === "1" ? "is-info" : ""}`, "data-cnsub-value": "1", children: "含中字磁鏈" }), " ", (0, import_jsx_runtime.jsx)("a", { style: { padding: "18px 18px !important" }, className: `button is-small ${hasCnsub === "0" ? "is-info" : ""}`, "data-cnsub-value": "0", children: "无字幕" }), " ", (0, import_jsx_runtime.jsx)("a", { style: { padding: "18px 18px !important" }, className: "button is-small", "data-cnsub-value": "", children: "重置" }) ] }), (0, import_jsx_runtime.jsx)("div", { className: "buttons has-addons", id: "conditionBox", dangerouslySetInnerHTML: { __html: yearButtonsHtml } })] }); } function Top250YearButton({ year, typeValue, hasCnsub }) { return (0, import_jsx_runtime.jsx)("a", { style: { padding: "18px 18px !important" }, className: `button is-small ${typeValue === year.toString() ? "is-info" : ""}`, href: `/advanced_search?handleTop=1&handleType=year&type_value=${year}&has_cnsub=${hasCnsub}`, children: year }); } var APP_AUTH_KEY = "jhs_appAuthorization"; var Top250Plugin = class extends BasePlugin { hasCnsub = ""; contentBox = $(".section .container"); movies = []; getName() { return "Top250Plugin"; } async handle() { $(".main-tabs ul li:contains(\"猜你喜歡\")").html(jsxToString((0, import_jsx_runtime.jsx)(Top250NavLink, {}))); $("a[href*=\"rankings/top\"]").on("click", (event) => { event.preventDefault(); event.stopPropagation(); const target = $(event.target); const href = (target.is("a") ? target : target.closest("a")).attr("href"); const queryString = href?.includes("?") ? href.split("?")[1] : href ?? ""; const params = new URLSearchParams(queryString); this.checkLogin(event, params); }); this.handleTop().then(); } hookPage() { $("h2.section-title").contents().first().replaceWith("Top250"); $(".empty-message").remove(); $(".section .container .box").remove(); $("#sort-toggle-btn").remove(); this.contentBox.append(jsxToString((0, import_jsx_runtime.jsx)(RankingContainers, {}))); this.renderPagination(); } renderPagination() { const params = new URLSearchParams(window.location.search); const page = parseInt(params.get("page") ?? "") || 1; this.contentBox.append(jsxToString((0, import_jsx_runtime.jsx)(Top250Pagination, { page }))); this.contentBox.on("click", ".pagination-link, .pagination-previous, .pagination-next", (event) => { event.preventDefault(); const pageNum = parseInt($(event.currentTarget).data("page")); if (!isNaN(pageNum) && pageNum > 0) ((targetPage) => { params.set("page", String(targetPage)); window.history.pushState({}, "", "?" + params.toString()); window.location.reload(); })(pageNum); }); } async handleTop() { if (!window.location.href.includes("handleTop=1")) return; const params = new URLSearchParams(window.location.search); const handleType = params.get("handleType") || "all"; const typeValue = params.get("type_value") || ""; this.hasCnsub = params.get("has_cnsub") || ""; const page = parseInt(params.get("page") ?? "") || 1; this.toolBar(handleType, typeValue, page); this.hookPage(); const movieListEl = $(".movie-list"); movieListEl.html(""); await withLoading(async () => { let success = false; for (let attempt = 1; attempt <= 3 && !success; attempt++) try { const response = await fetchTopMovies(handleType, typeValue, page, 50); const successFlag = response.success; const message = response.message; const action = response.action; if (successFlag === 1) { const movies = response.data.movies; if (movies.length === 0) { show.error("无数据"); return; } this.movies = movies; const filteredMovies = movies.filter((movie) => this.hasCnsub === "1" ? movie.has_cnsub : this.hasCnsub !== "0" || !movie.has_cnsub); const hitShowPlugin = this.getBean("HitShowPlugin"); const html = hitShowPlugin.markDataListHtml(filteredMovies); movieListEl.html(html); hitShowPlugin.loadScore(filteredMovies); success = true; } else { console.error(response); movieListEl.html(jsxToString((0, import_jsx_runtime.jsx)(Top250ErrorMessage, { message }))); show.error(message); if (action === "JWTVerificationError") { await localStorage.removeItem(APP_AUTH_KEY); await this.checkLogin(null, new URLSearchParams(window.location.search)); } success = true; } } catch (error) { if (attempt < 3) { clog.error(`获取Top数据失败 (第 ${attempt} 次重试):`, error); await new Promise((resolve) => setTimeout(resolve, 1e3)); } else { clog.error("所有重试尝试均失败,无法获取Top数据。", error); movieListEl.html(jsxToString((0, import_jsx_runtime.jsx)(Top250LoadError, {}))); } } }); } toolBar(handleType, typeValue, page) { if (page.toString() === "5") $(".pagination-next").remove(); $(".pagination-ellipsis").closest("li").remove(); $(".pagination-list li a").each(function() { if (parseInt($(this).text()) > 5) $(this).closest("li").remove(); }); let yearButtonsHtml = ""; for (let year = new Date().getFullYear(); year >= 2008; year--) yearButtonsHtml += jsxToString((0, import_jsx_runtime.jsx)(Top250YearButton, { year, typeValue, hasCnsub: this.hasCnsub })); this.contentBox.append(jsxToString((0, import_jsx_runtime.jsx)(Top250ToolBar, { handleType, typeValue, hasCnsub: this.hasCnsub, yearButtonsHtml }))); $("a[data-cnsub-value]").on("click", (event) => { const cnsubValue = $(event.currentTarget).data("cnsub-value"); this.hasCnsub = cnsubValue.toString(); $("a[data-cnsub-value]").removeClass("is-info"); $(event.currentTarget).addClass("is-info"); $(".toolbar a.button").not("[data-cnsub-value]").each((_index, el) => { const hrefEl = $(el); const url = new URL(hrefEl.attr("href") ?? "", window.location.origin); url.searchParams.set("has_cnsub", cnsubValue); hrefEl.attr("href", url.toString()); }); const filteredMovies = this.movies.filter((movie) => this.hasCnsub === "1" ? movie.has_cnsub : this.hasCnsub !== "0" || !movie.has_cnsub); const hitShowPlugin = this.getBean("HitShowPlugin"); const listHtml = hitShowPlugin.markDataListHtml(filteredMovies); $(".movie-list").html(listHtml); hitShowPlugin.loadScore(filteredMovies); }); } async checkLogin(event, params) { if (!localStorage.getItem(APP_AUTH_KEY)) { show.error("该类别依赖移动端接口,请先完成登录"); this.openLoginDialog(); return; } let handleType = "all"; let typeValue = ""; const tValue = params.get("t") || ""; if (/^y\d+$/.test(tValue)) { handleType = "year"; typeValue = tValue.substring(1); } else if (tValue !== "") { handleType = "video_type"; typeValue = tValue; } const path = `/advanced_search?handleTop=1&handleType=${handleType}&type_value=${typeValue}`; if (event && (event.ctrlKey || event.metaKey)) GM_openInTab(window.location.origin + path, { insert: false }); else window.location.href = path; } openLoginDialog() { layer.open({ type: 1, title: "JavDB", closeBtn: 1, area: ["360px", "auto"], shadeClose: false, content: jsxToString((0, import_jsx_runtime.jsx)(LoginDialog, {})), success: (_layero, index) => { $("#username, #password").on("focus", function() { this.style.borderColor = "#4a8bfc"; this.style.background = "#fff"; }).on("blur", function() { this.style.borderColor = "#e0e0e0"; this.style.background = "#f9f9f9"; }); $("#loginBtn").on("mouseover", function() { this.style.background = "#3a7be0"; }).on("mouseout", function() { this.style.background = "#4a8bfc"; }); $("#loginBtn").click(function() { const username = $("#username").val(); const password = $("#password").val(); if (!username || !password) { show.error("请输入用户名和密码"); return; } withLoading(async () => { const response = await (async (username, password) => { const url = `${API_BASE}/v1/sessions?username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}&device_uuid=04b9534d-5118-53de-9f87-2ddded77111e&device_name=iPhone&device_model=iPhone&platform=ios&system_version=17.4&app_version=official&app_version_number=1.9.29&app_channel=official`; const headers = { "user-agent": "Dart/3.5 (dart:io)", "accept-language": "zh-TW", "content-type": "multipart/form-data; boundary=--dio-boundary-2210433284", jdsignature: await reBuildSignature() }; return await gmHttp.post(url, void 0, headers); })(username, password); const successFlag = response.success; if (successFlag === 0) show.error(response.message); else { if (successFlag !== 1) { clog.error("登录失败", response); throw new Error(response.message); } { const token = response.data.token; await localStorage.setItem(APP_AUTH_KEY, token); show.ok("登录成功"); layer.close(index); window.location.href = "/advanced_search?handleTop=1&period=daily"; } } }).catch((error) => { clog.error("登录异常:", error); show.error(error instanceof Error ? error.message : String(error)); }); }); } }); } }; function NavOtherDropdown() { return (0, import_jsx_runtime.jsxs)("div", { className: "navbar-item has-dropdown is-hoverable", children: [(0, import_jsx_runtime.jsx)("a", { className: "navbar-link", children: "其它" }), (0, import_jsx_runtime.jsxs)("div", { className: "navbar-dropdown is-boxed", children: [(0, import_jsx_runtime.jsx)("a", { className: "navbar-item", href: "/feedbacks/new", target: "_blank", children: "反饍" }), (0, import_jsx_runtime.jsx)("a", { className: "navbar-item", rel: "nofollow noopener", target: "_blank", href: "https://theporndude.com/zh", children: "ThePornDude" })] })] }); } function NavSearchBox() { return (0, import_jsx_runtime.jsx)("div", { className: "navbar-menu", id: "search-box", children: (0, import_jsx_runtime.jsxs)("div", { className: "navbar-start", style: { display: "flex", alignItems: "center", gap: "5px" }, children: [ (0, import_jsx_runtime.jsxs)("select", { id: "search-type", style: { padding: "8px 12px", border: "1px solid #555", borderRadius: "4px", backgroundColor: "#333", color: "#eee", fontSize: "14px", outline: "none" }, children: [ (0, import_jsx_runtime.jsx)("option", { value: "all", children: "影片" }), (0, import_jsx_runtime.jsx)("option", { value: "actor", children: "演員" }), (0, import_jsx_runtime.jsx)("option", { value: "series", children: "系列" }), (0, import_jsx_runtime.jsx)("option", { value: "maker", children: "片商" }), (0, import_jsx_runtime.jsx)("option", { value: "director", children: "導演" }), (0, import_jsx_runtime.jsx)("option", { value: "code", children: "番號" }), (0, import_jsx_runtime.jsx)("option", { value: "list", children: "清單" }) ] }), (0, import_jsx_runtime.jsx)("input", { id: "search-keyword", type: "text", placeholder: "輸入影片番號,演員名等關鍵字進行檢索", style: { padding: "8px 12px", border: "1px solid #555", borderRadius: "4px", flexGrow: "1", fontSize: "14px", backgroundColor: "#333", color: "#eee", outline: "none" } }), (0, import_jsx_runtime.jsx)("a", { href: "/advanced_search?noFold=1", title: "進階檢索", style: { padding: "6px 12px", backgroundColor: "#444", borderRadius: "4px", textDecoration: "none", color: "#ddd", fontSize: "14px", border: "1px solid #555" }, children: (0, import_jsx_runtime.jsx)("span", { children: "..." }) }), (0, import_jsx_runtime.jsx)("a", { id: "search-img-btn", title: "以图识图", style: { padding: "6px 16px", backgroundColor: "#444", color: "#fff", borderRadius: "4px", textDecoration: "none", fontWeight: "500", cursor: "pointer", border: "1px solid #555" }, children: "识图" }), (0, import_jsx_runtime.jsx)("a", { id: "search-btn", style: { padding: "6px 16px", backgroundColor: "#444", color: "#fff", borderRadius: "4px", textDecoration: "none", fontWeight: "500", cursor: "pointer", border: "1px solid #555" }, children: "检索" }) ] }) }); } var nav_bar_plugin_default = ".highlight-red {\n /* 核心要求:高亮红色文本 */\n color: red !important;\n\n /* 建议:增加字体加粗,效果更明显 */\n font-weight: bold;\n\n /* 建议:增加背景色,效果更突出 */\n\n /* background-color: yellow; */\n}\n"; var NavBarPlugin = class extends BasePlugin { getName() { return "NavBarPlugin"; } async initCss() { return nav_bar_plugin_default; } async handle() { this.mergeNav(); this.hookSearch(); this.hookOldSearch(); this.toggleOtherNavItem(); $(window).resize(this.toggleOtherNavItem); if (window.location.href.includes("/search")) { const params = new URLSearchParams(window.location.search); const keyword = params.get("q"); const searchType = params.get("f"); $("#search-keyword").val(keyword ?? ""); if (searchType) $("#search-type").val(searchType); if (keyword) this.highlightKeyword(keyword); } } highlightKeyword(keyword) { const trimmed = keyword.trim(); if (!trimmed) return; const lower = trimmed.toLowerCase(); $(".video-title strong, .actor-box strong").each(function() { const el = $(this); if (el.text().toLowerCase().includes(lower)) el.addClass("highlight-red"); }); } hookSearch() { $("#navbar-menu-hero").after(jsxToString((0, import_jsx_runtime.jsx)(NavSearchBox, {}))); const $keyword = $("#search-keyword"); if (!featureFlags.navBarNoPaste) $keyword.on("paste", (event) => { const items = event.originalEvent.clipboardData.items; for (let index = 0; index < items.length; index++) if (items[index].type.indexOf("image") !== -1) return; setTimeout(() => { $("#search-btn").click(); }, 0); }); $keyword.on("keypress", (event) => { if (event.key === "Enter") setTimeout(() => { $("#search-btn").click(); }, 0); }); $("#search-btn").on("click", (_event) => { const keyword = String($("#search-keyword").val() ?? ""); const searchType = String($("#search-type option:selected").val() ?? ""); if (keyword !== "") { if (window.location.href.includes("/search")) window.location.href = "/search?q=" + encodeURIComponent(keyword) + "&f=" + encodeURIComponent(searchType); else window.open("/search?q=" + encodeURIComponent(keyword) + "&f=" + encodeURIComponent(searchType), "_blank", "noopener,noreferrer"); } }); if (featureFlags.imageRecognitionPlugin) $("#search-img-btn").on("click", (e) => { e.preventDefault(); this.getBean("ImageRecognitionPlugin")?.open?.(); }); else $("#search-img-btn").hide(); } hookOldSearch() { const searchImageEl = document.querySelector(".search-image"); if (!searchImageEl) return; const clonedEl = searchImageEl.cloneNode(true); searchImageEl.parentNode.replaceChild(clonedEl, searchImageEl); $("#button-search-image").attr("data-tooltip", "以图识图"); if (featureFlags.imageRecognitionPlugin) $(".search-image").on("click", (event) => { event.preventDefault(); event.stopPropagation(); this.getBean("ImageRecognitionPlugin")?.open?.(); }); } mergeNav() { $("a[href*=\"/feedbacks/new\"]").remove(); $("a[href*=\"theporndude.com\"]").remove(); $("a.navbar-link[href=\"/makers\"]").parent().after(jsxToString((0, import_jsx_runtime.jsx)(NavOtherDropdown, {}))); } toggleOtherNavItem() { const searchBox = $("#search-box"); const searchBarContainer = $("#search-bar-container"); if (($(window).width() ?? 0) > 1023) { searchBox.show(); searchBarContainer.hide(); } } }; var AsyncTaskQueue = class { concurrency; running = 0; pending = []; idleWaiters = []; constructor(concurrency = 1) { this.concurrency = Math.max(1, Math.floor(Number(concurrency)) || 1); } setConcurrency(concurrency) { this.concurrency = Math.max(1, Math.floor(Number(concurrency)) || 1); this.pump(); } getConcurrency() { return this.concurrency; } addTask(task) { this.pending.push(task); this.pump(); } async waitAllFinished() { if (this.running === 0 && this.pending.length === 0) return; return new Promise((resolve) => { this.idleWaiters.push(resolve); this.pump(); }); } pump() { while (this.running < this.concurrency && this.pending.length > 0) { const task = this.pending.shift(); this.running++; Promise.resolve().then(() => task()).catch((err) => { clog.error("执行异步队列任务失败:", err); }).finally(() => { this.running--; this.pump(); this.notifyIdle(); }); } this.notifyIdle(); } notifyIdle() { if (this.running !== 0 || this.pending.length !== 0) return; const waiters = this.idleWaiters.splice(0); for (const w of waiters) w(); } }; var TaskSupervisor = class { controller = new AbortController(); entries = []; _aborted = false; get aborted() { return this._aborted; } get signal() { return this.controller.signal; } setTimeout(fn, ms) { if (this._aborted) return -1; const id = window.setTimeout(() => { this.removeEntry(id, "timer"); fn(); }, ms); this.entries.push({ type: "timer", cleanup: () => window.clearTimeout(id) }); return id; } setInterval(fn, ms) { if (this._aborted) return -1; const id = window.setInterval(fn, ms); this.entries.push({ type: "interval", cleanup: () => window.clearInterval(id) }); return id; } observe(target, callback, options) { const observer = new MutationObserver(callback); if (!this._aborted) observer.observe(target, options); this.entries.push({ type: "observer", cleanup: () => observer.disconnect() }); return observer; } addEventListener(target, type, listener, options) { if (this._aborted) return; const opts = { ...options, signal: this.controller.signal }; target.addEventListener(type, listener, opts); this.entries.push({ type: "listener", cleanup: () => target.removeEventListener(type, listener, options) }); } abort() { if (this._aborted) return; this._aborted = true; this.controller.abort(); for (const entry of this.entries) try { entry.cleanup(); } catch {} this.entries.length = 0; } removeEntry(id, type) { const index = this.entries.findIndex((e) => e.type === type && e.cleanup.toString().includes(String(id))); if (index !== -1) this.entries.splice(index, 1); } }; var ENABLED_SITES_KEY = "jhs_enabled_sites"; function getEnabledSites(plugin) { const stored = localStorage.getItem(ENABLED_SITES_KEY); if (stored) try { return JSON.parse(stored); } catch {} return plugin.siteConfigs.map((siteConfig) => siteConfig.id); } function saveEnabledSites(siteIds) { localStorage.setItem(ENABLED_SITES_KEY, JSON.stringify(siteIds)); } var PRELOAD_CACHE_MAX = 5e3; function evictStaleCache(cache) { const keys = Object.keys(cache); if (keys.length > 5e3) { const tsOf = (k) => { return cache[k]?.ts ?? 0; }; const sorted = keys.filter((k) => tsOf(k) !== 0).sort((a, b) => tsOf(a) - tsOf(b)); const toRemove = [...keys.filter((k) => tsOf(k) === 0), ...sorted].slice(0, keys.length - PRELOAD_CACHE_MAX); for (const k of toRemove) delete cache[k]; } } function isCacheEntryValid(entry, preloadCacheTTLDays) { if (!entry) return false; if (!preloadCacheTTLDays) return true; const ts = entry.ts; if (!ts) return true; return Date.now() - ts < preloadCacheTTLDays * 864e5; } function getItemPreloadStatus(item) { const badge = item.querySelector(".jhs-preload-status .jhs-ps-badge"); if (!badge) return "none"; for (const key of [ "queued", "requesting", "success", "failed" ]) if (badge.classList.contains(`jhs-ps-${key}`)) return key; return "none"; } var PRELOAD_CHIPS = [ { value: "queued", label: "排队中" }, { value: "requesting", label: "请求中" }, { value: "success", label: "成功匹配" }, { value: "failed", label: "匹配失败" } ]; var PRELOAD_HIDDEN_ATTR = "data-preload-hidden"; var OTHER_HIDDEN_ATTRS = [ "data-hide", "data-lrs-hidden", "data-status-tag-hidden", "data-video-lists-tag-hidden" ]; function initFilterBar(plugin) { if (!plugin.preloadStatusEnabled) return; if (tryBuildFilterBar(plugin)) return; let retries = 0; plugin.filterBarObserver = plugin.supervisor.observe(document.body, () => { if (document.querySelector(".preload-filter-bar")) { plugin.filterBarObserver?.disconnect(); plugin.filterBarObserver = null; return; } if (tryBuildFilterBar(plugin) || ++retries > 50) { plugin.filterBarObserver?.disconnect(); plugin.filterBarObserver = null; } }, { childList: true, subtree: true }); plugin.supervisor.setTimeout(() => { plugin.filterBarObserver?.disconnect(); plugin.filterBarObserver = null; }, 1e4); } function tryBuildFilterBar(plugin) { if (document.querySelector(".preload-filter-bar")) return true; const target = findFilterMountTarget(); if (!target) return false; buildFilterBar$1(plugin, target); return true; } function findFilterMountTarget() { const stf = document.querySelector(".status-tag-filter-bar"); if (stf) return stf; const vlt = document.querySelector(".jhs-vlt-filter-bar"); if (vlt) return vlt; if (/^\/actors\//.test(window.location.pathname)) { const actorTags = document.querySelector("body > section > div > div.actor-tags.tags"); if (actorTags) return actorTags; } else { const tabsBoxed = document.querySelector("body > section > div > div.tabs.is-boxed"); if (tabsBoxed) return tabsBoxed; } const section = document.querySelector("body > section > div > div"); if (section && section.firstElementChild) { if (!section.firstElementChild.matches(".preload-filter-bar, .status-tag-filter-bar, .jhs-vlt-filter-bar")) return section.firstElementChild; } const container = document.querySelector("body > section > div"); if (container && container.firstElementChild) { if (!container.firstElementChild.matches(".preload-filter-bar, .status-tag-filter-bar, .jhs-vlt-filter-bar")) return container.firstElementChild; } return null; } function buildFilterBar$1(_plugin, mountTarget) { const bar = document.createElement("div"); bar.className = "preload-filter-bar"; const label = document.createElement("span"); label.className = "preload-filter-label"; label.textContent = "预加载:"; bar.appendChild(label); const chipsContainer = document.createElement("div"); chipsContainer.className = "preload-filter-chips"; bar.appendChild(chipsContainer); const refreshChips = () => { const activeValues = new Set(Array.from(chipsContainer.querySelectorAll(".preload-filter-chip.active")).map((c) => c.dataset.value || "")); chipsContainer.innerHTML = ""; const counts = collectPreloadCounts(); for (const chip of PRELOAD_CHIPS) { const el = document.createElement("span"); el.className = "preload-filter-chip"; el.dataset.value = chip.value; const dot = document.createElement("span"); dot.className = "pf-dot"; el.appendChild(dot); el.appendChild(document.createTextNode(`${chip.label} ${counts[chip.value] ?? 0}`)); if (activeValues.has(chip.value)) el.classList.add("active"); el.addEventListener("click", () => { el.classList.toggle("active"); applyPreloadFilter(); }); chipsContainer.appendChild(el); } }; refreshChips(); if (mountTarget.matches(".status-tag-filter-bar, .jhs-vlt-filter-bar, .actor-tags.tags, .tabs.is-boxed")) mountTarget.insertAdjacentElement("afterend", bar); else mountTarget.insertAdjacentElement("beforebegin", bar); bar._refreshChips = refreshChips; } function collectPreloadCounts() { const counts = { queued: 0, requesting: 0, success: 0, failed: 0 }; document.querySelectorAll(".movie-list .item").forEach((item) => { if (OTHER_HIDDEN_ATTRS.some((a) => item.hasAttribute(a))) return; const st = getItemPreloadStatus(item); if (st in counts) counts[st]++; }); return counts; } function applyPreloadFilter() { const activeChips = document.querySelectorAll(".preload-filter-chip.active"); const selected = new Set(Array.from(activeChips).map((c) => c.dataset.value || "")); const HIDDEN = PRELOAD_HIDDEN_ATTR; if (selected.size === 0) { document.querySelectorAll(`.item[${HIDDEN}]`).forEach((item) => { const el = item; if (OTHER_HIDDEN_ATTRS.some((a) => el.hasAttribute(a))) return; el.removeAttribute(HIDDEN); el.style.display = ""; }); return; } document.querySelectorAll(".movie-list .item").forEach((item) => { const el = item; if (OTHER_HIDDEN_ATTRS.some((a) => el.hasAttribute(a))) return; if (el.style.display === "none" && !el.hasAttribute(HIDDEN)) return; const st = getItemPreloadStatus(el); if (selected.has(st)) { el.removeAttribute(HIDDEN); el.style.display = ""; } else { el.setAttribute(HIDDEN, ""); el.style.display = "none"; } }); } function refreshFilterBar(plugin) { if (!plugin.preloadStatusEnabled) return; clearTimeout(plugin.filterRefreshDebounce); plugin.filterRefreshDebounce = plugin.supervisor.setTimeout(() => { const bar = document.querySelector(".preload-filter-bar"); if (bar && bar._refreshChips) { bar._refreshChips(); applyPreloadFilter(); } }, 150); } function ensureFilterBar(plugin) { if (!plugin.preloadStatusEnabled) return; if (!document.querySelector(".preload-filter-bar")) tryBuildFilterBar(plugin); const bar = document.querySelector(".preload-filter-bar"); if (!bar) return; const stf = document.querySelector(".status-tag-filter-bar"); if (stf && bar.previousElementSibling !== stf) { stf.insertAdjacentElement("afterend", bar); refreshFilterBar(plugin); return; } const vlt = document.querySelector(".jhs-vlt-filter-bar"); if (!stf && vlt && bar.previousElementSibling !== vlt) { vlt.insertAdjacentElement("afterend", bar); refreshFilterBar(plugin); } } var PRELOAD_STATUS_MAP = { queued: { cls: "jhs-ps-queued", text: "排队中" }, requesting: { cls: "jhs-ps-requesting", text: "请求中" }, success: { cls: "jhs-ps-success", text: "成功匹配" }, failed: { cls: "jhs-ps-failed", text: "匹配失败" } }; function PreloadStatusBar() { return (0, import_jsx_runtime.jsx)("div", { className: "jhs-preload-status-bar" }); } function PreloadStatusBadge({ siteId, siteName, status }) { const { cls, text } = PRELOAD_STATUS_MAP[status]; return (0, import_jsx_runtime.jsxs)("span", { className: "jhs-preload-status", "data-site-id": siteId, children: [(0, import_jsx_runtime.jsx)("span", { className: "jhs-ps-name", children: siteName }), (0, import_jsx_runtime.jsx)("span", { className: `jhs-ps-badge ${cls}`, children: text })] }); } var PRELOAD_LOG = "[OtherSite 预加载]"; async function preloadListPage(plugin) { if (await storageManager.getSetting("enableLoadOtherSite", "yes") !== "yes") { console.log(`${PRELOAD_LOG} 已禁用,跳过预加载`); return; } const listPagePlugin = plugin.getBean("ListPagePlugin"); if (!listPagePlugin) { console.log(`${PRELOAD_LOG} ListPagePlugin 未就绪,跳过`); return; } const enabledSiteIds = getEnabledSites(plugin); const $items = $(".movie-list .item"); if ($items.length === 0) { console.log(`${PRELOAD_LOG} 无 .item,跳过`); return; } const storageKey = "jhs_other_site"; let cache = {}; try { const raw = localStorage.getItem(storageKey); if (raw) cache = JSON.parse(raw); } catch {} let cachedCount = 0; let skippedHiddenCount = 0; let preloadCount = 0; $items.each((_index, itemEl) => { const $item = $(itemEl); if ($item.attr("data-hide") === "yes") { skippedHiddenCount++; return; } let carNum; try { carNum = listPagePlugin.findCarNumAndHref($item).carNum; } catch { return; } if (!carNum) return; for (const siteConfig of plugin.siteConfigs) { if (!enabledSiteIds.includes(siteConfig.id)) continue; if (siteConfig.condition && siteConfig.condition(siteConfig.sourceCarNum) === false) continue; if (plugin.blockedSiteIds.has(siteConfig.id)) continue; if (siteConfig.initUrl) continue; const cacheKey = carNum + "_" + siteConfig.id.replace("Btn", ""); if (isCacheEntryValid(cache[cacheKey], plugin.preloadCacheTTLDays)) { cachedCount++; continue; } updatePreloadStatus(plugin, $item, siteConfig.id, "queued"); plugin.preloadQueue.addTask(async () => { if (plugin.blockedSiteIds.has(siteConfig.id)) { updatePreloadStatus(plugin, $item, siteConfig.id, "failed"); return; } updatePreloadStatus(plugin, $item, siteConfig.id, "requesting"); await preloadSite(plugin, carNum, siteConfig, $item); }); preloadCount++; } }); console.log(`${PRELOAD_LOG} ${$items.length} 个 item(屏蔽 ${skippedHiddenCount})| 已缓存 ${cachedCount} | 入队 ${preloadCount} 个任务` + (plugin.blockedSiteIds.size > 0 ? ` | 被拦截站点: ${[...plugin.blockedSiteIds].map((id) => id.replace("Btn", "")).join(", ")}` : "")); } async function preloadSite(plugin, carNum, siteConfig, $item) { const dedupeKey = `${carNum}_${siteConfig.id}`; const existing = plugin.inflightPreloads.get(dedupeKey); if (existing) return existing; const promise = doPreloadSite(plugin, carNum, siteConfig, $item); plugin.inflightPreloads.set(dedupeKey, promise); try { await promise; } finally { plugin.inflightPreloads.delete(dedupeKey); } } async function doPreloadSite(plugin, carNum, siteConfig, $item) { try { const baseUrl = await siteConfig.getBaseUrl(); const searchUrl = siteConfig.searchPath(baseUrl, carNum); const htmlContent = await gmHttp.get(searchUrl, void 0, siteConfig.headers, true); const $dom = utils.htmlTo$dom(String(htmlContent)); const detailHrefs = []; $dom.find(siteConfig.itemSelector).each((_index, element) => { const itemEl = $(element); if (!siteConfig.findCarNumOrTitle(itemEl).toLowerCase().includes(carNum.toLowerCase())) return; let href = siteConfig.getDetailPageHref(itemEl, baseUrl, carNum); if (!href) throw new Error("解析href失败"); if (!href.includes("http")) href = baseUrl + (href.startsWith("/") ? href : "/" + href); detailHrefs.push(href); }); let resultData = null; if (detailHrefs.length === 1) resultData = { type: "single", url: detailHrefs[0] }; else if (detailHrefs.length > 1) resultData = { type: "multiple", url: searchUrl }; if (resultData) { updatePreloadStatus(plugin, $item, siteConfig.id, "success"); const storageKey = "jhs_other_site"; let cache = {}; try { const raw = localStorage.getItem(storageKey); if (raw) cache = JSON.parse(raw); } catch {} const cacheKey = carNum + "_" + siteConfig.id.replace("Btn", ""); cache[cacheKey] = { ...resultData, ts: Date.now() }; evictStaleCache(cache); localStorage.setItem(storageKey, JSON.stringify(cache)); console.log(`${PRELOAD_LOG} ✓ ${carNum} ${siteConfig.id.replace("Btn", "")} ${resultData.type === "single" ? "命中" : "多结果"}`); } else { updatePreloadStatus(plugin, $item, siteConfig.id, "failed"); console.log(`${PRELOAD_LOG} ✗ ${carNum} ${siteConfig.id.replace("Btn", "")} 未命中`); } } catch (error) { const siteName = siteConfig.id.replace("Btn", ""); const errorMsg = String(error); if (errorMsg.includes("Just a moment") || errorMsg.includes("403")) { plugin.blockedSiteIds.add(siteConfig.id); updatePreloadStatus(plugin, $item, siteConfig.id, "failed"); console.warn(`${PRELOAD_LOG} ⚠ ${siteName} 被 Cloudflare 拦截,跳过本轮剩余任务`); return; } if (errorMsg.includes("重定向")) { updatePreloadStatus(plugin, $item, siteConfig.id, "failed"); console.warn(`${PRELOAD_LOG} ⚠ ${siteName} 域名失效(被重定向): ${errorMsg.slice(0, 60)}`); return; } updatePreloadStatus(plugin, $item, siteConfig.id, "failed"); console.warn(`${PRELOAD_LOG} ✗ ${carNum} ${siteName} 失败: ${errorMsg.slice(0, 60)}`); } } function updatePreloadStatus(plugin, $item, siteId, status) { if (!plugin.preloadStatusEnabled) return; const $videoTitle = $item.find(".video-title").first(); if ($videoTitle.length === 0) return; if ($item.find(".jhs-preload-status-bar").length === 0) $videoTitle.after(jsxToString((0, import_jsx_runtime.jsx)(PreloadStatusBar, {}))); const $bar = $item.find(".jhs-preload-status-bar"); const $badge = $bar.find(`[data-site-id="${siteId}"]`); if ($badge.length === 0) { $bar.append(jsxToString((0, import_jsx_runtime.jsx)(PreloadStatusBadge, { siteId, siteName: siteId.replace("Btn", ""), status }))); return; } const { cls, text } = PRELOAD_STATUS_MAP[status]; $badge.find(".jhs-ps-badge").attr("class", `jhs-ps-badge ${cls}`).text(text); refreshFilterBar(plugin); } function getPreloadableSites(plugin) { const enabled = getEnabledSites(plugin); return plugin.siteConfigs.filter((s) => enabled.includes(s.id) && !s.initUrl); } function syncAllBadges(plugin) { if (!plugin.preloadStatusEnabled) return; const listPagePlugin = plugin.getBean("ListPagePlugin"); if (!listPagePlugin) return; const sites = getPreloadableSites(plugin); if (sites.length === 0) return; let cache = {}; try { const raw = localStorage.getItem("jhs_other_site"); if (raw) cache = JSON.parse(raw); } catch {} $(".movie-list .item").each((_index, el) => { const $item = $(el); if ($item.attr("data-hide") === "yes") return; let carNum; try { carNum = listPagePlugin.findCarNumAndHref($item).carNum; } catch { return; } if (!carNum) return; for (const site of sites) { const cacheKey = carNum + "_" + site.id.replace("Btn", ""); if (isCacheEntryValid(cache[cacheKey], plugin.preloadCacheTTLDays)) { const $badge = $item.find(`[data-site-id="${site.id}"] .jhs-ps-badge`); if (!$badge.length || !$badge.hasClass("jhs-ps-success")) updatePreloadStatus(plugin, $item, site.id, "success"); } else if (!$item.find(`[data-site-id="${site.id}"]`).length) updatePreloadStatus(plugin, $item, site.id, "queued"); } }); } function startPreloadObserver(plugin) { const containerEl = document.querySelector(".movie-list"); if (!containerEl) return; plugin.supervisor.observe(containerEl, () => { syncAllBadges(plugin); ensureFilterBar(plugin); refreshFilterBar(plugin); clearTimeout(plugin.preloadDebounce); plugin.preloadDebounce = plugin.supervisor.setTimeout(() => { preloadListPage(plugin).then(); }, plugin.preloadDebounceMs); }, { childList: true }); } function SiteResultTag() { return (0, import_jsx_runtime.jsx)("span", { className: "site-tag", style: { top: "-15px" }, children: "多结果" }); } async function probeSite(plugin, carNum, siteConfig) { const buttonEl = $(`#${siteConfig.id}`); if (siteConfig.initUrl) { buttonEl.attr("href", await siteConfig.initUrl(carNum)); buttonEl.css("backgroundColor", plugin.warnBackgroundColor); } if (siteConfig.noHandle && siteConfig.noHandle === true) { const dmmStorageKey = "jhs_other_site_dmm"; let dmmCache = {}; try { const raw = localStorage.getItem(dmmStorageKey); if (raw) dmmCache = JSON.parse(raw); } catch {} const dmmCachedResult = dmmCache[carNum]; if (dmmCachedResult) { if (dmmCachedResult.type === "single") { buttonEl.attr("href", dmmCachedResult.url); buttonEl.css("backgroundColor", plugin.okBackgroundColor); } else if (dmmCachedResult.type === "multiple") { buttonEl.attr("href", dmmCachedResult.url); buttonEl.append(jsxToString((0, import_jsx_runtime.jsx)(SiteResultTag, {}))); buttonEl.css("backgroundColor", plugin.okBackgroundColor); } } } else try { if (buttonEl.attr("href")) return; if (utils.isHidden(buttonEl)) return; const storageKey = "jhs_other_site"; let cache = {}; try { const raw = localStorage.getItem(storageKey); if (raw) cache = JSON.parse(raw); } catch {} const cacheKey = carNum + "_" + siteConfig.id.replace("Btn", ""); const cachedResult = cache[cacheKey]; if (isCacheEntryValid(cachedResult, plugin.preloadCacheTTLDays)) { if (cachedResult.type === "single") { buttonEl.attr("href", cachedResult.url); buttonEl.css("backgroundColor", plugin.okBackgroundColor); } else if (cachedResult.type === "multiple") { buttonEl.attr("href", cachedResult.url); buttonEl.append(jsxToString((0, import_jsx_runtime.jsx)(SiteResultTag, {}))); buttonEl.css("backgroundColor", plugin.okBackgroundColor); } return; } const baseUrl = await siteConfig.getBaseUrl(); const searchUrl = siteConfig.searchPath(baseUrl, carNum); buttonEl.attr("href", searchUrl); const htmlContent = await gmHttp.get(searchUrl, void 0, siteConfig.headers, true); const $dom = utils.htmlTo$dom(String(htmlContent)); const detailHrefs = []; $dom.find(siteConfig.itemSelector).each((_index, element) => { const itemEl = $(element); if (!siteConfig.findCarNumOrTitle(itemEl).toLowerCase().includes(carNum.toLowerCase())) return; let href = siteConfig.getDetailPageHref(itemEl, baseUrl, carNum); if (!href) throw new Error("解析href失败"); if (!href.includes("http")) href = baseUrl + (href.startsWith("/") ? href : "/" + href); detailHrefs.push(href); }); let tagHtml = ""; let resultData = null; if (detailHrefs.length === 1) { const detailHref = detailHrefs[0]; buttonEl.attr("href", detailHref); buttonEl.css("backgroundColor", plugin.okBackgroundColor); resultData = { type: "single", url: detailHref }; } else if (detailHrefs.length > 1) { buttonEl.attr("href", searchUrl); tagHtml += jsxToString((0, import_jsx_runtime.jsx)(SiteResultTag, {})); buttonEl.css("backgroundColor", plugin.okBackgroundColor); resultData = { type: "multiple", url: searchUrl }; } else { buttonEl.attr("href", searchUrl); buttonEl.attr("title", "未查询到, 点击前往搜索页"); buttonEl.css("backgroundColor", plugin.errorBackgroundColor); } if (resultData) new AsyncTaskQueue().addTask(() => { let cache = {}; try { const raw = localStorage.getItem(storageKey); if (raw) cache = JSON.parse(raw); } catch {} cache[cacheKey] = { ...resultData, ts: Date.now() }; evictStaleCache(cache); localStorage.setItem(storageKey, JSON.stringify(cache)); }); if (tagHtml) buttonEl.append(tagHtml); } catch (error) { const errorMsg = String(error); const siteName = siteConfig.id.replace("Btn", ""); if (errorMsg.includes("Just a moment")) { buttonEl.attr("title", "请求失败:Cloudflare 安全检查。"); buttonEl.css("backgroundColor", plugin.warnBackgroundColor); clog.warn(`检测第三方资源失败, ${siteName} 需Cloudflare安全检查`); } else if (errorMsg.includes("重定向")) { buttonEl.attr("title", "域名失效"); buttonEl.css("backgroundColor", plugin.domainErrorBackgroundColor); clog.warn(`检测第三方资源失败, ${siteName} 域名被重定向`); } else if (errorMsg.includes("404 Page Not Found")) { buttonEl.attr("title", "未查询到, 点击前往搜索页"); buttonEl.css("backgroundColor", plugin.errorBackgroundColor); } else { clog.error(error); buttonEl.attr("title", "请求失败。"); buttonEl.css("backgroundColor", plugin.errorBackgroundColor); clog.warn(`检测第三方资源失败, ${siteName}`); } } } function OtherSiteBox({ siteButtonsHtml, isJavdbSite }) { return (0, import_jsx_runtime.jsx)("div", { id: "otherSiteBox", className: "panel-block", style: { marginTop: isJavdbSite ? "8px" : "10px", fontSize: "13px", userSelect: "none" }, children: (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", gap: "5px", flexWrap: "wrap" }, dangerouslySetInnerHTML: { __html: siteButtonsHtml } }) }); } function OtherSiteBtn({ id, enabled }) { return (0, import_jsx_runtime.jsx)("a", { target: "_blank", className: "site-btn", style: enabled ? void 0 : { display: "none" }, id, children: (0, import_jsx_runtime.jsx)("span", { children: id.replace("Btn", "") }) }); } function OtherSiteCheckbox({ id, isEnabled, isJavdbSite }) { return (0, import_jsx_runtime.jsxs)("div", { style: { marginRight: "15px", display: "flex", alignItems: isJavdbSite ? "center" : "flex-start" }, children: [(0, import_jsx_runtime.jsx)("input", { type: "checkbox", id: `checkbox-${id}`, "data-site-id": id, checked: isEnabled, style: { marginRight: "8px", cursor: "pointer" } }), (0, import_jsx_runtime.jsx)("label", { for: `checkbox-${id}`, style: { color: "#333", fontWeight: "500", cursor: "pointer" }, children: id.replace("Btn", "") })] }); } var other_site_plugin_default = ".site-btn {\n position: relative !important;\n min-width: 80px;\n display: inline-block;\n padding: 5px 10px;\n color: white !important;\n background-color: #938585;\n text-decoration: none;\n border-radius: 4px;\n text-align: center;\n margin-bottom: 5px;\n}\n\n.site-btn:hover {\n color: white;\n transform: translateY(-2px);\n box-shadow: 0 2px 5px rgb(0, 0, 0, 0.1);\n}\n\n.site-tag {\n position: absolute;\n top: -15px;\n right: 0;\n background-color: #ffc107;\n color: #333;\n font-size: 12px;\n padding: 2px 6px;\n border-radius: 4px;\n}\n"; var preload_status_badge_default = "/**\n * 预加载状态徽标 + 筛选栏样式(OtherSitePlugin 列表页预加载实时状态)。\n *\n * 两部分:\n * 1. .jhs-preload-status-bar / .jhs-preload-status —— .item 卡片内 .video-title\n * 下方每站点状态徽标(排队中/请求中/成功匹配/匹配失败)。\n * 2. .preload-filter-bar / .preload-filter-chip —— 筛选栏(与 .status-tag-filter-bar\n * 同行挂载),按状态过滤卡片,实时刷新计数。\n *\n * 样式风格与 .status-tag-filter-bar 对齐(药丸芯片、border、active 配色),\n * 确保多个筛选栏视觉统一。\n */\n\n/* ========== 卡片内状态徽标 ========== */\n.jhs-preload-status-bar {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n margin-top: 4px;\n line-height: 1.2;\n}\n\n.jhs-preload-status {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 1px 7px;\n border-radius: 9px;\n background-color: rgb(0, 0, 0, 0.05);\n font-size: 11px;\n line-height: 1.5;\n white-space: nowrap;\n}\n\n.jhs-ps-name {\n font-weight: 600;\n color: #9b9b9b;\n}\n\n.jhs-ps-badge {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-weight: 600;\n}\n\n.jhs-ps-queued {\n color: #8a8a8a;\n}\n\n.jhs-ps-requesting {\n color: #d97706;\n}\n\n.jhs-ps-requesting::before {\n content: '';\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background-color: #d97706;\n animation: jhs-ps-pulse 1s ease-in-out infinite;\n}\n\n.jhs-ps-success {\n color: #198754;\n}\n\n.jhs-ps-success::before {\n content: '\\2713';\n font-weight: 700;\n}\n\n.jhs-ps-failed {\n color: #dc2626;\n}\n\n.jhs-ps-failed::before {\n content: '\\2715';\n font-weight: 700;\n}\n\n@keyframes jhs-ps-pulse {\n 0%,\n 100% {\n opacity: 1;\n }\n\n 50% {\n opacity: 0.2;\n }\n}\n\n/* ========== 筛选栏 ========== */\n\n.preload-filter-chip {\n display: inline-flex;\n align-items: center;\n}\n\n.preload-filter-chip .pf-dot {\n display: inline-block;\n width: 7px;\n height: 7px;\n border-radius: 50%;\n margin-right: 5px;\n}\n\n.preload-filter-chip[data-value='queued'] .pf-dot {\n background-color: #8a8a8a;\n}\n\n.preload-filter-chip[data-value='requesting'] .pf-dot {\n background-color: #d97706;\n}\n\n.preload-filter-chip[data-value='success'] .pf-dot {\n background-color: #198754;\n}\n\n.preload-filter-chip[data-value='failed'] .pf-dot {\n background-color: #dc2626;\n}\n\n.preload-filter-chip.active[data-value='queued'] {\n background: #8a8a8a;\n border-color: #8a8a8a;\n color: #fff;\n}\n\n.preload-filter-chip.active[data-value='requesting'] {\n background: #d97706;\n border-color: #d97706;\n color: #fff;\n}\n\n.preload-filter-chip.active[data-value='success'] {\n background: #198754;\n border-color: #198754;\n color: #fff;\n}\n\n.preload-filter-chip.active[data-value='failed'] {\n background: #dc2626;\n border-color: #dc2626;\n color: #fff;\n}\n"; var OtherSitePlugin = class extends BasePlugin { okBackgroundColor = "#7bc73b"; errorBackgroundColor = "#de3333"; warnBackgroundColor = "#d7a80c"; domainErrorBackgroundColor = "#d7780c"; siteConfigs = [{ id: "missAvBtn", getBaseUrl: async () => await this.getMissAvUrl(), itemSelector: ".text-secondary", searchPath: (baseUrl, carNum) => `${baseUrl}/search/${carNum}`, getDetailPageHref: (item) => item.attr("href"), findCarNumOrTitle: (item) => item.text() }, { id: "supJavBtn", getBaseUrl: async () => await this.getSupJavUrl(), itemSelector: ".posts post", searchPath: (baseUrl, carNum) => `${baseUrl}/?s=${carNum}`, getDetailPageHref: (item) => item.attr("href"), findCarNumOrTitle: (item) => item.attr("title"), initUrl: async (carNum) => `${await this.getSupJavUrl()}/?s=${carNum}` }]; settingCache = null; lastFetchTime = 0; CACHE_DURATION = 1e4; preloadQueue = new AsyncTaskQueue(); supervisor = new TaskSupervisor(); preloadDebounce = 0; blockedSiteIds = new Set(); filterBarObserver = null; filterRefreshDebounce = 0; preloadStatusEnabled = true; preloadDebounceMs = 300; preloadConcurrency = 1; preloadCacheTTLDays = 0; inflightPreloads = new Map(); getName() { return "OtherSitePlugin"; } get pageTypes() { return ["detail", "list"]; } async initCss() { return `${other_site_plugin_default}\n${preload_status_badge_default}`; } async handle() { this.blockedSiteIds.clear(); if (await storageManager.getSetting("enableLoadOtherSite", "yes") !== "yes") return; if (window.isDetailPage) this.loadOtherSite().then(); else if (document.querySelector(".movie-list")) { if (await storageManager.getSetting("enablePreload", "yes") !== "yes") return; this.preloadStatusEnabled = await storageManager.getSetting("enablePreloadStatus", "yes") === "yes"; const debounceRaw = await storageManager.getSetting("preloadDebounce", 300); this.preloadDebounceMs = typeof debounceRaw === "number" ? debounceRaw : Number(debounceRaw); if (isNaN(this.preloadDebounceMs) || this.preloadDebounceMs < 0) this.preloadDebounceMs = 300; const concRaw = await storageManager.getSetting("preloadConcurrency", 1); let conc = typeof concRaw === "number" ? concRaw : Number(concRaw); if (isNaN(conc) || conc < 1) conc = 1; if (conc > 10) conc = 10; this.preloadConcurrency = Math.floor(conc); this.preloadQueue.setConcurrency(this.preloadConcurrency); this.preloadCacheTTLDays = Number(await storageManager.getSetting("preloadCacheTTL", 0)) || 0; syncAllBadges(this); preloadListPage(this).then(); startPreloadObserver(this); initFilterBar(this); } } async loadOtherSite(carNum, sourceCarNum) { if (await storageManager.getSetting("enableLoadOtherSite", "yes") !== "yes") return; carNum ||= this.getPageInfo().carNum ?? ""; const enabledSiteIds = this.getEnabledSites(); const siteButtonsHtml = this.siteConfigs.map((siteConfig) => { siteConfig.sourceCarNum = sourceCarNum; if (siteConfig.condition && siteConfig.condition(siteConfig.sourceCarNum) === false) return ""; return jsxToString((0, import_jsx_runtime.jsx)(OtherSiteBtn, { id: siteConfig.id, enabled: enabledSiteIds.includes(siteConfig.id) })); }).join(""); const boxHtml = jsxToString((0, import_jsx_runtime.jsx)(OtherSiteBox, { siteButtonsHtml, isJavdbSite })); $(".movie-panel-info").append(boxHtml); $(".container .info").append(boxHtml); await Promise.all(this.siteConfigs.map(async (siteConfig) => { if (!siteConfig.condition || siteConfig.condition(siteConfig.sourceCarNum) !== false) await this.handleSite(carNum, siteConfig); })); this.renderSettingsArea(); this.setupEventListeners(); } async handleSite(carNum, siteConfig) { return probeSite(this, carNum, siteConfig); } async getSettingCache() { const now = Date.now(); if (!this.settingCache || now - this.lastFetchTime > this.CACHE_DURATION) { this.settingCache = await storageManager.getSetting(); this.lastFetchTime = now; } return this.settingCache; } async getMissAvUrl() { return (await this.getSettingCache()).missAvUrl || "https://missav.live"; } async getJavDbUrl() { return (await this.getSettingCache()).javDbUrl || "https://javdb.com"; } async getSupJavUrl() { return (await this.getSettingCache()).supJavUrl || "https://supjav.com"; } getEnabledSites() { return getEnabledSites(this); } saveEnabledSites(siteIds) { saveEnabledSites(siteIds); } renderSettingsArea() { const enabledSiteIds = this.getEnabledSites(); const checkboxContainer = document.getElementById("siteCheckboxes"); if (checkboxContainer) checkboxContainer.innerHTML = this.siteConfigs.map((siteConfig) => { const isEnabled = enabledSiteIds.includes(siteConfig.id); return jsxToString((0, import_jsx_runtime.jsx)(OtherSiteCheckbox, { id: siteConfig.id, isEnabled, isJavdbSite })); }).join(""); } setupEventListeners() { const settingsArea = document.getElementById("settingsArea"); if (settingsArea) this.supervisor.addEventListener(settingsArea, "change", (event) => { const target = event.target; if (target.type === "checkbox") { const siteId = target.getAttribute("data-site-id"); if (target.checked) { $(`#${siteId}`).show(); const carNum = this.getPageInfo().carNum ?? ""; const siteConfig = this.siteConfigs.find((config) => config.id === siteId); this.handleSite(carNum, siteConfig).then(); } else $(`#${siteId}`).hide(); const checkedSiteIds = Array.from(settingsArea.querySelectorAll("input[type=\"checkbox\"]:checked")).map((checkbox) => checkbox.getAttribute("data-site-id")); this.saveEnabledSites(checkedSiteIds); } }); } destroy() { this.supervisor.abort(); } }; var SAFE_INLINE_TAGS = { B: true, I: true, EM: true, STRONG: true, BR: true, P: true, SPAN: true }; function parseInlineStyle(s) { if (!s) return void 0; try { const o = {}; for (const part of s.split(";")) { const idx = part.indexOf(":"); if (idx <= 0) continue; const k = part.slice(0, idx).trim(); const v = part.slice(idx + 1).trim(); if (!k || !v) continue; o[k.replace(/-([a-z])/g, (_m, c) => c.toUpperCase())] = v; } return Object.keys(o).length ? o : void 0; } catch { return; } } function safeReviewNodes(html) { const doc = new DOMParser().parseFromString(html, "text/html"); const walk = (nodes, keyPrefix) => { const res = []; Array.from(nodes).forEach((node, i) => { const key = `${keyPrefix}${i}`; if (node.nodeType === Node.TEXT_NODE) { const t = node.textContent ?? ""; if (t) res.push(t); } else if (node.nodeType === Node.ELEMENT_NODE) { const el = node; const tag = el.tagName.toUpperCase(); const cls = el.getAttribute("class") || void 0; const stl = parseInlineStyle(el.getAttribute("style")); if (tag === "A") res.push((0, import_jsx_runtime.jsx)("a", { href: sanitizeUrl(el.getAttribute("href") || ""), target: "_blank", rel: "noopener noreferrer", className: cls, style: stl, children: walk(el.childNodes, key + "-") }, key)); else if (tag === "BR") res.push((0, import_jsx_runtime.jsx)("br", {}, key)); else if (SAFE_INLINE_TAGS[tag]) { const inner = walk(el.childNodes, key + "-"); if (tag === "P") res.push((0, import_jsx_runtime.jsx)("p", { className: cls, style: stl, children: inner }, key)); else if (tag === "B") res.push((0, import_jsx_runtime.jsx)("b", { className: cls, style: stl, children: inner }, key)); else if (tag === "I") res.push((0, import_jsx_runtime.jsx)("i", { className: cls, style: stl, children: inner }, key)); else if (tag === "EM") res.push((0, import_jsx_runtime.jsx)("em", { className: cls, style: stl, children: inner }, key)); else if (tag === "STRONG") res.push((0, import_jsx_runtime.jsx)("strong", { className: cls, style: stl, children: inner }, key)); else if (tag === "SPAN") res.push((0, import_jsx_runtime.jsx)("span", { className: cls, style: stl, children: inner }, key)); } else res.push(...walk(el.childNodes, key + "-")); } }); return res; }; return walk(doc.body.childNodes, ""); } function ReviewItem({ floor, username, stars, time, likesCount, content }) { return (0, import_jsx_runtime.jsxs)("div", { className: "item columns is-desktop", style: { display: "block", marginTop: "6px", backgroundColor: "#ffffff", padding: "10px", marginLeft: "-10px", wordBreak: "break-word", position: "relative" }, children: [ (0, import_jsx_runtime.jsxs)("span", { style: { position: "absolute", top: "5px", right: "10px", color: "#999", fontSize: "12px" }, children: [ "#", floor, "楼" ] }), username, " \xA0\xA0", " ", (0, import_jsx_runtime.jsx)("span", { className: "score-stars", dangerouslySetInnerHTML: { __html: stars } }), " ", (0, import_jsx_runtime.jsx)("span", { className: "time", children: time }), " \xA0\xA0 点赞:", likesCount, (0, import_jsx_runtime.jsx)("p", { className: "review-content", style: { marginTop: "5px" }, children: safeReviewNodes(content) }) ] }); } function ReviewLinkContent({ match }) { if (match.startsWith("ed2k://")) return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ " ", (0, import_jsx_runtime.jsx)("span", { style: { wordBreak: "break-all", background: "#e0f2fe", color: "#0369a1" }, children: match }), " " ] }); if (match.startsWith("magnet:")) return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ " ", (0, import_jsx_runtime.jsx)("a", { href: match, className: "a-primary", style: { padding: 0, wordBreak: "break-all", whiteSpace: "pre-wrap" }, target: "_blank", rel: "noopener noreferrer", children: match }), " " ] }); if (match.startsWith("http://") || match.startsWith("https://")) return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ " ", (0, import_jsx_runtime.jsx)("a", { href: match, className: "a-primary", style: { padding: 0, wordBreak: "break-all", whiteSpace: "pre-wrap" }, target: "_blank", rel: "noopener noreferrer", children: match }), " " ] }); return match; } function ReviewStarIcon() { return (0, import_jsx_runtime.jsx)("i", { className: "icon-star" }); } function SectionContainers({ containerId, footerId }) { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [(0, import_jsx_runtime.jsx)("div", { id: containerId }), (0, import_jsx_runtime.jsx)("div", { id: footerId })] }); } function SectionEndMessage({ text }) { return (0, import_jsx_runtime.jsx)("div", { style: { textAlign: "center", padding: "10px", color: "#666", marginTop: "10px" }, children: text }); } function SectionErrorMessage({ text, retryId, linkColor }) { return (0, import_jsx_runtime.jsxs)("div", { style: { marginTop: "15px", backgroundColor: "#ffffff", padding: "10px", marginLeft: "-10px" }, children: [text, (0, import_jsx_runtime.jsx)("a", { id: retryId, style: { marginLeft: "10px", color: linkColor, textDecoration: "none", cursor: "pointer" }, children: "重试" })] }); } function SectionHeader({ title, foldId, linkColor, tooltip, foldText, iconText }) { return (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", margin: "16px 0", color: "#666", fontSize: "14px" }, children: [ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1, height: "1px", background: "linear-gradient(to right, transparent, #999, transparent)" } }), (0, import_jsx_runtime.jsx)("span", { style: { padding: "0 10px" }, "data-tip": tooltip, children: title }), (0, import_jsx_runtime.jsxs)("a", { id: foldId, style: { marginLeft: "8px", color: linkColor, textDecoration: "none", display: "flex", alignItems: "center" }, children: [(0, import_jsx_runtime.jsx)("span", { className: "toggle-text", children: foldText }), (0, import_jsx_runtime.jsx)("span", { className: "toggle-icon", style: { marginLeft: "4px" }, children: iconText })] }), (0, import_jsx_runtime.jsx)("span", { style: { flex: 1, height: "1px", background: "linear-gradient(to right, transparent, #999, transparent)" } }) ] }); } function SectionLoadMore({ loadMoreId, endId, text, endText }) { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [(0, import_jsx_runtime.jsx)("button", { id: loadMoreId, style: { width: "100%", backgroundColor: "#e1f5fe", border: "none", padding: "10px", marginTop: "10px", cursor: "pointer", color: "#0277bd", fontWeight: "bold", borderRadius: "4px" }, children: text }), (0, import_jsx_runtime.jsx)("div", { id: endId, style: { display: "none", textAlign: "center", padding: "10px", color: "#666", marginTop: "10px" }, children: endText })] }); } function SectionStatusMessage({ text, id }) { return (0, import_jsx_runtime.jsx)("div", { id, style: { marginTop: "15px", backgroundColor: "#ffffff", padding: "10px", marginLeft: "-10px" }, children: text }); } var ReviewPlugin = class extends BasePlugin { floorIndex = 1; isInit = false; constructor() { super(); } getName() { return "ReviewPlugin"; } get pageTypes() { return ["detail"]; } async handle() { if (window.isDetailPage) { if (isJavdbSite) { const movieId = this.parseMovieId(window.location.href); await this.showReview(movieId); await this.getBean("RelatedPlugin").showRelated($("#magnets-content"), movieId); } } } async showReview(movieId, container) { const isExpanded = await storageManager.getSetting("enableLoadReview", "yes"); const target = container || $("#magnets-content"); target.append(jsxToString((0, import_jsx_runtime.jsx)(SectionHeader, { title: "❓ 评论区", foldId: "reviewsFold", linkColor: "#1890ff", tooltip: "想要发表评论? 滑上去, 点击上面的按钮-看过", foldText: isExpanded === "yes" ? "折叠" : "展开", iconText: isExpanded === "yes" ? "▲" : "▼" }))); $("#reviewsFold").on("click", (event) => { event.preventDefault(); event.stopPropagation(); const toggleText = $("#reviewsFold .toggle-text"); const toggleIcon = $("#reviewsFold .toggle-icon"); const isExpand = toggleText.text() === "展开"; toggleText.text(isExpand ? "折叠" : "展开"); toggleIcon.text(isExpand ? "▲" : "▼"); if (isExpand) { $("#reviewsContainer").show(); $("#reviewsFooter").show(); if (!this.isInit) { this.fetchAndDisplayReviews(movieId); this.isInit = true; } storageManager.saveSettingItem("enableLoadReview", "yes"); } else { $("#reviewsContainer").hide(); $("#reviewsFooter").hide(); storageManager.saveSettingItem("enableLoadReview", "no"); } }); target.append(jsxToString((0, import_jsx_runtime.jsx)(SectionContainers, { containerId: "reviewsContainer", footerId: "reviewsFooter" }))); if (isExpanded === "yes") await this.fetchAndDisplayReviews(movieId); } async fetchAndDisplayReviews(movieId) { const container = $("#reviewsContainer"); const footer = $("#reviewsFooter"); container.append(jsxToString((0, import_jsx_runtime.jsx)(SectionStatusMessage, { text: "获取评论中...", id: "reviewsLoading" }))); const limit = await storageManager.getSetting("reviewCount", 20); let reviews = null; try { reviews = await fetchMovieReviews(movieId, 1, limit); } catch (err) { if (String(err).includes("簽名已過期")) show.error("生成签名失败, 请检查系统时间及时区是否正确!"); clog.error("获取评论失败:", err); } finally { $("#reviewsLoading").remove(); } if (!reviews) { container.append(jsxToString((0, import_jsx_runtime.jsx)(SectionErrorMessage, { text: "获取评论失败", retryId: "retryFetchReviews", linkColor: "#1890ff" }))); $("#retryFetchReviews").on("click", async () => { $("#retryFetchReviews").parent().remove(); await this.fetchAndDisplayReviews(movieId); }); return; } if (reviews.length === 0) { container.append(jsxToString((0, import_jsx_runtime.jsx)(SectionStatusMessage, { text: "无评论" }))); return; } this.displayReviews(reviews, container); if (reviews.length === limit) { footer.html(jsxToString((0, import_jsx_runtime.jsx)(SectionLoadMore, { loadMoreId: "loadMoreReviews", endId: "reviewsEnd", text: "加载更多评论", endText: "已加载全部评论" }))); let page = 1; const loadMoreBtn = $("#loadMoreReviews"); loadMoreBtn.on("click", async () => { let moreReviews; loadMoreBtn.text("加载中...").prop("disabled", true); page++; try { moreReviews = await fetchMovieReviews(movieId, page, limit); } catch (err) { clog.error("加载更多评论失败:", err); } finally { loadMoreBtn.text("加载失败, 请点击重试").prop("disabled", false); } if (moreReviews) { this.displayReviews(moreReviews, container); if (moreReviews.length < limit) { loadMoreBtn.remove(); $("#reviewsEnd").show(); } else loadMoreBtn.text("加载更多评论").prop("disabled", false); } }); } else footer.html(jsxToString((0, import_jsx_runtime.jsx)(SectionEndMessage, { text: "已加载全部评论" }))); } displayReviews(reviews, container) { if (reviews.length) reviews.forEach((review) => { const stars = Array(review.score).fill(jsxToString((0, import_jsx_runtime.jsx)(ReviewStarIcon, {}))).join(""); const content = review.content.replace(/ed2k:\/\/\|file\|[^|]+\|\d+\|[a-fA-F0-9]{32}\|\/|magnet:\?[^\s"'<>`\u4e00-\u9fa5,。?!()【】]+|https?:\/\/[^\s"'<>`\u4e00-\u9fa5,。?!()【】]+/g, (match) => jsxToString((0, import_jsx_runtime.jsx)(ReviewLinkContent, { match }))); const html = jsxToString((0, import_jsx_runtime.jsx)(ReviewItem, { floor: this.floorIndex++, username: review.username, stars, time: utils.formatDate(review.created_at), likesCount: review.likes_count, content })); container.append(html); }); } }; var BLACKLIST_DEFAULT_LABEL = "加入黑名单"; var BLACKLIST_DEFAULT_COLOR = "#d22020"; var BLACKLIST_TIP = "将演员加入黑名单, 后续有作品更新也会纳入屏蔽中"; var SORT_DEFAULT_LABEL = "当前排序方式: 默认"; function menuBtnStyle(backgroundColor) { return { backgroundColor: `${backgroundColor} !important` }; } function MenuButtonBoxHtml({ blacklistLabel = BLACKLIST_DEFAULT_LABEL, blacklistColor = BLACKLIST_DEFAULT_COLOR, tagsPage = false, advancedSearch = false, searchOrUserPage = false, sortLabel = SORT_DEFAULT_LABEL, newVideoCount = 0 }) { const showSortToggle = !searchOrUserPage && !advancedSearch; return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [(0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", flexGrow: advancedSearch ? void 0 : 1 }, children: [ (0, import_jsx_runtime.jsx)("a", { id: "waitCheckBtn", className: "jhs-toolbar-menu-btn main-tab-btn", style: menuBtnStyle("#56c938"), children: (0, import_jsx_runtime.jsx)("span", { children: "打开待鉴定" }) }), (0, import_jsx_runtime.jsx)("a", { id: "waitDownBtn", className: "jhs-toolbar-menu-btn main-tab-btn", style: menuBtnStyle("#2caac0"), children: (0, import_jsx_runtime.jsx)("span", { children: "打开已收藏" }) }), tagsPage && (0, import_jsx_runtime.jsx)("a", { id: "addBlacklistBtn", className: "jhs-toolbar-menu-btn main-tab-btn", style: menuBtnStyle(blacklistColor), "data-tip": BLACKLIST_TIP, children: (0, import_jsx_runtime.jsx)("span", { children: blacklistLabel }) }) ] }), (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center" }, children: [ (0, import_jsx_runtime.jsx)("a", { id: "newVideoBtn", className: "jhs-toolbar-menu-btn main-tab-btn", style: menuBtnStyle("#2c6cc0"), children: (0, import_jsx_runtime.jsxs)("span", { children: [ "新作品检测 (", (0, import_jsx_runtime.jsx)("span", { id: "newVideoCount", children: newVideoCount }), ")" ] }) }), (0, import_jsx_runtime.jsx)("a", { id: "blacklistBtn", className: "jhs-toolbar-menu-btn main-tab-btn", style: menuBtnStyle("#34393f"), children: (0, import_jsx_runtime.jsx)("span", { children: "演员黑名单" }) }), showSortToggle && (0, import_jsx_runtime.jsx)("a", { id: "sort-toggle-btn", className: "jhs-toolbar-menu-btn main-tab-btn", style: menuBtnStyle("#8783ab"), children: ` ${sortLabel} ` }) ] })] }); } var HAS_DOWN_TEXT = "📥️ 已下载"; var ListPageButtonPlugin = class extends BasePlugin { getName() { return "ListPageButtonPlugin"; } get pageTypes() { return ["list"]; } async handle() { if (!window.isListPage) return; await this.createMenuBtn(); this.bindEvent(); } async createMenuBtn() { if (isJavdbSite) { let containerEl = $(".main-tabs, .tabs"); const blacklistLabel = "加入黑名单"; const blacklistColor = "#d22020"; let tagBlacklistEntry = null; if (currentHref.includes("/tags")) utils.loopDetector(() => $("#jhs-check-tag").text().trim() !== "", async () => { const addBlacklistBtnEl = $("#addBlacklistBtn"); addBlacklistBtnEl.attr("data-tip", "将当前分类标签加入到黑名单, 后续有作品更新也会纳入屏蔽中"); const tagName = $("#jhs-check-tag").text().trim(); if (!tagName) return; const tagStarId = "no-" + tagName; tagBlacklistEntry = (await storageManager.getBlacklist()).find((entry) => entry.starId === tagStarId); if (tagBlacklistEntry) { addBlacklistBtnEl.css("backgroundColor", "#885d5d"); $("#addBlacklistBtn span").text("已加入黑名单"); } }); const isAdvancedSearch = currentHref.includes("advanced_search"); if (isAdvancedSearch) containerEl = $("h2.section-title"); const sortMethod = localStorage.getItem("jhs_sortMethod"); const sortLabel = "当前排序方式: " + (sortMethod === "rateCount" ? "评价人数" : sortMethod === "date" ? "时间" : "默认"); containerEl.append(jsxToString((0, import_jsx_runtime.jsx)(MenuButtonBoxHtml, { blacklistLabel, blacklistColor, tagsPage: currentHref.includes("/tags"), advancedSearch: isAdvancedSearch, searchOrUserPage: isSearchOrUserPage, sortLabel }))); } } bindEvent() { $("#waitCheckBtn").on("click", () => { this.openWaitCheck().then(); }); $("#waitDownBtn").on("click", () => { this.openFavorite().then(); }); $("#newVideoBtn").on("click", () => { this.getBean("NewVideoPlugin").openDialog(); }); $("#blacklistBtn").on("click", () => { this.getBean("BlacklistPlugin").openBlacklistDialog(); }); $("#sort-toggle-btn").on("click", (event) => { const currentMethod = localStorage.getItem("jhs_sortMethod"); const nextMethod = currentMethod && currentMethod !== "default" ? currentMethod === "rateCount" ? "date" : "default" : "rateCount"; $(event.target).text(`当前排序方式: ${{ default: "默认", rateCount: "评价人数", date: "时间" }[nextMethod]}`); localStorage.setItem("jhs_sortMethod", nextMethod); this.sortItems().then(); }); const blacklistPlugin = this.getBean("BlacklistPlugin"); $("#addBlacklistBtn").on("click", async (event) => { await blacklistPlugin.addBlacklist(event); }); } async sortItems() { if (currentHref.includes("handle") || currentHref.includes("advanced_search")) return; if (isSearchOrUserPage) return; const sortMethod = localStorage.getItem("jhs_sortMethod"); if (!sortMethod) return; $(".movie-list .item").each((index, element) => { if (!$(element).attr("data-original-index")) $(element).attr("data-original-index", index); }); const listEl = $(".movie-list"); const itemEls = $(".item", listEl); if (sortMethod === "default") itemEls.get().sort((itemA, itemB) => $(itemA).data("original-index") - $(itemB).data("original-index")).forEach((el) => listEl.append(el)); else { const items = itemEls.get(); items.sort((itemA, itemB) => { if (sortMethod === "rateCount") { const getRateCount = (el) => { const match = $(el).find(".score .value").text().match(/由(\d+)人/); if (match) return parseFloat(match[1]); else return 0; }; return getRateCount(itemB) - getRateCount(itemA); } else { const getDate = (el) => { const dateStr = $(el).find(".meta").text().trim(); return new Date(dateStr).getTime(); }; return getDate(itemB) - getDate(itemA); } }); listEl.empty().append(items); } } async openWaitCheck() { const selectorConfig = this.getSelector(); const maxCount = await storageManager.getSetting("waitCheckCount", 5); const skipTags = [ BLOCKED_TEXT, FAVORITED_TEXT, HAS_DOWN_TEXT, WATCHED_TEXT ]; let openedCount = 0; $(`${selectorConfig.itemSelector}:visible`).each((_index, element) => { if (openedCount >= maxCount) return false; const itemEl = $(element); if (skipTags.some((tagText) => itemEl.find(`span.tag:contains('${tagText}')`).length > 0)) return; const { carNum, aHref } = this.getBean("ListPagePlugin").findCarNumAndHref(itemEl); if (carNum.includes("FC2-")) { const movieId = this.parseMovieId(aHref); this.getBean("Fc2Plugin")?.openFc2Page(movieId, carNum, aHref); } else { const url = aHref + (aHref.includes("?") ? "&autoPlay=1" : "?autoPlay=1"); window.open(url, "_blank", "noopener,noreferrer"); } openedCount++; }); if (openedCount === 0) show.info("没有需鉴定的视频"); } async openFavorite() { const maxCount = await storageManager.getSetting("waitCheckCount", 5); const favoriteList = (await storageManager.getCarList()).filter((item) => item.status === FAVORITE_ACTION).sort((a, b) => Number(b.createDate) - Number(a.createDate)); for (let i = 0; i < maxCount; i++) { if (i >= favoriteList.length) return; const carInfo = favoriteList[i]; const carNum = carInfo.carNum; const url = carInfo.url; if (carNum.includes("FC2-")) { const movieId = this.parseMovieId(url); await this.getBean("Fc2Plugin")?.openFc2Page(movieId, carNum, url); } else window.open(url, "_blank", "noopener,noreferrer"); clog.debug("打开已收藏", carNum, url); } } }; var PaginationStateMachine = class { _phase = "idle"; _hasMore = false; get phase() { return this._phase; } get hasMore() { return this._hasMore; } get canLoad() { return this._phase === "idle" && this._hasMore; } get isLoading() { return this._phase === "loading"; } init(hasMore) { this._hasMore = hasMore; this._phase = hasMore ? "idle" : "exhausted"; } startLoading() { if (!this.canLoad) return false; this._phase = "loading"; return true; } loadSuccess(hasMore) { this._hasMore = hasMore; this._phase = hasMore ? "idle" : "exhausted"; } loadError() { this._phase = "error"; } retry() { if (this._phase === "error") this._phase = "idle"; } reset() { this._phase = "idle"; this._hasMore = false; } }; async function fetchJavdbPageHtml(url) { let response = null; for (let attempt = 0; attempt < 3; attempt++) { response = await fetch(url, { method: "GET", credentials: "include", cache: "no-store", headers: { Accept: "text/html,application/xhtml+xml" } }); if (response.ok) break; if (![ 429, 502, 503, 504 ].includes(response.status) || attempt === 2) throw new Error(`页面请求失败:HTTP ${response.status}`); await new Promise((resolve) => setTimeout(resolve, 400 * (attempt + 1))); } if (!response?.ok) throw new Error("页面请求失败"); const html = await response.text(); const title = (html.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1] || "").replace(/\s+/g, " ").trim().toLowerCase(); if (!html.includes("<body") || title.includes("cloudflare") || title.includes("just a moment") || /<form[^>]*action=["'][^"']*sign_in/i.test(html)) throw new Error("页面被登录页或人机验证页替代"); return html; } async function loadNextPage$1(plugin) { if (!plugin.pagination.canLoad || !plugin.nextUrl) return; plugin.pagination.startLoading(); plugin.setState("waterfall-loading", "加载中..."); const selectorConfig = plugin.getSelector(); try { const pageNum = utils.getUrlParam(plugin.nextUrl, "page"); let maxPage = 60; if (currentHref.includes("c11")) maxPage = 30; if (isJavdbSite && Number(pageNum) > maxPage || currentHref.includes("month")) { const beyond60Plugin = plugin.getBean("Beyond60Plugin"); if (beyond60Plugin) { const { html, nextUrl: beyondNextUrl, hasMore: beyondHasMore } = await beyond60Plugin.handleBeyond60(plugin.nextUrl); if (html) { const scrollHeight = plugin.container.scrollHeight; plugin.pageItems.push({ page: plugin.currentPage + 1, top: scrollHeight, url: plugin.nextUrl }); $(".movie-list").append(html); } plugin.pagination.loadSuccess(beyondHasMore); plugin.nextUrl = beyondNextUrl; const paginationHtml = beyond60Plugin.createPagination(Number(pageNum), beyondHasMore); $(".pagination").html(paginationHtml); plugin.setState("", ""); if (!plugin.pagination.hasMore) plugin.setState("waterfall-no-more", "已经到底了"); return; } } const responseHtml = await fetchJavdbPageHtml(plugin.nextUrl); clog.log("请求下一页内容:", plugin.nextUrl); const $dom = utils.htmlTo$dom(responseHtml); const items = $dom.find(plugin.getSelector().requestDomItemSelector); const existingList = plugin.getBoxCarInfoList(); const newList = plugin.getBoxCarInfoList(items); if (plugin.checkDuplicateCarNumbers(existingList, newList)) { plugin.nextUrl = null; plugin.pagination.loadError(); plugin.setState("waterfall-error", "翻页内容出现重复数据, 页码受JavDB限制, 已停止瀑布流"); return; } const scrollHeight = plugin.container.scrollHeight; plugin.pageItems.push({ page: plugin.currentPage + 1, top: scrollHeight, url: plugin.nextUrl }); const listPagePlugin = plugin.getBean("ListPagePlugin"); const coverImgs = $dom.find(plugin.getSelector().coverImgSelector); listPagePlugin.replaceHdImg(coverImgs); $(plugin.getSelector().boxSelector).append(items); const nextLinkEl = $dom.find(selectorConfig.nextPageSelector); plugin.nextUrl = nextLinkEl == null ? void 0 : nextLinkEl.attr("href"); plugin.pagination.loadSuccess(!!plugin.nextUrl); const paginationEl = $dom.find(".pagination"); $(".pagination").replaceWith(paginationEl); plugin.setState("", ""); if (!plugin.pagination.hasMore) plugin.setState("waterfall-no-more", "已经到底了"); } catch (err) { clog.error("加载失败:", err); plugin.pagination.loadError(); plugin.setState("waterfall-error", "加载失败,点击重试"); } finally { if (plugin.loadMode === "click" && plugin.pagination.hasMore && plugin.nextUrl) plugin.checkLoad(); } } function checkScrollPosition(plugin) { const scrollY = window.scrollY; for (let i = plugin.pageItems.length - 1; i >= 0; i--) { const item = plugin.pageItems[i]; if (scrollY >= item.top) { if (plugin.currentPage !== item.page) plugin.currentPage = item.page; break; } } } function checkLoad$1(plugin) { if (!plugin.loader) return; if (plugin.loader.getBoundingClientRect().top < window.innerHeight + plugin.preloadDistance) { if (plugin.loadMode === "click") { if (plugin.pagination.isLoading || !plugin.pagination.hasMore || !plugin.nextUrl || plugin.loader.classList.contains("waterfall-error") || plugin.loader.classList.contains("waterfall-loading") || plugin.loader.classList.contains("waterfall-no-more")) return; plugin.setState("waterfall-click", "点击加载下一页"); } else plugin.loadNextPage().then(); } } function setLoadMode(plugin, mode) { plugin.loadMode = mode === "click" ? "click" : "auto"; if (plugin.loadMode === "auto") plugin.checkLoad(); else if (plugin.loader && plugin.pagination.hasMore && plugin.nextUrl && !plugin.pagination.isLoading && !plugin.loader.classList.contains("waterfall-no-more")) { if (plugin.loader.getBoundingClientRect().top < window.innerHeight + plugin.preloadDistance) plugin.setState("waterfall-click", "点击加载下一页"); } } function showLoadAllBtn(plugin) { if (plugin.loadAllBtn) return; if (!plugin.container || !plugin.pagination.hasMore) return; plugin.createLoadAllBtn(); } function hideLoadAllBtn(plugin) { if (!plugin.loadAllBtn) return; plugin.loadAllBtn.remove(); plugin.loadAllBtn = null; } function createLoadAllBtn(plugin) { const btn = document.createElement("div"); btn.className = "jhs-load-all-btn"; btn.textContent = "加载全部"; plugin.supervisor.addEventListener(btn, "click", () => { if (!plugin.isLoadingAll) plugin.loadAllPages().then(); }); document.body.appendChild(btn); plugin.loadAllBtn = btn; } async function loadAllPages(plugin) { if (plugin.isLoadingAll || !plugin.pagination.hasMore) return; plugin.isLoadingAll = true; plugin.updateLoadAllBtn("加载中...", true); try { while (plugin.pagination.hasMore && plugin.nextUrl) { const before = plugin.pageItems.length; await plugin.loadNextPage(); if (plugin.pageItems.length === before) break; plugin.updateLoadAllBtn(`加载中...(第 ${plugin.currentPage} 页)`, true); } if (plugin.loader?.classList.contains("waterfall-error")) { if ((plugin.loader?.textContent || "").includes("重复")) plugin.updateLoadAllBtn("已停止(页码受限)", false); else plugin.updateLoadAllBtn("加载失败,点击重试", false); } else { plugin.updateLoadAllBtn("✓ 已全部加载", false); plugin.supervisor.setTimeout(() => { plugin.loadAllBtn?.classList.add("jhs-load-all-fadeout"); plugin.supervisor.setTimeout(() => { plugin.loadAllBtn?.remove(); plugin.loadAllBtn = null; }, 500); }, 2e3); } } catch { plugin.updateLoadAllBtn("加载失败,点击重试", false); } finally { plugin.isLoadingAll = false; } } function updateLoadAllBtn(plugin, text, disabled) { if (!plugin.loadAllBtn) return; plugin.loadAllBtn.textContent = text; if (disabled) plugin.loadAllBtn.classList.add("jhs-load-all-disabled"); else plugin.loadAllBtn.classList.remove("jhs-load-all-disabled"); } function setState$1(plugin, stateClass, text) { plugin.loader.className = `jhs-scroll ${stateClass}`; plugin.loader.textContent = text; } var auto_page_plugin_default = ".jhs-scroll {\n text-align: center;\n padding-top: 20px;\n font-size: 14px;\n}\n\n.jhs-scroll.waterfall-loading {\n color: #000;\n}\n\n.jhs-scroll.waterfall-error {\n color: #f44336;\n cursor: pointer;\n}\n\n.jhs-scroll.waterfall-no-more {\n color: #4caf50;\n}\n\n.jhs-scroll.waterfall-click {\n color: #1976d2;\n cursor: pointer;\n font-weight: 600;\n}\n\n.jhs-scroll.waterfall-click:hover {\n color: #0d47a1;\n text-decoration: underline;\n}\n\n.jhs-load-all-btn {\n position: fixed;\n left: 20px;\n bottom: 80px;\n z-index: var(--jhs-z-page);\n padding: 10px 20px;\n background: linear-gradient(to right, #5d87c2, #4a90d9);\n color: #fff;\n border-radius: 20px;\n cursor: pointer;\n font-size: 14px;\n box-shadow: 0 2px 8px rgb(0, 0, 0, 0.3);\n transition: opacity 0.5s, transform 0.2s;\n user-select: none;\n}\n\n.jhs-load-all-btn:hover {\n transform: translateY(-2px);\n box-shadow: 0 4px 12px rgb(0, 0, 0, 0.4);\n}\n\n.jhs-load-all-btn.jhs-load-all-disabled {\n opacity: 0.6;\n cursor: wait;\n}\n\n.jhs-load-all-btn.jhs-load-all-fadeout {\n opacity: 0;\n}\n"; var AutoPagePlugin = class extends BasePlugin { preloadDistance = 500; currentPage = this.getInitialPageNumber(); pageItems = []; container = null; loader = null; nextUrl = void 0; pagination = new PaginationStateMachine(); loadAllBtn = null; isLoadingAll = false; loadMode = "auto"; supervisor = new TaskSupervisor(); getName() { return "AutoPagePlugin"; } destroy() { this.supervisor.abort(); } get pageTypes() { return ["list"]; } async initCss() { return auto_page_plugin_default; } async handle() { this.waterfall().then(); } getInitialPageNumber() { if (isJavdbSite) { const match = currentHref.match(/[?&]page=(\d+)/); if (match) return parseInt(match[1], 10); else return 1; } return 1; } async waterfall() { if (await this.shouldDisablePaging()) return; const selectorConfig = this.getSelector(); this.container = document.querySelector(selectorConfig.boxSelector); if (!this.container) { clog.error("没有找到容器节点,停止瀑布流!"); return; } const container = this.container; this.loader = document.createElement("div"); const loader = this.loader; loader.className = "jhs-scroll"; container.parentNode.insertBefore(loader, container.nextSibling); this.pageItems.push({ page: this.currentPage, top: 0, url: window.location.href }); this.supervisor.addEventListener(loader, "click", () => { if (loader.classList.contains("waterfall-error") || loader.classList.contains("waterfall-click")) { this.pagination.retry(); this.loadNextPage().then(); } }); this.supervisor.addEventListener(window, "scroll", () => { this.checkLoad(); this.checkScrollPosition(); }); const nextLinkEl = document.querySelector(selectorConfig.nextPageSelector); this.nextUrl = nextLinkEl == null ? void 0 : nextLinkEl.href; this.pagination.init(!!this.nextUrl); const mode = await storageManager.getSetting("autoPageLoadMode", "auto"); this.loadMode = mode === "click" ? "click" : "auto"; this.supervisor.setTimeout(() => { this.checkLoad(); }, 1e3); if (!this.pagination.hasMore) this.setState("waterfall-no-more", "已经到底了"); else this.showLoadAllBtn(); } async loadNextPage() { return loadNextPage$1(this); } checkScrollPosition() { checkScrollPosition(this); } checkLoad() { checkLoad$1(this); } setLoadMode(mode) { setLoadMode(this, mode); } showLoadAllBtn() { showLoadAllBtn(this); } hideLoadAllBtn() { hideLoadAllBtn(this); } createLoadAllBtn() { createLoadAllBtn(this); } async loadAllPages() { return loadAllPages(this); } updateLoadAllBtn(text, disabled) { updateLoadAllBtn(this, text, disabled); } setState(stateClass, text) { setState$1(this, stateClass, text); } async shouldDisablePaging() { if (!window.isListPage) return true; return [ "search?q", "handlePlayback=1", "handleTop=1", "/want_watch_videos", "/watched_videos", "/advanced_search?type=100" ].some((pattern) => currentHref.includes(pattern)); } updatePageUrl_old(_url) {} updatePageUrl(_url) {} }; var TABULATOR_ZH_CN = { pagination: { first: "首页", first_title: "首页", last: "尾页", last_title: "尾页", prev: "上一页", prev_title: "上一页", next: "下一页", next_title: "下一页", all: "所有", page_size: "每页行数" } }; function createTabulatorOptions(columns, data) { const options = { layout: "fitColumns", placeholder: "暂无数据", virtualDom: true, responsiveLayout: "collapse", responsiveLayoutCollapse: true, columnDefaults: { headerHozAlign: "center", hozAlign: "center" }, columns, locale: "zh-cn", langs: { "zh-cn": TABULATOR_ZH_CN } }; if (data) options.data = data; return options; } function BlacklistActionCell() { return (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: (0, import_jsx_runtime.jsxs)("a", { className: "a-danger delete-btn", children: [ " ", (0, import_jsx_runtime.jsx)("span", { children: "✂️ 删除" }), " " ] }) }); } function BlacklistNameCell({ url, name }) { return (0, import_jsx_runtime.jsx)("a", { className: "open-url", "data-url": url, href: url, "data-name": name, target: "_blank", children: name }); } function BlacklistPaginationCounter({ actorCount, currentCarCount }) { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ "演员: ", actorCount, " \xA0\xA0\xA0番号总数: ", currentCarCount, " ", (0, import_jsx_runtime.jsx)("span", { id: "checkBlacklistMsg", style: { marginLeft: "10px" } }) ] }); } function BlacklistStatusCell({ tipText, statusText }) { return (0, import_jsx_runtime.jsx)("span", { "data-tip": tipText, style: { color: tipText ? "#cc4444" : void 0 }, children: statusText }); } function BlacklistUrlTypeCell({ hasTag }) { return (0, import_jsx_runtime.jsx)("span", { style: { color: hasTag ? "#cc4444" : void 0 }, children: hasTag ? "按所选分类屏蔽" : "未筛选分类" }); } function buildBlacklistTableOptions(plugin, data) { return { ...createTabulatorOptions([ { title: "演员", field: "name", sorter: "string", minWidth: 100, responsive: 0, headerSort: false, formatter: (cell, _formatterParams, _onRendered) => { const rowData = cell.getData(); return jsxToString((0, import_jsx_runtime.jsx)(BlacklistNameCell, { url: rowData.url, name: rowData.name })); } }, { title: "性别角色", field: "role", sorter: "string", width: 120, responsive: 5, formatter: (cell, _formatterParams, _onRendered) => { const role = cell.getData().role; let roleText = role; if (role === "actor") roleText = "男演员"; else if (role === "actress") roleText = "女演员"; return roleText; } }, { title: "影视类别", field: "movieType", sorter: "string", width: 120, responsive: 5, formatter: (cell, _formatterParams, _onRendered) => { const movieType = cell.getData().movieType; let movieTypeText = movieType; if (movieType === "censored") movieTypeText = "有码"; else if (movieType === "uncensored") movieTypeText = "无码"; return movieTypeText; } }, { title: "屏蔽类型", field: "url", sorter: "string", minWidth: 120, responsive: 4, visible: isJavdbSite, formatter: (cell, _formatterParams, _onRendered) => { const hasTag = cell.getData().url.includes("t="); return jsxToString((0, import_jsx_runtime.jsx)(BlacklistUrlTypeCell, { hasTag })); } }, { title: "番号数量", field: "count", sorter: "number", width: 170, responsive: 1 }, { title: "创建时间", field: "createTime", sorter: "string", width: 170, responsive: 5 }, { title: "最后发行时间", field: "lastPublishTime", sorter: "string", width: 170, responsive: 1 }, { title: "状态", field: "isUnCheck", sorter: "string", width: 120, responsive: 1, formatter: (cell, _formatterParams, _onRendered) => { let tipText = ""; let statusText = "正常检测"; if (cell.getData().isUnCheck) { tipText = `停更${plugin.checkBlacklist_ruleTime / 24 / 365}年以上, 下轮任务不再进行检测`; statusText = "停止检测"; } return jsxToString((0, import_jsx_runtime.jsx)(BlacklistStatusCell, { tipText, statusText })); } }, { title: "操作", sorter: "string", cssClass: "action-cell-dropdown", minWidth: 150, responsive: 0, headerSort: false, formatter: (cell, _formatterParams, onRendered) => { const rowData = cell.getData(); onRendered(() => { const deleteBtn = cell.getElement().querySelector(".delete-btn"); if (deleteBtn != null) deleteBtn.addEventListener("click", (event) => { const name = rowData.name; const starId = rowData.starId; if (name) { if (starId) utils.q(event, `是否移除对 ${name} 的屏蔽?`, async () => { await storageManager.removeBlacklistCarList(starId); await storageManager.deleteBlacklistItem(starId); show.info("操作成功"); plugin.reloadTable().then(); }); else show.error("获取starId失败"); } else show.error("获取名称失败"); }); const keywordBtn = cell.getElement().querySelector(".keyword-btn"); if (keywordBtn != null) keywordBtn.addEventListener("click", () => { const prefixCounts = rowData.carList.reduce((acc, carItem) => { const prefix = carItem.carNum.split("-")[0] + "-"; acc[prefix] = (acc[prefix] || 0) + 1; return acc; }, {}); const sortedPrefixes = Object.entries(prefixCounts).map(([prefix, count]) => ({ prefix, count })).sort((a, b) => b.count - a.count); console.log(sortedPrefixes); }); }); return jsxToString((0, import_jsx_runtime.jsx)(BlacklistActionCell, {})); } } ], data), pagination: true, paginationMode: "local", paginationSize: 20, paginationSizeSelector: [ 20, 50, 100, 1e3, 99999 ], paginationCounter: (_pageSize, _pageNo, _maxPage, actorCount, _pages) => jsxToString((0, import_jsx_runtime.jsx)(BlacklistPaginationCounter, { actorCount, currentCarCount: plugin.currentCarCount })), index: "starId", initialSort: [{ column: "createTime", dir: "desc" }] }; } function MovieListWrapper({ html, nextUrl }) { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [(0, import_jsx_runtime.jsx)("div", { className: "movie-list", dangerouslySetInnerHTML: { __html: html } }), nextUrl ? (0, import_jsx_runtime.jsx)("a", { className: "pagination-next", href: nextUrl }) : null] }); } async function filterAllVideo(plugin, names, $dom) { let items; let nextPageHref; if ($dom) { items = $dom.find(plugin.getSelector().requestDomItemSelector); nextPageHref = $dom.find(plugin.getSelector().nextPageSelector).attr("href"); } else { items = $(plugin.getSelector().itemSelector); nextPageHref = $(plugin.getSelector().nextPageSelector).attr("href"); } if (nextPageHref && items.length === 0) failWithToast("解析列表失败"); for (const itemEl of items) { const $item = $(itemEl); const { carNum, url, publishTime } = plugin.getBean("ListPagePlugin").findCarNumAndHref($item); if (url && carNum) try { if (await storageManager.getCar(carNum)) continue; await storageManager.saveCar({ carNum, url, names, actionType: FILTER_ACTION, publishTime }); clog.log("屏蔽演员番号", names, carNum); } catch (error) { clog.error(`保存失败 [${carNum}]:`, error); } } if (nextPageHref) { show.info("请不要关闭窗口, 正在解析下一页:" + nextPageHref); await new Promise((resolve) => setTimeout(resolve, 500)); const pageHtml = await gmHttp.get(nextPageHref); const domParser = new DOMParser(); await filterAllVideo(plugin, names, $(domParser.parseFromString(String(pageHtml), "text/html"))); } else { show.ok("执行结束!"); refresh(); } } async function filterActorVideo(plugin, name, starId, $dom) { const { nextPageLink } = await parseAndSaveFilterInfo(plugin, $dom, name, starId); plugin.nextPageLink = nextPageLink; if (nextPageLink) { let nextDom; plugin.lastPageLink = nextPageLink; show.info("请不要关闭窗口, 正在解析下一页:" + nextPageLink); const pageNum = utils.getUrlParam(nextPageLink, "page") || 0; const beyond60Plugin = plugin.getBean("Beyond60Plugin"); if (isJavdbSite && beyond60Plugin && Number(pageNum) > 60) { const { html, nextUrl, hasMore: _hasMore } = await beyond60Plugin.handleBeyond60(nextPageLink); const wrapperHtml = jsxToString((0, import_jsx_runtime.jsx)(MovieListWrapper, { html, nextUrl })); nextDom = utils.htmlTo$dom(wrapperHtml); } else { clog.log("正在请求下一页内容:", nextPageLink); const pageHtml = await gmHttp.get(nextPageLink); nextDom = utils.htmlTo$dom(String(pageHtml)); } await filterActorVideo(plugin, name, starId, nextDom); } else { show.ok("执行结束!"); refresh(); } } async function parseAndSaveFilterInfo(plugin, $dom, names, starId) { let items; let nextPageHref; if ($dom) { const siteType = JAVDB; items = $dom.find(plugin.getSelector(siteType).requestDomItemSelector); nextPageHref = $dom.find(plugin.getSelector(siteType).nextPageSelector).attr("href"); } else { items = $(plugin.getSelector().itemSelector); nextPageHref = $(plugin.getSelector().nextPageSelector).attr("href"); } if (nextPageHref && items.length === 0) return { nextPageLink: void 0, lastPublishTime: null }; const carList = []; let lastPublishTime = null; for (const itemEl of items) { const $item = $(itemEl); const { carNum, url, publishTime } = plugin.getBean("ListPagePlugin").findCarNumAndHref($item); lastPublishTime ||= publishTime; if (url && carNum) carList.push({ carNum, url, names, actionType: FILTER_ACTION, starId: starId ?? void 0, publishTime }); } try { await storageManager.batchSaveBlacklistCarList(carList); } catch (error) { clog.error("保存失败:", error); } return { nextPageLink: nextPageHref, lastPublishTime }; } function BlacklistConfirmMessage({ tagName, name, isAlreadyBlacklisted, isActress, notFirstPageByQuery }) { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ isAlreadyBlacklisted ? isActress ? "演员 " : "分类 " : isActress ? "是否将该演员 " : "是否将分类 ", (0, import_jsx_runtime.jsx)("span", { style: { color: "#f40" }, children: isActress ? name : tagName }), isAlreadyBlacklisted ? " 已在黑名单中, 是否从当前页开始追加屏蔽?" : " 加入到黑名单中?", notFirstPageByQuery && (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [(0, import_jsx_runtime.jsx)("br", {}), " 注意: 当前页面非第一页, 屏蔽数据将从此页面开始"] }) ] }); } async function addBlacklist(plugin, event) { const position = { clientX: event.clientX, clientY: event.clientY + 80 }; const isAlreadyBlacklisted = $("#addBlacklistBtn span").text().includes("已加入"); let blacklistInfo; let isActress; let tagName = ""; if (currentHref.includes("/tags")) { const tagUrl = new URL(currentHref); tagUrl.searchParams.delete("page"); tagName = $("#jhs-check-tag").text().trim(); blacklistInfo = { starId: "no-" + tagName, name: "虚拟演员-" + tagName, allName: ["虚拟演员"], role: "虚拟演员", movieType: tagName, blacklistUrl: tagUrl.toString() }; isActress = false; } else { blacklistInfo = plugin.getActressPageInfo(); isActress = true; } const { starId, name, allName, role, movieType, blacklistUrl } = blacklistInfo; const notFirstPageByQuery = currentHref.includes("page") && !currentHref.includes("page=1"); const confirmMessage = jsxToString((0, import_jsx_runtime.jsx)(BlacklistConfirmMessage, { tagName, name, isAlreadyBlacklisted, isActress, notFirstPageByQuery })); utils.q(position, confirmMessage, async () => { navigator.locks.request("checkNewActressActorFilterCar", { ifAvailable: true }, async (lock) => { clog.debug("获取锁", lock); if (lock) { plugin.loadObj = loading(); try { await storageManager.addBlacklistItem({ starId: starId ?? void 0, name, allName, role, movieType, url: blacklistUrl ?? void 0 }); await plugin.filterActorVideo(name, starId); const okShow = show.ok(`屏蔽结束,是否跳转到最后一页: ${plugin.lastPageLink}`, { duration: -1, close: true, onClick: () => { okShow.closeShow?.(); window.location.href = plugin.lastPageLink; } }); } catch (error) { clog.error(error); const errorShow = show.error("发生错误, 是否填转到解析失败的那一页? (点击并跳转)", { duration: -1, close: true, onClick: () => { errorShow.closeShow?.(); window.location.href = plugin.nextPageLink; } }); } finally { plugin.loadObj.close(); } } else show.error("当前有定时任务在后台执行中, 无法发起此操作"); }).catch((lockError) => { clog.error("锁任务出现错误:", lockError); }); }); } function BlacklistDataTypeOptions({ totalCount, actorCount, actressCount }) { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ (0, import_jsx_runtime.jsxs)("option", { value: "", children: [ "所有 (", totalCount, ")" ] }), (0, import_jsx_runtime.jsxs)("option", { value: "actor", children: [ "男演员 (", actorCount, ")" ] }), (0, import_jsx_runtime.jsxs)("option", { value: "actress", children: [ "女演员 (", actressCount, ")" ] }) ] }); } async function getTableData(plugin) { const blacklist = await storageManager.getBlacklist(); const blacklistCars = await storageManager.getBlacklistCarList(); const searchValue = $("#searchValue").val(); const statusType = $("#statusType").val(); const $dataTypeSelect = $("#dataType"); const dataType = $dataTypeSelect.val(); const urlType = $("#urlType").val(); const totalCount = blacklist.length; let actorCount = 0; let actressCount = 0; const enrichedList = blacklist.map((item) => { if (item.role === "actor") actorCount++; else if (item.role === "actress") actressCount++; let isUnCheck = false; if (item.lastPublishTime) isUnCheck = !utils.isUnnecessaryCheck(item.lastPublishTime, plugin.checkBlacklist_ruleTime); return { ...item, isUnCheck }; }).filter((item) => (!searchValue || !!item.name.includes(searchValue)) && (statusType !== "normal" || !item.isUnCheck) && (statusType !== "stop" || !!item.isUnCheck) && (dataType ? item.role === dataType : (urlType !== "hasT" || !!item.url.includes("t=")) && (urlType !== "noT" || !item.url.includes("t=")))); $dataTypeSelect.html(jsxToString((0, import_jsx_runtime.jsx)(BlacklistDataTypeOptions, { totalCount, actorCount, actressCount }))); $dataTypeSelect.val(dataType); const carsByStarId = new Map(); for (const carItem of blacklistCars) { const starId = carItem.starId; if (!carsByStarId.has(starId)) carsByStarId.set(starId, []); carsByStarId.get(starId).push(carItem); } const finalData = enrichedList.map((item) => { const starId = item.starId; const cars = carsByStarId.get(starId) || []; return { ...item, carList: cars, count: cars.length }; }); plugin.currentCarCount = finalData.reduce((sum, row) => sum + (row.count || 0), 0); return finalData; } function BlacklistDialog({ showUrlType }) { return (0, import_jsx_runtime.jsxs)("div", { style: { padding: "10px 20px", height: "100%", overflow: "hidden" }, children: [(0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "space-between" }, children: [(0, import_jsx_runtime.jsx)("div", { style: { display: "flex", gap: "5px" } }), (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "5px" }, children: [ (0, import_jsx_runtime.jsxs)("select", { id: "dataType", style: { textAlign: "center", minWidth: "150px" }, children: [ (0, import_jsx_runtime.jsx)("option", { value: "", selected: true, children: "所有" }), (0, import_jsx_runtime.jsx)("option", { value: "actor", children: "男演员" }), (0, import_jsx_runtime.jsx)("option", { value: "actress", children: "女演员" }) ] }), (0, import_jsx_runtime.jsxs)("select", { id: "statusType", style: { textAlign: "center", minWidth: "150px" }, children: [ (0, import_jsx_runtime.jsx)("option", { value: "", selected: true, children: "--检测状态--" }), (0, import_jsx_runtime.jsx)("option", { value: "normal", children: "正常检测" }), (0, import_jsx_runtime.jsx)("option", { value: "stop", children: "停止检测" }) ] }), (0, import_jsx_runtime.jsxs)("select", { id: "urlType", "data-tip": "在演员页屏蔽时,是否选择了分类", style: { textAlign: "center", minWidth: "150px", display: showUrlType ? void 0 : "none" }, children: [ (0, import_jsx_runtime.jsx)("option", { value: "", selected: true, children: "--屏蔽类型--" }), (0, import_jsx_runtime.jsx)("option", { value: "hasT", children: "按所选分类屏蔽" }), (0, import_jsx_runtime.jsx)("option", { value: "noT", children: "未筛选分类" }) ] }), (0, import_jsx_runtime.jsx)("input", { id: "searchValue", type: "text", placeholder: "搜索演员", style: { padding: "4px 5px" } }), (0, import_jsx_runtime.jsx)("a", { id: "cleanQueryBtn", className: "a-info", style: { marginLeft: "0" }, children: "重置" }) ] })] }), (0, import_jsx_runtime.jsx)("div", { id: "table-container", style: { height: "calc(100% - 50px)" } })] }); } var BlacklistPlugin = class extends BasePlugin { loadObj = null; tableObj; nextPageLink; lastPageLink; checkBlacklist_ruleTime = 8760; currentCarCount = 0; getName() { return "BlacklistPlugin"; } async addBlacklist(event) { return addBlacklist(this, event); } async resetBtnTip() { this.checkBlacklist_ruleTime = await storageManager.getSetting("checkBlacklist_ruleTime", 8760); } async openBlacklistDialog() { const dialogHtml = jsxToString((0, import_jsx_runtime.jsx)(BlacklistDialog, { showUrlType: isJavdbSite })); layer.open({ type: 1, title: "演员黑名单", content: dialogHtml, scrollbar: false, area: utils.getResponsiveArea(["80%", "90%"]), anim: -1, success: async (_layerEl) => { await this.loadTableData(); $(".layui-layer-content").on("click", "#cleanQueryBtn", async (_event) => { $("#searchValue").val(""); $("#dataType").val(""); $("#statusType").val(""); await this.reloadTable(); }).on("focusout keydown", "#searchValue", async (event) => { if (event.type === "focusout" || event.key === "Enter") { if (event.key === "Enter") event.preventDefault(); if (event.type === "keydown" && event.key !== "Enter") return; $("#dataType").val(""); await this.reloadTable(); } }).on("change", "#dataType", async () => { $("#searchValue").val(""); await this.reloadTable(); }).on("change", "#statusType", async () => { await this.reloadTable(); }).on("change", "#urlType", async () => { await this.reloadTable(); }).on("click", ".open-url", (event) => { event.preventDefault(); const $link = $(event.currentTarget); const url = $link.attr("data-url"); const name = $link.attr("data-name"); utils.openPage(url ?? "", name ?? "", true, event); }); }, end: () => { if (this.tableObj) { this.tableObj.destroy(); this.tableObj = null; } refresh(); } }); } async reloadTable() { if (!this.tableObj) return; const data = await this.getTableData(); this.tableObj.setData(data); } async getTableData() { return getTableData(this); } async loadTableData() { this.checkBlacklist_ruleTime = await storageManager.getSetting("checkBlacklist_ruleTime") || 8760; const data = await this.getTableData(); this.tableObj = new Tabulator("#table-container", buildBlacklistTableOptions(this, data)); } getCurrentStarUrl() { let url = window.location.href.replace(/([&?])sort_type=[^&]+(&|$)/, "$1"); url = url.replace(/[&?]$/, ""); url = url.replace(/\?&/, "?"); let result = url; result = result.replace(/([&?])page=\d+(&|$)/, "$1"); result = result.replace(/[&?]$/, ""); result = result.replace(/\?&/, "?"); result = result.replace(/\/(\d+)(?:\/(\d+))?(\?|$)/, (match, seg1, seg2, tail) => seg2 !== void 0 ? `/${seg1}${tail}` : match); return result; } parseUrlId(url) { if (!url) throw new Error("url未传入"); return new URL(url).pathname.split("/").filter((seg) => seg.trim() !== "").pop(); } async filterAllVideo(names, $dom) { return filterAllVideo(this, names, $dom); } async filterActorVideo(name, starId, $dom) { return filterActorVideo(this, name, starId, $dom); } async parseAndSaveFilterInfo($dom, names, starId) { return parseAndSaveFilterInfo(this, $dom, names, starId); } }; function ConfirmWarn({ text }) { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ (0, import_jsx_runtime.jsx)("br", {}), " ", (0, import_jsx_runtime.jsx)(ColoredTextCell, { text, color: "#f40" }) ] }); } function WantWatchedHintSpan({ variant }) { return (0, import_jsx_runtime.jsxs)("span", { style: { marginLeft: "8px", color: "#888", fontSize: "12px" }, children: [ "(JHS 现已在详情页自动同步\"", variant === "want" ? "想看" : "看過", "\",本页按钮仅用于初始补录)" ] }); } function WantWatchedImportButton({ variant }) { return (0, import_jsx_runtime.jsx)("a", { className: variant === "want" ? "a-primary" : "a-success", id: "wantWatchBtn", style: { padding: "10px" }, children: "导入至 JHS" }); } var WantAndWatchedVideosPlugin = class extends BasePlugin { type = null; currentPage = 1; getName() { return "WantAndWatchedVideosPlugin"; } async handle() { if (window.location.href.includes("/want_watch_videos")) { $("h3").append(jsxToString((0, import_jsx_runtime.jsx)(WantWatchedImportButton, { variant: "want" }))); $("h3").append(jsxToString((0, import_jsx_runtime.jsx)(WantWatchedHintSpan, { variant: "want" }))); $("#wantWatchBtn").on("click", (clickEvent) => { this.type = FAVORITE_ACTION; this.importWantWatchVideos(clickEvent, "是否将 想看的影片 导入到 JHS-收藏?"); }); } if (window.location.href.includes("/watched_videos")) { $("h3").append(jsxToString((0, import_jsx_runtime.jsx)(WantWatchedImportButton, { variant: "watched" }))); $("h3").append(jsxToString((0, import_jsx_runtime.jsx)(WantWatchedHintSpan, { variant: "watched" }))); $("#wantWatchBtn").on("click", (clickEvent) => { this.type = HAS_WATCH_ACTION; this.importWantWatchVideos(clickEvent, "是否将 看过的影片 导入到 JHS-已观看?"); }); } } importWantWatchVideos(_clickEvent, confirmMessage) { utils.q(null, confirmMessage + " " + jsxToString((0, import_jsx_runtime.jsx)(ConfirmWarn, { text: "执行此功能前请记得备份数据" })), async () => { await withLoading(async () => { this.currentPage = 1; await this.parseMovieList(); }).catch((error) => { clog.error(error); }); }); } async parseMovieList(rootEl) { if (featureFlags.wantWatchBatchImport) { await this.parseMovieListBatch(rootEl); return; } let itemElements; let nextPageHref; if (rootEl) { itemElements = rootEl.find(this.getSelector().itemSelector); nextPageHref = rootEl.find(".pagination-next").attr("href") ?? ""; } else { itemElements = $(this.getSelector().itemSelector); nextPageHref = $(".pagination-next").attr("href") ?? ""; } for (const rawItem of itemElements) { const $item = $(rawItem); const href = $item.find("a").attr("href") ?? ""; const carNum = $item.find(".video-title strong").text().trim(); const metaText = $item.find(".meta").text().trim(); if (href && carNum) try { if (await storageManager.getCar(carNum)) { show.info(`${carNum} 已存在, 跳过`); continue; } await storageManager.saveCar({ carNum, url: href, names: void 0, actionType: this.type, publishTime: metaText ?? void 0 }); } catch (error) { clog.error(`保存失败 [${carNum}]:`, error); } } if (nextPageHref) { show.info("发现下一页,正在解析:", nextPageHref); await new Promise((resolve) => setTimeout(resolve, 1e3)); $.ajax({ url: nextPageHref, method: "GET", success: (responseHtml) => { const domParser = new DOMParser(); const $parsedDom = $(domParser.parseFromString(responseHtml, "text/html")); this.parseMovieList($parsedDom); }, error: function(request) { clog.error(request); show.error("加载下一页失败:" + request.message); } }); } else { show.ok("导入结束!"); refresh(); } } async parseMovieListBatch(rootEl) { const container = rootEl || $("body"); const movieList = container.find(this.getSelector().itemSelector); const nextPageLink = container.find(".pagination-next").attr("href"); if (movieList.length === 0) { show.ok("导入结束!"); refresh(); return; } show.info(`正在处理第 ${this.currentPage} 页...`); const allLocalCars = await storageManager.getCarList(); const carNumCache = new Set(allLocalCars.map((c) => featureFlags.caseInsensitiveCarNum ? c.carNum.toLowerCase() : c.carNum)); const currentPageRecords = []; movieList.each((_i, element) => { const item = $(element); const carNum2 = item.find(".video-title strong").text().trim(); const href = item.find("a").attr("href"); const publishTime = item.find(".meta").text().trim(); const key = featureFlags.caseInsensitiveCarNum ? carNum2.toLowerCase() : carNum2; if (carNum2 && href && !carNumCache.has(key)) { currentPageRecords.push({ carNum: carNum2, url: href, names: null, actionType: this.type, publishTime }); carNumCache.add(key); } }); if (currentPageRecords.length > 0) try { await storageManager.saveCarList(currentPageRecords); clog.log(`第 ${this.currentPage} 页:成功写入 ${currentPageRecords.length} 条`); } catch (e) { clog.error("批量保存失败:", e); } this.currentPage++; if (!nextPageLink) { show.ok("导入结束!"); refresh(); return; } await new Promise((resolve) => setTimeout(resolve, 200)); try { const html = await gmHttp.get(nextPageLink); const next$dom = utils.htmlTo$dom(String(html)); await this.parseMovieListBatch(next$dom); } catch (e) { clog.error("请求下一页失败", e); show.error("加载下一页失败"); } } }; function RelatedItem({ index, relatedId, name, movieCount, collectionCount, viewCount, createTime }) { return (0, import_jsx_runtime.jsxs)("div", { className: "item columns is-desktop", style: { display: "block", marginTop: "6px", backgroundColor: "#ffffff", padding: "10px", marginLeft: "-10px", wordBreak: "break-word", position: "relative" }, children: [ (0, import_jsx_runtime.jsxs)("span", { style: { position: "absolute", top: "5px", right: "10px", color: "#999", fontSize: "12px" }, children: ["#", index] }), (0, import_jsx_runtime.jsxs)("span", { style: { position: "absolute", bottom: "5px", right: "10px", color: "#999", fontSize: "12px" }, children: ["创建时间: ", createTime] }), (0, import_jsx_runtime.jsx)("p", { children: (0, import_jsx_runtime.jsx)("a", { href: `/lists/${relatedId}`, target: "_blank", style: { color: "#2e8abb" }, children: name }) }), (0, import_jsx_runtime.jsxs)("p", { style: { marginTop: "5px" }, children: ["视频个数: ", movieCount] }), (0, import_jsx_runtime.jsxs)("p", { style: { marginTop: "5px" }, children: [ "收藏次数: ", collectionCount, " 被查看次数: ", viewCount ] }) ] }); } var RelatedPlugin = class extends BasePlugin { floorIndex = 1; isInit = false; getName() { return "RelatedPlugin"; } async showRelated(container, movieId) { const isExpanded = await storageManager.getSetting("enableLoadRelated", "no"); const target = container || $("#magnets-content"); target.append(jsxToString((0, import_jsx_runtime.jsx)(SectionHeader, { title: "相关清单", foldId: "relatedFold", linkColor: "#1897ff", foldText: isExpanded === "yes" ? "折叠" : "展开", iconText: isExpanded === "yes" ? "▲" : "▼" }))); $("#relatedFold").on("click", (event) => { event.preventDefault(); event.stopPropagation(); const toggleText = $("#relatedFold .toggle-text"); const toggleIcon = $("#relatedFold .toggle-icon"); const isExpand = toggleText.text() === "展开"; toggleText.text(isExpand ? "折叠" : "展开"); toggleIcon.text(isExpand ? "▲" : "▼"); if (isExpand) { $("#relatedContainer").show(); $("#relatedFooter").show(); if (!this.isInit) { this.fetchAndDisplayRelateds(movieId); this.isInit = true; } storageManager.saveSettingItem("enableLoadRelated", "yes"); } else { $("#relatedContainer").hide(); $("#relatedFooter").hide(); storageManager.saveSettingItem("enableLoadRelated", "no"); } }); target.append(jsxToString((0, import_jsx_runtime.jsx)(SectionContainers, { containerId: "relatedContainer", footerId: "relatedFooter" }))); if (isExpanded === "yes") await this.fetchAndDisplayRelateds(movieId); } async fetchAndDisplayRelateds(movieId) { const container = $("#relatedContainer"); const footer = $("#relatedFooter"); container.append(jsxToString((0, import_jsx_runtime.jsx)(SectionStatusMessage, { text: "获取清单中...", id: "relatedLoading" }))); const limit = 20; let list = null; try { list = await fetchRelatedCollections(movieId, 1, limit); } catch (err) { if (String(err).includes("簽名已過期")) show.error("生成签名失败, 请检查系统时间及时区是否正确!"); clog.error("获取清单失败:", err); } finally { $("#relatedLoading").remove(); } if (!list) { container.append(jsxToString((0, import_jsx_runtime.jsx)(SectionErrorMessage, { text: "获取清单失败", retryId: "retryFetchRelateds", linkColor: "#1897ff" }))); $("#retryFetchRelateds").on("click", async () => { $("#retryFetchRelateds").parent().remove(); await this.fetchAndDisplayRelateds(movieId); }); return; } if (list.length === 0) { container.append(jsxToString((0, import_jsx_runtime.jsx)(SectionStatusMessage, { text: "无清单" }))); return; } this.displayRelateds(list, container); if (list.length === limit) { footer.html(jsxToString((0, import_jsx_runtime.jsx)(SectionLoadMore, { loadMoreId: "loadMoreRelateds", endId: "relatedEnd", text: "加载更多清单", endText: "已加载全部清单" }))); let page = 1; const loadMoreBtn = $("#loadMoreRelateds"); loadMoreBtn.on("click", async () => { let moreList; loadMoreBtn.text("加载中...").prop("disabled", true); page++; try { moreList = await fetchRelatedCollections(movieId, page, limit); } catch (err) { clog.error("加载更多清单失败:", err); } finally { loadMoreBtn.text("加载失败, 请点击重试").prop("disabled", false); } if (moreList) { this.displayRelateds(moreList, container); if (moreList.length < limit) { loadMoreBtn.remove(); $("#relatedEnd").show(); } else loadMoreBtn.text("加载更多清单").prop("disabled", false); } }); } else footer.html(jsxToString((0, import_jsx_runtime.jsx)(SectionEndMessage, { text: "已加载全部清单" }))); } displayRelateds(list, container) { if (list.length) list.forEach((item) => { const html = jsxToString((0, import_jsx_runtime.jsx)(RelatedItem, { index: this.floorIndex++, relatedId: item.relatedId, name: item.name, movieCount: item.movieCount, collectionCount: item.collectionCount, viewCount: item.viewCount, createTime: item.createTime })); container.append(html); }); } }; function FavoriteActressAvatarColumn() { return (0, import_jsx_runtime.jsx)("div", { className: "column actor-avatar", children: (0, import_jsx_runtime.jsx)("div", { className: "image", children: (0, import_jsx_runtime.jsx)("span", { className: "avatar" }) }) }); } var FavoriteActressesPlugin = class extends BasePlugin { getName() { return "FavoriteActressesPlugin"; } async initCss() { const settings = await storageManager.getSetting(); return fold_category_plugin_default.replace("__HIGHLIGHTED_TAG_NUMBER__", String(settings.highlightedTagNumber || 1)).replace("__HIGHLIGHTED_TAG_COLOR__", settings.highlightedTagColor || "#ce2222"); } async handle() { this.bindEvent(); await this.highlightActress(); this.replaceActressAvatar(); } async highlightActress() { if (!window.isDetailPage) return; if (await storageManager.getSetting("enableFavoriteActresses", "yes") !== "yes") return; const favoriteActresses = await storageManager.getFavoriteActressList(); if (!favoriteActresses || favoriteActresses.length === 0) return; const starIdSet = new Set(); favoriteActresses.forEach((actress) => { if (actress.starId) starIdSet.add(String(actress.starId).trim()); }); if (starIdSet.size !== 0) $(".female").prev().each((_index, element) => { const $link = $(element); const href = $link.attr("href"); let urlStarId = null; if (href) { const segments = (href.endsWith("/") ? href.slice(0, -1) : href).split("/"); const lastSegment = segments[segments.length - 1]; if (lastSegment) urlStarId = lastSegment.trim(); } let isFavorite = false; if (urlStarId) isFavorite = starIdSet.has(urlStarId); if (isFavorite) { $link.addClass("highlighted"); $link.attr("title", "高亮已收藏演员, 可在设置-基础配置中关闭"); } }); } async removeActorFromStorage(starId) { if (await storageManager.removeFavoriteActress(starId)) clog.log("移除演员成功"); } bindEvent() { const actorIdRegex = /\/actors\/(\w+)\/(collect|uncollect)/; $(document).on("confirm:complete", "a[href*=\"/actors/\"][href*=\"/uncollect\"]", async (event) => { const [confirmResult] = event.detail; if (!confirmResult) return; const match = $(event.currentTarget).attr("href")?.match(actorIdRegex); const actorId = match ? match[1] : null; if (actorId) await this.removeActorFromStorage(actorId); }); $("#button-collect-actor").click(async (_event) => { const match = $("#button-collect-actor").attr("href")?.match(actorIdRegex); const actorId = match ? match[1] : null; const actressNames = []; const $nameSection = $(".actor-section-name"); if ($nameSection.length) $nameSection.text().trim().split(",").forEach((name) => { actressNames.push(name.trim()); }); const $metaSection = $(".section-meta:not(:contains('影片'))"); if ($metaSection.length) $metaSection.text().trim().split(",").forEach((name) => { actressNames.push(name.trim()); }); if (!actressNames) { clog.error("获取演员名称失败"); return; } const firstName = actressNames[0]; if (!actorId) { clog.error("无法获取演员ID进行收藏操作。"); return; } const actressData = { starId: actorId, name: firstName, allName: actressNames, avatar: ($(".avatar").first().css("background-image") || "").replace(/^url\(["']?|["']?\)$/g, "") }; if (await storageManager.addFavoriteActressList([actressData]) === 1) clog.log(`收藏演员成功: ${firstName} (ID: ${actorId})`); else clog.log(`收藏演员失败: ${firstName} (ID: ${actorId})`); }); $("#button-uncollect-actor").click(async (_event) => { const match = $("#button-uncollect-actor").attr("href")?.match(actorIdRegex); const actorId = match ? match[1] : null; if (actorId) await this.removeActorFromStorage(actorId); else clog.error("无法获取演员ID进行取消收藏操作。"); }); } async replaceActressAvatar() { const actressId = this.getActressId(); if (!actressId) return; const matchedActress = (await storageManager.getFavoriteActressList()).find((actress) => actress.starId === actressId); if (matchedActress && matchedActress.avatar) { const cssUrl = `url('${matchedActress.avatar}')`; let $avatar = $(".avatar").first(); if ($avatar.length === 0) { const avatarColumnHtml = jsxToString((0, import_jsx_runtime.jsx)(FavoriteActressAvatarColumn, {})); $(".section-columns").prepend(avatarColumnHtml); $avatar = $(".avatar").first(); } if ($avatar.length === 0) return; if ($avatar.css("background-image").trim().toLowerCase() !== cssUrl.trim().toLowerCase()) { $avatar.css("background-image", cssUrl); $avatar.css("background-size", "cover"); $avatar.css("background-position", "top center"); $avatar.css("background-repeat", "no-repeat"); } } } }; function ActressCard({ starId, avatar, name, allNameText, actressUrl, lastCheckTime, lastPublishTime, isStale, ruleTimeYears, remark, editSvg, deleteSvg, btnStyle, typeLabel, typeColor, newVideoCount }) { return (0, import_jsx_runtime.jsxs)("div", { className: "actress-card", "data-starId": starId, style: { ...isStale ? { background: "#d4cece" } : null, minHeight: "370px" }, children: [ (0, import_jsx_runtime.jsx)("a", { href: actressUrl, target: "_blank", style: { textDecoration: "none", color: "inherit", display: "block", flexGrow: 1 }, children: (0, import_jsx_runtime.jsx)("img", { src: avatar, alt: allNameText, className: "actress-card-avatar" }) }), (0, import_jsx_runtime.jsxs)("div", { children: [(0, import_jsx_runtime.jsx)("a", { href: actressUrl, target: "_blank", style: { textDecoration: "none", color: "inherit", display: "block", flexGrow: 1 }, children: (0, import_jsx_runtime.jsx)("div", { className: "actress-card-name", children: name }) }), (0, import_jsx_runtime.jsx)("div", { className: "actress-card-allname", title: allNameText, children: allNameText })] }), (0, import_jsx_runtime.jsx)("div", { style: { fontSize: "0.8em", marginTop: "5px" }, children: (0, import_jsx_runtime.jsxs)("span", { children: ["上次检测: ", lastCheckTime] }) }), (0, import_jsx_runtime.jsx)("div", { style: { fontSize: "0.8em", marginTop: "5px" }, children: (0, import_jsx_runtime.jsxs)("span", { children: ["最后发行作品: ", lastPublishTime] }) }), (0, import_jsx_runtime.jsx)("div", { style: { fontSize: "0.7em", color: "#cc4444", marginTop: "5px", minHeight: "18px" }, children: (0, import_jsx_runtime.jsx)("span", { children: isStale ? `停更${ruleTimeYears}年以上, 下轮任务不再进行检测` : "" }) }), (0, import_jsx_runtime.jsx)("div", { style: { fontSize: "0.8em", marginTop: "5px", color: "#3765c5", minHeight: "10px" }, children: (0, import_jsx_runtime.jsx)("span", { children: remark }) }), (0, import_jsx_runtime.jsxs)("div", { style: { marginTop: "10px", display: "flex", justifyContent: "center", gap: "10px" }, children: [(0, import_jsx_runtime.jsx)("a", { title: "编辑", className: "card-btn btn-edit-actress", style: btnStyle, "data-starId": starId, dangerouslySetInnerHTML: { __html: editSvg } }), (0, import_jsx_runtime.jsx)("a", { title: "取消收藏", className: "card-btn btn-delete-actress", style: btnStyle, "data-starId": starId, dangerouslySetInnerHTML: { __html: deleteSvg } })] }), (0, import_jsx_runtime.jsx)("div", { className: "card-tag", style: { backgroundColor: typeColor }, children: typeLabel }), (0, import_jsx_runtime.jsxs)("div", { className: "card-new-count-tag", "data-tip": `最新作品数量: ${newVideoCount}`, children: ["🔔 ", newVideoCount] }) ] }); } var NAV_BTN_STYLE = { padding: "8px 12px", margin: "0 5px", background: "#f0f0f0", border: "1px solid #ddd", borderRadius: "4px", cursor: "pointer" }; var PAGE_BTN_BASE_STYLE = { padding: "8px 12px", margin: "0 3px", border: "1px solid #ddd", borderRadius: "4px", cursor: "pointer" }; var ACTIVE_STYLE = { background: "#007bff", color: "white", borderColor: "#007bff" }; function ActressPagination({ totalCount, totalPages, page }) { if (totalPages === 0) return (0, import_jsx_runtime.jsx)("span", { style: { color: "#666" }, children: "共 0 条记录" }); let rangeStart = Math.max(1, page - Math.floor(2.5)); const rangeEnd = Math.min(totalPages, rangeStart + 5 - 1); if (rangeEnd - rangeStart < 4) rangeStart = Math.max(1, rangeEnd - 5 + 1); return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ page > 1 && totalPages > 5 && (0, import_jsx_runtime.jsx)("button", { className: "pagination-btn", "data-page": 1, style: NAV_BTN_STYLE, children: "首页" }), page > 1 && (0, import_jsx_runtime.jsx)("button", { className: "pagination-btn", "data-page": page - 1, style: NAV_BTN_STYLE, children: "上一页" }), Array.from({ length: rangeEnd - rangeStart + 1 }, (_, i) => { const pageNum = rangeStart + i; return (0, import_jsx_runtime.jsx)("button", { className: `pagination-btn page-number-btn ${pageNum === page ? "active" : ""}`, "data-page": pageNum, style: { ...PAGE_BTN_BASE_STYLE, ...pageNum === page ? ACTIVE_STYLE : null }, children: pageNum }, pageNum); }), page < totalPages && (0, import_jsx_runtime.jsx)("button", { className: "pagination-btn", "data-page": page + 1, style: NAV_BTN_STYLE, children: "下一页" }), page < totalPages && totalPages > 5 && (0, import_jsx_runtime.jsx)("button", { className: "pagination-btn", "data-page": totalPages, style: NAV_BTN_STYLE, children: "尾页" }), (0, import_jsx_runtime.jsxs)("span", { style: { marginLeft: "20px", color: "#666" }, children: [ "共 ", totalCount, " 条记录 (第 ", page, "/", totalPages, " 页)" ] }) ] }); } var ACTRESS_TYPE_MAP = { [UNCENSORED]: { label: "无码", color: "#4CAF50" }, [CENSORED]: { label: "有码", color: "#FF9800" } }; var DEFAULT_TYPE = { label: "未知", color: "#9E9E9E" }; async function renderActressCards(plugin) { const $container = $("#actress-card-container"); if (!$container.length) return; let favoriteActresses = await storageManager.getFavoriteActressList(); const typeFilter = String($("#paramActressType").val() ?? ""); if (typeFilter !== "all") favoriteActresses = favoriteActresses.filter((actress) => actress.actressType === typeFilter); const sortedActresses = utils.genericSort(favoriteActresses, [{ key: (actress) => actress.newVideoList?.length ?? 0, order: "desc" }, { key: "lastPublishTime", order: "desc" }]); const totalCount = sortedActresses.length; const totalPages = Math.ceil(totalCount / plugin.pageSize); const startIndex = (plugin.currentPage - 1) * plugin.pageSize; const endIndex = startIndex + plugin.pageSize; const pageActresses = sortedActresses.slice(startIndex, endIndex); const javDbUrl = await plugin.getBean("OtherSitePlugin").getJavDbUrl(); const ruleTimeHours = await storageManager.getSetting("checkNewVideo_ruleTime") || 8760; const cardsHtml = pageActresses.map((actress) => { const allNameText = Array.isArray(actress.allName) ? actress.allName.join(",") : ""; if (Array.isArray(actress.newVideoList)) actress.newVideoList.join(","); const actressUrl = `${javDbUrl}/actors/${actress.starId}?t=d`; let isStale = false; if (actress.lastPublishTime) isStale = !utils.isUnnecessaryCheck(actress.lastPublishTime, ruleTimeHours); const typeInfo = ACTRESS_TYPE_MAP[actress.actressType ?? ""] ?? DEFAULT_TYPE; let btnStyle = {}; if (isStale) btnStyle = { background: "linear-gradient(145deg, #e0e0e0 0%, #cabdbd 100%)", boxShadow: "none" }; return jsxToString((0, import_jsx_runtime.jsx)(ActressCard, { starId: actress.starId, avatar: actress.avatar || "https://c0.jdbstatic.com/images/actor_unknow.jpg", name: actress.name, allNameText, actressUrl, lastCheckTime: actress.lastCheckTime || "", lastPublishTime: actress.lastPublishTime || "", isStale, ruleTimeYears: ruleTimeHours / 24 / 365, remark: actress.remark || "", editSvg: plugin.editSvg, deleteSvg: plugin.deleteSvg, btnStyle, typeLabel: typeInfo.label, typeColor: typeInfo.color, newVideoCount: actress.newVideoList?.length || 0 })); }).join(""); $container.html(cardsHtml); $(".btn-delete-actress").off("click").on("click", (event) => { event.preventDefault(); const starId = $(event.currentTarget).attr("data-starId") ?? ""; const actress = sortedActresses.find((item) => item.starId === starId); if (!actress) return; utils.q(event, `是否取消收藏 ${actress.name}?`, async () => { const uncollectUrl = `${await plugin.getBean("OtherSitePlugin").getJavDbUrl()}/actors/${starId}/uncollect`; const csrfToken = document.querySelector("meta[name=csrf-token]")?.getAttribute("content") ?? ""; const response = await gmHttp.post(uncollectUrl, void 0, { "x-csrf-token": csrfToken }); if (response.includes("removeClass")) { await storageManager.removeFavoriteActress(starId); plugin.loadData(); } else { show.error("移除失败"); clog.error("移除失败,返回值:", response); } }); }); $(".btn-edit-actress").off("click").on("click", (event) => { event.preventDefault(); const starId = $(event.currentTarget).attr("data-starId") ?? ""; const actress = sortedActresses.find((item) => item.starId === starId); if (actress) plugin.editActress(actress); else show.error(`未找到 starId 为 ${starId} 的女优记录。`); }); renderPagination$1(plugin, totalCount, totalPages); show.ok("加载完成"); } function renderPagination$1(plugin, totalCount, totalPages) { const page = plugin.currentPage; $("#actress-pagination").html(jsxToString((0, import_jsx_runtime.jsx)(ActressPagination, { totalCount, totalPages, page }))); $(".pagination-btn").off("click").on("click", (event) => { if ($(event.currentTarget).is("[disabled]")) return; const targetPage = parseInt($(event.currentTarget).data("page")); if (targetPage >= 1 && targetPage <= totalPages && targetPage !== plugin.currentPage) { plugin.currentPage = targetPage; plugin.renderActressCards(); } }); } function AvatarSelectDialog({ avatarUrls }) { return (0, import_jsx_runtime.jsxs)("div", { id: "gfriends-image-list-container", children: [(0, import_jsx_runtime.jsxs)("p", { id: "gfriends-prompt", style: { textAlign: "center", fontSize: "15px", marginBottom: "15px" }, children: [ "点击图片即可选择(初始共 ", avatarUrls.length, " 张)" ] }), (0, import_jsx_runtime.jsx)("div", { style: { overflowY: "auto", height: "calc(100% - 40px)" }, children: (0, import_jsx_runtime.jsx)("div", { id: "gfriends-image-list", children: avatarUrls.map((url, index) => (0, import_jsx_runtime.jsxs)("div", { id: `wrapper-${index}`, className: "gfriends-image-item-wrapper", children: [(0, import_jsx_runtime.jsx)("img", { alt: "", src: url, "data-url": url, className: "gfriends-selectable-img", "data-wrapper-id": `wrapper-${index}` }), (0, import_jsx_runtime.jsx)("div", { className: "gfriends-size-tag", "data-size-for": `wrapper-${index}`, children: "..." })] }, index)) }) })] }); } var avatar_select_dialog_default = "/* 保持上一个回答的美化样式 */\n#gfriends-image-list-container {\n padding: 15px;\n height: 100%;\n box-sizing: border-box;\n background-color: #f8f9fa;\n}\n\n#gfriends-prompt {\n color: #555;\n font-weight: 500;\n border-bottom: 1px solid #eee;\n padding-bottom: 10px;\n}\n\n#gfriends-image-list {\n display: flex;\n flex-wrap: wrap;\n gap: 15px;\n justify-content: center;\n}\n\n.gfriends-image-item-wrapper {\n width: 160px;\n height: 225px; /* 增加高度以容纳尺寸标签 */\n overflow: hidden;\n border-radius: 6px;\n box-shadow: 0 4px 8px rgb(0, 0, 0, 0.1);\n transition:\n transform 0.2s ease,\n box-shadow 0.2s ease;\n cursor: pointer;\n position: relative;\n padding-bottom: 25px; /* 为尺寸标签留出空间 */\n}\n\n.gfriends-selectable-img {\n width: 100%;\n height: 200px; /* 固定图片高度 */\n object-fit: cover;\n border: 3px solid transparent;\n border-radius: 6px;\n transition: border 0.2s ease;\n}\n\n.gfriends-image-item-wrapper:hover {\n transform: translateY(-4px) scale(1.02);\n box-shadow: 0 8px 16px rgb(0, 0, 0, 0.2);\n}\n\n.gfriends-selectable-img.is-selected {\n border-color: #ff6347;\n box-shadow: 0 0 0 3px #ff6347;\n}\n\n/* 新增:尺寸标签样式 */\n.gfriends-size-tag {\n position: absolute;\n bottom: 0; /* 定位到图片容器底部 */\n left: 0;\n right: 0;\n height: 25px;\n line-height: 25px;\n text-align: center;\n background-color: rgb(0, 0, 0, 0.7); /* 半透明背景 */\n color: #fff;\n font-size: 11px;\n font-weight: bold;\n border-bottom-left-radius: 6px;\n border-bottom-right-radius: 6px;\n user-select: none;\n}\n"; async function searchAvatar() { const $nameInput = $("#edit-actress-name"); const $allNameInput = $("#edit-actress-allname"); const nameText = $nameInput.val().trim(); const searchNames = $allNameInput.val().trim().split(/[\uff0c,]/).map((name) => name.trim()).filter((name) => name.length > 0); if (nameText) searchNames.unshift(nameText); if (searchNames.length === 0) { show.error("请先填写女优主名称或别名进行搜索。"); return; } const loader = loading("正在搜索头像..."); let avatarUrls = []; try { avatarUrls = await loadGfriends(searchNames); } catch (error) { show.error(`头像数据加载或搜索失败: ${error instanceof Error ? error.message : String(error)}`); return; } finally { loader.close(); } if (avatarUrls.length === 0) { show.error(`未找到与 '${searchNames.join("、")}' 相关的头像。请检查名称。`); return; } const dialogContent = jsxToString((0, import_jsx_runtime.jsx)(StyleBlock, { css: avatar_select_dialog_default })) + jsxToString((0, import_jsx_runtime.jsx)(AvatarSelectDialog, { avatarUrls })); let errorCount = 0; layer.open({ type: 1, title: `选择女优头像 (${avatarUrls.length} 张)`, area: utils.getResponsiveArea(["900px", "85%"]), content: dialogContent, btn: ["关闭"], success: (layerEl, layerIndex) => { const $layer = $(layerEl); const $images = $layer.find(".gfriends-selectable-img"); const $prompt = $layer.find("#gfriends-prompt"); $images.each((_index, element) => { const $img = $(element); const wrapperId = String($img.data("wrapper-id") ?? ""); const $wrapper = $layer.find(`#${wrapperId}`); const $sizeTag = $layer.find(`.gfriends-size-tag[data-size-for="${wrapperId}"]`); $img.on("load", () => { const width = element.naturalWidth; const height = element.naturalHeight; $sizeTag.text(`${width} x ${height}`); }); $img.on("error", () => { $wrapper.remove(); errorCount++; const remaining = avatarUrls.length - errorCount; $prompt.text(`点击图片即可选择(已移除 ${errorCount} 张错误图片,剩余 ${remaining} 张)`); if (remaining === 0) { show.error("所有搜索到的头像链接均已失效,无法选择。"); layer.close(layerIndex); } }); if (element.complete) { if (element.naturalWidth > 0) $img.trigger("load"); else $img.trigger("error"); } }); $images.on("click", (event) => { const $img = $(event.currentTarget); const url = String($img.data("url") ?? ""); $("#edit-actress-avatar").val(url); $("#edit-avatar-preview").attr("src", url); $images.removeClass("is-selected"); $img.addClass("is-selected"); setTimeout(() => { layer.close(layerIndex); }, 150); }); utils.setupEscClose(layerIndex); } }); } var LABEL_STYLE$1 = { display: "block", marginBottom: "5px", fontWeight: "bold" }; var FIELD_ROW_STYLE$1 = { marginBottom: "15px" }; function EditActressDialog({ avatar, name, textareaStyle, allNameText, actressType, newVideoText, remark }) { return (0, import_jsx_runtime.jsxs)("div", { style: { padding: "20px" }, children: [ (0, import_jsx_runtime.jsxs)("div", { style: { marginBottom: "15px", textAlign: "center" }, children: [(0, import_jsx_runtime.jsx)("img", { id: "edit-avatar-preview", src: avatar, alt: "Avatar Preview", style: { width: "100px", height: "100px", borderRadius: "50%", objectFit: "cover", marginBottom: "10px", border: "2px solid #ddd" } }), (0, import_jsx_runtime.jsxs)("div", { style: { textAlign: "left" }, children: [ (0, import_jsx_runtime.jsx)("label", { style: LABEL_STYLE$1, children: "头像链接:" }), (0, import_jsx_runtime.jsx)("input", { type: "text", id: "edit-actress-avatar", value: avatar, style: { width: "100%", padding: "8px", border: "1px solid #ccc", borderRadius: "4px" } }), (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "5px", marginTop: "5px" }, children: [(0, import_jsx_runtime.jsx)("button", { type: "button", id: "search-avatar-btn", style: { flexGrow: 1, padding: "8px", backgroundColor: "#007bff", color: "white", border: "none", borderRadius: "4px", cursor: "pointer" }, children: "搜索头像" }), (0, import_jsx_runtime.jsx)("button", { type: "button", id: "select-cdn-btn", style: { width: "100px", padding: "8px", backgroundColor: "#6c757d", color: "white", border: "none", borderRadius: "4px", cursor: "pointer", fontSize: "12px" }, children: "选择 CDN 源" })] }) ] })] }), (0, import_jsx_runtime.jsxs)("div", { style: FIELD_ROW_STYLE$1, children: [(0, import_jsx_runtime.jsx)("label", { style: LABEL_STYLE$1, children: "主名称:" }), (0, import_jsx_runtime.jsx)("input", { type: "text", id: "edit-actress-name", value: name, style: { width: "100%", padding: "8px", border: "1px solid #ccc", borderRadius: "4px" } })] }), (0, import_jsx_runtime.jsxs)("div", { style: FIELD_ROW_STYLE$1, children: [(0, import_jsx_runtime.jsx)("label", { style: LABEL_STYLE$1, children: "所有别名(用逗号隔开):" }), (0, import_jsx_runtime.jsx)("textarea", { id: "edit-actress-allname", style: textareaStyle, children: allNameText })] }), (0, import_jsx_runtime.jsxs)("div", { style: FIELD_ROW_STYLE$1, children: [(0, import_jsx_runtime.jsx)("label", { style: LABEL_STYLE$1, children: "演员类别:" }), (0, import_jsx_runtime.jsxs)("select", { id: "actressType", style: { width: "100%", padding: "10px", border: "1px solid #ddd" }, children: [ (0, import_jsx_runtime.jsx)("option", { value: "", selected: actressType === "", children: "未知" }), (0, import_jsx_runtime.jsx)("option", { value: "censored", selected: actressType === "censored", children: "有码" }), (0, import_jsx_runtime.jsx)("option", { value: "uncensored", selected: actressType === "uncensored", children: "无码" }) ] })] }), (0, import_jsx_runtime.jsxs)("div", { style: FIELD_ROW_STYLE$1, children: [(0, import_jsx_runtime.jsx)("label", { style: LABEL_STYLE$1, children: "最新作品(用逗号隔开):" }), (0, import_jsx_runtime.jsx)("textarea", { id: "edit-actress-newvideolist", style: textareaStyle, children: newVideoText })] }), (0, import_jsx_runtime.jsxs)("div", { style: FIELD_ROW_STYLE$1, children: [(0, import_jsx_runtime.jsx)("label", { style: LABEL_STYLE$1, children: "备注:" }), (0, import_jsx_runtime.jsx)("textarea", { id: "edit-remark", style: textareaStyle, children: remark })] }) ] }); } function CdnSelectDialog({ sources, currentIndex }) { return (0, import_jsx_runtime.jsxs)("div", { style: { padding: "20px" }, children: [ (0, import_jsx_runtime.jsxs)("p", { style: { marginBottom: "15px", fontWeight: "bold", color: "#333" }, children: [ "请选择头像数据源 (当前: ", sources[currentIndex].name, "):" ] }), sources.map((source, index) => (0, import_jsx_runtime.jsxs)("div", { style: { marginBottom: "10px" }, children: [(0, import_jsx_runtime.jsx)("input", { type: "radio", id: `cdn-${index}`, name: "cdn-source", value: index, checked: index === currentIndex, style: { marginRight: "10px" } }), (0, import_jsx_runtime.jsxs)("label", { for: `cdn-${index}`, children: [ source.name, " ", source.json.includes("jsdelivr") ? "(推荐)" : "" ] })] }, index)), (0, import_jsx_runtime.jsx)("p", { style: { marginTop: "20px", color: "#555", fontSize: "12px" }, children: "切换源会清除本地缓存的数据,并在下次搜索时重新加载。" }) ] }); } async function editActress(plugin, actress) { const name = actress.name; const avatar = actress.avatar; const remark = actress.remark || ""; const allNameText = Array.isArray(actress.allName) ? actress.allName.join(",") : ""; const newVideoText = Array.isArray(actress.newVideoList) ? actress.newVideoList.join(",") : ""; const starId = actress.starId; const textareaStyle = { width: "100%", padding: "8px", border: "1px solid #ccc", borderRadius: "4px", minHeight: "60px", overflowY: "hidden" }; const actressType = actress.actressType || ""; const dialogContent = jsxToString((0, import_jsx_runtime.jsx)(EditActressDialog, { avatar, name, textareaStyle, allNameText, actressType, newVideoText, remark })); layer.open({ type: 1, title: `编辑女优: ${name} (${starId})`, area: ["500px", "750px"], content: dialogContent, btn: ["保存", "取消"], success: (_layerEl, layerIndex) => { const autoHeight = ($textarea) => { $textarea.css("height", "auto"); $textarea.css("height", $textarea[0].scrollHeight + 15 + "px"); }; $("#edit-actress-avatar").on("input", (event) => { const val = $(event.currentTarget).val(); $("#edit-avatar-preview").attr("src", val); }); const $allNameArea = $("#edit-actress-allname"); $allNameArea.on("input", (event) => { autoHeight($(event.currentTarget)); }); autoHeight($allNameArea); const $newVideoArea = $("#edit-actress-newvideolist"); $newVideoArea.on("input", (event) => { autoHeight($(event.currentTarget)); }); autoHeight($newVideoArea); $("#search-avatar-btn").on("click", async () => { await plugin.searchAvatar(); }); $("#select-cdn-btn").on("click", async () => { const currentIndex = getCurrentCdnSource().index; const cdnDialogContent = jsxToString((0, import_jsx_runtime.jsx)(CdnSelectDialog, { sources: GFRIENDS_SOURCES, currentIndex })); layer.open({ type: 1, title: "选择 CDN 源", area: ["400px", "auto"], content: cdnDialogContent, btn: ["确定", "取消"], success: (_cdnLayerEl, cdnLayerIndex) => { utils.setupEscClose(cdnLayerIndex); }, yes: async (cdnLayerIndex) => { const selectedValue = String($("input[name=\"cdn-source\"]:checked").val() ?? ""); const selectedIndex = parseInt(selectedValue, 10); if (selectedIndex !== currentIndex) { localStorage.setItem(GFRIENDS_CDN_INDEX_KEY, selectedIndex.toString()); clearCache(); try { await window.filetreeDb.set(FILETREE_DATA_KEY, null); } catch (error) { clog.error("清除 IndexedDB 缓存失败:", error); } show.ok(`CDN 源已切换为: ${GFRIENDS_SOURCES[selectedIndex].name}`); layer.close(cdnLayerIndex); } else layer.close(cdnLayerIndex); } }); }); utils.setupEscClose(layerIndex); }, yes: async (layerIndex) => { const avatar = String($("#edit-actress-avatar").val() ?? "").trim(); const name = String($("#edit-actress-name").val() ?? "").trim(); const allNameText = String($("#edit-actress-allname").val() ?? "").trim(); const newVideoText = String($("#edit-actress-newvideolist").val() ?? "").trim(); const remark = String($("#edit-remark").val() ?? "").trim(); const actressType = String($("#actressType").val() ?? ""); if (!name) { show.error("主名称不能为空"); return false; } const allNameArray = allNameText.split(/[\uff0c,]/).map((part) => part.trim()).filter((part) => part.length > 0); const newVideoArray = newVideoText.split(/[\uff0c,]/).map((part) => part.trim()).filter((part) => part.length > 0); actress.avatar = avatar; actress.name = name; actress.allName = allNameArray; actress.newVideoList = newVideoArray; actress.actressType = actressType; actress.remark = remark; if (await storageManager.updateFavoriteActress(actress)) show.error("修改失败"); else { plugin.renderActressCards().then(); show.ok(`女优 ${name} 信息已更新`); layer.close(layerIndex); } } }); } function NewVideoDialog({ refreshSvg }) { return (0, import_jsx_runtime.jsxs)("div", { className: "newVideoToolBox", style: { display: "flex", flexDirection: "column", height: "100%", overflow: "hidden", padding: "10px" }, children: [ (0, import_jsx_runtime.jsxs)("div", { style: { marginBottom: "15px", display: "flex", justifyContent: "space-between" }, children: [(0, import_jsx_runtime.jsx)("div", { children: (0, import_jsx_runtime.jsx)("span", { id: "checkNewVideoMsg" }) }), (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "flex-start" }, children: [(0, import_jsx_runtime.jsxs)("select", { id: "paramActressType", style: { textAlign: "center", height: "100%", minWidth: "150px", border: "1px solid #ddd", marginRight: "10px" }, children: [ (0, import_jsx_runtime.jsx)("option", { value: "all", selected: true, children: "所有" }), (0, import_jsx_runtime.jsx)("option", { value: "uncensored", children: "无码" }), (0, import_jsx_runtime.jsx)("option", { value: "censored", children: "有码" }), (0, import_jsx_runtime.jsx)("option", { value: "", children: "未知" }) ] }), (0, import_jsx_runtime.jsx)("a", { className: "a-normal", id: "reLoad", dangerouslySetInnerHTML: { __html: `${refreshSvg} 刷新` } })] })] }), (0, import_jsx_runtime.jsx)("div", { id: "actress-card-container", className: "jhs-scrollbar" }), (0, import_jsx_runtime.jsx)("div", { id: "actress-pagination" }) ] }); } function NewVideoDialogTitle() { return (0, import_jsx_runtime.jsx)("span", { style: { padding: "0 10px" }, "data-tip": "数据来源: 女优页面首页,含磁链分类", children: "新作品检测 ❓" }); } var new_video_plugin_default = "#actress-card-container {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(243px, 1fr)); /* 响应式3-5列 */\n gap: 20px;\n padding-bottom: 20px;\n padding-right: 10px;\n background: #f9f9f9;\n border-radius: 5px;\n overflow-y: auto;\n}\n\n.actress-card {\n background: #fff;\n border: 1px solid #e0e0e0;\n border-radius: 8px;\n box-shadow: 0 4px 8px rgb(0, 0, 0, 0.08);\n padding: 15px;\n text-align: center;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n position: relative;\n overflow: hidden;\n}\n\n.actress-card:hover {\n box-shadow: 0 8px 15px rgb(0, 0, 0, 0.15);\n}\n\n.actress-card-name {\n font-size: 1.2em;\n font-weight: bold;\n color: #007bff;\n margin-top: 10px;\n}\n\n.actress-card-allname {\n font-size: 0.9em;\n color: #999;\n margin-top: 5px;\n height: 30px; /* 保证高度一致性 */\n overflow: hidden;\n white-space: nowrap; /* 防止文字换行 */\n text-overflow: ellipsis; /* 当文本溢出时,显示省略号 */\n}\n\n.actress-card-avatar {\n width: 100px;\n height: 100px;\n border-radius: 50%;\n object-fit: contain;\n margin: 0 auto;\n border: 4px solid #f0f0f0;\n box-shadow: 0 2px 4px rgb(0, 0, 0, 0.1);\n}\n\n.card-tag {\n position: absolute;\n top: 15px; /* 调整标签距离顶部的距离 */\n right: -50px; /* 调整标签距离右侧的距离,负值让它移到外面一点 */\n width: 150px; /* 标签的宽度,影响斜角长度 */\n padding: 5px 0; /* 上下内边距 */\n text-align: center;\n background-color: #ff4757; /* 标签颜色 */\n color: white; /* 文字颜色 */\n font-size: 14px;\n font-weight: bold;\n z-index: 10; /* 确保标签在其他内容之上 */\n\n /* 3. 核心:旋转标签,使其倾斜 */\n transform: rotate(45deg); /* 45度斜角 */\n\n /* 可选:添加一些阴影或边框效果 */\n box-shadow: 0 0 10px rgb(0, 0, 0, 0.3);\n}\n\n.card-new-count-tag {\n position: absolute;\n top: 5px;\n text-align: center;\n font-size: 14px;\n font-weight: bold;\n z-index: 10;\n}\n\n#actress-pagination {\n padding-top: 10px;\n text-align: center;\n border-top: 1px solid #ddd;\n}\n\n@media (width <= 600px) {\n .page-number-btn {\n display: none !important;\n }\n}\n\n.card-btn {\n width: 44px;\n height: 44px;\n border-radius: 50%;\n display: flex;\n justify-content: center;\n align-items: center;\n text-decoration: none;\n border: none;\n cursor: pointer;\n background: linear-gradient(145deg, #e0e0e0 0%, #f7f7f7 100%);\n box-shadow:\n 8px 8px 16px rgb(0, 0, 0, 0.08),\n -8px -8px 16px rgb(255, 255, 255, 1);\n transition:\n box-shadow 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),\n transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),\n background-color 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);\n}\n\n.card-btn svg,\n.card-btn svg path {\n transition: fill 0.3s ease;\n}\n\n.card-btn:hover {\n box-shadow:\n inset 5px 5px 10px rgb(0, 0, 0, 0.1),\n inset -5px -5px 10px rgb(255, 255, 255, 0.9);\n transform: scale(0.97);\n background: #e0e0e0;\n}\n\n.btn-check-actress svg path {\n fill: #4caf50;\n}\n\n.btn-check-actress:hover svg path {\n fill: #388e3c;\n}\n\n.btn-edit-actress svg path {\n fill: #ffc107;\n}\n\n.btn-edit-actress:hover svg path {\n fill: #ffb300;\n}\n\n.btn-delete-actress svg path {\n fill: #f44336;\n}\n\n.btn-delete-actress:hover svg path {\n fill: #d32f2f;\n}\n"; var NewVideoPlugin = class extends BasePlugin { currentPage = 1; pageSize = 30; getName() { return "NewVideoPlugin"; } async initCss() { return new_video_plugin_default; } async handle() { await this.showNewVideoCount(); } async showNewVideoCount() { const totalCount = (await storageManager.getFavoriteActressList()).reduce((total, actress) => { return total + (actress.newVideoList?.length ?? 0); }, 0); $("#newVideoCount").text(`${totalCount}`); } async resetBtnTip() {} async openDialog() { const dialogContent = jsxToString((0, import_jsx_runtime.jsx)(NewVideoDialog, { refreshSvg: this.refreshSvg })); layer.open({ type: 1, title: jsxToString((0, import_jsx_runtime.jsx)(NewVideoDialogTitle, {})), content: dialogContent, scrollbar: false, area: utils.getResponsiveArea(["80%", "90%"]), anim: -1, success: async (_layerEl, layerIndex) => { this.loadData(); this.bindClick(); utils.setupEscClose(layerIndex); } }); } bindClick() { $("#reLoad").on("click", (_event) => { this.loadData(); $("#checkNewVideoMsg").text(""); }); $("#paramActressType").on("change", (_event) => { this.loadData(); }); } loadData() { this.currentPage = 1; this.renderActressCards().then(); } async renderActressCards() { await renderActressCards(this); } async editActress(actress) { await editActress(this, actress); } renderPagination(totalCount, totalPages) { renderPagination$1(this, totalCount, totalPages); } async searchAvatar() { await searchAvatar(); } }; function handleBatchAction(plugin, event, btn) { event.preventDefault(); event.stopPropagation(); const selectedRows = plugin.tableObj.getSelectedData(); let actionLabel = ""; let actionType = ""; if (btn.hasClass("multiple-history-filterBtn")) { actionLabel = "屏蔽"; actionType = FILTER_ACTION; } else if (btn.hasClass("multiple-history-favoriteBtn")) { actionLabel = "收藏"; actionType = FAVORITE_ACTION; } else if (btn.hasClass("multiple-history-hasWatchBtn")) { actionLabel = "已观看"; actionType = HAS_WATCH_ACTION; } else if (btn.hasClass("multiple-history-deleteBtn")) { actionLabel = "移除"; actionType = "delete"; } utils.q(event, `当前已勾选${selectedRows.length}条数据, 是否全标记为 ${actionLabel}?`, async () => { const loader = loading(); try { if (actionType === "delete") { const carNums = selectedRows.map((row) => row.carNum); const removed = await storageManager.batchRemoveCars(carNums); if (Number(removed) > 0) show.ok(`已成功删除 ${removed} 个番号`); else if (removed === false) show.error("提供的番号中没有一个存在于列表中。"); } else { const updates = JSON.parse(JSON.stringify(selectedRows)); updates.forEach((item) => { item.actionType = actionType; }); await storageManager.saveCarList(updates); show.ok("操作成功"); } plugin.tableObj.deselectRow(); plugin.reloadTable().then(); } catch (err) { clog.error("批量操作失败:", err); show.error("操作失败: " + (err instanceof Error ? err.message : String(err))); } finally { loader.close(); } }); } function HistoryActionButtons({ carNum, url, watchedColor, watchedText, favoriteColor, favoriteText, blockColor, blockText }) { return (0, import_jsx_runtime.jsxs)("div", { className: "action-btns", style: { display: "flex", gap: "5px", justifyContent: "center" }, "data-car-num": carNum, "data-href": url, children: [(0, import_jsx_runtime.jsxs)("div", { className: "sub-btns", children: [(0, import_jsx_runtime.jsx)("a", { className: "jhs-toolbar-menu-btn sub-btns-toggle", style: { backgroundColor: "#c59d36", color: "white", marginBottom: "5px" }, children: (0, import_jsx_runtime.jsx)("span", { children: "✏️ 变更" }) }), (0, import_jsx_runtime.jsxs)("div", { className: "sub-btns-menu", children: [ (0, import_jsx_runtime.jsx)("a", { className: "jhs-toolbar-menu-btn history-editBtn", style: { backgroundColor: "#007bff", color: "white", marginBottom: "5px" }, children: (0, import_jsx_runtime.jsx)("span", { children: "✏️ 编辑" }) }), (0, import_jsx_runtime.jsx)("a", { className: "jhs-toolbar-menu-btn history-deleteBtn", style: { backgroundColor: "#8c8080", color: "white", marginBottom: "5px" }, children: (0, import_jsx_runtime.jsx)("span", { children: "✂️ 移除" }) }), (0, import_jsx_runtime.jsx)("a", { className: "jhs-toolbar-menu-btn history-hasWatchBtn", style: { backgroundColor: watchedColor, marginBottom: "5px" }, children: watchedText }), (0, import_jsx_runtime.jsx)("a", { className: "jhs-toolbar-menu-btn history-favoriteBtn", style: { backgroundColor: favoriteColor, marginBottom: "5px" }, children: favoriteText }), (0, import_jsx_runtime.jsx)("a", { className: "jhs-toolbar-menu-btn history-filterBtn", style: { backgroundColor: blockColor, marginBottom: "5px" }, children: blockText }) ] })] }), (0, import_jsx_runtime.jsx)("a", { className: "jhs-toolbar-menu-btn history-detailBtn", style: { backgroundColor: "#3397de", color: "white", marginBottom: "5px" }, children: (0, import_jsx_runtime.jsx)("span", { children: "📄 详情页" }) })] }); } function TableLinkParam({ text }) { return (0, import_jsx_runtime.jsx)("a", { className: "table-link-param", children: text }); } function buildHistoryTableOptions(plugin) { return { ...createTabulatorOptions([ { formatter: "rowSelection", titleFormatter: "rowSelection", hozAlign: "center", headerSort: false, responsive: 0, width: 40, titleFormatterParams: { rowRange: "active" }, cellClick: (_event, cell) => { cell.getRow().toggleSelect(); } }, { title: "番号", field: "carNum", width: 120, sorter: "string", responsive: 0, formatter: (cell, _formatterParams, _onRendered) => { const carNum = cell.getData().carNum; const dashIndex = carNum.indexOf("-"); if (dashIndex === -1) return carNum; return jsxToString((0, import_jsx_runtime.jsx)(TableLinkParam, { text: carNum.substring(0, dashIndex + 1) })) + carNum.substring(dashIndex + 1); } }, { title: "演员", field: "names", minWidth: 200, sorter: "string", responsive: 5, headerSort: true, formatter: (cell, _formatterParams, _onRendered) => (cell.getData().names || "").split(" ").filter((part) => part.trim() !== "").map((part) => jsxToString((0, import_jsx_runtime.jsx)(TableLinkParam, { text: part }))).join(" ") }, { title: "创建时间", field: "createDate", width: 170, sorter: "string", responsive: 4 }, { title: "修改时间", field: "updateDate", width: 170, sorter: "string", responsive: 4 }, { title: "发行时间", field: "publishTime", width: 170, sorter: "string", responsive: 4 }, { title: "来源", field: "url", width: 80, sorter: "string", responsive: 5, hozAlign: "left", formatter: (cell, _formatterParams, _onRendered) => { const url = cell.getData().url; if (url) { if (url.includes("javdb")) return jsxToString((0, import_jsx_runtime.jsx)(ColoredTextCell, { text: "Javdb", color: "#d34f9e" })); else return jsxToString((0, import_jsx_runtime.jsx)(ColoredTextCell, { text: url, color: "#050505" })); } else return ""; } }, { title: "状态", field: "status", width: 100, sorter: "string", responsive: 1, headerSort: false, formatter: (cell, _formatterParams, _onRendered) => { const status = cell.getData().status; let color = ""; let text; switch (status) { case "filter": color = BLOCK_COLOR; text = BLOCK_TEXT; break; case "favorite": color = FAVORITE_COLOR; text = FAVORITE_TEXT; break; case "hasWatch": color = WATCHED_COLOR; text = WATCHED_TEXT; break; default: text = status; } return jsxToString((0, import_jsx_runtime.jsx)(ColoredTextCell, { text, color })); } }, { title: "备注", field: "remark", width: 100, sorter: "string", responsive: 6 }, { title: "操作", sorter: "string", minWidth: 150, cssClass: "action-cell-dropdown", responsive: 0, headerSort: false, formatter: (cell, _formatterParams, onRendered) => { const data = cell.getData(); onRendered(() => { const editBtn = cell.getElement().querySelector(".history-editBtn"); if (editBtn != null) editBtn.addEventListener("click", () => { plugin.editRecord(data); }); }); return jsxToString((0, import_jsx_runtime.jsx)(HistoryActionButtons, { carNum: data.carNum, url: data.url || "", watchedColor: WATCHED_COLOR, watchedText: WATCHED_TEXT, favoriteColor: FAVORITE_COLOR, favoriteText: FAVORITE_TEXT, blockColor: BLOCK_COLOR, blockText: BLOCK_TEXT })); } } ]), pagination: true, paginationMode: "remote", sortMode: "remote", ajaxURL: "queryRealm", dataLoader: false, ajaxRequestFunc: async (_url, _config, params) => { const page = params.page; const size = params.size; const sort = params.sort; return await plugin.getDataList(page, size, sort); }, dataReceiveParams: { last_page: "maxPage", last_row: "totalCount", data: "dataList" }, paginationSize: 50, paginationSizeSelector: [ 50, 100, 1e3, 99999 ], paginationCounter: (_pageNumber, _pageSize, _totalPages, recordCount, _data) => `共 ${recordCount} 条记录`, selectableRowsPersistence: false, index: "carNum", initialSort: [{ column: "updateDate", dir: "desc" }] }; } async function getDataList(plugin, page, size, sort) { const carList = await storageManager.getCarList(); plugin.allCount = carList.length; plugin.filterCount = 0; plugin.favoriteCount = 0; plugin.hasWatchCount = 0; carList.forEach((car) => { switch (car.status) { case FILTER_ACTION: plugin.filterCount++; break; case FAVORITE_ACTION: plugin.favoriteCount++; break; case HAS_WATCH_ACTION: plugin.hasWatchCount++; } }); $("#dataType option[value=\"all\"]").text(`所有 (${plugin.allCount})`); $("#dataType option[value=\"filter\"]").text(`${BLOCKED_TEXT} (${plugin.filterCount})`); $("#dataType option[value=\"favorite\"]").text(`${FAVORITED_TEXT} (${plugin.favoriteCount})`); $("#dataType option[value=\"hasWatch\"]").text(`${WATCHED_TEXT} (${plugin.hasWatchCount})`); const selectedType = $("#dataType").val(); let filtered = selectedType === "all" ? carList : carList.filter((car) => car.status === selectedType); const searchText = String($("#searchCarNum").val() ?? "").trim(); if (searchText) { const normalizedSearch = searchText.toLowerCase().replace("-c", "").replace("-uc", "").replace("-4k", ""); filtered = filtered.filter((row) => { const matchCar = row.carNum.toLowerCase().includes(normalizedSearch); const matchName = (row.names ? row.names : "").toLowerCase().includes(normalizedSearch); return matchCar || matchName; }); } if (sort && sort.length > 0) { const sortConfig = sort[0]; const field = sortConfig.field; const dir = sortConfig.dir; filtered.sort((rowA, rowB) => { const valA = rowA[field]; const valB = rowB[field]; const isEmptyA = valA == null || valA === ""; const isEmptyB = valB == null || valB === ""; if (isEmptyA && !isEmptyB) return 1; else if (!isEmptyA && isEmptyB) return -1; else if (isEmptyA && isEmptyB) return 0; else if (valA < valB) { if (dir === "asc") return -1; else return 1; } else if (valA > valB) { if (dir === "asc") return 1; else return -1; } else return 0; }); } const totalCount = filtered.length; const maxPage = Math.ceil(totalCount / size); const start = (page - 1) * size; const end = start + size; filtered = filtered.slice(start, end); return { maxPage, dataList: filtered, totalCount }; } var LABEL_STYLE = { display: "block", marginBottom: "5px", fontWeight: "bold" }; var FIELD_ROW_STYLE = { marginBottom: "15px" }; function EditRecordDialog({ carNum, names, status, url, remark, inputStyle, textareaStyle, statusOptions }) { return (0, import_jsx_runtime.jsxs)("div", { style: { padding: "20px" }, children: [ (0, import_jsx_runtime.jsxs)("div", { style: FIELD_ROW_STYLE, children: [(0, import_jsx_runtime.jsx)("label", { style: LABEL_STYLE, children: "番号:" }), (0, import_jsx_runtime.jsx)("input", { type: "text", id: "edit-carNum", value: carNum, style: { ...inputStyle, backgroundColor: "#f0f0f0" }, readOnly: true })] }), (0, import_jsx_runtime.jsxs)("div", { style: FIELD_ROW_STYLE, children: [(0, import_jsx_runtime.jsx)("label", { style: LABEL_STYLE, children: "演员 (用空格隔开):" }), (0, import_jsx_runtime.jsx)("textarea", { id: "edit-names", style: textareaStyle, children: names })] }), (0, import_jsx_runtime.jsxs)("div", { style: FIELD_ROW_STYLE, children: [(0, import_jsx_runtime.jsx)("label", { style: LABEL_STYLE, children: "状态:" }), (0, import_jsx_runtime.jsxs)("select", { id: "edit-status", style: { width: "100%", padding: "10px", border: "1px solid #ddd" }, children: [(0, import_jsx_runtime.jsx)("option", { value: "", selected: status === "", children: "-- 请选择 --" }), statusOptions.map((opt) => (0, import_jsx_runtime.jsx)("option", { value: opt.value, selected: status === opt.value, children: opt.text }, opt.value))] })] }), (0, import_jsx_runtime.jsxs)("div", { style: FIELD_ROW_STYLE, children: [(0, import_jsx_runtime.jsx)("label", { style: LABEL_STYLE, children: "链接:" }), (0, import_jsx_runtime.jsx)("input", { type: "text", id: "edit-url", value: url, style: inputStyle })] }), (0, import_jsx_runtime.jsxs)("div", { style: FIELD_ROW_STYLE, children: [(0, import_jsx_runtime.jsx)("label", { style: LABEL_STYLE, children: "备注:" }), (0, import_jsx_runtime.jsx)("textarea", { id: "edit-remark", style: textareaStyle, children: remark })] }) ] }); } function HistoryDialog({ blockedText, favoritedText, watchedText, watchedColor, favoriteColor, favoriteText, blockColor, blockText }) { return (0, import_jsx_runtime.jsxs)("div", { style: { padding: "10px 20px", height: "100%", overflow: "hidden" }, children: [ (0, import_jsx_runtime.jsxs)("div", { id: "filterBox", style: { display: "flex", gap: "5px" }, children: [ (0, import_jsx_runtime.jsxs)("select", { id: "dataType", style: { textAlign: "center", minWidth: "150px" }, children: [ (0, import_jsx_runtime.jsx)("option", { value: "all", selected: true, children: "所有" }), (0, import_jsx_runtime.jsx)("option", { value: "filter", children: blockedText }), (0, import_jsx_runtime.jsx)("option", { value: "favorite", children: favoritedText }), (0, import_jsx_runtime.jsx)("option", { value: "hasWatch", children: watchedText }) ] }), (0, import_jsx_runtime.jsx)("input", { id: "searchCarNum", type: "text", placeholder: "搜索番号|演员", style: { padding: "4px 5px" } }), (0, import_jsx_runtime.jsx)("a", { id: "clearSearchbtn", className: "a-info", style: { marginLeft: "0" }, children: "重置" }) ] }), (0, import_jsx_runtime.jsxs)("div", { id: "allSelectBox", style: { marginTop: "8px", display: "none" }, children: [ (0, import_jsx_runtime.jsx)("a", { className: "jhs-toolbar-menu-btn multiple-history-deleteBtn", style: { backgroundColor: "#8c8080", color: "white", marginBottom: "5px" }, children: (0, import_jsx_runtime.jsx)("span", { children: "✂️ 移除" }) }), (0, import_jsx_runtime.jsx)("a", { className: "jhs-toolbar-menu-btn multiple-history-hasWatchBtn", style: { backgroundColor: watchedColor, marginBottom: "5px" }, children: watchedText }), (0, import_jsx_runtime.jsx)("a", { className: "jhs-toolbar-menu-btn multiple-history-favoriteBtn", style: { backgroundColor: favoriteColor, marginBottom: "5px" }, children: favoriteText }), (0, import_jsx_runtime.jsx)("a", { className: "jhs-toolbar-menu-btn multiple-history-filterBtn", style: { backgroundColor: blockColor, marginBottom: "5px" }, children: blockText }) ] }), (0, import_jsx_runtime.jsx)("div", { id: "table-container", style: { height: "calc(100% - 50px)", overflowX: "hidden" } }) ] }); } function HistoryNavButton({ variant }) { switch (variant) { case "desktop": return (0, import_jsx_runtime.jsx)("div", { className: "navbar-item has-sub-btns is-hoverable historyBtnBox", children: (0, import_jsx_runtime.jsx)("a", { id: "historyBtn", className: "navbar-link nav-btn", style: { color: "#aade66 !important", paddingRight: "15px !important" }, children: "鉴定记录" }) }); case "mini": return (0, import_jsx_runtime.jsx)("div", { className: "navbar-item miniHistoryBtnBox", children: (0, import_jsx_runtime.jsx)("a", { id: "miniHistoryBtn", className: "navbar-link nav-btn", style: { color: "#aade66 !important", paddingLeft: "0 !important", paddingRight: "0 !important" }, children: "鉴定记录" }) }); default: return null; } } var history_plugin_default = "/* 下拉菜单容器(相对定位) */\n.sub-btns {\n position: relative;\n display: inline-block;\n}\n\n/* 下拉菜单内容(默认隐藏) */\n.sub-btns-menu {\n display: none;\n position: absolute;\n right: 80px;\n top: -10px;\n background: white;\n padding: 10px;\n box-shadow: 0 2px 8px rgb(0, 0, 0, 0.15);\n z-index: 100;\n border-radius: 4px;\n overflow: hidden;\n}\n\n/* 点击后显示菜单(JS 控制) */\n.sub-btns-menu.show {\n display: flex !important;\n flex-direction: column;\n}\n\n.table-link-param {\n cursor: pointer;\n}\n"; var HistoryPlugin = class extends BasePlugin { tableObj = null; allCount = 0; filterCount = 0; favoriteCount = 0; hasWatchCount = 0; constructor() { super(); } getName() { return "HistoryPlugin"; } async initCss() { return history_plugin_default; } handleResize() { if ($(".navbar-search").is(":hidden")) { $(".historyBtnBox").show(); $(".miniHistoryBtnBox").hide(); } else { $(".historyBtnBox").hide(); $(".miniHistoryBtnBox").show(); } } async handle() { if (isJavdbSite) { $(".navbar-end").prepend(jsxToString((0, import_jsx_runtime.jsx)(HistoryNavButton, { variant: "desktop" }))); $(".navbar-search").css("margin-left", "0").before(jsxToString((0, import_jsx_runtime.jsx)(HistoryNavButton, { variant: "mini" }))); this.handleResize(); $(window).resize(() => { this.handleResize(); }); $("#historyBtn,#miniHistoryBtn").on("click", () => this.openHistory()); } this.bindClick(); } openHistory() { layer.open({ type: 1, title: "鉴定记录", content: jsxToString((0, import_jsx_runtime.jsx)(HistoryDialog, { blockedText: BLOCKED_TEXT, favoritedText: FAVORITED_TEXT, watchedText: WATCHED_TEXT, watchedColor: WATCHED_COLOR, favoriteColor: FAVORITE_COLOR, favoriteText: FAVORITE_TEXT, blockColor: BLOCK_COLOR, blockText: BLOCK_TEXT })), scrollbar: false, shadeClose: true, area: utils.getResponsiveArea(["70%", "90%"]), anim: -1, success: async () => { await this.loadTableData(); $(".layui-layer-content").on("click", "#clearSearchbtn", async () => { $("#searchCarNum").val(""); $("#dataType").val("all"); await this.reloadTable(); $("#allSelectBox").hide(); }).on("focusout keydown", "#searchCarNum", async (event) => { if (event.type === "focusout" || event.key === "Enter") { if (event.key === "Enter") event.preventDefault(); if (event.type === "keydown" && event.key !== "Enter") return; await this.reloadTable(); } }).on("click", ".table-link-param", async (event) => { const link = $(event.currentTarget); $("#searchCarNum").val(link.text()); await this.reloadTable(); }).on("change", "#dataType", async () => { await this.reloadTable(); }); }, end: () => { if (this.tableObj) { this.tableObj.destroy(); this.tableObj = null; } refresh(); } }); } async reloadTable() { this.tableObj.deselectRow(); this.tableObj.setPage(1); } bindClick() { document.addEventListener("click", function(event) { const target = event.target; if (target.closest(".sub-btns-toggle")) { const menu = target.closest(".sub-btns").querySelector(".sub-btns-menu"); document.querySelectorAll(".sub-btns-menu.show").forEach((el) => { if (el !== menu) el.classList.remove("show"); }); menu.classList.toggle("show"); } else document.querySelectorAll(".sub-btns-menu.show").forEach((el) => { el.classList.remove("show"); }); }); $(document).on("click", ".history-deleteBtn, .history-filterBtn, .history-favoriteBtn, .history-hasWatchBtn, .history-detailBtn", (event) => { event.preventDefault(); event.stopPropagation(); const btn = $(event.currentTarget); const actionBtns = btn.closest(".action-btns"); const carNum = actionBtns.attr("data-car-num"); const href = actionBtns.attr("data-href"); const applyAction = async (actionType) => { await storageManager.saveCar({ carNum: carNum ?? "", url: href, names: void 0, actionType }); refresh(); await this.reloadTable(); }; if (btn.hasClass("history-filterBtn")) utils.q(event, `是否屏蔽${carNum}?`, () => applyAction(FILTER_ACTION)); else if (btn.hasClass("history-favoriteBtn")) applyAction(FAVORITE_ACTION).then(); else if (btn.hasClass("history-hasWatchBtn")) applyAction(HAS_WATCH_ACTION).then(); else if (btn.hasClass("history-deleteBtn")) this.handleDelete(event, carNum); else if (btn.hasClass("history-detailBtn")) this.handleClickDetail(event, { carNum, url: href }).then(); }); $(document).on("click", ".multiple-history-deleteBtn, .multiple-history-filterBtn, .multiple-history-favoriteBtn, .multiple-history-hasWatchBtn", (event) => { handleBatchAction(this, event, $(event.currentTarget)); }); } async getDataList(page, size, sort) { return getDataList(this, page, size, sort); } async loadTableData() { this.tableObj = new Tabulator("#table-container", buildHistoryTableOptions(this)); this.tableObj.on("rowSelectionChanged", (rows, _deselectedRows, _selected, _deselected) => { const selectBox = $("#allSelectBox"); const filterBox = $("#filterBox"); if (rows && rows.length > 0) { filterBox.hide(); selectBox.show(); } else { filterBox.show(); selectBox.hide(); } }); this.tableObj.on("rowDblClick", (_event, row) => { row.toggleSelect(); }); this.tableObj.on("tableBuilt", async () => {}); } handleDelete(event, carNum) { utils.q(event, `是否移除${carNum}?`, async () => { await storageManager.removeCar(carNum); this.getBean("ListPagePlugin").showCarNumBox(carNum); this.reloadTable().then(); }); } async handleClickDetail(event, carInfo) { if (isJavdbSite) { if (carInfo.carNum.includes("FC2-")) { const movieId = this.parseMovieId(carInfo.url); this.getBean("Fc2Plugin")?.openFc2Dialog(movieId, carInfo.carNum, carInfo.url); } else { if (!carInfo.url) { window.open("/search?q=" + encodeURIComponent(carInfo.carNum), "_blank", "noopener,noreferrer"); return; } utils.openPage(carInfo.url, carInfo.carNum, false, event); } } } async editRecord(record) { const carNum = record.carNum; const names = record.names || ""; const url = record.url || ""; const status = record.status; const remark = record.remark || ""; const textareaStyle = { width: "100%", padding: "8px", border: "1px solid #ccc", borderRadius: "4px", minHeight: "60px", overflowY: "hidden" }; const inputStyle = { width: "100%", padding: "8px", border: "1px solid #ccc", borderRadius: "4px" }; const statusOptions = [ { value: FILTER_ACTION, text: BLOCK_TEXT }, { value: FAVORITE_ACTION, text: FAVORITE_TEXT }, { value: HAS_WATCH_ACTION, text: WATCHED_TEXT } ]; console.log(statusOptions); layer.open({ type: 1, title: `编辑记录: ${carNum}`, area: ["500px", "650px"], content: jsxToString((0, import_jsx_runtime.jsx)(EditRecordDialog, { carNum, names, status, url, remark, inputStyle, textareaStyle, statusOptions })), btn: ["保存", "取消"], success: () => { const autoResize = (el) => { el.css("height", "auto"); el.css("height", el[0].scrollHeight + 15 + "px"); }; const namesInput = $("#edit-names"); namesInput.on("input", (event) => autoResize($(event.currentTarget))); autoResize(namesInput); const remarkInput = $("#edit-remark"); remarkInput.on("input", (event) => autoResize($(event.currentTarget))); autoResize(remarkInput); }, yes: async (index) => { const names = String($("#edit-names").val() ?? "").trim(); const status = $("#edit-status").val(); const url = String($("#edit-url").val() ?? "").trim(); const remark = String($("#edit-remark").val() ?? "").trim(); const updated = { ...record, names, actionType: status, url, remark }; await storageManager.updateCarInfo(updated); this.tableObj.setData(); layer.close(index); } }); } }; var VIDEO_QUALITY_LIST = [ { id: "video-mhb", quality: "dmb_w", text: "旧视频源-中画质宽版 (404p)", canSelect: false }, { id: "video-mhb", quality: "sm_s", text: "旧视频源-低画质 (240p)", canSelect: false }, { id: "video-mhb", quality: "dm_s", text: "旧视频源-中画质 (360p)", canSelect: false }, { id: "video-mhb", quality: "dmb_s", text: "旧视频源-中画质 (480p)", canSelect: false }, { id: "video-mhb", quality: "mhb_w", text: "旧视频源-高画质宽版 (404p)", canSelect: false }, { id: "video-mmb", quality: "mmb", text: "中画质 (432p)", canSelect: true }, { id: "video-mhb", quality: "mhb", text: "高画质 (576p)", canSelect: true }, { id: "video-hmb", quality: "hmb", text: "HD (720p)", canSelect: true }, { id: "video-hhb", quality: "hhb", text: "FullHD (1080p)", canSelect: true }, { id: "video-hhbs", quality: "hhbs", text: "FullHD (1080p60fps)", canSelect: true }, { id: "video-4k", quality: "4k", text: "4K (2160p)", canSelect: true }, { id: "video-4ks", quality: "4ks", text: "4K (2160p60fps)", canSelect: true } ]; var CREDENTIAL_ID_KEY = "jhs:credential_id"; var LAST_AUTO_BACKUP_DATE_KEY = "jhs:last_auto_backup_date"; var DEFAULT_AUTO_BACKUP_CONFIG = { enabled: true, frequency: "daily" }; function generateUuid() { if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID(); const bytes = new Uint8Array(16); crypto.getRandomValues(bytes); bytes[6] = bytes[6] & 15 | 64; bytes[8] = bytes[8] & 63 | 128; const hex = []; bytes.forEach((b) => hex.push(b.toString(16).padStart(2, "0"))); return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`; } function getCredentialId() { let id = GM_getValue(CREDENTIAL_ID_KEY, void 0); if (!id) { id = generateUuid(); GM_setValue(CREDENTIAL_ID_KEY, id); } return id; } function getTodayStr() { const now = new Date(); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; } function shouldAutoBackup(config) { if (!config.enabled || config.frequency === "disabled") return false; if (config.frequency === "everyOpen") return true; return getTodayStr() !== GM_getValue(LAST_AUTO_BACKUP_DATE_KEY, void 0); } function markAutoBackupDone() { GM_setValue(LAST_AUTO_BACKUP_DATE_KEY, getTodayStr()); } function getAutoBackupFileName() { return `auto_${getCredentialId()}.json`; } var require_react_production = __commonJSMin(((exports) => { var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"); var ReactNoopUpdateQueue = { isMounted: function() { return !1; }, enqueueForceUpdate: function() {}, enqueueReplaceState: function() {}, enqueueSetState: function() {} }; var assign = Object.assign; var emptyObject = {}; function Component(props, context, updater) { this.props = props; this.context = context; this.refs = emptyObject; this.updater = updater || ReactNoopUpdateQueue; } Component.prototype.isReactComponent = {}; Component.prototype.setState = function(partialState, callback) { if ("object" !== typeof partialState && "function" !== typeof partialState && null != partialState) throw Error("takes an object of state variables to update or a function which returns an object of state variables."); this.updater.enqueueSetState(this, partialState, callback, "setState"); }; Component.prototype.forceUpdate = function(callback) { this.updater.enqueueForceUpdate(this, callback, "forceUpdate"); }; function ComponentDummy() {} ComponentDummy.prototype = Component.prototype; function PureComponent(props, context, updater) { this.props = props; this.context = context; this.refs = emptyObject; this.updater = updater || ReactNoopUpdateQueue; } var pureComponentPrototype = PureComponent.prototype = new ComponentDummy(); pureComponentPrototype.constructor = PureComponent; assign(pureComponentPrototype, Component.prototype); pureComponentPrototype.isPureReactComponent = !0; Array.isArray; var hasOwnProperty = Object.prototype.hasOwnProperty; function ReactElement(type, key, props) { var refProp = props.ref; return { $$typeof: REACT_ELEMENT_TYPE, type, key, ref: void 0 !== refProp ? refProp : null, props }; } exports.createElement = function(type, config, children) { var propName, props = {}, key = null; if (null != config) for (propName in void 0 !== config.key && (key = "" + config.key), config) hasOwnProperty.call(config, propName) && "key" !== propName && "__self" !== propName && "__source" !== propName && (props[propName] = config[propName]); var childrenLength = arguments.length - 2; if (1 === childrenLength) props.children = children; else if (1 < childrenLength) { for (var childArray = Array(childrenLength), i = 0; i < childrenLength; i++) childArray[i] = arguments[i + 2]; props.children = childArray; } if (type && type.defaultProps) for (propName in childrenLength = type.defaultProps, childrenLength) void 0 === props[propName] && (props[propName] = childrenLength[propName]); return ReactElement(type, key, props); }; })); var import_react = __commonJSMin(((exports, module) => { module.exports = require_react_production(); }))(); function HelpDialog() { return (0, import_jsx_runtime.jsxs)("div", { className: "help-container", children: [ (0, import_jsx_runtime.jsx)("h1", { style: { fontSize: "22px", marginBottom: "20px", color: "#2c3e50", borderBottom: "1px solid #eee", paddingBottom: "10px" }, children: "使用说明" }), (0, import_jsx_runtime.jsxs)("details", { className: "help-section", children: [(0, import_jsx_runtime.jsx)("summary", { children: "1. 无法查看预览视频,提示分流?" }), (0, import_jsx_runtime.jsxs)("div", { className: "help-content", children: [ (0, import_jsx_runtime.jsx)("p", { children: "JavDB限制日本IP的访问,而预览视频来自DMM,需要日本IP才能访问。" }), (0, import_jsx_runtime.jsx)("p", { children: "这样会导致二者无法同时使用,需要对其一进行代理转发。" }), (0, import_jsx_runtime.jsx)("p", { children: "将 cc3001.dmm.co.jp 及 dmm.co 分流到日本ip。" }), (0, import_jsx_runtime.jsxs)("p", { children: [ (0, import_jsx_runtime.jsxs)("a", { href: "https://youtu.be/wQUK8z_YeU4?t=121", target: "_blank", children: ["Clash Verge分流规则设置", " "] }), " ", "(如果你是别的代理软件,自行搜索如何分流)" ] }) ] })] }), (0, import_jsx_runtime.jsxs)("details", { className: "help-section", children: [(0, import_jsx_runtime.jsx)("summary", { children: "2. 如何屏蔽某一系列的番号?" }), (0, import_jsx_runtime.jsx)("div", { className: "help-content", children: (0, import_jsx_runtime.jsx)("p", { children: "设置中-屏蔽配置-添加视频标题关键词,如: VENX-" }) })] }), (0, import_jsx_runtime.jsxs)("details", { className: "help-section", children: [(0, import_jsx_runtime.jsx)("summary", { children: "3. 屏蔽某演员,如何只屏蔽单体影片?" }), (0, import_jsx_runtime.jsxs)("div", { className: "help-content", children: [(0, import_jsx_runtime.jsx)("p", { children: "屏蔽演员前,先筛选分类,再点屏蔽" }), (0, import_jsx_runtime.jsx)("img", { src: "https://imgur.com/Ue7eCAi.png", alt: "屏蔽演员前,先筛选分类,再点屏蔽" })] })] }) ] }); } var help_dialog_default = ".help-container {\n font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n color: #333;\n padding: 15px;\n max-height: 100%;\n overflow-y: auto;\n}\n\n.help-section {\n margin-bottom: 25px;\n}\n\n.help-section summary {\n font-size: 18px;\n color: #3498db;\n margin-bottom: 12px;\n cursor: pointer;\n}\n\n.help-content {\n background-color: #f9f9f9;\n border-radius: 5px;\n padding: 15px;\n border-left: 4px solid #3498db;\n}\n\n.help-content p {\n line-height: 1.6;\n margin-bottom: 10px;\n}\n\n.help-section img {\n max-width: 100%;\n height: auto;\n border: 1px solid #ddd;\n border-radius: 4px;\n box-shadow: 0 2px 4px rgb(0, 0, 0, 0.1);\n}\n"; var DEFAULT_LIST_MERGE_PATTERN = "^(.+?)(\\d+)$"; function parseListName(name, pattern) { const trimmed = name.trim(); if (!trimmed) return null; const matched = pattern.exec(trimmed); if (!matched) return null; const base = (matched[1] ?? "").trim(); const numRaw = matched[2] ?? ""; if (!base || !numRaw) return null; if (!/[^\d]/.test(base)) return null; const num = Number(numRaw); if (!Number.isFinite(num)) return null; return { base, num }; } function compileMergePattern(source) { const text = (source ?? "").trim() || "^(.+?)(\\d+)$"; try { return new RegExp(text); } catch { return new RegExp(DEFAULT_LIST_MERGE_PATTERN); } } function isMergeExcluded(fullName, base, config) { if (config.excludeNames.size === 0) return false; if (config.excludeNames.has(fullName.trim().toLowerCase())) return true; if (base && config.excludeNames.has(base.trim().toLowerCase())) return true; return false; } function buildMergeConfig(settings) { const enabled = settings.listMergeEnabled !== "no"; const pattern = compileMergePattern(settings.listMergePattern); const rawNames = settings.listMergeExcludeNames; const nameList = Array.isArray(rawNames) ? rawNames : typeof rawNames === "string" ? rawNames.split(",") : []; return { enabled, pattern, excludeNames: new Set(nameList.map((n) => String(n).trim().toLowerCase()).filter(Boolean)) }; } function isMemberFull(member) { if (member.count !== null && member.count >= 501) return true; if (member.disabled && !member.checked) return true; return false; } function sortMembersByNum(members) { return [...members].sort((a, b) => { if (a.num !== b.num) return a.num - b.num; return a.listId < b.listId ? -1 : a.listId > b.listId ? 1 : 0; }); } function pickTargetMember(members) { for (const member of sortMembersByNum(members)) { if (member.checked) continue; if (isMemberFull(member)) continue; return member; } return null; } function nextListName(group) { const maxNum = group.members.reduce((max, m) => Math.max(max, m.num), 0); return `${group.base}${maxNum + 1}`; } function computeGroupCheckState(group) { const containing = group.members.filter((m) => m.checked); const checked = containing.length > 0; return { checked, indeterminate: checked && containing.length < group.members.length, containing }; } function groupEntries(items, config) { const groupMap = new Map(); const singleIndexes = []; if (!config.enabled) return { groups: [], singleIndexes: items.map((_, i) => i) }; const pendingSingles = []; items.forEach((item, index) => { const parsed = parseListName(item.name, config.pattern); if (!parsed || isMergeExcluded(item.name, parsed.base, config)) { pendingSingles.push(index); return; } let group = groupMap.get(parsed.base); if (!group) { group = { base: parsed.base, members: [] }; groupMap.set(parsed.base, group); } group.members.push({ listId: item.listId, name: item.name, num: parsed.num, count: item.count, checked: item.checked, disabled: item.disabled }); }); for (const index of pendingSingles) { const item = items[index]; const group = groupMap.get(item.name.trim()); if (group && !isMergeExcluded(item.name, null, config)) group.members.push({ listId: item.listId, name: item.name, num: 0, count: item.count, checked: item.checked, disabled: item.disabled }); else singleIndexes.push(index); } const collator = new Intl.Collator("zh-CN", { numeric: true, sensitivity: "base" }); const groups = Array.from(groupMap.values()).sort((a, b) => collator.compare(a.base, b.base)); singleIndexes.sort((a, b) => a - b); return { groups, singleIndexes }; } var GM_KEY_WEBDAV_URL = "jhs_webdav_url"; var GM_KEY_WEBDAV_USERNAME = "jhs_webdav_username"; var GM_KEY_WEBDAV_PASSWORD = "jhs_webdav_password"; var GM_KEY_BACKUP_PASSWORD = "jhs_backup_password"; var GM_KEY_GIT_API_URL = "jhs_git_api_url"; var GM_KEY_GIT_REPO = "jhs_git_repo"; var GM_KEY_GIT_BRANCH = "jhs_git_branch"; var GM_KEY_GIT_TOKEN = "jhs_git_token"; var PBKDF2_ITERATIONS = 1e5; var SALT_BYTES = 16; var IV_BYTES = 12; function base64ToBytes(b64) { const binary = atob(b64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); return bytes; } function bytesToBase64(bytes) { let binary = ""; for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); return btoa(binary); } async function deriveKey(password, salt) { const encoder = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(password), "PBKDF2", false, ["deriveKey"]); return crypto.subtle.deriveKey({ name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]); } async function encryptBackupV2(plaintext, password) { const encoder = new TextEncoder(); const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES)); const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES)); const key = await deriveKey(password, salt); const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoder.encode(plaintext)); const envelope = { v: 2, alg: "AES-GCM", kdf: "PBKDF2-SHA-256", iter: PBKDF2_ITERATIONS, salt: bytesToBase64(salt), iv: bytesToBase64(iv), ct: bytesToBase64(new Uint8Array(ciphertext)), ts: new Date().toISOString() }; return JSON.stringify(envelope); } async function decryptBackupV2(envelopeJson, password) { const envelope = JSON.parse(envelopeJson); if (envelope.v !== 2) throw new Error(`不支持的备份格式版本: ${envelope.v}`); const salt = base64ToBytes(envelope.salt); const iv = base64ToBytes(envelope.iv); const ct = base64ToBytes(envelope.ct); const key = await deriveKey(password, salt); const plainBuffer = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct); return new TextDecoder().decode(plainBuffer); } function isBackupV2(content) { try { const parsed = JSON.parse(content); return parsed && parsed.v === 2 && parsed.alg === "AES-GCM"; } catch { return false; } } var BACKUP_LOCAL_STORAGE_KEYS = [ "jhs_other_site", "jhs_other_site_dmm", "jhs_dmm_video", "jhs_translate", "jhs_actress_info", "jhs_score_info", "jhs_screenShot", "jhs_visit_history", "jdb:rating_cache_v2", "jhs_enabled_sites", "jhs_foldCategory", "jhs_sortMethod", "jhs_fancyboxThumbs", "jhs_upgrade_flags", "jhs_videoMuted", "jhs_magnetHub_selectedEngine", "jhs_appAuthorization" ]; var BACKUP_GM_KEYS = [ "jdb:list-reading-status", "jdb:list-rating", "jdb:list-last-uri", "jdb:list-sort", "jdb:list-filter-read", "jdb:list-filter-rating", "jdb:list-waterfall-enabled" ]; function collectLocalStorageBackup() { const out = {}; for (const key of BACKUP_LOCAL_STORAGE_KEYS) try { const v = localStorage.getItem(key); if (v != null && v !== "") out[key] = v; } catch {} return out; } function collectGmStorageBackup() { const out = {}; for (const key of BACKUP_GM_KEYS) try { const v = GM_getValue(key, void 0); if (v !== void 0) out[key] = v; } catch {} return out; } var ALLOWED_LOCALSTORAGE_KEYS = new Set(BACKUP_LOCAL_STORAGE_KEYS); var ALLOWED_GM_KEYS = new Set(BACKUP_GM_KEYS); function stripBackupExtras(data) { const ls = data.__localStorage; const gm = data.__gmStorage; delete data.__localStorage; delete data.__gmStorage; return { ls: ls && typeof ls === "object" ? ls : void 0, gm: gm && typeof gm === "object" ? gm : void 0 }; } function applyBackupExtras(extras) { if (extras.ls) for (const key of Object.keys(extras.ls)) { if (!ALLOWED_LOCALSTORAGE_KEYS.has(key)) { console.warn(`[JHS 备份] 跳过未授权的 localStorage 键: ${key}`); continue; } const raw = extras.ls[key]; if (raw == null || raw === "") continue; try { localStorage.setItem(key, typeof raw === "string" ? raw : JSON.stringify(raw)); } catch (err) { console.warn(`[JHS 备份] 恢复 localStorage.${key} 失败:`, err instanceof Error ? err.message : err); } } if (extras.gm) for (const key of Object.keys(extras.gm)) { if (!ALLOWED_GM_KEYS.has(key)) { console.warn(`[JHS 备份] 跳过未授权的 GM 键: ${key}`); continue; } try { GM_setValue(key, extras.gm[key]); } catch (err) { console.warn(`[JHS 备份] 恢复 GM.${key} 失败:`, err instanceof Error ? err.message : err); } } } var STATUS_LIST = [ "filter", "favorite", "hasDown", "hasWatch" ]; var STATUS_CONFIG = { favorite: { label: "⭐ 已收藏", color: "#25b1dc" }, hasWatch: { label: "🔍 已观看", color: "#d7a80c" }, filter: { label: "🚫 已屏蔽", color: "#de3333" }, hasDown: { label: "💾 已下载", color: "#1f7a3d" } }; var GM_KEY_CAR_STATUS_DATA = "jhs_car_status_data"; var GM_KEY_CAR_STATUS_DELTA = "jhs_car_status_delta"; var GM_KEY_CAR_STATUS_REVISION = "jhs_car_status_revision"; var MISSAV_DB_NAME = "MissAV-CarStatus"; var MISSAV_DB_STORE = "cars"; var MISSAV_DB_META_STORE = "sync_meta"; var MISSAV_SYNC_META_KEY = "car_status_revision"; var MISSAV_SYNC_LOCK_NAME = "jhs-missav-car-status-sync"; var LOG_PREFIX_JHS = "[JHS CarSync]"; var LOG_PREFIX_MISSAV = "[MissAV]"; function normalizeUrl(rawUrl) { let path; try { if (/^https?:\/\//.test(rawUrl)) { const u = new URL(rawUrl); path = u.pathname + (u.search || ""); } else if (rawUrl.startsWith("/")) path = rawUrl; else return { code: rawUrl, malformed: true }; } catch { return { code: rawUrl, malformed: true }; } if (path.startsWith("/v/")) return { code: path.slice(3), malformed: false }; return { code: path, malformed: false }; } function buildJavdbUrl(urlPath) { if (!urlPath) return "https://javdb.com"; if (urlPath.startsWith("/")) return "https://javdb.com" + urlPath; return "https://javdb.com/v/" + urlPath; } function toColumnar(carList) { const groups = Object.create(null); let dropped = 0; let malformed = 0; for (const item of carList) { if (!item || typeof item !== "object") { dropped++; continue; } if (!item.carNum || !item.url || !item.status) { dropped++; continue; } const status = item.status; if (!groups[status]) groups[status] = new Map(); const { code, malformed: bad } = normalizeUrl(item.url); if (bad) malformed++; groups[status].set(item.carNum, { url: code, updateDate: item.updateDate || "" }); } const byStatus = {}; for (const status of Object.keys(groups)) { const map = groups[status]; const carNums = []; const urls = []; const update_date = []; for (const [carNum, { url, updateDate }] of map) { carNums.push(carNum); urls.push(url); update_date.push(updateDate); } byStatus[status] = { carNums, urls, update_date }; } return { byStatus, dropped, malformed }; } function columnarToFlat(store) { const records = []; for (const status of Object.keys(store)) { const group = store[status]; if (!group || !group.carNums || !group.urls) continue; const { carNums, urls } = group; for (let i = 0; i < carNums.length; i++) records.push({ carNum: carNums[i], status, url_path: urls[i] }); } return records; } function countColumnar(store) { let n = 0; for (const status of Object.keys(store)) { const col = store[status]; if (col && col.carNums) n += col.carNums.length; } return n; } async function gzipToBase64(value) { try { if (typeof CompressionStream === "undefined") return null; const json = JSON.stringify(value); const blob = new Blob([json], { type: "application/json" }); const cs = new CompressionStream("gzip"); const stream = blob.stream().pipeThrough(cs); const buf = await new Response(stream).arrayBuffer(); const bytes = new Uint8Array(buf); let binary = ""; const chunkSize = 32768; for (let i = 0; i < bytes.length; i += chunkSize) binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize)); return btoa(binary); } catch { return null; } } async function gunzipFromBase64(base64) { try { if (typeof DecompressionStream === "undefined") return null; const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); const ds = new DecompressionStream("gzip"); const stream = new Blob([bytes]).stream().pipeThrough(ds); const text = await new Response(stream).text(); return JSON.parse(text); } catch { return null; } } var CONTROL_CHAR_REGEX$1 = new RegExp(`[\\u0000-\\u001f\\u007f]`); function isPlainRecord(value) { if (value === null || typeof value !== "object" || Array.isArray(value)) return false; const prototype = Object.getPrototypeOf(value); return prototype === Object.prototype || prototype === null; } function isStringArray(value) { return Array.isArray(value) && value.every((item) => typeof item === "string"); } function isSafeUrlPath(value) { const raw = value.trim(); if (!raw || CONTROL_CHAR_REGEX$1.test(raw)) return false; if (/^https?:\/\//i.test(raw) || raw.startsWith("/")) return !normalizeUrl(raw).malformed; return !raw.startsWith("//") && !/^[a-z][a-z\d+.-]*:/i.test(raw); } function validateColumnarSnapshot(store, expectedCount) { if (!Number.isInteger(expectedCount) || expectedCount < 0) return { ok: false, reason: "快照 count 必须是非负整数" }; if (!isPlainRecord(store)) return { ok: false, reason: "快照列存必须是普通对象" }; const validStatuses = new Set(STATUS_LIST); const seenCarNums = new Set(); const records = []; for (const [status, rawGroup] of Object.entries(store)) { if (!validStatuses.has(status)) return { ok: false, reason: `快照包含未知状态:${status}` }; if (!isPlainRecord(rawGroup)) return { ok: false, reason: `状态 ${status} 的列不是普通对象` }; const carNums = rawGroup.carNums; const urls = rawGroup.urls; const updateDates = rawGroup.update_date; if (!isStringArray(carNums) || !isStringArray(urls)) return { ok: false, reason: `状态 ${status} 的 carNums/urls 必须是字符串数组` }; if (carNums.length !== urls.length) return { ok: false, reason: `状态 ${status} 的 carNums/urls 长度不一致` }; if (updateDates !== void 0 && (!isStringArray(updateDates) || updateDates.length !== carNums.length)) return { ok: false, reason: `状态 ${status} 的 update_date 长度或类型无效` }; for (let index = 0; index < carNums.length; index++) { const carNum = carNums[index].trim(); const urlPath = urls[index].trim(); if (!carNum || !urlPath) return { ok: false, reason: `状态 ${status} 包含空番号或空路径` }; if (!isSafeUrlPath(urlPath)) return { ok: false, reason: `状态 ${status} 包含不安全链接` }; const normalizedCarNum = carNum.toUpperCase(); if (seenCarNums.has(normalizedCarNum)) return { ok: false, reason: `快照包含重复番号:${carNum}` }; seenCarNums.add(normalizedCarNum); records.push({ carNum, status, url_path: urlPath }); } } const count = expectedCount; if (records.length !== count) return { ok: false, reason: `快照记录数不一致:声明 ${count},实际 ${records.length}` }; return { ok: true, records, count }; } async function withExclusiveLock(lockName, task) { const browserNavigator = typeof navigator === "undefined" ? void 0 : navigator; if (!browserNavigator?.locks) return task(); return browserNavigator.locks.request(lockName, { mode: "exclusive" }, task); } var KeyedQueue = class { queues = new Map(); enqueue(key, task) { const prev = this.queues.get(key) ?? Promise.resolve(); let resolveTail; const tail = new Promise((resolve) => { resolveTail = resolve; }); this.queues.set(key, tail); const result = prev.then(task, task); result.then(() => resolveTail(), () => resolveTail()); tail.then(() => { if (this.queues.get(key) === tail) this.queues.delete(key); }); return result; } }; var INTENT_PREFIX = "jhs:car-sync-intent:"; function writeIntent(revision, kind) { try { localStorage.setItem(INTENT_PREFIX + revision, JSON.stringify({ type: kind, revision, ts: Date.now() })); } catch {} } function clearIntent(revision) { try { localStorage.removeItem(INTENT_PREFIX + revision); } catch {} } function readPendingIntents() { const intents = []; try { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key?.startsWith(INTENT_PREFIX)) try { const value = JSON.parse(localStorage.getItem(key) || ""); if (value && typeof value.revision === "number") intents.push(value); } catch {} } } catch {} return intents.sort((a, b) => a.ts - b.ts); } function cleanStaleIntents(maxAgeMs = 864e5) { const cutoff = Date.now() - maxAgeMs; try { const staleKeys = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key?.startsWith(INTENT_PREFIX)) try { const value = JSON.parse(localStorage.getItem(key) || ""); if (value && typeof value.ts === "number" && value.ts < cutoff) staleKeys.push(key); } catch { staleKeys.push(key); } } staleKeys.forEach((k) => localStorage.removeItem(k)); } catch {} } function openLocalDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(MISSAV_DB_NAME, 2); request.onupgradeneeded = (event) => { const db = event.target.result; const oldVersion = event.oldVersion; if (!db.objectStoreNames.contains("cars")) db.createObjectStore(MISSAV_DB_STORE, { keyPath: "carNum" }); if (oldVersion < 2 && !db.objectStoreNames.contains("sync_meta")) db.createObjectStore(MISSAV_DB_META_STORE, { keyPath: "key" }); }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); request.onblocked = () => reject(new Error("数据库被阻塞,请关闭其他标签页后刷新")); }); } async function queryLocalCars(carNums) { if (!carNums || carNums.length === 0) return new Map(); const db = await openLocalDB(); try { return await new Promise((resolve) => { const store = db.transaction(MISSAV_DB_STORE, "readonly").objectStore(MISSAV_DB_STORE); const result = new Map(); let pending = carNums.length; if (pending === 0) { resolve(result); return; } for (const cn of carNums) { const req = store.get(cn); req.onsuccess = () => { if (req.result) result.set(cn, { status: req.result.status, url: req.result.url }); pending--; if (pending === 0) resolve(result); }; req.onerror = () => { pending--; if (pending === 0) resolve(result); }; } }); } finally { db.close(); } } async function deleteLocalCars(carNums) { if (!carNums || carNums.length === 0) return; const db = await openLocalDB(); try { await new Promise((resolve) => { const tx = db.transaction(MISSAV_DB_STORE, "readwrite"); const store = tx.objectStore(MISSAV_DB_STORE); for (const cn of carNums) store.delete(cn); tx.oncomplete = () => resolve(); tx.onerror = () => resolve(); }); } finally { db.close(); } } async function exportLocalDB() { const db = await openLocalDB(); try { return await new Promise((resolve, reject) => { const store = db.transaction(MISSAV_DB_STORE, "readonly").objectStore(MISSAV_DB_STORE); const result = []; const req = store.openCursor(); req.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { result.push({ carNum: cursor.value.carNum, status: cursor.value.status, url: cursor.value.url }); cursor.continue(); } else resolve(result); }; req.onerror = () => reject(req.error); }); } finally { db.close(); } } var CONTROL_CHAR_REGEX = new RegExp(`[\\u0000-\\u001f\\u007f]`); var JAVDB_BASE_URL = "https://javdb.com"; var JAVDB_HOSTNAME = "javdb.com"; function normalizeJavdbUrlPath(value) { if (typeof value !== "string") throw new Error("车辆记录链接必须是字符串"); const raw = value.trim(); if (!raw || CONTROL_CHAR_REGEX.test(raw)) throw new Error("车辆记录链接为空或包含控制字符"); let candidate; try { if (/^https?:\/\//i.test(raw)) candidate = new URL(raw); else if (raw.startsWith("/")) { if (raw.startsWith("//")) throw new Error("不允许协议相对链接"); candidate = new URL(raw, JAVDB_BASE_URL); } else { if (/^[a-z][a-z\d+.-]*:/i.test(raw)) throw new Error("不允许非 HTTPS 链接"); candidate = new URL(`/v/${raw}`, JAVDB_BASE_URL); if (!candidate.pathname.startsWith("/v/")) throw new Error("车辆短码路径无效"); } } catch (error) { throw new Error(`车辆记录链接无效:${error instanceof Error ? error.message : String(error)}`); } if (candidate.protocol !== "https:" || candidate.hostname !== JAVDB_HOSTNAME) throw new Error("车辆记录链接必须指向 https://javdb.com"); if (candidate.pathname === "/") throw new Error("车辆记录链接路径为空"); const suffix = candidate.pathname.startsWith("/v/") ? candidate.pathname.slice(3) : candidate.pathname; if (!suffix) throw new Error("车辆记录链接路径为空"); return `${suffix}${candidate.search}${candidate.hash}`; } function normalizeLocalRecord(record) { if (record === null || typeof record !== "object") throw new Error("车辆记录格式无效"); if (typeof record.carNum !== "string" || record.carNum.trim() === "") throw new Error("车辆记录番号为空"); if (typeof record.status !== "string" || !STATUS_LIST.includes(record.status.trim())) throw new Error(`车辆记录状态无效:${String(record.status)}`); return { carNum: record.carNum.trim().toUpperCase(), status: record.status.trim(), url_path: normalizeJavdbUrlPath(record.url_path) }; } function canApplyRevision(incomingRevision, incomingKind, current) { if (!Number.isInteger(incomingRevision) || incomingRevision < 0) return false; if (!current) return true; if (incomingRevision < current.revision) return false; if (incomingRevision === current.revision && incomingKind === "full") return false; return true; } async function readAppliedRevision() { const db = await openLocalDB(); try { return await new Promise((resolve, reject) => { const tx = db.transaction(MISSAV_DB_META_STORE, "readonly"); const request = tx.objectStore(MISSAV_DB_META_STORE).get(MISSAV_SYNC_META_KEY); request.onsuccess = () => { const value = request.result; if (value && value.key === "car_status_revision" && typeof value.revision === "number" && Number.isInteger(value.revision) && value.revision >= 0 && (value.kind === "full" || value.kind === "delta")) resolve(value); else resolve(null); }; request.onerror = () => reject(request.error || new Error("读取同步修订号失败")); tx.onerror = () => reject(tx.error || new Error("读取同步元数据事务失败")); }); } finally { db.close(); } } async function writeAppliedRevision(meta) { const db = await openLocalDB(); try { await new Promise((resolve, reject) => { const tx = db.transaction(MISSAV_DB_META_STORE, "readwrite"); tx.objectStore(MISSAV_DB_META_STORE).put(meta); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error || new Error("写入同步修订号失败")); tx.onabort = () => reject(tx.error || new Error("写入同步修订号事务中止")); }); } finally { db.close(); } } async function clearAppliedRevision() { const db = await openLocalDB(); try { await new Promise((resolve, reject) => { const tx = db.transaction(MISSAV_DB_META_STORE, "readwrite"); tx.objectStore(MISSAV_DB_META_STORE).delete(MISSAV_SYNC_META_KEY); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error || new Error("清理同步修订号失败")); tx.onabort = () => reject(tx.error || new Error("清理同步修订号事务中止")); }); } finally { db.close(); } } async function withSyncRevision(revision, kind, task, rollback) { return withExclusiveLock(MISSAV_SYNC_LOCK_NAME, async () => { const current = await readAppliedRevision(); if (!canApplyRevision(revision, kind, current)) return { applied: false }; let metadataWriteStarted = false; try { const value = await task(); metadataWriteStarted = true; await writeAppliedRevision({ key: MISSAV_SYNC_META_KEY, revision, kind }); return { applied: true, value }; } catch (applyError) { const recoveryErrors = []; if (rollback) try { await rollback(); } catch (rollbackError) { recoveryErrors.push(`数据回滚失败:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`); } if (metadataWriteStarted) try { if (current) await writeAppliedRevision(current); else await clearAppliedRevision(); } catch (metadataRollbackError) { recoveryErrors.push(`修订号回滚失败:${metadataRollbackError instanceof Error ? metadataRollbackError.message : String(metadataRollbackError)}`); } if (recoveryErrors.length > 0) { const applyMessage = applyError instanceof Error ? applyError.message : String(applyError); throw new Error(`同步提交失败(${applyMessage});${recoveryErrors.join(";")}`); } throw applyError; } }); } async function upsertLocalCars(records) { const BATCH_SIZE = 3e3; const normalizedRecords = records.map(normalizeLocalRecord); let totalWritten = 0; const db = await openLocalDB(); try { for (let i = 0; i < normalizedRecords.length; i += BATCH_SIZE) { const batch = normalizedRecords.slice(i, i + BATCH_SIZE); totalWritten += await new Promise((resolveBatch, rejectBatch) => { const tx = db.transaction(MISSAV_DB_STORE, "readwrite"); const store = tx.objectStore(MISSAV_DB_STORE); let count = 0; for (const rec of batch) { const req = store.put({ carNum: rec.carNum, status: rec.status, url: buildJavdbUrl(rec.url_path), updateDate: "" }); req.onsuccess = () => count++; req.onerror = () => console.warn("[MissAV] 写入失败", rec.carNum, req.error); } tx.oncomplete = () => resolveBatch(count); tx.onerror = () => rejectBatch(tx.error || new Error("事务失败")); tx.onabort = () => rejectBatch(tx.error || new Error("事务中止")); }); } return totalWritten; } finally { db.close(); } } async function readLocalCarsSnapshot() { const db = await openLocalDB(); try { return await new Promise((resolve, reject) => { const tx = db.transaction(MISSAV_DB_STORE, "readonly"); const store = tx.objectStore(MISSAV_DB_STORE); const records = []; const request = store.openCursor(); request.onsuccess = () => { const cursor = request.result; if (!cursor) return; records.push(cursor.value); cursor.continue(); }; request.onerror = () => reject(request.error || new Error("读取本地状态快照失败")); tx.oncomplete = () => resolve(records); tx.onerror = () => reject(tx.error || new Error("读取本地状态快照事务失败")); tx.onabort = () => reject(tx.error || new Error("读取本地状态快照事务中止")); }); } finally { db.close(); } } async function restoreLocalCarsSnapshot(snapshot) { const snapshotKeys = new Set(snapshot.map((record) => String(record.carNum))); const db = await openLocalDB(); try { await new Promise((resolve, reject) => { const tx = db.transaction(MISSAV_DB_STORE, "readwrite"); const store = tx.objectStore(MISSAV_DB_STORE); const request = store.openCursor(); request.onsuccess = () => { const cursor = request.result; if (cursor) { if (!snapshotKeys.has(String(cursor.primaryKey))) cursor.delete(); cursor.continue(); return; } for (const record of snapshot) store.put(record); }; request.onerror = () => reject(request.error || new Error("恢复本地状态游标失败")); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error || new Error("恢复本地状态事务失败")); tx.onabort = () => reject(tx.error || new Error("恢复本地状态事务中止")); }); } finally { db.close(); } } async function readLocalCarsSelection(carNums) { const normalizedCarNums = Array.from(new Set(carNums.map((carNum) => carNum.trim().toUpperCase()).filter(Boolean))); if (normalizedCarNums.length === 0) return { carNums: [], records: [] }; const db = await openLocalDB(); try { return { carNums: normalizedCarNums, records: await new Promise((resolve, reject) => { const tx = db.transaction(MISSAV_DB_STORE, "readonly"); const store = tx.objectStore(MISSAV_DB_STORE); const snapshot = []; let settled = false; const rejectOnce = (error) => { if (settled) return; settled = true; reject(error); }; for (const carNum of normalizedCarNums) { const request = store.get(carNum); request.onsuccess = () => { if (request.result) snapshot.push(request.result); }; request.onerror = () => rejectOnce(request.error || new Error(`读取增量回滚快照失败:${carNum}`)); } tx.oncomplete = () => { if (settled) return; settled = true; resolve(snapshot); }; tx.onerror = () => rejectOnce(tx.error || new Error("读取增量回滚快照事务失败")); tx.onabort = () => rejectOnce(tx.error || new Error("读取增量回滚快照事务中止")); }) }; } finally { db.close(); } } async function restoreLocalCarsSelection(snapshot) { if (snapshot.carNums.length === 0) return; const db = await openLocalDB(); try { await new Promise((resolve, reject) => { const tx = db.transaction(MISSAV_DB_STORE, "readwrite"); const store = tx.objectStore(MISSAV_DB_STORE); for (const carNum of snapshot.carNums) store.delete(carNum); for (const record of snapshot.records) store.put(record); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error || new Error("恢复增量回滚快照事务失败")); tx.onabort = () => reject(tx.error || new Error("恢复增量回滚快照事务中止")); }); } finally { db.close(); } } async function replaceLocalCars(records, previousSnapshot) { const recordsByCarNum = new Map(); for (const record of records) { const normalized = normalizeLocalRecord(record); if (recordsByCarNum.has(normalized.carNum)) throw new Error(`快照包含重复番号:${normalized.carNum}`); recordsByCarNum.set(normalized.carNum, normalized); } const validRecords = Array.from(recordsByCarNum.values()); const rollbackSnapshot = previousSnapshot ?? await readLocalCarsSnapshot(); try { const written = await upsertLocalCars(validRecords); if (written !== validRecords.length) throw new Error(`快照写入条数不一致:声明 ${validRecords.length},实际 ${written},已停止删除旧数据`); const snapshotKeys = new Set(recordsByCarNum.keys()); const db = await openLocalDB(); let deleted = 0; try { deleted = await new Promise((resolve, reject) => { const tx = db.transaction(MISSAV_DB_STORE, "readwrite"); const request = tx.objectStore(MISSAV_DB_STORE).openCursor(); let deletedCount = 0; request.onsuccess = () => { const cursor = request.result; if (!cursor) return; const rawCarNum = String(cursor.primaryKey); const carNum = rawCarNum.toUpperCase(); if (!snapshotKeys.has(carNum) || rawCarNum !== carNum) { cursor.delete(); deletedCount++; } cursor.continue(); }; request.onerror = () => reject(request.error || new Error("快照对账游标读取失败")); tx.oncomplete = () => resolve(deletedCount); tx.onerror = () => reject(tx.error || new Error("快照对账事务失败")); tx.onabort = () => reject(tx.error || new Error("快照对账事务中止")); }); } finally { db.close(); } return { written, deleted }; } catch (replaceError) { try { await restoreLocalCarsSnapshot(rollbackSnapshot); } catch (rollbackError) { const replaceMessage = replaceError instanceof Error ? replaceError.message : String(replaceError); const rollbackMessage = rollbackError instanceof Error ? rollbackError.message : String(rollbackError); throw new Error(`状态快照对账失败且回滚失败(对账错误:${replaceMessage};回滚错误:${rollbackMessage})`); } throw replaceError; } } async function importLocalDB(records) { return withExclusiveLock(MISSAV_SYNC_LOCK_NAME, async () => { if (!Array.isArray(records)) throw new Error("导入记录必须是数组"); const normalizedRecords = records.map((record) => normalizeLocalRecord({ carNum: record?.carNum, status: record?.status, url_path: record?.url })); const [previousCars, previousRevision] = await Promise.all([readLocalCarsSnapshot(), readAppliedRevision()]); let replaced = false; try { const { written } = await replaceLocalCars(normalizedRecords, previousCars); replaced = true; await clearAppliedRevision(); return written; } catch (importError) { if (!replaced) throw importError; const recoveryErrors = []; try { await restoreLocalCarsSnapshot(previousCars); } catch (rollbackError) { recoveryErrors.push(`状态数据回滚失败:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`); } try { if (previousRevision) await writeAppliedRevision(previousRevision); else await clearAppliedRevision(); } catch (metadataRollbackError) { recoveryErrors.push(`修订号回滚失败:${metadataRollbackError instanceof Error ? metadataRollbackError.message : String(metadataRollbackError)}`); } if (recoveryErrors.length > 0) { const importMessage = importError instanceof Error ? importError.message : String(importError); throw new Error(`导入失败(${importMessage});${recoveryErrors.join(";")}`); } throw importError; } }); } var DB_NAME = "JAV-JHS"; var STORE_NAME = "appData"; var MAX_COUNT = 501; var BOOTSTRAP_COLORS$1 = [ { name: "primary", bg: "#0d6efd", text: "#fff" }, { name: "secondary", bg: "#6c757d", text: "#fff" }, { name: "success", bg: "#198754", text: "#fff" }, { name: "danger", bg: "#dc3545", text: "#fff" }, { name: "warning", bg: "#ffc107", text: "#212529" }, { name: "info", bg: "#0dcaf0", text: "#212529" }, { name: "dark", bg: "#212529", text: "#fff" } ]; var KEYS = { MOVIES: "vlt_movies", INVENTORY: "vlt_inventory", MOVIE_INVENTORY: "vlt_movie_inventory", META: "vlt_meta" }; function randomBootstrapStyle() { return BOOTSTRAP_COLORS$1[Math.floor(Math.random() * BOOTSTRAP_COLORS$1.length)]; } function openDb() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(STORE_NAME)) db.createObjectStore(STORE_NAME); }; }); } function requestResult(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } function transactionDone(transaction) { return new Promise((resolve, reject) => { transaction.oncomplete = () => resolve(); transaction.onabort = () => reject(transaction.error || new Error("IndexedDB 事务已中止")); transaction.onerror = () => reject(transaction.error || new Error("IndexedDB 事务失败")); }); } function normalizeMeta(value) { return { ...value, epoch: Number.isFinite(Number(value?.epoch)) ? Number(value?.epoch) : 0, listRevisions: value?.listRevisions ? { ...value.listRevisions } : {} }; } function associationDesignations(movieInventory, listId) { const suffix = `::${listId}`; return Object.keys(movieInventory).filter((key) => movieInventory[key] && key.endsWith(suffix)).map((key) => key.slice(0, -suffix.length)); } function listFingerprint(movieInventory, listId) { return associationDesignations(movieInventory, listId).sort().join(""); } function bumpListRevision(meta, listId) { meta.listRevisions[listId] = (meta.listRevisions[listId] || 0) + 1; } async function withState(mode, callback) { const db = await openDb(); const transaction = db.transaction(STORE_NAME, mode); const done = transactionDone(transaction); const store = transaction.objectStore(STORE_NAME); try { const [moviesValue, inventoryValue, movieInventoryValue, metaValue] = await Promise.all([ requestResult(store.get(KEYS.MOVIES)), requestResult(store.get(KEYS.INVENTORY)), requestResult(store.get(KEYS.MOVIE_INVENTORY)), requestResult(store.get(KEYS.META)) ]); const state = { movies: moviesValue || {}, inventory: inventoryValue || {}, movieInventory: movieInventoryValue || {}, meta: normalizeMeta(metaValue) }; const result = callback(state); if (mode === "readwrite") { store.put(state.movies, KEYS.MOVIES); store.put(state.inventory, KEYS.INVENTORY); store.put(state.movieInventory, KEYS.MOVIE_INVENTORY); store.put(state.meta, KEYS.META); } await done; return result; } catch (error) { try { transaction.abort(); } catch {} await done.catch(() => {}); throw error; } finally { db.close(); } } function validateSnapshot(snapshot) { if (!snapshot.listId || !snapshot.name || !snapshot.url) throw new Error("服务端清单快照缺少 listId/name/url"); if (!Number.isInteger(snapshot.expectedCount) || snapshot.expectedCount < 0) throw new Error("服务端清单快照数量无效"); if (snapshot.movies.length !== snapshot.expectedCount) throw new Error(`服务端清单快照不完整:期望 ${snapshot.expectedCount},实际 ${snapshot.movies.length}`); const designations = new Set(); for (const movie of snapshot.movies) { if (!movie.designation || !movie.href) throw new Error("服务端清单快照包含空番号或空链接"); if (designations.has(movie.designation)) throw new Error(`服务端清单快照包含重复番号:${movie.designation}`); designations.add(movie.designation); } } async function sync(designation, listId, movie, list, action, options = {}) { return withState("readwrite", (state) => { const { movies, inventory, movieInventory, meta } = state; const movieExisted = !!movies[designation]; const score = movie.score != null ? Number(movie.score) : null; const previousMovie = movies[designation]; movies[designation] = { designation, href: movie.href, title: movie.title ?? null, coverSrc: movie.cover_src ?? null, score: Number.isFinite(score) ? score : null, releaseDate: movie.release_date ?? null, createdAt: previousMovie?.createdAt ?? new Date().toISOString(), series: movie.series ?? null, code: movie.code ?? null }; const listExisted = !!inventory[listId]; if (!listExisted) inventory[listId] = { listId, name: list.name, url: list.url ?? null, count: 0, style: randomBootstrapStyle() }; const assocKey = `${designation}::${listId}`; const actualCount = associationDesignations(movieInventory, listId).length; inventory[listId].count = actualCount; let association; if (action === "add") { if (movieInventory[assocKey]) association = "existed"; else if (actualCount >= MAX_COUNT && !options.serverConfirmed) association = "limit_exceeded"; else { movieInventory[assocKey] = true; inventory[listId].count = actualCount + 1; association = "created"; } } else if (movieInventory[assocKey]) { delete movieInventory[assocKey]; inventory[listId].count = Math.max(0, actualCount - 1); association = "deleted"; } else association = "unchanged"; bumpListRevision(meta, listId); return { movie: movieExisted ? "existed" : "created", list: listExisted ? "existed" : "created", association }; }); } async function queryMoviesLists(designations) { return withState("readonly", (state) => { const result = {}; for (const designation of designations) result[designation] = []; for (const key of Object.keys(state.movieInventory)) { if (!state.movieInventory[key]) continue; const separator = key.indexOf("::"); if (separator < 0) continue; const designation = key.slice(0, separator); if (result[designation] === void 0) continue; const listId = key.slice(separator + 2); const list = state.inventory[listId]; if (!list) continue; result[designation].push({ name: list.name, url: list.url, style: list.style }); } return result; }); } async function check(designation, listId) { return withState("readonly", (state) => ({ movie: !!state.movies[designation], inventory: !!state.inventory[listId], movieInventory: !!state.movieInventory[`${designation}::${listId}`] })); } async function getAllInventory() { return withState("readonly", (state) => state.inventory); } async function deleteList(listId) { return withState("readwrite", (state) => { const inventoryDeleted = !!state.inventory[listId]; delete state.inventory[listId]; const suffix = `::${listId}`; let associations = 0; for (const key of Object.keys(state.movieInventory)) { if (!key.endsWith(suffix)) continue; delete state.movieInventory[key]; associations++; } bumpListRevision(state.meta, listId); return { inventory: inventoryDeleted, associations }; }); } async function renameList(listId, newName) { return withState("readwrite", (state) => { if (!state.inventory[listId]) return false; state.inventory[listId].name = newName; bumpListRevision(state.meta, listId); return true; }); } async function getListReconcileState(listId, designation) { return withState("readonly", (state) => ({ inventory: state.inventory[listId] || null, actualCount: associationDesignations(state.movieInventory, listId).length, hasDesignation: designation === void 0 ? null : !!state.movieInventory[`${designation}::${listId}`], guard: { epoch: state.meta.epoch, revision: state.meta.listRevisions[listId] || 0, fingerprint: listFingerprint(state.movieInventory, listId) } })); } async function getListAssociationCounts() { return withState("readonly", (state) => { const counts = {}; for (const listId of Object.keys(state.inventory)) counts[listId] = 0; for (const key of Object.keys(state.movieInventory)) { if (!state.movieInventory[key]) continue; const separator = key.indexOf("::"); if (separator < 0) continue; const listId = key.slice(separator + 2); counts[listId] = (counts[listId] || 0) + 1; } return counts; }); } async function reconcileListSnapshot(snapshot, guard) { validateSnapshot(snapshot); return withState("readwrite", (state) => { const currentGuard = { epoch: state.meta.epoch, revision: state.meta.listRevisions[snapshot.listId] || 0, fingerprint: listFingerprint(state.movieInventory, snapshot.listId) }; if (currentGuard.epoch !== guard.epoch || currentGuard.revision !== guard.revision || currentGuard.fingerprint !== guard.fingerprint) return { status: "conflict" }; const previous = new Set(associationDesignations(state.movieInventory, snapshot.listId)); const incoming = new Set(snapshot.movies.map((movie) => movie.designation)); const now = new Date().toISOString(); for (const movie of snapshot.movies) { const existing = state.movies[movie.designation]; state.movies[movie.designation] = existing ? { ...existing, href: movie.href || existing.href, title: movie.title ?? existing.title, coverSrc: movie.coverSrc ?? existing.coverSrc, score: movie.score ?? existing.score, releaseDate: movie.releaseDate ?? existing.releaseDate, series: movie.series ?? existing.series, code: movie.code ?? existing.code, createdAt: existing.createdAt ?? movie.createdAt ?? now } : { ...movie, createdAt: movie.createdAt ?? now }; } const suffix = `::${snapshot.listId}`; for (const key of Object.keys(state.movieInventory)) if (key.endsWith(suffix)) delete state.movieInventory[key]; for (const designation of incoming) state.movieInventory[`${designation}::${snapshot.listId}`] = true; const existingList = state.inventory[snapshot.listId]; state.inventory[snapshot.listId] = { listId: snapshot.listId, name: snapshot.name, url: snapshot.url, count: snapshot.expectedCount, style: existingList?.style ?? randomBootstrapStyle() }; bumpListRevision(state.meta, snapshot.listId); let added = 0; let removed = 0; for (const designation of incoming) if (!previous.has(designation)) added++; for (const designation of previous) if (!incoming.has(designation)) removed++; return { status: "applied", added, removed, count: snapshot.expectedCount }; }); } async function importData$1(data) { return withState("readwrite", (state) => { const nextEpoch = state.meta.epoch + 1; state.movies = data.movies; state.inventory = data.inventory; state.movieInventory = data.movieInventory; state.meta = { version: data._version, exportedAt: data._exportedAt, importedAt: new Date().toISOString(), source: data._source, epoch: nextEpoch, listRevisions: {} }; return { movies: Object.keys(data.movies).length, inventory: Object.keys(data.inventory).length, movieInventory: Object.keys(data.movieInventory).length }; }); } async function isImported() { return withState("readonly", (state) => !!state.meta.importedAt); } async function exportData$1() { return withState("readonly", (state) => ({ _version: state.meta.version ?? 1, _exportedAt: state.meta.exportedAt ?? new Date().toISOString(), _source: "IndexedDB (local export)", movies: state.movies, inventory: state.inventory, movieInventory: state.movieInventory })); } var VltDb = class { static sync = sync; static queryMoviesLists = queryMoviesLists; static check = check; static getListReconcileState = getListReconcileState; static getListAssociationCounts = getListAssociationCounts; static reconcileListSnapshot = reconcileListSnapshot; static importData = importData$1; static isImported = isImported; static getAllInventory = getAllInventory; static deleteList = deleteList; static renameList = renameList; static exportData = exportData$1; }; var TOAST_SVG_ICONS = { success: "<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>", error: "<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/></svg>", warning: "<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"17.01\"/></svg>", info: "<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"12\" y1=\"16\" x2=\"12\" y2=\"12\"/><line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"8.01\"/></svg>" }; function VltToastContent({ type, msg }) { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [(0, import_jsx_runtime.jsx)("span", { className: "jdb-toast__icon", dangerouslySetInnerHTML: { __html: TOAST_SVG_ICONS[type] } }), (0, import_jsx_runtime.jsx)("span", { className: "jdb-toast__msg", children: msg })] }); } var queue$1 = { MAX_VISIBLE: 2, SIMULTANEOUS_WINDOW: 500, EXTRA_DURATION: 1e3, visible: [], overflow: [], lastAddTime: 0 }; function getToastContainer() { let el = document.getElementById("jdb-toast-container"); if (!el) { el = document.createElement("div"); el.id = "jdb-toast-container"; document.body.appendChild(el); } return el; } function promoteOverflow() { if (queue$1.overflow.length === 0 || queue$1.visible.length >= queue$1.MAX_VISIBLE) return; const stacked = queue$1.visible.find((v) => v.el.classList.contains("jdb-toast--stacked")); if (stacked) stacked.el.classList.remove("jdb-toast--stacked"); const next = queue$1.overflow.shift(); getToastContainer().appendChild(next.el); next.el.offsetHeight; const now = Date.now(); const isSimultaneous = now - queue$1.lastAddTime < queue$1.SIMULTANEOUS_WINDOW; let adjustedDuration = next.duration; if (isSimultaneous) adjustedDuration += queue$1.EXTRA_DURATION; queue$1.lastAddTime = now; queue$1.visible.push({ el: next.el, timerId: setTimeout(() => dismissToast(next.el), adjustedDuration), createdAt: now }); } function dismissToast(el) { const idx = queue$1.visible.findIndex((v) => v.el === el); if (idx < 0) return; const onTransitionEnd = () => { el.removeEventListener("transitionend", onTransitionEnd); el.remove(); const i = queue$1.visible.findIndex((v) => v.el === el); if (i >= 0) queue$1.visible.splice(i, 1); promoteOverflow(); }; el.classList.add("removing"); el.addEventListener("transitionend", onTransitionEnd); setTimeout(() => { if (el.parentNode) { el.remove(); const i = queue$1.visible.findIndex((v) => v.el === el); if (i >= 0) queue$1.visible.splice(i, 1); promoteOverflow(); } }, 300); if (idx >= 0) clearTimeout(queue$1.visible[idx].timerId); } function showToast$1(msg, type = "info", duration = 2500) { const container = getToastContainer(); const toast = document.createElement("div"); toast.className = `jdb-toast jdb-toast--${type}`; toast.innerHTML = jsxToString((0, import_jsx_runtime.jsx)(VltToastContent, { type, msg })); toast.addEventListener("click", () => dismissToast(toast)); if (queue$1.visible.length < queue$1.MAX_VISIBLE) { const now = Date.now(); const isSimultaneous = now - queue$1.lastAddTime < queue$1.SIMULTANEOUS_WINDOW; let adjustedDuration = duration; if (isSimultaneous) adjustedDuration += queue$1.EXTRA_DURATION; queue$1.lastAddTime = now; container.appendChild(toast); queue$1.visible.push({ el: toast, timerId: setTimeout(() => dismissToast(toast), adjustedDuration), createdAt: now }); } else { toast.classList.add("jdb-toast--stacked"); toast.addEventListener("click", (e) => { e.stopPropagation(); if (queue$1.overflow.find((o) => o.el === toast)) { const i = queue$1.overflow.findIndex((o) => o.el === toast); queue$1.overflow.splice(i, 1); promoteOverflow(); } }); queue$1.overflow.push({ el: toast, msg, type, duration }); } } function downloadJson(content, filename) { const blob = new Blob([content], { type: "application/json" }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = filename; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); URL.revokeObjectURL(url); } async function buildBackupPayload(settings) { const exportData = await storageManager.exportData(); exportData.__meta = { credentialId: getCredentialId(), autoBackupConfig: settings.autoBackupConfig || DEFAULT_AUTO_BACKUP_CONFIG, backupTime: utils.getNowStr("_", "_") }; exportData.__localStorage = collectLocalStorageBackup(); exportData.__gmStorage = collectGmStorageBackup(); return exportData; } function importData(_plugin) { try { const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = ".json"; fileInput.onchange = (event) => { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (loadEvent) => { try { let fileContent = String(loadEvent.target?.result ?? ""); if (isBackupV2(fileContent)) { const backupPassword = GM_getValue(GM_KEY_BACKUP_PASSWORD, ""); if (!backupPassword) { show.error("该备份使用 V2 加密,请先在备份面板设置备份口令"); return; } try { fileContent = await decryptBackupV2(fileContent, backupPassword); } catch { show.error("备份口令错误或文件已损坏"); return; } } const parsedData = JSON.parse(fileContent); layer.confirm("确定是否要覆盖导入?", { icon: 3, title: "确认覆盖", btn: ["确定", "取消"] }, async (layerIndex) => { const extras = stripBackupExtras(parsedData); await storageManager.importData(parsedData); applyBackupExtras(extras); show.ok("数据导入成功"); layer.close(layerIndex); location.reload(); }); } catch (err) { console.error(err); show.error("导入失败:文件内容不是有效的JSON格式 " + (err instanceof Error ? err.message : String(err))); } }; reader.onerror = () => { show.error("读取文件时出错"); }; reader.readAsText(file); }; document.body.appendChild(fileInput); fileInput.click(); setTimeout(() => document.body.removeChild(fileInput), 1e3); } catch (err) { console.error(err); show.error("导入数据时出错: " + (err instanceof Error ? err.message : String(err))); } } async function exportData(_plugin) { try { const settings = await storageManager.getSetting(); const backupPassword = GM_getValue(GM_KEY_BACKUP_PASSWORD, ""); let exportText; if (backupPassword) exportText = await encryptBackupV2(JSON.stringify(await buildBackupPayload(settings)), backupPassword); else exportText = JSON.stringify(await buildBackupPayload(settings)); const fileName = `${utils.getNowStr("_", "_")}.json`; utils.download(exportText, fileName); show.ok("数据导出成功"); } catch (err) { console.error(err); show.error("导出数据时出错: " + (err instanceof Error ? err.message : String(err))); } } function handleVltImportClick() { $("#vlt-file-input").trigger("click"); } async function handleVltFileChange(event) { const input = event.target; const file = input.files?.[0]; if (!file) return; const $status = $("#vlt-status"); $status.text("⏳ 导入中...").css("color", "#0d6efd"); try { const text = await file.text(); const data = JSON.parse(text); const stats = await VltDb.importData(data); $status.html(`✓ 导入完成:${stats.movies} 部影片, ${stats.inventory} 个清单, ${stats.movieInventory} 条关联`).css("color", "#198754"); showToast$1(`✓ 导入完成:${stats.movies} 部影片, ${stats.inventory} 个清单, ${stats.movieInventory} 条关联`, "success"); setTimeout(() => window.location.reload(), 1500); } catch (err) { const msg = err instanceof Error ? err.message : String(err); $status.text(`✗ 导入失败:${msg}`).css("color", "#dc3545"); showToast$1(`✗ 导入失败:${msg}`, "error"); } input.value = ""; } async function handleVltExportClick() { const $status = $("#vlt-status"); $status.text("⏳ 导出中...").css("color", "#0d6efd"); try { const data = await VltDb.exportData(); downloadJson(JSON.stringify(data, null, 2), `vlt-export-${new Date().toISOString().split("T")[0]}.json`); $status.html(`✓ 导出完成:${Object.keys(data.movies).length} 部影片, ${Object.keys(data.inventory).length} 个清单, ${Object.keys(data.movieInventory).length} 条关联`).css("color", "#198754"); showToast$1("✓ 导出完成", "success"); } catch (err) { const msg = err instanceof Error ? err.message : String(err); $status.text(`✗ 导出失败:${msg}`).css("color", "#dc3545"); showToast$1(`✗ 导出失败:${msg}`, "error"); } } async function handleMissavSync() { const $status = $("#missav-status"); $status.text("⏳ 同步中...").css("color", "#0d6efd"); try { const carList = await storageManager.getCarList(); if (carList.length === 0) { $status.text("✗ car_list 为空").css("color", "#dc3545"); return; } const colRes = toColumnar(carList); const count = countColumnar(colRes.byStatus); const base64 = await gzipToBase64(colRes.byStatus); if (!base64) { $status.text("✗ 压缩失败").css("color", "#dc3545"); return; } const payload = { data: base64, hwm: new Date().toISOString(), count, ts: Date.now() }; GM_setValue(GM_KEY_CAR_STATUS_DATA, payload); $status.html(`✓ 同步完成:${count} 条记录已推送到 GM 存储`).css("color", "#198754"); showToast$1(`✓ 同步完成:${count} 条`, "success"); } catch (err) { const msg = err instanceof Error ? err.message : String(err); $status.text(`✗ 同步失败:${msg}`).css("color", "#dc3545"); showToast$1(`✗ 同步失败:${msg}`, "error"); } } function handleMissavImportClick() { $("#missav-file-input").trigger("click"); } async function handleMissavFileChange(event) { const input = event.target; const file = input.files?.[0]; if (!file) return; const $status = $("#missav-status"); $status.text("⏳ 导入中...").css("color", "#0d6efd"); try { const text = await file.text(); const data = JSON.parse(text); let records; if (Array.isArray(data)) records = data; else if (data.records && Array.isArray(data.records)) records = data.records; else records = columnarToFlat(data).map((r) => ({ carNum: r.carNum, status: r.status, url: buildJavdbUrl(r.url_path) })); const written = await importLocalDB(records); $status.html(`✓ 导入完成:${written} 条记录`).css("color", "#198754"); showToast$1(`✓ 导入完成:${written} 条`, "success"); } catch (err) { const msg = err instanceof Error ? err.message : String(err); $status.text(`✗ 导入失败:${msg}`).css("color", "#dc3545"); showToast$1(`✗ 导入失败:${msg}`, "error"); } input.value = ""; } async function handleMissavExportClick() { const $status = $("#missav-status"); $status.text("⏳ 导出中...").css("color", "#0d6efd"); try { const records = await exportLocalDB(); downloadJson(JSON.stringify(records, null, 2), `missav-status-${new Date().toISOString().split("T")[0]}.json`); $status.html(`✓ 导出完成:${records.length} 条记录`).css("color", "#198754"); showToast$1("✓ 导出完成", "success"); } catch (err) { const msg = err instanceof Error ? err.message : String(err); $status.text(`✗ 导出失败:${msg}`).css("color", "#dc3545"); showToast$1(`✗ 导出失败:${msg}`, "error"); } } function PreloadCacheStatsText({ count, missAv, supJav, size }) { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ "共 ", (0, import_jsx_runtime.jsx)("b", { children: count }), " 条(MissAv ", missAv, " / SupJav ", supJav, ")|占用 ", size ] }); } function getCacheStats(key) { const raw = localStorage.getItem(key); if (!raw) return { size: 0, count: 0 }; const size = new Blob([raw]).size; let count = 0; try { const parsed = JSON.parse(raw); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) count = Object.keys(parsed).length; else if (Array.isArray(parsed)) count = parsed.length; } catch {} return { size, count }; } function formatSize(bytes) { if (bytes === 0) return "0 B"; if (bytes < 1024) return `${bytes} B`; if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1048576).toFixed(1)} MB`; } function refreshCacheStats(_plugin, cacheKey) { const stats = getCacheStats(cacheKey); const $item = $(`.cache-item[data-cache-key="${cacheKey}"]`); $item.find(".cache-size").text(formatSize(stats.size)); $item.find(".cache-count").text(stats.count > 0 ? `${stats.count} 条` : "空"); } function refreshAllCacheStats(plugin) { let totalSize = 0; for (const item of plugin.cacheItems) { const stats = getCacheStats(item.key); totalSize += stats.size; const $item = $(`.cache-item[data-cache-key="${item.key}"]`); $item.find(".cache-size").text(formatSize(stats.size)); $item.find(".cache-count").text(stats.count > 0 ? `${stats.count} 条` : "空"); } totalSize += getCacheStats("jhs_other_site_dmm").size; $("#cache-total-size").text(`总占用 ${formatSize(totalSize)}`); } function refreshPreloadCacheStats(_plugin) { const stats = getCacheStats("jhs_other_site"); const raw = localStorage.getItem("jhs_other_site"); let missAv = 0; let supJav = 0; if (raw) try { const cache = JSON.parse(raw); const keys = Object.keys(cache); missAv = keys.filter((k) => k.endsWith("_missAv")).length; supJav = keys.filter((k) => k.endsWith("_supJav")).length; } catch {} $("#preload-cache-stats").html(jsxToString((0, import_react.createElement)(PreloadCacheStatsText, { count: stats.count, missAv, supJav, size: formatSize(stats.size) }))); } function handleCleanCache(plugin, event) { const cacheKey = String($(event.currentTarget).data("key")); const cacheItem = plugin.cacheItems.find((item) => item.key === cacheKey); localStorage.removeItem(cacheKey); if (cacheKey === "jhs_dmm_video") localStorage.removeItem("jhs_other_site_dmm"); show.ok(`${cacheItem.text} 清理成功`); $("#cache-data-display").hide(); refreshCacheStats(plugin, cacheKey); refreshAllCacheStats(plugin); } function handleCleanAllCache(plugin) { plugin.cacheItems.forEach((item) => localStorage.removeItem(item.key)); localStorage.removeItem("jhs_other_site_dmm"); show.ok("全部缓存已清理"); $("#cache-data-display").hide(); refreshAllCacheStats(plugin); } function handleViewCache(event) { const cacheKey = String($(event.currentTarget).data("key")); const cachedData = localStorage.getItem(cacheKey); const $display = $("#cache-data-display"); const $pre = $display.find("pre"); $display.show(); if (cachedData) try { const parsed = JSON.parse(cachedData); $pre.text(JSON.stringify(parsed, null, 2)); } catch { $pre.text(cachedData); } else $pre.text("无数据"); } function handleExportCache(plugin, event) { const cacheKey = String($(event.currentTarget).data("key")); const cachedData = localStorage.getItem(cacheKey); if (!cachedData) { show.error("缓存为空,无可导出数据"); return; } const cacheItem = plugin.cacheItems.find((item) => item.key === cacheKey); downloadJson(cachedData, `${cacheKey}-${new Date().toISOString().split("T")[0]}.json`); show.ok(`${cacheItem.text} 已导出`); } function bindKeywordContainer(plugin, containerSelector, noun) { $(`${containerSelector} .add-tag-btn`).on("click", () => plugin.addKeyword(containerSelector, noun)); $(`${containerSelector} .keyword-input`).on("keypress", (event) => { if (event.key === "Enter") plugin.addKeyword(containerSelector, noun); }); } function readKeywordLabels(containerSelector) { const list = []; $(`${containerSelector} .keyword-label`).toArray().forEach((label) => { const keyword = $(label).text().replace("×", "").replace(/[\r\n]+/g, " ").replace(/\s{2,}/g, " ").trim(); if (keyword) list.push(keyword); }); return list; } async function loadForm(plugin) { const settings = await storageManager.getSetting(); $("#videoQuality").val(settings.videoQuality); $("#reviewCount").val(settings.reviewCount || 20); $("#tagPosition").val(settings.tagPosition || "rightTop"); $("#movieShowType").val(settings.movieShowType || "hide"); $("#waitCheckCount").val(settings.waitCheckCount || 5); const tagNumber = settings.highlightedTagNumber || 1; const tagColor = settings.highlightedTagColor || "#ce2222"; $("#highlightedTagNumber").val(settings.highlightedTagNumber || 1); $("#highlightedTagColor").val(settings.highlightedTagColor || "#ce2222"); $("#highlightedTagLabel").css("border", `${tagNumber}px solid ${tagColor}`); $("#enableClog").val(settings.enableClog || "yes"); $("#clogMsgCount").val(settings.clogMsgCount || 2e3); $("#httpTimeout").val(settings.httpTimeout || 5e3); $("#httpRetryCount").val(settings.httpRetryCount || 3); const webDavUrl = GM_getValue("jhs_webdav_url", "") || settings.webDavUrl || ""; const webDavUsername = GM_getValue("jhs_webdav_username", "") || settings.webDavUsername || ""; const webDavPassword = GM_getValue("jhs_webdav_password", "") || settings.webDavPassword || ""; const backupPwd = GM_getValue("jhs_backup_password", "") || settings.backupPassword || ""; if (webDavUrl && !GM_getValue("jhs_webdav_url", "")) GM_setValue(GM_KEY_WEBDAV_URL, webDavUrl); if (webDavUsername && !GM_getValue("jhs_webdav_username", "")) GM_setValue(GM_KEY_WEBDAV_USERNAME, webDavUsername); if (webDavPassword && !GM_getValue("jhs_webdav_password", "")) GM_setValue(GM_KEY_WEBDAV_PASSWORD, webDavPassword); if (backupPwd && !GM_getValue("jhs_backup_password", "")) GM_setValue(GM_KEY_BACKUP_PASSWORD, backupPwd); $("#webDavUrl").val(webDavUrl); $("#webDavUsername").val(webDavUsername); $("#webDavPassword").val(webDavPassword); $("#backupPassword").val(backupPwd); $("#gitApiUrl").val(GM_getValue(GM_KEY_GIT_API_URL, "")); $("#gitRepo").val(GM_getValue(GM_KEY_GIT_REPO, "")); $("#gitBranch").val(GM_getValue(GM_KEY_GIT_BRANCH, "")); $("#gitToken").val(GM_getValue(GM_KEY_GIT_TOKEN, "")); const autoBackupConfig = settings.autoBackupConfig || DEFAULT_AUTO_BACKUP_CONFIG; $("#enableAutoBackup").prop("checked", autoBackupConfig.enabled); $("#autoBackupFrequency").val(autoBackupConfig.frequency); const backupChannels = settings.backupChannels || { webdav: true, git: false }; $("#enableWebdavChannel").prop("checked", backupChannels.webdav !== false); $("#enableGitChannel").prop("checked", backupChannels.git === true); $("#credentialIdDisplay").val(getCredentialId()); $("#enableFavoriteActresses").prop("checked", !settings.enableFavoriteActresses || settings.enableFavoriteActresses === "yes"); $("#enableSaveActressCarInfo").prop("checked", !!settings.enableSaveActressCarInfo && settings.enableSaveActressCarInfo === "yes"); const otherSitePlugin = plugin.getBean("OtherSitePlugin"); const missAvUrl = await otherSitePlugin.getMissAvUrl(); const supJavUrl = await otherSitePlugin.getSupJavUrl(); $("#missAvUrl").val(missAvUrl); $("#supJavUrl").val(supJavUrl); $("#enablePreload").prop("checked", !settings.enablePreload || settings.enablePreload === "yes"); $("#enablePreloadStatus").prop("checked", !settings.enablePreloadStatus || settings.enablePreloadStatus === "yes"); $("#preloadDebounce").val(settings.preloadDebounce || 300); $("#preloadConcurrency").val(settings.preloadConcurrency || 1); $("#preloadCacheTTL").val(settings.preloadCacheTTL ?? 0); const enabledSites = otherSitePlugin.getEnabledSites(); $("#preload-enable-missAvBtn").prop("checked", enabledSites.includes("missAvBtn")); $("#preload-enable-supJavBtn").prop("checked", enabledSites.includes("supJavBtn")); refreshPreloadCacheStats(plugin); $("#listMergeEnabled").prop("checked", !settings.listMergeEnabled || settings.listMergeEnabled === "yes"); $("#listReflowEnabled").prop("checked", !settings.listReflowEnabled || settings.listReflowEnabled === "yes"); $("#listDefaultSort").val(settings.listDefaultSort || "name-desc"); $("#autoReconcileDelay").val(settings.autoReconcileDelay ?? 800); $("#autoServerSync").prop("checked", !settings.autoServerSync || settings.autoServerSync === "yes"); $("#favSyncInterval").val(settings.jhsSyncInterval ?? 500); $("#reflowInterval").val(settings.reflowInterval ?? 400); $("#reflowTrialCount").val(settings.reflowTrialCount ?? 10); $("#reflowBatchSize").val(settings.reflowBatchSize ?? 20); $("#reflowMaxRetry").val(settings.reflowMaxRetry ?? 3); $("#autoFavCleanupEnabled").prop("checked", !settings.autoFavCleanupEnabled || settings.autoFavCleanupEnabled === "yes"); $("#listMergePattern").val(settings.listMergePattern || "^(.+?)(\\d+)$"); const titleFilterKeyword = await storageManager.getTitleFilterKeyword(); if (titleFilterKeyword) titleFilterKeyword.forEach((keyword) => { plugin.addLabelTag("#filterKeywordContainer", keyword); }); bindKeywordContainer(plugin, "#filterKeywordContainer", "屏蔽词"); (Array.isArray(settings.listMergeExcludeNames) ? settings.listMergeExcludeNames : []).forEach((name) => plugin.addLabelTag("#listMergeExcludeNamesContainer", name, "清单名")); bindKeywordContainer(plugin, "#listMergeExcludeNamesContainer", "清单名"); (Array.isArray(settings.protectedListKeywords) ? settings.protectedListKeywords : ["值得复盘"]).forEach((kw) => plugin.addLabelTag("#protectedListKeywordsContainer", kw, "关键词")); bindKeywordContainer(plugin, "#protectedListKeywordsContainer", "关键词"); } async function saveForm(plugin) { const settings = await storageManager.getSetting(); settings.videoQuality = $("#videoQuality").val(); settings.reviewCount = $("#reviewCount").val(); settings.tagPosition = $("#tagPosition").val(); settings.movieShowType = $("#movieShowType").val(); settings.waitCheckCount = $("#waitCheckCount").val(); settings.highlightedTagNumber = $("#highlightedTagNumber").val(); settings.highlightedTagColor = $("#highlightedTagColor").val(); settings.httpTimeout = $("#httpTimeout").val(); settings.httpRetryCount = $("#httpRetryCount").val(); settings.enableClog = $("#enableClog").val(); if (settings.enableClog === "yes") clog.show(); else clog.hide(); settings.clogMsgCount = $("#clogMsgCount").val(); GM_setValue(GM_KEY_WEBDAV_URL, $("#webDavUrl").val() || ""); GM_setValue(GM_KEY_WEBDAV_USERNAME, $("#webDavUsername").val() || ""); GM_setValue(GM_KEY_WEBDAV_PASSWORD, $("#webDavPassword").val() || ""); GM_setValue(GM_KEY_BACKUP_PASSWORD, $("#backupPassword").val() || ""); GM_setValue(GM_KEY_GIT_API_URL, $("#gitApiUrl").val() || ""); GM_setValue(GM_KEY_GIT_REPO, $("#gitRepo").val() || ""); GM_setValue(GM_KEY_GIT_BRANCH, $("#gitBranch").val() || ""); GM_setValue(GM_KEY_GIT_TOKEN, $("#gitToken").val() || ""); delete settings.webDavUrl; delete settings.webDavUsername; delete settings.webDavPassword; settings.autoBackupConfig = { enabled: $("#enableAutoBackup").is(":checked"), frequency: $("#autoBackupFrequency").val() }; settings.backupChannels = { webdav: $("#enableWebdavChannel").is(":checked"), git: $("#enableGitChannel").is(":checked") }; settings.missAvUrl = String($("#missAvUrl").val() ?? "").replace(/\/$/, ""); settings.supJavUrl = String($("#supJavUrl").val() ?? "").replace(/\/$/, ""); settings.enablePreload = $("#enablePreload").is(":checked") ? "yes" : "no"; settings.enablePreloadStatus = $("#enablePreloadStatus").is(":checked") ? "yes" : "no"; settings.preloadDebounce = $("#preloadDebounce").val(); settings.preloadConcurrency = $("#preloadConcurrency").val(); settings.preloadCacheTTL = $("#preloadCacheTTL").val(); const otherSitePluginForSave = plugin.getBean("OtherSitePlugin"); const enabledSiteIds = []; if ($("#preload-enable-missAvBtn").is(":checked")) enabledSiteIds.push("missAvBtn"); if ($("#preload-enable-supJavBtn").is(":checked")) enabledSiteIds.push("supJavBtn"); otherSitePluginForSave.saveEnabledSites(enabledSiteIds); settings.enableFavoriteActresses = $("#enableFavoriteActresses").is(":checked") ? "yes" : "no"; settings.enableSaveActressCarInfo = $("#enableSaveActressCarInfo").is(":checked") ? "yes" : "no"; settings.listMergeEnabled = $("#listMergeEnabled").is(":checked") ? "yes" : "no"; settings.listReflowEnabled = $("#listReflowEnabled").is(":checked") ? "yes" : "no"; settings.listDefaultSort = $("#listDefaultSort").val(); settings.autoReconcileDelay = Number($("#autoReconcileDelay").val()) || 800; settings.jhsSyncInterval = Number($("#favSyncInterval").val()) || 500; settings.reflowInterval = Number($("#reflowInterval").val()) || 400; settings.reflowTrialCount = Number($("#reflowTrialCount").val()) || 10; settings.reflowBatchSize = Number($("#reflowBatchSize").val()) || 20; settings.reflowMaxRetry = Number($("#reflowMaxRetry").val()) || 3; settings.autoFavCleanupEnabled = $("#autoFavCleanupEnabled").is(":checked") ? "yes" : "no"; settings.autoServerSync = $("#autoServerSync").is(":checked") ? "yes" : "no"; settings.listMergePattern = String($("#listMergePattern").val() ?? "").trim(); settings.listMergeExcludeNames = readKeywordLabels("#listMergeExcludeNamesContainer"); settings.protectedListKeywords = readKeywordLabels("#protectedListKeywordsContainer"); await storageManager.saveSetting(settings); const titleFilterKeywordList = readKeywordLabels("#filterKeywordContainer"); await storageManager.saveTitleFilterKeyword(titleFilterKeywordList); show.ok("保存成功"); refresh(); const newVideoPlugin = plugin.getBean("NewVideoPlugin"); if (newVideoPlugin) newVideoPlugin.resetBtnTip(); plugin.getBean("BlacklistPlugin").resetBtnTip(); plugin.getBean("BlacklistPlugin").reloadTable(); } var SESSION_KEY = "jhs:sync_session_v1"; function createSession() { const now = Date.now(); return { createdAt: now, updatedAt: now, phase: "idle", wantPage: 0, watchedPage: 0, javdbWant: [], javdbWatched: [], queue: [], queueDone: 0, synced: 0, failed: [], skipped: 0, conflicts: [] }; } function loadSession() { try { const raw = localStorage.getItem(SESSION_KEY); if (!raw) return null; return JSON.parse(raw); } catch { return null; } } function saveSession(session) { session.updatedAt = Date.now(); try { localStorage.setItem(SESSION_KEY, JSON.stringify(session)); } catch {} } function clearSession() { localStorage.removeItem(SESSION_KEY); } function isResumable(session) { return session.phase !== "idle" && session.phase !== "done" && session.phase !== "error"; } function getCsrfToken$2() { const meta = document.querySelector("meta[name=\"csrf-token\"]"); return meta ? meta.content : null; } function extractVideoId(url) { if (!url) return null; const m = url.match(/\/v\/([^/?#]+)/); return m ? m[1] : null; } async function fetchPageHtml(url) { const res = await fetch(url, { credentials: "same-origin" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.text(); } async function postWantToWatch(videoId) { const token = getCsrfToken$2(); if (!token) throw new Error("无法获取 CSRF token"); const res = await fetch(`/v/${videoId}/reviews/want_to_watch`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", "X-CSRF-Token": token }, body: `authenticity_token=${encodeURIComponent(token)}`, credentials: "same-origin" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); } async function postWatched(videoId, score = 0) { const token = getCsrfToken$2(); if (!token) throw new Error("无法获取 CSRF token"); const body = `${`authenticity_token=${encodeURIComponent(token)}`}&video_review[status]=watched&video_review[score]=${score}&video_review[content]=`; const res = await fetch(`/v/${videoId}/reviews`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", "X-CSRF-Token": token }, body, credentials: "same-origin" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); } function readRatingCacheMap() { const map = new Map(); try { const raw = localStorage.getItem("jdb:rating_cache_v2"); if (!raw) return map; const obj = JSON.parse(raw); for (const [code, entry] of Object.entries(obj)) if (entry && typeof entry.rating === "number" && entry.rating > 0) map.set(code, entry.rating); } catch {} return map; } var session = createSession(); var timer = null; var listeners = []; var ticking = false; function onSyncChange(fn) { listeners.push(fn); return () => { listeners = listeners.filter((l) => l !== fn); }; } function emit() { for (const fn of listeners) fn(session); } function isRunning() { return timer !== null; } function hasResumableSession() { const saved = loadSession(); return saved !== null && isResumable(saved); } async function fetchOnePage(url) { const absUrl = new URL(url, window.location.origin).href; const html = await fetchPageHtml(absUrl); const doc = new DOMParser().parseFromString(html, "text/html"); const entries = []; doc.querySelectorAll(".movie-list .item").forEach((item) => { const a = item.querySelector("a[href*=\"/v/\"]"); const strong = item.querySelector(".video-title strong"); if (!a) return; const m = (a.getAttribute("href") || "").match(/\/v\/([^/?#]+)/); if (!m) return; entries.push([m[1], strong ? (strong.textContent || "").trim() : ""]); }); const next = doc.querySelector("a.pagination-next"); return { entries, nextUrl: next ? next.getAttribute("href") : null }; } async function buildQueue() { const carList = await storageManager.getCarList(); const ratingCache = readRatingCacheMap(); const javdbWantMap = new Map(session.javdbWant); const javdbWatchedMap = new Map(session.javdbWatched); const jhsWantByVideoId = new Map(); const jhsWatchedByVideoId = new Map(); for (const car of carList) { const vid = extractVideoId(car.url); if (!vid) continue; if (car.status === "favorite") jhsWantByVideoId.set(vid, car.carNum); else if (car.status === "hasWatch") jhsWatchedByVideoId.set(vid, car.carNum); } const queue = []; const conflicts = []; let skipped = 0; for (const [vid, carNum] of jhsWantByVideoId) { if (javdbWantMap.has(vid)) { skipped++; continue; } if (javdbWatchedMap.has(vid)) { conflicts.push({ carNum, jhsStatus: "想看", javdbStatus: "已看" }); continue; } queue.push({ carNum, videoId: vid, action: "want-to-javdb" }); } for (const car of carList) { if (car.status !== "hasWatch") continue; const vid = extractVideoId(car.url); if (!vid) continue; if (javdbWatchedMap.has(vid)) { skipped++; continue; } if (javdbWantMap.has(vid)) { conflicts.push({ carNum: car.carNum, jhsStatus: "已看", javdbStatus: "想看" }); continue; } const score = typeof car.score === "number" && car.score > 0 ? car.score : ratingCache.get(car.carNum) ?? 0; queue.push({ carNum: car.carNum, videoId: vid, action: "watched-to-javdb", score }); } for (const [vid, carNum] of javdbWantMap) { if (!carNum) continue; if (jhsWantByVideoId.has(vid)) { skipped++; continue; } if (jhsWatchedByVideoId.has(vid)) { conflicts.push({ carNum, jhsStatus: "已看", javdbStatus: "想看" }); continue; } queue.push({ carNum, videoId: vid, action: "want-from-javdb" }); } for (const [vid, carNum] of javdbWatchedMap) { if (!carNum) continue; if (jhsWatchedByVideoId.has(vid)) { skipped++; continue; } if (jhsWantByVideoId.has(vid)) { conflicts.push({ carNum, jhsStatus: "想看", javdbStatus: "已看" }); continue; } queue.push({ carNum, videoId: vid, action: "watched-from-javdb" }); } session.queue = queue; session.queueDone = 0; session.skipped = skipped; session.conflicts = conflicts; session.phase = "execute"; saveSession(session); emit(); } async function executeOp(op) { switch (op.action) { case "want-to-javdb": await postWantToWatch(op.videoId); break; case "watched-to-javdb": await postWatched(op.videoId, op.score ?? 0); break; case "want-from-javdb": await storageManager.saveCar({ carNum: op.carNum, url: `/v/${op.videoId}`, actionType: FAVORITE_ACTION }); break; case "watched-from-javdb": await storageManager.saveCar({ carNum: op.carNum, url: `/v/${op.videoId}`, actionType: HAS_WATCH_ACTION, score: 0 }); } } async function tick() { if (ticking) return; ticking = true; try { switch (session.phase) { case "fetch-want": { const startUrl = session.wantPage === 0 ? "/users/want_watch_videos" : `/users/want_watch_videos?page=${session.wantPage + 1}`; if (session.wantPage >= 200) { session.phase = "fetch-watched"; saveSession(session); emit(); break; } const { entries, nextUrl } = await fetchOnePage(startUrl); session.javdbWant.push(...entries); session.wantPage++; if (!nextUrl || entries.length === 0) session.phase = "fetch-watched"; saveSession(session); emit(); break; } case "fetch-watched": { const startUrl = session.watchedPage === 0 ? "/users/watched_videos" : `/users/watched_videos?page=${session.watchedPage + 1}`; if (session.watchedPage >= 200) { session.phase = "compare"; saveSession(session); emit(); break; } const { entries, nextUrl } = await fetchOnePage(startUrl); session.javdbWatched.push(...entries); session.watchedPage++; if (!nextUrl || entries.length === 0) session.phase = "compare"; saveSession(session); emit(); break; } case "compare": await buildQueue(); break; case "execute": { if (session.queueDone >= session.queue.length) { session.phase = "done"; saveSession(session); emit(); stopTimer(); break; } const op = session.queue[session.queueDone]; try { await executeOp(op); session.synced++; } catch (err) { session.failed.push(`${op.carNum}(${err instanceof Error ? err.message : "失败"})`); } session.queueDone++; if (session.queueDone % 10 === 0 || session.queueDone >= session.queue.length) saveSession(session); emit(); break; } default: stopTimer(); } } catch (err) { session.phase = "error"; session.errorMsg = err instanceof Error ? err.message : String(err); saveSession(session); emit(); stopTimer(); } finally { ticking = false; } } function startSync(interval) { stopTimer(); session = createSession(); session.phase = "fetch-want"; saveSession(session); emit(); timer = setInterval(() => void tick(), interval); } function resumeSync(interval) { const saved = loadSession(); if (!saved || !isResumable(saved)) return false; stopTimer(); session = saved; emit(); timer = setInterval(() => void tick(), interval); return true; } function pauseSync() { stopTimer(); saveSession(session); emit(); } function resetSync() { stopTimer(); session = createSession(); clearSession(); emit(); } function stopTimer() { if (timer !== null) { clearInterval(timer); timer = null; } } function getProgress() { switch (session.phase) { case "idle": return 0; case "fetch-want": return Math.min(10, session.wantPage * .5); case "fetch-watched": return 10 + Math.min(10, session.watchedPage * .5); case "compare": return 20; case "execute": { const total = session.queue.length || 1; return 20 + Math.round(session.queueDone / total * 80); } case "done": return 100; case "error": return -1; default: return 0; } } function getPhaseLabel() { switch (session.phase) { case "idle": return "就绪"; case "fetch-want": return `抓取 JavDB 想看列表(第 ${session.wantPage} 页,已获 ${session.javdbWant.length} 条)`; case "fetch-watched": return `抓取 JavDB 已看列表(第 ${session.watchedPage} 页,已获 ${session.javdbWatched.length} 条)`; case "compare": return "对比中…"; case "execute": return `执行同步 ${session.queueDone}/${session.queue.length}(成功 ${session.synced},失败 ${session.failed.length})`; case "done": return `同步完成:成功 ${session.synced},失败 ${session.failed.length},跳过 ${session.skipped},冲突 ${session.conflicts.length}`; case "error": return `错误:${session.errorMsg || "未知"}`; default: return ""; } } var bound = false; function refreshUi(session) { const progress = getProgress(); const $bar = $("#fav-sync-progress-bar"); const $phase = $("#fav-sync-phase"); const $report = $("#fav-sync-report"); if (progress >= 0) $bar.css({ width: `${progress}%`, background: "#48c774" }); else $bar.css({ width: "100%", background: "#f14668" }); $phase.text(getPhaseLabel()); $("#fav-sync-stat-synced").text(String(session.synced)); $("#fav-sync-stat-failed").text(String(session.failed.length)); $("#fav-sync-stat-skipped").text(String(session.skipped)); $("#fav-sync-stat-conflicts").text(String(session.conflicts.length)); if (session.conflicts.length > 0 || session.failed.length > 0 || session.phase === "done") { let text = ""; if (session.conflicts.length > 0) { text += "⚠ 冲突(需手动处理):\n"; text += session.conflicts.map((c) => ` ${c.carNum}(脚本${c.jhsStatus} / 网站${c.javdbStatus})`).join("\n"); text += "\n\n"; } if (session.failed.length > 0) { text += "✗ 失败:\n"; text += session.failed.map((f) => ` ${f}`).join("\n"); text += "\n\n"; } if (session.phase === "done") text += `✓ 同步完成:成功 ${session.synced},失败 ${session.failed.length},跳过 ${session.skipped},冲突 ${session.conflicts.length}`; $report.text(text).show(); } else $report.hide(); const running = isRunning(); $("#fav-sync-start-btn").css({ "pointer-events": running ? "none" : "", opacity: running ? "0.5" : "" }); $("#fav-sync-resume-btn").css({ "pointer-events": running || !hasResumableSession() ? "none" : "", opacity: running || !hasResumableSession() ? "0.5" : "" }); $("#fav-sync-pause-btn").css({ "pointer-events": running ? "" : "none", opacity: running ? "" : "0.5" }); } function bindFavSyncPanel() { if (bound) return; bound = true; onSyncChange((session) => refreshUi(session)); $("#fav-sync-start-btn").on("click", () => { if (!isJavdbSite) { show.error("请在 JavDB 站点下执行此同步"); return; } utils.q(null, "确定开始双向增量同步?<br>将分页抓取 JavDB 想看/已看列表,对比后增量同步。<br>支持暂停/继续,关闭弹窗不中断。<br><span style=\"color:#f40\">建议先备份数据。</span>", () => { startSync(Number($("#favSyncInterval").val()) || 500); }); }); $("#fav-sync-resume-btn").on("click", () => { if (!isJavdbSite) { show.error("请在 JavDB 站点下执行此同步"); return; } if (!resumeSync(Number($("#favSyncInterval").val()) || 500)) show.error("没有可恢复的同步会话"); }); $("#fav-sync-pause-btn").on("click", () => { pauseSync(); show.ok("同步已暂停,可稍后继续"); }); $("#fav-sync-reset-btn").on("click", () => { utils.q(null, "确定重置同步?将清除所有进度和会话数据。", () => { resetSync(); $("#fav-sync-report").hide(); show.ok("同步已重置"); }); }); if (hasResumableSession()) { resumeSync(Number($("#favSyncInterval").val()) || 500); pauseSync(); } } function BackupFileDialog() { return (0, import_jsx_runtime.jsx)("div", { style: { height: "100%", overflow: "hidden" }, children: (0, import_jsx_runtime.jsx)("div", { id: "table-container", style: { margin: "auto auto !important" } }) }); } function BackupActionCell() { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ (0, import_jsx_runtime.jsx)("a", { className: "a-danger", children: "删除" }), " ", (0, import_jsx_runtime.jsx)("a", { className: "a-primary", children: "下载" }), " ", (0, import_jsx_runtime.jsx)("a", { className: "a-success", children: "导入" }) ] }); } function getWebDavCredentials() { const url = $("#webDavUrl").val() || GM_getValue("jhs_webdav_url", ""); const username = $("#webDavUsername").val() || GM_getValue("jhs_webdav_username", ""); const password = $("#webDavPassword").val() || GM_getValue("jhs_webdav_password", ""); if (!url) { show.error("请填写webDav服务地址并保存后, 再试此功能"); return null; } if (!username) { show.error("请填写webDav用户名并保存后, 再试此功能"); return null; } if (!password) { show.error("请填写webDav密码并保存后, 再试此功能"); return null; } return { url, username, password }; } function getWebDavCredentialsFromGm() { const url = GM_getValue(GM_KEY_WEBDAV_URL, ""); if (!url) return null; const username = GM_getValue(GM_KEY_WEBDAV_USERNAME, ""); const password = GM_getValue(GM_KEY_WEBDAV_PASSWORD, ""); if (!username || !password) return null; return { url, username, password }; } async function backupDataByWebDav(plugin) { const creds = getWebDavCredentials(); if (!creds) return; const settings = await storageManager.getSetting(); const fileName = utils.getNowStr("_", "_") + ".json"; const exportText = JSON.stringify(await buildBackupPayload(settings)); try { await withLoading(async () => { await new window.WebDavClient(creds.url, creds.username, creds.password).backup(plugin.folderName, fileName, exportText); show.ok("备份完成"); }); } catch (err) { console.error(err); show.error(String(err)); } } async function backupListBtnByWebDav(plugin) { const creds = getWebDavCredentials(); if (!creds) return; try { await withLoading(async () => { const webdavClient = new window.WebDavClient(creds.url, creds.username, creds.password); openFileListDialog(plugin, await webdavClient.getBackupList(plugin.folderName), webdavClient, "WebDav"); }); } catch (err) { console.error(err); show.error(`发生错误: ${err instanceof Error ? err.message : String(err)}`); } } function openFileListDialog(plugin, backupList, provider, titlePrefix) { layer.open({ type: 1, title: titlePrefix + "备份文件", content: jsxToString((0, import_react.createElement)(BackupFileDialog)), area: ["800px", "70%"], anim: -1, success: () => { const table = new Tabulator("#table-container", { layout: "fitColumns", placeholder: "暂无数据", virtualDom: true, data: backupList, responsiveLayout: "collapse", responsiveLayoutCollapse: true, columnDefaults: { headerHozAlign: "center", hozAlign: "center" }, columns: [ { title: "文件名", field: "name", width: 200, headerSort: false, responsive: 0 }, { title: "文件大小", field: "size", responsive: 1, headerSort: false, formatter: (cell) => { const units = [ "B", "KB", "MB", "GB", "TB", "PB" ]; let unitIndex = 0; let size = cell.getData().size; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size % 1 == 0 ? size.toFixed(0) : size.toFixed(2)} ${units[unitIndex]}`; } }, { title: "备份日期", field: "createTime", responsive: 2, headerSort: false, formatter: (cell) => { const data = cell.getData(); return `${utils.getNowStr("-", ":", new Date(data.createTime).getTime())}`; } }, { title: "操作", minWidth: 250, responsive: 0, headerSort: false, formatter: (cell, _params, onRendered) => { const rowData = cell.getData(); onRendered(() => { const $dangerBtn = cell.getElement().querySelector(".a-danger"); const $primaryBtn = cell.getElement().querySelector(".a-primary"); const $successBtn = cell.getElement().querySelector(".a-success"); if ($dangerBtn) $dangerBtn.addEventListener("click", () => { layer.confirm(`是否删除 ${rowData.name} ?`, { icon: 3, title: "提示", btn: ["确定", "取消"] }, async (layerIndex) => { layer.close(layerIndex); try { await withLoading(async () => { await provider.deleteFile(rowData.fileId); const newList = await provider.getBackupList(plugin.folderName); table.replaceData(newList); layer.alert("删除成功"); }); } catch (err) { console.error(err); show.error(`发生错误: ${err instanceof Error ? err.message : String(err)}`); } }); }); if ($primaryBtn) $primaryBtn.addEventListener("click", async () => { try { await withLoading(async () => { const fileContent = await provider.getFileContent(rowData.fileId); const backupPassword = GM_getValue(GM_KEY_BACKUP_PASSWORD, ""); let decrypted; if (isBackupV2(fileContent)) { if (!backupPassword) { show.error("该备份使用 V2 加密,请先设置备份口令"); return; } decrypted = await decryptBackupV2(fileContent, backupPassword); } else decrypted = window.decryptCredential(fileContent); utils.download(decrypted, rowData.name); }); } catch (err) { clog.error(err); show.error("下载失败: " + (err instanceof Error ? err.message : String(err))); } }); if ($successBtn) $successBtn.addEventListener("click", () => { layer.confirm(`是否将该云备份数据 ${rowData.name} 导入?`, { icon: 3, title: "提示", btn: ["确定", "取消"] }, async (layerIndex) => { layer.close(layerIndex); try { await withLoading(async () => { let fileContent = await provider.getFileContent(rowData.fileId); show.info("解密文件内容..."); const backupPassword = GM_getValue(GM_KEY_BACKUP_PASSWORD, ""); if (isBackupV2(fileContent)) { if (!backupPassword) { show.error("该备份使用 V2 加密,请先设置备份口令"); return; } fileContent = await decryptBackupV2(fileContent, backupPassword); } else fileContent = window.decryptCredential(fileContent); show.info("解密完成, 开始导入..."); const parsedData = JSON.parse(fileContent); const extras = stripBackupExtras(parsedData); await storageManager.importData(parsedData); applyBackupExtras(extras); show.ok("导入成功!"); window.location.reload(); }); } catch (err) { console.error(err); show.error(String(err)); } }); }); }); return jsxToString((0, import_react.createElement)(BackupActionCell)); } } ], locale: "zh-cn", langs: { "zh-cn": { pagination: { first: "首页", first_title: "首页", last: "尾页", last_title: "尾页", prev: "上一页", prev_title: "上一页", next: "下一页", next_title: "下一页", all: "所有", page_size: "每页行数" } } } }); } }); } var GitHttpError = class extends Error { status; constructor(status, statusText, serverMessage) { const base = `请求失败 ${status}: ${statusText}`; super(serverMessage ? `${base}(${serverMessage})` : base); this.name = "GitHttpError"; this.status = status; } }; function extractServerMessage(responseText) { try { const parsed = JSON.parse(responseText); return typeof parsed.message === "string" ? parsed.message : ""; } catch { return ""; } } function utf8ToBase64(text) { const bytes = new TextEncoder().encode(text); let binary = ""; const chunkSize = 32768; for (let offset = 0; offset < bytes.length; offset += chunkSize) binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize)); return btoa(binary); } function base64ToUtf8(base64) { const binary = atob(base64.replace(/\s/g, "")); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index); return new TextDecoder().decode(bytes); } function buildContentsPath(folder, filename) { const parts = [folder]; if (filename) parts.push(filename); return parts.map((part) => encodeURIComponent(part)).join("/"); } var GitBackupClient = class { apiUrl; repo; branch; token; folderName = null; constructor(config) { const baseUrl = (config.apiUrl || "https://api.github.com").trim(); this.apiUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl; this.repo = config.repo.trim(); this.branch = (config.branch || "main").trim(); this.token = config.token; } buildHeaders(extra = {}) { return { Accept: "application/vnd.github+json", Authorization: `Bearer ${this.token}`, "X-GitHub-Api-Version": "2022-11-28", "User-Agent": "JavDB-Power-Tools-Backup", ...extra }; } sendRequest(method, path, body, extraHeaders = {}) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method, url: this.apiUrl + path, headers: this.buildHeaders(extraHeaders), data: body !== void 0 ? JSON.stringify(body) : void 0, onload: (response) => { if (response.status >= 200 && response.status < 300) resolve(response); else reject(new GitHttpError(response.status, response.statusText, extractServerMessage(response.responseText))); }, onerror: (error) => { clog.error("请求 Git 备份服务发生错误:", error); reject(new Error("请求 Git 备份服务失败, 请检查网络/代理、API 地址与 Token")); } }); }); } contentsPath(folder, filename) { return `/repos/${this.repo}/contents/${buildContentsPath(folder, filename)}?ref=${encodeURIComponent(this.branch)}`; } async fetchFileMeta(folder, filename) { try { const response = await this.sendRequest("GET", this.contentsPath(folder, filename)); const parsed = JSON.parse(response.responseText); if (parsed.type === "file" && parsed.sha) return parsed; throw new Error(`路径 ${folder}/${filename} 不是文件`); } catch (error) { if (error instanceof GitHttpError && error.status === 404) return null; throw error; } } async fetchLastCommitDate(folder, filename) { try { const path = `/repos/${this.repo}/commits?path=${buildContentsPath(folder, filename)}&sha=${encodeURIComponent(this.branch)}`; const response = await this.sendRequest("GET", path); const latest = JSON.parse(response.responseText)[0]; return latest?.commit?.committer?.date || latest?.commit?.author?.date || ""; } catch { return ""; } } async backup(folder, filename, content) { const meta = await this.fetchFileMeta(folder, filename); const body = { message: `jhs-backup: ${filename} ${new Date().toISOString()}`, content: utf8ToBase64(content), branch: this.branch }; if (meta) body.sha = meta.sha; await this.sendRequest("PUT", this.contentsPath(folder, filename), body); } async getBackupList(folder) { this.folderName = folder; let entries; try { const response = await this.sendRequest("GET", this.contentsPath(folder)); const parsed = JSON.parse(response.responseText); entries = Array.isArray(parsed) ? parsed : []; } catch (error) { if (error instanceof GitHttpError && error.status === 404) return []; throw error; } const files = entries.filter((entry) => entry.type === "file"); const items = await Promise.all(files.map(async (entry) => ({ fileId: entry.name, name: entry.name, size: entry.size, createTime: await this.fetchLastCommitDate(folder, entry.name) }))); items.reverse(); return items; } async getFileContent(filename) { const folder = this.requireFolder(); const path = this.contentsPath(folder, filename); const response = await this.sendRequest("GET", path); const parsed = JSON.parse(response.responseText); if (typeof parsed.content === "string" && parsed.content !== "") return parsed.encoding === "base64" ? base64ToUtf8(parsed.content) : parsed.content; if ((parsed.size ?? 0) === 0) return ""; return (await this.sendRequest("GET", path, void 0, { Accept: "application/vnd.github.raw+json" })).responseText; } async deleteFile(filename) { const folder = this.requireFolder(); const meta = await this.fetchFileMeta(folder, filename); if (!meta) throw new Error(`文件 ${filename} 不存在或已被删除`); await this.sendRequest("DELETE", this.contentsPath(folder, filename), { message: `jhs-delete: ${filename} ${new Date().toISOString()}`, sha: meta.sha, branch: this.branch }); } requireFolder() { if (!this.folderName) throw new Error("Git 备份目录未初始化(需先调用 getBackupList)"); return this.folderName; } }; var GIT_REPO_PATTERN = /^[\w.-]+\/[\w.-]+$/; function getGitCredentials() { const apiUrl = ($("#gitApiUrl").val() || GM_getValue("jhs_git_api_url", "")).trim() || "https://api.github.com"; const repo = ($("#gitRepo").val() || GM_getValue("jhs_git_repo", "")).trim(); const branch = ($("#gitBranch").val() || GM_getValue("jhs_git_branch", "")).trim() || "main"; const token = $("#gitToken").val() || GM_getValue("jhs_git_token", ""); if (!repo) { show.error("请填写 Git 仓库(owner/repo)并保存后, 再试此功能"); return null; } if (!GIT_REPO_PATTERN.test(repo)) { show.error("Git 仓库格式应为 owner/repo"); return null; } if (!token) { show.error("请填写 Git Token 并保存后, 再试此功能"); return null; } return { apiUrl, repo, branch, token }; } function getGitCredentialsFromGm() { const repo = GM_getValue(GM_KEY_GIT_REPO, "").trim(); const token = GM_getValue(GM_KEY_GIT_TOKEN, ""); if (!repo || !GIT_REPO_PATTERN.test(repo) || !token) return null; return { apiUrl: GM_getValue("jhs_git_api_url", "").trim() || "https://api.github.com", repo, branch: GM_getValue("jhs_git_branch", "").trim() || "main", token }; } function buildGitClient(creds) { return new GitBackupClient(creds); } async function backupDataByGit(plugin) { const creds = getGitCredentials(); if (!creds) return; const settings = await storageManager.getSetting(); const fileName = utils.getNowStr("_", "_") + ".json"; const exportText = JSON.stringify(await buildBackupPayload(settings)); try { await withLoading(async () => { await buildGitClient(creds).backup(plugin.folderName, fileName, exportText); show.ok("备份完成"); }); } catch (err) { clog.error(err); show.error("备份失败: " + (err instanceof Error ? err.message : String(err))); } } async function backupListBtnByGit(plugin) { const creds = getGitCredentials(); if (!creds) return; try { await withLoading(async () => { const gitClient = buildGitClient(creds); openFileListDialog(plugin, await gitClient.getBackupList(plugin.folderName), gitClient, "Git"); }); } catch (err) { clog.error(err); show.error(`发生错误: ${err instanceof Error ? err.message : String(err)}`); } } function bindClick$1(plugin) { $(".side-menu-item").on("click", function() { $(".side-menu-item").removeClass("active"); $(this).addClass("active"); $(".content-panel").hide(); const panelName = $(this).data("panel"); $("#" + panelName).show(); if (panelName === "cache-panel") { $("#saveBtn").hide(); $("#clean-all").show(); refreshAllCacheStats(plugin); } else if (panelName === "vlt-panel") { $("#saveBtn").show(); $("#clean-all").hide(); } else if (panelName === "missav-panel") { $("#saveBtn").hide(); $("#clean-all").hide(); } else if (panelName === "fav-sync-panel") { $("#saveBtn").hide(); $("#clean-all").hide(); } else if (panelName === "preload-panel") { $("#saveBtn").show(); $("#clean-all").hide(); refreshPreloadCacheStats(plugin); } else if (panelName === "diagnostics-panel") { $("#saveBtn").hide(); $("#clean-all").hide(); plugin.refreshDiagnostics(); } else { $("#saveBtn").show(); $("#clean-all").hide(); } }); $("#importBtn").on("click", () => importData(plugin)); $("#exportBtn").on("click", () => exportData(plugin)); $("#webdavBackupBtn").on("click", () => backupDataByWebDav(plugin)); $("#webdavBackupListBtn").on("click", () => backupListBtnByWebDav(plugin)); $("#gitBackupBtn").on("click", () => backupDataByGit(plugin)); $("#gitBackupListBtn").on("click", () => backupListBtnByGit(plugin)); $("#saveBtn").on("click", () => saveForm(plugin)); $("#copy-diagnostics-btn").on("click", () => { const text = renderDiagnosticsText(plugin.pluginManager); navigator.clipboard.writeText(text).then(() => show.ok("诊断报告已复制到剪贴板"), () => show.error("复制失败,请手动选择文本复制")); }); $("#refresh-diagnostics-btn").on("click", () => { plugin.refreshDiagnostics(); show.ok("诊断报告已刷新"); }); refreshAllCacheStats(plugin); $(".clean-btn").on("click", (event) => handleCleanCache(plugin, event)); $("#clean-all").on("click", () => handleCleanAllCache(plugin)); $(".view-btn").on("click", (event) => handleViewCache(event)); $(".export-btn").on("click", (event) => handleExportCache(plugin, event)); const $tagNumber = $("#highlightedTagNumber"); const $tagColor = $("#highlightedTagColor"); const $tagLabel = $("#highlightedTagLabel"); function updateBorder() { const number = $tagNumber.val(); const color = $tagColor.val(); $tagLabel.css("border", `${number}px solid ${color}`); } $tagNumber.on("input", updateBorder); $tagColor.on("input", updateBorder); $("#vlt-import-btn").on("click", () => handleVltImportClick()); $("#vlt-file-input").on("change", (event) => handleVltFileChange(event)); $("#vlt-export-btn").on("click", () => handleVltExportClick()); bindFavSyncPanel(); $("#missav-sync-btn").on("click", () => handleMissavSync()); $("#missav-import-btn").on("click", () => handleMissavImportClick()); $("#missav-file-input").on("change", (event) => handleMissavFileChange(event)); $("#missav-export-btn").on("click", () => handleMissavExportClick()); } async function initSimpleSettingForm(plugin) { const settings = await storageManager.getSetting(); const ns = ".jhsQs"; $("#containerColumns").val(settings.containerColumns || 4); $("#showContainerColumns").text(settings.containerColumns || 4); $("#containerWidth").val((settings.containerWidth || 70) - 70); $("#showContainerWidth").text((settings.containerWidth || 70) + "%"); $("#dialogOpenDetail").prop("checked", !settings.dialogOpenDetail || settings.dialogOpenDetail === "yes"); $("#needClosePage").prop("checked", !settings.needClosePage || settings.needClosePage === "yes"); const loadMode = settings.autoPageLoadMode === "click" ? "click" : "auto"; $("#autoPageLoadMode").attr("data-value", loadMode); $("#autoPageLoadMode .jhs-qs-seg-btn").removeClass("is-active").filter(`[data-value="${loadMode}"]`).addClass("is-active"); $("#translateTitle").prop("checked", !settings.translateTitle || settings.translateTitle === "yes"); $("#enableLoadActressInfo").prop("checked", !settings.enableLoadActressInfo || settings.enableLoadActressInfo === "yes"); $("#enableLoadOtherSite").prop("checked", !settings.enableLoadOtherSite || settings.enableLoadOtherSite === "yes"); $("#containerColumns").off(`input${ns}`).on(`input${ns}`, async () => { const columns = $("#containerColumns").val(); $("#showContainerColumns").text(String(columns ?? "")); if (isJavdbSite) { const movieListEl = document.querySelector(".movie-list"); if (movieListEl) movieListEl.style.gridTemplateColumns = `repeat(${columns}, minmax(0, 1fr))`; } await storageManager.saveSettingItem("containerColumns", columns); plugin.applyImageMode(); }); $("#containerWidth").off(`input${ns}`).on(`input${ns}`, async (event) => { const rangeValue = parseInt(String($(event.target).val()), 10) || 0; const widthPercent = rangeValue + 70 + "%"; $("#showContainerWidth").text(widthPercent); if (isJavdbSite) { const containerEl = document.querySelector("section .container"); if (containerEl) containerEl.style.minWidth = widthPercent; } storageManager.saveSettingItem("containerWidth", rangeValue + 70); }); $("#dialogOpenDetail").off(`change${ns}`).on(`change${ns}`, () => { const value = $("#dialogOpenDetail").is(":checked") ? "yes" : "no"; storageManager.saveSettingItem("dialogOpenDetail", value); }); $("#showFilterItem").prop("checked", !!settings.showFilterItem && settings.showFilterItem === "yes"); $("#showFilterActorItem").prop("checked", !!settings.showFilterActorItem && settings.showFilterActorItem === "yes"); $("#showFilterKeywordItem").prop("checked", !!settings.showFilterKeywordItem && settings.showFilterKeywordItem === "yes"); $("#showFavoriteItem").prop("checked", !settings.showFavoriteItem || settings.showFavoriteItem === "yes"); $("#showHasWatchItem").prop("checked", !settings.showHasWatchItem || settings.showHasWatchItem === "yes"); $("#showFilterItem").off(`change${ns}`).on(`change${ns}`, async () => { const value = $("#showFilterItem").is(":checked") ? "yes" : "no"; await storageManager.saveSettingItem("showFilterItem", value); refresh(); }); $("#showFilterActorItem").off(`change${ns}`).on(`change${ns}`, async () => { const value = $("#showFilterActorItem").is(":checked") ? "yes" : "no"; await storageManager.saveSettingItem("showFilterActorItem", value); refresh(); }); $("#showFilterKeywordItem").off(`change${ns}`).on(`change${ns}`, async () => { const value = $("#showFilterKeywordItem").is(":checked") ? "yes" : "no"; await storageManager.saveSettingItem("showFilterKeywordItem", value); refresh(); }); $("#showFavoriteItem").off(`change${ns}`).on(`change${ns}`, async () => { const value = $("#showFavoriteItem").is(":checked") ? "yes" : "no"; await storageManager.saveSettingItem("showFavoriteItem", value); refresh(); }); $("#showHasWatchItem").off(`change${ns}`).on(`change${ns}`, async () => { const value = $("#showHasWatchItem").is(":checked") ? "yes" : "no"; await storageManager.saveSettingItem("showHasWatchItem", value); refresh(); }); const $filterCheckboxes = $("#showFilterItem, #showFilterActorItem, #showFilterKeywordItem, #showFavoriteItem, #showHasWatchItem"); const updateDisabledState = () => { const isChecked = $("#showAllItem").is(":checked"); $filterCheckboxes.prop("disabled", isChecked); if (isChecked) { $filterCheckboxes.attr("data-tip", "请先关闭显示所有才可点击"); $("#jhs-qs-filter-group").addClass("is-disabled"); } else { $filterCheckboxes.removeAttr("data-tip"); $("#jhs-qs-filter-group").removeClass("is-disabled"); } }; $("#showAllItem").prop("checked", !!settings.showAllItem && settings.showAllItem === "yes"); $("#showAllItem").off(`change${ns}`).on(`change${ns}`, async () => { const value = $("#showAllItem").is(":checked") ? "yes" : "no"; await storageManager.saveSettingItem("showAllItem", value); updateDisabledState(); refresh(); }); updateDisabledState(); $("#needClosePage").off(`change${ns}`).on(`change${ns}`, async () => { await storageManager.saveSettingItem("needClosePage", $("#needClosePage").is(":checked") ? "yes" : "no"); refresh(); }); $("#autoPageLoadMode").off(`click${ns}`).on(`click${ns}`, ".jhs-qs-seg-btn", async (event) => { const value = $(event.currentTarget).attr("data-value") === "click" ? "click" : "auto"; const $seg = $("#autoPageLoadMode"); if ($seg.attr("data-value") === value) return; $seg.attr("data-value", value); $seg.find(".jhs-qs-seg-btn").removeClass("is-active"); $(event.currentTarget).addClass("is-active"); await storageManager.saveSettingItem("autoPageLoadMode", value); const autoPagePlugin = plugin.getBean("AutoPagePlugin"); autoPagePlugin?.setLoadMode?.(value); autoPagePlugin?.showLoadAllBtn?.(); }); $("#translateTitle").off(`change${ns}`).on(`change${ns}`, async () => { const value = $("#translateTitle").is(":checked") ? "yes" : "no"; await storageManager.saveSettingItem("translateTitle", value); if (value === "yes") await plugin.getBean("ListPagePlugin").doFilter(); else { await plugin.getBean("ListPagePlugin").revertTranslation(); $(".translated-title").remove(); } }); $("#enableLoadActressInfo").off(`change${ns}`).on(`change${ns}`, async () => { const value = $("#enableLoadActressInfo").is(":checked") ? "yes" : "no"; await storageManager.saveSettingItem("enableLoadActressInfo", value); if (value === "yes") plugin.getBean("ActressInfoPlugin").loadActressInfo(); else $(".actress-info").remove(); }); $("#enableLoadOtherSite").off(`change${ns}`).on(`change${ns}`, async () => { const value = $("#enableLoadOtherSite").is(":checked") ? "yes" : "no"; await storageManager.saveSettingItem("enableLoadOtherSite", value); if (value === "yes") plugin.getBean("OtherSitePlugin").loadOtherSite().then(); else $("#otherSiteBox").remove(); }); $("#moreBtn").off(`click${ns}`).on(`click${ns}`, () => { plugin.cancelSimpleSettingHide(); $(".simple-setting, .mini-simple-setting").html("").hide(); plugin.openSettingDialog("base-panel"); }); $("#helpBtn").off(`click${ns}`).on(`click${ns}`, () => { layer.open({ type: 1, title: "", shadeClose: true, scrollbar: false, content: jsxToString((0, import_react.createElement)(StyleBlock, { css: help_dialog_default })) + jsxToString((0, import_react.createElement)(HelpDialog, null)), area: utils.getResponsiveArea(["50%", "90%"]) }); }); } function KeywordLabel({ keyword, bgColor, textColor, variant, href }) { const style = { backgroundColor: bgColor, color: textColor }; if (variant === "link") return (0, import_jsx_runtime.jsxs)("a", { className: "keyword-label", "data-keyword": keyword, style, href, target: "_blank", children: [keyword, (0, import_jsx_runtime.jsx)("span", { className: "keyword-remove", children: "×" })] }); return (0, import_jsx_runtime.jsxs)("div", { className: "keyword-label", "data-keyword": keyword, style, children: [keyword, (0, import_jsx_runtime.jsx)("span", { className: "keyword-remove", children: "×" })] }); } function addLabelTag(containerSelector, keyword, noun = "屏蔽词") { const $tagBox = $(`${containerSelector} .tag-box`); let $label; const bgColor = "#cbd5e1"; let textColor = "#333"; if (/^[a-z]{2,}-/i.test(keyword) && isJavdbSite) { textColor = "#3477ad"; $label = $(jsxToString((0, import_react.createElement)(KeywordLabel, { keyword, bgColor, textColor, variant: "link", href: `/video_codes/${keyword.replace("-", "")}` }))); } else $label = $(jsxToString((0, import_react.createElement)(KeywordLabel, { keyword, bgColor, textColor, variant: "div" }))); $label.find(".keyword-remove").click((event) => { event.stopPropagation(); event.preventDefault(); const $removeBtn = $(event.currentTarget); const removeKeyword = ($removeBtn.closest(".keyword-label").attr("data-keyword") ?? "").split(" ")[0]; utils.q(event, `是否移除${noun} ${removeKeyword}?`, async () => { $removeBtn.parent().remove(); }); }); $tagBox.append($label); } function addKeyword(containerSelector, noun = "屏蔽词") { const $input = $(`${containerSelector} .keyword-input`); const keyword = String($input.val() ?? "").trim(); if (keyword) { addLabelTag(containerSelector, keyword, noun); $input.val(""); } } function SettingButton({ id, color, children }) { return (0, import_jsx_runtime.jsx)("a", { id, className: `jhs-setting-btn jhs-setting-btn--${color}`, children }); } function SettingCheckbox({ id, label }) { return (0, import_jsx_runtime.jsxs)("label", { className: "jhs-checkbox-label", children: [(0, import_jsx_runtime.jsx)("input", { id, type: "checkbox" }), (0, import_jsx_runtime.jsx)("span", { children: label })] }); } function SettingInput({ id, type = "text", placeholder, readOnly, min, max, step, style }) { return (0, import_jsx_runtime.jsx)("input", { id, type, placeholder, readOnly, min, max, step, style }); } function SettingRow({ label, tooltip, children, className }) { return (0, import_jsx_runtime.jsxs)("div", { className: `setting-item${className ? " " + className : ""}`, children: [(0, import_jsx_runtime.jsxs)("span", { className: "setting-label", children: [label, tooltip ? (0, import_jsx_runtime.jsx)("span", { className: "jhs-tooltip-icon", "data-tip": tooltip, children: "?" }) : null] }), (0, import_jsx_runtime.jsx)("div", { className: "form-content", children })] }); } function SettingSection({ title, icon, children }) { return (0, import_jsx_runtime.jsxs)("div", { className: "jhs-setting-section", children: [title ? (0, import_jsx_runtime.jsxs)("div", { className: "jhs-setting-section-title", children: [icon ? (0, import_jsx_runtime.jsx)("span", { children: icon }) : null, title] }) : null, children] }); } function SettingSelect({ id, children, dangerouslySetInnerHTML }) { return (0, import_jsx_runtime.jsx)("select", { id, dangerouslySetInnerHTML, children }); } function BackupPanel({ panelName }) { return (0, import_jsx_runtime.jsxs)("div", { id: "backup-panel", className: "content-panel", style: { display: panelName === "backup-panel" ? "block" : "none" }, children: [ (0, import_jsx_runtime.jsx)(SettingSection, { children: (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "10px" }, children: [(0, import_jsx_runtime.jsx)(SettingButton, { id: "importBtn", color: "pink", children: "📥 导入数据" }), (0, import_jsx_runtime.jsx)(SettingButton, { id: "exportBtn", color: "mint", children: "📤 导出数据" })] }) }), (0, import_jsx_runtime.jsxs)(SettingSection, { title: "WebDav 备份", icon: "☁️", children: [ (0, import_jsx_runtime.jsx)(SettingRow, { label: "WebDav备份", children: (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [(0, import_jsx_runtime.jsx)(SettingButton, { id: "webdavBackupListBtn", color: "primary", children: "查看备份" }), (0, import_jsx_runtime.jsx)(SettingButton, { id: "webdavBackupBtn", color: "success", children: "备份数据" })] }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "服务地址:", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "webDavUrl" }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "用户名:", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "webDavUsername" }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "密码:", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "webDavPassword", type: "password" }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "备份口令:", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "backupPassword", type: "password", placeholder: "加密备份的口令(V2 格式)" }) }) ] }), (0, import_jsx_runtime.jsxs)(SettingSection, { title: "Git 备份(GitHub 兼容)", icon: "🔀", children: [ (0, import_jsx_runtime.jsx)(SettingRow, { label: "Git备份", children: (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [(0, import_jsx_runtime.jsx)(SettingButton, { id: "gitBackupListBtn", color: "primary", children: "查看备份" }), (0, import_jsx_runtime.jsx)(SettingButton, { id: "gitBackupBtn", color: "success", children: "备份数据" })] }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "API 地址:", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "gitApiUrl", placeholder: "默认 https://api.github.com(自建 Gitea 填 …/api/v1)" }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "仓库:", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "gitRepo", placeholder: "owner/repo(请先建好私有仓库)" }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "分支:", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "gitBranch", placeholder: "默认 main" }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "Token:", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "gitToken", type: "password", placeholder: "PAT(仅需仓库内容读写权限)" }) }) ] }), (0, import_jsx_runtime.jsxs)(SettingSection, { title: "自动备份", icon: "⏰", children: [ (0, import_jsx_runtime.jsx)(SettingRow, { label: "启用自动备份", children: (0, import_jsx_runtime.jsx)(SettingCheckbox, { id: "enableAutoBackup", label: "开启后按频率自动备份到已启用的通道" }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "备份通道:", children: (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "16px" }, children: [(0, import_jsx_runtime.jsx)(SettingCheckbox, { id: "enableWebdavChannel", label: "WebDav" }), (0, import_jsx_runtime.jsx)(SettingCheckbox, { id: "enableGitChannel", label: "Git" })] }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "备份频率:", children: (0, import_jsx_runtime.jsxs)(SettingSelect, { id: "autoBackupFrequency", children: [ (0, import_jsx_runtime.jsx)("option", { value: "daily", children: "每天第一次打开(推荐)" }), (0, import_jsx_runtime.jsx)("option", { value: "everyOpen", children: "每次打开" }), (0, import_jsx_runtime.jsx)("option", { value: "disabled", children: "不自动备份" }) ] }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "本机凭证:", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "credentialIdDisplay", readOnly: true, style: { fontSize: "11px", fontFamily: "monospace" } }) }) ] }) ] }); } function SettingColorRow({ numberId, colorId, labelId, label, className }) { return (0, import_jsx_runtime.jsx)(SettingRow, { label, className, children: (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "8px", alignItems: "center" }, children: [ (0, import_jsx_runtime.jsx)("input", { id: numberId, type: "number", style: { width: "80px", flexShrink: 0 } }), (0, import_jsx_runtime.jsx)("input", { id: colorId, type: "color", style: { flexShrink: 0 } }), (0, import_jsx_runtime.jsx)("span", { id: labelId, style: { display: "inline-block", padding: "4px 12px", borderRadius: "4px", fontSize: "13px", border: "1px solid #ce2222" }, children: "示例标签" }) ] }) }); } function SettingToggle({ id, label, tooltip, className }) { return (0, import_jsx_runtime.jsx)(SettingRow, { label, tooltip, className, children: (0, import_jsx_runtime.jsx)("input", { id, type: "checkbox", className: "jhs-toggle" }) }); } function BasePanel({ panelName, qualityOptionsHtml, isJavdbSite }) { return (0, import_jsx_runtime.jsxs)("div", { id: "base-panel", className: "content-panel", style: { display: panelName === "base-panel" ? "block" : "none" }, children: [ (0, import_jsx_runtime.jsxs)(SettingSection, { title: "展示配置", icon: "🎛️", children: [ (0, import_jsx_runtime.jsx)(SettingRow, { label: "打开待鉴定|已收藏 窗口数:", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "waitCheckCount", type: "number", min: "1", max: "20" }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "已鉴定标签展示位置:", children: (0, import_jsx_runtime.jsxs)(SettingSelect, { id: "tagPosition", children: [(0, import_jsx_runtime.jsx)("option", { value: "rightTop", children: "右上" }), (0, import_jsx_runtime.jsx)("option", { value: "leftTop", children: "左上" })] }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "已鉴定内容展示方式", tooltip: "hide=直接隐藏;visibility=透明占位保留布局", children: (0, import_jsx_runtime.jsxs)(SettingSelect, { id: "movieShowType", children: [(0, import_jsx_runtime.jsx)("option", { value: "hide", children: "隐藏 (hide)" }), (0, import_jsx_runtime.jsx)("option", { value: "visibility", children: "透明占位 (visibility)" })] }) }), (0, import_jsx_runtime.jsx)(SettingToggle, { id: "enableSaveActressCarInfo", label: "鉴定补录演员信息", tooltip: "在列表页进行鉴定是获取不到演员名称的, 开启后, 额外解析详情页补录演员名称, 因发请求解析费时, 会被以往慢1秒左右" }) ] }), (0, import_jsx_runtime.jsxs)(SettingSection, { title: "播放与评论", icon: "🎬", children: [ (0, import_jsx_runtime.jsx)(SettingRow, { label: "预览视频默认画质:", children: (0, import_jsx_runtime.jsx)(SettingSelect, { id: "videoQuality", dangerouslySetInnerHTML: { __html: qualityOptionsHtml } }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "评论区条数:", children: (0, import_jsx_runtime.jsxs)(SettingSelect, { id: "reviewCount", children: [ (0, import_jsx_runtime.jsx)("option", { value: "10", children: "10条" }), (0, import_jsx_runtime.jsx)("option", { value: "20", children: "20条" }), (0, import_jsx_runtime.jsx)("option", { value: "30", children: "30条" }), (0, import_jsx_runtime.jsx)("option", { value: "40", children: "40条" }), (0, import_jsx_runtime.jsx)("option", { value: "50", children: "50条" }) ] }) }), (0, import_jsx_runtime.jsx)(SettingToggle, { id: "enableFavoriteActresses", label: "高亮已收藏演员", tooltip: "详情页, 对已收藏的演员进行边框高亮提醒", className: isJavdbSite ? "" : "do-hide" }), (0, import_jsx_runtime.jsx)(SettingColorRow, { numberId: "highlightedTagNumber", colorId: "highlightedTagColor", labelId: "highlightedTagLabel", label: "分类标签|高亮演员-边框样式:", className: isJavdbSite ? "" : "do-hide" }) ] }), (0, import_jsx_runtime.jsxs)(SettingSection, { title: "网络请求", icon: "🌐", children: [(0, import_jsx_runtime.jsx)(SettingRow, { label: "请求超时时间(毫秒):", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "httpTimeout", type: "number", min: "1000", max: "10000" }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "请求失败重试次数:", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "httpRetryCount", type: "number", min: "0", max: "10" }) })] }), (0, import_jsx_runtime.jsxs)(SettingSection, { title: "日志", icon: "📝", children: [(0, import_jsx_runtime.jsx)(SettingRow, { label: "启用控制台日志:", children: (0, import_jsx_runtime.jsxs)(SettingSelect, { id: "enableClog", children: [(0, import_jsx_runtime.jsx)("option", { value: "no", children: "禁用" }), (0, import_jsx_runtime.jsx)("option", { value: "yes", children: "开启" })] }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "日志最大行数:", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "clogMsgCount", type: "number", min: "100", max: "3000" }) })] }) ] }); } function FilterPanel({ panelName }) { return (0, import_jsx_runtime.jsx)("div", { id: "filter-panel", className: "content-panel", style: { display: panelName === "filter-panel" ? "block" : "none" }, children: (0, import_jsx_runtime.jsx)(SettingSection, { title: "屏蔽关键词", icon: "🚫", children: (0, import_jsx_runtime.jsxs)("div", { id: "filterKeywordContainer", className: "filter-keyword-container", children: [(0, import_jsx_runtime.jsx)("div", { className: "tag-box" }), (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "8px", marginTop: "8px" }, children: [(0, import_jsx_runtime.jsx)("input", { type: "text", className: "keyword-input", placeholder: "输入关键词后按回车添加", style: { flex: 1 } }), (0, import_jsx_runtime.jsx)("a", { className: "add-tag-btn jhs-setting-btn jhs-setting-btn--primary", style: { flexShrink: 0 }, children: "添加" })] })] }) }) }); } function DomainPanel({ panelName }) { return (0, import_jsx_runtime.jsx)("div", { id: "domain-panel", className: "content-panel", style: { display: panelName === "domain-panel" ? "block" : "none" }, children: (0, import_jsx_runtime.jsxs)(SettingSection, { title: "第三方视频资源域名", icon: "🌐", children: [(0, import_jsx_runtime.jsx)(SettingRow, { label: "域名 - MissAv:", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "missAvUrl" }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "域名 - SupJav:", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "supJavUrl" }) })] }) }); } function CachePanel({ panelName, cacheItemsHtml }) { return (0, import_jsx_runtime.jsx)("div", { id: "cache-panel", className: "content-panel", style: { display: panelName === "cache-panel" ? "block" : "none" }, children: (0, import_jsx_runtime.jsxs)(SettingSection, { title: "缓存管理", icon: "🧹", children: [ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "4px" }, children: (0, import_jsx_runtime.jsx)("span", { id: "cache-total-size", style: { fontSize: "13px", color: "#999" } }) }), (0, import_jsx_runtime.jsx)("p", { style: { color: "#999", fontSize: "12px", margin: "0 0 16px 0" }, children: "以下操作不会对核心数据造成影响,可安全清理" }), (0, import_jsx_runtime.jsx)("div", { style: { display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: "15px" }, dangerouslySetInnerHTML: { __html: cacheItemsHtml } }), (0, import_jsx_runtime.jsx)("div", { id: "cache-data-display", style: { marginTop: "20px", display: "none" }, children: (0, import_jsx_runtime.jsx)("pre", { style: { background: "#f5f5f5", padding: "10px", borderRadius: "5px", maxHeight: "400px", overflow: "auto" } }) }) ] }) }); } function SyncPanel({ panelId, panelName, title, icon, description, buttons, statusId, fileInputId }) { return (0, import_jsx_runtime.jsx)("div", { id: panelId, className: "content-panel", style: { display: panelName === panelId ? "block" : "none" }, children: (0, import_jsx_runtime.jsxs)(SettingSection, { title, icon, children: [ (0, import_jsx_runtime.jsx)("p", { style: { margin: "0 0 16px 0", color: "#6c757d", fontSize: "13px", lineHeight: "1.6" }, children: description }), (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", gap: "12px", marginBottom: "20px" }, children: buttons.map((btn) => (0, import_jsx_runtime.jsx)(SettingButton, { id: btn.id, color: btn.color, children: btn.text }, btn.id)) }), (0, import_jsx_runtime.jsx)("div", { id: statusId, style: { fontSize: "13px", color: "#6c757d", marginTop: "8px" } }), (0, import_jsx_runtime.jsx)("input", { type: "file", id: fileInputId, accept: ".json,application/json", style: { display: "none" } }) ] }) }); } function ListManagerPanel({ panelName }) { return (0, import_jsx_runtime.jsxs)("div", { id: "vlt-panel", className: "content-panel", style: { display: panelName === "vlt-panel" ? "block" : "none" }, children: [ (0, import_jsx_runtime.jsxs)(SettingSection, { title: "序号合并", icon: "🗂️", children: [ (0, import_jsx_runtime.jsx)(SettingToggle, { id: "listMergeEnabled", label: "启用序号清单合并", tooltip: "详情页清单面板中,将「文字+数字」结尾的清单(如 等待更新1/等待更新2)按前缀合并为一行;勾选时自动放入最靠前的未满清单,全满自动新建下一序号清单" }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "合并匹配正则:", tooltip: "需含两个捕获组:第1组=前缀文字,第2组=末尾序号。留空使用默认规则", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "listMergePattern", placeholder: "^(.+?)(\\d+)$" }) }), (0, import_jsx_runtime.jsx)("div", { style: { margin: "12px 0 4px", fontWeight: 600, fontSize: "13px", color: "#3d4652" }, children: "不参与合并的清单名:" }), (0, import_jsx_runtime.jsxs)("div", { id: "listMergeExcludeNamesContainer", className: "filter-keyword-container", children: [(0, import_jsx_runtime.jsx)("div", { className: "tag-box" }), (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "8px", marginTop: "8px" }, children: [(0, import_jsx_runtime.jsx)("input", { type: "text", className: "keyword-input", placeholder: "输入清单名或前缀后按回车添加", style: { flex: 1 } }), (0, import_jsx_runtime.jsx)("a", { className: "add-tag-btn jhs-setting-btn jhs-setting-btn--primary", style: { flexShrink: 0 }, children: "添加" })] })] }) ] }), (0, import_jsx_runtime.jsx)(SettingSection, { title: "排序显示", icon: "📐", children: (0, import_jsx_runtime.jsx)(SettingRow, { label: "默认排序方式:", tooltip: "详情页清单面板打开时的默认排序(字段+方向,仍可在面板内临时切换)", children: (0, import_jsx_runtime.jsxs)(SettingSelect, { id: "listDefaultSort", children: [ (0, import_jsx_runtime.jsx)("option", { value: "name-desc", children: "名称降序" }), (0, import_jsx_runtime.jsx)("option", { value: "name-asc", children: "名称升序" }), (0, import_jsx_runtime.jsx)("option", { value: "count-desc", children: "数量降序" }), (0, import_jsx_runtime.jsx)("option", { value: "count-asc", children: "数量升序" }), (0, import_jsx_runtime.jsx)("option", { value: "num-desc", children: "序号降序" }), (0, import_jsx_runtime.jsx)("option", { value: "num-asc", children: "序号升序" }) ] }) }) }), (0, import_jsx_runtime.jsx)(SettingSection, { title: "编号整理", icon: "⚙️", children: (0, import_jsx_runtime.jsx)(SettingToggle, { id: "listReflowEnabled", label: "启用清单页编号整理", tooltip: "在 /lists/ 清单页显示「编号清单整理」面板:识别同前缀编号组、生成组内压实计划预览(当前版本只读,不执行搬运)" }) }), (0, import_jsx_runtime.jsxs)(SettingSection, { title: "压实执行", icon: "🚀", children: [ (0, import_jsx_runtime.jsx)(SettingRow, { label: "请求间隔(毫秒):", tooltip: "压实搬运时每个加入/移除请求之间的间隔;越大越温和、越慢,建议 300–800", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "reflowInterval", type: "number", min: "100", max: "5000" }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "试运行数量:", tooltip: "执行整理时先搬运这么多部并暂停,确认无误后再继续剩余", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "reflowTrialCount", type: "number", min: "1", max: "100" }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "每批数量:", tooltip: "断点续传的检查点粒度(每搬运这么多部存一次进度,期2b 生效)", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "reflowBatchSize", type: "number", min: "1", max: "200" }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "连续失败上限:", tooltip: "连续失败达到该次数后暂停并提示(期2b 自适应退避生效)", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "reflowMaxRetry", type: "number", min: "1", max: "20" }) }) ] }), (0, import_jsx_runtime.jsxs)(SettingSection, { title: "自动收藏联动", icon: "⭐", children: [ (0, import_jsx_runtime.jsx)(SettingToggle, { id: "autoFavCleanupEnabled", label: "启用自动收藏/移除联动", tooltip: "加入「除受保护清单外」的任意清单自动收藏(想看);从收藏改为已读星级时,自动从「除受保护清单外」的所有清单移出" }), (0, import_jsx_runtime.jsx)("div", { style: { margin: "12px 0 4px", fontWeight: 600, fontSize: "13px", color: "#3d4652" }, children: "受保护清单关键词:" }), (0, import_jsx_runtime.jsxs)("div", { id: "protectedListKeywordsContainer", className: "filter-keyword-container", children: [(0, import_jsx_runtime.jsx)("div", { className: "tag-box" }), (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "8px", marginTop: "8px" }, children: [(0, import_jsx_runtime.jsx)("input", { type: "text", className: "keyword-input", placeholder: "输入关键词后按回车添加(包含匹配)", style: { flex: 1 } }), (0, import_jsx_runtime.jsx)("a", { className: "add-tag-btn jhs-setting-btn jhs-setting-btn--primary", style: { flexShrink: 0 }, children: "添加" })] })] }) ] }), (0, import_jsx_runtime.jsxs)(SettingSection, { title: "对账校准", icon: "🔄", children: [(0, import_jsx_runtime.jsx)(SettingToggle, { id: "autoServerSync", label: "打开页面时自动校验同步", tooltip: "打开影片详情页时核实脚本与 JavDB 的状态:评分/已读/收藏或清单关联如果脚本已写入但服务器未同步(如网络失败),自动按脚本记录补推 JavDB;服务器有而脚本无则拉取补齐。勾选状态与服务器冲突时跳过,避免误覆盖" }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "自动对账延迟(毫秒):", tooltip: "详情页清单弹窗变动后,触发本地镜像自动对账的防抖延迟;过小易与服务端写入竞速产生良性校准提示,建议 600–1500", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "autoReconcileDelay", type: "number", min: "200", max: "5000" }) })] }), (0, import_jsx_runtime.jsxs)(SettingSection, { title: "数据管理", icon: "💾", children: [ (0, import_jsx_runtime.jsxs)("p", { style: { margin: "0 0 16px 0", color: "#6c757d", fontSize: "13px", lineHeight: "1.6" }, children: [ "管理影片与清单的关联数据(本地 IndexedDB,随 WebDav 备份)。", (0, import_jsx_runtime.jsx)("br", {}), (0, import_jsx_runtime.jsx)("strong", { children: "导入" }), ":从 JSON 文件导入数据(如 PostgreSQL 迁移数据)。", (0, import_jsx_runtime.jsx)("br", {}), (0, import_jsx_runtime.jsx)("strong", { children: "导出" }), ":将当前数据导出为 JSON 文件(备份/迁移)。" ] }), (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "12px", marginBottom: "20px" }, children: [(0, import_jsx_runtime.jsx)(SettingButton, { id: "vlt-import-btn", color: "primary", children: "导入数据" }), (0, import_jsx_runtime.jsx)(SettingButton, { id: "vlt-export-btn", color: "success", children: "导出数据" })] }), (0, import_jsx_runtime.jsx)("div", { id: "vlt-status", style: { fontSize: "13px", color: "#6c757d", marginTop: "8px" } }), (0, import_jsx_runtime.jsx)("input", { type: "file", id: "vlt-file-input", accept: ".json,application/json", style: { display: "none" } }) ] }) ] }); } function FavSyncPanel({ panelName }) { return (0, import_jsx_runtime.jsxs)("div", { id: "fav-sync-panel", className: "content-panel", style: { display: panelName === "fav-sync-panel" ? "block" : "none", overflowY: "auto", height: "100%" }, children: [(0, import_jsx_runtime.jsxs)(SettingSection, { title: "双向增量同步", icon: "🔄", children: [ (0, import_jsx_runtime.jsxs)("p", { style: { margin: "0 0 12px 0", color: "#6c757d", fontSize: "13px", lineHeight: "1.6" }, children: [ "双向增量同步「想看」和「已看」:脚本有/网站无 → 补到网站;网站有/脚本无 → 补到脚本。 冲突(双方状态不一致)仅报告番号,不自动处理。", (0, import_jsx_runtime.jsx)("br", {}), "支持 5 万+ 条目:分页抓取、断点续接、后台处理。关闭弹窗不中断同步,重新打开自动恢复进度。", (0, import_jsx_runtime.jsx)("br", {}), (0, import_jsx_runtime.jsx)("span", { style: { color: "#f40" }, children: "建议先备份数据。" }) ] }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "同步请求间隔(毫秒):", tooltip: "逐个操作时的请求间隔;越大越温和、越慢(建议 300-1000)", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "favSyncInterval", type: "number", min: "100", max: "5000" }) }), (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "10px", alignItems: "center", marginTop: "12px", flexWrap: "wrap" }, children: [ (0, import_jsx_runtime.jsx)(SettingButton, { id: "fav-sync-start-btn", color: "primary", children: "开始同步" }), (0, import_jsx_runtime.jsx)(SettingButton, { id: "fav-sync-resume-btn", color: "mint", children: "继续同步" }), (0, import_jsx_runtime.jsx)(SettingButton, { id: "fav-sync-pause-btn", color: "pink", children: "暂停" }), (0, import_jsx_runtime.jsx)(SettingButton, { id: "fav-sync-reset-btn", color: "danger", children: "重置" }) ] }) ] }), (0, import_jsx_runtime.jsxs)(SettingSection, { title: "同步进度", icon: "📊", children: [ (0, import_jsx_runtime.jsxs)("div", { style: { marginBottom: "12px" }, children: [(0, import_jsx_runtime.jsx)("div", { style: { height: "8px", borderRadius: "4px", background: "#e2e8f0", overflow: "hidden" }, children: (0, import_jsx_runtime.jsx)("div", { id: "fav-sync-progress-bar", style: { height: "100%", width: "0%", borderRadius: "4px", background: "#48c774", transition: "width 0.3s ease" } }) }), (0, import_jsx_runtime.jsx)("div", { id: "fav-sync-phase", style: { fontSize: "13px", color: "#6c757d", marginTop: "6px" }, children: "就绪" })] }), (0, import_jsx_runtime.jsxs)("div", { id: "fav-sync-stats", style: { display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: "8px", fontSize: "13px", marginBottom: "12px" }, children: [ (0, import_jsx_runtime.jsxs)("div", { style: { textAlign: "center", padding: "8px", background: "#f0fdf4", borderRadius: "6px" }, children: [(0, import_jsx_runtime.jsx)("div", { id: "fav-sync-stat-synced", style: { fontSize: "18px", fontWeight: 700, color: "#16a34a" }, children: "0" }), (0, import_jsx_runtime.jsx)("div", { style: { color: "#6c757d" }, children: "成功" })] }), (0, import_jsx_runtime.jsxs)("div", { style: { textAlign: "center", padding: "8px", background: "#fef2f2", borderRadius: "6px" }, children: [(0, import_jsx_runtime.jsx)("div", { id: "fav-sync-stat-failed", style: { fontSize: "18px", fontWeight: 700, color: "#dc2626" }, children: "0" }), (0, import_jsx_runtime.jsx)("div", { style: { color: "#6c757d" }, children: "失败" })] }), (0, import_jsx_runtime.jsxs)("div", { style: { textAlign: "center", padding: "8px", background: "#f8fafc", borderRadius: "6px" }, children: [(0, import_jsx_runtime.jsx)("div", { id: "fav-sync-stat-skipped", style: { fontSize: "18px", fontWeight: 700, color: "#6b7280" }, children: "0" }), (0, import_jsx_runtime.jsx)("div", { style: { color: "#6c757d" }, children: "跳过" })] }), (0, import_jsx_runtime.jsxs)("div", { style: { textAlign: "center", padding: "8px", background: "#fffbeb", borderRadius: "6px" }, children: [(0, import_jsx_runtime.jsx)("div", { id: "fav-sync-stat-conflicts", style: { fontSize: "18px", fontWeight: 700, color: "#d97706" }, children: "0" }), (0, import_jsx_runtime.jsx)("div", { style: { color: "#6c757d" }, children: "冲突" })] }) ] }), (0, import_jsx_runtime.jsx)("div", { id: "fav-sync-report", style: { fontSize: "12px", color: "#374151", whiteSpace: "pre-wrap", maxHeight: "200px", overflowY: "auto", padding: "10px", background: "#f9fafb", borderRadius: "6px", border: "1px solid #e5e7eb", display: "none" } }) ] })] }); } function PreloadPanel({ panelName }) { return (0, import_jsx_runtime.jsxs)("div", { id: "preload-panel", className: "content-panel", style: { display: panelName === "preload-panel" ? "block" : "none" }, children: [ (0, import_jsx_runtime.jsxs)(SettingSection, { title: "预加载行为", icon: "⚡", children: [ (0, import_jsx_runtime.jsx)(SettingToggle, { id: "enablePreload", label: "列表页预加载", tooltip: "在视频流(列表页/清单详情页/搜索页)后台预加载 missav 等站点搜索结果并缓存,打开详情页时按钮零延迟变绿。关闭后不预加载、不显示状态徽标与筛选栏(详情页按钮仍受「外部网站功能」总开关控制)" }), (0, import_jsx_runtime.jsx)(SettingToggle, { id: "enablePreloadStatus", label: "显示状态徽标与筛选栏", tooltip: "在每个卡片标题下方显示预加载状态(排队中/请求中/成功匹配/匹配失败),并在筛选栏实时计数、可点击过滤。关闭后仍后台预加载缓存,但不显示 UI" }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "预加载防抖延迟 (毫秒)", tooltip: "列表页瀑布流追加新卡片后,等待多久再开始批量入队预加载。过小可能与页面渲染抢资源;过大则新卡片状态延迟出现。缺省 300", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "preloadDebounce", type: "number", min: "0", max: "5000", step: "50" }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "预加载并发数 (1-10)", tooltip: "同时向 missav 等站点发起预加载请求的数量。1=串行最稳(默认,降低 Cloudflare 拦截风险);2~5 可明显加快列表预加载;过高易被拦截并触发本轮跳过该站点。建议从 2 试起", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "preloadConcurrency", type: "number", min: "1", max: "10", step: "1" }) }), (0, import_jsx_runtime.jsx)(SettingRow, { label: "预加载缓存有效期 (天)", tooltip: "命中缓存超过此天数后视为过期,下次打开列表页会重新预加载。0=永不过期(缺省)。建议 7-30 天平衡新鲜度与请求量", children: (0, import_jsx_runtime.jsx)(SettingInput, { id: "preloadCacheTTL", type: "number", min: "0", max: "365", step: "1" }) }) ] }), (0, import_jsx_runtime.jsxs)(SettingSection, { title: "预加载站点", icon: "🌍", children: [(0, import_jsx_runtime.jsx)(SettingRow, { label: "预加载站点:", children: (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "16px", flexWrap: "wrap", alignItems: "center" }, children: [(0, import_jsx_runtime.jsx)(SettingCheckbox, { id: "preload-enable-missAvBtn", label: "MissAv" }), (0, import_jsx_runtime.jsx)(SettingCheckbox, { id: "preload-enable-supJavBtn", label: "SupJav" })] }) }), (0, import_jsx_runtime.jsx)("p", { style: { fontSize: "12px", color: "#6c757d", margin: "8px 0 0" }, children: "SupJav 全站 Cloudflare 拦截严重,列表页预加载已跳过(仅详情页显示黄色搜索页链接),MissAv 为主要预加载源。" })] }), (0, import_jsx_runtime.jsxs)(SettingSection, { title: "缓存状态", icon: "📦", children: [(0, import_jsx_runtime.jsx)(SettingRow, { label: "预加载缓存状态:", children: (0, import_jsx_runtime.jsx)("div", { id: "preload-cache-stats", style: { fontSize: "13px", color: "#495057", lineHeight: "1.8" }, children: "加载中..." }) }), (0, import_jsx_runtime.jsx)("p", { style: { fontSize: "12px", color: "#6c757d", margin: "8px 0 0" }, children: "此缓存即「缓存管理」面板的「第三方站点缓存」(jhs_other_site),清理请前往该面板。总开关「外部网站功能」位于快捷设置面板。" })] }) ] }); } function DiagnosticsPanel({ panelName }) { return (0, import_jsx_runtime.jsx)("div", { id: "diagnostics-panel", className: "content-panel", style: { display: panelName === "diagnostics-panel" ? "block" : "none", overflowY: "auto" }, children: (0, import_jsx_runtime.jsxs)(SettingSection, { title: "插件执行状态", icon: "📊", children: [(0, import_jsx_runtime.jsx)("div", { id: "diagnostics-content" }), (0, import_jsx_runtime.jsxs)("div", { style: { marginTop: "12px" }, children: [(0, import_jsx_runtime.jsx)("button", { id: "copy-diagnostics-btn", className: "jhs-setting-btn jhs-setting-btn--primary", type: "button", children: "📋 复制诊断报告" }), (0, import_jsx_runtime.jsx)("button", { id: "refresh-diagnostics-btn", className: "jhs-setting-btn", type: "button", style: { marginLeft: "8px" }, children: "🔄 刷新诊断" })] })] }) }); } function SettingDialog({ panelName, cacheItemsHtml, qualityOptionsHtml, isJavdbSite }) { return (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", height: "100%" }, children: [(0, import_jsx_runtime.jsxs)("div", { style: { width: "170px", flexShrink: 0, padding: "12px 0", background: "#fbfcfd", borderRight: "1px solid #e2e8f0", overflowY: "auto" }, children: [ (0, import_jsx_runtime.jsx)("div", { className: `side-menu-item ${panelName === "backup-panel" ? "active" : ""}`, "data-panel": "backup-panel", children: "💾 数据备份" }), (0, import_jsx_runtime.jsx)("div", { className: `side-menu-item ${panelName === "base-panel" ? "active" : ""}`, "data-panel": "base-panel", children: "⚙️ 基础配置" }), (0, import_jsx_runtime.jsx)("div", { className: `side-menu-item ${panelName === "filter-panel" ? "active" : ""}`, "data-panel": "filter-panel", children: "🚫 屏蔽配置" }), (0, import_jsx_runtime.jsx)("div", { className: `side-menu-item ${panelName === "domain-panel" ? "active" : ""}`, "data-panel": "domain-panel", title: "第三方视频资源域名配置", children: "🌐 外部网站" }), (0, import_jsx_runtime.jsx)("div", { className: `side-menu-item ${panelName === "cache-panel" ? "active" : ""}`, "data-panel": "cache-panel", children: "🧹 缓存管理" }), (0, import_jsx_runtime.jsx)("div", { className: `side-menu-item ${panelName === "vlt-panel" ? "active" : ""}`, "data-panel": "vlt-panel", children: "📋 清单管理器" }), (0, import_jsx_runtime.jsx)("div", { className: `side-menu-item ${panelName === "fav-sync-panel" ? "active" : ""}`, "data-panel": "fav-sync-panel", title: "JHS ↔ JavDB 双向增量同步", children: "🔄 收藏同步" }), (0, import_jsx_runtime.jsx)("div", { className: `side-menu-item ${panelName === "missav-panel" ? "active" : ""}`, "data-panel": "missav-panel", children: "🎬 MissAV 同步" }), (0, import_jsx_runtime.jsx)("div", { className: `side-menu-item ${panelName === "preload-panel" ? "active" : ""}`, "data-panel": "preload-panel", title: "视频流外部网站预加载配置", children: "⚡ 预加载配置" }), (0, import_jsx_runtime.jsx)("div", { className: `side-menu-item ${panelName === "diagnostics-panel" ? "active" : ""}`, "data-panel": "diagnostics-panel", title: "插件执行状态诊断", children: "📊 插件诊断" }) ] }), (0, import_jsx_runtime.jsxs)("div", { style: { flex: 1, display: "flex", flexDirection: "column", height: "100%" }, children: [ (0, import_jsx_runtime.jsxs)("div", { style: { flex: 1, margin: "0 10px", paddingBottom: "20px", overflow: "hidden" }, children: [ (0, import_jsx_runtime.jsx)(BackupPanel, { panelName }), (0, import_jsx_runtime.jsx)(BasePanel, { panelName, qualityOptionsHtml, isJavdbSite }), (0, import_jsx_runtime.jsx)(FilterPanel, { panelName }), (0, import_jsx_runtime.jsx)(DomainPanel, { panelName }), (0, import_jsx_runtime.jsx)(CachePanel, { panelName, cacheItemsHtml }), (0, import_jsx_runtime.jsx)(ListManagerPanel, { panelName }), (0, import_jsx_runtime.jsx)(FavSyncPanel, { panelName }), (0, import_jsx_runtime.jsx)(SyncPanel, { panelId: "missav-panel", panelName, title: "MissAV 状态标签同步", icon: "🎬", description: (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ "在 MissAV 站点显示 JHS 鉴定状态标签(已收藏/已观看/已屏蔽/已下载)。", (0, import_jsx_runtime.jsx)("br", {}), "数据通过油猴 GM 存储跨域同步,无需后端服务器。", (0, import_jsx_runtime.jsx)("br", {}), (0, import_jsx_runtime.jsx)("strong", { children: "立即同步" }), ":读取当前 car_list 并推送到 GM 存储。", (0, import_jsx_runtime.jsx)("br", {}), (0, import_jsx_runtime.jsx)("strong", { children: "导入" }), ":从后端服务器导出的 JSON 导入历史数据。", (0, import_jsx_runtime.jsx)("br", {}), (0, import_jsx_runtime.jsx)("strong", { children: "导出" }), ":将 MissAV 本地 IndexedDB 数据导出为 JSON(可走 WebDav 备份)。" ] }), buttons: [ { id: "missav-sync-btn", text: "立即同步", color: "primary" }, { id: "missav-import-btn", text: "导入数据", color: "primary" }, { id: "missav-export-btn", text: "导出数据", color: "success" } ], statusId: "missav-status", fileInputId: "missav-file-input" }), (0, import_jsx_runtime.jsx)(PreloadPanel, { panelName }) ] }), (0, import_jsx_runtime.jsx)(DiagnosticsPanel, { panelName }), (0, import_jsx_runtime.jsxs)("div", { style: { flexShrink: 0, padding: "14px 20px", textAlign: "right", borderTop: "1px solid #e2e8f0", background: "#fbfcfd", display: "flex", justifyContent: "flex-end", gap: "10px", alignItems: "center" }, children: [(0, import_jsx_runtime.jsx)("button", { id: "saveBtn", className: "jhs-setting-btn jhs-setting-btn--success", children: "💾 保存设置" }), (0, import_jsx_runtime.jsx)("button", { id: "clean-all", className: "jhs-setting-btn jhs-setting-btn--danger", style: { display: "none" }, children: "♾️ 清理全部缓存" })] }) ] })] }); } function BackToTopButton() { return (0, import_jsx_runtime.jsx)("div", { id: "jhs-back-to-top", title: "回到顶部", children: (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", children: (0, import_jsx_runtime.jsx)("path", { d: "M12 4l-8 8h6v8h4v-8h6z" }) }) }); } function CacheItemHtml({ text, cacheKey, title, size, count }) { return (0, import_jsx_runtime.jsxs)("div", { className: "cache-item", "data-cache-key": cacheKey, style: { border: "1px solid #eee", borderRadius: "8px", padding: "12px" }, children: [ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: "bold", marginBottom: "4px" }, children: text }), (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: "12px", color: "#999", marginBottom: "8px" }, children: [ (0, import_jsx_runtime.jsx)("span", { className: "cache-size", children: size }), " · ", (0, import_jsx_runtime.jsx)("span", { className: "cache-count", children: count }) ] }), (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "6px" }, children: [ (0, import_jsx_runtime.jsx)("a", { className: "jhs-setting-menu-btn clean-btn", "data-key": cacheKey, style: { backgroundColor: "#448cc2", flex: 1, textAlign: "center", fontSize: "13px" }, title, children: (0, import_jsx_runtime.jsx)("span", { children: "清理" }) }), (0, import_jsx_runtime.jsx)("a", { className: "jhs-setting-menu-btn view-btn", "data-key": cacheKey, style: { backgroundColor: "#b2bec0", flex: 1, textAlign: "center", fontSize: "13px" }, children: (0, import_jsx_runtime.jsx)("span", { children: "查看" }) }), (0, import_jsx_runtime.jsx)("a", { className: "jhs-setting-menu-btn export-btn", "data-key": cacheKey, style: { backgroundColor: "#85d0a3", flex: 1, textAlign: "center", fontSize: "13px" }, title: "导出为 JSON 文件", children: (0, import_jsx_runtime.jsx)("span", { children: "导出" }) }) ] }) ] }); } function SettingMountBox({ variant }) { switch (variant) { case "navbar": return (0, import_jsx_runtime.jsxs)("div", { className: "navbar-item has-dropdown is-hoverable setting-box", style: { position: "relative" }, children: [(0, import_jsx_runtime.jsx)("a", { id: "setting-btn", className: "navbar-link nav-btn", style: { color: "#ff8400 !important", paddingRight: "15px !important" }, children: "设置" }), (0, import_jsx_runtime.jsx)("div", { className: "simple-setting" })] }); case "mini": return (0, import_jsx_runtime.jsxs)("div", { className: "navbar-item mini-setting-box", style: { position: "relative", marginLeft: "auto" }, children: [(0, import_jsx_runtime.jsx)("a", { id: "mini-setting-btn", className: "navbar-link nav-btn", style: { color: "#ff8400 !important", paddingLeft: "0 !important", paddingRight: "0 !important" }, children: "设置" }), (0, import_jsx_runtime.jsx)("div", { className: "mini-simple-setting" })] }); } } function Row({ label, tip, children, stacked }) { return (0, import_jsx_runtime.jsxs)("div", { className: stacked ? "jhs-qs-row jhs-qs-row-stacked" : "jhs-qs-row", children: [(0, import_jsx_runtime.jsxs)("div", { className: "jhs-qs-label", children: [tip ? (0, import_jsx_runtime.jsx)("span", { className: "jhs-qs-tip", "data-tip": tip, children: "?" }) : null, (0, import_jsx_runtime.jsx)("span", { children: label })] }), (0, import_jsx_runtime.jsx)("div", { className: "jhs-qs-control", children })] }); } function SimpleSettingPanel({ isJavdbSite }) { return (0, import_jsx_runtime.jsxs)("div", { className: "jhs-qs-panel", children: [ (0, import_jsx_runtime.jsxs)("div", { className: "jhs-qs-header", children: [(0, import_jsx_runtime.jsx)("span", { className: "jhs-qs-title", children: "快捷设置" }), (0, import_jsx_runtime.jsx)("span", { className: "jhs-qs-sub", children: "改动即时生效" })] }), (0, import_jsx_runtime.jsxs)("div", { className: "jhs-qs-body jhs-scrollbar", children: [ (0, import_jsx_runtime.jsxs)("div", { className: "jhs-qs-section", children: [ (0, import_jsx_runtime.jsx)("div", { className: "jhs-qs-section-title", children: "列表显示" }), (0, import_jsx_runtime.jsx)(Row, { label: "显示所有已鉴定", tip: "开启后忽略下方分项,强制显示全部已鉴定内容", children: (0, import_jsx_runtime.jsx)("input", { type: "checkbox", id: "showAllItem", className: "mini-switch" }) }), (0, import_jsx_runtime.jsxs)("div", { className: "jhs-qs-subcard", id: "jhs-qs-filter-group", children: [ (0, import_jsx_runtime.jsx)(Row, { label: "屏蔽单番号", children: (0, import_jsx_runtime.jsx)("input", { type: "checkbox", id: "showFilterItem", className: "mini-switch" }) }), (0, import_jsx_runtime.jsx)(Row, { label: "屏蔽演员", children: (0, import_jsx_runtime.jsx)("input", { type: "checkbox", id: "showFilterActorItem", className: "mini-switch" }) }), (0, import_jsx_runtime.jsx)(Row, { label: "屏蔽关键词", children: (0, import_jsx_runtime.jsx)("input", { type: "checkbox", id: "showFilterKeywordItem", className: "mini-switch" }) }), (0, import_jsx_runtime.jsx)(Row, { label: "收藏", children: (0, import_jsx_runtime.jsx)("input", { type: "checkbox", id: "showFavoriteItem", className: "mini-switch" }) }), (0, import_jsx_runtime.jsx)(Row, { label: "已观看", children: (0, import_jsx_runtime.jsx)("input", { type: "checkbox", id: "showHasWatchItem", className: "mini-switch" }) }) ] }) ] }), (0, import_jsx_runtime.jsxs)("div", { className: "jhs-qs-section", children: [ (0, import_jsx_runtime.jsx)("div", { className: "jhs-qs-section-title", children: "浏览行为" }), (0, import_jsx_runtime.jsx)(Row, { label: "弹窗打开详情", tip: "点击封面:弹窗打开,或新窗口打开", children: (0, import_jsx_runtime.jsx)("input", { type: "checkbox", id: "dialogOpenDetail", className: "mini-switch" }) }), (0, import_jsx_runtime.jsx)(Row, { label: "鉴定后关闭页面", children: (0, import_jsx_runtime.jsx)("input", { type: "checkbox", id: "needClosePage", className: "mini-switch" }) }), (0, import_jsx_runtime.jsx)(Row, { label: "触底加载", tip: "瀑布流常开。自动=滑到底自动加载下一页;点按钮=滑到底后点「点击加载下一页」。右下角「加载全部」始终可用", children: (0, import_jsx_runtime.jsxs)("div", { id: "autoPageLoadMode", className: "jhs-qs-seg", role: "group", "aria-label": "触底加载方式", children: [(0, import_jsx_runtime.jsx)("button", { type: "button", className: "jhs-qs-seg-btn", "data-value": "auto", children: "自动" }), (0, import_jsx_runtime.jsx)("button", { type: "button", className: "jhs-qs-seg-btn", "data-value": "click", children: "点按钮" })] }) }) ] }), (0, import_jsx_runtime.jsxs)("div", { className: "jhs-qs-section", children: [ (0, import_jsx_runtime.jsx)("div", { className: "jhs-qs-section-title", children: "功能" }), (0, import_jsx_runtime.jsx)(Row, { label: "标题翻译", children: (0, import_jsx_runtime.jsx)("input", { type: "checkbox", id: "translateTitle", className: "mini-switch" }) }), isJavdbSite ? (0, import_jsx_runtime.jsx)(Row, { label: "加载女优信息", tip: "详情页展示年龄、三围等信息", children: (0, import_jsx_runtime.jsx)("input", { type: "checkbox", id: "enableLoadActressInfo", className: "mini-switch" }) }) : null, (0, import_jsx_runtime.jsx)(Row, { label: "第三方视频资源", tip: "详情页 missAv / SupJav 等检测", children: (0, import_jsx_runtime.jsx)("input", { type: "checkbox", id: "enableLoadOtherSite", className: "mini-switch" }) }) ] }), (0, import_jsx_runtime.jsxs)("div", { className: "jhs-qs-section", children: [ (0, import_jsx_runtime.jsx)("div", { className: "jhs-qs-section-title", children: "页面布局" }), (0, import_jsx_runtime.jsx)(Row, { label: "列数", stacked: true, children: (0, import_jsx_runtime.jsxs)("div", { className: "jhs-qs-range", children: [(0, import_jsx_runtime.jsx)("input", { type: "range", id: "containerColumns", min: "2", max: "10", step: "1" }), (0, import_jsx_runtime.jsx)("span", { id: "showContainerColumns", className: "jhs-qs-range-val" })] }) }), (0, import_jsx_runtime.jsx)(Row, { label: "宽度", stacked: true, children: (0, import_jsx_runtime.jsxs)("div", { className: "jhs-qs-range", children: [(0, import_jsx_runtime.jsx)("input", { type: "range", id: "containerWidth", min: "0", max: "30", step: "1" }), (0, import_jsx_runtime.jsx)("span", { id: "showContainerWidth", className: "jhs-qs-range-val" })] }) }) ] }) ] }), (0, import_jsx_runtime.jsxs)("div", { className: "jhs-qs-footer", children: [(0, import_jsx_runtime.jsx)("button", { type: "button", id: "helpBtn", className: "jhs-qs-btn jhs-qs-btn-ghost", children: "常见问题" }), (0, import_jsx_runtime.jsx)("button", { type: "button", id: "moreBtn", className: "jhs-qs-btn jhs-qs-btn-primary", children: "更多设置" })] }) ] }); } function VideoQualityOption({ quality, text }) { return (0, import_jsx_runtime.jsx)("option", { value: quality, children: text }); } var setting_plugin_default = "/* stylelint-disable-next-line selector-type-case -- 运行时占位符(须保持大写),由 setting-plugin.tsx initCss() 的 replace 调用替换为动态容器宽度 CSS */\n__CSS_TEXT__\n\n.nav-btn::after {\n content: none !important;\n}\n\n#cache-data-display pre {\n font-family: Consolas, Monaco, 'Andale Mono', monospace;\n white-space: pre-wrap;\n overflow-wrap: break-word;\n line-height: 1.5;\n color: #333;\n border: 1px solid #ddd;\n}\n\n.cache-item {\n transition: box-shadow 0.2s ease, transform 0.2s ease;\n}\n\n.cache-item:hover {\n box-shadow: 0 2px 8px rgb(0, 0, 0, 0.1);\n transform: translateY(-2px);\n}\n\n.tooltip-icon {\n display: inline-block;\n width: 16px;\n height: 16px;\n line-height: 16px;\n text-align: center;\n border-radius: 50%;\n background-color: #ccc;\n color: white;\n font-size: 12px;\n margin-right: 5px;\n cursor: help;\n}\n\n/* ===== 设置项布局(统一表单网格)===== */\n.setting-item {\n display: grid;\n grid-template-columns: 180px 1fr;\n align-items: center;\n gap: 12px;\n padding: 8px 12px;\n border-radius: 6px;\n transition: background 0.15s ease;\n}\n\n.setting-item:hover {\n background: rgb(93, 135, 194, 0.04);\n}\n\n.simple-setting .setting-item {\n align-items: center;\n}\n\n.setting-label {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 5px;\n font-size: 13px;\n font-weight: 600;\n color: #475569;\n text-align: right;\n min-width: 0;\n}\n\n.form-content {\n min-width: 0;\n}\n\n/* form-content 内开关/颜色选择器宽度显式锁定,避免被通用控件规则拉宽 */\n.form-content .jhs-toggle {\n width: 40px !important;\n min-width: 40px;\n padding: 0 !important;\n}\n\n.form-content input[type='color'] {\n width: 36px !important;\n min-width: 36px;\n padding: 2px !important;\n}\n\n/* 统一表单控件(text/number/password/url/select/textarea) */\n.setting-item input[type='text'],\n.setting-item input[type='number'],\n.setting-item input[type='password'],\n.setting-item input[type='url'],\n.setting-item input:not([type]),\n.setting-item select,\n.setting-item textarea {\n width: 100%;\n height: 36px;\n padding: 6px 12px;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n font-size: 13px;\n color: #1e293b;\n background: #fff;\n box-sizing: border-box;\n transition:\n border-color 0.15s,\n box-shadow 0.15s;\n outline: none;\n}\n\n.setting-item textarea {\n height: auto;\n min-height: 72px;\n resize: vertical;\n}\n\n.setting-item input:focus,\n.setting-item select:focus,\n.setting-item textarea:focus {\n border-color: #5d87c2;\n box-shadow: 0 0 0 2px rgb(93, 135, 194, 0.15);\n}\n\n.setting-item input[readonly] {\n background: #f8fafc;\n color: #94a3b8;\n cursor: default;\n border-color: #e2e8f0;\n}\n\n/* 原生颜色选择器 */\n.setting-item input[type='color'] {\n width: 36px;\n height: 36px;\n padding: 2px;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n cursor: pointer;\n background: #fff;\n box-sizing: border-box;\n}\n\n/* 行内问号提示图标 */\n.jhs-tooltip-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 15px;\n height: 15px;\n flex-shrink: 0;\n border-radius: 50%;\n background: #cbd5e1;\n color: #fff;\n font-size: 10px;\n font-weight: 700;\n line-height: 1;\n cursor: help;\n}\n\n/* iOS 风格开关(设置面板内) */\n.jhs-toggle {\n position: relative;\n width: 40px;\n height: 22px;\n appearance: none;\n -webkit-appearance: none;\n background: #d1d5db;\n border-radius: 11px;\n cursor: pointer;\n transition: background 0.2s;\n border: none;\n outline: none;\n flex-shrink: 0;\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n accent-color: unset;\n}\n\n.jhs-toggle:checked {\n background: #22c55e;\n}\n\n.jhs-toggle::before {\n content: '';\n position: absolute;\n top: 2px;\n left: 2px;\n width: 18px;\n height: 18px;\n background: #fff;\n border-radius: 50%;\n transition: transform 0.2s;\n box-shadow: 0 1px 3px rgb(0, 0, 0, 0.15);\n}\n\n.jhs-toggle:checked::before {\n transform: translateX(18px);\n}\n\n/* 面板分区(替代渐变 hr 分隔线) */\n.jhs-setting-section {\n margin-top: 20px;\n padding-top: 16px;\n border-top: 1px solid #e5e7eb;\n}\n\n.jhs-setting-section:first-child {\n margin-top: 0;\n padding-top: 0;\n border-top: none;\n}\n\n.jhs-setting-section-title {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 14px;\n font-weight: 600;\n color: #5d87c2;\n margin-bottom: 12px;\n}\n\n/* 面板操作按钮(语义色修饰符) */\n.jhs-setting-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n height: 34px;\n padding: 6px 16px;\n border: none;\n border-radius: 6px;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition:\n opacity 0.15s,\n transform 0.1s;\n color: #fff;\n text-decoration: none;\n white-space: nowrap;\n box-sizing: border-box;\n}\n\n.jhs-setting-btn:hover {\n opacity: 0.9;\n}\n\n.jhs-setting-btn:active {\n transform: scale(0.97);\n}\n\n.jhs-setting-btn--primary {\n background: #5d87c2;\n}\n\n.jhs-setting-btn--success {\n background: #22c55e;\n}\n\n.jhs-setting-btn--danger {\n background: #ef4444;\n}\n\n.jhs-setting-btn--pink {\n background: #d25a88;\n}\n\n.jhs-setting-btn--mint {\n background: #85d0a3;\n}\n\n/* 复选框 + 文字标签 */\n.jhs-checkbox-label {\n display: flex;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n font-size: 13px;\n color: #475569;\n}\n\n.jhs-checkbox-label input[type='checkbox'] {\n width: 16px;\n height: 16px;\n accent-color: #5d87c2;\n cursor: pointer;\n margin: 0;\n flex-shrink: 0;\n}\n\n.keyword-label {\n display: inline-flex;\n align-items: center;\n padding: 4px 8px;\n border-radius: 4px;\n font-size: 14px;\n position: relative;\n margin-left: 8px;\n margin-bottom: 5px;\n}\n\n.keyword-remove {\n margin-left: 6px;\n cursor: pointer;\n font-size: 12px;\n line-height: 1;\n}\n\n.keyword-input {\n padding: 6px 12px;\n border: 1px solid #cbd5e1;\n border-radius: 6px;\n font-size: 14px;\n float: right;\n}\n\n.add-tag-btn {\n padding: 6px 12px;\n background-color: #e2e8f0;\n color: #334155;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n font-size: 14px;\n margin-left: 8px;\n float: right;\n}\n\n.add-tag-btn:hover {\n background-color: #cbd5e1;\n}\n\n.tag-box {\n margin-top: 15px;\n}\n\n/* 屏蔽关键词容器:标签流式换行 + 输入行自然布局 */\n.filter-keyword-container {\n padding: 8px 0;\n}\n\n.filter-keyword-container .tag-box {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n min-height: 32px;\n margin-top: 0;\n}\n\n/* ===== 通用按钮(jhs-setting-menu-btn 统一样式)===== */\n.jhs-setting-menu-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 7px 16px;\n border-radius: 6px;\n text-decoration: none;\n font-size: 13px;\n font-weight: 500;\n transition: opacity 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;\n cursor: pointer;\n border: none;\n white-space: nowrap;\n color: #fff !important;\n}\n\n.jhs-setting-menu-btn span {\n color: #fff !important;\n}\n\n.jhs-setting-menu-btn:hover {\n opacity: 0.88;\n transform: translateY(-1px);\n box-shadow: 0 2px 8px rgb(0, 0, 0, 0.15);\n}\n\n.jhs-setting-menu-btn:active {\n transform: translateY(0);\n}\n\n/* ===== 底部操作按钮(jhs-setting-btn 统一按钮系统)===== */\n#saveBtn,\n#clean-all {\n margin-top: 0;\n}\n\n/* ===== 悬浮快捷设置面板 ===== */\n.simple-setting,\n.mini-simple-setting {\n display: none;\n position: absolute;\n top: calc(100% + 6px);\n right: 0;\n left: auto;\n z-index: 10000;\n width: 340px;\n max-width: min(340px, 92vw);\n padding: 0;\n margin: 0;\n color: #1e293b;\n background: #fff;\n border: 1px solid #e2e8f0;\n border-radius: 12px;\n box-shadow:\n 0 10px 40px rgb(15, 23, 42, 0.12),\n 0 2px 8px rgb(15, 23, 42, 0.06);\n overflow: hidden;\n}\n\n/* 与触发按钮之间的隐形热区,减少移入时误关 */\n.simple-setting::before,\n.mini-simple-setting::before {\n content: '';\n position: absolute;\n top: -10px;\n left: 0;\n right: 0;\n height: 10px;\n}\n\n.mini-simple-setting {\n right: 0;\n left: auto;\n}\n\n/* 快捷设置内部布局 */\n.jhs-qs-panel {\n display: flex;\n flex-direction: column;\n max-height: min(78vh, 560px);\n}\n\n.jhs-qs-header {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 8px;\n padding: 14px 16px 10px;\n border-bottom: 1px solid #f1f5f9;\n background: linear-gradient(180deg, #f8fafc 0%, #fff 100%);\n}\n\n.jhs-qs-title {\n font-size: 15px;\n font-weight: 700;\n color: #0f172a;\n letter-spacing: 0.02em;\n}\n\n.jhs-qs-sub {\n font-size: 11px;\n color: #94a3b8;\n font-weight: 500;\n}\n\n.jhs-qs-body {\n flex: 1;\n overflow-x: hidden;\n overflow-y: auto;\n padding: 8px 10px 12px;\n}\n\n.jhs-qs-section {\n margin-bottom: 10px;\n padding: 8px 8px 4px;\n background: #f8fafc;\n border: 1px solid #f1f5f9;\n border-radius: 10px;\n}\n\n.jhs-qs-section:last-child {\n margin-bottom: 0;\n}\n\n.jhs-qs-section-title {\n font-size: 11px;\n font-weight: 700;\n color: #64748b;\n text-transform: none;\n letter-spacing: 0.04em;\n padding: 2px 6px 8px;\n}\n\n.jhs-qs-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n min-height: 36px;\n padding: 6px 8px;\n margin-bottom: 2px;\n border-radius: 8px;\n transition: background 0.12s ease;\n}\n\n.jhs-qs-row:hover {\n background: rgb(255, 255, 255, 0.9);\n}\n\n.jhs-qs-row-stacked {\n flex-direction: column;\n align-items: stretch;\n gap: 6px;\n min-height: 0;\n padding-bottom: 10px;\n}\n\n.jhs-qs-label {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n font-weight: 600;\n color: #334155;\n line-height: 1.35;\n min-width: 0;\n}\n\n.jhs-qs-tip {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 15px;\n height: 15px;\n flex-shrink: 0;\n border-radius: 50%;\n background: #cbd5e1;\n color: #fff;\n font-size: 10px;\n font-weight: 700;\n cursor: help;\n line-height: 1;\n}\n\n.jhs-qs-control {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n flex-shrink: 0;\n}\n\n.jhs-qs-row-stacked .jhs-qs-control {\n justify-content: stretch;\n}\n\n.jhs-qs-subcard {\n margin: 2px 0 6px;\n padding: 4px;\n background: #fff;\n border: 1px solid #e2e8f0;\n border-radius: 8px;\n}\n\n.jhs-qs-subcard.is-disabled {\n opacity: 0.55;\n pointer-events: none;\n}\n\n.jhs-qs-subcard .jhs-qs-row {\n min-height: 32px;\n padding: 4px 8px;\n}\n\n.jhs-qs-subcard .jhs-qs-label {\n font-weight: 500;\n font-size: 12.5px;\n color: #475569;\n}\n\n/* 触底加载等二选一:分段按钮 */\n.jhs-qs-seg {\n display: inline-flex;\n align-items: stretch;\n flex-shrink: 0;\n padding: 2px;\n background: #e2e8f0;\n border-radius: 9px;\n box-sizing: border-box;\n}\n\n.jhs-qs-seg-btn {\n appearance: none;\n border: none;\n margin: 0;\n padding: 6px 12px;\n min-width: 64px;\n border-radius: 7px;\n font-size: 12px;\n font-weight: 600;\n line-height: 1.2;\n color: #64748b;\n background: transparent;\n cursor: pointer;\n transition:\n background 0.15s ease,\n color 0.15s ease,\n box-shadow 0.15s ease;\n white-space: nowrap;\n}\n\n.jhs-qs-seg-btn:hover:not(.is-active) {\n color: #334155;\n background: rgb(255, 255, 255, 0.55);\n}\n\n.jhs-qs-seg-btn.is-active {\n color: #fff;\n background: linear-gradient(135deg, #5d87c2, #4a90d9);\n box-shadow: 0 1px 4px rgb(74, 144, 217, 0.35);\n}\n\n.jhs-qs-seg-btn:focus-visible {\n outline: 2px solid #5d87c2;\n outline-offset: 1px;\n}\n\n.jhs-qs-range {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n}\n\n.jhs-qs-range input[type='range'] {\n flex: 1;\n min-width: 0;\n height: 6px;\n padding: 0 !important;\n accent-color: #5d87c2;\n cursor: pointer;\n}\n\n.jhs-qs-range-val {\n min-width: 36px;\n text-align: right;\n font-size: 12px;\n font-weight: 700;\n color: #5d87c2;\n font-variant-numeric: tabular-nums;\n}\n\n.jhs-qs-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n padding: 10px 14px 12px;\n border-top: 1px solid #f1f5f9;\n background: #fafbfc;\n}\n\n.jhs-qs-btn {\n flex: 1;\n padding: 8px 12px;\n border: none;\n border-radius: 8px;\n font-size: 13px;\n font-weight: 600;\n cursor: pointer;\n transition:\n background 0.15s ease,\n transform 0.12s ease,\n box-shadow 0.15s ease;\n}\n\n.jhs-qs-btn:hover {\n transform: translateY(-1px);\n}\n\n.jhs-qs-btn:active {\n transform: translateY(0);\n}\n\n.jhs-qs-btn-ghost {\n background: #fff;\n color: #64748b;\n border: 1px solid #e2e8f0;\n}\n\n.jhs-qs-btn-ghost:hover {\n background: #f8fafc;\n color: #475569;\n border-color: #cbd5e1;\n}\n\n.jhs-qs-btn-primary {\n background: linear-gradient(135deg, #5d87c2, #4a90d9);\n color: #fff;\n box-shadow: 0 2px 8px rgb(74, 144, 217, 0.28);\n}\n\n.jhs-qs-btn-primary:hover {\n box-shadow: 0 4px 12px rgb(74, 144, 217, 0.35);\n}\n\n/* 快捷设置内开关:避免被 form-content 通配拉宽 */\n.jhs-qs-panel input.mini-switch[type='checkbox'] {\n width: 40px !important;\n min-width: 40px !important;\n max-width: 40px !important;\n}\n\n/* ===== 开关(mini-switch)===== */\n\n/* 排除 form-content 通配与 content-panel checkbox 规则的干扰 */\ninput.mini-switch[type='checkbox'] {\n appearance: none;\n -webkit-appearance: none;\n width: 40px;\n height: 20px;\n min-width: 40px;\n max-width: 40px;\n padding: 0;\n margin: 0;\n background: #e0e0e0;\n border: none;\n border-radius: 20px;\n position: relative;\n cursor: pointer;\n outline: none;\n box-sizing: border-box;\n flex-shrink: 0;\n vertical-align: middle;\n accent-color: unset;\n}\n\ninput.mini-switch[type='checkbox']:checked {\n background: #4caf50;\n}\n\ninput.mini-switch[type='checkbox']::before {\n content: '';\n position: absolute;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: white;\n top: 2px;\n left: 2px;\n box-shadow: 0 1px 3px rgb(0, 0, 0, 0.2);\n transition: left 0.15s ease;\n}\n\ninput.mini-switch[type='checkbox']:checked::before {\n left: calc(100% - 18px);\n}\n\n/* ===== 侧栏导航 ===== */\n.side-menu-item {\n padding: 11px 16px;\n cursor: pointer;\n color: #64748b;\n border-left: 3px solid transparent;\n transition: color 0.2s, background-color 0.2s, border-color 0.2s;\n display: flex;\n align-items: center;\n gap: 6px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n font-size: 14px;\n font-weight: 500;\n}\n\n.side-menu-item .icon {\n height: 24px;\n width: 24px;\n}\n\n.side-menu-item:hover {\n background-color: rgb(93, 135, 194, 0.08);\n color: #5d87c2;\n}\n\n.side-menu-item.active {\n background-color: rgb(93, 135, 194, 0.12);\n border-left: 3px solid #5d87c2;\n color: #5d87c2;\n font-weight: 600;\n}\n\n/* ===== 内容面板 ===== */\n.content-panel {\n display: none;\n margin-top: 16px;\n padding: 0 12px 10px 4px;\n height: 100%;\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.content-panel.active {\n display: block;\n}\n\n/* 表单控件统一样式已收敛至「设置项布局」区块的 .setting-item 作用域规则 */\n\n/* ===== 复选框美化(不含开关)===== */\n.content-panel input[type='checkbox']:not(.mini-switch, .jhs-toggle) {\n width: 16px;\n height: 16px;\n accent-color: #5d87c2;\n cursor: pointer;\n}\n\ninput[type='checkbox']:disabled {\n opacity: 0.6;\n cursor: default !important;\n}\n\n"; var back_to_top_button_default = "#jhs-back-to-top,\n#jdb-wf-back-to-top {\n position: fixed;\n bottom: 40px;\n right: 40px;\n width: 44px;\n height: 44px;\n background-color: rgb(30, 30, 30, 0.9);\n backdrop-filter: blur(4px);\n color: #e0e0e0;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n z-index: var(--jhs-z-page);\n opacity: 0;\n visibility: hidden;\n transform: translateY(20px) scale(0.9);\n transition:\n opacity 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),\n visibility 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),\n transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),\n background-color 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),\n color 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),\n box-shadow 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),\n border-color 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);\n box-shadow: 0 4px 12px rgb(0, 0, 0, 0.3);\n border: 1px solid rgb(255, 255, 255, 0.05);\n user-select: none;\n}\n\n#jhs-back-to-top:hover,\n#jdb-wf-back-to-top:hover {\n background-color: #ff8400;\n color: #fff;\n transform: translateY(0) scale(1);\n box-shadow: 0 8px 20px rgb(255, 132, 0, 0.3);\n border-color: #ff8400;\n}\n\n#jhs-back-to-top.show,\n#jdb-wf-back-to-top.show {\n opacity: 1;\n visibility: visible;\n transform: translateY(0) scale(1);\n}\n\n#jhs-back-to-top svg,\n#jdb-wf-back-to-top svg {\n width: 22px;\n height: 22px;\n fill: currentcolor;\n stroke: currentcolor;\n stroke-width: 0;\n}\n"; var setting_image_mode_horizontal_default = ".cover {\n min-height: auto !important;\n padding-top: 67% !important;\n}\n\n.cover img {\n object-fit: contain !important;\n object-position: 50% 50% !important;\n}\n\n/* bus的 */\n.masonry .movie-box img {\n min-height: auto !important;\n object-fit: contain !important;\n object-position: top;\n}\n"; var SettingPlugin = class extends BasePlugin { folderName = "JHS-数据备份"; simpleSettingHideTimer = null; cacheItems = [ { key: "jhs_dmm_video", text: "🎥 预览视频缓存", title: "DMM 预告片画质→URL 映射(联动清理 jhs_other_site_dmm)" }, { key: "jhs_other_site", text: "🌍 第三方站点缓存", title: "missav/supjav 等站点搜索结果(随 WebDav 备份)" }, { key: "jhs_other_site_dmm", text: "🌍 DMM 预览伴生缓存", title: "DMM 预览相关第三方链接(随 WebDav 备份)" }, { key: "jhs_translate", text: "🆎 标题翻译", title: "番号→中文标题翻译(Google Translate,随备份)" }, { key: "jhs_actress_info", text: "👩 演员信息", title: "演员身高/体重/三围/罩杯/生日(随备份)" }, { key: "jhs_score_info", text: "⭐ Top250|热播 评分数据", title: "影片评分 HTML 快照(随备份)" }, { key: "jhs_screenShot", text: "🖼️ 截图墙缓存", title: "javstore 截图墙(随备份)" }, { key: "jhs_visit_history", text: "🕐 访问记录", title: "页面访问时间记录(随备份)" }, { key: "jdb:rating_cache_v2", text: "⭐ 列表评分缓存", title: "评分显示插件缓存(随备份)" } ]; getName() { return "SettingPlugin"; } async initCss() { const settings = await storageManager.getSetting(); const containerWidth = (settings == null ? void 0 : settings.containerWidth) ?? "70"; const containerColumns = utils.isMobile() && window.innerWidth < 1e3 ? 1 : (settings == null ? void 0 : settings.containerColumns) ?? 4; this.applyImageMode().then(); const cssText = `\n section .container{\n max-width: 1000px !important;\n min-width: ${containerWidth}%;\n }\n .movie-list, .movie-list.v{\n grid-template-columns: repeat(${containerColumns}, minmax(0, 1fr));\n }\n `; return setting_plugin_default.replace("__CSS_TEXT__", cssText); } cancelSimpleSettingHide() { if (this.simpleSettingHideTimer != null) { clearTimeout(this.simpleSettingHideTimer); this.simpleSettingHideTimer = null; } } scheduleSimpleSettingHide(selector) { this.cancelSimpleSettingHide(); this.simpleSettingHideTimer = setTimeout(() => { $(selector).html("").hide(); this.simpleSettingHideTimer = null; }, 220); } openSimpleSetting(selector) { this.cancelSimpleSettingHide(); $(selector).html(this.simpleSetting()).show(); this.initSimpleSettingForm().then(); clog.lowZIndex(); } async handle() { if (await storageManager.getSetting("enableClog", "yes") === "yes") clog.show(); if (isJavdbSite) { const toggleSettingBox = function() { if ($(".navbar-search").is(":hidden")) { $(".mini-setting-box").hide(); $(".setting-box").show(); } else { $(".mini-setting-box").show(); $(".setting-box").hide(); } }; $("#navbar-menu-user .navbar-end").prepend(jsxToString((0, import_jsx_runtime.jsx)(SettingMountBox, { variant: "navbar" }))); utils.loopDetector(() => $("#miniHistoryBtn").length > 0, () => { $(".miniHistoryBtnBox").before(jsxToString((0, import_jsx_runtime.jsx)(SettingMountBox, { variant: "mini" }))); toggleSettingBox(); }); $(window).resize(toggleSettingBox); } $(".main-nav, .container-fluid").on("click", "#setting-btn, #mini-setting-btn", () => { clog.lowZIndex(); this.openSettingDialog(); }); $(".main-nav, .container-fluid").on("mouseenter", ".setting-box", () => { this.openSimpleSetting(".simple-setting"); }).on("mouseleave", ".setting-box", () => { this.scheduleSimpleSettingHide(".simple-setting"); }); $(".main-nav, .container-fluid").on("mouseenter", ".mini-setting-box", () => { this.openSimpleSetting(".mini-simple-setting"); }).on("mouseleave", ".mini-setting-box", () => { this.scheduleSimpleSettingHide(".mini-simple-setting"); }); this.addBackToTopBtn(); } addBackToTopBtn() { utils.insertStyle(back_to_top_button_default); const btn = $(jsxToString((0, import_jsx_runtime.jsx)(BackToTopButton, {}))); $("body").append(btn); btn.on("click", () => { utils.smoothScrollToTop(500); }); let ticking = false; $(window).on("scroll", () => { if (!ticking) { window.requestAnimationFrame(() => { if (($(window).scrollTop() ?? 0) > 300) btn.addClass("show"); else btn.removeClass("show"); ticking = false; }); ticking = true; } }); } async openSettingDialog(panelName = "backup-panel", callback) { const cacheItemsHtml = this.cacheItems.map((item) => { const stats = getCacheStats(item.key); return jsxToString((0, import_jsx_runtime.jsx)(CacheItemHtml, { text: item.text, cacheKey: item.key, title: item.title, size: formatSize(stats.size), count: stats.count > 0 ? `${stats.count} 条` : "空" })); }).join(""); let qualityOptionsHtml = ""; VIDEO_QUALITY_LIST.forEach((option) => { if (option.canSelect) qualityOptionsHtml += jsxToString((0, import_jsx_runtime.jsx)(VideoQualityOption, { quality: option.quality, text: option.text })); }); const dialogHtml = jsxToString((0, import_jsx_runtime.jsx)(SettingDialog, { panelName, cacheItemsHtml, qualityOptionsHtml, isJavdbSite })); layer.open({ type: 1, title: "设置", content: dialogHtml, area: utils.getResponsiveArea(["55%", "90%"]), scrollbar: false, success: (layerEl, layerIndex) => { $(layerEl).find(".layui-layer-content").css("position", "relative"); this.loadForm(); this.bindClick(); utils.setupEscClose(layerIndex); if (callback) callback(); } }); } simpleSetting() { return jsxToString((0, import_jsx_runtime.jsx)(SimpleSettingPanel, { isJavdbSite })); } async applyImageMode() { $("#verticalImgStyle").remove(); $(jsxToString((0, import_jsx_runtime.jsx)(StyleBlock, { id: "verticalImgStyle", css: setting_image_mode_horizontal_default }))).appendTo("head"); } refreshDiagnostics() { $("#diagnostics-content").html(renderDiagnosticsHtml(this.pluginManager)); } addLabelTag(containerSelector, keyword, noun = "屏蔽词") { addLabelTag(containerSelector, keyword, noun); } addKeyword(containerSelector, noun = "屏蔽词") { addKeyword(containerSelector, noun); } async autoBackup() { const settings = await storageManager.getSetting(); if (!shouldAutoBackup(settings.autoBackupConfig || DEFAULT_AUTO_BACKUP_CONFIG)) return; const channels = settings.backupChannels || { webdav: true, git: false }; const webdavCreds = channels.webdav !== false ? getWebDavCredentialsFromGm() : null; const gitCreds = channels.git === true ? getGitCredentialsFromGm() : null; if (!webdavCreds && !gitCreds) return; const exportText = JSON.stringify(await this.buildBackupPayload(settings)); const fileName = getAutoBackupFileName(); let succeeded = false; if (webdavCreds) try { await new window.WebDavClient(webdavCreds.url, webdavCreds.username, webdavCreds.password).backup(this.folderName, fileName, exportText); succeeded = true; console.log("[JHS 自动备份] WebDav 通道完成"); } catch (err) { console.warn("[JHS 自动备份] WebDav 通道失败:", err instanceof Error ? err.message : String(err)); } if (gitCreds) try { await buildGitClient(gitCreds).backup(this.folderName, fileName, exportText); succeeded = true; console.log("[JHS 自动备份] Git 通道完成"); } catch (err) { console.warn("[JHS 自动备份] Git 通道失败:", err instanceof Error ? err.message : String(err)); } if (succeeded) markAutoBackupDone(); } async loadForm() { return loadForm(this); } async saveForm() { return saveForm(this); } bindClick() { bindClick$1(this); } async initSimpleSettingForm() { return initSimpleSettingForm(this); } refreshCacheStats(cacheKey) { refreshCacheStats(this, cacheKey); } refreshPreloadCacheStats() { refreshPreloadCacheStats(this); } refreshAllCacheStats() { refreshAllCacheStats(this); } importData() { importData(this); } async exportData() { return exportData(this); } async buildBackupPayload(settings) { return buildBackupPayload(settings); } async backupDataByWebDav() { return backupDataByWebDav(this); } async backupListBtnByWebDav() { return backupListBtnByWebDav(this); } openFileListDialog(backupList, webdavClient, titlePrefix) { openFileListDialog(this, backupList, webdavClient, titlePrefix); } }; function broadcastSend(key, payload, opts = {}) { const { eventName = key, eventDetail = payload, onError } = opts; const json = JSON.stringify(payload); try { GM_setValue(key, json); } catch (error) { onError?.("gm", error); } try { localStorage.setItem(key, json); } catch (error) { onError?.("localStorage", error); } try { document.dispatchEvent(new CustomEvent(eventName, { detail: eventDetail })); } catch (error) { onError?.("customEvent", error); } } function broadcastSubscribe(key, handler, opts = {}) { const eventName = opts.eventName ?? key; const raw = opts.raw === true; const onDelivery = opts.onDelivery; document.addEventListener(eventName, (e) => { onDelivery?.({ channel: "customEvent" }); handler(e.detail, { channel: "customEvent" }); }); window.addEventListener("storage", (e) => { if (e.key !== key) return; const delivery = { channel: "storage", rawValue: e.newValue ?? void 0 }; onDelivery?.(delivery); if (!e.newValue) return; if (raw) { handler(e.newValue, delivery); return; } let payload; try { payload = JSON.parse(e.newValue); } catch { return; } handler(payload, delivery); }); try { GM_addValueChangeListener(key, (_name, _oldValue, newValue, remote) => { const delivery = { channel: "gm", rawValue: newValue, remote }; onDelivery?.(delivery); if (!newValue) return; if (raw) { handler(newValue, delivery); return; } let payload; try { payload = JSON.parse(newValue); } catch { return; } handler(payload, delivery); }); } catch {} } var LOG_PREFIX$6 = "[JavDB]"; var PENDING_SYNC_PREFIX = "jdb:vlt-pending-server-sync:"; var AUTO_FAVORITE_KEYWORD = "等待更新"; function getSeries$1(d) { if (!d) return d; const i = d.indexOf("-"); return i > 0 ? d.slice(0, i) : d; } function getCode$2(d) { if (!d) return null; const i = d.indexOf("-"); return i > 0 ? d.slice(i + 1) : null; } function getScore$1() { const spans = document.querySelectorAll("nav span"); const found = Array.from(spans).find((s) => s.textContent?.trim().endsWith("人評價")); if (!found) return 0; const m = found.innerHTML.match(/(\d+\.\d+)/); return m ? parseFloat(m[1]) : 0; } var _cachedScore; function getScoreCached() { if (_cachedScore === void 0) _cachedScore = getScore$1(); return _cachedScore; } function getMovieInfo(videoId) { const detail = document.querySelector(".video-detail"); if (!detail) { console.warn(`${LOG_PREFIX$6} 未找到 .video-detail,跳过`); return null; } const desEl = detail.querySelector(".panel-block.first-block a[data-clipboard-text]"); if (!desEl) { console.warn(`${LOG_PREFIX$6} 未找到番号元素,跳过`); return null; } const designation = desEl.dataset.clipboardText || ""; if (!designation) { console.warn(`${LOG_PREFIX$6} 番号为空,跳过`); return null; } const href = "https://javdb.com/v/" + videoId; const titleEl = detail.querySelector("strong.current-title"); const title = titleEl ? titleEl.innerHTML : ""; const dateEl = detail.querySelector(".video-meta-panel nav > div:nth-child(2) > span"); const release_date = dateEl ? dateEl.innerHTML : ""; const coverEl = detail.querySelector(".column-video-cover img"); return { designation, info: { href, title, release_date, cover_src: coverEl ? coverEl.src : "", score: getScoreCached(), series: getSeries$1(designation), code: getCode$2(designation) || "" } }; } function extractListNameFromInput(input) { const label = input.closest("label"); if (!label) return ""; const clone = label.cloneNode(true); clone.querySelector("input")?.remove(); (clone.querySelector(".jhs-list-item__count") || clone.querySelector("span"))?.remove(); return (clone.textContent || "").replace(/\s+/g, " ").trim(); } function getListName(listId) { const modalInputs = Array.from(document.querySelectorAll("#modal-save-list input[data-list-id]")); const panelInputs = Array.from(document.querySelectorAll(".jhs-list-panel input[data-list-id]")); const input = modalInputs.find((item) => item.dataset.listId === listId) || panelInputs.find((item) => item.dataset.listId === listId) || null; if (!input) return ""; return extractListNameFromInput(input); } function getListInfo(listId) { return { list_id: listId, info: { url: "https://javdb.com/lists/" + listId + "?locale=zh", name: getListName(listId) } }; } function normalizeListName(value) { return value.normalize("NFKC").replace(/\s+/g, " ").trim(); } function extractListIdFromHref(href) { const m = href.match(/[?&]id=([^&]+)/); return m ? m[1] : null; } function getCsrfToken$1() { return document.querySelector("meta[name=\"csrf-token\"]")?.content || ""; } function getCurrentListContainer() { return document.querySelector("#modal-save-list [data-list-target=\"listContainer\"]"); } function getCheckboxCount(input) { const label = input.closest("label"); const match = ((label?.querySelector(".jhs-list-item__count") || label?.querySelector("span"))?.textContent || "").match(/\d+/); if (!match) return null; const count = Number(match[0]); return Number.isSafeInteger(count) ? count : null; } function matchingListCheckboxes(listId) { return Array.from(document.querySelectorAll("#modal-save-list input[type=\"checkbox\"][data-list-id], .jhs-list-panel input[type=\"checkbox\"][data-list-id]")).filter((input) => input.dataset.listId === listId); } function setListCheckboxState(listId, checked) { for (const input of matchingListCheckboxes(listId)) input.checked = checked; } function setListCheckboxBusy(listId, busy) { const panel = document.querySelector(".jhs-list-panel"); const activeElement = document.activeElement; if (busy && panel && activeElement instanceof HTMLInputElement && panel.contains(activeElement) && activeElement.dataset.listId === listId) panel.__jhsPendingFocusListId = listId; for (const input of matchingListCheckboxes(listId)) if (busy) { if (input.dataset.vltWasDisabled === void 0) input.dataset.vltWasDisabled = input.disabled ? "1" : "0"; input.disabled = true; input.setAttribute("aria-busy", "true"); } else { if (input.dataset.vltWasDisabled === void 0) continue; input.disabled = input.dataset.vltWasDisabled === "1"; delete input.dataset.vltWasDisabled; input.removeAttribute("aria-busy"); } if (!busy && panel?.__jhsPendingFocusListId === listId) { delete panel.__jhsPendingFocusListId; requestAnimationFrame(() => { const currentActive = document.activeElement; if (!(!currentActive || currentActive === document.body || currentActive instanceof HTMLInputElement && currentActive.dataset.listId === listId)) return; Array.from(panel.querySelectorAll("input[type=\"checkbox\"][data-list-id]")).find((input) => input.dataset.listId === listId && !input.disabled)?.focus({ preventScroll: true }); }); } } function setListDisplayedCount(listId, count) { for (const input of matchingListCheckboxes(listId)) { const label = input.closest("label"); const span = label?.querySelector(".jhs-list-item__count") || label?.querySelector("span"); if (!span) continue; if (span.classList.contains("jhs-list-item__count")) { span.textContent = String(count); span.setAttribute("title", `清单内 ${count} 部影片`); span.setAttribute("aria-label", `${count} 部影片`); } else span.textContent = `(${count})`; } } function refreshListPanel() { const panel = document.querySelector(".jhs-list-panel"); if (!panel) return; if (panel.__jhsRefresh) { panel.__jhsRefresh(); return; } const lc = document.querySelector("#modal-save-list [data-list-target=\"listContainer\"]"); if (!lc) return; const items = panel.querySelector(".jhs-list-panel__items") || panel; items.innerHTML = ""; Array.from(lc.children).forEach((child) => { if (/[预預][设設]清[单單]/.test(child.textContent)) return; items.appendChild(child.cloneNode(true)); }); } var LOG_PREFIX$5 = "[JavDB]"; var LAST_SYNC_KEY$1 = "jdb:last-sync"; var LIST_MGMT_KEY$1 = "jdb:list-mgmt"; function broadcastSyncComplete(designation, action, association) { broadcastSend(LAST_SYNC_KEY$1, { designation, action, association, time: Date.now() }, { eventName: "jdb:sync-complete", onError: (channel, error) => { console.warn(`${LOG_PREFIX$5} ${channel === "gm" ? "GM 同步广播失败" : channel === "localStorage" ? "localStorage 同步广播失败" : "页面同步广播失败"}`, error); } }); } function broadcastListMgmt(type, listId, extra) { broadcastSend(LIST_MGMT_KEY$1, { type, listId, newName: extra?.newName, time: Date.now() }, { onError: () => {} }); } var queue = new KeyedQueue(); var LOCK_PREFIX = "javdb-power-tools:vlt:"; function enqueueAssociationTask(key, task) { return queue.enqueue(key, () => withExclusiveLock(LOCK_PREFIX + key, task)); } var SIMPLE_LISTS_PAGE_TIMEOUT_MS = 1e4; async function fetchSimpleListsPage(url, vid) { const controller = new AbortController(); const timeout = window.setTimeout(() => controller.abort(), SIMPLE_LISTS_PAGE_TIMEOUT_MS); try { const response = await fetch(url, { credentials: "same-origin", cache: "no-store", headers: { Accept: "application/json" }, signal: controller.signal }); if (!response.ok) throw new Error(`清单分页请求失败:HTTP ${response.status}`); const payload = await response.json(); if (typeof payload.lists !== "string") throw new Error("清单分页响应格式异常"); const pageHtml = typeof payload.page === "string" ? payload.page : ""; const nextHref = new DOMParser().parseFromString(pageHtml, "text/html").querySelector("a[rel=\"next\"]")?.getAttribute("href"); if (!nextHref) return { entries: payload.lists, nextUrl: null }; const nextUrl = new URL(nextHref, url); if (nextUrl.origin !== window.location.origin || nextUrl.pathname !== "/users/simple_lists") throw new Error("清单下一页地址异常"); if (vid) nextUrl.searchParams.set("vid", vid); return { entries: payload.lists, nextUrl: nextUrl.href }; } finally { window.clearTimeout(timeout); } } function createPendingJournal(entry) { try { const randomPart = typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Math.random().toString(36).slice(2)}-${Date.now()}`; const key = `${PENDING_SYNC_PREFIX}${entry.createdAt}:${randomPart}`; localStorage.setItem(key, JSON.stringify(entry)); return key; } catch (error) { clog.error(`${LOG_PREFIX$6} 无法写入清单同步恢复日志`, error); return null; } } function removePendingJournal(key) { if (!key) return; try { localStorage.removeItem(key); } catch {} } function readPendingJournals() { const keys = []; try { for (let index = 0; index < localStorage.length; index++) { const key = localStorage.key(index); if (key?.startsWith("jdb:vlt-pending-server-sync:")) keys.push(key); } } catch { return []; } const entries = []; for (const key of keys) try { const value = localStorage.getItem(key); if (!value) continue; const entry = JSON.parse(value); if (entry.version !== 1 || !entry.videoId || !entry.movieInfo?.designation || !entry.listInfo?.list_id || typeof entry.desiredChecked !== "boolean" || typeof entry.createdAt !== "number") { removePendingJournal(key); continue; } entries.push({ key, entry }); } catch { removePendingJournal(key); } return entries.sort((left, right) => left.entry.createdAt - right.entry.createdAt); } async function postJavdbListMutation(videoId, listId, checked) { const csrfToken = getCsrfToken$1(); if (!csrfToken) return { kind: "rejected", message: "无法获取 CSRF token" }; try { const response = await fetch(new URL("/users/save_video_to_list", window.location.origin).href, { method: "POST", body: JSON.stringify({ video_id: videoId, checked, list_id: listId }), headers: { "Content-Type": "application/json", "X-CSRF-Token": csrfToken }, credentials: "same-origin" }); const data = await response.json().catch(() => null); const message = typeof data?.message === "string" ? data.message : `HTTP ${response.status}`; if (response.ok && data?.success === true) return { kind: "confirmed", message }; if (response.ok && data?.success === false) return { kind: "rejected", message }; return { kind: "unknown", message }; } catch (error) { return { kind: "unknown", message: error instanceof Error ? error.message : String(error) }; } } async function fetchAuthoritativeCheckboxState(videoId, listId) { try { let url = new URL("/users/simple_lists", window.location.origin); url.searchParams.set("vid", videoId); const visited = new Set(); for (let page = 0; url && page < 50; page++) { if (visited.has(url.href)) return null; visited.add(url.href); const result = await fetchSimpleListsPage(url.href, videoId); const documentNode = new DOMParser().parseFromString(result.entries, "text/html"); const input = Array.from(documentNode.querySelectorAll("input[type=\"checkbox\"][data-list-id]")).find((item) => item.dataset.listId === listId); if (input) return { checked: input.checked, count: getCheckboxCount(input), name: extractListNameFromInput(input) || null }; if (!result.nextUrl) return { checked: false, count: null, name: null }; url = new URL(result.nextUrl); } return null; } catch (error) { clog.error(`${LOG_PREFIX$6} 查询 JavDB 权威清单状态失败`, error); return null; } } var LOG_PREFIX$4 = "[JavDB][清单对账]"; var MAX_PAGES$2 = 100; var autoReconcileDelay = 800; var autoServerSyncEnabled = true; function setAutoReconcileDelay(ms) { if (Number.isFinite(ms) && ms > 0) autoReconcileDelay = ms; } function setAutoServerSyncEnabled(enabled) { autoServerSyncEnabled = enabled; } var reconcileQueues = new Map(); var automaticReconciliationInstalled = false; var automaticReconciliationInstalling = false; var automaticReconciliationTimer = null; var automaticReconciliationRunning = false; function normalizeText(value) { return (value || "").replace(/\s+/g, " ").trim(); } function getSeries(designation) { const index = designation.indexOf("-"); return index > 0 ? designation.slice(0, index) : designation || null; } function getCode$1(designation) { const index = designation.indexOf("-"); return index > 0 ? designation.slice(index + 1) : null; } function getDisplayedListCount(input) { const label = input.closest("label"); const match = (label?.querySelector("span")?.textContent || label?.textContent || "").match(/\((\d+)\)/); if (!match) return null; const count = Number(match[1]); return Number.isSafeInteger(count) ? count : null; } function getDisplayedListName(input) { return normalizeText(input.closest("label")?.textContent).replace(/\s*\(\d+\).*$/, "").trim(); } async function fetchHtmlDocument(url) { const html = await fetchJavdbPageHtml(url); return new DOMParser().parseFromString(html, "text/html"); } function parseExpectedCountAndName(documentNode) { const headings = Array.from(documentNode.querySelectorAll("h1, h2, h3, .title")); for (const heading of headings) { const match = normalizeText(heading.textContent).match(/^(.*?)\s+(\d+)\s*部影片/); if (!match) continue; return { name: match[1].trim(), expectedCount: Number(match[2]) }; } throw new Error("无法从清单标题读取服务端影片总数"); } function parseMovie(item, pageUrl) { const link = item.querySelector("a[href*=\"/v/\"]"); const designation = normalizeText(item.querySelector(".video-title strong")?.textContent); const hrefValue = link?.getAttribute("href") || ""; if (!designation || !hrefValue) throw new Error("清单分页中存在无法识别的影片卡片"); const titleText = normalizeText(item.querySelector(".video-title")?.textContent); const title = titleText.startsWith(designation) ? titleText.slice(designation.length).trim() : titleText; const image = item.querySelector("img"); const coverValue = image?.getAttribute("src") || image?.getAttribute("data-src") || image?.currentSrc || ""; const scoreMatch = normalizeText(item.querySelector(".score")?.textContent).match(/(\d+(?:\.\d+)?)/); const releaseDate = normalizeText(item.querySelector(".meta")?.textContent); return { designation, href: new URL(hrefValue, pageUrl).href, title: title || null, coverSrc: coverValue ? new URL(coverValue, pageUrl).href : null, score: scoreMatch ? Number(scoreMatch[1]) : null, releaseDate: releaseDate || null, createdAt: null, series: getSeries(designation), code: getCode$1(designation) }; } function parseListPage(documentNode, pageUrl, listId, firstPage) { const heading = firstPage ? parseExpectedCountAndName(documentNode) : null; const movies = Array.from(documentNode.querySelectorAll(".movie-list .item")).map((item) => parseMovie(item, pageUrl)); const nextHref = documentNode.querySelector("a.pagination-next")?.getAttribute("href"); let nextUrl = null; if (nextHref) { const candidate = new URL(nextHref, pageUrl); const expectedPath = `/lists/${encodeURIComponent(listId)}`; if (candidate.origin !== window.location.origin || candidate.pathname !== expectedPath) throw new Error(`清单下一页地址异常:${candidate.href}`); nextUrl = candidate.href; } return { expectedCount: heading?.expectedCount ?? null, name: heading?.name ?? null, movies, nextUrl }; } async function fetchCompleteListPass(listId) { const firstUrl = new URL(`/lists/${encodeURIComponent(listId)}`, window.location.origin); firstUrl.searchParams.set("locale", "zh"); const visited = new Set(); const movies = []; let expectedCount = null; let name = null; let nextUrl = firstUrl.href; while (nextUrl) { if (visited.size >= MAX_PAGES$2) throw new Error(`清单分页超过 ${MAX_PAGES$2} 页,已中止`); const normalizedUrl = new URL(nextUrl).href; if (visited.has(normalizedUrl)) throw new Error("清单分页出现循环,已中止"); visited.add(normalizedUrl); const parsed = parseListPage(await fetchHtmlDocument(normalizedUrl), normalizedUrl, listId, visited.size === 1); if (visited.size === 1) { expectedCount = parsed.expectedCount; name = parsed.name; } movies.push(...parsed.movies); nextUrl = parsed.nextUrl; } if (expectedCount === null || name === null) throw new Error("清单首页信息不完整"); const uniqueDesignations = new Set(movies.map((movie) => movie.designation)); if (movies.length !== expectedCount || uniqueDesignations.size !== expectedCount) throw new Error(`清单分页不完整或含重复番号:服务端 ${expectedCount},抓取 ${movies.length},唯一 ${uniqueDesignations.size}`); return { listId, name, url: firstUrl.href, expectedCount, movies }; } async function fetchCompleteListSnapshot(listId) { const first = await fetchCompleteListPass(listId); await new Promise((resolve) => setTimeout(resolve, 250)); const second = await fetchCompleteListPass(listId); const firstSequence = first.movies.map((movie) => movie.designation).join(""); const secondSequence = second.movies.map((movie) => movie.designation).join(""); if (first.expectedCount !== second.expectedCount || first.name !== second.name || firstSequence !== secondSequence) throw new Error("清单在两次完整抓取之间发生变化,已放弃本次快照"); return second; } function snapshotMatchesExpectation(snapshot, expectation) { if (expectation.expectedCount !== void 0 && snapshot.expectedCount !== expectation.expectedCount) return false; if (expectation.designation !== void 0 && expectation.checked !== void 0) return snapshot.movies.some((movie) => movie.designation === expectation.designation) === expectation.checked; return true; } async function performReconciliation(listId, expectation) { try { for (let attempt = 0; attempt < 2; attempt++) { const state = await VltDb.getListReconcileState(listId); const snapshot = await fetchCompleteListSnapshot(listId); if (!snapshotMatchesExpectation(snapshot, expectation)) throw new Error("服务端清单状态已变化,与触发对账时的状态不一致"); const result = await VltDb.reconcileListSnapshot(snapshot, state.guard); if (result.status === "conflict") { console.warn(`${LOG_PREFIX$4} 本地数据在抓取期间变化,准备重试 listId=${listId}`); continue; } console.log(`${LOG_PREFIX$4} 完成 listId=${listId} count=${result.count} added=${result.added} removed=${result.removed}`); if (!expectation.quiet && (result.added > 0 || result.removed > 0)) showToast$1(`✓ 已按 JavDB 校准「${snapshot.name}」:新增 ${result.added},移除 ${result.removed}`, "success"); return true; } throw new Error("本地清单持续变化,两次对账均发生冲突"); } catch (error) { clog.error(`${LOG_PREFIX$4} 失败 listId=${listId}`, error); if (!expectation.quiet) showToast$1(`⚠ 清单自动校准失败:${error instanceof Error ? error.message : String(error)}`, "warning"); return false; } } function reconcileListWithJavdb(listId, expectation = {}) { const current = (reconcileQueues.get(listId) || Promise.resolve(true)).catch(() => false).then(() => performReconciliation(listId, expectation)); reconcileQueues.set(listId, current); current.finally(() => { if (reconcileQueues.get(listId) === current) reconcileQueues.delete(listId); }); return current; } async function reconcileListBeforeMutation(input, designation, checkedBeforeMutation) { const listId = input.dataset.listId || ""; const serverCount = getDisplayedListCount(input); if (!listId || serverCount === null) return; const state = await VltDb.getListReconcileState(listId, designation); if (!state.inventory) return; if (state.actualCount === serverCount && state.hasDesignation === checkedBeforeMutation) return; console.warn(`${LOG_PREFIX$4} 检测到差异 listId=${listId} local=${state.actualCount} server=${serverCount} localChecked=${state.hasDesignation} serverChecked=${checkedBeforeMutation}`); await reconcileListWithJavdb(listId, { expectedCount: serverCount, designation, checked: checkedBeforeMutation, quiet: true }); } function reconcileAfterConfirmedMutation(listId, designation, checked) { return reconcileListWithJavdb(listId, { designation, checked, quiet: false }); } async function pushLocalOnlyAssociation(listId, designation) { const videoId = window.location.pathname.match(/\/v\/([^/?#]+)/)?.[1]; if (!videoId) return; const movieInfo = getMovieInfo(videoId); const listInfo = getListInfo(listId); if (!movieInfo || !listInfo.info.name) return; await enqueueAssociationTask(`${designation}::${listId}`, async () => { try { const result = await postJavdbListMutation(videoId, listId, true); if (result.kind === "confirmed") { const state = await VltDb.getListReconcileState(listId); if (state.inventory && state.actualCount !== null) setListDisplayedCount(listId, state.actualCount); console.log(`${LOG_PREFIX$4} 已补推本地独有关联:${designation} → ${listInfo.info.name} (listId=${listId})`); showToast$1(`✓ [${designation}] 已补推至「${listInfo.info.name}」(服务器未同步)`, "success"); } else showToast$1(`⚠ [${designation}] 补推至「${listInfo.info.name}」未确认:${result.message}`, "warning"); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(`${LOG_PREFIX$4} 补推本地独有关联失败:${designation} → ${listId}`, error); showToast$1(`⚠ [${designation}] 补推至「${listInfo.info.name}」失败:${message}`, "warning"); } }); } async function runAutomaticReconciliation(designation) { if (automaticReconciliationRunning) return; automaticReconciliationRunning = true; try { const inputs = Array.from(document.querySelectorAll("#modal-save-list input[type=\"checkbox\"][data-list-id][data-action=\"change->list#listCheckboxChanged\"]")); for (const input of inputs) { const listId = input.dataset.listId || ""; const serverCount = getDisplayedListCount(input); if (!listId || serverCount === null) continue; const state = await VltDb.getListReconcileState(listId, designation); if (!state.inventory) continue; if (state.hasDesignation === input.checked) { if (state.actualCount === serverCount) continue; console.warn(`${LOG_PREFIX$4} 页面加载发现差异「${getDisplayedListName(input)}」 local=${state.actualCount} server=${serverCount}`); await reconcileListWithJavdb(listId, { expectedCount: serverCount, designation, checked: input.checked, quiet: true }); continue; } if (state.hasDesignation && !input.checked) { if (autoServerSyncEnabled) { console.warn(`${LOG_PREFIX$4} 页面加载发现本地独有关联「${getDisplayedListName(input)}」 ${designation} local=关联 server=未勾选,补推 JavDB`); await pushLocalOnlyAssociation(listId, designation); } else console.warn(`${LOG_PREFIX$4} 自动补推已关闭,跳过本地独有关联「${getDisplayedListName(input)}」 ${designation}`); continue; } console.warn(`${LOG_PREFIX$4} 页面加载发现差异「${getDisplayedListName(input)}」 local=${state.actualCount} server=${serverCount}`); await reconcileListWithJavdb(listId, { expectedCount: serverCount, designation, checked: input.checked, quiet: true }); } } finally { automaticReconciliationRunning = false; } } function setupAutomaticListReconciliation(designation) { if (automaticReconciliationInstalled || automaticReconciliationInstalling) return; automaticReconciliationInstalling = true; const schedule = () => { if (automaticReconciliationTimer) clearTimeout(automaticReconciliationTimer); automaticReconciliationTimer = setTimeout(() => { automaticReconciliationTimer = null; runAutomaticReconciliation(designation).then(); }, autoReconcileDelay); }; const attach = () => { const modal = document.querySelector("#modal-save-list"); if (!modal) { setTimeout(attach, 250); return; } automaticReconciliationInstalling = false; automaticReconciliationInstalled = true; new MutationObserver(schedule).observe(modal, { childList: true, subtree: true }); schedule(); }; attach(); } async function commitAuthoritativeState(entry, authoritative, notify) { if (authoritative.checked !== entry.desiredChecked) { if ((await postJavdbListMutation(entry.videoId, entry.listInfo.list_id, entry.desiredChecked)).kind !== "confirmed") { console.warn(`${LOG_PREFIX$6} 权威状态与期望相反且重推未确认,保留恢复日志:${entry.movieInfo.designation} ${entry.listInfo.list_id}`); return false; } const recheck = await fetchAuthoritativeCheckboxState(entry.videoId, entry.listInfo.list_id); if (!recheck || recheck.checked !== entry.desiredChecked) { console.warn(`${LOG_PREFIX$6} 重推后仍无法确认期望状态,保留恢复日志:${entry.movieInfo.designation} ${entry.listInfo.list_id}`); return false; } authoritative = recheck; } const listInfo = { ...entry.listInfo, info: { ...entry.listInfo.info, name: authoritative.name || entry.listInfo.info.name } }; let reconciledBeforeCommit = false; if (authoritative.count !== null) { const before = await VltDb.getListReconcileState(entry.listInfo.list_id, entry.movieInfo.designation); if (before.actualCount + (authoritative.checked && before.hasDesignation === false ? 1 : !authoritative.checked && before.hasDesignation === true ? -1 : 0) !== authoritative.count) { reconciledBeforeCommit = await reconcileListWithJavdb(entry.listInfo.list_id, { expectedCount: authoritative.count, designation: entry.movieInfo.designation, checked: authoritative.checked, quiet: !notify }); if (!reconciledBeforeCommit) return false; } } if (!await handleCheckboxChange(entry.movieInfo, listInfo, authoritative.checked, { serverConfirmed: true, silent: !notify || reconciledBeforeCommit, skipAutomation: !notify })) return false; setListCheckboxState(entry.listInfo.list_id, authoritative.checked); if (authoritative.count !== null) { setListDisplayedCount(entry.listInfo.list_id, authoritative.count); const state = await VltDb.getListReconcileState(entry.listInfo.list_id, entry.movieInfo.designation); if (state.inventory && (state.actualCount !== authoritative.count || state.hasDesignation !== authoritative.checked)) return reconcileListWithJavdb(entry.listInfo.list_id, { expectedCount: authoritative.count, designation: entry.movieInfo.designation, checked: authoritative.checked, quiet: !notify }); } return true; } async function recoverPendingServerSyncs() { for (const { key, entry } of readPendingJournals()) await enqueueAssociationTask(`${entry.movieInfo.designation}::${entry.listInfo.list_id}`, async () => { const authoritative = await fetchAuthoritativeCheckboxState(entry.videoId, entry.listInfo.list_id); if (!authoritative) return; if (await commitAuthoritativeState(entry, authoritative, false)) { removePendingJournal(key); console.log(`${LOG_PREFIX$6} 已恢复未完成清单同步:${entry.movieInfo.designation} ${authoritative.checked ? "add" : "remove"} ${entry.listInfo.list_id}`); } }); } var checkboxListenerInstalled = false; async function performServerCheckboxMutation(movieInfo, listInfo, checked, input, checkedBeforeMutation) { const listId = listInfo.list_id; let confirmedChecked = null; setListCheckboxState(listId, checked); setListCheckboxBusy(listId, true); try { if (input) await reconcileListBeforeMutation(input, movieInfo.designation, checkedBeforeMutation); const pending = { version: 1, videoId: input?.value || movieInfo.info.href.match(/\/v\/([^/?#]+)/)?.[1] || "", desiredChecked: checked, movieInfo, listInfo, createdAt: Date.now() }; if (!pending.videoId) throw new Error("无法识别 JavDB 影片 ID"); const journalKey = createPendingJournal(pending); if (!journalKey) throw new Error("无法建立清单同步恢复日志,本次操作已取消"); const serverResult = await postJavdbListMutation(pending.videoId, listId, checked); if (serverResult.kind === "rejected") { removePendingJournal(journalKey); setListCheckboxState(listId, checkedBeforeMutation); showToast$1(`✗ JavDB 清单更新失败:${serverResult.message}`, "error"); return; } if (serverResult.kind === "unknown") { const observed = await fetchAuthoritativeCheckboxState(pending.videoId, listId); setListCheckboxState(listId, observed?.checked ?? checkedBeforeMutation); if (observed?.count !== null && observed?.count !== void 0) setListDisplayedCount(listId, observed.count); showToast$1(`⚠ 无法确认 JavDB 最终状态(${serverResult.message}),本地未改动;下次打开页面会自动核对`, "warning"); return; } confirmedChecked = checked; const authoritative = await fetchAuthoritativeCheckboxState(pending.videoId, listId); if (authoritative) { confirmedChecked = authoritative.checked; if (await commitAuthoritativeState(pending, authoritative, true)) removePendingJournal(journalKey); else showToast$1("⚠ JavDB 已更新,本地完整清单将在下次打开页面时继续校准", "warning"); return; } setListCheckboxState(listId, checked); if (await reconcileAfterConfirmedMutation(listId, movieInfo.designation, checked) ? await handleCheckboxChange(movieInfo, listInfo, checked, { serverConfirmed: true }) : null) removePendingJournal(journalKey); else showToast$1("⚠ JavDB 已更新,但暂时无法复核最终清单;恢复日志已保留", "warning"); } catch (error) { setListCheckboxState(listId, confirmedChecked === null ? checkedBeforeMutation : confirmedChecked); const message = error instanceof Error ? error.message : String(error); console.error(`${LOG_PREFIX$6} JavDB 清单同步失败`, error); showToast$1(confirmedChecked === null ? `✗ JavDB 清单同步失败:${message}` : `⚠ JavDB 已更新,本地镜像将在下次打开页面时继续恢复:${message}`, confirmedChecked === null ? "error" : "warning"); } finally { setListCheckboxBusy(listId, false); } } function enqueueServerCheckboxMutation(movieInfo, listInfo, checked, input, checkedBeforeMutation) { return enqueueAssociationTask(`${movieInfo.designation}::${listInfo.list_id}`, () => performServerCheckboxMutation(movieInfo, listInfo, checked, input, checkedBeforeMutation)); } function setupCheckboxListener() { if (checkboxListenerInstalled) return; checkboxListenerInstalled = true; document.addEventListener("change", (event) => { const input = event.target; if (input.tagName !== "INPUT" || input.type !== "checkbox" || input.dataset.action !== "change->list#listCheckboxChanged" || !input.closest("#modal-save-list")) return; event.preventDefault(); event.stopImmediatePropagation(); const checked = input.checked; const checkedBeforeMutation = !checked; const movieInfo = getMovieInfo(input.value); const listInfo = getListInfo(input.dataset.listId || ""); if (!movieInfo || !listInfo.list_id || !listInfo.info.name) { setListCheckboxState(listInfo.list_id, checkedBeforeMutation); showToast$1("✗ 无法读取影片或清单信息,本次操作已取消", "error"); return; } enqueueServerCheckboxMutation(movieInfo, listInfo, checked, input, checkedBeforeMutation).then(); }, true); const currentVideoId = window.location.pathname.match(/\/v\/([^/?#]+)/)?.[1]; const currentMovie = currentVideoId ? getMovieInfo(currentVideoId) : null; if (currentMovie) setupAutomaticListReconciliation(currentMovie.designation); recoverPendingServerSyncs().then(); } var autoFavCleanupEnabled = true; var protectedKeywords = ["值得复盘"]; function setAutoFavCleanupEnabled(enabled) { autoFavCleanupEnabled = enabled; } function setProtectedKeywords(keywords) { if (keywords.length > 0) protectedKeywords = keywords; } function isProtectedList(name) { return protectedKeywords.some((kw) => kw !== "" && name.includes(kw)); } var ASSOC_TOAST = { created: (des, lname, created) => { return { msg: `✓ [${des}] 已添加至「${lname}」${created.length > 0 ? `(已登记${created.join("和")})` : ""}`, type: "success" }; }, existed: (des, lname) => ({ msg: `✓ [${des}] 已在「${lname}」中,数据一致`, type: "success" }), limit_exceeded: (des, lname) => ({ msg: `✗ [${des}]「${lname}」已达收藏上限(501 条)`, type: "error" }), deleted: (des, lname) => ({ msg: `✓ [${des}] 已从「${lname}」移除`, type: "success" }), unchanged: (des, lname) => ({ msg: `✓ [${des}] 未关联「${lname}」,数据一致`, type: "success" }) }; async function syncMoviesLists(movieInfo, listInfo, action, serverConfirmed = false) { console.log(`${LOG_PREFIX$6} 同步(IDB): ${movieInfo.designation} → ${listInfo.info.name} (${action})`); const result = await VltDb.sync(movieInfo.designation, listInfo.list_id, { href: movieInfo.info.href, title: movieInfo.info.title, cover_src: movieInfo.info.cover_src, score: movieInfo.info.score, release_date: movieInfo.info.release_date, series: movieInfo.info.series, code: movieInfo.info.code }, { url: listInfo.info.url, name: listInfo.info.name }, action, { serverConfirmed }); console.log(`${LOG_PREFIX$6} 同步结果: movie=${result.movie} list=${result.list} association=${result.association}`); return result; } async function autoFavoriteIfPendingUpdate(movieInfo, lname) { if (!autoFavCleanupEnabled) return; if (isProtectedList(lname)) return; const des = movieInfo.designation; try { const carRecord = await storageManager.getCar(des); if (carRecord && carRecord.status === "favorite") return; if (carRecord && carRecord.status) { showToast$1(`ℹ️ [${des}] 已标记为「${carRecord.status}」,跳过自动收藏`, "info"); return; } } catch (err) { clog.error(`${LOG_PREFIX$6} 自动收藏前读取状态失败: ${des}`, err); return; } const favBtn = document.querySelector(".jhs-fav-btn"); if (!favBtn) { console.warn(`${LOG_PREFIX$6} 未找到收藏按钮,跳过自动收藏: ${des}`); return; } favBtn.click(); console.log(`${LOG_PREFIX$6} 自动收藏: ${des}(触发清单:${lname})`); } async function handleCheckboxChange(movieInfo, listInfo, checked, options = {}) { const des = movieInfo.designation; const lname = listInfo.info.name; const action = checked ? "add" : "remove"; console.log(`${LOG_PREFIX$6} ═══ ${checked ? "勾选" : "取消"} [${des}] → ${lname} ═══`); let result; try { result = await syncMoviesLists(movieInfo, listInfo, action, options.serverConfirmed === true); } catch (err) { const message = err instanceof Error ? err.message : "请检查 IndexedDB"; clog.error(`${LOG_PREFIX$6} 同步失败`, err); if (!options.silent) showToast$1(`✗ [${des}] 同步失败:${message}`, "error"); return null; } const created = []; if (result.movie === "created") created.push("影片"); if (result.list === "created") created.push("清单"); if (!options.silent) { const entry = ASSOC_TOAST[result.association]; if (entry) { const { msg, type } = entry(des, lname, created); showToast$1(msg, type); } else showToast$1(`✗ [${des}] 未知响应:${result.association}`, "error"); } if (checked && !options.skipAutomation && result.association !== "limit_exceeded" && autoFavCleanupEnabled) { autoFavoriteIfPendingUpdate(movieInfo, lname).then(); if (!lname.includes("等待更新")) uncheckPendingUpdateListCheckboxes(); } console.log(`${LOG_PREFIX$6} ═══ 完成 ═══`); broadcastSyncComplete(des, action, result.association); return result; } async function removeFromNonProtectedListsOnWatch() { if (!autoFavCleanupEnabled) return; try { if (uncheckNonProtectedListCheckboxes() > 0) return; const pending = await findCurrentMovieListsMatching((name) => !isProtectedList(name)); if (pending.length === 0) return; for (let i = 0; i < 15; i++) { await new Promise((r) => setTimeout(r, 200)); if (uncheckNonProtectedListCheckboxes() > 0) return; } for (const item of pending) { const movieInfo = getMovieInfo(item.videoId); if (!movieInfo) continue; await enqueueServerCheckboxMutation(movieInfo, { list_id: item.listId, info: { url: item.url, name: item.name } }, false, null, true); } } catch (err) { clog.error(`${LOG_PREFIX$6} 评分后移出非受保护清单失败`, err); } } function uncheckListsMatching(predicate) { const inputs = document.querySelectorAll("#modal-save-list input[type=\"checkbox\"][data-action=\"change->list#listCheckboxChanged\"]"); const seen = new Set(); let count = 0; for (const input of inputs) { if (!input.checked) continue; const listId = input.dataset.listId || ""; if (!listId || seen.has(listId)) continue; const name = getListName(listId); if (!predicate(name)) continue; seen.add(listId); count++; input.checked = false; input.dispatchEvent(new Event("change", { bubbles: true })); const panelCb = document.querySelector(`.jhs-list-panel input[data-list-id="${listId}"]`); if (panelCb) panelCb.checked = false; console.log(`${LOG_PREFIX$6} 取消勾选「${name}」(listId=${listId})`); } return count; } function uncheckPendingUpdateListCheckboxes() { return uncheckListsMatching((name) => name.includes(AUTO_FAVORITE_KEYWORD)); } function uncheckNonProtectedListCheckboxes() { return uncheckListsMatching((name) => !isProtectedList(name)); } async function findCurrentMovieListsMatching(predicate) { const videoId = window.location.pathname.match(/\/v\/([^/?#]+)/)?.[1]; if (!videoId) return []; const movieInfo = getMovieInfo(videoId); if (!movieInfo) return []; const lists = (await VltDb.queryMoviesLists([movieInfo.designation]))[movieInfo.designation] || []; const result = []; for (const item of lists) { if (!predicate(item.name)) continue; const listId = item.url?.match(/\/lists\/([^/?#]+)/)?.[1]; if (!listId) continue; result.push({ videoId, listId, name: item.name, url: item.url || `https://javdb.com/lists/${listId}?locale=zh` }); } return result; } async function createList(listName) { const modal = document.querySelector("#modal-save-list"); if (!modal) { showToast$1("✗ 未找到存入清单弹窗,请重新进入详情页", "error"); return "failed"; } const nameInput = modal.querySelector("input[data-list-target=\"listNewNameInput\"]"); const form = modal.querySelector("#new_list"); const listContainer = modal.querySelector("[data-list-target=\"listContainer\"]"); if (!nameInput || !form || !listContainer) { showToast$1("✗ 清单新建表单未就绪,请重新进入详情页", "error"); return "failed"; } const formData = {}; Array.from(form.querySelectorAll("input, textarea, select")).forEach((el) => { const field = el; if (field.name && field.type !== "submit" && field.type !== "button" && field.type !== "reset") formData[field.name] = field.value; }); if (nameInput.name) formData[nameInput.name] = listName; const csrfToken = document.querySelector("meta[name=\"csrf-token\"]")?.content || formData["authenticity_token"] || ""; const beforeIds = new Set(Array.from(listContainer.querySelectorAll("input[type=\"checkbox\"][data-list-id]")).map((el) => el.dataset.listId)); const videoId = formData["video_id"] || form.querySelector("input[name=\"video_id\"]")?.value || ""; if (!videoId) { showToast$1("✗ 无法识别当前影片,清单未新建", "error"); return "failed"; } showToast$1("正在新建清单…", "info"); console.log(`${LOG_PREFIX$6} ═══ 新增清单「${listName}」(video_id=${videoId}) ═══`); let responseText; try { responseText = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: "https://javdb.com/lists/remote_create", headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", Accept: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*;q=0.01", "X-CSRF-Token": csrfToken, "X-Requested-With": "XMLHttpRequest" }, data: Object.entries(formData).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&"), timeout: 15e3, onload: (r) => { if (r.status >= 200 && r.status < 300) resolve(r.responseText || ""); else reject(new Error(`HTTP ${r.status}: ${r.responseText?.slice(0, 200) || ""}`)); }, onerror: () => reject(new Error("网络错误")), ontimeout: () => reject(new Error("请求超时")) }); }); } catch (err) { const message = err instanceof Error ? err.message : "请稍后重试"; console.error(`${LOG_PREFIX$6} 新增清单失败`, err); showToast$1(`✗ 新建清单失败:${message}`, "error"); return "failed"; } console.log(`${LOG_PREFIX$6} 服务端响应(前 500 字): ${responseText.slice(0, 500)}`); if (responseText.trim()) try { const script = document.createElement("script"); script.textContent = responseText; (document.body || document.documentElement).appendChild(script); script.remove(); console.log(`${LOG_PREFIX$6} JS 响应已注入执行`); } catch (e) { console.warn(`${LOG_PREFIX$6} JS 响应注入失败`, e); } if (/\btoastr\s*(?:\.\s*error|\[\s*['"]error['"]\s*\])\s*\(/i.test(responseText)) { showToast$1("✗ JavDB 拒绝新建清单,请检查名称后重试", "error"); return "failed"; } let newCheckboxes = await pollForNewCheckboxes(modal, beforeIds, 200); if (newCheckboxes.length > 0) { console.log(`${LOG_PREFIX$6} 侦测到 ${newCheckboxes.length} 个新 checkbox,走正常完成流程`); const confirmedCheckbox = await resolveCreatedListCheckbox(newCheckboxes, listName, videoId, beforeIds); if (confirmedCheckbox) { finishCreateList(confirmedCheckbox, listName); return "created"; } } const listIdMatch = responseText.match(/data-list-id=["']([^"']+)["']/); if (listIdMatch) { const listId = listIdMatch[1]; const confirmedCheckbox = await resolveAuthoritativeCreatedCheckbox(listId, listName, videoId, beforeIds); if (confirmedCheckbox) { console.log(`${LOG_PREFIX$6} 从响应提取并权威确认 list-id=${listId}`); finishCreateList(confirmedCheckbox, listName); return "created"; } } console.log(`${LOG_PREFIX$6} 响应无 list-id,通过切换 #save-list-button 重载清单列表`); const saveListBtn = document.querySelector("#save-list-button"); if (saveListBtn) { saveListBtn.click(); await new Promise((r) => setTimeout(r, 200)); saveListBtn.click(); newCheckboxes = await pollForNewCheckboxes(modal, beforeIds, 5e3); if (newCheckboxes.length > 0) { console.log(`${LOG_PREFIX$6} 重载后侦测到 ${newCheckboxes.length} 个新 checkbox,走正常完成流程`); const confirmedCheckbox = await resolveCreatedListCheckbox(newCheckboxes, listName, videoId, beforeIds); if (confirmedCheckbox) { finishCreateList(confirmedCheckbox, listName); return "created"; } } } console.log(`${LOG_PREFIX$6} 按钮重载未找到新 checkbox,尝试 /users/lists 作为最后手段`); const listId = await fetchListIdByName(listName); if (listId) { const confirmedCheckbox = await resolveAuthoritativeCreatedCheckbox(listId, listName, videoId, beforeIds); if (confirmedCheckbox) { console.log(`${LOG_PREFIX$6} 从 /users/lists 查得并权威确认 list_id=${listId}`); finishCreateList(confirmedCheckbox, listName); return "created"; } } console.error(`${LOG_PREFIX$6} 新增清单后无法定位新 checkbox。响应前 300 字: ${responseText.slice(0, 300)}`); refreshListPanel(); showToast$1(`⚠ 清单「${listName}」可能已新建,请刷新页面确认`, "warning"); return "unknown"; } function pollForNewCheckboxes(modal, beforeIds, timeoutMs) { return new Promise((resolve) => { const start = Date.now(); const check = () => { const lc = modal.querySelector("[data-list-target=\"listContainer\"]"); if (lc) { const newOnes = Array.from(lc.querySelectorAll("input[type=\"checkbox\"][data-list-id]")).filter((cb) => !beforeIds.has(cb.dataset.listId)); if (newOnes.length > 0) { resolve(newOnes); return; } } if (Date.now() - start < timeoutMs) setTimeout(check, 100); else resolve([]); }; check(); }); } function findCurrentListCheckbox(listId) { const container = getCurrentListContainer(); if (!container) return null; return Array.from(container.querySelectorAll("input[type=\"checkbox\"][data-list-id]")).find((input) => input.dataset.listId === listId) || null; } async function resolveAuthoritativeCreatedCheckbox(listId, listName, videoId, beforeIds) { if (!listId || !videoId || beforeIds.has(listId)) return null; const authoritative = await fetchAuthoritativeCheckboxState(videoId, listId); if (!authoritative?.checked || !authoritative.name || normalizeListName(authoritative.name) !== normalizeListName(listName) || authoritative.count !== null && authoritative.count < 1) return null; let input = findCurrentListCheckbox(listId); if (!input) { const container = getCurrentListContainer(); input = container ? manuallyBuildCheckbox(container, listId, authoritative.name, videoId, authoritative.count ?? 1) : null; } if (!input) return null; input.checked = true; if (authoritative.count !== null) setListDisplayedCount(listId, authoritative.count); return input; } async function resolveCreatedListCheckbox(candidates, listName, videoId, beforeIds) { const candidateIds = Array.from(new Set(candidates.map((input) => input.dataset.listId).filter(Boolean))); const confirmed = []; for (const listId of candidateIds) { const input = await resolveAuthoritativeCreatedCheckbox(listId, listName, videoId, beforeIds); if (input) confirmed.push(input); } if (confirmed.length === 1) return confirmed[0]; if (confirmed.length > 1) console.warn(`${LOG_PREFIX$6} 新建清单出现多个权威候选,拒绝写入本地`, candidateIds); return null; } async function fetchListIdByName(listName) { try { const html = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: "https://javdb.com/users/lists", timeout: 1e4, onload: (r) => { if (r.status >= 200 && r.status < 300) resolve(r.responseText || ""); else reject(new Error(`HTTP ${r.status}`)); }, onerror: () => reject(new Error("网络错误")), ontimeout: () => reject(new Error("请求超时")) }); }); const doc = new DOMParser().parseFromString(html, "text/html"); let links = doc.querySelectorAll("a[href*=\"/lists/\"]"); if (links.length === 0) links = doc.querySelectorAll("[href*=\"/lists/\"]"); console.log(`${LOG_PREFIX$6} /users/lists 页面解析到 ${links.length} 条 /lists/ 链接`); const matchedIds = new Set(); const expectedName = normalizeListName(listName); for (const link of Array.from(links)) { const m = (link.getAttribute("href") || "").match(/\/lists\/([^/?#]+)/); if (!m) continue; const id = m[1]; if (id === "users" || id === "" || id === "new") continue; if (normalizeListName((link.textContent || "").replace(/\s*\(\d+\)\s*$/, "")) === expectedName) matchedIds.add(id); } if (matchedIds.size === 1) { const [id] = matchedIds; console.log(`${LOG_PREFIX$6} /users/lists 精确匹配清单「${listName}」→ list_id=${id}`); return id; } if (matchedIds.size > 1) { console.warn(`${LOG_PREFIX$6} /users/lists 存在重名清单,拒绝猜测 list-id`, matchedIds); return null; } const bodyText = doc.body?.textContent?.slice(0, 1e3) || ""; console.warn(`${LOG_PREFIX$6} /users/lists 未精确找到「${listName}」(共 ${links.length} 条链接)。页面文本前 500 字: ${bodyText.slice(0, 500)}`); return null; } catch (err) { console.error(`${LOG_PREFIX$6} 获取 /users/lists 失败`, err); return null; } } function manuallyBuildCheckbox(listContainer, listId, listName, videoId, count) { const duplicate = Array.from(listContainer.querySelectorAll("input[type=\"checkbox\"][data-list-id]")).find((input) => input.dataset.listId === listId); if (duplicate) { duplicate.checked = true; return duplicate; } const existingCb = listContainer.querySelector("input[type=\"checkbox\"][data-list-id]"); if (!existingCb) return null; const existingLabel = existingCb.closest("label"); if (!existingLabel) return null; const clone = (existingLabel.closest("p.control") || existingLabel).cloneNode(true); const label = clone.matches("label") ? clone : clone.querySelector("label"); const cb = label?.querySelector("input[type=\"checkbox\"]"); if (!label || !cb) return null; cb.dataset.listId = listId; cb.value = videoId; cb.checked = true; cb.disabled = false; cb.removeAttribute("id"); cb.removeAttribute("disabled"); delete cb.dataset.vltWasDisabled; cb.removeAttribute("aria-busy"); label.removeAttribute("for"); for (const node of Array.from(label.childNodes)) if (node.nodeType === Node.TEXT_NODE) node.remove(); const countSpan = label.querySelector("span"); const nameNode = document.createTextNode(` ${listName} `); if (cb.parentNode === label) label.insertBefore(nameNode, cb.nextSibling); else label.appendChild(nameNode); if (countSpan) countSpan.textContent = `(${count})`; else { const createdCount = document.createElement("span"); createdCount.textContent = `(${count})`; label.appendChild(createdCount); } listContainer.appendChild(clone); return cb; } function finishCreateList(checkbox, listName) { const modal = document.querySelector("#modal-save-list"); if (modal) try { modal.querySelector("a[data-action=\"list#cancelNewList\"]")?.click(); } catch {} restoreCreateListUi(); showToast$1(`✓ 清单「${listName}」已新建,并已关联当前影片`, "success"); refreshListPanel(); const movieInfo = getMovieInfo(checkbox.value); if (!movieInfo) { console.warn(`${LOG_PREFIX$6} 新建清单后无法取得影片资讯,跳过同步`, checkbox); return; } const listInfo = getListInfo(checkbox.dataset.listId || ""); if (!listInfo.info.name) { console.warn(`${LOG_PREFIX$6} 新建清单后无法从 DOM 取得清单名称,使用传入名称「${listName}」`); listInfo.info.name = listName; } handleCheckboxChange(movieInfo, listInfo, true, { serverConfirmed: true }).then(); } function restoreCreateListUi() { const w = document.querySelector(".jhs-list-create-wrap"); if (!w) return; const activeElement = document.activeElement; const shouldRestoreFocus = !activeElement || activeElement === document.body || w.contains(activeElement); const btn = w.querySelector(".jhs-list-create-btn"); const f = w.querySelector(".jhs-list-create-form"); const inp = w.querySelector(".jhs-list-create-input"); if (btn) { btn.style.display = "inline-flex"; btn.setAttribute("aria-expanded", "false"); } if (f) f.style.display = "none"; if (inp) inp.value = ""; if (btn && shouldRestoreFocus) btn.focus({ preventScroll: true }); } function VltCreateListForm() { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [(0, import_jsx_runtime.jsx)("button", { type: "button", className: "button is-info is-small jhs-list-create-btn", "aria-expanded": "false", "aria-controls": "jhs-list-create-form", children: "+ 新建清单" }), (0, import_jsx_runtime.jsxs)("span", { id: "jhs-list-create-form", className: "jhs-list-create-form", style: { display: "none" }, children: [ (0, import_jsx_runtime.jsx)("label", { className: "jhs-visually-hidden", htmlFor: "jhs-list-create-input", children: "清单名称" }), (0, import_jsx_runtime.jsx)("input", { id: "jhs-list-create-input", type: "text", className: "input is-small jhs-list-create-input", placeholder: "清单名称", maxLength: 50, autoComplete: "off" }), (0, import_jsx_runtime.jsx)("button", { type: "button", className: "button is-info is-small jhs-list-create-save", children: "新建" }), (0, import_jsx_runtime.jsx)("button", { type: "button", className: "button is-light is-small jhs-list-create-cancel", children: "取消" }) ] })] }); } var _createListUiInjected = false; var _createListInFlight = false; function setupCreateListButton() { if (_createListUiInjected) return; const panel = document.querySelector(".jhs-list-panel"); if (!panel) { setTimeout(setupCreateListButton, 400); return; } if (document.querySelector(".jhs-list-create-wrap")) { _createListUiInjected = true; return; } const wrap = document.createElement("div"); wrap.className = "jhs-list-create-wrap"; wrap.innerHTML = jsxToString((0, import_jsx_runtime.jsx)(VltCreateListForm, {})); panel.insertAdjacentElement("afterend", wrap); bindCreateListEvents(wrap); bindCreateListAvailability(panel, wrap); _createListUiInjected = true; } function bindCreateListAvailability(panel, wrap) { const items = panel.querySelector(".jhs-list-panel__items"); const btn = wrap.querySelector(".jhs-list-create-btn"); if (!items || !btn) return; const sync = () => { const ready = isCreateListPanelReady(); btn.disabled = !ready; btn.title = ready ? "新建清单" : "清单载入后可新建"; }; const observer = new MutationObserver(sync); observer.observe(items, { attributes: true, attributeFilter: ["aria-busy"] }); observer.observe(panel, { attributes: true, attributeFilter: ["data-list-coverage"] }); sync(); } function isCreateListPanelReady() { const panel = document.querySelector(".jhs-list-panel"); const items = document.querySelector(".jhs-list-panel__items"); return panel?.dataset.listCoverage === "complete" && items?.getAttribute("aria-busy") === "false" && Boolean(getCurrentListContainer()?.querySelector("input[data-list-id]")); } function bindCreateListEvents(wrap) { const btn = wrap.querySelector(".jhs-list-create-btn"); const form = wrap.querySelector(".jhs-list-create-form"); const input = wrap.querySelector(".jhs-list-create-input"); const saveBtn = wrap.querySelector(".jhs-list-create-save"); const cancelBtn = wrap.querySelector(".jhs-list-create-cancel"); btn.addEventListener("click", () => { btn.style.display = "none"; btn.setAttribute("aria-expanded", "true"); form.style.display = "inline-flex"; input.value = ""; input.focus(); }); cancelBtn.addEventListener("click", () => { if (_createListInFlight) return; restoreCreateListUi(); }); saveBtn.addEventListener("click", async () => { if (_createListInFlight) return; if (!isCreateListPanelReady()) { showToast$1("清单仍在载入,请稍后再新建", "warning"); return; } const name = input.value.trim(); if (!name) { showToast$1("请输入清单名称", "warning"); input.focus(); return; } _createListInFlight = true; setCreateListBusy(wrap, true); let result = "failed"; try { result = await createList(name); } catch (error) { console.error(`${LOG_PREFIX$6} 新增清单发生未处理异常`, error); showToast$1("✗ 新增清单失败,请稍后重试", "error"); } finally { _createListInFlight = false; setCreateListBusy(wrap, false); } if (result !== "created") input.focus(); }); input.addEventListener("keydown", (e) => { if (e.isComposing) return; if (e.key === "Enter") { e.preventDefault(); saveBtn.click(); } else if (e.key === "Escape") cancelBtn.click(); }); } function setCreateListBusy(wrap, busy) { const input = wrap.querySelector(".jhs-list-create-input"); const saveBtn = wrap.querySelector(".jhs-list-create-save"); const cancelBtn = wrap.querySelector(".jhs-list-create-cancel"); wrap.setAttribute("aria-busy", String(busy)); if (input) input.disabled = busy; if (saveBtn) { saveBtn.disabled = busy; saveBtn.textContent = busy ? "新建中…" : "新建"; } if (cancelBtn) cancelBtn.disabled = busy; } function setupListManagementListener() { console.log(`${LOG_PREFIX$6} /users/lists 清单管理监听已启动`); document.addEventListener("click", (e) => { const deleteLink = e.target.closest?.("a[href*=\"remove_list\"][data-method=\"delete\"]"); if (!deleteLink) return; e.preventDefault(); e.stopPropagation(); const href = deleteLink.getAttribute("href") || ""; const listId = extractListIdFromHref(href); if (!listId) { console.warn(`${LOG_PREFIX$6} 删除链接无法提取 listId: ${href}`); return; } const confirmMsg = deleteLink.dataset.confirm || "確認移除嗎?"; if (!confirm(confirmMsg)) return; handleListDeletion(listId, href).then(); }, true); const editing = { listId: "", oldName: "" }; document.addEventListener("click", (e) => { const editBtn = e.target.closest?.(".modal-edit-list-button"); if (!editBtn) return; const listId = editBtn.dataset.listId || ""; if (!listId) return; const nameEl = editBtn.closest("[id^=\"list-\"]")?.querySelector(".list-name"); editing.listId = listId; editing.oldName = nameEl?.textContent?.trim() || ""; console.log(`${LOG_PREFIX$6} 编辑清单快照: listId=${listId} oldName=${editing.oldName}`); openEditModal(listId, editing.oldName); }, true); document.addEventListener("click", (e) => { if (!e.target.closest?.("[data-action=\"list#updateList\"]")) return; e.preventDefault(); e.stopPropagation(); if (!editing.listId) { console.warn(`${LOG_PREFIX$6} 保存清单改名但无快照,跳过`); return; } const newName = (document.querySelector("#modal-edit-list")?.querySelector("[data-list-target=\"inputName\"]"))?.value?.trim() || ""; if (!newName) { showToast$1("請輸入清單名稱", "warning"); return; } if (newName === editing.oldName) { console.log(`${LOG_PREFIX$6} 清单改名前后名称相同(${newName}),跳过`); closeEditModal(); return; } handleListRename(editing.listId, newName).then(); }, true); } function openEditModal(listId, listName) { const modal = document.querySelector("#modal-edit-list"); if (!modal) return; const nameInput = modal.querySelector("[data-list-target=\"inputName\"]"); const idInput = modal.querySelector("[data-list-target=\"inputId\"]"); if (nameInput) nameInput.value = listName; if (idInput) idInput.value = listId; modal.classList.add("is-active"); document.documentElement.classList.add("is-clipped"); } function closeEditModal() { const modal = document.querySelector("#modal-edit-list"); if (!modal) return; modal.classList.remove("is-active"); document.documentElement.classList.remove("is-clipped"); } async function handleListDeletion(listId, href) { const csrfToken = getCsrfToken$1(); if (!csrfToken) { showToast$1("✗ 无法获取 CSRF token,删除失败", "error"); return; } console.log(`${LOG_PREFIX$6} ═══ 删除清单 listId=${listId} ═══`); showToast$1("正在同步删除…", "info"); const serverOk = await sendDeleteRequest(href, csrfToken); if (!serverOk) { showToast$1("✗ JavDB 删除清单失败,本地数据未改动", "error"); return; } const idbResult = await VltDb.deleteList(listId); document.querySelector(`#list-${listId}`)?.remove(); console.log(`${LOG_PREFIX$6} 删除完成: listId=${listId} server=${serverOk} inventory=${idbResult.inventory} associations=${idbResult.associations}`); broadcastListMgmt("delete", listId); showToast$1(`✓ 清单已删除(${idbResult.associations} 条关联已清除)`, "success"); console.log(`${LOG_PREFIX$6} ═══ 删除完成 ═══`); } function sendDeleteRequest(href, csrfToken) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: "DELETE", url: window.location.origin + href, headers: { "X-CSRF-Token": csrfToken, "X-Requested-With": "XMLHttpRequest", Accept: "text/javascript, application/javascript, application/xhtml+xml, */*" }, timeout: 15e3, onload: (r) => resolve(r.status >= 200 && r.status < 400), onerror: () => resolve(false), ontimeout: () => resolve(false) }); }); } async function handleListRename(listId, newName) { const csrfToken = getCsrfToken$1(); if (!csrfToken) { showToast$1("✗ 无法获取 CSRF token,改名失败", "error"); return; } showToast$1("正在改名…", "info"); console.log(`${LOG_PREFIX$6} ═══ 改名清单 listId=${listId} newName=${newName} ═══`); try { const response = await new Promise((resolve) => { GM_xmlhttpRequest({ method: "POST", url: "https://javdb.com/users/update_list", headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", "X-CSRF-Token": csrfToken, "X-Requested-With": "XMLHttpRequest", Accept: "application/json, text/javascript, */*;q=0.01" }, data: `id=${encodeURIComponent(listId)}&name=${encodeURIComponent(newName)}`, timeout: 15e3, onload: (r) => { if (r.status >= 200 && r.status < 300) try { resolve(JSON.parse(r.responseText)); } catch { resolve({ success: false, name: "", message: "响应解析失败" }); } else resolve({ success: false, name: "", message: `HTTP ${r.status}` }); }, onerror: () => resolve({ success: false, name: "", message: "网络错误" }), ontimeout: () => resolve({ success: false, name: "", message: "请求超时" }) }); }); if (!response.success) { showToast$1(`✗ 改名失败:${response.message || "未知错误"}`, "error"); return; } const finalName = response.name || newName; console.log(`${LOG_PREFIX$6} 服务端改名成功: listId=${listId} name=${finalName}`); if (await VltDb.renameList(listId, finalName)) console.log(`${LOG_PREFIX$6} IDB 清单改名完成: listId=${listId} name=${finalName}`); else console.warn(`${LOG_PREFIX$6} IDB 清单改名跳过: listId=${listId} 不在本地 inventory 中`); broadcastListMgmt("rename", listId, { newName: finalName }); const nameEl = document.querySelector(`#list-${listId} .list-name`); if (nameEl) nameEl.textContent = finalName; closeEditModal(); showToast$1(`✓ 清单已改名为「${finalName}」`, "success"); console.log(`${LOG_PREFIX$6} ═══ 改名完成 ═══`); } catch (err) { const message = err instanceof Error ? err.message : "未知错误"; console.error(`${LOG_PREFIX$6} 清单改名失败: listId=${listId}`, err); showToast$1(`✗ 清单改名失败:${message}`, "error"); } } function removeDetailPageCheckbox(listId) { const panelCb = document.querySelector(`.jhs-list-panel input[data-list-id="${listId}"]`); if (panelCb) (panelCb.closest("p.control") ?? panelCb.closest("label"))?.remove(); const modalCb = document.querySelector(`#modal-save-list input[data-list-id="${listId}"]`); if (modalCb) (modalCb.closest("p.control") ?? modalCb.closest("label"))?.remove(); refreshListPanel(); console.log(`${LOG_PREFIX$6} 详情页已移除清单 ${listId} 的 checkbox`); } function updateDetailPageCheckboxLabel(listId, newName) { document.querySelectorAll(`input[data-list-id="${listId}"]`).forEach((cb) => { const label = cb.closest("label"); if (!label) return; const displayName = label.querySelector(".jhs-list-item__name"); if (displayName) { displayName.textContent = newName; return; } Array.from(label.childNodes).forEach((node) => { if (node.nodeType === Node.TEXT_NODE) node.textContent = ""; }); const inputEl = label.querySelector("input"); if (inputEl && inputEl.nextSibling) inputEl.nextSibling.textContent = `\u00A0\n ${newName}\u00A0\n `; else if (inputEl) inputEl.insertAdjacentText("afterend", `\u00A0${newName}\u00A0`); }); refreshListPanel(); console.log(`${LOG_PREFIX$6} 详情页已更新清单 ${listId} 的标签为「${newName}」`); } function hookWantAndWatchedButtons(plugin) { if (!isJavdbSite) return; if (plugin._wantWatchedObserved) return; const self = plugin; const ensure = () => { const container = document.querySelector("body > section > div > div.video-detail > div.video-meta-panel > div > div:nth-child(2) > nav > div.review-buttons"); if (!container) { setTimeout(ensure, 200); return; } if (container.__jhsObserved) return; container.__jhsObserved = true; self._wantWatchedObserved = true; self._lastWantState = self.detectWantWatchedState(container); new MutationObserver(() => { if (self._wantWatchedSyncing) return; clearTimeout(self._wantWatchedDebounce ?? void 0); self._wantWatchedDebounce = setTimeout(() => { self._wantWatchedSyncing = true; try { const currentState = self.detectWantWatchedState(container); const lastState = self._lastWantState || { want: false, watched: false }; if (currentState.want !== lastState.want) { if (currentState.want) self.onWantAdded(); else self.onWantRemoved(); } if (currentState.watched !== lastState.watched) { if (currentState.watched) self.onWatchedAdded(); else self.onWatchedRemoved(); } self._lastWantState = currentState; } finally { self._wantWatchedSyncing = false; } }, 150); }).observe(container, { childList: true, subtree: true }); }; ensure(); } function detectWantWatchedState(container) { const wantTag = container.querySelector("a[href='/users/want_watch_videos'] .tag.is-info.is-light"); const watchedTag = container.querySelector("a[href='/users/watched_videos'] .tag.is-success.is-light"); return { want: !!wantTag, watched: !!watchedTag }; } async function onWantAdded(plugin) { const pageInfo = plugin.getPageInfo(); try { const carRecord = await storageManager.getCar(pageInfo.carNum); if (carRecord && carRecord.status === "favorite") {} else { await storageManager.saveCar({ carNum: pageInfo.carNum, url: pageInfo.url ?? void 0, names: pageInfo.actress ?? void 0, actionType: FAVORITE_ACTION, publishTime: pageInfo.publishTime ?? void 0 }); plugin.broadcastWantWatchedSync({ carNum: pageInfo.carNum, status: FAVORITE_ACTION, op: "add" }); show.ok(`${pageInfo.carNum} 已收藏`); } } catch (err) { clog.error("[JHS-想看自动同步] 写入失败", err); } plugin.showStatus(pageInfo.carNum).then(); } async function onWantRemoved(plugin) { const pageInfo = plugin.getPageInfo(); try { if (await plugin.removeCarIfStatus(pageInfo.carNum, "favorite")) { plugin.broadcastWantWatchedSync({ carNum: pageInfo.carNum, status: FAVORITE_ACTION, op: "remove" }); show.ok(`${pageInfo.carNum} 已取消收藏`); } } catch (err) { clog.error("[JHS-想看自动同步] 移除失败", err); } plugin.showStatus(pageInfo.carNum).then(); } async function onWatchedAdded(plugin) { const pageInfo = plugin.getPageInfo(); try { const carRecord = await storageManager.getCar(pageInfo.carNum); const wasFavorite = carRecord?.status === FAVORITE_ACTION; if (carRecord && carRecord.status === "hasWatch") {} else { await storageManager.saveCar({ carNum: pageInfo.carNum, url: pageInfo.url ?? void 0, names: pageInfo.actress ?? void 0, actionType: HAS_WATCH_ACTION, publishTime: pageInfo.publishTime ?? void 0 }); plugin.broadcastWantWatchedSync({ carNum: pageInfo.carNum, status: HAS_WATCH_ACTION, op: "add" }); show.ok(`${pageInfo.carNum} 已标记看过`); } if (wasFavorite) removeFromNonProtectedListsOnWatch().then(); } catch (err) { clog.error("[JHS-观看自动同步] 写入失败", err); } plugin.showStatus(pageInfo.carNum).then(); } async function onWatchedRemoved(plugin) { const pageInfo = plugin.getPageInfo(); try { if (await plugin.removeCarIfStatus(pageInfo.carNum, "hasWatch")) { plugin.broadcastWantWatchedSync({ carNum: pageInfo.carNum, status: HAS_WATCH_ACTION, op: "remove" }); show.ok(`${pageInfo.carNum} 已取消看过`); } } catch (err) { clog.error("[JHS-观看自动同步] 移除失败", err); } plugin.showStatus(pageInfo.carNum).then(); } async function removeCarIfStatus(carNum, status) { const carRecord = await storageManager.getCar(carNum); if (!carRecord) return false; if (carRecord.status !== status) return false; return await storageManager.removeCar(carNum); } function broadcastWantWatchedSync(payload) { try { broadcastSend("jdb:want-watched-sync", { ...payload, time: Date.now() }, { eventDetail: payload, onError: () => {} }); } catch (err) { clog.error("[JHS-想看/观看同步] 广播失败", err); } } function setupWantWatchedSyncListener(plugin) { if (!isJavdbSite) return; if (plugin._wantWatchedListenerInstalled) return; plugin._wantWatchedListenerInstalled = true; const self = plugin; const handleSync = (rawData) => { const payload = rawData?.detail || (() => { try { return JSON.parse(rawData); } catch { return null; } })(); if (!payload || !payload.carNum) return; storageManager.clearCarListCache(); try { const currentCarNum = self.getPageInfo().carNum; if (currentCarNum && payload.carNum === currentCarNum) self.showStatus(currentCarNum).then(() => {}); } catch {} self.refreshItemStatusTag(payload.carNum); }; broadcastSubscribe("jdb:want-watched-sync", (rawData) => handleSync(rawData), { raw: true }); } function refreshItemStatusTag(plugin, carNum) { try { const itemSelector = plugin.getSelector().itemSelector; const items = document.querySelectorAll(itemSelector); for (const item of items) { const strongEl = item.querySelector("a > div.video-title > strong"); if (!strongEl || strongEl.innerHTML !== carNum) continue; const listPagePlugin = plugin.getBean("ListPagePlugin"); if (!listPagePlugin) continue; listPagePlugin.renderItemStatusTag(item, carNum); } } catch (err) { clog.error("[JHS-想看/观看] 刷新列表项 status-tag 失败", err); } } function RatingBarHtml() { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [(0, import_jsx_runtime.jsx)("div", { className: "jhs-stars", "data-score": 0, children: [ 1, 2, 3, 4, 5 ].map((n) => (0, import_jsx_runtime.jsx)("span", { className: "jhs-star", "data-score": n, children: "★" }, n)) }), (0, import_jsx_runtime.jsxs)("div", { className: "jhs-rating-actions", children: [ (0, import_jsx_runtime.jsx)("button", { className: "jhs-fav-btn", type: "button", title: "设为想看(收藏)", children: "♥ 收藏" }), (0, import_jsx_runtime.jsx)("button", { className: "jhs-read-btn", type: "button", title: "设为已观看(0星)", children: "已读" }), (0, import_jsx_runtime.jsx)("button", { className: "jhs-block-btn", type: "button", title: "拉黑该影片(屏蔽,设为已读0星后关闭页面)", children: "🚫 拉黑" }) ] })] }); } var rating_bar_default = "/* 隐藏 review-buttons 内第二个 panel-block(下載/訂正磁力按钮) */\n.review-buttons > .panel-block ~ .panel-block {\n display: none !important;\n}\n\n/* 隐藏 review-buttons 后紧跟的 panel-block(N人想看/看過统计) */\n.review-buttons + .panel-block {\n display: none !important;\n}\n\n/* 隐藏原生评价状态标签和按钮(星星组件已替代) */\n.review-buttons .review-title {\n display: none !important;\n}\n\n.review-buttons .buttons.are-small.review-buttons {\n display: none !important;\n}\n\n/* 永久隐藏清单 modal(DOM 保留供 Stimulus ajax 操作) */\n#modal-save-list {\n display: none !important;\n}\n\n/* 星星评分组件 */\n.jhs-rating-bar {\n display: flex;\n flex-direction: column;\n gap: 6px;\n margin: 4px 0 12px;\n user-select: none;\n}\n\n.jhs-stars {\n display: inline-flex;\n gap: 6px;\n align-items: center;\n}\n\n.jhs-star {\n font-size: 26px;\n line-height: 1.2;\n padding: 2px 4px;\n cursor: pointer;\n color: #d0d0d0;\n transition:\n color 0.15s,\n transform 0.15s;\n}\n\n.jhs-star:hover {\n transform: scale(1.15);\n}\n\n.jhs-star.is-preview {\n color: #f5b301;\n}\n\n.jhs-star.is-active {\n color: #f5b301;\n}\n\n.jhs-star.is-active:hover {\n color: #ffce3a;\n}\n\n.jhs-star.is-popping {\n animation: jhs-star-pop 0.3s ease;\n}\n\n@keyframes jhs-star-pop {\n 0% {\n transform: scale(1);\n }\n\n 40% {\n transform: scale(1.4);\n }\n\n 100% {\n transform: scale(1);\n }\n}\n\n.jhs-stars.is-disabled .jhs-star {\n cursor: default;\n opacity: 0.4;\n pointer-events: none;\n}\n\n.jhs-rating-actions {\n display: flex;\n gap: 8px;\n align-items: center;\n}\n\n.jhs-read-btn,\n.jhs-fav-btn,\n.jhs-block-btn {\n border: 1px solid #dbdbdb;\n border-radius: 6px;\n padding: 5px 16px;\n font-size: 14px;\n background: #fff;\n cursor: pointer;\n transition: color 0.15s, background-color 0.15s, border-color 0.15s, box-shadow 0.15s;\n white-space: nowrap;\n}\n\n.jhs-read-btn:hover,\n.jhs-fav-btn:hover,\n.jhs-block-btn:hover {\n border-color: #b5b5b5;\n box-shadow: 0 1px 4px rgb(0, 0, 0, 0.08);\n}\n\n.jhs-read-btn.is-active {\n background: #48c774;\n border-color: #48c774;\n color: #fff;\n}\n\n.jhs-fav-btn.is-active {\n background: #3273dc;\n border-color: #3273dc;\n color: #fff;\n}\n\n.jhs-block-btn.is-active {\n background: #de3333;\n border-color: #de3333;\n color: #fff;\n}\n\n.jhs-read-btn.is-popping,\n.jhs-fav-btn.is-popping,\n.jhs-block-btn.is-popping {\n animation: jhs-star-pop 0.3s ease;\n}\n\n.jhs-rating-bar.is-busy {\n pointer-events: none;\n opacity: 0.6;\n}\n\n/* 详情页清单面板(位于 #otherSiteBox 下方) */\n.jhs-list-panel {\n --jhs-list-accent: #3273dc;\n --jhs-list-border: #dfe4ea;\n --jhs-list-muted: #5f6b7a;\n --jhs-list-surface: #fff;\n --jhs-list-subtle: #f6f8fa;\n\n display: block;\n margin: 10px 0 0;\n padding: 0;\n background: var(--jhs-list-surface);\n border: 1px solid var(--jhs-list-border);\n border-radius: 10px;\n color: #2f3540;\n}\n\n.jhs-list-panel__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n min-height: 48px;\n padding: 8px 10px;\n border-bottom: 1px solid #edf0f3;\n}\n\n.jhs-list-panel__heading {\n display: flex;\n min-width: 0;\n flex-direction: column;\n gap: 1px;\n}\n\n.jhs-list-panel__title {\n margin: 0;\n color: #2f3540;\n font-size: 14px;\n font-weight: 700;\n line-height: 1.3;\n}\n\n.jhs-list-panel__summary {\n color: var(--jhs-list-muted);\n font-size: 12px;\n line-height: 1.3;\n}\n\n.jhs-list-panel__summary.is-error {\n color: #a32626;\n font-weight: 600;\n}\n\n.jhs-list-panel__sort {\n display: inline-flex;\n flex-shrink: 0;\n align-items: center;\n gap: 5px;\n color: var(--jhs-list-muted);\n font-size: 12px;\n font-weight: 600;\n}\n\n.jhs-list-panel__sort-select {\n height: 32px;\n padding: 0 8px;\n border: 1px solid #cbd2da;\n border-radius: 6px;\n background: #fff;\n color: #3d4652;\n cursor: pointer;\n font-size: 12px;\n font-weight: 600;\n}\n\n.jhs-list-panel__sort-select:hover:not(:disabled) {\n border-color: #9aa6b2;\n}\n\n.jhs-list-panel__sort-select:disabled {\n cursor: wait;\n opacity: 0.58;\n}\n\n.jhs-list-panel__search-wrap {\n position: relative;\n display: flex;\n align-items: center;\n margin: 10px;\n}\n\n.jhs-list-panel__search-icon {\n position: absolute;\n left: 10px;\n color: #6b7683;\n font-size: 15px;\n line-height: 1;\n pointer-events: none;\n}\n\n.jhs-list-panel__search {\n width: 100%;\n height: 34px;\n padding: 6px 32px 6px 30px;\n border: 1px solid #cbd2da;\n border-radius: 7px;\n background: #fff;\n color: #2f3540;\n font-size: 13px;\n outline: none;\n transition:\n border-color 0.15s ease,\n box-shadow 0.15s ease;\n}\n\n.jhs-list-panel__search::placeholder {\n color: #667281;\n}\n\n.jhs-list-panel__search::-webkit-search-cancel-button {\n display: none;\n}\n\n.jhs-list-panel__search:focus-visible,\n.jhs-list-panel__sort-select:focus-visible {\n border-color: var(--jhs-list-accent);\n box-shadow: 0 0 0 2px rgb(50, 115, 220, 0.2);\n outline: none;\n}\n\n.jhs-list-panel__search:disabled {\n cursor: wait;\n opacity: 0.58;\n}\n\n.jhs-list-panel__search-clear {\n position: absolute;\n right: 5px;\n display: inline-flex;\n width: 26px;\n height: 26px;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 0;\n border-radius: 50%;\n background: transparent;\n color: #596574;\n cursor: pointer;\n font-size: 18px;\n line-height: 1;\n}\n\n.jhs-list-panel__search-clear[hidden] {\n display: none !important;\n}\n\n.jhs-list-panel__search-clear:hover {\n background: #e9edf2;\n color: #27313c;\n}\n\n.jhs-list-panel__search-clear:focus-visible,\n.jhs-list-panel__retry:focus-visible {\n outline: 2px solid var(--jhs-list-accent);\n outline-offset: 2px;\n}\n\n.jhs-list-panel__items {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(100%, 160px), 1fr));\n gap: 6px;\n max-height: 320px;\n padding: 0 10px 10px;\n overflow-y: auto;\n overscroll-behavior-y: auto;\n}\n\n.jhs-list-panel .jhs-list-item {\n min-width: 0;\n margin: 0;\n}\n\n.jhs-list-panel .jhs-list-item[hidden] {\n display: none !important;\n}\n\n.jhs-list-panel .jhs-list-item__label {\n display: grid;\n grid-template-columns: auto minmax(0, 1fr) auto;\n min-height: 40px;\n align-items: center;\n gap: 7px;\n padding: 6px 8px;\n border: 1px solid #e0e5eb;\n border-radius: 8px;\n background: #fff;\n color: #3d4652;\n cursor: pointer;\n font-size: 13px;\n line-height: 1.25;\n transition:\n border-color 0.15s ease,\n background-color 0.15s ease,\n color 0.15s ease;\n}\n\n.jhs-list-panel .jhs-list-item__label:hover {\n border-color: #aeb8c3;\n background: #f8fafc;\n}\n\n.jhs-list-panel .jhs-list-item__label:focus-within {\n border-color: var(--jhs-list-accent);\n box-shadow: 0 0 0 2px rgb(50, 115, 220, 0.18);\n}\n\n.jhs-list-panel .jhs-list-item:has(input:checked) .jhs-list-item__label {\n border-color: var(--jhs-list-accent);\n background: #eef4ff;\n color: #234f96;\n}\n\n.jhs-list-panel .jhs-list-item:has(input:disabled) .jhs-list-item__label {\n border-color: #e1e5ea;\n background: #f7f8fa;\n color: #65707d;\n cursor: not-allowed;\n}\n\n.jhs-list-panel\n .jhs-list-item:has(input[data-vlt-was-disabled])\n .jhs-list-item__label {\n border-color: #eadba7;\n background: #fffdf5;\n cursor: wait;\n}\n\n.jhs-list-panel .jhs-list-item__label input[type='checkbox'] {\n width: 16px;\n height: 16px;\n margin: 0;\n accent-color: var(--jhs-list-accent);\n}\n\n.jhs-list-item__name {\n display: -webkit-box;\n min-width: 0;\n max-height: 2.5em;\n overflow: hidden;\n font-weight: 600;\n overflow-wrap: anywhere;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n line-clamp: 2;\n white-space: normal;\n}\n\n.jhs-list-item__count {\n min-width: 24px;\n padding: 2px 6px;\n border-radius: 50rem;\n background: #edf0f4;\n color: #4d5967;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n font-weight: 700;\n text-align: center;\n}\n\n.jhs-list-panel .jhs-list-item:has(input:checked) .jhs-list-item__count {\n background: #d9e7ff;\n color: #234f96;\n}\n\n.jhs-list-item__status {\n display: inline-flex;\n min-width: 32px;\n align-items: center;\n justify-content: center;\n padding: 2px 5px;\n border-radius: 50rem;\n background: #edf0f4;\n color: #596574;\n font-size: 10px;\n font-weight: 700;\n white-space: nowrap;\n}\n\n.jhs-list-item__status[hidden] {\n display: none !important;\n}\n\n.jhs-list-item__status[data-state='busy'] {\n background: #fff3cd;\n color: #725500;\n}\n\n.jhs-list-item__status[data-state='full'],\n.jhs-list-item__status[data-state='unavailable'] {\n background: #f8e2e2;\n color: #8d2929;\n}\n\n.jhs-list-panel .jhs-list-item:has(.jhs-list-item__status:not([hidden])) .jhs-list-item__count {\n display: none;\n}\n\n.jhs-list-panel__empty {\n margin: 0;\n padding: 4px 12px 14px;\n color: var(--jhs-list-muted);\n font-size: 12px;\n line-height: 1.45;\n text-align: center;\n}\n\n/* ── 序号清单合并行(详情页清单面板) ─────────────────────── */\n.jhs-list-panel .jhs-list-group:has(input:indeterminate) .jhs-list-item__label {\n border-color: var(--jhs-list-accent);\n border-style: dashed;\n}\n\n.jhs-list-group__bar {\n display: flex;\n gap: 6px;\n align-items: center;\n padding: 3px 8px 0;\n}\n\n.jhs-list-group__toggle,\n.jhs-list-group__reflow {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n padding: 2px 8px;\n border: 1px solid #e0e5eb;\n border-radius: 50rem;\n background: #f6f8fa;\n color: #4d5967;\n cursor: pointer;\n font-size: 11px;\n font-weight: 600;\n line-height: 1.4;\n transition:\n border-color 0.15s ease,\n background-color 0.15s ease,\n color 0.15s ease;\n}\n\n.jhs-list-group__toggle:hover,\n.jhs-list-group__reflow:hover {\n border-color: #aeb8c3;\n background: #eef1f5;\n color: #27313c;\n}\n\n.jhs-list-group__reflow {\n margin-left: auto;\n}\n\n.jhs-list-group__detail {\n display: flex;\n flex-direction: column;\n gap: 2px;\n margin: 4px 0 2px;\n padding: 4px 6px 4px 12px;\n border-left: 2px solid #e0e5eb;\n list-style: none;\n}\n\n.jhs-list-group__detail[hidden] {\n display: none !important;\n}\n\n.jhs-list-group__member {\n display: grid;\n grid-template-columns: auto minmax(0, 1fr) auto auto auto;\n gap: 6px;\n align-items: center;\n padding: 2px 4px;\n border-radius: 5px;\n color: #4d5967;\n font-size: 12px;\n line-height: 1.3;\n}\n\n.jhs-list-group__member-num {\n min-width: 18px;\n padding: 0 4px;\n border-radius: 4px;\n background: #edf0f4;\n color: #4d5967;\n font-size: 10px;\n font-weight: 700;\n text-align: center;\n}\n\n.jhs-list-group__member-name {\n min-width: 0;\n overflow: hidden;\n overflow-wrap: anywhere;\n font-weight: 600;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.jhs-list-group__member-count {\n padding: 1px 6px;\n border-radius: 50rem;\n background: #edf0f4;\n color: #4d5967;\n font-size: 10px;\n font-variant-numeric: tabular-nums;\n font-weight: 700;\n}\n\n.jhs-list-group__member-flag {\n padding: 1px 5px;\n border-radius: 50rem;\n font-size: 10px;\n font-weight: 700;\n white-space: nowrap;\n}\n\n.jhs-list-group__member-flag.is-checked {\n background: #d9e7ff;\n color: #234f96;\n}\n\n.jhs-list-group__member-flag.is-full {\n background: #f8e2e2;\n color: #8d2929;\n}\n\n.jhs-list-panel__skeleton {\n height: 40px;\n border-radius: 8px;\n background: linear-gradient(90deg, #edf0f3 25%, #f7f8fa 50%, #edf0f3 75%);\n background-size: 200% 100%;\n animation: jhs-list-skeleton 1.1s ease-in-out infinite;\n}\n\n@keyframes jhs-list-skeleton {\n from {\n background-position: 100% 0;\n }\n\n to {\n background-position: -100% 0;\n }\n}\n\n.jhs-list-panel__error {\n grid-column: 1 / -1;\n display: flex;\n min-height: 56px;\n align-items: center;\n justify-content: center;\n gap: 8px;\n color: #a32626;\n font-size: 12px;\n text-align: center;\n}\n\n.jhs-list-panel__retry {\n padding: 5px 9px;\n border: 1px solid #c5ccd5;\n border-radius: 6px;\n background: #fff;\n color: #344252;\n cursor: pointer;\n font-size: 12px;\n font-weight: 600;\n}\n\n.jhs-list-panel__retry:hover {\n border-color: #8e9aa7;\n background: #f6f8fa;\n}\n\n@media (width <= 768px) {\n .jhs-list-panel__header {\n align-items: flex-start;\n flex-wrap: wrap;\n }\n\n .jhs-list-panel__sort {\n margin-left: auto;\n }\n\n .jhs-list-panel__sort-select,\n .jhs-list-panel__search {\n height: 44px;\n }\n\n .jhs-list-panel__search {\n padding-right: 48px;\n }\n\n .jhs-list-panel__search-clear {\n right: 0;\n width: 44px;\n height: 44px;\n }\n\n .jhs-list-panel__items {\n grid-template-columns: 1fr;\n max-height: none;\n overflow: visible;\n }\n\n .jhs-list-panel .jhs-list-item__label,\n .jhs-list-panel__skeleton {\n min-height: 44px;\n }\n\n .jhs-list-panel__retry {\n min-width: 44px;\n min-height: 44px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .jhs-list-panel *,\n .jhs-list-panel *::before,\n .jhs-list-panel *::after {\n scroll-behavior: auto !important;\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n }\n}\n\n/* 隐藏详情页菜单按钮组的状态行(屏蔽/收藏/已观看)\n 快捷评分条(星星+已读+收藏)已覆盖这三个功能,拉黑待第二步移入评分条\n DOM 保留:createMenuBtn 的事件绑定 + showStatus 文案更新仍可运行 */\n.jhs-menu-status-row {\n display: none !important;\n}\n"; function ReviewConflictDialog({ carNum, localDesc, serverDesc }) { return (0, import_jsx_runtime.jsxs)("div", { style: { padding: "8px 4px 4px", fontSize: "14px", lineHeight: "1.8" }, children: [ (0, import_jsx_runtime.jsxs)("p", { style: { margin: "0 0 10px" }, children: [ "「", carNum, "」的脚本记录与 JavDB 状态不一致,请选择以哪一侧为准:" ] }), (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", gap: "12px", marginBottom: "8px" }, children: [(0, import_jsx_runtime.jsx)("span", { style: { color: "#6c757d" }, children: "脚本记录:" }), (0, import_jsx_runtime.jsx)("strong", { children: localDesc })] }), (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", gap: "12px", marginBottom: "14px" }, children: [(0, import_jsx_runtime.jsx)("span", { style: { color: "#6c757d" }, children: "网站数据:" }), (0, import_jsx_runtime.jsx)("strong", { children: serverDesc })] }), (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "10px", justifyContent: "flex-end" }, children: [ (0, import_jsx_runtime.jsx)("button", { type: "button", "data-conflict-choice": "local", style: { padding: "6px 14px", border: "1px solid #2e8ded", borderRadius: "3px", background: "#2e8ded", color: "#fff", cursor: "pointer" }, children: "使用脚本记录" }), (0, import_jsx_runtime.jsx)("button", { type: "button", "data-conflict-choice": "server", style: { padding: "6px 14px", border: "1px solid #48c774", borderRadius: "3px", background: "#48c774", color: "#fff", cursor: "pointer" }, children: "使用网站数据" }), (0, import_jsx_runtime.jsx)("button", { type: "button", "data-conflict-choice": "cancel", style: { padding: "6px 14px", border: "1px solid #dedede", borderRadius: "3px", background: "#f1f1f1", color: "#333", cursor: "pointer" }, children: "取消" }) ] }), (0, import_jsx_runtime.jsx)("p", { style: { margin: "12px 0 0", fontSize: "12px", color: "#999" }, children: "选择后立即同步;冲突未处理前不会自动覆盖任一侧数据。" }) ] }); } var REVIEW_SYNC_TIMEOUT_MS = 1e4; var REVIEW_SYNC_START_DELAY_MS = 1200; function readServerReviewState() { const rb = document.querySelector(".review-buttons"); if (!rb) return { want: false, watched: false, score: 0 }; const want = !!rb.querySelector("a[href='/users/want_watch_videos'] .tag.is-info.is-light"); const watched = !!rb.querySelector("a[href='/users/watched_videos'] .tag.is-success.is-light"); const checked = rb.querySelector("input[name=\"video_review[score]\"][checked]"); const score = checked ? Number(checked.value) : 0; return { want, watched, score: Number.isFinite(score) ? score : 0 }; } function setupAutoReviewSync(plugin, enabled) { if (!enabled || plugin._autoSyncChecked) return; const startedAt = Date.now(); const ensure = () => { if (!document.querySelector(".review-buttons")) { if (Date.now() - startedAt > REVIEW_SYNC_TIMEOUT_MS) { plugin._autoSyncChecked = true; clog.warn("[JHS] 评价状态自动校验:等待 .review-buttons 超时,本次跳过"); return; } setTimeout(ensure, 400); return; } plugin._autoSyncChecked = true; autoSyncReviewState(plugin).then(); }; setTimeout(ensure, REVIEW_SYNC_START_DELAY_MS); } async function autoSyncReviewState(plugin) { const pageInfo = plugin.getPageInfo(); const carNum = pageInfo.carNum; if (!carNum) return; const server = readServerReviewState(); const carRecord = await storageManager.getCar(carNum); const localStatus = carRecord?.status; const localScore = typeof carRecord?.score === "number" ? carRecord.score : 0; if (server.want && localStatus === "favorite") return; if (server.watched && localStatus === "hasWatch" && server.score === localScore) return; if (!server.want && !server.watched) { if (localStatus === "favorite") await enqueuePush(plugin, carNum, "want"); else if (localStatus === "hasWatch") await enqueuePush(plugin, carNum, "review", localScore); return; } if (!localStatus) { if (server.watched) { await storageManager.saveCar({ carNum, url: pageInfo.url ?? void 0, names: pageInfo.actress ?? void 0, actionType: HAS_WATCH_ACTION, publishTime: pageInfo.publishTime ?? void 0, score: server.score || 0 }); plugin.broadcastWantWatchedSync({ carNum, status: HAS_WATCH_ACTION, op: "add", score: server.score || 0 }); clog.log(`[JHS] 评价状态自动校验:已从服务器补齐本地已看记录 ${carNum}(${server.score} 星)`); plugin.showStatus(carNum).then(); } else if (server.want) { await storageManager.saveCar({ carNum, url: pageInfo.url ?? void 0, names: pageInfo.actress ?? void 0, actionType: FAVORITE_ACTION, publishTime: pageInfo.publishTime ?? void 0 }); plugin.broadcastWantWatchedSync({ carNum, status: FAVORITE_ACTION, op: "add" }); clog.log(`[JHS] 评价状态自动校验:已从服务器补齐本地收藏记录 ${carNum}`); plugin.showStatus(carNum).then(); } return; } const choice = await confirmReviewConflict(carNum, localStatus === "favorite" ? "⭐ 已收藏(想看)" : localStatus === "hasWatch" ? `🔍 已看${localScore > 0 ? ` ${localScore} 星` : ""}` : localStatus === "filter" ? "🚫 已拉黑" : localStatus || "无记录", server.want ? "⭐ 想看" : server.watched ? `🔍 已看${server.score > 0 ? ` ${server.score} 星` : ""}` : "无评价"); if (choice === "local") { if (localStatus === "favorite") await enqueuePush(plugin, carNum, "want"); else if (localStatus === "hasWatch") await enqueuePush(plugin, carNum, "review", localScore); else if (localStatus === "filter") await enqueuePush(plugin, carNum, "review", 0); } else if (choice === "server") { if (server.watched) { await storageManager.saveCar({ carNum, url: pageInfo.url ?? void 0, names: pageInfo.actress ?? void 0, actionType: HAS_WATCH_ACTION, publishTime: pageInfo.publishTime ?? void 0, score: server.score || 0 }); plugin.broadcastWantWatchedSync({ carNum, status: HAS_WATCH_ACTION, op: "add", score: server.score || 0 }); clog.log(`[JHS] 评价状态冲突:已按网站数据更新本地已看记录 ${carNum}(${server.score} 星)`); plugin.showStatus(carNum).then(); } else if (server.want) { await storageManager.saveCar({ carNum, url: pageInfo.url ?? void 0, names: pageInfo.actress ?? void 0, actionType: FAVORITE_ACTION, publishTime: pageInfo.publishTime ?? void 0 }); plugin.broadcastWantWatchedSync({ carNum, status: FAVORITE_ACTION, op: "add" }); clog.log(`[JHS] 评价状态冲突:已按网站数据更新本地收藏记录 ${carNum}`); plugin.showStatus(carNum).then(); } } } function confirmReviewConflict(carNum, localDesc, serverDesc) { return new Promise((resolve) => { let settled = false; const settle = (choice) => { if (settled) return; settled = true; resolve(choice); }; layer.open({ type: 1, title: `评价状态冲突 - ${carNum}`, area: ["520px", "auto"], content: jsxToString((0, import_jsx_runtime.jsx)(ReviewConflictDialog, { carNum, localDesc, serverDesc })), success: (layerEl, layerIndex) => { const $layer = $(layerEl); $layer.find("[data-conflict-choice=\"local\"]").on("click", function() { layer.close(layerIndex); settle("local"); }); $layer.find("[data-conflict-choice=\"server\"]").on("click", function() { layer.close(layerIndex); settle("server"); }); $layer.find("[data-conflict-choice=\"cancel\"]").on("click", function() { layer.close(layerIndex); settle(null); }); }, cancel: () => { settle(null); } }); }); } function enqueuePush(plugin, carNum, kind, score = 0) { return new Promise((resolve) => { plugin._reviewChain = (plugin._reviewChain || Promise.resolve()).then(async () => { plugin._wantWatchedSyncing = true; try { if (kind === "want") { await plugin._triggerJavdbWant(); show.ok(`${carNum} 已同步收藏到 JavDB`); } else { await plugin._triggerJavdbReview(score); show.ok(`${carNum} 已同步${score > 0 ? `评分 ${score} 星` : "已读"}到 JavDB`); } plugin._syncRatingBar(); } catch (err) { clog.error(`[JHS] 评价状态自动校验:推送失败 ${carNum}`, err); show.error(`同步失败: ${carNum} ` + (err instanceof Error ? err.message : String(err))); } finally { plugin._wantWatchedSyncing = false; } }).catch(() => {}).finally(() => resolve()); }); } function QuickBlockConfirmMessage({ carNum }) { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ "是否拉黑 ", carNum, "?", (0, import_jsx_runtime.jsx)("br", {}), (0, import_jsx_runtime.jsx)("span", { style: { color: "#f40" }, children: "拉黑后该影片将被屏蔽,列表页自动隐藏,且设为已读0星。此操作不可在详情页撤销。" }) ] }); } async function quickSetHasWatch(plugin, score) { const pageInfo = plugin.getPageInfo(); if (!pageInfo.carNum) return; try { const carRecord = await storageManager.getCar(pageInfo.carNum); const wasFavorite = carRecord?.status === FAVORITE_ACTION; const serverState = readServerReviewState(); if (serverState.watched && serverState.score === score) { show.ok(pageInfo.carNum + " 评分未变化"); return; } if (carRecord && carRecord.status === "favorite") await storageManager.removeCar(pageInfo.carNum); await storageManager.saveCar({ carNum: pageInfo.carNum, url: pageInfo.url ?? void 0, names: pageInfo.actress ?? void 0, actionType: HAS_WATCH_ACTION, publishTime: pageInfo.publishTime ?? void 0, score }); plugin.broadcastWantWatchedSync({ carNum: pageInfo.carNum, status: HAS_WATCH_ACTION, op: "add", score }); show.ok(pageInfo.carNum + " 已标记看过 " + (score > 0 ? "★" + score : "")); if (wasFavorite) removeFromNonProtectedListsOnWatch().then(); } catch (err) { clog.error("[JHS-快键] 设为已观看失败", err); show.error("操作失败: " + (err instanceof Error ? err.message : String(err))); return; } plugin._reviewChain = (plugin._reviewChain || Promise.resolve()).then(async () => { plugin._wantWatchedSyncing = true; try { await plugin._triggerJavdbReview(score); plugin._syncRatingBar(); } finally { plugin._wantWatchedSyncing = false; } }).catch(() => {}); } async function quickBlock(plugin) { const pageInfo = plugin.getPageInfo(); if (!pageInfo.carNum) return; utils.q(null, jsxToString((0, import_jsx_runtime.jsx)(QuickBlockConfirmMessage, { carNum: pageInfo.carNum })), async () => { plugin._setRatingBusy(true); try { if (await storageManager.getCar(pageInfo.carNum)) await storageManager.removeCar(pageInfo.carNum); await storageManager.saveCar({ carNum: pageInfo.carNum, url: pageInfo.url ?? void 0, names: pageInfo.actress ?? void 0, actionType: FILTER_ACTION, publishTime: pageInfo.publishTime ?? void 0 }); plugin.broadcastWantWatchedSync({ carNum: pageInfo.carNum, status: FILTER_ACTION, op: "add" }); show.ok(`${pageInfo.carNum} 已拉黑`); } catch (err) { clog.error("[JHS-快键] 拉黑失败", err); show.error("操作失败: " + (err instanceof Error ? err.message : String(err))); return; } plugin._reviewChain = (plugin._reviewChain || Promise.resolve()).then(async () => { plugin._wantWatchedSyncing = true; try { await plugin._triggerJavdbReview(0); plugin._syncRatingBar(); } finally { plugin._wantWatchedSyncing = false; } }).catch(() => {}); plugin.showStatus(pageInfo.carNum).then(); refresh(); utils.closePage(); }); } async function quickConvertToFav(plugin) { const pageInfo = plugin.getPageInfo(); if (!pageInfo.carNum) return; try { if (readServerReviewState().want) { show.ok(pageInfo.carNum + " 已是已收藏"); return; } if (await storageManager.getCar(pageInfo.carNum)) await storageManager.removeCar(pageInfo.carNum); await storageManager.saveCar({ carNum: pageInfo.carNum, url: pageInfo.url ?? void 0, names: pageInfo.actress ?? void 0, actionType: FAVORITE_ACTION, publishTime: pageInfo.publishTime ?? void 0 }); plugin.broadcastWantWatchedSync({ carNum: pageInfo.carNum, status: FAVORITE_ACTION, op: "add" }); show.ok(pageInfo.carNum + " 已转为收藏"); } catch (err) { clog.error("[JHS-快键] 转为已收藏失败", err); show.error("操作失败: " + (err instanceof Error ? err.message : String(err))); return; } plugin._reviewChain = (plugin._reviewChain || Promise.resolve()).then(async () => { plugin._wantWatchedSyncing = true; try { await plugin._triggerJavdbWant(); plugin._syncRatingBar(); } finally { plugin._wantWatchedSyncing = false; } }).catch(() => {}); } function getCsrfToken() { const meta = document.querySelector("meta[name=\"csrf-token\"]"); return meta ? meta.content : null; } function getVideoId() { return location.pathname.match(/\/v\/([^/]+)/)?.[1] || null; } function getReviewId() { const del = document.querySelector(".review-buttons a[data-method='delete'][href*='/reviews/']"); if (!del) return null; return del.getAttribute("href")?.match(/\/reviews\/(\d+)/)?.[1] || null; } function execRailsJs(jsText) { try { const script = document.createElement("script"); script.textContent = jsText; document.head?.appendChild(script); script.remove(); } catch (err) { clog.error("[JHS-快键] 执行 Rails JS 失败", err); } } async function javdbReviewApi(plugin, action, score = 0) { const token = getCsrfToken(); if (!token) throw new Error("无法获取 CSRF token"); const videoId = getVideoId(); if (!videoId) throw new Error("无法获取 videoId"); const reviewId = getReviewId(); const headers = { "Content-Type": "application/x-www-form-urlencoded", "X-CSRF-Token": token }; const tokenParam = `authenticity_token=${encodeURIComponent(token)}`; if (action === "watched") { const url = reviewId ? `/v/${videoId}/reviews/${reviewId}` : `/v/${videoId}/reviews`; const body = `${tokenParam}${reviewId ? "&_method=patch" : ""}&video_review[status]=watched&video_review[score]=${score}&video_review[content]=`; const res = await fetch(url, { method: "POST", headers, body, credentials: "same-origin" }); if (!res.ok) throw new Error(`设为已观看失败: HTTP ${res.status}`); execRailsJs(await res.text()); } else if (action === "wanted") { if (reviewId) { const delRes = await fetch(`/v/${videoId}/reviews/${reviewId}`, { method: "POST", headers, body: `${tokenParam}&_method=delete`, credentials: "same-origin" }); if (!delRes.ok) throw new Error(`删除旧评价失败: HTTP ${delRes.status}`); execRailsJs(await delRes.text()); } const res = await fetch(`/v/${videoId}/reviews/want_to_watch`, { method: "POST", headers, body: tokenParam, credentials: "same-origin" }); if (!res.ok) throw new Error(`设为想看失败: HTTP ${res.status}`); execRailsJs(await res.text()); } const rb = document.querySelector(".review-buttons"); if (rb && plugin._wantWatchedObserved) plugin._lastWantState = plugin.detectWantWatchedState(rb); } async function triggerJavdbReview(plugin, score) { await javdbReviewApi(plugin, "watched", score); } async function triggerJavdbWant(plugin) { await javdbReviewApi(plugin, "wanted"); } function addQuickActionButtons(plugin) { if (!window.isDetailPage) return; if (plugin._quickActionAdded) return; plugin._quickActionAdded = true; const self = plugin; plugin._injectRatingStyles(); const ensure = () => { const nav = document.querySelector("body > section > div > div.video-detail > div.video-meta-panel > div > div:nth-child(2) > nav"); if (!nav) { setTimeout(ensure, 400); return; } self._buildRatingBar(nav); self._syncRatingBar(); self._ensureListPanel(nav); const rb = nav.querySelector(".review-buttons"); if (rb && !rb.__jhsRatingObserved) { rb.__jhsRatingObserved = true; new MutationObserver(() => { if (self._wantWatchedSyncing) return; clearTimeout(self._ratingSyncDebounce ?? void 0); self._ratingSyncDebounce = setTimeout(() => { self._buildRatingBar(nav); self._syncRatingBar(); }, 200); }).observe(rb, { childList: true, subtree: true }); } }; ensure(); } function buildRatingBar(plugin, nav) { const column = nav.querySelector("div.review-buttons > div:nth-child(1) > div > div"); if (!column) return; if (column.querySelector(".jhs-rating-bar")) return; const self = plugin; const bar = document.createElement("div"); bar.className = "jhs-rating-bar"; bar.innerHTML = jsxToString((0, import_jsx_runtime.jsx)(RatingBarHtml, {})); const starsEl = bar.querySelector(".jhs-stars"); const stars = bar.querySelectorAll(".jhs-star"); const readBtn = bar.querySelector(".jhs-read-btn"); const favBtn = bar.querySelector(".jhs-fav-btn"); const blockBtn = bar.querySelector(".jhs-block-btn"); if (!starsEl || !readBtn || !favBtn || !blockBtn) return; starsEl.addEventListener("pointerover", (e) => { const star = e.target.closest(".jhs-star"); if (!star) return; const score = +star.dataset.score; stars.forEach((s, i) => s.classList.toggle("is-preview", i < score)); }); starsEl.addEventListener("pointerleave", () => stars.forEach((s) => s.classList.remove("is-preview"))); stars.forEach((star) => { star.addEventListener("click", async (e) => { e.preventDefault(); const score = +star.dataset.score; star.classList.add("is-popping"); setTimeout(() => star.classList.remove("is-popping"), 300); self._setRatingBusy(true); try { await self.quickSetHasWatch(score); } finally { self._setRatingBusy(false); self.showStatus(self.getPageInfo().carNum).then(); } }); }); readBtn.addEventListener("click", async (e) => { e.preventDefault(); readBtn.classList.add("is-popping"); setTimeout(() => readBtn.classList.remove("is-popping"), 300); self._setRatingBusy(true); try { await self.quickSetHasWatch(0); } finally { self._setRatingBusy(false); self.showStatus(self.getPageInfo().carNum).then(); } }); favBtn.addEventListener("click", async (e) => { e.preventDefault(); favBtn.classList.add("is-popping"); setTimeout(() => favBtn.classList.remove("is-popping"), 300); self._setRatingBusy(true); try { await self.quickConvertToFav(); } finally { self._setRatingBusy(false); self.showStatus(self.getPageInfo().carNum).then(); } }); blockBtn.addEventListener("click", async (e) => { e.preventDefault(); blockBtn.classList.add("is-popping"); setTimeout(() => blockBtn.classList.remove("is-popping"), 300); await self.quickBlock(); }); column.insertBefore(bar, column.firstChild); } function injectRatingStyles() { if (document.getElementById("jhs-rating-styles")) return; const style = document.createElement("style"); style.id = "jhs-rating-styles"; style.textContent = rating_bar_default; document.head?.appendChild(style); } function syncRatingBar(plugin) { let bar = document.querySelector(".jhs-rating-bar"); if (!bar) { const nav = document.querySelector("body > section > div > div.video-detail > div.video-meta-panel > div > div:nth-child(2) > nav"); if (nav) plugin._buildRatingBar(nav); bar = document.querySelector(".jhs-rating-bar"); } if (!bar) return; const rb = document.querySelector(".review-buttons"); if (!rb) return; const want = !!rb.querySelector("a[href='/users/want_watch_videos'] .tag.is-info.is-light"); const watched = !!rb.querySelector("a[href='/users/watched_videos'] .tag.is-success.is-light"); const checked = rb.querySelector("input[name=\"video_review[score]\"][checked]"); const score = checked ? +checked.value : 0; const stars = bar.querySelectorAll(".jhs-star"); const starsEl = bar.querySelector(".jhs-stars"); const readBtn = bar.querySelector(".jhs-read-btn"); const favBtn = bar.querySelector(".jhs-fav-btn"); const blockBtn = bar.querySelector(".jhs-block-btn"); if (!starsEl || !readBtn || !favBtn || !blockBtn) return; stars.forEach((s) => s.classList.remove("is-active")); starsEl.classList.remove("is-disabled"); readBtn.classList.remove("is-active"); favBtn.classList.remove("is-active"); blockBtn.classList.remove("is-active"); if (want) favBtn.classList.add("is-active"); else if (watched) { stars.forEach((s, i) => s.classList.toggle("is-active", i < score)); readBtn.classList.toggle("is-active", score === 0); } const carNum = plugin.getPageInfo().carNum; if (carNum) storageManager.getCar(carNum).then((carRecord) => { if (carRecord && carRecord.status === "filter") blockBtn.classList.add("is-active"); }); } function setRatingBusy(busy) { const bar = document.querySelector(".jhs-rating-bar"); if (bar) bar.classList.toggle("is-busy", busy); } var NATIVE_LIST_CONTAINER = "#modal-save-list [data-list-target=\"listContainer\"]"; var SERIAL_CHANGE_GAP_MS = 220; async function loadListMergeConfig() { return buildMergeConfig(await storageManager.getSetting()); } function cssEscape(value) { return typeof CSS !== "undefined" && CSS.escape ? CSS.escape(value) : value.replace(/"/g, "\\\""); } function findNativeInput(listId) { const container = document.querySelector(NATIVE_LIST_CONTAINER); if (!container) return null; return container.querySelector(`input[type="checkbox"][data-list-id="${cssEscape(listId)}"]`); } function readNativeCount(input) { const matched = (input.closest("label")?.querySelector("span"))?.textContent?.match(/\d+/); return matched ? Number(matched[0]) : null; } function readItemMeta(element) { const input = element.querySelector("input[type=\"checkbox\"]"); const listId = element.dataset.listId || input?.dataset.listId || ""; if (!listId) return null; const matched = (element.querySelector(".jhs-list-item__count")?.textContent || "").match(/\d+/); return { name: element.dataset.listName || "", listId, count: matched ? Number(matched[0]) : null, checked: Boolean(input?.checked), disabled: Boolean(input?.disabled) }; } function parseMemberRefs(groupElement) { try { const parsed = JSON.parse(groupElement.dataset.listMembers || "[]"); return Array.isArray(parsed) ? parsed : []; } catch { return []; } } function liveMembers(groupElement) { const members = []; for (const ref of parseMemberRefs(groupElement)) { const input = findNativeInput(ref.id); if (!input) continue; members.push({ listId: ref.id, name: ref.name, num: ref.num, count: readNativeCount(input), checked: input.checked, disabled: input.disabled }); } return members; } function setNativeCheckbox(listId, checked) { const input = findNativeInput(listId); if (!input) return false; if (checked && input.disabled) return false; input.checked = checked; input.dispatchEvent(new Event("change", { bubbles: true })); return true; } function renderMemberDetail(groupElement, members) { const detail = groupElement.querySelector(".jhs-list-group__detail"); if (!detail) return; detail.replaceChildren(); for (const member of sortMembersByNum(members)) { const li = document.createElement("li"); li.className = "jhs-list-group__member"; li.dataset.listId = member.listId; const num = document.createElement("span"); num.className = "jhs-list-group__member-num"; num.textContent = member.num === 0 ? "★" : String(member.num); num.title = member.num === 0 ? "裸名清单(最优先)" : `序号 ${member.num}`; const name = document.createElement("span"); name.className = "jhs-list-group__member-name"; name.textContent = member.name; name.title = member.name; const count = document.createElement("span"); count.className = "jhs-list-group__member-count"; count.textContent = member.count === null ? "—" : String(member.count); li.append(num, name, count); if (member.checked) { const flag = document.createElement("span"); flag.className = "jhs-list-group__member-flag is-checked"; flag.textContent = "已含"; li.append(flag); } if (isMemberFull(member)) { const flag = document.createElement("span"); flag.className = "jhs-list-group__member-flag is-full"; flag.textContent = "已满"; li.append(flag); } detail.append(li); } } function refreshMergedRowDisplay(groupElement) { const members = liveMembers(groupElement); const checkbox = groupElement.querySelector(".jhs-list-item__label input[type=\"checkbox\"]"); const countElement = groupElement.querySelector(".jhs-list-item__count"); const statusElement = groupElement.querySelector(".jhs-list-item__status"); const toggle = groupElement.querySelector(".jhs-list-group__toggle"); const detail = groupElement.querySelector(".jhs-list-group__detail"); if (!checkbox || !countElement || !statusElement) return; const state = computeGroupCheckState({ base: groupElement.dataset.listGroup || "", members }); checkbox.checked = state.checked; checkbox.indeterminate = state.indeterminate; const allKnown = members.length > 0 && members.every((m) => m.count !== null); const total = members.reduce((sum, m) => sum + (m.count ?? 0), 0); countElement.textContent = allKnown ? String(total) : "—"; countElement.title = allKnown ? `组内共 ${total} 部影片` : "部分清单数量未知"; if (members.length > 0 && members.every((m) => isMemberFull(m)) && !state.checked) { statusElement.hidden = false; statusElement.dataset.state = "full"; statusElement.textContent = "全满"; } else { statusElement.hidden = true; delete statusElement.dataset.state; statusElement.textContent = ""; } if (toggle) toggle.textContent = `${Boolean(detail && !detail.hidden) ? "▾" : "▸"} ${members.length} 个清单`; renderMemberDetail(groupElement, members); } function buildMergedRow(group) { const element = document.createElement("div"); element.className = "jhs-list-item jhs-list-group"; element.dataset.listName = group.base; element.dataset.listGroup = group.base; const refs = sortMembersByNum(group.members).map((m) => ({ id: m.listId, num: m.num, name: m.name })); element.dataset.listMembers = JSON.stringify(refs); const label = document.createElement("label"); label.className = "jhs-list-item__label"; const input = document.createElement("input"); input.type = "checkbox"; input.dataset.listGroup = group.base; const name = document.createElement("span"); name.className = "jhs-list-item__name"; name.textContent = group.base; name.title = group.base; const count = document.createElement("span"); count.className = "jhs-list-item__count"; const status = document.createElement("span"); status.className = "jhs-list-item__status"; status.setAttribute("aria-live", "polite"); status.setAttribute("aria-atomic", "true"); status.hidden = true; label.append(input, name, count, status); const bar = document.createElement("div"); bar.className = "jhs-list-group__bar"; const toggle = document.createElement("button"); toggle.type = "button"; toggle.className = "jhs-list-group__toggle"; toggle.setAttribute("aria-expanded", "false"); const reflow = document.createElement("button"); reflow.type = "button"; reflow.className = "jhs-list-group__reflow"; reflow.textContent = "整理"; reflow.title = "将当前影片收敛到组内最靠前的清单"; bar.append(toggle, reflow); const detail = document.createElement("ul"); detail.className = "jhs-list-group__detail"; detail.hidden = true; element.append(label, bar, detail); refreshMergedRowDisplay(element); return element; } function buildListPanelRows(plugin, entries) { const config = plugin._listMergeConfig; const { groups, singleIndexes } = groupEntries(entries.map((entry) => readItemMeta(entry.element) ?? { name: "", listId: "", count: null, checked: false, disabled: false }), config); const rows = groups.map((group) => buildMergedRow(group)); for (const index of singleIndexes) rows.push(entries[index].element); return rows; } function setGroupBusy(groupElement, busy) { const checkbox = groupElement.querySelector(".jhs-list-item__label input[type=\"checkbox\"]"); const status = groupElement.querySelector(".jhs-list-item__status"); if (checkbox) checkbox.disabled = busy; if (busy && status) { status.hidden = false; status.dataset.state = "busy"; status.textContent = "处理中"; } } async function addToGroup(plugin, panel, groupElement) { const base = groupElement.dataset.listGroup || ""; const members = liveMembers(groupElement); if (computeGroupCheckState({ base, members }).containing.length > 0) { refreshMergedRowDisplay(groupElement); plugin._showListPanelNotice(panel, "该影片已在此清单组中"); return; } const target = pickTargetMember(members); if (target) { if (!setNativeCheckbox(target.listId, true)) plugin._showListPanelNotice(panel, `无法加入「${target.name}」,请稍后重试`); refreshMergedRowDisplay(groupElement); return; } groupElement.dataset.jhsBusy = "1"; setGroupBusy(groupElement, true); const newName = nextListName({ base, members }); let result; try { result = await createList(newName); } catch { result = "failed"; } delete groupElement.dataset.jhsBusy; setGroupBusy(groupElement, false); if (result === "created") plugin._scheduleListPanelSync(0); else { refreshMergedRowDisplay(groupElement); plugin._showListPanelNotice(panel, result === "unknown" ? `清单「${newName}」状态未知,请刷新确认` : "自动新建清单失败,请手动处理"); } } async function removeFromGroup(plugin, panel, groupElement) { const containing = sortMembersByNum(liveMembers(groupElement).filter((m) => m.checked)); if (containing.length === 0) { refreshMergedRowDisplay(groupElement); return; } groupElement.dataset.jhsBusy = "1"; setGroupBusy(groupElement, true); const failed = []; for (const member of containing) { if (!setNativeCheckbox(member.listId, false)) failed.push(member.name); await sleep$1(SERIAL_CHANGE_GAP_MS); } delete groupElement.dataset.jhsBusy; plugin._scheduleListPanelSync(0); if (failed.length > 0) plugin._showListPanelNotice(panel, `以下清单移除失败:${failed.join("、")}`); } function forwardListGroupChange(plugin, panel, displayInput) { const groupElement = displayInput.closest(".jhs-list-group"); if (!groupElement) return; if (groupElement.dataset.jhsBusy === "1") { refreshMergedRowDisplay(groupElement); return; } if (displayInput.checked) addToGroup(plugin, panel, groupElement); else removeFromGroup(plugin, panel, groupElement); } async function reflowGroup(plugin, panel, groupElement) { if (groupElement.dataset.jhsBusy === "1") return; const containing = sortMembersByNum(liveMembers(groupElement).filter((m) => m.checked)); if (containing.length <= 1) { plugin._showListPanelNotice(panel, "当前影片在该清单组内无需整理"); return; } const keep = containing.find((m) => !isMemberFull(m)) ?? containing[0]; groupElement.dataset.jhsBusy = "1"; setGroupBusy(groupElement, true); for (const member of containing) { if (member.listId === keep.listId) continue; setNativeCheckbox(member.listId, false); await sleep$1(SERIAL_CHANGE_GAP_MS); } delete groupElement.dataset.jhsBusy; plugin._scheduleListPanelSync(0); plugin._showListPanelNotice(panel, `已整理:当前影片仅保留在「${keep.name}」`); } function toggleGroupDetail(toggleButton) { const groupElement = toggleButton.closest(".jhs-list-group"); const detail = groupElement?.querySelector(".jhs-list-group__detail"); if (!groupElement || !detail) return; detail.hidden = !detail.hidden; toggleButton.setAttribute("aria-expanded", String(!detail.hidden)); refreshMergedRowDisplay(groupElement); } function syncMergedRowsForListId(listId) { document.querySelectorAll(".jhs-list-group").forEach((groupElement) => { if (parseMemberRefs(groupElement).some((ref) => ref.id === listId)) refreshMergedRowDisplay(groupElement); }); } function ListPanel() { return (0, import_jsx_runtime.jsxs)("section", { className: "jhs-list-panel", "aria-labelledby": "jhs-list-panel-title", children: [ (0, import_jsx_runtime.jsxs)("header", { className: "jhs-list-panel__header", children: [(0, import_jsx_runtime.jsxs)("div", { className: "jhs-list-panel__heading", children: [(0, import_jsx_runtime.jsx)("h3", { id: "jhs-list-panel-title", className: "jhs-list-panel__title", children: "加入清单" }), (0, import_jsx_runtime.jsx)("span", { className: "jhs-list-panel__summary", "aria-live": "polite", children: "正在读取…" })] }), (0, import_jsx_runtime.jsxs)("label", { className: "jhs-list-panel__sort", children: [(0, import_jsx_runtime.jsx)("span", { children: "排序" }), (0, import_jsx_runtime.jsxs)("select", { className: "jhs-list-panel__sort-select", "aria-label": "清单排序", disabled: true, children: [ (0, import_jsx_runtime.jsx)("option", { value: "name-desc", children: "名称降序" }), (0, import_jsx_runtime.jsx)("option", { value: "name-asc", children: "名称升序" }), (0, import_jsx_runtime.jsx)("option", { value: "count-desc", children: "数量降序" }), (0, import_jsx_runtime.jsx)("option", { value: "count-asc", children: "数量升序" }), (0, import_jsx_runtime.jsx)("option", { value: "num-desc", children: "序号降序" }), (0, import_jsx_runtime.jsx)("option", { value: "num-asc", children: "序号升序" }) ] })] })] }), (0, import_jsx_runtime.jsxs)("div", { className: "jhs-list-panel__search-wrap", children: [ (0, import_jsx_runtime.jsx)("span", { className: "jhs-list-panel__search-icon", "aria-hidden": "true", children: "⌕" }), (0, import_jsx_runtime.jsx)("input", { className: "jhs-list-panel__search", type: "search", placeholder: "搜索清单", "aria-label": "搜索清单", autoComplete: "off", disabled: true }), (0, import_jsx_runtime.jsx)("button", { className: "jhs-list-panel__search-clear", type: "button", "aria-label": "清空清单搜索", title: "清空搜索", hidden: true, children: "×" }) ] }), (0, import_jsx_runtime.jsxs)("div", { className: "jhs-list-panel__items", role: "group", "aria-label": "清单列表", "aria-busy": "true", children: [ (0, import_jsx_runtime.jsx)("span", { className: "jhs-list-panel__skeleton", "aria-hidden": "true" }), (0, import_jsx_runtime.jsx)("span", { className: "jhs-list-panel__skeleton", "aria-hidden": "true" }), (0, import_jsx_runtime.jsx)("span", { className: "jhs-list-panel__skeleton", "aria-hidden": "true" }), (0, import_jsx_runtime.jsx)("span", { className: "jhs-list-panel__skeleton", "aria-hidden": "true" }) ] }), (0, import_jsx_runtime.jsx)("p", { className: "jhs-list-panel__empty", role: "status", hidden: true }) ] }); } function ListPanelSkeletonSpan() { return (0, import_jsx_runtime.jsx)("span", { className: "jhs-list-panel__skeleton", "aria-hidden": "true" }); } function createListPanelEntry(plugin, source) { const element = source.cloneNode(true); const label = element.matches("label") ? element : element.querySelector("label"); const input = element.querySelector("input[type=\"checkbox\"]"); const originalCount = label?.querySelector("span") || null; const listId = input?.dataset.listId || ""; if (!label || !input || !listId) return null; input.removeAttribute("id"); input.removeAttribute("name"); input.removeAttribute("data-action"); label.removeAttribute("for"); const name = Array.from(label.childNodes).filter((node) => node !== input && node !== originalCount).map((node) => node.textContent || "").join(" ").replace(/\s+/g, " ").trim() || "未命名清单"; const countMatch = originalCount?.textContent?.match(/\d+/); const count = countMatch ? Number(countMatch[0]) : null; const nameElement = document.createElement("span"); nameElement.className = "jhs-list-item__name"; nameElement.textContent = name; nameElement.title = name; const countElement = document.createElement("span"); countElement.className = "jhs-list-item__count"; countElement.textContent = count === null ? "—" : String(count); countElement.title = count === null ? "影片数量未知" : `清单内 ${count} 部影片`; countElement.setAttribute("aria-label", count === null ? "影片数量未知" : `${count} 部影片`); const statusElement = document.createElement("span"); statusElement.className = "jhs-list-item__status"; statusElement.setAttribute("aria-live", "polite"); statusElement.setAttribute("aria-atomic", "true"); statusElement.hidden = true; element.classList.add("jhs-list-item"); element.dataset.listId = listId; element.dataset.listName = name; label.classList.add("jhs-list-item__label"); label.replaceChildren(input, nameElement, countElement, statusElement); plugin._updateListPanelEntryState(input); return { element }; } function showListPanelLoading(plugin, panel) { const items = panel.querySelector(".jhs-list-panel__items"); const summary = panel.querySelector(".jhs-list-panel__summary"); const empty = panel.querySelector(".jhs-list-panel__empty"); const search = panel.querySelector(".jhs-list-panel__search"); const sort = panel.querySelector(".jhs-list-panel__sort-select"); const clear = panel.querySelector(".jhs-list-panel__search-clear"); plugin._clearListPanelNotice(panel); if (summary) summary.textContent = "正在读取…"; if (empty) empty.hidden = true; if (search) search.disabled = true; if (sort) sort.disabled = true; if (clear) clear.hidden = true; if (items) { if (items.getAttribute("aria-busy") === "true" && items.firstElementChild) return; items.setAttribute("aria-busy", "true"); items.innerHTML = Array.from({ length: 4 }, () => jsxToString((0, import_jsx_runtime.jsx)(ListPanelSkeletonSpan, {}))).join(""); } } function showListPanelError(plugin, panel, message) { const items = panel.querySelector(".jhs-list-panel__items"); const summary = panel.querySelector(".jhs-list-panel__summary"); const search = panel.querySelector(".jhs-list-panel__search"); const sort = panel.querySelector(".jhs-list-panel__sort-select"); const clear = panel.querySelector(".jhs-list-panel__search-clear"); plugin._clearListPanelNotice(panel); if (summary) summary.textContent = "读取失败"; if (search) search.disabled = true; if (sort) sort.disabled = true; if (clear) clear.hidden = true; if (!items) return; items.setAttribute("aria-busy", "false"); items.replaceChildren(); const state = document.createElement("div"); state.className = "jhs-list-panel__error"; state.setAttribute("role", "alert"); const text = document.createElement("span"); text.textContent = message; const retry = document.createElement("button"); retry.type = "button"; retry.className = "jhs-list-panel__retry"; retry.dataset.listPanelRetry = "true"; retry.textContent = "重新加载"; state.append(text, retry); items.appendChild(state); } var LIST_PANEL_MAX_PAGES = 50; function loadAllListPages(plugin, panel, container) { if (plugin._listPanelPaginationPromise) return; const videoId = window.location.pathname.match(/\/v\/([^/?#]+)/)?.[1]; if (!videoId) { container.dataset.jhsPagesStatus = "complete"; plugin._scheduleListPanelSync(0); return; } container.dataset.jhsPagesStatus = "loading"; const generation = plugin._listPanelPaginationGeneration; plugin._listPanelPaginationPromise = plugin._fetchAllListPageEntries(videoId).then((fetchedEntries) => { const currentContainer = document.querySelector("#modal-save-list [data-list-target=\"listContainer\"]"); if (!currentContainer) throw new Error("原生清单容器已移除"); if (generation !== plugin._listPanelPaginationGeneration || currentContainer !== container) return; const existingIds = new Set(Array.from(currentContainer.querySelectorAll("input[type=\"checkbox\"][data-list-id]")).map((input) => input.dataset.listId)); const fragment = document.createDocumentFragment(); for (const entry of fetchedEntries) { const listId = entry.querySelector("input[type=\"checkbox\"][data-list-id]")?.dataset.listId; if (!listId || existingIds.has(listId)) continue; existingIds.add(listId); fragment.appendChild(document.importNode(entry, true)); } currentContainer.dataset.jhsPagesStatus = "complete"; currentContainer.appendChild(fragment); }).catch((error) => { if (generation !== plugin._listPanelPaginationGeneration) return; const currentContainer = document.querySelector("#modal-save-list [data-list-target=\"listContainer\"]"); if (currentContainer) currentContainer.dataset.jhsPagesStatus = "partial"; console.warn("详情页清单分页聚合失败,将显示当前已加载内容", error); plugin._showListPanelNotice(panel, "部分清单未载入,排序与搜索仅覆盖已显示项"); }).finally(() => { plugin._listPanelPaginationPromise = null; plugin._scheduleListPanelSync(0); }); } async function fetchAllListPageEntries(videoId) { const entries = []; const visited = new Set(); let url = new URL("/users/simple_lists", window.location.origin); url.searchParams.set("vid", videoId); while (url) { if (visited.size >= LIST_PANEL_MAX_PAGES) throw new Error(`清单分页超过 ${LIST_PANEL_MAX_PAGES} 页`); if (visited.has(url.href)) throw new Error("清单分页出现循环"); visited.add(url.href); const page = await fetchSimpleListsPage(url.href, videoId); const listDocument = new DOMParser().parseFromString(page.entries, "text/html"); for (const child of Array.from(listDocument.body.children)) if (child.querySelector("input[type=\"checkbox\"][data-list-id]")) entries.push(child); if (!page.nextUrl) { url = null; continue; } url = new URL(page.nextUrl); } return entries; } var DETAIL_LIST_COLLATOR = new Intl.Collator("zh-CN", { numeric: true, sensitivity: "base" }); function readItemCount(item) { const matched = (item.querySelector(".jhs-list-item__count")?.textContent || "").match(/\d+/); return matched ? Number(matched[0]) : -1; } function readItemNum(item) { const matched = (item.dataset.listName || "").match(/(\d+)\s*$/); return matched ? Number(matched[1]) : -1; } var LIST_PANEL_INIT_TIMEOUT_MS = 1e4; var DETAIL_DOM_WAIT_TIMEOUT_MS = 15e3; function ensureListPanel(plugin, nav, startTime = Date.now()) { if (plugin._listPanelEnsured) return; const otherSite = nav.querySelector("#otherSiteBox"); if (!otherSite) { if (Date.now() - startTime > DETAIL_DOM_WAIT_TIMEOUT_MS) clog.warn("[JHS] 清单面板:等待 #otherSiteBox 超时,回退挂载到 nav"); else { setTimeout(() => ensureListPanel(plugin, nav, startTime), 400); return; } } plugin._listPanelEnsured = true; if (!nav.querySelector(".jhs-list-panel")) { if (otherSite) otherSite.insertAdjacentHTML("afterend", jsxToString((0, import_jsx_runtime.jsx)(ListPanel, {}))); else nav.insertAdjacentHTML("beforeend", jsxToString((0, import_jsx_runtime.jsx)(ListPanel, {}))); } const listPanel = nav.querySelector(".jhs-list-panel"); if (!listPanel) return; const sortSelectInit = listPanel.querySelector(".jhs-list-panel__sort-select"); if (sortSelectInit) sortSelectInit.value = plugin._listDisplayConfig.defaultSort; plugin._bindListPanelControls(listPanel); listPanel.__jhsRefresh = () => plugin._scheduleListPanelSync(0); plugin._initListPanel(); } function bindListPanelControls(plugin, panel) { if (panel.__jhsListControlsBound) return; panel.__jhsListControlsBound = true; const search = panel.querySelector(".jhs-list-panel__search"); const sort = panel.querySelector(".jhs-list-panel__sort-select"); const clear = panel.querySelector(".jhs-list-panel__search-clear"); search?.addEventListener("input", () => plugin._applyListPanelView(panel)); sort?.addEventListener("change", () => plugin._applyListPanelView(panel)); clear?.addEventListener("click", () => { if (!search) return; search.value = ""; search.focus(); plugin._applyListPanelView(panel); }); panel.addEventListener("click", (event) => { const target = event.target; if (!(target instanceof Element) || !target.closest("[data-list-panel-retry]")) return; plugin._listPanelIniting = false; plugin._showListPanelLoading(panel); plugin._initListPanel(true); }); panel.addEventListener("click", (event) => { const target = event.target; if (!(target instanceof Element)) return; const toggleButton = target.closest(".jhs-list-group__toggle"); if (toggleButton) { toggleGroupDetail(toggleButton); return; } const reflowButton = target.closest(".jhs-list-group__reflow"); if (reflowButton) { const groupElement = reflowButton.closest(".jhs-list-group"); if (groupElement) reflowGroup(plugin, panel, groupElement); } }); panel.addEventListener("change", (event) => { const displayInput = event.target; if (!(displayInput instanceof HTMLInputElement) || displayInput.type !== "checkbox") return; plugin._forwardListPanelChange(panel, displayInput); }); } function forwardListPanelChange(plugin, panel, displayInput) { if (displayInput.dataset.listGroup) { forwardListGroupChange(plugin, panel, displayInput); return; } const checkedBeforeMutation = !displayInput.checked; const listId = displayInput.dataset.listId; const listContainer = document.querySelector("#modal-save-list [data-list-target=\"listContainer\"]"); const sourceInput = listId ? Array.from(listContainer?.querySelectorAll("input[type=\"checkbox\"][data-list-id]") || []).find((input) => input.dataset.listId === listId) ?? null : null; if (!sourceInput || sourceInput.disabled) { displayInput.checked = checkedBeforeMutation; plugin._applyListPanelView(panel); const message = "清单状态已变化,已恢复原选择,请稍后重试"; plugin._showListPanelNotice(panel, message); show.error(message, { duration: 3e3 }); plugin._scheduleListPanelSync(80); return; } sourceInput.checked = displayInput.checked; sourceInput.dispatchEvent(new Event("change", { bubbles: true })); plugin._applyListPanelView(panel); } function initListPanel(plugin, forceReload = false) { if (plugin._listPanelIniting) return; plugin._listPanelIniting = true; const startedAt = Date.now(); const ensure = () => { const btn = document.querySelector("#save-list-button"); const modal = document.querySelector("#modal-save-list"); const panel = document.querySelector(".jhs-list-panel"); if (!panel) return; if (!btn || !modal) { if (Date.now() - startedAt >= LIST_PANEL_INIT_TIMEOUT_MS) { plugin._listPanelIniting = false; plugin._showListPanelError(panel, "清单加载失败,请重试"); return; } setTimeout(ensure, 400); return; } if (!plugin._listPanelObserver) { plugin._listPanelObserver = new MutationObserver((mutations) => { if (mutations.some((mutation) => mutation.type === "childList" && Array.from(mutation.removedNodes).some((node) => node instanceof Element && (node.matches("input[data-list-id]") || Boolean(node.querySelector("input[data-list-id]")))))) { plugin._listPanelPaginationGeneration++; const currentContainer = document.querySelector("#modal-save-list [data-list-target=\"listContainer\"]"); if (currentContainer) delete currentContainer.dataset.jhsPagesStatus; } if (mutations.every((mutation) => mutation.type === "attributes" && mutation.target instanceof HTMLInputElement)) { for (const mutation of mutations) plugin._syncListPanelInputState(mutation.target); const currentPanel = document.querySelector(".jhs-list-panel"); if (currentPanel) plugin._applyListPanelView(currentPanel); return; } plugin._scheduleListPanelSync(); }); plugin._listPanelObserver.observe(modal, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["checked", "disabled"] }); } plugin._startListPanelLoadTimeout(panel); if (forceReload && plugin._listAjaxTriggered) { btn.click(); setTimeout(() => btn.click(), 200); } else if (!plugin._listAjaxTriggered) { plugin._listAjaxTriggered = true; btn.click(); } setTimeout(() => plugin._scheduleListPanelSync(0), 500); }; ensure(); } function syncListPanelInputState(plugin, sourceInput) { const listId = sourceInput.dataset.listId; if (!listId) return; syncMergedRowsForListId(listId); const displayInput = Array.from(document.querySelectorAll(".jhs-list-panel input[type=\"checkbox\"][data-list-id]")).find((input) => input.dataset.listId === listId) || null; if (!displayInput) return; displayInput.checked = sourceInput.checked; displayInput.disabled = sourceInput.disabled; if (sourceInput.dataset.vltWasDisabled !== void 0) { displayInput.dataset.vltWasDisabled = sourceInput.dataset.vltWasDisabled; displayInput.setAttribute("aria-busy", "true"); } else { delete displayInput.dataset.vltWasDisabled; displayInput.removeAttribute("aria-busy"); } plugin._updateListPanelEntryState(displayInput); } function updateListPanelEntryState(input) { const label = input.closest(".jhs-list-item__label"); const status = label?.querySelector(".jhs-list-item__status"); if (!label || !status) return; const countMatch = (label.querySelector(".jhs-list-item__count")?.textContent || "").match(/\d+/); const count = countMatch ? Number(countMatch[0]) : null; const state = input.dataset.vltWasDisabled !== void 0 ? "busy" : input.disabled && count !== null && count >= 501 && !input.checked ? "full" : input.disabled ? "unavailable" : null; label.removeAttribute("title"); status.hidden = state === null; if (state === null) { status.textContent = ""; delete status.dataset.state; } else { status.dataset.state = state; status.textContent = state === "busy" ? "同步中" : state === "full" ? "已满" : "不可用"; } } function scheduleListPanelSync(plugin, delay = 48) { if (plugin._listPanelSyncTimer !== null) window.clearTimeout(plugin._listPanelSyncTimer); plugin._listPanelSyncTimer = window.setTimeout(() => { plugin._listPanelSyncTimer = null; plugin._renderListPanel(); }, delay); } function renderListPanel(plugin) { const panel = document.querySelector(".jhs-list-panel"); const listContainer = document.querySelector("#modal-save-list [data-list-target=\"listContainer\"]"); const itemsContainer = panel?.querySelector(".jhs-list-panel__items"); if (!panel || !itemsContainer) return; if (!plugin._isListContainerLoaded(listContainer)) { plugin._showListPanelLoading(panel); plugin._startListPanelLoadTimeout(panel); return; } const nativeContainer = listContainer; const pagesStatus = nativeContainer.dataset.jhsPagesStatus; if (!pagesStatus || pagesStatus === "loading") { plugin._showListPanelLoading(panel); plugin._loadAllListPages(panel, nativeContainer); return; } panel.dataset.listCoverage = pagesStatus; const activeElement = document.activeElement; const activeListId = activeElement instanceof HTMLInputElement && itemsContainer.contains(activeElement) ? activeElement.dataset.listId : void 0; const entries = Array.from(listContainer.children).filter((child) => !/[预預][设設]清[单單]/.test(child.textContent || "")).map((child) => plugin._createListPanelEntry(child)).filter((entry) => entry !== null); itemsContainer.replaceChildren(...buildListPanelRows(plugin, entries)); itemsContainer.setAttribute("aria-busy", "false"); plugin._clearListPanelLoadTimeout(); plugin._listPanelIniting = false; const search = panel.querySelector(".jhs-list-panel__search"); const sort = panel.querySelector(".jhs-list-panel__sort-select"); if (search) search.disabled = false; if (sort) sort.disabled = false; plugin._applyListPanelView(panel); if (activeListId) Array.from(itemsContainer.querySelectorAll("input[type=\"checkbox\"][data-list-id]")).find((input) => input.dataset.listId === activeListId)?.focus({ preventScroll: true }); } function applyListPanelView(panel) { const itemsContainer = panel.querySelector(".jhs-list-panel__items"); const sortSelect = panel.querySelector(".jhs-list-panel__sort-select"); const searchInput = panel.querySelector(".jhs-list-panel__search"); const clearButton = panel.querySelector(".jhs-list-panel__search-clear"); const summary = panel.querySelector(".jhs-list-panel__summary"); const empty = panel.querySelector(".jhs-list-panel__empty"); if (!itemsContainer || !sortSelect || !searchInput || !summary || !empty) return; const sortValue = sortSelect.value || "name-desc"; const dashIndex = sortValue.lastIndexOf("-"); const field = dashIndex > 0 ? sortValue.slice(0, dashIndex) : "name"; const direction = sortValue.slice(dashIndex + 1) === "asc" ? 1 : -1; const items = Array.from(itemsContainer.querySelectorAll(".jhs-list-item")); items.sort((left, right) => { let primary; if (field === "count") primary = readItemCount(left) - readItemCount(right); else if (field === "num") primary = readItemNum(left) - readItemNum(right); else primary = DETAIL_LIST_COLLATOR.compare(left.dataset.listName || "", right.dataset.listName || ""); if (primary !== 0) return primary * direction; const byName = DETAIL_LIST_COLLATOR.compare(left.dataset.listName || "", right.dataset.listName || ""); if (byName !== 0) return byName; return DETAIL_LIST_COLLATOR.compare(left.dataset.listId || "", right.dataset.listId || ""); }); itemsContainer.append(...items); const query = searchInput.value.trim().toLocaleLowerCase(); let visibleCount = 0; let selectedCount = 0; for (const item of items) { if (item.querySelector("input[type=\"checkbox\"]")?.checked) selectedCount++; const visible = !query || (item.dataset.listName || "").toLocaleLowerCase().includes(query); item.hidden = !visible; if (visible) visibleCount++; } if (clearButton) clearButton.hidden = query.length === 0; const coverageSuffix = panel.dataset.listCoverage === "partial" ? " · 部分清单" : ""; const summaryText = query ? `已加入 ${selectedCount} · 显示 ${visibleCount}/${items.length}${coverageSuffix}` : `已加入 ${selectedCount} / ${items.length}${coverageSuffix}`; summary.textContent = panel.__jhsNotice || summaryText; summary.classList.toggle("is-error", Boolean(panel.__jhsNotice)); empty.hidden = visibleCount > 0; empty.textContent = items.length === 0 ? "还没有自建清单,可在下方创建。" : `没有找到"${searchInput.value.trim()}"`; } function isListContainerLoaded(container) { return Boolean(container?.querySelector("input[type=\"checkbox\"][data-list-id]")); } function startListPanelLoadTimeout(plugin, panel) { if (plugin._listPanelLoadTimer !== null) return; plugin._listPanelLoadTimer = window.setTimeout(() => { plugin._listPanelLoadTimer = null; const container = document.querySelector("#modal-save-list [data-list-target=\"listContainer\"]"); if (plugin._isListContainerLoaded(container)) return; plugin._listPanelIniting = false; plugin._showListPanelError(panel, "清单内容未能载入,请重试"); }, LIST_PANEL_INIT_TIMEOUT_MS); } function clearListPanelLoadTimeout(plugin) { if (plugin._listPanelLoadTimer === null) return; window.clearTimeout(plugin._listPanelLoadTimer); plugin._listPanelLoadTimer = null; } function showListPanelNotice(plugin, panel, message) { if (panel.__jhsNoticeTimer !== void 0) window.clearTimeout(panel.__jhsNoticeTimer); panel.__jhsNotice = message; plugin._applyListPanelView(panel); panel.__jhsNoticeTimer = window.setTimeout(() => { delete panel.__jhsNotice; delete panel.__jhsNoticeTimer; plugin._applyListPanelView(panel); }, 3e3); } function clearListPanelNotice(panel) { if (panel.__jhsNoticeTimer !== void 0) window.clearTimeout(panel.__jhsNoticeTimer); delete panel.__jhsNotice; delete panel.__jhsNoticeTimer; panel.querySelector(".jhs-list-panel__summary")?.classList.remove("is-error"); } function SubtitleActionCell() { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ (0, import_jsx_runtime.jsx)("a", { className: "a-primary", children: "预览" }), " ", (0, import_jsx_runtime.jsx)("a", { className: "a-success", children: "下载" }) ] }); } function SubtitleLine({ paddedNum, line }) { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ (0, import_jsx_runtime.jsx)("span", { style: { color: "#AAA" }, children: `${paddedNum}. ` }), line, "\n" ] }); } function SubtitlePreviewDialog({ content }) { return (0, import_jsx_runtime.jsx)("div", { style: { padding: "15px 5px", background: "#1E1E1E", color: "#FFF", fontFamily: "Consolas,Monaco,monospace", whiteSpace: "pre-wrap", overflow: "auto", height: "100%" }, dangerouslySetInnerHTML: { __html: content } }); } function SubtitleTableDialog() { return (0, import_jsx_runtime.jsx)("div", { style: { height: "100%", overflow: "hidden" }, children: (0, import_jsx_runtime.jsx)("div", { id: "xunlei-table-container", style: { height: "100%", paddingBottom: "20px" } }) }); } async function searchXunLeiSubtitle(plugin, carNum) { try { await withLoading(async () => { const subtitleList = (await gmHttp.get(`https://api-shoulei-ssl.xunlei.com/oracle/subtitle?gcid=&cid=&name=${carNum}`)).data; if (subtitleList && subtitleList.length !== 0) layer.open({ type: 1, title: "迅雷字幕", content: jsxToString((0, import_jsx_runtime.jsx)(SubtitleTableDialog, {})), scrollbar: false, area: utils.getResponsiveArea(["60%", "70%"]), anim: -1, success: (_layero, index) => { new Tabulator("#xunlei-table-container", createTabulatorOptions([ { title: "文件名", field: "name", headerSort: false, responsive: 0 }, { title: "类型", field: "ext", headerSort: false, responsive: 0 }, { title: "操作", responsive: 0, headerSort: false, formatter: (cell, _formatterParams, onRendered) => { const rowData = cell.getData(); onRendered(() => { const previewBtn = cell.getElement().querySelector(".a-primary"); const downloadBtn = cell.getElement().querySelector(".a-success"); if (previewBtn) previewBtn.addEventListener("click", async (_event) => { const subUrl = rowData.url; const subFilename = carNum + "." + rowData.ext; plugin.previewSubtitle(subUrl, subFilename); }); if (downloadBtn) downloadBtn.addEventListener("click", async (_event) => { const subUrl = rowData.url; const subFilename = carNum + "." + rowData.ext; const content = await gmHttp.get(subUrl); utils.download(content, subFilename); }); }); return jsxToString((0, import_jsx_runtime.jsx)(SubtitleActionCell, {})); } } ], subtitleList)); utils.setupEscClose(index); } }); else show.error("迅雷中找不到相关字幕!"); }); } catch (err) { clog.error(err); show.error("字幕搜索失败: " + (err instanceof Error ? err.message : String(err))); } } async function previewSubtitle(url, filename) { if (!url) { clog.error("未提供文件URL"); return; } const ext = url.split(".").pop().toLowerCase(); if (ext === "ass" || ext === "srt") try { const subtitleContent = await gmHttp.get(url); let title = "字幕预览"; if (ext === "ass") title = "ASS字幕预览 - " + filename; else if (ext === "srt") title = "SRT字幕预览 - " + filename; const lines = subtitleContent.split("\n"); let output = ""; const numWidth = String(lines.length).length; lines.forEach((line, idx) => { const paddedNum = String(idx + 1).padStart(numWidth, " "); output += jsxToString((0, import_jsx_runtime.jsx)(SubtitleLine, { paddedNum, line })); }); const htmlContent = output; layer.open({ type: 1, title, area: ["80%", "80%"], scrollbar: false, content: jsxToString((0, import_jsx_runtime.jsx)(SubtitlePreviewDialog, { content: htmlContent })), btn: ["下载", "关闭"], btn1: function(_index, _layero, _instance) { utils.download(subtitleContent, filename); return false; } }); } catch (err) { show.error(`预览失败: ${err instanceof Error ? err.message : String(err)}`); clog.error("预览字幕文件出错:", err); } else show.error("仅支持预览ASS和SRT字幕文件"); } function waitForDomChange(selector, ms) { return new Promise((resolve) => { const start = Date.now(); const el = document.querySelector(selector); if (!el) { resolve(null); return; } const before = el.innerHTML; const check = () => { const cur = document.querySelector(selector); if (!cur) { resolve(null); return; } if (cur.innerHTML !== before) { resolve(cur); return; } if (Date.now() - start > ms) { resolve(cur); return; } setTimeout(check, 200); }; setTimeout(check, 200); }); } function waitForEl(fn, ms) { return new Promise((resolve) => { const start = Date.now(); const check = () => { const el = fn(); if (el) { resolve(el); return; } if (Date.now() - start > ms) { resolve(null); return; } setTimeout(check, 150); }; check(); }); } var wrapperStyle$1 = { margin: "10px auto", display: "flex", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: "20px" }; var rowStyle = { display: "flex", gap: "10px", flexWrap: "wrap" }; function statusBtnStyle(width, backgroundColor) { return { width, backgroundColor, color: "white", textAlign: "center", padding: "8px 0" }; } function DetailMenuButtons({ filterText, filterColor, favoriteText, favoriteColor, watchedText, watchedColor, showMagnetSearch }) { return (0, import_jsx_runtime.jsxs)("div", { style: wrapperStyle$1, children: [(0, import_jsx_runtime.jsxs)("div", { className: "jhs-menu-status-row", style: rowStyle, children: [ (0, import_jsx_runtime.jsx)("a", { id: "filterBtn", className: "jhs-toolbar-menu-btn", style: statusBtnStyle("120px", filterColor), children: (0, import_jsx_runtime.jsx)("span", { children: filterText }) }), (0, import_jsx_runtime.jsx)("a", { id: "favoriteBtn", className: "jhs-toolbar-menu-btn", style: statusBtnStyle("120px", favoriteColor), children: (0, import_jsx_runtime.jsx)("span", { children: favoriteText }) }), (0, import_jsx_runtime.jsx)("a", { id: "hasWatchBtn", className: "jhs-toolbar-menu-btn", style: statusBtnStyle("120px", watchedColor), children: (0, import_jsx_runtime.jsx)("span", { children: watchedText }) }) ] }), (0, import_jsx_runtime.jsxs)("div", { className: "jhs-menu-tools-row", style: rowStyle, children: [ (0, import_jsx_runtime.jsx)("a", { id: "enable-magnets-filter", className: "jhs-toolbar-menu-btn", style: { width: "140px", backgroundColor: "#c2bd4c", color: "white", textAlign: "center", padding: "8px 0" }, children: (0, import_jsx_runtime.jsx)("span", { id: "magnets-span", children: "关闭磁力过滤" }) }), (0, import_jsx_runtime.jsx)("a", { id: "xunLeiSubtitleBtn", className: "jhs-toolbar-menu-btn", style: { width: "120px", background: "linear-gradient(to left, #375f7c, #2196F3)", color: "white", textAlign: "center", padding: "8px 0" }, children: (0, import_jsx_runtime.jsx)("span", { children: "字幕 (迅雷)" }) }), (0, import_jsx_runtime.jsx)("a", { id: "search-subtitle-btn", className: "jhs-toolbar-menu-btn", style: { width: "160px", background: "linear-gradient(to bottom, #8d5656, rgb(196,159,91))", color: "white", textAlign: "center", padding: "8px 0" }, children: (0, import_jsx_runtime.jsx)("span", { children: "字幕 (SubTitleCat)" }) }), showMagnetSearch && (0, import_jsx_runtime.jsx)("a", { id: "magnetSearchBtn", className: "jhs-toolbar-menu-btn", style: { width: "120px", background: "linear-gradient(to right, rgb(245,140,1), rgb(84,161,29))", color: "white", textAlign: "center", padding: "8px 0" }, children: (0, import_jsx_runtime.jsx)("span", { children: "磁力搜索" }) }) ] })] }); } function MagnetHubMountBox() { return (0, import_jsx_runtime.jsx)("div", { id: "magnet-hub-mount", style: { padding: "10px" } }); } var DetailPageButtonPlugin = class extends BasePlugin { _wantWatchedObserved = false; _lastWantState = null; _wantWatchedSyncing = false; _wantWatchedDebounce = null; _wantWatchedListenerInstalled = false; _quickActionAdded = false; _listPanelEnsured = false; _listPanelIniting = false; _listAjaxTriggered = false; _listPanelObserver = null; _listPanelSyncTimer = null; _listPanelLoadTimer = null; _listPanelPaginationPromise = null; _listPanelPaginationGeneration = 0; _listMergeConfig = { enabled: false, pattern: compileMergePattern(DEFAULT_LIST_MERGE_PATTERN), excludeNames: new Set() }; _listDisplayConfig = { defaultSort: "name-desc" }; _ratingSyncDebounce = null; _reviewChain = null; _autoSyncChecked = false; getName() { return "DetailPageButtonPlugin"; } get pageTypes() { return ["detail"]; } async handle() { this.hideVideoControls(); if (window.isDetailPage) { try { this._listMergeConfig = await loadListMergeConfig(); } catch (error) { clog.error("[JHS] 清单合并配置加载失败,已按关闭处理: " + (error instanceof Error ? error.message : String(error))); } try { const settings = await storageManager.getSetting(); const validListSorts = [ "name-desc", "name-asc", "count-desc", "count-asc", "num-desc", "num-asc" ]; this._listDisplayConfig = { defaultSort: validListSorts.includes(settings.listDefaultSort) ? settings.listDefaultSort : "name-desc" }; } catch (error) { clog.error("[JHS] 清单显示配置加载失败,已按默认处理: " + (error instanceof Error ? error.message : String(error))); } this.createMenuBtn(); this.hookWantAndWatchedButtons(); this.addQuickActionButtons(); this.setupWantWatchedSyncListener(); try { const syncSettings = await storageManager.getSetting("autoServerSync", "yes"); setupAutoReviewSync(this, syncSettings !== "no"); } catch (error) { clog.error("[JHS] 评价状态自动校验配置加载失败,已按默认开启处理: " + (error instanceof Error ? error.message : String(error))); } } } async createMenuBtn() { const carNum = this.getPageInfo().carNum; const menuHtml = jsxToString((0, import_jsx_runtime.jsx)(DetailMenuButtons, { filterText: BLOCK_TEXT, filterColor: BLOCK_COLOR, favoriteText: FAVORITE_TEXT, favoriteColor: FAVORITE_COLOR, watchedText: WATCHED_TEXT, watchedColor: WATCHED_COLOR, showMagnetSearch: featureFlags.magnetHubPlugin })); if (isJavdbSite) $(".tabs").after(menuHtml); $("#favoriteBtn").on("click", () => this.favoriteOne()); $("#filterBtn").on("click", (event) => this.filterOne(event)); $("#hasWatchBtn").on("click", async () => this.hasWatchOne()); const highlightPlugin = this.getBean("HighlightMagnetPlugin"); const enableFilter = await storageManager.getSetting("enableMagnetsFilter", "yes"); $("#magnets-span").text(enableFilter === "yes" ? "关闭磁力过滤" : "开启磁力过滤"); if (enableFilter === "yes") highlightPlugin.doFilterMagnet(); $("#enable-magnets-filter").on("click", (_event) => { const $span = $("#magnets-span"); if ($span.text() === "关闭磁力过滤") { highlightPlugin.showAll(); $span.text("开启磁力过滤"); storageManager.saveSettingItem("enableMagnetsFilter", "no"); } else { highlightPlugin.doFilterMagnet(); $span.text("关闭磁力过滤"); storageManager.saveSettingItem("enableMagnetsFilter", "yes"); } }); $("#search-subtitle-btn").on("click", (event) => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum}`, carNum, false, event)); $("#xunLeiSubtitleBtn").on("click", () => this.searchXunLeiSubtitle(carNum)); if (featureFlags.magnetHubPlugin) $("#magnetSearchBtn").on("click", () => { const hub = this.getBean("MagnetHubPlugin"); if (!hub) return; layer.open({ type: 1, title: `磁力搜索 - ${carNum}`, content: jsxToString((0, import_jsx_runtime.jsx)(MagnetHubMountBox, {})), area: utils.getResponsiveArea(["80%", "80%"]), success: () => { $("#magnet-hub-mount").append(hub.createMagnetHub(carNum ?? "")); } }); }); this.showStatus(carNum).then(); } async showStatus(carNum) { const $filterSpan = $("#filterBtn span"); const $favoriteSpan = $("#favoriteBtn span"); const $hasWatchSpan = $("#hasWatchBtn span"); $filterSpan.text(BLOCK_TEXT); $favoriteSpan.text(FAVORITE_TEXT); $hasWatchSpan.text(WATCHED_TEXT); const carRecord = await storageManager.getCar(carNum); if (carRecord) switch (carRecord.status) { case FILTER_ACTION: $filterSpan.text(BLOCKED_TEXT); break; case FAVORITE_ACTION: $favoriteSpan.text(FAVORITED_TEXT); break; case HAS_WATCH_ACTION: $hasWatchSpan.text("🔍 已标记观看"); } } hookWantAndWatchedButtons() { hookWantAndWatchedButtons(this); } detectWantWatchedState(container) { return detectWantWatchedState(container); } async onWantAdded() { return onWantAdded(this); } async onWantRemoved() { return onWantRemoved(this); } async onWatchedAdded() { return onWatchedAdded(this); } async onWatchedRemoved() { return onWatchedRemoved(this); } async removeCarIfStatus(carNum, status) { return removeCarIfStatus(carNum, status); } broadcastWantWatchedSync(payload) { broadcastWantWatchedSync(payload); } setupWantWatchedSyncListener() { setupWantWatchedSyncListener(this); } refreshItemStatusTag(carNum) { refreshItemStatusTag(this, carNum); } addQuickActionButtons() { addQuickActionButtons(this); } _buildRatingBar(nav) { buildRatingBar(this, nav); } _injectRatingStyles() { injectRatingStyles(); } _syncRatingBar() { syncRatingBar(this); } _setRatingBusy(busy) { setRatingBusy(busy); } async quickSetHasWatch(score) { return quickSetHasWatch(this, score); } async quickBlock() { return quickBlock(this); } async quickConvertToFav() { return quickConvertToFav(this); } async _triggerJavdbReview(score) { return triggerJavdbReview(this, score); } async _triggerJavdbWant() { return triggerJavdbWant(this); } _ensureListPanel(nav) { ensureListPanel(this, nav); } _bindListPanelControls(panel) { bindListPanelControls(this, panel); } _forwardListPanelChange(panel, displayInput) { forwardListPanelChange(this, panel, displayInput); } _initListPanel(forceReload = false) { initListPanel(this, forceReload); } _syncListPanelInputState(sourceInput) { syncListPanelInputState(this, sourceInput); } _updateListPanelEntryState(input) { updateListPanelEntryState(input); } _scheduleListPanelSync(delay = 48) { scheduleListPanelSync(this, delay); } _renderListPanel() { renderListPanel(this); } _createListPanelEntry(source) { return createListPanelEntry(this, source); } _applyListPanelView(panel) { applyListPanelView(panel); } _showListPanelLoading(panel) { showListPanelLoading(this, panel); } _showListPanelError(panel, message) { showListPanelError(this, panel, message); } _isListContainerLoaded(container) { return isListContainerLoaded(container); } _startListPanelLoadTimeout(panel) { startListPanelLoadTimeout(this, panel); } _clearListPanelLoadTimeout() { clearListPanelLoadTimeout(this); } _loadAllListPages(panel, container) { loadAllListPages(this, panel, container); } async _fetchAllListPageEntries(videoId) { return fetchAllListPageEntries(videoId); } _showListPanelNotice(panel, message) { showListPanelNotice(this, panel, message); } _clearListPanelNotice(panel) { clearListPanelNotice(panel); } searchXunLeiSubtitle(carNum) { searchXunLeiSubtitle(this, carNum); } async previewSubtitle(url, filename) { return previewSubtitle(url, filename); } _waitForDomChange(selector, ms) { return waitForDomChange(selector, ms); } _waitForEl(fn, ms) { return waitForEl(fn, ms); } async favoriteOne() { const pageInfo = this.getPageInfo(); await storageManager.saveCar({ carNum: pageInfo.carNum, url: pageInfo.url ?? void 0, names: pageInfo.actress ?? void 0, actionType: FAVORITE_ACTION, publishTime: pageInfo.publishTime ?? void 0 }); const clr = this.getBean("CarListReaderPlugin"); if (clr) await clr.waitForPendingPush(); this.showStatus(pageInfo.carNum).then(); refresh(); utils.closePage(); } async hasWatchOne() { const pageInfo = this.getPageInfo(); await storageManager.saveCar({ carNum: pageInfo.carNum, url: pageInfo.url ?? void 0, names: pageInfo.actress ?? void 0, actionType: HAS_WATCH_ACTION, publishTime: pageInfo.publishTime ?? void 0 }); const clr = this.getBean("CarListReaderPlugin"); if (clr) await clr.waitForPendingPush(); this.showStatus(pageInfo.carNum).then(); refresh(); utils.closePage(); } async filterOne(event) { if (event) event.preventDefault(); const pageInfo = this.getPageInfo(); utils.q(event, `是否屏蔽${pageInfo.carNum}?`, async () => { await storageManager.saveCar({ carNum: pageInfo.carNum, url: pageInfo.url ?? void 0, names: pageInfo.actress ?? void 0, actionType: FILTER_ACTION, publishTime: pageInfo.publishTime ?? void 0 }); const clr = this.getBean("CarListReaderPlugin"); if (clr) await clr.waitForPendingPush(); this.showStatus(pageInfo.carNum).then(); refresh(); utils.closePage(); }); } speedVideo() { if ($("#preview-video").is(":visible")) { const videoEl = document.getElementById("preview-video"); if (videoEl) { videoEl.muted = false; videoEl.controls = false; if (videoEl.currentTime + 5 < videoEl.duration) videoEl.currentTime += 5; else { show.info("预览视频结束, 已回到开头"); videoEl.currentTime = 1; } } return; } const $iframe = $("iframe[id^=\"layui-layer-iframe\"]"); if ($iframe.length > 0) { $iframe[0].contentWindow?.postMessage("speedVideo", "*"); return; } const $container = $(".preview-video-container"); if ($container.length > 0) { $container[0].click(); const videoEl = document.getElementById("preview-video"); if (videoEl) { videoEl.currentTime += 5; videoEl.muted = false; } } else $("#javTrailersBtn").click(); } hideVideoControls() { $(document).on("mouseenter", "#preview-video", function() { $(this).prop("controls", true); }); } }; var MAX_PAGES$1 = 200; async function fetchText(url) { const response = await fetch(url, { credentials: "same-origin" }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.text(); } function absoluteNextUrl(doc, baseUrl) { const href = doc.querySelector("a.pagination-next")?.getAttribute("href"); if (!href) return null; return new URL(href, baseUrl).href; } function extractListId(href) { const pathMatch = href.match(/\/lists\/([^/?#]+)/); if (pathMatch && pathMatch[1]) return pathMatch[1]; try { return new URL(href, window.location.origin).searchParams.get("id") || null; } catch { return null; } } function getCurrentListId() { return window.location.pathname.match(/\/lists\/([^/?#]+)/)?.[1] ?? null; } async function fetchAllUserLists() { const lists = []; const seenIds = new Set(); let url = new URL("/users/lists", window.location.origin).href; const visited = new Set(); for (let page = 0; url && page < MAX_PAGES$1; page++) { if (visited.has(url)) break; visited.add(url); const html = await fetchText(url); const doc = new DOMParser().parseFromString(html, "text/html"); doc.querySelectorAll("#lists > ul > li").forEach((li) => { const anchor = li.querySelector("div.column.is-10 > a") || li.querySelector("a"); if (!anchor) return; const listId = extractListId(anchor.getAttribute("href") || ""); if (!listId || listId === "users" || listId === "new" || seenIds.has(listId)) return; seenIds.add(listId); const text = (anchor.textContent || "").trim(); const countMatch = text.match(/\((\d+)\)\s*$/); const count = countMatch ? Number(countMatch[1]) : null; const name = text.replace(/\s*\(\d+\)\s*$/, "").trim(); if (name) lists.push({ listId, name, count }); }); url = absoluteNextUrl(doc, url); } return lists; } function discoverGroup(currentListId, allLists, config) { const current = allLists.find((l) => l.listId === currentListId); if (!current) return null; const parsed = parseListName(current.name, config.pattern); if (!parsed || isMergeExcluded(current.name, parsed.base, config)) return null; const base = parsed.base; const members = []; for (const list of allLists) { if (list.listId === currentListId) { members.push({ listId: list.listId, name: list.name, num: parsed.num, count: list.count }); continue; } const p = parseListName(list.name, config.pattern); if (p && p.base === base && !isMergeExcluded(list.name, p.base, config)) members.push({ listId: list.listId, name: list.name, num: p.num, count: list.count }); else if (list.name.trim() === base && !isMergeExcluded(list.name, null, config)) members.push({ listId: list.listId, name: list.name, num: 0, count: list.count }); } members.sort((a, b) => a.num !== b.num ? a.num - b.num : a.listId < b.listId ? -1 : 1); return { base, members }; } async function enumerateListVideos(listId) { const videoIds = []; const seen = new Set(); let url = new URL(`/lists/${listId}`, window.location.origin).href; const visited = new Set(); for (let page = 0; url && page < MAX_PAGES$1; page++) { if (visited.has(url)) break; visited.add(url); const html = await fetchText(url); const doc = new DOMParser().parseFromString(html, "text/html"); doc.querySelectorAll(".movie-list .item").forEach((item) => { const matched = (item.querySelector("a[href*=\"/v/\"]")?.getAttribute("href") || "").match(/\/v\/([^/?#]+)/); if (!matched) return; const videoId = matched[1]; if (!seen.has(videoId)) { seen.add(videoId); videoIds.push(videoId); } }); url = absoluteNextUrl(doc, url); } return videoIds; } var JOURNAL_KEY = "jhs:reflow:journal"; var ROLLBACK_PREFIX = "jhs:reflow:rollback:"; function saveJournal(entry) { entry.updatedAt = Date.now(); try { localStorage.setItem(JOURNAL_KEY, JSON.stringify(entry)); } catch {} } function loadJournal() { try { const raw = localStorage.getItem(JOURNAL_KEY); if (!raw) return null; return JSON.parse(raw); } catch { return null; } } function clearJournal() { localStorage.removeItem(JOURNAL_KEY); } function hasResumableJournal() { const j = loadJournal(); return j !== null && (j.status === "running" || j.status === "paused"); } function appendRollback(sessionId, entry) { const key = ROLLBACK_PREFIX + sessionId; try { const raw = localStorage.getItem(key); const list = raw ? JSON.parse(raw) : []; list.push(entry); localStorage.setItem(key, JSON.stringify(list)); } catch {} } function loadRollback(sessionId) { const key = ROLLBACK_PREFIX + sessionId; try { const raw = localStorage.getItem(key); if (!raw) return []; return JSON.parse(raw); } catch { return []; } } function createSessionId() { return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function executeMove(move, nameOf) { const errors = []; let addedTo = null; if (move.addTo !== null) { const add = await postJavdbListMutation(move.videoId, move.addTo, true); if (add.kind === "rejected") { errors.push(`[${move.videoId}] 加入「${nameOf(move.addTo)}」被拒:${add.message}(跳过,不删来源)`); return { errors, rollback: null, cleanup: false }; } if (add.kind === "unknown") { if (!(await fetchAuthoritativeCheckboxState(move.videoId, move.addTo))?.checked) { errors.push(`[${move.videoId}] 加入「${nameOf(move.addTo)}」未确认(跳过,不删来源)`); return { errors, rollback: null, cleanup: false }; } } addedTo = move.addTo; } const removedFrom = []; let hasCleanup = false; for (const removeId of move.removeFrom) { const rm = await postJavdbListMutation(move.videoId, removeId, false); if (rm.kind === "confirmed") removedFrom.push(removeId); else if (rm.kind === "rejected") errors.push(`[${move.videoId}] 移除「${nameOf(removeId)}」被拒:${rm.message}`); else if ((await fetchAuthoritativeCheckboxState(move.videoId, removeId))?.checked) { errors.push(`[${move.videoId}] 移除「${nameOf(removeId)}」未确认(待清理)`); hasCleanup = true; } else removedFrom.push(removeId); } return { errors, rollback: addedTo !== null || removedFrom.length > 0 ? { videoId: move.videoId, addedTo, removedFrom } : null, cleanup: hasCleanup }; } async function executeCompactionPlan(plan, options) { const { startIndex, count, signal, onProgress } = options; const nameOf = options.nameOf ?? ((id) => id); const baseInterval = options.interval; const batchSize = options.batchSize ?? 20; const maxRetry = options.maxRetry ?? 3; const sessionId = options.sessionId ?? createSessionId(); const base = options.base ?? plan.base; const end = Math.min(plan.moves.length, startIndex + count); const errors = []; const pendingCleanup = []; let completed = 0; let failed = 0; let nextIndex = startIndex; let currentInterval = baseInterval; let backoffLevel = 0; let consecutiveFailures = 0; const journal = { sessionId, base, totalMoves: plan.moves.length, nextIndex: startIndex, completed: 0, failed: 0, createdAt: Date.now(), updatedAt: Date.now(), status: "running" }; saveJournal(journal); for (let i = startIndex; i < end; i++) { if (signal.aborted) { journal.status = "aborted"; journal.nextIndex = i; journal.completed = completed; journal.failed = failed; saveJournal(journal); return { status: "aborted", completed, failed, nextIndex: i, errors, sessionId, pendingCleanup }; } if (consecutiveFailures >= maxRetry) { journal.status = "paused"; journal.nextIndex = i; journal.completed = completed; journal.failed = failed; saveJournal(journal); errors.push(`连续失败 ${consecutiveFailures} 次,暂停执行(可从 ${i} 续传)`); return { status: "suspended", completed, failed, nextIndex: i, errors, sessionId, pendingCleanup }; } const move = plan.moves[i]; onProgress?.({ done: i - startIndex, total: end - startIndex, videoId: move.videoId, currentInterval, backoffLevel }); try { const result = await executeMove(move, nameOf); if (result.errors.length === 0) { completed++; consecutiveFailures = 0; if (backoffLevel > 0) { backoffLevel = Math.max(0, backoffLevel - 1); currentInterval = baseInterval * Math.pow(2, backoffLevel); } } else { failed++; consecutiveFailures++; errors.push(...result.errors); if (backoffLevel < 3) { backoffLevel++; currentInterval = baseInterval * Math.pow(2, backoffLevel); } } if (result.cleanup) pendingCleanup.push(move.videoId); if (result.rollback) appendRollback(sessionId, result.rollback); } catch (err) { failed++; consecutiveFailures++; errors.push(`[${move.videoId}] 异常:${err instanceof Error ? err.message : String(err)}`); if (backoffLevel < 3) { backoffLevel++; currentInterval = baseInterval * Math.pow(2, backoffLevel); } } nextIndex = i + 1; if ((i - startIndex + 1) % batchSize === 0 || i + 1 >= end) { journal.nextIndex = nextIndex; journal.completed = completed; journal.failed = failed; saveJournal(journal); } if (i + 1 < end && !signal.aborted) await sleep(currentInterval); } const status = nextIndex >= plan.moves.length ? "done" : "paused"; journal.status = status === "done" ? "done" : "paused"; journal.nextIndex = nextIndex; journal.completed = completed; journal.failed = failed; saveJournal(journal); if (status === "done") clearJournal(); return { status, completed, failed, nextIndex, errors, sessionId, pendingCleanup }; } async function undoSession(sessionId, options) { const rollbackLog = loadRollback(sessionId); const errors = []; let undone = 0; let failed = 0; for (let i = rollbackLog.length - 1; i >= 0; i--) { if (options.signal.aborted) break; const entry = rollbackLog[i]; options.onProgress?.({ done: rollbackLog.length - 1 - i, total: rollbackLog.length, videoId: entry.videoId }); try { for (const listId of entry.removedFrom) await postJavdbListMutation(entry.videoId, listId, true); if (entry.addedTo) await postJavdbListMutation(entry.videoId, entry.addedTo, false); undone++; } catch (err) { failed++; errors.push(`[${entry.videoId}] 撤销失败:${err instanceof Error ? err.message : String(err)}`); } if (i > 0 && !options.signal.aborted) await sleep(options.interval); } return { undone, failed, errors }; } function sortListsAsc(lists) { return [...lists].sort((a, b) => { if (a.num !== b.num) return a.num - b.num; return a.listId < b.listId ? -1 : a.listId > b.listId ? 1 : 0; }); } function dedupePreserveOrder(ids) { const seen = new Set(); const out = []; for (const id of ids) if (!seen.has(id)) { seen.add(id); out.push(id); } return out; } function planCompaction(base, lists) { const sorted = sortListsAsc(lists); const count = sorted.length; const dedupedVideos = sorted.map((list) => dedupePreserveOrder(list.videoIds)); const membership = new Map(); sorted.forEach((list, index) => { for (const videoId of dedupedVideos[index]) { const existing = membership.get(videoId); if (existing) existing.push(list.listId); else membership.set(videoId, [list.listId]); } }); const uniqueVideos = Array.from(membership.keys()); const totalUnique = uniqueVideos.length; const targets = new Array(count).fill(0); let remaining = totalUnique; for (let i = 0; i < count; i++) { const take = Math.min(501, remaining); targets[i] = take; remaining -= take; } const cap = [...targets]; const final = new Map(); const placed = new Set(); for (let i = 0; i < count; i++) for (const videoId of dedupedVideos[i]) if (cap[i] > 0 && !placed.has(videoId)) { final.set(videoId, sorted[i].listId); placed.add(videoId); cap[i]--; } const overflow = []; for (const videoId of uniqueVideos) { if (placed.has(videoId)) continue; let assigned = false; for (let i = 0; i < count; i++) if (cap[i] > 0) { final.set(videoId, sorted[i].listId); placed.add(videoId); cap[i]--; assigned = true; break; } if (!assigned) overflow.push(videoId); } const moves = []; for (const [videoId, finalList] of final) { const current = membership.get(videoId) ?? []; const removeFrom = current.filter((id) => id !== finalList); const addTo = current.includes(finalList) ? null : finalList; if (removeFrom.length > 0 || addTo !== null) moves.push({ videoId, removeFrom, addTo }); } const duplicates = uniqueVideos.filter((videoId) => (membership.get(videoId)?.length ?? 0) > 1); const targetRows = sorted.map((list, i) => ({ listId: list.listId, num: list.num, name: list.name, current: dedupedVideos[i].length, target: targets[i] })); const addCount = moves.filter((m) => m.addTo !== null).length; const removeCount = moves.reduce((sum, m) => sum + m.removeFrom.length, 0); return { base, totalUnique, duplicateCount: duplicates.length, duplicates, targets: targetRows, moves, addCount, removeCount, overflow }; } var MOVE_PREVIEW_CAP = 100; var ID_PREVIEW_CAP = 50; function div(className, text) { const element = document.createElement("div"); element.className = className; if (text !== void 0) element.textContent = text; return element; } function previewIds(ids, cap) { const head = ids.slice(0, cap).join("、"); return ids.length > cap ? `${head}…` : head; } function renderPlan(planElement, plan) { planElement.replaceChildren(); planElement.appendChild(div("jhs-reflow-plan__summary", `组内共 ${plan.totalUnique} 部(去重后)· 重复 ${plan.duplicateCount} 部 · 需搬运 ${plan.moves.length} 部(加入 ${plan.addCount} 次 / 移除 ${plan.removeCount} 次)`)); planElement.appendChild(div("jhs-reflow-plan__targets", "目标分布:" + plan.targets.map((t) => `${t.name} ${t.current}→${t.target}`).join(" | "))); if (plan.overflow.length > 0) planElement.appendChild(div("jhs-reflow-plan__overflow", `⚠ 组总容量不足,${plan.overflow.length} 部无法安置(期2 可自动新建清单消化):` + previewIds(plan.overflow, ID_PREVIEW_CAP))); if (plan.duplicates.length > 0) planElement.appendChild(div("jhs-reflow-plan__dups", `⚠ 收藏错误审计——${plan.duplicateCount} 部在组内多个清单重复:` + previewIds(plan.duplicates, ID_PREVIEW_CAP))); const nameOf = new Map(plan.targets.map((t) => [t.listId, t.name])); const movesList = document.createElement("ul"); movesList.className = "jhs-reflow-plan__moves"; for (const move of plan.moves.slice(0, MOVE_PREVIEW_CAP)) { const li = document.createElement("li"); li.className = "jhs-reflow-plan__move"; const parts = []; if (move.removeFrom.length > 0) parts.push(`移出 ${move.removeFrom.map((id) => nameOf.get(id) ?? id).join("、")}`); if (move.addTo !== null) parts.push(`移入 ${nameOf.get(move.addTo) ?? move.addTo}`); li.textContent = `${move.videoId}:${parts.join(",")}`; movesList.appendChild(li); } if (plan.moves.length > MOVE_PREVIEW_CAP) { const more = document.createElement("li"); more.className = "jhs-reflow-plan__more"; more.textContent = `…及其他 ${plan.moves.length - MOVE_PREVIEW_CAP} 条`; movesList.appendChild(more); } planElement.appendChild(movesList); planElement.appendChild(div("jhs-reflow-plan__note", "期1 仅预览,不执行任何写入;搬运顺序优化与执行为期2 内容。")); } async function runPlanPreview(panel, group, setStatus) { const planElement = panel.querySelector("[data-reflow-plan]"); if (!planElement) return null; planElement.hidden = false; planElement.replaceChildren(div("jhs-reflow-plan__summary", "正在枚举组内影片…")); const inputs = []; for (let i = 0; i < group.members.length; i++) { const member = group.members[i]; setStatus(`正在枚举 ${member.name}(${i + 1}/${group.members.length})…`); const videoIds = await enumerateListVideos(member.listId); inputs.push({ listId: member.listId, num: member.num, name: member.name, videoIds }); } const plan = planCompaction(group.base, inputs); renderPlan(planElement, plan); setStatus(`计划已生成:需搬运 ${plan.moves.length} 部`, "is-ok"); return plan; } function ReflowPanel() { return (0, import_jsx_runtime.jsxs)("div", { className: "jhs-reflow-panel", children: [ (0, import_jsx_runtime.jsxs)("div", { className: "jhs-reflow-panel__header", children: [(0, import_jsx_runtime.jsx)("span", { className: "jhs-reflow-panel__title", children: "🗂️ 编号清单整理" }), (0, import_jsx_runtime.jsx)("span", { className: "jhs-reflow-panel__status", "data-reflow-status": true, children: "待识别" })] }), (0, import_jsx_runtime.jsx)("div", { className: "jhs-reflow-panel__lists", "data-reflow-lists": true, hidden: true }), (0, import_jsx_runtime.jsx)("div", { className: "jhs-reflow-panel__plan", "data-reflow-plan": true, hidden: true }), (0, import_jsx_runtime.jsxs)("div", { className: "jhs-reflow-panel__progress", "data-reflow-progress": true, hidden: true, children: [(0, import_jsx_runtime.jsx)("div", { className: "jhs-reflow-panel__progress-bar", children: (0, import_jsx_runtime.jsx)("div", { className: "jhs-reflow-panel__progress-fill", "data-reflow-progress-fill": true }) }), (0, import_jsx_runtime.jsx)("span", { className: "jhs-reflow-panel__progress-text", "data-reflow-progress-text": true })] }), (0, import_jsx_runtime.jsxs)("div", { className: "jhs-reflow-panel__actions", children: [ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "jhs-reflow-panel__identify", "data-reflow-identify": true, children: "识别编号组" }), (0, import_jsx_runtime.jsx)("button", { type: "button", className: "jhs-reflow-panel__plan-btn", "data-reflow-plan-btn": true, hidden: true, children: "生成整理计划" }), (0, import_jsx_runtime.jsx)("button", { type: "button", className: "jhs-reflow-panel__exec-btn", "data-reflow-exec-btn": true, hidden: true, children: "执行整理" }), (0, import_jsx_runtime.jsx)("button", { type: "button", className: "jhs-reflow-panel__abort-btn", "data-reflow-abort-btn": true, hidden: true, children: "中止" }), (0, import_jsx_runtime.jsx)("button", { type: "button", className: "jhs-reflow-panel__resume-btn", "data-reflow-resume-btn": true, hidden: true, children: "继续上次整理" }), (0, import_jsx_runtime.jsx)("button", { type: "button", className: "jhs-reflow-panel__undo-btn", "data-reflow-undo-btn": true, hidden: true, children: "撤销本次整理" }) ] }), (0, import_jsx_runtime.jsx)("div", { className: "jhs-reflow-panel__report", "data-reflow-report": true, hidden: true }) ] }); } function toElement(html) { const template = document.createElement("template"); template.innerHTML = html.trim(); return template.content.firstElementChild; } function renderMemberRows(listsElement, group) { listsElement.replaceChildren(); for (const member of group.members) { const row = document.createElement("div"); row.className = "jhs-reflow-list"; row.dataset.listId = member.listId; const num = document.createElement("span"); num.className = "jhs-reflow-list__num"; num.textContent = member.num === 0 ? "★" : String(member.num); num.title = member.num === 0 ? "裸名清单(压实最优先)" : `序号 ${member.num}`; const name = document.createElement("span"); name.className = "jhs-reflow-list__name"; name.textContent = member.name; name.title = member.name; const count = document.createElement("span"); count.className = "jhs-reflow-list__count"; count.textContent = member.count === null ? "—" : String(member.count); row.append(num, name, count); if (member.num === 0) { const flag = document.createElement("span"); flag.className = "jhs-reflow-list__flag is-bare"; flag.textContent = "裸名"; row.append(flag); } if (member.count !== null && member.count >= 501) { const flag = document.createElement("span"); flag.className = "jhs-reflow-list__flag is-full"; flag.textContent = "已满"; row.append(flag); } listsElement.append(row); } } function renderReport(reportElement, result) { reportElement.replaceChildren(); reportElement.hidden = false; const lines = []; lines.push(`✓ 成功:${result.completed}`); if (result.failed > 0) lines.push(`✗ 失败:${result.failed}`); if (result.pendingCleanup.length > 0) lines.push(`⚠ 待清理重复:${result.pendingCleanup.length}(${result.pendingCleanup.join("、")})`); if (result.errors.length > 0) { lines.push(""); lines.push("失败明细:"); for (const err of result.errors.slice(0, 20)) lines.push(` ${err}`); if (result.errors.length > 20) lines.push(` …共 ${result.errors.length} 条`); } const pre = document.createElement("pre"); pre.className = "jhs-reflow-panel__report-text"; pre.textContent = lines.join("\n"); reportElement.append(pre); } function updateProgress(progressElement, fillElement, textElement, done, total, backoffLevel) { progressElement.hidden = false; const pct = total > 0 ? Math.round(done / total * 100) : 0; fillElement.style.width = `${pct}%`; textElement.textContent = `${done}/${total} (${pct}%)${backoffLevel > 0 ? `(退避 ×${Math.pow(2, backoffLevel)})` : ""}`; } function initReflowPanel(config) { const currentListId = getCurrentListId(); if (!currentListId) return; if (document.querySelector(".jhs-reflow-panel")) return; const panel = toElement(jsxToString((0, import_jsx_runtime.jsx)(ReflowPanel, {}))); const anchor = document.querySelector(".movie-list"); if (anchor) anchor.insertAdjacentElement("beforebegin", panel); else (document.querySelector("main") || document.body).prepend(panel); const statusElement = panel.querySelector("[data-reflow-status]"); const listsElement = panel.querySelector("[data-reflow-lists]"); const planElement = panel.querySelector("[data-reflow-plan]"); const progressElement = panel.querySelector("[data-reflow-progress]"); const progressFill = panel.querySelector("[data-reflow-progress-fill]"); const progressText = panel.querySelector("[data-reflow-progress-text]"); const reportElement = panel.querySelector("[data-reflow-report]"); const identifyButton = panel.querySelector("[data-reflow-identify]"); const planButton = panel.querySelector("[data-reflow-plan-btn]"); const execButton = panel.querySelector("[data-reflow-exec-btn]"); const abortButton = panel.querySelector("[data-reflow-abort-btn]"); const resumeButton = panel.querySelector("[data-reflow-resume-btn]"); const undoButton = panel.querySelector("[data-reflow-undo-btn]"); if (!statusElement || !listsElement || !planElement || !progressElement || !progressFill || !progressText || !reportElement || !identifyButton || !planButton || !execButton || !abortButton || !resumeButton || !undoButton) return; const setStatus = (message, stateClass = "") => { statusElement.textContent = message; statusElement.className = "jhs-reflow-panel__status" + (stateClass ? ` ${stateClass}` : ""); }; let group = null; let plan = null; let execSignal = { aborted: false }; let lastSessionId = null; if (hasResumableJournal()) { resumeButton.hidden = false; const journal = loadJournal(); if (journal) setStatus(`检测到上次未完成的整理(${journal.base},已完成 ${journal.completed}/${journal.totalMoves})`); } identifyButton.addEventListener("click", () => { identifyButton.disabled = true; setStatus("正在识别编号组…"); (async () => { try { const allLists = await fetchAllUserLists(); clog.log(`[编号整理] /users/lists 识别到 ${allLists.length} 个清单`); group = discoverGroup(currentListId, allLists, config); if (!group) { clog.log(`[编号整理] 当前清单 ${currentListId} 未归入编号组(名称未匹配序号规则或被排除)`); listsElement.hidden = true; planButton.hidden = true; if (planElement) planElement.hidden = true; setStatus("此清单不属于编号组"); return; } renderMemberRows(listsElement, group); listsElement.hidden = false; planButton.hidden = false; setStatus(`编号组「${group.base}」· ${group.members.length} 个清单`, "is-ok"); } catch (error) { setStatus("识别失败:" + (error instanceof Error ? error.message : String(error)), "is-error"); } finally { identifyButton.disabled = false; } })(); }); planButton.addEventListener("click", () => { if (!group) return; planButton.disabled = true; execButton.hidden = true; runPlanPreview(panel, group, setStatus).then((result) => { plan = result; if (plan && plan.moves.length > 0) execButton.hidden = false; }).catch((error) => { setStatus("生成计划失败:" + (error instanceof Error ? error.message : String(error)), "is-error"); }).finally(() => { planButton.disabled = false; }); }); const finishExecution = (result) => { execButton.disabled = false; execButton.hidden = false; abortButton.hidden = true; resumeButton.hidden = true; if (result) { lastSessionId = result.sessionId; if (result.completed > 0) undoButton.hidden = false; renderReport(reportElement, result); } }; const runExecution = async (thePlan, startIndex) => { execSignal = { aborted: false }; execButton.disabled = true; execButton.hidden = true; abortButton.hidden = false; resumeButton.hidden = true; undoButton.hidden = true; reportElement.hidden = true; progressElement.hidden = false; const settings = await storageManager.getSetting(); const interval = Number(settings.reflowInterval) || 400; const trialCount = Number(settings.reflowTrialCount) || 10; const batchSize = Number(settings.reflowBatchSize) || 20; const maxRetry = Number(settings.reflowMaxRetry) || 3; const nameOf = (id) => thePlan.targets.find((t) => t.listId === id)?.name ?? id; const onProgress = (p) => { updateProgress(progressElement, progressFill, progressText, p.done, p.total, p.backoffLevel); setStatus(`执行中 ${p.done + 1}/${p.total}:${p.videoId}`); }; const trial = await executeCompactionPlan(thePlan, { startIndex, count: startIndex === 0 ? trialCount : Infinity, interval, signal: execSignal, onProgress, nameOf, batchSize, maxRetry, base: thePlan.base }); if (trial.status === "aborted") { setStatus(`已中止(完成 ${trial.completed},失败 ${trial.failed})`, "is-error"); finishExecution(trial); return; } if (trial.status === "suspended") { setStatus(`连续失败暂停(完成 ${trial.completed},失败 ${trial.failed})`, "is-error"); resumeButton.hidden = false; finishExecution(trial); return; } if (trial.status === "done") { setStatus(`完成:成功 ${trial.completed},失败 ${trial.failed}`, trial.failed > 0 ? "is-error" : "is-ok"); finishExecution(trial); return; } utils.q(null, `试运行完成:成功 ${trial.completed},失败 ${trial.failed}。继续剩余 ${thePlan.moves.length - trial.nextIndex} 个?`, () => { (async () => { const rest = await executeCompactionPlan(thePlan, { startIndex: trial.nextIndex, count: Infinity, interval, signal: execSignal, onProgress, nameOf, batchSize, maxRetry, base: thePlan.base, sessionId: trial.sessionId }); const merged = { status: rest.status === "aborted" ? "aborted" : "done", completed: trial.completed + rest.completed, failed: trial.failed + rest.failed, nextIndex: rest.nextIndex, errors: [...trial.errors, ...rest.errors], sessionId: rest.sessionId, pendingCleanup: [...trial.pendingCleanup, ...rest.pendingCleanup] }; setStatus(merged.status === "aborted" ? `已中止(完成 ${merged.completed},失败 ${merged.failed})` : `完成:成功 ${merged.completed},失败 ${merged.failed}`, merged.status === "aborted" || merged.failed > 0 ? "is-error" : "is-ok"); finishExecution(merged); })(); }, () => { setStatus(`试运行后取消(已完成 ${trial.completed})`); finishExecution(trial); }); }; execButton.addEventListener("click", () => { if (!plan || plan.moves.length === 0) return; utils.q(null, `将搬运 ${plan.moves.length} 部影片(先试运行,确认无误后继续)。确认开始?`, () => { runExecution(plan, 0); }); }); abortButton.addEventListener("click", () => { execSignal.aborted = true; setStatus("正在中止…"); }); resumeButton.addEventListener("click", () => { const journal = loadJournal(); if (!journal) { setStatus("无可续传的会话"); return; } if (!plan) { setStatus("请先生成整理计划再续传", "is-error"); return; } utils.q(null, `检测到上次未完成的整理(已完成 ${journal.completed}/${journal.totalMoves}),从第 ${journal.nextIndex + 1} 步继续?`, () => { runExecution(plan, journal.nextIndex); }); }); undoButton.addEventListener("click", () => { if (!lastSessionId) return; utils.q(null, "确定撤销本次整理?将按回滚日志逆序执行反向搬运(同样先加后删 + 限速)。", () => { undoButton.disabled = true; setStatus("正在撤销…"); execSignal = { aborted: false }; abortButton.hidden = false; (async () => { const settings = await storageManager.getSetting(); const interval = Number(settings.reflowInterval) || 400; const nameOf = (id) => plan?.targets.find((t) => t.listId === id)?.name ?? id; const result = await undoSession(lastSessionId, { interval, signal: execSignal, nameOf, onProgress: (p) => { setStatus(`撤销中 ${p.done + 1}/${p.total}:${p.videoId}`); updateProgress(progressElement, progressFill, progressText, p.done, p.total, 0); } }); setStatus(`撤销完成:成功 ${result.undone},失败 ${result.failed}`, result.failed > 0 ? "is-error" : "is-ok"); undoButton.disabled = false; undoButton.hidden = true; abortButton.hidden = true; clearJournal(); })(); }); }); } var list_reflow_default = "/* ── 清单页「编号清单整理」面板(doc/202,期1 只读) ─────────── */\n.jhs-reflow-panel {\n --jhs-reflow-accent: #3273dc;\n --jhs-reflow-border: #dfe4ea;\n --jhs-reflow-muted: #5f6b7a;\n\n display: block;\n margin: 12px 0;\n padding: 10px 12px;\n border: 1px solid var(--jhs-reflow-border);\n border-radius: 10px;\n background: #fff;\n}\n\n.jhs-reflow-panel__header {\n display: flex;\n gap: 8px;\n align-items: center;\n justify-content: space-between;\n}\n\n.jhs-reflow-panel__title {\n color: #2f3540;\n font-size: 14px;\n font-weight: 700;\n}\n\n.jhs-reflow-panel__status {\n padding: 2px 8px;\n border-radius: 50rem;\n background: #edf0f4;\n color: var(--jhs-reflow-muted);\n font-size: 11px;\n font-weight: 700;\n white-space: nowrap;\n}\n\n.jhs-reflow-panel__status.is-error {\n background: #f8e2e2;\n color: #8d2929;\n}\n\n.jhs-reflow-panel__status.is-ok {\n background: #d9e7ff;\n color: #234f96;\n}\n\n.jhs-reflow-panel__lists {\n display: flex;\n flex-direction: column;\n gap: 3px;\n margin-top: 10px;\n}\n\n.jhs-reflow-panel__lists[hidden],\n.jhs-reflow-panel__plan[hidden] {\n display: none !important;\n}\n\n.jhs-reflow-list {\n display: grid;\n grid-template-columns: auto minmax(0, 1fr) auto auto;\n gap: 7px;\n align-items: center;\n padding: 5px 8px;\n border: 1px solid #eef1f5;\n border-radius: 7px;\n background: #fafbfc;\n font-size: 12px;\n}\n\n.jhs-reflow-list__num {\n min-width: 20px;\n padding: 0 5px;\n border-radius: 4px;\n background: #edf0f4;\n color: #4d5967;\n font-size: 10px;\n font-weight: 700;\n text-align: center;\n}\n\n.jhs-reflow-list__name {\n min-width: 0;\n overflow: hidden;\n color: #3d4652;\n font-weight: 600;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.jhs-reflow-list__count {\n padding: 1px 7px;\n border-radius: 50rem;\n background: #edf0f4;\n color: #4d5967;\n font-size: 10px;\n font-variant-numeric: tabular-nums;\n font-weight: 700;\n}\n\n.jhs-reflow-list__flag {\n padding: 1px 6px;\n border-radius: 50rem;\n font-size: 10px;\n font-weight: 700;\n white-space: nowrap;\n}\n\n.jhs-reflow-list__flag.is-full {\n background: #f8e2e2;\n color: #8d2929;\n}\n\n.jhs-reflow-list__flag.is-bare {\n background: #d9e7ff;\n color: #234f96;\n}\n\n.jhs-reflow-panel__actions {\n display: flex;\n gap: 8px;\n margin-top: 10px;\n}\n\n.jhs-reflow-panel__identify,\n.jhs-reflow-panel__plan-btn {\n display: inline-flex;\n align-items: center;\n padding: 5px 12px;\n border: 1px solid var(--jhs-reflow-accent);\n border-radius: 7px;\n background: #fff;\n color: var(--jhs-reflow-accent);\n cursor: pointer;\n font-size: 12px;\n font-weight: 700;\n transition:\n background-color 0.15s ease,\n color 0.15s ease,\n opacity 0.15s ease;\n}\n\n.jhs-reflow-panel__identify:hover,\n.jhs-reflow-panel__plan-btn:hover {\n background: var(--jhs-reflow-accent);\n color: #fff;\n}\n\n.jhs-reflow-panel__identify:disabled,\n.jhs-reflow-panel__plan-btn:disabled {\n cursor: wait;\n opacity: 0.55;\n}\n\n.jhs-reflow-panel__plan-btn[hidden] {\n display: none !important;\n}\n\n.jhs-reflow-panel__plan {\n margin-top: 10px;\n padding: 10px;\n border: 1px dashed #cdd5df;\n border-radius: 8px;\n background: #f8fafc;\n}\n\n.jhs-reflow-plan__summary {\n color: #2f3540;\n font-size: 13px;\n font-weight: 700;\n line-height: 1.5;\n}\n\n.jhs-reflow-plan__targets {\n margin-top: 6px;\n color: #3d4652;\n font-size: 12px;\n line-height: 1.6;\n}\n\n.jhs-reflow-plan__dups,\n.jhs-reflow-plan__overflow {\n margin-top: 6px;\n padding: 6px 8px;\n border-radius: 6px;\n background: #fff3cd;\n color: #725500;\n font-size: 12px;\n line-height: 1.5;\n overflow-wrap: anywhere;\n}\n\n.jhs-reflow-plan__overflow {\n background: #f8e2e2;\n color: #8d2929;\n}\n\n.jhs-reflow-plan__moves {\n display: flex;\n flex-direction: column;\n gap: 2px;\n max-height: 240px;\n margin: 8px 0 0;\n padding: 6px 8px;\n border-radius: 6px;\n overflow-y: auto;\n background: #fff;\n list-style: none;\n font-size: 12px;\n}\n\n.jhs-reflow-plan__move {\n color: #4d5967;\n line-height: 1.5;\n}\n\n.jhs-reflow-plan__move-add {\n color: #1f7a3d;\n font-weight: 700;\n}\n\n.jhs-reflow-plan__move-remove {\n color: #8d2929;\n font-weight: 700;\n}\n\n.jhs-reflow-plan__more {\n color: var(--jhs-reflow-muted);\n font-weight: 700;\n}\n\n.jhs-reflow-plan__note {\n margin: 8px 0 0;\n color: var(--jhs-reflow-muted);\n font-size: 11px;\n}\n\n/* ── 期2b/2c:执行按钮 + 进度条 + 报告区 ─────────── */\n\n.jhs-reflow-panel__exec-btn,\n.jhs-reflow-panel__abort-btn,\n.jhs-reflow-panel__resume-btn,\n.jhs-reflow-panel__undo-btn {\n display: inline-flex;\n align-items: center;\n padding: 5px 12px;\n border: 1px solid var(--jhs-reflow-accent);\n border-radius: 7px;\n background: #fff;\n color: var(--jhs-reflow-accent);\n cursor: pointer;\n font-size: 12px;\n font-weight: 700;\n transition:\n background-color 0.15s ease,\n color 0.15s ease,\n opacity 0.15s ease;\n}\n\n.jhs-reflow-panel__exec-btn:hover,\n.jhs-reflow-panel__resume-btn:hover {\n background: var(--jhs-reflow-accent);\n color: #fff;\n}\n\n.jhs-reflow-panel__abort-btn {\n border-color: #f14668;\n color: #f14668;\n}\n\n.jhs-reflow-panel__abort-btn:hover {\n background: #f14668;\n color: #fff;\n}\n\n.jhs-reflow-panel__undo-btn {\n border-color: #ff9800;\n color: #ff9800;\n}\n\n.jhs-reflow-panel__undo-btn:hover {\n background: #ff9800;\n color: #fff;\n}\n\n.jhs-reflow-panel__exec-btn:disabled,\n.jhs-reflow-panel__abort-btn:disabled,\n.jhs-reflow-panel__resume-btn:disabled,\n.jhs-reflow-panel__undo-btn:disabled {\n cursor: wait;\n opacity: 0.55;\n}\n\n.jhs-reflow-panel__exec-btn[hidden],\n.jhs-reflow-panel__abort-btn[hidden],\n.jhs-reflow-panel__resume-btn[hidden],\n.jhs-reflow-panel__undo-btn[hidden] {\n display: none !important;\n}\n\n.jhs-reflow-panel__progress {\n margin-top: 10px;\n}\n\n.jhs-reflow-panel__progress[hidden] {\n display: none !important;\n}\n\n.jhs-reflow-panel__progress-bar {\n height: 8px;\n border-radius: 4px;\n background: #e2e8f0;\n overflow: hidden;\n}\n\n.jhs-reflow-panel__progress-fill {\n height: 100%;\n width: 0%;\n border-radius: 4px;\n background: #48c774;\n transition: width 0.3s ease;\n}\n\n.jhs-reflow-panel__progress-text {\n display: block;\n margin-top: 4px;\n color: var(--jhs-reflow-muted);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n}\n\n.jhs-reflow-panel__report {\n margin-top: 10px;\n}\n\n.jhs-reflow-panel__report[hidden] {\n display: none !important;\n}\n\n.jhs-reflow-panel__report-text {\n max-height: 200px;\n margin: 0;\n padding: 10px;\n border: 1px solid #e5e7eb;\n border-radius: 6px;\n overflow-y: auto;\n background: #f9fafb;\n color: #374151;\n font-family: inherit;\n font-size: 12px;\n line-height: 1.6;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n"; var ListReflowPlugin = class extends BasePlugin { _reflowConfig = null; getName() { return "ListReflowPlugin"; } get pageTypes() { return ["list"]; } get sites() { return ["javdb"]; } async initCss() { return list_reflow_default; } async handle() { if (!getCurrentListId()) return; const settings = await storageManager.getSetting(); if (settings.listReflowEnabled === "no") return; this._reflowConfig = buildMergeConfig(settings); initReflowPanel(this._reflowConfig); } }; function parsePositionStyle(positionStyle) { if (positionStyle.includes("right")) return { right: 0, top: "5px" }; return { left: 0, top: "5px" }; } function StatusTagHtml({ text, color, dataTip, positionStyle }) { const posStyle = parsePositionStyle(positionStyle); const spanStyle = { marginRight: "5px", borderRadius: "10px", position: "absolute", zIndex: 10, backgroundColor: `${color} !important`, ...posStyle }; return (0, import_jsx_runtime.jsx)("span", { className: "tag is-success status-tag", "data-tip": dataTip, title: "", style: spanStyle, children: text }); } var STATUS_TAG_CONFIG = { IS_FILTERED: { text: BLOCKED_TEXT, color: BLOCK_COLOR, reasonType: "单番号屏蔽", isCounted: true, countKey: "currentPageFilterCount" }, IS_FAVORITE: { text: FAVORITED_TEXT, color: FAVORITE_COLOR, reasonType: "", isCounted: true, countKey: "currentPageFavoriteCount" }, IS_HAS_WATCH: { text: WATCHED_TEXT, color: WATCHED_COLOR, reasonType: "", isCounted: true, countKey: "currentPageHasWatchCount" }, IS_KEYWORD_FILTER: { text: "❌ 关键词屏蔽", color: "#de3333", reasonType: "", isCounted: true, countKey: "currentPageKeywordFilterCount" }, IS_ACTOR_FILTER: { text: "♂️ 男演员屏蔽", color: "#b22222", reasonType: "", isCounted: true, countKey: "currentPageActorFilterCount" }, IS_ACTRESS_FILTER: { text: "♀️ 女演员屏蔽", color: "#cd5c5c", reasonType: "", isCounted: true, countKey: "currentPageActorFilterCount" }, IS_WAIT_CHECK: { text: "", color: "", reasonType: "", isCounted: true, countKey: "currentPageWaitCheckCount" } }; function getStatusTagPositionStyle(tagPosition) { return tagPosition === "rightTop" ? "right: 0; top:5px;" : "left: 0; top:5px;"; } function buildStatusTagHtml(variant, text, color, dataTip, positionStyle) { return jsxToString((0, import_jsx_runtime.jsx)(StatusTagHtml, { variant, text, color, dataTip, positionStyle })); } function injectStatusTag($item, tagHtml) { const tagsEl = $item.find(".tags"); if (tagsEl.length) { tagsEl.append(tagHtml); return; } const itemTagEl = $item.find(".item-tag"); if (itemTagEl.length) { itemTagEl.append(tagHtml); return; } $item.find(".photo-info > span > div").append(tagHtml); } function PageCountTable({ readDataTime, assembleDataTime, processPageTime, totalTime, filterCount, favoriteCount, actorFilterCount, keywordFilterCount, hasWatchCount, waitCheckCount, totalCount }) { const tableStyle = { borderCollapse: "collapse", width: "100%" }; const tdStyle = { padding: "3px", border: "1px solid #ccc" }; const tdBoldStyle = { padding: "3px", border: "1px solid #ccc", fontWeight: "bold" }; return (0, import_jsx_runtime.jsxs)("table", { className: "countTable", style: tableStyle, children: [ (0, import_jsx_runtime.jsxs)("tr", { children: [(0, import_jsx_runtime.jsx)("td", { colSpan: 2, style: tdStyle, children: readDataTime }), (0, import_jsx_runtime.jsx)("td", { colSpan: 2, style: tdStyle, children: assembleDataTime })] }), (0, import_jsx_runtime.jsxs)("tr", { children: [(0, import_jsx_runtime.jsx)("td", { colSpan: 2, style: tdStyle, children: processPageTime }), (0, import_jsx_runtime.jsx)("td", { colSpan: 2, style: tdStyle, children: totalTime })] }), (0, import_jsx_runtime.jsxs)("tr", { children: [ (0, import_jsx_runtime.jsx)("td", { style: tdBoldStyle, children: "项目" }), (0, import_jsx_runtime.jsx)("td", { style: tdBoldStyle, children: "数量" }), (0, import_jsx_runtime.jsx)("td", { style: tdBoldStyle, children: "项目" }), (0, import_jsx_runtime.jsx)("td", { style: tdBoldStyle, children: "数量" }) ] }), (0, import_jsx_runtime.jsxs)("tr", { children: [ (0, import_jsx_runtime.jsx)("td", { style: tdStyle, children: "屏蔽单番号" }), (0, import_jsx_runtime.jsx)("td", { style: tdStyle, children: (0, import_jsx_runtime.jsx)("strong", { children: filterCount }) }), (0, import_jsx_runtime.jsx)("td", { style: tdStyle, children: "收藏" }), (0, import_jsx_runtime.jsx)("td", { style: tdStyle, children: (0, import_jsx_runtime.jsx)("strong", { children: favoriteCount }) }) ] }), (0, import_jsx_runtime.jsxs)("tr", { children: [ (0, import_jsx_runtime.jsx)("td", { style: tdStyle, children: "屏蔽演员" }), (0, import_jsx_runtime.jsx)("td", { style: tdStyle, children: (0, import_jsx_runtime.jsx)("strong", { children: actorFilterCount }) }), (0, import_jsx_runtime.jsx)("td", { style: tdStyle, children: "屏蔽关键词" }), (0, import_jsx_runtime.jsx)("td", { style: tdStyle, children: (0, import_jsx_runtime.jsx)("strong", { children: keywordFilterCount }) }) ] }), (0, import_jsx_runtime.jsxs)("tr", { children: [ (0, import_jsx_runtime.jsx)("td", { style: tdStyle, children: "已观看" }), (0, import_jsx_runtime.jsx)("td", { style: tdStyle, children: (0, import_jsx_runtime.jsx)("strong", { children: hasWatchCount }) }), (0, import_jsx_runtime.jsx)("td", { style: tdStyle, children: "待鉴定" }), (0, import_jsx_runtime.jsx)("td", { style: tdStyle, children: (0, import_jsx_runtime.jsx)("strong", { children: waitCheckCount }) }) ] }), (0, import_jsx_runtime.jsxs)("tr", { children: [(0, import_jsx_runtime.jsx)("td", { style: tdStyle, children: (0, import_jsx_runtime.jsx)("strong", { children: "总数" }) }), (0, import_jsx_runtime.jsx)("td", { style: tdStyle, children: (0, import_jsx_runtime.jsx)("strong", { children: totalCount }) })] }) ] }); } async function renderItemStatusTag(item, carNum) { try { const $item = $(item); const carInfo = await storageManager.getCar(carNum); const status = carInfo ? carInfo.status : ""; $item.find(".status-tag").remove(); let tagConfig = null; if (status === "favorite") tagConfig = STATUS_TAG_CONFIG.IS_FAVORITE; else if (status === "hasWatch") tagConfig = STATUS_TAG_CONFIG.IS_HAS_WATCH; else if (status === "filter") tagConfig = STATUS_TAG_CONFIG.IS_FILTERED; if (!tagConfig || !tagConfig.text) return; const positionStyle = getStatusTagPositionStyle((await storageManager.getSetting()).tagPosition || "rightTop"); injectStatusTag($item, buildStatusTagHtml("render", tagConfig.text, tagConfig.color, tagConfig.reasonType, positionStyle)); } catch (err) { clog.error("[JHS-想看/观看] renderItemStatusTag 失败", err); } } async function filterMovieList(plugin, itemEls) { utils.time("累计耗费时间"); utils.time("读取数据耗时"); const [carList, titleFilterKeywords, blacklist, blacklistCarList, setting] = await Promise.all([ storageManager.getCarList(), storageManager.getTitleFilterKeyword(), storageManager.getBlacklist(), storageManager.getBlacklistCarList(), storageManager.getSetting() ]); const readDataTime = utils.time("读取数据耗时"); const useLower = featureFlags.caseInsensitiveCarNum; const statusCarSets = carList.reduce((acc, car) => { const status = car.status ?? ""; if (Object.prototype.hasOwnProperty.call(acc, status)) acc[status].add(useLower ? car.carNum.toLowerCase() : car.carNum); return acc; }, { [FILTER_ACTION]: new Set(), [FAVORITE_ACTION]: new Set(), [HAS_WATCH_ACTION]: new Set() }); utils.time("组装数据耗时"); const blacklistRoleMap = new Map(blacklist.map((item) => [item.starId, item.role])); const { actorCarNumToNameMap, actressCarNumToNameMap } = blacklistCarList.reduce((acc, item) => { const role = blacklistRoleMap.get(item.starId); if (!role) { clog.error("黑名单数据源丢失演员信息", item); return acc; } const targetMap = role === "actor" ? acc.actorCarNumToNameMap : acc.actressCarNumToNameMap; if (!targetMap.has(item.carNum)) targetMap.set(item.carNum, item.names); return acc; }, { actorCarNumToNameMap: new Map(), actressCarNumToNameMap: new Map() }); const assembleDataTime = utils.time("组装数据耗时"); const showFilterItem = setting?.showFilterItem ?? "no"; const showFilterActorItem = setting?.showFilterActorItem ?? "no"; const showFilterKeywordItem = setting?.showFilterKeywordItem ?? "no"; const showFavoriteItem = setting?.showFavoriteItem ?? "yes"; const showHasWatchItem = setting?.showHasWatchItem ?? "yes"; const showAllItem = setting?.showAllItem ?? "no"; const tagPosition = setting?.tagPosition || "rightTop"; const movieShowType = featureFlags.movieShowTypeVisibility ? setting?.movieShowType || "hide" : "hide"; plugin.currentPageFilterCount = 0; plugin.currentPageFavoriteCount = 0; plugin.currentPageHasWatchCount = 0; plugin.currentPageKeywordFilterCount = 0; plugin.currentPageActorFilterCount = 0; plugin.currentPageWaitCheckCount = 0; plugin.currentPageTotalCount = 0; utils.time("处理页面耗时"); await Promise.all(itemEls.map(async (itemEl) => { const $item = $(itemEl); const { carNum, title } = plugin.findCarNumAndHref($item); const { filter: filterSet, favorite: favoriteSet, hasWatch: hasWatchSet } = statusCarSets; const lookupCarNum = useLower ? carNum.toLowerCase() : carNum; const lowerTitle = useLower ? (title || "").toLowerCase() : title || ""; const isFavorite = favoriteSet.has(lookupCarNum); const isHasWatch = hasWatchSet.has(lookupCarNum); const isFiltered = filterSet.has(lookupCarNum); const isActorBlacklist = actorCarNumToNameMap.has(carNum); const isActressBlacklist = actressCarNumToNameMap.has(carNum); const isBlacklistActor = isActorBlacklist || isActressBlacklist; const matchedKeyword = titleFilterKeywords.find((keyword) => { if (useLower) return lowerTitle.includes(keyword.toLowerCase()) || lookupCarNum.startsWith(keyword.toLowerCase()); return title.includes(keyword) || carNum.startsWith(keyword); }); const hasKeywordMatch = !!matchedKeyword; if (!isSearchOrUserPage) { let shouldHide = showFavoriteItem === "no" && isFavorite || showHasWatchItem === "no" && isHasWatch || showFilterItem === "no" && isFiltered && !isFavorite && !isHasWatch || showFilterActorItem === "no" && isBlacklistActor || showFilterKeywordItem === "no" && hasKeywordMatch; if ($item.attr("data-movieShowType") !== movieShowType) { $item.css("border", ""); $item.children().css("visibility", ""); $item.removeAttr("data-hide"); $item.show(); } const isHidden = $item.attr("data-hide") === "yes"; if (showAllItem === "yes") shouldHide = false; if (shouldHide !== isHidden) { if (shouldHide) $item.attr("data-hide", "yes"); else $item.removeAttr("data-hide"); if (movieShowType === "hide") { if (shouldHide) $item.hide(); else $item.show(); } else if (movieShowType === "visibility") { const borderStyle = shouldHide ? "1px solid rgb(192 176 176)" : "none"; const visibilityValue = shouldHide ? "hidden" : "visible"; $item.css("border", borderStyle); $item.children().css("visibility", visibilityValue); } else throw new Error("movieShowType值有误:" + movieShowType); if ($item.attr("data-movieShowType") !== movieShowType) $item.attr("data-movieShowType", movieShowType); } } let tagConfig = STATUS_TAG_CONFIG.IS_WAIT_CHECK; let reasonText = null; if (isFiltered) tagConfig = STATUS_TAG_CONFIG.IS_FILTERED; else if (isFavorite) tagConfig = STATUS_TAG_CONFIG.IS_FAVORITE; else if (isHasWatch) tagConfig = STATUS_TAG_CONFIG.IS_HAS_WATCH; else if (hasKeywordMatch) { tagConfig = STATUS_TAG_CONFIG.IS_KEYWORD_FILTER; reasonText = matchedKeyword || "未知"; } else if (isActorBlacklist) { tagConfig = STATUS_TAG_CONFIG.IS_ACTOR_FILTER; reasonText = actorCarNumToNameMap.get(carNum) || ""; } else if (isActressBlacklist) { tagConfig = STATUS_TAG_CONFIG.IS_ACTRESS_FILTER; reasonText = actressCarNumToNameMap.get(carNum) || ""; } reasonText ||= tagConfig.reasonType; if (tagConfig.isCounted) plugin[tagConfig.countKey]++; plugin.currentPageTotalCount++; $item.find(".status-tag").remove(); const positionStyle = getStatusTagPositionStyle(tagPosition); if (tagConfig.text) { const tagHtml = buildStatusTagHtml("filter", tagConfig.text, tagConfig.color, reasonText, positionStyle); if (isJavdbSite) $item.find(".tags").append(tagHtml); } await plugin.translate($item); })); const processPageTime = utils.time("处理页面耗时"); const totalTime = utils.time("累计耗费时间"); $("#waitDownBtn span").text(`打开已收藏 (${statusCarSets.favorite.size})`); clog.log(jsxToString((0, import_jsx_runtime.jsx)(PageCountTable, { readDataTime: readDataTime ?? "", assembleDataTime: assembleDataTime ?? "", processPageTime: processPageTime ?? "", totalTime: totalTime ?? "", filterCount: plugin.currentPageFilterCount, favoriteCount: plugin.currentPageFavoriteCount, actorFilterCount: plugin.currentPageActorFilterCount, keywordFilterCount: plugin.currentPageKeywordFilterCount, hasWatchCount: plugin.currentPageHasWatchCount, waitCheckCount: plugin.currentPageWaitCheckCount, totalCount: plugin.currentPageTotalCount }))); } async function translateText(text, sourceLang = "ja", targetLang = "zh-CN") { if (!text) throw new Error("翻译文本不能为空"); const url = "https://translate-pa.googleapis.com/v1/translate?" + new URLSearchParams({ "params.client": "gtx", dataTypes: "TRANSLATION", key: "AIzaSyDLEeFI5OtFBwYBIoK_jj5m32rZK5CkCXA", "query.sourceLanguage": sourceLang, "query.targetLanguage": targetLang, "query.text": text }); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 1e4); try { const response = await fetch(url, { signal: controller.signal }); if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); return (await response.json()).translation; } finally { clearTimeout(timeoutId); } } function replaceTitleTextNodes($videoTitle, newText, excludeCarNum) { $videoTitle.contents().each((_index, node) => { if (node.nodeType === 3 && node.textContent.trim() !== "" && (excludeCarNum === void 0 || !node.textContent.includes(excludeCarNum))) node.textContent = " " + newText + " "; }); } function findCarNumAndHref($item) { let carNum; let title; let publishTime; const linkEl = $item.find("a"); const aHref = linkEl.attr("href"); const videoTitleEl = $item.find(".video-title"); if (videoTitleEl.length > 0) { const strongEl = videoTitleEl.find("strong"); if (strongEl.length > 0) carNum = strongEl.text().trim(); title = linkEl.attr("title") ? linkEl.attr("title").trim() : carNum ? videoTitleEl.text().replace(carNum, "").trim() : videoTitleEl.text().trim(); publishTime = $item.find(".meta").text().trim(); } if (!carNum) { const imgEl = $item.find("img"); if (aHref && imgEl.length > 0) title = imgEl.attr("title")?.trim() || imgEl.attr("data-title")?.trim(); const isDate = (val) => /^\d{4}-\d{1,2}-\d{1,2}$/.test(val); publishTime = $item.find("date").map((_index, el) => $(el).text().trim()).get().find(isDate); carNum = $item.find("date").map((_index, el) => $(el).text().trim()).get().find((val) => !isDate(val)); } if (!carNum) failWithToast("提取番号信息失败"); return { carNum, aHref: aHref || "", url: aHref || "", title: title || "", publishTime: publishTime || "" }; } function replaceHdImg(plugin, imgEls) { if (imgEls && typeof imgEls.jquery == "string") imgEls = imgEls.toArray(); imgEls ||= document.querySelectorAll(plugin.getSelector().coverImgSelector); if (isJavdbSite) imgEls.forEach((img) => { img.src = img.src.replace("thumbs", "covers"); img.title = ""; }); } function showCarNumBox(carNum) { const matchedEl = $(".movie-list .item").toArray().find((itemEl) => $(itemEl).find(".video-title strong").text() === carNum); if (matchedEl) { const $matched = $(matchedEl); if ($matched.attr("data-hide") === `${carNum}-hide`) { $matched.show(); $matched.removeAttr("data-hide"); } } } async function bindClick(plugin) { const selectorConfig = plugin.getSelector(); $(selectorConfig.boxSelector).on("click", ".item .cover", async (event) => { event.preventDefault(); event.stopPropagation(); if ($(event.target).closest("div.meta-buttons").length) return; const $item = $(event.target).closest(".item"); const { carNum, aHref } = plugin.findCarNumAndHref($item); const dialogOpenDetail = await storageManager.getSetting("dialogOpenDetail", "yes"); if (carNum.includes("FC2-")) { const movieId = plugin.parseMovieId(aHref); plugin.getBean("Fc2Plugin")?.openFc2Dialog(movieId, carNum, aHref); } else if (dialogOpenDetail === "yes") utils.openPage(aHref, carNum, true, event); else window.open(aHref, "_blank", "noopener,noreferrer"); }); $(selectorConfig.boxSelector).on("click", ".item video", async (event) => { const videoEl = event.currentTarget; if (videoEl.paused) videoEl.play().catch((err) => clog.error("播放失败:", err)); else videoEl.pause(); event.preventDefault(); event.stopPropagation(); }); $(selectorConfig.boxSelector).on("click", ".item .video-title", async (event) => { if ($(event.target).closest("[class^=\"jhs-match-\"]").length) return; const $item = $(event.currentTarget).closest(".item"); const { carNum, aHref } = plugin.findCarNumAndHref($item); if (carNum.includes("FC2-")) { event.preventDefault(); const movieId = plugin.parseMovieId(aHref); plugin.getBean("Fc2Plugin")?.openFc2Dialog(movieId, carNum, aHref); } }); } async function translate(plugin, $item) { if (await storageManager.getSetting("translateTitle", "yes") !== "yes") return; let sourceText = ""; let carNum = ""; const videoTitleEl = $item.find(".video-title"); if (isJavdbSite) { sourceText = videoTitleEl.contents().filter((_index, node) => node.nodeType === 3 && node.textContent.trim() !== "").text().trim(); carNum = $item.find(".video-title strong").text().trim(); } if (plugin.cache[carNum]) { replaceTitleTextNodes(videoTitleEl, plugin.cache[carNum]); videoTitleEl.attr("title", plugin.cache[carNum]); return; } translateText(sourceText).then((translation) => { if (isJavdbSite) { replaceTitleTextNodes(videoTitleEl, translation, carNum); videoTitleEl.attr("title", translation); } plugin.writeQueue = plugin.writeQueue.then(() => { plugin.cache[carNum] = translation; localStorage.setItem("jhs_translate", JSON.stringify(plugin.cache)); }); }).catch((err) => { clog.error("翻译失败:", err); }); } async function revertTranslation(plugin) { $(plugin.getSelector().itemSelector).toArray().forEach((itemEl) => { const $item = $(itemEl); const originalTitle = $item.find(".box").attr("title") || $item.find(".video-title").attr("title") || $item.find("img").attr("data-title"); const carNum = $item.find(".video-title strong").text().trim(); const videoTitleEl = $item.find(".video-title"); replaceTitleTextNodes(videoTitleEl, originalTitle, carNum); videoTitleEl.removeAttr("title"); }); } function JumpPageControl({ controlId, value }) { return (0, import_jsx_runtime.jsxs)("li", { id: controlId, children: [(0, import_jsx_runtime.jsx)("input", { type: "number", id: "jumpPageInput", placeholder: "页码", min: "1", style: { width: "60px", marginLeft: "10px", padding: "10px", border: "1px solid #ccc", fontSize: "14px" }, value }), (0, import_jsx_runtime.jsx)("button", { style: { marginLeft: "5px", padding: "9px 8px", cursor: "pointer", border: "1px solid #ccc", backgroundColor: "#f0f0f0", fontSize: "14px" }, children: "跳转" })] }); } function addJumpPageControl() { const controlId = "gemini-jump-page-control"; if ($("#gemini-jump-page-control").length > 0) return; if ($(".pagination-link.is-current").length === 0) return; const currentPage = utils.getUrlParam(currentHref, "page") || 1; const $li = $(jsxToString((0, import_jsx_runtime.jsx)(JumpPageControl, { controlId, value: Number(currentPage) + 1 }))); $(".pagination-list").append($li); const $pageInput = $li.find("#jumpPageInput"); const $jumpBtn = $li.find("button"); const jumpToPage = () => { const pageNum = parseInt(String($pageInput.val()), 10); if (isNaN(pageNum) || pageNum < 1) { $pageInput.focus(); return; } const url = new URL(window.location.href); url.searchParams.set("page", pageNum.toString()); window.location.href = url.toString(); }; $jumpBtn.on("click", jumpToPage); $pageInput.on("keypress", (event) => { if (event.which === 13) { jumpToPage(); event.preventDefault(); } }); } var ListPagePlugin = class extends BasePlugin { currentPageFilterCount = 0; currentPageFavoriteCount = 0; currentPageHasWatchCount = 0; currentPageKeywordFilterCount = 0; currentPageActorFilterCount = 0; currentPageWaitCheckCount = 0; currentPageTotalCount = 0; supervisor = new TaskSupervisor(); cache = (() => { try { const raw = localStorage.getItem("jhs_translate"); return raw ? JSON.parse(raw) : {}; } catch { return {}; } })(); writeQueue = Promise.resolve(); getName() { return "ListPagePlugin"; } destroy() { this.supervisor.abort(); } get pageTypes() { return ["list"]; } async handle() { new BroadcastChannel("channel-refresh").addEventListener("message", async (event) => { const msgType = event.data.type; if (msgType === "refresh") { storageManager.clearCarListCache(); await this.doFilter(); const historyPlugin = this.getBean("HistoryPlugin"); if (historyPlugin.tableObj) historyPlugin.tableObj.setData(); const newVideoPlugin = this.getBean("NewVideoPlugin"); if (newVideoPlugin) { newVideoPlugin.showNewVideoCount().then(); newVideoPlugin.loadData(); } } else if (msgType === "cleanCache_filter_actor_actress_car_list") storageManager.clearFilterActorActressCarListCache(); else if (msgType === "clean_cacheSettingObj") storageManager.clearSettingCache(); }); this.replaceHdImg(); this.addJumpPageControl(); await this.doFilter(); this.bindClick().then(); this.rememberTagExpand(); $(this.getSelector().itemSelector + " a").attr("target", "_blank"); this.checkDom(); this.getBean("DetailPageButtonPlugin").setupWantWatchedSyncListener(); } rememberTagExpand() { if (!window.location.href.includes("actors")) return; const expandBtn = $(".tag-expand"); if (expandBtn.length === 0) return; const storageKey = "jhs_tag_expand"; const wasExpanded = localStorage.getItem(storageKey) === "true"; const contentEl = $(".actor-tags .content"); if (wasExpanded && contentEl.hasClass("collapse")) expandBtn[0].click(); expandBtn.on("click", function() { const isExpanded = !contentEl.hasClass("collapse"); localStorage.setItem(storageKey, isExpanded.toString()); }); } checkDom() { if (!window.isListPage) return; const selectorConfig = this.getSelector(); const containerEl = document.querySelector(selectorConfig.boxSelector); if (!containerEl) { clog.error("没有找到容器节点!"); return; } const observerOptions = { childList: true, subtree: false }; const observer = this.supervisor.observe(containerEl, async () => { observer.disconnect(); try { this.replaceHdImg(); this.addJumpPageControl(); await this.doFilter(); await this.getBean("ListPageButtonPlugin").sortItems(); $(this.getSelector().itemSelector + " a").attr("target", "_blank"); this.getBean("AutoPagePlugin").checkLoad(); } finally { observer.observe(containerEl, observerOptions); } }, observerOptions); } async doFilter() { if (!window.isListPage) return; const itemEls = $(this.getSelector().itemSelector).toArray(); if (itemEls.length) await this.filterMovieList(itemEls); } async parseActressName(url) { let actressName = null; if (await storageManager.getSetting("enableSaveActressCarInfo", "no") === "yes") { clog.debug("鉴定补录演员信息-已启用, 开始解析详情页"); clog.debug("开始解析演员详情页", url); const html = await gmHttp.get(url); const $dom = utils.htmlTo$dom(String(html)); if (isJavdbSite) actressName = $dom.find(".female").prev().map((_index, el) => $(el).text()).get().join(" "); clog.debug("解析到名称:", actressName); } return actressName; } async renderItemStatusTag(item, carNum) { return renderItemStatusTag(item, carNum); } async filterMovieList(itemEls) { return filterMovieList(this, itemEls); } async translate($item) { return translate(this, $item); } async revertTranslation() { return revertTranslation(this); } findCarNumAndHref($item) { return findCarNumAndHref($item); } replaceHdImg(imgEls) { return replaceHdImg(this, imgEls); } showCarNumBox(carNum) { return showCarNumBox(carNum); } async bindClick() { return bindClick(this); } addJumpPageControl() { return addJumpPageControl(); } }; var DMM_VIDEO_CACHE_KEY = "jhs_dmm_video"; var DMM_OTHER_SITE_KEY = "jhs_other_site_dmm"; var DmmPreviewVideoResolver = class { carNum; showErrorMessages; constructor(carNum, showErrorMessages = true) { this.carNum = carNum; this.showErrorMessages = showErrorMessages; } _checkCache() { const rawCache = localStorage.getItem(DMM_VIDEO_CACHE_KEY); const cache = rawCache ? JSON.parse(rawCache) : {}; if (cache[this.carNum]) { clog.debug("缓存中存在预览视频信息", cache[this.carNum]); return cache[this.carNum]; } return null; } _updateCache(videoMap) { const rawCache = localStorage.getItem(DMM_VIDEO_CACHE_KEY); const cache = rawCache ? JSON.parse(rawCache) : {}; cache[this.carNum] = videoMap; clog.debug("成功解析出预览视频并已缓存:", videoMap); localStorage.setItem(DMM_VIDEO_CACHE_KEY, JSON.stringify(cache)); } async _searchContentIds() { const carNum = this.carNum; const noHyphen = carNum.replace(/-/g, ""); const candidates = [ { keyword: carNum.replace("-", "00"), name: "00-替换关键词" }, { keyword: carNum, name: "原始番号关键词" }, { keyword: noHyphen, name: "无连字符关键词" } ]; const carNumLower = carNum.toLowerCase(); for (const candidate of candidates) { const { keyword, name } = candidate; const keywordLower = keyword.toLowerCase(); clog.debug(`--- 尝试使用 ${name} (${keyword}) 进行 API 搜索 ---`); const apiUrl = `https://api.dmm.com/affiliate/v3/ItemList?${new URLSearchParams({ api_id: "UrwskPfkqQ0DuVry2gYL", affiliate_id: "10278-996", output: "json", site: "FANZA", sort: "match", keyword }).toString()}`; let response; try { response = await gmHttp.get(apiUrl); } catch (err) { clog.error(`API 请求失败,跳过 ${name}:`, err); continue; } if (!response || !response.result || !response.result.result_count) { clog.debug("API 返回无结果,尝试下一个关键词。"); continue; } const matched = []; for (const item of response.result.items) { if (matched.length >= 2) break; const contentId = item.content_id || ""; const makerProduct = item.maker_product || ""; if (contentId.includes(keywordLower.replace("-", "")) || carNumLower === makerProduct.toLowerCase() || contentId.includes(noHyphen.toLowerCase())) { matched.push({ serviceCode: item.service_code || "", floorCode: item.floor_code || "", contentId, pageUrl: item.URL || "" }); clog.debug(`[${name}] cid|makerProduct 匹配成功:`, contentId, makerProduct); } } if (matched.length > 0) { clog.debug(`--- 成功通过 ${name} 找到 Content IDs ---`); const $fanzaBtn = $("#fanzaBtn"); let siteUrl = `https://www.dmm.co.jp/search/=/searchstr=${keyword}`; let linkType = "single"; if (matched.length > 1) { $fanzaBtn.attr("href", siteUrl); $fanzaBtn.append(jsxToString((0, import_jsx_runtime.jsx)(SiteResultTag, {}))); $fanzaBtn.css("backgroundColor", "#7bc73b"); linkType = "multiple"; } else { siteUrl = matched[0].pageUrl; $fanzaBtn.attr("href", siteUrl); $fanzaBtn.css("backgroundColor", "#7bc73b"); } const rawOther = localStorage.getItem(DMM_OTHER_SITE_KEY); const otherSiteCache = rawOther ? JSON.parse(rawOther) : {}; otherSiteCache[this.carNum] = { type: linkType, url: siteUrl }; localStorage.setItem(DMM_OTHER_SITE_KEY, JSON.stringify(otherSiteCache)); return matched; } clog.debug(`[${name}] API 返回结果数 ${response.result.result_count},但无精确匹配的 Content ID。`); } clog.warn("所有关键词尝试均未找到匹配的Content ID, 解析Dmm视频失败"); const $fanzaBtn = $("#fanzaBtn"); $fanzaBtn.attr("href", `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}`); $fanzaBtn.attr("title", "未查询到, 点击前往搜索页"); $fanzaBtn.css("backgroundColor", "#de3333"); return null; } async _extractTrailerLinks({ contentId, serviceCode, floorCode }) { const playerUrl = `https://www.dmm.co.jp/service/digitalapi/-/html5_player/=/cid=${contentId}/mtype=AhRVShI_/service=${serviceCode}/floor=${floorCode}/mode=/`; const html = await gmHttp.get(playerUrl, void 0, { "accept-language": "ja-JP,ja;q=0.9", Cookie: "age_check_done=1" }); if (typeof html != "string") { clog.error(html); throw new Error("解析播放页内容失败, 非文本内容"); } if (html.includes("このサービスはお住まいの地域からは")) throw new Error("节点不可用,请将DMM域名分流到日本ip"); const argsMatch = html.match(/const\s+args\s+=\s+(.*);/); if (!argsMatch) throw new Error("未在脚本中找到 const args = ... 变量"); let bitrates; try { ({bitrates} = JSON.parse(argsMatch[1])); } catch (err) { throw new Error(`解析播放器脚本 JSON 失败: ${err.message}`); } const videoMap = {}; const qualityPattern = VIDEO_QUALITY_LIST.map((opt) => opt.quality).join("|"); const qualityRegex = new RegExp(`(${qualityPattern})\\.mp4$`); if (!Array.isArray(bitrates)) { clog.error("解析画质链接失败: bitrates 字段不是一个数组或不存在"); throw new Error("解析画质链接失败: bitrates 字段不是一个数组或不存在"); } clog.debug("原始数据返回:", bitrates); for (const item of bitrates) { const src = item == null ? void 0 : item.src; if (!src || typeof src != "string" || !src.endsWith(".mp4")) continue; const qualityMatch = src.match(qualityRegex); let quality = ""; if (qualityMatch && qualityMatch[1]) quality = qualityMatch[1]; if (quality && !videoMap[quality]) videoMap[quality] = src; } if (Object.keys(videoMap).length === 0) throw new Error("未找到匹配要求的预览画质视频"); return videoMap; } async fetchVideo() { const cached = this._checkCache(); if (cached) return cached; let contentIds; try { const carNumLower = this.carNum.toLowerCase(); if (carNumLower.startsWith("heyzo") || /^(n\d+|\d+(-\d+)*)$/.test(carNumLower) || /^n\d+$/.test(carNumLower)) throw new Error("无码番号类型, 取消dmm解析"); if (this.carNum.includes("VR-")) throw new Error("VR类型, 取消dmm解析"); contentIds = await this._searchContentIds(); } catch (err) { clog.error("DMM API 搜索失败:", err); const $fanzaBtn = $("#fanzaBtn"); $fanzaBtn.attr("href", `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}`); $fanzaBtn.attr("title", "未查询到, 点击前往搜索页"); $fanzaBtn.css("backgroundColor", "#de3333"); return null; } if (!contentIds || contentIds.length === 0) return null; try { const videoMap = await Promise.any(contentIds.map((id) => this._extractTrailerLinks(id))); this._updateCache(videoMap); return videoMap; } catch (err) { const errors = err && typeof err === "object" && "errors" in err && Array.isArray(err.errors) ? err.errors : [err]; if (errors.some((e) => e instanceof Error && e.message.includes("节点不可用"))) { if (this.showErrorMessages) show.error("节点不可用,请将DMM域名分流到日本ip"); } else { const first = errors[0]; const firstMsg = first instanceof Error && first.message ? first.message : first; clog.error(`解析失败: ${firstMsg}`, errors); if (this.showErrorMessages) show.error(`解析失败: ${firstMsg}`); } const $fanzaBtn = $("#fanzaBtn"); $fanzaBtn.attr("href", `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}`); $fanzaBtn.attr("title", "未查询到, 点击前往搜索页"); $fanzaBtn.css("backgroundColor", "#de3333"); return null; } } }; var fetchDmmPreviewVideo = async (carNum, showErrorMessages = true) => new DmmPreviewVideoResolver(carNum, showErrorMessages).fetchVideo(); var selectAvailableVideoQuality = (available, desired) => { if (!available || available.length === 0) return null; const availableSet = new Set(available); if (availableSet.has(desired)) return desired; const descending = VIDEO_QUALITY_LIST.map((opt) => opt.quality).reverse(); for (const quality of descending) if (availableSet.has(quality)) return quality; return available[0]; }; function PreviewVideoActionBtn({ id, color, label }) { return (0, import_jsx_runtime.jsx)("button", { className: "jhs-toolbar-menu-btn", id, style: { minWidth: "120px", backgroundColor: color }, children: label }); } function PreviewVideoQualityBtn({ opt, src, isActive }) { return (0, import_jsx_runtime.jsx)("button", { className: `video-control-btn${isActive ? " active" : ""}`, id: opt.id, "data-quality": opt.quality, "data-video-src": src, style: { minWidth: "40px", border: "1px solid #ccc", backgroundColor: isActive ? "#007bff" : "#fff", color: isActive ? "white" : "black" }, children: opt.text }); } var VIDEO_MUTED_KEY = "jhs_videoMuted"; async function buildVideoToolbar(plugin) { const $video = $("#preview-video"); if (!$video.length) return; const $videoWrap = $video.parent(); $videoWrap.css("position", "relative"); const videoEl = $video[0]; const mutedStored = localStorage.getItem(VIDEO_MUTED_KEY); if (mutedStored) videoEl.muted = mutedStored === "yes"; videoEl.addEventListener("volumechange", () => { localStorage.setItem(VIDEO_MUTED_KEY, videoEl.muted ? "yes" : "no"); }); videoEl.play(); const carNum = plugin.getPageInfo().carNum; const videoMap = await fetchDmmPreviewVideo(carNum); const $toolbar = $("<div></div>").attr("id", "video-bottom-toolbar").css({ display: "flex", gap: "5px", "align-items": "center", "flex-wrap": "wrap" }); const $qualityGroup = $("<div></div>").css({ display: "flex", gap: "5px", "align-items": "center" }); let currentQuality = null; if (videoMap) { const desiredQuality = await storageManager.getSetting("videoQuality"); currentQuality = selectAvailableVideoQuality(Object.keys(videoMap), desiredQuality); const currentUrl = videoMap[currentQuality]; if ($video.attr("src") !== currentUrl) { $video.attr("src", currentUrl); videoEl.load(); videoEl.play(); } VIDEO_QUALITY_LIST.forEach((opt) => { const src = videoMap[opt.quality]; if (src) { const isActive = currentQuality === opt.quality; const $btn = $(jsxToString((0, import_jsx_runtime.jsx)(PreviewVideoQualityBtn, { opt, src, isActive }))); $qualityGroup.append($btn); } }); } $toolbar.append($qualityGroup); const $actionGroup = $("<div></div>").css({ display: "flex", gap: "5px", "align-items": "center", "margin-left": "auto" }); const $filterBtn = $(jsxToString((0, import_jsx_runtime.jsx)(PreviewVideoActionBtn, { id: "video-filterBtn", color: "#de3333", label: "屏蔽" }))); $actionGroup.append($filterBtn); const $favoriteBtn = $(jsxToString((0, import_jsx_runtime.jsx)(PreviewVideoActionBtn, { id: "video-favoriteBtn", color: "#25b1dc", label: "收藏" }))); $actionGroup.append($favoriteBtn); const $speedBtn = $(jsxToString((0, import_jsx_runtime.jsx)(PreviewVideoActionBtn, { id: "speed-btn", color: "#76b45d", label: "快进" }))); $actionGroup.append($speedBtn); $toolbar.append($actionGroup); $videoWrap.append($toolbar); $toolbar.on("click", ".video-control-btn", async (event) => { const $btn = $(event.currentTarget); const src = $btn.data("video-src"); if (!$btn.hasClass("active")) try { const currentTime = videoEl.currentTime; $video.attr("src", src); videoEl.load(); videoEl.currentTime = currentTime; await videoEl.play(); $toolbar.find(".video-control-btn").removeClass("active").css({ "background-color": "#fff", color: "black" }); $btn.addClass("active").css({ "background-color": "#007bff", color: "white" }); } catch (err) { console.error("切换画质失败:", err); } }); $("#speed-btn").on("click", () => { plugin.getBean("DetailPageButtonPlugin").speedVideo(); }); utils.rightClick(document.body, "#speed-btn", (event) => { plugin.getBean("DetailPageButtonPlugin").filterOne(event); }); $("#video-filterBtn").on("click", (event) => { plugin.getBean("DetailPageButtonPlugin").filterOne(event); }); $("#video-favoriteBtn").on("click", () => { plugin.getBean("DetailPageButtonPlugin").favoriteOne(); }); } function PreviewVideoContainer({ coverSrc }) { return (0, import_jsx_runtime.jsxs)("a", { className: "preview-video-container", "data-fancybox": "gallery", href: "#preview-video", children: [(0, import_jsx_runtime.jsx)("span", { children: "預告片" }), (0, import_jsx_runtime.jsx)("img", { src: coverSrc, className: "video-cover", style: { width: "150px", height: "auto" }, alt: "" })] }); } var preview_video_plugin_default = ".video-control-btn {\n min-width: 120px;\n padding: 7px 12px;\n font-size: 12px;\n background: rgb(0, 0, 0, 0.7);\n color: white;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n}\n\n.video-control-btn.active {\n background-color: #1890ff;\n color: white;\n font-weight: bold;\n border: 2px solid #096dd9;\n}\n"; var PreviewVideoPlugin = class extends BasePlugin { getName() { return "PreviewVideoPlugin"; } async initCss() { return preview_video_plugin_default; } async handle() { if (!window.isDetailPage) return; const $container = $(".preview-video-container"); $container.on("click", () => { utils.loopDetector(() => $(".fancybox-content #preview-video").length > 0, () => { this.handleVideo().then(); }); }); if (!currentHref.includes("autoPlay=1")) this.initDmm().then(); const href = window.location.href; if (href.includes("gallery-1") || href.includes("gallery-2")) utils.loopDetector(() => $(".fancybox-content #preview-video").length > 0, () => { if ($(".fancybox-content #preview-video").length > 0) this.handleVideo().then(); }); if (href.includes("autoPlay=1") && $container.length > 0) $container[0].click(); } async initDmm() { try { const videoMap = await fetchDmmPreviewVideo(this.getPageInfo().carNum, false); if (!videoMap) return; const desiredQuality = await storageManager.getSetting("videoQuality"); clog.debug("解析其它画质预览视频", "设置-期望画质", desiredQuality); const videoUrl = videoMap[selectAvailableVideoQuality(Object.keys(videoMap), desiredQuality)]; clog.log("切换其它画质预览视频: ", videoUrl); const $video = $("#preview-video"); const videoEl = $video.length ? $video[0] : null; const isHidden = !videoEl || utils.isHidden($video); if ($video.length) { if (videoEl) { const currentTime = videoEl.currentTime; $video.attr("src", videoUrl); if (!isHidden) { clog.debug("播放器已手动打开, 变更进度条"); videoEl.currentTime = currentTime; videoEl.play(); } } } else { clog.debug("JavDB没有视频播放元素, 开始创建..."); const coverSrc = $(".column-video-cover img").attr("src"); $(".preview-images").prepend(jsxToString((0, import_jsx_runtime.jsx)(PreviewVideoContainer, { coverSrc: coverSrc ?? "" }))); $(".preview-video-container").on("click", () => { utils.loopDetector(() => $(".fancybox-content #preview-video").length > 0, async () => { await this.handleVideo(); }); }); } } catch (err) { clog.error("预加载dmm失败:", err); } } async handleVideo() { await buildVideoToolbar(this); } }; var RatingRenderer = { hideNativeBadge(cover) { cover.querySelectorAll(":scope > span").forEach((s) => { if (s.classList.length === 0 || s.className === "score") s.style.display = "none"; }); }, showPlaceholder(item) { const cover = item.querySelector(".cover"); if (!cover) return; if (cover.querySelector(".jhs-user-rating")) return; this.hideNativeBadge(cover); const el = document.createElement("div"); el.className = "jhs-user-rating is-placeholder"; el.textContent = "已看"; el.title = "已看(悬停加载评分)"; cover.appendChild(el); }, showRating(item, rating) { const cover = item.querySelector(".cover"); if (!cover) return; let el = cover.querySelector(".jhs-user-rating"); if (!el) { this.hideNativeBadge(cover); el = document.createElement("div"); el.className = "jhs-user-rating"; cover.appendChild(el); } el.className = "jhs-user-rating"; el.innerHTML = ""; if (rating === 0) { el.classList.add("is-rated"); el.innerHTML = jsxToString((0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [(0, import_jsx_runtime.jsx)("span", { className: "jhs-rd-icon", children: "★" }), (0, import_jsx_runtime.jsx)("span", { className: "jhs-rd-num", children: "0" })] })); el.title = "已看 · 0/5 星(未评分)"; } else if (rating && rating >= 1 && rating <= 5) { el.classList.add("is-rated"); el.innerHTML = jsxToString((0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [(0, import_jsx_runtime.jsx)("span", { className: "jhs-rd-icon", children: "★" }), (0, import_jsx_runtime.jsx)("span", { className: "jhs-rd-num", children: rating })] })); el.title = `已看 · ${rating}/5 星`; } else { el.classList.add("is-placeholder"); el.textContent = "已看"; el.title = "已看(未评分)"; } }, removeFrom(item) { const el = item?.querySelector(".jhs-user-rating"); if (el) el.remove(); const cover = item?.querySelector(".cover"); if (cover) cover.querySelectorAll(":scope > span").forEach((s) => { if (s.style.display === "none" && (s.classList.length === 0 || s.className === "score")) s.style.display = ""; }); } }; var RATING_CONFIG = { DEBUG_MODE: false, IDB_NAME: "JAV-JHS", IDB_STORE: "appData", CAR_LIST_KEY: "car_list", RATING_CACHE_KEY: "jdb:rating_cache_v2", IDB_RATING_KEY: "jhsRatingDisplay_data", ITEM_SELECTOR: ".movie-list .item, .is-user-page .column.is-one-quarter", HOVER_DELAY: 500, FETCH_CONCURRENCY: 3, FETCH_TIMEOUT: 8e3, FETCH_RETRY: 1 }; var parser = new DOMParser(); function log(action, ...args) { if (RATING_CONFIG.DEBUG_MODE) console.log(`%c[JHS-Rating][${action}]`, "color:#fff;background:#ffc107;padding:2px 4px;border-radius:2px;", ...args); } function debounce(fn, wait) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), wait); }; } function createLimiter(max) { let active = 0; const queue = []; const runNext = () => { if (active >= max || queue.length === 0) return; active++; const job = queue.shift(); Promise.resolve().then(job.fn).then(job.resolve).catch(job.reject).finally(() => { active--; runNext(); }); }; return { run(fn) { return new Promise((resolve, reject) => { queue.push({ fn, resolve, reject }); runNext(); }); } }; } function getSafeUrl(url, base = window.location.origin) { if (!url) return ""; try { if (url.startsWith("http")) return url; if (url.startsWith("//")) return "https:" + url; return new URL(url, base).href; } catch { return url; } } function normalizeCode(raw) { if (!raw) return ""; return raw.trim().toUpperCase().replace(/\s+/g, "-"); } function getCode(item) { if (item.dataset.code) return item.dataset.code; const code = normalizeCode(item.querySelector(".video-title strong")?.textContent || ""); if (code) item.dataset.code = code; return code; } function getAnchor(item) { return item?.querySelector("a.box, .box a") ?? null; } var RatingUtils = { parser, log, debounce, createLimiter, getSafeUrl, normalizeCode, getCode, getAnchor }; function openIdb() { return new Promise((resolve, reject) => { const req = indexedDB.open(RATING_CONFIG.IDB_NAME); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function buildWatchedMap() { const list = await storageManager.getCarList(); const map = new Map(); list.forEach((item) => { if (item && item.status === "hasWatch" && item.carNum) map.set(RatingUtils.normalizeCode(item.carNum), item); }); RatingUtils.log("WATCHED_MAP", `${map.size} 项`); return map; } var RatingCache = { _data: {}, async load() { try { const raw = localStorage.getItem(RATING_CONFIG.RATING_CACHE_KEY); if (raw) { const parsed = JSON.parse(raw); if (typeof parsed === "object" && parsed !== null) { this._data = parsed; RatingUtils.log("CACHE", `localStorage: ${Object.keys(parsed).length} 条`); return; } } } catch {} try { const db = await openIdb(); const idbData = await new Promise((resolve) => { try { const req = db.transaction(RATING_CONFIG.IDB_STORE, "readonly").objectStore(RATING_CONFIG.IDB_STORE).get(RATING_CONFIG.IDB_RATING_KEY); req.onsuccess = () => resolve(req.result ?? null); req.onerror = () => resolve(null); } catch { resolve(null); } }); if (idbData && typeof idbData === "object") { this._data = idbData; this.saveToLS(); RatingUtils.log("CACHE", `IndexedDB 恢复: ${Object.keys(idbData).length} 条`); return; } } catch (e) { RatingUtils.log("CACHE_IDB_LOAD_ERROR", e); } this._data = {}; }, saveToLS() { try { localStorage.setItem(RATING_CONFIG.RATING_CACHE_KEY, JSON.stringify(this._data)); } catch (e) { RatingUtils.log("CACHE_SAVE_ERROR", e); } }, save() { this.saveToLS(); this._syncToIdb(); }, async _syncToIdb() { try { (await openIdb()).transaction(RATING_CONFIG.IDB_STORE, "readwrite").objectStore(RATING_CONFIG.IDB_STORE).put(this._data, RATING_CONFIG.IDB_RATING_KEY); } catch (e) { RatingUtils.log("IDB_SYNC_ERROR", e); } }, get(code) { return this._data[code] || null; }, set(code, rating) { const existed = this._data[code]; if (existed && existed.rating === rating) return false; this._data[code] = { rating, updatedAt: Date.now() }; this.save(); return true; }, clear() { this._data = {}; localStorage.removeItem(RATING_CONFIG.RATING_CACHE_KEY); openIdb().then((db) => { try { db.transaction(RATING_CONFIG.IDB_STORE, "readwrite").objectStore(RATING_CONFIG.IDB_STORE).delete(RATING_CONFIG.IDB_RATING_KEY); } catch {} }).catch(() => {}); }, size() { return Object.keys(this._data).length; } }; var limiter = RatingUtils.createLimiter(RATING_CONFIG.FETCH_CONCURRENCY); async function request(url, retries = RATING_CONFIG.FETCH_RETRY) { for (let i = 0; i <= retries; i++) try { return await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url, timeout: RATING_CONFIG.FETCH_TIMEOUT, headers: { "X-Requested-With": "XMLHttpRequest" }, onload: resolve, onerror: reject, ontimeout: reject, onabort: reject }); }); } catch (err) { if (i === retries) throw err; await new Promise((resolve) => setTimeout(resolve, 400 * (i + 1))); } throw new Error("unreachable"); } function parseRating(doc) { if (!doc.querySelector(".review-title .tag.is-success.is-light")) return null; const chk = doc.querySelector("input[name=\"video_review[score]\"][checked], .rating-star input:checked"); if (!chk) return null; const ratingValue = parseInt(chk.value, 10); return ratingValue >= 1 && ratingValue <= 5 ? ratingValue : null; } async function fetchRating(item) { if (item.dataset.jhsrdFetching === "true" || item.dataset.jhsrdLoaded === "true") return; const anchor = RatingUtils.getAnchor(item); if (!anchor) return; const code = RatingUtils.getCode(item); if (!code) return; item.dataset.jhsrdFetching = "true"; try { const url = RatingUtils.getSafeUrl(anchor.href); const res = await limiter.run(() => request(url)); if (res.status !== 200) { item.dataset.jhsrdLoaded = "true"; return; } const rating = parseRating(RatingUtils.parser.parseFromString(res.responseText, "text/html")); if (rating !== null) { RatingCache.set(code, rating); RatingRenderer.showRating(item, rating); } item.dataset.jhsrdLoaded = "true"; } catch (err) { RatingUtils.log("FETCH_ERROR", code, err); } finally { delete item.dataset.jhsrdFetching; } } var rating_display_default = ".jhs-user-rating {\n position: absolute;\n bottom: 6px;\n right: 6px;\n z-index: 5;\n pointer-events: auto;\n font-size: 13px;\n font-weight: 700;\n line-height: 1;\n padding: 5px 10px;\n border-radius: 12px;\n box-shadow: 0 2px 8px rgb(0, 0, 0, 0.35), 0 0 0 1px rgb(255,255,255,0.1) inset;\n text-shadow: 0 1px 2px rgb(0, 0, 0, 0.4);\n color: #fff;\n white-space: nowrap;\n opacity: 0.95;\n transition: transform 0.25s ease, opacity 0.2s ease, background 0.3s ease;\n display: inline-flex;\n align-items: center;\n gap: 3px;\n letter-spacing: 0.5px;\n}\n\n/* 占位(绿色) */\n.jhs-user-rating.is-placeholder {\n background: linear-gradient(145deg, #48c78e, #2fb972);\n}\n\n/* 已评分(金色) */\n.jhs-user-rating.is-rated {\n background: linear-gradient(145deg, #ffc107, #f57c00);\n}\n\n.jhs-rd-icon {\n font-size: 14px;\n filter: drop-shadow(0 1px 1px rgb(0,0,0,0.3));\n}\n\n.jhs-rd-num {\n font-size: 14px;\n font-weight: 900;\n}\n\n.jhs-user-rating:hover {\n transform: scale(1.1);\n opacity: 1;\n}\n\n/* 加载中 */\n.jhs-user-rating.is-loading {\n animation: jhs-rd-pulse 1s infinite;\n pointer-events: none;\n}\n\n@keyframes jhs-rd-pulse {\n 0%, 100% { opacity: 0.55; }\n 50% { opacity: 1; }\n}\n\n/* Toast */\n.jhs-rd-toast {\n position: fixed;\n left: 50%;\n transform: translateX(-50%);\n bottom: 2.5rem;\n background: rgb(0, 0, 0, 0.88);\n color: #fff;\n padding: 0.5rem 0.9rem;\n border-radius: 0.5rem;\n z-index: 20000;\n font-size: 0.85rem;\n box-shadow: 0 4px 16px rgb(0, 0, 0, 0.4);\n animation: jhs-rd-toast-in 0.22s;\n pointer-events: none;\n}\n\n@keyframes jhs-rd-toast-in {\n from { opacity: 0; transform: translateX(-50%) translateY(8px); }\n to { opacity: 1; transform: translateX(-50%) translateY(0); }\n}\n"; var RatingDisplayPlugin = class extends BasePlugin { watchedMap = new Map(); _dirty = false; getName() { return "RatingDisplayPlugin"; } async initCss() { return rating_display_default; } toast(msg) { document.querySelector(".jhs-rd-toast")?.remove(); const div = document.createElement("div"); div.className = "jhs-rd-toast"; div.textContent = msg; document.body.appendChild(div); setTimeout(() => div.remove(), 2500); } processItem(item) { if (!item || item.dataset.jhsrdProcessed === "true") return; item.dataset.jhsrdProcessed = "true"; const code = RatingUtils.getCode(item); if (!code) return; if (!this.watchedMap.has(code)) { RatingRenderer.removeFrom(item); return; } const cached = RatingCache.get(code); if (cached) { RatingRenderer.showRating(item, cached.rating); item.dataset.jhsrdLoaded = "true"; item.dataset.jhsrdFromCache = "true"; } else RatingRenderer.showPlaceholder(item); } refreshAll() { document.querySelectorAll(RATING_CONFIG.ITEM_SELECTOR).forEach((item) => { const el = item; RatingRenderer.removeFrom(el); el.dataset.jhsrdProcessed = "false"; delete el.dataset.jhsrdLoaded; delete el.dataset.jhsrdFetching; delete el.dataset.jhsrdFromCache; this.processItem(el); }); } clearPageCache() { const codes = new Set(); document.querySelectorAll(RATING_CONFIG.ITEM_SELECTOR).forEach((item) => { const code = RatingUtils.getCode(item); if (code) codes.add(code); }); let cleared = 0; codes.forEach((code) => { if (RatingCache._data[code]) { delete RatingCache._data[code]; cleared++; } }); if (cleared > 0) RatingCache.save(); document.querySelectorAll(RATING_CONFIG.ITEM_SELECTOR).forEach((item) => { const el = item; RatingRenderer.removeFrom(el); delete el.dataset.jhsrdLoaded; delete el.dataset.jhsrdFromCache; el.dataset.jhsrdProcessed = "false"; this.processItem(el); }); this.toast(`已清除 ${cleared} 个评分缓存,当前页 ${codes.size} 张卡片`); } loadAll() { const targets = []; document.querySelectorAll(RATING_CONFIG.ITEM_SELECTOR).forEach((item) => { const el = item; if (el.dataset.jhsrdFetching !== "true" && el.dataset.jhsrdLoaded !== "true") targets.push(el); }); if (targets.length === 0) { this.toast("当前页面所有卡片已加载评分"); return; } this.toast(`开始加载 ${targets.length} 个评分...`); targets.forEach((item) => { const tag = item.querySelector(".jhs-user-rating"); if (tag) tag.classList.add("is-loading"); fetchRating(item).finally(() => { if (tag) tag.classList.remove("is-loading"); }); }); } _invalidateCards(code, status, op, score) { if (status === "hasWatch" && op === "add") { if (!this.watchedMap.has(code)) this.watchedMap.set(code, { carNum: code, status: "hasWatch" }); } else this.watchedMap.delete(code); if (status === "hasWatch" && op === "add" && typeof score === "number") RatingCache.set(code, score); else { delete RatingCache._data[code]; RatingCache.save(); } document.querySelectorAll(RATING_CONFIG.ITEM_SELECTOR).forEach((item) => { const el = item; if (RatingUtils.getCode(el) === code) { RatingRenderer.removeFrom(el); delete el.dataset.jhsrdLoaded; delete el.dataset.jhsrdFromCache; el.dataset.jhsrdProcessed = "false"; this.processItem(el); } }); this._dirty = true; const cur = localStorage.getItem("jdb:want-watched-sync"); if (cur) localStorage.setItem("jhsrd:last-sync-digest", cur); } async init() { await RatingCache.load(); const syncVal = localStorage.getItem("jdb:want-watched-sync"); const lastDigest = localStorage.getItem("jhsrd:last-sync-digest"); if (syncVal && syncVal !== lastDigest) { RatingUtils.log("INIT_SYNC", "jhs 有新的同步事件,全量清空评分缓存"); RatingCache.clear(); this._dirty = true; localStorage.setItem("jhsrd:last-sync-digest", syncVal); } this.watchedMap = await buildWatchedMap(); document.querySelectorAll(RATING_CONFIG.ITEM_SELECTOR).forEach((i) => this.processItem(i)); document.querySelectorAll(".jhs-user-rating.is-rated").forEach((el) => { const item = el.closest(RATING_CONFIG.ITEM_SELECTOR); if (item && item.dataset.jhsrdFromCache === "true") { delete item.dataset.jhsrdLoaded; delete item.dataset.jhsrdFromCache; } }); const pending = new Set(); const flushPending = RatingUtils.debounce(() => { pending.forEach((item) => this.processItem(item)); pending.clear(); }, 150); new MutationObserver((muts) => { for (const mutation of muts) { for (const addedNode of mutation.addedNodes) { if (addedNode.nodeType !== 1) continue; const el = addedNode; if (el.matches(RATING_CONFIG.ITEM_SELECTOR)) pending.add(el); const found = el.querySelectorAll(RATING_CONFIG.ITEM_SELECTOR); if (found && found.length) found.forEach((node) => pending.add(node)); } const target = mutation.target; if (target instanceof Element && target.closest(".tags.has-addons")) { const item = target.closest(RATING_CONFIG.ITEM_SELECTOR); if (item) { item.dataset.jhsrdProcessed = "false"; this.processItem(item); } } } flushPending(); }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class"] }); let hoverTimer = null; let hoveredItem = null; document.body.addEventListener("pointerover", (e) => { const item = e.target?.closest(RATING_CONFIG.ITEM_SELECTOR); if (!item || item === hoveredItem) return; hoveredItem = item; if (item.dataset.jhsrdLoaded === "true") return; const tag = item.querySelector(".jhs-user-rating.is-placeholder"); if (!tag) return; if (hoverTimer) clearTimeout(hoverTimer); hoverTimer = setTimeout(() => { if (hoveredItem !== item) return; tag.classList.add("is-loading"); fetchRating(item).finally(() => { tag.classList.remove("is-loading"); }); }, RATING_CONFIG.HOVER_DELAY); }); document.body.addEventListener("pointerout", (e) => { const item = e.target?.closest(RATING_CONFIG.ITEM_SELECTOR); if (!item || item !== hoveredItem) return; hoveredItem = null; if (hoverTimer) clearTimeout(hoverTimer); const tag = item.querySelector(".jhs-user-rating.is-loading"); if (tag) tag.classList.remove("is-loading"); }); GM_registerMenuCommand("全量加载当前页评分", () => { this.loadAll(); }); GM_registerMenuCommand("清空当前页评分缓存", () => { this.clearPageCache(); }); GM_registerMenuCommand("清空所有评分缓存", () => { RatingCache.clear(); this.refreshAll(); this.toast("所有评分缓存已清空"); }); GM_registerMenuCommand("刷新已看列表", async () => { this.watchedMap = await buildWatchedMap(); this.refreshAll(); this.toast(`已看: ${this.watchedMap.size} 项 · 缓存: ${RatingCache.size()} 条`); }); document.addEventListener("jdb:want-watched-sync", (e) => { const { carNum, status, op, score } = e.detail || {}; if (!carNum || !status || !op) return; const code = RatingUtils.normalizeCode(carNum); RatingUtils.log("SYNC_EVENT", code, status, op, score); this._invalidateCards(code, status, op, score); }, { passive: true }); window.addEventListener("storage", (e) => { if (e.key === "jdb:want-watched-sync" && e.newValue) try { const payload = JSON.parse(e.newValue); if (payload && payload.carNum && payload.status && payload.op) { const code = RatingUtils.normalizeCode(payload.carNum); RatingUtils.log("STORAGE_SYNC", "跨标签页精确刷新", code, payload.status, payload.op); this._invalidateCards(code, payload.status, payload.op, payload.score); } } catch { this._dirty = true; } }); document.addEventListener("visibilitychange", () => { if (document.hidden) return; document.querySelectorAll(".jhs-user-rating.is-rated").forEach((el) => { const item = el.closest(RATING_CONFIG.ITEM_SELECTOR); if (item) item.dataset.jhsrdLoaded = "false"; }); if (this._dirty) { this._dirty = false; this.refreshAll(); } }, { passive: true }); RatingUtils.log("INIT", `已看 ${this.watchedMap.size} · 缓存 ${RatingCache.size()}`); } async handle() { await this.init(); } }; var panelInfoStyle$1 = { marginTop: "20px" }; var toolbarStyle$1 = { margin: "30px 0" }; var videoPanelStyle = { marginTop: "20px" }; var magnetsStyle = { margin: "0 0.75rem" }; var hiddenStyle$2 = { display: "none" }; function Fc2DetailDialog() { return (0, import_jsx_runtime.jsxs)("div", { className: "movie-detail-container", children: [ (0, import_jsx_runtime.jsx)("div", { className: "movie-info-container", children: (0, import_jsx_runtime.jsx)("div", { className: "search-loading", children: "加载中..." }) }), (0, import_jsx_runtime.jsx)("div", { className: "movie-panel-info fc2-movie-panel-info", style: panelInfoStyle$1, children: (0, import_jsx_runtime.jsx)("strong", { children: "第三方资源: " }) }), (0, import_jsx_runtime.jsxs)("div", { style: toolbarStyle$1, children: [ (0, import_jsx_runtime.jsx)("a", { id: "filterBtn", className: "jhs-toolbar-menu-btn", style: { backgroundColor: BLOCK_COLOR }, children: (0, import_jsx_runtime.jsx)("span", { children: BLOCK_TEXT }) }), (0, import_jsx_runtime.jsx)("a", { id: "favoriteBtn", className: "jhs-toolbar-menu-btn", style: { backgroundColor: FAVORITE_COLOR }, children: (0, import_jsx_runtime.jsx)("span", { children: FAVORITE_TEXT }) }), (0, import_jsx_runtime.jsx)("a", { id: "hasDownBtn", className: "jhs-toolbar-menu-btn", style: { backgroundColor: HAS_DOWN_COLOR }, children: (0, import_jsx_runtime.jsx)("span", { children: HAS_DOWN_TEXT$1 }) }), (0, import_jsx_runtime.jsx)("a", { id: "hasWatchBtn", className: "jhs-toolbar-menu-btn", style: { backgroundColor: WATCHED_COLOR }, children: (0, import_jsx_runtime.jsx)("span", { children: WATCHED_TEXT }) }), (0, import_jsx_runtime.jsx)("a", { id: "search-subtitle-btn", className: "jhs-toolbar-menu-btn fr-btn", style: { background: "linear-gradient(to bottom, #8d5656, rgb(196,159,91))" }, children: (0, import_jsx_runtime.jsx)("span", { children: "字幕 (SubTitleCat)" }) }), (0, import_jsx_runtime.jsx)("a", { id: "xunLeiSubtitleBtn", className: "jhs-toolbar-menu-btn fr-btn", style: { background: "linear-gradient(to left, #375f7c, #2196F3)" }, children: (0, import_jsx_runtime.jsx)("span", { children: "字幕 (迅雷)" }) }), (0, import_jsx_runtime.jsx)("a", { id: "magnetSearchBtn", className: "jhs-toolbar-menu-btn fr-btn", style: { width: "120px", background: "linear-gradient(to right, rgb(245,140,1), rgb(84,161,29))", color: "white", textAlign: "center", padding: "8px 0" }, children: (0, import_jsx_runtime.jsx)("span", { children: "磁力搜索" }) }) ] }), (0, import_jsx_runtime.jsx)("div", { className: "message video-panel", style: videoPanelStyle, children: (0, import_jsx_runtime.jsx)("div", { id: "magnets-content", className: "magnet-links", style: magnetsStyle, children: (0, import_jsx_runtime.jsx)("div", { className: "search-loading", children: "加载中..." }) }) }), (0, import_jsx_runtime.jsx)("div", { id: "reviews-content" }), (0, import_jsx_runtime.jsx)("div", { id: "related-content" }), (0, import_jsx_runtime.jsx)("span", { id: "data-actress", style: hiddenStyle$2 }) ] }); } function Fc2MagnetItem({ magnet, rowClass }) { const magnetUri = `magnet:?xt=urn:btih:${magnet.hash}`; return (0, import_jsx_runtime.jsxs)("div", { className: `item columns is-desktop ${rowClass}`, children: [ (0, import_jsx_runtime.jsx)("div", { className: "magnet-name column is-four-fifths", children: (0, import_jsx_runtime.jsxs)("a", { href: magnetUri, title: "右鍵點擊並選擇「複製鏈接地址」", children: [ (0, import_jsx_runtime.jsx)("span", { className: "name", children: magnet.name }), (0, import_jsx_runtime.jsx)("br", {}), (0, import_jsx_runtime.jsxs)("span", { className: "meta", children: [ (magnet.size / 1024).toFixed(2), "GB, ", magnet.files_count, "個文件" ] }), (0, import_jsx_runtime.jsx)("br", {}), (0, import_jsx_runtime.jsxs)("div", { className: "tags", children: [magnet.hd ? (0, import_jsx_runtime.jsx)("span", { className: "tag is-primary is-small is-light", children: "高清" }) : "", magnet.cnsub ? (0, import_jsx_runtime.jsx)("span", { className: "tag is-warning is-small is-light", children: "字幕" }) : ""] }) ] }) }), (0, import_jsx_runtime.jsx)("div", { className: "buttons column", children: (0, import_jsx_runtime.jsx)("button", { className: "button is-info is-small copy-to-clipboard", "data-clipboard-text": magnetUri, type: "button", children: "\xA0複製\xA0" }) }), (0, import_jsx_runtime.jsx)("div", { className: "date column", children: (0, import_jsx_runtime.jsx)("span", { className: "time", children: magnet.created_at }) }) ] }); } var galleryStyle$1 = { marginTop: "10px" }; var fc2MarketStyle = { marginLeft: "5px" }; var hiddenStyle$1 = { display: "none" }; function Fc2MovieDetail({ detail, actors, imgList }) { const carNumWithoutPrefix = (detail.carNum || "").replace("FC2-", ""); return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ (0, import_jsx_runtime.jsx)("h3", { className: "movie-title", children: (0, import_jsx_runtime.jsx)("strong", { className: "current-title", children: detail.title || "无标题" }) }), (0, import_jsx_runtime.jsxs)("div", { className: "movie-meta", children: [ (0, import_jsx_runtime.jsxs)("span", { children: [(0, import_jsx_runtime.jsx)("strong", { children: "番号: " }), detail.carNum || "未知"] }), (0, import_jsx_runtime.jsxs)("span", { children: [(0, import_jsx_runtime.jsx)("strong", { children: "年份: " }), detail.releaseDate || "未知"] }), (0, import_jsx_runtime.jsxs)("span", { children: [(0, import_jsx_runtime.jsx)("strong", { children: "评分: " }), detail.score || "无"] }), (0, import_jsx_runtime.jsxs)("span", { children: [(0, import_jsx_runtime.jsx)("strong", { children: "时长: " }), detail.duration + " m" || "无"] }) ] }), (0, import_jsx_runtime.jsx)("div", { className: "movie-meta", children: (0, import_jsx_runtime.jsxs)("span", { children: [ (0, import_jsx_runtime.jsx)("strong", { children: "站点: " }), (0, import_jsx_runtime.jsx)("a", { href: `https://fc2ppvdb.com/articles/${carNumWithoutPrefix}`, target: "_blank", children: "fc2ppvdb" }), (0, import_jsx_runtime.jsx)("a", { style: fc2MarketStyle, href: `https://adult.contents.fc2.com/article/${carNumWithoutPrefix}/`, target: "_blank", children: "fc2电子市场" }) ] }) }), (0, import_jsx_runtime.jsx)("div", { className: "movie-actors", children: (0, import_jsx_runtime.jsxs)("div", { className: "actor-list", children: [(0, import_jsx_runtime.jsx)("strong", { children: "主演: " }), actors.length > 0 ? actors.map((actor) => (0, import_jsx_runtime.jsx)("span", { className: "actor-tag", children: (0, import_jsx_runtime.jsx)("a", { href: `/actors/${actor.id}`, target: "_blank", children: actor.name }) })) : (0, import_jsx_runtime.jsx)("span", { className: "no-data", children: "暂无演员信息" })] }) }), (0, import_jsx_runtime.jsxs)("div", { className: "movie-gallery", style: galleryStyle$1, children: [(0, import_jsx_runtime.jsx)("strong", { children: "剧照: " }), (0, import_jsx_runtime.jsx)("div", { className: "image-list", children: Array.isArray(imgList) && imgList.length > 0 ? imgList.map((img, idx) => (0, import_jsx_runtime.jsx)("a", { href: img, "data-fancybox": "movie-gallery", "data-caption": `剧照 ${idx + 1}`, children: (0, import_jsx_runtime.jsx)("img", { src: img, className: "movie-image-thumb", alt: "" }) })) : (0, import_jsx_runtime.jsx)("div", { className: "no-data", children: "暂无剧照" }) })] }), (0, import_jsx_runtime.jsx)("div", { id: "data-releaseDate", style: hiddenStyle$1, children: detail.releaseDate || "" }) ] }); } function MovieError({ message }) { return (0, import_jsx_runtime.jsxs)("div", { className: "movie-error", children: ["加载失败: ", message] }); } var innerStyle = { marginTop: "50px", color: "#000", cursor: "auto" }; function ScreenLoadingPlaceholder({ maxHeight }) { return (0, import_jsx_runtime.jsx)("a", { className: "tile-item screen-container", style: { overflow: "hidden", maxHeight, maxWidth: maxHeight, textAlign: "center" }, children: (0, import_jsx_runtime.jsx)("div", { style: innerStyle, children: "正在加载缩略图" }) }); } var fc2_plugin_default = "/* Fc2Plugin 弹层样式(提取自未删减版 jhs.user.js Fc2Plugin.initCss) */\n\n/* 弹层样式 */\n.movie-detail-layer .layui-layer-title {\n font-size: 18px;\n color: #333;\n background: #f8f8f8;\n}\n\n/* 容器样式 */\n.movie-detail-container {\n margin: 40px;\n height: 100%;\n background: #fff;\n}\n\n.movie-poster-container {\n flex: 0 0 60%;\n padding: 15px;\n}\n\n.right-box {\n flex: 1;\n padding: 20px;\n overflow-y: auto;\n}\n\n/* 预告片iframe */\n.movie-trailer {\n width: 100%;\n height: 100%;\n min-height: 400px;\n background: #000;\n border-radius: 4px;\n}\n\n/* 电影信息样式 */\n.movie-title {\n font-size: 24px;\n margin-bottom: 15px;\n color: #333;\n}\n\n.movie-meta {\n margin-bottom: 20px;\n color: #666;\n}\n\n.movie-meta span {\n margin-right: 15px;\n}\n\n/* 演员列表 */\n.actor-list {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n margin-top: 10px;\n}\n\n.actor-tag {\n padding: 4px 12px;\n background: #f0f0f0;\n border-radius: 15px;\n font-size: 12px;\n color: #555;\n}\n\n/* 图片列表 */\n.image-list {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n margin-top: 10px;\n}\n\n.movie-image-thumb {\n width: 120px;\n height: 80px;\n object-fit: cover;\n border-radius: 4px;\n cursor: pointer;\n transition: transform 0.3s;\n}\n\n.movie-image-thumb:hover {\n transform: scale(1.05);\n}\n\n/* 加载中和错误状态 */\n.search-loading,\n.movie-error {\n padding: 40px;\n text-align: center;\n color: #999;\n}\n\n.movie-error {\n color: #f56c6c;\n}\n\n.fancybox-container {\n z-index: var(--jhs-z-top);\n}\n\n/* 错误提示样式 */\n.movie-not-found,\n.movie-error {\n text-align: center;\n padding: 30px;\n color: #666;\n}\n\n.movie-not-found h3,\n.movie-error h3 {\n color: #f56c6c;\n margin: 15px 0;\n}\n\n.icon-warning,\n.icon-error {\n font-size: 50px;\n color: #e6a23c;\n}\n\n.icon-error {\n color: #f56c6c;\n}\n\n.fc2-movie-panel-info .panel-block {\n padding: 0 !important;\n}\n"; var Fc2Plugin = class extends BasePlugin { getName() { return "Fc2Plugin"; } async initCss() { return fc2_plugin_default; } async handle() { const fc2SearchHref = "/advanced_search?type=3&score_min=0&d=1"; $(".navbar-item:contains(\"FC2\")").attr("href", fc2SearchHref); $(".tabs a:contains(\"FC2\")").attr("href", fc2SearchHref); if (currentHref.includes("advanced_search?type=3")) { $("h2.section-title").contents().first().replaceWith("Fc2PPV"); $(".section .container > .box").remove(); } if (currentHref.includes("collection_codes?movieId")) { $("section").html(""); const params = new URLSearchParams(window.location.search); const movieId = params.get("movieId"); const carNum = params.get("carNum"); const url = params.get("url"); if (movieId && carNum && url) this.openFc2Dialog(movieId, carNum, url); } } openFc2Dialog(movieId, carNum, url) { const carNumWithoutPrefix = carNum.replace("FC2-", ""); if (url.includes("123av")) { const fc2By123Av = this.getBean("Fc2By123AvPlugin"); if (fc2By123Av) { fc2By123Av.open123AvFc2Dialog(carNum, url); return; } } const dialogHtml = jsxToString((0, import_jsx_runtime.jsx)(Fc2DetailDialog, {})); layer.open({ type: 1, title: carNum, content: dialogHtml, area: utils.getResponsiveArea(["70%", "90%"]), skin: "movie-detail-layer", scrollbar: false, success: (_layerEl, layerIndex) => { this.loadData(movieId, carNum); $("#favoriteBtn").on("click", async () => { const names = $("#data-actress").text(); const publishTime = $("#data-releaseDate").text(); await storageManager.saveCar({ carNum, url, names, actionType: FAVORITE_ACTION, publishTime }); window.refresh(); layer.closeAll(); }); $("#filterBtn").on("click", (event) => { utils.q(event, `是否屏蔽${carNum}?`, async () => { const names = $("#data-actress").text(); const publishTime = $("#data-releaseDate").text(); await storageManager.saveCar({ carNum, url, names, actionType: FILTER_ACTION, publishTime }); window.refresh(); layer.closeAll(); if (window.location.href.includes("collection_codes?movieId")) utils.closePage(); }); }); $("#hasDownBtn").on("click", async () => { const names = $("#data-actress").text(); const publishTime = $("#data-releaseDate").text(); await storageManager.saveCar({ carNum, url, names, actionType: HAS_DOWN_ACTION, publishTime }); window.refresh(); layer.closeAll(); }); $("#hasWatchBtn").on("click", async () => { const names = $("#data-actress").text(); const publishTime = $("#data-releaseDate").text(); await storageManager.saveCar({ carNum, url, names, actionType: HAS_WATCH_ACTION, publishTime }); window.refresh(); layer.closeAll(); }); $("#search-subtitle-btn").on("click", (event) => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum}`, carNum, false, event)); $("#xunLeiSubtitleBtn").on("click", () => this.getBean("DetailPageButtonPlugin")?.searchXunLeiSubtitle(carNum)); $("#magnetSearchBtn").on("click", () => { const magnetHub = this.getBean("MagnetHubPlugin"); if (!magnetHub) return; const magnetHubHtml = magnetHub.createMagnetHub(carNum); layer.open({ type: 1, title: "磁力搜索", content: jsxToString((0, import_jsx_runtime.jsx)("div", { id: "magnetHubBox" })), area: utils.getResponsiveArea(["60%", "80%"]), scrollbar: false, success: () => { $("#magnetHubBox").append(magnetHubHtml); } }); }); this.getBean("OtherSitePlugin")?.loadOtherSite(carNumWithoutPrefix, carNum).then(); utils.setupEscClose(layerIndex); }, end() { if (window.location.href.includes("collection_codes?movieId")) utils.closePage(); } }); } loadData(movieId, carNum) { const carNumWithoutPrefix = carNum.replace("FC2-", ""); this.handleMovieDetail(movieId); this.handleLongImg(carNumWithoutPrefix); this.handleMagnets(movieId); this.getBean("ReviewPlugin")?.showReview(String(movieId), $("#reviews-content")).then(); this.getBean("RelatedPlugin")?.showRelated($("#related-content"), String(movieId)).then(); } handleMovieDetail(movieId) { fetchMovieDetail(movieId).then((detail) => { const actors = detail.actors || []; const imgList = detail.imgList || []; let actressNames = ""; for (let i = 0; i < actors.length; i++) { const actor = actors[i]; if (actor.gender === 0) actressNames += actor.name + " "; } if (actressNames) $("#data-actress").text(actressNames); $(".movie-info-container").html(jsxToString((0, import_jsx_runtime.jsx)(Fc2MovieDetail, { detail, actors, imgList }))); this.getBean("TranslatePlugin")?.translate(detail.carNum, false)?.then(); }).catch((err) => { clog.error(err); $(".movie-info-container").html(jsxToString((0, import_jsx_runtime.jsx)(MovieError, { message: err instanceof Error ? err.message : String(err) }))); }); } handleLongImg(carNumWithoutPrefix) { utils.loopDetector(() => $(".movie-gallery .image-list").length > 0, async () => { $(".movie-gallery .image-list").prepend(jsxToString((0, import_jsx_runtime.jsx)(ScreenLoadingPlaceholder, { maxHeight: "150px" }))); const screenShot = this.getBean("ScreenShotPlugin"); if (!screenShot) return; const screenshotUrl = await screenShot.getScreenshot(carNumWithoutPrefix); if (screenshotUrl) await screenShot.addImg("缩略图", screenshotUrl); }); } handleMagnets(movieId) { (async () => { const url = `${API_BASE}/v1/movies/${movieId}/magnets`; const headers = { jdSignature: await reBuildSignature() }; return (await gmHttp.get(url, void 0, headers)).data.magnets; })().then((magnets) => { let magnetsHtml = ""; if (magnets.length > 0) for (let i = 0; i < magnets.length; i++) { const magnet = magnets[i]; let rowClass = ""; if (i % 2 === 0) rowClass = "odd"; magnetsHtml += jsxToString((0, import_jsx_runtime.jsx)(Fc2MagnetItem, { magnet, rowClass })); } else magnetsHtml = jsxToString((0, import_jsx_runtime.jsx)("span", { className: "no-data", children: "暂无磁力信息" })); $("#magnets-content").html(magnetsHtml); }).catch((err) => { clog.error(err); $("#magnets-content").html(jsxToString((0, import_jsx_runtime.jsx)(MovieError, { message: err instanceof Error ? err.message : String(err) }))); }); } async openFc2Page(movieId, carNum, url) { const javdbUrl = await this.getBean("OtherSitePlugin")?.getJavDbUrl(); window.open(`${javdbUrl}/users/collection_codes?movieId=${encodeURIComponent(movieId)}&carNum=${encodeURIComponent(carNum)}&url=${encodeURIComponent(url)}`, "_blank", "noopener,noreferrer"); } }; var SELECTOR_LEFT = "body > section > div > nav > a.pagination-previous"; var SELECTOR_RIGHT = "body > section > div > nav > a.pagination-next"; var KeyPageTurningPlugin = class extends BasePlugin { lockLeft = false; lockRight = false; getName() { return "KeyPageTurningPlugin"; } get pageTypes() { return [ "list", "fc2", "unknown" ]; } async initCss() { return ""; } async handle() { if (window.isDetailPage) return; window.addEventListener("keydown", (e) => { if (this.isTyping()) return; if (e.key === "ArrowLeft" && !this.lockLeft) { const a = document.querySelector(SELECTOR_LEFT) || null; if (a) { e.preventDefault(); this.safeClick(a); this.lockLeft = true; } } else if (e.key === "ArrowRight" && !this.lockRight) { const a = document.querySelector(SELECTOR_RIGHT) || null; if (a) { e.preventDefault(); this.safeClick(a); this.lockRight = true; } } }, { passive: false }); window.addEventListener("keyup", (e) => { if (e.key === "ArrowLeft") this.lockLeft = false; if (e.key === "ArrowRight") this.lockRight = false; }); } isTyping() { const el = document.activeElement; if (!el) return false; const tag = el.tagName; return el.isContentEditable || tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT"; } safeClick(a) { if (!a) return false; if (typeof a.click === "function") { a.click(); return true; } const evt = new MouseEvent("click", { bubbles: true, cancelable: true }); return a.dispatchEvent(evt); } }; var ModMyListOpenWayPlugin = class extends BasePlugin { getName() { return "ModMyListOpenWayPlugin"; } async initCss() { return ""; } async handle() { if (!location.pathname.includes("/users/lists")) return; const segments = location.pathname.split("/").filter(Boolean); const last = segments[segments.length - 1]; const ul = document.querySelector("#lists > ul"); if (!ul) return; const lis = ul.children; const isListsPage = last === "lists"; const page = Number(new URLSearchParams(location.search).get("page")) || 1; if (isListsPage && page < 2) lis[0].style.display = "none"; for (let i = isListsPage && page < 2 ? 1 : 0; i < lis.length; i++) { const a = lis[i].querySelector("div.column.is-10 > a"); if (!a) continue; a.target = "_blank"; if (isListsPage) a.href = `/lists/${new URL(a.href).searchParams.get("id")}`; } } }; var page_sort_plugin_default = "/**\n * page-sort 插件样式 —— 排序按钮选中态。\n *\n * 原脚本 injectSortStyles() 用 document.createElement('style') 注入:\n * style.textContent = `\n * .button.is-small.selected-method,\n * .button.is-small.selected-method:hover {\n * background-color: rgb(25, 135, 84) !important;\n * border-color: transparent !important;\n * color: #fff !important;\n * }\n * `;\n *\n * 此处提取为独立 .css,由 initCss() 返回 → PluginManager utils.insertStyle 注入。\n * CSS 规则内容与原脚本一致(缩进标准化为 4 空格,CSS 解析忽略空白,渲染等价)。\n */\n.button.is-small.selected-method,\n.button.is-small.selected-method:hover {\n background-color: rgb(25, 135, 84) !important;\n border-color: transparent !important;\n color: #fff !important;\n}\n"; var LOG$2 = "[pageSort]"; function compareItems($itemA, $itemB) { const getStrongText = ($item) => { const $strong = $item.find("a > div.video-title > strong"); return $strong.length ? $strong.text().trim() : ""; }; const textA = getStrongText($itemA); const textB = getStrongText($itemB); return textA.localeCompare(textB); } function getScore($item) { const $span = $item.find("a > div.score > span"); if (!$span.length) return 0; const match = $span.text().trim().match(/(\d+\.\d+)/); if (!match) return 0; return parseFloat(match[1]) || 0; } function newOption(text) { return $("<span>", { text, class: "button is-small" }); } var SORT_CONFIGS = [ { label: "按照名称升序", sortFn: (items) => items.sort((a, b) => compareItems($(a), $(b))), implemented: true }, { label: "按照名称降序", sortFn: (items) => items.sort((a, b) => compareItems($(b), $(a))), implemented: true }, { label: "按照评分升序", sortFn: (items) => items.sort((a, b) => getScore($(a)) - getScore($(b))), implemented: true }, { label: "按照评分降序", sortFn: (items) => items.sort((a, b) => getScore($(b)) - getScore($(a))), implemented: true } ]; var PageSortPlugin = class extends BasePlugin { $container = null; activeSort = null; isApplyingSort = false; sortGuardRetries = 0; MAX_GUARD_RETRIES = 5; sortGuard = null; $buttons = null; getName() { return "PageSortPlugin"; } async initCss() { return page_sort_plugin_default; } async handle() { if (!window.isListPage) return; this.waitForContainer(); } waitForContainer() { if (this.createSortSelector()) return; console.log(`${LOG$2} 页面容器未就绪,开始 MutationObserver 等待`); new MutationObserver((_mutations, obs) => { if (this.createSortSelector()) obs.disconnect(); }).observe(document.body, { childList: true, subtree: true }); } createSortSelector() { const $toolbar = $("body > section > div > div.toolbar"); if (!$toolbar.length) { console.warn(`${LOG$2} 未找到工具栏 .toolbar,跳过排序选择器注入`); return false; } const $container = $("body > section > div > div.movie-list.h.cols-4.vcols-8"); if (!$container.length) { console.warn(`${LOG$2} 未找到视频列表容器 .movie-list,跳过排序选择器注入`); return false; } this.$container = $container; const $items = $container.children(".item"); if ($items.length === 0) { console.warn(`${LOG$2} 视频列表为空,跳过排序选择器注入`); return false; } $items.each(function(i) { if (!this.hasAttribute("data-original-index")) this.setAttribute("data-original-index", String(i)); }); const sortGuard = new MutationObserver(() => { if (this.activeSort && !this.isApplyingSort) { if (this.sortGuardRetries >= this.MAX_GUARD_RETRIES) { console.warn(`${LOG$2} 排序守卫重试已达上限 (${this.MAX_GUARD_RETRIES}),放弃自动恢复`); return; } this.sortGuardRetries++; console.log(`${LOG$2} 检测到 DOM 顺序被外部修改,重新应用排序 (${this.sortGuardRetries}/${this.MAX_GUARD_RETRIES})`); this.applySort(this.activeSort); } }); this.sortGuard = sortGuard; sortGuard.observe($container[0], { childList: true }); const $buttonGroup = $("<div>", { css: { display: "inline-block", marginBottom: ".1rem", lineHeight: "2.2rem", marginRight: ".4rem", marginLeft: "auto" } }); const $buttons = $("<div>", { class: "buttons has-addons" }); this.$buttons = $buttons; $buttonGroup.append($buttons); $toolbar.append($buttonGroup); SORT_CONFIGS.forEach((config) => { $buttons.append(newOption(config.label)); }); $buttons.on("click", ".button.is-small", (e) => { e.preventDefault(); e.stopPropagation(); const $this = $(e.currentTarget); const isActive = $this.hasClass("selected-method"); this.sortGuardRetries = 0; localStorage.removeItem("jhs_sortMethod"); if (isActive) { $this.removeClass("selected-method"); this.activeSort = null; this.applySort(null); } else { $this.siblings(".selected-method").removeClass("selected-method"); $this.addClass("selected-method"); const method = $this.text(); this.activeSort = method; this.applySort(method); } }); $("#sort-toggle-btn").on("click", () => { this.activeSort = null; this.sortGuardRetries = 0; if (this.$buttons) this.$buttons.find(".selected-method").removeClass("selected-method"); }); console.log(`${LOG$2} 排序选择器已注入,共 ${$items.length} 个影片`); return true; } applySort(sortMethod) { if (this.isApplyingSort) return; this.isApplyingSort = true; this.sortGuard?.disconnect(); try { const currentItems = this.$container.children(".item").get(); if (currentItems.length === 0) return; if (sortMethod === null) { currentItems.sort((a, b) => { return (parseInt(a.getAttribute("data-original-index") || "0") || 0) - (parseInt(b.getAttribute("data-original-index") || "0") || 0); }); this.$container.append(currentItems); console.log(`${LOG$2} 已恢复原始顺序`); } else { const config = SORT_CONFIGS.find((c) => c.label === sortMethod); if (!config) { console.warn(`${LOG$2} 未识别的排序选项: ${sortMethod}`); return; } if (!config.implemented || !config.sortFn) { console.log(`${LOG$2} ${sortMethod} - 待实现`); return; } config.sortFn(currentItems); this.$container.append(currentItems); console.log(`${LOG$2} 已${sortMethod}排列 ${currentItems.length} 个影片`); } this.sortGuard?.takeRecords(); } finally { if (this.$container && this.sortGuard) this.sortGuard.observe(this.$container[0], { childList: true }); this.isApplyingSort = false; } } }; var STATUS_TAG_SELECTOR = ".item .tags.has-addons .tag.is-success.status-tag"; var JHS_HIDE_ATTR = "data-hide"; var NO_TAG_VALUE$1 = "no-tag"; function collectStatusTagCounts() { const counts = {}; document.querySelectorAll(STATUS_TAG_SELECTOR).forEach((el) => { const itemEl = el.closest(".item"); if (itemEl && itemEl.hasAttribute("data-hide")) return; const text = el.textContent?.trim() || ""; if (text) counts[text] = (counts[text] || 0) + 1; }); return counts; } function countNoStatusItems() { let count = 0; document.querySelectorAll(".item").forEach((item) => { if (item.hasAttribute("data-hide")) return; if (!item.querySelector(".item .tags.has-addons .tag.is-success.status-tag")) count++; }); return count; } var HIDDEN_ATTR = "data-status-tag-hidden"; function applyFilter$3() { const activeChips = document.querySelectorAll(".status-tag-filter-chip.active"); const selectedValues = new Set(Array.from(activeChips).map((c) => c.dataset.value || "")); if (selectedValues.size === 0) { document.querySelectorAll(`.item[${HIDDEN_ATTR}]`).forEach((item) => { const htmlItem = item; if (htmlItem.hasAttribute("data-hide")) return; htmlItem.removeAttribute(HIDDEN_ATTR); htmlItem.style.display = ""; }); return; } const showNoTag = selectedValues.has(NO_TAG_VALUE$1); const selectedTags = new Set([...selectedValues].filter((v) => v !== NO_TAG_VALUE$1)); document.querySelectorAll(".item").forEach((item) => { const htmlItem = item; const hiddenByJhs = htmlItem.hasAttribute(JHS_HIDE_ATTR); const hiddenByOther = htmlItem.style.display === "none" && !htmlItem.hasAttribute(HIDDEN_ATTR); if (hiddenByJhs || hiddenByOther) return; const itemStatusTags = new Set(); item.querySelectorAll(STATUS_TAG_SELECTOR).forEach((el) => { const text = el.textContent?.trim() || ""; if (text) itemStatusTags.add(text); }); let tagMatch = false; if (selectedTags.size > 0) tagMatch = [...selectedTags].some((t) => itemStatusTags.has(t)); const noTagMatch = showNoTag && itemStatusTags.size === 0; if (tagMatch || noTagMatch) { htmlItem.removeAttribute(HIDDEN_ATTR); htmlItem.style.display = ""; } else { htmlItem.setAttribute(HIDDEN_ATTR, ""); htmlItem.style.display = "none"; } }); } var LOG$1 = "[状态标签筛选]"; function createFilterChip$2(labelText, value, opts = {}) { const { isNoTag = false, count } = opts; const chip = document.createElement("span"); chip.className = "status-tag-filter-chip"; if (isNoTag) chip.classList.add("no-status"); chip.textContent = count !== void 0 ? `${labelText} ${count}` : labelText; chip.dataset.value = value; chip.addEventListener("click", () => { chip.classList.toggle("active"); applyFilter$3(); }); return chip; } function doBuild(mountTarget) { if (document.querySelector(".status-tag-filter-bar")) return; const filterBar = document.createElement("div"); filterBar.className = "status-tag-filter-bar"; const label = document.createElement("span"); label.className = "status-tag-filter-label"; label.textContent = "状态:"; filterBar.appendChild(label); const chipsContainer = document.createElement("div"); chipsContainer.className = "status-tag-filter-chips"; filterBar.appendChild(chipsContainer); const refreshChips = () => { const activeValues = new Set(Array.from(chipsContainer.querySelectorAll(".status-tag-filter-chip.active")).map((c) => c.dataset.value || "")); chipsContainer.innerHTML = ""; const noTagChip = createFilterChip$2("无状态标签", NO_TAG_VALUE$1, { isNoTag: true, count: countNoStatusItems() }); if (activeValues.has("no-tag")) noTagChip.classList.add("active"); chipsContainer.appendChild(noTagChip); const allCounts = collectStatusTagCounts(); Object.keys(allCounts).sort().forEach((tagName) => { const chip = createFilterChip$2(tagName, tagName, { count: allCounts[tagName] }); if (activeValues.has(tagName)) chip.classList.add("active"); chipsContainer.appendChild(chip); }); }; refreshChips(); if (mountTarget.matches(".jhs-vlt-filter-bar, .actor-tags.tags, .tabs.is-boxed")) mountTarget.insertAdjacentElement("afterend", filterBar); else mountTarget.insertAdjacentElement("beforebegin", filterBar); filterBar._refreshChips = refreshChips; console.log(`${LOG$1} 筛选栏已挂载`); } function findMountTarget() { const tagFilterBar = document.querySelector(".jhs-vlt-filter-bar"); if (tagFilterBar) return tagFilterBar; if (/^\/actors\//.test(window.location.pathname)) { const actorTags = document.querySelector("body > section > div > div.actor-tags.tags"); if (actorTags) return actorTags; } else { const tabsBoxed = document.querySelector("body > section > div > div.tabs.is-boxed"); if (tabsBoxed) return tabsBoxed; } const section = document.querySelector("body > section > div > div"); if (section && section.firstElementChild) { if (!section.firstElementChild.matches(".jhs-vlt-filter-bar, .status-tag-filter-bar")) return section.firstElementChild; } const container = document.querySelector("body > section > div"); if (container && container.firstElementChild) { if (!container.firstElementChild.matches(".jhs-vlt-filter-bar, .status-tag-filter-bar")) return container.firstElementChild; } return null; } var status_tag_filter_plugin_default = "/**\n * status-tag-filter 插件样式 —— 状态标签筛选栏 + 芯片。\n *\n * 原脚本 GM_addStyle(`...`) 注入,此处提取为独立 .css,由 initCss() 返回 →\n * PluginManager utils.insertStyle 注入。CSS 规则内容与原脚本一致(缩进标准化,\n * CSS 解析忽略空白,渲染等价)。\n */\n\n.status-tag-filter-chip.active {\n background: #198754;\n border-color: #198754;\n color: #fff;\n}\n\n.status-tag-filter-chip.no-status {\n border-style: dashed;\n color: #6c757d;\n}\n\n.status-tag-filter-chip.no-status.active {\n background: #6c757d;\n border-color: #6c757d;\n border-style: solid;\n color: #fff;\n}\n"; var StatusTagFilterPlugin = class extends BasePlugin { observerDebounce = 0; supervisor = new TaskSupervisor(); getName() { return "StatusTagFilterPlugin"; } get pageTypes() { return ["list"]; } async initCss() { return status_tag_filter_plugin_default; } async handle() { if (!window.isListPage) return; this.init(); } init() { console.log(`${LOG$1} 初始化, pathname:`, window.location.pathname); if (!document.body) { console.warn(`${LOG$1} document.body 不存在,延后初始化`); this.supervisor.setTimeout(() => this.init(), 100); return; } if (this.tryBuild()) { this.startObserving(); return; } console.log(`${LOG$1} 未找到挂载目标,开始 MutationObserver 等待`); let mountDebounce = 0; const mountObserver = this.supervisor.observe(document.body, () => { clearTimeout(mountDebounce); mountDebounce = this.supervisor.setTimeout(() => { if (this.tryBuild()) { console.log(`${LOG$1} MutationObserver 挂载成功`); mountObserver.disconnect(); this.startObserving(); clearTimeout(mountTimeout); } }, 100); }, { childList: true, subtree: true }); const mountTimeout = this.supervisor.setTimeout(() => { mountObserver.disconnect(); if (!document.querySelector(".status-tag-filter-bar")) { const fallback = findMountTarget() || document.body.firstElementChild; if (fallback) { doBuild(fallback); this.startObserving(); console.warn(`${LOG$1} 通过最终回退挂载`, fallback); } else console.warn(`${LOG$1} 超时后仍无挂载目标`); } }, 1e4); } tryBuild() { if (document.querySelector(".status-tag-filter-bar")) return true; const mountTarget = findMountTarget(); if (!mountTarget) return false; doBuild(mountTarget); return true; } updateFilterBar() { const filterBar = document.querySelector(".status-tag-filter-bar"); if (filterBar && filterBar._refreshChips) { filterBar._refreshChips(); applyFilter$3(); } } startObserving() { this.supervisor.observe(document.body, (mutations) => { let shouldRefresh = false; for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue; const el = node; if (el.matches?.(".item") || el.querySelectorAll?.(".item").length > 0 || el.matches?.(".tag.is-success.status-tag") || el.querySelectorAll?.(".tag.is-success.status-tag").length > 0) { shouldRefresh = true; break; } } if (shouldRefresh) break; } if (shouldRefresh) { clearTimeout(this.observerDebounce); this.observerDebounce = this.supervisor.setTimeout(() => { this.updateFilterBar(); }, 150); } }, { childList: true, subtree: true }); } destroy() { this.supervisor.abort(); } }; var SCROLL_TOP_SVG_PATH = "M12 4l-8 8h6v8h4v-8h6z"; function ScrollTopIcon() { return (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", children: (0, import_jsx_runtime.jsx)("path", { d: SCROLL_TOP_SVG_PATH }) }); } function scrollTopIconHtml() { return jsxToString((0, import_jsx_runtime.jsx)(ScrollTopIcon, {})); } function createBackToTopButton(options) { const id = options?.selector ?? "jdb-wf-back-to-top"; const threshold = options?.threshold ?? 300; if (document.getElementById("jhs-back-to-top") || document.getElementById(id)) return () => {}; const btn = document.createElement("div"); btn.id = id; btn.title = "回到顶部"; btn.innerHTML = scrollTopIconHtml(); document.body.appendChild(btn); let ticking = false; const onScroll = () => { if (ticking) return; ticking = true; window.requestAnimationFrame(() => { if (window.scrollY > threshold) btn.classList.add("show"); else btn.classList.remove("show"); ticking = false; }); }; const onClick = () => { window.scrollTo({ top: 0, behavior: "smooth" }); }; window.addEventListener("scroll", onScroll, { passive: true }); btn.addEventListener("click", onClick); return () => { window.removeEventListener("scroll", onScroll); btn.removeEventListener("click", onClick); btn.remove(); }; } var LOG_PREFIX$3 = "[listWaterfall]"; var MAX_PAGES = 200; var SEL = { box: "#lists > ul", item: "#lists > ul > li", nextLink: "a.pagination-next", paginationNav: "nav.pagination", itemLink: "div.column.is-10 > a" }; function findNextUrl$1(doc) { const a = doc.querySelector(SEL.nextLink); return a ? a.getAttribute("href") : null; } function toAbsolute(href) { return new URL(href, location.href).href; } function rewriteItemLinks(container) { container.querySelectorAll(SEL.itemLink).forEach((a) => { const anchor = a; anchor.target = "_blank"; const href = anchor.getAttribute("href") || ""; if (/^\/lists\/[^/]+$/.test(href)) return; const m = href.match(/[?&]id=([^&]+)/); if (m) anchor.href = `/lists/${m[1]}`; }); } function setState(plugin, cls, text) { if (!plugin.loader) return; plugin.loader.className = `jdb-wf-loader ${cls}`.trim(); plugin.loader.textContent = text; } async function loadNextPage(plugin) { if (plugin.isLoading || !plugin.nextUrl || !plugin.hasMore) return; if (plugin.currentPage >= MAX_PAGES) { setState(plugin, "no-more", `已达最大页数 ${MAX_PAGES},停止加载`); plugin.hasMore = false; return; } plugin.isLoading = true; setState(plugin, "loading", "加载中..."); try { const html = await fetchJavdbPageHtml(plugin.nextUrl); const doc = new DOMParser().parseFromString(html, "text/html"); const remoteUl = doc.querySelector(SEL.box); if (!remoteUl) throw new Error("下一页未找到列表容器"); const newLis = Array.from(remoteUl.children).filter((li) => li.tagName === "LI"); if (newLis.length > 0 && newLis[0].id && plugin.loadedIds.has(newLis[0].id)) { plugin.hasMore = false; plugin.nextUrl = null; setState(plugin, "no-more", "已经到底了"); console.log(`${LOG_PREFIX$3} 检测到重复数据,停止瀑布流`); return; } const topBefore = plugin.container.scrollHeight; const fragment = document.createDocumentFragment(); newLis.forEach((li) => { if (li.id) plugin.loadedIds.add(li.id); fragment.appendChild(li); }); plugin.container.appendChild(fragment); rewriteItemLinks(plugin.container); plugin.currentPage += 1; plugin.pageItems.push({ page: plugin.currentPage, top: topBefore, url: plugin.nextUrl }); const nextHref = findNextUrl$1(doc); plugin.nextUrl = nextHref ? toAbsolute(nextHref) : null; plugin.hasMore = !!plugin.nextUrl; const remoteNav = doc.querySelector(SEL.paginationNav); if (remoteNav) { const currentNav = document.querySelector(SEL.paginationNav); if (currentNav) currentNav.replaceWith(remoteNav); } setState(plugin, "", ""); if (!plugin.hasMore) setState(plugin, "no-more", "已经到底了"); console.log(`${LOG_PREFIX$3} 已加载第 ${plugin.currentPage} 页,追加 ${newLis.length} 项`); } catch (err) { clog.error(`${LOG_PREFIX$3} 加载失败:`, err); setState(plugin, "error", "加载失败,点击重试"); } finally { plugin.isLoading = false; } } var PRELOAD_DISTANCE$1 = 800; function checkLoad(plugin) { if (!plugin.loader || !plugin.hasMore) return; if (plugin.loader.getBoundingClientRect().top < window.innerHeight + PRELOAD_DISTANCE$1) loadNextPage(plugin); } function updateCurrentPageFromScroll(plugin) { const y = window.scrollY; for (let i = plugin.pageItems.length - 1; i >= 0; i--) { const item = plugin.pageItems[i]; if (y >= item.top) { if (plugin.currentPage !== item.page) plugin.currentPage = item.page; break; } } } var list_waterfall_plugin_default = "/**\n * list-waterfall 插件样式 —— 瀑布流 loader 三态 + 回到顶部按钮。\n *\n * 原脚本 GM_addStyle(`...`) 注入(L80-153),此处提取为独立 .css,由 initCss()\n * 返回 → PluginManager utils.insertStyle 注入。CSS 规则内容与原脚本一致(缩进\n * 标准化,CSS 解析忽略空白,渲染等价)。\n */\n.jdb-wf-loader {\n text-align: center;\n padding: 24px 0 40px;\n font-size: 0.9rem;\n color: #6c757d;\n min-height: 44px;\n box-sizing: border-box;\n}\n\n.jdb-wf-loader.loading {\n color: #485fc7;\n}\n\n.jdb-wf-loader.loading::before {\n content: \"\";\n display: inline-block;\n width: 14px;\n height: 14px;\n margin-right: 8px;\n border: 2px solid currentcolor;\n border-top-color: transparent;\n border-radius: 50%;\n vertical-align: -2px;\n animation: jdb-wf-spin 0.8s linear infinite;\n}\n\n.jdb-wf-loader.error {\n color: #f44336;\n cursor: pointer;\n text-decoration: underline;\n}\n\n.jdb-wf-loader.no-more {\n color: #4CAF50;\n}\n\n@keyframes jdb-wf-spin {\n to {\n transform: rotate(360deg);\n }\n}\n"; var ENABLED_KEY = "jdb:list-waterfall-enabled"; var ALLOWED_PATHS = new Set(["/users/lists", "/users/favorite_lists"]); function getInitialPage() { const m = location.search.match(/[?&]page=(\d+)/); return m ? parseInt(m[1], 10) : 1; } function isEnabled() { return GM_getValue(ENABLED_KEY, true); } var ListWaterfallPlugin = class extends BasePlugin { currentPage = getInitialPage(); nextUrl = null; hasMore = false; isLoading = false; loader = null; container = null; loadedIds = new Set(); pageItems = []; _onScroll = null; cleanupBackToTop = null; supervisor = new TaskSupervisor(); getName() { return "ListWaterfallPlugin"; } async initCss() { return list_waterfall_plugin_default; } async handle() { if (!ALLOWED_PATHS.has(location.pathname)) return; if (!isEnabled()) { console.log(`${LOG_PREFIX$3} 瀑布流已关闭,退出`); return; } GM_registerMenuCommand(`${isEnabled() ? "✓" : "✗"} 清单瀑布流自动翻页`, this.toggleEnabled.bind(this)); this.init(); } toggleEnabled() { const next = !isEnabled(); GM_setValue(ENABLED_KEY, next); console.log(`${LOG_PREFIX$3} 瀑布流开关: ${next ? "开启" : "关闭"}`); if (window.confirm(`清单瀑布流自动翻页已${next ? "开启" : "关闭"},是否刷新页面应用?`)) window.location.reload(); } init() { this.container = document.querySelector(SEL.box); if (!this.container) { console.warn(`${LOG_PREFIX$3} 未找到容器 ${SEL.box},退出`); return; } this.container.querySelectorAll(SEL.item).forEach((li) => { if (li.id) this.loadedIds.add(li.id); }); this.loader = document.createElement("div"); this.loader.className = "jdb-wf-loader"; this.container.parentNode.insertBefore(this.loader, this.container.nextSibling); this.supervisor.addEventListener(this.loader, "click", () => { if (this.loader.classList.contains("error")) this.loadNextPage(); }); const nextHref = findNextUrl$1(document); this.nextUrl = nextHref ? toAbsolute(nextHref) : null; this.hasMore = !!this.nextUrl; this.pageItems.push({ page: this.currentPage, top: 0, url: window.location.href }); this.cleanupBackToTop = createBackToTopButton(); this._onScroll = () => { this.checkLoad(); this.updateCurrentPageFromScroll(); }; this.supervisor.addEventListener(window, "scroll", this._onScroll, { passive: true }); this.supervisor.setTimeout(() => this.checkLoad(), 500); if (!this.hasMore) this.setState("no-more", "已经到底了"); else this.setState("", ""); console.log(`${LOG_PREFIX$3} 已启动 (页码 ${this.currentPage})`); } async loadNextPage() { return loadNextPage(this); } checkLoad() { checkLoad(this); } updateCurrentPageFromScroll() { updateCurrentPageFromScroll(this); } setState(cls, text) { setState(this, cls, text); } destroy() { this.cleanupBackToTop?.(); this.supervisor.abort(); } }; var LOG_PREFIX$2 = "[listReadingStatus]"; var STORAGE_KEY = "jdb:list-reading-status"; var RATING_STORAGE_KEY = "jdb:list-rating"; var LAST_URI_KEY = "jdb:list-last-uri"; var SORT_KEY = "jdb:list-sort"; var FILTER_READ_KEY = "jdb:list-filter-read"; var FILTER_RATING_KEY = "jdb:list-filter-rating"; var JHS_DB_NAME = "JAV-JHS"; var JHS_STORE_NAME = "appData"; var BACKUP_KEY = "listReadingStatus_data"; function getRatingMap() { const raw = GM_getValue(RATING_STORAGE_KEY); if (!raw) return {}; try { return JSON.parse(raw); } catch { return {}; } } function saveRatingMap(map) { GM_setValue(RATING_STORAGE_KEY, JSON.stringify(map)); syncToIndexedDB(); } function setRating(listId, rating) { const map = getRatingMap(); if (rating > 0) map[listId] = rating; else delete map[listId]; saveRatingMap(map); console.log(`${LOG_PREFIX$2} 评分 ${listId}: ${rating || "清除"}`); } function getRating(listId) { return getRatingMap()[listId] || 0; } function getLastUriMap() { const raw = GM_getValue(LAST_URI_KEY); if (!raw) return {}; try { return JSON.parse(raw); } catch { return {}; } } function saveLastUri(listId) { const map = getLastUriMap(); map[listId] = { path: location.pathname + location.search, timestamp: Date.now() }; GM_setValue(LAST_URI_KEY, JSON.stringify(map)); syncToIndexedDB(); } function getLastUri(listId) { return getLastUriMap()[listId] || null; } function getReadSet() { const raw = GM_getValue(STORAGE_KEY); if (!raw) return new Set(); try { const arr = JSON.parse(raw); return new Set(Array.isArray(arr) ? arr : []); } catch { return new Set(); } } function saveReadSet(readSet) { GM_setValue(STORAGE_KEY, JSON.stringify([...readSet])); syncToIndexedDB(); } function markAsRead(listId) { const readSet = getReadSet(); readSet.add(listId); saveReadSet(readSet); console.log(`${LOG_PREFIX$2} 已标记已读完: ${listId}`); } function markAsUnread(listId) { const readSet = getReadSet(); readSet.delete(listId); saveReadSet(readSet); console.log(`${LOG_PREFIX$2} 已取消标记: ${listId}`); } function isRead(listId) { return getReadSet().has(listId); } function openJhsDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(JHS_DB_NAME); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(JHS_STORE_NAME)) db.createObjectStore(JHS_STORE_NAME); }; }); } function syncToIndexedDB() { try { const data = { readingStatus: [...getReadSet()], ratings: getRatingMap(), lastUris: getLastUriMap(), _updatedAt: new Date().toISOString() }; openJhsDB().then((db) => { const tx = db.transaction(JHS_STORE_NAME, "readwrite"); tx.objectStore(JHS_STORE_NAME).put(data, BACKUP_KEY); tx.oncomplete = () => db.close(); tx.onerror = () => db.close(); }).catch(() => {}); } catch {} } async function restoreFromIndexedDB() { try { const db = await openJhsDB(); const store = db.transaction(JHS_STORE_NAME, "readonly").objectStore(JHS_STORE_NAME); const data = await new Promise((resolve, reject) => { const req = store.get(BACKUP_KEY); req.onsuccess = () => resolve(req.result); req.onerror = reject; }); db.close(); if (!data || !data.readingStatus) return false; let restored = false; const localReadSet = getReadSet(); const cloudRead = Array.isArray(data.readingStatus) ? data.readingStatus : []; const mergedRead = new Set([...localReadSet, ...cloudRead]); if (mergedRead.size > localReadSet.size) { GM_setValue(STORAGE_KEY, JSON.stringify([...mergedRead])); restored = true; } const localRatings = getRatingMap(); const cloudRatings = data.ratings && typeof data.ratings === "object" ? data.ratings : {}; let ratingChanged = false; for (const [id, rating] of Object.entries(cloudRatings)) if (!(id in localRatings)) { localRatings[id] = rating; ratingChanged = true; } if (ratingChanged) { GM_setValue(RATING_STORAGE_KEY, JSON.stringify(localRatings)); restored = true; } const localLastUris = getLastUriMap(); const cloudLastUris = data.lastUris && typeof data.lastUris === "object" ? data.lastUris : {}; let lastUriChanged = false; for (const [id, uri] of Object.entries(cloudLastUris)) { const local = localLastUris[id]; const cloudUri = uri; if (!local || cloudUri.timestamp && cloudUri.timestamp > (local.timestamp || 0)) { localLastUris[id] = cloudUri; lastUriChanged = true; } } if (lastUriChanged) { GM_setValue(LAST_URI_KEY, JSON.stringify(localLastUris)); restored = true; } if (restored) console.log(`${LOG_PREFIX$2} 已从云端合并恢复数据 (更新于 ${data._updatedAt || "未知"})`); return restored; } catch { return false; } } var STAR_PATH = "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"; var LRS_HIDDEN_ATTR = "data-lrs-hidden"; var LRS_ORDER_ATTR = "data-lrs-order"; function updateSelectAppearance(select) { select.setAttribute("data-status", select.value); } function renderStars(starsEl, rating) { starsEl.querySelectorAll(".list-rating-star").forEach((svg, i) => { if (i < rating) svg.classList.add("filled"); else svg.classList.remove("filled"); }); } function createStarWidget(plugin, listId) { const container = document.createElement("span"); container.className = "list-rating-stars"; container.title = "评分 1-5 星,再次点击取消评分"; const currentRating = getRating(listId); const NS = "http://www.w3.org/2000/svg"; for (let i = 1; i <= 5; i++) { const svg = document.createElementNS(NS, "svg"); svg.setAttribute("viewBox", "0 0 24 24"); svg.classList.add("list-rating-star"); svg.dataset.rating = String(i); const path = document.createElementNS(NS, "path"); path.setAttribute("d", STAR_PATH); svg.appendChild(path); if (i <= currentRating) svg.classList.add("filled"); plugin.supervisor.addEventListener(svg, "click", (e) => { e.stopPropagation(); const clicked = parseInt(svg.dataset.rating || "0"); const newRating = clicked === getRating(listId) ? 0 : clicked; setRating(listId, newRating); renderStars(container, newRating); }); plugin.supervisor.addEventListener(svg, "mouseenter", () => { renderStars(container, parseInt(svg.dataset.rating || "0")); }); container.appendChild(svg); } plugin.supervisor.addEventListener(container, "mouseleave", () => { renderStars(container, getRating(listId)); }); return container; } function createDropdown(plugin, listId) { const select = document.createElement("select"); select.className = "list-reading-dropdown"; const optionUnread = document.createElement("option"); optionUnread.value = "unread"; optionUnread.textContent = "未读完"; const optionRead = document.createElement("option"); optionRead.value = "read"; optionRead.textContent = "已读完"; select.appendChild(optionUnread); select.appendChild(optionRead); select.value = isRead(listId) ? "read" : "unread"; updateSelectAppearance(select); plugin.supervisor.addEventListener(select, "change", () => { if (select.value === "read") markAsRead(listId); else markAsUnread(listId); updateSelectAppearance(select); }); return select; } function ensureWidgets(plugin, li) { const listId = li.id; if (!listId) return; const container = li.querySelector(":scope > div"); if (!container) return; let select = container.querySelector(".list-reading-dropdown"); if (select) { select.value = isRead(listId) ? "read" : "unread"; updateSelectAppearance(select); } else { select = createDropdown(plugin, listId); container.prepend(select); } let stars = container.querySelector(".list-rating-stars"); if (stars) renderStars(stars, getRating(listId)); else { stars = createStarWidget(plugin, listId); container.prepend(stars); } let uriLink = container.querySelector(".list-last-uri-link"); const lastUri = getLastUri(listId); if (lastUri) { if (!uriLink) { uriLink = document.createElement("a"); uriLink.className = "list-last-uri-link"; const d = new Date(lastUri.timestamp); const pad = (n) => String(n).padStart(2, "0"); const dateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; const search = lastUri.path.includes("?") ? " " + lastUri.path.slice(lastUri.path.indexOf("?") + 1) : ""; uriLink.textContent = `继续浏览 →${search} (${dateStr})`; container.appendChild(uriLink); } uriLink.href = lastUri.path; } else if (uriLink) uriLink.remove(); } function ensureHeaderWidgets(plugin) { if (plugin.isProcessing) return; plugin.isProcessing = true; const m = location.pathname.match(/\/lists\/([^/]+)$/); if (!m) { plugin.isProcessing = false; return; } const listId = "list-" + m[1]; saveLastUri(listId); const h2 = document.querySelector("body > section > div > div.columns.is-mobile.section-columns > div > h2"); if (!h2) { plugin.isProcessing = false; return; } let select = h2.querySelector(".list-reading-dropdown"); if (select) { select.value = isRead(listId) ? "read" : "unread"; updateSelectAppearance(select); } else { select = createDropdown(plugin, listId); h2.prepend(select); } let stars = h2.querySelector(".list-rating-stars"); if (stars) renderStars(stars, getRating(listId)); else { stars = createStarWidget(plugin, listId); h2.prepend(stars); } Promise.resolve().then(() => { plugin.isProcessing = false; }); } function processAllItems(plugin) { if (plugin.isProcessing) return false; plugin.isProcessing = true; const ul = document.querySelector("#lists > ul"); if (!ul) { plugin.isProcessing = false; return false; } const items = ul.querySelectorAll(":scope > li"); items.forEach((li) => ensureWidgets(plugin, li)); plugin.applySortAndFilter(); Promise.resolve().then(() => { plugin.isProcessing = false; }); return items.length > 0; } function getListMeta(_plugin, li) { const listId = li.id || ""; const titleEl = li.querySelector("a[href*='/lists/']"); const title = titleEl ? titleEl.textContent.trim() : ""; const metaTxt = li.querySelector(".meta")?.textContent ?? ""; const moviesMatch = metaTxt.match(/([\d,]+)\s*部影片/); const clicksMatch = metaTxt.match(/點擊了\s*([\d,]+)/); return { listId, title, movies: moviesMatch ? parseInt(moviesMatch[1].replace(/,/g, "")) : 0, clicks: clicksMatch ? parseInt(clicksMatch[1].replace(/,/g, "")) : 0, rating: getRating(listId), visited: getLastUri(listId)?.timestamp ?? 0, order: parseInt(li.getAttribute("data-lrs-order") || "0") || 0 }; } function ensureLiOrder(plugin, li) { if (!li.hasAttribute("data-lrs-order")) li.setAttribute(LRS_ORDER_ATTR, String(plugin.orderCounter++)); } function compareLi(plugin, a, b) { const ma = getListMeta(plugin, a); const mb = getListMeta(plugin, b); let cmp; switch (plugin.currentSort) { case "rating-desc": cmp = mb.rating - ma.rating; break; case "rating-asc": cmp = ma.rating - mb.rating; break; case "movies-desc": cmp = mb.movies - ma.movies; break; case "movies-asc": cmp = ma.movies - mb.movies; break; case "clicks-desc": cmp = mb.clicks - ma.clicks; break; case "clicks-asc": cmp = ma.clicks - mb.clicks; break; case "visited-desc": cmp = mb.visited - ma.visited; break; case "visited-asc": cmp = ma.visited - mb.visited; break; case "title-asc": cmp = ma.title.localeCompare(mb.title, "zh"); break; default: cmp = 0; } if (cmp === 0) cmp = ma.order - mb.order; return cmp; } function applySort(plugin) { if (plugin.currentSort === "default") return; const ul = document.querySelector("#lists > ul"); if (!ul) return; const lis = [...ul.querySelectorAll(":scope > li")]; if (lis.length < 2) return; lis.forEach((li) => ensureLiOrder(plugin, li)); lis.sort((a, b) => compareLi(plugin, a, b)); if (lis.map((li) => li.id).join(",") === [...ul.querySelectorAll(":scope > li")].map((li) => li.id).join(",")) return; const frag = document.createDocumentFragment(); lis.forEach((li) => frag.appendChild(li)); ul.appendChild(frag); } function saveFilterReadState(plugin, v) { plugin.filterReadStatus = v; GM_setValue(FILTER_READ_KEY, v); } function saveFilterRatingState(plugin, set) { plugin.filterRatingChips = set; GM_setValue(FILTER_RATING_KEY, JSON.stringify([...set])); } function matchesFilter(plugin, li) { const listId = li.id; if (!listId) return true; if (plugin.filterReadStatus !== "all") { const read = isRead(listId); if (plugin.filterReadStatus === "read" && !read) return false; if (plugin.filterReadStatus === "unread" && read) return false; } if (plugin.filterRatingChips.size > 0) { const rating = getRating(listId); let match = false; if (plugin.filterRatingChips.has("rated") && rating > 0) match = true; if (plugin.filterRatingChips.has("unrated") && rating === 0) match = true; for (let i = 1; i <= 5; i++) if (plugin.filterRatingChips.has(`rating-${i}`) && rating === i) { match = true; break; } if (!match) return false; } return true; } function applyFilter$2(plugin) { const ul = document.querySelector("#lists > ul"); if (!ul) return; const lis = ul.querySelectorAll(":scope > li"); if (plugin.filterReadStatus === "all" && plugin.filterRatingChips.size === 0) { lis.forEach((li) => { const htmlLi = li; if (htmlLi.hasAttribute("data-lrs-hidden")) { htmlLi.removeAttribute(LRS_HIDDEN_ATTR); htmlLi.style.display = ""; } }); return; } lis.forEach((li) => { const htmlLi = li; if (htmlLi.style.display === "none" && !htmlLi.hasAttribute("data-lrs-hidden")) return; if (matchesFilter(plugin, htmlLi)) { htmlLi.removeAttribute(LRS_HIDDEN_ATTR); htmlLi.style.display = ""; } else { htmlLi.setAttribute(LRS_HIDDEN_ATTR, ""); htmlLi.style.display = "none"; } }); } function applySortAndFilter(plugin) { if (plugin.isToolbarProcessing) return; plugin.isToolbarProcessing = true; try { applySort(plugin); applyFilter$2(plugin); } finally { plugin.isToolbarProcessing = false; } } function countFilterStats() { const stats = { read: 0, unread: 0, rated: 0, unrated: 0, stars: [ 0, 0, 0, 0, 0 ] }; const ul = document.querySelector("#lists > ul"); if (!ul) return stats; ul.querySelectorAll(":scope > li").forEach((li) => { const id = li.id; if (!id) return; if (isRead(id)) stats.read++; else stats.unread++; const r = getRating(id); if (r > 0) { stats.rated++; if (r >= 1 && r <= 5) stats.stars[r - 1]++; } else stats.unrated++; }); return stats; } function createFilterChip$1(plugin, label, value, count) { const chip = document.createElement("span"); chip.className = "list-filter-chip"; chip.dataset.value = value; const labelText = document.createElement("span"); labelText.textContent = label; chip.appendChild(labelText); const countSpan = document.createElement("span"); countSpan.className = "chip-count"; countSpan.textContent = String(count); chip.appendChild(countSpan); plugin.supervisor.addEventListener(chip, "click", () => { if (value === "read" || value === "unread") { const wasActive = chip.classList.contains("active"); document.querySelectorAll(".list-filter-chip[data-value='read'], .list-filter-chip[data-value='unread']").forEach((c) => c.classList.remove("active")); if (!wasActive) { chip.classList.add("active"); saveFilterReadState(plugin, value); } else saveFilterReadState(plugin, "all"); } else { chip.classList.toggle("active"); const next = new Set(plugin.filterRatingChips); if (chip.classList.contains("active")) next.add(value); else next.delete(value); saveFilterRatingState(plugin, next); } applySortAndFilter(plugin); }); return chip; } function loadToolbarState(plugin) { const s = GM_getValue(SORT_KEY); if (s) plugin.currentSort = s; const r = GM_getValue(FILTER_READ_KEY); if (r === "read" || r === "unread") plugin.filterReadStatus = r; const rc = GM_getValue(FILTER_RATING_KEY); if (rc) try { const arr = JSON.parse(rc); if (Array.isArray(arr)) plugin.filterRatingChips = new Set(arr); } catch { plugin.filterRatingChips = new Set(); } } function saveSortState(plugin, v) { plugin.currentSort = v; GM_setValue(SORT_KEY, v); } function buildToolbar(plugin) { if (plugin.isDetailPage) return null; const lists = document.querySelector("#lists"); if (!lists) return null; if (document.querySelector(".list-toolbar")) return document.querySelector(".list-toolbar"); const toolbar = document.createElement("div"); toolbar.className = "list-toolbar"; const sortGroup = document.createElement("div"); sortGroup.className = "list-toolbar-group"; const sortLabel = document.createElement("span"); sortLabel.className = "list-toolbar-label"; sortLabel.textContent = "排序:"; const sortSelect = document.createElement("select"); sortSelect.className = "list-sort-select"; [ ["default", "默认"], ["rating-desc", "评分 ↓"], ["rating-asc", "评分 ↑"], ["movies-desc", "影片数 ↓"], ["movies-asc", "影片数 ↑"], ["clicks-desc", "点击数 ↓"], ["clicks-asc", "点击数 ↑"], ["visited-desc", "最近访问 ↓"], ["visited-asc", "最久访问 ↓"], ["title-asc", "标题 A-Z"] ].forEach(([val, txt]) => { const opt = document.createElement("option"); opt.value = val; opt.textContent = txt; if (val === plugin.currentSort) opt.selected = true; sortSelect.appendChild(opt); }); plugin.supervisor.addEventListener(sortSelect, "change", () => { saveSortState(plugin, sortSelect.value); applySortAndFilter(plugin); }); sortGroup.appendChild(sortLabel); sortGroup.appendChild(sortSelect); toolbar.appendChild(sortGroup); const div1 = document.createElement("div"); div1.className = "list-filter-divider"; toolbar.appendChild(div1); const readGroup = document.createElement("div"); readGroup.className = "list-toolbar-group"; const readLabel = document.createElement("span"); readLabel.className = "list-toolbar-label"; readLabel.textContent = "阅读:"; readGroup.appendChild(readLabel); const readChipsHost = document.createElement("div"); readGroup.appendChild(readChipsHost); toolbar.appendChild(readGroup); const div2 = document.createElement("div"); div2.className = "list-filter-divider"; toolbar.appendChild(div2); const ratingGroup = document.createElement("div"); ratingGroup.className = "list-toolbar-group"; const ratingLabel = document.createElement("span"); ratingLabel.className = "list-toolbar-label"; ratingLabel.textContent = "评分:"; ratingGroup.appendChild(ratingLabel); const ratingChipsHost = document.createElement("div"); ratingGroup.appendChild(ratingChipsHost); toolbar.appendChild(ratingGroup); const reset = document.createElement("a"); reset.className = "list-toolbar-reset"; reset.textContent = "重置筛选"; plugin.supervisor.addEventListener(reset, "click", (e) => { e.preventDefault(); saveSortState(plugin, "default"); saveFilterReadState(plugin, "all"); saveFilterRatingState(plugin, new Set()); sortSelect.value = "default"; toolbar.querySelectorAll(".list-filter-chip.active").forEach((c) => c.classList.remove("active")); applySortAndFilter(plugin); }); toolbar.appendChild(reset); lists.insertAdjacentElement("beforebegin", toolbar); toolbar._readHost = readChipsHost; toolbar._ratingHost = ratingChipsHost; toolbar._sortSelect = sortSelect; refreshChips$1(plugin, toolbar); console.log(`${LOG_PREFIX$2} 排序与筛选工具栏已挂载`); return toolbar; } function refreshChips$1(plugin, toolbar) { const stats = countFilterStats(); const readHost = toolbar._readHost; if (readHost) { readHost.innerHTML = ""; const readChip = createFilterChip$1(plugin, "已读完", "read", stats.read); if (plugin.filterReadStatus === "read") readChip.classList.add("active"); readHost.appendChild(readChip); const unreadChip = createFilterChip$1(plugin, "未读完", "unread", stats.unread); if (plugin.filterReadStatus === "unread") unreadChip.classList.add("active"); readHost.appendChild(unreadChip); } const ratingHost = toolbar._ratingHost; if (ratingHost) { ratingHost.innerHTML = ""; const ratedChip = createFilterChip$1(plugin, "有评分", "rated", stats.rated); if (plugin.filterRatingChips.has("rated")) ratedChip.classList.add("active"); ratingHost.appendChild(ratedChip); const unratedChip = createFilterChip$1(plugin, "无评分", "unrated", stats.unrated); if (plugin.filterRatingChips.has("unrated")) unratedChip.classList.add("active"); ratingHost.appendChild(unratedChip); for (let i = 1; i <= 5; i++) { const starChip = createFilterChip$1(plugin, `${i}星`, `rating-${i}`, stats.stars[i - 1]); if (plugin.filterRatingChips.has(`rating-${i}`)) starChip.classList.add("active"); ratingHost.appendChild(starChip); } } } function tryBuildToolbar(plugin) { if (plugin.isDetailPage) return false; const toolbar = buildToolbar(plugin); if (!toolbar) return false; refreshChips$1(plugin, toolbar); applySortAndFilter(plugin); return true; } function restoreToolbarUI(plugin) { const toolbar = document.querySelector(".list-toolbar"); if (!toolbar) return; const sortSelect = toolbar._sortSelect; if (sortSelect) sortSelect.value = plugin.currentSort; refreshChips$1(plugin, toolbar); } var list_reading_status_plugin_default = "/**\n * list-reading-status 插件样式 —— 阅读进度下拉框 + 星级评分 + 排序筛选工具栏。\n *\n * 原脚本 3 段 GM_addStyle(L263-303 下拉框 / L305-349 星级评分+访问链接 /\n * L353-445 工具栏),此处合并提取为独立 .css,由 initCss() 返回 →\n * PluginManager utils.insertStyle 注入。CSS 规则内容与原脚本一致(缩进标准化,\n * CSS 解析忽略空白,渲染等价)。\n */\n\n/* ---------- 阅读进度下拉框 ---------- */\n.list-reading-dropdown {\n appearance: none;\n -webkit-appearance: none;\n display: inline-flex;\n align-items: center;\n height: 24px;\n padding: 0 24px 0 10px;\n font-size: 0.72rem;\n font-weight: 600;\n line-height: 22px;\n border-radius: 50rem;\n cursor: pointer;\n user-select: none;\n border: 1.5px solid #ced4da;\n color: #6c757d;\n background-color: #fff;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%236c757d'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 8px center;\n background-size: 10px 6px;\n transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;\n margin-right: 6px;\n vertical-align: middle;\n}\n\n.list-reading-dropdown:hover {\n border-color: #adb5bd;\n background-color: #f8f9fa;\n}\n\n.list-reading-dropdown[data-status=\"read\"] {\n color: #fff;\n background-color: #198754;\n border-color: #198754;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23fff'/%3E%3C/svg%3E\");\n}\n\n.list-reading-dropdown[data-status=\"read\"]:hover {\n background-color: #157347;\n border-color: #157347;\n}\n\n/* ---------- 星级评分 ---------- */\n.list-rating-stars {\n display: inline-flex;\n gap: 2px;\n cursor: pointer;\n user-select: none;\n margin-right: 10px;\n vertical-align: middle;\n align-items: center;\n}\n\n.list-rating-star {\n width: 18px;\n height: 18px;\n flex-shrink: 0;\n transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1),\n filter 0.2s ease;\n}\n\n.list-rating-star path {\n fill: #cbd5e1;\n stroke: #94a3b8;\n stroke-width: 0.6;\n transition: fill 0.2s ease, stroke 0.2s ease;\n}\n\n.list-rating-star.filled path {\n fill: #f59e0b;\n stroke: #d97706;\n}\n\n.list-rating-star:hover {\n transform: scale(1.25);\n}\n\n.list-rating-star.filled:hover {\n filter: drop-shadow(0 0 5px rgb(245, 158, 11, 0.45));\n}\n\n.list-last-uri-link {\n font-size: 0.72rem;\n color: #485fc7;\n margin-left: 8px;\n text-decoration: none;\n transition: color 0.15s ease;\n}\n\n.list-last-uri-link:hover {\n color: #363636;\n text-decoration: underline;\n}\n\n/* ---------- 排序与筛选工具栏 ---------- */\n.list-toolbar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px 16px;\n padding: 10px 0;\n margin-bottom: 6px;\n border-bottom: 1px solid #e9ecef;\n font-size: 0.82rem;\n}\n\n.list-toolbar-group {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n}\n\n.list-toolbar-label {\n font-weight: 600;\n color: #6c757d;\n flex-shrink: 0;\n}\n\n.list-sort-select {\n appearance: none;\n -webkit-appearance: none;\n height: 26px;\n padding: 0 22px 0 10px;\n font-size: 0.78rem;\n font-weight: 600;\n border: 1.5px solid #ced4da;\n border-radius: 50rem;\n color: #495057;\n background-color: #fff;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%236c757d'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 8px center;\n background-size: 10px 6px;\n cursor: pointer;\n transition: background-color 0.2s ease, border-color 0.2s ease;\n}\n\n.list-sort-select:hover {\n border-color: #adb5bd;\n background-color: #f8f9fa;\n}\n\n.list-filter-chip {\n display: inline-flex;\n align-items: center;\n padding: 2px 10px;\n font-size: 0.76rem;\n font-weight: 600;\n border: 1.5px solid #dee2e6;\n border-radius: 50rem;\n color: #6c757d;\n background-color: #fff;\n cursor: pointer;\n user-select: none;\n transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;\n line-height: 1.4;\n}\n\n.list-filter-chip:hover {\n border-color: #adb5bd;\n background-color: #f8f9fa;\n}\n\n.list-filter-chip.active {\n color: #fff;\n background-color: #485fc7;\n border-color: #485fc7;\n}\n\n.list-filter-chip .chip-count {\n margin-left: 4px;\n opacity: 0.85;\n font-weight: 700;\n}\n\n.list-filter-divider {\n width: 1px;\n height: 18px;\n background: #dee2e6;\n margin: 0 4px;\n flex-shrink: 0;\n}\n\n.list-toolbar-reset {\n font-size: 0.74rem;\n color: #adb5bd;\n cursor: pointer;\n text-decoration: none;\n margin-left: auto;\n transition: color 0.15s ease;\n}\n\n.list-toolbar-reset:hover {\n color: #6c757d;\n text-decoration: underline;\n}\n"; var ListReadingStatusPlugin = class extends BasePlugin { isProcessing = false; currentSort = "default"; filterReadStatus = "all"; filterRatingChips = new Set(); isToolbarProcessing = false; orderCounter = 0; lastLiCount = -1; supervisor = new TaskSupervisor(); isDetailPage = false; getName() { return "ListReadingStatusPlugin"; } async initCss() { return list_reading_status_plugin_default; } async handle() { const pathname = location.pathname; const isListPage = pathname.startsWith("/users/favorite_lists") || pathname.startsWith("/users/lists"); const isDetail = /\/lists\/[^/]+$/.test(pathname); if (!isListPage && !isDetail) return; this.isDetailPage = isDetail; this.loadToolbarState(); if (this.isDetailPage) this.ensureHeaderWidgets(); else { this.processAllItems(); this.tryBuildToolbar(); } restoreFromIndexedDB().then((restored) => { if (restored) { if (this.isDetailPage) this.ensureHeaderWidgets(); else { this.processAllItems(); const tb = document.querySelector(".list-toolbar"); if (tb) this.refreshChips(tb); } } }); this.supervisor.observe(document.body, () => { if (this.isProcessing) return; if (this.isDetailPage) this.ensureHeaderWidgets(); else { this.processAllItems(); const ul = document.querySelector("#lists > ul"); const cnt = ul ? ul.querySelectorAll(":scope > li").length : 0; if (cnt !== this.lastLiCount) { this.lastLiCount = cnt; const tb = document.querySelector(".list-toolbar"); if (tb) this.refreshChips(tb); } } }, { childList: true, subtree: true }); if (typeof GM_addValueChangeListener !== "undefined") { const refreshListToolbar = () => { if (this.isDetailPage) return; const tb = document.querySelector(".list-toolbar"); if (tb) this.refreshChips(tb); }; GM_addValueChangeListener(STORAGE_KEY, () => { if (this.isDetailPage) this.ensureHeaderWidgets(); else { this.processAllItems(); refreshListToolbar(); } }); GM_addValueChangeListener(RATING_STORAGE_KEY, () => { if (this.isDetailPage) this.ensureHeaderWidgets(); else { this.processAllItems(); refreshListToolbar(); } }); GM_addValueChangeListener(SORT_KEY, () => { const v = GM_getValue(SORT_KEY); if (v) this.currentSort = v; if (this.isDetailPage) return; this.restoreToolbarUI(); this.applySortAndFilter(); }); GM_addValueChangeListener(FILTER_READ_KEY, () => { const v = GM_getValue(FILTER_READ_KEY); if (v === "read" || v === "unread") this.filterReadStatus = v; else this.filterReadStatus = "all"; if (this.isDetailPage) return; this.restoreToolbarUI(); this.applySortAndFilter(); }); GM_addValueChangeListener(FILTER_RATING_KEY, () => { const rc = GM_getValue(FILTER_RATING_KEY); try { const arr = JSON.parse(rc); this.filterRatingChips = new Set(Array.isArray(arr) ? arr : []); } catch { this.filterRatingChips = new Set(); } if (this.isDetailPage) return; this.restoreToolbarUI(); this.applySortAndFilter(); }); } } loadToolbarState() { loadToolbarState(this); } ensureHeaderWidgets() { ensureHeaderWidgets(this); } processAllItems() { return processAllItems(this); } tryBuildToolbar() { return tryBuildToolbar(this); } refreshChips(toolbar) { refreshChips$1(this, toolbar); } restoreToolbarUI() { restoreToolbarUI(this); } applySortAndFilter() { applySortAndFilter(this); } destroy() { this.supervisor.abort(); } }; var LOG_PREFIX$1 = "[modalListDisabler]"; var TARGET_LIST_ID = 501; var CONTAINER_SELECTOR = "#modal-save-list > div.modal-card.fixed-height-600 > section > div > div:nth-child(1)"; var ModalListDisablerPlugin = class extends BasePlugin { supervisor = new TaskSupervisor(); getName() { return "ModalListDisablerPlugin"; } async initCss() { return ""; } async handle() { this.tryDisable(); this.supervisor.observe(document.body, () => { this.tryDisable(); }, { childList: true, subtree: true }); } destroy() { this.supervisor.abort(); } disableTargetItem(container) { let found = false; for (const child of Array.from(container.children)) { const span = child.querySelector("label > span"); if (!span) continue; const match = span.innerHTML.match(/\((\d+)\)/); if (match && parseInt(match[1], 10) === TARGET_LIST_ID) { const input = child.querySelector("label > input"); if (input && !input.disabled && !input.checked) { input.disabled = true; console.log(`${LOG_PREFIX$1} 已禁用清单 #${TARGET_LIST_ID}`); } found = true; } } return found; } tryDisable() { const container = document.querySelector(CONTAINER_SELECTOR); if (!container) return false; return this.disableTargetItem(container); } }; var TAG_HIDDEN_ATTR = "data-video-lists-tag-hidden"; var FILTER_MODES = { CONTAINS_ANY: "contains-any", CONTAINS_ALL: "contains-all", EXCLUDES_ALL: "excludes-all", EXCLUDES_ANY: "excludes-any" }; function collectAllUniqueTags() { const tagNames = new Set(); document.querySelectorAll(".jhs-vlt-tag-link:not([style*='pointer-events: none'])").forEach((el) => { const name = el.textContent?.trim() || ""; if (name && name !== "—") tagNames.add(name); }); return [...tagNames].sort(); } function collectTagCounts() { const counts = {}; document.querySelectorAll(".jhs-vlt-tag-link:not([style*='pointer-events: none'])").forEach((el) => { const name = el.textContent?.trim() || ""; if (name && name !== "—") counts[name] = (counts[name] || 0) + 1; }); return counts; } function countNoTagItems() { let count = 0; document.querySelectorAll(".item").forEach((item) => { if (item.querySelectorAll(".jhs-vlt-tag-link:not([style*='pointer-events: none'])").length === 0) count++; }); return count; } function applyFilter$1(plugin) { const activeChips = document.querySelectorAll(".jhs-vlt-filter-chip.jhs-vlt-active"); const selectedValues = new Set(Array.from(activeChips).map((c) => c.dataset.value || "")); if (selectedValues.size === 0) { document.querySelectorAll(`.item[${TAG_HIDDEN_ATTR}]`).forEach((item) => { item.removeAttribute(TAG_HIDDEN_ATTR); item.style.display = ""; }); return; } const showNoTag = selectedValues.has("no-tag"); const selectedTags = new Set([...selectedValues].filter((v) => v !== "no-tag")); document.querySelectorAll(".item").forEach((item) => { const htmlItem = item; if (htmlItem.style.display === "none" && !htmlItem.hasAttribute("data-video-lists-tag-hidden")) return; const tagLinks = item.querySelectorAll(".jhs-vlt-tag-link:not([style*='pointer-events: none'])"); const itemTagNames = new Set(Array.from(tagLinks).map((el) => el.textContent?.trim() || "")); let tagMatch = false; if (selectedTags.size > 0) switch (plugin.currentFilterMode) { case FILTER_MODES.CONTAINS_ANY: tagMatch = [...selectedTags].some((t) => itemTagNames.has(t)); break; case FILTER_MODES.CONTAINS_ALL: tagMatch = [...selectedTags].every((t) => itemTagNames.has(t)); break; case FILTER_MODES.EXCLUDES_ALL: tagMatch = ![...selectedTags].some((t) => itemTagNames.has(t)); break; case FILTER_MODES.EXCLUDES_ANY: tagMatch = ![...selectedTags].every((t) => itemTagNames.has(t)); break; default: tagMatch = [...selectedTags].some((t) => itemTagNames.has(t)); } const noTagMatch = showNoTag && itemTagNames.size === 0; if (tagMatch || noTagMatch) { htmlItem.removeAttribute(TAG_HIDDEN_ATTR); htmlItem.style.display = ""; } else { htmlItem.setAttribute(TAG_HIDDEN_ATTR, ""); htmlItem.style.display = "none"; } }); } var FILTER_MODE_CONFIG = [ { value: FILTER_MODES.CONTAINS_ANY, label: "包含任意一个", group: "include" }, { value: FILTER_MODES.CONTAINS_ALL, label: "全都包含", group: "include" }, { value: FILTER_MODES.EXCLUDES_ALL, label: "不包含以下标签", group: "exclude" }, { value: FILTER_MODES.EXCLUDES_ANY, label: "不包含以下任意一个", group: "exclude" } ]; function buildFilterModeDropdown(plugin) { const details = document.createElement("details"); details.className = "jhs-vlt-filter-mode-dropdown"; const summary = document.createElement("summary"); summary.className = "jhs-vlt-filter-mode-summary"; const indicator = document.createElement("span"); indicator.className = "jhs-vlt-filter-mode-indicator"; summary.appendChild(indicator); const modeText = document.createElement("span"); modeText.className = "jhs-vlt-filter-mode-text"; summary.appendChild(modeText); const arrow = document.createElement("span"); arrow.className = "jhs-vlt-filter-mode-arrow"; arrow.textContent = "▼"; summary.appendChild(arrow); details.appendChild(summary); const panel = document.createElement("div"); panel.className = "jhs-vlt-filter-mode-panel"; const updateModeUI = () => { const config = FILTER_MODE_CONFIG.find((m) => m.value === plugin.currentFilterMode) || FILTER_MODE_CONFIG[0]; modeText.textContent = config.label; indicator.className = "jhs-vlt-filter-mode-indicator jhs-vlt-" + config.group; panel.querySelectorAll(".jhs-vlt-filter-mode-item").forEach((el) => { el.classList.toggle("jhs-vlt-selected", el.dataset.mode === plugin.currentFilterMode); }); }; let lastGroup = null; FILTER_MODE_CONFIG.forEach((mode) => { if (lastGroup && lastGroup !== mode.group) { const separator = document.createElement("div"); separator.className = "jhs-vlt-filter-mode-separator"; panel.appendChild(separator); } lastGroup = mode.group; const item = document.createElement("div"); item.className = "jhs-vlt-filter-mode-item"; if (mode.group === "exclude") item.classList.add("jhs-vlt-exclude"); item.dataset.mode = mode.value; const dot = document.createElement("span"); dot.className = "jhs-vlt-filter-mode-indicator jhs-vlt-" + mode.group; item.appendChild(dot); const text = document.createElement("span"); text.textContent = mode.label; item.appendChild(text); item.addEventListener("click", () => { plugin.currentFilterMode = mode.value; updateModeUI(); details.open = false; applyFilter$1(plugin); }); panel.appendChild(item); }); details.appendChild(panel); details._updateModeUI = updateModeUI; updateModeUI(); return details; } function createFilterChip(plugin, labelText, value, opts = {}) { const { isNoTag = false, count } = opts; const chip = document.createElement("span"); chip.className = "jhs-vlt-filter-chip"; if (isNoTag) chip.classList.add("jhs-vlt-no-tag"); chip.textContent = count !== void 0 ? `${labelText} ${count}` : labelText; chip.dataset.value = value; chip.addEventListener("click", () => { chip.classList.toggle("jhs-vlt-active"); applyFilter$1(plugin); }); return chip; } function refreshChips(plugin, chipsContainer) { const activeValues = new Set(Array.from(chipsContainer.querySelectorAll(".jhs-vlt-filter-chip.jhs-vlt-active")).map((c) => c.dataset.value || "")); chipsContainer.innerHTML = ""; const allCounts = collectTagCounts(); const noTagChip = createFilterChip(plugin, "无标签", "no-tag", { isNoTag: true, count: countNoTagItems() }); if (activeValues.has("no-tag")) noTagChip.classList.add("jhs-vlt-active"); chipsContainer.appendChild(noTagChip); collectAllUniqueTags().forEach((tagName) => { const chip = createFilterChip(plugin, tagName, tagName, { count: allCounts[tagName] }); if (activeValues.has(tagName)) chip.classList.add("jhs-vlt-active"); chipsContainer.appendChild(chip); }); } function buildFilterBar(plugin) { if (document.querySelector(".jhs-vlt-filter-bar")) return; const mountTarget = /^\/actors\//.test(window.location.pathname) ? document.querySelector("body > section > div > div.actor-tags.tags") : document.querySelector("body > section > div > div.tabs.is-boxed"); if (!mountTarget) return; const filterBar = document.createElement("div"); filterBar.className = "jhs-vlt-filter-bar"; const label = document.createElement("span"); label.className = "jhs-vlt-filter-label"; label.textContent = "筛选:"; filterBar.appendChild(label); const modeDropdown = buildFilterModeDropdown(plugin); filterBar.appendChild(modeDropdown); filterBar._modeDropdown = modeDropdown; const chipsContainer = document.createElement("div"); chipsContainer.className = "jhs-vlt-filter-chips"; filterBar.appendChild(chipsContainer); const toggleWrapper = document.createElement("label"); toggleWrapper.className = "jhs-vlt-auto-refresh-toggle"; toggleWrapper.title = "监听清单操作,自动刷新标签"; const switchSpan = document.createElement("span"); switchSpan.className = "jhs-vlt-auto-refresh-switch"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = plugin.autoRefreshEnabled; const slider = document.createElement("span"); slider.className = "jhs-vlt-auto-refresh-slider"; switchSpan.appendChild(checkbox); switchSpan.appendChild(slider); const toggleLabel = document.createElement("span"); toggleLabel.textContent = "自动刷新"; toggleWrapper.appendChild(switchSpan); toggleWrapper.appendChild(toggleLabel); filterBar.appendChild(toggleWrapper); checkbox.addEventListener("change", () => { plugin.autoRefreshEnabled = checkbox.checked; plugin.saveAutoRefreshState(plugin.autoRefreshEnabled); if (plugin.autoRefreshEnabled) console.log("[视频清单标签] 自动刷新已开启"); else console.log("[视频清单标签] 自动刷新已关闭"); }); const boundRefreshChips = () => refreshChips(plugin, chipsContainer); boundRefreshChips(); mountTarget.insertAdjacentElement("afterend", filterBar); filterBar._refreshChips = boundRefreshChips; } function updateFilterBar(plugin) { const filterBar = document.querySelector(".jhs-vlt-filter-bar"); if (filterBar && filterBar._refreshChips) { filterBar._refreshChips(); applyFilter$1(plugin); } } var BOOTSTRAP_COLORS = [ { name: "primary", bg: "#0d6efd", text: "#fff" }, { name: "secondary", bg: "#6c757d", text: "#fff" }, { name: "success", bg: "#198754", text: "#fff" }, { name: "danger", bg: "#dc3545", text: "#fff" }, { name: "warning", bg: "#ffc107", text: "#212529" }, { name: "info", bg: "#0dcaf0", text: "#212529" }, { name: "dark", bg: "#212529", text: "#fff" } ]; function getRandomBootstrapColor() { const color = BOOTSTRAP_COLORS[Math.floor(Math.random() * BOOTSTRAP_COLORS.length)]; return { bg: color.bg, text: color.text, isLight: color.text !== "#fff" }; } function tagsEqual(a, b) { if (a === b) return true; if (!a || !b) return false; if (a.length !== b.length) return false; return JSON.stringify(a) === JSON.stringify(b); } function createTagBadge(tag) { const urlTrimmed = tag.url?.trim(); const hasUrl = !!urlTrimmed; const el = document.createElement(hasUrl ? "a" : "span"); el.className = "jhs-vlt-tag-link"; if (hasUrl && urlTrimmed) { const anchor = el; anchor.href = urlTrimmed; anchor.target = "_blank"; anchor.rel = "noopener noreferrer"; } el.textContent = tag.name; const style = tag.style; if (style && style.bg && style.text) { el.style.backgroundColor = style.bg; el.style.color = style.text; if (style.text !== "#fff") el.classList.add("jhs-vlt-light-bg"); } else { const color = getRandomBootstrapColor(); el.style.backgroundColor = color.bg; el.style.color = color.text; if (color.isLight) el.classList.add("jhs-vlt-light-bg"); } return el; } function addTagDisplay(plugin, item) { if (item.querySelector(".jhs-vlt-tags-display")) return; const strongEl = item.querySelector("a > div.video-title > strong"); if (!strongEl) return; const designation = strongEl.innerHTML; const tagsCache = plugin.getTagsCache(); if (!tagsCache) return; const meta = item.querySelector(".meta"); if (!meta) return; const container = document.createElement("div"); container.className = "jhs-vlt-tags-display"; const tags = tagsCache[designation]; if (tags && tags.length > 0) tags.forEach((tag) => { container.appendChild(createTagBadge(tag)); }); else { const placeholder = document.createElement("span"); placeholder.className = "jhs-vlt-tag-link"; placeholder.textContent = "—"; placeholder.style.backgroundColor = "#e9ecef"; placeholder.style.color = "#6c757d"; placeholder.style.pointerEvents = "none"; placeholder.style.userSelect = "none"; container.appendChild(placeholder); } container.addEventListener("wheel", (e) => { if (e.deltaY !== 0) { const atStart = container.scrollLeft <= 0 && e.deltaY < 0; const atEnd = container.scrollLeft + container.clientWidth >= container.scrollWidth - 1 && e.deltaY > 0; if (!atStart && !atEnd) { e.preventDefault(); container.scrollLeft += e.deltaY; } } }, { passive: false }); meta.insertAdjacentElement("afterend", container); } function renderTags(plugin) { document.querySelectorAll(".item").forEach((item) => addTagDisplay(plugin, item)); } var LAST_SYNC_KEY = "jdb:last-sync"; var NO_CHANGE_ASSOCS = { existed: true, limit_exceeded: true, unchanged: true }; var SYNC_DEBOUNCE_MS = 500; function handleSyncNotify(plugin, payload, onSync, state) { if (!plugin.autoRefreshEnabled) { console.log("[视频清单标签] 收到同步事件但自动刷新已关闭,跳过"); return; } if (!payload || !payload.designation) { console.warn("[视频清单标签] 收到同步事件但无 designation:", payload); return; } if (NO_CHANGE_ASSOCS[payload.association ?? ""]) { console.log(`[视频清单标签] association=${payload.association} 不影响标签,跳过`); return; } const now = Date.now(); if (now - state.lastRefreshTime < SYNC_DEBOUNCE_MS) { console.log(`[视频清单标签] 距上次刷新 ${now - state.lastRefreshTime}ms < ${SYNC_DEBOUNCE_MS}ms,防抖跳过`); return; } state.lastRefreshTime = now; console.log("[视频清单标签] 收到同步事件 → 精准刷新", payload); onSync(payload); } function setupAutoRefreshListener(plugin, onSync) { const state = { lastRefreshTime: 0 }; broadcastSubscribe(LAST_SYNC_KEY, (payload) => handleSyncNotify(plugin, payload, onSync, state), { eventName: "jdb:sync-complete", onDelivery: (delivery) => { if (delivery.channel === "customEvent") console.log("[视频清单标签] [CustomEvent] 收到 jdb:sync-complete"); else if (delivery.channel === "storage") console.log(`[视频清单标签] [storage] key=${LAST_SYNC_KEY} newValue=${delivery.rawValue ? "<set>" : "<empty>"}`); else console.log(`[视频清单标签] [GM_addValueChangeListener] remote=${delivery.remote} newValue=${delivery.rawValue ? "<set>" : "<empty>"}`); } }); console.log(`[视频清单标签] 自动刷新监听已就绪 (autoRefreshEnabled=${plugin.autoRefreshEnabled})`); } var AUTO_REFRESH_KEY = "jdb:auto-refresh-enabled"; var VltTags = class { tagsCache = null; currentFilterMode = "contains-any"; autoRefreshEnabled = true; pendingNewItems = []; debounceTimer = null; DEBOUNCE_MS = 300; getTagsCache() { return this.tagsCache; } async fetchTags(designations) { this.tagsCache = await VltDb.queryMoviesLists(designations); } async fetchAndMergeTags(designations) { const newTags = await VltDb.queryMoviesLists(designations); if (!this.tagsCache) this.tagsCache = newTags; else Object.assign(this.tagsCache, newTags); } addTagDisplay(item) { addTagDisplay(this, item); } async refreshAllTags() { const items = document.querySelectorAll(".item"); const lists = Array.from(items).map((item) => { const strong = item.querySelector("a > div.video-title > strong"); return strong ? strong.innerHTML : null; }).filter((s) => !!s); if (lists.length === 0) return; console.log(`[视频清单标签] 开始刷新,共 ${lists.length} 个番号`); try { await this.fetchTags(lists); } catch (err) { clog.error(`自动刷新标签数据失败(${lists.length} 个番号):`, err); return; } document.querySelectorAll(".jhs-vlt-tags-display").forEach((el) => el.remove()); renderTags(this); this.updateFilterBar(); console.log("[视频清单标签] 自动刷新完成"); } async refreshDesignation(designation) { console.log(`[视频清单标签] refreshDesignation: ${designation}`); const allItems = document.querySelectorAll(".item"); const targetItem = Array.from(allItems).find((item) => { const strong = item.querySelector("a > div.video-title > strong"); return strong && strong.innerHTML === designation; }); if (!targetItem) { console.log(`[视频清单标签] ${designation} 不在当前页面 (共 ${allItems.length} 张卡片)`); return; } const oldTags = this.tagsCache ? this.tagsCache[designation] || [] : null; try { await this.fetchAndMergeTags([designation]); } catch (err) { clog.error(`[视频清单标签] 刷新 ${designation} 失败:`, err); return; } const newTags = this.tagsCache[designation] || []; if (tagsEqual(oldTags, newTags)) { console.log(`[视频清单标签] ${designation} 标签未变化,跳过 DOM 更新 (${newTags.length} 个标签)`); return; } const oldContainer = targetItem.querySelector(".jhs-vlt-tags-display"); if (oldContainer) oldContainer.remove(); this.addTagDisplay(targetItem); this.updateFilterBar(); console.log(`[视频清单标签] ${designation} 标签已更新 (${oldTags ? oldTags.length : 0} → ${newTags.length})`); } loadAutoRefreshState() { try { const val = GM_getValue(AUTO_REFRESH_KEY); if (val === void 0) { this.autoRefreshEnabled = true; console.log("[视频清单标签] 自动刷新: 未设置过 → 默认开启"); } else { this.autoRefreshEnabled = val === true || val === "true"; console.log(`[视频清单标签] 自动刷新: 存储值 = ${this.autoRefreshEnabled} (原始=${JSON.stringify(val)})`); } } catch (e) { this.autoRefreshEnabled = true; console.warn("[视频清单标签] 自动刷新: 读取异常 → 默认开启", e); } } saveAutoRefreshState(enabled) { try { GM_setValue(AUTO_REFRESH_KEY, enabled); } catch {} } applyFilter() { applyFilter$1(this); } buildFilterBar() { buildFilterBar(this); } updateFilterBar() { updateFilterBar(this); } collectAllUniqueTags() { return collectAllUniqueTags(); } collectTagCounts() { return collectTagCounts(); } countNoTagItems() { return countNoTagItems(); } async initExistingItems() { this.loadAutoRefreshState(); const items = document.querySelectorAll(".item"); if (items.length === 0) return; const lists = Array.from(items).map((item) => { const strong = item.querySelector("a > div.video-title > strong"); return strong ? strong.innerHTML : null; }).filter((s) => !!s); if (lists.length === 0) return; try { await this.fetchTags(lists); console.log(`[视频清单标签] 初始化加载完成: ${lists.length} 个番号, ${Object.keys(this.tagsCache || {}).length} 个有标签`); } catch (err) { clog.error("获取标签数据失败:", err); this.tagsCache = {}; return; } renderTags(this); this.buildFilterBar(); } async handleNewItems(newItems) { newItems.forEach((item) => this.addTagDisplay(item)); this.pendingNewItems.push(...newItems); if (this.debounceTimer) clearTimeout(this.debounceTimer); this.debounceTimer = setTimeout(async () => { const itemsToRefresh = [...this.pendingNewItems]; this.pendingNewItems = []; this.debounceTimer = null; const uncachedCodes = []; const seen = new Set(); for (const item of itemsToRefresh) { const strong = item.querySelector("a > div.video-title > strong"); if (!strong) continue; const code = strong.innerHTML; if (!code || seen.has(code)) continue; if (!this.tagsCache || !(code in this.tagsCache)) { uncachedCodes.push(code); seen.add(code); } } if (uncachedCodes.length === 0) { this.updateFilterBar(); return; } try { await this.fetchAndMergeTags(uncachedCodes); } catch (err) { clog.error("[视频清单标签] 获取新增标签数据失败:", err); return; } itemsToRefresh.forEach((item) => { const oldContainer = item.querySelector(".jhs-vlt-tags-display"); if (oldContainer) oldContainer.remove(); this.addTagDisplay(item); }); this.updateFilterBar(); console.log(`[视频清单标签] 已为 ${uncachedCodes.length} 个新番号获取标签`); }, this.DEBOUNCE_MS); } setupAutoRefreshListener(onSync) { setupAutoRefreshListener(this, onSync); } }; var LOG_PREFIX = "[JavDB]"; var LIST_MGMT_KEY = "jdb:list-mgmt"; function setupListMgmtBroadcastListener(onDelete, onRename) { const handlePayload = (payload) => { if (!payload || !payload.type || !payload.listId) return; console.log(`${LOG_PREFIX} 收到清单管理广播:`, payload); if (payload.type === "delete") onDelete(payload.listId); else if (payload.type === "rename" && payload.newName) onRename(payload.listId, payload.newName); }; broadcastSubscribe(LIST_MGMT_KEY, (payload) => handlePayload(payload)); } var video_lists_tag_default = "/**\n * video-lists-tag 插件样式 —— 标签显示 + 筛选栏 + Toast。\n *\n * 合并自 archetype/videoListsTag.user.js 的 GM_addStyle(L24-275)\n * 和 archetype/listsOptionSync.user.js 的 injectToastStyles(L40-115)。\n * CSS 规则内容与原脚本一致(缩进标准化,渲染等价)。\n */\n\n/* ---------- 标签显示(Bootstrap Badge Pill 风格) ---------- */\n.jhs-vlt-tags-display {\n margin: 5px 0;\n padding: 6px;\n white-space: nowrap;\n overflow-x: auto;\n overflow-y: hidden;\n scrollbar-width: none;\n -webkit-overflow-scrolling: touch;\n padding-bottom: 2px;\n}\n\n.jhs-vlt-tags-display::-webkit-scrollbar {\n display: none;\n}\n\n.jhs-vlt-tag-link {\n display: inline-block;\n padding: 0.35em 0.65em;\n margin-right: 6px;\n font-size: 0.75em;\n font-weight: 700;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 50rem;\n text-decoration: none;\n color: #fff;\n transition:\n filter 0.15s ease,\n transform 0.15s ease;\n box-shadow: 0 1px 2px rgb(0, 0, 0, 0.1);\n}\n\n.jhs-vlt-tag-link:hover {\n filter: brightness(0.92);\n transform: translateY(-1px);\n box-shadow: 0 2px 5px rgb(0, 0, 0, 0.15);\n}\n\n.jhs-vlt-tag-link.jhs-vlt-light-bg {\n color: #212529;\n}\n\n/* ---------- 标签筛选栏 ---------- */\n\n.jhs-vlt-filter-chip.jhs-vlt-active {\n background: #0d6efd;\n border-color: #0d6efd;\n color: #fff;\n}\n\n.jhs-vlt-filter-chip.jhs-vlt-no-tag {\n border-style: dashed;\n color: #6c757d;\n}\n\n.jhs-vlt-filter-chip.jhs-vlt-no-tag.jhs-vlt-active {\n background: #6c757d;\n border-color: #6c757d;\n border-style: solid;\n color: #fff;\n}\n\n/* ---------- 自动刷新开关 ---------- */\n.jhs-vlt-auto-refresh-toggle {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-left: auto;\n flex-shrink: 0;\n font-size: 0.78em;\n color: #6c757d;\n user-select: none;\n}\n\n.jhs-vlt-auto-refresh-switch {\n position: relative;\n width: 36px;\n height: 20px;\n flex-shrink: 0;\n}\n\n.jhs-vlt-auto-refresh-switch input {\n opacity: 0;\n width: 0;\n height: 0;\n}\n\n.jhs-vlt-auto-refresh-slider {\n position: absolute;\n inset: 0;\n background: #ced4da;\n border-radius: 50rem;\n cursor: pointer;\n transition: background 0.2s;\n}\n\n.jhs-vlt-auto-refresh-slider::before {\n content: '';\n position: absolute;\n left: 2px;\n top: 2px;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: #fff;\n transition: transform 0.2s;\n}\n\n.jhs-vlt-auto-refresh-switch input:checked + .jhs-vlt-auto-refresh-slider {\n background: #198754;\n}\n\n.jhs-vlt-auto-refresh-switch input:checked + .jhs-vlt-auto-refresh-slider::before {\n transform: translateX(16px);\n}\n\n/* ---------- 筛选模式下拉 ---------- */\n.jhs-vlt-filter-mode-dropdown {\n position: relative;\n display: inline-block;\n flex-shrink: 0;\n}\n\n.jhs-vlt-filter-mode-summary {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 0.25em 0.7em;\n font-size: 0.75em;\n font-weight: 600;\n line-height: 1.5;\n border-radius: 50rem;\n cursor: pointer;\n user-select: none;\n border: 1.5px solid #dee2e6;\n background: #fff;\n color: #495057;\n transition: background-color 0.15s ease, border-color 0.15s ease;\n list-style: none;\n}\n\n.jhs-vlt-filter-mode-summary::-webkit-details-marker {\n display: none;\n}\n\n.jhs-vlt-filter-mode-summary:hover {\n border-color: #adb5bd;\n background: #f8f9fa;\n}\n\n.jhs-vlt-filter-mode-indicator {\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n.jhs-vlt-filter-mode-indicator.jhs-vlt-include {\n background: #0d6efd;\n box-shadow: 0 0 0 2px rgb(13, 110, 253, 0.2);\n}\n\n.jhs-vlt-filter-mode-indicator.jhs-vlt-exclude {\n background: #dc3545;\n box-shadow: 0 0 0 2px rgb(220, 53, 69, 0.2);\n}\n\n.jhs-vlt-filter-mode-arrow {\n font-size: 0.7em;\n color: #adb5bd;\n transition: transform 0.2s;\n}\n\n.jhs-vlt-filter-mode-dropdown[open] .jhs-vlt-filter-mode-arrow {\n transform: rotate(180deg);\n}\n\n.jhs-vlt-filter-mode-panel {\n position: absolute;\n top: calc(100% + 4px);\n left: 0;\n min-width: 190px;\n background: #fff;\n border: 1px solid #dee2e6;\n border-radius: 8px;\n box-shadow: 0 4px 16px rgb(0, 0, 0, 0.12);\n padding: 4px 0;\n z-index: 1000;\n animation: jhs-vlt-filter-mode-in 0.15s ease;\n}\n\n@keyframes jhs-vlt-filter-mode-in {\n from {\n opacity: 0;\n transform: translateY(-4px);\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.jhs-vlt-filter-mode-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 14px;\n font-size: 0.78em;\n font-weight: 500;\n color: #495057;\n cursor: pointer;\n transition: background 0.1s;\n white-space: nowrap;\n}\n\n.jhs-vlt-filter-mode-item:hover {\n background: #f8f9fa;\n}\n\n.jhs-vlt-filter-mode-item.jhs-vlt-selected {\n background: rgb(13, 110, 253, 0.08);\n color: #0d6efd;\n font-weight: 600;\n}\n\n.jhs-vlt-filter-mode-item.jhs-vlt-exclude.jhs-vlt-selected {\n background: rgb(220, 53, 69, 0.08);\n color: #dc3545;\n}\n\n.jhs-vlt-filter-mode-item .jhs-vlt-filter-mode-indicator {\n flex-shrink: 0;\n}\n\n.jhs-vlt-filter-mode-separator {\n height: 1px;\n margin: 4px 8px;\n background: #e9ecef;\n}\n\n/* ---------- Toast 通知 ---------- */\n#jdb-toast-container {\n position: fixed;\n top: 72px;\n left: 20px;\n z-index: var(--jhs-z-page);\n display: flex;\n flex-direction: column;\n pointer-events: none;\n}\n\n.jdb-toast {\n display: flex;\n align-items: center;\n gap: 10px;\n min-width: 220px;\n max-width: 380px;\n max-height: 200px;\n padding: 12px 16px;\n border-radius: 8px;\n font-size: 13px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n color: #fff;\n line-height: 1.4;\n box-shadow: 0 4px 16px rgb(0, 0, 0, 0.28);\n backdrop-filter: blur(6px);\n pointer-events: auto;\n overflow: hidden;\n margin-bottom: 8px;\n animation: jdb-toast-in 0.2s cubic-bezier(0.21, 1.02, 0.73, 1) forwards;\n transition:\n opacity 0.2s,\n transform 0.2s,\n max-height 0.2s,\n margin-bottom 0.2s;\n}\n\n.jdb-toast:last-child {\n margin-bottom: 0;\n}\n\n.jdb-toast.removing {\n opacity: 0;\n max-height: 0;\n margin-bottom: 0;\n padding-top: 0;\n padding-bottom: 0;\n transform: translateX(-20px);\n}\n\n.jdb-toast--stacked {\n max-height: 10px;\n opacity: 0.35;\n transform: translateX(12px);\n margin-bottom: 2px;\n cursor: pointer;\n}\n\n.jdb-toast--success {\n background: rgb(22, 163, 74, 0.92);\n}\n\n.jdb-toast--error {\n background: rgb(220, 38, 38, 0.92);\n}\n\n.jdb-toast--warning {\n background: rgb(217, 119, 6, 0.92);\n}\n\n.jdb-toast--info {\n background: rgb(37, 99, 235, 0.92);\n}\n\n.jdb-toast__icon {\n flex-shrink: 0;\n width: 20px;\n height: 20px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.jdb-toast__msg {\n flex: 1;\n word-break: break-word;\n}\n\n@keyframes jdb-toast-in {\n from {\n opacity: 0;\n transform: translateX(-30px);\n }\n\n to {\n opacity: 1;\n transform: translateX(0);\n }\n}\n\n/* ---------- 新增清單(詳情頁展開面板旁) ---------- */\n.jhs-list-panel:has(+ .jhs-list-create-wrap) {\n border-radius: 10px 10px 0 0;\n}\n\n.jhs-list-create-wrap {\n display: flex;\n align-items: center;\n gap: 6px;\n min-height: 44px;\n margin: -1px 0 10px;\n padding: 7px 10px;\n border: 1px solid #dfe4ea;\n border-top-color: #edf0f3;\n border-radius: 0 0 10px 10px;\n background: #f6f8fa;\n flex-wrap: wrap;\n}\n\n.jhs-list-create-btn {\n min-height: 32px;\n font-size: 12px;\n font-weight: 600;\n}\n\n.jhs-list-create-form {\n display: flex;\n min-width: 0;\n flex: 1;\n align-items: center;\n gap: 6px;\n flex-wrap: nowrap;\n}\n\n.jhs-list-create-form .input.is-small {\n width: auto;\n min-width: 120px;\n flex: 1;\n font-size: 12px;\n}\n\n.jhs-list-create-wrap[aria-busy='true'] {\n cursor: wait;\n}\n\n.jhs-list-create-wrap .button:focus-visible,\n.jhs-list-create-wrap .input:focus-visible {\n outline: 2px solid #3273dc;\n outline-offset: 2px;\n}\n\n.jhs-visually-hidden {\n position: absolute !important;\n width: 1px !important;\n height: 1px !important;\n padding: 0 !important;\n margin: -1px !important;\n overflow: hidden !important;\n clip: rect(0, 0, 0, 0) !important;\n white-space: nowrap !important;\n border: 0 !important;\n}\n\n@media (width <= 768px) {\n .jhs-list-create-wrap .button,\n .jhs-list-create-wrap .input.is-small {\n min-height: 44px;\n }\n\n .jhs-list-create-form {\n width: 100%;\n flex-wrap: wrap;\n }\n\n .jhs-list-create-form .input.is-small {\n flex-basis: 100%;\n }\n}\n"; var VideoListsTagPlugin = class extends BasePlugin { vltTags = null; observer = null; getName() { return "VideoListsTagPlugin"; } async initCss() { return video_lists_tag_default; } async handle() { if (window.isDetailPage) { const settings = await storageManager.getSetting(); setAutoReconcileDelay(Number(settings.autoReconcileDelay)); setAutoServerSyncEnabled(settings.autoServerSync !== "no"); setAutoFavCleanupEnabled(settings.autoFavCleanupEnabled !== "no"); setProtectedKeywords(String(settings.protectedListKeywords || "值得复盘").split(",").map((s) => s.trim()).filter(Boolean)); setupCheckboxListener(); setupCreateListButton(); setupListMgmtBroadcastListener((listId) => removeDetailPageCheckbox(listId), (listId, newName) => updateDetailPageCheckboxLabel(listId, newName)); return; } if (window.location.href.includes("/users/lists")) { setupListManagementListener(); return; } if (!window.isListPage) return; this.vltTags = new VltTags(); await this.vltTags.initExistingItems(); this.setupItemObserver(); setupListMgmtBroadcastListener(() => this.vltTags?.refreshAllTags().then(), () => this.vltTags?.refreshAllTags().then()); this.vltTags.setupAutoRefreshListener((payload) => { if (payload?.designation) this.vltTags?.refreshDesignation(payload.designation).then(); }); } setupItemObserver() { this.observer = new MutationObserver((mutations) => { const newItems = []; for (const mutation of mutations) for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue; const el = node; if (el.matches?.(".item")) newItems.push(el); else if (el.querySelectorAll?.(".item").length > 0) newItems.push(...Array.from(el.querySelectorAll(".item"))); } if (newItems.length > 0) this.vltTags?.handleNewItems(newItems).then(); }); this.observer.observe(document.body, { childList: true, subtree: true }); } }; function getPayloadRevision(payload) { const revision = payload.revision ?? payload.ts; return Number.isInteger(revision) && revision >= 0 ? revision : -1; } function isDeltaPayload(value) { if (value === null || typeof value !== "object" || Array.isArray(value)) return false; const payload = value; if (payload.action !== "upsert" && payload.action !== "delete") return false; if (!Array.isArray(payload.items) || payload.items.length === 0) return false; if (typeof payload.ts !== "number" || !Number.isInteger(payload.ts) || payload.ts < 0) return false; if (payload.revision !== void 0 && (typeof payload.revision !== "number" || !Number.isInteger(payload.revision) || payload.revision < 0)) return false; return payload.items.every((item) => { if (item === null || typeof item !== "object" || Array.isArray(item)) return false; const record = item; if (typeof record.carNum !== "string" || record.carNum.trim() === "") return false; if (payload.action === "delete") return true; return typeof record.status === "string" && record.status.trim() !== "" && STATUS_LIST.includes(record.status) && typeof record.url_path === "string" && record.url_path.trim() !== ""; }); } function readDeltaJournal(value) { return (Array.isArray(value) ? value : value ? [value] : []).filter(isDeltaPayload); } function sortDeltaJournal(journal) { return journal.map((payload, index) => ({ payload, index })).sort((left, right) => { return getPayloadRevision(left.payload) - getPayloadRevision(right.payload) || left.index - right.index; }).map(({ payload }) => payload); } function countDeltaItems(journal) { return journal.reduce((total, payload) => total + payload.items.length, 0); } function compactDeltaJournal(journal) { if (countDeltaItems(journal) <= 256) return journal; const latestByCar = new Map(); for (const payload of sortDeltaJournal(journal)) for (const item of payload.items) latestByCar.set(item.carNum.trim().toUpperCase(), { payload, item }); const grouped = new Map(); for (const { payload, item } of latestByCar.values()) { const key = `${getPayloadRevision(payload)}:${payload.action}:${payload.ts}`; const existing = grouped.get(key); if (existing) existing.items.push(item); else grouped.set(key, { ...payload, items: [item] }); } return sortDeltaJournal(Array.from(grouped.values())); } function createLogger(prefix) { return { info(step, ...args) { console.log(`%c${prefix}%c ${step}`, "color:#25b1dc;font-weight:bold;", "", ...args); }, ok(step, ...args) { console.log(`%c${prefix} ✓%c ${step}`, "color:#1f7a3d;font-weight:bold;", "", ...args); }, warn(step, ...args) { console.warn(`%c${prefix} ⚠%c ${step}`, "color:#d7a80c;font-weight:bold;", "", ...args); }, err(step, ...args) { console.error(`%c${prefix} ✗%c ${step}`, "color:#de3333;font-weight:bold;", "", ...args); } }; } var { info: logInfo$1, ok: logOk$1, warn: logWarn$1, err: logErr$1 } = createLogger(LOG_PREFIX_JHS); var GM_KEY_CAR_LIST_REV = "jhs_car_list_rev"; var GM_KEY_SYNCED_REV = "jhs_car_status_synced_rev"; var GM_KEY_LAST_SYNC_TIME = "jhs_car_status_last_sync"; var DAILY_SYNC_INTERVAL = 864e5; var CarListReaderPlugin = class extends BasePlugin { getName() { return "CarListReaderPlugin"; } async initCss() { return ""; } async handle() { storageManager.setCarListChangeCallback((event) => { this.pushDelta(event); }); if (window.self !== window.top) return; try { GM_registerMenuCommand("🔁 全量同步车辆状态到 missav", () => { this.syncFullCarStatus(); }); } catch {} try { GM_registerMenuCommand("🔍 诊断:跨站同步发布侧", () => { (async () => { const delta = GM_getValue(GM_KEY_CAR_STATUS_DELTA); const data = GM_getValue(GM_KEY_CAR_STATUS_DATA); const journal = Array.isArray(delta) ? delta : delta ? [delta] : []; const info = { site: "javdb", version: GM_info.script.version, gmRevision: GM_getValue(GM_KEY_CAR_STATUS_REVISION, 0), carListRev: GM_getValue(GM_KEY_CAR_LIST_REV, 0), syncedRev: GM_getValue(GM_KEY_SYNCED_REV, -1), journal: { length: journal.length, entries: journal.slice(-5).map((e) => { const entry = e; return { action: entry?.action, revision: entry?.revision ?? entry?.ts, carNums: Array.isArray(entry?.items) ? entry.items.map((i) => i?.carNum) : void 0 }; }) }, fullSnapshot: data ? { revision: data.revision ?? data.ts, count: data.count, ready: data.ready } : null }; const text = JSON.stringify(info, null, 2); console.log("[JHS CarSync 诊断-发布侧]", text); try { await navigator.clipboard.writeText(text); } catch {} show.ok("诊断信息已输出到控制台并复制到剪贴板"); })(); }); } catch {} const currentRev = GM_getValue(GM_KEY_CAR_LIST_REV, 0); const syncedRev = GM_getValue(GM_KEY_SYNCED_REV, -1); const lastSyncTime = GM_getValue(GM_KEY_LAST_SYNC_TIME, 0); if (currentRev !== syncedRev || Date.now() - lastSyncTime > DAILY_SYNC_INTERVAL) { logInfo$1("全量同步已调度", `rev=${currentRev}, synced=${syncedRev}`); setTimeout(() => { this.syncFullCarStatus(); }, 2e3); } else logInfo$1("跳过全量同步", `数据未变更 (rev=${currentRev}),上次同步 ${new Date(lastSyncTime).toLocaleString()}`); } pendingPush = Promise.resolve(); pushDelta(event) { this.pendingPush = this.pushDeltaAsync(event).catch((err) => { logErr$1("增量推送失败", err instanceof Error ? err.message : String(err)); }); } async waitForPendingPush() { await this.pendingPush; } async pushDeltaAsync(event) { if (event.action === "upsert" && event.upserts) { const items = event.upserts.map((car) => { const { code } = normalizeUrl(car.url || ""); return { carNum: car.carNum, status: car.status, url_path: code }; }); if (items.length === 0) return; if (items.length > 500) { logWarn$1("批量变更超过增量上限,改发压缩全量快照", `${items.length} 条`); await this.syncFullCarStatus(); return; } const revision = await nextRevision(); const needsFullSnapshot = await appendDeltaJournal({ action: "upsert", items, ts: revision, revision }); logInfo$1("增量推送", `upsert ${items.length} 条`); if (needsFullSnapshot) { logInfo$1("增量日志超过阈值,发布压缩全量快照"); await this.syncFullCarStatus(); } } else if (event.action === "delete" && event.deletes) { if (event.deletes.length === 0) return; if (event.deletes.length > 500) { logWarn$1("批量删除超过增量上限,改发压缩全量快照", `${event.deletes.length} 条`); await this.syncFullCarStatus(); return; } const revision = await nextRevision(); const needsFullSnapshot = await appendDeltaJournal({ action: "delete", items: event.deletes.map((carNum) => ({ carNum })), ts: revision, revision }); logInfo$1("增量推送", `delete ${event.deletes.length} 条`); if (needsFullSnapshot) { logInfo$1("增量日志超过阈值,发布压缩全量快照"); await this.syncFullCarStatus(); } } GM_setValue(GM_KEY_CAR_LIST_REV, GM_getValue(GM_KEY_CAR_LIST_REV, 0) + 1); } async syncFullCarStatus() { if (isFullSyncing) { fullResyncRequested = true; logWarn$1("全量同步任务在进行中,已安排完成后补发最新快照"); return false; } isFullSyncing = true; try { logInfo$1("全量同步开始"); const revision = await nextRevision(); const carList = await storageManager.getCarList(); logInfo$1("1-读取", `本地 car_list 记录数:${carList.length}`); if (carList.length === 0) { logWarn$1("car_list 为空,跳过同步(jhs 可能尚未写入数据)"); return false; } const colRes = toColumnar(carList); const count = countColumnar(colRes.byStatus); logInfo$1("2-转列存", `列存 ${count} 条 / 丢弃 ${colRes.dropped} / url 异常 ${colRes.malformed}`); if (count === 0) { logWarn$1("列存后无有效记录,跳过"); return false; } const base64 = await gzipToBase64(colRes.byStatus); if (!base64) { logErr$1("gzip 压缩失败(CompressionStream 不可用?非 https 环境?)"); return false; } logInfo$1("3-压缩", `gzip+base64 体积:约 ${(base64.length / 1024).toFixed(1)} KB`); const payload = { data: base64, hwm: new Date(revision).toISOString(), count, ts: revision, revision, mode: "snapshot", ready: true }; if (!await publishSnapshot(payload)) { logWarn$1("发现更新的全量快照,放弃发布本次旧快照", `revision=${revision}`); return false; } logOk$1("4-完成", `已写入 GM 存储,count=${count},hwm=${payload.hwm}`); GM_setValue(GM_KEY_SYNCED_REV, GM_getValue(GM_KEY_CAR_LIST_REV, 0)); GM_setValue(GM_KEY_LAST_SYNC_TIME, Date.now()); return true; } catch (err) { logErr$1("全量同步失败", err instanceof Error ? err.message : String(err), err); return false; } finally { isFullSyncing = false; if (fullResyncRequested) { fullResyncRequested = false; setTimeout(() => void this.syncFullCarStatus(), 0); } } } }; var isFullSyncing = false; var fullResyncRequested = false; var lastIssuedRevision = 0; var JAVDB_SYNC_LOCK_NAME = "jhs-javdb-car-status-publisher"; async function nextRevision() { return withExclusiveLock(JAVDB_SYNC_LOCK_NAME, async () => { const now = Date.now(); let persisted = 0; try { const value = GM_getValue(GM_KEY_CAR_STATUS_REVISION, 0); if (typeof value === "number" && Number.isInteger(value) && value >= 0) persisted = value; } catch {} lastIssuedRevision = Math.max(now, persisted + 1, lastIssuedRevision + 1); GM_setValue(GM_KEY_CAR_STATUS_REVISION, lastIssuedRevision); return lastIssuedRevision; }); } async function appendDeltaJournal(payload) { return withExclusiveLock(JAVDB_SYNC_LOCK_NAME, async () => { const journal = readDeltaJournal(GM_getValue(GM_KEY_CAR_STATUS_DELTA)); journal.push(payload); const compacted = compactDeltaJournal(sortDeltaJournal(journal)); GM_setValue(GM_KEY_CAR_STATUS_DELTA, compacted); return countDeltaItems(compacted) > 256; }); } async function publishSnapshot(payload) { return withExclusiveLock(JAVDB_SYNC_LOCK_NAME, async () => { const current = GM_getValue(GM_KEY_CAR_STATUS_DATA); if ((current ? getPayloadRevision(current) : -1) >= getPayloadRevision(payload)) return false; GM_setValue(GM_KEY_CAR_STATUS_DATA, payload); const journal = readDeltaJournal(GM_getValue(GM_KEY_CAR_STATUS_DELTA)); const retained = journal.filter((delta) => getPayloadRevision(delta) >= getPayloadRevision(payload)); if (retained.length !== journal.length) GM_setValue(GM_KEY_CAR_STATUS_DELTA, sortDeltaJournal(retained)); return true; }); } var visit_history_tooltip_default = "/* 访问记录悬浮提示:时间分级配色(由近=安全 → 远=危险) */\n.jhs-visit-tooltip {\n position: fixed;\n padding: 8px 12px;\n border-radius: 6px;\n white-space: nowrap;\n max-width: 600px;\n pointer-events: none;\n font-size: 14px;\n line-height: 1.5;\n font-weight: 600;\n z-index: var(--jhs-z-top);\n box-shadow: 0 6px 20px rgb(0, 0, 0, 0.22);\n display: none;\n border: 1px solid transparent;\n}\n\n/* 无访问记录:白底灰字 */\n.jhs-visit-tooltip--none {\n background: #fff;\n color: #64748b;\n border-color: #e2e8f0;\n box-shadow: 0 4px 14px rgb(15, 23, 42, 0.12);\n font-weight: 500;\n}\n\n/* <1 分钟:最安全 · 翠绿 */\n.jhs-visit-tooltip--sec {\n background: #ecfdf5;\n color: #047857;\n border-color: #6ee7b7;\n}\n\n/* <1 小时:安全 · 青绿 */\n.jhs-visit-tooltip--min {\n background: #f0fdf4;\n color: #15803d;\n border-color: #86efac;\n}\n\n/* <1 天:留意 · 琥珀 */\n.jhs-visit-tooltip--hour {\n background: #fffbeb;\n color: #b45309;\n border-color: #fcd34d;\n}\n\n/* <1 周:警告 · 橙 */\n.jhs-visit-tooltip--day {\n background: #fff7ed;\n color: #c2410c;\n border-color: #fdba74;\n}\n\n/* ≥1 周:危险 · 红 */\n.jhs-visit-tooltip--old {\n background: #fef2f2;\n color: #b91c1c;\n border-color: #fca5a5;\n}\n"; var VISIT_HISTORY_STORAGE_KEY = "jhs_visit_history"; var MAX_ENTRIES = 5e3; var TICK_MS = 500; var TIER_CLASS = { none: "jhs-visit-tooltip--none", sec: "jhs-visit-tooltip--sec", min: "jhs-visit-tooltip--min", hour: "jhs-visit-tooltip--hour", day: "jhs-visit-tooltip--day", old: "jhs-visit-tooltip--old" }; var ALL_TIER_CLASSES = Object.values(TIER_CLASS).join(" "); var VisitHistoryPlugin = class extends BasePlugin { supervisor = new TaskSupervisor(); tooltipEl = null; tooltipTimer = null; activePath = null; dismissListenerBound = false; pageshowBound = false; getName() { return "VisitHistoryPlugin"; } async initCss() { return visit_history_tooltip_default; } async handle() { if (!isJavdbSite) return; this.recordVisit(); this.bindPageshow(); if (window.isDetailPage) this.injectMetaLinkTooltips(); } bindPageshow() { if (this.pageshowBound) return; this.pageshowBound = true; this.supervisor.addEventListener(window, "pageshow", (event) => { if (!isJavdbSite) return; this.recordVisit(); if (event.persisted && window.isDetailPage) this.injectMetaLinkTooltips(true); }); } recordVisit() { const path = location.pathname + location.search; const history = this.readHistory(); history[path] = Date.now(); this.evictIfNeeded(history); try { localStorage.setItem(VISIT_HISTORY_STORAGE_KEY, JSON.stringify(history)); } catch { localStorage.removeItem(VISIT_HISTORY_STORAGE_KEY); } } readHistory() { try { const raw = localStorage.getItem(VISIT_HISTORY_STORAGE_KEY); return raw ? JSON.parse(raw) : {}; } catch { return {}; } } evictIfNeeded(history) { const keys = Object.keys(history); if (keys.length <= MAX_ENTRIES) return; keys.sort((a, b) => history[a] - history[b]); const removeCount = keys.length - MAX_ENTRIES; for (let i = 0; i < removeCount; i++) delete history[keys[i]]; } injectMetaLinkTooltips(forceRebind = false) { const container = document.querySelector("body > section > div > div.video-detail > div.video-meta-panel > div > div:nth-child(2)") || document.querySelector(".movie-panel-info"); if (!container) return; container.querySelectorAll(".panel-block .value a[href]").forEach((a) => { const href = a.getAttribute("href") || ""; const path = this.normalizePath(href); if (!path) return; const el = a; if (forceRebind && el.__jhsVisitEnter) { el.removeEventListener("mouseenter", el.__jhsVisitEnter); el.removeEventListener("mouseleave", el.__jhsVisitLeave); } if (el.__jhsVisitEnter && !forceRebind) return; const onEnter = () => { this.activePath = path; this.showVisitTooltip(el); }; const onLeave = () => { this.hideVisitTooltip(); }; el.__jhsVisitEnter = onEnter; el.__jhsVisitLeave = onLeave; this.supervisor.addEventListener(el, "mouseenter", onEnter); this.supervisor.addEventListener(el, "mouseleave", onLeave); }); } normalizePath(href) { if (!href || href.startsWith("javascript:")) return null; try { const url = new URL(href, location.origin); if (url.origin !== location.origin) return null; return url.pathname + url.search; } catch { return null; } } ensureTooltipEl() { if (!this.tooltipEl || !this.tooltipEl.isConnected) { const el = document.createElement("div"); el.className = "jhs-visit-tooltip"; document.body.appendChild(el); this.tooltipEl = el; } if (!this.dismissListenerBound) { this.dismissListenerBound = true; this.supervisor.addEventListener(window, "scroll", () => this.hideVisitTooltip(), { capture: true }); this.supervisor.addEventListener(window, "resize", () => this.hideVisitTooltip(), { capture: true }); } return this.tooltipEl; } applyTier(el, tier) { el.classList.remove(...ALL_TIER_CLASSES.split(" ")); el.classList.add(TIER_CLASS[tier]); } showVisitTooltip(link) { const el = this.ensureTooltipEl(); const update = () => { let ts; if (this.activePath) ts = this.readHistory()[this.activePath]; if (!ts) { this.applyTier(el, "none"); el.textContent = "无访问记录"; } else { const { text, tier } = this.formatVisitTime(ts); this.applyTier(el, tier); el.textContent = text; } const rect = link.getBoundingClientRect(); const tw = el.offsetWidth; let left = rect.left + rect.width / 2 - tw / 2; if (left < 8) left = 8; if (left + tw > window.innerWidth - 8) left = window.innerWidth - tw - 8; el.style.left = `${Math.round(left)}px`; }; el.style.display = "block"; update(); const rect = link.getBoundingClientRect(); const th = el.offsetHeight; let top = rect.top - th - 6; if (top < 8) top = rect.bottom + 6; el.style.top = `${Math.round(top)}px`; update(); if (this.tooltipTimer) clearInterval(this.tooltipTimer); this.tooltipTimer = this.supervisor.setInterval(update, TICK_MS); } hideVisitTooltip() { this.activePath = null; if (this.tooltipTimer) { clearInterval(this.tooltipTimer); this.tooltipTimer = null; } if (this.tooltipEl) { this.tooltipEl.style.display = "none"; this.tooltipEl.textContent = ""; this.tooltipEl.classList.remove(...ALL_TIER_CLASSES.split(" ")); } } destroy() { this.supervisor.abort(); } formatVisitTime(ts) { const diff = Date.now() - ts; if (diff < 6e4) return { text: `${Math.max(1, Math.floor(diff / 1e3))}秒前打开过`, tier: "sec" }; if (diff < 36e5) return { text: `${Math.floor(diff / 6e4)}分钟前打开过`, tier: "min" }; if (diff < 864e5) return { text: `${Math.floor(diff / 36e5)}小时前打开过`, tier: "hour" }; if (diff < 6048e5) return { text: `${Math.floor(diff / 864e5)}天前打开过`, tier: "day" }; return { text: `${this.formatDate(ts)} 打开过`, tier: "old" }; } formatDate(ts) { const d = new Date(ts); const p = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; } }; function normalizeCarNum(raw) { if (!raw) return ""; const filename = raw.split("/").pop() || ""; if (/^FC2-PPV-\d+$/i.test(filename)) return filename.toUpperCase(); const sep = filename.includes("_") ? "_" : "-"; const parts = filename.split(sep); return (parts.length >= 2 ? parts[0] + "-" + parts[1] : filename).toUpperCase(); } function createBadge(status, url) { const cfg = STATUS_CONFIG[status]; if (!cfg) return null; const a = document.createElement("a"); a.className = "missav-status-tag"; a.dataset.status = status; a.textContent = cfg.label; const safeUrl = sanitizeUrl(url || ""); if (safeUrl && safeUrl !== "#") { a.href = safeUrl; a.target = "_blank"; a.rel = "noopener noreferrer"; } else a.removeAttribute("href"); a.title = "点击跳转到 javdb 详情页"; a.style.cssText = [ "border-radius:10px", "position:absolute", "z-index:10", "right:5px", "top:5px", "left:auto", "-webkit-font-smoothing:antialiased", "text-rendering:optimizelegibility", "font-family:\"Helvetica Neue\",\"Luxi Sans\",\"DejaVu Sans\",Tahoma,\"Hiragino Sans GB\",\"Microsoft Yahei\",sans-serif", "cursor:pointer", "align-items:center", "display:inline-flex", "font-size:0.75rem", "height:2em", "justify-content:center", "line-height:1.5", "white-space:nowrap", "color:rgb(255,255,255)", "pointer-events:auto", `background-color:${cfg.color} !important`, "width:80px", "text-decoration:none" ].join(";"); return a; } function renderBadges(container, carMap) { if (!container || carMap.size === 0) return { total: 0, matched: 0 }; const items = container.querySelectorAll(".thumbnail.group"); let total = 0; let matched = 0; for (const item of items) { const link = item.querySelector("a[alt]"); if (!link) continue; const carNum = link.getAttribute("alt"); if (!carNum) continue; const normalized = normalizeCarNum(carNum); const record = carMap.get(normalized); if (!record) continue; const existing = item.querySelector(".missav-status-tag"); if (existing) { if (existing.dataset.status === record.status) continue; existing.remove(); } total++; const thumbDiv = item.querySelector("div.relative"); if (!thumbDiv) continue; thumbDiv.style.position = "relative"; const badge = createBadge(record.status, record.url); if (badge) { thumbDiv.appendChild(badge); matched++; } } return { total, matched }; } function isVideoPage() { return document.querySelector("#sprite-plyr") !== null; } var { info: logInfo, ok: logOk, warn: logWarn, err: logErr } = createLogger(LOG_PREFIX_MISSAV); var MissavStatusTagPlugin = class extends BasePlugin { consumeQueue = new KeyedQueue(); lastAppliedRevision = -1; lastAppliedKind = null; getName() { return "MissavStatusTagPlugin"; } async initCss() { return ""; } async handle() { logInfo("脚本启动", `页面类型: ${isVideoPage() ? "视频播放页" : "列表页"}`); try { GM_registerMenuCommand("🔄 MissAV 刷新标签", () => { this.processAll(); }); } catch {} try { GM_registerMenuCommand("🔄 强制重新同步(清除水位线)", () => { (async () => { try { show.ok("正在清除同步水位线..."); await clearAppliedRevisionLocal(); this.lastAppliedRevision = -1; this.lastAppliedKind = "delta"; show.ok("水位线已清除,开始重新消费..."); await this.consumeFull(void 0); await this.consumeDeltaJournal(GM_getValue(GM_KEY_CAR_STATUS_DELTA)); await this.processAll(); show.ok("强制重新同步完成"); } catch (err) { show.error("强制重新同步失败: " + (err instanceof Error ? err.message : String(err))); } })(); }); } catch {} try { GM_registerMenuCommand("🔍 诊断:跨站同步消费侧", () => { (async () => { const delta = GM_getValue(GM_KEY_CAR_STATUS_DELTA); const data = GM_getValue(GM_KEY_CAR_STATUS_DATA); const journal = Array.isArray(delta) ? delta : delta ? [delta] : []; let syncMeta = null; let carCount = -1; let mgt140 = null; try { const db = await openLocalDB(); try { syncMeta = await new Promise((resolve) => { const req = db.transaction(MISSAV_DB_META_STORE, "readonly").objectStore(MISSAV_DB_META_STORE).get(MISSAV_SYNC_META_KEY); req.onsuccess = () => resolve(req.result ?? null); req.onerror = () => resolve(null); }); carCount = await new Promise((resolve) => { const req = db.transaction(MISSAV_DB_STORE, "readonly").objectStore(MISSAV_DB_STORE).count(); req.onsuccess = () => resolve(req.result); req.onerror = () => resolve(-1); }); mgt140 = await new Promise((resolve) => { const req = db.transaction(MISSAV_DB_STORE, "readonly").objectStore(MISSAV_DB_STORE).get("MGT-140"); req.onsuccess = () => resolve(req.result ?? null); req.onerror = () => resolve(null); }); } finally { db.close(); } } catch {} const info = { site: "missav", version: GM_info.script.version, gmRevision: GM_getValue(GM_KEY_CAR_STATUS_REVISION, 0), journal: { length: journal.length, lastEntries: journal.slice(-3).map((e) => { const entry = e; return { action: entry?.action, revision: entry?.revision ?? entry?.ts, carNums: Array.isArray(entry?.items) ? entry.items.map((i) => i?.carNum) : void 0 }; }) }, fullSnapshot: data ? { revision: data.revision ?? data.ts, count: data.count, ready: data.ready } : null, idb: { syncMeta, carCount, MGT140: mgt140 }, inMemory: { lastAppliedRevision: this.lastAppliedRevision, lastAppliedKind: this.lastAppliedKind } }; const text = JSON.stringify(info, null, 2); console.log("[MissAV 诊断-消费侧]", text); try { await navigator.clipboard.writeText(text); } catch {} show.ok("诊断信息已输出到控制台并复制到剪贴板"); })(); }); } catch {} try { GM_addValueChangeListener(GM_KEY_CAR_STATUS_DELTA, (_key, _oldValue, newValue, remote) => { if (remote && newValue) this.consumeDeltaJournal(newValue); }); logOk("已注册增量变更监听"); } catch { logWarn("GM_addValueChangeListener 不可用"); } try { GM_addValueChangeListener(GM_KEY_CAR_STATUS_DATA, (_key, _oldValue, newValue, remote) => { if (remote && newValue) { logInfo("收到 javdb 端全量数据通知"); this.consumeFull(newValue); } }); } catch {} this.observeAndProcess(); await this.consumeFull(void 0); await this.consumeDeltaJournal(GM_getValue(GM_KEY_CAR_STATUS_DELTA)); try { const gmRevision = GM_getValue(GM_KEY_CAR_STATUS_REVISION, 0); if (typeof gmRevision === "number" && gmRevision > 0 && this.lastAppliedRevision < gmRevision) { logWarn("消费验证:IDB 水位线落后于 GM 修订号", `idb=${this.lastAppliedRevision} gm=${gmRevision},尝试补消费`); await this.consumeDeltaJournal(GM_getValue(GM_KEY_CAR_STATUS_DELTA)); } } catch {} const pending = readPendingIntents(); if (pending.length > 0) { logInfo("恢复未完成消费", `${pending.length} 条待处理意图`); for (const intent of pending) { if (intent.type === "delta") { const journal = GM_getValue(GM_KEY_CAR_STATUS_DELTA); await this.consumeDeltaJournal(journal); } clearIntent(intent.revision); } } cleanStaleIntents(); } async processAll() { try { const allCarNums = new Set(); const allThumbs = document.querySelectorAll(".thumbnail.group"); for (const thumb of allThumbs) { const link = thumb.querySelector("a[alt]"); if (link) { const cn = link.getAttribute("alt"); if (cn) allCarNums.add(normalizeCarNum(cn)); } } if (allCarNums.size === 0) return; const carMap = await queryLocalCars(Array.from(allCarNums)); if (carMap.size === 0) return; this.processPage(carMap); } catch (err) { logErr("处理页面失败", err instanceof Error ? err.message : String(err)); } } processPage(carMap) { if (isVideoPage()) this.processVideoPage(carMap); else this.processListPage(carMap); } processVideoPage(carMap) { const bottomGrid = document.querySelector("body > div:nth-child(3) > div.sm\\:container.mx-auto.px-4.content-without-search.pb-12 > div > div.flex-1.order-first > div.relative.overflow-hidden > div.grid.grid-cols-2.md\\:grid-cols-3.xl\\:grid-cols-4.gap-5"); if (bottomGrid) this.queryAndRender(bottomGrid, carMap.size > 0 ? carMap : null, "视频页-底部推荐"); const sideContainer = document.querySelector("body > div:nth-child(3) > div.sm\\:container.mx-auto.px-4.content-without-search.pb-12 > div > div.hidden.lg\\:flex.h-full.ml-6.order-last > div"); if (sideContainer) this.queryAndRender(sideContainer, carMap.size > 0 ? carMap : null, "视频页-侧边推荐"); if (!bottomGrid && !sideContainer) { logWarn("视频页选择器未命中,回退到全页扫描"); this.queryAndRender(document.body, carMap.size > 0 ? carMap : null, "视频页-兜底全页"); } } processListPage(carMap) { this.queryAndRender(document.body, carMap.size > 0 ? carMap : null, "列表页"); } async queryAndRender(container, existingMap, label) { if (!container) return; let map = existingMap; if (!map || map.size === 0) { const carNums = new Set(); const thumbs = container.querySelectorAll(".thumbnail.group"); for (const thumb of thumbs) { const link = thumb.querySelector("a[alt]"); if (link) { const cn = link.getAttribute("alt"); if (cn) carNums.add(normalizeCarNum(cn)); } } if (carNums.size === 0) return; map = await queryLocalCars(Array.from(carNums)); } if (!map || map.size === 0) return; const { total, matched } = renderBadges(container, map); const cumulativeMatched = container.querySelectorAll(".missav-status-tag").length; const totalThumbs = container.querySelectorAll(".thumbnail.group").length; logInfo("渲染标签", `${label}: +${total} 新增, +${matched} 命中 | 累计 ${cumulativeMatched}/${totalThumbs} 个标签`); } observeAndProcess() { let retries = 0; const maxRetries = 30; const tryProcess = () => { if (document.querySelectorAll(".thumbnail.group").length > 0) { this.processAll(); return; } retries++; if (retries < maxRetries) setTimeout(tryProcess, 500); }; tryProcess(); let debounceTimer = null; new MutationObserver((mutations) => { let hasNewThumbnails = false; for (const mutation of mutations) { if (mutation.type === "childList") for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue; const el = node; if (el.matches?.(".thumbnail.group") || el.querySelector?.(".thumbnail.group")) { hasNewThumbnails = true; break; } } if (mutation.type === "attributes" && mutation.attributeName === "alt" && mutation.target.matches?.("a[alt]") && mutation.target.closest?.(".thumbnail.group")) hasNewThumbnails = true; if (hasNewThumbnails) break; } if (hasNewThumbnails) { if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(() => this.processAll(), 300); } }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["alt"] }); } async consumeDeltaJournal(value) { const journal = sortDeltaJournal(readDeltaJournal(value)); for (const delta of journal) await this.consumeQueue.enqueue("consume", () => this.applyDelta(delta)); if (journal.length > 0) await this.processAll(); } async consumeFull(payload) { return this.consumeQueue.enqueue("consume", () => this.applyFull(payload)); } async applyDelta(delta) { try { const revision = getPayloadRevision(delta); if (revision < 0 || !isDeltaPayload(delta)) { logWarn("忽略无效增量载荷"); return; } if (!this.shouldApply(revision, "delta")) return; writeIntent(revision, "delta"); let rollback; if (!(await withSyncRevision(revision, "delta", async () => { const previous = await readLocalCarsSelection(delta.items.map((item) => item.carNum)); rollback = () => restoreLocalCarsSelection(previous); if (delta.action === "upsert") { const records = delta.items.map((item) => ({ carNum: item.carNum.trim(), status: (item.status || "").trim(), url_path: (item.url_path || "").trim() })); await upsertLocalCars(records); logInfo("增量更新", `upsert ${records.length} 条`); } else { const carNums = delta.items.map((item) => item.carNum.trim()); await deleteLocalCars(carNums); logInfo("增量更新", `delete ${carNums.length} 条`); } }, async () => { if (rollback) await rollback(); })).applied) { this.markApplied(revision, "delta"); clearIntent(revision); return; } this.markApplied(revision, "delta"); clearIntent(revision); } catch (err) { logErr("增量消费失败", err instanceof Error ? err.message : String(err)); } } async applyFull(payload) { try { let data = payload ?? GM_getValue("jhs_car_status_data"); if (!payload && data) { const rev1 = data.revision ?? data.ts; await new Promise((resolve) => setTimeout(resolve, 50)); const data2 = GM_getValue(GM_KEY_CAR_STATUS_DATA); const rev2 = data2 ? data2.revision ?? data2.ts : -1; if (rev1 !== rev2) { logWarn("全量快照双读不一致", `rev1=${rev1} rev2=${rev2},使用第二次读取结果`); data = data2; } } if (!data || typeof data !== "object" || typeof data.data !== "string" || !data.data) { if (payload !== void 0) logWarn("GM 存储中无全量数据"); return; } const revision = getPayloadRevision(data); if (revision < 0 || !Number.isInteger(data.count) || data.count < 0 || data.mode !== void 0 && data.mode !== "snapshot" || data.ready === false || data.count === 0 && data.ready !== true) { logWarn("拒绝未就绪或格式无效的全量快照"); return; } if (!this.shouldApply(revision, "full")) return; writeIntent(revision, "full"); logInfo("全量消费开始", `count=${data.count} hwm=${data.hwm} revision=${revision}`); const store = await gunzipFromBase64(data.data); if (!store) { logErr("解压失败(DecompressionStream 不可用?数据损坏?)"); return; } const validation = validateColumnarSnapshot(store, data.count); if (!validation.ok) { logErr("全量快照校验失败,拒绝覆盖本地数据", validation.reason); return; } logInfo("解压完成", `${validation.records.length} 条记录`); let rollback; const result = await withSyncRevision(revision, "full", async () => { const previous = await readLocalCarsSnapshot(); rollback = () => restoreLocalCarsSnapshot(previous); return replaceLocalCars(validation.records, previous); }, async () => { if (rollback) await rollback(); }); if (!result.applied) { this.markApplied(revision, "full"); clearIntent(revision); return; } const { written, deleted } = result.value; logOk("对账本地 IDB", `写入 ${written} 条,删除 ${deleted} 条`); this.markApplied(revision, "full"); await this.processAll(); clearIntent(revision); } catch (err) { logErr("全量消费失败", err instanceof Error ? err.message : String(err)); } } shouldApply(revision, kind) { if (revision < this.lastAppliedRevision) { logWarn("忽略过期同步载荷", `${kind} revision=${revision} < ${this.lastAppliedRevision}`); return false; } if (revision === this.lastAppliedRevision && kind === "full") { logWarn("忽略重复或落后的全量快照", `revision=${revision}, last=${this.lastAppliedKind || "none"}`); return false; } return true; } markApplied(revision, kind) { this.lastAppliedRevision = Math.max(this.lastAppliedRevision, revision); this.lastAppliedKind = kind; } }; async function clearAppliedRevisionLocal() { const db = await openLocalDB(); try { await new Promise((resolve, reject) => { const tx = db.transaction(MISSAV_DB_META_STORE, "readwrite"); tx.objectStore(MISSAV_DB_META_STORE).delete(MISSAV_SYNC_META_KEY); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error || new Error("清理同步修订号失败")); tx.onabort = () => reject(tx.error || new Error("清理同步修订号事务中止")); }); } finally { db.close(); } } var JAVDB_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-letter-j-small"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 8h4v6a2 2 0 1 1 -4 0" /></svg>`; function JavDBIcon() { return (0, import_jsx_runtime.jsx)("span", { dangerouslySetInnerHTML: { __html: JAVDB_ICON_SVG } }); } var LOG = "[MissAV 快速复制]"; var MENU_BAR_SELECTOR = "body > div:nth-child(3) > div.sm\\:container.mx-auto.px-4.content-without-search.pb-12 > div > div.flex-1.order-first > div.mt-4 > div"; function extractCode(pathname) { const filename = pathname.split("/").pop() || ""; if (/^FC2-PPV-\d+$/i.test(filename)) return filename; const parts = filename.split("-"); return parts.length >= 2 ? parts[0] + "-" + parts[1] : filename; } function copyToClipboard(text) { return navigator.clipboard.writeText(text).then(() => true).catch(() => false); } function showToast(message, duration = 2e3) { const toast = document.createElement("div"); toast.textContent = message; Object.assign(toast.style, { position: "fixed", bottom: "20px", left: "50%", transform: "translateX(-50%)", background: "rgba(0,0,0,0.7)", color: "#fff", padding: "8px 16px", borderRadius: "4px", fontSize: "14px", zIndex: "9999", transition: "opacity 0.3s", opacity: "1" }); document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = "0"; setTimeout(() => toast.remove(), 300); }, duration); } function openInNewTab(url) { console.log(`${LOG} openInNewTab 触发`); try { const a = document.createElement("a"); a.href = url; a.target = "_blank"; a.style.display = "none"; document.body.appendChild(a); a.click(); console.log(`${LOG} 执行 模拟单击 a 标签`); document.body.removeChild(a); return true; } catch (e) { console.log(`${LOG} 单击 a 标签失败:`, e); return false; } } function createButton(label, svgElement, onClick) { const button = document.createElement("button"); button.type = "button"; button.className = "inline-flex items-center whitespace-nowrap text-sm text-nord4 leading-4 font-medium focus:outline-none hover:text-nord6"; button.appendChild(svgElement); button.appendChild(document.createTextNode(label)); button.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); onClick(); }); return button; } function createSVGClipboard() { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("xmlns", "http://www.w3.org/2000/svg"); svg.setAttribute("fill", "currentColor"); svg.setAttribute("class", "mr-1 md:mr-2 h-3 w-3 xs:h-4 xs:w-4"); svg.setAttribute("viewBox", "0 0 16 16"); const path1 = document.createElementNS(svg.namespaceURI, "path"); path1.setAttribute("fill-rule", "evenodd"); path1.setAttribute("d", "M10.854 7.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 9.793l2.646-2.647a.5.5 0 0 1 .708 0"); const path2 = document.createElementNS(svg.namespaceURI, "path"); path2.setAttribute("d", "M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1z"); const path3 = document.createElementNS(svg.namespaceURI, "path"); path3.setAttribute("d", "M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0z"); svg.appendChild(path1); svg.appendChild(path2); svg.appendChild(path3); return svg; } function createSVGJavDB() { const html = jsxToString((0, import_jsx_runtime.jsx)(JavDBIcon, {})); const template = document.createElement("template"); template.innerHTML = html; const svg = template.content.querySelector("svg"); if (svg instanceof SVGElement) { svg.removeAttribute("width"); svg.removeAttribute("height"); svg.setAttribute("class", "mr-1 md:mr-2 h-3 w-3 xs:h-4 xs:w-4"); } return svg; } function insertButtons(menuBar) { const fanhao = extractCode(window.location.pathname); const copyBtn = createButton("复制番号", createSVGClipboard(), () => { copyToClipboard(fanhao).then((success) => { showToast(success ? `番号 ${fanhao} 已复制` : "复制失败"); }); }); menuBar.prepend(copyBtn); const javdbBtn = createButton("转到JavDB搜索", createSVGJavDB(), () => { const javdbUrl = `https://javdb.com/search?q=${encodeURIComponent(fanhao)}&f=all`; console.log(`${LOG} 尝试打开 JavDB 链接:`, javdbUrl); if (openInNewTab(javdbUrl)) showToast(`正在跳转到 JavDB 搜索: ${fanhao}`); else { showToast("跳转失败,请检查弹窗阻止设置"); copyToClipboard(javdbUrl).then((copied) => { if (copied) showToast("链接已复制到剪贴板,请手动打开"); }); } }); menuBar.appendChild(javdbBtn); console.log(`${LOG} ✅ Copy 和 JavDB 按钮已插入 menu_bar`); } var MissavQuickCopyPlugin = class extends BasePlugin { observer = null; getName() { return "MissavQuickCopyPlugin"; } async initCss() { return ""; } async handle() { const existing = document.querySelector(MENU_BAR_SELECTOR); if (existing) { insertButtons(existing); return; } this.observer = new MutationObserver(() => { const menuBar = document.querySelector(MENU_BAR_SELECTOR); if (menuBar) { this.observer?.disconnect(); this.observer = null; insertButtons(menuBar); } }); this.observer.observe(document.body, { childList: true, subtree: true }); } }; var NO_TAG_VALUE = "no-tag"; var STATUS_ORDER = [ "favorite", "hasWatch", "filter", "hasDown" ]; function isVideoDetailPath(path) { if (path.includes("/search/")) return false; const last = path.split("/").pop() || ""; return /^[a-z0-9]+(?:-[a-z0-9]+)+$/i.test(last); } function statusToLabel(status) { return STATUS_CONFIG[status]?.label || status; } function getItemStatus(item) { return item.querySelector(".missav-status-tag")?.dataset.status || null; } function collectStatusCounts() { const counts = {}; let noTag = 0; document.querySelectorAll(".thumbnail.group").forEach((item) => { const status = getItemStatus(item); if (status) { const name = statusToLabel(status); counts[name] = (counts[name] || 0) + 1; } else noTag++; }); if (noTag > 0) counts[NO_TAG_VALUE] = noTag; return counts; } var MAP_SEL = { thumb: ".thumbnail.group", grid: "div.grid.grid-cols-2.md\\:grid-cols-3.xl\\:grid-cols-4.gap-5", nextLink: "a[href*='page=']" }; function findNextUrl(doc) { const next = Array.from(doc.querySelectorAll(MAP_SEL.nextLink)).find((a) => { const text = a.textContent?.trim() || ""; return text === "下一页" || text === "Next"; }); return next ? next.href : null; } function extractThumbs(container) { return Array.from(container.querySelectorAll(MAP_SEL.thumb)); } function collectCarNums(thumbs) { return thumbs.map((t) => { const a = t.querySelector("a[alt]"); return a ? normalizeCarNum(a.getAttribute("alt") || "") : ""; }).filter(Boolean); } function liftDataSrcToSrc(container) { let lifted = 0; container.querySelectorAll("img").forEach((img) => { const dataSrc = img.getAttribute("data-src"); if (dataSrc) { img.src = dataSrc; lifted++; } }); return lifted; } function gmGet(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url, timeout: 3e4, onload: (resp) => { if (resp.status >= 200 && resp.status < 300) resolve(resp.responseText); else reject(new Error(`HTTP ${resp.status}`)); }, onerror: () => reject(new Error("network error")), ontimeout: () => reject(new Error("timeout")) }); }); } async function fetchNextPage(plugin) { if (!plugin.pagination.canLoad || !plugin.nextUrl) return; plugin.pagination.startLoading(); plugin.setLoader("jhs-map-loading", "加载中..."); try { const html = await gmGet(plugin.nextUrl); const doc = new DOMParser().parseFromString(String(html), "text/html"); const thumbs = extractThumbs(doc); if (thumbs.length === 0) throw new Error("下一页无缩略图"); const carNums = collectCarNums(thumbs); if (carNums.length > 0 && plugin.loadedCarNums.has(carNums[0])) { plugin.nextUrl = null; plugin.pagination.loadSuccess(false); plugin.setLoader("jhs-map-no-more", "已经到底了"); return; } carNums.forEach((c) => c && plugin.loadedCarNums.add(c)); liftDataSrcToSrc(doc); const fragment = document.createDocumentFragment(); thumbs.forEach((t) => fragment.appendChild(t)); plugin.container.appendChild(fragment); plugin.loadedPages += 1; const next = findNextUrl(doc); plugin.nextUrl = next; plugin.pagination.loadSuccess(!!next); plugin.setLoader("", ""); if (!plugin.nextUrl) plugin.setLoader("jhs-map-no-more", "已经到底了"); } catch (err) { clog.error("[MissAV 瀑布流] 加载失败:", err); plugin.pagination.loadError(); plugin.setLoader("jhs-map-error", "加载失败,点击重试"); } finally { if (!plugin.pagination.isLoading && plugin.pagination.hasMore && plugin.nextUrl) plugin.checkLoad(); } } var missav_auto_page_default = "/* MissAV 列表页瀑布流(MissavAutoPagePlugin)—— loader / 加载全部按钮样式。\n * 回到顶部按钮样式复用 back-to-top-button.css(initCss 拼接注入)。\n */\n\n/* 底部 loader:空闲透明占位,click 态显示「点击加载下一页」按钮 */\n.jhs-map-loader {\n text-align: center;\n padding: 16px;\n color: #888;\n user-select: none;\n}\n\n.jhs-map-loader.jhs-map-loading {\n color: #000;\n cursor: default;\n}\n\n.jhs-map-loader.jhs-map-error {\n color: #f44336;\n cursor: pointer;\n}\n\n.jhs-map-loader.jhs-map-no-more {\n color: #4caf50;\n cursor: default;\n font-size: 13px;\n}\n\n.jhs-map-loader.jhs-map-click {\n color: #1976d2;\n cursor: pointer;\n font-weight: 600;\n}\n\n.jhs-map-loader.jhs-map-click:hover {\n color: #0d47a1;\n text-decoration: underline;\n}\n\n/* 「加载全部」浮动按钮(左下角,避开右侧回到顶部按钮;nord 暗色与 missav 页面融合) */\n.jhs-map-load-all-btn {\n position: fixed;\n left: 20px;\n bottom: 80px;\n z-index: var(--jhs-z-page);\n padding: 10px 20px;\n background: #2e3440; /* nord0 */\n border: 1px solid #4c566a; /* nord3 */\n color: #d8dee9; /* nord4 */\n border-radius: 20px;\n cursor: pointer;\n font-size: 14px;\n box-shadow: 0 2px 8px rgb(0 0 0 / 0.3);\n transition:\n transform 0.2s ease,\n box-shadow 0.2s ease,\n border-color 0.2s ease,\n background-color 0.2s ease,\n opacity 0.5s ease;\n}\n\n.jhs-map-load-all-btn:hover {\n transform: translateY(-2px);\n box-shadow: 0 4px 12px rgb(0 0 0 / 0.4);\n background: #3b4252; /* nord1 */\n border-color: #81a1c1; /* frost */\n}\n\n.jhs-map-load-all-btn.jhs-map-load-all-disabled {\n opacity: 0.6;\n cursor: wait;\n}\n\n.jhs-map-load-all-btn.jhs-map-load-all-fadeout {\n opacity: 0;\n}"; var PRELOAD_DISTANCE = 800; var MissavAutoPagePlugin = class extends BasePlugin { container = null; nextUrl = null; pagination = new PaginationStateMachine(); supervisor = new TaskSupervisor(); loader = null; loadAllBtn = null; isLoadingAll = false; loadedPages = 1; loadedCarNums = new Set(); backToTopCleanup = null; getName() { return "MissavAutoPagePlugin"; } async initCss() { return missav_auto_page_default + back_to_top_button_default; } async handle() { if (isVideoPage() || isVideoDetailPath(location.pathname)) return; let container = document.querySelector(MAP_SEL.grid); if (!container) container = document.querySelector(MAP_SEL.thumb)?.parentElement?.parentElement ?? null; if (!container) return; this.container = container; const loader = document.createElement("div"); loader.className = "jhs-map-loader"; container.parentNode?.insertBefore(loader, container.nextSibling); this.loader = loader; this.nextUrl = findNextUrl(document); this.pagination.init(!!this.nextUrl); collectCarNums(extractThumbs(container)).forEach((c) => c && this.loadedCarNums.add(c)); this.supervisor.addEventListener(loader, "click", () => { if (this.pagination.isLoading) return; if (this.pagination.phase === "error") this.pagination.retry(); if (this.pagination.canLoad && this.nextUrl) fetchNextPage(this); }); this.supervisor.addEventListener(window, "scroll", () => this.checkLoad(), { passive: true }); this.showLoadAllBtn(); this.backToTopCleanup = createBackToTopButton(); this.supervisor.setTimeout(() => { this.checkLoad(); }, 500); if (!this.pagination.hasMore) this.setLoader("jhs-map-no-more", "已经到底了"); } checkLoad() { if (!this.loader) return; if (this.loader.getBoundingClientRect().top < window.innerHeight + PRELOAD_DISTANCE) { if (this.pagination.isLoading || !this.pagination.hasMore || !this.nextUrl || this.loader.classList.contains("jhs-map-error") || this.loader.classList.contains("jhs-map-loading") || this.loader.classList.contains("jhs-map-no-more")) return; this.setLoader("jhs-map-click", "点击加载下一页"); } } showLoadAllBtn() { if (this.loadAllBtn) return; if (!this.container || !this.pagination.hasMore) return; const btn = document.createElement("div"); btn.className = "jhs-map-load-all-btn"; btn.textContent = "加载全部"; this.supervisor.addEventListener(btn, "click", () => { if (!this.isLoadingAll) this.loadAllPages(); }); document.body.appendChild(btn); this.loadAllBtn = btn; } async loadAllPages() { if (this.isLoadingAll || !this.pagination.hasMore) return; this.isLoadingAll = true; this.updateLoadAllBtn("加载中...", true); try { while (this.pagination.hasMore && this.nextUrl) { const before = this.loadedCarNums.size; await fetchNextPage(this); if (this.loadedCarNums.size === before) break; this.updateLoadAllBtn(`加载中...(第 ${this.loadedPages} 页)`, true); } if (this.loader?.classList.contains("jhs-map-error")) this.updateLoadAllBtn("加载失败,点击重试", false); else { this.updateLoadAllBtn("✓ 已全部加载", false); this.supervisor.setTimeout(() => { this.loadAllBtn?.classList.add("jhs-map-load-all-fadeout"); this.supervisor.setTimeout(() => { this.loadAllBtn?.remove(); this.loadAllBtn = null; }, 500); }, 2e3); } } catch (err) { clog.error("[MissAV 瀑布流] 加载全部失败:", err); this.updateLoadAllBtn("加载失败,点击重试", false); } finally { this.isLoadingAll = false; } } updateLoadAllBtn(text, disabled) { if (!this.loadAllBtn) return; this.loadAllBtn.textContent = text; this.loadAllBtn.classList.toggle("jhs-map-load-all-disabled", disabled); } setLoader(cls, text) { if (!this.loader) return; this.loader.className = `jhs-map-loader ${cls}`.trim(); this.loader.textContent = text; } destroy() { this.supervisor.abort(); this.backToTopCleanup?.(); this.backToTopCleanup = null; } }; function shouldShow(itemStatus, activeValues) { if (activeValues.size === 0) return true; return activeValues.has(itemStatus ?? "no-tag"); } function getCell(item) { const parent = item.parentElement; if (parent && parent !== document.body && !String(parent.className).includes("grid-cols")) return parent; return item; } function applyFilter(activeValues) { document.querySelectorAll(".thumbnail.group").forEach((item) => { const status = item.querySelector(".missav-status-tag")?.dataset.status ?? null; const cell = getCell(item); if (shouldShow(status, activeValues)) { cell.style.display = ""; cell.removeAttribute("data-mstf-hidden"); } else { cell.style.display = "none"; cell.setAttribute("data-mstf-hidden", "yes"); } }); } var missav_status_filter_default = "/* MissAV 状态标签筛选栏 —— missav 是 Tailwind nord 暗色主题,样式独立定义\n * (不参与 filter-chip-base.css 的 javdb 浅色共享基础,doc/223)。\n * 配色参考 missav 页面 Tailwind nord 令牌:nord0 #2e3440 / nord1 #3b4252 /\n * nord3 #4c566a / nord4 #d8dee9 / frost #81a1c1。\n */\n\n/* ---------- 筛选栏容器 ---------- */\n.missav-status-filter-bar {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 8px 0 16px 0; /* 无分割线,用底部间距与下方内容自然分离(doc/227) */\n}\n\n/* ---------- 筛选栏标签 ---------- */\n.missav-status-filter-label {\n flex-shrink: 0;\n font-size: 0.8em;\n font-weight: 600;\n color: #d8dee9; /* nord4 */\n line-height: 1.8;\n margin-right: 4px;\n}\n\n/* ---------- 芯片容器 ---------- */\n.missav-status-filter-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n/* ---------- 芯片基础 ---------- */\n.missav-status-filter-chip {\n display: inline-block;\n padding: 0.25em 0.7em;\n font-size: 0.75em;\n font-weight: 600;\n line-height: 1.5;\n border-radius: 50rem;\n cursor: pointer;\n user-select: none;\n border: 1.5px solid #4c566a; /* nord3 */\n background: #2e3440; /* nord0 */\n color: #d8dee9; /* nord4 */\n transition:\n color 0.15s ease,\n background-color 0.15s ease,\n border-color 0.15s ease;\n}\n\n/* ---------- 芯片悬停 ---------- */\n.missav-status-filter-chip:hover {\n border-color: #81a1c1; /* frost */\n background: #3b4252; /* nord1 */\n}\n\n/* ---------- 芯片选中 ---------- */\n.missav-status-filter-chip.active {\n background: #5d87c2;\n border-color: #5d87c2;\n border-style: solid;\n color: #fff;\n}\n\n/* ---------- 无状态标签芯片 ---------- */\n.missav-status-filter-chip.no-status {\n color: #7c8794;\n border-style: dashed;\n}\n\n.missav-status-filter-chip.no-status.active {\n background: #8fbcbb; /* aurora frost */\n border-color: #8fbcbb;\n color: #1a1a1a;\n}"; var MOUNT_SELECTOR = "div.flex.justify-between.mb-6"; var MissavStatusFilterPlugin = class extends BasePlugin { supervisor = new TaskSupervisor(); observerDebounce = 0; filterBar = null; getName() { return "MissavStatusFilterPlugin"; } async initCss() { return missav_status_filter_default; } async handle() { if (isVideoPage() || isVideoDetailPath(location.pathname)) return; this.tryBuild(); this.startObserving(); } tryBuild() { if (document.querySelector(".missav-status-filter-bar")) return true; const mount = document.querySelector(MOUNT_SELECTOR) ?? document.querySelector("h1") ?? document.querySelector(".thumbnail.group")?.parentElement?.parentElement; if (!mount) return false; this.buildFilterBar(mount); return true; } buildFilterBar(mount) { const bar = document.createElement("div"); bar.className = "missav-status-filter-bar"; const label = document.createElement("span"); label.className = "missav-status-filter-label"; label.textContent = "状态:"; const chips = document.createElement("div"); chips.className = "missav-status-filter-chips"; bar.append(label, chips); mount.insertAdjacentElement("afterend", bar); this.filterBar = bar; this.renderChips(chips); chips.addEventListener("click", (e) => { const chip = e.target.closest?.(".missav-status-filter-chip"); if (chip) { chip.classList.toggle("active"); this.applyActiveFilter(); } }); } renderChips(chips) { const active = new Set(Array.from(chips.querySelectorAll(".missav-status-filter-chip.active")).map((c) => c.dataset.value || "")); chips.innerHTML = ""; const counts = collectStatusCounts(); const ordered = [...STATUS_ORDER, ...counts["no-tag"] ? [NO_TAG_VALUE] : []]; for (const status of ordered) { const label = statusToLabel(status); const c = counts[label] ?? 0; if (c === 0) continue; const chip = document.createElement("span"); chip.className = "missav-status-filter-chip"; if (status === "no-tag") chip.classList.add("no-status"); chip.textContent = status === "no-tag" ? `无状态标签 ${c}` : `${label} ${c}`; chip.dataset.value = status; if (active.has(status)) chip.classList.add("active"); chips.appendChild(chip); } } applyActiveFilter() { if (!this.filterBar) return; const chips = this.filterBar.querySelector(".missav-status-filter-chips"); applyFilter(new Set(Array.from(chips?.querySelectorAll(".missav-status-filter-chip.active") ?? []).map((c) => c.dataset.value || ""))); } startObserving() { this.supervisor.observe(document.body, (mutations) => { if (!this.observerNeedsRefresh(mutations)) return; if (!this.filterBar) this.tryBuild(); clearTimeout(this.observerDebounce); this.observerDebounce = this.supervisor.setTimeout(() => { const bar = this.filterBar; if (!bar) return; const chips = bar.querySelector(".missav-status-filter-chips"); if (chips) { const active = new Set(Array.from(chips.querySelectorAll(".missav-status-filter-chip.active")).map((c) => c.dataset.value || "")); this.renderChips(chips); applyFilter(active); } }, 150); }, { childList: true, subtree: true, attributes: true, attributeFilter: ["data-status"] }); } observerNeedsRefresh(mutations) { for (const mutation of mutations) { if (mutation.type === "attributes" && mutation.attributeName === "data-status") return true; if (mutation.type !== "childList") continue; for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue; const el = node; if (el.matches?.(".thumbnail.group, .missav-status-tag") || el.querySelector?.(".thumbnail.group, .missav-status-tag")) return true; } for (const node of mutation.removedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue; const el = node; if (el.matches?.(".thumbnail.group, .missav-status-tag") || el.querySelector?.(".thumbnail.group, .missav-status-tag")) return true; } } return false; } destroy() { this.supervisor.abort(); } }; var CARD_SEL = ".thumbnail.group"; function findDetailUrl(card) { const a = card.querySelector("a[href]"); return a ? a.href : null; } function isPlainLeftClick(e) { return e.button === 0 && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey; } var TOP_BAR_SEL = "div.fixed.z-max.w-full.bg-gradient-to-b.from-darkest"; var MissavVideoPreviewPlugin = class extends BasePlugin { supervisor = new TaskSupervisor(); getName() { return "MissavVideoPreviewPlugin"; } async initCss() { return ""; } async handle() { this.supervisor.addEventListener(document, "click", (evt) => { const e = evt; const target = e.target; const card = target?.closest(CARD_SEL); if (!card || !isPlainLeftClick(e)) return; if (target?.closest(".missav-status-tag")) return; const url = findDetailUrl(card); if (!url) return; e.preventDefault(); e.stopPropagation(); const width = Math.min(1023, window.innerWidth - 40); const topBar = document.querySelector(TOP_BAR_SEL); const topBarPrevDisplay = topBar ? topBar.style.display : ""; if (topBar) topBar.style.display = "none"; layer.open({ type: 2, content: url, scrollbar: false, area: [`${width}px`, "100%"], isOutAnim: false, anim: -1, end: () => { if (topBar) topBar.style.display = topBarPrevDisplay; } }); }, { capture: true }); } destroy() { this.supervisor.abort(); } }; var errorStyle = { color: "red" }; function TranslatedTitle({ error, children }) { return (0, import_jsx_runtime.jsx)("div", { className: "translated-title", style: error ? errorStyle : void 0, children }); } var translate_plugin_default = "/* 标题翻译插件样式(原 translate-plugin.tsx initCss 返回的 <style> 块,\n 改走 CSS 管线:utils.insertStyle 自动包裹 <style> 注入 head,时机/作用域与原先一致)。 */\n.translated-title {\n margin-top: 8px;\n padding: 12px;\n border-radius: 5px;\n border-left: 4px solid rgb(76, 175, 80);\n background: linear-gradient(135deg, rgb(255, 255, 255) 0%, rgb(245, 245, 245) 100%);\n box-shadow: 0 4px 8px rgb(0, 0, 0, 0.08);\n font-size: 20px;\n}\n"; var TranslatePlugin = class extends BasePlugin { getName() { return "TranslatePlugin"; } get pageTypes() { return ["detail"]; } async initCss() { return translate_plugin_default; } async handle() { if (!featureFlags.translatePlugin) return; if (window.isDetailPage) this.translate().then(); } async translate(carNum, showCarNum = true) { if (await storageManager.getSetting("translateTitle", "yes") !== "yes") return; let $titleElement = $(".origin-title"); if (!$titleElement.length) $titleElement = $(".current-title"); if (!$titleElement.length) $titleElement = $("h3"); if (!$titleElement.length) return; const originalText = $titleElement.text().trim(); if (!originalText) { show.error("获取标题失败, 无法进行翻译"); return; } if ($titleElement.next(".translated-title").length) return; $titleElement.after(jsxToString((0, import_jsx_runtime.jsx)(TranslatedTitle, { children: "翻译中..." }))); const $loadingElement = $titleElement.next(".translated-title"); if (!carNum) try { carNum = this.getPageInfo().carNum || ""; } catch { carNum = ""; } const cache = localStorage.getItem("jhs_translate") ? JSON.parse(localStorage.getItem("jhs_translate")) : {}; if (carNum && cache[carNum]) { $loadingElement.html(showCarNum ? carNum + " " + cache[carNum] : cache[carNum]); return; } try { const translatedText = await translateText(originalText, "ja", "zh-CN"); $loadingElement.html(showCarNum && carNum ? carNum + " " + translatedText : translatedText); if (carNum) { cache[carNum] = translatedText; localStorage.setItem("jhs_translate", JSON.stringify(cache)); } } catch (error) { clog.error("翻译失败:", error); const message = error instanceof DOMException && error.name === "AbortError" ? "翻译超时,请稍后重试" : `翻译失败: ${error instanceof Error ? error.message : String(error)}`; $loadingElement.replaceWith(jsxToString((0, import_jsx_runtime.jsx)(TranslatedTitle, { error: true, children: message }))); } } }; var reloadingStyle = { marginTop: "50px", cursor: "auto", color: "#000" }; function ScreenReloading() { return (0, import_jsx_runtime.jsx)("div", { style: reloadingStyle, children: "正在重新加载..." }); } var fallbackStyle = { marginTop: "50px", cursor: "auto", color: "#000" }; function ScreenShotFallback({ carNum }) { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ (0, import_jsx_runtime.jsx)("div", { style: fallbackStyle, children: "获取缩略图失败" }), (0, import_jsx_runtime.jsx)("br", {}), (0, import_jsx_runtime.jsx)("a", { href: "#", className: "retry-link", children: "点击重试" }), " ", "或", " ", (0, import_jsx_runtime.jsx)("a", { className: "check-link", href: `https://javstore.net/search?q=${carNum}`, target: "_blank", children: "前往确认" }) ] }); } var fullWidthStyle = { width: "100%" }; function ScreenShotImage({ src, alt }) { return (0, import_jsx_runtime.jsx)("img", { src, alt, loading: "lazy", style: fullWidthStyle }); } var ScreenShotPlugin = class extends BasePlugin { getName() { return "ScreenShotPlugin"; } get pageTypes() { return ["detail"]; } async handle() { if (!featureFlags.screenShotPlugin) return; this.loadScreenShot().then(); } async loadScreenShot() { if (!window.isDetailPage) return; const carNum = this.getPageInfo().carNum; $(".preview-images .tile-item").first().before(jsxToString((0, import_jsx_runtime.jsx)(ScreenLoadingPlaceholder, { maxHeight: "215px" }))); try { const imgUrl = await this.getScreenshot(carNum); this.addImg("缩略图", imgUrl); clog.log("加载缩略图:", imgUrl); } catch (e) { this.showErrorFallback(carNum, e); } } async getScreenshot(carNum) { let cacheData = {}; try { const cached = localStorage.getItem("jhs_screenShot"); if (cached) cacheData = JSON.parse(cached); } catch {} if (cacheData[carNum]) { clog.debug("缓存中存在缩略图:", carNum, cacheData[carNum]); return cacheData[carNum]; } let imgUrl = null; try { imgUrl = await this.getJavStoreScreenShot(carNum); } catch (e) { clog.error("获取缩略图资源失败:", imgUrl, e); throw e; } if (!imgUrl) { this.showErrorFallback(carNum, null); return null; } const httpsIndex = imgUrl.indexOf("https://"); if (httpsIndex !== -1) imgUrl = imgUrl.substring(httpsIndex); cacheData[carNum] = imgUrl; clog.log("缩略图获取成功:", imgUrl); localStorage.setItem("jhs_screenShot", JSON.stringify(cacheData)); return imgUrl; } async getJavStoreScreenShot(carNum) { const url = `https://javstore.net/search?q=${encodeURIComponent(carNum.toLowerCase().replace("fc2-", ""))}`; clog.log("正在解析缩略图:", url); const html = await gmHttp.get(url); const $dom = utils.htmlTo$dom(String(html)); const tempCarNum = carNum.toLowerCase().replace(/fc2-(ppv-)?/g, "").replace(/-/g, ""); let detailPageUrl = null; const $itemList = $dom.find("main .grid a"); for (let i = 0; i < $itemList.length; i++) { const href = $($itemList[i]).attr("href") || ""; if (href.toLowerCase().replace(/fc2-(ppv-)?/g, "").replace(/-/g, "").includes(tempCarNum)) { detailPageUrl = new URL(href, "https://javstore.net").href; break; } } if (!detailPageUrl) { clog.error("JavStore, 查询番号失败:", url); return null; } const detailPageHtml = await gmHttp.get(detailPageUrl); const $detailPageDom = utils.htmlTo$dom(String(detailPageHtml)); const imgUrl = $detailPageDom.find("a:contains('CLICK HERE')").attr("href") || $detailPageDom.find("img[src*='_s.jpg']").attr("src"); if (!imgUrl) { clog.error("JavStore, 解析预览图失败:", url); return null; } return String(imgUrl).replace(".th", ""); } addImg(title, imgUrl) { if (imgUrl) { $(".screen-container").html(jsxToString((0, import_jsx_runtime.jsx)(ScreenShotImage, { src: imgUrl, alt: title }))); $(".screen-container").on("click", (event) => { event.stopPropagation(); event.preventDefault(); window.showImageViewer(event.currentTarget); }); } } showErrorFallback(carNum, error) { clog.error("获取缩略图失败:", error instanceof Error ? error.message.substring(0, 100) : String(error)); $(".screen-container").html(jsxToString((0, import_jsx_runtime.jsx)(ScreenShotFallback, { carNum }))).off("click", ".retry-link").off("click", ".check-link").on("click", ".retry-link", async (e) => { e.stopPropagation(); e.preventDefault(); $(".screen-container").html(jsxToString((0, import_jsx_runtime.jsx)(ScreenReloading, {}))); try { const imgUrl = await this.getScreenshot(carNum); this.addImg("缩略图", imgUrl); } catch (err) { this.showErrorFallback(carNum, err); } }).on("click", ".check-link", async (e) => { e.stopPropagation(); e.preventDefault(); window.open(`https://javstore.net/search?q=${encodeURIComponent(carNum)}`, "_blank", "noopener,noreferrer"); }); } }; function MagnetHubContainer() { return (0, import_jsx_runtime.jsx)("div", { className: "magnet-container" }); } function MagnetHubTabs() { return (0, import_jsx_runtime.jsx)("div", { className: "magnet-tabs" }); } function MagnetHubResults() { return (0, import_jsx_runtime.jsx)("div", { className: "magnet-results" }); } function MagnetHubLoading({ engineName, keyword }) { return (0, import_jsx_runtime.jsxs)("div", { className: "magnet-loading", children: [ "正在从 ", engineName, " 搜索 \"", keyword, "\"..." ] }); } function MagnetHubError({ message }) { return (0, import_jsx_runtime.jsx)("div", { className: "magnet-error", children: message }); } function MagnetHubTab({ engineId, engineName }) { return (0, import_jsx_runtime.jsx)("div", { className: "magnet-tab", "data-engine": engineId, children: engineName }); } function MagnetHubTargetLink({ href }) { return (0, import_jsx_runtime.jsx)("a", { style: { marginRight: "20px", marginTop: "3px" }, id: "targetBox", href, target: "_blank", children: "原网页" }); } function MagnetResultCard({ magnet, title, size, date }) { return (0, import_jsx_runtime.jsxs)("div", { className: "magnet-result", children: [ (0, import_jsx_runtime.jsx)("div", { className: "magnet-title", children: (0, import_jsx_runtime.jsx)("a", { href: magnet, children: title }) }), (0, import_jsx_runtime.jsxs)("div", { className: "magnet-info", children: [(0, import_jsx_runtime.jsxs)("span", { children: ["大小: ", size] }), (0, import_jsx_runtime.jsxs)("span", { children: ["日期: ", date] })] }), (0, import_jsx_runtime.jsx)("div", { className: "magnet-copy", children: (0, import_jsx_runtime.jsx)("button", { className: "magnet-hub-btn copy-btn", "data-magnet": magnet, children: "复制链接" }) }) ] }); } var magnet_hub_plugin_default = "/*\n * 磁链聚合 MagnetHubPlugin 样式。\n *\n * 原脚本位于 MagnetHubPlugin.initCss() 返回的 `<style>` 字符串内(经\n * PluginManager.processCss → utils.insertStyle 注入 head),此处提取为\n * 独立 .css,由 initCss() 返回 → PluginManager utils.insertStyle 注入\n * (insertStyle 检测无 <style> 标签时自动包裹)。注入目标始终为 head\n * (全局作用域),与原实现一致。CSS 规则内容与原字符串一致(缩进标准化,\n * CSS 解析忽略空白,渲染等价)。\n */\n.magnet-container { margin: 20px auto; width: 100%; font-family: Arial, sans-serif; }\n.magnet-tabs { display: flex; border-bottom: 1px solid #ddd; margin-bottom: 15px; justify-content: space-between; }\n.magnet-tab { padding: 5px 12px; cursor: pointer; border: 1px solid transparent; border-bottom: none; margin-right: 5px; background: #f5f5f5; border-radius: 5px 5px 0 0; }\n.magnet-tab.active { background: #fff; border-color: #ddd; border-bottom: 1px solid #fff; margin-bottom: -1px; font-weight: bold; }\n.magnet-tab:hover:not(.active) { background: #e9e9e9; }\n.magnet-results { min-height: 200px; }\n.magnet-result { padding: 15px; border-bottom: 1px solid #eee; position: relative; }\n.magnet-result:hover { background-color: #f9f9f9; }\n.magnet-title { font-weight: bold; margin-bottom: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 80px; }\n.magnet-info { display: flex; justify-content: space-between; color: #666; font-size: 13px; }\n.magnet-copy { position: absolute; right: 15px; top: 15px; }\n.magnet-hub-btn { padding: 4px 10px; margin-left: 4px; cursor: pointer; border: 1px solid #ddd; border-radius: 3px; background: #f5f5f5; }\n.magnet-hub-btn.copied { background: #4caf50; color: #fff; }\n.magnet-loading, .magnet-error { padding: 20px; text-align: center; color: #666; }\n.magnet-error { color: #c00; }\n"; var MagnetHubPlugin = class extends BasePlugin { currentEngine = null; _searchSeq = 0; searchEngines = [ { name: "U9A9", id: "u9a9", url: "https://u9a9.com/?type=2&search={keyword}", targetPage: "https://u9a9.com/?type=2&search={keyword}", parseHtml: this.parseU3C3 }, { name: "U3C3", id: "u3c3", url: "https://u3c3.com/?search2=a8lr16lo&search={keyword}", targetPage: "https://u3c3.com/?search2=a8lr16lo&search={keyword}", parseHtml: this.parseU3C3 }, { name: "Sukebei", id: "Sukebei", url: "https://sukebei.nyaa.si/?f=0&c=0_0&q={keyword}", targetPage: "https://sukebei.nyaa.si/?f=0&c=0_0&q={keyword}", parseHtml: this.parseSukebei } ]; getName() { return "MagnetHubPlugin"; } async initCss() { return magnet_hub_plugin_default; } createMagnetHub(keyword) { if (!featureFlags.magnetHubPlugin) return $(jsxToString((0, import_jsx_runtime.jsx)(MagnetHubError, { message: "磁力聚合未启用" }))); keyword = keyword.replace("FC2-", ""); const $container = $(jsxToString((0, import_jsx_runtime.jsx)(MagnetHubContainer, {}))); const $tabs = $(jsxToString((0, import_jsx_runtime.jsx)(MagnetHubTabs, {}))); const key = "jhs_magnetHub_selectedEngine"; const savedEngineId = localStorage.getItem(key); let defaultEngineIndex = 0; const $tabBox = $(document.createElement("div")).css("display", "flex"); this.searchEngines.forEach((engine, index) => { const $tab = $(jsxToString((0, import_jsx_runtime.jsx)(MagnetHubTab, { engineId: engine.id, engineName: engine.name }))); if (savedEngineId && engine.id === savedEngineId) { $tab.addClass("active"); this.currentEngine = engine; defaultEngineIndex = index; } else if (index === 0 && !savedEngineId) { $tab.addClass("active"); this.currentEngine = engine; } $tabBox.append($tab); }); $tabs.append($tabBox); const engine = this.currentEngine || this.searchEngines[0]; $tabs.append(jsxToString((0, import_jsx_runtime.jsx)(MagnetHubTargetLink, { href: engine.targetPage.replace("{keyword}", encodeURIComponent(keyword)) }))); $container.append($tabs); const $resultsContainer = $(jsxToString((0, import_jsx_runtime.jsx)(MagnetHubResults, {}))); $container.append($resultsContainer); $container.on("click", ".magnet-tab", (e) => { const engineId = $(e.target).data("engine"); this.currentEngine = this.searchEngines.find((eng) => eng.id === engineId) || null; if (this.currentEngine) { $("#targetBox").attr("href", this.currentEngine.targetPage.replace("{keyword}", encodeURIComponent(keyword))); localStorage.setItem(key, engineId); $container.find(".magnet-tab").removeClass("active"); $(e.target).addClass("active"); this.searchEngine($resultsContainer, this.currentEngine, keyword); } }); this.searchEngine($resultsContainer, this.currentEngine || this.searchEngines[defaultEngineIndex], keyword); return $container; } searchEngine($container, engine, keyword) { const seq = ++this._searchSeq; $container.html(jsxToString((0, import_jsx_runtime.jsx)(MagnetHubLoading, { engineName: engine.name, keyword }))); const cacheKey = `${engine.name}_${keyword}`; const cached = sessionStorage.getItem(cacheKey); if (cached) try { this.displayResults($container, JSON.parse(cached), engine.name); return; } catch {} const url = engine.url.replace("{keyword}", encodeURIComponent(keyword)); if (engine.parseHtml) GM_xmlhttpRequest({ method: "GET", url, timeout: 15e3, onload: (response) => { try { const results = engine.parseHtml.call(this, response.responseText, keyword); if (results.length > 0) sessionStorage.setItem(cacheKey, JSON.stringify(results)); if (seq !== this._searchSeq) return; this.displayResults($container, results, engine.name); } catch (e) { $container.html(jsxToString((0, import_jsx_runtime.jsx)(MagnetHubError, { message: `解析 ${engine.name} 结果失败: ${e instanceof Error ? e.message : String(e)}` }))); } }, onerror: (error) => { if (seq !== this._searchSeq) return; $container.html(jsxToString((0, import_jsx_runtime.jsx)(MagnetHubError, { message: `从 ${engine.name} 获取数据失败: ${error.statusText || "网络错误"}` }))); }, ontimeout: () => { if (seq !== this._searchSeq) return; $container.html(jsxToString((0, import_jsx_runtime.jsx)(MagnetHubError, { message: `从 ${engine.name} 获取数据超时` }))); } }); } displayResults($container, results, _engineName) { $container.empty(); if (results.length === 0) { $container.append(jsxToString((0, import_jsx_runtime.jsx)(MagnetHubError, { message: "没有找到相关结果" }))); return; } results.forEach((result) => { const $result = $(jsxToString((0, import_jsx_runtime.jsx)(MagnetResultCard, { magnet: result.magnet, title: result.title, size: result.size || "未知", date: result.date || "未知" }))); $container.append($result); }); $container.on("click", ".copy-btn", function() { const $btn = $(this); const magnet = $btn.data("magnet"); const showCopied = () => { const originalText = $btn.text(); $btn.addClass("copied").text("已复制"); setTimeout(() => { $btn.removeClass("copied").text(originalText); }, 2e3); }; if (navigator.clipboard) navigator.clipboard.writeText(magnet).then(showCopied).catch(() => { utils.copyToClipboard("磁链", magnet); showCopied(); }); else { utils.copyToClipboard("磁链", magnet); showCopied(); } }); } parseU3C3(html, keyword) { const $dom = utils.htmlTo$dom(html); const results = []; $dom.find(".torrent-list tbody tr").each((_i, el) => { const $el = $(el); if ($el.text().includes("置顶")) return; const title = $el.find("td:nth-child(2) a").attr("title") || $el.find("td:nth-child(2) a").text().trim(); if (!title.toLowerCase().includes(keyword.toLowerCase())) return; const magnet = $el.find("td:nth-child(3) a[href^='magnet:']").attr("href"); const size = $el.find("td:nth-child(4)").text().trim(); const date = $el.find("td:nth-child(5)").text().trim(); if (magnet) results.push({ title, magnet, size, date }); }); return results; } parseSukebei(html, keyword) { return this.parseU3C3(html, keyword); } }; function ImageRecognitionDialog() { return (0, import_jsx_runtime.jsxs)("div", { style: { padding: "20px" }, children: [(0, import_jsx_runtime.jsxs)("div", { id: "upload-area", children: [ (0, import_jsx_runtime.jsxs)("div", { style: { color: "#555", marginBottom: "15px" }, children: [(0, import_jsx_runtime.jsx)("p", { children: "拖拽图片到此处 或 点击按钮选择图片" }), (0, import_jsx_runtime.jsx)("p", { children: "也可以直接 Ctrl+V 粘贴图片 / 图片URL" })] }), (0, import_jsx_runtime.jsx)("button", { id: "select-image-btn", children: "选择图片" }), (0, import_jsx_runtime.jsx)("input", { type: "file", style: { display: "none" }, id: "image-file", accept: "image/*" }) ] }), (0, import_jsx_runtime.jsxs)("div", { id: "preview-area", style: { marginBottom: "20px", textAlign: "center", display: "none" }, children: [ (0, import_jsx_runtime.jsx)("img", { id: "preview-image", alt: "", src: "", style: { maxWidth: "100%", maxHeight: "300px", borderRadius: "4px" } }), (0, import_jsx_runtime.jsxs)("div", { style: { marginTop: "15px", display: "flex", justifyContent: "center", gap: "10px" }, id: "action-btns", children: [(0, import_jsx_runtime.jsx)("button", { id: "handle-btn", children: "搜索图片" }), (0, import_jsx_runtime.jsx)("button", { id: "cancel-btn", children: "取消" })] }), (0, import_jsx_runtime.jsxs)("div", { id: "search-results", style: { display: "none" }, children: [(0, import_jsx_runtime.jsxs)("p", { style: { margin: "20px auto" }, children: ["请选择识图网站:", (0, import_jsx_runtime.jsx)("a", { id: "openAll", style: { cursor: "pointer" }, children: "全部打开" })] }), (0, import_jsx_runtime.jsx)("div", { className: "search-img-site-btns-container", id: "search-img-site-btns-container" })] }) ] })] }); } function ImageRecognitionHint() { return (0, import_jsx_runtime.jsx)("p", { style: { color: "#c00" }, children: "图片尚未获得公网 URL,请重试搜索" }); } function ImageRecognitionSiteButton({ href, icon, name }) { return (0, import_jsx_runtime.jsxs)("a", { className: "search-img-site-btn", href, target: "_blank", children: [(0, import_jsx_runtime.jsx)("img", { src: icon, alt: "" }), (0, import_jsx_runtime.jsx)("span", { children: name })] }); } var image_recognition_plugin_default = "/*\n * 以图识图 ImageRecognitionPlugin 样式。\n *\n * 原脚本位于 ImageRecognitionPlugin.initCss() 返回的 `\\3c style>` 字符串内\n * (经 PluginManager.processCss → utils.insertStyle 注入 head),此处提取\n * 为独立 .css,由 initCss() 返回 → PluginManager utils.insertStyle 注入\n * (insertStyle 检测无 \\3c style> 标签时自动包裹)。注入目标始终为 head\n * (全局作用域),与原实现一致。CSS 规则内容与原字符串一致(缩进标准化,\n * CSS 解析忽略空白,渲染等价)。\n */\n#upload-area {\n border: 2px dashed #85af68; border-radius: 8px; padding: 40px;\n text-align: center; margin-bottom: 20px; transition: border-color 0.3s, background-color 0.3s;\n background-color: #f9f9f9;\n}\n#upload-area:hover { border-color: #76b947; background-color: #f0f0f0; }\n#upload-area.highlight { border-color: #2196F3; background-color: #e3f2fd; }\n\n#select-image-btn {\n background-color: #4CAF50; color: white; border: none;\n padding: 10px 20px; border-radius: 4px; cursor: pointer; font-size: 16px;\n}\n\n#handle-btn, #cancel-btn {\n padding: 8px 16px; border-radius: 4px; cursor: pointer;\n font-size: 14px; border: none;\n}\n#handle-btn { background-color: #2196F3; color: white; }\n#cancel-btn { background-color: #f44336; color: white; }\n\n.search-img-site-btns-container {\n display: flex; flex-wrap: wrap; gap: 10px; margin-top: 15px;\n}\n\n.search-img-site-btn {\n display: flex; align-items: center; padding: 8px 12px;\n background-color: #f5f5f5; border-radius: 4px; text-decoration: none;\n color: #333; font-size: 14px; border: 1px solid #ddd;\n}\n.search-img-site-btn img { width: 16px; height: 16px; margin-right: 6px; }\n"; var ImageRecognitionPlugin = class extends BasePlugin { siteList = [ { name: "Google旧版", url: "https://www.google.com/searchbyimage?image_url={placeholder}&client=firefox-b-d", ico: "https://www.google.com/favicon.ico" }, { name: "Google", url: "https://lens.google.com/uploadbyurl?url={placeholder}", ico: "https://www.google.com/favicon.ico" }, { name: "Yandex", url: "https://yandex.ru/images/search?rpt=imageview&url={placeholder}", ico: "https://yandex.ru/favicon.ico" } ]; isUploading = false; currentImageUrl = null; getName() { return "ImageRecognitionPlugin"; } async initCss() { return image_recognition_plugin_default; } open(onOpenFun) { if (!featureFlags.imageRecognitionPlugin) return; layer.open({ type: 1, title: "以图识图", content: jsxToString((0, import_jsx_runtime.jsx)(ImageRecognitionDialog, {})), area: utils.isMobile() ? utils.getResponsiveArea() : ["40%", "80%"], success: async () => { this.initEventListeners(); onOpenFun?.(); }, end: () => { $(document).off("paste.searchImg"); } }); } initEventListeners() { const $uploadArea = $("#upload-area"); const $fileInput = $("#image-file"); const $selectBtn = $("#select-image-btn"); const $previewArea = $("#preview-area"); const $previewImage = $("#preview-image"); const $searchImage = $("#handle-btn"); const $cancelBtn = $("#cancel-btn"); const $searchResults = $("#search-results"); const $siteBtnsContainer = $("#search-img-site-btns-container"); $uploadArea.on("dragover", (e) => { e.preventDefault(); $uploadArea.addClass("highlight"); }).on("dragleave", () => $uploadArea.removeClass("highlight")).on("drop", (e) => { e.preventDefault(); $uploadArea.removeClass("highlight"); const files = e.originalEvent?.dataTransfer?.files; if (files?.[0]) this.handleImageFile(files[0]); }); $selectBtn.on("click", () => $fileInput.trigger("click")); $fileInput.on("change", (e) => { const files = e.target.files; if (files?.[0]) this.handleImageFile(files[0]); }); $(document).on("paste.searchImg", async (e) => { const clipData = e.originalEvent?.clipboardData; const items = clipData?.items; if (!items) return; for (let i = 0; i < items.length; i++) if (items[i].type.indexOf("image") !== -1) { const file = items[i].getAsFile(); if (file) this.handleImageFile(file); return; } const text = clipData?.getData("text"); if (text && /^https?:\/\//.test(text)) { this.currentImageUrl = text; $previewImage.attr("src", text); $uploadArea.hide(); $previewArea.show(); } }); $searchImage.on("click", async () => { if (!this.currentImageUrl) { show.error("请先选择图片"); return; } await this.searchByImage(this.currentImageUrl); $searchResults.show(); this.renderSiteButtons($siteBtnsContainer); }); $cancelBtn.on("click", () => { this.currentImageUrl = null; $previewArea.hide(); $uploadArea.show(); $searchResults.hide(); $fileInput.val(""); }); $("#openAll").on("click", () => { this.siteList.forEach((site) => { if (this.currentImageUrl) window.open(site.url.replace("{placeholder}", encodeURIComponent(this.currentImageUrl)), "_blank"); }); }); } handleImageFile(file) { const reader = new FileReader(); reader.onload = (e) => { const dataUrl = e.target.result; this.currentImageUrl = dataUrl; $("#preview-image").attr("src", dataUrl); $("#upload-area").hide(); $("#preview-area").show(); this.searchByImage(dataUrl).then(() => { $("#search-results").show(); this.renderSiteButtons($("#search-img-site-btns-container")); }); }; reader.readAsDataURL(file); } async searchByImage(imageSrc) { if (this.isUploading) return; if (/^https?:\/\//.test(imageSrc) && !imageSrc.startsWith("data:")) { this.currentImageUrl = imageSrc; return; } this.isUploading = true; try { const base64 = imageSrc.replace(/^data:image\/\w+;base64,/, ""); const resp = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: "https://api.imgur.com/3/image", headers: { Authorization: "Client-ID d70305e7c3ac5c6", "Content-Type": "application/json" }, data: JSON.stringify({ image: base64, type: "base64" }), onload: (r) => { try { resolve(JSON.parse(r.responseText)); } catch (err) { reject(err); } }, onerror: reject }); }); if (resp?.data?.link) this.currentImageUrl = resp.data.link; else show.error("图片上传失败"); } catch (e) { clog.error(e); show.error("图片上传失败: " + (e instanceof Error ? e.message : String(e))); } finally { this.isUploading = false; } } renderSiteButtons($container) { $container.empty(); if (!this.currentImageUrl || this.currentImageUrl.startsWith("data:")) { $container.append(jsxToString((0, import_jsx_runtime.jsx)(ImageRecognitionHint, {}))); return; } this.siteList.forEach((site) => { const href = site.url.replace("{placeholder}", encodeURIComponent(this.currentImageUrl)); $container.append(jsxToString((0, import_jsx_runtime.jsx)(ImageRecognitionSiteButton, { href, icon: site.ico, name: site.name }))); }); } }; var wrapperStyle = { marginBottom: "15px" }; var searchRowStyle = { display: "flex", gap: "8px", flexWrap: "wrap", marginBottom: "10px" }; var keywordInputStyle = { maxWidth: "280px" }; var sortSelectStyle = { height: "2.5em" }; var paginationStyle = { marginTop: "15px", textAlign: "center" }; function Fc2BrowsePage({ keyword }) { return (0, import_jsx_runtime.jsxs)("div", { style: wrapperStyle, children: [ (0, import_jsx_runtime.jsx)("h2", { className: "title is-4", children: "123Av FC2 浏览" }), (0, import_jsx_runtime.jsxs)("div", { style: searchRowStyle, children: [ (0, import_jsx_runtime.jsx)("input", { id: "fc2-123av-keyword", className: "input", style: keywordInputStyle, placeholder: "搜索关键词", value: keyword || "" }), (0, import_jsx_runtime.jsx)("button", { id: "fc2-123av-search", className: "button is-info", children: "搜索" }), (0, import_jsx_runtime.jsxs)("select", { id: "fc2-123av-sort", className: "select", style: sortSelectStyle, children: [ (0, import_jsx_runtime.jsx)("option", { value: "release_date", children: "发布日期" }), (0, import_jsx_runtime.jsx)("option", { value: "recent_update", children: "最近更新" }), (0, import_jsx_runtime.jsx)("option", { value: "trending", children: "热门" }), (0, import_jsx_runtime.jsx)("option", { value: "most_viewed_today", children: "今天最多观看" }), (0, import_jsx_runtime.jsx)("option", { value: "most_viewed_week", children: "本周最多观看" }), (0, import_jsx_runtime.jsx)("option", { value: "most_viewed_month", children: "本月最多观看" }), (0, import_jsx_runtime.jsx)("option", { value: "most_viewed", children: "最多观看" }), (0, import_jsx_runtime.jsx)("option", { value: "most_favourited", children: "最受欢迎" }) ] }) ] }), (0, import_jsx_runtime.jsx)("div", { id: "fc2-123av-list", className: "movie-list h cols-4 vcols-8" }), (0, import_jsx_runtime.jsx)("div", { id: "fc2-123av-pagination", style: paginationStyle }) ] }); } function hookPage(plugin) { document.title = "123Av FC2"; const $box = plugin.$contentBox; if (!$box?.length) return; $box.html(jsxToString((0, import_jsx_runtime.jsx)(Fc2BrowsePage, { keyword: plugin.keyword ?? void 0 }))); $("#fc2-123av-sort").val(plugin.sortVal); $("#fc2-123av-search").on("click", () => { plugin.keyword = String($("#fc2-123av-keyword").val() || "") || null; plugin.currentPage = 1; plugin.handleQuery(); }); $("#fc2-123av-sort").on("change", () => { plugin.sortVal = String($("#fc2-123av-sort").val() || "release_date"); plugin.currentPage = 1; plugin.handleQuery(); }); } function renderPagination(plugin) { const $pg = $("#fc2-123av-pagination"); if (!plugin.maxPage || plugin.maxPage <= 1) { $pg.empty(); return; } let html = ""; const cur = plugin.currentPage; const max = plugin.maxPage; const pages = []; for (let i = 1; i <= max; i++) if (i === 1 || i === max || i >= cur - 2 && i <= cur + 2) pages.push(i); let last = 0; for (const p of pages) { if (last && p - last > 1) html += jsxToString((0, import_jsx_runtime.jsx)("span", { style: { margin: "0 4px" }, children: "…" })); html += jsxToString((0, import_jsx_runtime.jsx)("a", { className: `button is-small ${p === cur ? "is-primary" : ""}`, "data-page": p, style: { margin: "2px" }, children: p })); last = p; } $pg.html(html); $pg.find("a[data-page]").on("click", (e) => { e.preventDefault(); plugin.currentPage = parseInt($(e.currentTarget).attr("data-page") || "1", 10); plugin.handleQuery(); }); } async function handleQuery(plugin) { const seq = ++plugin._querySeq; const base = await plugin.getBaseUrl(); const fetchPage = async (page) => { let url; if (plugin.keyword) url = `${base}/search?keyword=${encodeURIComponent(plugin.keyword)}&page=${page}&sort=${plugin.sortVal}`; else url = `${base}/makers/fc2?page=${page}&sort=${plugin.sortVal}`; const html = await gmHttp.get(url); return utils.htmlTo$dom(String(html)); }; try { show.info("正在加载 123Av FC2..."); const pageA = (plugin.currentPage - 1) * 2 + 1; const pageB = pageA + 1; const $dom1 = await fetchPage(pageA); let movies = parseCards(plugin, $dom1); try { const $dom2 = await fetchPage(pageB); movies = movies.concat(parseCards(plugin, $dom2)); } catch {} if (seq !== plugin._querySeq) return; const lastPageText = $dom1.find(".pagination .page-link").last().text() || $dom1.find(".pagination a").last().text(); plugin.maxPage = Math.max(1, Math.ceil((parseInt(lastPageText, 10) || 1) / 2)); const html = movies.map((movie) => jsxToString((0, import_jsx_runtime.jsx)(HitShowMovieItem, { movie }))).join(""); $("#fc2-123av-list").html(html || jsxToString((0, import_jsx_runtime.jsx)("p", { children: "无结果" }))); renderPagination(plugin); $("#fc2-123av-list .item a.box").on("click", (e) => { e.preventDefault(); const href = $(e.currentTarget).attr("href") || ""; const carNum = $(e.currentTarget).find("strong").text().trim() || href.split("/").pop() || ""; plugin.open123AvFc2Dialog(carNum, href.startsWith("http") ? href : base + href); }); } catch (e) { clog.error(e); show.error("加载 123Av FC2 失败: " + (e instanceof Error ? e.message : String(e))); } } function parseCards(_plugin, $dom) { const list = []; $dom.find(".card, .movie-list .item, .box").each((_i, el) => { const $el = $(el); const $a = $el.find("a").first(); const href = $a.attr("href") || ""; const title = $a.attr("title") || $el.find(".video-title, .title, h3").text().trim() || ""; const number = $el.find("strong").text().trim() || (title.match(/FC2[-_]?PPV[-_]?(\d+)/i) ? "FC2-" + title.match(/FC2[-_]?PPV[-_]?(\d+)/i)[1] : title.split(" ")[0]); const cover = $el.find("img").attr("data-src") || $el.find("img").attr("src") || ""; const id = href.split("/").filter(Boolean).pop() || number; if (number || href) list.push({ id, number: number.startsWith("FC2") ? number : `FC2-${number}`, origin_title: title, cover_url: cover, release_date: $el.find(".meta, .date").text().trim() || "", has_cnsub: false, magnets_count: 0, new_magnets: false, _href: href }); }); return list; } var containerStyle = { padding: "15px" }; var panelInfoStyle = { marginTop: "15px" }; var toolbarStyle = { margin: "20px 0" }; var galleryStyle = { marginTop: "10px" }; var btnBaseStyle = { color: "#fff", padding: "6px 12px" }; var filterBtnStyle = { ...btnBaseStyle, backgroundColor: "#de3333", marginRight: "6px" }; var favoriteBtnStyle = { ...btnBaseStyle, backgroundColor: "#25b1dc", marginRight: "6px" }; var hasDownBtnStyle = { ...btnBaseStyle, backgroundColor: "#7bc73b", marginRight: "6px" }; var hasWatchBtnStyle = { ...btnBaseStyle, backgroundColor: "#d7a80c", marginRight: "6px" }; var magnetSearchBtnStyle = { ...btnBaseStyle, background: "linear-gradient(to right,#f58c01,#54a11d)" }; function Fc2123avDetailDialog() { return (0, import_jsx_runtime.jsxs)("div", { className: "movie-detail-container", style: containerStyle, children: [ (0, import_jsx_runtime.jsx)("div", { className: "movie-info-container", children: (0, import_jsx_runtime.jsx)("div", { className: "search-loading", children: "加载中..." }) }), (0, import_jsx_runtime.jsx)("div", { className: "movie-panel-info", style: panelInfoStyle, children: (0, import_jsx_runtime.jsx)("strong", { children: "第三方资源: " }) }), (0, import_jsx_runtime.jsxs)("div", { style: toolbarStyle, children: [ (0, import_jsx_runtime.jsx)("a", { id: "filterBtn", className: "jhs-toolbar-menu-btn", style: filterBtnStyle, children: "🚫 屏蔽" }), (0, import_jsx_runtime.jsx)("a", { id: "favoriteBtn", className: "jhs-toolbar-menu-btn", style: favoriteBtnStyle, children: "⭐ 收藏" }), (0, import_jsx_runtime.jsx)("a", { id: "hasDownBtn", className: "jhs-toolbar-menu-btn", style: hasDownBtnStyle, children: "📥️ 已下载" }), (0, import_jsx_runtime.jsx)("a", { id: "hasWatchBtn", className: "jhs-toolbar-menu-btn", style: hasWatchBtnStyle, children: "🔍 已观看" }), (0, import_jsx_runtime.jsx)("a", { id: "magnetSearchBtn", className: "jhs-toolbar-menu-btn", style: magnetSearchBtnStyle, children: "磁力搜索" }) ] }), (0, import_jsx_runtime.jsx)("div", { id: "magnets-content", className: "magnet-links" }), (0, import_jsx_runtime.jsxs)("div", { className: "movie-gallery", style: galleryStyle, children: [(0, import_jsx_runtime.jsx)("strong", { children: "剧照: " }), (0, import_jsx_runtime.jsx)("div", { className: "image-list" })] }) ] }); } var posterWrapStyle = { marginTop: "8px" }; var posterImgStyle = { maxWidth: "320px" }; var actorsStyle = { marginTop: "8px" }; var actressTagStyle = { margin: "2px" }; var hiddenStyle = { display: "none" }; function Fc2123avMovieInfo({ info, carNum, actresses }) { return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ (0, import_jsx_runtime.jsx)("div", { className: "movie-title", children: (0, import_jsx_runtime.jsx)("strong", { children: info.title || carNum }) }), (0, import_jsx_runtime.jsxs)("div", { children: ["番号: ", info.id || carNum] }), (0, import_jsx_runtime.jsxs)("div", { children: ["发布日: ", info.releaseDate || ""] }), info.poster ? (0, import_jsx_runtime.jsx)("div", { style: posterWrapStyle, children: (0, import_jsx_runtime.jsx)("img", { src: info.poster, style: posterImgStyle }) }) : "", (0, import_jsx_runtime.jsxs)("div", { className: "movie-actors", style: actorsStyle, children: [(0, import_jsx_runtime.jsx)("strong", { children: "主演: " }), actresses.length > 0 ? actresses.map((actress) => (0, import_jsx_runtime.jsx)("span", { className: "tag", style: actressTagStyle, children: actress })) : "未知"] }), (0, import_jsx_runtime.jsx)("div", { id: "data-releaseDate", style: hiddenStyle, children: info.releaseDate || "" }) ] }); } var HAS_DOWN_STATUS = "hasDown"; async function open123AvFc2Dialog(plugin, carNum, href) { const dialogHtml = jsxToString((0, import_jsx_runtime.jsx)(Fc2123avDetailDialog, {})); layer.open({ type: 1, title: carNum, content: dialogHtml, area: utils.getResponsiveArea(["80%", "90%"]), success: async () => { await loadData(plugin, carNum, href); handleLongImg(plugin, carNum.replace(/^FC2-/, "")); $("#filterBtn").on("click", () => saveAction(plugin, carNum, href, FILTER_ACTION)); $("#favoriteBtn").on("click", () => saveAction(plugin, carNum, href, FAVORITE_ACTION)); $("#hasDownBtn").on("click", () => saveAction(plugin, carNum, href, HAS_DOWN_STATUS)); $("#hasWatchBtn").on("click", () => saveAction(plugin, carNum, href, HAS_WATCH_ACTION)); $("#magnetSearchBtn").on("click", () => { const magnetHub = plugin.getBean("MagnetHubPlugin"); if (magnetHub) $("#magnets-content").empty().append(magnetHub.createMagnetHub(carNum)); }); } }); } async function saveAction(_plugin, carNum, url, actionType) { await storageManager.saveCar({ carNum, url, names: void 0, actionType, publishTime: $("#data-releaseDate").text() || "" }); show.ok("操作成功"); refresh(); } async function loadData(plugin, carNum, href) { try { const info = await get123AvVideoInfo(plugin, href); const fc2Num = carNum.replace(/^FC2-?(PPV-?)?/i, ""); const actressInfo = await getActressInfo(plugin, fc2Num).catch(() => null); const imgList = await getImgList(plugin, fc2Num).catch(() => []); const actresses = actressInfo?.actors?.length ? actressInfo.actors : []; const imagesHtml = imgList.map((src) => jsxToString((0, import_jsx_runtime.jsx)("a", { className: "tile-item", href: src, target: "_blank", children: (0, import_jsx_runtime.jsx)("img", { src, style: { maxWidth: "150px", margin: "4px" }, loading: "lazy" }) }))).join(""); $(".movie-info-container").html(jsxToString((0, import_jsx_runtime.jsx)(Fc2123avMovieInfo, { info, carNum, actresses }))); $(".movie-gallery .image-list").html(imagesHtml); plugin.getBean("TranslatePlugin")?.translate?.(carNum, false)?.then?.(); } catch (e) { clog.error(e); $(".movie-info-container").html(jsxToString((0, import_jsx_runtime.jsx)(MovieError, { message: e instanceof Error ? e.message : String(e) }))); } } function handleLongImg(plugin, carNum) { utils.loopDetector(() => $(".movie-gallery .image-list").length > 0, async () => { $(".movie-gallery .image-list").prepend(jsxToString((0, import_jsx_runtime.jsx)(ScreenLoadingPlaceholder, { maxHeight: "150px" }))); const screenShot = plugin.getBean("ScreenShotPlugin"); if (!screenShot) return; try { const screenshotUrl = await screenShot.getScreenshot(carNum); if (screenshotUrl) screenShot.addImg("缩略图", screenshotUrl); } catch {} }); } async function get123AvVideoInfo(plugin, href) { const fullHref = href.startsWith("http") ? href : await plugin.getBaseUrl() + href; const html = await gmHttp.get(fullHref); const $dom = utils.htmlTo$dom(String(html)); const title = $dom.find("h1").first().text().trim() || $dom.find(".title").first().text().trim() || ""; const poster = $dom.find("video").attr("poster") || $dom.find(".detail-img img, .cover img").attr("src") || ""; const releaseDate = $dom.find(".detail-item:contains(\"発売日\"), .detail-item:contains(\"发布\")").text().replace(/.*?[::]/, "").trim() || $dom.find("time").text().trim() || ""; return { id: $dom.find(".detail-item:contains(\"品番\"), .detail-item:contains(\"番号\")").text().replace(/.*?[::]/, "").trim() || title.split(" ")[0] || "", releaseDate, title, poster }; } async function getActressInfo(_plugin, fc2Num) { try { const html = await gmHttp.get(`https://fc2ppvdb.com/articles/${fc2Num}`); const $dom = utils.htmlTo$dom(String(html)); const actors = []; $dom.find("a[href*=\"/actress\"], .actress a, .actors a").each((_i, el) => { const name = $(el).text().trim(); if (name) actors.push(name); }); return { actors }; } catch { return null; } } async function getImgList(_plugin, fc2Num) { try { const html = await gmHttp.get(`https://adult.contents.fc2.com/article/${fc2Num}/`); const $dom = utils.htmlTo$dom(String(html)); const imgs = []; $dom.find("img").each((_i, el) => { const src = $(el).attr("src") || $(el).attr("data-src") || ""; if (src && /sample|preview|main/i.test(src)) imgs.push(src); }); return imgs.slice(0, 20); } catch { return []; } } var Fc2By123AvPlugin = class extends BasePlugin { $contentBox = null; urlParams = new URLSearchParams(window.location.search); sortVal = "release_date"; currentPage = 1; maxPage = null; keyword = null; _querySeq = 0; constructor() { super(); this.sortVal = this.urlParams.get("sort") || "release_date"; this.currentPage = parseInt(this.urlParams.get("page") || "", 10) || 1; this.keyword = this.urlParams.get("keyword") || null; } getName() { return "Fc2By123AvPlugin"; } async getBaseUrl() { return "https://123av.com".replace(/\/$/, "") + "/ja"; } async handle() { if (!featureFlags.fc2By123AvPlugin) return; this.injectNavEntry(); if (window.location.href.includes("/advanced_search?type=100")) { this.$contentBox = $(".section .container"); this.hookPage(); await this.handleQuery(); } } injectNavEntry() { const href = "/advanced_search?type=100&released_start=2099-09"; if ($("#navbar-item-123av-fc2").length === 0) { const $fc2Nav = $("#navbar-menu-hero a.navbar-item").filter((_i, el) => { const text = $(el).text().trim(); const link = $(el).attr("href") || ""; return text === "FC2" || link.includes("advanced_search?type=3"); }).first(); if ($fc2Nav.length) $fc2Nav.after(jsxToString((0, import_jsx_runtime.jsx)("a", { className: "navbar-item", id: "navbar-item-123av-fc2", href, children: "123Av-Fc2" }))); else $("#navbar-menu-hero .navbar-start").first().append(jsxToString((0, import_jsx_runtime.jsx)("a", { className: "navbar-item", id: "navbar-item-123av-fc2", href, children: "123Av-Fc2" }))); } if ($(".tabs li:contains(\"FC2\")").length && !$(".tabs a[href*=\"advanced_search?type=100\"]").length) $(".tabs li:contains(\"FC2\")").after(jsxToString((0, import_jsx_runtime.jsx)("li", { id: "tab-123av-fc2", children: (0, import_jsx_runtime.jsx)("a", { href, children: (0, import_jsx_runtime.jsx)("span", { children: "123Av-Fc2" }) }) }))); } hookPage() { hookPage(this); } renderPagination() { renderPagination(this); } async handleQuery() { await handleQuery(this); } parseCards($dom) { return parseCards(this, $dom); } async open123AvFc2Dialog(carNum, href) { await open123AvFc2Dialog(this, carNum, href); } async saveAction(carNum, url, actionType) { await saveAction(this, carNum, url, actionType); } async loadData(carNum, href) { await loadData(this, carNum, href); } handleLongImg(carNum) { handleLongImg(this, carNum); } async get123AvVideoInfo(href) { return get123AvVideoInfo(this, href); } async getActressInfo(fc2Num) { return getActressInfo(this, fc2Num); } async getImgList(fc2Num) { return getImgList(this, fc2Num); } }; var loading_default = ".loading-container {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n background-color: rgb(0, 0, 0, 0.1);\n z-index: var(--jhs-z-top);\n}\n\n.loading-animation {\n position: relative;\n width: 60px;\n height: 12px;\n background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);\n border-radius: 6px;\n animation: loading-animate 1.8s ease-in-out infinite;\n box-shadow: 0 4px 12px rgb(0, 0, 0, 0.1);\n}\n\n.loading-animation::before,\n.loading-animation::after {\n position: absolute;\n display: block;\n content: '';\n animation: loading-animate 1.8s ease-in-out infinite;\n height: 12px;\n border-radius: 6px;\n box-shadow: 0 4px 12px rgb(0, 0, 0, 0.1);\n}\n\n.loading-animation::before {\n top: -20px;\n left: 10px;\n width: 40px;\n background: linear-gradient(90deg, #ff758c 0%, #ff7eb3 100%);\n}\n\n.loading-animation::after {\n bottom: -20px;\n width: 35px;\n background: linear-gradient(90deg, #ff9a9e 0%, #fad0c4 100%);\n}\n\n@keyframes loading-animate {\n 0% {\n transform: translateX(40px);\n }\n\n 50% {\n transform: translateX(-30px);\n }\n\n 100% {\n transform: translateX(40px);\n }\n}\n"; var viewer_default = ".viewer-canvas {\n overflow: auto !important;\n}\n\n.viewer-close {\n background: rgb(255, 0, 0, 0.6) !important;\n}\n\n.viewer-close:hover {\n background: rgb(255, 0, 0, 0.8) !important;\n}\n"; var logger_default = ".console-logger-container {\n position: fixed;\n bottom: 0;\n right: 0;\n z-index: var(--jhs-z-top);\n font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n width: fit-content;\n}\n\n.console-logger-toggle {\n width: 40px;\n height: 30px;\n background: #2c3e50;\n border-radius: 120px 10px 0 0;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n box-shadow: -2px 0 5px rgb(0, 0, 0, 0.1);\n transition: background-color 0.3s ease;\n color: white;\n font-size: 16px;\n}\n\n.console-logger-toggle:hover {\n background: #34495e;\n}\n\n.console-logger-toggle::after {\n content: '▼';\n transition: transform 0.3s ease;\n}\n\n.console-logger-toggle.collapsed::after {\n content: '▲';\n}\n\n.console-logger-window {\n width: 400px;\n height: 400px;\n background: white;\n border-radius: 10px 0 10px 10px;\n box-shadow: 0 5px 15px rgb(0, 0, 0, 0.1);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n transform: translateY(0);\n opacity: 1;\n\n /* 简化过渡属性 */\n transition:\n width 0.3s ease,\n height 0.3s ease,\n opacity 0.3s ease,\n transform 0.3s ease;\n}\n\n.console-logger-window.maximized {\n width: 600px !important;\n height: 85vh !important;\n border-radius: 10px 0 0 10px; /* 调整圆角以匹配右下角 */\n}\n\n.console-logger-window.collapsed {\n height: 0 !important;\n min-height: 0 !important;\n opacity: 0;\n}\n\n.console-logger-header {\n background: #2c3e50;\n color: white;\n padding: 12px 15px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n flex-shrink: 0;\n}\n\n.console-logger-title {\n font-weight: 600;\n font-size: 16px;\n}\n\n.console-logger-controls {\n display: flex;\n gap: 10px;\n}\n\n.console-logger-controls button {\n background: transparent;\n border: 1px solid rgb(255, 255, 255, 0.3);\n padding: 5px 10px;\n font-size: 12px;\n color: white;\n border-radius: 4px;\n cursor: pointer;\n transition: background 0.3s;\n}\n\n.console-logger-controls button:hover {\n background: rgb(255, 255, 255, 0.1);\n}\n\n/* 新增的按钮样式 */\n.console-logger-maximize-toggle {\n line-height: 1;\n font-size: 14px !important; /* 使箭头看起来更大 */\n padding: 5px 8px !important;\n}\n\n.console-logger-maximize-toggle::before {\n content: '⇱'; /* Unicode symbol for maximized */\n}\n\n.console-logger-maximize-toggle.active::before {\n content: '⇲'; /* Unicode symbol for minimized */\n}\n\n.console-logger-filters {\n display: flex;\n align-items: center;\n gap: 5px;\n padding: 10px;\n background: #f8f9fa;\n border-bottom: 1px solid #e9ecef;\n flex-shrink: 0;\n overflow-x: hidden;\n}\n\n/* 新增: 过滤器按钮组的容器,负责滚动 */\n.console-logger-filter-group {\n display: flex;\n gap: 5px;\n overflow-x: auto; /* 允许过滤器按钮滚动 */\n flex-grow: 1; /* 占据剩余空间 */\n padding-right: 10px; /* 避免滚动条影响按钮 */\n}\n\n.console-logger-filter {\n padding: 5px 10px;\n font-size: 12px;\n border-radius: 15px;\n background: #ecf0f1;\n color: #7f8c8d;\n border: 1px solid #ddd;\n cursor: pointer;\n transition: background-color 0.3s, color 0.3s, border-color 0.3s;\n white-space: nowrap;\n flex-shrink: 0; /* 确保不被压缩 */\n}\n\n.console-logger-filter.active {\n background: #3498db;\n color: white;\n border-color: #3498db;\n}\n\n/* 新增: 滚动到底部按钮的样式 (位于 filtersContainer 内部右侧) */\n.console-logger-scroll-to-bottom {\n background: #3498db;\n border: none;\n padding: 5px 10px;\n font-size: 12px;\n color: white;\n border-radius: 4px;\n cursor: pointer;\n transition: background 0.3s;\n line-height: 1;\n height: fit-content;\n white-space: nowrap;\n margin-left: auto; /* 将按钮推到最右侧 */\n flex-shrink: 0; /* 确保不被压缩 */\n}\n\n.console-logger-scroll-to-bottom:hover {\n background: #2980b9;\n}\n\n.console-logger-content {\n flex: 1;\n overflow-y: auto;\n padding: 10px;\n background: #fff;\n overflow-wrap: break-word;\n text-align: left;\n}\n\n.console-logger-entry {\n padding: 8px 10px;\n margin-bottom: 3px;\n border-radius: 4px;\n font-size: 12px;\n line-height: 1.4;\n\n /* animation: consoleFadeIn 0.3s ease; */\n border-left: 3px solid transparent;\n}\n\n@keyframes consoleFadeIn {\n from {\n opacity: 0;\n transform: translateY(5px);\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.console-logger-timestamp {\n color: #7f8c8d;\n font-size: 11px;\n margin-right: 2px;\n}\n\n@media (width <= 768px) {\n .console-logger-container {\n right: 10px;\n bottom: 10px;\n }\n\n .console-logger-window {\n width: calc(100vw - 20px);\n height: 300px;\n }\n}\n\n.console-logger-message[data-type='json'] {\n white-space: pre-wrap;\n}\n"; var javdb_site_default = "/* __HIDENAV2__ */\n\n.navbar {\n z-index: 12345679 !important;\n padding: 0 0;\n}\n\n.navbar-link:not(.is-arrowless) {\n padding-right: 33px;\n}\n\n.sub-header,\n /*#search-bar-container, !*搜索框*!*/\n #footer,\n /*.search-recent-keywords, !*搜索框底部热搜词条*!*/\n .app-desktop-banner,\n div[data-controller=\"movie-tab\"] .tabs,\n h3.main-title,\n div.video-detail > div:nth-child(4) > div > div.tabs.no-bottom > ul > li:nth-child(3), /* 相关清单*/\n div.video-detail > div:nth-child(4) > div > div.tabs.no-bottom > ul > li:nth-child(2), /* 短评按钮*/\n div.video-detail > div:nth-child(4) > div > div.tabs.no-bottom > ul > li:nth-child(1), /*磁力面板 按钮*/\n .top-meta,\n .float-buttons {\n display: none !important;\n}\n\ndiv.tabs.no-bottom,\n.tabs ul {\n border-bottom: none !important;\n}\n\n/* 视频列表项 相对相对 方便标签绝对定位 */\n.movie-list .item {\n position: relative !important;\n}\n\n.video-title {\n white-space: normal !important;\n height: 80px; /* 固定高度 容器就不会出现高低不一 */\n display: -webkit-box; /* 必须设置,使接下来的属性生效 */\n -webkit-box-orient: vertical; /* 垂直方向堆叠行 */\n -webkit-line-clamp: 3; /* 设置文本最多显示的行数 */\n line-clamp: 3;\n}\n\n/* 列表页顶部分类自适应 */\n.main-tabs,\n.tabs {\n overflow-x: hidden;\n flex-wrap: wrap;\n justify-content: flex-start;\n}\n\n.main-tabs ul,\n.tabs ul {\n flex-wrap: wrap;\n flex-grow: 0;\n}\n\n/* 二级工具栏 大小封面,可播放,含磁链... */\n.toolbar {\n display: flex;\n}\n"; var common_toolbar_default = "/* 全局通用样式 */\n.fr-btn {\n float: right;\n margin-left: 4px !important;\n}\n\n.menu-box {\n position: fixed;\n right: 10px;\n top: 50%;\n transform: translateY(-50%);\n display: flex;\n flex-direction: column;\n z-index: 1000;\n gap: 6px;\n}\n\n.jhs-toolbar-menu-btn {\n display: inline-block;\n min-width: 80px;\n padding: 7px 12px;\n border-radius: 4px;\n color: white !important;\n text-decoration: none;\n font-weight: bold;\n font-size: 12px;\n text-align: center;\n cursor: pointer;\n transition: transform 0.3s ease, box-shadow 0.3s ease, opacity 0.3s ease;\n box-shadow: 0 2px 5px rgb(0, 0, 0, 0.1);\n text-shadow: 0 1px 1px rgb(0, 0, 0, 0.2);\n border: none;\n line-height: 1.3;\n margin: 0;\n}\n\n.jhs-toolbar-menu-btn:hover {\n transform: translateY(-1px);\n box-shadow: 0 3px 6px rgb(0, 0, 0, 0.15);\n opacity: 0.9;\n}\n\n.jhs-toolbar-menu-btn:active {\n transform: translateY(0);\n box-shadow: 0 1px 2px rgb(0, 0, 0, 0.1);\n}\n\n.do-hide {\n display: none !important;\n}\n\n.main-tab-btn {\n border-bottom: none !important;\n border-radius: 3px !important;\n height: 30px;\n margin-left: 5px !important;\n}\n\n.jhs-icon {\n width: 16px;\n height: 16px;\n}\n\n/* 表格内按钮溢出,防止被隐藏 */\n.tabulator .tabulator-row .action-cell-dropdown {\n overflow: visible !important;\n}\n\n/* 去除行内鼠标小手 */\n.tabulator .tabulator-row.tabulator-selectable:hover {\n cursor: default !important;\n}\n\n/* 排序小箭头颜色 */\n.tabulator .tabulator-col.tabulator-sortable[aria-sort='ascending'] .tabulator-arrow {\n border-bottom-color: #337ab7 !important;\n}\n\n.tabulator .tabulator-col.tabulator-sortable[aria-sort='descending'] .tabulator-arrow {\n border-top-color: #337ab7 !important;\n}\n\n/* 针对折叠行的容器或内容进行样式修改 */\n.tabulator-responsive-collapse {\n border-top: none !important;\n}\n\n.tabulator-responsive-collapse table {\n margin-left: 50px !important;\n}\n\n.tabulator-cell {\n height: auto !important;\n}\n\n/* 列允许换行,去除省略号 */\n.tabulator .tabulator-cell {\n white-space: normal !important;\n text-overflow: clip !important;\n}\n\n.tabulator-tableholder {\n overflow-x: hidden !important;\n}\n\n/*\n * Toastify 宽度修复:toastify-js 源 CSS 中 .toastify-center 有两行\n * max-width: fit-content;\n * max-width: -moz-fit-content;\n * lightningcss 压缩时删除了标准 fit-content 只留 -moz-fit-content(Firefox\n * 前缀),Chrome 不认 -moz- 前缀致 max-width 失效,toast 退回 .toastify 的\n * calc(50% - 20px) 变成超宽(~937px)。此处用 !important 强制 fit-content\n * 覆盖,恢复 toast 自适应内容宽度。详见 doc/32-css-layout-fix.md。\n */\n.toastify-center {\n max-width: fit-content !important;\n}\n\n/* __SCROLLBAR__ */\n"; var a_normal_buttons_default = ".a-normal, /* 白色 */\n .a-primary, /* 浅蓝色 */\n .a-success, /* 浅绿色 */\n .a-danger, /* 浅粉色 */\n .a-warning, /* 浅橙色 */\n .a-info /* 灰色 */ {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 6px 14px;\n margin-right: 10px;\n border-radius: 6px;\n text-decoration: none;\n font-size: 13px;\n font-weight: 500;\n transition: background-color 0.2s ease;\n cursor: pointer;\n border: 1px solid rgb(0, 0, 0, 0.08);\n white-space: nowrap;\n}\n\n.a-primary {\n background: #e0f2fe;\n color: #0369a1;\n border-color: #bae6fd;\n}\n\n.a-primary:hover {\n background: #bae6fd;\n}\n\n.a-success {\n background: #dcfce7;\n color: #166534;\n border-color: #bbf7d0;\n}\n\n.a-success:hover {\n background: #bbf7d0;\n}\n\n.a-danger {\n background: #fee2e2;\n color: #b91c1c;\n border-color: #fecaca;\n}\n\n.a-danger:hover {\n background: #fecaca;\n}\n\n.a-warning {\n background: #ffedd5;\n color: #9a3412;\n border-color: #fed7aa;\n}\n\n.a-warning:hover {\n background: #fed7aa;\n}\n\n.a-info {\n background: #e2e8f0;\n color: #334155;\n border-color: #cbd5e1;\n}\n\n.a-info:hover {\n background: #cbd5e1;\n}\n\n.a-normal {\n background: transparent;\n color: #64748b;\n border-color: #cbd5e1;\n}\n\n.a-normal:hover {\n background: #f8fafc;\n}\n"; var accessibility_default = "/* 无障碍基础样式(doc/145 批次 5 基础) */\n\n/* focus-visible:键盘导航时显示焦点环,鼠标点击时不显示 */\n:focus-visible {\n outline: 2px solid #5d87c2;\n outline-offset: 2px;\n}\n\n/* 交互元素的焦点样式 */\nbutton:focus-visible,\na:focus-visible,\ninput:focus-visible,\nselect:focus-visible,\ntextarea:focus-visible,\n[tabindex]:focus-visible {\n outline: 2px solid #5d87c2;\n outline-offset: 2px;\n}\n\n/* 减少动态效果:尊重用户系统偏好 */\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n scroll-behavior: auto !important;\n }\n}\n"; var design_tokens_default = "/* JavDB Power Tools 设计令牌(doc/147 批次 5 基础) */\n\n/* 作用域:.jhs-ui 或全局 :root,供所有插件 CSS 引用 */\n\n:root {\n /* 主色 */\n --jhs-color-primary: #5d87c2;\n --jhs-color-primary-hover: #4a73b0;\n --jhs-color-primary-light: #e8eef6;\n\n /* 语义色 */\n --jhs-color-success: #64bb69;\n --jhs-color-warning: #d7a80c;\n --jhs-color-error: #de3333;\n --jhs-color-info: #25b1dc;\n\n /* 中性色 */\n --jhs-color-text: #363636;\n --jhs-color-text-secondary: #6c757d;\n --jhs-color-text-muted: #999;\n --jhs-color-border: #e2e8f0;\n --jhs-color-bg: #fff;\n --jhs-color-bg-secondary: #fbfcfd;\n --jhs-color-bg-hover: #f5f5f5;\n\n /* 间距 */\n --jhs-space-xs: 4px;\n --jhs-space-sm: 8px;\n --jhs-space-md: 12px;\n --jhs-space-lg: 16px;\n --jhs-space-xl: 20px;\n --jhs-space-2xl: 24px;\n\n /* 圆角 */\n --jhs-radius-sm: 4px;\n --jhs-radius-md: 6px;\n --jhs-radius-lg: 8px;\n --jhs-radius-full: 9999px;\n\n /* 字体 */\n --jhs-font-size-xs: 11px;\n --jhs-font-size-sm: 12px;\n --jhs-font-size-md: 13px;\n --jhs-font-size-lg: 14px;\n --jhs-font-size-xl: 16px;\n\n /* 阴影 */\n --jhs-shadow-sm: 0 1px 3px rgb(0, 0, 0, 0.08);\n --jhs-shadow-md: 0 2px 8px rgb(0, 0, 0, 0.12);\n --jhs-shadow-lg: 0 4px 16px rgb(0, 0, 0, 0.16);\n\n /* z-index 层级(layer.js z-index 无上限递增,above-layer 层需 genuinely large 常量) */\n --jhs-z-dropdown: 100;\n --jhs-z-sticky: 200;\n --jhs-z-page: 9999;\n --jhs-z-top: 999999999;\n\n /* 过渡 */\n --jhs-transition-fast: 150ms ease;\n --jhs-transition-normal: 250ms ease;\n\n /* 触控目标最小尺寸 */\n --jhs-touch-target: 44px;\n}\n"; var filter_chip_base_default = "/**\n * 筛选栏 + 芯片共享基础样式 —— status-tag-filter / video-lists-tag / preload-status-badge\n * 三个插件的筛选栏布局与芯片基础样式逐字节相同,此处以分组选择器统一,\n * 各插件 CSS 仅保留 active 配色与 no-tag 变体。\n *\n * 注意:missav-status-filter 不在此共享基础内——missav 是 Tailwind nord 暗色主题,\n * 浅色基础样式与其风格冲突(doc/223),由 missav-status-filter.css 独立定义暗色样式。\n *\n * 注入方式:main.tsx 顶层 injectCss(始终注入,与 design-tokens.css 同级)。\n */\n\n/* ---------- 筛选栏容器 ---------- */\n.status-tag-filter-bar,\n.jhs-vlt-filter-bar,\n.preload-filter-bar {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 8px 0 12px 0;\n border-bottom: 1px solid #e9ecef;\n}\n\n/* ---------- 筛选栏标签 ---------- */\n.status-tag-filter-label,\n.jhs-vlt-filter-label,\n.preload-filter-label {\n flex-shrink: 0;\n font-size: 0.8em;\n font-weight: 600;\n color: #6c757d;\n line-height: 1.8;\n margin-right: 4px;\n}\n\n/* ---------- 芯片容器 ---------- */\n.status-tag-filter-chips,\n.jhs-vlt-filter-chips,\n.preload-filter-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n/* ---------- 芯片基础 ---------- */\n.status-tag-filter-chip,\n.jhs-vlt-filter-chip,\n.preload-filter-chip {\n display: inline-block;\n padding: 0.25em 0.7em;\n font-size: 0.75em;\n font-weight: 600;\n line-height: 1.5;\n border-radius: 50rem;\n cursor: pointer;\n user-select: none;\n border: 1.5px solid #dee2e6;\n background: #fff;\n color: #495057;\n transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;\n}\n\n/* ---------- 芯片悬停 ---------- */\n.status-tag-filter-chip:hover,\n.jhs-vlt-filter-chip:hover,\n.preload-filter-chip:hover {\n border-color: #adb5bd;\n background: #f8f9fa;\n}"; var javdbHideNavCss = ""; if (window.location.href.includes("hideNav=1")) javdbHideNavCss = "\n .main-nav,#search-bar-container {\n display: none !important;\n }\n \n html {\n padding-top:0px!important;\n }\n "; var javdbSiteCss = javdb_site_default.replace("/*__HIDENAV2__*/", javdbHideNavCss); function generateScrollbarCss() { const scrollbarSelectors = [ ".jhs-scrollbar", ".content-panel", ".tabulator-tableholder", ".has-navbar-fixed-top", ".layui-layer-content" ]; const appendPseudo = (selectors, pseudo) => selectors.map((selector) => `${selector}${pseudo}`).join(","); return `\n ${appendPseudo(scrollbarSelectors, "::-webkit-scrollbar")}{width:6px;height:6px;}\n ${appendPseudo(scrollbarSelectors, "::-webkit-scrollbar-track")}{background:#f1f1f1;border-radius:10px;}\n ${appendPseudo(scrollbarSelectors, "::-webkit-scrollbar-thumb")}{background:#888;border-radius:10px;}\n ${appendPseudo(scrollbarSelectors, "::-webkit-scrollbar-thumb:hover")}{background:#555;}\n `.trim().replace(/\n/g, ""); } var commonToolbarCss = common_toolbar_default.replace("/*__SCROLLBAR__*/", generateScrollbarCss()); if (isJavdbSite) injectCss(javdbSiteCss); injectCss(a_normal_buttons_default); injectCss(commonToolbarCss); injectCss(accessibility_default); injectCss(design_tokens_default); injectCss(filter_chip_base_default); window.utils = new CommonUtil(); window.gmHttp = new GmHttp(); window.storageManager = new StorageManager(); window.loadGfriends = loadGfriends; window.filetreeDb = filetreeDb; window.WebDavClient = WebDavClient; var refreshChannel = new BroadcastChannel("channel-refresh"); window.refresh = function() { refreshChannel.postMessage({ type: "refresh" }); }; window.cleanCache_filter_actor_actress_car_list = function() { refreshChannel.postMessage({ type: "cleanCache_filter_actor_actress_car_list" }); }; window.clean_cacheSettingObj = function() { refreshChannel.postMessage({ type: "clean_cacheSettingObj" }); }; injectCss(loading_default); window.loading = createLoading; window.show = show$1; (function() { function resetOverflow(delay = 10) { setTimeout(() => { const shadeCount = document.querySelectorAll(".layui-layer-shade").length; document.documentElement.style.overflow = shadeCount > 0 ? "hidden" : ""; }, delay); } injectCss(viewer_default); window.showImageViewer = function(src, alt = "") { let container; let isTemporary = false; if (typeof src == "string" || src instanceof String) { container = $(jsxToString((0, import_jsx_runtime.jsx)(TemporaryImageContainer, { src: String(src), alt }))).appendTo("body"); isTemporary = true; } else container = $(src); const viewerRef = { current: null }; const viewerOptions = VIEWER_CONFIG({ container, isTemporary, resetOverflow, viewerRef }); viewerRef.current = new Viewer(container[0], viewerOptions); viewerRef.current.show(); }; })(); (async function() { injectCss(logger_default); try { if (unsafeWindow.parent.clog && typeof unsafeWindow.parent.clog.log == "function") window.clog = unsafeWindow.parent.clog; else window.clog = new Logger(); } catch (err) { console.error("创建日志控制台出现异常", err); window.clog = new Logger(); } (function() { const logger = window.clog || console; window.addEventListener("error", function(event) { const filename = event.filename; const message = event.message; if (!filename.includes("javdb")) logger.error(`[全局 Error 异常捕获] ${message} 来源: ${filename}`); }); window.addEventListener("unhandledrejection", function(event) { const reason = event.reason; const reasonMessage = (reason == null ? void 0 : reason.message) ?? ""; if (reasonMessage.includes("play()")) return; if (reasonMessage.includes("The element has no supported sources")) { show$1.error("播放失败, 请检查是否已对节点分流?"); logger.error("播放失败, 请检查是否已对节点分流?"); return; } if (reasonMessage.includes("<span>1005</span>") && reasonMessage.includes("fc2ppvdb")) return; const logMessage = `[全局 Promise 异常捕获] ${reason.message || reason}`; logger.error(logMessage, reason); event.preventDefault(); }); })(); document.addEventListener("mousedown", (event) => { const clog = window.clog; if (!clog.isInitialized || !clog.container) return; const target = event.target; const loggerSelectors = [ ".console-logger-container", ".layui-layer-shade", ".loading-container" ].join(","); if (target.closest(loggerSelectors)) clog.highZIndex(); else clog.lowZIndex(); }); })(); setupTooltip(); window.encryptCredential = encryptCredential; window.decryptCredential = decryptCredential; setupLayerWrapper(); var pluginManager = (function() { const manager = new PluginManager(); if (isJavdbSite) { manager.register(ListPagePlugin); manager.register(AutoPagePlugin); manager.register(FoldCategoryPlugin); manager.register(ListPageButtonPlugin); manager.register(HistoryPlugin); manager.register(SettingPlugin); manager.register(NavBarPlugin); manager.register(HitShowPlugin); manager.register(Top250Plugin); manager.register(DetailPagePlugin); manager.register(ReviewPlugin); manager.register(DetailPageButtonPlugin); manager.register(HighlightMagnetPlugin); manager.register(PreviewVideoPlugin); manager.register(ActressInfoPlugin); manager.register(OtherSitePlugin); manager.register(WantAndWatchedVideosPlugin); manager.register(RelatedPlugin); manager.register(BlacklistPlugin); manager.register(FavoriteActressesPlugin); manager.register(NewVideoPlugin); manager.register(RatingDisplayPlugin); manager.register(Fc2Plugin); manager.register(KeyPageTurningPlugin); manager.register(ModMyListOpenWayPlugin); manager.register(PageSortPlugin); manager.register(StatusTagFilterPlugin); manager.register(ListWaterfallPlugin); manager.register(ListReadingStatusPlugin); manager.register(ModalListDisablerPlugin); manager.register(VideoListsTagPlugin); manager.register(CarListReaderPlugin); manager.register(VisitHistoryPlugin); manager.register(ListReflowPlugin); if (featureFlags.translatePlugin) manager.register(TranslatePlugin, "translatePlugin"); if (featureFlags.screenShotPlugin) manager.register(ScreenShotPlugin, "screenShotPlugin"); if (featureFlags.magnetHubPlugin) manager.register(MagnetHubPlugin, "magnetHubPlugin"); if (featureFlags.imageRecognitionPlugin) manager.register(ImageRecognitionPlugin, "imageRecognitionPlugin"); if (featureFlags.fc2By123AvPlugin) manager.register(Fc2By123AvPlugin, "fc2By123AvPlugin"); } if (isMissavSite) { manager.register(MissavStatusTagPlugin); manager.register(MissavQuickCopyPlugin); manager.register(MissavAutoPagePlugin); manager.register(MissavStatusFilterPlugin); manager.register(MissavVideoPreviewPlugin); } return manager; })(); (async function() { const ctx = initPageContext(); window.isDetailPage = ctx.isDetailPage; window.isListPage = ctx.isListPage; window.isFc2Page = ctx.isFc2Page; await pluginManager.processCss(); if (isJavdbSite) await storageManager.runMigrations(); storageRevision.init(); storageRevision.onRemoteChange(() => { storageManager.clearCarListCache(); storageManager.clearSettingCache(); storageManager.clearFilterActorActressCarListCache(); }); if (isJavdbSite && /(^|;)\s*locale\s*=\s*en\s*($|;)/i.test(document.cookie)) show$1.error("请切换到中文语言下才可正常使用本脚本", { duration: -1 }); await pluginManager.processPlugins(); if (isJavdbSite) { const settingPlugin = pluginManager.getBean("SettingPlugin"); if (settingPlugin) settingPlugin.autoBackup().catch((err) => window.clog.error("自动备份失败", err)); } registerDiagnosticsMenu(pluginManager); window.addEventListener("beforeunload", () => pluginManager.destroyAll()); })(); })($, layer, Tabulator, Toastify, localforage, Viewer, md5);