// ==UserScript== // @name 2Libra Plus // @namespace https://github.com/utags // @homepageURL https://github.com/utags/userscripts#readme // @supportURL https://github.com/utags/userscripts/issues // @version 0.0.2 // @description 2Libra.com 增强工具 // @icon https://2libra.com/favicon.ico // @author Pipecraft // @license MIT // @match https://2libra.com/* // @run-at document-end // @grant GM_registerMenuCommand // @grant GM_addStyle // @grant GM.addStyle // @grant GM_info // @grant GM.info // @grant GM.addValueChangeListener // @grant GM_addValueChangeListener // @grant GM.getValue // @grant GM_getValue // @grant GM.setValue // @grant GM_setValue // ==/UserScript== // ;(() => { 'use strict' var __defProp = Object.defineProperty var __getOwnPropSymbols = Object.getOwnPropertySymbols var __hasOwnProp = Object.prototype.hasOwnProperty var __propIsEnum = Object.prototype.propertyIsEnumerable var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value, }) : (obj[key] = value) var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]) if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]) } return a } var style_default = '[data-unread-mark="1"]{position:relative}[data-unread-mark="1"]:before{background-color:#f97316;border-radius:9999px;bottom:0;content:"";left:-1px;position:absolute;top:0;width:4px}' function registerMenu(caption, onClick, options) { if (typeof GM_registerMenuCommand === 'function') { return GM_registerMenuCommand(caption, onClick, options) } return 0 } async function addStyle(css) { if (typeof GM_addStyle === 'function') { const style2 = GM_addStyle(css) if (style2 instanceof HTMLStyleElement) return style2 } if (typeof GM !== 'undefined' && typeof GM.addStyle === 'function') { const style2 = await GM.addStyle(css) if (style2 instanceof HTMLStyleElement) return style2 } const style = document.createElement('style') style.textContent = css ;(document.head || document.documentElement).append(style) return style } var style_default2 = '/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer properties;@layer theme, base, components, utilities;@layer theme{:host,:root{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-50:oklch(97.1% 0.013 17.38);--color-red-500:oklch(63.7% 0.237 25.331);--color-blue-300:oklch(80.9% 0.105 251.813);--color-blue-400:oklch(70.7% 0.165 254.624);--color-blue-600:oklch(54.6% 0.245 262.881);--color-blue-700:oklch(48.8% 0.243 264.376);--color-gray-50:oklch(98.5% 0.002 247.839);--color-gray-100:oklch(96.7% 0.003 264.542);--color-gray-300:oklch(87.2% 0.01 258.338);--color-gray-400:oklch(70.7% 0.022 261.325);--color-gray-500:oklch(55.1% 0.027 264.364);--color-gray-600:oklch(44.6% 0.03 256.802);--color-gray-700:oklch(37.3% 0.034 259.733);--color-gray-800:oklch(27.8% 0.033 256.848);--color-gray-900:oklch(21% 0.034 264.665);--color-white:#fff;--spacing:4px;--font-weight-semibold:600;--font-weight-bold:700;--radius-md:6px;--radius-xl:12px;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,::backdrop,::file-selector-button,:after,:before{border:0 solid;box-sizing:border-box;margin:0;padding:0}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-size:1em;font-variation-settings:var(--default-mono-font-variation-settings,normal)}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}menu,ol,ul{list-style:none}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}::file-selector-button,button,input,optgroup,select,textarea{background-color:transparent;border-radius:0;color:inherit;font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;opacity:1}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::-moz-placeholder{opacity:1}::placeholder{opacity:1}@supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::-moz-placeholder{color:currentcolor;@supports (color:color-mix(in lab,red,red)){color:color-mix(in oklab,currentcolor 50%,transparent)}}::placeholder{color:currentcolor;@supports (color:color-mix(in lab,red,red)){color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}::file-selector-button,button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer utilities{.container{width:100%;@media (width >= 40rem){max-width:640px}@media (width >= 48rem){max-width:768px}@media (width >= 64rem){max-width:1024px}@media (width >= 80rem){max-width:1280px}@media (width >= 96rem){max-width:1536px}}.grid{display:grid}}:host{all:initial}.user-settings{position:fixed;right:calc(var(--spacing)*3);top:calc(var(--spacing)*3);z-index:2147483647;--tw-ring-color:var(--user-color-ring,#111827)}.user-settings .panel{background-color:var(--color-gray-100);border-bottom-left-radius:var(--radius-xl);border-bottom-right-radius:var(--radius-xl);color:var(--color-gray-900);font-family:var(--font-sans);font-size:14px;max-height:90vh;overflow-y:auto;padding-inline:calc(var(--spacing)*4);padding-bottom:calc(var(--spacing)*4);padding-top:calc(var(--spacing)*0);width:420px;--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,rgba(0,0,0,.1)),0 8px 10px -6px var(--tw-shadow-color,rgba(0,0,0,.1));background:#f2f2f7;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);box-shadow:0 10px 39px 10px #3e424238!important;scrollbar-color:rgba(156,163,175,.25) transparent;scrollbar-width:thin}.user-settings .grid{display:flex;flex-direction:column;gap:calc(var(--spacing)*3)}.user-settings .row{align-items:center;display:flex;gap:calc(var(--spacing)*3);justify-content:space-between;padding-block:calc(var(--spacing)*3);padding-inline:calc(var(--spacing)*4)}.user-settings .group{background-color:var(--color-white);border-radius:var(--radius-xl);gap:calc(var(--spacing)*0);overflow:hidden}.user-settings .group .row{background-color:var(--color-white);border-radius:0;border-style:var(--tw-border-style);border-width:0;padding-block:calc(var(--spacing)*3);padding-inline:calc(var(--spacing)*4);position:relative}.user-settings .group .row:not(:last-child):after{background:#e5e7eb;bottom:0;content:"";height:1px;left:16px;position:absolute;right:0}.user-settings .header-row{align-items:center;border-radius:0;display:flex;justify-content:center;padding-inline:calc(var(--spacing)*0);padding-bottom:calc(var(--spacing)*3);padding-top:calc(var(--spacing)*0)}.user-settings .panel-stuck .header-row .panel-title{opacity:0;transform:translateY(-2px);transition:opacity .15s ease,transform .15s ease}.user-settings label{color:var(--color-gray-600)}.user-settings .label-wrap{display:flex;flex-direction:column;gap:calc(var(--spacing)*1);min-width:60px;text-align:left}.user-settings .btn{border-color:var(--color-gray-300);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;color:var(--color-gray-700);padding-block:calc(var(--spacing)*1);padding-inline:calc(var(--spacing)*3);white-space:nowrap;&:hover{@media (hover:hover){background-color:var(--color-gray-50)}}}.user-settings .btn-danger{border-color:var(--color-red-500);color:var(--color-red-500);&:hover{@media (hover:hover){background-color:var(--color-red-50)}}}.user-settings .btn-ghost{border-radius:var(--radius-md);color:var(--color-gray-500);padding-block:calc(var(--spacing)*1);padding-inline:calc(var(--spacing)*2);&:hover{@media (hover:hover){background-color:var(--color-gray-100)}}}.user-settings input[type=text]{border-color:transparent;border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;color:var(--color-gray-700);padding-block:calc(var(--spacing)*2);padding-inline:calc(var(--spacing)*3);text-align:right;width:180px;--tw-outline-style:none;outline-style:none}.user-settings input[type=text]:focus,.user-settings input[type=text]:hover{border-color:var(--color-gray-300)}.user-settings select{background-color:var(--color-white);border-color:transparent;border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;color:var(--color-gray-700);padding-block:calc(var(--spacing)*2);padding-inline:calc(var(--spacing)*3);text-align:right;width:180px;--tw-outline-style:none;outline-style:none}.user-settings select:focus,.user-settings select:hover{border-color:var(--color-gray-300)}.user-settings input[type=color]{border-color:var(--color-gray-300);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;height:calc(var(--spacing)*8);padding:calc(var(--spacing)*0);width:80px}.user-settings textarea{border-color:transparent;border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;color:var(--color-gray-700);padding-block:calc(var(--spacing)*2);padding-inline:calc(var(--spacing)*3);text-align:right;width:100%;--tw-outline-style:none;outline-style:none}.user-settings textarea:focus,.user-settings textarea:hover{border-color:var(--color-gray-300)}.user-settings .switch,.user-settings .toggle-wrap{align-items:center;display:flex;gap:calc(var(--spacing)*2)}.user-settings .toggle-checkbox{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#e5e5ea;border:1px solid #d1d1d6;border-radius:9999px;box-shadow:inset 0 1px 1px rgba(0,0,0,.1);cursor:pointer;display:inline-block;height:22px;position:relative;transition:background-color .2s ease,border-color .2s ease;width:42px}.user-settings .toggle-checkbox:before{background:#fff;border-radius:9999px;box-shadow:0 2px 4px rgba(0,0,0,.25);content:"";height:18px;left:2px;position:absolute;top:50%;transform:translateY(-50%);transition:transform .2s ease,background-color .2s ease,left .2s ease,right .2s ease;width:18px}.user-settings .toggle-checkbox:checked{background:var(--user-toggle-on-bg,#34c759);border-color:var(--user-toggle-on-bg,#34c759)}.user-settings .panel-title{font-size:20px;--tw-font-weight:var(--font-weight-bold);color:var(--color-gray-800);font-weight:var(--font-weight-bold)}.user-settings .outer-header{align-items:center;background-color:var(--color-gray-100);background:#f2f2f7;border-top-left-radius:var(--radius-xl);border-top-right-radius:var(--radius-xl);display:flex;font-family:var(--font-sans);height:calc(var(--spacing)*11);justify-content:center;position:relative}.user-settings .outer-header .outer-title{font-size:20px;opacity:0;transition:opacity .15s ease;--tw-font-weight:var(--font-weight-bold);color:var(--color-gray-800);font-weight:var(--font-weight-bold)}.user-settings .outer-header.stuck .outer-title{opacity:1}.user-settings .outer-header:after{background:#e5e7eb;bottom:0;content:"";height:1px;left:0;opacity:0;position:absolute;right:0;transition:opacity .15s ease}.user-settings .outer-header.stuck:after{opacity:1}.user-settings .group-title{font-size:13px;padding-inline:calc(var(--spacing)*1);--tw-font-weight:var(--font-weight-semibold);color:var(--color-gray-600);font-weight:var(--font-weight-semibold)}.user-settings .btn-ghost.icon{align-items:center;border-radius:calc(infinity*1px);color:var(--color-gray-500);cursor:pointer;display:flex;font-size:16px;height:calc(var(--spacing)*9);justify-content:center;transition:background-color .15s ease,color .15s ease;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:calc(var(--spacing)*9);&:hover{@media (hover:hover){background-color:var(--color-gray-100)}}&:hover{@media (hover:hover){color:var(--color-gray-700)}}}.user-settings .close-btn:hover{background-color:var(--color-gray-300);box-shadow:0 0 0 1px rgba(0,0,0,.05);color:var(--color-gray-900);font-size:19px;transform:translateY(-50%)}.user-settings .close-btn{position:absolute;right:12px;top:50%;transform:translateY(-50%);transition:transform .15s ease,background-color .15s ease,color .15s ease,font-size .15s ease}.user-settings .toggle-checkbox:checked:before{background:#fff;left:auto;right:2px;transform:translateY(-50%)}.user-settings .color-row{align-items:center;display:flex;gap:calc(var(--spacing)*1.5)}.user-settings .color-swatch{border-radius:var(--radius-md);cursor:pointer;height:calc(var(--spacing)*6);width:calc(var(--spacing)*6)}.user-settings .color-swatch.active{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-color:var(--user-color-ring,#111827)}.user-settings .seg{align-items:center;display:flex;flex-wrap:wrap;gap:calc(var(--spacing)*2)}.user-settings .seg.vertical{align-items:flex-end;flex-direction:column}.user-settings .seg-btn{border-color:var(--color-gray-300);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;color:var(--color-gray-700);cursor:pointer;padding-block:calc(var(--spacing)*1);padding-inline:calc(var(--spacing)*3);-webkit-user-select:none;-moz-user-select:none;user-select:none;&:hover{@media (hover:hover){background-color:var(--color-gray-50)}}}.user-settings .seg-btn.active{background:var(--user-active-bg,#111827);border-color:var(--user-active-bg,#111827);color:var(--user-active-fg,#fff)}.user-settings .value-wrap{align-items:flex-end;display:flex;flex-direction:column;gap:calc(var(--spacing)*1);text-align:right}.user-settings .tabs{align-items:center;display:flex;gap:calc(var(--spacing)*2);margin-bottom:calc(var(--spacing)*2)}.user-settings .tab-btn{border-color:var(--color-gray-300);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;color:var(--color-gray-700);cursor:pointer;padding-block:calc(var(--spacing)*1);padding-inline:calc(var(--spacing)*3);-webkit-user-select:none;-moz-user-select:none;user-select:none;&:hover{@media (hover:hover){background-color:var(--color-gray-50)}}}.user-settings .tab-btn.active{background:var(--user-active-bg,#111827);border-color:var(--user-active-bg,#111827);color:var(--user-active-fg,#fff)}.user-settings .field-help{color:var(--color-gray-400);font-size:11px}.row.help-row .field-help{margin-left:calc(var(--spacing)*0)}.user-settings .field-help a{color:var(--color-blue-600);text-decoration:underline;text-decoration-style:dashed;text-underline-offset:2px;&:hover{@media (hover:hover){color:var(--color-blue-700)}}}@media (prefers-color-scheme:dark){.user-settings .panel{background-color:var(--color-gray-800);border-bottom-left-radius:var(--radius-xl);border-bottom-right-radius:var(--radius-xl);box-shadow:0 10px 39px 10px #00000040!important;color:var(--color-gray-100)}.user-settings .row{background-color:transparent;border-style:var(--tw-border-style);border-width:0}.user-settings .header-row{background-color:var(--color-gray-800);border-color:var(--color-gray-700)}.user-settings .outer-header{background-color:var(--color-gray-800);border-top-left-radius:var(--radius-xl);border-top-right-radius:var(--radius-xl)}.user-settings .outer-header:after{background:#4b5563}.user-settings .footer a.issue-link{color:var(--color-gray-300);&:hover{@media (hover:hover){color:var(--color-gray-100)}}}.user-settings .footer .brand{color:var(--color-gray-400)}.user-settings label{color:var(--color-gray-300)}.user-settings .field-help{color:var(--color-gray-400)}.user-settings .field-help a{color:var(--color-blue-400);&:hover{@media (hover:hover){color:var(--color-blue-300)}}}.user-settings .group{background-color:var(--color-gray-700)}.user-settings .group .row:not(:last-child):after{background:#4b5563}}.user-settings .panel::-webkit-scrollbar{width:4px}.user-settings .panel::-webkit-scrollbar-track{background:transparent}.user-settings .panel::-webkit-scrollbar-thumb{background:rgba(156,163,175,.25);border-radius:9999px;opacity:.25}.user-settings .footer{align-items:center;color:var(--color-gray-500);display:flex;flex-direction:column;font-size:12px;gap:calc(var(--spacing)*1);padding-bottom:calc(var(--spacing)*3);padding-top:calc(var(--spacing)*6)}.user-settings .footer a.issue-link{color:var(--color-gray-600);cursor:pointer;text-decoration-line:underline;text-underline-offset:2px;-webkit-user-select:none;-moz-user-select:none;user-select:none;&:hover{@media (hover:hover){color:var(--color-gray-800)}}}.user-settings .footer .brand{color:var(--color-gray-500);cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;&:hover{@media (hover:hover){color:var(--color-gray-700)}}}.user-settings button{-webkit-user-select:none;-moz-user-select:none;user-select:none}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@layer properties{*,::backdrop,:after,:before{--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-border-style:solid;--tw-font-weight:initial}}' var doc = document function c(tag, opts) { const el = doc.createElement(tag) if (!opts) return el if (opts.className) el.className = opts.className if (opts.classes) for (const cls of opts.classes) el.classList.add(cls) if (opts.dataset && el.dataset) for (const k of Object.keys(opts.dataset)) el.dataset[k] = opts.dataset[k] if (opts.attrs) for (const k of Object.keys(opts.attrs)) el.setAttribute(k, opts.attrs[k]) if (opts.style) for (const k of Object.keys(opts.style)) el.style[k] = opts.style[k] if ('text' in opts) el.textContent = opts.text || '' if (opts.type && 'type' in el) el.type = opts.type if ('value' in opts && 'value' in el) el.value = opts.value || '' if (opts.rows && 'rows' in el) el.rows = opts.rows if (opts.placeholder && 'placeholder' in el) el.placeholder = opts.placeholder if (typeof opts.checked === 'boolean' && 'checked' in el) el.checked = opts.checked if (opts.children) { for (const ch of opts.children) { if (typeof ch === 'string') el.append(doc.createTextNode(ch)) else el.append(ch) } } return el } function addStyleToShadow(shadowRoot, css) { try { if (shadowRoot.adoptedStyleSheets) { const sheet = new CSSStyleSheet() sheet.replaceSync(css) shadowRoot.adoptedStyleSheets = [ ...shadowRoot.adoptedStyleSheets, sheet, ] return } } catch (e) {} const s = c('style', { text: css }) shadowRoot.append(s) } function camelToKebab(str) { return str.replaceAll(/[A-Z]/g, (letter) => '-'.concat(letter.toLowerCase()) ) } function ensureShadowRoot(options) { const key = options.hostDatasetKey || 'userscriptHost' const val = options.hostId const attrKey = camelToKebab(key) const sel = '[data-'.concat(attrKey, '="').concat(val, '"]') const existing = doc.querySelector(sel) if (existing instanceof HTMLDivElement && existing.shadowRoot) { if (!existing.isConnected || options.moveToEnd) { try { doc.documentElement.append(existing) } catch (e) {} } return { host: existing, root: existing.shadowRoot, existed: true } } const host = c('div', { dataset: { [key]: val } }) const root = host.attachShadow({ mode: 'open' }) if (options.style) { addStyleToShadow(root, options.style) } doc.documentElement.append(host) return { host, root, existed: false } } var win = globalThis function isTopFrame() { return win.self === win.top } var normalizeToDefaultType = (val, dv) => { const t = typeof dv if (t === 'number') { const n = Number(val) return Number.isFinite(n) ? n : dv } if (t === 'object') { return val && typeof val === 'object' ? val : dv } return typeof val === t ? val : dv } function setOrDelete(obj, key, value, defaultValue) { const normalized = normalizeToDefaultType(value, defaultValue) const isEqual = (a, b) => { if (a === b) return true if (a && b && typeof a === 'object' && typeof b === 'object') { try { return JSON.stringify(a) === JSON.stringify(b) } catch (e) {} } return false } if (isEqual(normalized, defaultValue)) { delete obj[key] } else { obj[key] = normalized } } function deepEqual(a, b) { if (a === b) { return true } if ( typeof a !== 'object' || a === null || typeof b !== 'object' || b === null ) { return false } if (Array.isArray(a) !== Array.isArray(b)) { return false } if (Array.isArray(a)) { if (a.length !== b.length) { return false } for (let i = 0; i < a.length; i++) { if (!deepEqual(a[i], b[i])) { return false } } return true } const keysA = Object.keys(a) const keysB = Object.keys(b) if (keysA.length !== keysB.length) { return false } for (const key of keysA) { if ( !Object.prototype.hasOwnProperty.call(b, key) || !deepEqual(a[key], b[key]) ) { return false } } return true } var valueChangeListeners = /* @__PURE__ */ new Map() var valueChangeListenerIdCounter = 0 var valueChangeBroadcastChannel = new BroadcastChannel( 'gm_value_change_channel' ) var lastKnownValues = /* @__PURE__ */ new Map() var pollingIntervalId = null function startPolling() { if (pollingIntervalId || isNativeListenerSupported) return pollingIntervalId = setInterval(async () => { const keys = new Set( Array.from(valueChangeListeners.values()).map((l) => l.key) ) for (const key of keys) { const newValue = await getValue(key) if (!lastKnownValues.has(key)) { lastKnownValues.set(key, newValue) continue } const oldValue = lastKnownValues.get(key) if (!deepEqual(oldValue, newValue)) { lastKnownValues.set(key, newValue) triggerValueChangeListeners(key, oldValue, newValue, true) valueChangeBroadcastChannel.postMessage({ key, oldValue, newValue }) } } }, 1500) } var getScriptHandler = () => { if (typeof GM_info !== 'undefined') { return GM_info.scriptHandler || '' } if (typeof GM !== 'undefined' && GM.info) { return GM.info.scriptHandler || '' } return '' } var scriptHandler = getScriptHandler().toLowerCase() var isIgnoredHandler = scriptHandler === 'tamp' || scriptHandler.includes('stay') var isNativeListenerSupported = !isIgnoredHandler && ((typeof GM !== 'undefined' && typeof GM.addValueChangeListener === 'function') || typeof GM_addValueChangeListener === 'function') function triggerValueChangeListeners(key, oldValue, newValue, remote) { const list = Array.from(valueChangeListeners.values()).filter( (l) => l.key === key ) for (const l of list) { l.callback(key, oldValue, newValue, remote) } } valueChangeBroadcastChannel.addEventListener('message', (event) => { const { key, oldValue, newValue } = event.data lastKnownValues.set(key, newValue) triggerValueChangeListeners(key, oldValue, newValue, true) }) async function getValue(key, defaultValue) { if (typeof GM !== 'undefined' && typeof GM.getValue === 'function') { return GM.getValue(key, defaultValue) } if (typeof GM_getValue === 'function') { return GM_getValue(key, defaultValue) } return defaultValue } async function updateValue(key, newValue, updater) { let oldValue if (!isNativeListenerSupported) { oldValue = await getValue(key) } await updater() if (!isNativeListenerSupported) { if (deepEqual(oldValue, newValue)) { return } lastKnownValues.set(key, newValue) triggerValueChangeListeners(key, oldValue, newValue, false) valueChangeBroadcastChannel.postMessage({ key, oldValue, newValue }) } } async function setValue(key, value) { await updateValue(key, value, async () => { if (typeof GM !== 'undefined' && typeof GM.setValue === 'function') { await GM.setValue(key, value) } else if (typeof GM_setValue === 'function') { GM_setValue(key, value) } }) } async function addValueChangeListener(key, callback) { if (isNativeListenerSupported) { if ( typeof GM !== 'undefined' && typeof GM.addValueChangeListener === 'function' ) { return GM.addValueChangeListener(key, callback) } if (typeof GM_addValueChangeListener === 'function') { return GM_addValueChangeListener(key, callback) } } const id = ++valueChangeListenerIdCounter valueChangeListeners.set(id, { key, callback }) if (!lastKnownValues.has(key)) { void getValue(key).then((v) => { lastKnownValues.set(key, v) }) } startPolling() return id } function isObject(item) { return Boolean(item) && typeof item === 'object' } var currentHost function onKeyDown(e) { if (e.key === 'Escape') { closeSettingsPanel() } } function closeSettingsPanel() { try { currentHost == null ? void 0 : currentHost.remove() } catch (e) {} try { globalThis.removeEventListener('keydown', onKeyDown, true) } catch (e) {} currentHost = void 0 } function createFieldRow(opts, content) { const row = c('div', { className: 'row', dataset: { key: opts.key } }) const labWrap = c('div', { className: 'label-wrap' }) const lab = c('label', { text: opts.label }) labWrap.append(lab) if (opts.help) { labWrap.append(c('div', { className: 'field-help', text: opts.help })) } else if (opts.renderHelp) { const helpEl = c('div', { className: 'field-help' }) opts.renderHelp(helpEl) labWrap.append(helpEl) } const val = c('div', { className: 'value-wrap' }) if (Array.isArray(content)) { val.append(...content) } else { val.append(content) } row.append(labWrap) row.append(val) return row } function createToggleRow(opts) { const seg = c('div', { className: 'toggle-wrap' }) const chk = c('input', { type: 'checkbox', className: 'toggle-checkbox', dataset: { key: opts.key, isSitePref: opts.isSitePref ? '1' : '' }, }) seg.append(chk) const row = createFieldRow(opts, seg) return { row, chk } } function createInputRow(opts) { const inp = c('input', { type: 'text', placeholder: opts.placeholder || '', dataset: { key: opts.key, isSitePref: opts.isSitePref ? '1' : '' }, }) const row = createFieldRow(opts, inp) return { row, inp } } function createTextareaRow(opts) { const ta = c('textarea', { rows: opts.rows || 4, dataset: { key: opts.key, isSitePref: opts.isSitePref ? '1' : '' }, }) const row = createFieldRow(opts, ta) return { row, ta } } function createRadioRow(opts) { const seg = c('div', { className: 'seg' }) for (const o of opts.options) { const b = c('button', { className: 'seg-btn', dataset: { key: opts.key, value: o.value, isSitePref: opts.isSitePref ? '1' : '', }, text: o.label, }) seg.append(b) } const row = createFieldRow(opts, seg) return { row, seg } } function createColorRow(opts) { const seg = c('div', { className: 'color-row' }) for (const o of opts.options) { const b = c('button', { className: 'color-swatch', dataset: { key: opts.key, value: o.value, isSitePref: opts.isSitePref ? '1' : '', }, style: { backgroundColor: o.value }, }) seg.append(b) } const row = createFieldRow(opts, seg) return { row, seg } } function createSelectRow(opts) { const sel = c('select', { dataset: { key: opts.key, isSitePref: opts.isSitePref ? '1' : '' }, }) for (const o of opts.options) { const opt = c('option', { value: o.value, text: o.label }) sel.append(opt) } const row = createFieldRow(opts, sel) return { row, sel } } function createActionRow(opts) { const act = c('div', { className: 'seg'.concat(opts.layout === 'vertical' ? ' vertical' : ''), }) for (const a of opts.actions) { const b = c('button', { className: 'btn action-btn'.concat( a.kind === 'danger' ? ' btn-danger' : '' ), dataset: { key: opts.key, action: a.id }, text: a.text, }) act.append(b) } const row = createFieldRow(opts, act) return { row } } function openSettingsPanel(schema, store2, options) { if (!isTopFrame()) { return } const { host, root, existed } = ensureShadowRoot({ hostId: (options == null ? void 0 : options.hostDatasetValue) || 'settings', hostDatasetKey: (options == null ? void 0 : options.hostDatasetKey) || 'userHost', style: style_default2.concat( (options == null ? void 0 : options.styleText) || '' ), moveToEnd: true, }) currentHost = host if (existed) return let lastValues = { global: {}, site: {} } const wrap = c('div', { className: 'user-settings' }) applyThemeStyles(wrap, options == null ? void 0 : options.theme) const panel = c('div', { className: 'panel' }) const grid = c('div', { className: 'grid' }) const { row: headerRow } = buildHeader(schema.title) grid.append(headerRow) const fillers = {} const addFiller = (key, fn) => { if (!fillers[key]) fillers[key] = [] fillers[key].push(fn) } function appendAndFill(container, row, key, filler) { container.append(row) addFiller(key, filler) } function appendField(container, f) { switch (f.type) { case 'toggle': { const { row, chk } = createToggleRow({ label: f.label, key: f.key, help: f.help, renderHelp: f.renderHelp, isSitePref: f.isSitePref, }) appendAndFill(container, row, f.key, () => { fillToggleUI(chk, f.key) }) break } case 'input': { const { row, inp } = createInputRow({ label: f.label, key: f.key, placeholder: f.placeholder, help: f.help, isSitePref: f.isSitePref, }) appendAndFill(container, row, f.key, () => { fillInput(inp, f.key) }) break } case 'textarea': { const { row, ta } = createTextareaRow({ label: f.label, key: f.key, rows: f.rows, help: f.help, isSitePref: f.isSitePref, }) appendAndFill(container, row, f.key, () => { fillTextarea(ta, f.key) }) break } case 'radio': { const { row, seg } = createRadioRow({ label: f.label, key: f.key, options: f.options, help: f.help, isSitePref: f.isSitePref, }) appendAndFill(container, row, f.key, () => { fillRadioUI(seg, f.key) }) break } case 'select': { const { row, sel } = createSelectRow({ label: f.label, key: f.key, options: f.options, help: f.help, isSitePref: f.isSitePref, }) appendAndFill(container, row, f.key, () => { fillSelect(sel, f.key) }) break } case 'colors': { const { row, seg } = createColorRow({ label: f.label, key: f.key, options: f.options, help: f.help, isSitePref: f.isSitePref, }) appendAndFill(container, row, f.key, () => { fillColorUI(seg, f.key) }) break } case 'action': { const { row } = createActionRow({ label: f.label, key: f.key, actions: f.actions, help: f.help, renderHelp: f.renderHelp, layout: f.layout, }) container.append(row) break } case 'custom': { const row = c('div', { className: 'row custom-row' }) if (f.label) { const lab = c('label', { text: f.label }) row.append(lab) } if (f.help) { const help = c('div', { className: 'field-help', text: f.help }) row.append(help) } const { update } = f.render(row, { key: f.key, isSitePref: f.isSitePref, onChange(val) { void store2.set({ [f.key]: val }, !f.isSitePref) }, }) appendAndFill(container, row, f.key, () => { const value = getFieldValue(f.key, f.isSitePref) update(value) }) break } case 'help': { const row = c('div', { className: 'row help-row' }) const help = c('div', { className: 'field-help', text: f.help }) row.append(help) container.append(row) break } } } function applyThemeStyles(wrap2, theme) { if (!theme) return const properties = [] if (theme.activeBg) properties.push('--user-active-bg: '.concat(theme.activeBg, ';')) if (theme.activeFg) properties.push('--user-active-fg: '.concat(theme.activeFg, ';')) if (theme.colorRing) properties.push('--user-color-ring: '.concat(theme.colorRing, ';')) if (theme.toggleOnBg) properties.push('--user-toggle-on-bg: '.concat(theme.toggleOnBg, ';')) const accent = theme.activeBg || theme.colorRing if (accent) properties.push('--user-accent: '.concat(accent, ';')) if (properties.length > 0) wrap2.style.cssText = properties.join(' ') } function buildHeader(title) { const row = c('div', { className: 'row header-row' }) const titleEl = c('label', { className: 'panel-title', text: title }) row.append(titleEl) return { row } } function renderSimplePanel(container, data) { if (data.groups && Array.isArray(data.groups)) { renderGroupsPanel(container, data.groups) return } const fields = data.fields || [] const body = c('div', { className: 'grid group' }) container.append(body) for (const f of fields) appendField(body, f) } function renderTabsPanel(container, tabs) { var _a const tabsWrap = c('div', { className: 'tabs' }) const panels = {} let active = ((_a = tabs[0]) == null ? void 0 : _a.id) || '' for (const t of tabs) { const b = c('button', { className: 'tab-btn', dataset: { tabId: t.id }, text: t.title, }) tabsWrap.append(b) const p = c('div', { className: 'grid' }) panels[t.id] = p if (t.id !== active) p.style.display = 'none' if ('groups' in t && Array.isArray(t.groups)) { renderGroupsPanel(p, t.groups) } else if ('fields' in t && Array.isArray(t.fields)) { p.className = 'grid group' for (const f of t.fields) appendField(p, f) } } container.append(tabsWrap) for (const id of Object.keys(panels)) container.append(panels[id]) function updateTabsUI() { for (const b of Array.from(tabsWrap.querySelectorAll('.tab-btn'))) { const id = b.dataset.tabId || '' if (id === active) b.classList.add('active') else b.classList.remove('active') } for (const id of Object.keys(panels)) { panels[id].style.display = id === active ? '' : 'none' } } function onTabsClick(e) { const t = e.target const b = t.closest('.tab-btn') if (b && b instanceof HTMLElement) { active = b.dataset.tabId || '' updateTabsUI() } } tabsWrap.addEventListener('click', onTabsClick) updateTabsUI() } function renderGroupsPanel(container, groups) { for (const g of groups) { const body = c('div', { className: 'grid group' }) if (g.title) { const header = c('h2', { className: 'group-title', text: g.title }) container.append(header) } container.append(body) for (const f of g.fields) appendField(body, f) } } const refreshAll = async () => { try { const g = await store2.getAll(true) const s = await store2.getAll(false) lastValues = { global: g, site: s } } catch (e) {} for (const k of Object.keys(fillers)) { for (const fn of fillers[k]) { try { fn() } catch (e) {} } } } function wireStoreChange(store3, fillers2) { var _a try { ;(_a = store3.onChange) == null ? void 0 : _a.call(store3, (e) => { if (e.key === '*' || !fillers2[e.key]) { void refreshAll() return } for (const fn of fillers2[e.key]) { try { fn() } catch (e2) {} } }) } catch (e) {} } function getFieldValue(key, el) { const isSitePref = el instanceof HTMLElement ? Boolean(el.dataset.isSitePref) : Boolean(el) const values = isSitePref ? lastValues.site : lastValues.global return values[key] } function getFieldInfo(el) { const key = el.dataset.key if (!key) return null const isSitePref = Boolean(el.dataset.isSitePref) return { key, isSitePref } } function fillRadioUI(seg, key) { try { const btn = seg.querySelector('.seg-btn') if (!btn) return const v = getFieldValue(key, btn) for (const b of Array.from(seg.querySelectorAll('.seg-btn'))) { const val = b.dataset.value || '' if (val === String(v)) b.classList.add('active') else b.classList.remove('active') } } catch (e) {} } function fillColorUI(seg, key) { try { const btn = seg.querySelector('.color-swatch') if (!btn) return const v = getFieldValue(key, btn) for (const b of Array.from(seg.querySelectorAll('.color-swatch'))) { const val = b.dataset.value || '' if (val.toLowerCase() === String(v || '').toLowerCase()) b.classList.add('active') else b.classList.remove('active') } } catch (e) {} } function fillToggleUI(onBtn, key) { try { if (onBtn instanceof HTMLInputElement && onBtn.type === 'checkbox') { const v = getFieldValue(key, onBtn) onBtn.checked = Boolean(v) } } catch (e) {} } function fillInput(inp, key) { try { const v = getFieldValue(key, inp) inp.value = String(v != null ? v : '') } catch (e) {} } function fillTextarea(ta, key) { try { const v = getFieldValue(key, ta) ta.value = String(v != null ? v : '') } catch (e) {} } function fillSelect(sel, key) { try { const v = getFieldValue(key, sel) for (const o of Array.from(sel.querySelectorAll('option'))) { o.selected = o.value === String(v) } } catch (e) {} } async function handleSegButton(rb) { const info = getFieldInfo(rb) if (!info) return const val = rb.dataset.value || '' try { await store2.set(info.key, val, !info.isSitePref) } catch (e) {} } async function handleColorSwatch(cs) { const info = getFieldInfo(cs) if (!info) return const val = cs.dataset.value || '' try { await store2.set(info.key, val, !info.isSitePref) } catch (e) {} } function handleActionBtn(ab) { var _a const key = ab.dataset.key || '' const actionId = ab.dataset.action || '' try { ;(_a = options == null ? void 0 : options.onAction) == null ? void 0 : _a.call(options, { key, actionId, target: ab }) } catch (e) {} } function onPanelClick(e) { const t = e.target if (t === topCloseBtn) { closeSettingsPanel() return } const rb = t.closest('.seg-btn') if (rb && rb instanceof HTMLElement) { void handleSegButton(rb) return } const cs = t.closest('.color-swatch') if (cs && cs instanceof HTMLElement) { void handleColorSwatch(cs) return } const ab = t.closest('.action-btn') if (ab && ab instanceof HTMLElement) handleActionBtn(ab) } function handleInputChange(inp) { const info = getFieldInfo(inp) if (!info) return const isCheckbox = (inp.type || '').toLowerCase() === 'checkbox' const v = isCheckbox ? Boolean(inp.checked) : inp.value void store2.set(info.key, v, !info.isSitePref) } function handleTextareaChange(ta) { const info = getFieldInfo(ta) if (!info) return void store2.set(info.key, ta.value, !info.isSitePref) } function handleSelectChange(sel) { const info = getFieldInfo(sel) if (!info) return void store2.set(info.key, sel.value, !info.isSitePref) } function onPanelChange(e) { const t = e.target const inp = t.closest('input') if (inp && inp instanceof HTMLInputElement) { handleInputChange(inp) return } const ta = t.closest('textarea') if (ta && ta instanceof HTMLTextAreaElement) { handleTextareaChange(ta) return } const sel = t.closest('select') if (sel && sel instanceof HTMLSelectElement) { handleSelectChange(sel) } } switch (schema.type) { case 'simple': { renderSimplePanel(grid, schema) break } case 'tabs': { renderTabsPanel(grid, schema.tabs) break } } panel.addEventListener('click', onPanelClick) panel.addEventListener('change', onPanelChange) const outerHeader = c('div', { className: 'outer-header' }) const outerTitle = c('label', { className: 'outer-title', text: schema.title, }) const topCloseBtn = c('button', { className: 'btn-ghost icon close-btn', text: '\xD7', attrs: { 'aria-label': '\u5173\u95ED' }, }) outerHeader.append(outerTitle) outerHeader.append(topCloseBtn) try { outerHeader.addEventListener('click', (e) => { const t = e.target if (t === topCloseBtn) { closeSettingsPanel() } }) } catch (e) {} panel.append(grid) const footer = c('div', { className: 'footer' }) const issueLink = c('a', { className: 'issue-link', text: 'Report an Issue\u2026', attrs: { href: (options == null ? void 0 : options.issuesUrl) || 'https://github.com/utags/userscripts/issues', target: '_blank', rel: 'noopener noreferrer', }, }) const brand = c('a', { className: 'brand', text: 'Made with \u2764\uFE0F by Pipecraft', attrs: { href: 'https://www.pipecraft.net/', target: '_blank', rel: 'noopener noreferrer', }, }) footer.append(issueLink) footer.append(brand) panel.append(footer) const stickyThreshold = 22 let stickyTimer const stickyDebounceMs = 80 function updateHeaderStickyCore() { try { const sc = panel.scrollTop || 0 const stuck = sc > stickyThreshold if (stuck) { panel.classList.add('panel-stuck') outerHeader.classList.add('stuck') } else { panel.classList.remove('panel-stuck') outerHeader.classList.remove('stuck') } } catch (e) {} } function updateHeaderSticky() { try { if (stickyTimer !== void 0) globalThis.clearTimeout(stickyTimer) stickyTimer = globalThis.setTimeout( updateHeaderStickyCore, stickyDebounceMs ) } catch (e) {} } try { panel.addEventListener('scroll', updateHeaderSticky) updateHeaderStickyCore() } catch (e) {} wrap.append(outerHeader) wrap.append(panel) root.append(wrap) wireStoreChange(store2, fillers) void refreshAll() globalThis.addEventListener('keydown', onKeyDown, true) } function createSettingsStore( storageKey2, defaults, isSupportSitePref = false ) { const rootKey = storageKey2 || 'settings' let cache let globalCache let siteCache let initPromise const changeCbs = [] let listenerRegistered = false const getHostname = () => { var _a return ( ((_a = globalThis.location) == null ? void 0 : _a.hostname) || 'unknown' ) } let beforeSetHook function updateCache(obj) { if (isSupportSitePref) { const rootObj = isObject(obj) ? obj : {} const globalData = rootObj.global globalCache = __spreadValues({}, defaults) if (isObject(globalData)) { Object.assign(globalCache, globalData) } const hostname = getHostname() const siteData = rootObj[hostname] siteCache = __spreadValues({}, globalCache) if (isObject(siteData)) { Object.assign(siteCache, siteData) } cache = siteCache } else { cache = __spreadValues({}, defaults) if (isObject(obj)) Object.assign(cache, obj) } } function registerValueChangeListener() { if (listenerRegistered) return try { void addValueChangeListener(rootKey, (n, ov, nv, remote) => { try { updateCache(nv) for (const f of changeCbs) { f({ key: '*', oldValue: ov, newValue: nv, remote }) } } catch (e) {} }) listenerRegistered = true } catch (e) {} } registerValueChangeListener() async function ensure() { if (cache) return cache if (initPromise) return initPromise initPromise = (async () => { let obj try { obj = await getValue(rootKey, void 0) } catch (e) {} updateCache(obj) initPromise = void 0 return cache })() return initPromise } return { async get(key, isGlobalPref) { await ensure() if (isSupportSitePref) { if (isGlobalPref) return globalCache[key] return siteCache[key] } return cache[key] }, async getAll(isGlobalPref) { await ensure() if (isSupportSitePref) { if (isGlobalPref) return __spreadValues({}, globalCache) return __spreadValues({}, siteCache) } return __spreadValues({}, cache) }, async set(...args) { let obj try { obj = await getValue(rootKey, {}) } catch (e) {} if (!isObject(obj)) obj = {} let isGlobalPref = false let values = {} if (typeof args[0] === 'string') { values[args[0]] = args[1] isGlobalPref = Boolean(args[2]) } else { values = __spreadValues({}, args[0]) isGlobalPref = Boolean(args[1]) } if (beforeSetHook) { try { values = await beforeSetHook(values, isGlobalPref) } catch (e) {} } let target let global if (isSupportSitePref) { const hostname = isGlobalPref ? 'global' : getHostname() if (!isObject(obj[hostname])) obj[hostname] = {} target = obj[hostname] global = isObject(obj.global) ? obj.global : {} } else { target = obj } const isSitePref = isSupportSitePref && !isGlobalPref const apply = (key, value) => { if (isSitePref && key in global) { const normalized = normalizeToDefaultType(value, defaults[key]) target[key] = normalized return } setOrDelete(target, key, value, defaults[key]) } if (values) { for (const k of Object.keys(values)) { const v = values[k] apply(k, v) } } if (isSupportSitePref && target && Object.keys(target).length === 0) { const hostname = isGlobalPref ? 'global' : getHostname() delete obj[hostname] } updateCache(obj) try { await setValue(rootKey, obj) } catch (e) {} }, async reset(isGlobalPref) { let obj if (isSupportSitePref) { try { obj = await getValue(rootKey, {}) } catch (e) {} if (!isObject(obj)) obj = {} const hostname = isGlobalPref ? 'global' : getHostname() delete obj[hostname] } else { obj = {} } updateCache(obj) try { await setValue(rootKey, obj) } catch (e) {} }, defaults() { return __spreadValues({}, defaults) }, onChange(cb) { changeCbs.push(cb) }, onBeforeSet(cb) { beforeSetHook = cb }, } } var urlCallbacks = /* @__PURE__ */ new Set() var urlWatcherInstalled = false function triggerUrlCallbacks() { for (const cb of urlCallbacks) { try { cb() } catch (error) { console.error(error) } } } function onUrlChange(callback) { urlCallbacks.add(callback) if (!urlWatcherInstalled) { installUrlWatcher() urlWatcherInstalled = true } return () => { urlCallbacks.delete(callback) } } function installUrlWatcher() { try { const origPush = history.pushState history.pushState = function (...args) { const ret = origPush.apply(history, args) triggerUrlCallbacks() return ret } } catch (e) {} try { const origReplace = history.replaceState history.replaceState = function (...args) { const ret = origReplace.apply(history, args) triggerUrlCallbacks() return ret } } catch (e) {} globalThis.addEventListener('popstate', triggerUrlCallbacks) globalThis.addEventListener('hashchange', triggerUrlCallbacks) } var domCallbacks = /* @__PURE__ */ new Set() var domObserver function triggerDomCallbacks() { for (const cb of domCallbacks) { try { cb() } catch (error) { console.error(error) } } } function onDomChange(callback) { domCallbacks.add(callback) ensureDomObserver() return () => { domCallbacks.delete(callback) } } function ensureDomObserver() { if (domObserver) return const root = document.body || document.documentElement if (!root) { if (document.readyState === 'loading') { document.addEventListener( 'DOMContentLoaded', () => { ensureDomObserver() }, { once: true } ) } return } domObserver = new MutationObserver(() => { triggerDomCallbacks() }) domObserver.observe(root, { childList: true, subtree: true, }) } var initialized = false var clickTimer function isNotificationsPage() { const loc = globalThis.location if (!loc) return false return loc.pathname === '/notifications' } function markUnreadItems() { if (!isNotificationsPage()) return const items = document.querySelectorAll( 'div[data-main-left] .card > div.flex' ) for (const item of items) { if (item.dataset.unreadMark === '1') continue const spans = item.querySelectorAll('span') let isUnread = false for (const span of spans) { if (span.textContent && span.textContent.trim() === '\u672A\u8BFB') { isUnread = true break } } if (!isUnread) continue item.dataset.unreadMark = '1' } } function tryClickMarkButton(getSettings) { const settings = getSettings() if (!settings.enabled || !settings.autoMarkNotificationsRead) return if (!isNotificationsPage()) return markUnreadItems() const btn = document.querySelector( 'div[data-main-left] button.btn-primary:not(.btn-disabled)' ) if (!btn) return console.info( '[2libra-plus] \u{1F518} \u81EA\u52A8\u70B9\u51FB"\u5DF2\u8BFB\u5F53\u524D\u9875"\u6309\u94AE' ) btn.click() } function scheduleClick(getSettings) { if (clickTimer !== void 0) { globalThis.clearTimeout(clickTimer) } clickTimer = globalThis.setTimeout(() => { clickTimer = void 0 tryClickMarkButton(getSettings) }, 800) } function runAutoMarkNotificationsRead(getSettings) { scheduleClick(getSettings) } function initAutoMarkNotificationsRead(getSettings) { if (initialized) return initialized = true const check = () => { scheduleClick(getSettings) } if (document.readyState === 'loading') { document.addEventListener( 'DOMContentLoaded', () => { check() }, { once: true } ) } else { check() } onUrlChange(check) onDomChange(check) } var storageKey = '2libraPlus:lastHomeViewTime' var initialized2 = false var lastHomeViewBase function getListContainer() { return document.querySelector('[data-main-left="true"] ul.card') || void 0 } function getLastHomeViewTime() { try { const raw = globalThis.localStorage.getItem(storageKey) if (raw) { const n = Number.parseInt(raw, 10) if (Number.isFinite(n) && n > 0) { return n } } } catch (e) {} return void 0 } function logLastHomeViewTime(base) { if (!base) return const now = Date.now() const diffSeconds = Math.max(0, Math.floor((now - base) / 1e3)) const minute = 60 const hour = 60 * minute const day = 24 * hour let unit = '\u79D2' let value = diffSeconds if (diffSeconds >= minute && diffSeconds < hour) { unit = '\u5206' value = Math.floor(diffSeconds / minute) } else if (diffSeconds >= hour && diffSeconds < day) { unit = '\u5C0F\u65F6' value = Math.floor(diffSeconds / hour) } else if (diffSeconds >= day) { unit = '\u5929' value = Math.floor(diffSeconds / day) } const date = new Date(base) const pad = (n) => String(n).padStart(2, '0') const year = date.getFullYear() const month = pad(date.getMonth() + 1) const dayOfMonth = pad(date.getDate()) const hours = pad(date.getHours()) const minutes = pad(date.getMinutes()) const seconds = pad(date.getSeconds()) const formatted = '' .concat(year, '-') .concat(month, '-') .concat(dayOfMonth, ' ') .concat(hours, ':') .concat(minutes, ':') .concat(seconds) console.log( '[2libra-plus] \u{1F559} \u4E0A\u6B21\u9996\u9875\u8BBF\u95EE\u65F6\u95F4\uFF1A' .concat(value, ' ') .concat(unit, ' \u524D\uFF08') .concat(formatted, '\uFF09') ) } function updateReplyTimeColor(getSettings) { var _a let lastHomeViewTime = lastHomeViewBase const settings = getSettings() if (!settings.enabled || !settings.replyTimeColor) { const timeElements2 = Array.from( ((_a = getListContainer()) == null ? void 0 : _a.querySelectorAll('li time')) || [] ) for (const el of timeElements2) { el.style.removeProperty('color') } return } const list = getListContainer() if (!list) return const timeElements = Array.from(list.querySelectorAll('li time')) if (timeElements.length === 0) return const now = Date.now() const timestamps = [] if (lastHomeViewTime && lastHomeViewTime > now) { lastHomeViewTime = void 0 } for (const el of timeElements) { const dt = el.getAttribute('datetime') if (!dt) continue const t = Date.parse(dt) if (Number.isNaN(t)) continue if (t > now) continue timestamps.push(t) } if (timestamps.length === 0) return const min = Math.min(...timestamps) const max = Math.max(...timestamps) if (!lastHomeViewTime) { lastHomeViewTime = min } if (lastHomeViewTime < min) { lastHomeViewTime = min } else if (lastHomeViewTime > max) { lastHomeViewTime = max } for (const el of timeElements) { const dt = el.getAttribute('datetime') if (!dt) continue const t = Date.parse(dt) if (Number.isNaN(t)) continue if (t > now) continue let opacity if (t >= lastHomeViewTime) { const rangeNew = now - lastHomeViewTime || 1 const ageNew = now - t const ratioNew = Math.min(Math.max(ageNew / rangeNew, 0), 1) const eased = Math.sqrt(ratioNew) opacity = 1 - eased * 0.3 const percent = Math.round(opacity * 100) el.style.color = 'color-mix(in oklab,var(--color-primary) '.concat( percent, '%,transparent)' ) } else { const rangeOld = lastHomeViewTime - min || 1 const ageOld = lastHomeViewTime - t const ratioOld = Math.min(Math.max(ageOld / rangeOld, 0), 1) const eased = Math.sqrt(ratioOld) const maxOld = 0.69 const minOld = 0.3 opacity = maxOld - eased * (maxOld - minOld) const percent = Math.round(opacity * 100) el.style.color = 'color-mix(in oklab,var(--color-base-content) '.concat( percent, '%,transparent)' ) } } } function runReplyTimeColor(getSettings) { updateReplyTimeColor(getSettings) } function initReplyTimeColor(getSettings) { if (initialized2) return initialized2 = true const runUpdateColor = () => { updateReplyTimeColor(getSettings) } const handleHomeView = () => { const last = getLastHomeViewTime() lastHomeViewBase = last logLastHomeViewTime(last) runUpdateColor() if (globalThis.location.pathname === '/') { try { const now = Date.now() const fiveMinutes = 5 * 60 * 1e3 if ((!last || now - last >= fiveMinutes) && getListContainer()) { globalThis.localStorage.setItem(storageKey, String(now)) } } catch (e) {} } } if (document.readyState === 'loading') { document.addEventListener( 'DOMContentLoaded', () => { handleHomeView() }, { once: true } ) } else { handleHomeView() } onUrlChange(() => { handleHomeView() }) onDomChange(runUpdateColor) } var DEFAULT_SETTINGS = { enabled: true, autoMarkNotificationsRead: true, replyTimeColor: true, } var store = createSettingsStore('settings', DEFAULT_SETTINGS) var enabled = DEFAULT_SETTINGS.enabled var autoMarkNotificationsRead = DEFAULT_SETTINGS.autoMarkNotificationsRead var replyTimeColor = DEFAULT_SETTINGS.replyTimeColor function buildSettingsSchema() { const fields = [ { type: 'toggle', key: 'enabled', label: '\u542F\u7528' }, { type: 'toggle', key: 'autoMarkNotificationsRead', label: '\u81EA\u52A8\u5C06\u901A\u77E5\u9875\u8BBE\u4E3A\u5DF2\u8BFB', }, { type: 'toggle', key: 'replyTimeColor', label: '\u56DE\u590D\u65F6\u95F4\u989C\u8272\u6E10\u53D8', }, ] return { type: 'simple', title: '2Libra Plus \u8BBE\u7F6E', fields, } } function openSettings() { const schema = buildSettingsSchema() const s = store openSettingsPanel(schema, s, { hostDatasetKey: 'libraPlusHost', hostDatasetValue: '2libra-plus-settings', theme: { activeBg: '#2563eb', activeFg: '#ffffff', colorRing: '#2563eb', toggleOnBg: '#2563eb', }, }) } function registerMenus() { try { registerMenu('\u8BBE\u7F6E', () => { try { openSettings() } catch (e) {} }) } catch (e) {} } function listenSettings() { try { store.onChange(() => { void applySettingsFromStore() }) } catch (e) {} } async function applySettingsFromStore() { try { const prevEnabled = enabled const obj = await store.getAll() enabled = Boolean(obj.enabled) autoMarkNotificationsRead = Boolean(obj.autoMarkNotificationsRead) replyTimeColor = Boolean(obj.replyTimeColor) if (!prevEnabled && enabled && !featuresInitialized) { initFeatures() } else if (featuresInitialized) { runAutoMarkNotificationsRead(getSettingsSnapshot) runReplyTimeColor(getSettingsSnapshot) } } catch (e) {} } function getSettingsSnapshot() { return { enabled, autoMarkNotificationsRead, replyTimeColor, } } var featuresInitialized = false function initFeatures() { if (featuresInitialized) return featuresInitialized = true initAutoMarkNotificationsRead(getSettingsSnapshot) initReplyTimeColor(getSettingsSnapshot) } function bootstrap() { const d = document.documentElement const ds = d.dataset if (ds.libraPlus === '1') return ds.libraPlus = '1' void addStyle(style_default) registerMenus() listenSettings() void applySettingsFromStore() if (enabled) { initFeatures() } } bootstrap() })()