FC2/JAV 资源站增强脚本:磁力链接自动聚合、高清封面补全、图集预览、历史记录同步与过滤。支持 FD2PPV, Supjav, JavDB, MissAV, JavFC2, FC2DB, FC2CMADB 等站点。
// ==UserScript== // @name FC2PPVDB Enhanced // @name:en FC2PPVDB Enhanced // @namespace https://greasyfork.org/zh-CN/scripts/552583-fc2ppvdb-enhanced // @version 2.1.5 // @author Icarusle // @description FC2/JAV 资源站增强脚本:磁力链接自动聚合、高清封面补全、图集预览、历史记录同步与过滤。支持 FD2PPV, Supjav, JavDB, MissAV, JavFC2, FC2DB, FC2CMADB 等站点。 // @description:en Enhancement script for FC2/JAV sites: magnet aggregation, HD cover restoration, gallery preview, history sync and filtering. Supports FD2PPV, Supjav, JavDB, MissAV, JavFC2, FC2DB, FC2CMADB, etc. // @license MIT // @icon https://fc2ppvdb.com/favicon.ico // @match https://fd2ppv.cc/* // @match https://supjav.com/* // @match https://missav.ws/* // @match https://missav.ai/* // @match https://javdb.com/* // @match https://fc2db.net/* // @match https://javfc2.xyz/* // @match https://fc2cmadb.com/* // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/dexie.min.js // @connect fd2ppv.cc // @connect fourhoi.com // @connect paipancon.com // @connect sukebei.nyaa.si // @connect supabase.co // @grant GM_addStyle // @grant GM_addValueChangeListener // @grant GM_deleteValue // @grant GM_getValue // @grant GM_info // @grant GM_notification // @grant GM_openInTab // @grant GM_registerMenuCommand // @grant GM_removeValueChangeListener // @grant GM_setClipboard // @grant GM_setValue // @grant GM_unregisterMenuCommand // @grant GM_xmlhttpRequest // @grant unsafeWindow // @run-at document-end // @noframes // ==/UserScript== !function(){function e(e,t){return(t||"")+" (SystemJS Error#"+e+" https://github.com/systemjs/systemjs/blob/main/docs/errors.md#"+e+")"}function t(e,t){if(-1!==e.indexOf("\\")&&(e=e.replace(j,"/")),"/"===e[0]&&"/"===e[1])return t.slice(0,t.indexOf(":")+1)+e;if("."===e[0]&&("/"===e[1]||"."===e[1]&&("/"===e[2]||2===e.length&&(e+="/"))||1===e.length&&(e+="/"))||"/"===e[0]){var n,r=t.slice(0,t.indexOf(":")+1);if(n="/"===t[r.length+1]?"file:"!==r?(n=t.slice(r.length+2)).slice(n.indexOf("/")+1):t.slice(8):t.slice(r.length+("/"===t[r.length])),"/"===e[0])return t.slice(0,t.length-n.length-1)+e;for(var i=n.slice(0,n.lastIndexOf("/")+1)+e,o=[],s=-1,u=0;u<i.length;u++)-1!==s?"/"===i[u]&&(o.push(i.slice(s,u+1)),s=-1):"."===i[u]?"."!==i[u+1]||"/"!==i[u+2]&&u+2!==i.length?"/"===i[u+1]||u+1===i.length?u+=1:s=u:(o.pop(),u+=2):s=u;return-1!==s&&o.push(i.slice(s)),t.slice(0,t.length-n.length)+o.join("")}}function n(e,n){return t(e,n)||(-1!==e.indexOf(":")?e:t("./"+e,n))}function r(e,n,r,i,o){for(var s in e){var a=t(s,r)||s,f=e[s];if("string"==typeof f){var l=c(i,t(f,r)||f,o);l?n[a]=l:u("W1",s,f,"bare specifier did not resolve")}}}function i(e,t,i){var o;for(o in e.imports&&r(e.imports,i.imports,t,i,null),e.scopes||{}){var s=n(o,t);r(e.scopes[o],i.scopes[s]||(i.scopes[s]={}),t,i,s)}for(o in e.depcache||{})i.depcache[n(o,t)]=e.depcache[o];for(o in e.integrity||{})i.integrity[n(o,t)]=e.integrity[o]}function o(e,t){if(t[e])return e;var n=e.length;do{var r=e.slice(0,n+1);if(r in t)return r}while(-1!==(n=e.lastIndexOf("/",n-1)))}function s(e,t){var n=o(e,t);if(n){var r=t[n];if(null===r)return;if(!(e.length>n.length&&"/"!==r[r.length-1]))return r+e.slice(n.length);u("W2",n,r,"should have a trailing '/'")}}function u(t,n,r,i){console.warn(e(t,"Package target "+i+", resolving target '"+r+"' for "+n))}function c(e,t,n){for(var r=e.scopes,i=n&&o(n,r);i;){var u=s(t,r[i]);if(u)return u;i=o(i.slice(0,i.lastIndexOf("/")),r)}return s(t,e.imports)||-1!==t.indexOf(":")&&t}function a(){this[M]={}}function f(e){return e.id}function l(e,t,n,r){if(e.onload(n,t.id,t.d&&t.d.map(f),!!r),n)throw n}function d(t,n,r,i){var o=t[M][n];if(o)return o;var s=[],u=Object.create(null);P&&Object.defineProperty(u,P,{value:"Module"});var c=Promise.resolve().then((function(){return t.instantiate(n,r,i)})).then((function(r){if(!r)throw Error(e(2,"Module "+n+" did not instantiate"));var i=r[1]((function(e,t){o.h=!0;var n=!1;if("string"==typeof e)e in u&&u[e]===t||(u[e]=t,n=!0);else{for(var r in e)t=e[r],r in u&&u[r]===t||(u[r]=t,n=!0);e&&e.__esModule&&(u.__esModule=e.__esModule)}if(n)for(var i=0;i<s.length;i++){var c=s[i];c&&c(u)}return t}),2===r[1].length?{import:function(e,r){return t.import(e,n,r)},meta:t.createContext(n)}:void 0);return o.e=i.execute||function(){},[r[0],i.setters||[],r[2]||[]]}),(function(e){throw o.e=null,o.er=e,l(t,o,e,!0),e})),a=c.then((function(e){return Promise.all(e[0].map((function(r,i){var o=e[1][i],s=e[2][i];return Promise.resolve(t.resolve(r,n)).then((function(e){var r=d(t,e,n,s);return Promise.resolve(r.I).then((function(){return o&&(r.i.push(o),!r.h&&r.I||o(r.n)),r}))}))}))).then((function(e){o.d=e}))}));return o=t[M][n]={id:n,i:s,n:u,m:i,I:c,L:a,h:!1,d:void 0,e:void 0,er:void 0,E:void 0,C:void 0,p:void 0}}function h(e,t,n,r){if(!r[t.id])return r[t.id]=!0,Promise.resolve(t.L).then((function(){return t.p&&null!==t.p.e||(t.p=n),Promise.all(t.d.map((function(t){return h(e,t,n,r)})))})).catch((function(n){if(t.er)throw n;throw t.e=null,l(e,t,n,!1),n}))}function p(e,t){return t.C=h(e,t,t,{}).then((function(){return v(e,t,{})})).then((function(){return t.n}))}function v(e,t,n){function r(){try{var n=o.call(L);if(n)return n=n.then((function(){t.C=t.n,t.E=null,l(e,t,null,!0)}),(function(n){throw t.er=n,t.E=null,l(e,t,n,!0),n})),t.E=n;t.C=t.n,t.L=t.I=void 0}catch(r){throw t.er=r,r}finally{l(e,t,t.er,!0)}}if(!n[t.id]){if(n[t.id]=!0,!t.e){if(t.er)throw t.er;return t.E?t.E:void 0}var i,o=t.e;return t.e=null,t.d.forEach((function(r){try{var o=v(e,r,n);o&&(i=i||[]).push(o)}catch(s){throw t.er=s,l(e,t,s,!1),s}})),i?Promise.all(i).then(r):r()}}function m(){[].forEach.call(document.querySelectorAll("script"),(function(t){if(!t.sp)if("systemjs-module"===t.type){if(t.sp=!0,!t.src)return;System.import("import:"===t.src.slice(0,7)?t.src.slice(7):n(t.src,g)).catch((function(e){if(e.message.indexOf("https://github.com/systemjs/systemjs/blob/main/docs/errors.md#3")>-1){var n=document.createEvent("Event");n.initEvent("error",!1,!1),t.dispatchEvent(n)}return Promise.reject(e)}))}else if("systemjs-importmap"===t.type){t.sp=!0;var r=t.src?(System.fetch||fetch)(t.src,{integrity:t.integrity,priority:t.fetchPriority,passThrough:!0}).then((function(e){if(!e.ok)throw Error("Invalid status code: "+e.status);return e.text()})).catch((function(n){return n.message=e("W4","Error fetching systemjs-import map "+t.src)+"\n"+n.message,console.warn(n),"function"==typeof t.onerror&&t.onerror(),"{}"})):t.innerHTML;W=W.then((function(){return r})).then((function(n){!function(t,n,r){var o={};try{o=JSON.parse(n)}catch(s){console.warn(Error(e("W5","systemjs-importmap contains invalid JSON")+"\n\n"+n+"\n"))}i(o,r,t)}(N,n,t.src||g)}))}}))}var g,y="undefined"!=typeof Symbol,b="undefined"!=typeof self,S="undefined"!=typeof document,w=b?self:global;if(S){var O=document.querySelector("base[href]");O&&(g=O.href)}if(!g&&"undefined"!=typeof location){var E=(g=location.href.split("#")[0].split("?")[0]).lastIndexOf("/");-1!==E&&(g=g.slice(0,E+1))}var x,j=/\\/g,P=y&&Symbol.toStringTag,M=y?Symbol():"@",I=a.prototype;I.import=function(e,t,n){var r=this;return t&&"object"==typeof t&&(n=t,t=void 0),Promise.resolve(r.prepareImport()).then((function(){return r.resolve(e,t,n)})).then((function(e){var t=d(r,e,void 0,n);return t.C||p(r,t)}))},I.createContext=function(e){var t=this;return{url:e,resolve:function(n,r){return Promise.resolve(t.resolve(n,r||e))}}},I.onload=function(){},I.register=function(e,t,n){x=[e,t,n]},I.getRegister=function(){var e=x;return x=void 0,e};var L=Object.freeze(Object.create(null));w.System=new a;var C,R,W=Promise.resolve(),N={imports:{},scopes:{},depcache:{},integrity:{}},T=S;if(I.prepareImport=function(e){return(T||e)&&(m(),T=!1),W},I.getImportMap=function(){return JSON.parse(JSON.stringify(N))},S&&(m(),window.addEventListener("DOMContentLoaded",m)),I.addImportMap=function(e,t){i(e,t||g,N)},S){window.addEventListener("error",(function(e){J=e.filename,_=e.error}));var A=location.origin}I.createScript=function(e){var t=document.createElement("script");t.async=!0,e.indexOf(A+"/")&&(t.crossOrigin="anonymous");var n=N.integrity[e];return n&&(t.integrity=n),t.src=e,t};var J,_,k={},U=I.register;I.register=function(e,t){if(S&&"loading"===document.readyState&&"string"!=typeof e){var n=document.querySelectorAll("script[src]"),r=n[n.length-1];if(r){C=e;var i=this;R=setTimeout((function(){k[r.src]=[e,t],i.import(r.src)}))}}else C=void 0;return U.call(this,e,t)},I.instantiate=function(t,n){var r=k[t];if(r)return delete k[t],r;var i=this;return Promise.resolve(I.createScript(t)).then((function(r){return new Promise((function(o,s){r.addEventListener("error",(function(){s(Error(e(3,"Error loading "+t+(n?" from "+n:""))))})),r.addEventListener("load",(function(){if(document.head.removeChild(r),J===t)s(_);else{var e=i.getRegister(t);e&&e[0]===C&&clearTimeout(R),o(e)}})),document.head.appendChild(r)}))}))},I.shouldFetch=function(){return!1},"undefined"!=typeof fetch&&(I.fetch=fetch);var $=I.instantiate,B=/^(text|application)\/(x-)?javascript(;|$)/;I.instantiate=function(t,n,r){var i=this;return this.shouldFetch(t,n,r)?this.fetch(t,{credentials:"same-origin",integrity:N.integrity[t],meta:r}).then((function(r){if(!r.ok)throw Error(e(7,r.status+" "+r.statusText+", loading "+t+(n?" from "+n:"")));var o=r.headers.get("content-type");if(!o||!B.test(o))throw Error(e(4,'Unknown Content-Type "'+o+'", loading '+t+(n?" from "+n:"")));return r.text().then((function(e){return e.indexOf("//# sourceURL=")<0&&(e+="\n//# sourceURL="+t),(0,eval)(e),i.getRegister(t)}))})):$.apply(this,arguments)},I.resolve=function(n,r){return c(N,t(n,r=r||g)||n,r)||function(t,n){throw Error(e(8,"Unable to resolve bare specifier '"+t+(n?"' from "+n:"'")))}(n,r)};var F=I.instantiate;I.instantiate=function(e,t,n){var r=N.depcache[e];if(r)for(var i=0;i<r.length;i++)d(this,this.resolve(r[i],e),e);return F.call(this,e,t,n)},b&&"function"==typeof importScripts&&(I.instantiate=function(e){var t=this;return Promise.resolve().then((function(){return importScripts(e),t.getRegister(e)}))}),function(e){function t(t){return!e.hasOwnProperty(t)||!isNaN(t)&&t<e.length||a&&e[t]&&"undefined"!=typeof window&&e[t].parent===window}var n,r,i,o=e.System.constructor.prototype,s=o.import;o.import=function(o,u,c){return function(){for(var o in n=r=void 0,e)t(o)||(n?r||(r=o):n=o,i=o)}(),s.call(this,o,u,c)};var u=[[],function(){return{}}],c=o.getRegister;o.getRegister=function(){var o=c.call(this);if(o)return o;var s,a=function(o){var s,u,c=0;for(var a in e)if(!t(a)){if(0===c&&a!==n||1===c&&a!==r)return a;s?(i=a,u=o&&u||a):s=a===i,c++}return u}(this.firstGlobalProp);if(!a)return u;try{s=e[a]}catch(f){return u}return[[],function(e){return{execute:function(){e(s),e({default:s,__useDefault:!0})}}}]};var a="undefined"!=typeof navigator&&-1!==navigator.userAgent.indexOf("Trident")}("undefined"!=typeof self?self:global),function(e){var t=e.System.constructor.prototype,r=/^[^#?]+\.(css|html|json|wasm)([?#].*)?$/,i=t.shouldFetch.bind(t);t.shouldFetch=function(e){return i(e)||r.test(e)};var o=/^application\/json(;|$)/,s=/^text\/css(;|$)/,u=/^application\/wasm(;|$)/,c=t.fetch;t.fetch=function(t,r){return c(t,r).then((function(i){if(r.passThrough)return i;if(!i.ok)return i;var c=i.headers.get("content-type");return o.test(c)?i.json().then((function(e){return new Response(new Blob(['System.register([],function(e){return{execute:function(){e("default",'+JSON.stringify(e)+")}}})"],{type:"application/javascript"}))})):s.test(c)?i.text().then((function(e){return e=e.replace(/url\(\s*(?:(["'])((?:\\.|[^\n\\"'])+)\1|((?:\\.|[^\s,"'()\\])+))\s*\)/g,(function(e,r,i,o){return["url(",r,n(i||o,t),r,")"].join("")})),new Response(new Blob(["System.register([],function(e){return{execute:function(){var s=new CSSStyleSheet();s.replaceSync("+JSON.stringify(e)+');e("default",s)}}})'],{type:"application/javascript"}))})):u.test(c)?(WebAssembly.compileStreaming?WebAssembly.compileStreaming(i):i.arrayBuffer().then(WebAssembly.compile)).then((function(n){e.System.wasmModules||(e.System.wasmModules=Object.create(null)),e.System.wasmModules[t]=n;var r=[],i=[];return WebAssembly.Module.imports&&WebAssembly.Module.imports(n).forEach((function(e){var t=JSON.stringify(e.module);-1===r.indexOf(t)&&(r.push(t),i.push("function(m){i["+t+"]=m}"))})),new Response(new Blob(["System.register(["+r.join(",")+"],function(e){var i={};return{setters:["+i.join(",")+"],execute:function(){return WebAssembly.instantiate(System.wasmModules["+JSON.stringify(t)+"],i).then(function(m){e(m.exports)})}}})"],{type:"application/javascript"}))})):i}))}}("undefined"!=typeof self?self:global);var q="undefined"!=typeof Symbol&&Symbol.toStringTag;I.get=function(e){var t=this[M][e];if(t&&null===t.e&&!t.E)return t.er?null:t.n},I.set=function(t,n){try{new URL(t)}catch(s){console.warn(Error(e("W3",'"'+t+'" is not a valid URL to set in the module registry')))}var r;q&&"Module"===n[q]?r=n:(r=Object.assign(Object.create(null),n),q&&Object.defineProperty(r,q,{value:"Module"}));var i=Promise.resolve(r),o=this[M][t]||(this[M][t]={id:t,i:[],h:!1,d:[],e:null,er:void 0,E:void 0});return!o.e&&!o.E&&(Object.assign(o,{n:r,I:void 0,L:void 0,C:i}),r)},I.has=function(e){return!!this[M][e]},I.delete=function(e){var t=this[M],n=t[e];if(!n||n.p&&null!==n.p.e||n.E)return!1;var r=n.i;return n.d&&n.d.forEach((function(e){var t=e.i.indexOf(n);-1!==t&&e.i.splice(t,1)})),delete t[e],function(){var n=t[e];if(!n||!r||null!==n.e||n.E)return!1;r.forEach((function(e){n.i.push(e),e(n.n)})),r=null}};var D="undefined"!=typeof Symbol&&Symbol.iterator;I.entries=function(){var e,t,n=this,r=Object.keys(n[M]),i=0,o={next:function(){for(;void 0!==(t=r[i++])&&void 0===(e=n.get(t)););return{done:void 0===t,value:void 0!==t&&[t,e]}}};return o[D]=function(){return this},o}}(); !function(t){function e(t){t.registerRegistry=Object.create(null),t.namedRegisterAliases=Object.create(null)}var r=t.System;e(r);var i,s,n=r.constructor.prototype,l=r.constructor,a=function(){l.call(this),e(this)};a.prototype=n,r.constructor=a;var o=n.register;n.register=function(t,e,r,n){if("string"!=typeof t)return o.apply(this,arguments);var l=[e,r,n];return this.registerRegistry[t]=l,i||(i=l,s=t),Promise.resolve().then((function(){i=null,s=null})),o.apply(this,[e,r,n])};var u=n.resolve;n.resolve=function(t,e){try{return u.call(this,t,e)}catch(r){if(t in this.registerRegistry)return this.namedRegisterAliases[t]||t;throw r}};var c=n.instantiate;n.instantiate=function(t,e,r){var i=this.registerRegistry[t];return i?(this.registerRegistry[t]=null,i):c.call(this,t,e,r)};var g=n.getRegister;n.getRegister=function(t){var e=g.call(this,t);s&&t&&(this.namedRegisterAliases[s]=t);var r=i||e;return i=null,s=null,r}}("undefined"!=typeof self?self:global); ;(typeof System!='undefined')&&(System=new System.constructor()); System.addImportMap({ imports: {"dexie":"user:dexie"} }); System.set("user:dexie", (()=>{const _=Dexie;('default' in _)||(_.default=_);return _})()); System.register("./___monkey.entry.js", ['dexie'],(function(exports,module){'use strict';var Dexie;return{setters:[function(module){Dexie=module.default;}],execute:(function(){exports("r",resolveService);var resolveScriptVersion = () => { if (typeof GM_info !== "undefined") return GM_info.script.version; return "2.1.5"; }; var SCRIPT_INFO = exports("k",{ NAME: "FC2PPVDB Enhanced", VERSION: resolveScriptVersion(), GREASYFORK_URL: "https://greasyfork.org/scripts/552583" }); var NETWORK = { CHUNK_SIZE: 16, HEALTH_CHECK_TIMEOUT: 5e3 }; var TIMING = exports("i",{ DEBOUNCE_MS: 300, THROTTLE_MS: 200, RETRY_DELAY_MS: 1e3, MAGNET_BASE_DELAY_MS: 800, UI_TRANSITION_FAST: 150, UI_TRANSITION_NORMAL: 300, UI_TRANSITION_SLOW: 500, UI_ANIMATION_BADGE: 600, TOAST_DEFAULT_DURATION: 3e3, SCRAPER_TOAST_DURATION: 15e3, RELOAD_DELAY_FAST: 800, RELOAD_DELAY_NORMAL: 1500, SCRIPT_INJECTION_DELAY: 400, SYNC_DEBOUNCE_MS: 2e3, SYNC_INIT_DELAY: 5e3, SYNC_REALTIME_POLL_MS: 3e4, MAGNET_RANDOM_DELAY_MS: 3e3, CLOUDFLARE_BACKOFF_MS: 6e4, FD2_BACKOFF_MS: 3e5, RATE_LIMIT_BACKOFF_MS: 6e4, ACTRESS_BASE_DELAY_MS: 1500, ACTRESS_RANDOM_DELAY_MS: 1500, POLITE_DELAY_MS: 200, MAGNET_JITTER_MS: 3e3, POLL_INTERVAL_MS: 100, POLL_TIMEOUT_MS: 3e4, DOM_READY_TIMEOUT: 1e4, PREVIEW_ERROR_DELAY: 3e3, LINK_PRELOAD_TIMEOUT: 6e4, MAX_BACKOFF_MS: 6e5, PREVIEW_JITTER_MS: 500, HTTP_FD2_TIMEOUT: 1e4, SLIDESHOW_INTERVAL: 3e3, TITLE_MATCH_PROXIMITY: 15 }); var CLOUDFLARE_INDICATORS = [ "Just a moment...", "Checking your browser", "Attention Required!", "Cloudflare" ]; var TIMELINE = { RETENTION_MS: 2592e6, GC_INTERVAL_MS: 36e5 }; var VALIDATION = { ACTRESS_NAME_MAX_LENGTH: 20, ACTRESS_NAME_MIN_LENGTH: 2, JAV_ID_REGEX: /^(?=.*[A-Z])[A-Z0-9-]{1,10}-\d{2,8}$/i}; var EXTERNAL_URLS = { SUPJAV: "https://supjav.com/zh/?s={id}", MISSAV_FC2: "https://missav.ws/cn/fc2-ppv-{id}", MISSAV: "https://missav.ws/cn/{id}", JAVDB: "https://javdb.com/search?q={id}&f=all", JAVBUS: "https://www.javbus.com/{id}", JAVLIBRARY: "https://www.javlibrary.com/cn/vl_searchbyid.php?keyword={id}", DMM: "https://www.dmm.co.jp/search/=/searchstr={id}", FC2: "https://adult.contents.fc2.com/article/{id}/?tag=TXpZM05EY3lORFk9", FD2PPV: "https://fd2ppv.cc/articles/{id}", FC2DB: "https://fc2db.net/work/{id}/", FC2CMADB: "https://fc2cmadb.com/articles/{id}", SUKEBEI: "https://sukebei.nyaa.si/?f=0&c=0_0&q={id}", GOOGLE_LENS: "https://lens.google.com/uploadbyurl?url={url}", FOURHOI_BASE: "https://fourhoi.com" }; var SCRAPER_URLS = { SUKEBEI_SEARCH: "https://sukebei.nyaa.si/?f=0&c=0_0&q={query}&s=seeders&o=desc", PAIPANCON_DETAIL: "https://paipancon.com/fc2daily/detail/FC2-PPV-{id}", PAIPANCON_BASE: "https://paipancon.com" }; var MAGNET_CONFIG = { MAX_CONCURRENCY: 16, MAX_RETRIES: 2, RETRY_DELAY: 1e3, PREDICTIVE_LIMIT: 16, DEFAULT_TYPE: "fc2", SEARCH_TIMEOUT_MS: 6e4 }; var PREVIEW_BLACKLIST = [ "moechat_ads.jpg", "mc.yandex.ru", "linglan_ad1.jpg" ]; var JAV_PREFIX_BLACKLIST = [ "FC2", "PPV", "PAGE", "LIST", "NEW", "BEST", "FILE", "VIEW", "LOCALHOST", "HTTP", "HTTPS", "WWW" ]; /** * Blacklisted actress name strings — exact matches and regex patterns. * Used by `MediaUtils.cleanActressName()` to filter out navigation/UI text. */ var ACTRESS_BLACKLIST_STRINGS = [ "首页", "分类", "我的", "搜索", "排行榜", "导航", "菜单", "更多", "全部", "女优", "无码", "有码", "素人", "流出", "破解", "解密", "合集", "个人拍摄", "個人撮影" ]; /** Regex-only blacklist patterns (entries not covered by exact string match above). */ var ACTRESS_BLACKLIST = [/Top\s*\d+/i, /^[\d\s]+$/]; var PATTERNS = { FC2_PPV_PREFIX: "FC2-PPV-", PAIPANCON_COVER: "cover.jpg", PAIPANCON_MAIN: "main.jpg", CENSORED_INDICATOR: "icon-mosaic_free color_free0", FD2PPV_SELLER_MARKER: "賣家" }; /** * Persistent storage key constants. * - User settings: SETTINGS, LANGUAGE, CARD_DENSITY, ACCENT_COLOR, etc. * - Sync state: LAST_SYNC_TS, LAST_AUTO_SYNC_TS, WEBDAV_LAST_ETAG, etc. * - Credentials: SUPABASE_*, WEBDAV_* * - Internal runtime: DEBUG_MODE, etc. */ var STORAGE_KEYS = exports("j",{ SETTINGS: "settings_v1", SUPABASE_URL: "supabase_url", SUPABASE_KEY: "supabase_key", SUPABASE_EMAIL: "supabase_email", SUPABASE_PASSWORD: "supabase_password", SUPABASE_JWT: "supabase_jwt", SUPABASE_REFRESH: "supabase_refresh_token", SYNC_USER_ID: "sync_user_id", LAST_SYNC_TS: "last_sync_ts", LAST_AUTO_SYNC_TS: "last_auto_sync_ts", LAST_SETTINGS_TAB: "last_settings_tab", WEBDAV_URL: "webdav_url", WEBDAV_USER: "webdav_user", WEBDAV_PASS: "webdav_pass", WEBDAV_PATH: "webdav_path", WEBDAV_LAST_ETAG: "webdav_last_etag", WEBDAV_SYNC_LOCK: "webdav_sync_lock", SYNC_MODE: "sync_mode", LANGUAGE: "language", USER_GRID_COLUMNS: "user_grid_columns_preference", FAB_POSITION: "fab_pos_v2", DEBUG_MODE: "fc2_debug_mode", CARD_DENSITY: "card_density", LAST_SYNC_RESULT: "last_sync_result", SETTINGS_COLLAPSED_GROUPS: "settings_collapsed_groups", SETTINGS_PRESET: "settings_preset", ACCENT_COLOR: "accent_color", FD2_BACKOFF_UNTIL: "fd2_backoff_until", LAST_ACTRESS_FETCH: "last_actress_fetch", CONFLICT_LOG: "sync_conflict_log" }); var DATABASE = { NAME: "fc2_enhanced_db" }; var CACHE = { MEMORY_MAX_SIZE: 1e3, MEMORY_EXPIRATION_MS: 3e5, EXPIRATION_MS: 12096e5, PREVIEW_MAX_SIZE: 5, NEGATIVE_MAGNET_EXPIRATION_MS: 432e5 }; var SYNC_STATUS = { IDLE: "idle", SYNCING: "syncing", SUCCESS: "success", ERROR: "error", CONFLICT: "conflict" }; var SUPABASE_ENDPOINTS = { TOKEN: "/auth/v1/token", SIGNUP: "/auth/v1/signup", USER_HISTORY: "/rest/v1/user_history" }; var SYSTEM_KEYS = { MESSAGING_CHANNEL: "fc2-enhanced-sync"}; var UI_CONSTANTS = { DEFAULT_TIMESTAMP: "1970-01-01T00:00:00.000Z", DEFAULT_SYNC_FILENAME: "fc2_enhanced_sync.json", PLACEHOLDER_IMAGE: "data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"400\" height=\"300\" fill=\"%23333\"%3E%3Crect width=\"400\" height=\"300\"/%3E%3Ctext x=\"50%25\" y=\"50%25\" dominant-baseline=\"middle\" text-anchor=\"middle\" fill=\"%23888\" font-family=\"sans-serif\" font-size=\"16\"%3ENo Image%3C/text%3E%3C/svg%3E", SWIPE_DISMISS_THRESHOLD: 100 }; var DOM_IDS = exports("y",{ UI_HOST: "fc2-modern-ui-host", SETTINGS_HOST: "fc2-enh-settings-host", SETTINGS_CONTAINER: "fc2-enh-settings-container", TAB_CONTENT: "tab-content-container", LOG_LIST: "debug-log-list" }); var CSS_CLASSES = { cardRebuilt: "fc2-card-rebuilt", processedCard: "fc2-processed-card", noMagnet: "fc2-no-magnet", hideNoMagnet: "fc2-hide-no-magnet", videoPreviewContainer: "fc2-video-preview-container", staticPreview: "fc2-static-preview", previewElement: "fc2-preview-element", hidden: "fc2-hidden", infoArea: "fc2-info-area", customTitle: "fc2-custom-card-title", fc2IdBadge: "fc2-id-badge", cardMeta: "fc2-card-meta", cardMetaItem: "fc2-card-meta-item", cardActionRow: "fc2-card-action-row", badgeCopied: "fc2-copied", preservedIconsContainer: "fc2-preserved-icons-container", resourceLinksContainer: "fc2-resource-links-container", resourceBtn: "fc2-resource-btn", btnLoading: "fc2-is-loading", btnMagnet: "fc2-magnet", tooltip: "fc2-tooltip", buttonText: "fc2-button-text", extraPreviewContainer: "fc2-preview-container", extraPreviewTitle: "fc2-preview-title", extraPreviewGrid: "fc2-preview-grid", isCensored: "fc2-is-censored", hideCensored: "fc2-hide-censored", enhDropdown: "fc2-enh-dropdown", isViewed: "fc2-is-viewed", hideViewed: "fc2-hide-viewed", isDownloaded: "fc2-is-downloaded", btnPlayFullscreen: "fc2-btn-play-fullscreen" }; var Config = { EXTERNAL_URLS, SCRAPER_URLS, STORAGE_KEYS, TIMEOUTS: { API: 2e4 }, CLASSES: CSS_CLASSES, COPIED_BADGE_DURATION: 1500 }; /** Extract HTTP status from a caught error object. Returns undefined if not an HTTP error. */ var getHttpStatus = (error) => { if (error && typeof error === "object" && "status" in error) return error.status; }; var createAbortError = () => new DOMException("The request was aborted", "AbortError"); var attachAbort = (signal, onAbort) => { if (!signal) return () => {}; if (signal.aborted) { onAbort(); return () => {}; } signal.addEventListener("abort", onAbort, { once: true }); return () => signal.removeEventListener("abort", onAbort); }; var http = (url, options = {}) => { const respType = options.responseType ?? options.type ?? "json"; let data = options.data ?? options.body; if (data && typeof data === "object") data = JSON.stringify(data); if (options.signal?.aborted) return Promise.reject(createAbortError()); return new Promise((resolve, reject) => { let requestHandle = null; const detachAbort = attachAbort(options.signal, () => { requestHandle?.abort?.(); reject(createAbortError()); }); const settle = (fn) => { detachAbort(); fn(); }; requestHandle = GM_xmlhttpRequest({ method: options.method ?? "GET", url, headers: { ...data ? { "Content-Type": "application/json" } : {}, ...options.headers ?? {} }, data, timeout: options.timeout ?? Config.TIMEOUTS.API, responseType: respType === "json" ? "json" : "text", onload: (res) => { if (res.status >= 200 && res.status < 300) if (respType === "json") try { settle(() => resolve(res.response || JSON.parse(res.responseText))); } catch { settle(() => resolve(res.responseText)); } else settle(() => resolve(res.responseText || res.response)); else { const err = /* @__PURE__ */ new Error(`HTTP ${res.status}: ${res.statusText}`); err.status = res.status; err.response = res.responseText; settle(() => reject(err)); } }, onerror: (err) => { const error = /* @__PURE__ */ new Error(`Network Error: ${err.statusText || "Unknown"}`); error.status = 0; error.response = err; settle(() => reject(error)); }, ontimeout: () => { const err = /* @__PURE__ */ new Error("Timeout"); err.status = 408; settle(() => reject(err)); } }); }); }; /** * Fetches a resource as a Blob via GM_xmlhttpRequest to bypass CORS/CORP. */ var normalizeBlob = async (value) => { if (value instanceof Blob) return value; if (typeof value === "object" && value !== null && "arrayBuffer" in value && typeof value.arrayBuffer === "function") { const val = value; return new Blob([await val.arrayBuffer()], { type: val.type ?? "" }); } throw new Error("Response was not a Blob"); }; var fetchBlob = (url, headers = {}, signal) => { if (signal?.aborted) return Promise.reject(createAbortError()); return new Promise((resolve, reject) => { let requestHandle = null; const detachAbort = attachAbort(signal, () => { requestHandle?.abort?.(); reject(createAbortError()); }); const settle = (fn) => { detachAbort(); fn(); }; requestHandle = GM_xmlhttpRequest({ method: "GET", url, headers, responseType: "blob", timeout: Config.TIMEOUTS.API, onload: (res) => { if (res.status >= 200 && res.status < 300) normalizeBlob(res.response).then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error instanceof Error ? error : new Error(String(error))))); else { const err = /* @__PURE__ */ new Error(`HTTP ${res.status}: ${res.statusText}`); err.status = res.status; settle(() => reject(err)); } }, onerror: () => settle(() => reject(/* @__PURE__ */ new Error("Network error"))), ontimeout: () => settle(() => reject(/* @__PURE__ */ new Error("Timeout"))) }); }); }; /** * Loads an image via GM_xmlhttpRequest and returns a local blob: URL. * Useful for bypassing ERR_BLOCKED_BY_RESPONSE.NotSameOrigin (CORP). * * **Caller responsibility:** The returned blob: URL must be revoked via * `URL.revokeObjectURL()` when no longer needed (e.g. after the image loads * or the element is removed) to avoid memory leaks. */ var fetchImageAsBlobUrl = async (url, headers = {}, signal) => { const blob = await fetchBlob(url, headers, signal); return URL.createObjectURL(blob); }; var mediaBlobUrls = /* @__PURE__ */ new WeakMap(); var liveBlobUrls = /* @__PURE__ */ new Set(); var trackBlobUrl = (blobUrl) => { liveBlobUrls.add(blobUrl); }; var untrackBlobUrl = (blobUrl) => { liveBlobUrls.delete(blobUrl); }; var assignMediaBlobUrl = (el, blobUrl) => { const previous = mediaBlobUrls.get(el); if (previous && previous !== blobUrl) { URL.revokeObjectURL(previous); untrackBlobUrl(previous); } mediaBlobUrls.set(el, blobUrl); trackBlobUrl(blobUrl); el.src = blobUrl; }; /** Revoke every blob URL created via assignMediaBlobUrl (page hide / teardown). */ var revokeAllMediaBlobUrls = () => { liveBlobUrls.forEach((url) => { try { URL.revokeObjectURL(url); } catch {} }); liveBlobUrls.clear(); }; /** * Returns appropriate Referer headers for known domains to bypass anti-hotlinking. */ var getBypassHeaders = (url) => { const headers = {}; if (url.includes("paipancon.com")) headers["Referer"] = "https://paipancon.com/"; if (url.includes("fd2ppv.cc")) headers["Referer"] = "https://fd2ppv.cc/"; if (url.includes("fourhoi.com")) headers["Referer"] = "https://fourhoi.com/"; return headers; }; var runMediaBypass = async (el, fallbacks, listener) => { const currentSrc = el.src; if (!currentSrc || currentSrc.startsWith("blob:") || currentSrc.includes("data:image") || currentSrc.includes("placeholder")) return; el.removeEventListener("error", listener); try { const blobUrl = await fetchImageAsBlobUrl(currentSrc, getBypassHeaders(currentSrc)); el.addEventListener("error", listener, { once: true }); assignMediaBlobUrl(el, blobUrl); } catch { const nextFallback = fallbacks.find((f) => f && f !== currentSrc); if (nextFallback) { const remaining = fallbacks.slice(fallbacks.indexOf(nextFallback) + 1); el.addEventListener("error", listener, { once: true }); el.src = nextFallback; if (remaining.length > 0) attachMediaBypass(el, ...remaining); } } }; /** * Attaches an error handler to an image or video element to attempt a GM bypass if it fails to load. * Supports multiple fallbacks in sequence. */ var attachMediaBypass = (el, ...fallbacks) => { const handleMediaError = () => { runMediaBypass(el, fallbacks, handleMediaError); }; el.addEventListener("error", handleMediaError, { once: true }); }; /** * Smartly loads media: proactive GM bypass for known problematic domains, * and standard load with error-triggered bypass for others. * Supports a sequence of fallback URLs. */ var smartLoadMedia = (el, src, ...fallbacks) => { if (!src || src.startsWith("blob:") || src.includes("data:image")) { if (src) el.src = src; return; } if (src.includes("paipancon.com") || src.includes("fourhoi.com")) fetchImageAsBlobUrl(src, getBypassHeaders(src)).then((blobUrl) => { assignMediaBlobUrl(el, blobUrl); }).catch(() => { const nextFallback = fallbacks.find((f) => f && f !== src); if (nextFallback) smartLoadMedia(el, nextFallback, ...fallbacks.slice(fallbacks.indexOf(nextFallback) + 1)); }); else { el.src = src; attachMediaBypass(el, ...fallbacks); } }; var LogLevel = exports("z",/* @__PURE__ */ function(LogLevel) { LogLevel[LogLevel["SILENT"] = 0] = "SILENT"; LogLevel[LogLevel["ERROR"] = 1] = "ERROR"; LogLevel[LogLevel["WARN"] = 2] = "WARN"; LogLevel[LogLevel["INFO"] = 3] = "INFO"; LogLevel[LogLevel["DEBUG"] = 4] = "DEBUG"; LogLevel[LogLevel["TRACE"] = 5] = "TRACE"; return LogLevel; }({})); var PREFIX = "[FC2-ENH]"; var DEDUP_WINDOW_MS = 2e3; var THROTTLE_WINDOW_MS = 5e3; var LEVEL_NAMES = { [LogLevel.SILENT]: "SILENT", [LogLevel.ERROR]: "ERROR", [LogLevel.WARN]: "WARN", [LogLevel.INFO]: "INFO", [LogLevel.DEBUG]: "DEBUG", [LogLevel.TRACE]: "TRACE" }; var formatTime = () => { const date = /* @__PURE__ */ new Date(); return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}.${String(date.getMilliseconds()).padStart(3, "0")}`; }; var makeKey = (level, module, message) => `${level}|${module}|${message}`; var shouldOutputLogLevel = (level, currentLevel) => level <= currentLevel; var buildLogEntry = (level, module, message, data, traceId) => ({ level, levelName: LEVEL_NAMES[level] ?? "UNKNOWN", module, message, timestamp: formatTime(), count: 1, ...traceId ? { traceId } : {}, ...data !== void 0 ? { data } : {} }); var appendHistoryEntry = (history, entry) => { history.push(entry); if (history.length > 500) history.splice(0, history.length - 500); }; var formatLogTag = (entry) => { return `${PREFIX}${entry.traceId ? ` [${entry.traceId}]` : ""} [${entry.levelName}] [${entry.module}]`; }; var outputLogEntry = (entry, currentLevel) => { if (!shouldOutputLogLevel(entry.level, currentLevel)) return; const args = [formatLogTag(entry), entry.message]; if (entry.data !== void 0) args.push(entry.data); switch (entry.level) { case LogLevel.ERROR: console.error(...args); break; case LogLevel.WARN: console.warn(...args); break; case LogLevel.DEBUG: case LogLevel.TRACE: console.debug(...args); break; default: console.log(...args); } }; var createThrottleNoticeEntry = (suppressed) => ({ level: LogLevel.DEBUG, levelName: "DEBUG", module: "Throttle", message: `Suppressed ${suppressed} repeated messages in ${THROTTLE_WINDOW_MS}ms`, timestamp: formatTime(), count: 1 }); var SENSITIVE_KEY_RE = /(authorization|token|secret|password|passwd|api[-_]?key|jwt|cookie|refresh)/i; var EMAIL_RE = /([A-Z0-9._%+-]{2})[A-Z0-9._%+-]*@([A-Z0-9.-]+\.[A-Z]{2,})/gi; var QUERY_SECRET_RE = /([?&](?:token|key|apikey|api_key|auth|signature|password)=)([^&]+)/gi; var MAX_SANITIZE_DEPTH = 8; var CIRCULAR_MARKER = "[Circular]"; var DEPTH_MARKER = "[MaxDepth]"; var DOM_NODE_MARKER = "[DOM Node]"; var isDomNode = (value) => { if (typeof Node !== "undefined" && value instanceof Node) return true; return typeof value.nodeType === "number" && typeof value.nodeName === "string"; }; var maskLogString = (value) => value.replace(EMAIL_RE, (_match, user, domain) => `${user}***@${domain}`).replace(QUERY_SECRET_RE, (_match, prefix) => `${prefix}***`).replace(/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer ***"); var sanitizeLogData = (value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) => { if (typeof value === "string") return maskLogString(value); if (Array.isArray(value)) { if (depth >= MAX_SANITIZE_DEPTH) return DEPTH_MARKER; if (seen.has(value)) return CIRCULAR_MARKER; seen.add(value); const result = value.map((item) => sanitizeLogData(item, depth + 1, seen)); seen.delete(value); return result; } if (!value || typeof value !== "object") return value; if (isDomNode(value)) return DOM_NODE_MARKER; if (depth >= MAX_SANITIZE_DEPTH) return DEPTH_MARKER; if (seen.has(value)) return CIRCULAR_MARKER; seen.add(value); const result = Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, SENSITIVE_KEY_RE.test(key) ? "***" : sanitizeLogData(entryValue, depth + 1, seen)])); seen.delete(value); return result; }; var exportSanitizedLogHistory = (history, options = {}) => { const filtered = history.filter((entry) => !options.traceId || entry.traceId === options.traceId); if (!options.sanitized) return filtered.map((entry) => ({ ...entry })); return filtered.map((entry) => ({ ...entry, message: maskLogString(entry.message), data: entry.data !== void 0 ? sanitizeLogData(entry.data) : void 0 })); }; var LoggerImpl = class { _enabled = false; _history = []; _level = LogLevel.WARN; _timers = /* @__PURE__ */ new Map(); _traceCounter = 0; _groupDepth = 0; _scopeCache = /* @__PURE__ */ new Map(); _dedup = /* @__PURE__ */ new Map(); _dedupFlushTimer = null; _throttle = /* @__PURE__ */ new Map(); get enabled() { return this._enabled; } get history() { this._flushDedup(); return [...this._history]; } get traceId() { return `t-${++this._traceCounter}`; } get level() { return this._level; } init() { try { if (typeof GM_getValue !== "undefined" ? GM_getValue("fc2_debug_mode", false) : false) this.enable(false); } catch {} } enable(persist = true) { this._enabled = true; this._level = LogLevel.TRACE; this._persistDebugMode(persist, true); } disable(persist = true) { this._enabled = false; this._level = LogLevel.WARN; this._persistDebugMode(persist, false); } setLevel(level) { this._level = level; } clear() { this._history.length = 0; this._dedup.clear(); this._throttle.clear(); } scope(module) { let scoped = this._scopeCache.get(module); if (!scoped) { scoped = { error: (message, data, traceId) => this.error(module, message, data, traceId), warn: (message, data, traceId) => this.warn(module, message, data, traceId), info: (message, data, traceId) => this.info(module, message, data, traceId), debug: (message, data, traceId) => this.debug(module, message, data, traceId), trace: (message, data, traceId) => this.trace(module, message, data, traceId), success: (message, data, traceId) => this.info(module, message, data, traceId) }; this._scopeCache.set(module, scoped); } return scoped; } time(label) { this._timers.set(label, performance.now()); } timeEnd(label) { const start = this._timers.get(label); if (start === void 0) return; this._timers.delete(label); const message = `${label}: ${(performance.now() - start).toFixed(2)}ms`; this._push(LogLevel.DEBUG, "Timer", message); return message; } log(module, message, data, traceId) { this._push(LogLevel.INFO, module, message, data, traceId); } info(module, message, data, traceId) { this._push(LogLevel.INFO, module, message, data, traceId); } warn(module, message, data, traceId) { this._push(LogLevel.WARN, module, message, data, traceId); } error(module, message, data, traceId) { this._push(LogLevel.ERROR, module, message, data, traceId); } success(module, message, data, traceId) { this._push(LogLevel.INFO, module, message, data, traceId); } debug(module, message, data, traceId) { this._push(LogLevel.DEBUG, module, message, data, traceId); } trace(module, message, data, traceId) { this._push(LogLevel.TRACE, module, message, data, traceId); } group(module, label, traceId) { this._groupDepth += 1; this._push(LogLevel.INFO, module, `[group:start] ${label}`, void 0, traceId); if (this._shouldOutput(LogLevel.INFO)) console.group(`${PREFIX} [${module}] ${label}`); } groupEnd() { if (this._groupDepth <= 0) return; this._groupDepth -= 1; if (this._enabled) console.groupEnd(); } _persistDebugMode(persist, enabled) { if (!persist) return; try { if (typeof GM_setValue !== "undefined") GM_setValue("fc2_debug_mode", enabled); } catch {} } _shouldOutput(level) { return shouldOutputLogLevel(level, this._level); } _push(level, module, message, data, traceId) { if (level > this._level && level !== LogLevel.ERROR) return; const sanitizedMessage = maskLogString(message); const sanitizedData = data !== void 0 ? sanitizeLogData(data) : void 0; const key = makeKey(level, module, sanitizedMessage); if (level >= LogLevel.DEBUG && this._checkThrottle(key)) return; const existing = this._dedup.get(key); const now = Date.now(); if (existing && now - existing.firstTs < 2e3) { existing.count += 1; existing.lastEntry.count = existing.count; if (data !== void 0) existing.lastEntry.data = sanitizeLogData(data); this._scheduleDedupFlush(); return; } this._flushDedup(); const entry = buildLogEntry(level, module, sanitizedMessage, sanitizedData, traceId); this._dedup.set(key, { count: 1, firstTs: now, flushed: false, lastEntry: entry }); this._record(entry); this._output(entry); } _scheduleDedupFlush() { if (this._dedupFlushTimer) return; this._dedupFlushTimer = setTimeout(() => { this._dedupFlushTimer = null; this._flushDedup(); }, DEDUP_WINDOW_MS); } _flushDedup() { if (this._dedupFlushTimer) { clearTimeout(this._dedupFlushTimer); this._dedupFlushTimer = null; } for (const state of this._dedup.values()) if (state.count > 1 && !state.flushed) { state.flushed = true; state.lastEntry.count = state.count; if (this._shouldOutput(state.lastEntry.level)) console.debug(`${formatLogTag(state.lastEntry)} (x${state.count}) ${state.lastEntry.message}`); } const now = Date.now(); for (const [key, state] of this._dedup) if (now - state.firstTs > 2e3) this._dedup.delete(key); } _checkThrottle(key) { const now = Date.now(); let state = this._throttle.get(key); if (!state || now - state.windowStart > 5e3) { state = { count: 1, suppressed: 0, windowStart: now }; this._throttle.set(key, state); return false; } state.count += 1; if (state.count <= 3) return false; state.suppressed += 1; if (state.suppressed === 1) this._scheduleThrottleSummary(key, state, now); return true; } _scheduleThrottleSummary(key, state, now) { setTimeout(() => { if (state.suppressed > 0) { const entry = createThrottleNoticeEntry(state.suppressed); this._record(entry); if (this._shouldOutput(LogLevel.DEBUG)) console.debug(formatLogTag(entry), entry.message); } this._throttle.delete(key); }, THROTTLE_WINDOW_MS - (now - state.windowStart)); } _record(entry) { appendHistoryEntry(this._history, entry); } _output(entry) { outputLogEntry(entry, this._level); } }; var Logger = exports("L",new LoggerImpl()); var exportLogHistory = exports("E",(options = {}) => exportSanitizedLogHistory(Logger.history, options)); var log$54 = Logger.scope("Container"); /** Create a typed service token */ var createToken = (description) => Symbol(description); /** AppContainer — token-only DI + lifecycle */ var Container = class { tokens = /* @__PURE__ */ new Map(); tokenNames = /* @__PURE__ */ new Map(); initialized = false; bootstrapPromise = null; /** * Register a service by typed token (only registration API). */ registerToken(token, service) { if (this.tokens.has(token)) log$54.warn(`Token ${token.description ?? String(token)} already registered, overwriting`); this.tokens.set(token, service); this.tokenNames.set(token, token.description ?? String(token)); return service; } /** * Resolve a service by token (throws if missing). */ resolve(token) { const service = this.tokens.get(token); if (!service) throw new Error(`[Container] Service not resolved: ${token.description ?? String(token)}`); return service; } /** * Try to resolve a service by token. */ tryResolve(token) { return this.tokens.get(token); } /** * System bootstrap with single-flight guard. */ async bootstrap() { if (this.initialized) return; if (this.bootstrapPromise) return this.bootstrapPromise; this.bootstrapPromise = this.doBootstrap(); return this.bootstrapPromise; } async doBootstrap() { Logger.group("Container", "System bootstrap"); try { const allServices = this.collectAllServices(); log$54.debug("Stage 1: Initializing services"); for (const [token, service] of allServices) if (service.onInit) try { await service.onInit(); log$54.debug(`Initialized: ${this.nameOf(token)}`); } catch (error) { log$54.error(`Failed to initialize service: ${this.nameOf(token)}`, error); throw error; } log$54.debug("Stage 2: Orchestrating bootstrap"); for (const [token, service] of allServices) if (service.onBootstrap) try { await service.onBootstrap(); log$54.debug(`Bootstrapped: ${this.nameOf(token)}`); } catch (error) { log$54.error(`Failed to bootstrap service: ${this.nameOf(token)}`, error); throw error; } this.initialized = true; log$54.info("System bootstrap complete"); } catch (error) { this.bootstrapPromise = null; log$54.error("Bootstrap failed", error); throw error; } finally { Logger.groupEnd(); } } /** * Teardown all services; cleanup errors are isolated. */ async shutdown() { log$54.info("Shutting down services"); try { const allServices = this.collectAllServices(); for (const [token, service] of allServices) if (service.onCleanup) try { await service.onCleanup(); } catch (error) { log$54.error(`Service ${this.nameOf(token)} cleanup failed`, error); } } finally { this.tokens.clear(); this.tokenNames.clear(); this.initialized = false; this.bootstrapPromise = null; } } collectAllServices() { return Array.from(this.tokens.entries()); } nameOf(token) { return this.tokenNames.get(token) ?? token.description ?? String(token); } }; var AppContainer = new Container(); var log$53 = Logger.scope("Events"); var AppEvents = exports("A",/* @__PURE__ */ function(AppEvents) { AppEvents["BOOTSTRAP"] = "app:bootstrap"; AppEvents["SERVICES_READY"] = "app:services-ready"; AppEvents["UI_READY"] = "app:ui-ready"; AppEvents["STATE_CHANGED"] = "app:state-changed"; AppEvents["THEME_CHANGED"] = "ui:theme-changed"; AppEvents["LANGUAGE_CHANGED"] = "ui:language-changed"; AppEvents["GRID_CHANGED"] = "ui:grid-changed"; AppEvents["SYNC_STATUS_CHANGED"] = "sync:status-changed"; AppEvents["HISTORY_LOADED"] = "history:loaded"; AppEvents["HISTORY_ADDED"] = "history:added"; AppEvents["HISTORY_REMOVED"] = "history:removed"; AppEvents["HISTORY_CLEARED"] = "history:cleared"; AppEvents["SITE_RESET"] = "site:reset"; AppEvents["SITE_READY"] = "site:ready"; AppEvents["MAGNET_FOUND"] = "magnet:found"; AppEvents["MAGNET_FAILED"] = "magnet:failed"; AppEvents["CARD_READY"] = "card:ready"; AppEvents["VIEW_STATE_CHANGED"] = "view:state-changed"; AppEvents["COLLECTION_HEALTH_PROGRESS"] = "collection:health-progress"; AppEvents["COLLECTION_UPDATED"] = "collection:updated"; AppEvents["COLLECTION_STATS_CHANGED"] = "collection:stats-changed"; AppEvents["COLLECTION_EXPORT"] = "collection:export"; AppEvents["COLLECTION_IMPORT"] = "collection:import"; AppEvents["SHOW_TOAST"] = "ui:show-toast"; AppEvents["OPEN_SETTINGS"] = "ui:open-settings"; AppEvents["PANEL_OPENED"] = "ui:panel-opened"; AppEvents["PANEL_CLOSED"] = "ui:panel-closed"; AppEvents["HISTORY_CHANGED"] = "history:changed"; AppEvents["FILTER_COUNTS_CHANGED"] = "ui:filter-counts-changed"; AppEvents["CONFIG_CHANGED"] = "ui:config-changed"; AppEvents["SELECTION_CHANGED"] = "selection:changed"; AppEvents["CARD_DISCOVERED"] = "card:discovered"; AppEvents["VIEW_REFRESH_REQUESTED"] = "view:refresh-requested"; AppEvents["DETAIL_ENHANCEMENT_REQUESTED"] = "site:detail-enhancement-requested"; AppEvents["HISTORY_STATUS_NEEDED"] = "history:status-needed"; AppEvents["VISIBILITY_REFRESH_NEEDED"] = "ui:visibility-refresh-needed"; AppEvents["CROSS_TAB_BROADCAST"] = "messaging:cross-tab-broadcast"; return AppEvents; }({})); var CoreEventsImpl = class { handlers = /* @__PURE__ */ new Map(); on(event, handler) { if (!this.handlers.has(event)) this.handlers.set(event, /* @__PURE__ */ new Set()); this.handlers.get(event).add(handler); return () => this.off(event, handler); } off(event, handler) { const handlersSet = this.handlers.get(event); if (handlersSet) handlersSet.delete(handler); } emit(event, data) { log$53.trace(`Emit: ${event}`, data); const handlersSet = this.handlers.get(event); if (handlersSet) handlersSet.forEach((handler) => { try { handler(data); } catch (e) { log$53.error(`Handler error for ${event}`, e); } }); } }; var CoreEvents = exports("C",new CoreEventsImpl()); /** Resolve a registered service token (throws if missing). */ function resolveService(token) { return AppContainer.resolve(token); } var DEFAULT_LOCALE = "en-US"; var LEGACY_LOCALE_MAP = { zh: "zh-CN", en: "en-US", ja: "ja-JP" }; var normalizeLocalePreference = (preference) => { if (!preference || preference === "auto") return "auto"; if (Object.prototype.hasOwnProperty.call(LEGACY_LOCALE_MAP, preference)) { const legacyLocale = LEGACY_LOCALE_MAP[preference]; if (legacyLocale) return legacyLocale; } const normalized = preference.toLowerCase(); if (normalized.startsWith("zh")) return "zh-CN"; if (normalized.startsWith("en")) return "en-US"; if (normalized.startsWith("ja")) return "ja-JP"; return DEFAULT_LOCALE; }; var resolveBrowserLocale = (browserLocale) => { const normalized = (browserLocale || "en-US").toLowerCase(); if (normalized.startsWith("zh")) return "zh-CN"; if (normalized.startsWith("ja")) return "ja-JP"; return "en-US"; }; var resolveLocalePreference = (preference, browserLocale = DEFAULT_LOCALE) => { if (!preference || preference === "auto") return resolveBrowserLocale(browserLocale); return normalizeLocalePreference(preference); }; var baseTranslations = { "zh-CN": { managementCenter: "管理中心", tabSettings: "基本设置", tabStatistics: "数据概览", tabData: "同步与导出", tabAbout: "关于", tabDmca: "免责声明", groupFilters: "内容过滤", optionHideNoMagnet: "隐藏无磁力资源", optionHideCensored: "过滤有码内容", optionHideViewed: "过滤已阅内容", optionHideBlocked: "屏蔽黑名单内容", groupAppearance: "界面与交互", groupExternalPortals: "外部站点", labelPreviewMode: "预览功能", labelCardDensity: "卡片密度", labelAccentColor: "主题色", accentColorDefault: "经典白", accentColorPurple: "极光紫", accentColorPink: "落樱粉", accentColorBlue: "深海蓝", accentColorGreen: "翡翠绿", accentColorOrange: "活力橙", accentColorGold: "皇家金", previewModeStatic: "静态封面", previewModeHover: "悬停/点击播放预览", cardDensityCoverOnly: "仅封面", cardDensityCompact: "紧凑", cardDensityStandard: "标准", cardDensityImmersive: "沉浸", labelGridColumns: "网格排版", labelDefault: "默认", labelLanguage: "显示语言", langAuto: "跟随浏览器", langZh: "简体中文", langEn: "English", langJa: "日本語", groupDataHistory: "功能增强", groupCardActions: "卡片快捷按钮", cardActionDirectHint: "勾选后直出显示,未勾选则收进更多菜单。", cardActionId: "复制番号", cardActionViewed: "已阅状态", cardActionMagnet: "磁力搜索", cardActionPlay: "视频预览", cardActionPreview: "图集预览", cardActionExternal: "外部站点", optionEnableHistory: "记录浏览历史", optionLoadExtraPreviews: "开启图集预览抓取 (Paipancon)", optionEnableQuickBar: "显示右下角悬浮球", optionShowViewedBtn: "卡片显示已阅开关", optionShowIdBadge: "卡片显示番号复制按钮", optionEnableMagnets: "聚合 Sukebei 的磁力搜索", optionEnableExternalLinks: "开启外部站点跳转", optionSupjavSortByViews: "Supjav 列表按播放量排序", labelCacheManagement: "缓存管理", btnClearCache: "清理磁力搜索结果缓存", labelHistoryManagement: "历史管理", btnClearHistory: "清空所有历史记录", alertSettingsSaved: "设置已保存", alertCacheCleared: "磁力搜索结果已清理", alertHistoryCleared: "所有历史记录已清空", menuOpenSettings: "⚙️ 脚本配置", tooltipCopyMagnet: "复制磁力链接", tooltipCopied: "已复制", tooltipOpenPreviewGallery: "打开图集预览", tooltipOpenExternalLinks: "打开外部站点", tooltipHealthRecent: "近期已验证正常", tooltipHealthStale: "健康检查已过期", extraPreviewTitle: "图集预览", alertNoPreview: "未找到预览图", alertPreviewCfBlocked: "被反爬盾拦截,请先到源站验证", alertPreviewRateLimited: "请求过于频繁,请稍后再试", alertNoVideoPreview: "未找到视频预览", alertNoExternalLinks: "未找到外部站点", labelExternalLinks: "外部站点", groupDataManagement: "备份与恢复", btnExportData: "导出备份", btnImportData: "恢复备份", alertExportSuccess: "备份文件已开始下载", alertImportSuccess: "数据恢复成功,即将重新加载", alertImportError: "恢复失败:无效的备份文件", tooltipMarkAsViewed: "标为已阅", tooltipMarkAsUnviewed: "标为未阅", confirmResetDatabase: "确定重置脚本?这将清空所有本地数据与配置且无法恢复。", groupWebDAV: "WebDAV 同步", labelWebDAVUrl: "服务器地址", labelWebDAVUser: "账号", labelWebDAVPass: "密码/Token", btnWebDAVTest: "测试连接", btnWebDAVSync: "立即同步", btnSyncNow: "立即同步", btnForceSync: "强制重新同步", alertWebDAVSuccess: "连接成功", alertWebDAVError: "连接失败,请检查配置", alertWebDAVSyncSuccess: "云同步已完成", alertWebDAVSyncError: "同步失败:", alertSupabaseSyncSuccess: "云同步已完成", alertSupabaseSyncError: "同步失败:", syncStatus: "连接状态", labelSyncMode: "同步策略", syncModeNone: "关闭", syncModeSupabase: "Supabase 云端同步", syncModeWebDAV: "WebDAV 协议 (推荐)", labelLastSync: "上次同步时间", labelNever: "从未同步", labelSyncing: "同步中...", alertAlreadyUpToDate: "数据已同步到最新", alertSyncLocked: "同步任务已在其他标签页运行中", alertSyncLockActive: "其他标签页正在更新,请稍候", labelSyncInterval: "自动同步频率", syncInterval0: "实时同步(每次操作后同步)", syncInterval2: "每 2 分钟", syncInterval5: "每 5 分钟 (推荐)", syncInterval10: "每 10 分钟", syncInterval30: "每 30 分钟", syncIntervalManual: "仅手动触发", labelAuthEmail: "邮箱", labelAuthPass: "密码", btnConnectAndSync: "登录并同步", btnPullSync: "仅强制拉取云端数据", btnLogout: "注销", alertLoginRequired: "请填写登录信息", alertSbUrlRequired: "缺少 Supabase 配置信息", alertSyncAccountConnected: "账号连接成功", alertPushAllQuery: "确定将本地数据全量推送到云端?", alertPullAllQuery: "确定从云端覆盖本地数据?这将丢弃所有本地未同步的改动。", labelSupabaseSync: "Supabase 同步", labelSupabaseUrl: "服务器地址", labelSupabaseKey: "密钥 (anon key)", aboutDescription: "为 FC2PPVDB 等站点提供磁力评分聚合、高清封面替换、图集模式及历史记录同步。", aboutHelpTitle: "主要功能", aboutHelpContent: "聚合 Sukebei 磁力搜索,并自动过滤失效条目。\n自动补全 Fourhoi 高清封面,详情页支持图集预览及键盘操作。\n支持 WebDAV 或 Supabase 协议,安全同步多设备浏览历史。\n快捷悬浮球、一键复制番号、外部站点快速跳转。", aboutFooterTagline: "为效率与隐私而设计。", labelTechnicalLogs: "系统日志", btnCopyAll: "复制全部", btnSelectAll: "全选", btnDeselectAll: "取消全选", btnClearLogs: "清空日志", alertLogsCopied: "日志已复制到剪贴板", alertLogsCleared: "系统日志已清空", labelLogFilters: "日志过滤", btnCancel: "取消", btnSave: "保存设置", btnMoreOptions: "更多功能", btnClose: "关闭", btnBackToTop: "回顶部", labelDebugMode: "调试模式", statusDebugOn: "开发模式", statusDebugOff: "常规模式", alertDebugOn: "调试模式已激活", alertDebugOff: "已回到常规模式", btnCopyEnv: "导出诊断信息", tooltipCopyId: "复制番号", dmcaContent: "本脚本仅为本地增强型工具,<b>不托管或存储任何视频或图片文件</b>。所有索引内容均来自第三方公开网站。使用者需对使用行为涉及的法律风险自行负责。如有侵权内容展示,请联系该内容的来源网站发起移除。", labelDisclaimer: "声明条款", labelGreasyFork: "Greasy Fork 主页", labelLoading: "加载中...", labelPreview: "图集预览", labelPlayPreview: "播放视频预览", alertPreviewLoadFailed: "预览加载失败", toastNetworkRetrying: "网络错误,正在重试({attempt}/{total})...", alertLoggedOut: "已成功退出", mainPreview: "功能演示", gallerySearch: "以图搜图", gallerySlideshow: "开启幻灯片播放", searchPlaceholder: "在当前列表中搜索...", labelError: "发生错误", confirmDestructiveAction: "注意:此操作具有破坏性,执行后数据无法找回。", groupScraperDiagnosis: "爬虫连通性诊断", btnTestScrapers: "开始连通性诊断", scraperStatusPending: "待测试", scraperStatusOk: "连接正常", scraperStatusError: "连接失败", scraperStatusCF: "需要验证 CF", scraperStatus429: "请求频繁", scraperStatusLoading: "测试中...", scraperTargetSukebei: "Sukebei(磁力搜索)", scraperTargetFd2: "FD2PPV(演员抓取)", scraperTargetPaipancon: "Paipancon(图集预览)", alertScraperBlocked: "爬虫已被拦截:{target}", btnGoVerify: "去验证", settingsAdvancedSection: "高级选项", syncLatestTitle: "最近一次同步", syncLatestEmpty: "暂无同步结果。", syncCompletedAt: "完成时间", syncNoConflictYet: "当前没有冲突记录。", syncConflictLocalNewer: "本地较新: {count}", syncConflictRemoteNewer: "云端较新: {count}", syncConflictMerged: "已合并: {count}", debugTraceIdPlaceholder: "按 Trace ID 过滤", debugExportJson: "导出 JSON", debugPayload: "载荷", perfSyncExecute: "同步执行", perfUpdatedAt: "更新时间", syncActionDebugLogs: "调试日志", backupImportPreviewPlaceholder: "导入预览会显示在这里。", backupExportSuccessPlain: "已导出备份。", backupImportErrorInvalidJson: "备份文件不是有效的 JSON。", backupImportErrorInvalidPayload: "备份文件结构无效。", backupImportErrorUnsupportedVersion: "备份版本高于当前脚本,暂时无法导入。", backupImportErrorReadFailed: "读取备份文件失败。", backupImportErrorApplyFailed: "备份解析成功,但应用数据时失败。", backupImportConfirm: "确定导入这个备份吗?", backupSettingsAdded: "新增设置:{count}", backupSettingsChanged: "变更设置:{count}", backupHistoryRows: "历史记录:{count}", backupHistoryRowsWithCurrent: "历史记录:{count}(当前:{current})", backupChangedKeys: "变更键:{keys}", syncSummaryFailed: "同步失败", syncSummaryFailedWithMessage: "同步失败: {message}", syncSummaryCompleted: "同步完成", syncSummaryMode: "模式: {mode}", syncSummaryPushed: "上传 {count}", syncSummaryPulled: "下载 {count}", syncSummaryDirty: "脏数据 {count}", syncSummaryUpToDate: "无需同步,数据已是最新。", syncSummaryConflictRetry: "检测到冲突,已按较新版重试。", syncConflictWebdavRetry: "WebDAV ETag 冲突,系统已刷新云端状态后再次合并。", syncErrorSupabase: "Supabase 同步失败", syncErrorWebdav: "WebDAV 同步失败", syncErrorNoProvider: "未配置同步提供方", syncErrorLocked: "另一标签页中已有同步正在进行", settingsTaskSync: "同步任务", settingsTaskConfigure: "配置选项", settingsTaskDiagnose: "诊断工具", settingsTaskInfo: "关于信息", labelSelectionMode: "批量操作模式", batchActionMarkViewed: "标记已看", batchActionMarkUnviewed: "标记未看", batchActionCopyIds: "复制番号", batchActionExtractMagnets: "提取磁力", batchSelectionCount: "已选中 {count} 项", batchActionConfirm: "确认执行", batchActionCancel: "取消", batchExtractTitle: "批量提取磁力链接", batchExtractPlaceholder: "正在抓取磁力链接...", batchExtractProgress: "正在抓取磁力 {current}/{total}...", batchExtractPartial: "成功 {ok} 条,无结果/失败 {fail} 条", batchNoneSelected: "请先选择项目", batchExtractEmpty: "未找到磁力链接", batchExtractSuccess: "已成功提取 {count} 条磁力链接" }, "en-US": { managementCenter: "Management Center", tabSettings: "Preferences", tabStatistics: "Analytics", tabData: "Data & Sync", tabAbout: "About", tabDmca: "Disclaimer", groupFilters: "Filters", optionHideNoMagnet: "Filter No-Magnet", optionHideCensored: "Filter Censored", optionHideViewed: "Filter Viewed", optionHideBlocked: "Filter Blocked", groupAppearance: "Appearance", groupExternalPortals: "External Sites", labelPreviewMode: "Preview Mode", labelCardDensity: "Card Density", labelAccentColor: "Accent Color", accentColorDefault: "Classic White", accentColorPurple: "Cyber Purple", accentColorPink: "Sakura Pink", accentColorBlue: "Deep Sea Blue", accentColorGreen: "Emerald Green", accentColorOrange: "Vibrant Orange", accentColorGold: "Royal Gold", previewModeStatic: "Static Cover", previewModeHover: "Hover/Click Play", cardDensityCoverOnly: "Cover Only", cardDensityCompact: "Compact", cardDensityStandard: "Standard", cardDensityImmersive: "Immersive", labelGridColumns: "Grid Columns", labelDefault: "Default", labelLanguage: "Language", langAuto: "Auto", langZh: "Chinese", langEn: "English", langJa: "Japanese", groupDataHistory: "Enhancements", groupCardActions: "Card Shortcuts", cardActionDirectHint: "Checked actions stay visible on the card; unchecked actions collapse into the more menu.", cardActionId: "Copy ID", cardActionViewed: "Toggle Viewed", cardActionMagnet: "Magnet Search", cardActionPlay: "Video Preview", cardActionPreview: "Gallery Preview", cardActionExternal: "External Sites", optionEnableHistory: "Track History", optionLoadExtraPreviews: "Enable Gallery Preview (Paipancon)", optionEnableQuickBar: "Show Quick FAB", optionShowViewedBtn: "Show Viewed Button on Card", optionShowIdBadge: "Show ID Badge on Card", optionEnableMagnets: "Aggregate Magnet Search (Sukebei)", optionEnableExternalLinks: "Show External Links", optionSupjavSortByViews: "Sort Supjav List by Views", labelCacheManagement: "Storage", btnClearCache: "Clear Magnet Search Cache", labelHistoryManagement: "History", btnClearHistory: "Clear History", alertSettingsSaved: "Settings saved", alertCacheCleared: "Magnet search cache cleared", alertHistoryCleared: "History cleared", menuOpenSettings: "⚙️ Settings", tooltipCopyMagnet: "Copy Magnet", tooltipCopied: "Copied", tooltipOpenPreviewGallery: "Open gallery preview", tooltipOpenExternalLinks: "Open external sites", tooltipHealthRecent: "Recently verified healthy", tooltipHealthStale: "Health check is stale", extraPreviewTitle: "Gallery Preview", alertNoPreview: "No Previews", alertPreviewCfBlocked: "Blocked by Cloudflare — verify on the source site first", alertPreviewRateLimited: "Rate limited — please try again later", alertNoVideoPreview: "No Video Previews", alertNoExternalLinks: "No external links found", labelExternalLinks: "External Sites", groupDataManagement: "Backup", btnExportData: "Export Backup", btnImportData: "Restore Backup", alertExportSuccess: "Backup started", alertImportSuccess: "Restore successful, refreshing...", alertImportError: "Restore failed: Invalid file", tooltipMarkAsViewed: "Mark viewed", tooltipMarkAsUnviewed: "Unmark viewed", confirmResetDatabase: "Reset script? This will wipe ALL local data and settings permanently.", groupWebDAV: "WebDAV Sync", labelWebDAVUrl: "Server URL", labelWebDAVUser: "Username", labelWebDAVPass: "Password/Token", btnWebDAVTest: "Test", btnWebDAVSync: "Sync Now", btnSyncNow: "Sync Now", btnForceSync: "Force Full Sync", alertWebDAVSuccess: "Connected", alertWebDAVError: "Connection failed. Check settings.", alertWebDAVSyncSuccess: "Sync Complete", alertWebDAVSyncError: "Sync Failed: ", alertSupabaseSyncSuccess: "Sync Complete", alertSupabaseSyncError: "Sync Failed: ", syncStatus: "Status", labelSyncMode: "Sync Strategy", syncModeNone: "Off", syncModeSupabase: "Supabase (Advanced)", syncModeWebDAV: "WebDAV (Recommended)", labelLastSync: "Last Sync", labelNever: "Never", labelSyncing: "Syncing...", alertAlreadyUpToDate: "Already up to date", alertSyncLocked: "Sync already in progress in another tab", alertSyncLockActive: "Sync in progress in another tab...", labelSyncInterval: "Auto-Sync Interval", syncInterval0: "Real-time (High load)", syncInterval2: "2 minutes", syncInterval5: "5 minutes (Recommended)", syncInterval10: "10 minutes", syncInterval30: "30 minutes", syncIntervalManual: "Manual only", labelAuthEmail: "Email", labelAuthPass: "Password", btnConnectAndSync: "Login", btnPullSync: "Force Pull Sync", btnLogout: "Logout", alertLoginRequired: "Credentials missing", alertSbUrlRequired: "URL missing", alertSyncAccountConnected: "Account connected", alertPushAllQuery: "Push all local data?", alertPullAllQuery: "Restore all data from cloud? This will overwrite local conflicts.", labelSupabaseSync: "Supabase", labelSupabaseUrl: "Supabase URL", labelSupabaseKey: "Supabase Key", aboutDescription: "Inject magnet links, HD covers, gallery mode, and history sync for FC2PPVDB, Supjav, and more.", aboutHelpTitle: "Features", aboutHelpContent: "• <b>Magnet Search</b>: Aggregate Sukebei results with smart filtering.\n• <b>HD Covers</b>: Auto-replace covers from Fourhoi, gallery preview with keyboard nav.\n• <b>Cloud Sync</b>: Sync history via WebDAV / Supabase across devices.\n• <b>Efficiency</b>: Quick FAB, instant ID copy, external link shortcuts.", aboutFooterTagline: "Designed for efficiency and privacy.", labelTechnicalLogs: "Technical Logs", btnCopyAll: "Copy All", btnSelectAll: "Select All", btnDeselectAll: "Deselect All", btnClearLogs: "Clear", alertLogsCopied: "Logs copied to clipboard", alertLogsCleared: "System logs cleared", labelLogFilters: "Filters", btnCancel: "Cancel", btnSave: "Save", btnMoreOptions: "More Options", btnClose: "Close", btnBackToTop: "Top", labelDebugMode: "Debug", statusDebugOn: "Debug ON", statusDebugOff: "Normal", alertDebugOn: "Debug Enabled", alertDebugOff: "Debug Disabled", btnCopyEnv: "Copy Diagnostics", tooltipCopyId: "Copy ID", dmcaContent: "This script is a utility tool and <b>does NOT host any video or image files</b>. All content (including images, magnet links) is provided by third-party public websites. Users assume full responsibility for using this tool. If content displayed by this tool infringes your rights, please contact the source website directly for removal.", labelDisclaimer: "Disclaimer", labelGreasyFork: "Greasy Fork", labelLoading: "Loading...", labelPreview: "Gallery Preview", labelPlayPreview: "Play Preview Video", alertPreviewLoadFailed: "Preview failed to load", toastNetworkRetrying: "Network error, retrying ({attempt}/{total})...", alertLoggedOut: "Logged out", mainPreview: "Feature Preview", gallerySearch: "Search Image", gallerySlideshow: "Slideshow", searchPlaceholder: "Search in collection...", labelError: "Error", confirmDestructiveAction: "Warning: Irreversible action. Continue?", groupScraperDiagnosis: "Scraper Connectivity", btnTestScrapers: "Run Diagnostics", scraperStatusPending: "Pending", scraperStatusOk: "Healthy", scraperStatusError: "Connection Failed", scraperStatusCF: "CF Blocked", scraperStatus429: "Rate Limited", scraperStatusLoading: "Testing...", scraperTargetSukebei: "Sukebei (Magnet Search)", scraperTargetFd2: "FD2PPV (Actress Fetch)", scraperTargetPaipancon: "Paipancon (Gallery Preview)", alertScraperBlocked: "Scraper blocked: {target}", btnGoVerify: "Verify Now", settingsAdvancedSection: "Advanced options", syncLatestTitle: "Latest sync", syncLatestEmpty: "No sync result yet.", syncCompletedAt: "Completed", syncNoConflictYet: "No conflicts reported yet.", syncConflictLocalNewer: "Local newer: {count}", syncConflictRemoteNewer: "Remote newer: {count}", syncConflictMerged: "Merged: {count}", debugTraceIdPlaceholder: "Filter by Trace ID", debugExportJson: "Export JSON", debugPayload: "Payload", perfSyncExecute: "Sync Execute", perfUpdatedAt: "Updated", syncActionDebugLogs: "Debug logs", backupImportPreviewPlaceholder: "Import preview will appear here.", backupExportSuccessPlain: "Backup exported.", backupImportErrorInvalidJson: "Backup file is not valid JSON.", backupImportErrorInvalidPayload: "Backup file structure is invalid.", backupImportErrorUnsupportedVersion: "Backup version is newer than this script and cannot be imported yet.", backupImportErrorReadFailed: "Failed to read the backup file.", backupImportErrorApplyFailed: "Backup parsed successfully, but applying it failed.", backupImportConfirm: "Import this backup?", backupSettingsAdded: "Settings added: {count}", backupSettingsChanged: "Settings changed: {count}", backupHistoryRows: "History rows: {count}", backupHistoryRowsWithCurrent: "History rows to import: {count} (current: {current})", backupChangedKeys: "Changed keys: {keys}", syncSummaryFailed: "Sync failed", syncSummaryFailedWithMessage: "Sync failed: {message}", syncSummaryCompleted: "Sync completed", syncSummaryMode: "Mode: {mode}", syncSummaryPushed: "Pushed {count}", syncSummaryPulled: "Pulled {count}", syncSummaryDirty: "Dirty {count}", syncSummaryUpToDate: "Already up to date", syncSummaryConflictRetry: "Conflict detected, retrying with fresh remote state", syncConflictWebdavRetry: "WebDAV ETag conflict detected. The app retried after refreshing remote state.", syncErrorSupabase: "Supabase sync failed", syncErrorWebdav: "WebDAV sync failed", syncErrorNoProvider: "No sync provider configured", syncErrorLocked: "Sync already in progress in another tab", settingsTaskSync: "Sync Tasks", settingsTaskConfigure: "Configure", settingsTaskDiagnose: "Diagnosis", settingsTaskInfo: "Information", labelSelectionMode: "Batch Selection Mode", batchActionMarkViewed: "Mark as Viewed", batchActionMarkUnviewed: "Mark as Unviewed", batchActionCopyIds: "Copy IDs", batchActionExtractMagnets: "Extract Magnets", batchSelectionCount: "{count} items selected", batchActionConfirm: "Execute", batchActionCancel: "Cancel", batchExtractTitle: "Batch Extract Magnets", batchExtractPlaceholder: "Fetching magnet links...", batchExtractProgress: "Fetching magnets {current}/{total}...", batchExtractPartial: "{ok} found, {fail} missing/failed", batchNoneSelected: "Select items first", batchExtractEmpty: "No magnets found", batchExtractSuccess: "Successfully extracted {count} magnets" }, "ja-JP": { managementCenter: "管理センター", tabSettings: "基本設定", tabStatistics: "統計", tabData: "同期とエクスポート", tabAbout: "バージョン情報", tabDmca: "免責事項", groupFilters: "コンテンツフィルタ", optionHideNoMagnet: "マグネットなしを非表示", optionHideCensored: "有修正をフィルタ", optionHideViewed: "既読をフィルタ", optionHideBlocked: "ブラックリストをフィルタ", groupAppearance: "外観と操作", groupExternalPortals: "外部サイト", labelPreviewMode: "プレビュー機能", labelCardDensity: "カード密度", labelAccentColor: "アクセントカラー", accentColorDefault: "クラシックホワイト", accentColorPurple: "サイバーパープル", accentColorPink: "サクラピンク", accentColorBlue: "ディープシーブルー", accentColorGreen: "エメラルドグリーン", accentColorOrange: "ビブラントオレンジ", accentColorGold: "ロイヤルゴールド", previewModeStatic: "静止画カバー", previewModeHover: "ホバー/クリック再生", cardDensityCoverOnly: "カバーのみ", cardDensityCompact: "コンパクト", cardDensityStandard: "スタンダード", cardDensityImmersive: "イマーシブ", labelGridColumns: "グリッドレイアウト", labelDefault: "デフォルト", labelLanguage: "表示言語", langAuto: "ブラウザ設定に従う", langZh: "简体中文", langEn: "English", langJa: "日本語", groupDataHistory: "機能拡張", groupCardActions: "ショートカットボタン", cardActionDirectHint: "チェックした項目はカード上に直接表示されます。", cardActionId: "品番をコピー", cardActionViewed: "既読状態", cardActionMagnet: "マグネット検索", cardActionPlay: "動画プレビュー", cardActionPreview: "ギャラリー", cardActionExternal: "外部サイト", optionEnableHistory: "閲覧履歴を記録", optionLoadExtraPreviews: "ギャラリープレビューを有効化 (Paipancon)", optionEnableQuickBar: "クイック操作球を表示", optionShowViewedBtn: "既読スイッチを表示", optionShowIdBadge: "品番コピーボタンを表示", optionEnableMagnets: "Sukebei マグネット検索を統合", optionEnableExternalLinks: "外部サイトへのジャンプを有効化", optionSupjavSortByViews: "Supjav リストを再生数順にソート", labelCacheManagement: "キャッシュ管理", btnClearCache: "マグネット検索キャッシュを消去", labelHistoryManagement: "履歴管理", btnClearHistory: "すべての閲覧履歴を消去", alertSettingsSaved: "設定を保存しました", alertCacheCleared: "キャッシュを消去しました", alertHistoryCleared: "履歴を消去しました", menuOpenSettings: "⚙️ スクリプト設定", tooltipCopyMagnet: "マグネットをコピー", tooltipCopied: "コピー完了", tooltipOpenPreviewGallery: "ギャラリーを開く", tooltipOpenExternalLinks: "外部サイトを開く", tooltipHealthRecent: "最近の検証で正常", tooltipHealthStale: "検証データが古い", extraPreviewTitle: "プレビュー", alertNoPreview: "プレビューが見つかりません", alertPreviewCfBlocked: "Cloudflareにブロックされました。先に元サイトで認証してください", alertPreviewRateLimited: "リクエストが多すぎます。しばらくしてからお試しください", alertNoVideoPreview: "動画プレビューが見つかりません", alertNoExternalLinks: "外部リンクが見つかりません", labelExternalLinks: "外部サイト", groupDataManagement: "バックアップと復元", btnExportData: "バックアップをエクスポート", btnImportData: "バックアップから復元", alertExportSuccess: "エクスポートを開始しました", alertImportSuccess: "復元に成功しました。再読み込みします", alertImportError: "復元失敗:無効なファイルです", tooltipMarkAsViewed: "既読にする", tooltipMarkAsUnviewed: "未読にする", confirmResetDatabase: "スクリプトをリセットしますか?すべてのデータが消去されます。", groupWebDAV: "WebDAV 同期", labelWebDAVUrl: "サーバーURL", labelWebDAVUser: "ユーザー名", labelWebDAVPass: "パスワード/トークン", btnWebDAVTest: "接続テスト", btnWebDAVSync: "今すぐ同期", btnSyncNow: "同期実行", btnForceSync: "強制再同期", alertWebDAVSuccess: "接続成功", alertWebDAVError: "接続失敗。設定を確認してください", alertWebDAVSyncSuccess: "同期が完了しました", alertWebDAVSyncError: "同期失敗:", alertSupabaseSyncSuccess: "同期が完了しました", alertSupabaseSyncError: "同期失敗:", syncStatus: "接続ステータス", labelSyncMode: "同期モード", syncModeNone: "無効", syncModeSupabase: "Supabase (クラウド)", syncModeWebDAV: "WebDAV (推奨)", labelLastSync: "最終同期", labelNever: "未同期", labelSyncing: "同期中...", alertAlreadyUpToDate: "最新の状態です", alertSyncLocked: "他のタブで同期が実行中です", alertSyncLockActive: "同期処理中、お待ちください", labelSyncInterval: "自動同期の間隔", syncInterval0: "リアルタイム", syncInterval2: "2分ごと", syncInterval5: "5分ごと (推奨)", syncInterval10: "10分ごと", syncInterval30: "30分ごと", syncIntervalManual: "手動のみ", labelAuthEmail: "メールアドレス", labelAuthPass: "パスワード", btnConnectAndSync: "ログインして同期", btnPullSync: "クラウドから強制取得", btnLogout: "ログアウト", alertLoginRequired: "ログイン情報を入力してください", alertSbUrlRequired: "Supabase 設定が不足しています", alertSyncAccountConnected: "アカウントに接続しました", alertPushAllQuery: "ローカルデータをクラウドに上書きしますか?", alertPullAllQuery: "クラウドデータでローカルを上書きしますか?", labelSupabaseSync: "Supabase 同期", labelSupabaseUrl: "API URL", labelSupabaseKey: "API Key (anon)", aboutDescription: "FC2PPVDB 等のサイトにマグネット検索、高画質カバー、ギャラリーモード、同期機能を提供します。", aboutHelpTitle: "主な機能", aboutHelpContent: "Sukebei マグネット検索の統合とフィルタリング。\nFourhoi 高画質カバーへの自動置換、キーボード操作対応のギャラリー。\nWebDAV / Supabase 経由のマルチデバイス履歴同期。\nクイックボタン、品番コピー、外部サイト連携。", aboutFooterTagline: "効率とプライバシーのために設計。", labelTechnicalLogs: "システムログ", btnCopyAll: "すべてコピー", btnSelectAll: "すべて選択", btnDeselectAll: "選択解除", btnClearLogs: "ログをクリア", alertLogsCopied: "ログをクリップボードにコピーしました", alertLogsCleared: "ログを消去しました", labelLogFilters: "フィルタ", btnCancel: "キャンセル", btnSave: "保存", btnMoreOptions: "詳細設定", btnClose: "閉じる", btnBackToTop: "トップへ", labelDebugMode: "デバッグモード", statusDebugOn: "デバッグ有効", statusDebugOff: "通常モード", alertDebugOn: "デバッグを有効にしました", alertDebugOff: "通常モードに戻りました", btnCopyEnv: "診断情報をコピー", tooltipCopyId: "品番をコピー", dmcaContent: "このスクリプトはローカル拡張ツールであり、<b>動画や画像ファイルをホストまたは保存しません</b>。すべての内容は第三者の公開サイトから取得されています。使用者は自己の責任において利用するものとします。", labelDisclaimer: "免责事项", labelGreasyFork: "Greasy Fork ページ", labelLoading: "読み込み中...", labelPreview: "ギャラリー", labelPlayPreview: "プレビュー再生", alertPreviewLoadFailed: "読み込み失敗", toastNetworkRetrying: "ネットワークエラー。リトライ中 ({attempt}/{total})...", alertLoggedOut: "ログアウトしました", mainPreview: "機能デモ", gallerySearch: "画像検索", gallerySlideshow: "スライドショー", searchPlaceholder: "リスト内を検索...", labelError: "エラー", confirmDestructiveAction: "警告:この操作は取り消せません。続行しますか?", groupScraperDiagnosis: "スクレイパー診断", btnTestScrapers: "診断を開始", scraperStatusPending: "未テスト", scraperStatusOk: "正常", scraperStatusError: "接続エラー", scraperStatusCF: "CF 検証が必要", scraperStatus429: "レート制限", scraperStatusLoading: "テスト中...", scraperTargetSukebei: "Sukebei (マグネット)", scraperTargetFd2: "FD2PPV (出演者)", scraperTargetPaipancon: "Paipancon (ギャラリー)", alertScraperBlocked: "ブロックされました: {target}", btnGoVerify: "検証に行く", settingsAdvancedSection: "詳細オプション", syncLatestTitle: "最新の同期", syncLatestEmpty: "同期記録なし。", syncCompletedAt: "完了時刻", syncNoConflictYet: "コンフリクトはありません。", syncConflictLocalNewer: "ローカルが新しい: {count}", syncConflictRemoteNewer: "クラウドが新しい: {count}", syncConflictMerged: "マージ済み: {count}", debugTraceIdPlaceholder: "Trace ID でフィルタ", debugExportJson: "JSON 出力", debugPayload: "ペイロード", perfSyncExecute: "同期実行", perfUpdatedAt: "更新時刻", syncActionDebugLogs: "デバッグログ", backupImportPreviewPlaceholder: "プレビューがここに表示されます。", backupExportSuccessPlain: "エクスポート完了。", backupImportErrorInvalidJson: "無効な JSON です。", backupImportErrorInvalidPayload: "無効なバックアップ構造です。", backupImportErrorUnsupportedVersion: "バージョンが新しすぎるため読み込めません。", backupImportErrorReadFailed: "読み込みに失敗しました。", backupImportErrorApplyFailed: "データの適用に失敗しました。", backupImportConfirm: "このバックアップを復元しますか?", backupSettingsAdded: "新規設定: {count}", backupSettingsChanged: "変更設定: {count}", backupHistoryRows: "履歴: {count}", backupHistoryRowsWithCurrent: "履歴: {count} (現在: {current})", backupChangedKeys: "変更キー: {keys}", syncSummaryFailed: "同期失敗", syncSummaryFailedWithMessage: "失敗: {message}", syncSummaryCompleted: "同期完了", syncSummaryMode: "モード: {mode}", syncSummaryPushed: "アップロード {count}", syncSummaryPulled: "ダウンロード {count}", syncSummaryDirty: "未同期 {count}", syncSummaryUpToDate: "最新の状態です。", syncSummaryConflictRetry: "衝突を検出し、新しい状態で再試行しました。", syncConflictWebdavRetry: "WebDAV ETag 衝突。リフレッシュ後に再試行しました。", syncErrorSupabase: "Supabase 同期エラー", syncErrorWebdav: "WebDAV 同期エラー", syncErrorNoProvider: "プロバイダー未設定", syncErrorLocked: "他のタブで同期が進行中です", settingsTaskSync: "同期タスク", settingsTaskConfigure: "設定", settingsTaskDiagnose: "診断", settingsTaskInfo: "情報", labelSelectionMode: "一括操作モード", batchActionMarkViewed: "既読にする", batchActionMarkUnviewed: "未読にする", batchActionCopyIds: "IDをコピー", batchActionExtractMagnets: "マグネットを抽出", batchSelectionCount: "{count}個を選択中", batchActionConfirm: "実行", batchActionCancel: "キャンセル", batchExtractTitle: "マグネットリンクの一括抽出", batchExtractPlaceholder: "マグネットリンクを取得中...", batchExtractProgress: "マグネット取得中 {current}/{total}...", batchExtractPartial: "成功 {ok} 件、失敗/なし {fail} 件", batchNoneSelected: "先に項目を選択してください", batchExtractEmpty: "マグネットリンクが見つかりません", batchExtractSuccess: "{count}個のマグネットリンクを抽出しました" } }; /** Optional reader injected from composition root after State is ready. */ var localeReader = null; var setLocaleReader = (reader) => { localeReader = reader; }; var getActiveLocale = () => { const language = localeReader?.() ?? "auto"; return resolveLocalePreference(String(language || "auto"), navigator.language); }; var Localization = { _translations: baseTranslations, get locale() { return getActiveLocale(); }, t(key, params) { const locale = getActiveLocale(); let value = (this._translations[locale] || this._translations["en-US"])?.[key] || this._translations["en-US"]?.[key] || key; if (params && typeof value === "string") Object.entries(params).forEach(([paramKey, paramValue]) => { value = value.replaceAll(`{${paramKey}}`, String(paramValue)); }); return value; }, resolvePath(obj, path) { try { return path.split(".").reduce((prev, curr) => { if (prev && typeof prev === "object") return prev[curr]; }, obj); } catch { return null; } }, register(locale, newTranslations) { if (!this._translations[locale]) this._translations[locale] = {}; const target = this._translations[locale]; if (target) this.deepMerge(target, newTranslations); }, deepMerge(target, source) { for (const key in source) if (source[key] instanceof Object && key in target && target[key] instanceof Object) this.deepMerge(target[key], source[key]); else target[key] = source[key]; return target; } }; var t = exports("t",(key, params) => Localization.t(key, params)); var SVG_NS = "http://www.w3.org/2000/svg"; /** Tags allowed in sanitized HTML (localized content + static inline SVG icons). */ var ALLOWED_TAGS = /* @__PURE__ */ new Set([ "a", "b", "br", "code", "div", "em", "i", "li", "ol", "p", "small", "span", "strong", "ul", "svg", "path", "g", "circle", "rect", "line", "polyline", "polygon", "ellipse", "defs", "title", "use", "text" ]); /** Attributes copied verbatim from the source element. */ var ALLOWED_ATTRS = /* @__PURE__ */ new Set([ "class", "id", "title", "alt", "rel", "target", "role", "aria-label", "xmlns", "viewbox", "d", "fill", "stroke", "stroke-width", "stroke-linecap", "stroke-linejoin", "fill-rule", "clip-rule", "width", "height", "x", "y", "cx", "cy", "r", "rx", "ry", "points", "transform", "opacity", "version" ]); /** URL-valued attributes that must pass the protocol allowlist. */ var URL_ATTRS = /* @__PURE__ */ new Set([ "href", "src", "xlink:href" ]); var SAFE_URL_PROTOCOLS = /* @__PURE__ */ new Set([ "http:", "https:", "mailto:" ]); var isSafeUrl = (value) => { const trimmed = value.trim(); if (!trimmed) return true; try { return SAFE_URL_PROTOCOLS.has(new URL(trimmed, "https://local.invalid").protocol); } catch { return false; } }; var sanitizeNode = (source, output, stats) => { for (const child of Array.from(source.childNodes)) { if (child.nodeType === Node.TEXT_NODE) { output.appendChild(document.createTextNode(child.textContent ?? "")); continue; } if (child.nodeType !== Node.ELEMENT_NODE) continue; const element = child; const tag = element.localName.toLowerCase(); if (!ALLOWED_TAGS.has(tag)) { stats.droppedElements += 1; continue; } const created = element.namespaceURI === SVG_NS ? document.createElementNS(SVG_NS, tag) : document.createElement(tag); for (const attr of Array.from(element.attributes)) { const name = attr.name; const lower = name.toLowerCase(); if (lower.startsWith("on")) { stats.droppedAttributes += 1; continue; } if (lower === "style") { stats.droppedAttributes += 1; continue; } if (URL_ATTRS.has(lower)) { if (!isSafeUrl(attr.value)) { stats.droppedAttributes += 1; continue; } } else if (!ALLOWED_ATTRS.has(lower)) { stats.droppedAttributes += 1; continue; } created.setAttribute(name, attr.value); } sanitizeNode(element, created, stats); output.appendChild(created); } }; /** * Sanitizes untrusted HTML against script execution, event handlers, and unsafe URLs. * Returns both the sanitized markup and what was stripped, so callers can distinguish * "re-serialized verbatim" from "unsafe content actually removed". */ var sanitizeHtmlDetailed = (raw) => { const template = document.createElement("template"); template.innerHTML = raw; const output = document.createElement("div"); const stats = { droppedElements: 0, droppedAttributes: 0 }; sanitizeNode(template.content, output, stats); return { html: output.innerHTML, droppedElements: stats.droppedElements, droppedAttributes: stats.droppedAttributes }; }; var h = exports("h",(tag, props = {}, ...children) => { const el = document.createElement(tag); for (const [key, val] of Object.entries(props)) if (key === "className") el.className = val; else if (key === "style" && typeof val === "object" && val !== null) Object.entries(val).forEach(([sKey, sVal]) => { if (sKey.startsWith("--")) el.style.setProperty(sKey, String(sVal)); else el.style[sKey] = String(sVal); }); else if (key === "dataset" && typeof val === "object" && val !== null) Object.assign(el.dataset, val); else if (key.startsWith("on") && typeof val === "function") { const eventName = key.toLowerCase().substring(2); el.addEventListener(eventName, val); } else if (key === "innerHTML") { const raw = String(val); if (tag === "style") el.textContent = raw; else { const { html, droppedElements, droppedAttributes } = sanitizeHtmlDetailed(raw); if (droppedElements > 0 || droppedAttributes > 0) Logger.warn("DOM", `Sanitized unsafe innerHTML (removed ${droppedElements} element(s), ${droppedAttributes} attribute(s))`); el.innerHTML = html; } } else if (val !== null && val !== void 0 && val !== false) { const target = el; if (key in target) target[key] = val; else el.setAttribute(key, String(val)); } children.flat().forEach((child) => { if (child === null || child === void 0 || child === false) return; el.appendChild(child instanceof Node ? child : document.createTextNode(String(child))); }); return el; }); var getBestImageSource = (img) => { if (!img) return ""; const d = img.dataset; const src = d.original || d.src || d.lazySrc || img.src || ""; if (src.startsWith("data:image") || src.includes("pixel.gif")) return d.original || d.src || d.lazySrc || ""; return src; }; /** * Service tokens ?typed symbols for pluggable service resolution. * * Each token maps to a service interface. Consumers resolve by token, * never by concrete class import. This enables: * - Swappable implementations (test mocks, alternative providers) * - No compile-time coupling between service modules * - Clean dependency declaration via resolve() calls */ var HISTORY_SERVICE = exports("H",createToken("HistoryService")); var SCRAPER_SERVICE = exports("p",createToken("ScraperService")); var MAGNET_SERVICE = createToken("MagnetService"); var PREVIEW_SERVICE = createToken("PreviewService"); var PORTAL_SERVICE = exports("P",createToken("PortalService")); var SYNC_SERVICE = exports("e",createToken("SyncService")); var BACKUP_SERVICE = exports("B",createToken("BackupService")); var SELECTION_SERVICE = createToken("SelectionService"); var MENU_SERVICE = createToken("MenuService"); var MESSAGING_SERVICE = createToken("MessagingService"); var TIMELINE_SERVICE = createToken("TimelineService"); var CLEANUP_SERVICE = createToken("CleanupService"); var GRID_SERVICE = createToken("GridService"); var SITE_SERVICE = createToken("SiteService"); var STYLE_SERVICE = createToken("StyleService"); var STATE_SERVICE = exports("S",createToken("StateService")); var DATA_SERVICE = exports("D",createToken("DataService")); var UI_MANAGER = createToken("UIManager"); var QUICKBAR_SERVICE = createToken("QuickBar"); var WEBDAV_PROVIDER = createToken("WebDAVProvider"); var SUPABASE_PROVIDER = createToken("SupabaseProvider"); var CARD_ENHANCEMENT_SERVICE = createToken("CardEnhancementService"); var log$52 = Logger.scope("UIHost"); var OverlayStackImpl = class { stack = []; handleKeyDown = (e) => { if (e.key === "Escape") { const top = this.stack[this.stack.length - 1]; if (top) { e.preventDefault(); e.stopPropagation(); top.close(); } } }; push(overlay) { if (this.stack.length === 0) document.addEventListener("keydown", this.handleKeyDown, true); if (!this.stack.includes(overlay)) this.stack.push(overlay); } remove(overlay) { const index = this.stack.indexOf(overlay); if (index > -1) this.stack.splice(index, 1); if (this.stack.length === 0) document.removeEventListener("keydown", this.handleKeyDown, true); } }; var OverlayStack = new OverlayStackImpl(); var UIHostImpl = class { host = null; _shadow = null; styleSheet = null; resetHostState() { this.host = null; this._shadow = null; this.styleSheet = null; } ensureConnectedHost() { if (!this.host?.isConnected || !this._shadow) { this.resetHostState(); this.onInit(); } } onInit() { if (this.host && this.host.isConnected) return; log$52.debug("Initializing shadow host"); this.host = h("div", { id: DOM_IDS.UI_HOST, style: { position: "fixed", inset: "0", pointerEvents: "none", zIndex: "2147483647" } }); this._shadow = this.host.attachShadow({ mode: "open" }); const css = resolveService(STYLE_SERVICE).getCss(); try { if ("adoptedStyleSheets" in this._shadow) { this.styleSheet = new CSSStyleSheet(); this.styleSheet.replaceSync(css); this._shadow.adoptedStyleSheets = [this.styleSheet]; } else throw new Error("Not supported"); } catch { const style = h("style", { innerHTML: css }); this._shadow.appendChild(style); } document.body.appendChild(this.host); } get shadow() { this.ensureConnectedHost(); return this._shadow; } add(el) { this.ensureConnectedHost(); this._shadow.appendChild(el); } }; var UIHost = new UIHostImpl(); var log$51 = Logger.scope("Browser"); var BrowserService = class { /** * Opens a URL in a new tab using GM_openInTab if available, fallback to window.open. */ static openTab(url, active = true) { if (typeof GM_openInTab !== "undefined") try { GM_openInTab(url, { active, insert: true, setParent: true }); return; } catch (error) { log$51.error("Failed to open tab via GM_openInTab", error); } window.open(url, "_blank", "noopener,noreferrer"); } /** * Shows a system-level notification using GM_notification. */ static notify(text, title = "FC2PPVDB Enhanced", image, onClick) { if (typeof GM_notification !== "undefined") try { GM_notification({ text, title, image: image || "data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\"/>", timeout: 5e3, onclick: onClick }); } catch (error) { log$51.error("Failed to show system notification", error); } } }; var IconArrowUp = "<svg viewBox=\"0 0 384 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.3l105.4 105.3c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z\"/></svg>"; var IconGear = exports("x","<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M495.9 166.6c3.2 8.7.5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4l-55.6 17.8c-8.8 2.8-18.6.3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4c-1.1-8.4-1.7-16.9-1.7-25.5s.6-17.1 1.7-25.4l-43.3-39.4c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336a80 80 0 1 0 0-160a80 80 0 1 0 0 160\"/></svg>"); var IconBan = "<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M367.2 412.5L99.5 144.8C77.1 176.1 64 214.5 64 256c0 106 86 192 192 192c41.5 0 79.9-13.1 111.2-35.5m45.3-45.3C434.9 335.9 448 297.5 448 256c0-106-86-192-192-192c-41.5 0-79.9 13.1-111.2 35.5zM0 256a256 256 0 1 1 512 0a256 256 0 1 1-512 0\"/></svg>"; var IconMagnet = "<svg viewBox=\"0 0 448 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M0 160v96c0 123.7 100.3 224 224 224s224-100.3 224-224v-96H320v96c0 53-43 96-96 96s-96-43-96-96v-96zm0-32h128V64c0-17.7-14.3-32-32-32H32C14.3 32 0 46.3 0 64zm320 0h128V64c0-17.7-14.3-32-32-32h-64c-17.7 0-32 14.3-32 32z\"/></svg>"; var IconEyeSlash = exports("K","<svg viewBox=\"0 0 640 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2s-6.3 25.5 4.1 33.7l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7l-105.2-82.4c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8zm184.3 144.4c25.5-23.3 59.6-37.5 96.9-37.5c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c8.4-19.3 10.6-41.4 4.8-63.3c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zM373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9.5-13.6 1.4-20.2l-94.3-74.3c-22.8 29.7-39.1 59.3-48.6 82.2c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1c47 43.8 111.7 80.6 192.5 80.6c47.8 0 89.9-12.9 126.2-32.5z\"/></svg>"); var IconRotate = exports("n","<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M142.9 142.9c-17.5 17.5-30.1 38-37.8 59.8c-5.9 16.7-24.2 25.4-40.8 19.5S38.9 198 44.8 181.4c10.8-30.7 28.4-59.4 52.8-83.8c87.2-87.2 228.3-87.5 315.8-1L455 55c6.9-6.9 17.2-8.9 26.2-5.2S496 62.3 496 72v128c0 13.3-10.7 24-24 24H344c-9.7 0-18.5-5.8-22.2-14.8s-1.7-19.3 5.2-26.2l41.1-41.1c-62.6-61.5-163.1-61.2-225.3 1zM16 312c0-13.3 10.7-24 24-24h128c9.7 0 18.5 5.8 22.2 14.8s1.7 19.3-5.2 26.2l-41.1 41.1c62.6 61.5 163.1 61.2 225.3-1c17.5-17.5 30.1-38 37.8-59.8c5.9-16.7 24.2-25.4 40.8-19.5s25.4 24.2 19.5 40.8c-10.8 30.6-28.4 59.3-52.9 83.8c-87.2 87.2-228.3 87.5-315.8 1L57 457c-6.9 6.9-17.2 8.9-26.2 5.2S16 449.7 16 440V312.1z\"/></svg>"); var IconPlus = "<svg viewBox=\"0 0 448 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32v144H48c-17.7 0-32 14.3-32 32s14.3 32 32 32h144v144c0 17.7 14.3 32 32 32s32-14.3 32-32V288h144c17.7 0 32-14.3 32-32s-14.3-32-32-32H256z\"/></svg>"; var IconImages = "<svg viewBox=\"0 0 576 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M160 32c-35.3 0-64 28.7-64 64v224c0 35.3 28.7 64 64 64h352c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64zm236 106.7l96 144c4.9 7.4 5.4 16.8 1.2 24.6S480.9 320 472 320H200c-9.2 0-17.6-5.3-21.6-13.6s-2.9-18.2 2.9-25.4l64-80c4.6-5.7 11.4-9 18.7-9s14.2 3.3 18.7 9l17.3 21.6l56-84c4.5-6.6 12-10.6 20-10.6s15.5 4 20 10.7M192 128a32 32 0 1 1 64 0a32 32 0 1 1-64 0m-144-8c0-13.3-10.7-24-24-24S0 106.7 0 120v224c0 75.1 60.9 136 136 136h320c13.3 0 24-10.7 24-24s-10.7-24-24-24H136c-48.6 0-88-39.4-88-88z\"/></svg>"; var IconSpinner = "<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M304 48a48 48 0 1 0-96 0a48 48 0 1 0 96 0m0 416a48 48 0 1 0-96 0a48 48 0 1 0 96 0M48 304a48 48 0 1 0 0-96a48 48 0 1 0 0 96m464-48a48 48 0 1 0-96 0a48 48 0 1 0 96 0M142.9 437A48 48 0 1 0 75 369.1a48 48 0 1 0 67.9 67.9m0-294.2A48 48 0 1 0 75 75a48 48 0 1 0 67.9 67.9zM369.1 437a48 48 0 1 0 67.9-67.9a48 48 0 1 0-67.9 67.9\"/></svg>"; var IconCircleCheck = "<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M256 512a256 256 0 1 0 0-512a256 256 0 1 0 0 512m113-303L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z\"/></svg>"; var IconCircleXmark = "<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M256 512a256 256 0 1 0 0-512a256 256 0 1 0 0 512m-81-337c9.4-9.4 24.6-9.4 33.9 0l47 47l47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47l47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47l-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47l-47-47c-9.4-9.4-9.4-24.6 0-33.9\"/></svg>"; var IconTriangleExclamation = exports("F","<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7.2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8.2-40.1l216-368C228.7 39.5 241.8 32 256 32m0 128c-13.3 0-24 10.7-24 24v112c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24m32 224a32 32 0 1 0-64 0a32 32 0 1 0 64 0\"/></svg>"); var IconCircleInfo = exports("G","<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M256 512a256 256 0 1 0 0-512a256 256 0 1 0 0 512m-40-176h24v-64h-24c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-80c-13.3 0-24-10.7-24-24s10.7-24 24-24m40-208a32 32 0 1 1 0 64a32 32 0 1 1 0-64\"/></svg>"); var IconXmark = "<svg viewBox=\"0 0 384 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7L86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256L41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3l105.4 105.3c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256z\"/></svg>"; var IconSliders = exports("b","<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M0 416c0 17.7 14.3 32 32 32h54.7c12.3 28.3 40.5 48 73.3 48s61-19.7 73.3-48H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H233.3c-12.3-28.3-40.5-48-73.3-48s-61 19.7-73.3 48H32c-17.7 0-32 14.3-32 32m128 0a32 32 0 1 1 64 0a32 32 0 1 1-64 0m192-160a32 32 0 1 1 64 0a32 32 0 1 1-64 0m32-80c-32.8 0-61 19.7-73.3 48H32c-17.7 0-32 14.3-32 32s14.3 32 32 32h246.7c12.3 28.3 40.5 48 73.3 48s61-19.7 73.3-48H480c17.7 0 32-14.3 32-32s-14.3-32-32-32h-54.7c-12.3-28.3-40.5-48-73.3-48m-160-48a32 32 0 1 1 0-64a32 32 0 1 1 0 64m73.3-64C253 35.7 224.8 16 192 16s-61 19.7-73.3 48H32C14.3 64 0 78.3 0 96s14.3 32 32 32h86.7c12.3 28.3 40.5 48 73.3 48s61-19.7 73.3-48H480c17.7 0 32-14.3 32-32s-14.3-32-32-32z\"/></svg>"); var IconDatabase = exports("l","<svg viewBox=\"0 0 448 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M448 80v48c0 44.2-100.3 80-224 80S0 172.2 0 128V80C0 35.8 100.3 0 224 0s224 35.8 224 80m-54.8 134.7c20.8-7.4 39.9-16.9 54.8-28.6V288c0 44.2-100.3 80-224 80S0 332.2 0 288V186.1c14.9 11.8 34 21.2 54.8 28.6C99.7 230.7 159.5 240 224 240s124.3-9.3 169.2-25.3M0 346.1c14.9 11.8 34 21.2 54.8 28.6C99.7 390.7 159.5 400 224 400s124.3-9.3 169.2-25.3c20.8-7.4 39.9-16.9 54.8-28.6V432c0 44.2-100.3 80-224 80S0 476.2 0 432z\"/></svg>"); var IconFilter = exports("v","<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M3.9 54.9C10.5 40.9 24.5 32 40 32h432c15.5 0 29.5 8.9 36.1 22.9s4.6 30.5-5.2 42.5L320 320.9V448c0 12.1-6.8 23.2-17.7 28.6s-23.8 4.3-33.5-3l-64-48c-8.1-6-12.8-15.5-12.8-25.6v-79.1L9 97.3C-.7 85.4-2.8 68.8 3.9 54.9\"/></svg>"); var IconPalette = exports("u","<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M512 256v2.7c-.4 36.5-33.6 61.3-70.1 61.3H344c-26.5 0-48 21.5-48 48c0 3.4.4 6.7 1 9.9c2.1 10.2 6.5 20 10.8 29.9c6.1 13.8 12.1 27.5 12.1 42c0 31.8-21.6 60.7-53.4 62c-3.5.1-7 .2-10.6.2C114.6 512 0 397.4 0 256S114.6 0 256 0s256 114.6 256 256m-384 32a32 32 0 1 0-64 0a32 32 0 1 0 64 0m0-96a32 32 0 1 0 0-64a32 32 0 1 0 0 64m160-96a32 32 0 1 0-64 0a32 32 0 1 0 64 0m96 96a32 32 0 1 0 0-64a32 32 0 1 0 0 64\"/></svg>"); var IconClockRotateLeft = exports("w","<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M75 75L41 41C25.9 25.9 0 36.6 0 57.9V168c0 13.3 10.7 24 24 24h110.1c21.4 0 32.1-25.9 17-41l-30.8-30.8C155 85.5 203 64 256 64c106 0 192 86 192 192s-86 192-192 192c-40.8 0-78.6-12.7-109.7-34.4c-14.5-10.1-34.4-6.6-44.6 7.9s-6.6 34.4 7.9 44.6C151.2 495 201.7 512 256 512c141.4 0 256-114.6 256-256S397.4 0 256 0C185.3 0 121.3 28.7 75 75m181 53c-13.3 0-24 10.7-24 24v104c0 6.4 2.5 12.5 7 17l72 72c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-65-65V152c0-13.3-10.7-24-24-24z\"/></svg>"); var IconFileExport = exports("a","<svg viewBox=\"0 0 576 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M0 64C0 28.7 28.7 0 64 0h160v128c0 17.7 14.3 32 32 32h128v128H216c-13.3 0-24 10.7-24 24s10.7 24 24 24h168v112c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64zm384 272v-48h110.1l-39-39c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l80 80c9.4 9.4 9.4 24.6 0 33.9l-80 80c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l39-39zm0-208H256V0z\"/></svg>"); var IconFileImport = exports("I","<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M128 64c0-35.3 28.7-64 64-64h160v128c0 17.7 14.3 32 32 32h128v288c0 35.3-28.7 64-64 64H192c-35.3 0-64-28.7-64-64V336h174.1l-39 39c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l80-80c9.4-9.4 9.4-24.6 0-33.9l-80-80c-9.4-9.4-24.6-9.4-33.9 0s-9.4 24.6 0 33.9l39 39l-174.1.1zm0 224v48H24c-13.3 0-24-10.7-24-24s10.7-24 24-24zm384-160H384V0z\"/></svg>"); var IconChevronLeft = "<svg viewBox=\"0 0 320 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256L246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z\"/></svg>"; var IconChevronRight = "<svg viewBox=\"0 0 320 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256L73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z\"/></svg>"; var IconEye = exports("J","<svg viewBox=\"0 0 576 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32M144 256a144 144 0 1 1 288 0a144 144 0 1 1-288 0m144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3\"/></svg>"); var IconLink = exports("f","<svg viewBox=\"0 0 640 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l112.2-112.3c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0z\"/></svg>"); var IconBolt = "<svg viewBox=\"0 0 448 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M349.4 44.6c5.9-13.7 1.5-29.7-10.6-38.5s-28.6-8-39.9 1.8l-256 224c-10 8.8-13.6 22.9-8.9 35.3S50.7 288 64 288h111.5L98.6 467.4c-5.9 13.7-1.5 29.7 10.6 38.5s28.6 8 39.9-1.8l256-224c10-8.8 13.6-22.9 8.9-35.3s-16.6-20.7-30-20.7H272.5z\"/></svg>"; var IconPlayCircle = "<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M0 256a256 256 0 1 1 512 0a256 256 0 1 1-512 0m188.3-108.9c-7.6 4.2-12.3 12.3-12.3 20.9v176c0 8.7 4.7 16.7 12.3 20.9s16.8 4.1 24.3-.5l144-88c7.1-4.4 11.5-12.1 11.5-20.5s-4.4-16.1-11.5-20.5l-144-88c-7.4-4.5-16.7-4.7-24.3-.5z\"/></svg>"; var IconPlay = exports("d","<svg viewBox=\"0 0 384 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80v352c0 17.4 9.4 33.4 24.5 41.9S58.2 482 73 473l288-176c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41z\"/></svg>"); var IconPause = "<svg viewBox=\"0 0 320 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M48 64C21.5 64 0 85.5 0 112v288c0 26.5 21.5 48 48 48h32c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48zm192 0c-26.5 0-48 21.5-48 48v288c0 26.5 21.5 48 48 48h32c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48z\"/></svg>"; var IconVial = exports("o","<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M342.6 9.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l9.4 9.4L28.1 342.6C10.1 360.6 0 385 0 410.5v5.5c0 53 43 96 96 96h5.5c25.5 0 49.9-10.1 67.9-28.1L448 205.3l9.4 9.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-32-32l-96-96l-32-32zM205.3 256L352 109.3l50.7 50.7l-96 96H205.2z\"/></svg>"); var IconImageSearch = "<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M416 208c0 45.9-14.9 88.3-40 122.7l126.6 126.7c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0s208 93.1 208 208M208 352a144 144 0 1 0 0-288a144 144 0 1 0 0 288\"/></svg>"; var IconListUl = "<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M64 144a48 48 0 1 0 0-96a48 48 0 1 0 0 96m128-80c-17.7 0-32 14.3-32 32s14.3 32 32 32h288c17.7 0 32-14.3 32-32s-14.3-32-32-32zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32h288c17.7 0 32-14.3 32-32s-14.3-32-32-32zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32h288c17.7 0 32-14.3 32-32s-14.3-32-32-32zM64 464a48 48 0 1 0 0-96a48 48 0 1 0 0 96m48-208a48 48 0 1 0-96 0a48 48 0 1 0 96 0\"/></svg>"; var IconServer = exports("m","<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M64 32C28.7 32 0 60.7 0 96v64c0 35.3 28.7 64 64 64h384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64zm280 72a24 24 0 1 1 0 48a24 24 0 1 1 0-48m48 24a24 24 0 1 1 48 0a24 24 0 1 1-48 0M64 288c-35.3 0-64 28.7-64 64v64c0 35.3 28.7 64 64 64h384c35.3 0 64-28.7 64-64v-64c0-35.3-28.7-64-64-64zm280 72a24 24 0 1 1 0 48a24 24 0 1 1 0-48m56 24a24 24 0 1 1 48 0a24 24 0 1 1-48 0\"/></svg>"); var PORTAL_ICONS = { dmm: IconPlayCircle, fc2: IconLink, supjav: IconBolt, missav: IconPlayCircle, javdb: IconDatabase, javbus: IconDatabase, javlibrary: IconDatabase, fd2ppv: IconServer, fc2db: IconDatabase, sukebei: "<svg viewBox=\"0 0 512 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M416 208c0 45.9-14.9 88.3-40 122.7l126.6 126.7c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0s208 93.1 208 208M208 352a144 144 0 1 0 0-288a144 144 0 1 0 0 288\"/></svg>", fc2cmadb: IconDatabase }; var log$50 = Logger.scope("Retry"); var isServerErrorStatus = (status) => typeof status === "number" && status >= 500 && status < 600; var RetryManager = class { static defaultConfig = { maxRetries: 3, backoffMs: [ 1e3, 3e3, 5e3 ], shouldRetry: (error) => { const status = getHttpStatus(error); return status === 0 || isServerErrorStatus(status); } }; static async executeWithRetry(operation, config = {}) { const finalConfig = { ...this.defaultConfig, ...config }; let lastError; for (let attempt = 0; attempt <= finalConfig.maxRetries; attempt++) try { log$50.debug(`Attempt ${attempt + 1}/${finalConfig.maxRetries + 1}`); return await operation(); } catch (error) { lastError = error; if (attempt === finalConfig.maxRetries) { log$50.error("All retries exhausted", error); throw error; } if (finalConfig.shouldRetry && !finalConfig.shouldRetry(error)) { log$50.warn("Error not retryable", error); throw error; } if (finalConfig.onRetry) try { finalConfig.onRetry(error, attempt + 1); } catch (retryError) { log$50.warn("Error in onRetry callback", retryError); } const delay = finalConfig.backoffMs[attempt] ?? finalConfig.backoffMs[finalConfig.backoffMs.length - 1] ?? 0; log$50.debug(`Retry in ${delay}ms`, { attempt: attempt + 1, error }); await new Promise((resolve) => setTimeout(resolve, delay)); } throw lastError; } /** * Standard retry presets (Pure logic only) */ static presets = { network: (onRetry) => ({ maxRetries: 3, backoffMs: [ 1e3, 3e3, 5e3 ], shouldRetry: (error) => { const status = getHttpStatus(error); return !!(status === 0 || status === 408 || isServerErrorStatus(status)); }, onRetry }), sync: { maxRetries: 2, backoffMs: [2e3, 5e3], shouldRetry: (error) => { const status = getHttpStatus(error); return !!(status !== 401 && status !== 403); } }, supabaseSync: { maxRetries: 2, backoffMs: [1e3, 3e3], shouldRetry: (error) => { const status = getHttpStatus(error); return !!(status === 0 || status === 408 || status === 429 || isServerErrorStatus(status)); } }, magnet: { maxRetries: 2, backoffMs: [1500, 3e3], shouldRetry: (error) => { return !!(getHttpStatus(error) !== 429); } } }; }; var MediaUtils = { extractFC2Id: (url) => url?.match(/(?:articles\/|fc2-ppv-|fc2ppv-|fc2-|work\/|watch\/|^)(\d{5,8})(?:$|\/|\.|_)/i)?.[1] ?? null, parseVideoId: (text, url = "") => { const input = (text + " " + url).replace(/[+\s_]/g, "-").toUpperCase(); const fc2Prefix = input.match(/(?:FC2[^\d]*PPV[^\d]*|FC2-)(\d{5,8})/i); if (fc2Prefix && fc2Prefix[1]) { const id = fc2Prefix[1]; return { id, type: "fc2", previewSlug: `fc2-ppv-${id}` }; } const fc2FromUrl = MediaUtils.extractFC2Id(url); if (fc2FromUrl) return { id: fc2FromUrl, type: "fc2", previewSlug: `fc2-ppv-${fc2FromUrl}` }; const jav = input.match(/([A-Z0-9]{2,20})[^A-Z0-9]*?(\d{2,8}(?:-\d{1,4})?)/); if (jav && jav[1] && jav[2]) { let prefix = jav[1]; if (!/^\d+$/.test(prefix)) { if (prefix === "DM" && (input.includes("/DM") || input.includes("-DM"))) return null; if (prefix === "PONDO" || prefix === "1PONDO") prefix = "1PON"; if (prefix === "MUSUME" || prefix === "10MUSUME") prefix = "10MU"; if (prefix === "PACOPACOMAMA") prefix = "PACO"; if (prefix === "CARIBBEANCOM") prefix = "CARIB"; if (!JAV_PREFIX_BLACKLIST.includes(prefix)) return { id: `${prefix}-${jav[2]}`, type: "jav" }; } } const dateMatch = input.match(/(\d{6,8})-(\d{1,4})/); if (dateMatch && dateMatch[1] && dateMatch[2]) { const d = dateMatch[1]; if (d.length === 6 || d.length === 8) return { id: `${dateMatch[1]}-${dateMatch[2]}`, type: "jav" }; } const fc2Raw = input.match(/(?:^|[^A-Z0-9])(\d{5,10})(?:$|[^A-Z0-9])/); if (fc2Raw && fc2Raw[1]) { const id = fc2Raw[1]; return { id, type: "fc2", previewSlug: `fc2-ppv-${id}` }; } return null; }, cleanActressName: (name) => { if (!name) return null; const n = name.trim(); const blacklist = ACTRESS_BLACKLIST; if (ACTRESS_BLACKLIST_STRINGS.includes(n) || blacklist.some((reg) => reg.test(n))) return null; if (n.length > VALIDATION.ACTRESS_NAME_MAX_LENGTH || n.length < VALIDATION.ACTRESS_NAME_MIN_LENGTH) return null; return n; }, formatDate: (dateStr) => { if (!dateStr) return ""; try { const date = new Date(dateStr); if (isNaN(date.getTime())) return dateStr; return date.toLocaleDateString(void 0, { year: "numeric", month: "2-digit", day: "2-digit" }); } catch { return dateStr; } }, cleanImageUrl: (url) => { if (!url) return url; return url.replace(/!\d+x\d+\.(jpg|jpeg|png|webp)/gi, ""); }, /** * Splits a video ID into parts for flexible matching (especially for date-based brands) * e.g., "1PONDO-042526-001" -> { brand: "1PONDO", short: "1PON", date: "042526", serial: "001" } */ parseDateIdParts: (id) => { const match = id.toUpperCase().match(/^(1PON|10MU|CARIB|PACO|1PONDO|10MUSUME|CARIBBEANCOM|PACOPACOMAMA)-(\d{6,8})-(\d{1,4})$/); if (!match || !match[1] || !match[2] || !match[3]) return null; let brand = match[1]; const date = match[2]; const serial = match[3]; if (brand === "1PON") brand = "1PONDO"; if (brand === "10MU") brand = "10MUSUME"; if (brand === "CARIB") brand = "CARIBBEANCOM"; if (brand === "PACO") brand = "PACOPACOMAMA"; return { brand, short: { "1PONDO": "1PON", "10MUSUME": "10MU", CARIBBEANCOM: "CARIB", PACOPACOMAMA: "PACO" }[brand] || brand, date, serial }; } }; var Storage = { get: (key, def) => typeof GM_getValue !== "undefined" ? GM_getValue(key, def) : def, set: (key, val) => { if (typeof GM_setValue !== "undefined") GM_setValue(key, val); }, delete: (key) => { if (typeof GM_deleteValue !== "undefined") GM_deleteValue(key); } }; /** Convert hex color string to "r, g, b" format for CSS custom properties */ var hexToRgb = (hex) => { const h = hex.startsWith("#") ? hex : `#${hex}`; const r = parseInt(h.slice(1, 3), 16); const g = parseInt(h.slice(3, 5), 16); const b = parseInt(h.slice(5, 7), 16); if (isNaN(r) || isNaN(g) || isNaN(b)) { console.warn(`[hexToRgb] Invalid hex color: "${hex}", falling back to black`); return "0, 0, 0"; } return `${r}, ${g}, ${b}`; }; var Utils = { debounce: (func, delay) => { let t; return (...a) => { if (t) clearTimeout(t); t = setTimeout(() => func(...a), delay); }; }, chunk: (arr, size) => Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => arr.slice(i * size, i * size + size)), sleep: (ms) => new Promise((res) => setTimeout(res, ms)), copyToClipboard: async (text) => { if (typeof GM_setClipboard !== "undefined") { GM_setClipboard(text); return; } if (typeof navigator !== "undefined" && navigator.clipboard) await navigator.clipboard.writeText(text); } }; var log$49 = Logger.scope("Device"); var DeviceService = class { static _platform = null; static get platform() { if (!this._platform) this._platform = this.detectPlatform(); return this._platform; } static detectPlatform() { let os = "unknown"; let browser = "unknown"; const info = typeof GM_info !== "undefined" ? GM_info : null; if (info?.platform) { const p = info.platform; os = p.os || "unknown"; browser = p.browserName || "unknown"; } const ua = navigator.userAgent.toLowerCase(); if (os === "unknown") { if (/android/.test(ua)) os = "android"; else if (/iphone|ipad|ipod/.test(ua)) os = "ios"; else if (/windows/.test(ua)) os = "windows"; else if (/macintosh/.test(ua)) os = "macos"; else if (/linux/.test(ua)) os = "linux"; } const isTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0 || window.matchMedia("(pointer: coarse)").matches; const isSmallScreen = window.matchMedia("(max-width: 768px)").matches; const isMobile = os === "android" || os === "ios" || isSmallScreen && isTouch; log$49.debug(`[Precision Detect] OS: ${os}, Small: ${isSmallScreen}, Touch: ${isTouch} -> MobileMode: ${isMobile}`); return { os, browser, isMobile, isTouch, isSmallScreen }; } /** * Should use mobile-optimized UI (Touch friendly, stacked layout) */ static isMobile() { return this.platform.isMobile; } /** * Specifically identifies phone-sized devices */ static isPhone() { return this.isMobileOS() && this.platform.isSmallScreen; } /** * Specifically identifies tablets (Mobile OS but large screen) */ static isTablet() { return this.isMobileOS() && !this.platform.isSmallScreen; } /** * True desktop (Desktop OS and not forced into a small window) */ static isDesktop() { return !this.isMobileOS() && !this.platform.isSmallScreen; } static isTouch() { return this.platform.isTouch; } static isMobileOS() { return this.platform.os === "android" || this.platform.os === "ios"; } }; var log$48 = Logger.scope("Gzip"); var GzipServiceImpl = class { _isSupported = null; isSupported() { if (this._isSupported !== null) return this._isSupported; try { this._isSupported = typeof window.CompressionStream === "function" && typeof window.DecompressionStream === "function"; } catch { this._isSupported = false; } return this._isSupported; } async compress(data) { if (!this.isSupported()) return data instanceof Blob ? data : new Blob([data], { type: "application/json" }); try { const compressedStream = (data instanceof Blob ? data.stream() : new Blob([data], { type: "application/json" }).stream()).pipeThrough(new CompressionStream("gzip")); return await new Response(compressedStream).blob(); } catch (error) { log$48.error("Compression error", error); return data instanceof Blob ? data : new Blob([data], { type: "application/json" }); } } async decompress(data) { if (!this.isSupported()) return await data.text(); try { if (!await this.checkIfGzip(data)) return await data.text(); const decompressedStream = data.stream().pipeThrough(new DecompressionStream("gzip")); return await new Response(decompressedStream).text(); } catch (error) { log$48.error("Decompression error", error); return await data.text(); } } async checkIfGzip(blob) { if (blob.size < 2) return false; const arrayBuffer = await blob.slice(0, 2).arrayBuffer(); const header = new Uint8Array(arrayBuffer); return header[0] === 31 && header[1] === 139; } }; var GzipService = new GzipServiceImpl(); var UIUtils = exports("U",{ h, icon: (svgContent, className = "") => { return h("span", { className: `fc2-icon ${className}`.trim(), innerHTML: svgContent }); }, copyButtonBehavior: (btn, textToCopy, i18nCopied) => { if (btn.dataset.copied === "true") return; Utils.copyToClipboard(textToCopy); btn.dataset.copied = "true"; const tt = btn.querySelector(`.${Config.CLASSES.tooltip}`); if (tt) { const originalTip = tt.textContent; tt.textContent = i18nCopied; setTimeout(() => { tt.textContent = originalTip; btn.dataset.copied = "false"; }, Config.COPIED_BADGE_DURATION); } else { const originalText = btn.textContent; btn.textContent = i18nCopied; setTimeout(() => { btn.textContent = originalText; btn.dataset.copied = "false"; }, Config.COPIED_BADGE_DURATION); } }, toggleLoading: (cont, show, btnCreator) => { if (!cont?.isConnected) return; const spinner = cont.querySelector(`.${Config.CLASSES.btnLoading}`); if (show) { if (!spinner) { const btn = btnCreator(IconSpinner, t("labelLoading")); btn.classList.add(Config.CLASSES.btnLoading); cont.appendChild(btn); } cont.classList.add("fc2-skeleton"); } else { if (spinner) spinner.remove(); cont.classList.remove("fc2-skeleton"); } } }); var log$47 = Logger.scope("Toast"); var MAX_TOASTS = 8; var resolveToastDisplayOptions = (options) => ({ duration: options.duration === 0 ? 0 : options.duration || TIMING.TOAST_DEFAULT_DURATION, showClose: options.showClose ?? true }); var getToastIconSvg = (type) => { switch (type) { case "success": return IconCircleCheck; case "error": return IconCircleXmark; case "warn": return IconTriangleExclamation; default: return IconCircleInfo; } }; var getToastColor = (type) => { switch (type) { case "success": return "var(--fc2-success)"; case "error": return "var(--fc2-danger)"; case "warn": return "var(--fc2-warn)"; default: return "var(--fc2-info)"; } }; var createToastContainer = () => h("div", { className: "fc2-toast-container" }); var createToastProgress = (duration, color) => duration > 0 ? h("div", { className: "fc2-toast-progress", style: { "--fc2-toast-color": color, "--fc2-toast-duration": `${duration}ms` } }) : null; var createToastElement = (message, type, duration, showClose, options, onDismiss) => { const toast = h("div", { className: `fc2-toast-item toast-${type}`, onclick: options.onClick }); const dismissToast = (event) => { event.stopPropagation(); onDismiss(toast); }; const actionButton = options.action ? h("button", { className: "fc2-toast-action", onclick: (event) => { event.stopPropagation(); options.action?.onClick(); onDismiss(toast); } }, options.action.label) : null; const closeButton = showClose ? h("button", { className: "fc2-toast-close", onclick: dismissToast }, UIUtils.icon(IconXmark)) : null; const color = getToastColor(type); const icon = h("div", { className: "fc2-toast-icon" }, UIUtils.icon(getToastIconSvg(type))); const content = h("span", { className: "fc2-toast-content" }, message); const progress = createToastProgress(duration, color); toast.append(icon, content); if (actionButton) toast.appendChild(actionButton); if (closeButton) toast.appendChild(closeButton); if (progress) toast.appendChild(progress); return toast; }; var ToastManager = class { container = null; toasts = /* @__PURE__ */ new Set(); MAX_TOASTS = MAX_TOASTS; init() { if (this.container && !this.container.isConnected) { this.container = null; this.toasts.clear(); } if (this.container) return; this.container = createToastContainer(); UIHost.add(this.container); } show(message, type = "info", options = {}) { this.init(); const { duration, showClose } = resolveToastDisplayOptions(options); const toast = createToastElement(message, type, duration, showClose, options, (el) => this.remove(el)); log$47.trace(`Showing [${type}]: ${message}`); this.container?.appendChild(toast); this.toasts.add(toast); this.evictOverflowToasts(); if (duration > 0) setTimeout(() => this.remove(toast), duration); if (options.system) BrowserService.notify(message, void 0, void 0, options.onClick); return toast; } remove(toast) { if (!this.toasts.has(toast)) return; this.toasts.delete(toast); toast.classList.add("hiding"); const onEnd = () => toast.remove(); const fallbackTimer = setTimeout(onEnd, TIMING.UI_TRANSITION_SLOW); toast.addEventListener("animationend", (event) => { if (event.animationName === "fc2-toast-out") { clearTimeout(fallbackTimer); onEnd(); } }, { once: true }); } evictOverflowToasts() { while (this.toasts.size > MAX_TOASTS) { const oldest = this.toasts.values().next().value; if (oldest) this.remove(oldest); } } }; var Toast = exports("T",new ToastManager()); var toastBridged = false; /** Wire toast manager to app events (called once from app/modules). */ function initToastBridge() { if (toastBridged) return; toastBridged = true; CoreEvents.on(AppEvents.SHOW_TOAST, (payload) => { Toast.show(payload.message, payload.type); }); } var BatchSelectionService = class { selectedIds = /* @__PURE__ */ new Set(); detachStateListener = null; onInit() { this.detachStateListener = resolveService(STATE_SERVICE).on("isSelectionMode", (value) => { if (!value) this.clearSelection(); }); } onCleanup() { this.detachStateListener?.(); this.detachStateListener = null; this.clearSelection(); } toggleSelection(id) { if (this.selectedIds.has(id)) this.selectedIds.delete(id); else this.selectedIds.add(id); this.emitUpdate(); } setSelection(id, selected) { if (selected) this.selectedIds.add(id); else this.selectedIds.delete(id); this.emitUpdate(); } isSelected(id) { return this.selectedIds.has(id); } getSelectedIds() { return Array.from(this.selectedIds); } getCount() { return this.selectedIds.size; } clearSelection() { if (this.selectedIds.size === 0) return; this.selectedIds.clear(); this.emitUpdate(); } selectAll(ids) { ids.forEach((id) => this.selectedIds.add(id)); this.emitUpdate(); } emitUpdate() { CoreEvents.emit(AppEvents.SELECTION_CHANGED, { ids: this.getSelectedIds(), count: this.getCount() }); } }; var SelectionService = new BatchSelectionService(); var BatchBar = class { static element = null; static countElement = null; static unsubscribers = []; static init() { this.unsubscribers.forEach((fn) => fn()); this.unsubscribers = []; this.unsubscribers.push(CoreEvents.on(AppEvents.SELECTION_CHANGED, ({ count }) => { this.updateVisibility(count); this.updateCount(count); })); this.unsubscribers.push(resolveService(STATE_SERVICE).on("isSelectionMode", (value) => { if (!value) this.hide(); else this.updateVisibility(SelectionService.getCount()); })); } static updateVisibility(count) { if (resolveService(STATE_SERVICE).proxy.isSelectionMode && count > 0) this.show(); else this.hide(); } static show() { if (!this.element) this.render(); if (this.element) this.element.classList.add("is-visible"); } static hide() { if (this.element) this.element.classList.remove("is-visible"); } static updateCount(count) { if (this.countElement) this.countElement.textContent = t("batchSelectionCount", { count }); } static render() { this.countElement = h("span", { className: "batch-bar-count" }); const actions = h("div", { className: "batch-bar-actions" }, h("button", { className: "batch-btn primary", onclick: () => this.handleMarkViewed(true) }, t("batchActionMarkViewed")), h("button", { className: "batch-btn", onclick: () => this.handleMarkViewed(false) }, t("batchActionMarkUnviewed")), h("button", { className: "batch-btn", onclick: () => this.handleCopyIds() }, t("batchActionCopyIds")), h("button", { className: "batch-btn accent", onclick: () => this.handleExtractMagnets() }, t("batchActionExtractMagnets")), h("button", { className: "batch-btn danger", onclick: () => SelectionService.clearSelection() }, t("batchActionCancel"))); this.element = h("div", { id: "fc2-batch-bar", className: "fc2-batch-bar" }, this.countElement, actions); UIHost.add(this.element); } static async handleMarkViewed(viewed) { const ids = SelectionService.getSelectedIds(); if (ids.length === 0) { Toast.show(t("batchNoneSelected"), "info"); return; } try { if (viewed) await resolveService(HISTORY_SERVICE).addBatch(ids, "watched"); else await resolveService(HISTORY_SERVICE).removeBatch(ids); Toast.show(t("alertSettingsSaved"), "success"); SelectionService.clearSelection(); resolveService(STATE_SERVICE).proxy.isSelectionMode = false; } catch { Toast.show(t("labelError"), "error"); } } static handleCopyIds() { const ids = SelectionService.getSelectedIds(); if (ids.length === 0) { Toast.show(t("batchNoneSelected"), "info"); return; } const text = ids.join("\n"); navigator.clipboard.writeText(text).then(() => { Toast.show(t("tooltipCopied"), "success"); }); } static async handleExtractMagnets() { const ids = SelectionService.getSelectedIds(); if (ids.length === 0) { Toast.show(t("batchNoneSelected"), "info"); return; } const total = ids.length; Toast.show(t("batchExtractProgress", { current: 0, total }), "info"); const magnets = []; let fail = 0; const type = /fc2ppvdb|fd2ppv|fc2cmadb|javfc2|fc2db/i.test(location.hostname) ? "fc2" : "jav"; for (let i = 0; i < ids.length; i++) { const id = ids[i]; try { const url = await resolveService(MAGNET_SERVICE).fetchMagnet(id, type); if (url && url.startsWith("magnet:?")) magnets.push(url); else fail += 1; } catch { fail += 1; } if ((i + 1) % 5 === 0 || i === ids.length - 1) Toast.show(t("batchExtractProgress", { current: i + 1, total }), "info"); } if (magnets.length === 0) { Toast.show(t("batchExtractEmpty"), "warn"); return; } try { await navigator.clipboard.writeText(magnets.join("\n")); if (fail > 0) Toast.show(t("batchExtractPartial", { ok: magnets.length, fail }), "warn"); else Toast.show(t("batchExtractSuccess", { count: magnets.length }), "success"); } catch { Toast.show(t("labelError"), "error"); } } }; var pendingIntent = null; var setPendingSettingsNavigationIntent = (intent) => { pendingIntent = intent ? { ...intent } : null; }; var consumePendingSettingsNavigationIntent = exports("c",(targetTab) => { if (!pendingIntent || pendingIntent.targetTab !== targetTab) return null; const nextIntent = pendingIntent; pendingIntent = null; return nextIntent; }); var resolveSettingsFocusGroupsFromIntent = exports("s",(intent) => { if (!intent || intent.targetTab !== "settings" || intent.presetKey !== "quick-preferences") return []; return [ "appearance", "card-actions", "enhancements" ]; }); var glassEffect = ` backdrop-filter: blur(var(--fc2-blur)); -webkit-backdrop-filter: blur(var(--fc2-blur)); background: var(--fc2-surface-item); border: var(--fc2-border-width) solid var(--fc2-border); `; var focusRing = ` &:focus-visible { outline: calc(var(--fc2-border-width) * 2) solid var(--fc2-primary); outline-offset: var(--fc2-space-xxs); box-shadow: var(--fc2-focus-ring); } `; var rippleEffect = ` position: relative; overflow: hidden; &::after { content: ""; position: absolute; top: 50%; left: 50%; width: 100%; height: 100%; background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); opacity: 0; border-radius: inherit; transform: translate(-50%, -50%) scale(1); pointer-events: none; transition: opacity var(--fc2-motion-slow); } &:active::after { animation: fc2-ripple var(--fc2-motion-slow) var(--fc2-ease-standard); opacity: 1; } `; var minifyCss = (css) => { return css.replace(/\/\*(?![!])[\s\S]*?\*\//g, "").replace(/\n/g, "").replace(/\s+/g, " ").replace(/\s*([{}:;,])\s*/g, "$1").trim(); }; var base = (_C) => ` /* ============================================================ BASE COMPONENTS & RESET ============================================================ */ /* Global Mono Penetration for industrial feel */ .fc2-stat-value, .fc2-id-badge, .fc2-password-wrapper input, #debug-log-list { font-family:var(--fc2-font-mono)!important; letter-spacing:-0.02em; } *,::before,::after { box-sizing:border-box; } .fc2-icon { display:inline-flex; align-items:center; justify-content:center; width:1em; height:1em; vertical-align:-0.125em; } .fc2-icon svg { width:100%; height:100%; fill:currentColor; } .fc2-icon-dropdown-caret { display:inline-flex; margin-left:var(--fc2-space-xs); opacity: var(--fc2-opacity-dim); font-size: var(--fc2-text-xs); width:1em; height:1em; } .fc2-icon-dropdown-caret svg { fill:currentColor; } .fc2-dim { opacity: var(--fc2-opacity-dim); color:var(--fc2-text-muted); } /* ── Utility Classes ───────────────────────────────────────── */ .fc2-mt-sm { margin-top: var(--fc2-space-sm); } .fc2-gap-sm { gap: var(--fc2-space-sm); } `; var actionsheet = (_C) => ` /* ============================================================ ACTION SHEET (MOBILE) ============================================================ */ .fc2-action-sheet-backdrop { position: fixed; inset: 0; z-index: var(--fc2-z-modal); opacity: 0; will-change: opacity, visibility; visibility: hidden; transition: all var(--fc2-motion-base) var(--fc2-ease-standard); pointer-events: auto; } .fc2-action-sheet-backdrop.active { opacity: 1; visibility: visible; } .fc2-action-sheet { position: fixed; left: 0; right: 0; bottom: 0; background: var(--fc2-surface-float); border-top-left-radius: var(--fc2-radius-xl); will-change: transform; border-top-right-radius: var(--fc2-radius-xl); z-index: calc(var(--fc2-z-modal) + 1); transform: translateY(100%); transition: transform var(--fc2-motion-slow) var(--fc2-ease-out); padding-bottom: calc(var(--fc2-space-lg) + env(safe-area-inset-bottom,0px)); border: var(--fc2-border-width) solid var(--fc2-border); font-family: var(--fc2-font); pointer-events: auto; } .fc2-action-sheet.desktop { top: 50%; bottom: auto; left: 50%; right: auto; width: 400px; max-width: 90vw; border-radius: var(--fc2-radius-xl); transform: translate(-50%,-50%) scale(0.9); box-shadow: var(--fc2-shadow); opacity: 0; visibility: hidden; pointer-events: none; transition: all var(--fc2-motion-base) var(--fc2-ease-out); } .fc2-action-sheet.active { transform: translateY(0); } .fc2-action-sheet.desktop.active { transform: translate(-50%,-50%) scale(1); opacity: 1; visibility: visible; pointer-events: auto; } .fc2-action-sheet-header { display: flex; align-items: center; justify-content: space-between; padding: var(--fc2-space-md) var(--fc2-space-lg); border-bottom: var(--fc2-border-width) solid var(--fc2-border); } .fc2-action-sheet-title { font-size: var(--fc2-text-lg) !important; font-weight: var(--fc2-font-semibold) !important; color: var(--fc2-text); } .fc2-action-sheet-close-btn { background: transparent; border: none; color: var(--fc2-text-dim); font-size: var(--fc2-text-xl) !important; cursor: pointer; padding: var(--fc2-space-xs); line-height: 1; transition: color var(--fc2-motion-fast); } .fc2-action-sheet-close-btn:hover { color: var(--fc2-text); } .fc2-action-sheet-close-btn:focus-visible, .fc2-action-sheet-item:focus-visible { outline: none; box-shadow: 0 0 0 calc(var(--fc2-space-xxs) + var(--fc2-border-width)) rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-action-sheet-grid { display: grid; grid-template-columns: repeat(auto-fill,minmax(calc(var(--fc2-space-2xl) + var(--fc2-space-2xl) + var(--fc2-space-sm) + var(--fc2-space-base)),1fr)); gap: var(--fc2-space-base); padding: var(--fc2-space-lg); max-height: 60vh; overflow-y: auto; } .fc2-action-sheet-item { display: flex; flex-direction: column; align-items: center; gap: var(--fc2-space-sm); padding: var(--fc2-space-base); background: linear-gradient(180deg, rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)), var(--fc2-surface-item)); border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-btn-radius); text-decoration: none !important; color: var(--fc2-text-dim) !important; transition: all var(--fc2-motion-fast); text-align: center; box-shadow: inset 0 var(--fc2-border-width) 0 rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); } .fc2-action-sheet-item:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); border-color: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-muted)); color: var(--fc2-text) !important; transform: translateY(calc(-1 * var(--fc2-border-width))); box-shadow: var(--fc2-shadow-md); } .fc2-action-sheet-item .fc2-icon { font-size: var(--fc2-text-xl); margin-bottom: var(--fc2-space-xs); } .fc2-action-sheet-item span: last-child { font-size: var(--fc2-text-xs) !important; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%; } `; var batch = (_C) => ` .fc2-batch-bar { position: fixed; bottom: var(--fc2-space-xl); left: 50%; transform: translateX(-50%) translateY(150%); display: flex; align-items: center; gap: var(--fc2-space-lg); padding: var(--fc2-space-sm) var(--fc2-space-lg); background: var(--fc2-surface-float); border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-radius-full); box-shadow: var(--fc2-shadow-lg); backdrop-filter: blur(var(--fc2-blur)); -webkit-backdrop-filter: blur(var(--fc2-blur)); z-index: var(--fc2-z-modal); transition: transform var(--fc2-motion-slow) var(--fc2-ease-spring), opacity var(--fc2-motion-base); pointer-events: none; opacity: 0; } .fc2-batch-bar.is-visible { transform: translateX(-50%) translateY(0); pointer-events: all; opacity: 1; } .batch-bar-count { font-size: var(--fc2-text-sm); font-weight: var(--fc2-font-bold); color: var(--fc2-primary); padding-right: var(--fc2-space-md); border-right: var(--fc2-border-width) solid var(--fc2-border); } .batch-bar-actions { display: flex; gap: var(--fc2-space-sm); } .batch-btn { height: var(--fc2-btn-sm); padding: 0 var(--fc2-space-md); border-radius: var(--fc2-radius-full); font-size: var(--fc2-text-xs); font-weight: var(--fc2-font-semibold); background: var(--fc2-surface-item); border: var(--fc2-border-width) solid var(--fc2-border); color: var(--fc2-text); cursor: pointer; transition: all var(--fc2-motion-fast) var(--fc2-ease-out); display: flex; align-items: center; justify-content: center; white-space: nowrap; } .batch-btn:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); border-color: var(--fc2-primary); } .batch-btn.primary { background: var(--fc2-primary); color: var(--fc2-on-primary); border-color: var(--fc2-primary); } .batch-btn.accent { background: rgba(var(--fc2-info-rgb), var(--fc2-opacity-muted)); color: var(--fc2-info); border-color: var(--fc2-info); } .batch-btn.danger { color: var(--fc2-danger); } .batch-btn.danger:hover { background: rgba(var(--fc2-danger-rgb), var(--fc2-opacity-subtle)); border-color: var(--fc2-danger); } @media (max-width: 768px) { .fc2-batch-bar { width: 90%; flex-direction: column; border-radius: var(--fc2-radius-lg); padding: var(--fc2-space-md); bottom: calc(var(--fc2-space-md) + env(safe-area-inset-bottom, 0px)); gap: var(--fc2-space-md); } .batch-bar-count { border-right: none; border-bottom: var(--fc2-border-width) solid var(--fc2-border); padding-right: 0; padding-bottom: var(--fc2-space-xs); width: 100%; text-align: center; } .batch-bar-actions { flex-wrap: wrap; justify-content: center; } } `; /** * Button Styles */ var button = (C) => ` /* ============================================================ UNIFIED BUTTON SYSTEM ============================================================ */ .fc2-card-top-right-controls { position: absolute; top: var(--fc2-space-sm); right: var(--fc2-space-sm); z-index: var(--fc2-z-dropdown); display: flex; flex-direction: row; flex-wrap: nowrap; align-items: stretch; ${glassEffect} border-radius: var(--fc2-radius-full); box-shadow: var(--fc2-shadow-sm); overflow: hidden; padding: 0; gap: 0; max-width: calc(100% - (var(--fc2-space-sm) * 2)); } .${C.resourceBtn}, .fc2-btn-actress, .verify-cf-btn { position: relative; display: inline-flex; align-items: center; justify-content: center; min-width: 0; min-height: var(--fc2-btn-sm); background: linear-gradient(180deg, rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)), var(--fc2-surface-item)); color: var(--fc2-text); border: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-muted)); border-radius: var(--fc2-btn-radius); font-size: var(--fc2-text-sm); font-weight: var(--fc2-font-semibold); line-height: 1; text-decoration: none; backdrop-filter: blur(var(--fc2-blur)); -webkit-backdrop-filter: blur(var(--fc2-blur)); cursor: pointer; white-space: nowrap; box-shadow: inset 0 var(--fc2-border-width) 0 rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); transition: background-color var(--fc2-motion-base) var(--fc2-ease-out), color var(--fc2-motion-base) var(--fc2-ease-out), border-color var(--fc2-motion-base) var(--fc2-ease-out), box-shadow var(--fc2-motion-base) var(--fc2-ease-out), transform var(--fc2-motion-fast) var(--fc2-ease-out); } .fc2-card-top-right-controls > * { background: transparent !important; border: none !important; border-radius: 0 !important; box-shadow: none !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; position: relative; display: inline-flex; align-items: center; justify-content: center; color: var(--fc2-text); cursor: pointer; padding: 0 var(--fc2-space-sm); white-space: nowrap; margin: 0 !important; transition: background-color var(--fc2-motion-base) var(--fc2-ease-out); } .fc2-card-top-right-controls > *:not(:last-child) { border-right: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; } .${C.resourceBtn} *, .fc2-fab-btn *, .fc2-fab-trigger *, .fc2-close-btn *, .verify-cf-btn * { pointer-events: none !important; } .${C.resourceBtn}:hover, .fc2-btn-actress:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); color: var(--fc2-text); border-color: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-muted)); } .fc2-card-top-right-controls > *:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; border-color: transparent; transform: translateY(calc(-1 * var(--fc2-border-width))); box-shadow: var(--fc2-shadow-md); } .${C.resourceBtn}:active, .fc2-card-top-right-controls > *:active, .fc2-btn-actress:active { transform: translateY(0) scale(0.98); } /* Focus-visible for keyboard accessibility */ .${C.resourceBtn}:focus-visible, .fc2-card-top-right-controls > *:focus-visible, .fc2-btn-actress:focus-visible, .verify-cf-btn:focus-visible { ${focusRing.replace("&:focus-visible", "")} } /* Micro-Interactions */ .${C.resourceBtn}, .fc2-fab-btn, .fc2-enh-btn, .fc2-btn { ${rippleEffect} } .verify-cf-btn { margin: var(--fc2-space-xs) auto; padding: 0 var(--fc2-space-md); color: var(--fc2-warn) !important; border-color: rgba(var(--fc2-warn-rgb), var(--fc2-opacity-muted)) !important; } .verify-cf-btn:hover { background: rgba(var(--fc2-warn-rgb), var(--fc2-opacity-muted)) !important; color: var(--fc2-text) !important; border-color: var(--fc2-warn) !important; transform: translateY(calc(-1 * var(--fc2-space-xxs))); box-shadow: 0 var(--fc2-space-xs) calc(var(--fc2-space-md) - var(--fc2-border-width)) rgba(var(--fc2-warn-rgb), var(--fc2-opacity-muted)) !important; } .fc2-card-top-right-controls > * { min-width: 0; min-height: var(--fc2-btn-sm); height: var(--fc2-btn-sm); padding: 0 var(--fc2-space-base); font-size: var(--fc2-text-xs); } .${C.resourceBtn} { height: var(--fc2-btn-sm); padding: 0 var(--fc2-space-base); font-size: var(--fc2-text-sm); } @container (max-width: 250px) { .fc2-card-top-right-controls > * { height: calc(var(--fc2-btn-sm) - var(--fc2-space-xs)); padding: 0 calc(var(--fc2-space-sm) - var(--fc2-space-xxs)); font-size: var(--fc2-text-xs); } .fc2-card-top-right-controls { top: calc(var(--fc2-space-sm) - var(--fc2-space-xxs)); right: calc(var(--fc2-space-sm) - var(--fc2-space-xxs)); gap: calc(var(--fc2-space-xs) - var(--fc2-border-width)); } .${C.resourceBtn} { height: var(--fc2-btn-sm); padding: 0 var(--fc2-space-base); font-size: var(--fc2-text-xs); } } @container (min-width: 350px) { .fc2-card-top-right-controls > * { height: calc(var(--fc2-btn-sm) - var(--fc2-space-xxs)); padding: 0 var(--fc2-space-base); font-size: var(--fc2-text-xs); } .${C.resourceBtn} { height: calc(var(--fc2-btn-sm) + var(--fc2-space-xxs)); padding: 0 var(--fc2-space-md); font-size: var(--fc2-text-sm); } } .${C.resourceBtn}.${C.btnMagnet} { font-weight: var(--fc2-font-semibold); margin-left: auto; animation: fc2-magnet-in var(--fc2-motion-slow) var(--fc2-ease-spring); } .fc2-btn-toggle-view.fc2-is-viewed { color: var(--fc2-primary) !important; } .fc2-btn-toggle-view:not(.fc2-is-viewed) .fc2-icon-viewed { display: none; } .fc2-btn-toggle-view:not(.fc2-is-viewed) .fc2-icon-unviewed { display: inline-flex; } .fc2-btn-toggle-view.fc2-is-viewed .fc2-icon-viewed { display: inline-flex; } .fc2-btn-toggle-view.fc2-is-viewed .fc2-icon-unviewed { display: none; } /* 隐藏所有功能按钮的文字以保持图标化风格 */ .${C.processedCard} .${C.resourceBtn} .${C.buttonText}, .fc2-enh-toolbar .${C.resourceBtn} .${C.buttonText} { display: none !important; } /* 确保 ID 徽章文字显示 */ .${C.processedCard} .${C.resourceBtn}.${C.fc2IdBadge} .${C.buttonText}, .fc2-enh-toolbar .${C.resourceBtn}.${C.fc2IdBadge} .${C.buttonText} { display: inline-block !important; } .fc2-enh-toolbar .${C.resourceBtn}.fc2-btn-actress .${C.buttonText} { display: inline-block !important; } .${C.processedCard} .fc2-card-status-slot .${C.resourceBtn} { height: var(--fc2-btn-sm); min-height: var(--fc2-btn-sm); padding: 0 var(--fc2-space-base); font-size: var(--fc2-text-xs); } .${C.processedCard} .fc2-card-primary-actions .${C.resourceBtn}.${C.btnMagnet} { margin-left: 0; justify-content: center; min-width: 0; } .fc2-btn-actress { margin: var(--fc2-space-xs) auto; padding: 0 var(--fc2-space-md); } .${C.fc2IdBadge} { font-family: var(--fc2-font-mono); letter-spacing: var(--fc2-letter-tight); } .${C.fc2IdBadge} .${C.buttonText} { font-weight: var(--fc2-font-semibold); } .${C.fc2IdBadge}.${C.badgeCopied} { background: var(--fc2-success) !important; color: var(--fc2-on-primary) !important; border-color: var(--fc2-success); font-weight: var(--fc2-font-bold) !important; animation: fc2-copy-success var(--fc2-motion-slow) ease; } /* Button Industrial Style Override */ .fc2-btn-industrial { position: relative; overflow: hidden; border: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)) !important; transition: all var(--fc2-motion-fast) var(--fc2-ease-out) !important; } .fc2-btn-industrial:active { transform: scale(0.97) translateY(var(--fc2-space-xxs)) !important; background: rgba(var(--fc2-black-rgb), var(--fc2-opacity-muted)) !important; box-shadow: inset 0 var(--fc2-space-xxs) var(--fc2-space-xs) rgba(var(--fc2-black-rgb), var(--fc2-opacity-muted)) !important; } .fc2-btn-industrial.primary { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; border-color: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-muted)) !important; color: var(--fc2-primary) !important; } .fc2-enh-btn, .fc2-btn { min-height: var(--fc2-btn-md); height: var(--fc2-btn-md); gap: var(--fc2-space-sm); padding: 0 var(--fc2-space-md); background: linear-gradient(180deg, rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)), var(--fc2-surface-item)); color: var(--fc2-text-dim); border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-btn-radius); box-shadow: inset 0 var(--fc2-border-width) 0 rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); font-family: inherit; font-size: var(--fc2-text-sm); font-weight: var(--fc2-font-semibold); cursor: pointer; line-height: 1; transition: background-color var(--fc2-motion-fast) var(--fc2-ease-standard), color var(--fc2-motion-fast) var(--fc2-ease-standard), border-color var(--fc2-motion-fast) var(--fc2-ease-standard), box-shadow var(--fc2-motion-fast) var(--fc2-ease-standard), transform var(--fc2-motion-fast) var(--fc2-ease-standard); display: inline-flex; align-items: center; justify-content: center; white-space: nowrap; min-width: 0; } .fc2-enh-btn .fc2-btn-text, .fc2-btn .fc2-btn-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .fc2-enh-btn:hover, .fc2-btn:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); color: var(--fc2-text); border-color: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-muted)); transform: translateY(calc(-1 * var(--fc2-border-width))); box-shadow: var(--fc2-shadow-md); } .fc2-enh-btn:focus-visible, .fc2-btn:focus-visible { ${focusRing.replace("&:focus-visible", "")} } .fc2-enh-btn.ghost, .fc2-btn.ghost { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); border-color: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); color: var(--fc2-text-dim); } .fc2-enh-btn.ghost:hover, .fc2-btn.ghost:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); color: var(--fc2-text); border-color: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-enh-btn.micro, .fc2-btn.micro { min-height: var(--fc2-btn-sm); height: var(--fc2-btn-sm); padding: 0 var(--fc2-space-md); border-radius: var(--fc2-btn-radius); font-size: var(--fc2-text-xs) !important; font-weight: var(--fc2-font-semibold) !important; } .fc2-enh-btn.icon-only, .fc2-btn.icon-only { width: var(--fc2-btn-md); padding: 0; justify-content: center; } .fc2-enh-btn.active, .fc2-btn.active { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-muted)); color: var(--fc2-primary); border-color: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-muted)); } .fc2-enh-btn:disabled, .fc2-btn:disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; } .fc2-enh-btn.primary, .fc2-btn.primary { background: var(--fc2-primary); color: var(--fc2-on-primary); border-color: transparent; font-weight: var(--fc2-font-bold); } .fc2-enh-btn.primary:hover, .fc2-btn.primary:hover { background: var(--fc2-text); transform: translateY(calc(-1 * var(--fc2-border-width))); box-shadow: 0 var(--fc2-space-base) var(--fc2-btn-sm) calc(-1 * (var(--fc2-space-md) + var(--fc2-space-xxs))) rgba(var(--fc2-primary-rgb), var(--fc2-opacity-dim)); } .fc2-enh-btn.danger, .fc2-btn.danger { color: var(--fc2-danger); border-color: rgba(var(--fc2-danger-rgb), var(--fc2-opacity-muted)); } .fc2-enh-btn.danger:hover, .fc2-btn.danger:hover { background: rgba(var(--fc2-danger-rgb), 0.08); border-color: var(--fc2-danger); } /* Industrial feedback for buttons in panel */ .fc2-enh-settings-panel .fc2-enh-btn, .fc2-enh-settings-panel .fc2-btn { box-shadow: inset 0 var(--fc2-border-width) 0 rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); } .fc2-enh-settings-panel .fc2-enh-btn.primary, .fc2-enh-settings-panel .fc2-btn.primary, .fc2-enh-settings-panel .fc2-enh-btn.danger, .fc2-enh-settings-panel .fc2-btn.danger { position: relative; overflow: hidden; transition: all var(--fc2-motion-fast) var(--fc2-ease-out) !important; } .fc2-enh-settings-panel .fc2-enh-btn.primary:hover, .fc2-enh-settings-panel .fc2-btn.primary:hover { transform: translateY(calc(-1 * var(--fc2-border-width))); box-shadow: 0 var(--fc2-space-base) var(--fc2-space-lg) calc(-1 * (var(--fc2-space-md) + var(--fc2-space-xxs))) rgba(var(--fc2-primary-rgb), var(--fc2-opacity-dim)); } .fc2-enh-settings-panel .fc2-enh-btn.primary:active, .fc2-enh-settings-panel .fc2-btn.primary:active, .fc2-enh-settings-panel .fc2-enh-btn.danger:active, .fc2-enh-settings-panel .fc2-btn.danger:active { transform: scale(0.97) translateY(var(--fc2-border-width)); box-shadow: inset 0 var(--fc2-space-xxs) var(--fc2-space-xs) rgba(var(--fc2-black-rgb), var(--fc2-opacity-muted)); } `; var card = (C) => ` /* ============================================================ CARD SYSTEM ============================================================ */ .${C.cardRebuilt} { position: relative; display: flex; flex-direction: column; align-items: stretch; padding: 0 !important; margin: 0 !important; min-width: 0; animation: fc2-card-reveal var(--fc2-motion-slow) var(--fc2-ease-out) backwards !important; will-change: transform, opacity; container-type: inline-size; container-name: card; transition: none !important; } .${C.cardRebuilt}.fc2-has-active-dropdown { z-index: var(--fc2-z-dropdown) !important; } /* Global Visibility Toggles */ body.fc2-hide-viewed-btn .btn-toggle-view { display: none !important; } body.fc2-hide-id-badge .${C.fc2IdBadge} { display: none !important; } body.fc2-searching .${C.cardRebuilt}:not(.fc2-search-match), body.fc2-searching [data-enh-grid-container] > *:has(.${C.cardRebuilt}:not(.fc2-search-match)) { display: none !important; } body.fc2-searching .${C.cardRebuilt}.fc2-search-match { animation: fc2-fade-in-scale var(--fc2-motion-slow) var(--fc2-ease-out); } .${C.processedCard} { position: relative; display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; height: 100%; background: var(--fc2-surface); border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-radius-lg); overflow: visible; box-shadow: var(--fc2-shadow-sm); transition: transform var(--fc2-motion-slow) var(--fc2-ease-out), box-shadow var(--fc2-motion-slow) var(--fc2-ease-out), border-color var(--fc2-motion-base) var(--fc2-ease-standard); } .${C.processedCard}:hover { transform: translateY(-2px) scale(1.005); border-color: rgba(var(--fc2-primary-rgb), 0.15); box-shadow: var(--fc2-shadow-md); z-index: var(--fc2-z-dropdown); } .${C.processedCard}.fc2-has-active-dropdown, .${C.cardRebuilt}.fc2-has-active-dropdown { z-index: var(--fc2-z-tooltip) !important; overflow: visible !important; } .${C.processedCard}.${C.isViewed} { border-color: var(--fc2-accent) !important; } /* Detail Page Poster Style (Vertical) */ .${C.processedCard}.fc2-is-detail { width: 100% !important; max-width: 1180px !important; height: auto !important; margin-inline: auto; overflow: hidden; box-shadow: var(--fc2-shadow-md); } .${C.processedCard}.fc2-is-detail .${C.videoPreviewContainer} { aspect-ratio: auto !important; height: auto !important; background: transparent !important; border-bottom: var(--fc2-border-width) solid var(--fc2-border); } .${C.processedCard}.fc2-is-detail .${C.videoPreviewContainer} img { position: static !important; display: block !important; width: 100% !important; height: auto !important; object-fit: cover !important; } .${C.processedCard}.fc2-is-detail .${C.infoArea} { gap: var(--fc2-space-sm); padding: var(--fc2-space-md) var(--fc2-space-md) var(--fc2-space-lg) !important; margin-top: 0 !important; background: linear-gradient(180deg, rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)), rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost))) !important; } .${C.processedCard}.fc2-is-detail .${C.resourceLinksContainer} { margin-top: 0 !important; flex-wrap: wrap; } .${C.processedCard}.fc2-is-detail .${C.customTitle} { height: auto !important; min-height: 0 !important; -webkit-line-clamp: 3 !important; margin-bottom: var(--fc2-space-xs) !important; font-size: var(--fc2-text-lg) !important; line-height: var(--fc2-line-tight) !important; } .${C.processedCard}.fc2-is-detail .${C.preservedIconsContainer} { margin-bottom: var(--fc2-space-base); } .${C.processedCard}.fc2-is-detail .${C.cardMeta} { gap: var(--fc2-space-sm); margin-bottom: var(--fc2-space-base); font-size: var(--fc2-text-xs); } .${C.processedCard}.fc2-is-detail .fc2-card-meta-row, .${C.processedCard}.fc2-is-detail .${C.cardActionRow} { gap: var(--fc2-space-base); } .${C.processedCard}.fc2-is-detail .fc2-card-primary-actions, .${C.processedCard}.fc2-is-detail .fc2-card-secondary-actions, .${C.processedCard}.fc2-is-detail .${C.resourceLinksContainer} { min-width: 0; } /* Minimal Card (Collection Tab) */ .${C.processedCard}.fc2-is-minimal { height: auto !important; min-height: 0; padding-top: 0 !important; aspect-ratio: auto !important; background: var(--fc2-surface) !important; border-radius: var(--fc2-radius-md) !important; overflow: hidden; } .${C.processedCard}.fc2-is-minimal .${C.videoPreviewContainer} { display: block !important; } .${C.processedCard}.fc2-is-minimal .fc2-card-top-right-controls { top: var(--fc2-space-xs) !important; right: var(--fc2-space-xs) !important; } .${C.processedCard}.fc2-is-minimal .fc2-card-top-right-controls > * { display: none !important; } .${C.processedCard}.fc2-is-minimal .${C.fc2IdBadge} { display: none !important; } .${C.processedCard}.fc2-is-minimal .${C.infoArea} { padding: var(--fc2-space-xs) var(--fc2-space-sm) var(--fc2-space-sm) !important; background: transparent !important; border-top: none !important; } .${C.processedCard}.fc2-is-minimal .${C.customTitle} { height: auto !important; -webkit-line-clamp: 1 !important; margin-bottom: 0 !important; padding-right: var(--fc2-space-xl) !important; font-size: var(--fc2-text-xs) !important; } .${C.processedCard}.fc2-is-minimal .fc2-card-left-actions { display: none !important; } .${C.videoPreviewContainer} { position: relative; width: 100%; aspect-ratio: 16 / 9; background: var(--fc2-bg); border-top-left-radius: var(--fc2-radius-lg); border-top-right-radius: var(--fc2-radius-lg); overflow: hidden; } .${C.videoPreviewContainer} video, .${C.videoPreviewContainer} img.${C.staticPreview} { width: 100%; height: 100%; object-fit: contain; transition: opacity var(--fc2-motion-base) var(--fc2-ease-out) !important; opacity: 0; position: absolute !important; top: 0 !important; left: 0 !important; transform: translate3d(0,0,0); } .${C.videoPreviewContainer} .fc2-reveal-content:not(.${C.hidden}) { animation: fc2-content-reveal 0.4s var(--fc2-ease-out) forwards !important; } .${C.videoPreviewContainer}:not(.fc2-preview-active) img.${C.staticPreview}:not(.${C.hidden}) { animation: none !important; opacity: 1 !important; filter: none !important; transform: translate3d(0,0,0) !important; } .${C.videoPreviewContainer}:not(.fc2-preview-active) video { opacity: 0 !important; pointer-events: none !important; } .${C.videoPreviewContainer} .${C.hidden} { opacity: 0 !important; pointer-events: none !important; } .${C.processedCard}:hover .${C.videoPreviewContainer} video, .${C.processedCard}:hover .${C.videoPreviewContainer} img.${C.staticPreview} { transform: translate3d(0,0,0) !important; } .${C.previewElement} { position: absolute; top: 0; left: 0; opacity: 1; transition: opacity var(--fc2-motion-slow) var(--fc2-ease-standard); } .${C.previewElement}.${C.hidden} { opacity: 0; pointer-events: none; } /* Pill Action Styles for Play Button */ .${C.resourceLinksContainer} .${C.btnPlayFullscreen}, .fc2-card-primary-actions .${C.btnPlayFullscreen} { color: var(--fc2-success) !important; } .${C.resourceLinksContainer} .${C.btnPlayFullscreen}:hover, .fc2-card-primary-actions .${C.btnPlayFullscreen}:hover { background: rgba(var(--fc2-success-rgb), var(--fc2-opacity-subtle)) !important; } .${C.infoArea} { display: flex; flex-direction: column; justify-content: flex-end; flex-grow: 1; padding: var(--fc2-space-base); background: var(--fc2-surface-item); border-top: var(--fc2-border-width) solid var(--fc2-border); border-bottom-left-radius: var(--fc2-radius-lg); border-bottom-right-radius: var(--fc2-radius-lg); } .${C.customTitle} { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; height: var(--fc2-btn-md); margin: 0 0 var(--fc2-space-sm); color: var(--fc2-text) !important; font-size: var(--fc2-text-sm); font-weight: var(--fc2-font-semibold) !important; line-height: 1.5; text-decoration: none !important; overflow: hidden; transition: color var(--fc2-motion-fast); } .${C.customTitle}:hover { color: var(--fc2-primary) !important; } .${C.preservedIconsContainer} { display: flex; flex-wrap: wrap; align-items: center; gap: var(--fc2-space-xs); margin: 0 0 var(--fc2-space-sm); min-height: 0; } .${C.preservedIconsContainer} > * { flex: 0 0 auto; max-width: 100%; } .fc2-card-meta-row { display: flex; align-items: center; justify-content: space-between; gap: var(--fc2-space-sm); margin-bottom: var(--fc2-space-sm); } .fc2-card-identity-slot, .fc2-card-status-slot, .fc2-card-secondary-actions { display: flex; align-items: center; gap: var(--fc2-space-xs); min-width: 0; } .fc2-card-identity-slot { flex: 1 1 auto; min-width: 0; } .fc2-card-status-slot { flex: 0 0 auto; } .fc2-card-left-actions { display: flex; align-items: center; justify-content: flex-start; gap: var(--fc2-space-xs); margin-top: auto; } .${C.resourceLinksContainer} { display: flex; flex-direction: row; align-items: stretch; justify-content: flex-end; flex-wrap: nowrap; gap: 0 !important; max-width: 100%; min-width: 0; margin-left: auto; background: var(--fc2-surface-item); border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-radius-full); box-shadow: var(--fc2-shadow-sm); backdrop-filter: blur(var(--fc2-blur)); -webkit-backdrop-filter: blur(var(--fc2-blur)); overflow: hidden; padding: 0; height: var(--fc2-btn-sm); /* Default height */ } .${C.resourceLinksContainer} > * { background: transparent !important; border: none !important; border-radius: 0 !important; box-shadow: none !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; position: relative; display: inline-flex; align-items: center; justify-content: center; color: var(--fc2-text) !important; cursor: pointer; padding: 0 var(--fc2-space-sm) !important; white-space: nowrap; margin: 0 !important; height: 100% !important; transition: background-color var(--fc2-motion-base) var(--fc2-ease-out); flex: 0 0 auto; /* Prevent buttons from stretching horizontally */ } .${C.resourceLinksContainer} > *:not(:last-child) { border-right: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; } .${C.resourceLinksContainer} > *:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; } .${C.resourceLinksContainer} .${C.btnMagnet} { color: var(--fc2-primary) !important; animation: none !important; background: transparent !important; } .${C.resourceLinksContainer} .${C.btnMagnet}:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; } /* Visibility Rules are managed globally in base.ts */ .${C.cardMeta} { display: flex; flex-wrap: wrap; gap: var(--fc2-space-xs); margin: 0 0 var(--fc2-space-sm); color: var(--fc2-text-dim); font-size: var(--fc2-text-xs); line-height: 1.4; } .${C.cardMetaItem} { max-width: 100%; padding: var(--fc2-space-xxs) var(--fc2-space-sm); border: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); border-radius: var(--fc2-radius-full); background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .${C.cardActionRow} { display: flex; align-items: center; justify-content: flex-end; gap: var(--fc2-space-sm); margin-top: auto; min-width: 0; } .fc2-card-primary-actions, .fc2-card-left-actions { display: flex; flex-direction: row; align-items: stretch; justify-content: flex-start; flex-wrap: nowrap; gap: 0 !important; flex: 0 1 auto; /* Prevent pill wrapper from stretching */ min-width: 0; margin-left: 0; background: var(--fc2-surface-item); border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-radius-full); box-shadow: var(--fc2-shadow-sm); backdrop-filter: blur(var(--fc2-blur)); -webkit-backdrop-filter: blur(var(--fc2-blur)); overflow: hidden; padding: 0; height: var(--fc2-btn-sm); } .fc2-card-primary-actions:empty, .fc2-card-left-actions:empty, .fc2-resource-links-container:empty { display: none !important; } .fc2-card-primary-actions > *, .fc2-card-left-actions > * { background: transparent !important; border: none !important; border-radius: 0 !important; box-shadow: none !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; position: relative; display: inline-flex; align-items: center; justify-content: center; color: var(--fc2-text) !important; cursor: pointer; padding: 0 var(--fc2-space-sm) !important; white-space: nowrap; margin: 0 !important; height: 100% !important; transition: background-color var(--fc2-motion-base) var(--fc2-ease-out); flex: 0 0 auto; } .fc2-card-primary-actions > *:not(:last-child), .fc2-card-left-actions > *:not(:last-child) { border-right: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; } .fc2-card-primary-actions > *:hover, .fc2-card-left-actions > *:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; } .fc2-card-secondary-actions { flex: 0 1 auto; min-width: 0; margin-left: 0; display: flex; gap: 0; } .fc2-card-overflow-actions { margin-left: 0; } .fc2-card-action-stash { display: none !important; } .${C.processedCard}.${C.noMagnet} { border-color: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; } body.fc2-card-density-cover-only .${C.infoArea} { display: none !important; } body.fc2-card-density-cover-only .${C.cardRebuilt} { container-type: unset; } body.fc2-card-density-compact .${C.infoArea} { padding: var(--fc2-space-xs) var(--fc2-space-sm); gap: 0; } body.fc2-card-density-compact .${C.customTitle} { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: var(--fc2-text-xs); line-height: 1.2; -webkit-line-clamp: unset; height: auto; margin-bottom: 0; } body.fc2-card-density-compact .${C.cardMeta}, body.fc2-card-density-compact .fc2-card-status-slot, body.fc2-card-density-compact .fc2-card-primary-actions, body.fc2-card-density-compact .fc2-card-secondary-actions, body.fc2-card-density-compact .${C.resourceLinksContainer} { display: none !important; } body.fc2-card-density-compact .${C.processedCard}:hover .fc2-card-primary-actions, body.fc2-card-density-compact .${C.processedCard}:hover .fc2-card-secondary-actions { display: flex !important; } body.fc2-card-density-compact .${C.processedCard}:hover .${C.cardMeta} { display: flex !important; } body.fc2-card-density-standard .${C.customTitle} { -webkit-line-clamp: 2; height: calc(var(--fc2-btn-md) + var(--fc2-space-xxs)); } body.fc2-card-density-immersive .${C.processedCard} { border-radius: var(--fc2-radius-xl); box-shadow: var(--fc2-shadow-md); } body.fc2-card-density-immersive .${C.processedCard}:hover { transform: translateY(-4px) scale(1.015); box-shadow: var(--fc2-shadow); } body.fc2-card-density-immersive .${C.infoArea} { padding: var(--fc2-space-md); gap: var(--fc2-space-base); background: linear-gradient(180deg, var(--fc2-surface-item), var(--fc2-surface-low)); } body.fc2-card-density-immersive .${C.customTitle} { -webkit-line-clamp: 3; height: auto; min-height: 4.5em; font-size: var(--fc2-text-lg); letter-spacing: var(--fc2-letter-tight); line-height: 1.5; } body.fc2-card-density-immersive .${C.cardMeta} { font-size: var(--fc2-text-xs); margin-bottom: var(--fc2-space-base); } body.fc2-card-density-immersive .${C.resourceBtn} { height: var(--fc2-btn-md); padding: 0 var(--fc2-space-md); font-size: var(--fc2-text-base); } @container card (max-width: 280px) { .${C.infoArea} { padding: var(--fc2-space-xs) var(--fc2-space-sm) !important; } .fc2-card-meta-row, .${C.cardActionRow} { flex-wrap: wrap; gap: var(--fc2-space-xs) !important; } .${C.customTitle} { -webkit-line-clamp: 2; height: auto; min-height: calc(var(--fc2-btn-sm) + var(--fc2-space-xxs)); margin-bottom: var(--fc2-space-xs); font-size: var(--fc2-text-xs); } .fc2-card-primary-actions, .fc2-card-secondary-actions, .${C.resourceLinksContainer} { justify-content: flex-start; margin-left: 0; height: var(--fc2-btn-sm) !important; /* Shrink buttons for narrow cards */ } .fc2-card-primary-actions .${C.btnMagnet} { flex: 1 1 auto; min-width: 0; } } /* Ultra-compact mode for very narrow cards (e.g. 3+ columns on mobile) */ @container card (max-width: 160px) { .${C.infoArea} { padding: var(--fc2-space-xxs) var(--fc2-space-xs) !important; } .${C.customTitle} { -webkit-line-clamp: 2; /* Allow 2 lines even in ultra-compact */ font-size: 11px !important; } /* Keep everything visible, but use wrapping and smaller scales */ .${C.cardMeta}, .fc2-card-status-slot, .fc2-card-primary-actions, .fc2-card-secondary-actions, .${C.resourceLinksContainer} { display: flex !important; flex-wrap: wrap !important; gap: var(--fc2-space-xxs) !important; } .fc2-card-primary-actions, .${C.resourceLinksContainer} { width: 100% !important; height: auto !important; /* Allow growing vertically to fit wrapped buttons */ min-height: var(--fc2-btn-sm); border-radius: var(--fc2-radius-sm) !important; } .fc2-card-primary-actions > *, .${C.resourceLinksContainer} > * { flex: 1 1 auto !important; padding: 0 var(--fc2-space-xs) !important; font-size: 10px !important; } } @media (max-width: 768px) { .${C.processedCard}.fc2-is-detail .${C.infoArea} { padding: var(--fc2-space-base) !important; } .${C.processedCard}.fc2-is-detail .${C.customTitle} { font-size: var(--fc2-text-base) !important; } .${C.processedCard}.fc2-is-detail .fc2-card-meta-row, .${C.processedCard}.fc2-is-detail .${C.cardActionRow} { flex-wrap: wrap; } .${C.processedCard}.fc2-is-detail .fc2-card-primary-actions, .${C.processedCard}.fc2-is-detail .fc2-card-secondary-actions, .${C.processedCard}.fc2-is-detail .${C.resourceLinksContainer} { justify-content: flex-start; width: 100%; margin-left: 0; } } /* Selection Overlay */ .fc2-card-selection-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: var(--fc2-z-dropdown); transition: background-color var(--fc2-motion-base); border-radius: var(--fc2-radius-lg); } .fc2-selection-mode-active .fc2-card-selection-overlay { pointer-events: all; cursor: pointer; } .fc2-selection-mode-active:hover .fc2-card-selection-overlay { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); } .fc2-is-selected .fc2-card-selection-overlay { background: rgba(var(--fc2-primary-rgb), 0.15); box-shadow: inset 0 0 0 2px var(--fc2-primary); } .fc2-card-selection-checkbox { position: absolute; top: var(--fc2-space-sm); left: var(--fc2-space-sm); width: 24px; height: 24px; appearance: none; background: var(--fc2-surface-float); border: 2px solid var(--fc2-border-strong); border-radius: var(--fc2-radius-sm); cursor: pointer; opacity: 0; transform: scale(0.8); transition: all var(--fc2-motion-base) var(--fc2-ease-spring); display: flex; align-items: center; justify-content: center; margin: 0; box-shadow: var(--fc2-shadow-sm); } .fc2-selection-mode-active .fc2-card-selection-checkbox { opacity: 1; transform: scale(1); } .fc2-card-selection-checkbox:checked { background: var(--fc2-primary); border-color: var(--fc2-primary); } .fc2-card-selection-checkbox:checked::after { content: ""; width: 6px; height: 12px; border: solid var(--fc2-on-primary); border-width: 0 2.5px 2.5px 0; transform: rotate(45deg); margin-bottom: 2px; } .fc2-selection-mode-active .${C.infoArea} { pointer-events: none; } /* ============================================================ ACTRESS AVATAR & PROFILE ============================================================ */ .fc2-actress-unit { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 0 10px; transition: transform var(--fc2-motion-fast) var(--fc2-ease-standard), opacity var(--fc2-motion-fast) var(--fc2-ease-standard); } .fc2-actress-unit-linkable { cursor: pointer; } .fc2-actress-avatar-original { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; border: calc(var(--fc2-border-width) * 2) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-muted)); box-shadow: var(--fc2-shadow-md); background-color: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-actress-avatar-hidden { display: none; } .fc2-actress-name-original { margin-top: var(--fc2-space-sm); font-size: var(--fc2-text-base); font-weight: var(--fc2-font-bold); color: var(--fc2-text-bright); text-align: center; text-shadow: var(--fc2-text-shadow); max-width: calc(var(--fc2-space-2xl) + var(--fc2-space-2xl) + 10px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .fc2-actress-profile-badge-original { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: center; gap: var(--fc2-space-sm); margin-bottom: var(--fc2-space-md); } .fc2-health-indicator { margin-left: calc(var(--fc2-space-sm) - var(--fc2-space-xxs)); font-size: calc(var(--fc2-text-xs) - var(--fc2-space-xxs)); display: flex; } .fc2-health-indicator--recent { color: var(--fc2-success); opacity: 1; } .fc2-health-indicator--stale { color: var(--fc2-text-dim); opacity: 0.5; } `; var common = (_C) => ` /* ============================================================ GLOBAL ANIMATIONS (OVERWRITES OR SPECIALS) ============================================================ */ @keyframes fc2-dropdown-in { from { transform: translateY(calc(-1 * var(--fc2-space-base))); opacity: 0; } to { transform: translateY(0); opacity: 1; } } /* ============================================================ DROPDOWN & TOOLTIP ============================================================ */ .fc2-enh-dropdown { position: relative; display: inline-flex; } .fc2-enh-dropdown-content { position: absolute; top: calc(100% + var(--fc2-space-sm)); right: 0; z-index: var(--fc2-z-dropdown); display: none; flex-direction: column; gap: var(--fc2-space-xs); min-width: calc(var(--fc2-space-2xl) + var(--fc2-space-2xl) + var(--fc2-space-lg) + var(--fc2-space-lg) + var(--fc2-space-xs) + var(--fc2-space-xxs)); padding: var(--fc2-space-sm); background: rgba(var(--fc2-black-rgb), var(--fc2-opacity-dim)); border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-radius-md); box-shadow: var(--fc2-shadow-lg); backdrop-filter: blur(var(--fc2-blur)); -webkit-backdrop-filter: blur(var(--fc2-blur)); animation: fc2-dropdown-in var(--fc2-motion-fast) var(--fc2-ease-standard); } .fc2-enh-dropdown.active .fc2-enh-dropdown-content { display: flex; } .fc2-resource-btn .fc2-tooltip { display: none; position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%) translateY(calc(-1 * var(--fc2-space-sm))); padding: var(--fc2-space-xs) var(--fc2-space-sm); background: var(--fc2-surface-float); color: var(--fc2-text); border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-radius-sm); font-size: var(--fc2-text-xs); white-space: nowrap; pointer-events: none; opacity: 0; transition: opacity var(--fc2-motion-fast); z-index: var(--fc2-z-tooltip); } .fc2-resource-btn:hover .fc2-tooltip { display: block; opacity: 1; } /* ============================================================ SKELETON & LOADING ============================================================ */ .fc2-skeleton { position: relative; overflow: hidden !important; background: var(--fc2-surface-low); border-radius: var(--fc2-radius-sm); } .fc2-skeleton::after { content:''; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)) 20%, rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) 50%, rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)) 80%, transparent); background-size: 200% 100%; animation: fc2-shimmer 2.5s infinite linear; } /* ============================================================ LOADING STATES ============================================================ */ .fc2-loading-indicator { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: var(--fc2-space-2xl); gap: var(--fc2-space-md); } .fc2-loading-indicator .fc2-spinner { width: var(--fc2-space-2xl); height: var(--fc2-space-2xl); border: calc(var(--fc2-border-width) * 3) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-muted)); border-top-color: var(--fc2-primary); border-radius: 50%; animation: fc2-spin 0.8s linear infinite; } .fc2-loading-indicator-text { color: var(--fc2-text-dim); font-size: var(--fc2-text-base); } .fc2-progress-bar { width: 100%; height: var(--fc2-space-xs); background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); border-radius: var(--fc2-radius-sm); overflow: hidden; } .fc2-progress-fill { height: 100%; background: var(--fc2-primary); transition: width var(--fc2-motion-base) var(--fc2-ease-standard); width: var(--fc2-progress, 0%); } .fc2-fade-out { opacity: 0; transition: opacity var(--fc2-motion-base) var(--fc2-ease-standard); } `; var fab = (_C) => ` /* ============================================================ ELEGANT FAB (UNIFIED) ============================================================ */ .fc2-fab-container { position: fixed; bottom: var(--fc2-space-lg); right: var(--fc2-space-lg); z-index: var(--fc2-z-fab); display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: var(--fc2-space-base); pointer-events: none; max-height: 90vh; } .fc2-fab-trigger, .fc2-fab-actions { pointer-events: auto; } .fc2-fab-trigger { display: flex; align-items: center; justify-content: center; width: calc(var(--fc2-space-lg) + var(--fc2-space-lg)); height: calc(var(--fc2-space-lg) + var(--fc2-space-lg)); background: var(--fc2-accent-grad); color: var(--fc2-text); border: var(--fc2-liquid-border); border-radius: var(--fc2-radius-full); box-shadow: var(--fc2-shadow),var(--fc2-rim-light); font-size: var(--fc2-text-xl); cursor: pointer; transition: all var(--fc2-motion-base) var(--fc2-ease-out); touch-action: none; -webkit-tap-highlight-color: transparent; will-change: transform; backdrop-filter: blur(var(--fc2-blur)); } .fc2-fab-trigger:hover { transform: scale(1.12); box-shadow: var(--fc2-shadow); } .fc2-fab-trigger:active { transform: scale(0.92); } .fc2-fab-trigger.active { transform: rotate(135deg); background: var(--fc2-primary); color: var(--fc2-on-primary); animation: fc2-float 3s ease-in-out infinite; } .fc2-fab-trigger.active:hover { transform: scale(1.1) rotate(135deg); animation-play-state: paused; } .fc2-fab-actions { display: flex; flex-direction: column; gap: var(--fc2-space-base); opacity: 0; transform: translateY(var(--fc2-space-lg)) scale(0.8); pointer-events: none; transition: all var(--fc2-motion-slow) var(--fc2-ease-out); max-height: 60vh; width: calc(var(--fc2-space-2xl) + var(--fc2-space-lg)); overflow-y: auto !important; overflow-x: hidden; padding: var(--fc2-space-sm); margin-bottom: var(--fc2-space-sm); background: var(--fc2-liquid-bg); backdrop-filter: blur(var(--fc2-blur)) saturate(var(--fc2-glass-saturate)); -webkit-backdrop-filter: blur(var(--fc2-blur)) saturate(var(--fc2-glass-saturate)); border: var(--fc2-liquid-border); box-shadow: var(--fc2-shadow),var(--fc2-rim-light); border-radius: var(--fc2-radius-lg); scrollbar-width: thin; scrollbar-color: var(--fc2-primary) transparent; touch-action: pan-y; position: relative; } .fc2-fab-actions::before { content: ""; position: absolute; inset: 0; background: var(--fc2-glass-noise); opacity: 0.05; pointer-events: none; z-index: 0; } .fc2-fab-actions::-webkit-scrollbar { width: calc(var(--fc2-space-sm) - var(--fc2-space-xxs)); display: block !important; } .fc2-fab-actions::-webkit-scrollbar-track { background: transparent; } .fc2-fab-actions::-webkit-scrollbar-thumb { background-color: var(--fc2-primary); border-radius: var(--fc2-radius-full); border: var(--fc2-border-width) solid var(--fc2-border); } .fc2-fab-actions.visible { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; } .fc2-fab-actions.visible .fc2-fab-btn:nth-child(1) { transition-delay: 0.05s; } .fc2-fab-actions.visible .fc2-fab-btn:nth-child(2) { transition-delay: 0.1s; } .fc2-fab-actions.visible .fc2-fab-btn:nth-child(3) { transition-delay: 0.15s; } .fc2-fab-actions.visible .fc2-fab-btn:nth-child(4) { transition-delay: 0.2s; } .fc2-fab-actions.visible .fc2-fab-btn:nth-child(5) { transition-delay: 0.25s; } .fc2-fab-btn { position: relative; display: flex; align-items: center; justify-content: center; width: var(--fc2-btn-lg); height: var(--fc2-btn-lg); background: var(--fc2-surface-item); color: var(--fc2-text-dim); border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-radius-full); font-size: var(--fc2-text-lg); cursor: pointer; transition: all var(--fc2-motion-base) var(--fc2-ease-out); -webkit-tap-highlight-color: transparent; z-index: var(--fc2-z-fab); } .fc2-fab-btn:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); color: var(--fc2-text); border-color: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-muted)); transform: scale(1.15); box-shadow: var(--fc2-shadow-md); } .fc2-fab-btn:focus-visible, .fc2-fab-trigger:focus-visible { outline: none; box-shadow: 0 0 0 calc(var(--fc2-space-xxs) + var(--fc2-border-width)) rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-fab-btn.active { background: var(--fc2-primary); color: var(--fc2-on-primary); border-color: transparent; box-shadow: 0 0 calc(var(--fc2-space-sm) + var(--fc2-space-base)) rgba(var(--fc2-primary-rgb), var(--fc2-opacity-dim)); } .fc2-fab-btn::before { content: attr(data-title); position: absolute; right: calc(var(--fc2-space-2xl) + var(--fc2-space-base)); top: 50%; visibility: hidden; padding: var(--fc2-space-xs) var(--fc2-space-sm); background: rgba(var(--fc2-black-rgb), var(--fc2-opacity-strong)); color: var(--fc2-on-primary); border-radius: var(--fc2-radius-sm); font-size: var(--fc2-text-xs); white-space: nowrap; opacity: 0; backdrop-filter: blur(var(--fc2-blur)); transform: translateY(-50%) translateX(var(--fc2-space-xs)); transition: all var(--fc2-motion-fast); pointer-events: none; } .fc2-fab-btn:hover::before { visibility: visible; opacity: 1; transform: translateY(-50%) translateX(0); } .fc2-sync-dot { position: absolute; top: calc(-1 * var(--fc2-space-xxs)); right: calc(-1 * var(--fc2-space-xxs)); width: var(--fc2-btn-sm); height: var(--fc2-btn-sm); background: var(--fc2-text-muted); border: var(--fc2-border-width) solid var(--fc2-bg); border-radius: var(--fc2-radius-full); transition: all var(--fc2-motion-base); } .fc2-sync-dot.syncing { background: var(--fc2-info); animation: fc2-pulse-sync 1.5s infinite; } .fc2-sync-dot.success { background: var(--fc2-success); } .fc2-sync-dot.error { background: var(--fc2-danger); } .fc2-sync-dot.conflict { background: var(--fc2-warn); } `; var form = (_C) => ` /* ============================================================ FORM ELEMENTS (GLASSMORPHISM) ============================================================ */ .fc2-enh-form-row { display: flex; flex-direction: column; align-items: stretch; margin-bottom: 0; padding: var(--fc2-space-sm) var(--fc2-space-md); border-bottom: var(--fc2-border-width) solid var(--fc2-border); transition: background-color var(--fc2-motion-fast); min-height: 0; } .fc2-enh-form-row:last-child { border-bottom: none; } .fc2-enh-form-row:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); } .fc2-enh-label { display: block; margin-bottom: var(--fc2-space-sm); color: var(--fc2-text); font-size: var(--fc2-text-xs); font-weight: var(--fc2-font-semibold); letter-spacing: 0.02em; opacity: var(--fc2-opacity-dim); } .fc2-enh-select, .fc2-enh-input, .fc2-enh-textarea { width: 100%; padding: var(--fc2-space-sm) var(--fc2-space-base); background: rgba(var(--fc2-black-rgb), var(--fc2-opacity-muted)) !important; color: var(--fc2-text) !important; border: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); border-radius: var(--fc2-radius-sm); font-family: inherit; font-size: var(--fc2-text-sm); font-weight: var(--fc2-font-medium); outline: none; transition: all var(--fc2-motion-fast) var(--fc2-ease-out); appearance: none; -webkit-appearance: none; } .fc2-enh-select option { background: var(--fc2-bg); color: var(--fc2-text); padding: var(--fc2-space-sm); } .fc2-enh-select option: checked, .fc2-enh-select option: hover { background: var(--fc2-primary) !important; color: var(--fc2-on-primary) !important; } .fc2-enh-select:focus, .fc2-enh-input:focus, .fc2-enh-textarea:focus { border-color: var(--fc2-primary); background: var(--fc2-surface) !important; box-shadow: 0 0 0 calc(var(--fc2-space-xxs) + var(--fc2-border-width)) rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-enh-checkbox-label { display: flex; align-items: center; gap: var(--fc2-space-sm); cursor: pointer; user-select: none; padding: var(--fc2-space-xs) 0; } .fc2-enh-form-row input[type="checkbox"] { position: relative; appearance: none; -webkit-appearance: none; width: 1.25rem; height: 1.25rem; background: var(--fc2-surface-lowest); border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-radius-sm); cursor: pointer; transition: all var(--fc2-motion-fast) var(--fc2-ease-out); display: flex; align-items: center; justify-content: center; } .fc2-enh-form-row input[type="checkbox"]:focus-visible { outline: none; box-shadow: 0 0 0 calc(var(--fc2-space-xxs) + var(--fc2-border-width)) rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-enh-form-row input[type="checkbox"]:checked { background: var(--fc2-primary); border-color: var(--fc2-primary); } .fc2-enh-form-row input[type="checkbox"]::after { content: ""; width: var(--fc2-space-sm); height: var(--fc2-space-sm); background-color: var(--fc2-liquid-bg); clip-path: polygon(14% 44%,0 65%,50% 100%,100% 16%,80% 0%,43% 62%); transform: scale(0); transition: transform var(--fc2-motion-fast) var(--fc2-ease-spring); } .fc2-enh-form-row input[type="checkbox"]:checked::after { transform: scale(1); } .fc2-enh-checkbox-text { font-size: var(--fc2-text-sm); color: var(--fc2-text); font-weight: var(--fc2-font-semibold); opacity: var(--fc2-opacity-dim); } /* Range Input */ .fc2-enh-range { -webkit-appearance: none; width: 100%; height: var(--fc2-space-xs); background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); border-radius: var(--fc2-radius-sm); outline: none; } .fc2-enh-range::-webkit-slider-thumb { -webkit-appearance: none; width: var(--fc2-text-base); height: var(--fc2-text-base); border-radius: var(--fc2-radius-full); background: var(--fc2-primary); cursor: pointer; box-shadow: 0 0 0 var(--fc2-space-xxs) var(--fc2-bg); } .fc2-enh-form-row.checkbox { flex-direction: row; justify-content: flex-start; align-items: center; gap: var(--fc2-space-base); padding: var(--fc2-space-sm) var(--fc2-space-md); margin: 0; background: transparent; border: none; border-bottom: var(--fc2-border-width) solid var(--fc2-border); border-radius: 0; transition: all var(--fc2-motion-fast) var(--fc2-ease-out); cursor: pointer; width: 100%; box-sizing: border-box; } .fc2-enh-form-row.checkbox:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-enh-form-row.checkbox:has(input[type="checkbox"]:checked) { background: rgba(var(--fc2-primary-rgb), 0.02); } .fc2-enh-form-row.checkbox:has(input[type="checkbox"]:checked) .fc2-enh-checkbox-text { opacity: 1; font-weight: var(--fc2-font-semibold); color: var(--fc2-text) !important; } .fc2-input-group { display: flex; gap: var(--fc2-space-sm); align-items: center; width: 100%; } .fc2-input-group .fc2-enh-input { flex: 1; min-width: 0; } .fc2-input-toggle { display: inline-flex; align-items: center; justify-content: center; width: var(--fc2-btn-md); height: var(--fc2-btn-md); flex-shrink: 0; padding: 0; background: var(--fc2-surface-item); color: var(--fc2-text-dim); border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-radius-sm); cursor: pointer; transition: all var(--fc2-motion-fast) var(--fc2-ease-standard); } .fc2-input-toggle:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); color: var(--fc2-text); } .fc2-input-toggle:focus-visible { outline: none; box-shadow: 0 0 0 calc(var(--fc2-space-xxs) + var(--fc2-border-width)) rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-input-toggle svg { width: var(--fc2-text-base); height: var(--fc2-text-base); fill: currentColor; } /* Hide Native Edge/IE reveal icons as we use our own */ input[type="password"]::-ms-reveal, input[type="password"]::-ms-clear { display: none !important; } `; var gallery = (_C) => ` /* ============================================================ GALLERY & VIEWER ============================================================ */ .fc2-enh-viewer-backdrop { position:fixed; inset:0; z-index:var(--fc2-z-modal); display:flex; flex-direction:column; background:var(--fc2-surface-float); backdrop-filter:blur(calc(var(--fc2-blur) * 2)) saturate(var(--fc2-glass-saturate)); -webkit-backdrop-filter:blur(calc(var(--fc2-blur) * 2)) saturate(var(--fc2-glass-saturate)); pointer-events: auto; } .fc2-enh-viewer-stage { position:relative; display:flex; flex:1; align-items:center; justify-content:center; overflow:hidden; padding:var(--fc2-space-md); width: 100%; } .fc2-enh-viewer-stage.zoomed { overflow:auto; align-items:flex-start; justify-content:flex-start; } .fc2-enh-viewer-stage img, .fc2-enh-viewer-stage video { max-width:100%; max-height:100%; object-fit:contain; border-radius:var(--fc2-radius-lg); box-shadow:var(--fc2-shadow); transition:transform var(--fc2-motion-base) var(--fc2-ease-out); will-change:transform; } .fc2-enh-viewer-stage-image { cursor:zoom-in; width:100%; height:100%; object-fit:contain; } .fc2-enh-viewer-stage-image.zoomed { cursor:zoom-out; max-width:none; max-height:none; width:auto; height:auto; margin:auto; transform:scale(1); } .slide-next img, .slide-next video { animation: fc2-slide-right-in var(--fc2-motion-base) var(--fc2-ease-out); } .slide-prev img, .slide-prev video { animation: fc2-slide-left-in var(--fc2-motion-base) var(--fc2-ease-out); } .slide-init img, .slide-init video { animation: fc2-fade-in var(--fc2-motion-base) var(--fc2-ease-out); } .fc2-enh-viewer-nav { position:absolute; top:50%; transform:translateY(-50%); z-index:var(--fc2-z-toast); width:calc(var(--fc2-space-lg) + var(--fc2-space-lg)); height:calc(var(--fc2-space-lg) + var(--fc2-space-lg)); display:flex; align-items:center; justify-content:center; background:rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); border-radius: var(--fc2-radius-full); cursor:pointer; transition:all var(--fc2-motion-fast); color: var(--fc2-text); } .fc2-enh-viewer-nav:hover { background:rgba(var(--fc2-primary-rgb), var(--fc2-opacity-muted)); transform:translateY(-50%) scale(1.1); } .fc2-enh-viewer-nav.prev { left:calc(var(--fc2-space-sm) + var(--fc2-space-base)); } .fc2-enh-viewer-nav.next { right:calc(var(--fc2-space-sm) + var(--fc2-space-base)); } .fc2-enh-viewer-nav-icon { transform:scale(1.5); } .fc2-enh-viewer-close { position:absolute; top:calc(var(--fc2-space-sm) + var(--fc2-space-base)); right:calc(var(--fc2-space-sm) + var(--fc2-space-base)); z-index:var(--fc2-z-toast); width:var(--fc2-space-2xl); height:var(--fc2-space-2xl); display:flex; align-items:center; justify-content:center; background:rgba(var(--fc2-black-rgb),var(--fc2-opacity-muted)); border-radius: var(--fc2-radius-full); cursor:pointer; color:var(--fc2-text); transition:background var(--fc2-motion-fast); } .fc2-enh-viewer-close:hover { background:rgba(var(--fc2-black-rgb),var(--fc2-opacity-dim)); } .fc2-enh-viewer-actions { position:absolute; bottom:calc(var(--fc2-space-2xl) + var(--fc2-space-2xl) + var(--fc2-space-sm) + var(--fc2-space-base)); right:calc(var(--fc2-space-sm) + var(--fc2-space-base)); z-index:var(--fc2-z-toast); display:flex; flex-direction:column; gap:var(--fc2-space-base); } .fc2-enh-viewer-action { width:var(--fc2-space-2xl); height:var(--fc2-space-2xl); border-radius: var(--fc2-radius-full); background:rgba(var(--fc2-black-rgb), var(--fc2-opacity-muted)); border: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); color:var(--fc2-text); display:flex; align-items:center; justify-content:center; cursor:pointer; transition:all var(--fc2-motion-fast); } .fc2-enh-viewer-action:hover { background:var(--fc2-primary); color:var(--fc2-on-primary); } .fc2-enh-viewer-action.fc2-hidden { display:none; } .fc2-enh-viewer-counter { position:absolute; top:calc(var(--fc2-space-sm) + var(--fc2-space-base)); left:calc(var(--fc2-space-sm) + var(--fc2-space-base)); z-index:var(--fc2-z-toast); background:rgba(var(--fc2-black-rgb),var(--fc2-opacity-dim)); padding: var(--fc2-space-xs) var(--fc2-space-base); border-radius:var(--fc2-radius-xl); color:var(--fc2-text); font-size: var(--fc2-text-base); } .fc2-enh-viewer-counter-current { color:var(--fc2-text-bright); font-weight:var(--fc2-font-bold); } .fc2-enh-viewer-counter-separator { opacity:var(--fc2-opacity-dim); margin:0 var(--fc2-space-xs); } .fc2-enh-viewer-thumbs { height:calc(var(--fc2-space-2xl) + var(--fc2-space-2xl)); width:100%; display:flex; gap:var(--fc2-space-sm); padding:var(--fc2-space-sm) calc(var(--fc2-space-sm) + var(--fc2-space-base)); overflow-x:auto; background:rgba(var(--fc2-black-rgb),var(--fc2-opacity-muted)); z-index:var(--fc2-z-toast); } .fc2-enh-viewer-single .pb-play, .fc2-enh-viewer-single .fc2-enh-viewer-counter, .fc2-enh-viewer-single .fc2-enh-viewer-thumbs { display:none; } .fc2-enh-thumb-item { height:100%; aspect-ratio:16/9; border-radius:var(--fc2-radius-sm); overflow:hidden; cursor:pointer; opacity:var(--fc2-opacity-dim); transition:opacity var(--fc2-motion-fast); border:calc(var(--fc2-border-width) * 2) solid transparent; flex-shrink:0; } .fc2-enh-thumb-item.active { opacity:1; border-color:var(--fc2-primary); } .fc2-enh-thumb-item img, .fc2-enh-thumb-item video { width:100%; height:100%; object-fit:cover; } /* ============================================================ PREVIEW STATES (loading, error, active) ============================================================ */ .fc2-enh-viewer-stage-media { width:100%; height:100%; object-fit:contain; border-radius:var(--fc2-radius-md); } .preview-loading { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; background:linear-gradient(180deg, var(--fc2-surface-low), var(--fc2-surface-float)); z-index:var(--fc2-z-tooltip); } .preview-spinner { width:var(--fc2-space-2xl); height:var(--fc2-space-2xl); border:calc(var(--fc2-border-width) * 3) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-muted)); border-top-color:var(--fc2-text-bright); border-radius:50%; animation:fc2-spin 0.8s linear infinite; } .preview-progress { margin-top:10px; color:var(--fc2-text-bright); font-size:var(--fc2-text-xs); text-align:center; } .preview-error { position:absolute; top:50%; left:50%; transform:translate(-50%, -50%); color:var(--fc2-danger); font-size:var(--fc2-text-xs); text-align:center; z-index:var(--fc2-z-tooltip); } .preview-error-icon { font-size:var(--fc2-space-lg); margin-bottom:var(--fc2-space-xs); display:inline-block; } `; var modal = (_C) => ` /* ============================================================ MODAL & SETTINGS PANEL ============================================================ */ .fc2-enh-modal-backdrop { position: absolute; inset: 0; z-index: var(--fc2-z-modal); background: rgba(var(--fc2-black-rgb), var(--fc2-opacity-muted)); transition: background-color var(--fc2-motion-base) var(--fc2-ease-standard), backdrop-filter var(--fc2-motion-base) var(--fc2-ease-standard); backdrop-filter: blur(var(--fc2-blur)); -webkit-backdrop-filter: blur(var(--fc2-blur)); pointer-events: auto; } .fc2-enh-modal-panel { position: fixed; top: 50%; left: 50%; z-index: var(--fc2-z-modal); background-color: var(--fc2-liquid-bg); color: var(--fc2-text); border: var(--fc2-border-width) solid transparent; background-clip: padding-box,border-box; background-origin: padding-box,border-box; background-image: linear-gradient(to bottom,transparent,transparent), var(--fc2-liquid-iridescent); border-radius: var(--fc2-radius-lg); box-shadow: var(--fc2-rim-light), var(--fc2-shadow); overflow: hidden; animation: fc2-pop-in var(--fc2-motion-slow) var(--fc2-ease-out); transform: translate(-50%,-50%); will-change: transform; pointer-events: auto; } .fc2-enh-settings-panel { width: min(1180px, calc(100vw - var(--fc2-space-xl))); height: min(900px, calc(100vh - var(--fc2-space-xl))); height: min(900px, calc(100dvh - var(--fc2-space-xl))); display: flex; flex-direction: column; overflow: hidden; position: relative; z-index: var(--fc2-z-modal) !important; background: var(--fc2-liquid-bg); backdrop-filter: none; -webkit-backdrop-filter: none; box-shadow: inset 0 var(--fc2-border-width) 0 rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)), var(--fc2-shadow); pointer-events: auto; border-radius: var(--fc2-radius-lg); border: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-enh-settings-header { display: flex; align-items: center; justify-content: space-between; gap: var(--fc2-space-md); padding: var(--fc2-space-md) var(--fc2-space-lg); background: linear-gradient(180deg, rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)), rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost))); border-bottom: var(--fc2-border-width) solid var(--fc2-border); flex-shrink: 0; z-index: var(--fc2-z-modal); } .fc2-enh-settings-heading { display: flex; align-items: center; min-width: 0; } .fc2-enh-settings-header h2 { margin: 0; font-size: var(--fc2-text-lg); font-weight: var(--fc2-font-bold); line-height: var(--fc2-line-tight); letter-spacing: 0.01em; } .fc2-header-actions { display: flex; align-items: center; gap: var(--fc2-space-sm); flex-shrink: 0; } .fc2-enh-settings-body { display: flex; flex: 1; min-height: 0; overflow: hidden; } .fc2-enh-settings-content-shell { display: flex; flex: 1; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; } .fc2-enh-settings-content { position: relative; flex: 1; min-height: 0; padding: 0; overflow: hidden; background: linear-gradient(180deg, rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)), transparent 1180px); display: flex; flex-direction: column; } .fc2-close-btn { display: inline-flex; align-items: center; justify-content: center; width: var(--fc2-space-2xl); height: var(--fc2-space-2xl); padding: 0; background: transparent; color: var(--fc2-text-dim); border: var(--fc2-border-width) solid transparent; border-radius: var(--fc2-radius-sm); cursor: pointer; transition: background-color var(--fc2-motion-fast) var(--fc2-ease-standard), color var(--fc2-motion-fast) var(--fc2-ease-standard), border-color var(--fc2-motion-fast) var(--fc2-ease-standard), box-shadow var(--fc2-motion-fast) var(--fc2-ease-standard); } .fc2-close-btn:hover { background: var(--fc2-surface-item); color: var(--fc2-text); border-color: var(--fc2-border); } .fc2-close-btn:focus-visible { outline: none; box-shadow: var(--fc2-focus-ring); } .fc2-loading-overlay { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--fc2-space-base); height: 100%; min-height: 200px; color: var(--fc2-text-dim); } .fc2-loading-spinner { width: var(--fc2-space-xl); height: var(--fc2-space-xl); border: var(--fc2-border-width) solid var(--fc2-border); border-top-color: var(--fc2-primary); border-radius: var(--fc2-radius-full); animation: fc2-spin 0.8s linear infinite; } .fc2-loading-text { font-size: var(--fc2-text-sm) !important; opacity: 0.6; } .fc2-error-state { display: flex; align-items: center; justify-content: center; height: 100%; min-height: 200px; color: var(--fc2-danger); font-size: var(--fc2-text-base) !important; } @media(max-width:768px) { .fc2-enh-settings-body { flex-direction: column; } .fc2-enh-settings-content-shell { order: 1; } .fc2-enh-settings-panel { width: 100vw; height: 100vh; height: 100dvh; border-radius: 0; border: none; } .fc2-enh-settings-content { overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; } .fc2-enh-settings-header { padding: 0.85rem 1rem; } } `; var settings = (_C) => ` /* ============================================================ SETTINGS GROUPS & PORTALS ============================================================ */ .fc2-panel-card { display: flex; flex-direction: column; gap: 0; padding: 0; background: var(--fc2-surface-item); border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-radius-xl); box-shadow: var(--fc2-shadow-sm); backdrop-filter: blur(calc(var(--fc2-blur) * 1.5)); -webkit-backdrop-filter: blur(calc(var(--fc2-blur) * 1.5)); transition: all var(--fc2-motion-fast) var(--fc2-ease-standard); height: 100%; position: relative; overflow: hidden; will-change: background-color, border-color, transform; } .fc2-panel-card:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); border-color: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-panel-card-heading { display: flex; align-items: center; gap: var(--fc2-space-sm); margin: 0; padding: var(--fc2-space-base) var(--fc2-space-md); border-bottom: var(--fc2-border-width) solid var(--fc2-border); background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); transition: background-color var(--fc2-motion-fast); } .fc2-panel-card-heading .fc2-icon { font-size: var(--fc2-text-lg); opacity: var(--fc2-opacity-dim); color: var(--fc2-text); } .fc2-card-title { font-size: var(--fc2-text-sm); font-weight: var(--fc2-font-semibold); color: var(--fc2-text); opacity: var(--fc2-opacity-full); } .fc2-card-subtitle { font-size: var(--fc2-text-xs) !important; color: var(--fc2-text-muted); margin-left: auto; opacity: var(--fc2-opacity-dim); white-space: nowrap; } .fc2-panel-card-body { display: flex; flex-direction: column; padding: 0; margin: 0; background: transparent; } .fc2-portal-grid { display: grid; grid-template-columns: repeat(auto-fill,minmax(180px,1fr)); gap: 0; width: 100%; box-sizing: border-box; } .fc2-portal-item { display: flex; align-items: center; gap: var(--fc2-space-base); padding: var(--fc2-space-sm) var(--fc2-space-md); border: none; border-bottom: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); border-radius: 0; cursor: pointer; transition: all var(--fc2-motion-base) var(--fc2-ease-out); background: transparent; } .fc2-portal-item:last-child { border-bottom: none; } .fc2-portal-item span { color: var(--fc2-text) !important; font-weight: var(--fc2-font-medium) !important; font-size: var(--fc2-text-sm) !important; opacity: var(--fc2-opacity-strong); } .fc2-portal-item:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-portal-item.active { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); box-shadow: inset 0 0 0 var(--fc2-border-width) rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-filter-chip { padding: var(--fc2-space-xs) var(--fc2-space-base); background: var(--fc2-surface-item); border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-radius-lg); font-size: var(--fc2-text-xs); cursor: pointer; transition: all var(--fc2-motion-fast) var(--fc2-ease-standard); color: var(--fc2-text-dim); } .fc2-filter-chip.active { background: var(--fc2-primary); border-color: var(--fc2-primary); color: var(--fc2-on-primary); box-shadow: 0 0 var(--fc2-space-base) rgba(var(--fc2-primary-rgb), var(--fc2-opacity-muted)); } .fc2-filter-chip:focus-visible { outline: none; box-shadow: 0 0 0 calc(var(--fc2-space-xxs) + var(--fc2-border-width)) rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-settings-grid { display: grid; grid-template-columns: minmax(0,1.3fr) minmax(320px,0.9fr); gap: var(--fc2-space-lg); align-items: start; } .fc2-settings-grid-main, .fc2-settings-grid-side { display: grid; gap: var(--fc2-space-md); align-content: start; } .fc2-portal-actions { display: flex; align-items: center; gap: var(--fc2-space-sm); padding: var(--fc2-space-sm) var(--fc2-space-md); background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); border-bottom: var(--fc2-border-width) solid var(--fc2-border); } .fc2-settings-tab { display: flex; flex-direction: column; gap: var(--fc2-space-md); } .fc2-data-container .fc2-enh-form-row { flex-direction: row !important; justify-content: flex-start; align-items: center; border-bottom: var(--fc2-border-width) solid var(--fc2-border); padding: var(--fc2-space-sm) var(--fc2-space-md); gap: var(--fc2-space-md); } .fc2-data-container .fc2-enh-form-row .fc2-enh-label { flex: 0 0 140px; margin-bottom: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; opacity: var(--fc2-opacity-dim); } .fc2-data-container .fc2-enh-form-row .fc2-enh-select, .fc2-data-container .fc2-enh-form-row .fc2-enh-input, .fc2-data-container .fc2-enh-form-row .fc2-input-group { flex: 1; width: auto; } .fc2-data-container .fc2-enh-form-row:last-child { border-bottom: none; } .fc2-settings-card-grid { display: flex; flex-direction: column; align-items: stretch; width: 100%; } .fc2-settings-advanced-block { margin-top: var(--fc2-space-base); padding-top: var(--fc2-space-base); border-top: var(--fc2-border-width) dashed rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-settings-advanced-label { font-size: var(--fc2-text-xs) !important; font-weight: var(--fc2-font-semibold) !important; color: var(--fc2-text-muted); margin-bottom: var(--fc2-space-sm); letter-spacing: 0.02em; text-transform: uppercase; } .fc2-panel-card.warning { border-color: rgba(var(--fc2-warn-rgb), 0.2); } .fc2-panel-card.warning h4 .fc2-icon { color: var(--fc2-warn); } .fc2-settings-empty-state { display: none; padding: calc(var(--fc2-space-md) + var(--fc2-space-xs)); border: var(--fc2-border-width) dashed rgba(var(--fc2-primary-rgb), var(--fc2-opacity-muted)); border-radius: var(--fc2-radius-lg); background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); } .fc2-settings-empty-title { font-size: var(--fc2-text-base) !important; font-weight: var(--fc2-font-bold) !important; color: var(--fc2-text); margin-bottom: var(--fc2-space-xs); } .fc2-settings-empty-hint { font-size: var(--fc2-text-xs) !important; color: var(--fc2-text-muted); line-height: var(--fc2-line-base); } .fc2-label-dim { font-size: var(--fc2-text-xs) !important; color: var(--fc2-text-muted); opacity: var(--fc2-opacity-dim); } .fc2-divider { height: var(--fc2-border-width); background: var(--fc2-border); margin: var(--fc2-space-base) 0; opacity: 0.6; } .fc2-grid-actions { display: flex; flex-wrap: wrap; align-items: stretch; gap: var(--fc2-space-sm); } .fc2-grid-actions, .fc2-debug-actions, .fc2-folder-actions, .fc2-sync-result-panel .fc2-card-actions, .fc2-auth-section .fc2-card-actions, .fc2-data-overview-actions { display: flex; flex-direction: row; align-items: stretch; justify-content: flex-start; flex-wrap: wrap; gap: 0 !important; padding: 0; border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-radius-md); background: var(--fc2-surface-item); box-shadow: var(--fc2-shadow-sm); backdrop-filter: blur(var(--fc2-blur)); -webkit-backdrop-filter: blur(var(--fc2-blur)); overflow: hidden; min-height: var(--fc2-btn-md); } .fc2-grid-actions > .fc2-enh-btn, .fc2-debug-actions > .fc2-enh-btn, .fc2-folder-actions > .fc2-enh-btn, .fc2-sync-result-panel .fc2-card-actions > .fc2-enh-btn, .fc2-auth-section .fc2-card-actions > .fc2-enh-btn, .fc2-data-overview-actions > .fc2-enh-btn { flex: 1 0 auto; min-width: 120px; height: var(--fc2-btn-md) !important; background: transparent !important; border: none !important; border-radius: 0 !important; margin: 0 !important; padding: 0 var(--fc2-space-sm) !important; box-shadow: none !important; backdrop-filter: none !important; font-size: var(--fc2-text-xs) !important; } .fc2-grid-actions > .fc2-enh-btn:not(:last-child), .fc2-debug-actions > .fc2-enh-btn:not(:last-child), .fc2-folder-actions > .fc2-enh-btn:not(:last-child), .fc2-sync-result-panel .fc2-card-actions > .fc2-enh-btn:not(:last-child), .fc2-auth-section .fc2-card-actions > .fc2-enh-btn:not(:last-child), .fc2-data-overview-actions > .fc2-enh-btn:not(:last-child) { border-right: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; border-bottom: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; } .fc2-grid-actions > .fc2-enh-btn:hover, .fc2-debug-actions > .fc2-enh-btn:hover, .fc2-sync-result-panel .fc2-card-actions > .fc2-enh-btn:hover, .fc2-auth-section .fc2-card-actions > .fc2-enh-btn:hover, .fc2-data-overview-actions > .fc2-enh-btn:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; transform: none !important; } .fc2-enh-hint { font-size: var(--fc2-text-xs) !important; line-height: var(--fc2-line-base); padding: var(--fc2-space-sm) var(--fc2-space-md); color: var(--fc2-text-muted); } .fc2-led-group { display: inline-flex; align-items: center; gap: var(--fc2-space-xs); } .fc2-led-dot { width: 8px; height: 8px; border-radius: var(--fc2-radius-full); background: var(--fc2-text-muted); display: inline-block; flex-shrink: 0; transition: background var(--fc2-motion-fast); } .fc2-led-dot.active { background: var(--fc2-success); box-shadow: 0 0 6px rgba(var(--fc2-success-rgb), var(--fc2-opacity-dim)); } .fc2-led-label { font-size: var(--fc2-text-xs) !important; color: var(--fc2-text-dim); } .fc2-grid-actions.compact { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 0; } .fc2-grid-actions.compact > .fc2-enh-btn { min-width: 0; height: var(--fc2-btn-sm) !important; font-size: var(--fc2-text-xs) !important; } .fc2-auth-section { margin-top: var(--fc2-space-base); padding-top: var(--fc2-space-base); border-top: var(--fc2-border-width) solid var(--fc2-border); } .fc2-auth-section .fc2-dim { font-size: var(--fc2-text-xs) !important; color: var(--fc2-text-muted); } .fc2-data-overview-grid { display: flex; flex-direction: column; align-items: stretch; gap: 0; } .fc2-data-overview-grid .fc2-stat-item { border-bottom: var(--fc2-border-width) solid var(--fc2-border); } .fc2-data-overview-grid .fc2-stat-item:last-child { border-bottom: none; } .fc2-data-overview-sync { display: flex; flex-direction: column; align-items: flex-start; gap: var(--fc2-space-xs); } .fc2-data-overview-meta { font-size: var(--fc2-text-xs) !important; color: var(--fc2-text-muted); line-height: 1.4; } .fc2-data-overview-actions { flex-wrap: nowrap; border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-radius-full); margin: var(--fc2-space-base) 0; height: var(--fc2-btn-md); } .fc2-sync-summary { display: flex; align-items: flex-start; gap: var(--fc2-space-base); padding: var(--fc2-space-sm) var(--fc2-space-md); background: transparent; border: none; border-bottom: var(--fc2-border-width) solid var(--fc2-border); } .fc2-sync-result-panel { display: flex; flex-direction: column; gap: 0; padding: 0; margin: 0; background: transparent; border: none; border-bottom: var(--fc2-border-width) solid var(--fc2-border); } .fc2-sync-result-body { padding: var(--fc2-space-sm) var(--fc2-space-md); display: flex; flex-direction: column; gap: var(--fc2-space-xs); border-bottom: var(--fc2-border-width) solid var(--fc2-border); } .fc2-sync-result-summary { font-size: var(--fc2-text-sm) !important; font-weight: var(--fc2-font-semibold) !important; color: var(--fc2-text); line-height: 1.4; } .fc2-sync-conflicts-list { margin-top: var(--fc2-space-xs); display: flex; flex-direction: column; gap: var(--fc2-space-xxs); } .fc2-sync-summary-copy { display: flex; flex-direction: column; gap: var(--fc2-space-xxs); min-width: 0; } .fc2-sync-summary-title { font-size: var(--fc2-text-sm) !important; font-weight: var(--fc2-font-bold) !important; color: var(--fc2-text); } .fc2-sync-summary-detail { font-size: var(--fc2-text-xs) !important; color: var(--fc2-text-muted); line-height: 1.4; } .fc2-sync-status-pill { display: inline-flex; align-items: center; justify-content: center; min-width: var(--fc2-btn-lg); padding: var(--fc2-space-xxs) var(--fc2-space-sm); border-radius: var(--fc2-radius-full); border: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); font-size: var(--fc2-text-xs) !important; font-weight: var(--fc2-font-bold) !important; white-space: nowrap; text-transform: uppercase; letter-spacing: 0.02em; } .fc2-sync-status-pill.idle { background: var(--fc2-surface-item); color: var(--fc2-text-muted); border-color: var(--fc2-border); } .fc2-sync-status-pill.syncing { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); color: var(--fc2-primary); border-color: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-muted)); } .fc2-sync-status-pill.success { background: rgba(var(--fc2-success-rgb), var(--fc2-opacity-subtle)); color: var(--fc2-success); border-color: rgba(var(--fc2-success-rgb), var(--fc2-opacity-muted)); } .fc2-sync-status-pill.error { background: rgba(var(--fc2-danger-rgb), var(--fc2-opacity-subtle)); color: var(--fc2-danger); border-color: rgba(var(--fc2-danger-rgb), var(--fc2-opacity-muted)); } .fc2-sync-status-pill.conflict { background: rgba(var(--fc2-warn-rgb), var(--fc2-opacity-subtle)); color: var(--fc2-warn); border-color: rgba(var(--fc2-warn-rgb), var(--fc2-opacity-muted)); } /* ============================================================ DEBUG TAB ============================================================ */ .fc2-debug-container { display: flex; flex-direction: column; gap: var(--fc2-space-md); height: 100%; } .fc2-debug-header { display: flex; flex-direction: column; gap: var(--fc2-space-sm); flex-shrink: 0; padding: var(--fc2-space-md) var(--fc2-space-md); border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-radius-md); background: linear-gradient(180deg, rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)), rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle))); } .fc2-debug-actions { display: flex; gap: var(--fc2-space-sm); flex-wrap: wrap; } .fc2-debug-filters { display: flex; align-items: center; gap: var(--fc2-space-md); flex-wrap: wrap; padding-top: var(--fc2-space-md); border-top: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-debug-filters .fc2-enh-input { flex: 1 1 220px; max-width: 320px; } .fc2-log-list-container { flex: 1; overflow-y: auto; min-height: 0; border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-radius-sm); background: rgba(var(--fc2-black-rgb), var(--fc2-opacity-muted)); padding: var(--fc2-space-sm); scrollbar-width: thin; } .fc2-log-item { display: flex; align-items: flex-start; gap: var(--fc2-space-sm); padding: var(--fc2-space-xs) var(--fc2-space-sm); border-bottom: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); font-family: var(--fc2-font-mono) !important; font-size: var(--fc2-text-xs) !important; line-height: var(--fc2-line-base) !important; transition: background var(--fc2-motion-fast); } .fc2-log-item:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); } .fc2-log-item:last-child { border-bottom: none; } .fc2-log-item.level-error { border-left: calc(var(--fc2-border-width) * 3) solid var(--fc2-danger); } .fc2-log-item.level-warn { border-left: calc(var(--fc2-border-width) * 3) solid var(--fc2-warn); } .fc2-log-item.level-info { border-left: calc(var(--fc2-border-width) * 3) solid var(--fc2-text-dim); } .fc2-log-item.level-success { border-left: calc(var(--fc2-border-width) * 3) solid var(--fc2-success); } .fc2-log-item.level-debug { border-left: calc(var(--fc2-border-width) * 3) solid var(--fc2-text-muted); } .fc2-log-time { color: var(--fc2-text-muted); white-space: nowrap; flex-shrink: 0; } .fc2-log-level { font-weight: var(--fc2-font-semibold) !important; white-space: nowrap; flex-shrink: 0; min-width: var(--fc2-btn-lg); } .fc2-log-module { color: var(--fc2-text-dim); white-space: nowrap; flex-shrink: 0; } .fc2-log-msg { color: var(--fc2-text); word-break: break-word; flex: 1; } .fc2-log-trace { font-size: var(--fc2-text-xs) !important; color: var(--fc2-text-muted); font-family: var(--fc2-font-mono); margin-left: auto; white-space: nowrap; flex-shrink: 0; } .level-error .fc2-log-level { color: var(--fc2-danger); } .level-warn .fc2-log-level { color: var(--fc2-warn); } .level-info .fc2-log-level { color: var(--fc2-text-dim); } .level-success .fc2-log-level { color: var(--fc2-success); } .level-debug .fc2-log-level { color: var(--fc2-text-muted); } .fc2-log-payload { margin: var(--fc2-space-xs) 0 0 0; padding: var(--fc2-space-sm); background: rgba(var(--fc2-black-rgb), var(--fc2-opacity-muted)); border-radius: var(--fc2-radius-sm); font-size: var(--fc2-text-xs) !important; color: var(--fc2-text-dim); overflow-x: auto; white-space: pre-wrap; word-break: break-all; } .fc2-log-payload-toggle { padding: var(--fc2-space-xxs) var(--fc2-space-sm); background: var(--fc2-surface-item); border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-radius-sm); color: var(--fc2-text-dim); font-size: var(--fc2-text-xs) !important; cursor: pointer; margin-left: auto; flex-shrink: 0; transition: all var(--fc2-motion-fast); } .fc2-log-payload-toggle:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); color: var(--fc2-text); } /* ============================================================ SCRAPER DIAGNOSIS ============================================================ */ .fc2-stat-item { display: flex; justify-content: space-between; align-items: center; padding: var(--fc2-space-sm) var(--fc2-space-md); width: 100%; box-sizing: border-box; border-bottom: var(--fc2-border-width) solid var(--fc2-border); } .fc2-stat-item:last-child { border-bottom: none; } .fc2-stat-value { font-size: var(--fc2-text-sm) !important; font-weight: var(--fc2-font-bold) !important; color: var(--fc2-primary); order: 2; } .fc2-stat-unit { font-size: var(--fc2-text-xs) !important; color: var(--fc2-text-muted); margin-left: var(--fc2-space-xxs); } .fc2-stat-label { font-size: var(--fc2-text-sm) !important; font-weight: var(--fc2-font-semibold) !important; color: var(--fc2-text-dim); order: 1; opacity: var(--fc2-opacity-strong); } /* Scraper Diagnosis */ .fc2-scraper-list { display: flex; flex-direction: column; gap: 0; border: var(--fc2-border-width) solid var(--fc2-border); border-radius: var(--fc2-radius-md); overflow: hidden; } .fc2-scraper-row { display: flex; justify-content: space-between; align-items: center; padding: var(--fc2-space-sm) var(--fc2-space-base); background: transparent; border-bottom: var(--fc2-border-width) solid var(--fc2-border); transition: all var(--fc2-motion-fast) var(--fc2-ease-standard); } .fc2-scraper-row:last-child { border-bottom: none; } .fc2-scraper-row:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-scraper-info { display: flex; align-items: center; gap: var(--fc2-space-sm); } .fc2-scraper-label { font-size: var(--fc2-text-sm) !important; font-weight: var(--fc2-font-semibold) !important; color: var(--fc2-text); } .fc2-scraper-url { font-size: var(--fc2-text-xs); color: var(--fc2-text-muted); transition: color var(--fc2-motion-base); } .fc2-scraper-url:hover { color: var(--fc2-primary); } .fc2-status-indicator { font-size: var(--fc2-text-xs) !important; font-weight: var(--fc2-font-bold) !important; padding: var(--fc2-space-xxs) var(--fc2-space-base); border-radius: var(--fc2-radius-full); text-transform: uppercase; border: var(--fc2-border-width) solid transparent; display: inline-flex; align-items: center; justify-content: center; } .fc2-status-indicator.ok { background: rgba(var(--fc2-success-rgb), var(--fc2-opacity-subtle)); color: var(--fc2-success); } .fc2-status-indicator.cf { background: rgba(var(--fc2-warn-rgb), var(--fc2-opacity-subtle)); color: var(--fc2-warn); } .fc2-status-indicator.error { background: rgba(var(--fc2-danger-rgb), var(--fc2-opacity-subtle)); color: var(--fc2-danger); } .fc2-status-indicator.429 { background: rgba(var(--fc2-warn-rgb), var(--fc2-opacity-muted)); color: var(--fc2-warn); } /* ============================================================ ABOUT TAB ============================================================ */ .fc2-about-tab { display: flex; flex-direction: column; gap: var(--fc2-space-md); } .fc2-about-header { text-align: center; padding: var(--fc2-space-xl) 0 var(--fc2-space-md); } .fc2-version-badge { display: inline-block; margin-top: var(--fc2-space-sm); padding: var(--fc2-space-xs) var(--fc2-space-md); background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); border: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-muted)); border-radius: var(--fc2-radius-full); font-size: var(--fc2-text-xs) !important; font-weight: var(--fc2-font-bold) !important; color: var(--fc2-text); letter-spacing: 0.1em; text-transform: uppercase; } .fc2-about-desc { margin-top: var(--fc2-space-base); color: var(--fc2-text-dim); font-size: var(--fc2-text-base) !important; line-height: 1.6 !important; max-width: 500px; margin-left: auto; margin-right: auto; } .fc2-about-content { font-size: var(--fc2-text-sm) !important; line-height: var(--fc2-line-base) !important; color: var(--fc2-text-dim); } .fc2-about-content.dmca { color: var(--fc2-text-muted); } .fc2-about-footer { text-align: center; padding: var(--fc2-space-lg) 0; font-size: var(--fc2-text-sm) !important; color: var(--fc2-text-muted); } .fc2-link { color: var(--fc2-primary); text-decoration: none; transition: opacity var(--fc2-motion-fast); } .fc2-link:hover { opacity: 0.7; } /* ============================================================ DATA TAB PREVIEW & LOGS ============================================================ */ .fc2-backup-preview-lines { display: flex; flex-direction: column; background: rgba(var(--fc2-black-rgb), var(--fc2-opacity-muted)); max-height: 240px; overflow-y: auto; border-bottom: var(--fc2-border-width) solid var(--fc2-border); scrollbar-width: thin; } .fc2-backup-preview-line { padding: var(--fc2-space-xs) var(--fc2-space-md); font-size: var(--fc2-text-xs) !important; font-family: var(--fc2-font-mono); color: var(--fc2-text-dim); border-bottom: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .fc2-backup-preview-line:last-child { border-bottom: none; } @media(max-width:768px) { .fc2-settings-grid { grid-template-columns: 1fr; } .fc2-debug-filters .fc2-enh-input { flex-basis: 100%; max-width: none; } .fc2-settings-card-grid { grid-template-columns: 1fr; } .fc2-grid-actions { flex-direction: column; } .fc2-grid-actions, .fc2-debug-actions, .fc2-folder-actions, .fc2-sync-result-panel .fc2-card-actions, .fc2-auth-section .fc2-card-actions, .fc2-data-overview-actions { padding: var(--fc2-space-sm); } .fc2-grid-actions, .fc2-debug-actions, .fc2-sync-result-panel .fc2-card-actions, .fc2-auth-section .fc2-card-actions, .fc2-data-overview-actions { flex-direction: column; } .fc2-grid-actions > .fc2-enh-btn, .fc2-debug-actions > .fc2-enh-btn, .fc2-sync-result-panel .fc2-card-actions > .fc2-enh-btn, .fc2-auth-section .fc2-card-actions > .fc2-enh-btn, .fc2-data-overview-actions > .fc2-enh-btn { flex-basis: auto; } .fc2-portal-actions { flex-wrap: wrap; } .fc2-data-overview-meta, .fc2-sync-result-summary, .fc2-sync-summary-detail { overflow-wrap: anywhere; } .fc2-sync-status-pill, .fc2-log-level { white-space: normal; } .fc2-log-level { min-width: 0; } .fc2-debug-filters { gap: var(--fc2-space-sm); } .fc2-log-item { flex-wrap: wrap; gap: var(--fc2-space-xs); } .fc2-log-time, .fc2-log-module { display: none; } } /* Color Picker */ .fc2-color-picker { display: flex; flex-wrap: wrap; gap: var(--fc2-space-sm); padding: var(--fc2-space-xxs) 0; } .fc2-color-option { width: var(--fc2-space-lg); height: var(--fc2-space-lg); border-radius: var(--fc2-radius-full); border: calc(var(--fc2-border-width) * 2) solid transparent; background-color: var(--option-color); cursor: pointer; transition: all var(--fc2-motion-fast) var(--fc2-ease-out); padding: 0; position: relative; box-shadow: var(--fc2-shadow-sm); } .fc2-color-option:hover { transform: scale(1.1); box-shadow: var(--fc2-shadow-md); } .fc2-color-option.is-active { border-color: var(--fc2-text-bright); transform: scale(1.1); box-shadow: 0 0 12px var(--option-color); } .fc2-color-option.is-active::after { content: ''; position: absolute; top: calc(-1 * var(--fc2-space-xs)); left: calc(-1 * var(--fc2-space-xs)); right: calc(-1 * var(--fc2-space-xs)); bottom: calc(-1 * var(--fc2-space-xs)); border: var(--fc2-border-width) solid var(--option-color); border-radius: var(--fc2-radius-full); opacity: var(--fc2-opacity-dim); } /* Fix for light colors on dark background */ .fc2-color-option[data-value="#ffffff"].is-active { border-color: var(--fc2-text); box-shadow: 0 0 12px rgba(255, 255, 255, 0.4); } `; var tabs = (_C) => ` /* ============================================================ SETTINGS NAVIGATION ============================================================ */ .fc2-enh-settings-primary-nav { display:flex; width: 240px; flex-direction:column; gap:0; padding:var(--fc2-space-sm) 0; border-right: var(--fc2-border-width) solid var(--fc2-border); background:rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); overflow-y:auto; flex-shrink:0; } .fc2-enh-tab-btn { display:flex; align-items:center; gap: var(--fc2-space-sm); width:100%; padding: var(--fc2-space-base) var(--fc2-space-md); background:transparent; color:var(--fc2-text); opacity: var(--fc2-opacity-dim); border:none; border-bottom: var(--fc2-border-width) solid var(--fc2-border); border-radius:0; font-size: var(--fc2-text-sm); font-weight: var(--fc2-font-semibold); text-align:left; transition:all var(--fc2-motion-fast) var(--fc2-ease-standard); cursor:pointer; position:relative; } .fc2-enh-tab-btn:last-child { border-bottom:none; } .fc2-enh-tab-btn:hover { background:rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); color:var(--fc2-text); } .fc2-enh-tab-btn:focus-visible { outline:none; box-shadow: inset 0 0 0 calc(var(--fc2-space-xxs) + var(--fc2-border-width)) rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-enh-tab-btn.active { background:rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); color:var(--fc2-primary); font-weight: var(--fc2-font-semibold); opacity:1; box-shadow:inset calc(var(--fc2-border-width) * 3) 0 0 var(--fc2-primary); } .fc2-enh-tab-btn .fc2-icon { font-size:1.1em; opacity:0.8; } .fc2-enh-tab-btn.active .fc2-icon { opacity:1; } .fc2-enh-tab-btn:active { transform:scale(0.96); } .fc2-tab-content-wrapper > :is(.fc2-dashboard-container, .fc2-data-container, .fc2-settings-tab) { display:flex; flex-direction:column; gap: var(--fc2-space-md); min-width:0; min-height:100%; } .fc2-tab-content-wrapper:not(.full-width) > :is(.fc2-dashboard-container, .fc2-data-container, .fc2-settings-tab) { width:min(100%, 1180px); margin:0 auto; } /* Tab Content Transitions */ .fc2-tab-content-wrapper { height:100%; overflow-y:auto; padding: var(--fc2-space-lg) var(--fc2-space-lg) var(--fc2-space-lg); scrollbar-width:thin; transition: opacity var(--fc2-motion-slow) var(--fc2-ease-out), transform var(--fc2-motion-slow) var(--fc2-ease-out); opacity: 0; transform: translateY(var(--fc2-space-md)) scale(0.98); will-change: opacity, transform; } .fc2-tab-content-wrapper.fc2-entering { opacity: 1; transform: translateY(0) scale(1); } .fc2-tab-content-wrapper.fc2-leaving { opacity: 0; transform: translateY(calc(-1 * var(--fc2-space-md))) scale(1.02); pointer-events: none; } /* Mobile Bottom Navigation */ @media(max-width:768px) { .fc2-enh-settings-primary-nav { width:100%; flex-direction:row; flex-wrap:wrap; overflow-x:visible; border-right:none; border-bottom:var(--fc2-border-width) solid var(--fc2-border); padding:0.6rem 0.75rem; gap:0.5rem; } .fc2-enh-nav-label { display:none; } .fc2-tab-content-wrapper { height:auto; overflow:visible; padding:1rem 1rem calc(1rem + env(safe-area-inset-bottom)); } .fc2-tab-content-wrapper > :is(.fc2-dashboard-container, .fc2-data-container, .fc2-settings-tab) { gap:0.85rem; } .fc2-enh-tab-btn { flex-direction:row; gap: var(--fc2-space-sm); padding: var(--fc2-space-sm) var(--fc2-space-base); font-size: var(--fc2-text-xs); justify-content:center; border-radius:var(--fc2-radius-full); flex:1 1 auto; min-width:0; text-align:center; white-space:normal; line-height:1; background:rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)); border:var(--fc2-border-width) solid var(--fc2-border); } .fc2-enh-tab-btn.active { background:var(--fc2-primary); color:var(--fc2-on-primary); border-color:transparent; } .fc2-enh-tab-btn .fc2-icon { font-size:1.4em; margin-bottom:var(--fc2-space-xxs); } } `; var toast = (_C) => ` /* ============================================================ TOAST NOTIFICATIONS ============================================================ */ .fc2-toast-container { position:fixed; top:var(--fc2-space-md); right:var(--fc2-space-md); z-index:var(--fc2-z-toast); display:flex; flex-direction:column; gap:var(--fc2-space-sm); pointer-events:none; } .fc2-toast-item { position:relative; display:flex; align-items:center; min-width:280px; max-width:420px; padding:var(--fc2-space-sm) var(--fc2-space-md); background:var(--fc2-surface-float); color:var(--fc2-text); border-radius:var(--fc2-radius-md); box-shadow:var(--fc2-shadow-lg); font-size: var(--fc2-text-base); font-weight: var(--fc2-font-medium); backdrop-filter:blur(var(--fc2-blur)); -webkit-backdrop-filter:blur(var(--fc2-blur)); border: var(--fc2-border-width) solid var(--fc2-border); opacity:0; pointer-events:auto; animation:fc2-toast-in var(--fc2-motion-base) var(--fc2-ease-out) forwards; } .fc2-toast-item.hiding { animation:fc2-toast-out var(--fc2-motion-slow) var(--fc2-ease-standard) forwards; } .fc2-toast-icon { display:flex; align-items:center; justify-content:center; font-size: var(--fc2-text-xl); margin-right:var(--fc2-space-sm); flex-shrink:0; } .fc2-toast-content { flex-grow:1; line-height:1.4; word-break:break-word; overflow:hidden; } .fc2-toast-close { background:none; border:none; color:var(--fc2-text-dim); cursor:pointer; padding:var(--fc2-space-xs); margin-left:var(--fc2-space-sm); border-radius:var(--fc2-radius-full); transition:all var(--fc2-motion-fast); display:flex; align-items:center; } .fc2-toast-close:hover { background:var(--fc2-surface-item); color:var(--fc2-text); } .fc2-toast-close:focus-visible { outline:none; box-shadow:0 0 0 calc(var(--fc2-space-xxs) + var(--fc2-border-width)) rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); } .fc2-toast-progress { position:absolute; bottom:0; left:0; height:var(--fc2-space-xxs); width:100%; transform-origin:left; background:var(--fc2-toast-color, var(--fc2-primary)); animation:fc2-toast-shrink var(--fc2-toast-duration, 3000ms) linear forwards; } .toast-success { border-left:var(--fc2-space-xs) solid var(--fc2-success); } .toast-success .fc2-toast-icon { color:var(--fc2-success); } .toast-error { border-left:var(--fc2-space-xs) solid var(--fc2-danger); } .toast-error .fc2-toast-icon { color:var(--fc2-danger); } .toast-warn { border-left:var(--fc2-space-xs) solid var(--fc2-warn); } .toast-warn .fc2-toast-icon { color:var(--fc2-warn); } .toast-info { border-left:var(--fc2-space-xs) solid var(--fc2-info); } .toast-info .fc2-toast-icon { color:var(--fc2-info); } @keyframes fc2-toast-shrink { from { width:100%; } to { width:0%; } } .fc2-toast-action { margin-left:var(--fc2-space-sm); padding:var(--fc2-space-xxs) var(--fc2-space-sm); background:rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); border:var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb),var(--fc2-opacity-muted)); border-radius:var(--fc2-radius-sm); color:var(--fc2-primary); font-size:var(--fc2-text-xs); font-weight: var(--fc2-font-semibold); cursor:pointer; transition:all var(--fc2-motion-fast); } .fc2-toast-action:hover { background:var(--fc2-primary); color:var(--fc2-on-primary); border-color:transparent; } `; var toolbar = (_C) => ` /* ============================================================ PLAYER TOOLBAR (DESKTOP) ============================================================ */ .fc2-enh-toolbar { width: 100%; max-width: min(100%, 1180px); margin: var(--fc2-space-md) auto; background: transparent !important; border: var(--fc2-border-width) solid var(--fc2-border) !important; border-radius: var(--fc2-radius-lg) !important; backdrop-filter: blur(var(--fc2-blur)) !important; -webkit-backdrop-filter: blur(var(--fc2-blur)) !important; } .fc2-enh-toolbar--detail { margin: var(--fc2-space-lg) auto; max-width: 100%; } .fc2-enh-toolbar .fc2-info-area { display: grid !important; grid-template-columns: auto 1fr auto !important; align-items: center !important; width: 100% !important; padding: 0 !important; gap: var(--fc2-space-md) !important; } .fc2-enh-toolbar[data-layout="without-actress"] .fc2-info-area { grid-template-columns: auto 1fr !important; } .fc2-enh-toolbar[data-layout="with-actress"] .fc2-info-area { grid-template-columns: auto 1fr auto !important; } .fc2-enh-toolbar .fc2-toolbar-section-controls { grid-column: 1; display: flex; align-items: center; justify-content: flex-start; min-width: 0; width: auto !important; } .fc2-enh-toolbar .fc2-toolbar-section-links { grid-column: 3; display: flex; align-items: center; justify-content: flex-end; min-width: 0; width: auto !important; } .fc2-enh-toolbar .fc2-card-top-right-controls, .fc2-enh-toolbar .fc2-resource-links-container { position: static !important; display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important; align-items: stretch !important; background: var(--fc2-surface-item) !important; border: var(--fc2-border-width) solid var(--fc2-border) !important; border-radius: var(--fc2-radius-full) !important; padding: 0 !important; box-shadow: var(--fc2-shadow-sm) !important; overflow: hidden !important; backdrop-filter: blur(var(--fc2-blur)) !important; -webkit-backdrop-filter: blur(var(--fc2-blur)) !important; height: var(--fc2-btn-md) !important; gap: 0 !important; } .fc2-enh-toolbar .fc2-btn-actress { grid-column: 2; justify-self: center; margin: 0 !important; height: var(--fc2-btn-md) !important; line-height: var(--fc2-line-base) !important; padding: 0 var(--fc2-space-lg) !important; background: var(--fc2-surface-item) !important; border: var(--fc2-border-width) solid var(--fc2-border) !important; border-radius: var(--fc2-radius-full) !important; box-shadow: var(--fc2-shadow-sm) !important; backdrop-filter: blur(var(--fc2-blur)) !important; -webkit-backdrop-filter: blur(var(--fc2-blur)) !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; max-width: min(100%, 28rem) !important; text-align: center !important; } .fc2-enh-toolbar .fc2-card-top-right-controls > *, .fc2-enh-toolbar .fc2-resource-links-container > * { background: transparent !important; border: none !important; border-radius: 0 !important; box-shadow: none !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; height: 100% !important; padding: 0 var(--fc2-space-md) !important; margin: 0 !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; color: var(--fc2-text) !important; transition: background var(--fc2-motion-fast), transform var(--fc2-motion-fast); } .fc2-enh-toolbar .fc2-resource-links-container > .icon-only { padding: 0 var(--fc2-space-sm) !important; width: var(--fc2-btn-lg) !important; } .fc2-enh-toolbar .fc2-card-top-right-controls .fc2-id-badge { font-size: var(--fc2-text-sm) !important; } .fc2-enh-toolbar .fc2-card-top-right-controls > *:not(:last-child), .fc2-enh-toolbar .fc2-resource-links-container > *:not(:last-child) { border-right: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; } .fc2-enh-toolbar .fc2-card-top-right-controls > *:hover, .fc2-enh-toolbar .fc2-resource-links-container > *:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; } .fc2-enh-toolbar .fc2-btn-actress:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; border-color: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; transform: translateY(calc(-1 * var(--fc2-border-width))) !important; } .fc2-enh-toolbar .fc2-resource-links-container .fc2-magnet { animation: none !important; color: var(--fc2-primary) !important; } .fc2-enh-toolbar .fc2-resource-links-container .fc2-magnet:hover { background: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; } @media (max-width: 920px) { .fc2-enh-toolbar .fc2-info-area { grid-template-columns: auto minmax(0, 1fr) auto !important; gap: var(--fc2-space-sm) !important; } .fc2-enh-toolbar .fc2-toolbar-section-controls, .fc2-enh-toolbar .fc2-toolbar-section-links { width: auto !important; } .fc2-enh-toolbar .fc2-btn-actress { max-width: min(100%, 20rem) !important; } } @media (max-width: 768px) and (hover: hover), (max-width: 768px) and (pointer: fine) { .fc2-enh-toolbar .fc2-info-area { grid-template-columns: minmax(0, 1fr) auto !important; } .fc2-enh-toolbar .fc2-toolbar-section-controls { grid-row: 1 !important; } .fc2-enh-toolbar .fc2-toolbar-section-links { grid-row: 1 !important; } .fc2-enh-toolbar .fc2-btn-actress { grid-column: 1 / -1 !important; grid-row: 2 !important; justify-self: center !important; height: 34px !important; line-height: 32px !important; font-size: var(--fc2-text-xs) !important; width: fit-content !important; max-width: min(100%, 18rem) !important; padding: 0 var(--fc2-space-md) !important; } } @media (max-width: 768px) { .fc2-enh-toolbar .fc2-info-area { grid-template-columns: minmax(0, 1fr) auto !important; } .fc2-enh-toolbar .fc2-btn-actress { grid-column: 1 / -1 !important; grid-row: 2 !important; height: var(--fc2-btn-sm) !important; line-height: var(--fc2-line-base) !important; font-size: var(--fc2-text-xs) !important; max-width: min(100%, 18rem) !important; } } `; var getComponentStyles = (C) => ` ${base()} ${toast()} ${fab()} ${card(C)} ${button(C)} ${form()} ${tabs()} ${settings()} ${modal()} ${actionsheet()} /* Refactored Blocks */ ${common()} ${toolbar()} ${gallery()} ${batch()} `; var mkIcon = (svg) => UIUtils.icon(svg); var buildWorkspaceButtons = (tabsConfig, onTabSelect) => tabsConfig.map((tab, index) => h("button", { className: "fc2-enh-tab-btn fc2-enh-workspace-btn", role: "tab", "aria-selected": index === 0 ? "true" : "false", tabindex: index === 0 ? "0" : "-1", "data-tab": tab.id, "data-settings-category": tab.category, onclick: () => onTabSelect(tab.id) }, mkIcon(tab.icon), tab.navLabel)); var handleTablistKeydown = (event, navContainer, onTabSelect) => { const buttons = Array.from(navContainer.querySelectorAll("button[role=\"tab\"]")); const current = buttons.findIndex((btn) => btn === document.activeElement || btn.contains(document.activeElement)); if (current < 0) return; let next = -1; if (event.key === "ArrowRight" || event.key === "ArrowDown") next = (current + 1) % buttons.length; else if (event.key === "ArrowLeft" || event.key === "ArrowUp") next = (current - 1 + buttons.length) % buttons.length; else if (event.key === "Home") next = 0; else if (event.key === "End") next = buttons.length - 1; if (next >= 0) { event.preventDefault(); const target = buttons[next]; if (!target) return; buttons.forEach((btn, i) => { btn.setAttribute("tabindex", i === next ? "0" : "-1"); btn.setAttribute("aria-selected", i === next ? "true" : "false"); }); target.focus(); const tabId = target.dataset.tab; if (tabId) onTabSelect(tabId); } }; var createSettingsPanelView = ({ tabsConfig, onTabSelect, onHide }) => { const panel = h("div", { id: DOM_IDS.SETTINGS_HOST, className: "fc2-is-hidden" }); const container = h("div", { id: DOM_IDS.SETTINGS_CONTAINER }); const shadow = container.attachShadow({ mode: "open" }); shadow.appendChild(h("style", {}, getComponentStyles(Config.CLASSES))); const header = h("div", { className: "fc2-enh-settings-header" }, h("div", { className: "fc2-enh-settings-heading" }, h("h2", {}, t("managementCenter"))), h("div", { className: "fc2-header-actions" }, h("button", { className: "fc2-close-btn", onclick: () => onHide(), title: t("btnCancel") || "Close" }, mkIcon(IconXmark)))); const workspaceNav = h("div", { className: "fc2-enh-settings-primary-nav", id: "tab-buttons", role: "tablist" }, ...buildWorkspaceButtons(tabsConfig, onTabSelect)); workspaceNav.addEventListener("keydown", (e) => handleTablistKeydown(e, workspaceNav, onTabSelect)); const panelInner = h("div", { className: "fc2-enh-settings-panel has-shadow-isolate", tabindex: "-1" }, header, h("div", { className: "fc2-enh-settings-body" }, workspaceNav, h("div", { className: "fc2-enh-settings-content", id: DOM_IDS.TAB_CONTENT }))); const backdrop = h("div", { className: "fc2-enh-modal-backdrop", onclick: () => onHide() }); shadow.append(backdrop, panelInner); panel.appendChild(container); const focusFirstInPanel = () => { (panelInner.querySelector("button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])") ?? panelInner).focus(); }; new MutationObserver(() => { if (!panel.classList.contains("fc2-is-hidden")) requestAnimationFrame(focusFirstInPanel); }).observe(panel, { attributes: true, attributeFilter: ["class"] }); return { panel, shadow }; }; var createDataTabDefinition = () => ({ id: "data", title: t("tabData"), navLabel: t("tabData"), category: "sync", entrypoints: ["sync-center"], icon: IconDatabase, render: async (shadow, switchTab) => { const { renderDataTab } = await module.import('./DataTab-bfpDUmRQ-C_bNm5Gc.js'); return renderDataTab(shadow, switchTab); }, onUnmount: () => { module.import('./DataTab-bfpDUmRQ-C_bNm5Gc.js').then(({ onUnmount }) => onUnmount()); } }); var createSettingsTabDefinition = () => ({ id: "settings", title: t("tabSettings"), navLabel: t("tabSettings"), category: "configure", entrypoints: ["quick-preferences"], icon: IconSliders, render: async () => { const { renderSettingsTab } = await module.import('./SettingsTab-8ebeRFDG-BDxX9kp7.js'); return renderSettingsTab(); }, onUnmount: () => { module.import('./SettingsTab-8ebeRFDG-BDxX9kp7.js').then(({ onUnmount }) => onUnmount()); } }); var createDebugTabDefinition = () => ({ id: "debug", title: t("labelTechnicalLogs"), navLabel: t("labelTechnicalLogs"), category: "diagnose", entrypoints: ["support-bundle"], icon: IconBolt, render: async () => { const { renderDebugTab } = await module.import('./DebugTab-BvxslCZw-BzciC75p.js'); return renderDebugTab(); }, onUnmount: () => { module.import('./DebugTab-BvxslCZw-BzciC75p.js').then(({ onUnmount }) => onUnmount()); } }); var createAboutTabDefinition = () => ({ id: "about", title: t("tabAbout"), navLabel: t("tabAbout"), category: "info", entrypoints: [], icon: IconCircleInfo, render: async () => { const { renderAboutTab } = await module.import('./AboutTab-BYbIrdck-YaaPEqkJ.js'); return renderAboutTab(); } }); var getTabsConfig = () => [ createDataTabDefinition(), createSettingsTabDefinition(), createDebugTabDefinition(), createAboutTabDefinition() ]; var SETTINGS_PANEL_DEFAULT_TAB_ID = "data"; var createLoadingOverlay = () => h("div", { className: "fc2-loading-overlay" }, h("div", { className: "fc2-loading-spinner" }), h("div", { className: "fc2-loading-text" }, `${t("labelLoading")}...`)); var createErrorState = (tabId) => h("div", { className: "fc2-error-state" }, `${t("labelError")}: ${tabId}`); var createTabContentWrapper = (tabId, element, isFullWidth) => h("div", { className: `fc2-tab-content-wrapper${isFullWidth ? " full-width" : ""}`, "data-tab": tabId }, element); var shouldInvalidateTabCache = (currentTabId, nextTabId, hasCachedTab) => currentTabId === nextTabId && hasCachedTab; var resolveTabEnterDelay = (hasOldContent, hasCachedTab) => hasOldContent && hasCachedTab ? TIMING.UI_TRANSITION_FAST : 0; var waitForTabTransition = (delay) => new Promise((resolve) => setTimeout(resolve, delay)); var log$46 = Logger.scope("SettingsPanel"); var PanelSettingsPanel = class { tabsConfig; panel = null; shadow = null; currentTab = SETTINGS_PANEL_DEFAULT_TAB_ID; isSwitching = false; pendingTabId = null; pendingMountTimer = null; switchToken = 0; tabCache = /* @__PURE__ */ new Map(); overlay = { close: () => this.hide() }; constructor(tabsConfig) { this.tabsConfig = tabsConfig; } show(activeTabId) { this.ensurePanel(); if (!this.panel) return; const initialTabId = this.resolveInitialTabId(activeTabId); if (this.panel.classList.contains("fc2-is-hidden") || this.currentTab !== initialTabId) { this.panel.classList.remove("fc2-is-hidden"); document.body.classList.add("fc2-settings-open"); document.addEventListener("keydown", this.handleKeyDown); OverlayStack.push(this.overlay); CoreEvents.emit(AppEvents.PANEL_OPENED, {}); this.switchTab(initialTabId); } } render(activeTabId) { this.show(activeTabId); } hide(event) { if (event) { event.preventDefault(); event.stopPropagation(); } if (!this.panel) return; this.panel.classList.add("fc2-is-hidden"); document.body.classList.remove("fc2-settings-open"); document.removeEventListener("keydown", this.handleKeyDown); if (this.pendingMountTimer !== null) { window.clearTimeout(this.pendingMountTimer); this.pendingMountTimer = null; } this.switchToken += 1; this.isSwitching = false; this.pendingTabId = null; OverlayStack.remove(this.overlay); CoreEvents.emit(AppEvents.PANEL_CLOSED, {}); } clearCache() { this.tabCache.clear(); } get currentTabId() { return this.currentTab; } async switchTab(tabId, intent) { this.ensurePanel(); if (this.isSwitching) { this.pendingTabId = tabId; if (intent) setPendingSettingsNavigationIntent({ ...intent, targetTab: tabId }); return; } if (intent) setPendingSettingsNavigationIntent({ ...intent, targetTab: tabId }); if (shouldInvalidateTabCache(this.currentTab, tabId, this.tabCache.has(tabId))) this.tabCache.delete(tabId); const previousTab = this.tabsConfig.find((tab) => tab.id === this.currentTab); const nextTab = this.tabsConfig.find((tab) => tab.id === tabId); if (!nextTab || !this.shadow) return; const token = ++this.switchToken; this.isSwitching = true; this.pendingTabId = null; const contentContainer = this.getContentContainer(); if (!contentContainer) { log$46.error("Tab content container not found"); this.finishSwitchCycle(token); return; } try { this.unmountTab(previousTab); const oldContent = contentContainer.querySelector(".fc2-tab-content-wrapper"); if (oldContent) oldContent.classList.add("fc2-leaving"); this.updateNavigationState(tabId); let content = this.tabCache.get(tabId); if (!content) { if (oldContent) await waitForTabTransition(TIMING.UI_TRANSITION_FAST); content = await this.renderTabContent(nextTab, tabId, contentContainer); } const enterDelay = resolveTabEnterDelay(Boolean(oldContent), this.tabCache.has(tabId)); this.mountTabContent(contentContainer, content, tabId, enterDelay, token); } catch (error) { this.handleTabRenderError(tabId, contentContainer, error, token); } } handleKeyDown = (event) => { if (!this.panel || this.panel.classList.contains("fc2-is-hidden")) return; if ((event.ctrlKey || event.metaKey) && event.key === "s") { event.preventDefault(); this.saveAndClose(); } if (event.key === "Escape") { event.preventDefault(); this.hide(); } }; saveAndClose() { Toast.show(t("alertSettingsSaved"), "success"); this.hide(); } ensurePanel() { if (this.panel && this.shadow) return; const view = createSettingsPanelView({ tabsConfig: this.tabsConfig, onTabSelect: (tabId) => { this.switchTab(tabId); }, onHide: (event) => this.hide(event) }); this.panel = view.panel; this.shadow = view.shadow; document.body.appendChild(this.panel); } resolveInitialTabId(activeTabId) { if (activeTabId) return activeTabId; const lastTab = Storage.get(STORAGE_KEYS.LAST_SETTINGS_TAB, ""); if (lastTab && this.tabsConfig.some((t) => t.id === lastTab)) return lastTab; return SETTINGS_PANEL_DEFAULT_TAB_ID; } getContentContainer() { return this.shadow?.getElementById(DOM_IDS.TAB_CONTENT) ?? null; } updateNavigationState(tabId) { this.shadow?.querySelectorAll(".fc2-enh-workspace-btn").forEach((button) => { button.classList.toggle("active", button.dataset.tab === tabId); }); } unmountTab(tabDef) { if (!tabDef?.onUnmount || !this.shadow) return; try { tabDef.onUnmount(this.shadow); } catch (error) { log$46.warn(`Tab unmount error: ${tabDef.id}`, error); } } async renderTabContent(tabDef, tabId, contentContainer) { contentContainer.textContent = ""; contentContainer.appendChild(createLoadingOverlay()); const content = createTabContentWrapper(tabId, await tabDef.render(this.shadow, (nextTabId) => this.switchTab(nextTabId)), Boolean(tabDef.isFullWidth)); this.tabCache.set(tabId, content); return content; } mountTabContent(contentContainer, content, tabId, enterDelay, token) { if (this.pendingMountTimer !== null) window.clearTimeout(this.pendingMountTimer); this.pendingMountTimer = window.setTimeout(() => { this.pendingMountTimer = null; if (token !== this.switchToken || !this.panel || !this.shadow) return; contentContainer.textContent = ""; content.classList.remove("fc2-entering", "fc2-leaving"); contentContainer.appendChild(content); requestAnimationFrame(() => { requestAnimationFrame(() => { if (token !== this.switchToken) return; content.classList.add("fc2-entering"); this.currentTab = tabId; Storage.set(STORAGE_KEYS.LAST_SETTINGS_TAB, tabId); this.finishSwitchCycle(token); }); }); }, enterDelay); } handleTabRenderError(tabId, contentContainer, error, token) { log$46.error(`Failed to render tab: ${tabId}`, error); contentContainer.textContent = ""; contentContainer.appendChild(createErrorState(tabId)); Toast.show(`${t("labelError")}: ${tabId}`, "error"); this.finishSwitchCycle(token); } finishSwitchCycle(token) { if (token !== this.switchToken) return; this.isSwitching = false; const nextTabId = this.pendingTabId; this.pendingTabId = null; if (nextTabId && nextTabId !== this.currentTab) this.switchTab(nextTabId); } }; var createSettingsPanelController = ({ tabsConfig = getTabsConfig() } = {}) => new PanelSettingsPanel(tabsConfig); var settingsPanel = null; /** Lazy: tabs call `t()` and must not construct before DI registers StateService. */ function getSettingsPanel() { if (!settingsPanel) settingsPanel = createSettingsPanelController(); return settingsPanel; } /** Compatible facade — all access is deferred until first use (after modules register). */ var SettingsPanel = { show: (activeTabId) => getSettingsPanel().show(activeTabId), render: (activeTabId) => getSettingsPanel().render(activeTabId), hide: () => getSettingsPanel().hide(), clearCache: () => getSettingsPanel().clearCache(), switchTab: (tabId, intent) => getSettingsPanel().switchTab(tabId, intent), get currentTabId() { return getSettingsPanel().currentTabId; } }; var bridged = false; /** Wire settings panel to app events (called once from app/modules, after tokens). */ function initSettingsPanelBridge() { if (bridged) return; bridged = true; CoreEvents.on(AppEvents.OPEN_SETTINGS, () => { SettingsPanel.show(); }); CoreEvents.on(AppEvents.LANGUAGE_CHANGED, () => { SettingsPanel.clearCache(); if (!document.body.classList.contains("fc2-settings-open")) return; SettingsPanel.switchTab(SettingsPanel.currentTabId); }); } /** * Post-registration UI wiring — keeps modules.ts free of bridge side-effects. */ var log$45 = Logger.scope("Lifecycle"); var wired = false; /** Call after registerModules(), before AppContainer.bootstrap(). */ function wireUiBridges() { if (wired) { log$45.debug("UI bridges already wired"); return; } wired = true; initSettingsPanelBridge(); initToastBridge(); BatchBar.init(); log$45.info("UI bridges wired"); } var log$44 = Logger.scope("Messaging"); var MessageType = /* @__PURE__ */ function(MessageType) { MessageType["SETTING_UPDATE"] = "SETTING_UPDATE"; MessageType["HISTORY_UPDATE"] = "HISTORY_UPDATE"; MessageType["UI_REFRESH"] = "UI_REFRESH"; return MessageType; }({}); var MessagingService = class { CHANNEL_NAME = SYSTEM_KEYS.MESSAGING_CHANNEL; tabId = Math.random().toString(36).substring(2, 11); channel = null; listeners = /* @__PURE__ */ new Set(); createChannel() { return new BroadcastChannel(this.CHANNEL_NAME); } onInit() { this.onCleanup(); this.channel = this.createChannel(); this.channel.onmessage = (event) => { const msg = event.data; if (msg.sourceTabId === this.tabId) return; log$44.debug(`Received ${msg.type} from other tab`); this.listeners.forEach((l) => l(msg)); }; } broadcast(type, payload) { if (!this.channel) this.channel = this.createChannel(); const msg = { type, payload, sourceTabId: this.tabId }; this.channel.postMessage(msg); log$44.debug(`Broadcasted ${type}`); } onMessage(handler) { this.listeners.add(handler); return () => this.listeners.delete(handler); } destroy() { this.listeners.clear(); this.channel?.close(); this.channel = null; } onCleanup() { this.destroy(); } }; /** Constructed once from app/modules (composition root). */ function createMessagingService() { return new MessagingService(); } var STATE_SERVICE_SENSITIVE_PROPS = [ "supabaseKey", "supabasePassword", "webdavPass" ]; var STATE_RUNTIME_ONLY_PROPS = [ "syncStatus", "isSelectionMode", "lastSyncTs" ]; var STATE_SERVICE_SENSITIVE_MAP = { supabaseKey: "SUPABASE_KEY", supabasePassword: "SUPABASE_PASSWORD", webdavPass: "WEBDAV_PASS" }; var createDefaultAppState = () => { return { previewMode: "static", hideNoMagnet: false, hideCensored: false, enableHistory: true, hideViewed: false, supjavSortByViews: false, loadExtraPreviews: false, enableQuickBar: true, showViewedBtn: true, showIdBadge: true, enableMagnets: true, enableExternalLinks: true, language: normalizeLocalePreference(Storage.get(STORAGE_KEYS.LANGUAGE, "auto") || "auto"), lastSyncTs: UI_CONSTANTS.DEFAULT_TIMESTAMP, supabaseUrl: Storage.get(STORAGE_KEYS.SUPABASE_URL, "") || "", supabaseKey: Storage.get(STORAGE_KEYS.SUPABASE_KEY, "") || "", supabaseEmail: Storage.get(STORAGE_KEYS.SUPABASE_EMAIL, "") || "", supabasePassword: Storage.get(STORAGE_KEYS.SUPABASE_PASSWORD, "") || "", webdavUrl: Storage.get(STORAGE_KEYS.WEBDAV_URL, "") || "", webdavUser: Storage.get(STORAGE_KEYS.WEBDAV_USER, "") || "", webdavPass: Storage.get(STORAGE_KEYS.WEBDAV_PASS, "") || "", webdavPath: Storage.get(STORAGE_KEYS.WEBDAV_PATH, UI_CONSTANTS.DEFAULT_SYNC_FILENAME) || UI_CONSTANTS.DEFAULT_SYNC_FILENAME, syncMode: "none", syncStatus: "idle", syncInterval: 5, enabledPortals: [ "supjav", "missav", "javdb", "javbus", "javlibrary", "dmm", "fc2", "fd2ppv", "fc2db", "sukebei", "fc2cmadb" ], userGridColumns: Number(Storage.get(STORAGE_KEYS.USER_GRID_COLUMNS, 0)) || 0, debugMode: Storage.get(STORAGE_KEYS.DEBUG_MODE, false) || false, cardDensity: Storage.get(STORAGE_KEYS.CARD_DENSITY, "standard") || "standard", cardPrimaryActions: [ "id", "viewed", "play", "preview", "magnet", "external" ], lastSyncResult: Storage.get(STORAGE_KEYS.LAST_SYNC_RESULT, null) || null, settingsCollapsedGroups: Storage.get(STORAGE_KEYS.SETTINGS_COLLAPSED_GROUPS, []) || [], settingsPreset: Storage.get(STORAGE_KEYS.SETTINGS_PRESET, "custom") || "custom", accentColor: Storage.get(STORAGE_KEYS.ACCENT_COLOR, "#ffffff") || "#ffffff", isSelectionMode: false }; }; /** Current state schema version — bump when adding/removing/rename fields */ var STATE_SCHEMA_VERSION = 3; var migrateState = (stored, version) => { if (version < 2) { delete stored["enableFollows"]; delete stored["supabaseKey"]; delete stored["supabasePassword"]; delete stored["webdavPass"]; } if (version < 3) { if (stored["cardDensity"] === "minimal") stored["cardDensity"] = "compact"; if (stored["cardDensity"] === "balanced") stored["cardDensity"] = "standard"; } return stored; }; var createStoredAppState = () => { const defaults = createDefaultAppState(); const stored = Storage.get(Config.STORAGE_KEYS.SETTINGS, {}) || {}; const storedVersion = stored._version ?? 1; if (storedVersion < STATE_SCHEMA_VERSION) migrateState(stored, storedVersion); if (stored.syncMode === void 0) { const value = Storage.get(STORAGE_KEYS.SYNC_MODE, "none"); stored.syncMode = [ "none", "supabase", "webdav" ].includes(value) ? value : "none"; } for (const prop of STATE_SERVICE_SENSITIVE_PROPS) delete stored[prop]; for (const prop of STATE_RUNTIME_ONLY_PROPS) delete stored[prop]; const knownKeys = new Set(Object.keys(defaults)); for (const key of Object.keys(stored)) if (key !== "_version" && !knownKeys.has(key)) delete stored[key]; const merged = { ...defaults, ...stored, _version: STATE_SCHEMA_VERSION }; merged.language = normalizeLocalePreference(String(merged.language || "auto")); return merged; }; var createPersistState = (log) => { return Utils.debounce((data) => { try { const appState = data; const { syncStatus: _syncStatus, lastSyncTs: _lastSyncTs, supabaseKey: _supabaseKey, supabasePassword: _supabasePassword, webdavPass: _webdavPass, isSelectionMode: _isSelectionMode, ...toSave } = appState; Storage.set(Config.STORAGE_KEYS.SETTINGS, toSave); for (const prop of STATE_SERVICE_SENSITIVE_PROPS) { const value = appState[prop]; if (typeof value !== "string" || !value) continue; const storageKey = STATE_SERVICE_SENSITIVE_MAP[prop]; if (storageKey) { log.debug(`Saving sensitive prop: ${prop} (len: ${value.length})`); Storage.set(STORAGE_KEYS[storageKey], value); } } } catch (error) { log.error("Failed to save state", error); } }, TIMING.DEBOUNCE_MS); }; var createStateProxy = ({ rawState, persistState, shouldBroadcast, broadcastUpdate, emitStateChange, emitLanguageChange }) => { return new Proxy(rawState, { get: (target, prop) => { return Reflect.get(target, prop); }, set: (target, prop, value) => { if (typeof prop !== "string" || !(prop in target)) { Reflect.set(target, prop, value); return true; } const key = prop; if (key === "language" && typeof value === "string") value = normalizeLocalePreference(value); const currentValue = target[key]; if (currentValue === value) return true; if (typeof currentValue === "object" && currentValue !== null && typeof value === "object" && value !== null) { if (Array.isArray(currentValue) && Array.isArray(value)) { if (currentValue.length === value.length && currentValue.every((v, i) => v === value[i])) return true; } else if (!Array.isArray(currentValue) && !Array.isArray(value)) { const curKeys = Object.keys(currentValue); const newKeys = Object.keys(value); if (curKeys.length === newKeys.length && curKeys.every((k) => currentValue[k] === value[k])) return true; } } target[key] = value; if (key === "syncStatus") { emitStateChange({ prop: key, value }); return true; } persistState(target); if (shouldBroadcast()) broadcastUpdate({ prop: key, value }); if (key === "cardPrimaryActions") CoreEvents.emit(AppEvents.CONFIG_CHANGED, { key, value }); emitStateChange({ prop: key, value }); if (key === "language") emitLanguageChange(value); return true; } }); }; var hydrateSensitiveState = (proxy, log) => { for (const prop of STATE_SERVICE_SENSITIVE_PROPS) { const sensitiveKey = STATE_SERVICE_SENSITIVE_MAP[prop]; if (!sensitiveKey) continue; const storageKey = STORAGE_KEYS[sensitiveKey]; const raw = Storage.get(storageKey, ""); log.debug(`Hydrating ${prop}: storageKey=${storageKey}, hasValue=${!!raw}, len=${raw?.length || 0}`); if (!raw) continue; proxy[prop] = raw; } }; var bindCrossTabStateSync = (onRemoteUpdate, messaging) => { const detachMessaging = messaging.onMessage((message) => { if (message.type !== MessageType.SETTING_UPDATE) return; onRemoteUpdate(message.payload); }); let detachGM = null; if (typeof GM_addValueChangeListener !== "undefined") { const listenerId = GM_addValueChangeListener(STORAGE_KEYS.SETTINGS, (_key, oldVal, newVal, remote) => { if (!remote) return; const oldObj = oldVal || {}; const newObj = newVal || {}; Object.keys(newObj).forEach((k) => { const prop = k; if (newObj[prop] !== oldObj[prop] && JSON.stringify(newObj[prop]) !== JSON.stringify(oldObj[prop])) onRemoteUpdate({ prop, value: newObj[prop] }); }); }); detachGM = () => GM_removeValueChangeListener(listenerId); } return () => { detachMessaging(); detachGM?.(); }; }; var log$43 = Logger.scope("State"); var StateService = class { deps; proxy; ready; skipBroadcast = false; resolveReady; detachCrossTabSync = null; detachDebugModeListener = null; constructor(deps) { this.deps = deps; this.ready = new Promise((resolve) => { this.resolveReady = resolve; }); const persistState = createPersistState(log$43); this.proxy = createStateProxy({ rawState: createStoredAppState(), persistState, shouldBroadcast: () => !this.skipBroadcast, broadcastUpdate: (change) => { this.deps.messaging.broadcast(MessageType.SETTING_UPDATE, change); }, emitStateChange: (change) => { CoreEvents.emit(AppEvents.STATE_CHANGED, { prop: change.prop, value: change.value }); }, emitLanguageChange: (language) => { CoreEvents.emit(AppEvents.LANGUAGE_CHANGED, language); } }); } onInit() { try { this.onCleanup(); log$43.debug("Initializing StateService (secret decryption)"); this.detachCrossTabSync = bindCrossTabStateSync(({ prop, value }) => { if (this.proxy[prop] === value) return; this.skipBroadcast = true; try { this.proxy[prop] = value; } finally { this.skipBroadcast = false; } }, this.deps.messaging); this.detachDebugModeListener = CoreEvents.on(AppEvents.STATE_CHANGED, ({ prop, value }) => { if (prop !== "debugMode") return; if (value) { Logger.enable(false); return; } Logger.disable(false); }); if (this.proxy.debugMode) Logger.enable(false); hydrateSensitiveState(this.proxy, log$43); } finally { this.resolveReady(); log$43.info("StateService ready"); } } onCleanup() { this.detachCrossTabSync?.(); this.detachCrossTabSync = null; this.detachDebugModeListener?.(); this.detachDebugModeListener = null; this.skipBroadcast = false; } get(key) { return this.proxy[key]; } set(key, value) { this.proxy[key] = value; } setDynamic(prop, value) { this.proxy[prop] = value; } toRecord() { return { ...this.proxy }; } on(prop, listener) { if (typeof prop === "function") { const handler = prop; return CoreEvents.on(AppEvents.STATE_CHANGED, handler); } return CoreEvents.on(AppEvents.STATE_CHANGED, (change) => { if (change.prop === prop && listener) listener(change.value); }); } }; var createStateService = (deps) => new StateService(deps); /** Settings keys allowed in backup import/export (whitelist). */ var ALLOWED_BACKUP_SETTINGS_KEYS = /* @__PURE__ */ new Set([ "previewMode", "hideNoMagnet", "hideCensored", "enableHistory", "hideViewed", "loadExtraPreviews", "enableQuickBar", "showViewedBtn", "showIdBadge", "enableMagnets", "enableExternalLinks", "language", "syncMode", "syncInterval", "enabledPortals", "userGridColumns", "debugMode", "cardDensity", "cardPrimaryActions", "settingsPreset" ]); var filterImportedSettings = (settings) => { const filtered = []; for (const [key, value] of Object.entries(settings)) if (ALLOWED_BACKUP_SETTINGS_KEYS.has(key)) filtered.push([key, value]); return filtered; }; var VALID_HISTORY_STATUSES = /* @__PURE__ */ new Set(["watched", "downloaded"]); var parseTimestamp = (value) => { if (typeof value === "number" && Number.isFinite(value) && value > 0) return value; if (typeof value === "string" && value.trim()) { const parsed = Date.parse(value); if (Number.isFinite(parsed) && parsed > 0) return parsed; } return null; }; var normalizeBinaryFlag = (value) => value === 1 || value === true ? 1 : 0; var getRecordTimestamp = (record) => { return parseTimestamp(record.updated_at) ?? parseTimestamp(record.timestamp) ?? 0; }; var shouldReplaceImportedRecord = (incoming, existing) => { return getRecordTimestamp(incoming) > getRecordTimestamp(existing); }; var normalizeHistoryItem = (value, now) => { if (!value || typeof value !== "object") return null; const candidate = value; const id = typeof candidate.id === "string" ? candidate.id.trim() : ""; const status = typeof candidate.status === "string" ? candidate.status : ""; if (!id || !VALID_HISTORY_STATUSES.has(status)) return null; const updatedAtTimestamp = parseTimestamp(candidate.updated_at) ?? parseTimestamp(candidate.timestamp) ?? now; return { id, status, timestamp: parseTimestamp(candidate.timestamp) ?? updatedAtTimestamp, updated_at: new Date(updatedAtTimestamp).toISOString(), is_deleted: normalizeBinaryFlag(candidate.is_deleted), sync_dirty: normalizeBinaryFlag(candidate.sync_dirty) }; }; var normalizeUniqueRecords = (items, normalizeItem, now) => { const dedupedItems = /* @__PURE__ */ new Map(); items.forEach((item) => { const normalized = normalizeItem(item, now); if (!normalized) return; const existing = dedupedItems.get(normalized.id); if (!existing || shouldReplaceImportedRecord(normalized, existing)) dedupedItems.set(normalized.id, normalized); }); return Array.from(dedupedItems.values()); }; var normalizeBackupPayload = (value, defaultAppName) => { const now = Date.now(); return { appName: typeof value.appName === "string" && value.appName.trim() ? value.appName : defaultAppName, version: value.version, timestamp: parseTimestamp(value.timestamp) ?? now, settings: value.settings, history: normalizeUniqueRecords(value.history, normalizeHistoryItem, now) }; }; var classifyBackupPayload = (data, maxVersion = 4) => { if (!data || typeof data !== "object") return { ok: false, errorCode: "invalid_payload" }; const candidate = data; const version = Number(candidate.version); if (!Number.isInteger(version) || version <= 0) return { ok: false, errorCode: "invalid_payload" }; if (version > maxVersion) return { ok: false, errorCode: "unsupported_version" }; if (!candidate.settings || typeof candidate.settings !== "object" || !Array.isArray(candidate.history)) return { ok: false, errorCode: "invalid_payload" }; const payload = { appName: typeof candidate.appName === "string" ? candidate.appName : "", version, timestamp: parseTimestamp(candidate.timestamp) ?? Date.now(), settings: candidate.settings, history: candidate.history }; return { ok: true, payload: normalizeBackupPayload(payload, payload.appName || "unknown") }; }; var parseBackupJson = (content, maxVersion = 4) => { let parsed; try { parsed = JSON.parse(content); } catch { return { ok: false, errorCode: "invalid_json" }; } return classifyBackupPayload(parsed, maxVersion); }; var buildBackupImportDiff = ({ payload, currentSettings, existingHistoryCount }) => { const settingsAdded = []; const settingsChanged = []; for (const [key, value] of filterImportedSettings(payload.settings || {})) { if (!(key in currentSettings)) { settingsAdded.push(String(key)); continue; } const currentValue = currentSettings[key]; if (JSON.stringify(currentValue) !== JSON.stringify(value)) settingsChanged.push(String(key)); } return { settingsAdded, settingsChanged, historyToImport: payload.history.length, existingHistoryCount }; }; var createBackupExportPayload$1 = ({ appName, settings, history, version = 4, timestamp = Date.now() }) => ({ appName, version, timestamp, settings, history }); var createBackupExportPayload = ({ settings, history }) => createBackupExportPayload$1({ appName: SCRIPT_INFO.NAME, settings, history }); var parseBackupContentWithMetadataDetailed = (content) => { return parseBackupJson(content, 4); }; var createBackupImportPreview = ({ payload, currentSettings, existingHistoryCount }) => ({ payload, diff: buildBackupImportDiff({ payload, currentSettings, existingHistoryCount }) }); var defaultTranslate$1 = (key, params) => { let value = { backupSettingsAdded: "Settings added: {count}", backupSettingsChanged: "Settings changed: {count}", backupHistoryRowsWithCurrent: "History rows to import: {count} (current: {current})", backupChangedKeys: "Changed keys: {keys}" }[key] || key; if (params) Object.entries(params).forEach(([paramKey, paramValue]) => { value = value.replace(new RegExp(`{${paramKey}}`, "g"), String(paramValue)); }); return value; }; var formatBackupImportDiff = exports("g",(diff, translate = defaultTranslate$1) => { const lines = [ translate("backupSettingsAdded", { count: diff.settingsAdded.length }), translate("backupSettingsChanged", { count: diff.settingsChanged.length }), translate("backupHistoryRowsWithCurrent", { count: diff.historyToImport, current: diff.existingHistoryCount }) ]; if (diff.settingsChanged.length > 0) lines.push(translate("backupChangedKeys", { keys: diff.settingsChanged.join(", ") })); return lines.join("\n"); }); var log$42 = Logger.scope("Backup"); var BackupService = class { deps; constructor(deps) { this.deps = deps; } get data() { return this.deps.data; } get state() { return this.deps.state; } onInit() { log$42.debug("Service initialized"); } async readFileAsText(file) { return await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (event) => resolve(String(event.target?.result || "")); reader.onerror = () => reject(/* @__PURE__ */ new Error("Failed to read backup file")); reader.readAsText(file); }); } async parseFile(file) { return parseBackupContentWithMetadataDetailed(await this.readFileAsText(file)); } async exportData() { const history = await this.data.history.getAll(); const data = createBackupExportPayload({ settings: Object.fromEntries(filterImportedSettings(this.state.proxy)), history }); try { const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10); anchor.href = url; anchor.download = `fc2_enhanced_backup_${dateStr}.json`; anchor.click(); setTimeout(() => { URL.revokeObjectURL(url); anchor.remove(); }, 100); log$42.info("Data exported successfully"); return true; } catch (error) { log$42.error("Export failed", error); return false; } } async previewImportDetailed(file) { try { const parsed = await this.parseFile(file); if (!parsed.ok) return { ok: false, errorCode: parsed.errorCode }; const historyCount = await this.data.db.history.count(); return { ok: true, preview: createBackupImportPreview({ payload: parsed.payload, currentSettings: this.state.proxy, existingHistoryCount: historyCount }) }; } catch (error) { log$42.error("Preview import failed", error); return { ok: false, errorCode: "read_failed" }; } } async previewImport(file) { return (await this.previewImportDetailed(file)).preview ?? null; } async importDataDetailed(file) { const previewResult = await this.previewImportDetailed(file); if (!previewResult.ok || !previewResult.preview) return { success: false, errorCode: previewResult.errorCode ?? "invalid_payload" }; try { await this.applyImport(previewResult.preview.payload); log$42.info("Backup imported", { diff: formatBackupImportDiff(previewResult.preview.diff) }); return { success: true, preview: previewResult.preview }; } catch (error) { log$42.error("Import failed during parsing/saving", error); return { success: false, preview: previewResult.preview, errorCode: "apply_failed" }; } } async importData(file) { return (await this.importDataDetailed(file)).success; } async applyImport(data) { log$42.debug("Importing settings..."); const settings = data.settings || {}; for (const [key, value] of filterImportedSettings(settings)) { const nextValue = key === "language" && typeof value === "string" ? normalizeLocalePreference(value) : value; this.state.proxy[key] = nextValue; } log$42.debug("Importing history..."); await this.data.db.transaction("rw", [this.data.db.history], async () => { if (Array.isArray(data.history)) await this.data.db.history.bulkPut(data.history.map((item) => ({ ...item, sync_dirty: 1 }))); }); } }; /** Constructed once from app/modules (composition root). */ function createBackupService(deps) { return new BackupService(deps); } var ENH_GRID_CONTAINER_ATTR = "data-enh-grid-container"; var ENH_GRID_CONTAINER_SELECTOR = `[${ENH_GRID_CONTAINER_ATTR}]`; var markGridContainer = (container) => { if (!container) return; container.setAttribute(ENH_GRID_CONTAINER_ATTR, "1"); }; var SITE_GRID_MAP = [ ["fd2ppv.cc", { cont: ENH_GRID_CONTAINER_SELECTOR, card: `> .${Config.CLASSES.cardRebuilt}` }], ["supjav.com", { cont: ENH_GRID_CONTAINER_SELECTOR, card: `> .${Config.CLASSES.cardRebuilt}` }], ["missav", { cont: ENH_GRID_CONTAINER_SELECTOR, card: `> .${Config.CLASSES.cardRebuilt}` }], ["javdb", { cont: ENH_GRID_CONTAINER_SELECTOR, card: `> .${Config.CLASSES.cardRebuilt}` }], ["fc2db", { cont: ENH_GRID_CONTAINER_SELECTOR, card: `> .${Config.CLASSES.cardRebuilt}` }], ["javfc2.xyz", { cont: `${ENH_GRID_CONTAINER_SELECTOR}:not(.owl-stage)`, card: `> *` }], ["fc2cmadb.com", { cont: ENH_GRID_CONTAINER_SELECTOR, card: `> .${Config.CLASSES.cardRebuilt}` }] ]; var shouldSkipGridApply = (hostname, pathname) => { if (!hostname.includes("missav")) return false; return /\/(cn\/|en\/|ja\/)?(fc2-ppv-|[a-z]{2,5}-)\d+/i.test(pathname); }; var resolveGridSelectors = (hostname) => { return SITE_GRID_MAP.find(([match]) => hostname.includes(match))?.[1] ?? null; }; var buildGridCss = (hostname, pathname, cols) => { if (shouldSkipGridApply(hostname, pathname)) return ""; const selectors = resolveGridSelectors(hostname); if (!selectors || cols <= 0) return ""; const cardRules = selectors.cont.split(",").map((selector) => selector.trim()).map((selector) => `${selector} ${selectors.card}`).join(", "); const baseGridCss = ` display: grid !important; grid-template-columns: repeat(${Math.min(2, cols)}, minmax(0, 1fr)) !important; gap: var(--fc2-space-md) !important; margin: 0 !important; padding: var(--fc2-space-md) 10px !important; width: 100% !important; max-width: none !important; box-sizing: border-box !important; `; const cardCss = selectors.card ? `${cardRules} { padding: 0 !important; margin: 0 !important; width: 100% !important; max-width: none !important; flex: none !important; box-sizing: border-box !important; }` : ""; let siteSpecificCss = ""; if (hostname.includes("javfc2.xyz")) { const C = Config.CLASSES; siteSpecificCss = ` ${selectors.cont} > [class*="col-"] { width: 100% !important; max-width: none !important; flex: none !important; padding: 0 !important; display: flex !important; } .${C.cardRebuilt} { width: 100% !important; max-width: none !important; height: 100% !important; display: flex !important; } .${C.processedCard} { width: 100% !important; height: 100% !important; flex: 1 1 auto !important; } .${C.videoPreviewContainer} { aspect-ratio: 3 / 4 !important; background-color: var(--fc2-bg) !important; flex: 0 0 auto !important; } .${C.infoArea} { flex: 1 1 auto !important; min-height: calc(var(--fc2-btn-lg) + var(--fc2-space-xl) + var(--fc2-space-xs)) !important; padding: var(--fc2-space-sm) !important; } `; } const tabletCols = Math.min(cols, 4); return ` ${siteSpecificCss} ${selectors.cont} { ${baseGridCss} } ${cardCss} ${selectors.cont} .inner { padding: 0 !important; } @media (max-width: 480px) { ${selectors.cont} { grid-template-columns: minmax(0, 1fr) !important; gap: var(--fc2-space-sm) !important; } } @media (min-width: 768px) { ${selectors.cont} { grid-template-columns: repeat(${tabletCols}, minmax(0, 1fr)) !important; } } @media (min-width: 1200px) { ${selectors.cont} { grid-template-columns: repeat(${cols}, minmax(0, 1fr)) !important; } } `; }; var GridService = class { styleEl = null; cleanupFns = []; onBootstrap() { this.onCleanup(); this.cleanupFns.push(CoreEvents.on(AppEvents.GRID_CHANGED, (cols) => this.apply(cols))); this.cleanupFns.push(CoreEvents.on(AppEvents.STATE_CHANGED, ({ prop, value }) => { if (prop === "userGridColumns") this.apply(Number(value)); })); const cols = resolveService(STATE_SERVICE).proxy.userGridColumns; if (cols > 0) this.apply(cols); } onCleanup() { this.cleanupFns.splice(0).forEach((cleanup) => cleanup()); this.styleEl?.remove(); this.styleEl = null; } apply(cols) { if (!document.head) return; if (!this.styleEl) { this.styleEl = document.createElement("style"); this.styleEl.id = "fc2-modern-grid-style"; document.head.appendChild(this.styleEl); } this.styleEl.textContent = buildGridCss(location.hostname, location.pathname, cols); } }; /** Constructed once from app/modules (composition root). */ function createGridManager() { return new GridService(); } /** Canonical media ID normalization (FC2 numeric + JAV codes). */ var IdNormalizer = { normalize(raw) { if (!raw) return ""; const trimmed = raw.trim().toUpperCase(); const fc2Match = trimmed.match(/^(?:FC2|PPV)?-?(\d{5,8})$/); if (fc2Match && fc2Match[1]) return fc2Match[1].replace(/^0+/, "") || ""; if (trimmed.match(VALIDATION.JAV_ID_REGEX)) return trimmed; return trimmed; }, isSame(id1, id2) { const n1 = this.normalize(id1); const n2 = this.normalize(id2); if (!n1 || !n2) return false; if (n1 === n2) return true; if (n1.replace(/-/g, "") === n2.replace(/-/g, "")) return true; return false; } }; var touchHistoryCacheEntry = (cache, key, value, maxSize) => { if (cache.has(key)) cache.delete(key); cache.set(key, value); if (cache.size > maxSize) { const firstKey = cache.keys().next().value; if (firstKey) cache.delete(firstKey); } }; var hydrateHistoryCache = (cache, items, normalizeId) => { cache.clear(); for (let index = items.length - 1; index >= 0; index--) { const item = items[index]; if (item && !item.is_deleted) cache.set(normalizeId(item.id), item.status || "watched"); } }; var hasHistoryEntry = (enabled, cache, id, status) => { if (!enabled) return false; if (status) return cache.get(id) === status; return cache.has(id); }; var log$41 = Logger.scope("History"); var HistoryService = class { deps; CACHE_SIZE = 5e3; historyCache = /* @__PURE__ */ new Map(); detachMessageListener = null; detachTimelineListeners = []; lastTimelineGcTime = 0; loadReadyPromise = Promise.resolve(); constructor(deps) { this.deps = deps; } get data() { return this.deps.data; } get state() { return this.deps.state; } get messaging() { return this.deps.messaging; } get historyEnabled() { return !!this.state.get("enableHistory"); } getHistorySnapshot() { return this.historyCache; } onBootstrap() { this.onCleanup(); log$41.debug("Warming up cache"); this.loadReadyPromise = this.load().catch((error) => log$41.error("Failed to load history cache", error)); this.detachMessageListener = this.messaging.onMessage((message) => { if (message.type !== MessageType.HISTORY_UPDATE) return; const { action, id, status } = message.payload; if (action === "add" && status) { this.add(id, status, true); return; } if (action === "remove") { this.remove(id, true); return; } if (action === "add-batch" && message.payload.ids && status) { this.addBatch(message.payload.ids, status, true); return; } if (action === "remove-batch" && message.payload.ids) { this.removeBatch(message.payload.ids, true); return; } if (action === "clear") this.clear(true); }); this.detachTimelineListeners.push(CoreEvents.on(AppEvents.CARD_DISCOVERED, (payload) => { this.record("card", "discovered", payload.id).catch((e) => log$41.debug("timeline record failed", e)); })); this.detachTimelineListeners.push(CoreEvents.on(AppEvents.MAGNET_FOUND, (payload) => { this.record("magnet", "found", payload.id).catch((e) => log$41.debug("timeline record failed", e)); })); this.detachTimelineListeners.push(CoreEvents.on(AppEvents.HISTORY_CHANGED, () => { this.record("history", "changed").catch((e) => log$41.debug("timeline record failed", e)); })); this.detachTimelineListeners.push(CoreEvents.on(AppEvents.SYNC_STATUS_CHANGED, (status) => { if (status === "success") this.record("sync", "completed").catch((e) => log$41.debug("timeline record failed", e)); })); } onCleanup() { this.detachMessageListener?.(); this.detachMessageListener = null; this.detachTimelineListeners.forEach((fn) => fn()); this.detachTimelineListeners = []; } getCache() { return new Map(this.historyCache); } async waitUntilReady() { await this.loadReadyPromise; } async load() { Logger.time("HistoryService.load"); if (!this.historyEnabled) { this.historyCache.clear(); Logger.timeEnd("HistoryService.load"); return; } try { let items = []; try { items = await this.data.history.table.orderBy("timestamp").reverse().limit(this.CACHE_SIZE).toArray(); } catch (indexError) { log$41.warn("Index scan failed, falling back to full scan", indexError); items = await this.data.history.table.toArray(); items.sort((left, right) => right.timestamp - left.timestamp); if (items.length > this.CACHE_SIZE) items = items.slice(0, this.CACHE_SIZE); } hydrateHistoryCache(this.historyCache, items, (id) => this.getNormalizedId(id)); CoreEvents.emit(AppEvents.HISTORY_LOADED, items.length); log$41.info(`Loaded ${items.length} recent items`); } catch (error) { log$41.error("Failed to load history", error); this.historyCache.clear(); } Logger.timeEnd("HistoryService.load"); } async add(id, status = "watched", remote = false) { if (!this.historyEnabled || !id) return; const normalizedId = this.getNormalizedId(String(id)); if (this.historyCache.get(normalizedId) === status) { this.touch(normalizedId, status); if (!remote) return; } const previousStatus = this.historyCache.get(normalizedId); this.touch(normalizedId, status); if (!remote) { try { await this.data.history.add(normalizedId, status); } catch (error) { log$41.error(`Failed to persist history add for ${normalizedId}`, error); if (previousStatus) this.touch(normalizedId, previousStatus); else this.historyCache.delete(normalizedId); return; } this.messaging.broadcast(MessageType.HISTORY_UPDATE, { action: "add", id: normalizedId, status }); } CoreEvents.emit(AppEvents.HISTORY_ADDED, { id: normalizedId, status }); log$41.debug(`${remote ? "Remote" : "Local"} added: ${normalizedId} as ${status}`); if (!remote) CoreEvents.emit(AppEvents.HISTORY_CHANGED, {}); } async remove(id, remote = false) { if (!this.historyEnabled || !id) return; const normalizedId = this.getNormalizedId(String(id)); const previousStatus = this.historyCache.get(normalizedId); this.historyCache.delete(normalizedId); if (!remote) { try { await this.data.history.remove(normalizedId); } catch (error) { log$41.error(`Failed to persist history remove for ${normalizedId}`, error); if (previousStatus) this.touch(normalizedId, previousStatus); return; } this.messaging.broadcast(MessageType.HISTORY_UPDATE, { action: "remove", id: normalizedId }); } CoreEvents.emit(AppEvents.HISTORY_REMOVED, { id: normalizedId, prevStatus: previousStatus }); log$41.debug(`${remote ? "Remote" : "Local"} removed: ${normalizedId}`); if (!remote) CoreEvents.emit(AppEvents.HISTORY_CHANGED, {}); } async addBatch(ids, status = "watched", remote = false) { if (!this.historyEnabled || ids.length === 0) return; const normalizedIds = ids.map((id) => this.getNormalizedId(id)); const previousStatuses = /* @__PURE__ */ new Map(); normalizedIds.forEach((id) => { previousStatuses.set(id, this.historyCache.get(id)); this.touch(id, status); }); if (!remote) try { await this.data.history.bulkAdd(normalizedIds, status); this.messaging.broadcast(MessageType.HISTORY_UPDATE, { action: "add-batch", ids: normalizedIds, status }); } catch (error) { log$41.error(`Failed to persist history bulk add`, error); previousStatuses.forEach((prev, id) => { if (prev) this.touch(id, prev); else this.historyCache.delete(id); }); return; } normalizedIds.forEach((id) => { CoreEvents.emit(AppEvents.HISTORY_ADDED, { id, status }); }); if (!remote) CoreEvents.emit(AppEvents.HISTORY_CHANGED, {}); } async removeBatch(ids, remote = false) { if (!this.historyEnabled || ids.length === 0) return; const normalizedIds = ids.map((id) => this.getNormalizedId(id)); const prevStates = /* @__PURE__ */ new Map(); normalizedIds.forEach((id) => { prevStates.set(id, this.historyCache.get(id)); this.historyCache.delete(id); }); if (!remote) try { await this.data.history.bulkRemove(normalizedIds); this.messaging.broadcast(MessageType.HISTORY_UPDATE, { action: "remove-batch", ids: normalizedIds }); } catch (error) { log$41.error(`Failed to persist history bulk remove`, error); prevStates.forEach((status, id) => { if (status) this.touch(id, status); }); return; } normalizedIds.forEach((id) => { CoreEvents.emit(AppEvents.HISTORY_REMOVED, { id, prevStatus: prevStates.get(id) }); }); if (!remote) CoreEvents.emit(AppEvents.HISTORY_CHANGED, {}); } async clear(remote = false) { const count = this.historyCache.size; const backup = new Map(this.historyCache); this.historyCache.clear(); if (!remote) { try { await this.data.history.clear(); } catch (error) { log$41.error("Failed to persist history clear", error); backup.forEach((value, key) => this.historyCache.set(key, value)); return; } this.messaging.broadcast(MessageType.HISTORY_UPDATE, { action: "clear" }); } CoreEvents.emit(AppEvents.HISTORY_CLEARED, count); log$41.warn(`${remote ? "Remote" : "Local"} cleared ${count} items`); if (!remote) CoreEvents.emit(AppEvents.HISTORY_CHANGED, {}); } async record(category, action, detail) { try { await this.data.timeline.add({ timestamp: Date.now(), category, action, detail }); await this.gcTimeline(); } catch (e) { log$41.warn("Failed to record timeline event", e); } } async getEvents(options) { try { return await this.data.timeline.list({ category: options?.category, limit: options?.limit ?? 100 }); } catch { return []; } } async gcTimeline() { const now = Date.now(); if (now - this.lastTimelineGcTime < TIMELINE.GC_INTERVAL_MS) return; this.lastTimelineGcTime = now; try { const threshold = now - TIMELINE.RETENTION_MS; await this.data.timeline.purgeBefore(threshold); } catch (e) { log$41.debug("Timeline GC failed", e); } } has(id, status) { return hasHistoryEntry(this.historyEnabled, this.historyCache, this.getNormalizedId(String(id)), status); } getStatus(id) { return this.historyCache.get(this.getNormalizedId(String(id))) || null; } touch(key, value) { touchHistoryCacheEntry(this.historyCache, key, value, this.CACHE_SIZE); } getNormalizedId(id) { return IdNormalizer.normalize(id); } }; /** Constructed once from app/modules (composition root). */ function createHistoryService(deps) { return new HistoryService(deps); } var createNegativeMagnetCacheValue = () => ({ kind: "no-magnet", expiresAt: Date.now() + CACHE.NEGATIVE_MAGNET_EXPIRATION_MS, retryable: true }); var isNegativeMagnetCacheValue = (value) => { return value === "@@NO_MAGNET@@" || !!value && typeof value === "object" && value.kind === "no-magnet"; }; var isExpiredNegativeMagnetCacheValue = (value) => { if (value === "@@NO_MAGNET@@") return false; if (!isNegativeMagnetCacheValue(value)) return false; return typeof value.expiresAt === "number" && value.expiresAt <= Date.now(); }; var log$40 = Logger.scope("Magnet"); var MagnetService = class { deps; constructor(deps) { this.deps = deps; } get data() { return this.deps.data; } get scraper() { return this.deps.scraper; } queue = /* @__PURE__ */ new Map(); activeSearches = /* @__PURE__ */ new Set(); taskTimeouts = /* @__PURE__ */ new Map(); maxConcurrency = MAGNET_CONFIG.MAX_CONCURRENCY; onProgress = null; flushTimer = null; detachUiReady = null; onBootstrap() { log$40.debug("Subscribed to UI_READY"); this.detachUiReady?.(); this.detachUiReady = CoreEvents.on(AppEvents.UI_READY, () => { log$40.info("Manager active"); }); } onCleanup() { this.detachUiReady?.(); this.detachUiReady = null; if (this.flushTimer) { clearTimeout(this.flushTimer); this.flushTimer = null; } this.taskTimeouts.forEach((timer) => clearTimeout(timer)); this.taskTimeouts.clear(); this.queue.forEach((task) => { if (task.status !== "found" && task.status !== "failed") { task.status = "failed"; task.resolve(null); } }); this.queue.clear(); this.activeSearches.clear(); this._updateStatus(); } async fetchMagnet(id, type, options = {}) { if (options.forceRetry) await this.data.cache.delete(id); const cached = await this.data.cache.get(id); if (cached) { if (isNegativeMagnetCacheValue(cached)) { this._notifyUI(id, "failed"); return null; } this._notifyUI(id, "found", cached); return cached; } const existingTask = this.queue.get(id); if (existingTask) return new Promise((resolve) => { const originalResolve = existingTask.resolve; existingTask.resolve = (url) => { originalResolve(url); resolve(url); }; }); return new Promise((resolve) => { const task = { id, type: type || MAGNET_CONFIG.DEFAULT_TYPE, resolve, retryCount: 0, status: "pending", startTime: Date.now() }; this.queue.set(id, task); const timeoutHandle = setTimeout(() => { this.taskTimeouts.delete(id); const currentTask = this.queue.get(id); if (currentTask === task && currentTask.status !== "found" && currentTask.status !== "failed") { log$40.warn(`Search timed out for ${task.id}`); currentTask.status = "failed"; this._notifyUI(task.id, "failed"); currentTask.resolve(null); this.queue.delete(id); } }, MAGNET_CONFIG.SEARCH_TIMEOUT_MS); this.taskTimeouts.set(id, timeoutHandle); this._requestProcess(); }); } async retryFailedMagnet(id, type) { return await this.fetchMagnet(id, type, { forceRetry: true }); } predictiveSearch(card) { const { id, type } = card.dataset; if (id && type && !this.queue.has(id) && this.queue.size < MAGNET_CONFIG.PREDICTIVE_LIMIT) this.fetchMagnet(id, type); } _requestProcess() { if (this.flushTimer) clearTimeout(this.flushTimer); this.flushTimer = setTimeout(() => { this._processQueue(); this.flushTimer = null; }, 100); } async _processQueue() { if (this.activeSearches.size >= this.maxConcurrency) return; const batch = Array.from(this.queue.values()).filter((task) => task.status === "pending").sort((left, right) => left.startTime - right.startTime).slice(0, this.maxConcurrency - this.activeSearches.size); if (batch.length === 0) return; batch.forEach((task) => { task.status = "searching"; this.activeSearches.add(task.id); }); this._updateStatus(); try { await this.scraper.fetchMagnets(batch.map((task) => ({ id: task.id, type: task.type })), (id, url) => { const task = this.queue.get(id); if (!task) return; if (url) this._onTaskSuccess(task, url); else this._onTaskFailed(task, true); }); } catch (error) { log$40.error("Batch search error", error); batch.forEach((task) => { if (task.status === "searching") this._onTaskFailed(task, true); }); } finally { batch.forEach((task) => this.activeSearches.delete(task.id)); this._requestProcess(); } } async _onTaskSuccess(task, url) { this._clearTaskTimeout(task.id); await this.data.cache.set(task.id, url); task.status = "found"; task.resolve(url); this.queue.delete(task.id); this._updateStatus(); this._notifyUI(task.id, "found", url); } async _onTaskFailed(task, forceFail = false) { const maxRetries = MAGNET_CONFIG.MAX_RETRIES; if (!forceFail && task.retryCount < maxRetries) { task.retryCount++; task.status = "pending"; task.startTime = Date.now() + Math.pow(2, task.retryCount) * MAGNET_CONFIG.RETRY_DELAY; log$40.debug(`Retrying ${task.id} (${task.retryCount}/${maxRetries})`); this._updateStatus(); return; } this._clearTaskTimeout(task.id); task.status = "failed"; task.resolve(null); this.queue.delete(task.id); this._notifyUI(task.id, "failed"); await this.data.cache.set(task.id, createNegativeMagnetCacheValue()); this._updateStatus(); } _notifyUI(id, status, url) { if (status === "found" && url) CoreEvents.emit(AppEvents.MAGNET_FOUND, { id, url }); else if (status === "failed") CoreEvents.emit(AppEvents.MAGNET_FAILED, { id }); } _updateStatus() { if (!this.onProgress) return; this.onProgress({ total: this.queue.size, active: this.activeSearches.size, found: Array.from(this.queue.values()).filter((t) => t.status === "found").length, failed: Array.from(this.queue.values()).filter((t) => t.status === "failed").length }); } _clearTaskTimeout(id) { const timer = this.taskTimeouts.get(id); if (timer) { clearTimeout(timer); this.taskTimeouts.delete(id); } } }; /** Constructed once from app/modules (composition root). */ function createMagnetService(deps) { return new MagnetService(deps); } var log$39 = Logger.scope("Menu"); var MenuService = class { menuIds = []; onBootstrap() { this.onCleanup(); log$39.debug("Registering UserScript menus"); this.register(); } onCleanup() { if (typeof GM_unregisterMenuCommand === "undefined") { this.menuIds = []; return; } this.menuIds.forEach((id) => { try { GM_unregisterMenuCommand(id); } catch {} }); this.menuIds = []; } register() { this.onCleanup(); if (typeof GM_registerMenuCommand === "undefined") return; const settingsId = GM_registerMenuCommand(t("menuOpenSettings"), () => { CoreEvents.emit(AppEvents.OPEN_SETTINGS, {}); }, "s"); this.menuIds.push(settingsId); } }; /** Constructed once from app/modules (composition root). */ function createMenuService() { return new MenuService(); } var PORTAL_CATALOG = [ { id: "dmm", name: "DMM", urlTemplate: EXTERNAL_URLS.DMM, shouldShow: ({ hostname }) => !hostname.includes("dmm.co.jp") }, { id: "fc2", name: "FC2", urlTemplate: EXTERNAL_URLS.FC2, shouldShow: ({ type, hostname }) => type === "fc2" && !hostname.includes("fc2.com") }, { id: "supjav", name: "Supjav", urlTemplate: EXTERNAL_URLS.SUPJAV, shouldShow: ({ hostname }) => !hostname.includes("supjav") }, { id: "missav", name: "MissAV", urlTemplate: "", shouldShow: ({ hostname }) => !hostname.includes("missav") }, { id: "javdb", name: "JavDB", urlTemplate: EXTERNAL_URLS.JAVDB, shouldShow: ({ hostname }) => !hostname.includes("javdb") }, { id: "javbus", name: "JavBus", urlTemplate: EXTERNAL_URLS.JAVBUS, shouldShow: ({ hostname }) => !hostname.includes("javbus") }, { id: "javlibrary", name: "JavLibrary", urlTemplate: EXTERNAL_URLS.JAVLIBRARY, shouldShow: ({ hostname }) => !hostname.includes("javlibrary") }, { id: "fd2ppv", name: "FD2PPV", urlTemplate: EXTERNAL_URLS.FD2PPV, shouldShow: ({ type, hostname }) => type === "fc2" && !hostname.includes("fd2ppv") }, { id: "fc2db", name: "FC2DB", urlTemplate: EXTERNAL_URLS.FC2DB, shouldShow: ({ type, hostname }) => type === "fc2" && !hostname.includes("fc2db") }, { id: "sukebei", name: "Sukebei", urlTemplate: EXTERNAL_URLS.SUKEBEI, shouldShow: () => true }, { id: "fc2cmadb", name: "FC2CMADB", urlTemplate: EXTERNAL_URLS.FC2CMADB, shouldShow: ({ type, hostname }) => type === "fc2" && !hostname.includes("fc2cmadb") } ]; var resolvePortalUrlTemplate = (portal, type) => { if (portal.id !== "missav") return portal.urlTemplate; return type === "fc2" ? EXTERNAL_URLS.MISSAV_FC2 : EXTERNAL_URLS.MISSAV; }; var createAvailablePortals = ({ portals, enabledPortals, hostname, id, type }) => { return portals.filter((portal) => enabledPortals.includes(portal.id) && portal.shouldShow({ id, type, hostname })).map((portal) => ({ id: portal.id, name: portal.name, url: resolvePortalUrlTemplate(portal, type).replace("{id}", () => { const dateParts = MediaUtils.parseDateIdParts(id); if (portal.id === "sukebei" && dateParts) { const sep = dateParts.brand === "CARIBBEANCOM" ? "-" : "_"; return `${dateParts.date}${sep}${dateParts.serial}-${dateParts.short}`; } if (portal.id === "missav") { let missavId = id.toLowerCase(); if (missavId.startsWith("1pondo-") || missavId.startsWith("1pon-")) missavId = missavId.replace(/^(1pondo-|1pon-)/, "pondo-"); if (missavId.startsWith("10musume-") || missavId.startsWith("10mu-")) missavId = missavId.replace(/^(10musume-|10mu-)/, "musume-"); if (/^(pondo|pacopacomama|musume|caribbeancom)-/.test(missavId)) missavId = missavId.replace(/-(\d{2,4})$/, "_$1"); return missavId; } return id; }) })); }; var getAllPortals = (portals) => { return portals.map((portal) => ({ id: portal.id, name: portal.name })); }; var getAllPortalSites = (portals) => { return Array.from(new Set(portals.map((portal) => portal.id))); }; var DEFAULT_PORTAL_CACHE_MAX_SIZE = 300; var DEFAULT_PORTAL_CACHE_TTL_MS = 6e5; var PortalService = class { cache = /* @__PURE__ */ new Map(); portals; cacheMaxSize; cacheTtlMs; detachEnabledPortalsListener = null; constructor(portals = PORTAL_CATALOG, options = {}) { this.portals = portals; this.cacheMaxSize = options.cacheMaxSize ?? DEFAULT_PORTAL_CACHE_MAX_SIZE; this.cacheTtlMs = options.cacheTtlMs ?? DEFAULT_PORTAL_CACHE_TTL_MS; } onInit() { this.onCleanup(); this.detachEnabledPortalsListener = resolveService(STATE_SERVICE).on("enabledPortals", () => this.clearCache()); } onCleanup() { this.detachEnabledPortalsListener?.(); this.detachEnabledPortalsListener = null; this.clearCache(); } getAvailablePortals(data) { this.evictExpiredEntries(); const hostname = location.hostname; const { id, type } = data; const cacheKey = `${id}-${type}-${hostname}`; const cached = this.cache.get(cacheKey); if (cached) { this.cache.delete(cacheKey); this.cache.set(cacheKey, cached); return cached.results; } const results = createAvailablePortals({ portals: this.portals, enabledPortals: resolveService(STATE_SERVICE).proxy.enabledPortals || [], hostname, id, type }); this.cache.set(cacheKey, { results, timestamp: Date.now() }); this.enforceCacheLimit(); return results; } getAllPortals() { return getAllPortals(this.portals); } getAllSites() { return getAllPortalSites(this.portals); } clearCache() { this.cache.clear(); } evictExpiredEntries() { if (this.cache.size === 0) return; const now = Date.now(); for (const [key, entry] of this.cache.entries()) if (now - entry.timestamp >= this.cacheTtlMs) this.cache.delete(key); } enforceCacheLimit() { if (this.cache.size <= this.cacheMaxSize) return; const overflow = this.cache.size - this.cacheMaxSize; const keysToDelete = this.cache.keys(); for (let i = 0; i < overflow; i += 1) { const oldestKey = keysToDelete.next().value; if (!oldestKey) break; this.cache.delete(oldestKey); } } }; /** Constructed once from app/modules (composition root). */ function createPortalService() { return new PortalService(); } var log$38 = Logger.scope("Preview"); var PreviewService = class { cache = /* @__PURE__ */ new Map(); maxCacheSize = CACHE.PREVIEW_MAX_SIZE; inflightPreviewRequests = /* @__PURE__ */ new Map(); lastPreviewRequestAt = Number.NEGATIVE_INFINITY; previewCooldownUntil = 0; onBootstrap() { log$38.debug("Preview service bootstrapped"); } onCleanup() { this.clearCache(); this.inflightPreviewRequests.clear(); this.previewCooldownUntil = 0; this.lastPreviewRequestAt = Number.NEGATIVE_INFINITY; } async getPreviewBlob(id, type, previewSlug, signal) { const url = this.getPreviewUrl(id, type, previewSlug); const inflight = this.inflightPreviewRequests.get(url); if (inflight) return inflight; const now = Date.now(); if (now < this.previewCooldownUntil) { const err = /* @__PURE__ */ new Error("Preview cooldown active"); err.status = 403; throw err; } const elapsed = now - this.lastPreviewRequestAt; if (elapsed < TIMING.POLITE_DELAY_MS) { const err = /* @__PURE__ */ new Error("Rate limited"); err.retryAfterMs = TIMING.POLITE_DELAY_MS - elapsed; throw err; } this.lastPreviewRequestAt = now; const request = fetchBlob(url, {}, signal).catch((error) => { if (error?.status === 403 || error?.status === 429 || error?.status === 503) this.previewCooldownUntil = Date.now() + TIMING.RATE_LIMIT_BACKOFF_MS; throw error; }).finally(() => { if (this.inflightPreviewRequests.get(url) === request) this.inflightPreviewRequests.delete(url); }); this.inflightPreviewRequests.set(url, request); return request; } getPreviewUrl(id, type, previewSlug) { if (previewSlug) return `${EXTERNAL_URLS.FOURHOI_BASE}/${previewSlug.toLowerCase()}/preview.mp4`; if (type === "fc2") return `${EXTERNAL_URLS.FOURHOI_BASE}/fc2-ppv-${id}/preview.mp4`; return `${EXTERNAL_URLS.FOURHOI_BASE}/${id.toLowerCase()}/preview.mp4`; } async checkExistence(id, type, previewSlug) { const url = this.getPreviewUrl(id, type, previewSlug); return new Promise((resolve) => { GM_xmlhttpRequest({ method: "HEAD", url, timeout: 5e3, onload: (response) => resolve(response.status >= 200 && response.status < 300), onerror: () => resolve(false), ontimeout: () => resolve(false) }); }); } getCachedElement(id) { const entry = this.cache.get(id); if (entry) { entry.timestamp = Date.now(); return entry.element; } } cacheElement(id, element) { if (this.cache.has(id)) { this.cache.get(id).timestamp = Date.now(); return; } if (this.cache.size >= this.maxCacheSize) { const oldest = Array.from(this.cache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp)[0]; if (oldest) { this.disposeElement(oldest[1].element); this.cache.delete(oldest[0]); } } this.cache.set(id, { url: element.src, element, timestamp: Date.now() }); } clearCache() { this.cache.forEach((item) => this.disposeElement(item.element)); this.cache.clear(); log$38.debug("Cache cleared"); } disposeElement(element) { if (element instanceof HTMLVideoElement) { const src = element.src || element.currentSrc || element.getAttribute("src"); element.pause(); element.removeAttribute("src"); try { element.load(); } catch {} element.remove(); if (src?.startsWith("blob:")) URL.revokeObjectURL(src); } else element.remove(); } }; /** Constructed once from app/modules (composition root). */ function createPreviewService() { return new PreviewService(); } /** * Fetch HTML content from a URL and parse it into a Document. * Returns structured result so callers can distinguish "empty" from "failed". * HTTP errors (with status codes) are re-thrown so callers can handle 429, 403, etc. */ var fetchAndParseHtml = async (url, options) => { try { const rawHtml = await http(url, { type: "text", ...options }); return { doc: new DOMParser().parseFromString(rawHtml, "text/html") }; } catch (e) { if (e && typeof e === "object" && "status" in e) throw e; return { doc: null, error: { message: e instanceof Error ? e.message : "Unknown fetch error" } }; } }; var resolveScraperHealthUrl = (target, paipanconBaseUrl) => { if (target === "sukebei") return "https://sukebei.nyaa.si"; if (target === "fd2") return "https://fd2ppv.cc"; return paipanconBaseUrl; }; var detectScraperHealthStatus = (html) => { const doc = new DOMParser().parseFromString(html, "text/html"); if (doc.title && CLOUDFLARE_INDICATORS.some((indicator) => doc.title.includes(indicator))) return "cf"; return "ok"; }; var groupMagnetRequestItems = (items) => { const grouped = /* @__PURE__ */ new Map(); items.forEach((item) => { const key = item.type || "fc2"; if (!grouped.has(key)) grouped.set(key, []); grouped.get(key).push(item); }); return grouped; }; var chunkMagnetIds = (ids) => { const chunkSize = NETWORK.CHUNK_SIZE; const chunks = []; for (let index = 0; index < ids.length; index += chunkSize) chunks.push(ids.slice(index, index + chunkSize)); return chunks; }; var createPreviewExistsCacheKey = (fc2Id) => `has_previews_${fc2Id}`; var log$37 = Logger.scope("ActressProvider"); var ActressProvider = class { async fetchActressFromFD2(id) { const cacheKey = `actress_${id}`; const cached = await resolveService(DATA_SERVICE).cache.get(cacheKey); if (cached) { const cleanCached = MediaUtils.cleanActressName(cached); if (cleanCached) { log$37.debug(`Actress cache hit: ${id} = ${cleanCached}`); return cleanCached; } else { log$37.warn(`Removing invalid cached actress for ${id}: ${cached}`); await resolveService(DATA_SERVICE).cache.delete(cacheKey); } } const backoffEnd = Number(Storage.get(STORAGE_KEYS.FD2_BACKOFF_UNTIL, 0)); if (Date.now() < backoffEnd) { log$37.debug(`Skipping FD2 request, backoff active (${Math.ceil((backoffEnd - Date.now()) / 1e3)}s remaining)`); return null; } const now = Date.now(); const lastFetch = Number(Storage.get(STORAGE_KEYS.LAST_ACTRESS_FETCH, 0)); const waitTime = Math.max(0, lastFetch + TIMING.ACTRESS_BASE_DELAY_MS + Math.random() * TIMING.ACTRESS_RANDOM_DELAY_MS - now); if (waitTime > 0) await Utils.sleep(waitTime); Storage.set(STORAGE_KEYS.LAST_ACTRESS_FETCH, Date.now()); try { const url = EXTERNAL_URLS.FD2PPV.replace("{id}", id); const { doc, error } = await fetchAndParseHtml(url, { headers: getBypassHeaders(url), timeout: TIMING.HTTP_FD2_TIMEOUT }); if (!doc) { log$37.debug(`Failed to fetch FD2 page: ${error?.message || "unknown"}`); return null; } if (doc.title && CLOUDFLARE_INDICATORS.some((s) => doc.title.includes(s))) { log$37.warn("FD2PPV Cloudflare detected, backing off 5m"); Storage.set(STORAGE_KEYS.FD2_BACKOFF_UNTIL, Date.now() + TIMING.FD2_BACKOFF_MS); return null; } const actressLinks = Array.from(doc.querySelectorAll([ ".artist-info-card .artist-name a", ".artist-name a", "a.artistUrl", "h3.artist-name a" ].join(","))); let actress = null; for (const link of actressLinks) { const cleaned = MediaUtils.cleanActressName(link.textContent); if (cleaned) { actress = cleaned; break; } } if (actress) { await resolveService(DATA_SERVICE).cache.set(cacheKey, actress); log$37.info(`Fetched actress: ${id} = ${actress}`); } return actress; } catch (e) { const status = getHttpStatus(e); if (status === 403 || status === 429 || status === 503) { log$37.warn(`FD2PPV error (${status}), backing off 5m`); Storage.set(STORAGE_KEYS.FD2_BACKOFF_UNTIL, Date.now() + TIMING.FD2_BACKOFF_MS); } else log$37.error(`Failed to fetch actress: ${id}`, e); return null; } } resetBackoff() { Storage.delete(STORAGE_KEYS.FD2_BACKOFF_UNTIL); log$37.debug("FD2PPV backoff reset"); } }; var log$36 = Logger.scope("MagnetProvider"); var MagnetProvider = class { sukebeiQueue; constructor(sukebeiQueue) { this.sukebeiQueue = sukebeiQueue; } async fetchFromSukebei(chunk, onResult, traceId) { const queryParts = []; for (const id of chunk) { const dateParts = MediaUtils.parseDateIdParts(id); if (dateParts) { const sep = dateParts.brand === "CARIBBEANCOM" ? "-" : "_"; queryParts.push(`${dateParts.date}${sep}${dateParts.serial}-${dateParts.short}`); } else queryParts.push(id); } const query = queryParts.join("|"); const url = Config.SCRAPER_URLS.SUKEBEI_SEARCH.replace("{query}", encodeURIComponent(query)); try { const { doc, error } = await fetchAndParseHtml(url); if (!doc) { log$36.warn(`Failed to fetch Sukebei page: ${error?.message || "unknown"}`, void 0, traceId); return /* @__PURE__ */ new Set(); } if (doc.title && CLOUDFLARE_INDICATORS.some((s) => doc.title.includes(s))) { log$36.warn("Cloudflare blocked, backing off 60s", void 0, traceId); this.sukebeiQueue.triggerBackoff(TIMING.CLOUDFLARE_BACKOFF_MS, "Sukebei (Cloudflare)"); return /* @__PURE__ */ new Set(); } const rows = doc.querySelectorAll("table.torrent-list tbody tr"); const foundIds = /* @__PURE__ */ new Set(); const regexes = chunk.map((id) => { const escaped = id.toUpperCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const parts = MediaUtils.parseDateIdParts(id); return { id, regex: new RegExp(`(?<=[^0-9a-zA-Z]|^)${escaped.replace(/[-_\s]/g, "[-_\\s]")}(?=[^0-9a-zA-Z]|$)`, "i"), parts, searchId: id.toUpperCase() }; }); for (let i = 0; i < rows.length; i++) { const row = rows[i]; if (!row) continue; const magnetAnchor = row.querySelector("a[href^='magnet:?']"); const linkAnchor = row.querySelector("a[href*=\"/view/\"]") || row.querySelector("td:nth-child(2) a"); if (magnetAnchor && linkAnchor) { const title = linkAnchor.textContent?.trim().toUpperCase() || ""; const magnet = magnetAnchor.href; for (const { id, regex, searchId, parts } of regexes) { if (foundIds.has(id)) continue; const match = title.match(regex); let matchedIndex = match?.index ?? null; let isFlexibleMatch = false; if (!match && parts) { const { date, serial, short } = parts; const hasDate = title.includes(date); const hasSerial = title.includes(serial); if (hasDate && hasSerial) { const dateIdx = title.indexOf(date); const serialIdx = title.indexOf(serial); if (Math.abs(dateIdx - serialIdx) < TIMING.TITLE_MATCH_PROXIMITY) { if (title.includes(short) || !/[A-Z]{2,}-\d+/.test(title.replace(date, "").replace(serial, ""))) { matchedIndex = dateIdx; isFlexibleMatch = true; } } } } if (match || isFlexibleMatch) { if (!isFlexibleMatch) { if (!match) continue; const idx = matchedIndex; const matchedStr = match[0]; if (idx === null || !matchedStr) continue; const before = title[idx - 1], after = title[idx + matchedStr.length]; const isDigit = (c) => c !== void 0 && c >= "0" && c <= "9"; if (isDigit(searchId[0]) && isDigit(before)) continue; if (isDigit(searchId[searchId.length - 1]) && isDigit(after)) continue; } onResult(id, magnet); foundIds.add(id); } } } } log$36.info(`Sukebei: ${foundIds.size}/${chunk.length} found`, void 0, traceId); this.sukebeiQueue.resetBackoff(); return foundIds; } catch (e) { if (getHttpStatus(e) === 429) this.sukebeiQueue.triggerBackoff(TIMING.RATE_LIMIT_BACKOFF_MS, "Sukebei (429)"); else log$36.error("Sukebei fetch error", e, traceId); return /* @__PURE__ */ new Set(); } } }; var log$35 = Logger.scope("PreviewProvider"); /** Check if parsed HTML is a Cloudflare challenge page */ var isCloudflarePage = (doc) => { const title = doc.title || ""; if (CLOUDFLARE_INDICATORS.some((indicator) => title.includes(indicator))) return true; if (doc.querySelector("form#challenge-form, #challenge-running, #challenge-stage")) return true; const bodyText = doc.body?.textContent || ""; if (bodyText.length < 1e3 && CLOUDFLARE_INDICATORS.some((i) => bodyText.includes(i))) return true; return false; }; /** Check if an error response body is a Cloudflare challenge page */ var isCloudflareErrorResponse = (err) => { if (err && typeof err === "object" && "response" in err) { const html = err.response; if (typeof html === "string" && html.length > 0) { if (CLOUDFLARE_INDICATORS.some((indicator) => html.includes(indicator))) return true; if (html.includes("challenge-form") || html.includes("challenge-running")) return true; } } return false; }; var PreviewProvider = class { paipanconQueue; constructor(paipanconQueue) { this.paipanconQueue = paipanconQueue; } async fetchExtraPreviews(fc2Id) { try { const normalizedId = fc2Id.padStart(7, "0"); const idPattern = `${PATTERNS.FC2_PPV_PREFIX}${normalizedId}`; const { doc } = await fetchAndParseHtml(Config.SCRAPER_URLS.PAIPANCON_DETAIL.replace("{id}", normalizedId)); if (!doc) return { items: [], error: "network-error" }; if (isCloudflarePage(doc)) { log$35.warn(`Cloudflare challenge detected for ${fc2Id}`); return { items: [], error: "cf-blocked" }; } const results = []; const blacklist = PREVIEW_BLACKLIST; doc.querySelectorAll("img").forEach((img) => { let src = img.getAttribute("src"); if (src) { const lowSrc = src.toLowerCase(); const isBad = blacklist.some((key) => lowSrc.includes(key)); const isRelevant = src.includes(idPattern) || src.includes(fc2Id); if (!isBad && isRelevant) { if (src.startsWith("/")) src = Config.SCRAPER_URLS.PAIPANCON_BASE + src; results.push({ type: "image", src }); } } }); doc.querySelectorAll("video").forEach((video) => { let src = video.getAttribute("src") || video.querySelector("source")?.getAttribute("src"); if (src) { if (src.includes(idPattern) || src.includes(fc2Id)) { if (src.startsWith("/")) src = Config.SCRAPER_URLS.PAIPANCON_BASE + src; results.push({ type: "video", src }); } } }); return { items: results, error: results.length === 0 ? "not-found" : void 0 }; } catch (e) { const status = getHttpStatus(e); if (status === 404) { log$35.debug(`No previews found on Paipancon for ${fc2Id}`); return { items: [], error: "not-found" }; } if (status === 429 || status === 503) { this.paipanconQueue.triggerBackoff(TIMING.RATE_LIMIT_BACKOFF_MS, `Paipancon (${status})`); return { items: [], error: "rate-limited" }; } if (status === 403 || isCloudflareErrorResponse(e)) { log$35.warn(`Cloudflare/block detected for ${fc2Id} (Status: ${status || "Unknown"})`); return { items: [], error: "cf-blocked" }; } log$35.warn(`Failed to fetch extra previews: ${fc2Id} (Status: ${status || "Unknown"})`); return { items: [], error: "network-error" }; } } }; var log$34 = Logger.scope("ScraperQueue"); var DEFAULT_MAX_QUEUE_SIZE = 200; /** * Rate-limited task queue with exponential backoff for scraper requests. * Backoff state is stored via GM_setValue (cross-tab shared) to prevent * one tab from undoing another's rate-limit backoff. */ var ScraperQueue = class { maxConcurrent; scope; maxQueueSize; queue = []; queuedKeys = /* @__PURE__ */ new Set(); activeRequests = 0; constructor(maxConcurrent, scope, maxQueueSize = DEFAULT_MAX_QUEUE_SIZE) { this.maxConcurrent = maxConcurrent; this.scope = scope; this.maxQueueSize = maxQueueSize; } /** Number of tasks waiting to run. */ get size() { return this.queue.length; } /** Number of requests currently in flight. */ get pending() { return this.activeRequests; } getKey(suffix) { return `scraper_backoff_${this.scope}_${suffix}`; } getBackoff() { return Number(GM_getValue(this.getKey("until"), 0)); } getBackoffLevel() { return Number(GM_getValue(this.getKey("level"), 0)); } triggerBackoff(baseDelay, source = "Unknown") { const currentLevel = this.getBackoffLevel(); const exponentialDelay = baseDelay * Math.pow(2, currentLevel); const finalDelay = Math.min(exponentialDelay, TIMING.MAX_BACKOFF_MS); const totalDuration = finalDelay + Math.min(3e3, finalDelay * .1) * Math.random(); const until = Date.now() + totalDuration; const nextLevel = Math.min(currentLevel + 1, 6); GM_setValue(this.getKey("until"), until); GM_setValue(this.getKey("level"), nextLevel); log$34.warn(`[${this.scope}] Backoff triggered by ${source} for ${Math.ceil(totalDuration / 1e3)}s (L${nextLevel})`); } resetBackoff() { if (this.getBackoffLevel() > 0) { GM_setValue(this.getKey("level"), 0); log$34.debug(`[${this.scope}] Backoff level reset`); } } resetFullBackoff() { GM_deleteValue(this.getKey("until")); GM_deleteValue(this.getKey("level")); log$34.debug(`[${this.scope}] Full backoff reset`); } checkBackoff() { return Date.now() >= this.getBackoff(); } async waitBackoff() { const backoffUntil = this.getBackoff(); const now = Date.now(); if (now < backoffUntil) await Utils.sleep(backoffUntil - now); } /** * Adds a task to the queue. Returns false when a task with the same key * is already queued or when the bounded queue is full. */ enqueue(task, key) { if (key !== void 0 && key !== "") { if (this.queuedKeys.has(key)) return false; this.queuedKeys.add(key); this.queue.push({ key, run: () => { this.queuedKeys.delete(key); return task(); } }); } else this.queue.push({ run: task }); if (this.queue.length > this.maxQueueSize) { const dropped = this.queue.pop(); if (dropped?.key) this.queuedKeys.delete(dropped.key); log$34.warn(`[${this.scope}] Queue full (${this.maxQueueSize}), dropped newest queued task`); return false; } this.processQueue(); return true; } /** Drops queued (non-active) tasks. In-flight requests are not interrupted. */ clear() { this.queue.length = 0; this.queuedKeys.clear(); } async processQueue() { if (this.activeRequests >= this.maxConcurrent || this.queue.length === 0) return; this.activeRequests++; const task = this.queue.shift(); try { await task.run(); } catch (error) { log$34.warn(`[${this.scope}] Task failed`, error); } finally { this.activeRequests--; this.processQueue(); } } }; var log$33 = Logger.scope("ScraperService"); var ScraperService = class { MAX_MAGNET_CONCURRENT = 3; MAX_PREVIEW_CONCURRENT = 2; magnetQueue; previewQueue; magnetProvider; previewProvider; actressProvider; constructor() { this.magnetQueue = new ScraperQueue(this.MAX_MAGNET_CONCURRENT, "sukebei"); this.previewQueue = new ScraperQueue(this.MAX_PREVIEW_CONCURRENT, "paipancon"); this.magnetProvider = new MagnetProvider(this.magnetQueue); this.previewProvider = new PreviewProvider(this.previewQueue); this.actressProvider = new ActressProvider(); } onBootstrap() { log$33.debug("Scraper service bootstrapped, queues ready"); } onCleanup() { this.magnetQueue.resetFullBackoff(); this.previewQueue.resetFullBackoff(); this.magnetQueue.clear(); this.previewQueue.clear(); log$33.debug("Scraper service cleaned up, queues reset"); } notifyBlocked(target, url) { const { t } = Localization; CoreEvents.emit(AppEvents.SHOW_TOAST, { message: t("alertScraperBlocked", { target }), type: "warn", options: { duration: TIMING.SCRAPER_TOAST_DURATION, action: { label: t("btnGoVerify"), onClick: () => BrowserService.openTab(url) } } }); } async testHealth(target) { const url = resolveScraperHealthUrl(target, Config.SCRAPER_URLS.PAIPANCON_BASE); try { return detectScraperHealthStatus(await http(url, { type: "text", timeout: NETWORK.HEALTH_CHECK_TIMEOUT })); } catch (error) { const status = getHttpStatus(error); if (status === 429) return "429"; if (status === 403 || status === 503) return "cf"; return "error"; } } async fetchMagnets(items, onResult) { if (!items?.length) return; const traceId = Logger.traceId; log$33.info(`Starting magnet fetch for ${items.length} items`, { ids: items.map((item) => item.id) }, traceId); const tasks = []; for (const [type, itemList] of groupMagnetRequestItems(items)) for (const chunk of chunkMagnetIds(itemList.map((item) => item.id))) tasks.push(this.processMagnetChunk(chunk, type, onResult, traceId)); await Promise.all(tasks); } processMagnetChunk(chunk, _type, onResult, traceId) { return new Promise((resolve) => { if (!this.magnetQueue.enqueue(async () => { const foundIds = /* @__PURE__ */ new Set(); try { await this.magnetQueue.waitBackoff(); const delay = TIMING.MAGNET_BASE_DELAY_MS + Math.random() * TIMING.MAGNET_RANDOM_DELAY_MS; await Utils.sleep(delay); (await this.magnetProvider.fetchFromSukebei(chunk, (id, url) => { foundIds.add(id); onResult(id, url); }, traceId)).forEach((id) => foundIds.add(id)); } catch (error) { log$33.error("Magnet fetch task failed", error, traceId); if (await this.testHealth("sukebei") === "cf") this.notifyBlocked("Sukebei", "https://sukebei.nyaa.si"); } finally { chunk.forEach((id) => { if (!foundIds.has(id)) onResult(id, null); }); resolve(); } }, chunk.join(","))) { chunk.forEach((id) => onResult(id, null)); resolve(); } }); } async fetchActressFromFD2(id) { return this.actressProvider.fetchActressFromFD2(id); } resetFD2Backoff() { this.actressProvider.resetBackoff(); } async fetchExtraPreviews(fc2Id) { return this.previewProvider.fetchExtraPreviews(fc2Id); } async checkPreviewExists(fc2Id) { const cacheKey = createPreviewExistsCacheKey(fc2Id); const cached = await resolveService(DATA_SERVICE).cache.get(cacheKey); if (cached !== null) return cached === true; return new Promise((resolve) => { if (!this.previewQueue.enqueue(async () => { if (!this.previewQueue.checkBackoff()) { resolve(false); return; } const jitter = Math.random() * TIMING.PREVIEW_JITTER_MS; await Utils.sleep(TIMING.POLITE_DELAY_MS + jitter); try { const exists = (await this.fetchExtraPreviews(fc2Id)).items.length > 0; if (exists) this.previewQueue.resetBackoff(); await resolveService(DATA_SERVICE).cache.set(cacheKey, exists); resolve(exists); } catch (error) { log$33.trace(`Preview check failed for ${fc2Id}`, error); resolve(false); } }, fc2Id)) resolve(false); }); } }; /** Constructed once from app/modules (composition root). */ function createScraperService() { return new ScraperService(); } /** Detected page context — drives which enhancement pipeline (list/detail) activates */ var PageContext = /* @__PURE__ */ function(PageContext) { PageContext["Unknown"] = "unknown"; PageContext["List"] = "list"; PageContext["Detail"] = "detail"; PageContext["User"] = "user"; PageContext["Search"] = "search"; return PageContext; }({}); /** Run a promise without awaiting; log failures instead of swallowing. */ var fireAndForget = (promise, scope = "Async") => { promise.catch((error) => { Logger.scope(scope).error("Unhandled async failure", error); }); }; var SiteHostGuard = class { processedHosts = /* @__PURE__ */ new WeakSet(); hostProcessedAttr; constructor(hostProcessedAttr) { this.hostProcessedAttr = hostProcessedAttr; } isAlreadyProcessed(host) { if (this.processedHosts.has(host)) return true; if (host.getAttribute("data-enh-processed") === "true") { this.markProcessed(host); return true; } if (host.hasAttribute(this.hostProcessedAttr) || host.classList.contains(Config.CLASSES.cardRebuilt)) { this.markProcessed(host); return true; } const processedDescendants = host.querySelectorAll(`.${Config.CLASSES.processedCard}`); if (processedDescendants.length > 0) { if (processedDescendants.length > 1) for (let i = 1; i < processedDescendants.length; i++) processedDescendants[i]?.remove(); host.classList.add(Config.CLASSES.cardRebuilt); this.markProcessed(host); return true; } return false; } isNestedInsideProcessed(node) { return Boolean(node.parentElement?.closest(`[${this.hostProcessedAttr}="1"], [data-enh-rebuilding="true"], .${Config.CLASSES.cardRebuilt}`)); } markProcessed(host) { this.processedHosts.add(host); host.setAttribute(this.hostProcessedAttr, "1"); } isKnownHost(host) { return this.processedHosts.has(host); } clear() { this.processedHosts = /* @__PURE__ */ new WeakSet(); } }; var log$32 = Logger.scope("DomObserver"); var DomObserverService = class { observer = null; handlers = /* @__PURE__ */ new Set(); isInitialized = false; constructor() { if (typeof document !== "undefined") if (document.body) this.init(); else document.addEventListener("DOMContentLoaded", () => this.init(), { once: true }); } init() { if (this.isInitialized || typeof MutationObserver === "undefined" || !document.body) return; try { this.observer = new MutationObserver((mutations) => this.handleMutations(mutations)); this.observer.observe(document.body, { childList: true, subtree: true }); this.isInitialized = true; log$32.debug("Global DomObserver initialized on document.body"); } catch (e) { log$32.error("Failed to initialize MutationObserver", e); } } handleMutations(mutations) { const added = []; const removed = []; for (const m of mutations) { if (m.addedNodes.length > 0) m.addedNodes.forEach((node) => { if (typeof HTMLElement !== "undefined" && node instanceof HTMLElement) added.push(node); }); if (m.removedNodes.length > 0) m.removedNodes.forEach((node) => { if (typeof HTMLElement !== "undefined" && node instanceof HTMLElement) removed.push(node); }); } if (added.length > 0 || removed.length > 0) this.handlers.forEach((handler) => { try { handler(added, removed); } catch (e) { log$32.error("DomMutationHandler failed", e); } }); } subscribe(handler) { this.handlers.add(handler); if (!this.isInitialized) this.init(); return () => { this.handlers.delete(handler); }; } disconnect() { if (this.observer) { this.observer.disconnect(); this.observer = null; } this.handlers.clear(); this.isInitialized = false; } }; var DomObserver = new DomObserverService(); var SiteListObserver = class { observers = []; cardSelector; containerSelector; process; unsubs = []; initialized = false; constructor(cardSelector, containerSelector, process) { this.cardSelector = cardSelector; this.containerSelector = containerSelector; this.process = process; } init(mutationObserverTarget, observeMutations = true) { if (this.initialized) return this.observers; this.initialized = true; const obs = this.setupListMutationObserver(); if (mutationObserverTarget === "container" && !!this.containerSelector) this.setupContainerObserver(); else if (observeMutations !== false) this.unsubs.push(DomObserver.subscribe((added, _removed) => { this.handleAddedNodes(added); })); this.observers.push(obs); return this.observers; } setupListMutationObserver() { return new MutationObserver((muts) => { const added = []; for (const m of muts) m.addedNodes.forEach((n) => { if (n.nodeType === 1) added.push(n); }); if (added.length > 0) this.handleAddedNodes(added); }); } handleAddedNodes(nodes) { const added = /* @__PURE__ */ new Set(); for (const el of nodes) { if (!this.isInListScope(el)) continue; if (el.matches(this.cardSelector)) added.add(el); else { el.querySelectorAll(this.cardSelector).forEach((child) => added.add(child)); const closestCard = el.closest(this.cardSelector); if (closestCard) added.add(closestCard); } } if (added.size > 0) this.process(Array.from(added)); } isInListScope(el) { if (!this.containerSelector) return true; return el.matches(this.containerSelector) || el.closest(this.containerSelector) !== null || el.querySelector(this.containerSelector) !== null; } setupContainerObserver() { if (!this.containerSelector) return; const selector = this.containerSelector; const observedContainers = /* @__PURE__ */ new WeakSet(); const listObs = this.setupListMutationObserver(); const observe = (container) => { if (observedContainers.has(container)) return; observedContainers.add(container); listObs.observe(container, { childList: true, subtree: true }); }; const scan = (el) => { const found = []; if (el.matches(selector)) found.push(el); el.querySelectorAll(selector).forEach((c) => found.push(c)); return found; }; document.querySelectorAll(selector).forEach((c) => observe(c)); this.unsubs.push(DomObserver.subscribe((added, _removed) => { const newContainers = /* @__PURE__ */ new Set(); for (const el of added) { if (el.classList.contains(Config.CLASSES.processedCard) || el.closest(`.${Config.CLASSES.processedCard}`)) continue; scan(el).forEach((c) => newContainers.add(c)); } if (newContainers.size === 0) return; newContainers.forEach((c) => observe(c)); const candidates = /* @__PURE__ */ new Set(); newContainers.forEach((c) => { if (c.matches(this.cardSelector)) candidates.add(c); c.querySelectorAll(this.cardSelector).forEach((card) => candidates.add(card)); }); if (candidates.size > 0) this.process(Array.from(candidates)); })); this.observers.push(listObs); } disconnect() { this.observers.forEach((o) => o.disconnect()); this.observers = []; this.unsubs.forEach((fn) => fn()); this.unsubs = []; this.initialized = false; } }; var SourceKeyManager = class SourceKeyManager { inFlight = /* @__PURE__ */ new Set(); history = /* @__PURE__ */ new Map(); windowMs; limit; pruneCounter = 0; static PRUNE_EVERY = 50; constructor(windowMs = 6e4, limit = 3e3) { this.windowMs = windowMs; this.limit = limit; } isProcessing(key) { return this.inFlight.has(key); } isRecentlyProcessed(key) { const now = Date.now(); this.prune(now); const timestamp = this.history.get(key); return typeof timestamp === "number" && now - timestamp < this.windowMs; } markInFlight(key) { if (this.inFlight.size >= this.limit) this.inFlight.clear(); this.inFlight.add(key); } markProcessed(key) { const now = Date.now(); this.history.delete(key); this.history.set(key, now); this.inFlight.delete(key); this.pruneCounter += 1; if (this.pruneCounter >= SourceKeyManager.PRUNE_EVERY) { this.pruneCounter = 0; this.prune(now); } } release(key, markProcessed = false) { this.inFlight.delete(key); if (markProcessed) this.markProcessed(key); } clear() { this.inFlight.clear(); this.history.clear(); } prune(now) { const expireBefore = now - this.windowMs; for (const [key, timestamp] of this.history.entries()) if (timestamp < expireBefore) this.history.delete(key); if (this.history.size <= this.limit) return; const overflow = this.history.size - this.limit; const keys = this.history.keys(); for (let i = 0; i < overflow; i += 1) { const oldest = keys.next().value; if (!oldest) break; this.history.delete(oldest); } } }; var log$31 = Logger.scope("Site"); var BaseSite = class { config; observers = []; listObserver = null; activeContext = PageContext.Unknown; _unsubs = []; hostProcessedAttr = "data-enh-card-host"; sourceKeyAttr = "data-enh-source-key"; sourceKeyManager = new SourceKeyManager(); hostGuard = new SiteHostGuard(this.hostProcessedAttr); constructor(config) { this.config = config; } clearSubscriptions() { this._unsubs.forEach((fn) => fn()); this._unsubs = []; } async init() { try { log$31.debug(`Initializing ${this.config.name}`); this.clearSubscriptions(); this.config.context = { supjavSortByViews: resolveService(STATE_SERVICE).proxy.supjavSortByViews, getSetting: (key) => resolveService(STATE_SERVICE).proxy[key] }; if (this.config.onBeforeInit) await this.config.onBeforeInit(); this.activeContext = this.detectContext(); if (this.config.list && ([PageContext.List, PageContext.Search].includes(this.activeContext) || this.activeContext === PageContext.Detail && this.config.listOnDetail)) await this.initListMode(); if (this.config.detail && this.activeContext === PageContext.Detail) this.initDetailMode(); if (this.config.customInit) this.config.customInit(); if (this.config.onInit) await this.config.onInit(); if (this.config.onAfterInit) await this.config.onAfterInit(); this._unsubs.push(resolveService(STATE_SERVICE).on(({ prop }) => { if ([ "hideNoMagnet", "enableMagnets", "hideCensored", "hideViewed" ].includes(prop)) this.refreshVisibility(); })); this._unsubs.push(CoreEvents.on(AppEvents.HISTORY_LOADED, () => { this.refreshHistoryState(); this.refreshVisibility(); })); this.refreshHistoryState(); } catch (error) { log$31.error(`Init failed for ${this.config.name}`, error); throw error; } } refreshVisibility() { CoreEvents.emit(AppEvents.VISIBILITY_REFRESH_NEEDED, {}); } refreshHistoryState() { CoreEvents.emit(AppEvents.HISTORY_STATUS_NEEDED, {}); } async initListMode() { if (!this.config.list) return; const list = this.config.list; const process = async (nodes) => { for (const c of nodes) try { const host = c; if (list.containerSelector && !c.closest(list.containerSelector)) continue; if (this.hostGuard.isNestedInsideProcessed(host)) continue; if (this.hostGuard.isAlreadyProcessed(host) || host.hasAttribute("data-enh-rebuilding")) continue; if (list.shouldProcessCard && !list.shouldProcessCard(host)) continue; const sourceKey = list.identityKey ? list.identityKey(host)?.trim() || null : null; if (sourceKey) { if (this.sourceKeyManager.isProcessing(sourceKey) || this.sourceKeyManager.isRecentlyProcessed(sourceKey)) continue; this.sourceKeyManager.markInFlight(sourceKey); host.setAttribute(this.sourceKeyAttr, sourceKey); } host.setAttribute("data-enh-rebuilding", "true"); try { await this.processCard(host); } catch (e) { log$31.error("processCard failed", e); host.removeAttribute("data-enh-rebuilding"); const k = host.getAttribute(this.sourceKeyAttr); if (k) this.sourceKeyManager.release(k, false); host.removeAttribute(this.sourceKeyAttr); } } catch (e) { log$31.error("Card iteration error", e); } }; const initialNodes = Array.from(document.querySelectorAll(list.cardSelector)); if (initialNodes.length > 0) await process(initialNodes); this.listObserver = new SiteListObserver(list.cardSelector, list.containerSelector, (nodes) => { fireAndForget(process(nodes), "Site"); }); const obs = this.listObserver.init(list.mutationObserverTarget, list.observeMutations !== false); this.observers.push(...obs); } initDetailMode() { if (!this.config.detail) return; const detail = this.config.detail; const check = () => { const target = document.querySelector(detail.triggerSelector || detail.mainImageSelector || ""); if (target && !target.hasAttribute("data-enh-processed")) { target.setAttribute("data-enh-processed", "true"); if (detail.customDetailAction) Promise.resolve(detail.customDetailAction(target, obs)).then((result) => { if (result) CoreEvents.emit(AppEvents.DETAIL_ENHANCEMENT_REQUESTED, { siteName: this.config.name, config: result, observer: obs }); }).catch((e) => { log$31.error("Detail action failed", e); target.removeAttribute("data-enh-processed"); }); } }; const obs = new MutationObserver(check); check(); obs.observe(document.body, { childList: true, subtree: true }); this.observers.push(obs); } async processCard(card) { if (!this.config.list) return; const list = this.config.list; try { let data = list.extractor(card); if (data instanceof Promise) data = await data; if (!data) { card.removeAttribute("data-enh-rebuilding"); const k = card.getAttribute(this.sourceKeyAttr); if (k) this.sourceKeyManager.release(k, false); card.removeAttribute(this.sourceKeyAttr); return; } if (!data.id || !data.type) { log$31.warn(`Invalid card data from ${this.config.name}: missing id or type`, data); card.removeAttribute("data-enh-rebuilding"); const k = card.getAttribute(this.sourceKeyAttr); if (k) this.sourceKeyManager.release(k, false); card.removeAttribute(this.sourceKeyAttr); return; } card.setAttribute("data-enh-processed", "true"); card.classList.add(Config.CLASSES.cardRebuilt); this.hostGuard.markProcessed(card); const gridContainerSelector = list.gridContainerSelector ?? list.containerSelector; markGridContainer(gridContainerSelector ? card.closest(gridContainerSelector) : null); const extraUi = list.getExtraUi ? list.getExtraUi(card) : {}; CoreEvents.emit(AppEvents.CARD_DISCOVERED, { id: data.id, type: data.type, data, host: card, extraUi, postProcess: list.postProcess ? (final, newCard, classes) => { list.postProcess(card, final, newCard, data, classes); } : void 0 }); card.dataset.id = data.id; card.removeAttribute("data-enh-rebuilding"); const k = card.getAttribute(this.sourceKeyAttr); if (k) this.sourceKeyManager.release(k, true); } catch (error) { log$31.error("processCard failed", error); card.removeAttribute("data-enh-rebuilding"); const k = card.getAttribute(this.sourceKeyAttr); if (k) this.sourceKeyManager.release(k, false); card.removeAttribute(this.sourceKeyAttr); } } cleanup() { this.observers.forEach((o) => o.disconnect()); this.observers = []; if (this.listObserver) { this.listObserver.disconnect(); this.listObserver = null; } this.hostGuard.clear(); this.sourceKeyManager.clear(); this.clearSubscriptions(); if (this.config.onCleanup) this.config.onCleanup(); } }; var siteConfigRegistry = /* @__PURE__ */ new Map(); function registerSiteConfig(name, config) { config.name = name; siteConfigRegistry.set(name, config); } var log$30 = Logger.scope("SiteService"); var urlWatchers = /* @__PURE__ */ new Set(); var originalPushState = null; var originalReplaceState = null; var notifyUrlWatchers = () => { urlWatchers.forEach((check) => { try { check(); } catch (error) { log$30.error("URL watcher callback failed", error); } }); }; var installHistoryPatch = () => { if (originalPushState && originalReplaceState) return; originalPushState = history.pushState; originalReplaceState = history.replaceState; history.pushState = function(...args) { const result = originalPushState.apply(history, args); notifyUrlWatchers(); return result; }; history.replaceState = function(...args) { const result = originalReplaceState.apply(history, args); notifyUrlWatchers(); return result; }; window.addEventListener("popstate", notifyUrlWatchers); }; var uninstallHistoryPatch = () => { if (!originalPushState || !originalReplaceState || urlWatchers.size > 0) return; history.pushState = originalPushState; history.replaceState = originalReplaceState; originalPushState = null; originalReplaceState = null; window.removeEventListener("popstate", notifyUrlWatchers); }; var GenericSite = class extends BaseSite { detectContext() { if (this.config.detectContext) try { return this.config.detectContext(); } catch (error) { log$30.error("Custom detectContext failed", error); } const path = window.location.pathname; if (path.includes("/search/") || window.location.search.includes("keyword=")) return PageContext.Search; if (path.includes("/v/") || path.includes("/detail/") || path.includes("/movie/") || path.endsWith(".html")) return PageContext.Detail; return PageContext.Unknown; } }; var matchSiteConfig = (registry, hostname) => { for (const config of registry.values()) if (config.hostnames.some((hostnameMatcher) => typeof hostnameMatcher === "string" ? hostname.includes(hostnameMatcher) : hostnameMatcher.test(hostname))) return config; return null; }; var installUrlWatcher = (check) => { installHistoryPatch(); urlWatchers.add(check); return () => { urlWatchers.delete(check); uninstallHistoryPatch(); }; }; var log$29 = Logger.scope("SiteService"); var SiteService = class { registry = /* @__PURE__ */ new Map(); activeSite = null; currentUrl = location.href; disposeUrlWatcher = null; async onBootstrap() { log$29.debug("Bootstrapped, initiating site matching"); this.registry = siteConfigRegistry; await this.bootstrap(); } onCleanup() { this.disposeUrlWatcher?.(); this.disposeUrlWatcher = null; this.activeSite?.cleanup(); } async bootstrap() { const matchedConfig = matchSiteConfig(this.registry, location.hostname); if (!matchedConfig) { log$29.warn(`No site config matched for ${location.hostname}`); return; } log$29.info(`Matched site: ${matchedConfig.name}`); this.activeSite = new GenericSite(matchedConfig); CoreEvents.emit(AppEvents.SITE_RESET, { siteName: matchedConfig.name }); try { await this.activeSite.init(); } catch (error) { log$29.error(`Site init failed for ${matchedConfig.name}`, error); } this.disposeUrlWatcher?.(); this.disposeUrlWatcher = installUrlWatcher(() => { if (location.href === this.currentUrl) return; this.currentUrl = location.href; log$29.debug("URL change detected"); this.activeSite?.cleanup(); CoreEvents.emit(AppEvents.SITE_RESET, { siteName: matchedConfig.name }); this.activeSite?.init().then(() => { CoreEvents.emit(AppEvents.SITE_READY, { siteName: matchedConfig.name }); }).catch((error) => { log$29.error(`Re-init failed for ${matchedConfig.name}`, error); }); }); CoreEvents.emit(AppEvents.SITE_READY, { siteName: matchedConfig.name }); } getActiveSite() { return this.activeSite; } }; /** Constructed once from app/modules (composition root). */ function createSiteService() { return new SiteService(); } var getAnimations = (C) => { const reducedMotionTargets = `:is( #fc2-modern-ui-host, #fc2-enh-settings-host, #fc2-enh-settings-container, .fc2-enh-settings-panel, .fc2-enh-modal-overlay, .fc2-enh-modal-panel, .fc2-action-sheet, .fc2-collection-grid, .fc2-collection-toolbar, .fc2-fab-container, .fc2-enh-toolbar, .fc2-enh-viewer-backdrop, .${C.cardRebuilt}, .${C.processedCard} )`; return ` /* ============================================================ GLOBAL ANIMATIONS ============================================================ */ /* Generic Fade & Slide In */ @keyframes fc2-fade-in { from { opacity: 0; transform: translateY(var(--fc2-space-sm)); } to { opacity: 1; transform: translateY(0); } } /* Master Reveal Animation for Cards */ @keyframes fc2-card-reveal { 0% { opacity: 0; transform: translate3d(0, var(--fc2-space-base), 0) scale(0.97); filter: blur(calc(var(--fc2-blur) / 3)); } 100% { opacity: 1; transform: translate3d(0, 0, 0) scale(1); filter: blur(0); } } @keyframes fc2-content-reveal { 0% { opacity: 0; transform: translate3d(0, 0, 0); } 100% { opacity: 1; transform: translate3d(0, 0, 0); } } /* Core Spinner */ @keyframes fc2-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes fc2-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } @keyframes fc2-skeleton-pulse { 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } } /* Pulse Effects */ @keyframes fc2-pulse-sync { 0% { transform: scale(0.8); opacity: 0.6; } 50% { transform: scale(1.1); opacity: 1; } 100% { transform: scale(0.8); opacity: 0.6; } } @keyframes fc2-pulse-once { 0% { transform: scale(1); } 50% { transform: scale(1.15); background: var(--fc2-primary); color: var(--fc2-on-primary); } 100% { transform: scale(1); } } /* UI Logic Specific */ @keyframes fc2-copy-success { 0% { transform: scale(1); } 50% { transform: scale(1.1); background: var(--fc2-success); } 100% { transform: scale(1); } } @keyframes fc2-magnet-in { 0% { transform: scale(0.3) translateY(var(--fc2-space-lg)); opacity: 0; filter: blur(calc(var(--fc2-blur) / 2)); } 60% { transform: scale(1.1) translateY(calc(-1 * var(--fc2-space-xs))); filter: blur(0); } 85% { transform: scale(0.95) translateY(var(--fc2-border-width)); } 100% { transform: scale(1) translateY(0); opacity: 1; } } @keyframes fc2-pop-in { 0% { opacity: 0; transform: translate(-50%, -45%) scale(0.92); filter: blur(var(--fc2-blur)); } 70% { transform: translate(-50%, -51%) scale(1.02); filter: blur(0); } 100% { opacity: 1; transform: translate(-50%, -50%) scale(1); } } @keyframes fc2-ripple { 0% { opacity: 0.24; transform: translate(-50%, -50%) scale(0.72); } 100% { opacity: 0; transform: translate(-50%, -50%) scale(1.32); } } /* Gallery & Media Animations */ @keyframes fc2-gallery-zoom { 0% { opacity: 0; transform: scale(0.95); } 100% { opacity: 1; transform: scale(1); } } @keyframes fc2-slide-right-in { 0% { opacity: 0; transform: translateX(var(--fc2-space-xl)); } 100% { opacity: 1; transform: translateX(0); } } @keyframes fc2-slide-left-in { 0% { opacity: 0; transform: translateX(calc(-1 * var(--fc2-space-xl))); } 100% { opacity: 1; transform: translateX(0); } } @keyframes fc2-animate-pop { 0% { transform: scale(1); } 50% { transform: scale(1.25); } 100% { transform: scale(1); } } @keyframes fc2-star-burst { 0% { transform: scale(0); opacity: 1; } 100% { transform: scale(2.5); opacity: 0; } } @keyframes fc2-glow-pulse { 0% { filter: drop-shadow(0 0 var(--fc2-border-width) var(--fc2-primary)); } 50% { filter: drop-shadow(0 0 var(--fc2-space-sm) var(--fc2-primary)); } 100% { filter: drop-shadow(0 0 var(--fc2-border-width) var(--fc2-primary)); } } /* Toast Animations */ @keyframes fc2-toast-in { from { opacity: 0; transform: translateX(var(--fc2-space-xl)) scale(0.95); filter: blur(var(--fc2-border-width)); } to { opacity: 1; transform: translateX(0) scale(1); filter: blur(0); } } @keyframes fc2-toast-out { from { opacity: 1; transform: translateX(0) scale(1); } to { opacity: 0; transform: translateX(var(--fc2-space-xl)) scale(0.95); } } /* Helper Classes */ .fc2-pulse-once { animation: fc2-pulse-once var(--fc2-motion-base) var(--fc2-ease-out); } .fc2-animate-pop { animation: fc2-animate-pop var(--fc2-motion-base) var(--fc2-ease-out); } .fc2-star-burst::after { content: ''; position: absolute; inset: calc(-1 * var(--fc2-space-sm)); border: var(--fc2-border-width) solid var(--fc2-primary); border-radius: var(--fc2-radius-full); animation: fc2-star-burst var(--fc2-motion-base) var(--fc2-ease-out) forwards; pointer-events: none; } @media (prefers-reduced-motion: reduce) { ${reducedMotionTargets}, ${reducedMotionTargets} *, ${reducedMotionTargets} *::before, ${reducedMotionTargets} *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } ${reducedMotionTargets} :is( .fc2-tab-content-wrapper, .fc2-enh-modal-panel, .fc2-enh-btn, .fc2-btn, .fc2-fab-btn, .fc2-fab-trigger, .${C.resourceBtn}, .fc2-btn-actress, .fc2-enh-viewer-stage img, .fc2-enh-viewer-stage video ) { transform: none !important; filter: none !important; } } `; }; var getBaseStyles = (C) => { const scrollbarScope = `#fc2-modern-ui-host, #fc2-enh-settings-host, #fc2-enh-settings-container, .fc2-enh-settings-panel, .fc2-enh-modal-overlay, .fc2-enh-modal-panel, .fc2-action-sheet, .fc2-fab-container, .fc2-enh-toolbar, .fc2-enh-viewer-backdrop, .${C.cardRebuilt}, .${C.cardActionRow}, .${C.infoArea}`; return ` /* ============================================================ CSS RESET & GLOBAL OVERRIDES ============================================================ */ .fc2-enh-settings-panel, .fc2-enh-modal-overlay, .fc2-enh-modal-panel, .fc2-action-sheet, .${C.cardActionRow}, .fc2-fab-container { all: revert; box-sizing: border-box; -webkit-tap-highlight-color: transparent !important; -webkit-touch-callout: none !important; transition: background-color var(--fc2-motion-slow) var(--fc2-ease-standard), color var(--fc2-motion-slow) var(--fc2-ease-standard), border-color var(--fc2-motion-slow) var(--fc2-ease-standard), box-shadow var(--fc2-motion-slow) var(--fc2-ease-standard); } .fc2-enh-settings-panel *:not(svg):not(path):not(circle):not(rect):not(line):not(polyline):not(polygon), .fc2-enh-settings-panel *:not(svg):not(path):not(circle):not(rect):not(line):not(polyline):not(polygon), .fc2-enh-modal-panel *:not(svg):not(path):not(circle):not(rect):not(line):not(polyline):not(polygon), .fc2-fab-container *:not(svg):not(path):not(circle):not(rect):not(line):not(polyline):not(polygon), .fc2-action-sheet *:not(svg):not(path):not(circle):not(rect):not(line):not(polyline):not(polygon), .${C.cardActionRow} *:not(svg):not(path):not(circle):not(rect):not(line):not(polyline):not(polygon) { box-sizing: border-box; font-family: var(--fc2-font) !important; font-size: var(--fc2-text-base) !important; font-style: normal !important; line-height: var(--fc2-line-base) !important; letter-spacing: normal !important; text-transform: none !important; color: var(--fc2-text); text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; } /* Icons Visibility Fix */ .fc2-enh-settings-panel svg, .fc2-enh-settings-panel .fc2-icon, .fc2-fab-container svg, .fc2-fab-container .fc2-icon { display: inline-flex !important; align-items: center !important; justify-content: center !important; vertical-align: middle !important; color: inherit; } /* Headings */ .fc2-enh-settings-panel h2, .fc2-enh-settings-panel h3, .fc2-enh-settings-panel h4 { margin: 0 !important; padding: 0 !important; font-weight: var(--fc2-font-semibold) !important; } .fc2-enh-settings-panel h2 { font-size: var(--fc2-text-xl) !important; line-height: var(--fc2-line-tight) !important; } .fc2-enh-settings-panel h3 { font-size: var(--fc2-text-lg) !important; line-height: var(--fc2-line-tight) !important; } .fc2-enh-settings-panel h4 { font-size: var(--fc2-text-base) !important; line-height: var(--fc2-line-base) !important; } /* Forms & Controls */ .fc2-enh-settings-panel label, .fc2-enh-settings-panel input, .fc2-enh-settings-panel select, .fc2-enh-settings-panel button { font-size: var(--fc2-text-sm); line-height: var(--fc2-line-base) !important; } /* ============================================================ SELECT DROPDOWN STYLES ============================================================ */ .fc2-enh-settings-panel select, .fc2-enh-settings-panel .fc2-select { display: inline-block !important; min-height: var(--fc2-btn-md) !important; padding: var(--fc2-space-xs) var(--fc2-space-xl) var(--fc2-space-xs) var(--fc2-space-sm) !important; background-color: var(--fc2-bg) !important; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E%3Cpath fill='%239494a0' d='M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z'/%3E%3C/svg%3E") !important; background-repeat: no-repeat !important; background-position: right var(--fc2-space-sm) center !important; background-size: var(--fc2-btn-sm) !important; color: var(--fc2-text) !important; border: var(--fc2-border-width) solid var(--fc2-border) !important; border-radius: var(--fc2-radius-sm) !important; cursor: pointer !important; appearance: none !important; -webkit-appearance: none !important; color-scheme: light dark !important; transition: all var(--fc2-motion-fast) var(--fc2-ease-standard); } .fc2-enh-settings-panel select: hover { border-color: var(--fc2-text-dim) !important; background-color: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; } .fc2-enh-settings-panel select: focus { outline: none !important; border-color: var(--fc2-primary) !important; box-shadow: var(--fc2-focus-ring) !important; } .fc2-enh-settings-panel select option { padding: var(--fc2-space-xs) var(--fc2-space-sm) !important; background: var(--fc2-bg) !important; color: var(--fc2-text) !important; } .fc2-enh-settings-panel select option: checked, .fc2-enh-settings-panel select option: hover { background: var(--fc2-primary) !important; color: var(--fc2-on-primary) !important; } /* ============================================================ SCROLLBAR & UTILITIES ============================================================ */ /* Scoped Scrollbar */ :is(${scrollbarScope})::-webkit-scrollbar, :is(${scrollbarScope}) *::-webkit-scrollbar { width: calc(var(--fc2-space-sm) - var(--fc2-space-xxs)); height: calc(var(--fc2-space-sm) - var(--fc2-space-xxs)); } :is(${scrollbarScope})::-webkit-scrollbar-track, :is(${scrollbarScope}) *::-webkit-scrollbar-track { background: transparent; } :is(${scrollbarScope})::-webkit-scrollbar-thumb, :is(${scrollbarScope}) *::-webkit-scrollbar-thumb { background: var(--fc2-scrollbar-thumb) !important; border-radius: var(--fc2-btn-radius); } :is(${scrollbarScope})::-webkit-scrollbar-thumb:hover, :is(${scrollbarScope}) *::-webkit-scrollbar-thumb:hover { background: var(--fc2-scrollbar-hover) !important; } /* Functional Hide Classes */ .${C.hideNoMagnet}, .${C.hideCensored}, .${C.hideViewed}, .fc2-is-hidden { display: none !important; } [data-enh-grid-container] > *:has(.${C.hideNoMagnet}), [data-enh-grid-container] > *:has(.${C.hideCensored}), [data-enh-grid-container] > *:has(.${C.hideViewed}) { display: none !important; } .fc2-is-invisible { visibility: hidden !important; } body.fc2-settings-open { overflow: hidden !important; } .fc2-ml-sm { margin-left: var(--fc2-space-sm) !important; } #fc2-enh-settings-host { position: fixed; inset: 0; z-index: var(--fc2-z-modal); } #fc2-enh-settings-container { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-family: var(--fc2-font); padding: var(--fc2-space-md); } `; }; var getMobileStyles = (C) => ` /* ============================================================ MOBILE TOUCH OPTIMIZATIONS ============================================================ */ @media (max-width: 768px) and (hover: none), (max-width: 768px) and (pointer: coarse) { /* Keep mobile interaction tuning scoped to userscript-owned UI only */ .${C.processedCard}, .${C.cardRebuilt}, .${C.resourceBtn}, .fc2-card-top-right-controls > *, .fc2-enh-tab-btn, .fc2-portal-item, .data-management-actions > *, .fc2-fab-btn, .fc2-fab-trigger, .fc2-enh-viewer-action, .fc2-enh-viewer-close, .fc2-enh-thumb-item, .fc2-enh-btn, .fc2-btn, .batch-btn, .fc2-action-sheet-item, .fc2-close-btn, .fc2-enh-settings-panel button, .fc2-enh-input, .fc2-enh-select, .fc2-enh-textarea { touch-action: manipulation; animation-duration: 0.2s !important; transition-duration: 0.2s !important; } /* Hardware Acceleration for Mobile */ .${C.processedCard}, .${C.cardRebuilt}, .${C.resourceBtn}, .fc2-fab-btn, .fc2-fab-trigger { transform: translateZ(0); -webkit-transform: translateZ(0); will-change: transform; } /* Disable hover effects on touch devices to prevent "sticky" hover */ @media (hover: none) { .${C.resourceBtn}:hover, .fc2-card-top-right-controls > *:hover, .fc2-fab-btn:hover, .fc2-fab-trigger:hover, .${C.processedCard}:hover { transform: none !important; box-shadow: none !important; border-color: rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)) !important; } } /* ============================================================ SETTINGS PANEL MOBILE ============================================================ */ .fc2-enh-settings-panel { display: flex !important; flex-direction: column !important; } .fc2-enh-settings-body { flex-direction: column !important; } .fc2-enh-settings-content { padding: 0 !important; } .fc2-enh-tab-btn { display: flex; flex: 1 1 1180px; width: auto !important; justify-content: center; padding: var(--fc2-space-sm) var(--fc2-space-xs) !important; min-height: var(--fc2-btn-lg) !important; height: auto !important; font-size: var(--fc2-text-xs) !important; border-radius: var(--fc2-radius-md) !important; min-width: 0; } .fc2-enh-form-row { flex-direction: column !important; align-items: flex-start !important; gap: var(--fc2-space-sm) !important; padding: var(--fc2-space-base) 0 !important; border-bottom: var(--fc2-border-width) solid rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)) !important; } /* Prevent checkboxes from splitting on mobile */ .fc2-enh-form-row.checkbox { flex-direction: row !important; align-items: center !important; padding: var(--fc2-space-base) var(--fc2-space-sm) !important; } .fc2-portal-grid { grid-template-columns: 1fr !important; gap: var(--fc2-space-sm) !important; } .fc2-portal-item { padding: var(--fc2-space-sm) !important; font-size: var(--fc2-text-sm) !important; } .data-management-actions { flex-direction: row !important; flex-wrap: wrap !important; gap: var(--fc2-space-sm) !important; } .data-management-actions > * { flex: 1 1 calc(50% - var(--fc2-space-sm)) !important; min-width: 120px !important; height: var(--fc2-btn-lg) !important; } /* ============================================================ GRID & CARD LAYOUT MOBILE ============================================================ */ .${C.cardRebuilt} { width: 100% !important; max-width: 100% !important; margin: 0 !important; flex-basis: auto !important; /* Allow grid to define width */ } /* Larger Touch Targets for Mobile */ .fc2-card-top-right-controls { top: var(--fc2-space-base) !important; right: var(--fc2-space-base) !important; gap: var(--fc2-space-sm) !important; } .fc2-card-top-right-controls > * { min-height: var(--fc2-btn-lg); min-width: var(--fc2-btn-lg); height: var(--fc2-btn-md) !important; padding: 0 var(--fc2-space-base) !important; font-size: var(--fc2-text-base) !important; } .${C.resourceBtn} { min-height: var(--fc2-btn-lg); height: var(--fc2-btn-lg) !important; padding: 0 var(--fc2-space-md) !important; font-size: var(--fc2-text-base) !important; } .${C.infoArea} { padding: var(--fc2-space-base) !important; } .${C.customTitle} { height: auto !important; min-height: var(--fc2-btn-md); margin-bottom: var(--fc2-space-sm) !important; } .${C.cardMeta} { margin-bottom: var(--fc2-space-sm) !important; } .${C.cardActionRow} { width: 100% !important; justify-content: flex-start !important; } .${C.cardActionRow} { display: flex !important; flex-wrap: wrap !important; justify-content: flex-end !important; align-items: start !important; gap: var(--fc2-space-sm) !important; } .${C.cardActionRow} .${C.resourceLinksContainer} { display: flex !important; flex-wrap: wrap !important; justify-content: flex-end !important; gap: var(--fc2-space-sm) !important; width: 100% !important; max-width: 100% !important; min-width: 0 !important; margin-left: 0 !important; padding: 0 !important; height: auto !important; min-height: var(--fc2-btn-lg) !important; overflow: visible !important; background: transparent !important; border: none !important; box-shadow: none !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; } .${C.cardActionRow} .${C.resourceLinksContainer} > * { flex: 0 0 auto !important; width: var(--fc2-btn-lg) !important; min-width: var(--fc2-btn-lg) !important; max-width: var(--fc2-btn-lg) !important; height: var(--fc2-btn-lg) !important; min-height: var(--fc2-btn-lg) !important; padding: 0 !important; margin: 0 !important; border-radius: var(--fc2-radius-md) !important; background: var(--fc2-surface-item) !important; border: var(--fc2-border-width) solid var(--fc2-border) !important; font-size: var(--fc2-text-lg) !important; justify-content: center !important; box-shadow: inset 0 var(--fc2-border-width) 0 rgba(var(--fc2-primary-rgb), var(--fc2-opacity-ghost)) !important; } .${C.cardActionRow} .${C.resourceLinksContainer} .${C.fc2IdBadge} { width: auto !important; min-width: 0 !important; max-width: 100% !important; padding: 0 var(--fc2-space-md) !important; font-size: var(--fc2-text-sm) !important; } .${C.cardActionRow} .${C.resourceLinksContainer} .fc2-icon { font-size: var(--fc2-text-xl) !important; width: 1em; height: 1em; } .fc2-card-action-stash, .fc2-card-overflow-actions[hidden] { display: none !important; width: 0 !important; min-width: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; box-shadow: none !important; } .fc2-btn-actress { width: 90% !important; margin: var(--fc2-space-sm) auto !important; min-height: var(--fc2-btn-lg) !important; padding: 0 var(--fc2-space-md) !important; font-size: var(--fc2-text-base) !important; } .fc2-fab-trigger { width: var(--fc2-btn-lg); height: var(--fc2-btn-lg); font-size: var(--fc2-text-xl) !important; } .fc2-fab-btn { width: var(--fc2-btn-md); height: var(--fc2-btn-md); font-size: var(--fc2-text-xl) !important; } /* ============================================================ TOOLBAR & MODAL MOBILE ============================================================ */ .fc2-enh-toolbar { display: block !important; height: auto !important; min-height: 0 !important; margin: var(--fc2-space-base) 0 !important; padding: 0 !important; border-radius: var(--fc2-radius-md) !important; overflow: hidden !important; } .fc2-enh-toolbar .fc2-info-area { display: grid !important; grid-template-columns: minmax(0, 1fr) auto !important; gap: var(--fc2-space-sm) !important; width: 100% !important; min-width: 0 !important; height: auto !important; padding: var(--fc2-space-sm) !important; align-items: center !important; } .fc2-enh-toolbar .fc2-card-top-right-controls, .fc2-enh-toolbar .fc2-resource-links-container { display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important; justify-content: flex-start !important; align-items: center !important; margin: 0 !important; min-width: 0 !important; width: auto !important; max-width: 100% !important; height: auto !important; min-height: var(--fc2-btn-lg) !important; } .fc2-enh-toolbar .fc2-toolbar-section-controls { grid-column: 1 !important; justify-self: start !important; } .fc2-enh-toolbar .fc2-toolbar-section-links { grid-column: 2 !important; justify-self: end !important; } .fc2-enh-toolbar .fc2-resource-links-container { gap: 0 !important; overflow: hidden !important; } .fc2-enh-toolbar .fc2-btn-actress { grid-column: 1 / -1 !important; justify-self: stretch !important; display: inline-flex !important; width: 100% !important; max-width: none !important; min-height: var(--fc2-btn-lg) !important; padding: 0 var(--fc2-space-md) !important; } .fc2-enh-toolbar .fc2-card-top-right-controls > *, .fc2-enh-toolbar .fc2-resource-links-container > * { flex: 0 0 auto !important; justify-content: center !important; min-width: var(--fc2-btn-lg) !important; height: var(--fc2-btn-lg) !important; min-height: var(--fc2-btn-lg) !important; } .fc2-enh-toolbar .fc2-card-top-right-controls .${C.fc2IdBadge} { flex-basis: auto !important; min-width: 0 !important; } .fc2-enh-toolbar .fc2-card-top-right-controls .fc2-icon, .fc2-enh-toolbar .fc2-resource-links-container .fc2-icon { font-size: var(--fc2-text-xl) !important; width: 1em; height: 1em; } .fc2-enh-viewer-nav { display: none !important; /* Hide arrows on mobile, rely on swipe */ } .fc2-enh-viewer-thumbs { bottom: var(--fc2-space-lg) !important; width: 95% !important; height: var(--fc2-btn-lg) !important; gap: var(--fc2-space-xs) !important; } .fc2-enh-thumb-item { width: var(--fc2-btn-lg) !important; height: var(--fc2-btn-lg) !important; } .fc2-enh-viewer-actions { top: var(--fc2-btn-sm) !important; padding: var(--fc2-space-xs) !important; } .fc2-enh-viewer-action { width: var(--fc2-btn-lg) !important; height: var(--fc2-btn-lg) !important; } .fc2-enh-viewer-close { top: var(--fc2-space-sm) !important; right: var(--fc2-space-sm) !important; width: var(--fc2-btn-lg) !important; height: var(--fc2-btn-lg) !important; background: rgba(var(--fc2-black-rgb), var(--fc2-opacity-muted)) !important; border-radius: var(--fc2-radius-full) !important; backdrop-filter: blur(var(--fc2-blur)) !important; } /* Keep vertical page pan while reserving horizontal swipes for gallery navigation */ .fc2-enh-viewer-stage { touch-action: pan-y; overscroll-behavior: contain; user-select: none; -webkit-user-select: none; } .fc2-enh-viewer-backdrop { overscroll-behavior: contain; } /* Prevent iOS auto-zoom when focusing script-owned form controls */ .fc2-enh-settings-panel input, .fc2-enh-settings-panel select, .fc2-enh-settings-panel textarea, .fc2-enh-input, .fc2-enh-select, .fc2-enh-textarea { font-size: 16px !important; } .fc2-enh-modal-panel { width: 95% !important; max-width: 95% !important; max-height: 90vh !important; } /* Improved Grid Responsiveness */ .preview-hero-section { margin-bottom: var(--fc2-space-md) !important; } .preview-hero-card { border-radius: var(--fc2-radius-md) !important; overflow: hidden !important; } .${C.extraPreviewGrid} { grid-template-columns: repeat(2, 1fr) !important; gap: var(--fc2-space-sm) !important; } .preview-item { height: 120px !important; border-radius: var(--fc2-radius-sm) !important; } /* FAB Container - safely above bottom bar by default, but allow JS to override */ .fc2-fab-container { bottom: calc(var(--fc2-space-2xl) + var(--fc2-space-2xl)); right: var(--fc2-space-lg); } /* Toasts mobile positioning with safe area */ .fc2-toast-container { top: calc(var(--fc2-space-sm) + env(safe-area-inset-top, 0px)) !important; right: var(--fc2-space-sm) !important; left: var(--fc2-space-sm) !important; /* Full width on mobile looks better */ } .fc2-toast-item { min-width: 0 !important; width: 100% !important; } /* Better touch feedback */ .fc2-fab-trigger, .fc2-fab-btn, .fc2-enh-viewer-action, .fc2-enh-thumb-item { -webkit-tap-highlight-color: rgba(var(--fc2-primary-rgb), 0.2) !important; } } `; var SITE_TONE_CLASS_PREFIX = "fc2-site-"; var SITE_CONFIGS = { default: { hostBgRgb: "16, 18, 22", cardRgb: "31, 34, 41", cardAltRgb: "24, 28, 35", toolbarRgb: "24, 27, 34", accentRgb: "255, 255, 255", cardAlpha: 1, borderAlpha: .08 }, fc2db: { hostBgRgb: "245, 246, 247", cardRgb: "255, 255, 255", cardAltRgb: "249, 250, 251", toolbarRgb: "255, 255, 255", accentRgb: "230, 230, 232", cardAlpha: 1, borderAlpha: 1, borderRadius: "8px", shadow: "0 1px 3px rgba(0, 0, 0, 0.06)", lightCardRgb: "255, 255, 255", lightCardAltRgb: "253, 248, 249" }, fd2ppv: { hostBgRgb: "18, 18, 18", cardRgb: "30, 30, 30", cardAltRgb: "42, 42, 42", toolbarRgb: "26, 26, 26", accentRgb: "51, 51, 51", cardAlpha: 1, borderAlpha: 1, borderRadius: "8px", shadow: "inset 0 -12px 24px rgba(0, 0, 0, 0.25)", lightCardRgb: "255, 255, 255", lightCardAltRgb: "245, 246, 247" }, javdb: { hostBgRgb: "23, 24, 28", cardRgb: "10, 10, 10", cardAltRgb: "18, 18, 18", toolbarRgb: "34, 34, 34", accentRgb: "51, 51, 51", cardAlpha: 1, borderAlpha: 1, borderRadius: "6px", shadow: "0 2px 8px rgba(0, 0, 0, 0.3)", lightCardRgb: "255, 255, 255", lightCardAltRgb: "245, 246, 247" }, missav: { hostBgRgb: "9, 8, 18", cardRgb: "17, 21, 32", cardAltRgb: "28, 32, 41", toolbarRgb: "36, 41, 51", accentRgb: "76, 86, 106", cardAlpha: .12, cardAltAlpha: .08, borderAlpha: .25, borderRadius: "8px", shadow: "0 2px 12px rgba(0, 0, 0, 0.4)", lightCardRgb: "255, 255, 255", lightCardAltRgb: "249, 250, 251" }, supjav: { hostBgRgb: "36, 36, 36", cardRgb: "24, 24, 24", cardAltRgb: "27, 27, 27", toolbarRgb: "27, 27, 27", accentRgb: "51, 51, 51", cardAlpha: .15, cardAltAlpha: .1, borderAlpha: .2, borderRadius: "6px", shadow: "0 2px 8px rgba(0, 0, 0, 0.3)", lightCardRgb: "255, 255, 255", lightCardAltRgb: "252, 245, 245" }, javfc2: { hostBgRgb: "19, 19, 19", cardRgb: "35, 35, 35", cardAltRgb: "27, 27, 27", toolbarRgb: "15, 15, 15", accentRgb: "33, 33, 33", cardAlpha: .12, cardAltAlpha: .08, borderAlpha: .2, borderRadius: "5px", shadow: "1px 1px 4px rgba(0, 0, 0, 0.3)" }, fc2cmadb: { hostBgRgb: "255, 255, 255", cardRgb: "255, 255, 255", cardAltRgb: "250, 250, 250", toolbarRgb: "255, 255, 255", accentRgb: "26, 26, 46", cardAlpha: 1, borderAlpha: 0, borderRadius: "8px", shadow: "0 2px 8px rgba(0, 0, 0, 0.08)", lightCardRgb: "255, 255, 255", lightCardAltRgb: "248, 248, 252" } }; var resolveSiteToneKey = (hostname) => { if (hostname.includes("fc2db.net")) return "fc2db"; if (hostname.includes("fd2ppv.cc")) return "fd2ppv"; if (hostname.includes("javdb")) return "javdb"; if (hostname.includes("missav")) return "missav"; if (hostname.includes("supjav.com")) return "supjav"; if (hostname.includes("javfc2.xyz")) return "javfc2"; if (hostname.includes("fc2cmadb.com")) return "fc2cmadb"; return "default"; }; var resolveSiteToneClass = (hostname) => { return `${SITE_TONE_CLASS_PREFIX}${resolveSiteToneKey(hostname)}`; }; var generateCommonVariables = () => ` :root { --fc2-host-bg-rgb: 16, 18, 22; --fc2-host-bg-luma: 21; --fc2-site-card-rgb: 31, 34, 41; --fc2-site-card-alt-rgb: 24, 28, 35; --fc2-site-toolbar-rgb: 24, 27, 34; --fc2-site-accent-rgb: 255, 255, 255; --fc2-site-card-alpha: 1; --fc2-site-card-alt-alpha: 1; --fc2-site-toolbar-alpha: 1; --fc2-site-border-alpha: 0.08; --fc2-bg: rgb(var(--fc2-host-bg-rgb)); --fc2-surface: rgba(var(--fc2-site-card-rgb), var(--fc2-site-card-alpha)); --fc2-surface-item: rgba(var(--fc2-site-card-alt-rgb), var(--fc2-site-card-alt-alpha)); --fc2-surface-float: rgba(var(--fc2-site-toolbar-rgb), var(--fc2-site-toolbar-alpha)); --fc2-border: rgba(var(--fc2-site-accent-rgb), var(--fc2-site-border-alpha)); --fc2-accent-grad: linear-gradient( 145deg, rgba(var(--fc2-site-accent-rgb), var(--fc2-opacity-subtle)), rgba(var(--fc2-site-card-rgb), var(--fc2-opacity-ghost)) ); } `; var generateSiteVariables = (key, config) => ` :root.${SITE_TONE_CLASS_PREFIX}${key} { --fc2-host-bg-rgb: ${config.hostBgRgb}; --fc2-site-card-rgb: ${config.cardRgb}; --fc2-site-card-alt-rgb: ${config.cardAltRgb}; --fc2-site-toolbar-rgb: ${config.toolbarRgb}; --fc2-site-accent-rgb: ${config.accentRgb}; --fc2-site-card-alpha: ${config.cardAlpha}; ${config.cardAltAlpha !== void 0 ? `--fc2-site-card-alt-alpha: ${config.cardAltAlpha};` : ""} ${config.toolbarAlpha !== void 0 ? `--fc2-site-toolbar-alpha: ${config.toolbarAlpha};` : ""} --fc2-site-border-alpha: ${config.borderAlpha}; } `; var generateSiteCardStyles = (key, config, C) => { const shadow = config.shadow || "0 1px 3px rgba(0, 0, 0, 0.12)"; const borderRadius = config.borderRadius || ""; return ` :root.${SITE_TONE_CLASS_PREFIX}${key} .${C.cardRebuilt}, :root.${SITE_TONE_CLASS_PREFIX}${key} .${C.processedCard}:not(.fc2-is-minimal) { background-image: none !important; background-color: rgba(var(--fc2-site-card-rgb), var(--fc2-site-card-alpha)); box-shadow: ${shadow}; border: 1px solid rgba(var(--fc2-site-accent-rgb), var(--fc2-site-border-alpha)); ${borderRadius ? `border-radius: ${borderRadius};` : ""} }`; }; var generateLightThemeOverrides = (key, config) => { if (!config.lightCardRgb || !config.lightCardAltRgb) return ""; return ` :root.fc2-light-theme.${SITE_TONE_CLASS_PREFIX}${key} { --fc2-site-card-rgb: ${config.lightCardRgb}; --fc2-site-card-alt-rgb: ${config.lightCardAltRgb}; }`; }; var generateCommonCardFusion = (C) => ` /* Default card fusion */ .${C.cardRebuilt}, .${C.processedCard}:not(.fc2-is-minimal) { background-image: none; background-color: rgba(var(--fc2-site-card-rgb), var(--fc2-site-card-alpha)); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); border: 1px solid rgba(var(--fc2-site-accent-rgb), var(--fc2-site-border-alpha)); } .${C.processedCard}.fc2-is-detail .${C.infoArea} { background-image: linear-gradient( 180deg, rgba(var(--fc2-site-toolbar-rgb), 0.04), rgba(var(--fc2-site-card-alt-rgb), 0.02) ); border-color: rgba(var(--fc2-site-accent-rgb), var(--fc2-site-border-alpha)); } .${C.videoPreviewContainer} { background: rgba(var(--fc2-host-bg-rgb), 0.94); } .fc2-enh-toolbar { background-color: rgba(var(--fc2-site-toolbar-rgb), 0.92); border: 1px solid rgba(var(--fc2-site-accent-rgb), 0.06); border-radius: var(--fc2-radius-md); } `; var generateLightThemeBase = () => ` /* Light Theme (global detection) */ :root.fc2-light-theme { --fc2-host-bg-rgb: 246, 247, 249; --fc2-site-card-rgb: 251, 251, 253; --fc2-site-card-alt-rgb: 245, 247, 250; --fc2-site-toolbar-rgb: 255, 255, 255; --fc2-site-accent-rgb: 17, 17, 17; --fc2-site-card-alpha: 1; --fc2-site-card-alt-alpha: 1; --fc2-site-toolbar-alpha: 1; --fc2-site-border-alpha: 0.10; } `; var FC2DB_EXTRA = (C) => ` /* FC2DB: poster ratio for cover images */ :root.${SITE_TONE_CLASS_PREFIX}fc2db .${C.videoPreviewContainer} { aspect-ratio: 71 / 100; } :root.${SITE_TONE_CLASS_PREFIX}fc2db .${C.videoPreviewContainer} img { object-fit: cover !important; } `; var JAVDB_EXTRA = (C) => ` /* JavDB: list covers use poster ratio */ :root.${SITE_TONE_CLASS_PREFIX}javdb .${C.videoPreviewContainer} { aspect-ratio: var(--fc2-javdb-cover-ratio, 2 / 3); } :root.${SITE_TONE_CLASS_PREFIX}javdb .${C.videoPreviewContainer} img { object-fit: contain !important; } `; var JAVFC2_EXTRA = (C) => ` /* JavFC2: Carousel & layout fixes */ :root.${SITE_TONE_CLASS_PREFIX}javfc2 .owl-carousel, :root.${SITE_TONE_CLASS_PREFIX}javfc2 .owl-stage-outer { overflow: hidden !important; } :root.${SITE_TONE_CLASS_PREFIX}javfc2 .owl-item { overflow: visible !important; } :root.${SITE_TONE_CLASS_PREFIX}javfc2 .owl-item .${C.infoArea} { background-color: rgba(var(--fc2-site-card-alt-rgb), 0.96) !important; } :root.${SITE_TONE_CLASS_PREFIX}javfc2 .owl-item .${C.customTitle} { font-size: var(--fc2-text-xs) !important; max-height: 2.8em !important; margin-bottom: var(--fc2-space-sm) !important; display: -webkit-box !important; -webkit-line-clamp: 2 !important; -webkit-box-orient: vertical !important; overflow: hidden !important; } :root.${SITE_TONE_CLASS_PREFIX}javfc2 .owl-item .${C.cardActionRow} { gap: var(--fc2-space-xs) !important; flex-wrap: wrap !important; justify-content: center !important; } :root.${SITE_TONE_CLASS_PREFIX}javfc2 .owl-item .en-pill-btn { padding: 2px var(--fc2-space-sm) !important; font-size: var(--fc2-text-xs) !important; min-width: var(--fc2-btn-sm) !important; } :root.${SITE_TONE_CLASS_PREFIX}javfc2 .owl-item .${C.cardMeta} { display: none !important; } :root.${SITE_TONE_CLASS_PREFIX}javfc2 .${C.processedCard} { box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.3) !important; height: auto !important; } :root.${SITE_TONE_CLASS_PREFIX}javfc2 .${C.videoPreviewContainer} { aspect-ratio: 3 / 4 !important; } :root.${SITE_TONE_CLASS_PREFIX}javfc2 .${C.videoPreviewContainer} img { object-fit: cover !important; } :root.${SITE_TONE_CLASS_PREFIX}javfc2 .${C.infoArea} { padding: var(--fc2-space-sm) !important; } :root.${SITE_TONE_CLASS_PREFIX}javfc2 .${C.customTitle} { font-size: var(--fc2-text-xs) !important; line-height: 1.4 !important; height: 2.8em !important; } /* JavFC2: strip double borders on replaced cards */ :root.${SITE_TONE_CLASS_PREFIX}javfc2 .${C.cardRebuilt} { background: none !important; border: none !important; box-shadow: none !important; padding: 0 !important; } /* Mobile and narrow screen optimizations */ @media (max-width: 640px) { :root.${SITE_TONE_CLASS_PREFIX}javfc2 .owl-item .${C.infoArea} { min-height: 90px !important; padding: var(--fc2-space-xs) !important; } :root.${SITE_TONE_CLASS_PREFIX}javfc2 .owl-item .${C.customTitle} { font-size: var(--fc2-text-xs) !important; height: 2.6em !important; margin-bottom: var(--fc2-space-xs) !important; } :root.${SITE_TONE_CLASS_PREFIX}javfc2 .owl-item .en-pill-btn { font-size: 9px !important; padding: 1px 4px !important; } } `; var FC2CMADB_EXTRA = (C) => ` /* DaisyUI v5 token passthrough */ @supports (color: var(--color-base-100)) { :root.${SITE_TONE_CLASS_PREFIX}fc2cmadb .${C.cardRebuilt}, :root.${SITE_TONE_CLASS_PREFIX}fc2cmadb .${C.processedCard}:not(.fc2-is-minimal) { background-color: var(--color-base-100); } :root.${SITE_TONE_CLASS_PREFIX}fc2cmadb .${C.infoArea} { background: rgba(128, 128, 128, 0.06); border-top-color: rgba(128, 128, 128, 0.15); } :root.${SITE_TONE_CLASS_PREFIX}fc2cmadb .${C.resourceLinksContainer} { background: rgba(128, 128, 128, 0.06); border-color: rgba(128, 128, 128, 0.12); } :root.${SITE_TONE_CLASS_PREFIX}fc2cmadb .${C.resourceLinksContainer} > *:not(:last-child) { border-right-color: rgba(128, 128, 128, 0.15) !important; } :root.${SITE_TONE_CLASS_PREFIX}fc2cmadb .fc2-card-meta-row .${C.resourceBtn} { background: rgba(128, 128, 128, 0.10); border-color: rgba(128, 128, 128, 0.15); } :root.${SITE_TONE_CLASS_PREFIX}fc2cmadb .fc2-card-action-row .${C.resourceLinksContainer} > *:hover { background: rgba(128, 128, 128, 0.10) !important; } } `; var getSiteFusionStyles = (C, hostname) => { const siteKey = resolveSiteToneKey(hostname || (typeof location !== "undefined" ? location.hostname : "")); const config = SITE_CONFIGS[siteKey]; let css = generateCommonVariables(); css += generateSiteVariables(siteKey, config); css += generateSiteCardStyles(siteKey, config, C); if (siteKey === "fc2db") css += FC2DB_EXTRA(C); else if (siteKey === "javdb") css += JAVDB_EXTRA(C); else if (siteKey === "javfc2") css += JAVFC2_EXTRA(C); else if (siteKey === "fc2cmadb") css += FC2CMADB_EXTRA(C); css += generateLightThemeBase(); css += generateLightThemeOverrides(siteKey, config); css += generateCommonCardFusion(C); return css; }; var tokens = ` /* ============================================================ DESIGN TOKENS — Unified Design System ============================================================ */ :root, :host { /* ── Colors (Semantic) ── */ --fc2-bg: #0a0a0c; --fc2-surface: rgba(22, 22, 26, 0.8); --fc2-surface-float: rgba(28, 28, 34, 0.92); --fc2-surface-low: rgba(255, 255, 255, 0.015); --fc2-surface-lowest: rgba(255, 255, 255, 0.01); --fc2-surface-item: rgba(255, 255, 255, 0.03); --fc2-text: #f4f4f7; --fc2-text-dim: #9494a0; --fc2-text-muted: #62626e; --fc2-text-bright: #ffffff; --fc2-text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); --fc2-border: rgba(255, 255, 255, 0.08); --fc2-primary: #ffffff; --fc2-on-primary: #000000; --fc2-success: #34d399; --fc2-danger: #f87171; --fc2-warn: #fab387; --fc2-info: #89b4fa; --fc2-accent: #e4e4e7; /* ── Semantic RGB (for rgba() usage) ── */ --fc2-black-rgb: 0, 0, 0; --fc2-primary-rgb: 255, 255, 255; --fc2-success-rgb: 52, 211, 153; --fc2-danger-rgb: 248, 113, 113; --fc2-warn-rgb: 250, 179, 135; --fc2-info-rgb: 137, 180, 250; /* ── Opacity Scale (6 levels) ── */ --fc2-opacity-ghost: 0.05; --fc2-opacity-subtle: 0.1; --fc2-opacity-muted: 0.25; --fc2-opacity-dim: 0.5; --fc2-opacity-strong: 0.85; --fc2-opacity-full: 1; /* ── Spacing (8px grid, 8 levels) ── */ --fc2-space-xxs: 2px; --fc2-space-xs: 4px; --fc2-space-sm: 8px; --fc2-space-base: 12px; --fc2-space-md: 16px; --fc2-space-lg: 24px; --fc2-space-xl: 32px; --fc2-space-2xl: 40px; /* ── Typography (5 sizes, 4 weights, 3 line-heights) ── */ --fc2-text-xs: 12px; --fc2-text-sm: 13px; --fc2-text-base: 14px; --fc2-text-lg: 16px; --fc2-text-xl: 20px; --fc2-font-medium: 500; --fc2-font-semibold: 600; --fc2-font-bold: 700; --fc2-line-tight: 1.25; --fc2-line-base: 1.5; --fc2-letter-tight: 0.5px; --fc2-font: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif; --fc2-font-mono: 'JetBrains Mono', ui-monospace, 'Cascadia Code', 'Fira Code', monospace; /* ── Border Radius (4 levels + full) ── */ --fc2-radius-sm: 6px; --fc2-radius-md: 12px; --fc2-radius-lg: 16px; --fc2-radius-xl: 20px; --fc2-radius-full: 9999px; /* ── Shadows (3 levels) ── */ --fc2-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2); --fc2-shadow-md: 0 4px 8px rgba(0, 0, 0, 0.25); --fc2-shadow-lg: 0 6px 16px rgba(0, 0, 0, 0.3); --fc2-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); --fc2-focus-ring: 0 0 0 calc(var(--fc2-space-xxs) + var(--fc2-border-width)) rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); /* ── Motion (3 speeds, 3 curves) ── */ --fc2-motion-fast: 0.18s; --fc2-motion-base: 0.24s; --fc2-motion-slow: 0.38s; --fc2-ease-out: cubic-bezier(0.16, 1, 0.3, 1); --fc2-ease-standard: cubic-bezier(0.4, 0, 0.2, 1); --fc2-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* ── Z-Index (5 levels) ── */ --fc2-z-dropdown: 1000; --fc2-z-tooltip: 100000; --fc2-z-fab: 2147483620; --fc2-z-modal: 2147483600; --fc2-z-toast: 2147483647; /* ── Buttons (4 sizes) ── */ --fc2-btn-sm: 32px; --fc2-btn-md: 36px; --fc2-btn-lg: 44px; --fc2-btn-radius: var(--fc2-radius-md); /* ── Stroke & Effects ── */ --fc2-border-width: 1px; --fc2-blur: 12px; --fc2-glass-saturate: 180%; /* ── Gradients ── */ --fc2-accent-grad: linear-gradient(135deg, #3f3f46, #18181b); /* ── Scrollbar ── */ --fc2-scrollbar-thumb: rgba(255, 255, 255, 0.08); --fc2-scrollbar-hover: rgba(255, 255, 255, 0.15); /* ── Glass Effect ── */ --fc2-liquid-bg: #0a0a0c; --fc2-liquid-border: 1px solid rgba(255, 255, 255, 0.08); --fc2-rim-light: inset 1px 1px 0px 0px rgba(255, 255, 255, 0.12), inset -1px -1px 0px 0px rgba(0, 0, 0, 0.3); --fc2-liquid-iridescent: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(180, 200, 255, 0.05) 30%, rgba(255, 180, 255, 0.04) 70%, rgba(255, 255, 255, 0.1) 100%); --fc2-glass-noise: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E"); /* ── Extended Borders ── */ --fc2-border-strong: rgba(255, 255, 255, 0.2); } /* ============================================================ LIGHT THEME OVERRIDES ============================================================ */ :root.fc2-light-theme { --fc2-bg: #f8f9fa; --fc2-surface: rgba(255, 255, 255, 0.8); --fc2-surface-float: rgba(255, 255, 255, 0.92); --fc2-surface-low: rgba(0, 0, 0, 0.02); --fc2-surface-lowest: rgba(0, 0, 0, 0.01); --fc2-surface-item: rgba(0, 0, 0, 0.03); --fc2-text: #1a1a1a; --fc2-text-dim: #52525b; --fc2-text-muted: #a1a1aa; --fc2-text-bright: var(--fc2-text); --fc2-text-shadow: none; --fc2-border: rgba(0, 0, 0, 0.08); --fc2-primary: #111111; --fc2-on-primary: #ffffff; --fc2-success: #16a34a; --fc2-danger: #dc2626; --fc2-warn: #d97706; --fc2-info: #2563eb; --fc2-accent: #3f3f46; --fc2-primary-rgb: 17, 17, 17; --fc2-success-rgb: 22, 163, 74; --fc2-danger-rgb: 220, 38, 38; --fc2-warn-rgb: 217, 119, 6; --fc2-info-rgb: 37, 99, 235; --fc2-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06); --fc2-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); --fc2-shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.12); --fc2-shadow: 0 12px 32px rgba(0, 0, 0, 0.1); --fc2-focus-ring: 0 0 0 calc(var(--fc2-space-xxs) + var(--fc2-border-width)) rgba(var(--fc2-primary-rgb), var(--fc2-opacity-subtle)); --fc2-accent-grad: linear-gradient(135deg, #e4e4e7, #f4f4f5); --fc2-scrollbar-thumb: rgba(0, 0, 0, 0.15); --fc2-scrollbar-hover: rgba(0, 0, 0, 0.25); --fc2-liquid-bg: #f0f0f2; --fc2-liquid-border: 1px solid rgba(0, 0, 0, 0.08); --fc2-rim-light: inset 1px 1px 0px 0px rgba(255, 255, 255, 0.8), inset -1px -1px 0px 0px rgba(0, 0, 0, 0.05); --fc2-liquid-iridescent: linear-gradient(135deg, rgba(0, 0, 0, 0.03) 0%, rgba(0, 50, 100, 0.02) 30%, rgba(100, 0, 100, 0.02) 70%, rgba(0, 0, 0, 0.03) 100%); --fc2-border-strong: rgba(0, 0, 0, 0.2); } `; /** * Styles Entry Point */ var cachedCss = null; var cachedOptions = null; var clearStyleCache = () => { cachedCss = null; cachedOptions = null; }; var getPerformanceFix = (hostname) => { if (!(hostname.includes("missav") || hostname.includes("supjav") || hostname.includes("javdb") || hostname.includes("fc2cmadb"))) return ""; return ` .${Config.CLASSES.processedCard}:nth-child(n+51) { content-visibility: auto; contain-intrinsic-size: 320px 280px; } `; }; var getSiteSpecificFix = (hostname) => { if (!hostname.includes("fd2ppv")) return ""; return ` .artist-card.fc2-card-rebuilt, .work-card.fc2-card-rebuilt, .work-list > div, .artist-list > div { overflow: visible !important; } `; }; var getConsolidatedCss = (options) => { const opts = { minify: true, includePerformanceFix: true, includeSiteFix: true, ...options }; if (cachedCss && cachedOptions && JSON.stringify(cachedOptions) === JSON.stringify(opts)) return cachedCss; const C = Config.CLASSES; const hostname = opts.hostname || (typeof location !== "undefined" ? location.hostname : ""); const rawCss = [ tokens, getSiteFusionStyles(C, hostname), getAnimations(C), getBaseStyles(C), getComponentStyles(C), opts.includePerformanceFix ? getPerformanceFix(hostname) : "", opts.includeSiteFix ? getSiteSpecificFix(hostname) : "", getMobileStyles(C) ].filter(Boolean).join("\n"); const finalCss = opts.minify ? minifyCss(rawCss) : rawCss; cachedCss = finalCss; cachedOptions = opts; return finalCss; }; var log$28 = Logger.scope("Style"); var StyleService = class { themeObserver = null; injectedStyle = null; domReadyHandler = null; preferredSchemeQuery = null; preferredSchemeHandler = null; currentSiteToneClass = null; pendingThemeDetection = false; dataThemeObserver = null; onInit() { log$28.debug("Injecting global styles"); this.injectCss(); this.initThemeDetection(); } onCleanup() { this.dataThemeObserver?.disconnect(); this.dataThemeObserver = null; this.themeObserver?.disconnect(); this.themeObserver = null; if (this.domReadyHandler) { document.removeEventListener("DOMContentLoaded", this.domReadyHandler); this.domReadyHandler = null; } if (this.preferredSchemeQuery && this.preferredSchemeHandler) { if (typeof this.preferredSchemeQuery.removeEventListener === "function") this.preferredSchemeQuery.removeEventListener("change", this.preferredSchemeHandler); else if (typeof this.preferredSchemeQuery.removeListener === "function") this.preferredSchemeQuery.removeListener(this.preferredSchemeHandler); } this.preferredSchemeQuery = null; this.preferredSchemeHandler = null; if (this.injectedStyle?.isConnected) this.injectedStyle.remove(); this.injectedStyle = null; const root = document.documentElement; if (this.currentSiteToneClass && root.classList.contains(this.currentSiteToneClass)) root.classList.remove(this.currentSiteToneClass); this.currentSiteToneClass = null; root.style.removeProperty("--fc2-host-bg-rgb"); root.style.removeProperty("--fc2-host-bg-luma"); clearStyleCache(); } parseRgbColor(color) { const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i); if (!match) return null; const r = Number(match[1]); const g = Number(match[2]); const b = Number(match[3]); if ([ r, g, b ].some((value) => Number.isNaN(value))) return null; return { r, g, b }; } resolveHostBackgroundColor() { const html = document.documentElement; if (html?.classList.contains("dark") || html?.getAttribute("data-theme") === "dark" || html?.getAttribute("data-mode") === "dark" || document.body?.classList.contains("dark")) return "rgb(17, 24, 39)"; if (window.location.hostname.includes("missav") || window.location.hostname.includes("javfc2.xyz") || document.querySelector("[class*=\"bg-nord\"]")) return "rgb(18, 18, 18)"; const candidates = [ document.body, document.querySelector("#app"), document.querySelector("#__nuxt"), document.querySelector(".app"), document.querySelector(".wrapper"), document.querySelector("main"), document.querySelector("#main"), document.documentElement ]; for (const candidate of candidates) { if (!candidate) continue; const bg = window.getComputedStyle(candidate).backgroundColor; if (!bg || bg === "transparent" || bg === "rgba(0, 0, 0, 0)") continue; return bg; } return null; } applySiteToneClass(root) { const nextClass = resolveSiteToneClass(location.hostname); if (this.currentSiteToneClass === nextClass) return; if (this.currentSiteToneClass) root.classList.remove(this.currentSiteToneClass); root.classList.add(nextClass); this.currentSiteToneClass = nextClass; } applyHostBackgroundVars(root, rgb, isLight) { const effective = rgb || (isLight ? { r: 246, g: 247, b: 249 } : { r: 16, g: 18, b: 22 }); const luma = effective.r * .299 + effective.g * .587 + effective.b * .114; const rgbStr = `${effective.r}, ${effective.g}, ${effective.b}`; const lumaStr = luma.toFixed(2); if (root.style.getPropertyValue("--fc2-host-bg-rgb") !== rgbStr) root.style.setProperty("--fc2-host-bg-rgb", rgbStr); if (root.style.getPropertyValue("--fc2-host-bg-luma") !== lumaStr) root.style.setProperty("--fc2-host-bg-luma", lumaStr); } injectCss() { if (this.injectedStyle?.isConnected) return; const css = getConsolidatedCss({ minify: true }); if (typeof GM_addStyle !== "undefined") this.injectedStyle = GM_addStyle(css); else { const style = document.createElement("style"); style.textContent = css; document.head.appendChild(style); this.injectedStyle = style; } log$28.debug(`Injected CSS: ${Math.round(css.length / 1024)}KB`); } removePreferredSchemeListener() { if (!this.preferredSchemeQuery || !this.preferredSchemeHandler) return; if (typeof this.preferredSchemeQuery.removeEventListener === "function") this.preferredSchemeQuery.removeEventListener("change", this.preferredSchemeHandler); else if (typeof this.preferredSchemeQuery.removeListener === "function") this.preferredSchemeQuery.removeListener(this.preferredSchemeHandler); } addPreferredSchemeListener(detectTheme) { if (!this.preferredSchemeQuery) return; this.preferredSchemeHandler = () => detectTheme(); if (typeof this.preferredSchemeQuery.addEventListener === "function") this.preferredSchemeQuery.addEventListener("change", this.preferredSchemeHandler); else if (typeof this.preferredSchemeQuery.addListener === "function") this.preferredSchemeQuery.addListener(this.preferredSchemeHandler); } setupThemeObservation(detectTheme) { const scheduleThemeDetection = () => { if (this.pendingThemeDetection) return; this.pendingThemeDetection = true; queueMicrotask(() => { this.pendingThemeDetection = false; detectTheme(); }); }; const hasThemeSignal = (className) => { return /(^|\s)(dark|light|theme|night|day|nord|black|white)(-|\b)/i.test(className); }; const startObserving = () => { if (!document.body) return; this.themeObserver?.disconnect(); this.themeObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type !== "attributes") continue; const target = mutation.target; if (target === document.documentElement) { if (mutation.attributeName === "class" || mutation.attributeName === "data-theme" || mutation.attributeName === "data-mode") { scheduleThemeDetection(); return; } } if (target === document.body && mutation.attributeName === "class" && hasThemeSignal(document.body.className)) { scheduleThemeDetection(); return; } } }); this.themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: [ "class", "data-theme", "data-mode" ] }); this.themeObserver.observe(document.body, { attributes: true, attributeFilter: ["class"] }); }; if (document.body) { detectTheme(); startObserving(); this.domReadyHandler = null; return; } this.domReadyHandler = () => { detectTheme(); startObserving(); this.domReadyHandler = null; }; document.addEventListener("DOMContentLoaded", this.domReadyHandler, { once: true }); } initThemeDetection() { let lastUpdate = 0; this.removePreferredSchemeListener(); this.preferredSchemeHandler = null; this.preferredSchemeQuery = typeof window.matchMedia === "function" ? window.matchMedia("(prefers-color-scheme: light)") : null; this.themeObserver?.disconnect(); this.themeObserver = null; this.dataThemeObserver?.disconnect(); this.dataThemeObserver = null; const syncDataTheme = () => { const root = document.documentElement; if (!root) return; const theme = root.getAttribute("data-theme"); const hasClass = root.classList.contains("fc2-light-theme"); if ((theme === "dark" || theme?.includes("dark")) && hasClass) { root.classList.remove("fc2-light-theme"); clearStyleCache(); } else if ((theme === "light" || theme === "cupcake" || theme === "winter") && !hasClass) { root.classList.add("fc2-light-theme"); clearStyleCache(); } }; const detectTheme = () => { const root = document.documentElement; if (!root) return; const now = Date.now(); if (now - lastUpdate < 500) return; lastUpdate = now; const applyTheme = (isLight) => { const hasClass = root.classList.contains("fc2-light-theme"); if (isLight && !hasClass) { root.classList.add("fc2-light-theme"); clearStyleCache(); } else if (!isLight && hasClass) { root.classList.remove("fc2-light-theme"); clearStyleCache(); } }; if (!document.body) { applyTheme(this.preferredSchemeQuery?.matches ?? false); this.applySiteToneClass(root); this.applyHostBackgroundVars(root, null, this.preferredSchemeQuery?.matches ?? false); return; } const htmlTheme = root.getAttribute("data-theme"); if (htmlTheme === "dark" || htmlTheme?.includes("dark")) { applyTheme(false); this.applySiteToneClass(root); this.applyHostBackgroundVars(root, { r: 17, g: 24, b: 39 }, false); return; } if (htmlTheme === "light" || htmlTheme === "cupcake" || htmlTheme === "winter") { applyTheme(true); this.applySiteToneClass(root); this.applyHostBackgroundVars(root, { r: 246, g: 247, b: 249 }, true); return; } const hostBg = this.resolveHostBackgroundColor(); const rgb = hostBg ? this.parseRgbColor(hostBg) : null; if (!rgb) { applyTheme(this.preferredSchemeQuery?.matches ?? false); this.applySiteToneClass(root); this.applyHostBackgroundVars(root, null, this.preferredSchemeQuery?.matches ?? false); return; } const isLight = rgb.r * .299 + rgb.g * .587 + rgb.b * .114 > 180; applyTheme(isLight); this.applySiteToneClass(root); this.applyHostBackgroundVars(root, rgb, isLight); }; if (this.domReadyHandler) document.removeEventListener("DOMContentLoaded", this.domReadyHandler); this.setupThemeObservation(detectTheme); this.addPreferredSchemeListener(detectTheme); this.dataThemeObserver = new MutationObserver((mutations) => { for (const mutation of mutations) if (mutation.type === "attributes" && mutation.attributeName === "data-theme") { syncDataTheme(); return; } }); this.dataThemeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] }); syncDataTheme(); } getCss() { return getConsolidatedCss({ minify: false }); } getCssSize() { const raw = getConsolidatedCss({ minify: false }); const minified = getConsolidatedCss({ minify: true }); return { raw: raw.length, minified: minified.length, ratio: Math.round((1 - minified.length / raw.length) * 100) }; } }; /** Constructed once from app/modules (composition root). */ function createStyleManager() { return new StyleService(); } var ports$1 = null; function configureProviderRegistry(next) { ports$1 = next; } function requirePorts$1() { if (!ports$1) throw new Error("[ProviderRegistry] Not configured. Call configureProviderRegistry from app/modules."); return ports$1; } var getActiveSyncProvider = () => { const { state, webdav, supabase } = requirePorts$1(); const mode = state.get("syncMode"); if (mode === "webdav") return webdav; if (mode === "supabase") return supabase; return null; }; var getSupabaseAuthProvider = () => requirePorts$1().supabase; var getWebDAVTestProvider = () => requirePorts$1().webdav; /** Status priority: higher value supersedes lower on conflict */ var STATUS_PRIORITY = { watched: 1, downloaded: 2 }; /** Compares only the user-visible history fields, ignoring sync bookkeeping flags. */ var historyItemsEqual = (left, right) => left.id === right.id && left.timestamp === right.timestamp && left.status === right.status && left.updated_at === right.updated_at && Boolean(left.is_deleted) === Boolean(right.is_deleted); /** True when user-visible history fields changed; ignores sync bookkeeping and version timestamps. */ var historySemanticChanged = (left, right) => left.id !== right.id || left.timestamp !== right.timestamp || left.status !== right.status || Boolean(left.is_deleted) !== Boolean(right.is_deleted); /** * Pure history merge for sync (local vs remote). * Priority: preferRemote flag > tombstone > status priority > timestamp. */ var MergeEngine = class { static resolveRecordTime(updatedAt, fallbackTimestamp) { const parsed = updatedAt ? Date.parse(updatedAt) : NaN; return Number.isFinite(parsed) ? parsed : fallbackTimestamp; } static normalizeHistoryItem(item) { const fallbackTime = typeof item.timestamp === "number" ? item.timestamp : 0; return { ...item, updated_at: new Date(this.resolveRecordTime(item.updated_at, fallbackTime)).toISOString() }; } /** Public so sync providers can decide whether a local record should be pushed. */ static chooseWinner(local, remote, preferRemote) { if (preferRemote) return remote; if (local.is_deleted && !remote.is_deleted) return local; if (!local.is_deleted && remote.is_deleted) return remote; const localPriority = STATUS_PRIORITY[local.status] ?? 0; const remotePriority = STATUS_PRIORITY[remote.status] ?? 0; if (localPriority !== remotePriority) return localPriority > remotePriority ? local : remote; const localTime = new Date(local.updated_at).getTime(); const remoteTime = new Date(remote.updated_at).getTime(); if (Number.isNaN(localTime) && Number.isNaN(remoteTime)) return local; if (Number.isNaN(localTime)) return remote; if (Number.isNaN(remoteTime)) return local; return localTime >= remoteTime ? local : remote; } static merge(localHistory, remoteHistory, preferRemote = false) { const historyMap = /* @__PURE__ */ new Map(); if (remoteHistory) remoteHistory.forEach((r) => { const normalized = this.normalizeHistoryItem(r); historyMap.set(normalized.id, normalized); }); for (const candidate of localHistory) { const local = this.normalizeHistoryItem(candidate); const remote = historyMap.get(local.id); if (!remote) historyMap.set(local.id, local); else historyMap.set(local.id, this.chooseWinner(local, remote, preferRemote)); } return { history: Array.from(historyMap.values()) }; } }; var currentSiteHost = () => typeof location !== "undefined" && location.hostname ? location.hostname : "unknown"; var getSiteScopedStorageKey = (baseKey, host = currentSiteHost()) => `${baseKey}:${host}`; var getSyncCursorStorageKey = exports("q",() => getSiteScopedStorageKey(STORAGE_KEYS.LAST_SYNC_TS)); var readSyncCursor = () => GM_getValue(getSyncCursorStorageKey(), UI_CONSTANTS.DEFAULT_TIMESTAMP) || UI_CONSTANTS.DEFAULT_TIMESTAMP; var writeSyncCursor = (value) => { GM_setValue(getSyncCursorStorageKey(), value); }; var resetSyncCursor = () => { GM_deleteValue(getSyncCursorStorageKey()); }; var getLastAutoSyncStorageKey = () => getSiteScopedStorageKey(STORAGE_KEYS.LAST_AUTO_SYNC_TS); var readLastAutoSyncTs = () => GM_getValue(getLastAutoSyncStorageKey(), 0) || 0; var writeLastAutoSyncTs = (value) => { GM_setValue(getLastAutoSyncStorageKey(), value); }; var getWebDAVEtagStorageKey = () => getSiteScopedStorageKey(STORAGE_KEYS.WEBDAV_LAST_ETAG); var readWebDAVEtag = () => GM_getValue(getWebDAVEtagStorageKey(), "") || ""; var writeWebDAVEtag = (value) => { GM_setValue(getWebDAVEtagStorageKey(), value); }; var resetWebDAVEtag = () => { GM_deleteValue(getWebDAVEtagStorageKey()); }; var log$27 = Logger.scope("SupabaseClient"); var SupabaseClient = class { static getConfig() { let url = resolveService(STATE_SERVICE).proxy.supabaseUrl || ""; if (url.endsWith("/")) url = url.slice(0, -1); url = url.trim(); const key = (resolveService(STATE_SERVICE).proxy.supabaseKey || "").trim(); if (key) log$27.debug(`Using API Key (len: ${key.length})`); else log$27.warn("No Supabase API Key configured"); return { url, key }; } static async request(endpoint, method, body = null, headers = {}) { const { url, key } = this.getConfig(); if (!url || !key) throw new Error("No Supabase config"); const isAuth = endpoint.includes("/auth/v1/"); const isJwtKey = key.startsWith("eyJ"); const requestHeaders = { apikey: key, "Content-Type": "application/json", ...headers }; if (!requestHeaders.Authorization && !isAuth && isJwtKey) requestHeaders.Authorization = `Bearer ${key}`; if (isAuth) log$27.debug(`Auth request: ${method} ${endpoint}`, { url: `${url}${endpoint}`, hasApiKey: !!key, apiKeyLength: key.length }); log$27.trace(`Request: ${method} ${endpoint}`); try { const response = await http(`${url}${endpoint}`, { method, headers: requestHeaders, data: body ?? void 0 }); log$27.trace(`Response: ${method} ${endpoint}`, { response }); return response; } catch (e) { const status = getHttpStatus(e); if (status === 401 || status === 403) { const { key } = this.getConfig(); let hint = ""; if (!key) hint = " (Key is missing)"; else if (key.length < 20) hint = ` (Key is suspiciously short: ${key.length} chars)`; else if (key.startsWith("sb_")) hint = " (sb_ keys only work via apikey header, not Authorization: Bearer)"; else if (!isAuth && !requestHeaders.Authorization) hint = " (Missing Authorization header)"; const respBody = e.response; log$27.error(`Supabase Request failed: ${method} ${endpoint}${hint}${respBody ? ` | Response: ${respBody.slice(0, 500)}` : ""}`, e); } else log$27.error(`Request failed: ${method} ${endpoint}`, e); throw e; } } static async getAuthHeader() { const jwt = GM_getValue(STORAGE_KEYS.SUPABASE_JWT); if (jwt) try { const parts = jwt.split("."); if (parts.length >= 2 && parts[1]) { if (JSON.parse(atob(parts[1])).exp * 1e3 > Date.now() + 6e4) return `Bearer ${jwt}`; } } catch {} const refresh = GM_getValue(STORAGE_KEYS.SUPABASE_REFRESH); if (!refresh) return null; try { const data = await this.request(`${SUPABASE_ENDPOINTS.TOKEN}?grant_type=refresh_token`, "POST", { refresh_token: refresh }); if (data?.access_token) { GM_setValue(STORAGE_KEYS.SUPABASE_JWT, data.access_token); GM_setValue(STORAGE_KEYS.SUPABASE_REFRESH, data.refresh_token); GM_setValue(STORAGE_KEYS.SYNC_USER_ID, data.user.id); return `Bearer ${data.access_token}`; } } catch (e) { log$27.error("Token refresh failed", e); const status = getHttpStatus(e); const response = e.response; if (status === 401 || response && response.includes("invalid_grant")) { [ STORAGE_KEYS.SYNC_USER_ID, STORAGE_KEYS.SUPABASE_JWT, STORAGE_KEYS.SUPABASE_REFRESH ].forEach((k) => GM_deleteValue(k)); resetSyncCursor(); } } return null; } }; var notifyManualSyncStart = (isManual, syncingMessage) => { if (isManual) CoreEvents.emit(AppEvents.SHOW_TOAST, { message: syncingMessage, type: "info" }); }; var createSyncProviderErrorHandler = ({ mode, defaultErrorMessage, manualErrorToastPrefix, translate, buildSummary, logError }) => { return (error, isManual, traceId) => { const err = error; logError?.(error, traceId); resolveService(STATE_SERVICE).proxy.lastSyncResult = buildSummary({ mode, status: SYNC_STATUS.ERROR, completedAt: (/* @__PURE__ */ new Date()).toISOString(), errorMessage: err.message || translate(defaultErrorMessage), translate }); resolveService(STATE_SERVICE).proxy.syncStatus = SYNC_STATUS.ERROR; if (isManual) CoreEvents.emit(AppEvents.SHOW_TOAST, { message: translate(manualErrorToastPrefix) + (err.message || ""), type: "error" }); }; }; var defaultTranslate = (key, params) => { let value = { syncSummaryFailed: "Sync failed", syncSummaryFailedWithMessage: "Sync failed: {message}", syncSummaryCompleted: "Sync completed", syncSummaryMode: "Mode: {mode}", syncSummaryPushed: "Pushed {count}", syncSummaryPulled: "Pulled {count}", syncSummaryDirty: "Dirty {count}" }[key] || key; if (params) Object.entries(params).forEach(([paramKey, paramValue]) => { value = value.replace(new RegExp(`{${paramKey}}`, "g"), String(paramValue)); }); return value; }; var buildSyncSummaryText = (input) => { const translate = input.translate || defaultTranslate; if (input.summary && !input.excludeMode) return input.summary; if (input.status === "error") return input.errorMessage ? translate("syncSummaryFailedWithMessage", { message: input.errorMessage }) : translate("syncSummaryFailed"); const parts = [ input.excludeMode ? null : translate("syncSummaryMode", { mode: input.mode }), typeof input.pushedCount === "number" ? translate("syncSummaryPushed", { count: input.pushedCount }) : null, typeof input.pulledCount === "number" ? translate("syncSummaryPulled", { count: input.pulledCount }) : null, typeof input.dirtyCount === "number" ? translate("syncSummaryDirty", { count: input.dirtyCount }) : null ].filter(Boolean); return parts.length > 0 ? parts.join(" · ") : translate("syncSummaryCompleted"); }; var createSyncResultSummary = (input) => ({ mode: input.mode, status: input.status, startedAt: input.startedAt, completedAt: input.completedAt || (/* @__PURE__ */ new Date()).toISOString(), lastSyncTs: input.lastSyncTs, pushedCount: input.pushedCount, pulledCount: input.pulledCount, dirtyCount: input.dirtyCount, conflict: input.conflict || null, errorMessage: input.errorMessage, summary: buildSyncSummaryText(input), detailSummary: buildSyncSummaryText({ ...input, excludeMode: true }) }); var clearSupabaseSession = () => { [ STORAGE_KEYS.SYNC_USER_ID, STORAGE_KEYS.SUPABASE_JWT, STORAGE_KEYS.SUPABASE_REFRESH ].forEach((key) => { GM_deleteValue(key); }); resetSyncCursor(); }; var persistSupabaseLogin = (data) => { GM_setValue(STORAGE_KEYS.SYNC_USER_ID, data.user.id); GM_setValue(STORAGE_KEYS.SUPABASE_JWT, data.access_token); GM_setValue(STORAGE_KEYS.SUPABASE_REFRESH, data.refresh_token); writeSyncCursor(UI_CONSTANTS.DEFAULT_TIMESTAMP); return data.user; }; var buildSupabaseUpsertPayload = ({ dirty, userId, onProgress, log, traceId }) => { if (dirty.length === 0) return []; onProgress?.({ phase: "Pushing local data", percent: 30 }); const payload = dirty.map((record) => { return { fc2_id: isNaN(Number(record.id)) ? record.id : parseInt(record.id, 10), last_watched_at: new Date(record.timestamp).toISOString(), status: record.status || "watched", is_deleted: !!record.is_deleted, user_id: userId || null }; }); if (payload.length > 1) { const firstKeys = Object.keys(payload[0]).sort().join(","); const lastKeys = Object.keys(payload[payload.length - 1]).sort().join(","); if (firstKeys !== lastKeys) log.error("Payload keys inconsistent", { first: firstKeys, last: lastKeys }, traceId); } return payload; }; var normalizeSupabaseRemoteItem = (item) => ({ id: String(item.fc2_id), timestamp: new Date(item.last_watched_at).getTime(), status: item.status || "watched", updated_at: item.updated_at, is_deleted: item.is_deleted ? 1 : 0, sync_dirty: 0 }); var buildSupabaseIdFilter = (ids) => { return `fc2_id=in.(${ids.map((id) => { const numeric = parseInt(id, 10); return Number.isNaN(numeric) ? JSON.stringify(id) : numeric; }).join(",")})`; }; /** A dirty local record should be pushed only when it wins the same merge rules used on pull. */ var shouldPushLocalRecord = (local, remote) => { if (!remote) return true; const winner = MergeEngine.merge([local], [remote], false).history[0]; return historyItemsEqual(winner, local); }; var log$26 = Logger.scope("SupabaseProvider"); var SupabaseProvider = class { deps; constructor(deps) { this.deps = deps; } get data() { return this.deps.data; } get state() { return this.deps.state; } get history() { return this.deps.history; } name = "supabase"; isSyncing = false; needsRetry = false; retryPreferRemote = false; retryForceRefresh = false; _onProgress = null; onInit() {} logout(silent = false) { clearSupabaseSession(); if (!silent) { CoreEvents.emit(AppEvents.SHOW_TOAST, { message: t("alertLoggedOut"), type: "success" }); setTimeout(() => location.reload(), TIMING.RELOAD_DELAY_FAST); } } async login(email, password) { const data = await SupabaseClient.request(`${SUPABASE_ENDPOINTS.TOKEN}?grant_type=password`, "POST", { email, password }); if (data?.access_token) return persistSupabaseLogin(data); throw new Error("Login failed"); } async signup(email, password) { return SupabaseClient.request(SUPABASE_ENDPOINTS.SIGNUP, "POST", { email, password }); } get onProgress() { return this._onProgress; } set onProgress(value) { this._onProgress = value; } handleMissingAuth(auth, isManual, traceId) { if (auth) return false; log$26.debug("No auth session, skipping sync", void 0, traceId); this.state.proxy.syncStatus = SYNC_STATUS.IDLE; if (isManual) CoreEvents.emit(AppEvents.SHOW_TOAST, { message: t("alertLoginRequired"), type: "warn" }); return true; } resolveLastSync(forceRefresh) { return forceRefresh ? UI_CONSTANTS.DEFAULT_TIMESTAMP : readSyncCursor(); } async finalizeSuccessfulSync(syncStartedAt, isManual, pushedCount, nextCursor, pulledChanged) { if (pulledChanged > 0) await this.history.load(); writeSyncCursor(nextCursor); this.state.proxy.lastSyncResult = createSyncResultSummary({ mode: "supabase", status: SYNC_STATUS.SUCCESS, startedAt: syncStartedAt, completedAt: (/* @__PURE__ */ new Date()).toISOString(), lastSyncTs: nextCursor, pushedCount, dirtyCount: await this.data.db.history.where("sync_dirty").equals(1).count(), translate: t }); this.state.proxy.syncStatus = SYNC_STATUS.SUCCESS; log$26.info("Sync completed"); if (isManual) CoreEvents.emit(AppEvents.SHOW_TOAST, { message: t("alertSupabaseSyncSuccess"), type: "success" }); } handleSyncError(error, isManual, traceId) { createSyncProviderErrorHandler({ mode: "supabase", defaultErrorMessage: "syncErrorSupabase", manualErrorToastPrefix: "alertSupabaseSyncError", translate: t, buildSummary: (input) => createSyncResultSummary(input), logError: (err, trace) => log$26.error("Sync failed", err, trace) })(error, isManual, traceId); } async fetchRemoteRows(auth, ids) { if (ids.length === 0) return /* @__PURE__ */ new Map(); const filter = buildSupabaseIdFilter(ids); const remote = await RetryManager.executeWithRetry(() => SupabaseClient.request(`${SUPABASE_ENDPOINTS.USER_HISTORY}?${filter}&select=fc2_id,last_watched_at,is_deleted,updated_at,status`, "GET", null, { Authorization: auth }), RetryManager.presets.supabaseSync); return new Map((remote || []).map((item) => [String(item.fc2_id), normalizeSupabaseRemoteItem(item)])); } async finalizePushedRecords(auth, records) { try { const remoteById = await this.fetchRemoteRows(auth, records.map((record) => record.id)); if (remoteById.size === 0) return; const pushedById = new Map(records.map((record) => [record.id, record])); await this.data.db.history.where("id").anyOf(Array.from(remoteById.keys())).modify((row) => { const pushed = pushedById.get(row.id); const remote = remoteById.get(row.id); if (!pushed || !remote?.updated_at || !historyItemsEqual(row, pushed)) return; row.updated_at = remote.updated_at; row.sync_dirty = 0; row.retry_count = 0; }); } catch (error) { log$26.warn("Failed to finalize pushed records", error); } } async reconcileRemoteWinners(records, remoteById) { const remoteWinners = records.filter((record) => { const remote = remoteById.get(record.id); return !!remote && !shouldPushLocalRecord(record, remote); }); if (remoteWinners.length === 0) return; const pushedById = new Map(remoteWinners.map((record) => [record.id, record])); await this.data.db.transaction("rw", this.data.db.history, async () => { await this.data.db.history.where("id").anyOf(remoteWinners.map((record) => record.id)).modify((row) => { const pushed = pushedById.get(row.id); const remote = remoteById.get(row.id); if (!pushed || !remote || !historyItemsEqual(row, pushed)) return; row.timestamp = remote.timestamp; row.status = remote.status; row.updated_at = remote.updated_at; row.is_deleted = remote.is_deleted ?? 0; row.sync_dirty = 0; row.retry_count = 0; }); }); await this.history.load(); } async incrementRetryCount(records) { await this.data.db.history.where("id").anyOf(records.map((record) => record.id)).modify((record) => { record.retry_count = (record.retry_count || 0) + 1; }); } async uploadRecords(auth, records, traceId) { const payload = buildSupabaseUpsertPayload({ dirty: records, userId: GM_getValue(STORAGE_KEYS.SYNC_USER_ID), onProgress: this._onProgress, log: log$26, traceId }); try { await RetryManager.executeWithRetry(() => SupabaseClient.request(SUPABASE_ENDPOINTS.USER_HISTORY, "POST", payload, { Authorization: auth, Prefer: "resolution=merge-duplicates" }), RetryManager.presets.supabaseSync); await this.finalizePushedRecords(auth, records); return records.length; } catch (error) { await this.incrementRetryCount(records); throw error; } } async pushDirtyItems(auth, traceId) { const CHUNK_SIZE = 50; let pushed = 0; while (true) { const dirty = await this.data.db.history.where("sync_dirty").equals(1).limit(CHUNK_SIZE).toArray(); if (dirty.length === 0) break; try { const remoteById = await this.fetchRemoteRows(auth, dirty.map((record) => record.id)); const toPush = dirty.filter((record) => shouldPushLocalRecord(record, remoteById.get(record.id))); await this.reconcileRemoteWinners(dirty, remoteById); if (toPush.length === 0) break; pushed += await this.uploadRecords(auth, toPush, traceId); } catch (error) { await this.incrementRetryCount(dirty); throw error; } if (dirty.length < CHUNK_SIZE) break; } if (pushed > 0) log$26.info(`Pushed ${pushed} dirty items in chunks`); return pushed; } async pullRemoteHistory(auth, lastSync, preferRemote) { this._onProgress?.({ phase: "Fetching remote data", percent: 60 }); let page = 0; let hasMore = true; let changedCount = 0; let maxUpdatedAt = lastSync; const pendingPush = []; while (hasMore) { const remote = await RetryManager.executeWithRetry(() => SupabaseClient.request(`${SUPABASE_ENDPOINTS.USER_HISTORY}?updated_at=gt.${encodeURIComponent(lastSync)}&select=fc2_id,last_watched_at,is_deleted,updated_at,status&order=updated_at.asc`, "GET", null, { Authorization: auth, Range: `${page * 1e3}-${(page + 1) * 1e3 - 1}` }), RetryManager.presets.supabaseSync); if (!remote?.length) { hasMore = false; continue; } const remoteItems = remote.map(normalizeSupabaseRemoteItem); remoteItems.forEach((item) => { if (item.updated_at > maxUpdatedAt) maxUpdatedAt = item.updated_at; }); const ids = remoteItems.map((item) => item.id); const localRows = ids.length ? await this.data.db.history.where("id").anyOf(ids).toArray() : []; const localById = new Map(localRows.map((row) => [row.id, row])); const merged = remoteItems.map((remoteItem) => { const local = localById.get(remoteItem.id); if (!local) return { ...remoteItem, sync_dirty: 0 }; const winner = MergeEngine.merge([local], [remoteItem], preferRemote).history[0]; if (historyItemsEqual(winner, local) && local.sync_dirty) { const row = { ...winner, sync_dirty: 1 }; pendingPush.push(row); return row; } return { ...winner, sync_dirty: 0 }; }); await this.data.db.transaction("rw", this.data.db.history, async () => { await this.data.db.history.bulkPut(merged); }); merged.forEach((item) => { const local = localById.get(item.id); if (!local || historySemanticChanged(local, item)) changedCount += 1; }); hasMore = remote.length >= 1e3; if (hasMore) page += 1; } return { changedCount, maxUpdatedAt: maxUpdatedAt > lastSync ? maxUpdatedAt : lastSync, pendingPush }; } async performSync(isManual = false, forceRefresh = false, preferRemote = false, traceId) { if (this.isSyncing) { this.needsRetry = true; this.retryForceRefresh = this.retryForceRefresh || forceRefresh; this.retryPreferRemote = this.retryPreferRemote || preferRemote; return; } if (preferRemote) forceRefresh = true; Logger.group("Supabase", `Sync (Force: ${forceRefresh})`, traceId); this.isSyncing = true; notifyManualSyncStart(isManual, t("labelSyncing")); try { this.state.proxy.syncStatus = SYNC_STATUS.SYNCING; const auth = await SupabaseClient.getAuthHeader(); if (this.handleMissingAuth(auth, isManual, traceId)) return; const lastSync = this.resolveLastSync(forceRefresh); const syncStartedAt = (/* @__PURE__ */ new Date()).toISOString(); let pushedCount = 0; if (!preferRemote) pushedCount += await this.pushDirtyItems(auth, traceId); const { changedCount, maxUpdatedAt, pendingPush } = await this.pullRemoteHistory(auth, lastSync, preferRemote); if (!preferRemote && pendingPush.length > 0) pushedCount += await this.uploadRecords(auth, pendingPush, traceId); await this.finalizeSuccessfulSync(syncStartedAt, isManual, pushedCount, maxUpdatedAt, changedCount); } catch (error) { this.handleSyncError(error, isManual, traceId); } finally { this.isSyncing = false; Logger.groupEnd(); if (this.needsRetry) { this.needsRetry = false; const forceRefreshRetry = this.retryForceRefresh; const preferRemoteRetry = this.retryPreferRemote; this.retryForceRefresh = false; this.retryPreferRemote = false; await this.performSync(false, forceRefreshRetry, preferRemoteRetry, traceId).catch((e) => log$26.error("Retry sync failed", e)); } } } }; /** Constructed once from app/modules (composition root). */ function createSupabaseProvider(deps) { return new SupabaseProvider(deps); } var log$25 = Logger.scope("WebDAV"); var WebDAVClient = class { static knownDirectories = /* @__PURE__ */ new Set(); static getAuthHeader() { const { webdavUser, webdavPass } = resolveService(STATE_SERVICE).proxy; return "Basic " + btoa(`${webdavUser}:${webdavPass}`); } static async request(method, url, body = null, headers = {}, responseType = "text") { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method, url, headers: { Authorization: this.getAuthHeader(), ...headers }, data: body, responseType, timeout: 3e4, onload: resolve, onerror: reject, ontimeout: () => reject(/* @__PURE__ */ new Error("WebDAV Timeout")) }); }); } static async ensureDirectory(baseUrl, fullPath) { const parts = fullPath.split("/").filter((p) => p && !p.includes(".")); let currentPath = baseUrl.replace(/\/$/, ""); for (const part of parts) { currentPath += "/" + part; if (this.knownDirectories.has(currentPath)) continue; try { const res = await this.request("PROPFIND", currentPath, null, { Depth: "0" }); if (res.status === 404) { log$25.debug(`Creating directory: ${currentPath}`); await this.request("MKCOL", currentPath); } else if (res.status >= 400) throw new Error(`Failed to access WebDAV directory ${currentPath}: ${res.status}`); this.knownDirectories.add(currentPath); } catch (e) { log$25.error(`EnsureDirectory failed: ${currentPath}`, e); throw e; } } } static async fetchFile(url) { try { const res = await this.request("GET", url, null, {}, "blob"); if (res.status === 200) { const etag = res.responseHeaders.match(/etag:\s*(.*)/i)?.[1]?.replace(/"/g, "").trim(); const raw = res.response; const text = raw instanceof Blob ? await GzipService.decompress(raw) : String(res.responseText || raw || ""); return { ok: true, data: JSON.parse(text), etag }; } log$25.error(`Fetch returned status ${res.status} for ${url}`); return { ok: false, error: /* @__PURE__ */ new Error(`HTTP ${res.status}`) }; } catch (e) { log$25.error(`Fetch failed for ${url}`, e); return { ok: false, error: e }; } } }; var log$24 = Logger.scope("ConflictLog"); var MAX_CONFLICT_ENTRIES = 50; /** * Append a conflict summary to the persistent conflict log. * Old entries are pruned when the log exceeds MAX_CONFLICT_ENTRIES. */ var appendConflictLog = (summary, syncMode) => { try { const existing = getConflictLog(); const entry = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), summary, syncMode }; existing.unshift(entry); if (existing.length > MAX_CONFLICT_ENTRIES) existing.length = MAX_CONFLICT_ENTRIES; Storage.set(STORAGE_KEYS.CONFLICT_LOG, existing); log$24.info(`Conflict logged: ${summary.localNewer} local, ${summary.remoteNewer} remote, ${summary.merged} merged`); } catch (e) { log$24.warn("Failed to persist conflict log", e); } }; /** * Retrieve the conflict log (most recent first). */ var getConflictLog = () => { try { return Storage.get(STORAGE_KEYS.CONFLICT_LOG, []) || []; } catch { return []; } }; /** * Calculate SHA-256 hash of JSON-serialized data * @param data - Data to hash (will be JSON-serialized) * @returns Hex-encoded SHA-256 hash string */ async function calculateSHA256(data) { return calculateSHA256Text(JSON.stringify(data)); } /** * Calculate SHA-256 hash of an already-serialized string. * Lets callers hash the exact bytes they are about to upload. */ async function calculateSHA256Text(text) { const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)); return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join(""); } var extractWebDAVEtag = (responseHeaders) => { return responseHeaders.match(/etag:\s*(.*)/i)?.[1]?.replace(/"/g, "").trim(); }; var shouldSkipWebDAVSync = ({ preferRemote, forceRefresh, remoteETag, lastEtag, dirtyCount }) => { return !preferRemote && !forceRefresh && !!remoteETag && lastEtag === remoteETag && dirtyCount === 0; }; var createWebDAVTarget = (webdavUrl, webdavPath) => { return `${webdavUrl.replace(/\/$/, "")}/${webdavPath}`; }; var createWebDAVSyncPayload = async ({ mergedHistory }) => { const updatedAt = (/* @__PURE__ */ new Date()).toISOString(); const historyJson = JSON.stringify(mergedHistory); const checksum = await calculateSHA256Text(historyJson); return { version: 2, updated_at: updatedAt, history: mergedHistory, checksum, serialized: `{"version":2,"updated_at":${JSON.stringify(updatedAt)},"history":${historyJson},"checksum":${JSON.stringify(checksum)}}` }; }; var clearWebDAVSession = () => { [ STORAGE_KEYS.WEBDAV_URL, STORAGE_KEYS.WEBDAV_USER, STORAGE_KEYS.WEBDAV_PASS, STORAGE_KEYS.WEBDAV_PATH ].forEach((key) => { GM_deleteValue(key); }); resetWebDAVEtag(); }; var acquireWebDAVSyncLock = ({ isManual, emitLocked }) => { const now = Date.now(); if (now - GM_getValue(STORAGE_KEYS.WEBDAV_SYNC_LOCK, 0) < 1e4) { if (isManual) emitLocked(); return false; } GM_setValue(STORAGE_KEYS.WEBDAV_SYNC_LOCK, now); return true; }; var shouldFetchWebDAVRemoteState = ({ forceRefresh, preferRemote, lastEtag, remoteETag, headStatus }) => { return forceRefresh || preferRemote || !lastEtag || lastEtag !== remoteETag || headStatus === 404; }; var verifyRemoteChecksum = async ({ remoteData }) => { if (remoteData.checksum && await calculateSHA256(remoteData.history) !== remoteData.checksum) throw new Error("Remote checksum mismatch"); }; var log$23 = Logger.scope("WebDAVProvider"); var WebDAVProvider = class { deps; constructor(deps) { this.deps = deps; } get data() { return this.deps.data; } get state() { return this.deps.state; } get history() { return this.deps.history; } name = "webdav"; isSyncing = false; syncLockHeartbeat = null; onInit() {} checkSyncLock(isManual) { return acquireWebDAVSyncLock({ isManual, emitLocked: () => { CoreEvents.emit(AppEvents.SHOW_TOAST, { message: t("alertSyncLockActive"), type: "warn" }); } }); } startSyncLockHeartbeat() { this.stopSyncLockHeartbeat(); this.syncLockHeartbeat = setInterval(() => { GM_setValue(STORAGE_KEYS.WEBDAV_SYNC_LOCK, Date.now()); }, 8e3); } stopSyncLockHeartbeat() { if (!this.syncLockHeartbeat) return; clearInterval(this.syncLockHeartbeat); this.syncLockHeartbeat = null; } resolveSyncTarget() { const { webdavUrl, webdavPath } = this.state.proxy; if (!webdavUrl || !webdavPath) return null; return { webdavUrl, webdavPath }; } markSyncUpToDate(isManual) { this.state.proxy.lastSyncResult = createSyncResultSummary({ mode: "webdav", status: SYNC_STATUS.SUCCESS, completedAt: (/* @__PURE__ */ new Date()).toISOString(), summary: t("syncSummaryUpToDate"), translate: t }); this.state.proxy.syncStatus = SYNC_STATUS.SUCCESS; if (isManual) CoreEvents.emit(AppEvents.SHOW_TOAST, { message: t("alertAlreadyUpToDate"), type: "info" }); } handleSyncError(error, isManual, traceId) { createSyncProviderErrorHandler({ mode: "webdav", defaultErrorMessage: "syncErrorWebdav", manualErrorToastPrefix: "alertWebDAVSyncError", translate: t, buildSummary: (input) => createSyncResultSummary(input), logError: (err, trace) => log$23.error("Sync error", err, trace) })(error, isManual, traceId); } async fetchRemoteState(targetUrl, forceRefresh, preferRemote) { const headResponse = await WebDAVClient.request("HEAD", targetUrl); const remoteETag = headResponse.status === 200 ? extractWebDAVEtag(headResponse.responseHeaders) : void 0; const lastEtag = readWebDAVEtag(); if (shouldSkipWebDAVSync({ preferRemote, forceRefresh, remoteETag, lastEtag, dirtyCount: await this.data.db.history.where("sync_dirty").equals(1).count() })) return { remoteETag, shouldSkip: true }; if (headResponse.status === 404) { const legacyUrl = `${targetUrl}.gz`; if ((await WebDAVClient.request("HEAD", legacyUrl)).status === 200) { const legacy = await WebDAVClient.fetchFile(legacyUrl); if (!legacy.ok) throw legacy.error instanceof Error ? legacy.error : /* @__PURE__ */ new Error("Failed to migrate legacy WebDAV file"); await verifyRemoteChecksum({ remoteData: legacy.data }); return { remoteETag: void 0, remoteData: legacy.data, shouldSkip: false }; } } let remoteData; if (shouldFetchWebDAVRemoteState({ forceRefresh, preferRemote, lastEtag, remoteETag, headStatus: headResponse.status })) { const fetched = await WebDAVClient.fetchFile(targetUrl); if (!fetched.ok) { if (headResponse.status !== 404) throw fetched.error instanceof Error ? fetched.error : /* @__PURE__ */ new Error(`Failed to fetch remote WebDAV state (HTTP ${headResponse.status})`); } else { await verifyRemoteChecksum({ remoteData: fetched.data }); remoteData = fetched.data; } } return { remoteETag, remoteData, shouldSkip: false }; } async mergeAndUpload(targetUrl, useGzip, remoteETag, remoteData, preferRemote, isManual, retryOnConflict, traceId, propagateErrors = false) { const localHistory = await this.data.db.history.toArray(); const { history: mergedHistory } = MergeEngine.merge(localHistory, remoteData?.history, preferRemote); const { serialized } = await createWebDAVSyncPayload({ mergedHistory }); const putHeaders = { "Content-Type": useGzip ? "application/gzip" : "application/json" }; if (remoteETag) putHeaders["If-Match"] = `"${remoteETag.replace(/"/g, "")}"`; const body = useGzip ? await GzipService.compress(serialized) : serialized; const response = await WebDAVClient.request("PUT", targetUrl, body, putHeaders); if (response.status >= 200 && response.status < 300) { await this.handleSuccessfulUpload(response.responseHeaders, remoteETag, mergedHistory, isManual); return; } if (response.status === 412 && retryOnConflict) { const conflictSummary = { localNewer: localHistory.length, remoteNewer: remoteData?.history?.length ?? 0, merged: mergedHistory.length, explanation: t("syncConflictWebdavRetry") }; appendConflictLog(conflictSummary, "webdav"); this.state.proxy.lastSyncResult = createSyncResultSummary({ mode: "webdav", status: SYNC_STATUS.CONFLICT, completedAt: (/* @__PURE__ */ new Date()).toISOString(), summary: t("syncSummaryConflictRetry"), conflict: conflictSummary, translate: t }); this.state.proxy.syncStatus = SYNC_STATUS.CONFLICT; return this.runSync(isManual, true, false, false, traceId, propagateErrors, true); } throw new Error(`WebDAV PUT failed: ${response.status}`); } async handleSuccessfulUpload(responseHeaders, remoteETag, mergedHistory, isManual) { const newEtag = extractWebDAVEtag(responseHeaders) || remoteETag; if (newEtag) writeWebDAVEtag(newEtag); const uploadedRemote = mergedHistory.map((history) => ({ ...history, sync_dirty: 0, retry_count: 0 })); let reloadNeeded = false; await this.data.db.transaction("rw", this.data.db.history, async () => { const currentRows = await this.data.db.history.toArray(); const currentById = new Map(currentRows.map((row) => [row.id, row])); const { history: reconciled } = MergeEngine.merge(currentRows, uploadedRemote, false); const writes = reconciled.map((row) => { const current = currentById.get(row.id); const localWon = !!current && !!current.sync_dirty && historyItemsEqual(row, current); return { row: { ...row, sync_dirty: localWon ? 1 : 0, retry_count: localWon ? current?.retry_count || 0 : 0 }, current }; }).filter(({ row, current }) => { const bookkeepingChanged = !current || current.sync_dirty !== row.sync_dirty || (current.retry_count || 0) !== (row.retry_count || 0); return !current || !historyItemsEqual(current, row) || bookkeepingChanged; }).map(({ row }) => row); if (writes.length > 0) await this.data.db.history.bulkPut(writes); reloadNeeded = reconciled.some((row) => { const current = currentById.get(row.id); return !current || historySemanticChanged(current, row); }); }); if (reloadNeeded) await this.history.load(); const completedAt = (/* @__PURE__ */ new Date()).toISOString(); writeSyncCursor(completedAt); this.state.proxy.lastSyncResult = createSyncResultSummary({ mode: "webdav", status: SYNC_STATUS.SUCCESS, completedAt, lastSyncTs: completedAt, dirtyCount: await this.data.db.history.where("sync_dirty").equals(1).count(), translate: t }); this.state.proxy.syncStatus = SYNC_STATUS.SUCCESS; if (isManual) CoreEvents.emit(AppEvents.SHOW_TOAST, { message: t("alertWebDAVSyncSuccess"), type: "success" }); } async runSync(isManual = false, forceRefresh = false, retryOnConflict = true, preferRemote = false, traceId, propagateErrors = false, isRetry = false) { if (this.isSyncing && !isRetry || !isRetry && !this.checkSyncLock(isManual)) return; const syncTarget = this.resolveSyncTarget(); if (!syncTarget) return; Logger.group("WebDAV", `Sync (Force: ${forceRefresh}, CloudFirst: ${preferRemote})`, traceId); this.isSyncing = true; this.state.proxy.syncStatus = SYNC_STATUS.SYNCING; this.startSyncLockHeartbeat(); try { await WebDAVClient.ensureDirectory(syncTarget.webdavUrl, syncTarget.webdavPath); const useGzip = GzipService.isSupported(); const targetUrl = createWebDAVTarget(syncTarget.webdavUrl, syncTarget.webdavPath); const { remoteETag, remoteData, shouldSkip } = await this.fetchRemoteState(targetUrl, forceRefresh, preferRemote); if (shouldSkip) { this.markSyncUpToDate(isManual); return; } await this.mergeAndUpload(targetUrl, useGzip, remoteETag, remoteData, preferRemote, isManual, retryOnConflict, traceId, propagateErrors); } catch (error) { if (propagateErrors) throw error; this.handleSyncError(error, isManual, traceId); } finally { this.isSyncing = false; this.stopSyncLockHeartbeat(); GM_setValue(STORAGE_KEYS.WEBDAV_SYNC_LOCK, 0); Logger.groupEnd(); } } async test() { const { webdavUrl } = this.state.proxy; if (!webdavUrl) throw new Error("URL is empty"); return WebDAVClient.request("PROPFIND", webdavUrl.replace(/\/$/, "") + "/", null, { Depth: "0" }); } logout() { clearWebDAVSession(); } async performSync(isManual = false, forceRefresh = false, preferRemote = false, traceId) { if (forceRefresh || preferRemote) resetWebDAVEtag(); notifyManualSyncStart(isManual, t("labelSyncing")); try { await RetryManager.executeWithRetry(() => this.runSync(isManual, forceRefresh, true, preferRemote, traceId, true), RetryManager.presets.sync); } catch (error) { this.handleSyncError(error, isManual, traceId); } } }; /** Constructed once from app/modules (composition root). */ function createWebDAVProvider(deps) { return new WebDAVProvider(deps); } var MAX_HISTORY = 200; var roundMetric = (value) => Math.round(value * 100) / 100; var PerformanceMonitorImpl = class { history = []; record(name, durationMs, metadata) { const completedAt = /* @__PURE__ */ new Date(); const startedAt = new Date(completedAt.getTime() - Math.max(0, durationMs)); const entry = { name, durationMs: roundMetric(durationMs), startedAt: startedAt.toISOString(), completedAt: completedAt.toISOString(), metadata: metadata ? { ...metadata } : void 0 }; this.history.push(entry); if (this.history.length > MAX_HISTORY) this.history.splice(0, this.history.length - MAX_HISTORY); return entry; } measureSync(name, callback, metadata) { const start = performance.now(); try { return callback(); } finally { this.record(name, performance.now() - start, metadata); } } async measure(name, callback, metadata) { const start = performance.now(); try { return await callback(); } finally { this.record(name, performance.now() - start, metadata); } } getSummary() { const metrics = this.history.reduce((acc, entry) => { const current = acc[entry.name] || { count: 0, totalMs: 0, avgMs: 0, maxMs: 0, lastMs: 0 }; current.count += 1; current.totalMs = roundMetric(current.totalMs + entry.durationMs); current.maxMs = roundMetric(Math.max(current.maxMs, entry.durationMs)); current.lastMs = roundMetric(entry.durationMs); current.avgMs = roundMetric(current.totalMs / current.count); acc[entry.name] = current; return acc; }, {}); return { updatedAt: this.history.length > 0 ? this.history[this.history.length - 1]?.completedAt || null : null, metrics }; } exportSnapshot() { return { summary: this.getSummary(), history: this.history.map((entry) => ({ ...entry, metadata: entry.metadata ? { ...entry.metadata } : void 0 })) }; } clear() { this.history = []; } }; var PerformanceMonitor = new PerformanceMonitorImpl(); var log$22 = Logger.scope("SyncLock"); var SYNC_LOCK_NAME = "fc2_sync_lock"; var FALLBACK_LOCK_KEY = "fc2_sync_lock_fallback"; var FALLBACK_LOCK_TTL_MS = 6e4; var SyncLockCoordinator = class { async runWithLock(task, isManual = false) { if (typeof navigator !== "undefined" && navigator.locks) return await navigator.locks.request(SYNC_LOCK_NAME, { ifAvailable: true }, async (lock) => { if (!lock) { this.handleLockedSync(isManual); return false; } await task(); return true; }); log$22.debug("navigator.locks not available, using fallback lock"); const now = Date.now(); if (now < GM_getValue(FALLBACK_LOCK_KEY, 0)) { this.handleLockedSync(isManual); return false; } GM_setValue(FALLBACK_LOCK_KEY, now + FALLBACK_LOCK_TTL_MS); try { await task(); } finally { GM_deleteValue(FALLBACK_LOCK_KEY); } return true; } handleLockedSync(isManual) { if (isManual) { log$22.info("Manual sync skipped: lock held by another tab"); CoreEvents.emit(AppEvents.SHOW_TOAST, { message: t("alertSyncLocked") || "Sync already in progress in another tab", type: "warn" }); return; } log$22.debug("Auto-sync skipped: lock held by another tab"); } }; var syncLockCoordinator = new SyncLockCoordinator(); var log$21 = Logger.scope("SyncRecovery"); var SyncRecoveryActions = class { dependencies; constructor(dependencies) { this.dependencies = dependencies; } async forceFullSync(skipConfirm = false) { if (!skipConfirm) { if (!this.dependencies.confirmAction(this.dependencies.getPushAllMessage())) return false; } try { await this.dependencies.markAllHistoryDirty(); this.dependencies.resetLastSync(); await this.dependencies.performSync(true, true, false); return true; } catch (e) { log$21.error("forceFullSync failed", e); return false; } } async forcePullSync(skipConfirm = false) { if (!skipConfirm) { if (!this.dependencies.confirmAction(this.dependencies.getPullAllMessage())) return false; } try { this.dependencies.resetLastSync(); await this.dependencies.performSync(true, true, true); return true; } catch (e) { log$21.error("forcePullSync failed", e); return false; } } }; var log$20 = Logger.scope("SyncScheduler"); var SyncScheduler = class { dependencies; initialSyncTimer = null; syncRequestTimer = null; periodicSyncTimer = null; detachHistoryChanged = null; detachCollectionUpdated = null; detachStateChanged = null; onlineHandler = null; constructor(dependencies) { this.dependencies = dependencies; } recordAutoSyncTimestamp() { writeLastAutoSyncTs(Date.now()); } start() { log$20.debug("Starting auto-sync scheduler"); this.clearInitialSyncTimer(); this.detachHistoryChanged?.(); this.detachCollectionUpdated?.(); this.detachStateChanged?.(); const mode = this.dependencies.state.get("syncMode"); const interval = this.dependencies.state.get("syncInterval"); if (mode !== "none" && interval !== -1) { this.initialSyncTimer = setTimeout(() => this.dependencies.performSync(false).then(() => this.recordAutoSyncTimestamp()).catch((error) => { log$20.warn("Initial auto-sync failed", error); }), TIMING.SYNC_INIT_DELAY); this.schedulePeriodicSync(); } this.detachHistoryChanged = CoreEvents.on(AppEvents.HISTORY_CHANGED, () => { this.requestSync(); }); this.detachCollectionUpdated = CoreEvents.on(AppEvents.COLLECTION_UPDATED, () => { this.requestSync(); }); this.detachStateChanged = CoreEvents.on(AppEvents.STATE_CHANGED, ({ prop }) => { if (prop === "syncMode" || prop === "syncInterval") { if (this.dependencies.state.get("syncMode") === "none" || this.dependencies.state.get("syncInterval") === -1) this.clearInitialSyncTimer(); this.schedulePeriodicSync(); } if (prop === "syncMode" && this.dependencies.state.get("syncMode") !== "none") this.requestSync(); }); this.onlineHandler = () => { log$20.info("Network restored, requesting sync"); this.requestSync(); }; window.addEventListener("online", this.onlineHandler); } stop() { this.clearInitialSyncTimer(); this.clearRequestSyncTimer(); this.clearPeriodicSyncTimer(); this.detachHistoryChanged?.(); this.detachHistoryChanged = null; this.detachCollectionUpdated?.(); this.detachCollectionUpdated = null; this.detachStateChanged?.(); this.detachStateChanged = null; if (this.onlineHandler) { window.removeEventListener("online", this.onlineHandler); this.onlineHandler = null; } } requestSync() { const interval = this.dependencies.state.get("syncInterval"); const mode = this.dependencies.state.get("syncMode"); if (interval === -1 || mode === "none") return; if (interval === 0) { Logger.info("SyncService", `Sync requested (real-time mode), executing in ${TIMING.SYNC_DEBOUNCE_MS}ms`); this.clearRequestSyncTimer(); this.syncRequestTimer = setTimeout(() => this.dependencies.performSync().catch((error) => log$20.error("Debounced sync failed", error)), TIMING.SYNC_DEBOUNCE_MS); return; } const lastAutoSync = readLastAutoSyncTs(); const now = Date.now(); const minInterval = interval * 60 * 1e3; const elapsed = now - lastAutoSync; this.clearRequestSyncTimer(); const execute = () => { this.dependencies.performSync().then(() => { this.recordAutoSyncTimestamp(); }).catch((error) => log$20.error("Scheduled sync failed", error)); }; if (elapsed >= minInterval) { Logger.info("SyncService", `Sync requested (interval: ${interval}min), executing in ${TIMING.SYNC_DEBOUNCE_MS}ms`); this.syncRequestTimer = setTimeout(execute, TIMING.SYNC_DEBOUNCE_MS); return; } const waitMs = Math.max(0, minInterval - elapsed); Logger.info("SyncService", `Sync requested (interval: ${interval}min), queued for ${waitMs}ms`); this.syncRequestTimer = setTimeout(execute, waitMs); } clearInitialSyncTimer() { if (!this.initialSyncTimer) return; clearTimeout(this.initialSyncTimer); this.initialSyncTimer = null; } clearRequestSyncTimer() { if (!this.syncRequestTimer) return; clearTimeout(this.syncRequestTimer); this.syncRequestTimer = null; } clearPeriodicSyncTimer() { if (!this.periodicSyncTimer) return; clearTimeout(this.periodicSyncTimer); this.periodicSyncTimer = null; } schedulePeriodicSync() { this.clearPeriodicSyncTimer(); this.clearRequestSyncTimer(); const mode = this.dependencies.state.get("syncMode"); const interval = this.dependencies.state.get("syncInterval"); if (mode === "none" || interval === -1) return; const periodMs = interval === 0 ? TIMING.SYNC_REALTIME_POLL_MS : interval * 60 * 1e3; this.periodicSyncTimer = setTimeout(() => { this.schedulePeriodicSync(); this.requestSync(); }, periodMs); } }; var log$19 = Logger.scope("SyncService"); var SyncService = class { deps; scheduler; recoveryActions; pendingSync = false; pendingSyncTimer = null; constructor(deps) { this.deps = deps; this.scheduler = new SyncScheduler({ performSync: (isManual) => this.performSync(isManual), state: deps.state }); this.recoveryActions = new SyncRecoveryActions({ confirmAction: (message) => confirm(message), markAllHistoryDirty: async () => { await this.deps.data.db.history.toCollection().modify({ sync_dirty: 1 }); }, resetLastSync: () => { resetSyncCursor(); }, performSync: (isManual, forceRefresh, preferRemote) => this.performSync(isManual, forceRefresh, preferRemote), getPushAllMessage: () => t("alertPushAllQuery"), getPullAllMessage: () => t("alertPullAllQuery") }); } onBootstrap() { this.scheduler.start(); } onCleanup() { this.clearPendingSyncTimer(); this.scheduler.stop(); } getProvider() { return getActiveSyncProvider(); } set onProgress(val) { getSupabaseAuthProvider().onProgress = val; } async login(email, password) { return await getSupabaseAuthProvider().login(email, password); } async signup(email, password) { return await getSupabaseAuthProvider().signup(email, password); } async logout(silent = false) { const provider = this.getProvider(); if (provider) await provider.logout(silent); else await getSupabaseAuthProvider().logout(silent); this.deps.state.proxy.syncMode = "none"; this.deps.state.proxy.syncStatus = SYNC_STATUS.IDLE; this.deps.state.proxy.lastSyncResult = null; resetSyncCursor(); } async testWebDAV() { return await getWebDAVTestProvider().test(); } requestSync() { this.scheduler.requestSync(); } async performSync(isManual = false, forceRefresh = false, preferRemote = false) { await PerformanceMonitor.measure("sync.execute", async () => { await this.executeSync(isManual, forceRefresh, preferRemote); }, { isManual, forceRefresh, preferRemote, provider: this.getProvider()?.name || "none" }); } async executeSync(isManual, forceRefresh, preferRemote) { const traceId = Logger.traceId; try { const provider = this.getProvider(); if (!provider) { if (isManual) this.recordSyncFailure(t("syncErrorNoProvider"), true); return; } if (!await syncLockCoordinator.runWithLock(async () => { this.clearPendingSync(); log$19.info(`Starting sync (manual: ${isManual}, force: ${forceRefresh})`, traceId); await provider.performSync(isManual, forceRefresh, preferRemote, traceId); }, isManual)) if (isManual) this.recordSyncFailure(t("syncErrorLocked"), true); else this.schedulePendingSyncRetry(); else CoreEvents.emit(AppEvents.SYNC_STATUS_CHANGED, this.deps.state.proxy.syncStatus); } catch (error) { log$19.error("Sync failed", error, traceId); this.recordSyncFailure(error instanceof Error ? error.message : String(error), isManual); } } schedulePendingSyncRetry() { if (this.pendingSync) return; this.pendingSync = true; this.clearPendingSyncTimer(); this.pendingSyncTimer = setTimeout(() => { this.pendingSyncTimer = null; this.pendingSync = false; this.performSync(false); }, TIMING.RETRY_DELAY_MS); } clearPendingSync() { this.pendingSync = false; this.clearPendingSyncTimer(); } clearPendingSyncTimer() { if (!this.pendingSyncTimer) return; clearTimeout(this.pendingSyncTimer); this.pendingSyncTimer = null; } recordSyncFailure(message, notifyUser = false) { this.deps.state.proxy.lastSyncResult = createSyncResultSummary({ mode: this.getCurrentMode(), status: SYNC_STATUS.ERROR, completedAt: (/* @__PURE__ */ new Date()).toISOString(), errorMessage: message, translate: t }); this.deps.state.proxy.syncStatus = SYNC_STATUS.ERROR; if (notifyUser) CoreEvents.emit(AppEvents.SHOW_TOAST, { message, type: "error" }); } getCurrentMode() { const mode = this.deps.state.proxy.syncMode; return mode === "webdav" || mode === "supabase" ? mode : "none"; } async forceFullSync(skipConfirm = false) { return await this.recoveryActions.forceFullSync(skipConfirm); } async forcePullSync(skipConfirm = false) { return await this.recoveryActions.forcePullSync(skipConfirm); } }; /** Constructed once from app/modules (composition root). */ function createSyncService(deps) { return new SyncService(deps); } var log$18 = Logger.scope("TombstoneService"); var CleanupService = class { deps; constructor(deps) { this.deps = deps; } RETENTION_PERIOD = 2592e6; maintenanceTimer = null; onBootstrap() { this.onCleanup(); log$18.debug("Scheduling maintenance"); this.maintenanceTimer = setTimeout(() => { this.maintenanceTimer = null; this.runAllGC().catch((e) => log$18.error("Maintenance failed", e)); }, 5e3); } onCleanup() { if (this.maintenanceTimer) { clearTimeout(this.maintenanceTimer); this.maintenanceTimer = null; } } async runAllGC() { Logger.group("Cleanup", "System maintenance"); try { await this.runTombstoneGC(); await this.deps.data.runGC(); log$18.info("Maintenance complete"); } finally { Logger.groupEnd(); } } async runTombstoneGC() { try { const thresholdDate = new Date(Date.now() - this.RETENTION_PERIOD).toISOString(); const deletedCount = await this.deps.data.db.history.where("is_deleted").equals(1).and((item) => (item.updated_at ?? "") < thresholdDate).delete(); if (deletedCount > 0) log$18.info(`Purged ${deletedCount} tombstone records`); } catch (e) { log$18.error("Tombstone GC failed", e); } } }; /** Constructed once from app/modules (composition root). */ function createCleanupService(deps) { return new CleanupService(deps); } var QueryCache = class { cache = /* @__PURE__ */ new Map(); version = 0; get(key) { const entry = this.cache.get(key); return entry && entry.version === this.version ? entry.data : null; } set(key, data) { this.cache.set(key, { data, version: this.version }); } invalidate() { this.cache.clear(); this.version = this.version >= Number.MAX_SAFE_INTEGER ? 0 : this.version + 1; } }; var MemoryCache = class { cache = /* @__PURE__ */ new Map(); get(id, expirationMs) { const item = this.cache.get(id); if (!item) return null; if (Date.now() - item.timestamp >= expirationMs) { this.cache.delete(id); return null; } this.cache.delete(id); this.cache.set(id, item); return item.value; } set(id, value) { if (this.cache.size >= CACHE.MEMORY_MAX_SIZE) { const firstKey = this.cache.keys().next().value; if (firstKey) this.cache.delete(firstKey); } this.cache.set(id, { value, timestamp: Date.now() }); } clear() { this.cache.clear(); } delete(id) { this.cache.delete(id); } }; var FC2Database = class extends Dexie { history; cache; timeline; constructor() { super(DATABASE.NAME); this.version(6).stores({ history: "&id, timestamp, status, updated_at, is_deleted, sync_dirty, retry_count, [is_deleted+timestamp], [sync_dirty+updated_at], [status+is_deleted], [retry_count+sync_dirty]", cache: "&id, timestamp" }); this.version(8).stores({ history: "&id, timestamp, status, updated_at, is_deleted, sync_dirty, retry_count, [is_deleted+timestamp], [sync_dirty+updated_at], [status+is_deleted], [retry_count+sync_dirty]", cache: "&id, timestamp", itemDetails: null, timeline: "++id, timestamp, category" }); this.version(9).stores({ history: "&id, timestamp, status, updated_at, is_deleted, sync_dirty, retry_count, [is_deleted+timestamp], [sync_dirty+updated_at], [status+is_deleted], [retry_count+sync_dirty]", cache: "&id, timestamp, type", timeline: "++id, timestamp, category" }); } }; var log$17 = Logger.scope("DataService"); var nowIso = () => (/* @__PURE__ */ new Date()).toISOString(); /** * Keeps updated_at monotonic even when the device clock lags the server. * Local edits always get a timestamp strictly newer than the last known version. */ var nextUpdatedAt = (existing) => { const now = Date.now(); const previous = existing?.updated_at ? new Date(existing.updated_at).getTime() : NaN; if (!Number.isFinite(previous) || previous < now) return nowIso(); return new Date(previous + 1).toISOString(); }; var DataService = class { db = new FC2Database(); qCache = new QueryCache(); mCache = new MemoryCache(); async onInit() { log$17.debug("Opening Dexie database"); await this.db.open(); log$17.info("Database opened"); } onCleanup() { this.qCache.invalidate(); this.mCache.clear(); this.db.close(); } history = { table: null, add: async (id, status = "watched") => { const key = String(id); const now = nextUpdatedAt(await this.db.history.get(key)); await this.db.history.put({ id: key, timestamp: Date.now(), status, updated_at: now, is_deleted: 0, sync_dirty: 1 }); this.qCache.invalidate(); log$17.trace(`History added: ${id} as ${status}`); }, getAll: async () => { const cached = this.qCache.get("all"); if (cached) return cached; const items = await this.db.history.where("is_deleted").equals(0).toArray(); this.qCache.set("all", items); return items; }, remove: async (id) => { const key = String(id); const existing = await this.db.history.get(key); await this.db.history.update(key, { is_deleted: 1, updated_at: nextUpdatedAt(existing), sync_dirty: 1 }); this.qCache.invalidate(); }, bulkAdd: async (ids, status = "watched") => { const timestamp = Date.now(); const keys = ids.map(String); const existingRows = await this.db.history.bulkGet(keys); const existingById = new Map(existingRows.filter((row) => Boolean(row)).map((row) => [row.id, row])); await this.db.history.bulkPut(keys.map((id) => ({ id, timestamp, status, updated_at: nextUpdatedAt(existingById.get(id)), is_deleted: 0, sync_dirty: 1 }))); this.qCache.invalidate(); log$17.trace(`History bulk added: ${ids.length} items as ${status}`); }, bulkRemove: async (ids) => { await this.db.history.where("id").anyOf(ids.map(String)).modify((row) => { row.is_deleted = 1; row.updated_at = nextUpdatedAt(row); row.sync_dirty = 1; }); this.qCache.invalidate(); log$17.trace(`History bulk removed: ${ids.length} items`); }, clear: async () => { await this.db.history.toCollection().modify((row) => { row.is_deleted = 1; row.sync_dirty = 1; row.updated_at = nextUpdatedAt(row); }); this.qCache.invalidate(); }, markDirty: async (id) => { const item = await this.db.history.get(String(id)); const currentRetry = item?.retry_count || 0; await this.db.history.update(String(id), { sync_dirty: 1, updated_at: nextUpdatedAt(item), retry_count: currentRetry + 1 }); this.qCache.invalidate(); } }; cache = { get: async (id) => { const memoryValue = this.mCache.get(id, CACHE.MEMORY_EXPIRATION_MS); if (memoryValue !== null) return memoryValue; const row = await this.db.cache.get(id); if (!row) return null; if (isNegativeMagnetCacheValue(row.value) ? isExpiredNegativeMagnetCacheValue(row.value) : Date.now() - row.timestamp >= CACHE.EXPIRATION_MS) { await this.db.cache.delete(id); return null; } this.mCache.set(id, row.value); return row.value; }, set: async (id, value, type) => { await this.db.cache.put({ id: String(id), value, timestamp: Date.now(), type }); this.mCache.set(id, value); }, delete: async (id) => { await this.db.cache.delete(String(id)); this.mCache.delete(id); }, clear: async () => { await this.db.cache.clear(); this.mCache.clear(); }, clearByType: async (type) => { const keys = await this.db.cache.where("type").equals(type).primaryKeys(); if (keys.length > 0) await this.db.cache.bulkDelete(keys); }, getAll: async () => await this.db.cache.toArray() }; timeline = { add: async (event) => { return await this.db.timeline.add(event); }, list: async (options) => { let collection = this.db.timeline.orderBy("timestamp").reverse(); if (options?.category) collection = this.db.timeline.where("category").equals(options.category).reverse(); const limit = options?.limit ?? 100; return await collection.limit(limit).toArray(); }, purgeBefore: async (thresholdMs) => { return await this.db.timeline.where("timestamp").below(thresholdMs).delete(); } }; constructor() { this.history.table = this.db.history; } async runGC() { Logger.group("Database", "Garbage collection"); const now = Date.now(); await this.db.cache.where("timestamp").below(now - CACHE.EXPIRATION_MS).delete(); this.mCache.clear(); Logger.groupEnd(); } }; var createDataService = () => new DataService(); var Repository = createDataService(); Repository.db; var ports = null; function configureSitePorts(next) { ports = next; } function requirePorts() { if (!ports) throw new Error("[SitePorts] Not configured — registerModules must call configureSitePorts"); return ports; } var fetchActressFromFD2 = (id) => requirePorts().fetchActressFromFD2(id); var resetFD2Backoff = () => requirePorts().resetFD2Backoff(); var addToHistory = (id) => requirePorts().addToHistory(id); var cacheSet = (key, value) => requirePorts().cacheSet(key, value); function fromPatterns(patterns) { return () => { if (patterns.custom) return patterns.custom(); const path = location.pathname; const segments = path.split("/").filter(Boolean); if (patterns.blacklist?.some((bl) => segments.some((s) => s.toLowerCase().startsWith(bl.toLowerCase())))) return PageContext.List; if (patterns.detail.some((re) => re.test(path))) return PageContext.Detail; if (patterns.search.some((re) => re.test(path))) return PageContext.Search; return PageContext.List; }; } function fromUrlStructure(config) { return fromPatterns({ detail: config.detailPatterns.map((p) => new RegExp(p)), search: config.searchPatterns.map((p) => new RegExp(p)), blacklist: config.blacklistPaths, custom: config.custom }); } function fc2ContextDetector(options) { return fromUrlStructure({ detailPatterns: options?.detailPatterns || ["^/articles/\\d+", "^/\\d+"], searchPatterns: options?.searchPatterns || ["^/search", "^/actresses"], blacklistPaths: options?.blacklistPaths || [ "tags", "categories", "makers" ] }); } function javContextDetector(options) { return fromUrlStructure({ detailPatterns: options?.detailPatterns || ["^/[a-z]{2,10}-\\d+", "^/video/\\d+"], searchPatterns: options?.searchPatterns || ["^/search"], blacklistPaths: options?.blacklistPaths || [ "categories", "tags", "makers", "series" ] }); } function hybridContextDetector(options) { return fromUrlStructure({ detailPatterns: options?.detailPatterns || [ "^/articles/\\d+", "^/[a-z]{2,10}-\\d+", "^/fc2-ppv-\\d+" ], searchPatterns: options?.searchPatterns || ["^/search", "^/actresses"], blacklistPaths: options?.blacklistPaths || [ "tags", "categories", "makers", "genres" ] }); } function extractFC2Id(text) { if (!text) return null; for (const pattern of [ /FC2-PPV-(\d+)/i, /FC2PPV(\d+)/i, /(\d{5,8})/ ]) { const match = text.match(pattern); if (match?.[1]) return match[1]; } return null; } function extractJavCode(text) { if (!text) return null; return text.match(/([a-z]{2,10}-\d+)/i)?.[1]?.toUpperCase() || null; } function detectVideoType(id) { if (!id) return "jav"; if (/^\d{5,8}$/.test(id) || id.toUpperCase().startsWith("FC2")) return "fc2"; return "jav"; } function cleanTitle(title, id) { if (!title) return `Video-${id}`; const cleaned = title.replace(new RegExp(`(?:FC2-PPV-)?${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*`, "gi"), "").replace(/FC2-PPV-\d+\s*/gi, "").replace(/\s+/g, " ").replace(/^\s*[-–—]\s*/, "").replace(/\s*[-–—]\s*$/, "").trim(); if (!cleaned) return title.trim() || `Video-${id}`; return cleaned; } function extractTitle(title) { if (!title) return ""; return title.replace(/\s+/g, " ").replace(/^\s*[-–—]\s*/, "").replace(/\s*[-–—]\s*$/, "").trim(); } function resolveImageUrl$1(img) { if (!img) return void 0; return getBestImageSource(img) || void 0; } function normalizeUrl(url) { if (!url) return void 0; try { if (url.startsWith("/")) return url; const u = new URL(url, location.origin); return u.origin + u.pathname + u.search; } catch { return url; } } function normalizeOptionalText(text) { if (!text) return void 0; const cleaned = text.trim(); return cleaned.length > 0 ? cleaned : void 0; } function normalizeTags(tags) { return tags.filter((tag) => Boolean(tag)).map((tag) => tag.trim()).filter((tag) => tag.length > 0); } function resolvePreviewSlug(type, id, fallback) { if (fallback) return fallback; if (type === "fc2") return `fc2-ppv-${id}`; } function safeQuerySelector(container, selector) { try { return container.querySelector(selector); } catch { return null; } } function safeQuerySelectorAll(container, selector) { try { return Array.from(container.querySelectorAll(selector)); } catch { return []; } } function extractTextContent(element, selector) { if (!element) return void 0; return normalizeOptionalText((selector ? safeQuerySelector(element, selector) : element)?.textContent); } function createCardExtractor(config) { return (card) => { if (config.shouldSkip?.(card)) return null; const link = safeQuerySelector(card, config.linkSelector); if (!link?.href) return null; const id = config.idExtractor(link, card); if (!id) return null; const type = detectVideoType(id); const imageUrl = resolveImageUrl$1(safeQuerySelector(card, config.imageSelector || "img")); const title = extractTitle((config.titleSelector ? safeQuerySelector(card, config.titleSelector) : link)?.textContent?.trim() || ""); const actress = config.actressSelector ? extractTextContent(card, config.actressSelector) : void 0; const tags = config.tagsSelector ? normalizeTags(safeQuerySelectorAll(card, config.tagsSelector).map((el) => el.textContent)) : []; const previewSlug = config.previewSlugResolver ? config.previewSlugResolver(id, type) : resolvePreviewSlug(type, id); const baseData = { id, type, title, primaryImageUrl: imageUrl, imageUrl, fallbackImageUrl: imageUrl, articleUrl: normalizeUrl(link.href), actress, tags, previewSlug: previewSlug || void 0 }; if (config.customExtractor) return config.customExtractor(card, baseData); return baseData; }; } function createFC2CardExtractor(config) { return createCardExtractor({ ...config, idExtractor: (link, card) => { let id = extractFC2Id(link.href); if (!id) id = extractFC2Id(card.textContent || ""); return id; }, previewSlugResolver: (id) => resolvePreviewSlug("fc2", id, `fc2-ppv-${id}`) }); } function createJAVCardExtractor(config) { return createCardExtractor({ ...config, idExtractor: (link, card) => { let id = extractJavCode(link.textContent || ""); if (!id) id = extractJavCode(link.href); if (!id) id = extractJavCode(card.textContent || ""); return id; } }); } function createHybridCardExtractor(config) { return createCardExtractor({ ...config, idExtractor: (link, card) => { let id = extractFC2Id(link.href) || extractFC2Id(card.textContent || ""); if (!id) id = extractJavCode(link.textContent || "") || extractJavCode(link.href); return id; }, previewSlugResolver: (id, type) => { if (type === "fc2") return `fc2-ppv-${id}`; } }); } function createDetailEnhancer(config) { return (container) => { const info = config.idExtractor(); if (!info) return null; return { mode: config.mountMode || "card", anchorSelector: config.anchorSelector, anchorElement: config.anchorElement || container, idExtractor: () => info, metadataExtractor: (id, type) => { return { ...config.metadataExtractor(id, type), articleUrl: location.href }; }, mountOptions: config.mountOptions, onMounted: config.onMounted }; }; } function createFC2DetailEnhancer(config) { return createDetailEnhancer({ idExtractor: () => { const id = extractFC2Id(location.href); return id ? { id, type: "fc2" } : null; }, metadataExtractor: (id) => { const title = document.querySelector(config.titleSelector || "h1")?.textContent?.trim() || document.title.split("-")[0]?.trim(); const img = document.querySelector(config.imageSelector || "img"); return { title: cleanTitle(title || "", id), primaryImageUrl: resolveImageUrl$1(img) }; }, mountMode: config.mountMode, anchorSelector: config.anchorSelector, mountOptions: config.mountOptions, onMounted: config.onMounted }); } function createJAVDetailEnhancer(config) { return createDetailEnhancer({ idExtractor: () => { return MediaUtils.parseVideoId(document.title, location.href) || null; }, metadataExtractor: (_id, _type) => { return { title: extractTextContent(document.querySelector(config.titleSelector || "h1")) || document.title, actress: config.actressSelector ? extractTextContent(document.querySelector(config.actressSelector)) : void 0 }; }, mountMode: config.mountMode, anchorSelector: config.anchorSelector, mountOptions: config.mountOptions, onMounted: config.onMounted }); } function createDefaultMeta(overrides = {}) { return { name: "Unknown", hostnames: [], listOnDetail: true, type: "hybrid", enabled: true, priority: 100, features: [ "magnet", "preview", "history" ], ...overrides }; } function validateSiteMeta(meta) { const errors = []; if (!meta.name || meta.name.trim() === "") errors.push("Site name is required"); if (!meta.hostnames || meta.hostnames.length === 0) errors.push("At least one hostname is required"); if (![ "fc2", "jav", "hybrid" ].includes(meta.type)) errors.push(`Invalid site type: ${meta.type}`); if (meta.priority !== void 0 && (meta.priority < 0 || meta.priority > 1e3)) errors.push("Priority must be between 0 and 1000"); return { valid: errors.length === 0, errors }; } var AdapterBuilder = class { _meta = {}; _contextDetector; _cardExtractor; _detailEnhancer; _postProcessor; _identityKey; _shouldProcessCard; _containerSelector; _cardSelector; _getExtraUi; _triggerSelector; _mainImageSelector; withMeta(meta) { this._meta = { ...this._meta, ...meta }; return this; } withContextDetector(detector) { this._contextDetector = detector; return this; } withFC2Context(options) { this._contextDetector = fc2ContextDetector(options); return this; } withJAVContext(options) { this._contextDetector = javContextDetector(options); return this; } withHybridContext(options) { this._contextDetector = hybridContextDetector(options); return this; } withCardExtractor(extractor) { this._cardExtractor = extractor; return this; } withFC2CardExtractor(config) { this._cardExtractor = createFC2CardExtractor(config); return this; } withJAVCardExtractor(config) { this._cardExtractor = createJAVCardExtractor(config); return this; } withHybridCardExtractor(config) { this._cardExtractor = createHybridCardExtractor(config); return this; } withDetailEnhancer(enhancer) { this._detailEnhancer = enhancer; return this; } withFC2DetailEnhancer(config) { this._detailEnhancer = createFC2DetailEnhancer(config); return this; } withJAVDetailEnhancer(config) { this._detailEnhancer = createJAVDetailEnhancer(config); return this; } withPostProcessor(processor) { this._postProcessor = processor; return this; } withIdentityKey(extractor) { this._identityKey = extractor; return this; } withShouldProcessCard(checker) { this._shouldProcessCard = checker; return this; } withContainerSelector(selector) { this._containerSelector = selector; return this; } withCardSelector(selector) { this._cardSelector = selector; return this; } withExtraUi(extractor) { this._getExtraUi = extractor; return this; } withTriggerSelector(selector) { this._triggerSelector = selector; return this; } withMainImageSelector(selector) { this._mainImageSelector = selector; return this; } build() { const meta = createDefaultMeta(this._meta); const validation = validateSiteMeta(meta); if (!validation.valid) throw new Error(`Invalid site meta: ${validation.errors.join(", ")}`); if (!this._contextDetector) switch (meta.type) { case "fc2": this._contextDetector = fc2ContextDetector(); break; case "jav": this._contextDetector = javContextDetector(); break; default: this._contextDetector = hybridContextDetector(); } const config = { name: meta.name, hostnames: meta.hostnames, listOnDetail: meta.listOnDetail, detectContext: this._contextDetector }; if (this._cardExtractor) config.list = { containerSelector: this._containerSelector || this.getDefaultContainerSelector(meta.type), cardSelector: this._cardSelector || this.getDefaultCardSelector(), identityKey: this._identityKey || this.getDefaultIdentityKey(), extractor: this._cardExtractor, shouldProcessCard: this._shouldProcessCard, postProcess: this._postProcessor, getExtraUi: this._getExtraUi }; if (this._detailEnhancer) config.detail = { customDetailAction: this._detailEnhancer, triggerSelector: this._triggerSelector, mainImageSelector: this._mainImageSelector }; return config; } getDefaultContainerSelector(type) { switch (type) { case "fc2": return ".movie-list, .flex.flex-wrap, main"; case "jav": return ".movie-list, .grid, main"; default: return "main, .grid, .flex.flex-wrap"; } } getDefaultCardSelector() { return ".item:not(.fc2-card-rebuilt), div:not(.fc2-card-rebuilt)"; } getDefaultIdentityKey() { return (card) => { const link = card.querySelector("a"); if (!link?.href) return null; try { return new URL(link.href, location.origin).pathname.toLowerCase(); } catch { return link.href.toLowerCase(); } }; } }; /** * * ```typescript * import { fc2Site } from '../sites/core'; * * export const mysite = fc2Site({ * name: 'MySite', * hostnames: ['mysite.com'], * linkSelector: 'a[href*="/video/"]', * titleSelector: '.video-title', * imageSelector: '.video-thumb img' * }); * ``` * * ```typescript * import { AdapterBuilder } from '../sites/core'; * * export const customsite = new AdapterBuilder() * .withMeta({ name: 'Custom', hostnames: ['custom.com'], type: 'hybrid' }) * .withHybridContext() * .withHybridCardExtractor({ linkSelector: 'a.link' }) * .build(); * ``` * * ```typescript * import { extractFC2Id, cleanTitle, resolveImageUrl } from '../sites/core'; * * const id = extractFC2Id('FC2-PPV-12345'); // '12345' * const title = cleanTitle('FC2-PPV-12345 - Title', '12345'); // 'Title' * ``` */ /** * Fetch actress name from FD2 and mount an actress button if found. * Shared across site adapters that support actress display on detail pages. * * @returns The fetched actress name, or null if not found */ var fetchAndMountActress = async (id, containers, api) => { const fetchedActress = await fetchActressFromFD2(id); if (fetchedActress && containers.linksContainer) api.addActressButton(containers.linksContainer, fetchedActress); return fetchedActress ?? null; }; /** * Cache an actress name for a given video ID. * Shared across site adapters that extract actress from the page. */ var cacheActressName = (id, actress) => { if (actress) cacheSet(`actress_${id}`, actress); }; /** * * * Domain: fc2cmadb.com * Stack: Laravel + Livewire + DaisyUI (Tailwind CSS) * Language: Japanese (ja) */ var SITE_NAME = "FC2CMADB"; var CARD_CONTAINER_SELECTOR = ".flex.flex-wrap.bg-base-100.p-2"; var CARD_SELECTOR = ".card.bg-base-100.card-sm:not(.fc2-card-rebuilt)"; var DETAIL_IMAGE_SELECTOR = ".hero-content .bg-base-200"; var DETAIL_TITLE_SELECTOR = "h1.text-xl.font-bold"; var ARTICLE_LINK_SELECTOR = "a[href*=\"/articles/\"]"; var PENDING_RETRY_ATTR = "data-fc2cmadb-retry"; var resolveImageUrl = (card) => { const img = card.querySelector("figure img"); const dataSrc = img?.dataset.original || img?.dataset.src || img?.dataset.lazySrc; if (dataSrc) return dataSrc; const src = img?.src; if (src && !src.startsWith("data:") && !src.includes("pixel.gif")) return src; const srcset = img?.srcset; if (srcset) { const firstSrc = srcset.split(",")[0]?.trim().split(" ")[0]; if (firstSrc) return firstSrc; } return ""; }; var scheduleImageRetry = (card) => { if (card.hasAttribute(PENDING_RETRY_ATTR)) return; card.setAttribute(PENDING_RETRY_ATTR, "1"); let done = false; let observer = null; let timeout = void 0; let trackedImg = null; let onTrackedImgLoad = null; const detachImgListener = () => { if (trackedImg && onTrackedImgLoad) trackedImg.removeEventListener("load", onTrackedImgLoad); onTrackedImgLoad = null; }; const retry = () => { if (done || !resolveImageUrl(card)) return; done = true; observer?.disconnect(); if (timeout !== void 0) clearTimeout(timeout); detachImgListener(); card.removeAttribute(PENDING_RETRY_ATTR); const parent = card.parentElement; const next = card.nextSibling; if (parent) { parent.removeChild(card); if (next) parent.insertBefore(card, next); else parent.appendChild(card); } }; const syncImgListener = () => { const currentImg = card.querySelector("figure img"); if (currentImg === trackedImg) return; detachImgListener(); trackedImg = currentImg; if (trackedImg) { onTrackedImgLoad = retry; trackedImg.addEventListener("load", onTrackedImgLoad); } }; syncImgListener(); observer = new MutationObserver(() => { syncImgListener(); retry(); }); observer.observe(card, { childList: true, subtree: true, attributes: true, attributeFilter: ["src", "srcset"] }); if (resolveImageUrl(card)) setTimeout(retry, 50); timeout = setTimeout(() => { if (done) return; done = true; observer?.disconnect(); detachImgListener(); card.removeAttribute(PENDING_RETRY_ATTR); }, 8e3); }; var fc2cmadb = new AdapterBuilder().withMeta({ name: SITE_NAME, hostnames: ["fc2cmadb.com"], type: "fc2", listOnDetail: true }).withContextDetector(() => { const path = location.pathname; if (/^\/articles\/\d+/.test(path)) return PageContext.Detail; if (/^\/search/.test(path)) return PageContext.Search; return PageContext.List; }).withCardExtractor((card) => { if (!card.querySelector(ARTICLE_LINK_SELECTOR)) return null; const link = card.querySelector(ARTICLE_LINK_SELECTOR); const id = MediaUtils.extractFC2Id(link?.href || ""); if (!id) return null; const img = card.querySelector("figure img"); const imageUrl = resolveImageUrl(card); if (!imageUrl) { scheduleImageRetry(card); return null; } const titleEl = card.querySelector(".card-body h2.card-title a"); return { id, type: "fc2", title: titleEl?.title || titleEl?.textContent?.trim() || img?.alt || `FC2-PPV-${id}`, primaryImageUrl: imageUrl, imageUrl, fallbackImageUrl: imageUrl, articleUrl: link?.href || `/articles/${id}`, previewSlug: `fc2-ppv-${id}` }; }).withDetailEnhancer((cont) => { const buildConfig = () => ({ mode: "card", anchorElement: cont, idExtractor: () => { const id = MediaUtils.extractFC2Id(location.href); return id ? { id, type: "fc2" } : null; }, metadataExtractor: (id) => { const title = (document.querySelector(DETAIL_TITLE_SELECTOR)?.textContent || document.title.split("-")[0] || `FC2-PPV-${id}`).trim(); const fallbackImageUrl = getBestImageSource(cont.querySelector("img")) || void 0; return { title, primaryImageUrl: fallbackImageUrl, imageUrl: fallbackImageUrl, fallbackImageUrl }; }, mountOptions: { primaryActionsOverride: [ "id", "viewed", "magnet" ], width: "", maxWidth: "100%" }, onMounted: async (id, type, containers, api) => { cacheActressName(id, MediaUtils.cleanActressName(document.querySelector("[href*=\"/actresses/\"]")?.textContent)); if (type === "fc2") await fetchAndMountActress(id, containers, api); } }); if (cont.querySelector("img")) return buildConfig(); return new Promise((resolve) => { const observer = new MutationObserver(() => { if (cont.querySelector("img")) { observer.disconnect(); clearTimeout(timeout); resolve(buildConfig()); } }); observer.observe(cont, { childList: true, subtree: true }); const timeout = setTimeout(() => { observer.disconnect(); resolve(null); }, 8e3); }); }).withContainerSelector(CARD_CONTAINER_SELECTOR).withCardSelector(CARD_SELECTOR).withMainImageSelector(DETAIL_IMAGE_SELECTOR).withExtraUi((card) => { const badges = card.querySelectorAll("span.absolute.bottom-0.left-0, span.absolute.bottom-0.right-0"); if (badges.length === 0) return {}; return { preservedIconsHTML: Array.from(badges).map((b) => b.cloneNode(true)).map((n) => n.outerHTML).join("") }; }).withPostProcessor((card, _el, newCard) => { const figure = newCard?.querySelector("figure"); const mosaicBadge = card.querySelector("span.absolute.bottom-0.left-0"); if (mosaicBadge && figure) figure.appendChild(mosaicBadge.cloneNode(true)); const discountBadge = card.querySelector("span.absolute.bottom-0.right-0"); if (discountBadge && figure) figure.appendChild(discountBadge.cloneNode(true)); }).build(); var fc2db = new AdapterBuilder().withMeta({ name: "FC2DB", hostnames: ["fc2db.net"], type: "fc2", listOnDetail: true }).withContextDetector(() => { if (window.location.pathname.includes("/work/")) return PageContext.Detail; return PageContext.List; }).withFC2CardExtractor({ linkSelector: "a[href*=\"/work/\"]", titleSelector: ".mt-1.font-bold, .rec-carousel__title, h3 a, h3", imageSelector: "img.wp-post-image, img.rec-carousel__img, img", actressSelector: ".truncate" }).withFC2DetailEnhancer({ imageSelector: ".grid > div:first-child > a[href*=\"fc2.com/article/\"] img" }).withMainImageSelector(".grid > div:first-child > a[href*=\"fc2.com/article/\"] img").withContainerSelector(".grid.gap-4:not([class*=\"minmax\"]), #rec-carousel").withCardSelector(".bg-card:not(.fc2-card-rebuilt), .rec-carousel__slide:not(.fc2-card-rebuilt)").build(); var FD2PPV_PRESERVED_BADGES = ".float i, .float .icon, .badges span, span.absolute.top-0.right-0, .artist-tags a"; var extractActresses = (card) => { return Array.from(card.querySelectorAll(".artist-avatar-container a")).map((a) => { const img = a.querySelector("img"); const nameRaw = a.querySelector("h4")?.textContent || img?.getAttribute("alt") || img?.title || ""; const name = MediaUtils.cleanActressName(nameRaw); if (!name) return null; return { name, avatarUrl: img?.src || void 0, url: a.href || void 0 }; }).filter((a) => a !== null); }; var extractTags = (card) => { const tags = []; card.querySelectorAll(".float i, .float-icons i, .badges i").forEach((icon) => { const title = icon.getAttribute("title"); if (title && !tags.includes(title)) tags.push(title); }); card.querySelectorAll(".artist-tags a").forEach((tag) => { const text = tag.textContent?.trim(); if (text && !tags.includes(text)) tags.push(text); }); return tags; }; var fd2ppv = new AdapterBuilder().withMeta({ name: "FD2PPV", hostnames: ["fd2ppv.cc"], type: "fc2", listOnDetail: true }).withContextDetector(() => { const segments = window.location.pathname.split("/").filter(Boolean); if (segments[0] === "articles" && segments.length >= 2 && segments[1] && /^\d+$/.test(segments[1])) return PageContext.Detail; return PageContext.List; }).withCardExtractor((card) => { const link = card.querySelector("a[href*=\"/articles/\"]"); const id = MediaUtils.extractFC2Id(link?.href || ""); if (!id) return null; const carouselImages = Array.from(card.querySelectorAll(".carousel-slide img")); let imageUrl = ""; if (carouselImages.length > 0) imageUrl = getBestImageSource(carouselImages.find((img) => img.dataset.src?.includes("ppvdatabank.com") || img.src?.includes("ps1.webp")) || carouselImages[0] || null); if (!imageUrl) imageUrl = getBestImageSource(card.querySelector("img.other-work-image") || card.querySelector("img")); const isActressPage = !!document.querySelector(".artist-detail-name"); const pageActressName = isActressPage ? MediaUtils.cleanActressName(document.querySelector(".artist-detail-name")?.textContent) : null; const actresses = isActressPage ? [] : extractActresses(card); const firstActress = actresses[0]; const actress = pageActressName || firstActress?.name || null; const title = card.querySelector(".artist-content a")?.textContent?.trim() || card.querySelector(".other-work-title a")?.textContent?.trim() || card.querySelector("p a")?.textContent?.trim() || card.querySelector("p")?.textContent?.trim() || card.querySelector("h3 a")?.textContent?.trim() || `FC2-PPV-${id}`; const tags = extractTags(card); return { id, type: "fc2", site: "FD2PPV", title, actress: actress || void 0, actresses, actressAvatarUrl: firstActress?.avatarUrl, actressUrl: firstActress?.url, primaryImageUrl: imageUrl, imageUrl, fallbackImageUrl: imageUrl, tags: tags.length > 0 ? tags : void 0, articleUrl: link?.href || `/articles/${id}`, previewSlug: `fc2-ppv-${id}` }; }).withDetailEnhancer((cont) => ({ mode: "card", anchorElement: cont, idExtractor: () => { const id = MediaUtils.extractFC2Id(location.href) || MediaUtils.extractFC2Id(document.querySelector(".work-title")?.textContent || "") || MediaUtils.parseVideoId(document.querySelector(".work-meta-value a[href*=\"article\"]")?.getAttribute("href") || "")?.id; return id ? { id, type: "fc2" } : null; }, metadataExtractor: (id) => { const fc2Pattern = new RegExp(`(?:FC2-PPV-)?0*${id}`, "i"); const title = (document.querySelector(".work-brief")?.textContent || document.querySelector(".work-title")?.textContent || document.title.split("|")[0] || `FC2-PPV-${id}`).replace(fc2Pattern, "").trim() || `FC2-PPV-${id}`; const actressRaw = document.querySelector(".artist-info-card .artist-name a")?.textContent || document.querySelector(".artist-name a")?.textContent || Array.from(document.querySelectorAll(".work-meta-label")).find((el) => el.textContent?.trim() === PATTERNS.FD2PPV_SELLER_MARKER)?.nextElementSibling?.querySelector("a")?.textContent; const actress = MediaUtils.cleanActressName(actressRaw); const primaryImageUrl = getBestImageSource(cont.querySelector("img")) || void 0; return { title, actress: actress ?? void 0, primaryImageUrl, imageUrl: primaryImageUrl, fallbackImageUrl: primaryImageUrl, articleUrl: location.href }; }, mountOptions: { width: "" }, onMounted: (id) => { const actressRaw = document.querySelector(".artist-info-card .artist-name a")?.textContent || document.querySelector(".artist-name a")?.textContent || Array.from(document.querySelectorAll(".work-meta-label")).find((el) => el.textContent?.trim() === PATTERNS.FD2PPV_SELLER_MARKER)?.nextElementSibling?.querySelector("a")?.textContent; const actress = MediaUtils.cleanActressName(actressRaw); if (actress) cacheSet(`actress_${id}`, actress); } })).withContainerSelector("#artist-list, .artist-list, .work-list, .flex.flex-wrap:not(.flex-end):not(.flex-between), .container .grid, .other-works-grid").withCardSelector(".artist-card:not(.fc2-card-rebuilt), .work-card:not(.fc2-card-rebuilt), .work-list > div:not(.fc2-card-rebuilt)").withPostProcessor((card, _el, newCard, _data, classes) => { const stats = card.querySelector(".stats"); if (stats && newCard && classes) { const infoArea = newCard.querySelector(`.${classes.infoArea}`); if (infoArea) infoArea.insertBefore(stats.cloneNode(true), infoArea.querySelector(`.${classes.cardActionRow}`)); } }).withExtraUi((card) => { const badges = card.querySelectorAll(FD2PPV_PRESERVED_BADGES); if (badges.length === 0) return {}; return { preservedIconsHTML: Array.from(badges).map((b) => b.cloneNode(true)).map((n) => n.outerHTML).join("") }; }).withMainImageSelector(".work-image-large").build(); fd2ppv.onInit = () => { resetFD2Backoff(); }; var getJavdbMagnetLinks = () => { return Array.from(document.querySelectorAll("#magnets-content a[href^=\"magnet:?\"]")); }; var bindJavdbNativeHistoryButton = (selector, id) => { const btn = document.querySelector(selector); if (btn && !btn.hasAttribute("data-hooked")) { btn.setAttribute("data-hooked", "true"); btn.addEventListener("click", () => { fireAndForget(Promise.resolve(addToHistory(id)), "JavDB"); }); } }; var backfillJavdbActressForFc2 = async (id, actress, finalElement, api) => { if (actress) return; const fetchedActress = await fetchActressFromFD2(id); if (!fetchedActress) return; api.addActressButton(finalElement, fetchedActress); }; var attachJavdbNativeMagnets = async (id, type, magnetContainer, linksContainer, api) => { const magnetLinks = getJavdbMagnetLinks(); const firstLink = magnetLinks[0]; if (magnetLinks.length > 0 && firstLink) { cacheSet(id, firstLink.href); api.addMagnetButton(magnetContainer, firstLink.href); magnetLinks.slice(1, 3).forEach((link) => { api.addMagnetButton(linksContainer, link.href); }); return; } await api.attachFetchedMagnet?.(id, type, magnetContainer); }; var javdb = new AdapterBuilder().withMeta({ name: "JavDB", hostnames: ["javdb.com"], type: "jav", listOnDetail: true }).withContextDetector(() => { const path = location.pathname; if (path.includes("/search/") || location.search.includes("q=")) return PageContext.Search; if (path.startsWith("/v/")) return PageContext.Detail; return PageContext.List; }).withCardExtractor((card) => { const link = card.matches("a.box, a.tile-item, a[href^=\"/v/\"]") ? card : card.querySelector("a.box, a.tile-item, a[href^=\"/v/\"]"); if (!link) return null; const idStrong = card.querySelector(".video-title strong, .video-number"); const idText = idStrong ? idStrong.textContent : link.title || link.innerText; const titleEl = card.querySelector(".video-title"); let titleText; if (titleEl) { const clone = titleEl.cloneNode(true); clone.querySelector("strong")?.remove(); titleText = clone.textContent?.trim() || ""; } else titleText = link.title || link.innerText; const img = card.querySelector("img"); const info = MediaUtils.parseVideoId(idText || "", link.href); if (!info) return null; const tagsContainer = card.querySelector(".tags"); const tags = tagsContainer ? Array.from(tagsContainer.querySelectorAll(".tag")).map((t) => t.textContent?.trim()).filter(Boolean) : []; return { ...info, title: titleText || info.id, primaryImageUrl: img?.src, imageUrl: img?.src, fallbackImageUrl: img?.src, articleUrl: link.href, previewSlug: info.previewSlug, tags }; }).withDetailEnhancer((cont) => ({ mode: "card", anchorElement: cont, idExtractor: () => { const titleEl = document.querySelector("h2.title"); return MediaUtils.parseVideoId(titleEl?.textContent || "", location.href); }, metadataExtractor: (id, type) => { const titleEl = document.querySelector("h2.title"); const img = cont.querySelector("img.video-cover"); return { id, type, title: titleEl?.textContent?.trim() || "", primaryImageUrl: img?.src || "", articleUrl: location.href }; }, skipDefaultServices: true, onMounted: async (id, type, containers, api) => { const actress = MediaUtils.cleanActressName(cont.querySelector(".meta")?.textContent); api.addPreviewButton(containers.previewContainer, id); await attachJavdbNativeMagnets(id, type, containers.magnetContainer, containers.linksContainer, api); if (type === "fc2") await backfillJavdbActressForFc2(id, actress, containers.mainElement, api); bindJavdbNativeHistoryButton("form.button_to[action*=\"/reviews/watched\"] button", id); bindJavdbNativeHistoryButton("button.js-watched-video", id); }, mountOptions: { maxWidth: "100%", width: "" } })).withMainImageSelector(".column-video-cover").withContainerSelector(".movie-list, .tile-images:not(.preview-images)").withCardSelector(".item:not(.fc2-card-rebuilt), .tile-item:not(.fc2-card-rebuilt)").withPostProcessor((card, _el, newCard, _data, classes) => { const score = card.querySelector(".score"); const meta = card.querySelector(".meta"); const tags = card.querySelector(".tags"); if (newCard && classes) { const infoArea = newCard.querySelector(`.${classes.infoArea}`); if (infoArea) { const actionRow = infoArea.querySelector(`.${classes.cardActionRow}`); if (score) infoArea.insertBefore(score.cloneNode(true), actionRow); if (meta) infoArea.insertBefore(meta.cloneNode(true), actionRow); if (tags) infoArea.insertBefore(tags.cloneNode(true), actionRow); } } }).build(); var JAVFC2_PRESERVED_BADGES = ".video_quality span"; var JAVFC2_CONTAINER_SELECTORS = [ "#movie-details .row", "#movie-details > div > div", ".movie-container", ".owl-stage", ".movie-list-container .row", ".movie-list-container > div > div", ".similler-movie .row", ".similler-movie > div > div", ".similar-movie .row", ".similar-movie > div > div", "#main-content .row", ".main-content .row", "#hot .row", "#hot > div > div", "#section-opt .row", "#section-opt > div > div", "#latest-all .row", "#latest-all > div > div", "#latest-all-all .row", "#latest-all-all > div > div", "div[id^=\"section-\"] .row", "div[id^=\"section-\"] > div > div", "div[id^=\"latest-\"] .row", "div[id^=\"latest-\"] > div > div", ".col-md-9 .row", ".col-md-12 .row" ].join(", "); var JAVFC2_CARD_SELECTORS = [ ".latest-movie-img-container", ".movie-img", ".item" ].join(", "); var JAVFC2_IMAGE_TIMEOUT = 2e3; var waitForCardImage = (img) => { if (!img) return ""; const resolveSrc = () => { const src = getBestImageSource(img); if (src && !src.startsWith("data:") && !src.includes("pixel.gif")) return src; return ""; }; const immediate = resolveSrc(); if (immediate) return immediate; setTimeout(() => resolveSrc(), JAVFC2_IMAGE_TIMEOUT); if (img.complete) return img.src || ""; img.addEventListener("load", () => resolveSrc(), { once: true }); return img.src || ""; }; var javfc2 = new AdapterBuilder().withMeta({ name: "JavFC2", hostnames: ["javfc2.xyz"], type: "hybrid", listOnDetail: true }).withContextDetector(() => { if (window.location.pathname.includes("/watch/")) return PageContext.Detail; return PageContext.List; }).withCardExtractor((card) => { const titleLink = card.querySelector(".movie-title a, h3 a, a[href*=\"/watch/\"]"); const img = card.querySelector("img"); const altText = img?.getAttribute("alt"); const linkText = titleLink?.textContent; const rawTitleText = altText && altText.length > (linkText?.length || 0) ? altText : linkText || altText; const href = titleLink?.getAttribute("href") || card.querySelector("a")?.getAttribute("href") || ""; const videoInfo = MediaUtils.parseVideoId(rawTitleText || "", href); if (!videoInfo) return null; const imageUrl = waitForCardImage(img); return { ...videoInfo, site: "JavFC2", title: rawTitleText || `${videoInfo.type.toUpperCase()}-${videoInfo.id}`, primaryImageUrl: imageUrl, imageUrl, fallbackImageUrl: imageUrl, articleUrl: href, previewSlug: videoInfo.previewSlug }; }).withDetailEnhancer((playerDiv) => { const titleText = document.querySelector(".ml-title span.title")?.textContent || document.querySelector("h1")?.textContent || document.title; const videoInfo = MediaUtils.parseVideoId(titleText, location.href); if (!videoInfo) return null; return { mode: "toolbar", anchorElement: playerDiv, idExtractor: () => videoInfo, metadataExtractor: () => { const actressRaw = document.querySelector(".col-md-9 a[href*=\"/star/\"]")?.textContent; const actress = MediaUtils.cleanActressName(actressRaw); return { title: titleText.trim(), actress: actress ?? void 0, previewSlug: videoInfo.previewSlug ?? void 0 }; }, onMounted: async (id, type, containers, api) => { const actressRaw = document.querySelector(".col-md-9 a[href*=\"/star/\"]")?.textContent; const actress = MediaUtils.cleanActressName(actressRaw); if (actress) cacheSet(`actress_${id}`, actress); const videoJs = playerDiv.querySelector(".video-js, #play"); if (videoJs) videoJs.after(containers.mainElement); else playerDiv.after(containers.mainElement); if (type === "fc2") { const fetchedActress = await fetchActressFromFD2(id); if (fetchedActress && containers.linksContainer) api.addActressButton(containers.linksContainer, fetchedActress); } } }; }).withTriggerSelector("#play").withShouldProcessCard((card) => !card.closest("#owl-demo")).withContainerSelector(JAVFC2_CONTAINER_SELECTORS).withCardSelector(JAVFC2_CARD_SELECTORS).withExtraUi((card) => { const badges = card.querySelectorAll(JAVFC2_PRESERVED_BADGES); if (badges.length === 0) return {}; return { preservedIconsHTML: Array.from(badges).map((b) => b.cloneNode(true)).map((n) => n.outerHTML).join("") }; }).build(); var MISSAV_LIST_BLACKLIST = [ "search", "new", "actress", "maker", "dm", "genres", "series", "tags", "makers" ]; var MISSAV_TITLE_SELECTORS = [ "a.text-secondary", "a.hover\\:text-primary", "div.my-2 a", "div.mt-1 a", ".video-title a", ".thumbnail + div a" ].join(","); var MISSAV_FALLBACK_LINK_SELECTORS = "a[href*=\"/fc2-ppv-\"], a[href*=\"/en/\"], a[href*=\"/ja/\"], a[href*=\"/cn/\"]"; var MISSAV_HOST_TAG_SELECTORS = [ "a[x-show*=\"has_chinese_subtitle\"]", "a[x-show*=\"has_english_subtitle\"]", "a[x-show*=\"is_uncensored_leak\"]" ].join(","); var getMissavLastSegment = (path) => { const segments = path.split("/").filter(Boolean); return (segments[segments.length - 1] || "").toLowerCase(); }; var isMissavDetailPattern = (segment) => /^(fc2-ppv-|[a-z]{2,10}-)\d+/i.test(segment); var isMissavBlacklistedListPath = (segments) => segments.some((segment) => MISSAV_LIST_BLACKLIST.some((blacklist) => segment.toLowerCase().startsWith(blacklist))); var shouldSkipMissavCard = (card) => { return Boolean(card.closest("[x-for*=\"recommend\"]")) || Boolean(card.closest("[x-for*=\"trending\"]")) || Boolean(card.querySelector("[x-text*=\"item.\"]")) || card.hasAttribute("x-show"); }; var resolveMissavTitleLink = (card) => { return card.querySelector(MISSAV_TITLE_SELECTORS) || card.querySelector(MISSAV_FALLBACK_LINK_SELECTORS); }; var resolveMissavPreviewSlug = (video, fallbackSlug) => { return video?.dataset.src?.match(/fourhoi\.com\/([^/]+)\/preview\.mp4/)?.[1] || video?.src?.match(/fourhoi\.com\/([^/]+)\/preview\.mp4/)?.[1] || fallbackSlug || null; }; var resolveMissavHostTags = (card) => { return Array.from(card.querySelectorAll(MISSAV_HOST_TAG_SELECTORS)).map((node) => node.textContent?.trim() || "").filter(Boolean); }; var missav = new AdapterBuilder().withMeta({ name: "MissAV", hostnames: ["missav.ws", "missav.ai"], type: "hybrid", listOnDetail: true }).withContextDetector(() => { const path = window.location.pathname; const segments = path.split("/").filter(Boolean); const lastSegment = getMissavLastSegment(path); if (isMissavDetailPattern(lastSegment) && segments.length <= 3) return PageContext.Detail; if (isMissavBlacklistedListPath(segments)) return PageContext.List; if (segments.length === 1 && !MISSAV_LIST_BLACKLIST.includes(lastSegment)) return PageContext.Detail; return path.includes("/search/") ? PageContext.List : PageContext.List; }).withShouldProcessCard((card) => { if (shouldSkipMissavCard(card)) return false; const anyLink = resolveMissavTitleLink(card); const href = anyLink?.getAttribute("href"); return Boolean(anyLink?.href && href && href !== "#"); }).withCardExtractor((card) => { if (shouldSkipMissavCard(card)) return null; const img = card.querySelector("img"); const video = card.querySelector("video"); const anyLink = resolveMissavTitleLink(card); if (!anyLink || !anyLink.getAttribute("href") || anyLink.getAttribute("href") === "#") return null; const info = MediaUtils.parseVideoId(anyLink.textContent || "", anyLink.href || ""); if (!info) return null; if (info.id.length < 5 && info.type === "fc2") return null; const imageUrl = getBestImageSource(img) || ""; const previewSlug = resolveMissavPreviewSlug(video, info.previewSlug); const tags = resolveMissavHostTags(card); const actress = card.querySelector("a[href*=\"/actress/\"]")?.textContent?.trim() || card.querySelector("a[href*=\"/actresses/\"]")?.textContent?.trim(); const articleUrl = anyLink.href.split("#")[0] || anyLink.href; return { ...info, title: anyLink.textContent?.trim() || img?.alt || info.id, primaryImageUrl: imageUrl, imageUrl, fallbackImageUrl: imageUrl, articleUrl, previewSlug, tags, actress }; }).withDetailEnhancer((el) => { const title = document.querySelector("h1.text-base")?.textContent || document.title; const info = MediaUtils.parseVideoId(title, location.href); if (!info) return null; const titleEl = document.querySelector("h1.text-base"); return { mode: "toolbar", anchorElement: titleEl?.closest("div.mt-4") || titleEl || el, idExtractor: () => info, metadataExtractor: (_id, type) => { let actress = null; if (type === "jav") { const contentArea = document.querySelector("div.mt-4, div.text-secondary, main"); actress = Array.from((contentArea || document).querySelectorAll("a[href*=\"/actresses/\"]")).map((a) => MediaUtils.cleanActressName(a.textContent)).find((a) => !!a) || null; } return { title, actress: actress ?? void 0, previewSlug: info.previewSlug ?? void 0 }; }, onMounted: async (id, type, containers, api) => { if (type === "fc2") { const fetchedActress = await fetchActressFromFD2(id); if (fetchedActress && containers.linksContainer) api.addActressButton(containers.linksContainer, fetchedActress); } } }; }).withTriggerSelector("div[x-data*=\"player\"], div.player-container, h1.text-base, div.mt-4").withMainImageSelector("div[x-data*=\"player\"] img, .player-container img").withContainerSelector("main, div.grid[class*=\"grid-cols-\"], .sm\\:container > div.grid, div.posts, #main").withCardSelector("div.grid[class*=\"grid-cols-\"] > div.thumbnail.group:not(.fc2-card-rebuilt), div.grid[class*=\"grid-cols-\"] > a.thumbnail.group:not(.fc2-card-rebuilt), div.thumbnail.group:not(.fc2-card-rebuilt), a.thumbnail.group:not(.fc2-card-rebuilt), div.thumbnail:not(.fc2-card-rebuilt), a.thumbnail:not(.fc2-card-rebuilt)").withPostProcessor((card) => { card.removeAttribute("x-data"); card.removeAttribute("x-show"); card.removeAttribute("x-for"); card.addEventListener("mouseenter", (e) => e.stopPropagation(), true); }).build(); var SUPJAV_LIST_CONTAINERS = ".posts.clearfix, .posts"; var SUPJAV_TITLE_TRIGGER = ".archive-title h1, h1.entry-title, .post-title h1"; var SUPJAV_TITLE_BLOCK = ".archive-title, .post-title"; var SUPJAV_MEDIA_BLOCK = "#player-wrap, #player-wrap.player-wrap, .player-wrap, #dz_video, .video-container, .entry-content .video-player"; var SUPJAV_VIEW_BLOCK = ".dz_view"; var SUPJAV_POST_META = ".post-meta.clearfix"; var SUPJAV_EXCLUDE_SELECTORS = ".recommend, .swiper-slide-duplicate"; var findNextSiblingMatching = (element, selector) => { let current = element?.nextElementSibling; while (current) { if (current.matches(selector)) return current; if (current.matches(SUPJAV_TITLE_BLOCK) || current.matches(".posts.clearfix, .posts")) break; current = current.nextElementSibling; } return null; }; var resolveSupjavDetailContext = (titleEl) => { const titleAnchor = titleEl.closest(SUPJAV_TITLE_BLOCK) || titleEl; const mediaBlock = findNextSiblingMatching(titleAnchor, SUPJAV_MEDIA_BLOCK); const viewBlock = findNextSiblingMatching(mediaBlock, SUPJAV_VIEW_BLOCK); const postMeta = findNextSiblingMatching(viewBlock || mediaBlock, SUPJAV_POST_META); return { anchor: postMeta || viewBlock || mediaBlock || titleAnchor, postMeta }; }; var supjav = new AdapterBuilder().withMeta({ name: "Supjav", hostnames: ["supjav.com"], type: "jav", listOnDetail: true }).withContextDetector(() => { const path = window.location.pathname; if (path.includes("/search/") || window.location.search.includes("s=")) return PageContext.Search; const segments = path.replace(/^\/(zh|en|ja)\//, "/").split("/").filter(Boolean); if (segments.length === 1 && segments[0] && /^\d+\.html$/.test(segments[0])) return PageContext.Detail; if (segments.length === 1 && segments[0]?.toLowerCase() !== "new" && ![ "popular", "maker", "cast", "tag", "category" ].includes(segments[0]?.toLowerCase() || "")) return PageContext.Detail; return PageContext.List; }).withShouldProcessCard((card) => { if (card.closest(SUPJAV_EXCLUDE_SELECTORS)) return false; const link = card.querySelector("h3 a, a[rel=\"bookmark\"]"); if (!link?.href) return false; const text = link.title || link.textContent || card.textContent || ""; return Boolean(MediaUtils.parseVideoId(text, link.href)); }).withIdentityKey((card) => { const link = card.querySelector("h3 a, a[rel=\"bookmark\"]"); if (!link?.href) return null; return link.href; }).withCardExtractor((card) => { const link = card.querySelector("h3 a, a[rel=\"bookmark\"]"); if (!link?.href) return null; const text = link.title || link.textContent || card.textContent || ""; const info = MediaUtils.parseVideoId(text, link.href); if (!info) return null; const img = card.querySelector("img.thumb, img"); const imageSource = card.querySelector("img")?.getAttribute("data-original") || getBestImageSource(img) || ""; return { ...info, title: text || info.id, primaryImageUrl: imageSource, imageUrl: imageSource, fallbackImageUrl: imageSource, articleUrl: link.href, previewSlug: info.previewSlug, actress: card.querySelector(".post-meta a[href*=\"/cast/\"]")?.textContent?.trim() || card.querySelector(".meta a[href*=\"/cast/\"]")?.textContent?.trim() }; }).withDetailEnhancer((titleEl) => ({ mode: "toolbar", anchorElement: resolveSupjavDetailContext(titleEl).anchor, idExtractor: () => { return MediaUtils.parseVideoId(titleEl.textContent || "", location.href) || null; }, metadataExtractor: () => ({ title: titleEl.textContent?.trim() || "", actress: void 0, previewSlug: void 0 }), onMounted: async (id, type, containers, api) => { if (type === "fc2") { const fetchedActress = await fetchActressFromFD2(id); if (fetchedActress && containers.linksContainer) api.addActressButton(containers.linksContainer, fetchedActress); } } })).withTriggerSelector(SUPJAV_TITLE_TRIGGER).withMainImageSelector("#player-wrap img, .player-wrap img").withContainerSelector(SUPJAV_LIST_CONTAINERS).withCardSelector(".post:has(a.img):not(.fc2-card-rebuilt)").withPostProcessor((card, _el, newCard, data, classes) => { if ((data?.title?.includes("[有]") || card.innerText.includes("有码")) && classes?.isCensored) { card.classList.add(classes.isCensored); newCard?.classList.add(classes.isCensored); } const meta = card.querySelector(".meta"); if (meta && newCard && classes) { const infoArea = newCard.querySelector(`.${classes.infoArea}`); if (infoArea) infoArea.insertBefore(meta.cloneNode(true), infoArea.querySelector(`.${classes.cardActionRow}`)); } if (supjav.context?.supjavSortByViews) { const container = card.closest(SUPJAV_LIST_CONTAINERS); if (container && container.style.display !== "flex") { container.style.display = "flex"; container.style.flexWrap = "wrap"; } const match = ((meta?.querySelector(".date, .pv, .views, .stats") || meta)?.textContent || card.textContent || "").match(/([\d,.]+)[kKmM]?\s*(?:Views|views|播放)/); let views = 0; if (match && match[1]) { const numStr = match[1].replace(/,/g, ""); views = parseFloat(numStr); const matchText = match[0].toLowerCase(); if (matchText.includes("k")) views *= 1e3; if (matchText.includes("m")) views *= 1e6; } if (views > 0) card.style.order = `${-Math.round(views)}`; } }).build(); var sitesRegistered = false; function registerAllSites(registerSiteConfig) { if (sitesRegistered) return; sitesRegistered = true; registerSiteConfig("fc2db", fc2db); registerSiteConfig("fd2ppv", fd2ppv); registerSiteConfig("supjav", supjav); registerSiteConfig("missav", missav); registerSiteConfig("javdb", javdb); registerSiteConfig("javfc2", javfc2); registerSiteConfig("fc2cmadb", fc2cmadb); } var GlobalFilterManager = new class FilterManager { static REFRESH_DELAY_MS = 80; pendingTimer = null; lastCounts = null; static isSearchPage() { const p = location.pathname; const s = location.search; return p.includes("/search") || s.includes("s=") || s.includes("search") || s.includes("q=") || s.includes("keyword="); } requestRefresh() { if (this.pendingTimer !== null) return; this.pendingTimer = setTimeout(() => { this.pendingTimer = null; this.refreshNow(); }, FilterManager.REFRESH_DELAY_MS); } refreshNow() { const C = Config.CLASSES; const counts = { hideViewed: 0, hideNoMagnet: 0, hideCensored: 0 }; document.querySelectorAll(`.${C.cardRebuilt}`).forEach((card) => { const el = card; if (el.classList.contains(C.hideViewed)) counts.hideViewed += 1; if (el.classList.contains(C.hideNoMagnet)) counts.hideNoMagnet += 1; if (el.classList.contains(C.hideCensored)) counts.hideCensored += 1; }); if (!this.hasChanged(counts)) return; this.syncToBody(counts); this.lastCounts = counts; CoreEvents.emit(AppEvents.FILTER_COUNTS_CHANGED, counts); } hasChanged(next) { if (!this.lastCounts) return true; return this.lastCounts.hideViewed !== next.hideViewed || this.lastCounts.hideNoMagnet !== next.hideNoMagnet || this.lastCounts.hideCensored !== next.hideCensored; } syncToBody(counts) { const d = document.body.dataset; const v = String(counts.hideViewed); const n = String(counts.hideNoMagnet); const c = String(counts.hideCensored); if (d.fc2HideViewedCount !== v) d.fc2HideViewedCount = v; if (d.fc2HideNoMagnetCount !== n) d.fc2HideNoMagnetCount = n; if (d.fc2HideCensoredCount !== c) d.fc2HideCensoredCount = c; } static resolveCardRoot(el) { return el.classList.contains(Config.CLASSES.cardRebuilt) ? el : el.closest(`.${Config.CLASSES.cardRebuilt}`) || el; } static shouldSkipFilters(target) { if (this.isSearchPage()) return true; if ((target.classList.contains(Config.CLASSES.processedCard) ? target : target.querySelector(`.${Config.CLASSES.processedCard}`))?.classList.contains("fc2-is-detail")) return true; return false; } applyCardVisibility(c, hasM) { if (!c) return; const target = FilterManager.resolveCardRoot(c); if (FilterManager.shouldSkipFilters(target)) { target.classList.remove(Config.CLASSES.hideNoMagnet); return; } const isSearching = (target.classList.contains(Config.CLASSES.processedCard) ? target : target.querySelector(`.${Config.CLASSES.processedCard}`))?.dataset?.enhSearching === "true"; const shouldHide = resolveService(STATE_SERVICE).proxy.hideNoMagnet && !hasM && !isSearching; target.classList.toggle(Config.CLASSES.hideNoMagnet, shouldHide); } applyCensoredFilter(c) { if (!c) return; const target = FilterManager.resolveCardRoot(c); if (FilterManager.shouldSkipFilters(target)) { target.classList.remove(Config.CLASSES.hideCensored); return; } const isCensored = target.classList.contains(Config.CLASSES.isCensored) || !!target.querySelector(`.${Config.CLASSES.isCensored}`); target.classList.toggle(Config.CLASSES.hideCensored, resolveService(STATE_SERVICE).proxy.hideCensored && isCensored); } applyHistoryVisibility(c) { if (!c) return; const target = FilterManager.resolveCardRoot(c); if (FilterManager.shouldSkipFilters(target)) { target.classList.remove(Config.CLASSES.hideViewed); return; } const isViewed = resolveService(HISTORY_SERVICE).has(target.dataset.id || ""); target.classList.toggle(Config.CLASSES.hideViewed, resolveService(STATE_SERVICE).proxy.hideViewed && isViewed); } }(); var log$16 = Logger.scope("ViewStore"); var ViewStoreImpl = class { states = /* @__PURE__ */ new Map(); maxStateEntries; constructor(maxStateEntries = 2e3) { this.maxStateEntries = maxStateEntries; CoreEvents.on(AppEvents.SITE_RESET, () => this.clear()); } getDefaults() { return { isSearching: false, hasMagnet: false, hasPreviews: false }; } get(id) { const normalizedId = IdNormalizer.normalize(id); const existing = this.states.get(normalizedId); if (existing) { this.states.delete(normalizedId); this.states.set(normalizedId, existing); return existing; } const initialState = this.getDefaults(); this.states.set(normalizedId, initialState); this.enforceLimit(); return initialState; } set(id, key, value) { const normalizedId = IdNormalizer.normalize(id); const current = this.get(normalizedId); if (current[key] === value) return; current[key] = value; log$16.trace(`[${normalizedId}] ${String(key)} = ${value}`); CoreEvents.emit(AppEvents.VIEW_STATE_CHANGED, { id: normalizedId, key: String(key), value }); } update(id, updates) { Object.entries(updates).forEach(([key, value]) => { this.set(id, key, value); }); } release(id) { const normalizedId = IdNormalizer.normalize(id); this.states.delete(normalizedId); } clear() { this.states.clear(); } enforceLimit() { if (this.states.size > this.maxStateEntries) { const oldestKey = this.states.keys().next().value; if (oldestKey !== void 0) this.states.delete(oldestKey); } } }; var ViewStore = new ViewStoreImpl(); var log$15 = Logger.scope("MagnetResolver"); var isValidMagnetUrl = (url) => typeof url === "string" && url.startsWith("magnet:?"); /** Resolve magnet for a single card: check cache → fetch from service → emit result */ var resolveMagnetForCard = async (id, type, source) => { try { const cached = await resolveService(DATA_SERVICE).cache.get(id); if (isNegativeMagnetCacheValue(cached)) { CoreEvents.emit(AppEvents.MAGNET_FAILED, { id }); return; } if (isValidMagnetUrl(cached)) { CoreEvents.emit(AppEvents.MAGNET_FOUND, { id, url: cached }); return; } handleMagnetResult(id, await resolveService(MAGNET_SERVICE).fetchMagnet(id, type)); } catch (e) { log$15.error(`Magnet lookup failed for ${id} via ${source}`, e); handleMagnetResult(id, null); } }; /** Queue a magnet lookup for a card (sets searching state, fires async) */ var queueMagnetLookup = (id, type, source) => { ViewStore.set(id, "isSearching", true); fireAndForget(resolveMagnetForCard(id, type, source), "MagnetResolver"); }; /** Handle magnet result: update ViewStore and trigger filter refresh */ var handleMagnetResult = (id, url) => { const resolvedUrl = isValidMagnetUrl(url) ? url : null; ViewStore.set(id, "isSearching", false); ViewStore.set(id, "hasMagnet", !!resolvedUrl); ViewStore.set(id, "magnetUrl", resolvedUrl); GlobalFilterManager.requestRefresh(); }; var isNavigableHref = (href) => { return false; }; /** * Standardized Button Component * Supports multiple variants, sizes and modern glassmorphism aesthetics. */ var Button = (iconSvg, tip, href, onClick, className = "", options = {}) => { const classes = Config.CLASSES; const children = []; if (iconSvg) children.push(UIUtils.icon(iconSvg)); if (!options.iconOnly || options.size !== "micro") children.push(h("span", { className: classes.buttonText }, tip)); children.push(h("span", { className: classes.tooltip }, tip)); const variantClass = options.variant ? `${options.variant}` : ""; const sizeClass = options.size ? `${options.size}` : ""; const iconOnlyClass = options.iconOnly ? "icon-only" : ""; const combinedClassName = [ classes.resourceBtn, "fc2-enh-btn", variantClass, sizeClass, iconOnlyClass, className, options.className || "" ].filter(Boolean).join(" ").trim(); const buttonTitle = options.title || tip; const buttonProps = { className: combinedClassName, "aria-label": options.ariaLabel || buttonTitle, "data-title": options.dataTitle || buttonTitle, onclick: (e) => { if (!onClick) return; if (!isNavigableHref()) e.preventDefault(); e.stopPropagation(); onClick(e); } }; return isNavigableHref() ? h("a", { ...buttonProps, href, target: "_blank", rel: "noopener noreferrer" }, ...children) : h("button", { ...buttonProps, type: options.type || "button" }, ...children); }; var log$14 = Logger.scope("ActressButton"); var ActressButton = (cont, actress) => { if (!cont) return; if (!actress?.trim()) { log$14.debug("Skip actress button: empty name"); return; } if (cont.querySelector(".fc2-btn-actress")) return; const name = actress.trim(); const actBtn = Button(null, name, void 0, () => UIUtils.copyButtonBehavior(actBtn, name, t("tooltipCopied")), "fc2-btn-actress"); const actionRow = cont.querySelector(`.${Config.CLASSES.cardActionRow}`); const links = cont.querySelector(`.${Config.CLASSES.resourceLinksContainer}`); if (actionRow) cont.insertBefore(actBtn, actionRow); else if (links) cont.insertBefore(actBtn, links); else cont.appendChild(actBtn); const toolbar = cont.closest(".fc2-enh-toolbar"); if (toolbar) toolbar.dataset.layout = "with-actress"; }; var getMagnetButtonSelector = () => `.${Config.CLASSES.resourceBtn}.${Config.CLASSES.btnMagnet}`; var hasMagnetButton = (root) => !!root?.querySelector(getMagnetButtonSelector()); var findMagnetButton = (root) => root?.querySelector(getMagnetButtonSelector()) || null; var MagnetButton = (cont, url) => { if (cont && !hasMagnetButton(cont)) { const btn = Button(IconMagnet, t("tooltipCopyMagnet"), void 0, () => UIUtils.copyButtonBehavior(btn, url, t("tooltipCopied")), Config.CLASSES.btnMagnet, { ariaLabel: t("tooltipCopyMagnet"), title: t("tooltipCopyMagnet") }); cont.appendChild(btn); const card = cont.closest(`.${Config.CLASSES.cardRebuilt}`); if (card) GlobalFilterManager.applyCardVisibility(card, true); } }; /** Single mount helper for magnet copy button (list/detail/toolbar). */ var mountMagnetButton = MagnetButton; var hasActionSheetUrl = (url) => typeof url === "string" && url.trim().length > 0; var ActionSheet = class { static backdrop = null; static sheet = null; static hideTimer = null; static touchStartY = 0; static touchCurrentY = 0; static cancelPendingHide() { if (this.hideTimer !== null) { window.clearTimeout(this.hideTimer); this.hideTimer = null; } } static ensureElements() { if (this.backdrop && !this.backdrop.isConnected && this.sheet && !this.sheet.isConnected) { this.backdrop = null; this.sheet = null; } if (this.backdrop && this.sheet) return; this.backdrop = h("div", { className: "fc2-action-sheet-backdrop", "aria-hidden": "true" }); this.sheet = h("div", { className: "fc2-action-sheet", role: "dialog", "aria-modal": "true", "aria-labelledby": "fc2-sheet-title", tabindex: "-1" }); this.sheet.addEventListener("keydown", (e) => { if (e.key !== "Tab" || !this.sheet) return; const focusable = this.sheet.querySelectorAll("button:not([disabled]), a[href], [tabindex]:not([tabindex=\"-1\"])"); if (focusable.length === 0) return; const first = focusable[0]; const last = focusable[focusable.length - 1]; if (e.shiftKey) { if (document.activeElement === first || document.activeElement === this.sheet) { e.preventDefault(); last.focus(); } } else if (document.activeElement === last) { e.preventDefault(); first.focus(); } }); UIHost.add(this.backdrop); UIHost.add(this.sheet); if (!DeviceService.isMobile()) this.sheet.classList.add("desktop"); this.backdrop.onclick = (e) => { e.preventDefault(); e.stopPropagation(); this.hide(); }; this.initTouchEvents(); } static createCloseButton() { return h("button", { className: "fc2-action-sheet-close-btn", "aria-label": t("btnClose"), onclick: (e) => { e.preventDefault(); this.hide(); } }, "×"); } static createOptionItem(opt) { const iconEl = (typeof opt.icon === "string" ? UIUtils.icon(opt.icon) : opt.icon).cloneNode(true); if (opt.onSelect) return h("button", { className: "fc2-action-sheet-item", type: "button", onclick: (e) => { e.preventDefault(); e.stopPropagation(); this.hide(); window.setTimeout(() => opt.onSelect?.(), 0); } }, iconEl, h("span", {}, opt.name)); if (hasActionSheetUrl(opt.url)) { const item = h("a", { className: "fc2-action-sheet-item", href: opt.url, target: "_blank", rel: "noopener noreferrer" }, iconEl, h("span", {}, opt.name)); item.onclick = () => this.hide(); return item; } return h("button", { className: "fc2-action-sheet-item", type: "button", disabled: true, "aria-disabled": "true" }, iconEl, h("span", {}, opt.name)); } static createOptionsGrid(options) { const grid = h("div", { className: "fc2-action-sheet-grid" }); options.forEach((opt) => { grid.appendChild(this.createOptionItem(opt)); }); return grid; } static show(title, options) { this.cancelPendingHide(); this.ensureElements(); this.sheet.style.transform = ""; this.sheet.style.transition = ""; this.touchStartY = 0; this.touchCurrentY = 0; this.sheet.textContent = ""; this.sheet.appendChild(h("div", { className: "fc2-action-sheet-handle" })); const header = h("div", { className: "fc2-action-sheet-header" }, h("div", { className: "fc2-action-sheet-title", id: "fc2-sheet-title" }, title)); header.appendChild(this.createCloseButton()); this.sheet.appendChild(header); this.sheet.appendChild(this.createOptionsGrid(options)); requestAnimationFrame(() => { this.backdrop.classList.add("active"); this.sheet.classList.add("active"); (this.sheet.querySelector("button:not([disabled]), a[href], [tabindex]:not([tabindex=\"-1\"])") ?? this.sheet).focus(); }); OverlayStack.push(this); } static initTouchEvents() { if (!this.sheet) return; this.sheet.addEventListener("touchstart", (e) => { const touch = e.touches[0]; if (!touch) return; this.touchStartY = touch.clientY; if (this.sheet) this.sheet.style.transition = "none"; }, { passive: true }); this.sheet.addEventListener("touchmove", (e) => { const touch = e.touches[0]; if (!touch) return; this.touchCurrentY = touch.clientY; const deltaY = this.touchCurrentY - this.touchStartY; if (deltaY > 0 && this.sheet) this.sheet.style.transform = `translateY(${deltaY}px)`; }, { passive: true }); this.sheet.addEventListener("touchend", () => { if (!this.sheet) return; this.sheet.style.transition = ""; if (this.touchCurrentY - this.touchStartY > UI_CONSTANTS.SWIPE_DISMISS_THRESHOLD) this.hide(); else this.sheet.style.transform = ""; this.touchStartY = 0; this.touchCurrentY = 0; }); } static close() { this.hide(); } static hide() { if (!this.backdrop || !this.sheet) return; this.cancelPendingHide(); OverlayStack.remove(this); this.backdrop.classList.remove("active"); this.sheet.classList.remove("active"); const b = this.backdrop; const s = this.sheet; this.hideTimer = window.setTimeout(() => { if (s && s.parentElement) s.remove(); if (b && b.parentElement) b.remove(); if (this.backdrop === b) this.backdrop = null; if (this.sheet === s) this.sheet = null; if (this.hideTimer !== null) this.hideTimer = null; }, TIMING.UI_TRANSITION_NORMAL); } }; var log$13 = Logger.scope("Registry"); var COMP_SYM = Symbol("fc2_comp"); var Registry = class { components = /* @__PURE__ */ new Map(); constructor() { this.initGC(); this.initEvents(); } initEvents() { CoreEvents.on(AppEvents.SITE_RESET, () => { log$13.debug("Site change detected via events, clearing registry"); this.clear(); }); CoreEvents.on(AppEvents.VIEW_STATE_CHANGED, (payload) => { this.notify(payload.id, { key: payload.key, value: payload.value }); }); } initGC() { DomObserver.subscribe((_added, removed) => { removed.forEach((node) => this.gc(node)); }); } gc(root) { const elementsWithComponents = root.querySelectorAll("[data-fc2-comp]"); const checkAndClean = (el) => { const set = el[COMP_SYM]; if (set) Array.from(set).forEach((ins) => { log$13.trace(`Auto-GC: detached ${ins.id}`); this.unregister(ins); }); }; if (root.hasAttribute("data-fc2-comp")) checkAndClean(root); elementsWithComponents.forEach((el) => checkAndClean(el)); } register(component) { if (!this.components.has(component.id)) this.components.set(component.id, /* @__PURE__ */ new Set()); this.components.get(component.id).add(component); if (component.element) { const boundElement = component.element; let set = boundElement[COMP_SYM]; if (!set) { set = /* @__PURE__ */ new Set(); boundElement[COMP_SYM] = set; component.element.setAttribute("data-fc2-comp", ""); } set.add(component); } log$13.trace(`Registered: ${component.id}`); } unregister(component) { const id = component.id; const set = this.components.get(id); if (set) { set.delete(component); if (set.size === 0) { this.components.delete(id); ViewStore.release(id); } } if (component.element) { const boundElement = component.element; const symSet = boundElement[COMP_SYM]; if (symSet) { symSet.delete(component); if (symSet.size === 0) { delete boundElement[COMP_SYM]; component.element.removeAttribute("data-fc2-comp"); } } } if (component.destroy) try { component.destroy(); } catch (e) { log$13.warn(`Failed to destroy component ${id}`, e); } } getInstances(id) { const set = this.components.get(id); return set ? Array.from(set) : []; } getAllRegisteredIds() { return Array.from(this.components.keys()); } notify(id, data) { this.getInstances(id).forEach((ins) => { try { ins.update(data); } catch (e) { log$13.error(`Notify failed for ${id}`, e); } }); } clear() { const allComponents = []; const allIds = /* @__PURE__ */ new Set(); this.components.forEach((set, id) => { allIds.add(id); set.forEach((ins) => allComponents.push(ins)); }); this.components.clear(); allComponents.forEach((ins) => { if (ins.element) { const boundElement = ins.element; const symSet = boundElement[COMP_SYM]; if (symSet) { symSet.delete(ins); if (symSet.size === 0) { delete boundElement[COMP_SYM]; ins.element.removeAttribute("data-fc2-comp"); } } } if (ins.destroy) try { ins.destroy(); } catch (e) { log$13.warn(`Failed to destroy component ${ins.id}`, e); } }); allIds.forEach((id) => ViewStore.release(id)); log$13.debug("Component registry cleared"); } }; var ComponentRegistry = new Registry(); var log$12 = Logger.scope("HistoryManager"); var HistoryManager = class { isSyncing = false; pendingSync = false; unsubscribers = []; constructor() { this.unsubscribers.push(CoreEvents.on(AppEvents.HISTORY_LOADED, () => this.requestSync())); this.unsubscribers.push(CoreEvents.on(AppEvents.UI_READY, () => this.requestSync())); this.unsubscribers.push(CoreEvents.on(AppEvents.HISTORY_CHANGED, () => this.requestSync())); } /** * Request a sync, coalescing multiple rapid requests into one. */ requestSync() { if (this.isSyncing) { this.pendingSync = true; return; } this.syncAllComponents(); } schedulePendingSync() { if (this.pendingSync) { this.pendingSync = false; requestAnimationFrame(() => this.syncAllComponents()); } } normalizeId(id) { return IdNormalizer.normalize(id); } /** * Toggles the 'watched' status for a specific media ID. * Updates HistoryService, ViewStore, and triggers a filter refresh. */ async toggleWatched(id, isManual = true) { try { const normalizedId = IdNormalizer.normalize(id); const isCurrentlyWatched = resolveService(HISTORY_SERVICE).has(normalizedId, "watched"); const targetStatus = isCurrentlyWatched ? "none" : "watched"; if (isCurrentlyWatched) await resolveService(HISTORY_SERVICE).remove(normalizedId); else await resolveService(HISTORY_SERVICE).add(normalizedId, "watched"); ViewStore.set(normalizedId, "status", targetStatus); CoreEvents.emit(AppEvents.HISTORY_CHANGED, {}); GlobalFilterManager.requestRefresh(); log$12.debug(`Toggled watched status for ${normalizedId}: ${targetStatus} (Manual: ${isManual})`); } catch (error) { log$12.error(`Failed to toggle watched status for ${id}`, error); } } /** * Marks the 'watched' status for a specific media ID without toggling. */ async setWatched(id) { try { const normalizedId = IdNormalizer.normalize(id); if (resolveService(HISTORY_SERVICE).has(normalizedId, "watched")) return; await resolveService(HISTORY_SERVICE).add(normalizedId, "watched"); ViewStore.set(normalizedId, "status", "watched"); CoreEvents.emit(AppEvents.HISTORY_CHANGED, {}); GlobalFilterManager.requestRefresh(); log$12.debug(`Set watched status for ${normalizedId}`); } catch (error) { log$12.error(`Failed to set watched status for ${id}`, error); } } /** * Batch updates for multiple IDs. * Useful for 'Mark all as read' or similar bulk actions. */ async markBatchWatched(ids) { log$12.info(`Batch marking ${ids.length} items as watched`); await Promise.all(ids.map(async (id) => { const normalizedId = IdNormalizer.normalize(id); await resolveService(HISTORY_SERVICE).add(normalizedId, "watched"); ViewStore.set(normalizedId, "status", "watched"); })); CoreEvents.emit(AppEvents.HISTORY_CHANGED, {}); GlobalFilterManager.requestRefresh(); } /** * Synchronizes all registered components with the current history state. */ syncAllComponents() { this.isSyncing = true; const ids = ComponentRegistry.getAllRegisteredIds(); log$12.debug(`Syncing ${ids.length} components with history`); ids.forEach((id) => { const status = resolveService(HISTORY_SERVICE).getStatus(id); if (status) ViewStore.set(id, "status", status); }); this.isSyncing = false; this.schedulePendingSync(); } /** * Synchronizes a single card's DOM state with the history status. * This is a low-level helper for non-component cards or manual DOM patches. */ syncElement(el, id, classes) { const isWatched = resolveService(HISTORY_SERVICE).has(id, "watched"); el.classList.toggle(classes.isViewed, isWatched); } destroy() { this.unsubscribers.forEach((fn) => fn()); this.unsubscribers.length = 0; } }; var GlobalHistoryManager = new HistoryManager(); var getInitialToggleState = (id, type) => { return resolveService(HISTORY_SERVICE).getStatus(id) === type; }; var getToggleVisualConfig = () => ({ activeClass: "fc2-is-viewed", className: "fc2-btn-toggle-view", iconClass: "fc2-icon-viewed", iconOff: IconEyeSlash, iconOn: IconEye, tooltipOff: t("tooltipMarkAsViewed"), tooltipOn: t("tooltipMarkAsUnviewed") }); var decorateToggleIcons = (button, config, type) => { const firstIcon = button.querySelector(".fc2-icon"); if (firstIcon) { firstIcon.classList.add(config.iconClass); const iconOffClass = `fc2-icon-un${"viewed" }`; firstIcon.after(UIUtils.icon(config.iconOff, iconOffClass)); } }; var runToggleAnimation = (button) => { button.classList.add("fc2-animate-pop"); window.setTimeout(() => button.classList.remove("fc2-animate-pop"), TIMING.UI_TRANSITION_NORMAL); }; var syncToggleAccessibility = (button, label) => { button.setAttribute("aria-label", label); button.setAttribute("data-title", label); }; var applyToggleState = (button, isActive, config, _type) => { const classes = Config.CLASSES; const currentTip = isActive ? config.tooltipOn : config.tooltipOff; button.classList.toggle(config.activeClass, isActive); if (isActive) runToggleAnimation(button); const tooltip = button.querySelector(`.${classes.tooltip}`); if (tooltip) tooltip.textContent = currentTip; const buttonText = button.querySelector(`.${classes.buttonText}`); if (buttonText) buttonText.textContent = currentTip; button.setAttribute("aria-pressed", String(isActive)); syncToggleAccessibility(button, currentTip); }; var handleToggleClick = async (_button, id, _type, _activeClass) => { await GlobalHistoryManager.toggleWatched(id, true); }; var isTargetUpdate = (_type, key) => key === "status"; var StatusToggle = (id, type) => { const config = getToggleVisualConfig(); const isActive = getInitialToggleState(id, type); const currentLabel = isActive ? config.tooltipOn : config.tooltipOff; const button = Button(config.iconOn, currentLabel, void 0, () => { handleToggleClick(button, id); }, `${config.className} ${isActive ? config.activeClass : ""}`, { ariaLabel: currentLabel, title: currentLabel }); button.setAttribute("aria-pressed", String(isActive)); decorateToggleIcons(button, config); const component = { id, element: button, update: (raw) => { const data = raw; if (!isTargetUpdate(type, data?.key)) return; const isNextActive = data?.value === type; applyToggleState(button, isNextActive, config); } }; ComponentRegistry.register(component); return component; }; var UIToolbar = { createDetailToolbar: (data, addPreviewButton, addPlayButton) => { const { id, type, actress } = data; const C = Config.CLASSES; const toolbar = h("div", { className: "fc2-enh-toolbar fc2-enh-toolbar--detail", dataset: { layout: actress ? "with-actress" : "without-actress" } }); const infoArea = h("div", { className: C.infoArea }); const ctrls = h("div", { className: "fc2-card-top-right-controls fc2-toolbar-section-controls" }); if (resolveService(STATE_SERVICE).proxy.enableHistory) { const toggle = StatusToggle(id, "watched"); if (toggle && toggle.element) ctrls.appendChild(toggle.element); } const badge = Button("", id, void 0, (e) => { e.preventDefault(); e.stopPropagation(); UIUtils.copyButtonBehavior(badge, id, t("tooltipCopied")); }, C.fc2IdBadge); const btnText = badge.querySelector(`.${C.buttonText}`); if (btnText) btnText.textContent = id; ctrls.appendChild(badge); const links = h("div", { className: `${C.resourceLinksContainer} fc2-toolbar-section-links`.trim() }); addPlayButton(links, id, type, data.previewSlug); if (type === "fc2" && resolveService(STATE_SERVICE).proxy.loadExtraPreviews) addPreviewButton(links, id); if (resolveService(STATE_SERVICE).proxy.enableExternalLinks) { const trigger = Button(IconLink, t("labelExternalLinks"), void 0, (e) => { e.preventDefault(); e.stopPropagation(); const portals = AppContainer.resolve(PORTAL_SERVICE).getAvailablePortals({ id, type }); if (portals.length > 0) { const options = portals.map((p) => ({ ...p, icon: PORTAL_ICONS[p.id] || "<svg viewBox=\"0 0 640 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l112.2-112.3c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0z\"/></svg>" })); ActionSheet.show(`${id} - ${t("labelExternalLinks")}`, options); } else Toast.show(t("alertNoExternalLinks"), "info"); }); links.appendChild(trigger); } infoArea.appendChild(ctrls); if (actress) ActressButton(infoArea, actress); infoArea.appendChild(links); toolbar.appendChild(infoArea); resolveService(MAGNET_SERVICE).fetchMagnet(id, type).then((url) => { if (url && url.startsWith("magnet:?")) mountMagnetButton(links, url); }); return toolbar; } }; var PreviewLogic = class { static activePreviewTokens = /* @__PURE__ */ new WeakMap(); static previewAbortControllers = /* @__PURE__ */ new WeakMap(); static warmCleanupBindings = /* @__PURE__ */ new WeakMap(); static activeVideos = /* @__PURE__ */ new Set(); static lifecycleObservers = /* @__PURE__ */ new WeakMap(); static lifecycleCards = /* @__PURE__ */ new Set(); static containerBindings = /* @__PURE__ */ new Map(); static registerContainer(container) { const existing = this.containerBindings.get(container); if (existing) { container.removeEventListener(existing.mode, existing.handler, existing.capture); this.containerBindings.delete(container); } const mode = resolveService(STATE_SERVICE).proxy.previewMode; if (mode === "static") return; const selector = `.${Config.CLASSES.processedCard}`; const isTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0; const handler = (event) => { const target = event.target; if (isTouch) { const previewArea = target.closest(`.${Config.CLASSES.videoPreviewContainer}`); if (!previewArea) return; const card = previewArea.closest(selector); if (!card) return; event.preventDefault(); event.stopPropagation(); if (card.querySelector(`video.${Config.CLASSES.previewElement}`)) { this.disposePreviewForCard(card); return; } if (!previewArea.querySelector(`img.${Config.CLASSES.staticPreview}`)) return; this.loadVideoProgressive(card); } else if (mode === "hover") { const card = target.closest(selector); if (card && target === card) this.loadVideoProgressive(card); } }; const eventMode = isTouch ? "click" : "mouseenter"; container.addEventListener(eventMode, handler, true); this.containerBindings.set(container, { mode: eventMode, handler, capture: true }); } static disposePreviewForCard(card) { this.detachLifecycle(card); this.previewAbortControllers.get(card)?.abort(); this.previewAbortControllers.delete(card); this.activePreviewTokens.delete(card); const video = card.querySelector("video"); if (video) { this.activeVideos.delete(video); this.disposePreviewElement(video); } const binding = this.warmCleanupBindings.get(card); if (binding) { card.removeEventListener("mouseleave", binding.listener, binding.capture); this.warmCleanupBindings.delete(card); } } static async loadVideoProgressive(card) { const container = card.querySelector(`.${Config.CLASSES.videoPreviewContainer}`); const image = container?.querySelector(`img.${Config.CLASSES.staticPreview}`); if (!container || !image) return; const { id, type, previewSlug } = card.dataset; if (!id || !type) return; if (this.tryUseCachedPreview(card, container, image, id)) return; if (this.tryUseExistingVideo(card, container, image)) return; this.cleanupStaleVideo(container); this.previewAbortControllers.get(card)?.abort(); this.hideErrorIndicator(container); this.showLoadingIndicator(container); const video = this.createPreviewVideoElement(); this.activeVideos.add(video); container.appendChild(video); const previewToken = Symbol(id); this.activePreviewTokens.set(card, previewToken); const abortController = new AbortController(); this.previewAbortControllers.set(card, abortController); let isStillHovered = true; this.attachWarmCleanup(card, video, image, container, () => { isStillHovered = false; abortController.abort(); }); this.attachProgressListener(video, container, () => isStillHovered); const reveal = () => this.revealPreview(container, video, image); video.addEventListener("loadeddata", reveal, { once: true }); video.addEventListener("playing", () => this.handlePlayingEvent(video, id, () => isStillHovered, reveal), { once: true }); video.addEventListener("error", () => { if (isStillHovered) this.restoreStaticPreview(container, image, video); }); await this.fetchAndPlayPreview(card, container, image, video, id, type, previewSlug, previewToken, () => isStillHovered, abortController.signal); } static tryUseCachedPreview(card, container, image, id) { const cached = resolveService(PREVIEW_SERVICE).getCachedElement(id); if (!cached || !(cached instanceof HTMLVideoElement)) return false; if (cached.parentNode !== container) { cached.remove(); container.appendChild(cached); } this.showActivePreview(container, image, cached); this.attachWarmCleanup(card, cached, image, container); return true; } static tryUseExistingVideo(card, container, image) { const existingVideo = container.querySelector("video"); if (!existingVideo || !(existingVideo.currentSrc || existingVideo.getAttribute("src"))) return false; this.showActivePreview(container, image, existingVideo); this.attachWarmCleanup(card, existingVideo, image, container); return true; } static cleanupStaleVideo(container) { const existingVideo = container.querySelector("video"); if (existingVideo) { this.activeVideos.delete(existingVideo); this.disposePreviewElement(existingVideo); } } static createPreviewVideoElement() { return h("video", { referrerpolicy: "no-referrer", autoplay: true, loop: true, muted: true, playsInline: true, preload: "auto", className: `${Config.CLASSES.previewElement} ${Config.CLASSES.hidden}` }); } static attachProgressListener(video, container, isStillHovered) { video.addEventListener("progress", () => { if (video.buffered.length > 0 && isStillHovered() && video.isConnected) { const percent = video.buffered.end(0) / video.duration * 100; const progress = container.querySelector(".preview-progress"); if (progress) progress.textContent = `${Math.round(percent)}%`; } }); } static revealPreview(container, video, image) { container.classList.add("fc2-preview-active"); video.classList.remove(Config.CLASSES.hidden); video.classList.add("fc2-reveal-content"); image.classList.add(Config.CLASSES.hidden); this.hideLoadingIndicator(container); this.hideErrorIndicator(container); } static handlePlayingEvent(video, id, isStillHovered, reveal) { if (!isStillHovered() || !video.isConnected) { video.pause(); resolveService(PREVIEW_SERVICE).cacheElement(id, video); return; } requestAnimationFrame(() => { if (isStillHovered() && video.isConnected) reveal(); else { video.pause(); resolveService(PREVIEW_SERVICE).cacheElement(id, video); } }); } static async fetchAndPlayPreview(card, container, image, video, id, type, previewSlug, previewToken, isStillHovered, signal) { try { const blob = await resolveService(PREVIEW_SERVICE).getPreviewBlob(id, type, previewSlug, signal); if (!isStillHovered() || !card.isConnected || this.activePreviewTokens.get(card) !== previewToken) { this.activeVideos.delete(video); this.disposePreviewElement(video); return; } video.src = URL.createObjectURL(blob); video.load(); video.play().catch(() => {}); } catch (error) { this.handlePreviewError(card, container, image, video, previewToken, isStillHovered, error); } } static handlePreviewError(card, container, image, video, previewToken, isStillHovered, error) { if (error instanceof DOMException && error.name === "AbortError") { this.disposePreviewElement(video); this.activeVideos.delete(video); return; } const rateLimitError = error; if (typeof rateLimitError?.retryAfterMs === "number") { if (isStillHovered() && card.isConnected && this.activePreviewTokens.get(card) === previewToken) setTimeout(() => { if (isStillHovered() && card.isConnected && this.activePreviewTokens.get(card) === previewToken) this.loadVideoProgressive(card); }, rateLimitError.retryAfterMs); return; } if (isStillHovered() && card.isConnected && this.activePreviewTokens.get(card) === previewToken) this.restoreStaticPreview(container, image, video, true); else { this.activeVideos.delete(video); this.disposePreviewElement(video); } } static showActivePreview(container, image, video) { container.classList.add("fc2-preview-active"); video.classList.remove(Config.CLASSES.hidden); video.classList.add("fc2-reveal-content"); image.classList.add(Config.CLASSES.hidden); video.play().catch(() => {}); } static restoreStaticPreview(container, image, video, showError = true) { container.classList.remove("fc2-preview-active"); image.classList.remove(Config.CLASSES.hidden); this.hideLoadingIndicator(container); this.disposePreviewElement(video); if (showError) this.showErrorIndicator(container); } static attachWarmCleanup(card, video, image, container, onCleanup) { const cleanup = () => { onCleanup?.(); container.classList.remove("fc2-preview-active"); if (video.isConnected) { video.pause(); video.classList.add(Config.CLASSES.hidden); video.classList.remove("fc2-reveal-content"); } image.classList.remove(Config.CLASSES.hidden); this.hideLoadingIndicator(container); const id = card.dataset.id; if (id && (video.currentSrc || video.getAttribute("src"))) resolveService(PREVIEW_SERVICE).cacheElement(id, video); this.hideErrorIndicator(container); }; const existing = this.warmCleanupBindings.get(card); if (existing) card.removeEventListener("mouseleave", existing.listener, existing.capture); card.addEventListener("mouseleave", cleanup, { once: true }); this.warmCleanupBindings.set(card, { listener: cleanup, capture: false }); if (typeof IntersectionObserver !== "undefined") { this.detachLifecycle(card); const observer = new IntersectionObserver((entries) => { const entry = entries[0]; if (entry && !entry.isIntersecting) { this.detachLifecycle(card); cleanup(); } }, { threshold: 0 }); observer.observe(card); this.lifecycleObservers.set(card, observer); this.lifecycleCards.add(card); } } static detachLifecycle(card) { this.lifecycleObservers.get(card)?.disconnect(); this.lifecycleObservers.delete(card); this.lifecycleCards.delete(card); } static showLoadingIndicator(container) { if (container.querySelector(".preview-loading")) return; const loader = h("div", { className: "preview-loading" }, h("div", { className: "preview-spinner" }), h("div", { className: "preview-progress" }, t("labelLoading"))); container.appendChild(loader); } static hideLoadingIndicator(container) { container.querySelector(".preview-loading")?.remove(); } static showErrorIndicator(container) { this.hideErrorIndicator(container); const error = h("div", { className: "preview-error" }, h("span", { className: "fc2-icon preview-error-icon", innerHTML: IconTriangleExclamation }), h("div", {}, t("alertPreviewLoadFailed"))); container.appendChild(error); setTimeout(() => error.remove(), TIMING.PREVIEW_ERROR_DELAY); } static hideErrorIndicator(container) { container.querySelectorAll(".preview-error").forEach((e) => e.remove()); } static disposePreviewElement(element) { if (element instanceof HTMLVideoElement) { const src = element.src || element.currentSrc || element.getAttribute("src"); element.pause(); element.removeAttribute("src"); try { element.load(); } catch {} element.remove(); if (src?.startsWith("blob:")) URL.revokeObjectURL(src); } else element.remove(); } static clearActive() { this.lifecycleCards.forEach((card) => this.disposePreviewForCard(card)); this.lifecycleCards.clear(); this.activeVideos.forEach((v) => this.disposePreviewElement(v)); this.activeVideos.clear(); } }; var resolveActionLabel = (button) => button.getAttribute("data-title") || button.getAttribute("aria-label") || button.getAttribute("title") || button.textContent?.trim() || t("btnMoreOptions"); var resolveActionIcon = (button) => { const icon = button.querySelector(".fc2-icon"); return icon ? icon.cloneNode(true) : IconGear; }; var buildMoreActionOptions = (links) => Array.from(links.children).filter((child) => child instanceof HTMLElement).filter((el) => window.getComputedStyle(el).display !== "none").map((button, index) => ({ id: `${resolveActionLabel(button).replace(/\s+/g, "-").toLowerCase()}-${index}`, name: resolveActionLabel(button), icon: resolveActionIcon(button), onSelect: () => button.click() })); var createPortalButton = (data, minimal) => { if (minimal || !resolveService(STATE_SERVICE).proxy.enableExternalLinks) return null; return Button(IconLink, t("labelExternalLinks"), void 0, (event) => { event.preventDefault(); event.stopPropagation(); const portals = resolveService(PORTAL_SERVICE).getAvailablePortals(data); if (portals.length > 0) { const options = portals.map((p) => ({ ...p, icon: PORTAL_ICONS[p.id] || "<svg viewBox=\"0 0 640 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l112.2-112.3c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0z\"/></svg>" })); ActionSheet.show(t("labelExternalLinks"), options); } else Toast.show(t("alertNoExternalLinks"), "info"); }, "btn-external-links", { ariaLabel: t("tooltipOpenExternalLinks"), title: t("tooltipOpenExternalLinks") }); }; var isJavdbCard = (data) => { return data.site === "javdb" || data.articleUrl?.includes("://javdb.com/") || window.location.hostname.includes("javdb"); }; var applyAdaptiveJavdbPreviewRatio = (img, data) => { if (!isJavdbCard(data)) return; const container = img.parentElement; if (!container) return; const { naturalWidth, naturalHeight } = img; if (!naturalWidth || !naturalHeight) return; if (naturalWidth / naturalHeight > 1.05) { container.style.setProperty("--fc2-javdb-cover-ratio", `${naturalWidth} / ${naturalHeight}`); return; } container.style.removeProperty("--fc2-javdb-cover-ratio"); }; var createThumbnail = (data, classes) => { const img = h("img", { alt: data.title || data.id, className: `${classes.staticPreview} ${classes.previewElement}`, onload: function() { this.parentElement?.classList.replace("fc2-skeleton", "is-loaded"); applyAdaptiveJavdbPreviewRatio(this, data); this.classList.add("fc2-reveal-content"); } }); smartLoadMedia(img, data.primaryImageUrl || data.imageUrl || UI_CONSTANTS.PLACEHOLDER_IMAGE, data.imageUrl, data.fallbackImageUrl, ...data.extraFallbacks || []); return h(data.articleUrl ? "a" : "div", { className: `${classes.videoPreviewContainer} fc2-skeleton`, ...data.articleUrl ? { href: data.articleUrl, target: "_blank", rel: "noopener noreferrer" } : {} }, img); }; var createHeaderRow = (idBadge, healthIndicator, statusToggle) => h("div", { className: "fc2-card-meta-row" }, h("div", { className: "fc2-card-identity-slot" }, idBadge), h("div", { className: "fc2-card-status-slot", onclick: (e) => e.stopPropagation() }, healthIndicator, statusToggle?.element ? statusToggle.element : null)); var createPlayButton = (data) => { return Button(IconPlay, t("labelPlayPreview"), void 0, (event) => { event.preventDefault(); event.stopPropagation(); const card = event.currentTarget.closest(`.${Config.CLASSES.processedCard}`); if (card) { const playEvent = new CustomEvent("fc2-play-fullscreen", { detail: { card, data }, bubbles: true, composed: true }); card.dispatchEvent(playEvent); } }, Config.CLASSES.btnPlayFullscreen, { iconOnly: true, size: "micro" }); }; var createGalleryButton = (data) => { return Button(IconImages, t("extraPreviewTitle"), void 0, (event) => { event.preventDefault(); event.stopPropagation(); const card = event.currentTarget.closest(`.${Config.CLASSES.processedCard}`); if (card) { const galleryEvent = new CustomEvent("fc2-open-gallery", { detail: { card, data }, bubbles: true, composed: true }); card.dispatchEvent(galleryEvent); } }, "btn-open-gallery", { iconOnly: true, size: "micro" }); }; var buildActionArea = (data, minimal, links) => { const portalEntry = createPortalButton({ id: data.id, type: data.type }, minimal); const playEntry = createPlayButton(data); const galleryEntry = data.type === "fc2" && resolveService(STATE_SERVICE).proxy.loadExtraPreviews ? createGalleryButton(data) : null; return { portalEntry, playEntry, galleryEntry, secondaryActions: h("div", { className: "fc2-resource-links-container fc2-card-secondary-actions" }, playEntry, galleryEntry, portalEntry, Button(IconGear, t("btnMoreOptions"), void 0, (event) => { event.preventDefault(); event.stopPropagation(); const options = buildMoreActionOptions(links); if (options.length > 0) ActionSheet.show(t("btnMoreOptions"), options); }, "btn-more-actions", { ariaLabel: t("btnMoreOptions"), title: t("btnMoreOptions") })) }; }; var createCardTitle = (data, classes) => { if (!data.title) return null; return data.articleUrl ? h("a", { className: classes.customTitle, href: data.articleUrl, target: "_blank", rel: "noopener noreferrer", title: data.title, onclick: (event) => { event.stopPropagation(); GlobalHistoryManager.toggleWatched(data.id, true); } }, data.title) : h("span", { className: classes.customTitle, title: data.title }, data.title); }; var buildCardMetaParts = (actress, site, tags, hasAvatar = false) => { const parts = []; if (actress && !hasAvatar) parts.push(actress); if (site) parts.push(site.toUpperCase()); if (tags) parts.push(...tags.slice(0, 5)); return parts.filter(Boolean); }; var resolveHealthIndicatorMeta = (lastCheck, now = Date.now()) => { const isRecent = now - lastCheck < 864e5; return { isRecent, title: isRecent ? t("tooltipHealthRecent") : `${t("tooltipHealthStale")}: ${new Date(lastCheck).toLocaleDateString()}`, symbol: isRecent ? "?" : "?" }; }; var createActressUnit = (info) => h("div", { className: `fc2-actress-unit${info.url ? " fc2-actress-unit-linkable" : ""}`, title: info.name, ...info.url ? { tabindex: "0", role: "link" } : {}, onclick: (e) => { if (info.url) { e.stopPropagation(); BrowserService.openTab(info.url); } }, onkeydown: info.url ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); e.stopPropagation(); BrowserService.openTab(info.url); } } : void 0 }, info.avatarUrl ? h("img", { src: info.avatarUrl, alt: info.name || "", className: "fc2-actress-avatar-original", onerror: function() { this.classList.add("fc2-actress-avatar-hidden"); } }) : null, info.name ? h("span", { className: "fc2-actress-name-original" }, info.name) : null); var createActressProfile = (data) => { const list = [...data.actresses || []]; if (list.length === 0 && (data.actress || data.actressAvatarUrl)) list.push({ name: data.actress || "", avatarUrl: data.actressAvatarUrl, url: data.actressUrl }); if (list.length === 0) return null; return h("div", { className: "fc2-actress-profile-badge-original" }, ...list.map(createActressUnit)); }; var createHealthIndicator = (lastCheck) => { if (!lastCheck) return null; const meta = resolveHealthIndicatorMeta(lastCheck); return h("div", { className: `fc2-health-indicator ${meta.isRecent ? "fc2-health-indicator--recent" : "fc2-health-indicator--stale"}`, title: meta.title, "aria-label": meta.title }, meta.symbol); }; var isPrimaryAction = (action) => { const actions = resolveService(STATE_SERVICE).proxy.cardPrimaryActions; return Array.isArray(actions) ? actions.includes(action) : [ "id", "viewed", "play", "preview", "magnet", "external" ].includes(action); }; var createBadge = (id, classes) => { const badge = Button("", id, void 0, (event) => { event.preventDefault(); event.stopPropagation(); UIUtils.copyButtonBehavior(badge, id, t("tooltipCopied")); badge.classList.add("fc2-pulse-once"); setTimeout(() => badge.classList.remove("fc2-pulse-once"), TIMING.UI_ANIMATION_BADGE); }, classes.fc2IdBadge, { ariaLabel: t("tooltipCopyId"), title: t("tooltipCopyId"), dataTitle: id }); const text = badge.querySelector(`.${classes.buttonText}`); if (text) text.textContent = id; return badge; }; var createEnhancedCardMeta = (classes, items) => items.length === 0 ? null : h("div", { className: classes.cardMeta, title: items.join(" · ") }, items.join(" · ")); var createEnhancedCardView = ({ classes, data, onCardClick, minimal, suppressNativeDetailMeta = false }) => { const statusToggle = resolveService(STATE_SERVICE).proxy.enableHistory ? StatusToggle(data.id, "watched") : null; const idBadge = createBadge(data.id, classes); const healthIndicator = createHealthIndicator(data.lastCheck); const primaryLinks = h("div", { className: "fc2-resource-links-container fc2-card-primary-actions" }); const links = h("div", { className: "fc2-resource-links-container fc2-card-overflow-actions fc2-card-action-stash", hidden: true }); if (!isPrimaryAction("id")) links.appendChild(idBadge); if (!isPrimaryAction("viewed") && statusToggle?.element) links.appendChild(statusToggle.element); const { portalEntry, galleryEntry, playEntry, secondaryActions } = buildActionArea(data, minimal, links); const meta = (data.site || "").toLowerCase() !== "fd2ppv" ? createEnhancedCardMeta(classes, buildCardMetaParts(data.actress, data.site, data.tags, !!data.actress || !!data.actressAvatarUrl || !!(data.actresses && data.actresses.length > 0))) : null; const card = h("div", { className: `${classes.processedCard} ${classes.cardRebuilt} ${data.type}-card ${data.customClass || ""} ${minimal ? "fc2-is-minimal" : ""}`, dataset: { id: data.id, type: data.type, previewSlug: data.previewSlug || "", enhSearching: "true", hasMagnet: "false" }, onclick: onCardClick }); const selectionOverlay = h("div", { className: "fc2-card-selection-overlay" }, h("input", { type: "checkbox", className: "fc2-card-selection-checkbox", onclick: (e) => e.stopPropagation() })); const safePreservedIconsHTML = data.preservedIconsHTML ? (() => { const temp = document.createElement("div"); temp.innerHTML = data.preservedIconsHTML; temp.querySelectorAll("script, style, iframe, object, embed, link, meta, form, base, [onload], [onerror], [onclick], [onmouseover], [onfocus]").forEach((el) => el.remove()); return temp.innerHTML; })() : null; const infoArea = h("div", { className: classes.infoArea }, createHeaderRow(idBadge, healthIndicator, statusToggle), suppressNativeDetailMeta ? null : createActressProfile(data), suppressNativeDetailMeta ? null : createCardTitle(data, classes), safePreservedIconsHTML ? h("div", { className: classes.preservedIconsContainer, innerHTML: safePreservedIconsHTML }) : null, meta, h("div", { className: classes.cardActionRow }, secondaryActions), links); card.appendChild(createThumbnail(data, classes)); card.appendChild(infoArea); card.appendChild(selectionOverlay); return { card, links, primaryLinks, secondaryLinks: secondaryActions, meta, idBadge, healthIndicator, statusToggle, portalEntry, galleryEntry, playEntry, selectionOverlay }; }; var redistributeCardActions = (primaryLinks, links, secondaryLinks, controls, isPrimaryActionFn) => { const moreBtn = secondaryLinks.querySelector(".btn-more-actions"); const moveToTarget = (btn, isPrimary, container) => { if (!btn) return; if (isPrimary) container.appendChild(btn); else links.appendChild(btn); }; if (!isPrimaryActionFn("id") && !links.contains(controls.idBadge)) links.appendChild(controls.idBadge); if (controls.statusToggle?.element && !isPrimaryActionFn("viewed") && !links.contains(controls.statusToggle.element)) links.appendChild(controls.statusToggle.element); moveToTarget(controls.playEntry || primaryLinks.querySelector(`.${Config.CLASSES.btnPlayFullscreen}`) || links.querySelector(`.${Config.CLASSES.btnPlayFullscreen}`) || secondaryLinks.querySelector(`.${Config.CLASSES.btnPlayFullscreen}`), isPrimaryActionFn("play"), secondaryLinks); moveToTarget(controls.galleryEntry || primaryLinks.querySelector(".btn-open-gallery") || links.querySelector(".btn-open-gallery") || secondaryLinks.querySelector(".btn-open-gallery"), isPrimaryActionFn("preview"), secondaryLinks); moveToTarget(controls.portalEntry || primaryLinks.querySelector(".btn-external-links") || links.querySelector(".btn-external-links") || secondaryLinks.querySelector(".btn-external-links"), isPrimaryActionFn("external"), secondaryLinks); if (moreBtn instanceof HTMLElement) secondaryLinks.appendChild(moreBtn); moveToTarget(findMagnetButton(primaryLinks) || findMagnetButton(links) || findMagnetButton(secondaryLinks), isPrimaryActionFn("magnet"), secondaryLinks); if (moreBtn instanceof HTMLElement) { const hasOverflow = Array.from(links.children).some((child) => child.style.display !== "none"); moreBtn.style.display = hasOverflow ? "" : "none"; } }; var DOUBLE_TAP_DELTA = 10; var SWIPE_NAV_THRESHOLD = 50; var GALLERY_SLIDESHOW_INTERVAL = 3e3; var isGalleryImage = (preview) => preview?.type === "image"; var getGalleryWrappedIndex = (currentIndex, length, delta) => { if (length <= 0) return 0; return (currentIndex + delta + length) % length; }; var buildGallerySearchUrl = (src) => EXTERNAL_URLS.GOOGLE_LENS.replace("{url}", encodeURIComponent(src)); var resolveGalleryTouchAction = ({ diffX, diffY, isZoomed, item, lastTapTime, now }) => { if (Boolean(item) && isGalleryImage(item) && now - lastTapTime < TIMING.UI_TRANSITION_NORMAL && Math.abs(diffX) < DOUBLE_TAP_DELTA && Math.abs(diffY) < DOUBLE_TAP_DELTA) return "toggleZoom"; if (isZoomed) return "none"; if (diffY > UI_CONSTANTS.SWIPE_DISMISS_THRESHOLD && Math.abs(diffX) < SWIPE_NAV_THRESHOLD) return "dismiss"; if (Math.abs(diffX) > SWIPE_NAV_THRESHOLD && Math.abs(diffY) < SWIPE_NAV_THRESHOLD) return diffX > 0 ? "prev" : "next"; return "none"; }; var createGalleryCounter = (total) => { const current = h("span", { className: "fc2-enh-viewer-counter-current" }, "1"); return { counter: h("div", { className: "fc2-enh-viewer-counter" }, current, h("span", { className: "fc2-enh-viewer-counter-separator" }, "/"), h("span", {}, total.toString())), current }; }; var createGalleryThumbStrip = (previews, _currentIndex) => { const thumbItems = previews.map((preview, index) => { const el = preview.type === "image" ? h("img", { src: preview.src, loading: "eager" }) : h("video", { muted: true, src: preview.src }); return h("div", { className: `fc2-enh-thumb-item ${index === _currentIndex ? "active" : ""}`, "data-idx": index }, el); }); return { thumbItems, thumbStrip: h("div", { className: "fc2-enh-viewer-thumbs" }, ...thumbItems) }; }; var createGalleryNavigation = (previews) => { if (previews.length <= 1) return { navNext: null, navPrev: null }; return { navPrev: h("div", { className: "fc2-enh-viewer-nav prev" }, h("span", { className: "fc2-icon fc2-enh-viewer-nav-icon", innerHTML: IconChevronLeft })), navNext: h("div", { className: "fc2-enh-viewer-nav next" }, h("span", { className: "fc2-icon fc2-enh-viewer-nav-icon", innerHTML: IconChevronRight })) }; }; var createGalleryElements = (previews, currentIndex) => { const container = h("div", { className: "fc2-enh-viewer-backdrop" }); const closeBtn = h("div", { className: "fc2-enh-viewer-close" }, h("span", { className: "fc2-icon", innerHTML: IconXmark })); const btnSlideshow = h("button", { type: "button", className: "fc2-enh-viewer-action pb-play", title: t("gallerySlideshow"), innerHTML: IconPlay }); const btnSearch = h("button", { type: "button", className: "fc2-enh-viewer-action pb-search", title: t("gallerySearch"), innerHTML: IconImageSearch }); const actionBar = h("div", { className: "fc2-enh-viewer-actions" }, btnSlideshow, btnSearch); const { counter, current } = createGalleryCounter(previews.length); const stage = h("div", { className: "fc2-enh-viewer-stage" }); const { thumbItems, thumbStrip } = createGalleryThumbStrip(previews, currentIndex); const { navNext, navPrev } = createGalleryNavigation(previews); if (previews.length <= 1) container.classList.add("fc2-enh-viewer-single"); container.append(stage, closeBtn, actionBar, counter, thumbStrip); if (navPrev && navNext) container.append(navPrev, navNext); return { btnSearch, btnSlideshow, closeBtn, container, counterCurrent: current, navNext, navPrev, stage, thumbItems, thumbStrip }; }; var scrollActiveThumbIntoView = (thumbStrip) => { const activeThumb = thumbStrip.querySelector(".fc2-enh-thumb-item.active"); if (activeThumb) activeThumb.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" }); }; var createGalleryCloseHandler = (refs, state, cleanupFns, getDeferredScrollTimer, clearDeferredScrollTimer, stopSlideshow, isActive, clearActive) => { let isClosed = false; return () => { if (isClosed) return; isClosed = true; const deferredScrollTimer = getDeferredScrollTimer(); if (deferredScrollTimer !== null) { window.clearTimeout(deferredScrollTimer); clearDeferredScrollTimer(); } stopSlideshow(state, refs.btnSlideshow); cleanupFns.forEach((cleanup) => cleanup()); refs.container.remove(); if (isActive()) clearActive(); }; }; var attachThumbSelection = (refs, state, render) => { refs.thumbItems.forEach((thumbItem, index) => { thumbItem.onclick = (event) => { event.stopPropagation(); if (index !== state.index) { state.isZoomed = false; state.index = index; render("init"); } }; }); }; var bindGalleryControls = (previews, refs, state, render, close, navigate, toggleSlideshow) => { refs.closeBtn.onclick = (event) => { event.stopPropagation(); close(); }; refs.stage.onclick = (event) => { if (event.target === refs.stage) close(); }; refs.btnSlideshow.onclick = (event) => { event.stopPropagation(); toggleSlideshow(state, refs.btnSlideshow, () => navigate("next")); }; refs.btnSearch.onclick = (event) => { event.stopPropagation(); const item = previews[state.index]; if (isGalleryImage(item)) BrowserService.openTab(buildGallerySearchUrl(item.src)); }; attachThumbSelection(refs, state, render); if (refs.navPrev && refs.navNext) { refs.navPrev.onclick = (event) => { event.stopPropagation(); navigate("prev"); }; refs.navNext.onclick = (event) => { event.stopPropagation(); navigate("next"); }; } }; var scheduleInitialThumbSync = (refs, close, setDeferredScrollTimer, isActive) => { setDeferredScrollTimer(window.setTimeout(() => { setDeferredScrollTimer(null); if (isActive(close)) scrollActiveThumbIntoView(refs.thumbStrip); }, 50)); }; var activeGalleryClose = null; var createViewerState = (startIndex) => ({ index: startIndex, isZoomed: false, isTransitioning: false, slideshowInterval: null, lastTapTime: 0, touchStartX: 0, touchStartY: 0 }); var preloadGalleryItems = (previews) => { previews.forEach((item) => { if (item.type === "image") { const image = new Image(); image.src = item.src; return; } const video = document.createElement("video"); video.preload = "metadata"; video.src = item.src; }); }; var setSlideshowButtonState = (button, isActive) => { button.classList.toggle("active", isActive); button.textContent = ""; const temp = document.createElement("div"); temp.innerHTML = isActive ? IconPause : IconPlay; temp.querySelectorAll("script, style, [onload], [onerror]").forEach((el) => el.remove()); while (temp.firstChild) button.appendChild(temp.firstChild); }; var stopSlideshow = (state, button) => { if (state.slideshowInterval) { clearInterval(state.slideshowInterval); state.slideshowInterval = null; } setSlideshowButtonState(button, false); }; var toggleSlideshow = (state, button, onAdvance) => { if (state.slideshowInterval) { stopSlideshow(state, button); return; } state.slideshowInterval = window.setInterval(onAdvance, GALLERY_SLIDESHOW_INTERVAL); setSlideshowButtonState(button, true); }; var createGalleryStageMedia = (item, isZoomed, onToggleZoom) => { const el = item.type === "video" ? h("video", { controls: true, autoplay: true, loop: true, onclick: (event) => event.stopPropagation(), className: "fc2-enh-viewer-stage-media" }) : h("img", { draggable: false, onclick: (event) => { event.stopPropagation(); onToggleZoom(); }, className: `fc2-enh-viewer-stage-image${isZoomed ? " zoomed" : ""}` }); smartLoadMedia(el, item.src); return el; }; var renderGalleryView = (previews, state, refs, render, direction = "init") => { if (state.isTransitioning) { state.pendingIndex = state.index; return; } const item = previews[state.index]; if (!item) return; state.isTransitioning = true; refs.counterCurrent.textContent = (state.index + 1).toString(); refs.btnSearch.classList.toggle("fc2-hidden", !isGalleryImage(item)); refs.thumbItems.forEach((thumbItem, index) => thumbItem.classList.toggle("active", index === state.index)); scrollActiveThumbIntoView(refs.thumbStrip); refs.stage.className = `fc2-enh-viewer-stage slide-${direction}${state.isZoomed ? " zoomed" : ""}`; refs.stage.textContent = ""; refs.stage.appendChild(createGalleryStageMedia(item, state.isZoomed, () => { state.isZoomed = !state.isZoomed; render("init"); })); window.setTimeout(() => { state.isTransitioning = false; const pendingIndex = state.pendingIndex; state.pendingIndex = void 0; if (pendingIndex !== void 0 && refs.container.isConnected) { state.index = pendingIndex; render("init"); } }, TIMING.UI_TRANSITION_NORMAL); }; var attachKeyboardNavigation = (previews, state, render, close) => { const keyHandler = (event) => { const key = event.key.toLowerCase(); if (key === "a" || key === "arrowleft") { event.stopPropagation(); state.isZoomed = false; state.index = getGalleryWrappedIndex(state.index, previews.length, -1); render("prev"); } else if (key === "d" || key === "arrowright") { event.stopPropagation(); state.isZoomed = false; state.index = getGalleryWrappedIndex(state.index, previews.length, 1); render("next"); } else if (key === "escape") { event.stopPropagation(); close(); } }; window.addEventListener("keydown", keyHandler); return () => window.removeEventListener("keydown", keyHandler); }; var attachTouchNavigation = (container, previews, state, render, close) => { const onTouchStart = (event) => { const touch = event.changedTouches[0]; if (!touch) return; state.touchStartX = touch.screenX; state.touchStartY = touch.screenY; }; const onTouchEnd = (event) => { const touch = event.changedTouches[0]; if (!touch) return; const diffX = touch.screenX - state.touchStartX; const diffY = touch.screenY - state.touchStartY; const now = Date.now(); const action = resolveGalleryTouchAction({ diffX, diffY, isZoomed: state.isZoomed, item: previews[state.index], lastTapTime: state.lastTapTime, now }); state.lastTapTime = now; if (action === "toggleZoom") { event.preventDefault(); state.isZoomed = !state.isZoomed; render("init"); return; } if (action === "dismiss") { close(); return; } if (action === "prev" || action === "next") { state.index = getGalleryWrappedIndex(state.index, previews.length, action === "prev" ? -1 : 1); render(action); } }; container.addEventListener("touchstart", onTouchStart, { passive: true }); container.addEventListener("touchend", onTouchEnd, { passive: false }); return () => { container.removeEventListener("touchstart", onTouchStart); container.removeEventListener("touchend", onTouchEnd); }; }; var openGalleryViewer = (_id, previews, startIndex = 0) => { if (!previews.length) return; activeGalleryClose?.(); const state = createViewerState(startIndex); preloadGalleryItems(previews); const refs = createGalleryElements(previews, state.index); const cleanupFns = []; let deferredScrollTimer = null; const close = createGalleryCloseHandler(refs, state, cleanupFns, () => deferredScrollTimer, () => { deferredScrollTimer = null; }, stopSlideshow, () => activeGalleryClose === close, () => { activeGalleryClose = null; }); activeGalleryClose = close; const render = (direction = "init") => renderGalleryView(previews, state, refs, render, direction); const navigate = (direction) => { state.isZoomed = false; state.index = getGalleryWrappedIndex(state.index, previews.length, direction === "prev" ? -1 : 1); render(direction); }; bindGalleryControls(previews, refs, state, render, close, navigate, toggleSlideshow); cleanupFns.push(attachKeyboardNavigation(previews, state, render, close)); cleanupFns.push(attachTouchNavigation(refs.container, previews, state, render, close)); scheduleInitialThumbSync(refs, close, (timer) => { deferredScrollTimer = timer; }, (candidateClose) => activeGalleryClose === candidateClose); render(); UIHost.add(refs.container); }; var log$11 = Logger.scope("OpenExtraGallery"); /** * Single use-case: fetch extra previews (Paipancon etc.) and open the gallery lightbox. * All UI entry points (card / detail / toolbar) must call this — no duplicate fetch+toast logic. */ async function openExtraGallery(id) { try { const res = await resolveService(SCRAPER_SERVICE).fetchExtraPreviews(id); if (res.items.length > 0) { openGalleryViewer(id, res.items); return "opened"; } if (res.error === "cf-blocked") { Toast.show(t("alertPreviewCfBlocked"), "warn"); return "cf-blocked"; } if (res.error === "rate-limited") { Toast.show(t("alertPreviewRateLimited"), "warn"); return "rate-limited"; } Toast.show(t("alertNoPreview"), "info"); return "empty"; } catch (error) { log$11.warn("Extra gallery failed", error); Toast.show(t("alertPreviewLoadFailed"), "error"); return "error"; } } var log$10 = Logger.scope("OpenVideoPreview"); /** * Single use-case: resolve video preview URL and open it in the gallery lightbox. */ async function openVideoPreview(id, type, previewSlug) { try { const preview = resolveService(PREVIEW_SERVICE); if (!await preview.checkExistence(id, type, previewSlug)) { Toast.show(t("alertNoVideoPreview"), "info"); return "missing"; } openGalleryViewer(id, [{ type: "video", src: preview.getPreviewUrl(id, type, previewSlug) }]); return "opened"; } catch (error) { log$10.warn("Video preview failed", error); Toast.show(t("alertPreviewLoadFailed"), "error"); return "error"; } } var getSelectionService = () => resolveService(SELECTION_SERVICE); var hasCensoredIndicator = (preservedIconsHTML) => Boolean(preservedIconsHTML?.includes(PATTERNS.CENSORED_INDICATOR)); var applyCardVisualState = (card, state, classes) => { if (typeof state.isSearching === "boolean") if (state.isSearching) card.dataset.enhSearching = "true"; else delete card.dataset.enhSearching; if (typeof state.hasMagnet === "boolean") { card.dataset.hasMagnet = String(state.hasMagnet); card.classList.toggle(classes.noMagnet || "fc2-no-magnet", !state.hasMagnet); } if (typeof state.isViewed === "boolean") card.classList.toggle(classes.isViewed, state.isViewed); if (typeof state.isBlocked === "boolean" && classes.isBlocked) card.classList.toggle(classes.isBlocked, state.isBlocked); if (typeof state.isCensored === "boolean" && classes.isCensored) card.classList.toggle(classes.isCensored, state.isCensored); }; var updateMagnetState = ({ card, links, primaryLinks, secondaryLinks, state, options, classes, controls, isPrimaryActionFn }) => { const targetContainer = isPrimaryActionFn("magnet") ? secondaryLinks : links; if (state.hasMagnet && state.magnetUrl && !hasMagnetButton(targetContainer)) mountMagnetButton(targetContainer, state.magnetUrl); applyCardVisualState(card, { hasMagnet: state.hasMagnet }, classes); if (!options.skipFilters) GlobalFilterManager.applyCardVisibility(card, state.hasMagnet); redistributeCardActions(primaryLinks, links, secondaryLinks, controls, isPrimaryActionFn); }; var handlePlayFullscreen = async (data, e) => { const btn = e.target.closest(".fc2-enh-btn"); const iconContainer = btn?.querySelector(".fc2-icon"); const originalIcon = iconContainer?.innerHTML || ""; if (iconContainer) iconContainer.innerHTML = IconSpinner; if (btn) btn.classList.add(Config.CLASSES.btnLoading); try { await openVideoPreview(data.id, data.type, data.previewSlug || void 0); } finally { if (iconContainer) iconContainer.innerHTML = originalIcon || "<svg viewBox=\"0 0 384 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80v352c0 17.4 9.4 33.4 24.5 41.9S58.2 482 73 473l288-176c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41z\"/></svg>"; if (btn) btn.classList.remove(Config.CLASSES.btnLoading); } }; var handleOpenGallery = async (data, e) => { const btn = e.target.closest(".fc2-enh-btn"); const iconContainer = btn?.querySelector(".fc2-icon"); const originalIcon = iconContainer?.innerHTML || ""; if (iconContainer) iconContainer.innerHTML = IconSpinner; if (btn) btn.classList.add(Config.CLASSES.btnLoading); try { await openExtraGallery(data.id); } finally { if (iconContainer) iconContainer.innerHTML = originalIcon || "<svg viewBox=\"0 0 576 512\" width=\"1.2em\" height=\"1.2em\" fill=\"currentColor\"><path fill=\"currentColor\" d=\"M160 32c-35.3 0-64 28.7-64 64v224c0 35.3 28.7 64 64 64h352c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64zm236 106.7l96 144c4.9 7.4 5.4 16.8 1.2 24.6S480.9 320 472 320H200c-9.2 0-17.6-5.3-21.6-13.6s-2.9-18.2 2.9-25.4l64-80c4.6-5.7 11.4-9 18.7-9s14.2 3.3 18.7 9l17.3 21.6l56-84c4.5-6.6 12-10.6 20-10.6s15.5 4 20 10.7M192 128a32 32 0 1 1 64 0a32 32 0 1 1-64 0m-144-8c0-13.3-10.7-24-24-24S0 106.7 0 120v224c0 75.1 60.9 136 136 136h320c13.3 0 24-10.7 24-24s-10.7-24-24-24H136c-48.6 0-88-39.4-88-88z\"/></svg>"; if (btn) btn.classList.remove(Config.CLASSES.btnLoading); } }; var attachCardPreviewListeners = (card, data) => { card.addEventListener("fc2-play-fullscreen", (e) => { handlePlayFullscreen(data, e); }); card.addEventListener("fc2-open-gallery", (e) => { handleOpenGallery(data, e); }); }; var bindSelectionLogic = (data, card, checkbox, syncSelection) => { const unbindSelection = CoreEvents.on(AppEvents.SELECTION_CHANGED, () => { syncSelection(); }); const unbindMode = resolveService(STATE_SERVICE).on("isSelectionMode", (value) => { card.classList.toggle("fc2-selection-mode-active", !!value); if (!value) syncSelection(); }); checkbox.onchange = () => { getSelectionService().setSelection(data.id, checkbox.checked); }; syncSelection(); card.classList.toggle("fc2-selection-mode-active", !!resolveService(STATE_SERVICE).proxy.isSelectionMode); return () => { unbindSelection(); unbindMode(); }; }; var handleCardUpdate = (change, data, card, options, classes, controls, isPrimaryActionFn, links, primaryLinks, secondaryLinks, statusToggle) => { if (!change) return; const state = ViewStore.get(data.id); if (change.key === "isSearching") { applyCardVisualState(card, { isSearching: state.isSearching }, classes); if (!state.isSearching && !options.skipFilters) GlobalFilterManager.applyCardVisibility(card, state.hasMagnet); } else if (change.key === "hasMagnet" || change.key === "magnetUrl") updateMagnetState({ card, links, primaryLinks, secondaryLinks, state, options, classes, controls, isPrimaryActionFn }); else if (change.key === "status") { applyCardVisualState(card, { isViewed: String(change.value || "") === "watched" }, classes); if (!options.skipFilters) GlobalFilterManager.applyHistoryVisibility(card); statusToggle?.update?.(change); } }; var applyInitialCardState = (card, data, initialState, options, classes) => { applyCardVisualState(card, { hasMagnet: initialState.hasMagnet, isSearching: initialState.isSearching, isViewed: resolveService(HISTORY_SERVICE).has(data.id, "watched"), isCensored: hasCensoredIndicator(data.preservedIconsHTML) }, classes); if (!options.skipFilters) { GlobalFilterManager.applyCardVisibility(card, initialState.hasMagnet); GlobalFilterManager.applyCensoredFilter(card); GlobalFilterManager.applyHistoryVisibility(card); } }; var EnhancedCard = (data, options = {}) => { const classes = Config.CLASSES; const originalId = data.id; data.id = GlobalHistoryManager.normalizeId(originalId); const suppressNativeDetailMeta = Boolean(options.skipFilters); const handleSelectionToggle = (id) => { SelectionService.toggleSelection(id); }; const { card, links, primaryLinks, secondaryLinks, statusToggle, idBadge, portalEntry, galleryEntry, playEntry, selectionOverlay } = createEnhancedCardView({ classes, data, minimal: Boolean(options.minimal), suppressNativeDetailMeta, onCardClick: (event) => { if (resolveService(STATE_SERVICE).proxy.isSelectionMode) { event.preventDefault(); event.stopPropagation(); handleSelectionToggle(data.id); return; } if (!event.target.closest(`.${classes.resourceBtn}`)) GlobalHistoryManager.setWatched(data.id); } }); const checkbox = selectionOverlay.querySelector("input"); attachCardPreviewListeners(card, data); const unbindConfig = CoreEvents.on(AppEvents.CONFIG_CHANGED, ({ key }) => { if (key === "cardPrimaryActions") redistributeCardActions(primaryLinks, links, secondaryLinks, { idBadge, statusToggle, portalEntry, galleryEntry, playEntry }, isPrimaryAction); }); redistributeCardActions(primaryLinks, links, secondaryLinks, { idBadge, statusToggle, portalEntry, galleryEntry, playEntry }, isPrimaryAction); const syncSelection = () => { const isSelected = SelectionService.isSelected(data.id); checkbox.checked = isSelected; card.classList.toggle("fc2-is-selected", isSelected); }; const unbindSelection = bindSelectionLogic(data, card, checkbox, syncSelection); const initialState = ViewStore.get(data.id); applyInitialCardState(card, data, initialState, options, classes); if (initialState.hasMagnet && initialState.magnetUrl) updateMagnetState({ card, links, primaryLinks, secondaryLinks, state: initialState, options, classes, controls: { idBadge, statusToggle, portalEntry, galleryEntry, playEntry }, isPrimaryActionFn: isPrimaryAction }); const destroyHooks = []; const component = { id: data.id, element: card, update: (raw) => { handleCardUpdate(raw, data, card, options, classes, { idBadge, statusToggle, portalEntry, galleryEntry, playEntry }, isPrimaryAction, links, primaryLinks, secondaryLinks, statusToggle); }, addDestroyHook: (fn) => destroyHooks.push(fn), destroy: () => { unbindConfig(); unbindSelection(); statusToggle?.destroy?.(); PreviewLogic.disposePreviewForCard(card); destroyHooks.forEach((hook) => { try { hook(); } catch {} }); } }; ComponentRegistry.register(component); return { finalElement: card, linksContainer: links, primaryLinksContainer: primaryLinks, secondaryLinksContainer: secondaryLinks, newCard: card, component }; }; var UIBuilder = { createEnhancedCard: EnhancedCard, addMagnetButton: MagnetButton, addActressButton: ActressButton, addPlayButton: (container, id, type, previewSlug) => { if (!container || container.querySelector(`.${Config.CLASSES.btnPlayFullscreen}`)) return; const handlePlay = async (e) => { const target = e.currentTarget; if (target.classList.contains(Config.CLASSES.btnLoading)) return; target.classList.add(Config.CLASSES.btnLoading); try { await openVideoPreview(id, type, previewSlug); } finally { target.classList.remove(Config.CLASSES.btnLoading); } }; const btn = Button(IconPlay, t("labelPlayPreview"), void 0, (e) => { handlePlay(e); }, Config.CLASSES.btnPlayFullscreen, { iconOnly: true, size: "micro" }); container.appendChild(btn); }, /** * Detail/toolbar fallback only. List cards use btn-open-gallery from EnhancedCard. * Click always opens extra gallery (never cover progressive video). */ addPreviewButton: (container, id) => { if (!container || container.querySelector(".btn-preview")) return; if ((container.closest(`.${Config.CLASSES.processedCard}`) || container).querySelector(".btn-open-gallery")) return; const handleGallery = async (e) => { const target = e.currentTarget; if (target.classList.contains(Config.CLASSES.btnLoading)) return; target.classList.add(Config.CLASSES.btnLoading); try { await openExtraGallery(id); } finally { target.classList.remove(Config.CLASSES.btnLoading); } }; const btn = Button(IconImages, t("extraPreviewTitle"), void 0, (e) => { handleGallery(e); }, "btn-preview", { iconOnly: true, size: "micro" }); container.appendChild(btn); }, createDetailToolbar: UIToolbar.createDetailToolbar, createElement: (tag) => document.createElement(tag) }; var log$9 = Logger.scope("Reactive"); var ReactiveController = exports("R",class ReactiveController { bindings = /* @__PURE__ */ new Map(); cleanups = []; disposed = false; constructor() { const unbind = resolveService(STATE_SERVICE).on(({ prop, value }) => { if (this.disposed) return; this.bindings.forEach((options, el) => { if (options.key === prop) this.updateElement(el, options, value); }); }); this.cleanups.push(unbind); } get isDisposed() { return this.disposed; } bind(el, options) { if (this.disposed) { log$9.warn("Attempted to bind on a disposed controller"); return; } this.bindings.set(el, options); const currentValue = resolveService(STATE_SERVICE).get(options.key); this.updateElement(el, options, currentValue); const inputEl = el; const eventType = el.tagName === "TEXTAREA" || el.tagName === "INPUT" && [ "text", "password", "url", "email", "number", "range" ].includes(el.type) ? "input" : "change"; const listener = () => { if (this.disposed) return; let newValue; if (options.mode === "checked") newValue = el.checked; else if (options.mode === "toggle") newValue = !resolveService(STATE_SERVICE).get(options.key); else { const raw = inputEl.value; newValue = typeof resolveService(STATE_SERVICE).get(options.key) === "number" ? Number(raw) : raw; } resolveService(STATE_SERVICE).set(options.key, newValue); if (options.onChange) options.onChange(newValue); }; el.addEventListener(eventType, listener); this.cleanups.push(() => el.removeEventListener(eventType, listener)); } updateElement(el, options, value) { const inputEl = el; switch (options.mode) { case "checked": el.checked = !!value; break; case "value": inputEl.value = String(value ?? ""); break; case "text": el.textContent = String(value ?? ""); break; case "html": { const { html, droppedElements, droppedAttributes } = sanitizeHtmlDetailed(String(value ?? "")); if (droppedElements > 0 || droppedAttributes > 0) log$9.warn(`Sanitized unsafe html binding (removed ${droppedElements} element(s), ${droppedAttributes} attribute(s))`); el.innerHTML = html; break; } default: inputEl.value = String(value ?? ""); } } bindList(container, options) { if (this.disposed) return; const render = () => { const list = resolveService(STATE_SERVICE).get(options.key) || []; container.textContent = ""; list.forEach((item, index) => { container.appendChild(options.renderItem(item, index)); }); }; const unbind = resolveService(STATE_SERVICE).on(({ prop }) => { if (this.disposed) return; if (prop === options.key) render(); }); this.cleanups.push(unbind); render(); } listen(key, callback) { if (this.disposed) return; const unbind = resolveService(STATE_SERVICE).on(({ prop, value }) => { if (this.disposed) return; if (prop === key) callback(value); }); this.cleanups.push(unbind); callback(resolveService(STATE_SERVICE).get(key)); } dispose() { if (this.disposed) return; this.disposed = true; this.cleanups.forEach((fn) => fn()); this.cleanups = []; this.bindings.clear(); } static create() { return new ReactiveController(); } }); var log$8 = Logger.scope("CardPipeline"); var markSearching = (ctx) => { ViewStore.set(ctx.id, "isSearching", true); return ctx; }; var buildUI = (ctx) => { const result = UIBuilder.createEnhancedCard({ ...ctx.data, ...ctx.extraUi }); ctx.el = result.finalElement; ViewStore.set(ctx.id, "newCard", result.newCard); return ctx; }; var runPostProcess = (ctx) => { if (ctx.postProcess && ctx.el) ctx.postProcess(ctx.el, ViewStore.get(ctx.id).newCard, Config.CLASSES); return ctx; }; var replaceDom = (ctx) => { if (ctx.el) ctx.host.replaceChildren(ctx.el); return ctx; }; var applyFilters = (ctx) => { const newCard = ViewStore.get(ctx.id).newCard; if (newCard) { if (newCard.classList.contains(Config.CLASSES.isCensored)) ctx.host.classList.add(Config.CLASSES.isCensored); if (newCard.classList.contains(Config.CLASSES.isViewed)) ctx.host.classList.add(Config.CLASSES.isViewed); } const state = ViewStore.get(ctx.id); GlobalFilterManager.applyCardVisibility(ctx.host, state.hasMagnet || !ctx.flags.enableMagnets); GlobalFilterManager.applyCensoredFilter(ctx.host); GlobalFilterManager.applyHistoryVisibility(ctx.host); return ctx; }; var attachGalleryPreview = (ctx) => { return ctx; }; var triggerMagnetLookup = (ctx) => { if (ctx.flags.enableMagnets) queueMagnetLookup(ctx.id, ctx.type, "card-ready"); else handleMagnetResult(ctx.id, null); return ctx; }; var stages = [ markSearching, buildUI, runPostProcess, replaceDom, applyFilters, attachGalleryPreview, triggerMagnetLookup ]; var GlobalCardLazyLoader = new class CardLazyLoader { observer = null; observedElements = /* @__PURE__ */ new WeakSet(); pendingTasks = /* @__PURE__ */ new Map(); static LOAD_DELAY_MS = 150; resolveCardTargets(cardEl) { const C = Config.CLASSES; const processedCard = cardEl.classList.contains(C.processedCard) ? cardEl : cardEl.querySelector(`.${C.processedCard}`); return { hostCard: cardEl.classList.contains(C.processedCard) ? cardEl.parentElement?.closest(`.${C.cardRebuilt}:not(.${C.processedCard})`) : cardEl, processedCard }; } init() { this.observer?.disconnect(); this.observedElements = /* @__PURE__ */ new WeakSet(); this.pendingTasks.forEach((t) => window.clearTimeout(t)); this.pendingTasks.clear(); this.observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { const el = entry.target; if (this.pendingTasks.has(el)) window.clearTimeout(this.pendingTasks.get(el)); if (entry.isIntersecting) { const timeoutId = window.setTimeout(() => { this.pendingTasks.delete(el); this.applyFilters(el); GlobalFilterManager.requestRefresh(); this.observer?.unobserve(el); this.observedElements.delete(el); }, CardLazyLoader.LOAD_DELAY_MS); this.pendingTasks.set(el, timeoutId); } }); }, { rootMargin: "200px" }); } disconnect() { this.observer?.disconnect(); this.observer = null; this.observedElements = /* @__PURE__ */ new WeakSet(); this.pendingTasks.forEach((t) => window.clearTimeout(t)); this.pendingTasks.clear(); } observe(el) { if (this.observedElements.has(el)) return; this.observer?.observe(el); this.observedElements.add(el); } applyFilters(cardEl) { const C = Config.CLASSES; const id = cardEl.dataset.id; const { hostCard, processedCard } = this.resolveCardTargets(cardEl); if (resolveService(STATE_SERVICE).proxy.enableHistory && id) { const isViewed = resolveService(HISTORY_SERVICE).has(id, "watched"); const isDownloaded = resolveService(HISTORY_SERVICE).has(id, "downloaded"); const status = isViewed ? "watched" : isDownloaded ? "downloaded" : "none"; if (ViewStore.get(id).status !== status) ViewStore.set(id, "status", status); hostCard?.classList.toggle(C.isViewed, isViewed); processedCard?.classList.toggle(C.isViewed, isViewed); } GlobalFilterManager.applyHistoryVisibility(hostCard || cardEl); const hasMagnet = hasMagnetButton(processedCard || cardEl) || (processedCard || cardEl).dataset.hasMagnet === "true"; GlobalFilterManager.applyCardVisibility(hostCard || cardEl, hasMagnet); GlobalFilterManager.applyCensoredFilter(hostCard || cardEl); } setupLazyResources(_container, _id, _type, _el) {} }(); var snapshotFlags = (state) => ({ enableMagnets: !!state.proxy.enableMagnets, enableHistory: !!state.proxy.enableHistory }); var runCardPipeline = (payload) => { const flags = snapshotFlags(resolveService(STATE_SERVICE)); const ctx = { id: payload.id, type: payload.type, data: payload.data, host: payload.host, el: null, extraUi: payload.extraUi, flags, postProcess: payload.postProcess }; for (const stage of stages) try { stage(ctx); } catch (error) { log$8.error(`Stage failed for card ${ctx.id}`, error); break; } }; var log$7 = Logger.scope("CardEnhancement"); var CardEnhancementService = class { initialized = false; cleanupFns = []; init() { if (this.initialized) return; this.initialized = true; this.cleanupFns.push(CoreEvents.on(AppEvents.CARD_DISCOVERED, (payload) => { runCardPipeline(payload); })); this.cleanupFns.push(CoreEvents.on(AppEvents.VIEW_REFRESH_REQUESTED, ({ reason }) => { log$7.info(`Refresh requested: ${reason}`); })); } destroy() { if (!this.initialized && this.cleanupFns.length === 0) return; this.initialized = false; this.cleanupFns.splice(0).forEach((cleanup) => cleanup()); } }; /** Constructed once from app/modules (composition root). */ function createCardEnhancementService() { return new CardEnhancementService(); } var getPrimaryActionKeys = (override) => Array.isArray(override) ? override : Array.isArray(resolveService(STATE_SERVICE).proxy.cardPrimaryActions) ? resolveService(STATE_SERVICE).proxy.cardPrimaryActions : [ "id", "viewed", "magnet", "external" ]; var resolveDetailActionContainers = (_primaryLinksContainer, secondaryLinksContainer, primaryActionsOverride) => { const primaryActions = getPrimaryActionKeys(primaryActionsOverride); const visibleActionContainer = _primaryLinksContainer; return { magnetContainer: primaryActions.includes("magnet") ? visibleActionContainer : secondaryLinksContainer, previewContainer: primaryActions.includes("preview") ? visibleActionContainer : secondaryLinksContainer }; }; var createDetailCardMount = (data, options = {}, primaryActionsOverride) => { const { finalElement, linksContainer, primaryLinksContainer, secondaryLinksContainer } = UIBuilder.createEnhancedCard(data, { ...options, skipFilters: true }); finalElement.classList.add("fc2-is-detail"); return { finalElement, linksContainer, primaryLinksContainer, secondaryLinksContainer, ...resolveDetailActionContainers(primaryLinksContainer, secondaryLinksContainer, primaryActionsOverride) }; }; var attachFetchedDetailMagnet = async (id, type, magnetContainer, bindings = {}) => { const fetchMagnet = bindings.fetchMagnet || resolveService(MAGNET_SERVICE).fetchMagnet.bind(resolveService(MAGNET_SERVICE)); const addMagnetButton = bindings.addMagnetButton || UIBuilder.addMagnetButton; const url = await fetchMagnet(id, type); if (!url || !url.startsWith("magnet:?")) return; addMagnetButton(magnetContainer, url); }; var attachDetailPreviewButton = (id, type, previewContainer, bindings = {}) => { const addPreviewButton = bindings.addPreviewButton || UIBuilder.addPreviewButton; const loadExtraPreviews = typeof bindings.loadExtraPreviews === "boolean" ? bindings.loadExtraPreviews : resolveService(STATE_SERVICE).proxy.loadExtraPreviews; if (type === "fc2" && previewContainer.querySelector(".btn-open-gallery")) return; if (type === "fc2" && loadExtraPreviews) addPreviewButton(previewContainer, id); }; var normalizeDetailCardHost = (host, options) => { host.style.display = "block"; host.style.width = options.width ?? "100%"; host.style.padding = "0"; host.style.margin = "0"; host.style.lineHeight = "0"; host.style.background = "transparent"; host.style.height = "auto"; host.style.overflow = "visible"; if (options.maxWidth) host.style.maxWidth = options.maxWidth; if (!options.hideExistingChildren) return; Array.from(host.children).forEach((child) => { if (child instanceof HTMLElement) child.style.display = "none"; }); }; var mountDetailCardEnhancement = (host, data, options = {}) => { const mount = createDetailCardMount(data, options.options, options.primaryActionsOverride); normalizeDetailCardHost(host, options); if (host.tagName === "IMG") host.replaceWith(mount.finalElement); else if (options.hideExistingChildren) host.appendChild(mount.finalElement); else host.replaceChildren(mount.finalElement); return mount; }; var log$6 = Logger.scope("DetailOrchestrator"); var DetailOrchestrator = class { static MOUNT_ATTR = "data-enh-detail-mounted"; static initialized = false; static cleanupFns = []; static init() { if (this.initialized) return; this.initialized = true; this.cleanupFns.push(CoreEvents.on(AppEvents.DETAIL_ENHANCEMENT_REQUESTED, ({ siteName, config, observer }) => { this.enhance(siteName, config, observer); })); } static destroy() { if (!this.initialized && this.cleanupFns.length === 0) return; this.initialized = false; this.cleanupFns.splice(0).forEach((cleanup) => cleanup()); } static async enhance(siteName, config, observer) { try { const anchor = config.anchorElement || (config.anchorSelector ? document.querySelector(config.anchorSelector) : null); if (!anchor) return; const info = config.idExtractor(); if (!info) return; const mountId = `${info.type}:${info.id}`; if (anchor.getAttribute(this.MOUNT_ATTR) === mountId) return; const metadata = config.metadataExtractor(info.id, info.type); const defaultTitle = info.type === "fc2" ? `FC2-PPV-${info.id}` : `${String(info.type).toUpperCase()}-${info.id}`; const cardData = { id: info.id, type: info.type, title: metadata.title || defaultTitle, ...metadata }; let containers; if (config.mode === "card") { const result = mountDetailCardEnhancement(anchor, cardData, config.mountOptions); containers = { mainElement: result.finalElement, magnetContainer: result.magnetContainer, previewContainer: result.previewContainer, linksContainer: result.linksContainer }; } else { const toolbar = UIBuilder.createDetailToolbar({ id: cardData.id, type: cardData.type, title: cardData.title || "", actress: cardData.actress, previewSlug: cardData.previewSlug || void 0 }, UIBuilder.addPreviewButton, UIBuilder.addPlayButton); toolbar.setAttribute("data-enh-site-toolbar", siteName.toLowerCase()); toolbar.setAttribute("data-enh-detail-id", mountId); anchor.after(toolbar); containers = { mainElement: toolbar, magnetContainer: toolbar, previewContainer: toolbar, linksContainer: toolbar.querySelector(`.${Config.CLASSES.infoArea}`) }; } anchor.setAttribute(this.MOUNT_ATTR, mountId); if (!config.skipDefaultServices) this.attachServices(info.id, info.type, containers); if (config.onMounted) { const api = { addActressButton: UIBuilder.addActressButton, addMagnetButton: UIBuilder.addMagnetButton, addPreviewButton: UIBuilder.addPreviewButton, attachFetchedMagnet: (id, type, container) => attachFetchedDetailMagnet(id, type, container), attachDetailPreviewButton: (id, type, container) => attachDetailPreviewButton(id, type, container) }; await config.onMounted(info.id, info.type, containers, api); } if (observer) observer.disconnect(); } catch (e) { log$6.warn("Detail enhancement failed", { siteName, error: e }); } } static attachServices(id, type, containers) { const videoType = type; attachDetailPreviewButton(id, videoType, containers.previewContainer, { loadExtraPreviews: resolveService(STATE_SERVICE).proxy.loadExtraPreviews }); attachFetchedDetailMagnet(id, videoType, containers.magnetContainer); } }; var GlobalClick = { initialized: false, clickHandler: null, init() { if (this.initialized) return; this.initialized = true; this.clickHandler = (e) => { if (!e.target.closest(`.${CSS_CLASSES.enhDropdown}`)) document.querySelectorAll(`.${CSS_CLASSES.enhDropdown}.active`).forEach((d) => { d.classList.remove("active"); const card = d.closest(`.${CSS_CLASSES.processedCard}`) || d.closest(`.${CSS_CLASSES.cardRebuilt}`); if (card) card.classList.remove("fc2-has-active-dropdown"); }); }; document.addEventListener("click", this.clickHandler); }, destroy() { if (this.clickHandler) { document.removeEventListener("click", this.clickHandler); this.clickHandler = null; } this.initialized = false; } }; var SmartTooltips = { currentTooltip: null, hideTimeout: null, initialized: false, mouseOverHandler: null, mouseOutHandler: null, resolveTooltipTarget(target) { if (!(target instanceof HTMLElement)) return null; return target.closest("[data-tooltip], [data-title]"); }, init() { if (this.initialized) return; this.initialized = true; this.mouseOverHandler = (e) => { const target = e.target; if (!(target instanceof Element)) return; const tooltipTarget = this.resolveTooltipTarget(target); const tooltipText = tooltipTarget?.getAttribute("data-tooltip") || tooltipTarget?.getAttribute("data-title"); if (tooltipTarget && tooltipText && this.shouldShowTooltip(tooltipTarget)) this.show(tooltipTarget, tooltipText); }; this.mouseOutHandler = (e) => { const target = e.target; if (!(target instanceof Element)) return; const tooltipTarget = this.resolveTooltipTarget(target); if (!tooltipTarget) return; const related = e.relatedTarget instanceof Element ? this.resolveTooltipTarget(e.relatedTarget) : null; if (related && related === tooltipTarget) return; this.hide(); }; document.addEventListener("mouseover", this.mouseOverHandler, true); document.addEventListener("mouseout", this.mouseOutHandler, true); }, shouldShowTooltip(element) { if ("ontouchstart" in window) return false; if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") return false; if (element.querySelector(".tooltip")) return false; return true; }, show(element, text) { this.hide(true); const tooltip = h("div", { className: "smart-tooltip", style: { position: "fixed", background: "var(--fc2-surface-float)", color: "var(--fc2-text-bright)", padding: "var(--fc2-space-xs) var(--fc2-space-md)", borderRadius: "var(--fc2-radius-md)", fontSize: "var(--fc2-text-xs)", zIndex: "100000", pointerEvents: "none", whiteSpace: "normal", maxWidth: "min(300px, calc(100vw - var(--fc2-space-md)))", overflowWrap: "anywhere", wordBreak: "break-word", lineHeight: "var(--fc2-line-base)", backdropFilter: "blur(var(--fc2-blur))", boxShadow: "var(--fc2-shadow-md)", border: "var(--fc2-border-width) solid var(--fc2-border)", opacity: "0", willChange: "transform, opacity", transition: "opacity var(--fc2-motion-fast) var(--fc2-ease-standard), transform var(--fc2-motion-fast) var(--fc2-ease-standard)", transform: "translateY(calc(var(--fc2-space-xs) + var(--fc2-border-width)))" } }, text); UIHost.add(tooltip); const rect = element.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect(); let top = rect.bottom + 8; let left = rect.left + rect.width / 2 - tooltipRect.width / 2; if (left < 8) left = 8; if (left + tooltipRect.width > window.innerWidth - 8) left = window.innerWidth - tooltipRect.width - 8; if (top + tooltipRect.height > window.innerHeight - 8) top = rect.top - tooltipRect.height - 8; if (top < 8) top = 8; tooltip.style.top = `${top}px`; tooltip.style.left = `${left}px`; requestAnimationFrame(() => { tooltip.style.opacity = "1"; }); this.currentTooltip = tooltip; }, hide(immediate = false) { const tooltip = this.currentTooltip; if (!tooltip) return; this.currentTooltip = null; if (this.hideTimeout !== null) { window.clearTimeout(this.hideTimeout); this.hideTimeout = null; } if (immediate) { tooltip.remove(); return; } tooltip.style.opacity = "0"; this.hideTimeout = window.setTimeout(() => { tooltip.remove(); this.hideTimeout = null; }, 200); }, destroy() { this.hide(true); if (this.hideTimeout !== null) { window.clearTimeout(this.hideTimeout); this.hideTimeout = null; } if (this.mouseOverHandler) { document.removeEventListener("mouseover", this.mouseOverHandler, true); this.mouseOverHandler = null; } if (this.mouseOutHandler) { document.removeEventListener("mouseout", this.mouseOutHandler, true); this.mouseOutHandler = null; } this.initialized = false; } }; var log$5 = Logger.scope("UIManager"); var UIManager = class { cleanupFns = []; visibilityTimer = null; onInit() { this.onCleanup(); log$5.debug("Initializing UI interactions"); SmartTooltips.init(); GlobalClick.init(); resolveService(CARD_ENHANCEMENT_SERVICE).init?.(); DetailOrchestrator.init(); GlobalCardLazyLoader.init(); PreviewLogic.registerContainer(document.body); this.bindStateListeners(); this.bindEventListeners(); this.updateGlobalClasses(); this.applyAccentColor(); this.updateVisibilityForCards(); } scanAndTriggerInitialSearches(options = {}) { if (!resolveService(STATE_SERVICE).proxy.enableMagnets) return; const { includeSettledNoMagnet = false } = options; const C = Config.CLASSES; document.querySelectorAll(`.${C.cardRebuilt}`).forEach((card) => { const processed = card.querySelector(`.${C.processedCard}`); if (!processed) return; const { id, type } = processed.dataset; const hasMagnet = processed.dataset.hasMagnet === "true"; const isSearching = processed.dataset.enhSearching === "true"; if (id && type && (includeSettledNoMagnet ? !hasMagnet : isSearching && !hasMagnet)) queueMagnetLookup(id, type, includeSettledNoMagnet ? "enable-magnets" : "site-ready-scan"); }); } onCleanup() { GlobalCardLazyLoader.disconnect(); if (this.visibilityTimer) { clearTimeout(this.visibilityTimer); this.visibilityTimer = null; } this.cleanupFns.splice(0).forEach((cleanup) => { cleanup(); }); resolveService(CARD_ENHANCEMENT_SERVICE).destroy?.(); DetailOrchestrator.destroy(); SmartTooltips.destroy(); GlobalClick.destroy(); } bindStateListeners() { this.cleanupFns.push(resolveService(STATE_SERVICE).on("showViewedBtn", () => this.updateGlobalClasses())); this.cleanupFns.push(resolveService(STATE_SERVICE).on("showIdBadge", () => this.updateGlobalClasses())); this.cleanupFns.push(resolveService(STATE_SERVICE).on("cardDensity", () => this.updateGlobalClasses())); this.cleanupFns.push(resolveService(STATE_SERVICE).on("accentColor", () => this.applyAccentColor())); this.cleanupFns.push(resolveService(STATE_SERVICE).on("enableMagnets", (value) => { this.updateVisibilityForCards(); if (value) this.scanAndTriggerInitialSearches({ includeSettledNoMagnet: true }); })); [ "hideViewed", "hideNoMagnet", "hideCensored" ].forEach((prop) => { this.cleanupFns.push(resolveService(STATE_SERVICE).on(prop, () => this.updateVisibilityForCards())); }); } bindEventListeners() { this.cleanupFns.push(CoreEvents.on(AppEvents.MAGNET_FOUND, (data) => handleMagnetResult(data.id, data.url))); this.cleanupFns.push(CoreEvents.on(AppEvents.MAGNET_FAILED, (data) => handleMagnetResult(data.id, null))); this.cleanupFns.push(CoreEvents.on(AppEvents.HISTORY_ADDED, (data) => this.syncUIWithHistory(data.id, data.status))); this.cleanupFns.push(CoreEvents.on(AppEvents.HISTORY_REMOVED, (data) => this.syncUIWithHistory(data.id, "none"))); this.cleanupFns.push(CoreEvents.on(AppEvents.HISTORY_LOADED, () => this.updateVisibilityForCards())); this.cleanupFns.push(CoreEvents.on(AppEvents.SITE_READY, () => { GlobalCardLazyLoader.init(); this.updateVisibilityForCards(); this.scanAndTriggerInitialSearches(); })); } updateGlobalClasses() { const isShowViewed = resolveService(STATE_SERVICE).proxy.showViewedBtn !== false; const isShowId = resolveService(STATE_SERVICE).proxy.showIdBadge !== false; document.body.classList.toggle("fc2-hide-viewed-btn", !isShowViewed); document.body.classList.toggle("fc2-hide-id-badge", !isShowId); const density = resolveService(STATE_SERVICE).proxy.cardDensity; document.body.classList.toggle("fc2-card-density-cover-only", density === "cover-only"); document.body.classList.toggle("fc2-card-density-compact", density === "compact"); document.body.classList.toggle("fc2-card-density-standard", density === "standard"); document.body.classList.toggle("fc2-card-density-immersive", density === "immersive"); } applyAccentColor() { const color = resolveService(STATE_SERVICE).proxy.accentColor || "#ffffff"; const rgb = hexToRgb(color); document.documentElement.style.setProperty("--fc2-primary", color); document.documentElement.style.setProperty("--fc2-primary-rgb", rgb); } updateVisibilityForCards() { if (this.visibilityTimer) clearTimeout(this.visibilityTimer); this.visibilityTimer = setTimeout(() => { this.visibilityTimer = null; const C = Config.CLASSES; document.querySelectorAll(`.${C.cardRebuilt}`).forEach((card) => { const cardEl = card; GlobalCardLazyLoader.observe(cardEl); GlobalCardLazyLoader.applyFilters(cardEl); }); GlobalFilterManager.requestRefresh(); }, 50); } syncUIWithHistory(id, status) { ViewStore.set(IdNormalizer.normalize(id), "status", status); GlobalFilterManager.requestRefresh(); } }; var UIManagerInstance = new UIManager(); var DragManager = class { container; trigger; onDragEndCallback; documentCleanup = []; currentConfig = { anchorX: "right", x: 20, anchorY: "bottom", y: 40 }; isDragging = false; rafId = null; snapTimer = null; hasMoved = false; startX = 0; startY = 0; startLeft = 0; startTop = 0; constructor(container, trigger, onDragEndCallback) { this.container = container; this.trigger = trigger; this.onDragEndCallback = onDragEndCallback; this.init(); } init() { const savedConfig = typeof GM_getValue !== "undefined" ? GM_getValue(STORAGE_KEYS.FAB_POSITION) : null; if (savedConfig) try { this.applyAnchor(JSON.parse(savedConfig)); } catch { this.applyAnchor(this.currentConfig); } else this.applyAnchor(this.currentConfig); this.trigger.addEventListener("mousedown", this.onDragStart); this.trigger.addEventListener("touchstart", this.onDragStart, { passive: false }); document.addEventListener("mousemove", this.onDragMove, { passive: false }); document.addEventListener("touchmove", this.onDragMove, { passive: false }); document.addEventListener("mouseup", this.onDragEnd); document.addEventListener("touchend", this.onDragEnd); this.documentCleanup.push(() => document.removeEventListener("mousemove", this.onDragMove)); this.documentCleanup.push(() => document.removeEventListener("touchmove", this.onDragMove)); this.documentCleanup.push(() => document.removeEventListener("mouseup", this.onDragEnd)); this.documentCleanup.push(() => document.removeEventListener("touchend", this.onDragEnd)); const onResize = () => this.applyAnchor(this.currentConfig); window.addEventListener("resize", onResize); this.documentCleanup.push(() => window.removeEventListener("resize", onResize)); } applyAnchor(config) { this.currentConfig = config; this.container.style.transition = "none"; this.container.style.transform = "translate3d(0,0,0)"; this.container.style.left = config.anchorX === "left" ? `${config.x}px` : "auto"; this.container.style.right = config.anchorX === "right" ? `${config.x}px` : "auto"; this.container.style.top = config.anchorY === "top" ? `${config.y}px` : "auto"; this.container.style.bottom = config.anchorY === "bottom" ? `${config.y}px` : "auto"; } getClientPos(e) { if ("touches" in e && e.touches.length > 0) { const touch = e.touches[0]; return touch ? { x: touch.clientX, y: touch.clientY } : { x: 0, y: 0 }; } const mEvent = e; return { x: mEvent.clientX, y: mEvent.clientY }; } onDragStart = (e) => { if ("button" in e && e.button !== 0) return; this.isDragging = true; this.hasMoved = false; const pos = this.getClientPos(e); this.startX = pos.x; this.startY = pos.y; const rect = this.container.getBoundingClientRect(); this.startLeft = rect.left; this.startTop = rect.top; this.container.style.transition = "none"; this.container.style.right = "auto"; this.container.style.bottom = "auto"; this.container.style.left = `${this.startLeft}px`; this.container.style.top = `${this.startTop}px`; this.container.style.transform = "translate3d(0,0,0)"; e.stopPropagation(); }; onDragMove = (e) => { if (!this.isDragging) return; if (e.cancelable) e.preventDefault(); const pos = this.getClientPos(e); const dx = pos.x - this.startX; const dy = pos.y - this.startY; if (Math.abs(dx) > 5 || Math.abs(dy) > 5) this.hasMoved = true; this.rafId = requestAnimationFrame(() => { this.rafId = null; if (!this.isDragging) return; this.container.style.transform = `translate3d(${dx}px, ${dy}px, 0)`; }); }; onDragEnd = () => { if (!this.isDragging) return; this.isDragging = false; const rect = this.container.getBoundingClientRect(); this.container.style.transform = "translate3d(0,0,0)"; if (this.hasMoved) { const isRight = rect.left + rect.width / 2 > window.innerWidth / 2; const isBottom = rect.top + rect.height / 2 > window.innerHeight / 2; const targetMargin = 20; const targetTopPx = Math.min(window.innerHeight - rect.height - targetMargin, Math.max(targetMargin, rect.top)); const targetLeftPx = isRight ? window.innerWidth - rect.width - targetMargin : targetMargin; this.container.style.transition = "all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)"; this.container.style.left = `${targetLeftPx}px`; this.container.style.top = `${targetTopPx}px`; const config = { anchorX: isRight ? "right" : "left", x: targetMargin, anchorY: isBottom ? "bottom" : "top", y: isBottom ? window.innerHeight - targetTopPx - rect.height : targetTopPx }; if (typeof GM_setValue !== "undefined") GM_setValue(STORAGE_KEYS.FAB_POSITION, JSON.stringify(config)); this.snapTimer = setTimeout(() => { this.snapTimer = null; if (!this.isDragging) this.applyAnchor(config); }, 300); if (this.onDragEndCallback) this.onDragEndCallback(); } else this.applyAnchor(this.currentConfig); }; destroy() { this.isDragging = false; if (this.rafId !== null) { cancelAnimationFrame(this.rafId); this.rafId = null; } if (this.snapTimer !== null) { clearTimeout(this.snapTimer); this.snapTimer = null; } this.documentCleanup.forEach((fn) => fn()); this.documentCleanup = []; this.trigger.removeEventListener("mousedown", this.onDragStart); this.trigger.removeEventListener("touchstart", this.onDragStart); } }; var log$4 = Logger.scope("QuickBar"); var QUICKBAR_TOGGLE_PROPS = [ "hideViewed", "hideNoMagnet", "hideCensored", "isSelectionMode" ]; var QUICKBAR_SYNC_SUCCESS_DOT_DURATION_MS = TIMING.TOAST_DEFAULT_DURATION; var isQuickBarToggleProp = (prop) => QUICKBAR_TOGGLE_PROPS.includes(prop); var getQuickBarSuccessDotRemainingMs = (syncStatus, lastSyncResult, now = Date.now()) => { if (syncStatus !== SYNC_STATUS.SUCCESS || !lastSyncResult?.completedAt) return 0; const completedAt = Date.parse(lastSyncResult.completedAt); if (Number.isNaN(completedAt)) return 0; return Math.max(0, QUICKBAR_SYNC_SUCCESS_DOT_DURATION_MS - (now - completedAt)); }; var shouldShowQuickBarSyncDot = (syncMode, syncStatus, lastSyncResult, now = Date.now()) => { if (syncMode === "none" || syncStatus === SYNC_STATUS.IDLE) return false; if (syncStatus === SYNC_STATUS.SUCCESS) return getQuickBarSuccessDotRemainingMs(syncStatus, lastSyncResult, now) > 0; return true; }; var formatQuickBarFilterTitle = (baseTitle, count) => count > 0 ? `${baseTitle} (${count})` : baseTitle; var applyQuickBarFilterCountUpdate = (actions, counts) => { QUICKBAR_TOGGLE_PROPS.forEach((prop) => { if (prop === "isSelectionMode") return; const button = actions.querySelector(`[data-prop="${prop}"]`); if (!button) return; const nextTitle = formatQuickBarFilterTitle(button.getAttribute("data-base-title") || button.getAttribute("data-title") || "", Number(counts[prop] || 0)); button.setAttribute("data-title", nextTitle); button.setAttribute("aria-label", nextTitle); button.setAttribute("title", nextTitle); }); }; var readQuickBarFilterCountsFromBody = () => ({ hideViewed: Number(document.body.dataset.fc2HideViewedCount || 0), hideNoMagnet: Number(document.body.dataset.fc2HideNoMagnetCount || 0), hideCensored: Number(document.body.dataset.fc2HideCensoredCount || 0) }); var applyQuickBarStateChange = (elements, change, state) => { const dot = elements.trigger.querySelector(".fc2-sync-dot"); if (dot) { const nextSyncMode = change.prop === "syncMode" ? String(change.value) : state.syncMode; const nextSyncStatus = change.prop === "syncStatus" ? String(change.value) : state.syncStatus; const nextSyncResult = change.prop === "lastSyncResult" ? change.value ?? null : state.lastSyncResult; if (change.prop === "syncStatus") dot.className = `fc2-sync-dot ${String(change.value)}`; if (change.prop === "syncStatus" || change.prop === "syncMode" || change.prop === "lastSyncResult") dot.style.display = shouldShowQuickBarSyncDot(nextSyncMode, nextSyncStatus, nextSyncResult) ? "block" : "none"; } if (change.prop === "syncStatus") elements.syncButton.classList.toggle("active", change.value === "syncing"); if (isQuickBarToggleProp(change.prop)) { const button = elements.actions.querySelector(`[data-prop="${change.prop}"]`); if (button) button.classList.toggle("active", !!change.value); } }; var createQuickBarButton = (iconSvg, title, prop, onClick) => { const appState = resolveService(STATE_SERVICE).proxy; return h("button", { className: `fc2-fab-btn ${prop && appState[prop] ? "active" : ""}`, "data-title": title, "data-base-title": title, title, ...prop ? { "data-prop": prop } : {}, onclick: (event) => { event.preventDefault(); event.stopPropagation(); if (prop) { appState[prop] = !appState[prop]; return; } onClick?.(); } }, UIUtils.icon(iconSvg)); }; var createQuickBarView = () => { const container = h("div", { className: "fc2-fab-container" }); const actions = h("div", { className: "fc2-fab-actions" }); const syncButton = createQuickBarButton(IconRotate, t("labelSyncing"), null, () => { if (resolveService(STATE_SERVICE).proxy.syncMode === "none") { Toast.show(t("syncModeNone"), "info"); SettingsPanel.show("data"); return; } if (resolveService(STATE_SERVICE).proxy.syncStatus === "syncing") { Toast.show(t("labelSyncing"), "info"); return; } resolveService(SYNC_SERVICE).performSync(true).catch(() => void 0); }); actions.append(syncButton, createQuickBarButton(IconEyeSlash, t("optionHideViewed"), "hideViewed"), createQuickBarButton(IconMagnet, t("optionHideNoMagnet"), "hideNoMagnet"), createQuickBarButton(IconBan, t("optionHideCensored"), "hideCensored"), createQuickBarButton(IconListUl, t("labelSelectionMode"), "isSelectionMode"), createQuickBarButton(IconGear, t("tabSettings"), null, () => SettingsPanel.show()), createQuickBarButton(IconArrowUp, t("btnBackToTop"), null, () => window.scrollTo({ top: 0, behavior: "smooth" }))); const trigger = h("button", { type: "button", className: "fc2-fab-trigger", "aria-label": t("btnMoreOptions") }, UIUtils.icon(IconPlus), h("div", { className: `fc2-sync-dot ${resolveService(STATE_SERVICE).proxy.syncStatus}`, style: { display: shouldShowQuickBarSyncDot(resolveService(STATE_SERVICE).proxy.syncMode, resolveService(STATE_SERVICE).proxy.syncStatus, resolveService(STATE_SERVICE).proxy.lastSyncResult) ? "block" : "none" } })); applyQuickBarFilterCountUpdate(actions, readQuickBarFilterCountsFromBody()); container.append(actions, trigger); return { container, actions, trigger, syncButton }; }; var QuickBarService = class { container = null; hostWrapper = null; documentCleanup = []; serviceCleanup = []; syncDotHideTimer = null; onInit() { log$4.debug("Initializing QuickBar"); this.cleanupServiceListeners(); this.render(); this.bindGlobalEvents(); } onCleanup() { this.cleanupDocumentListeners(); this.cleanupServiceListeners(); this.unmount(); } bindGlobalEvents() { this.serviceCleanup.push(CoreEvents.on(AppEvents.LANGUAGE_CHANGED, () => { resolveService(MENU_SERVICE).register(); this.render(); })); this.serviceCleanup.push(CoreEvents.on(AppEvents.PANEL_OPENED, () => { if (this.container) this.container.style.display = "none"; })); this.serviceCleanup.push(CoreEvents.on(AppEvents.PANEL_CLOSED, () => { if (this.container) this.container.style.display = ""; })); } cleanupDocumentListeners() { this.clearSyncDotHideTimer(); this.documentCleanup.forEach((fn) => fn()); this.documentCleanup = []; } clearSyncDotHideTimer() { if (this.syncDotHideTimer) { clearTimeout(this.syncDotHideTimer); this.syncDotHideTimer = null; } } syncDotDisplayState() { return shouldShowQuickBarSyncDot(resolveService(STATE_SERVICE).proxy.syncMode, resolveService(STATE_SERVICE).proxy.syncStatus, resolveService(STATE_SERVICE).proxy.lastSyncResult) ? "block" : "none"; } scheduleSyncDotAutoHide(trigger) { this.clearSyncDotHideTimer(); const remainingMs = getQuickBarSuccessDotRemainingMs(resolveService(STATE_SERVICE).proxy.syncStatus, resolveService(STATE_SERVICE).proxy.lastSyncResult); if (remainingMs <= 0) return; this.syncDotHideTimer = setTimeout(() => { const dot = trigger.querySelector(".fc2-sync-dot"); if (dot) dot.style.display = this.syncDotDisplayState(); this.syncDotHideTimer = null; }, remainingMs); } bindStateUpdates(trigger, syncButton, actions) { const unsubscribe = resolveService(STATE_SERVICE).on((change) => { applyQuickBarStateChange({ trigger, syncButton, actions }, { prop: String(change.prop), value: change.value }, { syncMode: resolveService(STATE_SERVICE).proxy.syncMode, syncStatus: resolveService(STATE_SERVICE).proxy.syncStatus, lastSyncResult: resolveService(STATE_SERVICE).proxy.lastSyncResult }); if (change.prop === "syncStatus" || change.prop === "syncMode" || change.prop === "lastSyncResult") this.scheduleSyncDotAutoHide(trigger); }); this.documentCleanup.push(unsubscribe); const filterUnsubscribe = CoreEvents.on(AppEvents.FILTER_COUNTS_CHANGED, (counts) => { applyQuickBarFilterCountUpdate(actions, counts); }); this.documentCleanup.push(filterUnsubscribe); applyQuickBarFilterCountUpdate(actions, readQuickBarFilterCountsFromBody()); this.scheduleSyncDotAutoHide(trigger); } cleanupServiceListeners() { this.serviceCleanup.forEach((fn) => fn()); this.serviceCleanup = []; } mount(container) { this.container = container; this.hostWrapper = h("div", { className: "fc2-quickbar-host" }, container); UIHost.add(this.hostWrapper); } unmount() { this.container?.remove(); this.hostWrapper?.remove(); this.container = null; this.hostWrapper = null; } render() { this.cleanupDocumentListeners(); this.unmount(); if (!resolveService(STATE_SERVICE).proxy.enableQuickBar) return; const { container, actions, trigger, syncButton } = createQuickBarView(); this.bindStateUpdates(trigger, syncButton, actions); this.initDraggable(container, trigger, actions); this.mount(container); } initDraggable(container, trigger, actions) { const dragManager = new DragManager(container, trigger); trigger.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); if (!dragManager.hasMoved) { const visible = actions.classList.toggle("visible"); trigger.classList.toggle("active", visible); } }); const closeFAB = (event) => { if (!event.composedPath().includes(container) && actions.classList.contains("visible")) { actions.classList.remove("visible"); trigger.classList.remove("active"); } }; document.addEventListener("click", closeFAB, true); this.documentCleanup.push(() => { document.removeEventListener("click", closeFAB, true); dragManager.destroy(); }); } }; var QuickBar = new QuickBarService(); /** * Explicit module registration — composition root wiring. * Services are constructed here via create*() + deps, then registered by token. * UI bridges are wired from app/lifecycle after registration. */ var log$3 = Logger.scope("Modules"); var registered = false; /** Register tokens only — no UI side-effects. */ function registerModules() { if (registered) { log$3.debug("Modules already registered"); return; } registered = true; registerAllSites(registerSiteConfig); const messaging = createMessagingService(); const state = createStateService({ messaging }); setLocaleReader(() => state.get("language")); const data = Repository; const history = createHistoryService({ data, state, messaging }); const cleanup = createCleanupService({ data }); const scraper = createScraperService(); const magnet = createMagnetService({ data, scraper }); const preview = createPreviewService(); const portal = createPortalService(); const backup = createBackupService({ data, state }); const sync = createSyncService({ data, state }); const menu = createMenuService(); const site = createSiteService(); const grid = createGridManager(); const style = createStyleManager(); const webdav = createWebDAVProvider({ data, state, history }); const supabase = createSupabaseProvider({ data, state, history }); const cardEnhancement = createCardEnhancementService(); configureProviderRegistry({ state, webdav, supabase }); AppContainer.registerToken(DATA_SERVICE, data); AppContainer.registerToken(STATE_SERVICE, state); AppContainer.registerToken(MESSAGING_SERVICE, messaging); AppContainer.registerToken(STYLE_SERVICE, style); AppContainer.registerToken(GRID_SERVICE, grid); AppContainer.registerToken(SITE_SERVICE, site); AppContainer.registerToken(HISTORY_SERVICE, history); AppContainer.registerToken(TIMELINE_SERVICE, history); AppContainer.registerToken(CLEANUP_SERVICE, cleanup); AppContainer.registerToken(SCRAPER_SERVICE, scraper); AppContainer.registerToken(MAGNET_SERVICE, magnet); AppContainer.registerToken(PREVIEW_SERVICE, preview); AppContainer.registerToken(PORTAL_SERVICE, portal); AppContainer.registerToken(BACKUP_SERVICE, backup); AppContainer.registerToken(SYNC_SERVICE, sync); AppContainer.registerToken(MENU_SERVICE, menu); AppContainer.registerToken(WEBDAV_PROVIDER, webdav); AppContainer.registerToken(SUPABASE_PROVIDER, supabase); AppContainer.registerToken(CARD_ENHANCEMENT_SERVICE, cardEnhancement); AppContainer.registerToken(UI_MANAGER, UIManagerInstance); AppContainer.registerToken(SELECTION_SERVICE, SelectionService); AppContainer.registerToken(QUICKBAR_SERVICE, QuickBar); configureSitePorts({ fetchActressFromFD2: (id) => scraper.fetchActressFromFD2(id), resetFD2Backoff: () => scraper.resetFD2Backoff(), addToHistory: (id) => history.add(id).catch((error) => { log$3.error("SitePorts addToHistory failed", error); }), cacheSet: (key, value) => { data.cache.set(key, value).catch((error) => log$3.error("SitePorts cacheSet failed", error)); } }); log$3.info("All modules registered"); } /** * Application bootstrap — composition root entry used by main.ts */ var log$2 = Logger.scope("Bootstrap"); async function bootstrapApp() { registerModules(); wireUiBridges(); if (typeof window !== "undefined") window.addEventListener("pagehide", () => revokeAllMediaBlobUrls(), { once: true }); Logger.group("Main", `${SCRIPT_INFO.NAME} bootstrap`); try { await AppContainer.bootstrap(); CoreEvents.emit(AppEvents.UI_READY, {}); log$2.info("System initialized"); } finally { Logger.groupEnd(); } } var log$1 = Logger.scope("ErrorBoundary"); var ERROR_LOG_KEY = "fc2_error_log"; var MAX_ERROR_ENTRIES = 30; /** * Record an error to the persistent error log. * This provides visibility into background failures that users can't see via Toast. */ var recordError = (module, error) => { try { const entry = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), module, message: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack?.split("\n").slice(0, 5).join("\n") : void 0 }; const existing = getErrorLog(); existing.unshift(entry); if (existing.length > MAX_ERROR_ENTRIES) existing.length = MAX_ERROR_ENTRIES; Storage.set(ERROR_LOG_KEY, existing); } catch {} }; /** * Get the persistent error log (most recent first). */ var getErrorLog = () => { try { return Storage.get(ERROR_LOG_KEY, []) || []; } catch { return []; } }; /** * Install global error handlers to capture unhandled errors. * Call this once during app initialization. */ var installGlobalErrorHandlers = () => { window.addEventListener("unhandledrejection", (event) => { recordError("unhandled-promise", event.reason); log$1.error("Unhandled promise rejection", event.reason); }); window.addEventListener("error", (event) => { recordError("uncaught-error", event.error || event.message); log$1.error("Uncaught error", event.error || event.message); }); }; var log = Logger.scope("Main"); installGlobalErrorHandlers(); var isCloudflareChallenge = () => document.title.includes("Just a moment...") || !!document.querySelector("#challenge-running") || !!document.querySelector("iframe[src*=\"challenges.cloudflare.com\"]"); var main = async () => { Logger.init(); if (isCloudflareChallenge()) { log.warn("Cloudflare challenge detected, skipping initialization"); return; } try { await bootstrapApp(); } catch (error) { log.error("Bootstrap failed", error); } }; if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", () => { main(); }); else main();})}})); System.register("./DataTab-bfpDUmRQ-C_bNm5Gc.js", ['./___monkey.entry.js','./components-1yyvCgmd-C1dNzRD5.js','./TabLifecycle-BwHTz_iJ-BnPxZNF9.js','dexie'],(function(exports){'use strict';var consumePendingSettingsNavigationIntent,resolveService,STATE_SERVICE,h,t,CoreEvents,AppEvents,UIUtils,IconFileImport,IconFileExport,IconSliders,IconPlay,DATA_SERVICE,Logger,Toast,HISTORY_SERVICE,SYNC_SERVICE,IconLink,BACKUP_SERVICE,formatBackupImportDiff,TIMING,STORAGE_KEYS,SCRIPT_INFO,IconDatabase,IconServer,IconRotate,IconVial,SCRAPER_SERVICE,getSyncCursorStorageKey,Card,StatItem,Button,FormRow,Input,PasswordInput,Select,TabLifecycle;return{setters:[function(module){consumePendingSettingsNavigationIntent=module.c;resolveService=module.r;STATE_SERVICE=module.S;h=module.h;t=module.t;CoreEvents=module.C;AppEvents=module.A;UIUtils=module.U;IconFileImport=module.I;IconFileExport=module.a;IconSliders=module.b;IconPlay=module.d;DATA_SERVICE=module.D;Logger=module.L;Toast=module.T;HISTORY_SERVICE=module.H;SYNC_SERVICE=module.e;IconLink=module.f;BACKUP_SERVICE=module.B;formatBackupImportDiff=module.g;TIMING=module.i;STORAGE_KEYS=module.j;SCRIPT_INFO=module.k;IconDatabase=module.l;IconServer=module.m;IconRotate=module.n;IconVial=module.o;SCRAPER_SERVICE=module.p;getSyncCursorStorageKey=module.q;},function(module){Card=module.C;StatItem=module.S;Button=module.B;FormRow=module.F;Input=module.I;PasswordInput=module.P;Select=module.a;},function(module){TabLifecycle=module.T;},null],execute:(function(){var capitalize = (value) => value ? `${value.charAt(0).toUpperCase()}${value.slice(1)}` : value; var formatDataTabLastSync = (rawValue, neverLabel, formatter = (value) => new Date(value).toLocaleString()) => { if (rawValue === null || rawValue === void 0 || rawValue === "" || rawValue === neverLabel) return neverLabel; if (typeof rawValue === "number") return Number.isFinite(rawValue) ? formatter(rawValue) : neverLabel; const parsedTime = Date.parse(rawValue); if (Number.isNaN(parsedTime)) return rawValue; return formatter(rawValue); }; var buildDataTabSyncViewModel = (syncMode, syncStatus, translate, lastSyncDisplay, syncResult) => { const modeLabel = syncMode === "webdav" ? translate("syncModeWebDAV") : syncMode === "supabase" ? translate("syncModeSupabase") : translate("syncModeNone"); const resultSummary = syncResult?.summary ? ` · ${syncResult.summary}` : ""; if (syncMode === "none") return { modeLabel, statusLabel: modeLabel, detail: `${translate("labelLastSync")}: ${lastSyncDisplay}${resultSummary}`, tone: "idle" }; return { modeLabel, statusLabel: capitalize(syncStatus), detail: `${translate("labelLastSync")}: ${lastSyncDisplay}${resultSummary}`, tone: syncStatus }; }; var buildSyncResultPanelViewModel = (syncResult, neverLabel, translate, formatter = (value) => new Date(value).toLocaleString()) => { if (!syncResult) return { title: translate("syncLatestTitle"), summary: translate("syncLatestEmpty"), completedAt: neverLabel, conflictText: "", conflictStats: [] }; const conflictStats = syncResult.conflict ? [ translate("syncConflictLocalNewer", { count: syncResult.conflict.localNewer }), translate("syncConflictRemoteNewer", { count: syncResult.conflict.remoteNewer }), translate("syncConflictMerged", { count: syncResult.conflict.merged }) ] : []; const detailText = syncResult.errorMessage || syncResult.conflict?.explanation || ""; return { title: translate("syncLatestTitle"), summary: syncResult.detailSummary || syncResult.summary, completedAt: syncResult.completedAt ? formatDataTabLastSync(syncResult.completedAt, neverLabel, formatter) : neverLabel, conflictText: detailText, conflictStats }; }; var buildBackupDiffLines = (diff, translate) => { const lines = [ translate("backupSettingsAdded", { count: diff.settingsAdded.length }), translate("backupSettingsChanged", { count: diff.settingsChanged.length }), translate("backupHistoryRows", { count: diff.historyToImport }) ]; if (diff.settingsChanged.length > 0) lines.push(translate("backupChangedKeys", { keys: diff.settingsChanged.join(", ") })); return lines; }; /** * Data tab storage section — backup, cache/history reset, debug env. */ var BACKUP_IMPORT_ERROR_TRANSLATION_KEYS = { invalid_json: "backupImportErrorInvalidJson", invalid_payload: "backupImportErrorInvalidPayload", unsupported_version: "backupImportErrorUnsupportedVersion", read_failed: "backupImportErrorReadFailed", apply_failed: "backupImportErrorApplyFailed" }; var buildStoragePreviewEntries = (textValue, tags = []) => { const lines = textValue.split(/\r?\n/).filter(Boolean); return [...tags, ...lines]; }; var shouldConfirmDestructiveAction = (confirmDialog, confirmResetMessage, confirmDestructiveMessage) => confirmDialog(confirmResetMessage) && confirmDialog(confirmDestructiveMessage); var buildDebugEnvPayload = (input) => JSON.stringify({ version: input.version, ua: input.ua, url: input.url, syncMode: input.syncMode, debugMode: input.debugMode }, null, 2); var getBackupImportErrorTranslationKey = (errorCode) => BACKUP_IMPORT_ERROR_TRANSLATION_KEYS[errorCode]; var buildBackupImportErrorMessage = (errorCode, translate) => translate(getBackupImportErrorTranslationKey(errorCode)); var createBackupFilePicker = () => new Promise((resolve) => { const input = h("input", { type: "file", accept: ".json" }); input.onchange = () => resolve(input.files?.[0] || null); input.click(); }); var showImportError = (message) => { Toast.show(message || t("alertImportError"), "error"); }; var createDestructiveHandler = (action, successMessage, render, confirmDialog) => async () => { if (!shouldConfirmDestructiveAction(confirmDialog, t("confirmResetDatabase"), t("confirmDestructiveAction"))) return; await action(); Toast.show(successMessage, "success"); render("data"); }; var createImportHandler = ({ setPreview, pickBackupFile, confirmDialog, reloadPage }) => async () => { const file = await pickBackupFile(); if (!file) return; const previewResult = await resolveService(BACKUP_SERVICE).previewImportDetailed(file); if (!previewResult.ok || !previewResult.preview) { const errorMessage = buildBackupImportErrorMessage(previewResult.errorCode ?? "invalid_payload", t); setPreview(errorMessage, [t("labelError")]); Toast.show(errorMessage, "error"); return; } const preview = previewResult.preview; const tags = buildBackupDiffLines(preview.diff, t); const previewText = formatBackupImportDiff(preview.diff, t); setPreview(previewText, tags); if (!confirmDialog(`${t("backupImportConfirm")} ${previewText}`)) return; const importResult = await resolveService(BACKUP_SERVICE).importDataDetailed(file); if (!importResult.success) { const errorMessage = buildBackupImportErrorMessage(importResult.errorCode ?? "apply_failed", t); setPreview(errorMessage, [t("labelError")]); showImportError(errorMessage); return; } Toast.show(t("alertImportSuccess"), "success"); setTimeout(() => reloadPage(), TIMING.RELOAD_DELAY_NORMAL); }; var createExportHandler = () => async () => { if (!await resolveService(BACKUP_SERVICE).exportData()) { showImportError(); return; } Toast.show(t("backupExportSuccessPlain"), "success"); }; var createStorageSectionActions = ({ render, setPreview, pickBackupFile, confirmDialog, reloadPage, copyText }) => ({ handleExport: createExportHandler(), handleImport: createImportHandler({ setPreview, pickBackupFile, confirmDialog, reloadPage }), handleClearCache: createDestructiveHandler(() => resolveService(DATA_SERVICE).cache.clear(), t("alertCacheCleared"), render, confirmDialog), handleClearHistory: createDestructiveHandler(() => resolveService(HISTORY_SERVICE).clear(), t("alertHistoryCleared"), render, confirmDialog), handleToggleDebugMode: () => { resolveService(STATE_SERVICE).proxy.debugMode = !resolveService(STATE_SERVICE).proxy.debugMode; Toast.show(resolveService(STATE_SERVICE).proxy.debugMode ? t("alertDebugOn") : t("alertDebugOff"), "info"); render("data"); }, handleCopyEnv: () => { copyText(buildDebugEnvPayload({ version: SCRIPT_INFO.VERSION, ua: navigator.userAgent, url: location.href, syncMode: resolveService(STATE_SERVICE).proxy.syncMode, debugMode: resolveService(STATE_SERVICE).proxy.debugMode })); Toast.show(t("alertLogsCopied"), "success"); } }); var buildStorageSection = (render) => { const previewLines = h("div", { className: "fc2-backup-preview-lines" }); const setPreview = (textValue, tags = []) => { previewLines.textContent = ""; const entries = buildStoragePreviewEntries(textValue, tags); if (entries.length === 0) { previewLines.classList.add("fc2-is-hidden"); return; } previewLines.classList.remove("fc2-is-hidden"); entries.forEach((entry) => { previewLines.appendChild(h("div", { className: "fc2-backup-preview-line" }, entry)); }); }; const actions = createStorageSectionActions({ render, setPreview, pickBackupFile: createBackupFilePicker, confirmDialog: (message) => confirm(message), reloadPage: () => location.reload(), copyText: (text) => { navigator.clipboard.writeText(text); } }); setPreview(""); return Card({ title: t("groupDataManagement"), icon: IconDatabase, children: [ h("div", { className: "fc2-grid-actions" }, Button({ text: t("btnClearCache"), className: "danger ghost", onClick: actions.handleClearCache }), Button({ text: t("btnClearHistory"), className: "danger ghost", onClick: actions.handleClearHistory }), Button({ text: t("btnExportData"), icon: UIUtils.icon(IconFileExport), onClick: actions.handleExport }), Button({ text: t("btnImportData"), icon: UIUtils.icon(IconFileImport), onClick: actions.handleImport })), h("div", { className: "fc2-sync-result-panel" }, previewLines), FormRow({ label: t("labelDebugMode"), children: h("div", { className: "fc2-input-group" }, Button({ text: resolveService(STATE_SERVICE).proxy.debugMode ? t("statusDebugOn") : t("statusDebugOff"), className: resolveService(STATE_SERVICE).proxy.debugMode ? "primary" : "", onClick: actions.handleToggleDebugMode }), Button({ text: t("btnCopyEnv"), icon: UIUtils.icon(IconSliders), onClick: actions.handleCopyEnv })) }) ] }); }; var toErrorMessage = (error) => { let message = error instanceof Error ? error.message : String(error); if (error && typeof error === "object" && "response" in error && typeof error.response === "string") try { const parsed = JSON.parse(error.response); if (parsed.message) message = `${parsed.message}${parsed.hint ? ` (${parsed.hint})` : ""}`; } catch { return message; } return message; }; var getSyncProviderDiagnostic = (mode, syncResult) => { if (!syncResult || syncResult.mode !== mode) return ""; if (syncResult.status === "error") return syncResult.errorMessage || syncResult.summary; if (syncResult.status === "conflict") return syncResult.conflict?.explanation || syncResult.summary; return ""; }; var confirmDestructiveSyncAction = (mode) => { return confirm(t(mode === "push" ? "alertPushAllQuery" : "alertPullAllQuery")); }; var runDataTabSyncAction = async (action) => { try { await action(); } catch (error) { Toast.show(`${t("labelError") || "Error"}: ${toErrorMessage(error)}`, "error", { duration: 1e4 }); } }; var createSyncStatusSummary = (provider, lifecycle, getLastSyncDisplay) => { const lastSyncText = h("p", { className: "fc2-dim" }, `${t("labelLastSync")}: ${getLastSyncDisplay()}`); const diagnosticText = h("p", { className: "fc2-dim" }); const refreshStatus = () => { lastSyncText.textContent = `${t("labelLastSync")}: ${getLastSyncDisplay()}`; diagnosticText.textContent = getSyncProviderDiagnostic(provider, resolveService(STATE_SERVICE).proxy.lastSyncResult); diagnosticText.classList.toggle("fc2-is-hidden", !diagnosticText.textContent); }; lifecycle.addCleanup(resolveService(STATE_SERVICE).on("syncStatus", refreshStatus)); lifecycle.addCleanup(resolveService(STATE_SERVICE).on("lastSyncResult", refreshStatus)); refreshStatus(); return { lastSyncText, diagnosticText, refreshStatus }; }; /** * Data tab view builders — storage, sync config, WebDAV, Supabase. */ var createProviderActionRow = (actions) => h("div", { className: "fc2-card-actions" }, ...actions); var createSyncStatusSection = ({ lastSyncText, diagnosticText, actions }) => h("div", { className: "fc2-auth-section" }, lastSyncText, diagnosticText, createProviderActionRow(actions)); var createStandardSyncButtons = ({ syncLabel, forcePushLabel, forcePullLabel, onSync, onForcePush, onForcePull, confirmDestructive }) => { const buttons = [Button({ text: syncLabel, className: "primary", onClick: onSync }), Button({ text: forcePushLabel, className: "danger ghost", onClick: () => { if (!confirmDestructive("push")) return; onForcePush(); } })]; if (forcePullLabel && onForcePull) buttons.push(Button({ text: forcePullLabel, className: "danger ghost", onClick: () => { if (!confirmDestructive("pull")) return; onForcePull(); } })); return buttons; }; var createSyncStatusElements = (render) => { const statusPill = h("span", { className: "fc2-sync-status-pill idle" }, t("syncModeNone")); const statusTitle = h("span", { className: "fc2-sync-summary-title" }, t("syncModeNone")); const statusDetail = h("span", { className: "fc2-sync-summary-detail" }, `${t("labelLastSync")}: ${t("labelNever")}`); const resultSummary = h("div", { className: "fc2-sync-result-summary" }, t("syncLatestEmpty")); const resultMeta = h("div", { className: "fc2-label-dim" }, `${t("labelLastSync")}: ${t("labelNever")}`); const conflictHint = h("div", { className: "fc2-label-dim" }, t("syncNoConflictYet")); const conflictStats = h("div", { className: "fc2-sync-conflicts-list" }); return { statusPill, statusTitle, statusDetail, resultSummary, resultMeta, conflictHint, conflictStats, element: h("div", { className: "fc2-sync-result-panel" }, h("div", { className: "fc2-sync-summary" }, statusPill, h("div", { className: "fc2-sync-summary-copy" }, statusTitle, statusDetail)), h("div", { className: "fc2-sync-result-body" }, resultSummary, resultMeta, conflictHint, conflictStats), h("div", { className: "fc2-portal-actions" }, Button({ text: t("syncActionDebugLogs"), className: "ghost", onClick: () => render("debug") }))) }; }; var createSummaryRefresher = (ui, getLastSyncDisplay) => { return () => { const viewModel = buildDataTabSyncViewModel(resolveService(STATE_SERVICE).proxy.syncMode, resolveService(STATE_SERVICE).proxy.syncStatus, t, getLastSyncDisplay(), resolveService(STATE_SERVICE).proxy.lastSyncResult); const resultView = buildSyncResultPanelViewModel(resolveService(STATE_SERVICE).proxy.lastSyncResult, t("labelNever"), t); ui.statusPill.className = `fc2-sync-status-pill ${viewModel.tone}`; ui.statusPill.textContent = viewModel.statusLabel; ui.statusTitle.textContent = viewModel.modeLabel; ui.statusDetail.textContent = viewModel.detail; ui.resultSummary.textContent = resultView.summary; ui.resultMeta.textContent = `${t("syncCompletedAt")}: ${resultView.completedAt}`; ui.conflictHint.textContent = resultView.conflictText || t("syncNoConflictYet"); ui.conflictStats.textContent = ""; resultView.conflictStats.forEach((line) => { ui.conflictStats.appendChild(h("div", { className: "fc2-label-dim" }, line)); }); }; }; var buildSyncModeRow = (ctrl, render) => FormRow({ label: t("labelSyncMode"), children: Select({ id: "syncMode", options: [ { value: "none", label: t("syncModeNone") }, { value: "webdav", label: t("syncModeWebDAV") }, { value: "supabase", label: t("syncModeSupabase") } ], controller: ctrl, binding: "syncMode", onChange: () => void render("data") }) }); var buildSyncIntervalRow = (ctrl) => FormRow({ label: t("labelSyncInterval"), children: Select({ id: "syncInterval", options: [ { value: "0", label: t("syncInterval0") }, { value: "2", label: t("syncInterval2") }, { value: "5", label: t("syncInterval5") }, { value: "10", label: t("syncInterval10") }, { value: "30", label: t("syncInterval30") }, { value: "-1", label: t("syncIntervalManual") } ], controller: ctrl, binding: "syncInterval" }) }); var buildSyncSettingsRows = (ctrl, render) => { const rows = [buildSyncModeRow(ctrl, render)]; if (resolveService(STATE_SERVICE).proxy.syncMode !== "none") rows.push(buildSyncIntervalRow(ctrl)); return rows; }; var buildSyncConfigSection = (ctrl, render, lifecycle, getLastSyncDisplay) => { const ui = createSyncStatusElements(render); const refresh = createSummaryRefresher(ui, getLastSyncDisplay); lifecycle.addCleanup(resolveService(STATE_SERVICE).on("syncStatus", refresh)); lifecycle.addCleanup(resolveService(STATE_SERVICE).on("syncMode", refresh)); lifecycle.addCleanup(resolveService(STATE_SERVICE).on("lastSyncResult", refresh)); const children = [ui.element, ...buildSyncSettingsRows(ctrl, render)]; refresh(); return Card({ title: t("syncStatus"), icon: IconLink, children }); }; var log = Logger.scope("DataTabWebDAV"); var buildWebDAVSection = (ctrl, lifecycle, getLastSyncDisplay) => { const { lastSyncText, diagnosticText } = createSyncStatusSummary("webdav", lifecycle, getLastSyncDisplay); return Card({ title: t("groupWebDAV"), icon: IconServer, children: [ FormRow({ label: t("labelWebDAVUrl"), children: Input({ id: "webdavUrl", type: "url", controller: ctrl, binding: "webdavUrl", placeholder: "https://..." }) }), FormRow({ label: t("labelWebDAVUser"), children: Input({ id: "webdavUser", type: "text", controller: ctrl, binding: "webdavUser" }) }), FormRow({ label: t("labelWebDAVPass"), children: PasswordInput({ id: "webdavPass", controller: ctrl, binding: "webdavPass" }) }), createSyncStatusSection({ lastSyncText, diagnosticText, actions: [Button({ text: t("btnWebDAVTest"), onClick: async () => { try { await resolveService(SYNC_SERVICE).testWebDAV(); Toast.show(t("alertWebDAVSuccess"), "success"); } catch (e) { log.error("WebDAV test failed", e); Toast.show(t("alertWebDAVError"), "error"); } } }), ...createStandardSyncButtons({ syncLabel: t("btnWebDAVSync"), forcePushLabel: t("btnForceSync"), onSync: () => void runDataTabSyncAction(() => resolveService(SYNC_SERVICE).performSync(true)), onForcePush: () => void runDataTabSyncAction(() => resolveService(SYNC_SERVICE).forceFullSync(true)), confirmDestructive: confirmDestructiveSyncAction })] }) ] }); }; var handleSupabaseConnectAndSync = async (render) => { Logger.debug("DataTab", "Connect and Sync clicked", { url: resolveService(STATE_SERVICE).proxy.supabaseUrl, key: !!resolveService(STATE_SERVICE).proxy.supabaseKey, hasEmail: !!resolveService(STATE_SERVICE).proxy.supabaseEmail, hasPass: !!resolveService(STATE_SERVICE).proxy.supabasePassword }); try { if (!resolveService(STATE_SERVICE).proxy.supabaseUrl || !resolveService(STATE_SERVICE).proxy.supabaseKey) throw new Error(t("alertSbUrlRequired") || "Missing Supabase URL or Key"); if (resolveService(STATE_SERVICE).proxy.supabaseEmail && resolveService(STATE_SERVICE).proxy.supabasePassword) { Logger.debug("DataTab", "Attempting login..."); await resolveService(SYNC_SERVICE).login(resolveService(STATE_SERVICE).proxy.supabaseEmail, resolveService(STATE_SERVICE).proxy.supabasePassword); Toast.show(t("alertSyncAccountConnected") || "Account connected", "success"); render("data"); } await resolveService(SYNC_SERVICE).performSync(true); } catch (error) { Logger.error("DataTab", "Action failed", error); Toast.show(`${t("labelError") || "Error"}: ${toErrorMessage(error)}`, "error", { duration: 1e4 }); } }; var isLoggedIn = () => { return !!GM_getValue(STORAGE_KEYS.SYNC_USER_ID); }; var createSupabaseActionButtons = (render) => { if (isLoggedIn()) return [...createStandardSyncButtons({ syncLabel: t("btnSyncNow"), forcePushLabel: t("btnForceSync") || "Force Push", forcePullLabel: t("btnPullSync") || "Force Pull", onSync: () => void runDataTabSyncAction(() => resolveService(SYNC_SERVICE).performSync(true)), onForcePush: () => void runDataTabSyncAction(() => resolveService(SYNC_SERVICE).forceFullSync(true)), onForcePull: () => void runDataTabSyncAction(() => resolveService(SYNC_SERVICE).forcePullSync(true)), confirmDestructive: confirmDestructiveSyncAction }), Button({ text: t("btnLogout"), className: "danger", onClick: async () => { await resolveService(SYNC_SERVICE).logout(); render("data"); } })]; return [Button({ text: t("btnConnectAndSync"), className: "primary", onClick: () => handleSupabaseConnectAndSync(render) })]; }; var buildSupabaseSection = (ctrl, render, lifecycle, getLastSyncDisplay) => { const { lastSyncText, diagnosticText } = createSyncStatusSummary("supabase", lifecycle, getLastSyncDisplay); return Card({ title: t("labelSupabaseSync"), icon: IconRotate, children: [ FormRow({ label: t("labelSupabaseUrl"), children: Input({ id: "supabaseUrl", type: "url", controller: ctrl, binding: "supabaseUrl" }) }), FormRow({ label: t("labelSupabaseKey"), children: PasswordInput({ id: "supabaseKey", controller: ctrl, binding: "supabaseKey" }) }), FormRow({ label: t("labelAuthEmail") || "Email", children: Input({ id: "supabaseEmail", type: "email", controller: ctrl, binding: "supabaseEmail" }) }), FormRow({ label: t("labelAuthPass"), children: PasswordInput({ id: "supabasePassword", controller: ctrl, binding: "supabasePassword" }) }), createSyncStatusSection({ lastSyncText, diagnosticText, actions: createSupabaseActionButtons(render) }) ] }); }; var lifecycle = new TabLifecycle(); var getLastSyncDisplay = () => { return formatDataTabLastSync(typeof GM_getValue !== "undefined" ? GM_getValue(getSyncCursorStorageKey(), t("labelNever")) : t("labelNever"), t("labelNever"), (value) => new Date(value).toLocaleString()); }; var buildDataOverviewSection = (lc) => { const cacheValue = h("span", {}, t("labelLoading")); const historyValue = h("span", {}, t("labelLoading")); let disposed = false; const loadCounts = async () => { try { const [cacheCount, historyCount] = await Promise.all([resolveService(DATA_SERVICE).db.cache.count(), resolveService(DATA_SERVICE).db.history.where("is_deleted").equals(0).count()]); if (disposed) return; cacheValue.textContent = String(cacheCount); historyValue.textContent = String(historyCount); } catch (error) { Logger.error("DataTab", "Failed to load overview counts", error); if (disposed) return; cacheValue.textContent = "--"; historyValue.textContent = "--"; } }; lc.addCleanup(() => { disposed = true; }); lc.addCleanup(CoreEvents.on(AppEvents.COLLECTION_UPDATED, () => void loadCounts())); lc.addCleanup(CoreEvents.on(AppEvents.HISTORY_CHANGED, () => void loadCounts())); loadCounts(); return Card({ title: t("tabStatistics"), icon: IconServer, children: [h("div", { className: "fc2-settings-card-grid fc2-data-overview-grid" }, StatItem({ label: t("labelCacheManagement"), value: cacheValue }), StatItem({ label: t("labelHistoryManagement"), value: historyValue }))] }); }; var buildSystemDiagnosisSection = () => { const statusMap = {}; const targets = [ { id: "sukebei", label: t("scraperTargetSukebei"), url: "https://sukebei.nyaa.si" }, { id: "fd2", label: t("scraperTargetFd2"), url: "https://fd2ppv.cc" }, { id: "paipancon", label: t("scraperTargetPaipancon"), url: "https://paipancon.com" } ]; const createStatusRow = (label, id, url) => { const statusEl = h("span", { className: "fc2-status-indicator pending" }, t("scraperStatusPending") || "待测试"); statusMap[id] = statusEl; return h("div", { className: "fc2-scraper-row" }, h("div", { className: "fc2-scraper-info" }, h("span", { className: "fc2-scraper-label" }, label), h("a", { href: url, target: "_blank", rel: "noopener noreferrer", className: "fc2-scraper-url micro-link" }, UIUtils.icon(IconLink))), statusEl); }; const runTest = async () => { for (const target of targets) { const el = statusMap[target.id]; if (!el) continue; el.className = "fc2-status-indicator loading"; el.textContent = t("scraperStatusLoading") || "测试中..."; const result = await resolveService(SCRAPER_SERVICE).testHealth(target.id); el.className = `fc2-status-indicator ${result}`; if (result === "ok") el.textContent = t("scraperStatusOk") || "连接成功"; else if (result === "cf") el.textContent = t("scraperStatusCF") || "Cloudflare 拦截"; else if (result === "429") el.textContent = t("scraperStatus429") || "请求过于频繁"; else el.textContent = t("scraperStatusError") || "连接失败"; } }; return Card({ title: t("groupScraperDiagnosis"), icon: IconVial, children: [h("div", { className: "fc2-scraper-list" }, ...targets.map((target) => createStatusRow(target.label, target.id, target.url))), h("div", { className: "fc2-card-actions" }, Button({ text: t("btnTestScrapers") || "开始连通性诊断", icon: UIUtils.icon(IconPlay), onClick: runTest }))] }); }; var renderDataTab = exports("renderDataTab",(_shadow, render) => { const ctrl = lifecycle.reset(); const navigationIntent = consumePendingSettingsNavigationIntent("data"); const prioritizeSync = navigationIntent?.presetKey === "sync-center"; const overviewSection = buildDataOverviewSection(lifecycle); const storageSection = buildStorageSection(render); const syncConfigSection = buildSyncConfigSection(ctrl, render, lifecycle, getLastSyncDisplay); const providerSections = [resolveService(STATE_SERVICE).proxy.syncMode === "webdav" ? buildWebDAVSection(ctrl, lifecycle, getLastSyncDisplay) : null, resolveService(STATE_SERVICE).proxy.syncMode === "supabase" ? buildSupabaseSection(ctrl, render, lifecycle, getLastSyncDisplay) : null]; const sections = prioritizeSync ? [ syncConfigSection, overviewSection, storageSection, buildSystemDiagnosisSection(), ...providerSections ] : [ overviewSection, storageSection, buildSystemDiagnosisSection(), syncConfigSection, ...providerSections ]; return h("div", { className: "fc2-data-container", "data-intent-focus": navigationIntent?.presetKey || "" }, ...sections.filter((section) => section !== null)); }); var onUnmount = exports("onUnmount",() => { lifecycle.dispose(); });})}})); System.register("./SettingsTab-8ebeRFDG-BDxX9kp7.js", ['./___monkey.entry.js','./components-1yyvCgmd-C1dNzRD5.js','./TabLifecycle-BwHTz_iJ-BnPxZNF9.js','dexie'],(function(exports){'use strict';var consumePendingSettingsNavigationIntent,resolveSettingsFocusGroupsFromIntent,resolveService,PORTAL_SERVICE,h,STATE_SERVICE,t,IconPalette,IconFilter,IconClockRotateLeft,CoreEvents,AppEvents,IconGear,Card,Button,CheckboxRow,FormRow,ColorPicker,Select,TabLifecycle;return{setters:[function(module){consumePendingSettingsNavigationIntent=module.c;resolveSettingsFocusGroupsFromIntent=module.s;resolveService=module.r;PORTAL_SERVICE=module.P;h=module.h;STATE_SERVICE=module.S;t=module.t;IconPalette=module.u;IconFilter=module.v;IconClockRotateLeft=module.w;CoreEvents=module.C;AppEvents=module.A;IconGear=module.x;},function(module){Card=module.C;Button=module.B;CheckboxRow=module.b;FormRow=module.F;ColorPicker=module.c;Select=module.a;},function(module){TabLifecycle=module.T;},null],execute:(function(){var SETTINGS_GROUP_KEYS = [ "appearance", "filters", "enhancements", "card-actions", "portals" ]; var getCollapsedSettingsGroups = (collapsedGroups) => SETTINGS_GROUP_KEYS.filter((groupKey) => collapsedGroups.includes(groupKey)); var applySettingsCollapsedGroups = (grid, collapsedGroups) => { const collapsedSet = new Set(getCollapsedSettingsGroups(collapsedGroups)); grid.querySelectorAll("[data-settings-group]").forEach((card) => { const groupKey = card.getAttribute("data-settings-group"); if (!groupKey) return; const isCollapsed = collapsedSet.has(groupKey); card.classList.toggle("is-collapsed", isCollapsed); const toggle = card.querySelector(".fc2-card-collapse-toggle"); toggle?.classList.toggle("is-collapsed", isCollapsed); toggle?.setAttribute("aria-expanded", String(!isCollapsed)); }); }; var applySettingsGroupSpotlight = (grid, groupKeys) => { const spotlightGroups = new Set(groupKeys); grid.querySelectorAll("[data-settings-group]").forEach((card) => { const groupKey = card.getAttribute("data-settings-group"); card.classList.toggle("is-home-spotlight", !!groupKey && spotlightGroups.has(groupKey)); }); }; var getAllPortalIds = (portals) => portals.map((portal) => portal.id); var syncPortalGridState = (portalGrid, enabled) => { portalGrid.querySelectorAll(".fc2-portal-item").forEach((element) => { const portalId = element.getAttribute("data-portal-id"); if (!portalId) return; const isActive = enabled.includes(portalId); element.classList.toggle("active", isActive); const input = element.querySelector("input"); if (input instanceof HTMLInputElement) input.checked = isActive; }); }; var previewModeOptions = [{ value: "static", label: t("previewModeStatic") }, { value: "hover", label: t("previewModeHover") }]; var cardDensityOptions = [ { value: "cover-only", label: t("cardDensityCoverOnly") }, { value: "compact", label: t("cardDensityCompact") }, { value: "standard", label: t("cardDensityStandard") }, { value: "immersive", label: t("cardDensityImmersive") } ]; var accentColorOptions = [ { value: "#ffffff", label: t("accentColorDefault") }, { value: "#cba6f7", label: t("accentColorPurple") }, { value: "#f5c2e7", label: t("accentColorPink") }, { value: "#89b4fa", label: t("accentColorBlue") }, { value: "#a6e3a1", label: t("accentColorGreen") }, { value: "#fab387", label: t("accentColorOrange") }, { value: "#f9e2af", label: t("accentColorGold") } ]; var languageOptions = [ { value: "auto", label: t("langAuto") }, { value: "zh-CN", label: t("langZh") }, { value: "en-US", label: t("langEn") }, { value: "ja-JP", label: t("langJa") } ]; var gridColumnOptions = [ 0, 1, 2, 3, 4, 5, 6 ].map((value) => ({ value: String(value), label: value === 0 ? t("labelDefault") : String(value) })); var DEFAULT_CARD_PRIMARY_ACTIONS = [ "id", "viewed", "magnet", "external" ]; var getCardPrimaryActions = () => { const value = resolveService(STATE_SERVICE).proxy.cardPrimaryActions; return Array.isArray(value) ? value : DEFAULT_CARD_PRIMARY_ACTIONS; }; var toggleCardPrimaryAction = (action, checked) => { const current = getCardPrimaryActions(); const next = checked ? Array.from(/* @__PURE__ */ new Set([...current, action])) : current.filter((item) => item !== action); resolveService(STATE_SERVICE).proxy.cardPrimaryActions = [...next]; }; var mainGroups = [ { title: t("groupAppearance"), icon: IconPalette, key: "appearance", fields: [ { type: "select", key: "previewMode", label: t("labelPreviewMode"), binding: "previewMode", options: previewModeOptions }, { type: "select", key: "gridColumns", label: t("labelGridColumns"), binding: "userGridColumns", options: gridColumnOptions, onChange: (value) => CoreEvents.emit(AppEvents.GRID_CHANGED, Number(value)) }, { type: "select", key: "cardDensity", label: t("labelCardDensity"), binding: "cardDensity", options: cardDensityOptions }, { type: "color", key: "accentColor", label: t("labelAccentColor"), binding: "accentColor", options: accentColorOptions }, { type: "select", key: "language", label: t("labelLanguage"), binding: "language", options: languageOptions } ] }, { title: t("groupFilters"), icon: IconFilter, key: "filters", fields: [ { type: "checkbox", key: "hideNoMagnet", label: t("optionHideNoMagnet"), binding: "hideNoMagnet" }, { type: "checkbox", key: "hideCensored", label: t("optionHideCensored"), binding: "hideCensored" }, { type: "checkbox", key: "hideViewed", label: t("optionHideViewed"), binding: "hideViewed" } ] }, { title: t("groupDataHistory"), icon: IconClockRotateLeft, key: "enhancements", fields: [ { type: "checkbox", key: "enableMagnets", label: t("optionEnableMagnets"), binding: "enableMagnets" }, { type: "checkbox", key: "enableExternalLinks", label: t("optionEnableExternalLinks"), binding: "enableExternalLinks" }, { type: "checkbox", key: "enableQuickBar", label: t("optionEnableQuickBar"), binding: "enableQuickBar" }, { type: "checkbox", key: "enableHistory", label: t("optionEnableHistory"), binding: "enableHistory" }, { type: "advanced", fields: [ { type: "checkbox", key: "loadExtraPreviews", label: t("optionLoadExtraPreviews"), binding: "loadExtraPreviews" }, { type: "checkbox", key: "showViewedBtn", label: t("optionShowViewedBtn"), binding: "showViewedBtn" }, { type: "checkbox", key: "showIdBadge", label: t("optionShowIdBadge"), binding: "showIdBadge" }, { type: "checkbox", key: "supjavSortByViews", label: t("optionSupjavSortByViews"), binding: "supjavSortByViews" } ] } ] } ]; var createPortalGridItems = (portals, onSelectionChange) => portals.map((portal) => { const enabled = (resolveService(STATE_SERVICE).proxy.enabledPortals || []).includes(portal.id); return h("label", { className: `fc2-portal-item ${enabled ? "active" : ""}`, "data-portal-id": portal.id }, h("input", { type: "checkbox", checked: enabled, onchange: (e) => { const checked = e.target.checked; onSelectionChange(getAllPortalIds(portals).filter((id) => id === portal.id ? checked : (resolveService(STATE_SERVICE).proxy.enabledPortals || []).includes(id))); } }), h("span", {}, portal.name)); }); var renderSimpleField = (field, ctrl) => { switch (field.type) { case "checkbox": return CheckboxRow({ id: field.key, label: field.label, controller: ctrl, binding: field.binding }); case "select": return FormRow({ label: field.label, children: Select({ id: field.key, options: field.options, controller: ctrl, binding: field.binding, onChange: field.onChange }) }); case "color": return FormRow({ label: field.label, children: ColorPicker({ id: field.key, options: field.options, controller: ctrl, binding: field.binding }) }); case "hint": return h("div", { className: "fc2-enh-hint dim" }, field.text); } }; var renderCardActions = (ctrl) => { const rows = [ { key: "id", label: t("cardActionId") }, { key: "viewed", label: t("cardActionViewed") }, { key: "magnet", label: t("cardActionMagnet") }, { key: "play", label: t("cardActionPlay") }, { key: "preview", label: t("cardActionPreview") }, { key: "external", label: t("cardActionExternal") } ].map(({ key, label }) => { const row = CheckboxRow({ id: `card-action-${key}`, label, checked: getCardPrimaryActions().includes(key), onChange: (checked) => toggleCardPrimaryAction(key, checked) }); const input = row.querySelector("input"); ctrl.listen("cardPrimaryActions", (value) => { if (input) input.checked = Array.isArray(value) && value.includes(key); }); return row; }); return h("div", { className: "fc2-settings-card-grid" }, ...rows); }; var renderField = (field, ctrl) => { if (field.type === "advanced") return h("div", { className: "fc2-settings-advanced-block" }, h("div", { className: "fc2-settings-advanced-label" }, t("settingsAdvancedSection")), h("div", { className: "fc2-settings-card-grid" }, ...field.fields.map((f) => renderSimpleField(f, ctrl)))); if (field.type === "card-actions") return renderCardActions(ctrl); return renderSimpleField(field, ctrl); }; var renderGroup = (config, ctrl) => Card({ title: config.title, icon: config.icon, dataGroupKey: config.key, children: config.fields.map((f) => renderField(f, ctrl)) }); var renderSideGroup = (config, ctrl) => Card({ title: config.title, icon: config.icon, className: config.className, dataGroupKey: config.key, children: config.fields.map((f) => { if (f.type === "portals") { const grid = h("div", { className: "fc2-portal-grid" }, ...createPortalGridItems(f.portals, f.onSelectionChange)); ctrl.listen("enabledPortals", (value) => syncPortalGridState(grid, value || [])); return [h("div", { className: "fc2-portal-actions" }, Button({ text: t("btnSelectAll"), className: "ghost micro", onClick: () => f.onSelectionChange(getAllPortalIds(f.portals)) }), Button({ text: t("btnDeselectAll"), className: "ghost micro", onClick: () => f.onSelectionChange([]) })), grid]; } if (f.type === "card-actions") return [h("div", { className: "fc2-enh-hint dim" }, t("cardActionDirectHint")), renderCardActions(ctrl)]; return renderSimpleField(f, ctrl); }).flat() }); var createSettingsTabGrid = (ctrl, portals, onPortalSelectionChange) => { const sideGroup = { title: t("groupCardActions"), icon: IconGear, className: "is-advanced", fields: [ { type: "hint", text: t("cardActionDirectHint") }, { type: "card-actions" }, { type: "portals", portalGrid: h("div"), portals, onSelectionChange: onPortalSelectionChange } ] }; return h("div", { className: "fc2-settings-grid" }, h("div", { className: "fc2-settings-grid-main" }, ...mainGroups.map((g) => renderGroup(g, ctrl))), h("div", { className: "fc2-settings-grid-side" }, renderSideGroup(sideGroup, ctrl))); }; var lifecycle = new TabLifecycle(); var updateEnabledPortals = (enabledPortalIds) => { resolveService(STATE_SERVICE).proxy.enabledPortals = enabledPortalIds; resolveService(PORTAL_SERVICE).clearCache(); }; var renderSettingsTab = exports("renderSettingsTab",() => { const ctrl = lifecycle.reset(); const navigationIntent = consumePendingSettingsNavigationIntent("settings"); const spotlightGroups = resolveSettingsFocusGroupsFromIntent(navigationIntent); const grid = createSettingsTabGrid(ctrl, resolveService(PORTAL_SERVICE).getAllPortals(), updateEnabledPortals); const root = h("div", { className: `fc2-settings-tab is-advanced-visible`.trim(), "data-intent-focus": navigationIntent?.presetKey || "" }, grid); if (spotlightGroups.length > 0) resolveService(STATE_SERVICE).proxy.settingsCollapsedGroups = []; applySettingsCollapsedGroups(grid, resolveService(STATE_SERVICE).proxy.settingsCollapsedGroups || []); applySettingsGroupSpotlight(grid, spotlightGroups); return root; }); var onUnmount = exports("onUnmount",() => { lifecycle.dispose(); });})}})); System.register("./TabLifecycle-BwHTz_iJ-BnPxZNF9.js", ['./___monkey.entry.js'],(function(exports){'use strict';var ReactiveController;return{setters:[function(module){ReactiveController=module.R;}],execute:(function(){var TabLifecycle = exports("T",class { controller = null; cleanups = []; reset() { this.dispose(); this.controller = ReactiveController.create(); return this.controller; } get ctrl() { if (!this.controller) throw new Error("TabLifecycle: reset() must be called before accessing ctrl"); return this.controller; } addCleanup(fn) { this.cleanups.push(fn); } dispose() { if (this.controller) { this.controller.dispose(); this.controller = null; } this.cleanups.forEach((fn) => { try { fn(); } catch {} }); this.cleanups = []; } });})}})); System.register("./DebugTab-BvxslCZw-BzciC75p.js", ['./___monkey.entry.js','./components-1yyvCgmd-C1dNzRD5.js','dexie'],(function(exports){'use strict';var consumePendingSettingsNavigationIntent,h,t,DOM_IDS,UIUtils,IconFileExport,LogLevel,exportLogHistory,IconDatabase,Toast,Logger,SCRIPT_INFO,resolveService,STATE_SERVICE,Button,Checkbox;return{setters:[function(module){consumePendingSettingsNavigationIntent=module.c;h=module.h;t=module.t;DOM_IDS=module.y;UIUtils=module.U;IconFileExport=module.a;LogLevel=module.z;exportLogHistory=module.E;IconDatabase=module.l;Toast=module.T;Logger=module.L;SCRIPT_INFO=module.k;resolveService=module.r;STATE_SERVICE=module.S;},function(module){Button=module.B;Checkbox=module.d;},null],execute:(function(){var createEnvironmentSnapshot = () => ({ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "unknown", language: typeof navigator !== "undefined" ? navigator.language : "unknown", url: typeof location !== "undefined" ? location.href : "unknown", timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown" }); var buildLogSummary = (logs) => ({ total: logs.length, errors: logs.filter((e) => e.levelName === "ERROR").length, warns: logs.filter((e) => e.levelName === "WARN").length, infos: logs.filter((e) => e.levelName === "INFO").length, debugs: logs.filter((e) => e.levelName === "DEBUG").length, traces: logs.filter((e) => e.levelName === "TRACE").length }); var buildSupportBundlePayload = (logs, includeFullLogs = false, traceId) => ({ exportedAt: (/* @__PURE__ */ new Date()).toISOString(), version: SCRIPT_INFO.VERSION, traceId: traceId?.trim() || null, state: { syncMode: resolveService(STATE_SERVICE).proxy.syncMode, syncStatus: resolveService(STATE_SERVICE).proxy.syncStatus, debugMode: resolveService(STATE_SERVICE).proxy.debugMode, settingsPreset: resolveService(STATE_SERVICE).proxy.settingsPreset, language: resolveService(STATE_SERVICE).proxy.language, lastSyncResult: resolveService(STATE_SERVICE).proxy.lastSyncResult }, environment: createEnvironmentSnapshot(), logSummary: buildLogSummary(logs), logs: includeFullLogs ? logs : [] }); var downloadJsonFile = (filename, payload) => { const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = filename; anchor.click(); setTimeout(() => { URL.revokeObjectURL(url); anchor.remove(); }, 100); }; var defaultFilters = { [LogLevel.ERROR]: true, [LogLevel.WARN]: true, [LogLevel.INFO]: true, [LogLevel.DEBUG]: false, [LogLevel.TRACE]: false }; var container = null; var listContainer = null; var traceFilterInput = null; var activeFilters = { ...defaultFilters }; var getTraceId = () => traceFilterInput?.value.trim() || ""; var getFilteredLogs = () => { const traceId = getTraceId(); return exportLogHistory({ traceId: traceId || void 0 }).filter((entry) => { const matchesLevel = !!activeFilters[entry.level]; const matchesTraceId = !traceId || entry.traceId === traceId; return matchesLevel && matchesTraceId; }); }; var refresh = () => { if (!listContainer) return; listContainer.textContent = ""; getFilteredLogs().reverse().forEach((entry) => listContainer.appendChild(createDebugLogItem(entry))); }; var downloadLogs = (includeFullLogs) => { const traceId = getTraceId(); downloadJsonFile("fc2-debug.json", buildSupportBundlePayload(getFilteredLogs(), includeFullLogs, traceId)); }; var createDebugLogItem = (entry) => { const countTag = entry.count && entry.count > 1 ? ` x${entry.count}` : ""; const item = h("div", { className: `fc2-log-item level-${(entry.levelName || "INFO").toLowerCase()}` }, h("span", { className: "fc2-log-time" }, `[${entry.timestamp}]`), h("span", { className: "fc2-log-level" }, `${entry.levelName}${countTag}`), h("span", { className: "fc2-log-module" }, `[${entry.module}]`), h("span", { className: "fc2-log-msg" }, entry.message), entry.traceId ? h("span", { className: "fc2-log-trace" }, `#${entry.traceId}`) : null); if (entry.data) { const dataView = h("pre", { className: "fc2-log-payload fc2-is-hidden" }, JSON.stringify(entry.data, null, 2)); const toggle = h("button", { type: "button", className: "fc2-log-payload-toggle", onclick: () => dataView.classList.toggle("fc2-is-hidden") }, UIUtils.icon(IconDatabase), ` ${t("debugPayload")}`); item.append(toggle, dataView); } return item; }; var renderDebugTab = exports("renderDebugTab",() => { const navigationIntent = consumePendingSettingsNavigationIntent("debug"); traceFilterInput = h("input", { className: "fc2-enh-input", placeholder: t("debugTraceIdPlaceholder"), oninput: refresh }); container = h("div", { className: "fc2-debug-container", "data-intent-focus": navigationIntent?.presetKey || "" }, h("div", { className: "fc2-debug-header" }, h("div", { className: "fc2-debug-actions" }, Button({ icon: UIUtils.icon(IconFileExport), text: t("btnCopyAll"), onClick: () => { navigator.clipboard.writeText(getFilteredLogs().map((e) => `[${e.timestamp}] [${e.levelName}] [${e.module}] ${e.message}`).join("\\n")); Toast.show(t("alertLogsCopied"), "success"); } }), Button({ text: t("debugExportJson"), onClick: () => downloadLogs(false) }), Button({ text: t("btnClearLogs"), className: "danger", onClick: () => { Logger.clear(); refresh(); Toast.show(t("alertLogsCleared"), "success"); } })), h("div", { className: "fc2-debug-filters" }, h("span", { className: "fc2-label-dim" }, t("labelLogFilters")), traceFilterInput, ...[ LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO, LogLevel.DEBUG, LogLevel.TRACE ].map((level) => Checkbox({ id: `filter-${level}`, label: LogLevel[level], checked: activeFilters[level], onChange: (val) => { activeFilters[level] = val; refresh(); } })))), listContainer = h("div", { id: DOM_IDS.LOG_LIST, className: "fc2-log-list-container" })); refresh(); return container; }); var onUnmount = exports("onUnmount",() => { container = null; listContainer = null; traceFilterInput = null; activeFilters = { ...defaultFilters }; });})}})); System.register("./AboutTab-BYbIrdck-YaaPEqkJ.js", ['./___monkey.entry.js','./components-1yyvCgmd-C1dNzRD5.js','dexie'],(function(exports){'use strict';var h,SCRIPT_INFO,t,IconTriangleExclamation,IconCircleInfo,Card;return{setters:[function(module){h=module.h;SCRIPT_INFO=module.k;t=module.t;IconTriangleExclamation=module.F;IconCircleInfo=module.G;},function(module){Card=module.C;},null],execute:(function(){/** * Render localized HTML content safely. * Uses h() with innerHTML which auto-sanitizes via DomUtils. */ var safeContent = (htmlStr, className) => h("div", { className, innerHTML: htmlStr }); var renderAboutTab = exports("renderAboutTab",() => { return h("div", { className: "fc2-about-tab" }, h("div", { className: "fc2-about-header" }, h("h2", {}, SCRIPT_INFO.NAME), h("div", { className: "fc2-version-badge" }, `v${SCRIPT_INFO.VERSION}`), h("p", { className: "fc2-about-desc" }, t("aboutDescription"))), Card({ title: t("aboutHelpTitle"), icon: IconCircleInfo, children: safeContent(t("aboutHelpContent"), "fc2-about-content") }), Card({ title: t("tabDmca"), icon: IconTriangleExclamation, className: "warning", children: safeContent(t("dmcaContent"), "fc2-about-content dmca") }), h("div", { className: "fc2-about-footer" }, h("a", { href: SCRIPT_INFO.GREASYFORK_URL, target: "_blank", rel: "noopener noreferrer", className: "fc2-link" }, t("labelGreasyFork")), h("span", { className: "fc2-dim" }, " | "), h("span", { className: "fc2-dim" }, t("aboutFooterTagline")))); });})}})); System.register("./components-1yyvCgmd-C1dNzRD5.js", ['./___monkey.entry.js'],(function(exports){'use strict';var h,UIUtils,IconEye,IconEyeSlash,resolveService,STATE_SERVICE;return{setters:[function(module){h=module.h;UIUtils=module.U;IconEye=module.J;IconEyeSlash=module.K;resolveService=module.r;STATE_SERVICE=module.S;}],execute:(function(){var resolveIcon$1 = (icon) => typeof icon === "string" ? UIUtils.icon(icon) : icon; var Button = exports("B",(props) => { const { text, onClick, className = "", icon, title, disabled, style, variant = "primary", size = "md" } = props; const btn = h("button", { type: "button", className: `fc2-enh-btn fc2-enh-btn-${variant} fc2-enh-btn-size-${size} ${className}`.trim(), title: title || "", disabled: !!disabled, onclick: (e) => { const result = onClick(e); if (result instanceof Promise) result.catch((e) => console.error("UI", "Button action error", e)); } }, icon ? resolveIcon$1(icon) : null, text ? h("span", { className: "fc2-btn-text" }, text) : null); if (style) Object.assign(btn.style, style); return btn; }); var resolveIcon = (icon) => typeof icon === "string" ? UIUtils.icon(icon) : icon; var Card = exports("C",(props) => { const { title, subtitle, icon, children, className = "", collapsed, dataGroupKey } = props; const body = h("div", { className: "fc2-panel-card-body" }, ...Array.isArray(children) ? children : [children]); return h("div", { className: `fc2-panel-card${collapsed ? " is-collapsed" : ""} ${className}`.trim(), "data-settings-group": dataGroupKey }, h("h4", { className: "fc2-panel-card-heading" }, icon ? resolveIcon(icon) : null, h("span", { className: "fc2-card-title" }, title), subtitle ? h("div", { className: "fc2-card-subtitle" }, subtitle) : null), body); }); var Checkbox = exports("d",(props) => { const { id, label, checked, onChange, controller, binding } = props; const checkbox = h("input", { id: `set-${id}`, type: "checkbox", checked: checked || false, onchange: !controller || !binding ? (e) => { const isChecked = e.target.checked; if (onChange) onChange(isChecked); } : void 0 }); const container = h("label", { className: "fc2-enh-checkbox-label", htmlFor: `set-${id}` }, checkbox, h("span", { className: "fc2-enh-checkbox-text" }, label)); if (controller && binding) controller.bind(checkbox, { key: binding, mode: "checked" }); return container; }); var ColorPicker = exports("c",({ id, options, controller, binding, onChange }) => { const container = h("div", { className: "fc2-color-picker", id }); const updateSelection = (value) => { container.querySelectorAll(".fc2-color-option").forEach((el) => { const elValue = el.getAttribute("data-value"); el.classList.toggle("is-active", elValue === value); }); }; options.forEach((option) => { const item = h("button", { type: "button", className: "fc2-color-option", title: option.label, dataset: { value: option.value }, style: { "--option-color": option.value } }); item.addEventListener("click", () => { const value = option.value; if (binding) resolveService(STATE_SERVICE).proxy[binding] = value; else updateSelection(value); onChange?.(value); }); container.appendChild(item); }); if (controller && binding) { const initialValue = resolveService(STATE_SERVICE).proxy[binding]; updateSelection(initialValue); controller.listen(binding, (val) => updateSelection(val)); } return container; }); var FormRow = exports("F",(props) => { const { label, children, className = "" } = props; const childArray = Array.isArray(children) ? children : [children]; return h("div", { className: `fc2-enh-form-row ${className}` }, label ? h("label", { className: "fc2-enh-label" }, label) : null, ...childArray); }); var CheckboxRow = exports("b",(props) => { const { className = "" } = props; const checkboxLabel = Checkbox(props); checkboxLabel.className = `${checkboxLabel.className} fc2-enh-form-row checkbox ${className}`; return checkboxLabel; }); var Input = exports("I",(props) => { const { id, type, value, placeholder, validator, onChange, controller, binding } = props; const input = h("input", { id: `set-${id}`, className: "fc2-enh-input", type: type === "number" ? "text" : type, inputMode: type === "number" ? "numeric" : void 0, pattern: type === "number" ? "[0-9]*" : void 0, value: value || "", placeholder: placeholder || "", oninput: (e) => { const target = e.target; const newValue = target.value; if (validator) { const result = validator(newValue); if (!result.valid) { target.classList.add("invalid"); target.title = result.error || "Invalid input"; } else { target.classList.remove("invalid"); target.title = ""; } } if (!controller || !binding) { if (onChange) onChange(newValue); } }, autocomplete: "off", spellcheck: false }); if (controller && binding) controller.bind(input, { key: binding, mode: "value", onChange: onChange ? (v) => onChange(v) : void 0 }); return input; }); var renderToggleIcon = (isPassword) => UIUtils.icon(isPassword ? IconEye : IconEyeSlash); var PasswordInput = exports("P",(props) => { const input = Input({ ...props, type: "password" }); const toggle = h("button", { className: "fc2-input-toggle", type: "button", "aria-label": "Toggle password visibility", onclick: () => { const isPass = input.type === "password"; input.type = isPass ? "text" : "password"; toggle.textContent = ""; toggle.appendChild(renderToggleIcon(!isPass)); } }); toggle.appendChild(renderToggleIcon(true)); return h("div", { className: "fc2-input-group" }, input, toggle); }); var Select = exports("a",(props) => { const { id, options, value, onChange, controller, binding } = props; const select = h("select", { id: `set-${id}`, className: "fc2-enh-select", onchange: !controller || !binding ? (e) => { const newValue = e.target.value; if (onChange) onChange(newValue); } : void 0 }, ...options.map((opt) => h("option", { value: opt.value, selected: value !== void 0 ? opt.value === value : false }, opt.label))); if (controller && binding) controller.bind(select, { key: binding, mode: "value", onChange: onChange ? (v) => onChange(v) : void 0 }); return select; }); var StatItem = exports("S",(props) => { const { id, label, value, unit } = props; const valContainer = h("div", { className: "fc2-stat-value", id: id ? `stat-${id}` : void 0 }); if (value instanceof HTMLElement) valContainer.appendChild(value); else valContainer.textContent = String(value); if (unit) valContainer.appendChild(h("span", { className: "fc2-stat-unit" }, unit)); return h("div", { className: "fc2-stat-item" }, h("div", { className: "fc2-stat-label" }, label), valContainer); });})}})); System.import("./___monkey.entry.js", "./");