LibMAL

Utility library for my MAL userscript collection

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.org/scripts/563964/1741592/LibMAL.js

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         LibMAL
// @version      1.0.0
// @description  Utility library for my MAL userscript collection
// @author       SuperTouch
// @license      GPL-3.0
// ==/UserScript==

const libMAL = (function() {
  'use strict';

  const SCRIPT_PREFIX = 'cfg';
  const DEBUG = false;
  
  function debug(...args) { if (DEBUG) console.log(`[LibMAL]`, ...args); }

  function newElement(tag, props, children) {
    const el = document.createElement(tag);
    if (props) Object.assign(el, props);
    if (children) children.forEach(c => c && el.appendChild(typeof c === 'string' ? document.createTextNode(c) : c));
    return el;
  }

  function applyDefaults(opts, defaults) {
    return opts ? { ...defaults, ...opts } : { ...defaults };
  }

  function isDarkMode() {
    return document.documentElement.classList.contains('dark-mode') || document.body?.dataset?.skin === 'dark';
  }

  function getPageInfo() {
    const match = window.location.pathname.match(/^\/(anime|manga)\/(\d+)/);
    return match ? { type: match[1], id: match[2] } : { type: null, id: null };
  }

  function onReady(callback) {
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', callback);
    else callback();
  }

  function fetch(url, options = {}) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: options.method || 'GET',
        url,
        headers: options.headers,
        data: options.body,
        onload: response => {
          if (response.status >= 200 && response.status < 300) {
            try {
              const data = options.responseType === 'text' ? response.responseText : JSON.parse(response.responseText);
              resolve({ ok: true, status: response.status, data });
            } catch (e) { resolve({ ok: true, status: response.status, data: response.responseText }); }
          } else {
            resolve({ ok: false, status: response.status, data: null });
          }
        },
        onerror: () => reject(new Error('Network error'))
      });
    });
  }

  const utilities = { newElement, applyDefaults, isDarkMode, getPageInfo, onReady, fetch };

  let stylesInjected = false;
  function injectStyles() {
    if (stylesInjected) return;
    stylesInjected = true;
    debug('Injecting styles');

    const css = `
      .libmal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 99999; display: flex; align-items: center; justify-content: center; }
      .libmal-modal {
        background: var(--libmal-bg); color: var(--libmal-text); border-radius: 4px; width: 800px; max-width: 95vw; height: 70vh;
        display: flex; flex-direction: column; box-shadow: 0 4px 20px rgba(0,0,0,0.3); font: 11px Verdana, Arial, sans-serif;
        --libmal-bg: #fff; --libmal-bg-alt: #f5f5f5; --libmal-bg-hover: #eee; --libmal-text: #333; --libmal-muted: #666;
        --libmal-border: #ddd; --libmal-accent: #2e51a2; --libmal-accent-hover: #1d439b; --libmal-danger: #d9534f;
        --libmal-input-bg: #fff; --libmal-input-border: #ccc;
      }
      html.dark-mode .libmal-modal, [data-skin="dark"] .libmal-modal {
        --libmal-bg: #1c1c1c; --libmal-bg-alt: #252525; --libmal-bg-hover: #333; --libmal-text: #e0e0e0; --libmal-muted: #999;
        --libmal-border: #444; --libmal-input-bg: #2a2a2a; --libmal-input-border: #555;
      }
      .libmal-header { background: var(--libmal-accent); color: #fff; padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; border-radius: 4px 4px 0 0; }
      .libmal-header h2 { margin: 0; font-size: 14px; font-weight: bold; border-style: none !important; }
      .libmal-close { background: none; border: none; color: #fff; font-size: 20px; cursor: pointer; }
      .libmal-body { display: flex; flex: 1; overflow: hidden; }
      .libmal-sidebar { width: 180px; background: var(--libmal-bg-alt); border-right: 1px solid var(--libmal-border); overflow-y: auto; flex-shrink: 0; }
      .libmal-script-btn { display: block; width: 100%; padding: 10px 12px; border: none; background: transparent; text-align: left; cursor: pointer; font: inherit; color: var(--libmal-text); border-bottom: 1px solid var(--libmal-border); }
      .libmal-script-btn:hover { background: var(--libmal-bg-hover); }
      .libmal-script-btn.active { background: var(--libmal-bg); font-weight: bold; border-left: 3px solid var(--libmal-accent); }
      .libmal-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
      .libmal-tabs { display: flex; background: var(--libmal-bg-alt); border-bottom: 1px solid var(--libmal-border); flex-shrink: 0; overflow-x: auto; }
      .libmal-tab { padding: 8px 14px; border: none; background: transparent; cursor: pointer; font: inherit; color: var(--libmal-text); border-bottom: 2px solid transparent; }
      .libmal-tab:hover { background: var(--libmal-bg-hover); }
      .libmal-tab.active { background: var(--libmal-bg); border-bottom-color: var(--libmal-accent); font-weight: bold; }
      .libmal-panel { flex: 1; overflow-y: auto; padding: 16px; background: var(--libmal-bg); }
      .libmal-panel[hidden] { display: none; }
      .libmal-row { display: flex; align-items: flex-start; padding: 12px 0; border-bottom: 1px solid var(--libmal-border); }
      .libmal-row:last-child { border-bottom: none; }
      .libmal-label { min-width: 160px; flex-shrink: 0; font-weight: bold; padding-top: 6px; padding-right: 16px; text-align: left; }
      .libmal-control { flex: 1; display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
      .libmal-desc { color: var(--libmal-muted); font-size: 10px; line-height: 1.4; }
      .libmal-input { padding: 5px 8px; border: 1px solid var(--libmal-input-border); border-radius: 3px; font: inherit; background: var(--libmal-input-bg); color: var(--libmal-text); }
      .libmal-input:focus { outline: none; border-color: var(--libmal-accent); }
      .libmal-check { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; min-height: 20px; padding-top: 3px; }
      .libmal-check input { width: 14px; height: 14px; margin: 0; cursor: pointer; position: relative; top: 1px; }
      .libmal-btn { padding: 6px 12px; border: none; border-radius: 3px; cursor: pointer; font: inherit; }
      .libmal-btn-primary { background: var(--libmal-accent); color: #fff; }
      .libmal-btn-primary:hover { background: var(--libmal-accent-hover); }
      .libmal-btn-secondary { background: #6c757d; color: #fff; }
      .libmal-btn-secondary:hover { background: #5a6268; }
      .libmal-btn-danger { background: var(--libmal-danger); color: #fff; }
      .libmal-footer { padding: 12px 16px; border-top: 1px solid var(--libmal-border); display: flex; justify-content: space-between; background: var(--libmal-bg-alt); border-radius: 0 0 4px 4px; }
      .libmal-footer-left, .libmal-footer-right { display: flex; gap: 8px; }
      .libmal-table { width: 100%; border-collapse: collapse; }
      .libmal-table th { background: var(--libmal-accent); color: #fff; padding: 8px; }
      .libmal-table td { padding: 6px 8px; border-bottom: 1px solid var(--libmal-border); }
      .libmal-table tr:nth-child(odd) td { background: var(--libmal-bg-alt); }
      .libmal-table tr:hover td { background: var(--libmal-bg-hover); }
      .libmal-section-header { font-weight: bold; font-size: 12px; padding: 12px 0 8px; margin-top: 16px; border-bottom: 1px solid var(--libmal-border); color: var(--libmal-text); }
      .libmal-section-header:first-child { margin-top: 0; }
      .libmal-empty { text-align: center; padding: 20px; color: var(--libmal-muted); }
      .libmal-description { margin: 0 0 12px !important; color: var(--libmal-muted); line-height: 1.4 !important; }
      .libmal-checkbox-grid { column-gap: 24px; }
      .libmal-checkbox-grid label { display: flex; padding: 5px 0; break-inside: avoid; }
      .libmal-form-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; padding: 12px; background: var(--libmal-bg-alt); border-radius: 4px; margin-top: 12px; }
      .libmal-export-menu { position: relative; display: inline-block; }
      .libmal-export-dropdown { position: absolute; bottom: 100%; left: 0; background: var(--libmal-bg); border: 1px solid var(--libmal-border); border-radius: 3px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); display: none; min-width: 140px; }
      .libmal-export-menu.open .libmal-export-dropdown { display: block; }
      .libmal-export-item { display: block; width: 100%; padding: 8px 12px; border: none; background: transparent; text-align: left; cursor: pointer; font: inherit; color: var(--libmal-text); }
      .libmal-export-item:hover { background: var(--libmal-bg-hover); }
    `;

    if (typeof GM_addStyle !== 'undefined') GM_addStyle(css);
    else document.head.appendChild(newElement('style', { textContent: css }));
  }

  const pageWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
  
  if (!pageWindow.__libmal_queue__) {
    pageWindow.__libmal_queue__ = [];
    debug('Created global registration queue on', typeof unsafeWindow !== 'undefined' ? 'unsafeWindow' : 'window');
  }

  const localScripts = new Map();
  let modal = null;
  let activeScriptId = null;

  class Tab {
    constructor(section, name) {
      this.section = section;
      this.name = name;
      this.controls = [];
    }

    get(key, def) { return this.section.get(key, def); }
    set(key, val) { this.section.set(key, val); }

    addCheckbox(key, label, opts = {}) {
      opts = applyDefaults(opts, { default: true, description: null });
      this.controls.push({ type: 'checkbox', key, label, opts });
      return this;
    }

    addInput(key, label, opts = {}) {
      opts = applyDefaults(opts, { inputType: 'text', default: '', description: null, placeholder: '', width: null, min: null, max: null });
      this.controls.push({ type: 'input', key, label, opts });
      return this;
    }

    addDropdown(key, label, opts = {}) {
      opts = applyDefaults(opts, { options: [], default: null, description: null });
      this.controls.push({ type: 'dropdown', key, label, opts });
      return this;
    }

    addHeader(text) {
      this.controls.push({ type: 'header', text });
      return this;
    }

    addDescription(text) {
      this.controls.push({ type: 'description', text });
      return this;
    }

    addCheckboxGrid(key, label, opts = {}) {
      opts = applyDefaults(opts, { items: [], columns: 3, description: null });
      this.controls.push({ type: 'checkboxGrid', key, label, opts });
      return this;
    }

    addValueTable(key, label, opts = {}) {
      opts = applyDefaults(opts, { rows: [], inputType: 'number', inputWidth: '60px', description: null });
      this.controls.push({ type: 'valueTable', key, label, opts });
      return this;
    }

    addList(key, label, opts = {}) {
      opts = applyDefaults(opts, { columns: [], addFields: [], emptyText: 'No items', description: null });
      this.controls.push({ type: 'list', key, label, opts });
      return this;
    }

    addCustom(renderFn, saveFn = null) {
      this.controls.push({ type: 'custom', render: renderFn, save: saveFn });
      return this;
    }

    render() {
      const panel = newElement('div', { className: 'libmal-panel' });
      this.controls.forEach(c => {
        let el;
        if (c.type === 'checkbox') el = this._checkbox(c);
        else if (c.type === 'input') el = this._input(c);
        else if (c.type === 'dropdown') el = this._dropdown(c);
        else if (c.type === 'header') el = newElement('div', { className: 'libmal-section-header', textContent: c.text });
        else if (c.type === 'description') el = newElement('p', { className: 'libmal-description', innerHTML: c.text });
        else if (c.type === 'checkboxGrid') el = this._checkboxGrid(c);
        else if (c.type === 'valueTable') el = this._valueTable(c);
        else if (c.type === 'list') el = this._list(c);
        else if (c.type === 'custom') el = c.render(this);
        if (el) panel.appendChild(el);
      });
      return panel;
    }

    save(container) {
      container.querySelectorAll('[data-libmal-key]').forEach(el => {
        const key = el.dataset.dlcKey;
        let val;
        if (el.type === 'checkbox') val = el.checked;
        else if (el.type === 'number') val = parseFloat(el.value) || 0;
        else val = el.value;
        this.set(key, val);
      });
      this.controls.filter(c => c.type === 'custom' && c.save).forEach(c => c.save(container, this));
      this.controls.filter(c => c.type === 'checkboxGrid').forEach(c => {
        const checked = [];
        container.querySelectorAll(`[data-libmal-grid="${c.key}"] input:checked`).forEach(cb => checked.push(cb.value));
        this.set(c.key, checked);
      });
      this.controls.filter(c => c.type === 'valueTable').forEach(c => {
        const values = {};
        container.querySelectorAll(`[data-libmal-table="${c.key}"] input`).forEach(inp => { values[inp.dataset.row] = parseFloat(inp.value) || 0; });
        this.set(c.key, values);
      });
      this.controls.filter(c => c.type === 'list').forEach(c => {
        const listEl = container.querySelector(`[data-libmal-list="${c.key}"]`);
        if (listEl && listEl._dlcItems) this.set(c.key, listEl._dlcItems);
      });
    }

    _row(label, content) {
      return newElement('div', { className: 'libmal-row' }, [
        newElement('div', { className: 'libmal-label' }, [label]),
        newElement('div', { className: 'libmal-control' }, [content])
      ]);
    }

    _checkbox(c) {
      const cb = newElement('input', { type: 'checkbox', checked: this.get(c.key, c.opts.default) });
      cb.dataset.dlcKey = c.key;
      return this._row(c.label, newElement('label', { className: 'libmal-check' }, [cb, c.opts.description || '']));
    }

    _input(c) {
      const inp = newElement('input', { type: c.opts.inputType, className: 'libmal-input', value: this.get(c.key, c.opts.default), placeholder: c.opts.placeholder });
      inp.dataset.dlcKey = c.key;
      if (c.opts.inputType === 'number') { if (c.opts.min != null) inp.min = c.opts.min; if (c.opts.max != null) inp.max = c.opts.max; }
      if (c.opts.width) inp.style.width = c.opts.width;
      const frag = document.createDocumentFragment();
      frag.appendChild(inp);
      if (c.opts.description) frag.appendChild(newElement('div', { className: 'libmal-desc', textContent: c.opts.description }));
      return this._row(c.label, frag);
    }

    _dropdown(c) {
      const sel = newElement('select', { className: 'libmal-input' });
      sel.dataset.dlcKey = c.key;
      const cur = this.get(c.key, c.opts.default);
      c.opts.options.forEach(([text, val]) => {
        const opt = newElement('option', { value: val, textContent: text });
        if (val === cur) opt.selected = true;
        sel.appendChild(opt);
      });
      const frag = document.createDocumentFragment();
      frag.appendChild(sel);
      if (c.opts.description) frag.appendChild(newElement('div', { className: 'libmal-desc', textContent: c.opts.description }));
      return this._row(c.label, frag);
    }

    _checkboxGrid(c) {
      const container = newElement('div');
      if (c.opts.description) container.appendChild(newElement('p', { className: 'libmal-description', textContent: c.opts.description }));
      const grid = newElement('div', { className: 'libmal-checkbox-grid' });
      grid.style.columnCount = c.opts.columns;
      grid.dataset.dlcGrid = c.key;
      const current = this.get(c.key, []);
      const items = typeof c.opts.items === 'function' ? c.opts.items() : c.opts.items;
      items.forEach(item => {
        const label = newElement('label', { className: 'libmal-check' });
        const cb = newElement('input', { type: 'checkbox', value: item, checked: current.includes(item) });
        label.appendChild(cb);
        label.appendChild(document.createTextNode(item));
        grid.appendChild(label);
      });
      container.appendChild(grid);
      return container;
    }

    _valueTable(c) {
      const container = newElement('div');
      if (c.opts.description) container.appendChild(newElement('p', { className: 'libmal-description', textContent: c.opts.description }));
      const table = newElement('table', { className: 'libmal-table' });
      table.dataset.dlcTable = c.key;
      table.innerHTML = `<thead><tr><th>${c.label}</th><th style="width:${c.opts.inputWidth}">Value</th></tr></thead><tbody></tbody>`;
      const tbody = table.querySelector('tbody');
      const current = this.get(c.key, {});
      const rows = typeof c.opts.rows === 'function' ? c.opts.rows() : c.opts.rows;
      rows.forEach(row => {
        const tr = newElement('tr');
        const val = current[row.key] ?? row.default ?? 50;
        tr.innerHTML = `<td>${row.label}</td><td><input type="${c.opts.inputType}" class="libmal-input" data-row="${row.key}" value="${val}" style="width:${c.opts.inputWidth}"></td>`;
        tbody.appendChild(tr);
      });
      container.appendChild(table);
      return container;
    }

    _list(c) {
      const container = newElement('div');
      if (c.opts.description) container.appendChild(newElement('p', { className: 'libmal-description', textContent: c.opts.description }));
      const table = newElement('table', { className: 'libmal-table' });
      table.dataset.dlcList = c.key;
      const headerRow = c.opts.columns.map(col => `<th>${col}</th>`).join('') + '<th style="width:60px">Action</th>';
      table.innerHTML = `<thead><tr>${headerRow}</tr></thead><tbody></tbody>`;
      const tbody = table.querySelector('tbody');
      let items = [...this.get(c.key, [])];
      table._dlcItems = items;

      function renderList() {
        tbody.innerHTML = '';
        if (items.length === 0) {
          tbody.innerHTML = `<tr><td colspan="${c.opts.columns.length + 1}" class="libmal-empty">${c.opts.emptyText}</td></tr>`;
          return;
        }
        items.forEach((item, i) => {
          const tr = newElement('tr');
          const cells = c.opts.columns.map((_, ci) => `<td>${Object.values(item)[ci] || ''}</td>`).join('');
          tr.innerHTML = `${cells}<td><button class="libmal-btn libmal-btn-danger" data-remove="${i}" style="padding:3px 8px;font-size:10px">Remove</button></td>`;
          tbody.appendChild(tr);
        });
      }
      renderList();

      tbody.onclick = e => {
        if (e.target.dataset.remove !== undefined) {
          items.splice(parseInt(e.target.dataset.remove, 10), 1);
          table._dlcItems = items;
          renderList();
        }
      };

      if (c.opts.addFields.length > 0) {
        const form = newElement('div', { className: 'libmal-form-row' });
        const inputs = [];
        c.opts.addFields.forEach(field => {
          if (field.type === 'text') {
            const inp = newElement('input', { type: 'text', className: 'libmal-input', placeholder: field.placeholder || '' });
            if (field.flex) inp.style.flex = field.flex;
            if (field.minWidth) inp.style.minWidth = field.minWidth;
            inputs.push({ el: inp, field });
            form.appendChild(inp);
          } else if (field.type === 'select') {
            const sel = newElement('select', { className: 'libmal-input' });
            const options = typeof field.options === 'function' ? field.options() : field.options;
            sel.innerHTML = options.map(([text, val]) => `<option value="${val}">${text}</option>`).join('');
            inputs.push({ el: sel, field });
            form.appendChild(sel);
          }
        });
        const addBtn = newElement('button', { className: 'libmal-btn libmal-btn-primary', textContent: 'Add' });
        addBtn.onclick = () => {
          const newItem = {};
          inputs.forEach((inp, i) => { newItem[c.opts.addFields[i].key || `field${i}`] = inp.el.value; });
          if (Object.values(newItem).some(v => !v && v !== 0)) { alert('Please fill all fields'); return; }
          items.push(newItem);
          table._dlcItems = items;
          inputs.forEach(inp => { inp.el.value = ''; });
          renderList();
        };
        form.appendChild(addBtn);
        container.appendChild(table);
        container.appendChild(form);
      } else {
        container.appendChild(table);
      }
      return container;
    }
  }

  class Section {
    constructor(id, opts) {
      this.id = id;
      this.opts = applyDefaults(opts, { title: id });
      this.tabs = new Map();
      this._defaultTab = new Tab(this, 'Settings');
      this._knownKeys = new Set();
      debug(`Section created: ${id}`);
    }

    get(key, def) {
      const stored = GM_getValue(`${SCRIPT_PREFIX}_${key}`, undefined);
      if (stored === undefined) return def;
      this._knownKeys.add(key);
      return stored;
    }

    set(key, val) {
      this._knownKeys.add(key);
      GM_setValue(`${SCRIPT_PREFIX}_${key}`, val);
    }

    tab(name) {
      if (!this.tabs.has(name)) this.tabs.set(name, new Tab(this, name));
      return this.tabs.get(name);
    }

    addCheckbox(k, l, o) { this._defaultTab.addCheckbox(k, l, o); return this; }
    addInput(k, l, o) { this._defaultTab.addInput(k, l, o); return this; }
    addDropdown(k, l, o) { this._defaultTab.addDropdown(k, l, o); return this; }
    addHeader(t) { this._defaultTab.addHeader(t); return this; }
    addDescription(t) { this._defaultTab.addDescription(t); return this; }
    addCheckboxGrid(k, l, o) { this._defaultTab.addCheckboxGrid(k, l, o); return this; }
    addValueTable(k, l, o) { this._defaultTab.addValueTable(k, l, o); return this; }
    addList(k, l, o) { this._defaultTab.addList(k, l, o); return this; }
    addCustom(r, s) { this._defaultTab.addCustom(r, s); return this; }

    getTabs() {
      if (this.tabs.size > 0) return Array.from(this.tabs.values());
      if (this._defaultTab.controls.length > 0) return [this._defaultTab];
      return [];
    }

    exportData() {
      const data = {};
      this.getTabs().forEach(tab => {
        tab.controls.forEach(c => {
          if (c.key) this._knownKeys.add(c.key);
        });
      });
      this._knownKeys.forEach(key => {
        const val = this.get(key, undefined);
        if (val !== undefined) data[key] = val;
      });
      return data;
    }

    importData(data) {
      Object.entries(data).forEach(([k, v]) => this.set(k, v));
    }
  }

  // Collect all scripts from global queue
  function getAllScripts() {
    const all = new Map();
    // Add from global queue
    pageWindow.__libmal_queue__.forEach(s => {
      if (!all.has(s.id)) {
        all.set(s.id, s);
        debug(`Collected from queue: ${s.id}`);
      }
    });
    // Add local scripts
    localScripts.forEach((s, id) => {
      if (!all.has(id)) {
        all.set(id, s);
        debug(`Collected from local: ${id}`);
      }
    });
    debug(`Total scripts: ${all.size}`);
    return all;
  }

  const settings = {
    registerScript(id, opts = {}) {
      debug(`registerScript called: ${id}`);
      
      const existing = pageWindow.__libmal_queue__.find(s => s.id === id);
      if (existing) {
        debug(`Already registered in queue: ${id}`);
        return existing;
      }

      if (localScripts.has(id)) {
        debug(`Already registered locally: ${id}`);
        return localScripts.get(id);
      }

      const section = new Section(id, opts);
      localScripts.set(id, section);
      pageWindow.__libmal_queue__.push(section);
      debug(`Registered and queued: ${id}, queue size: ${pageWindow.__libmal_queue__.length}`);
      return section;
    },

    get(scriptId, key, def) {
      const s = localScripts.get(scriptId) || pageWindow.__libmal_queue__.find(s => s.id === scriptId);
      return s ? s.get(key, def) : def;
    },

    set(scriptId, key, val) {
      const s = localScripts.get(scriptId) || pageWindow.__libmal_queue__.find(s => s.id === scriptId);
      if (s) s.set(key, val);
    },

    showModal() {
      injectStyles();
      if (modal) modal.remove();
      
      const scripts = getAllScripts();
      debug(`showModal: ${scripts.size} scripts`);
      
      if (scripts.size === 0) {
        debug('No scripts registered!');
        return;
      }
      
      activeScriptId = scripts.keys().next().value;
      modal = createModal(scripts);
      document.body.appendChild(modal);
    },

    hideModal() {
      if (modal) { modal.remove(); modal = null; }
    },

    injectDropdownLink() {
      debug('injectDropdownLink called');
      
      const dropdown = document.querySelector('.header-profile-dropdown ul');
      if (!dropdown) {
        debug('Dropdown not found');
        return;
      }
      
      if (dropdown.querySelector('#libmal-settings-link')) {
        debug('Link already exists');
        return;
      }

      const logout = dropdown.querySelector('form[action*="logout"]');
      if (!logout) {
        debug('Logout form not found');
        return;
      }

      const li = newElement('li');
      li.innerHTML = `<a href="javascript:void(0);" id="libmal-settings-link"><i class="fa-solid fa-fw fa-gear mr4"></i>Userscript Settings</a>`;
      dropdown.insertBefore(li, logout.parentElement);
      li.querySelector('#libmal-settings-link').onclick = e => { e.preventDefault(); settings.showModal(); };
      debug('Dropdown link injected');
    }
  };

  function createModal(scripts) {
    debug('createModal called');
    const overlay = newElement('div', { className: 'libmal-overlay' });
    const m = newElement('div', { className: 'libmal-modal' });
    overlay.appendChild(m);

    const header = newElement('div', { className: 'libmal-header' }, [
      newElement('h2', {}, [newElement('i', { className: 'fa-solid fa-gear' }), ' Userscript Settings']),
      newElement('button', { className: 'libmal-close', textContent: '×', onclick: () => settings.hideModal() })
    ]);
    m.appendChild(header);

    const body = newElement('div', { className: 'libmal-body' });
    const sidebar = newElement('div', { className: 'libmal-sidebar' });
    const content = newElement('div', { className: 'libmal-content' });
    body.appendChild(sidebar);
    body.appendChild(content);
    m.appendChild(body);

    const scriptList = Array.from(scripts.entries()).sort((a, b) => a[1].opts.title.localeCompare(b[1].opts.title));
    debug(`Rendering ${scriptList.length} scripts in sidebar`);

    scriptList.forEach(([id, section], i) => {
      const btn = newElement('button', { className: `libmal-script-btn${i === 0 ? ' active' : ''}`, textContent: section.opts.title });
      btn.dataset.id = id;
      btn.onclick = () => selectScript(id);
      sidebar.appendChild(btn);
    });

    function selectScript(id) {
      activeScriptId = id;
      sidebar.querySelectorAll('.libmal-script-btn').forEach(b => b.classList.toggle('active', b.dataset.id === id));
      renderScriptContent(id);
    }

    function renderScriptContent(id) {
      content.innerHTML = '';
      const section = scripts.get(id);
      if (!section) return;

      const tabs = section.getTabs();
      debug(`Script ${id} has ${tabs.length} tabs`);
      
      if (tabs.length === 0) {
        content.appendChild(newElement('div', { className: 'libmal-panel libmal-empty', textContent: 'No settings configured.' }));
        return;
      }

      if (tabs.length > 1) {
        const tabBar = newElement('div', { className: 'libmal-tabs' });
        tabs.forEach((tab, i) => {
          const btn = newElement('button', { className: `libmal-tab${i === 0 ? ' active' : ''}`, textContent: tab.name });
          btn.dataset.tab = tab.name;
          btn.onclick = () => {
            tabBar.querySelectorAll('.libmal-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab.name));
            content.querySelectorAll('.libmal-panel').forEach(p => p.hidden = p.dataset.tab !== tab.name);
          };
          tabBar.appendChild(btn);
        });
        content.appendChild(tabBar);
      }

      tabs.forEach((tab, i) => {
        const panel = tab.render();
        panel.dataset.tab = tab.name;
        panel.dataset.scriptId = id;
        if (tabs.length > 1 && i > 0) panel.hidden = true;
        content.appendChild(panel);
      });
    }

    if (scriptList.length > 0) renderScriptContent(scriptList[0][0]);

    const footer = newElement('div', { className: 'libmal-footer' });
    const left = newElement('div', { className: 'libmal-footer-left' });
    const right = newElement('div', { className: 'libmal-footer-right' });

    const exportMenu = newElement('div', { className: 'libmal-export-menu' });
    const exportBtn = newElement('button', { className: 'libmal-btn libmal-btn-secondary', textContent: 'Export ▾' });
    const exportDropdown = newElement('div', { className: 'libmal-export-dropdown' });
    exportDropdown.appendChild(newElement('button', { className: 'libmal-export-item', textContent: 'This Script', onclick: () => exportScript(activeScriptId, scripts) }));
    exportDropdown.appendChild(newElement('button', { className: 'libmal-export-item', textContent: 'All Scripts', onclick: () => exportAll(scripts) }));
    exportBtn.onclick = () => exportMenu.classList.toggle('open');
    exportMenu.appendChild(exportBtn);
    exportMenu.appendChild(exportDropdown);
    left.appendChild(exportMenu);

    const importBtn = newElement('button', { className: 'libmal-btn libmal-btn-secondary', textContent: 'Import', onclick: () => importSettings(scripts) });
    left.appendChild(importBtn);

    const cancelBtn = newElement('button', { className: 'libmal-btn libmal-btn-secondary', textContent: 'Cancel', onclick: () => settings.hideModal() });
    const saveBtn = newElement('button', { className: 'libmal-btn libmal-btn-primary', textContent: 'Save & Reload', onclick: () => saveAll(scripts) });
    right.appendChild(cancelBtn);
    right.appendChild(saveBtn);

    footer.appendChild(left);
    footer.appendChild(right);
    m.appendChild(footer);

    overlay.onclick = e => { if (e.target === overlay) settings.hideModal(); };
    document.addEventListener('click', e => { if (!exportMenu.contains(e.target)) exportMenu.classList.remove('open'); });

    return overlay;
  }

  function saveAll(scripts) {
    const panels = modal.querySelectorAll('.libmal-panel[data-script-id]');
    panels.forEach(panel => {
      const id = panel.dataset.scriptId;
      const tabName = panel.dataset.tab;
      const section = scripts.get(id);
      if (!section) return;
      const tab = section.tabs.get(tabName) || section._defaultTab;
      tab.save(panel);
    });
    settings.hideModal();
    location.reload();
  }

  function exportScript(id, scripts) {
    debug(`Exporting script: ${id}`);
    const section = scripts.get(id);
    if (!section) {
      debug(`Script not found for export: ${id}`);
      return;
    }
    const data = { [id]: section.exportData() };
    debug(`Export data for ${id}:`, data);
    downloadJson(`${id}-settings.json`, data);
  }

  function exportAll(scripts) {
    const data = {};
    scripts.forEach((section, id) => { data[id] = section.exportData(); });
    downloadJson('mal-userscript-settings.json', data);
  }

  function downloadJson(filename, data) {
    const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = filename;
    a.click();
    URL.revokeObjectURL(a.href);
  }

  function importSettings(scripts) {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = '.json';
    input.onchange = e => {
      const file = e.target.files[0];
      if (!file) return;
      const reader = new FileReader();
      reader.onload = ev => {
        try {
          const data = JSON.parse(ev.target.result);
          const imported = [];
          const skipped = [];
          Object.entries(data).forEach(([id, vals]) => {
            const section = scripts.get(id);
            if (section) {
              section.importData(vals);
              imported.push(id);
            } else {
              skipped.push(id);
            }
          });
          if (imported.length === 0) {
            alert(`No matching scripts found in import file.\nFile contains: ${Object.keys(data).join(', ')}`);
            return;
          }
          if (skipped.length > 0) {
            alert(`Imported settings for: ${imported.join(', ')}\nSkipped (not installed): ${skipped.join(', ')}`);
          }
          settings.hideModal();
          location.reload();
        } catch { alert('Invalid settings file - could not parse JSON'); }
      };
      reader.readAsText(file);
    };
    input.click();
  }

  debug('Library loaded');
  return { settings, utilities, Tab, Section };
})();