Seiya-saiga Checkbox Tracker

Save/restore checkbox states on seiya-saiga / galge.seiya-saiga guide pages

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         Seiya-saiga Checkbox Tracker
// @namespace    https://greasyfork.org/en/users/890485-asafd
// @version      1.0.0
// @description  Save/restore checkbox states on seiya-saiga / galge.seiya-saiga guide pages
// @match        https://galge.seiya-saiga.com/game/*
// @match        https://seiya-saiga.com/game/*
// @run-at       document-end
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // --------------------------
  // Config
  // --------------------------
  const PAGE_KEY = `tm_checkbox_state::${location.origin}${location.pathname}`;
  const SAVE_DEBOUNCE_MS = 250;

  const PANEL_HIDDEN_KEY = `tm_checkbox_panel_hidden::${location.origin}`;

  // --------------------------
  // Utils
  // --------------------------
  function safeJsonParse(s, fallback) {
    try { return JSON.parse(s); } catch { return fallback; }
  }

  function getOptionText(cb) {
    let n = cb.nextSibling;
    while (n && n.nodeType === Node.TEXT_NODE && !n.textContent.trim()) n = n.nextSibling;
    if (n && n.nodeType === Node.TEXT_NODE) return n.textContent.trim();

    const p = cb.parentElement;
    if (!p) return '';
    return (p.textContent || '').replace(/\s+/g, ' ').trim();
  }

  function createEl(tag, props = {}, children = []) {
    const el = document.createElement(tag);
    Object.assign(el, props);
    for (const c of children) el.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
    return el;
  }

  function toast(msg) {
    const t = createEl('div', {
      textContent: msg,
      style: `
        position: fixed; left: 50%; top: 14px; transform: translateX(-50%);
        background: rgba(0,0,0,.82); color: #fff; padding: 8px 12px;
        border-radius: 10px; font-size: 12px; z-index: 999999;
        max-width: 80vw; white-space: pre-wrap;
      `
    });
    document.body.appendChild(t);
    setTimeout(() => t.remove(), 2200);
  }

  // --------------------------
  // Load / Save state
  // --------------------------
  let state = GM_getValue(PAGE_KEY, null);
  if (typeof state === 'string') state = safeJsonParse(state, null);
  if (!state || typeof state !== 'object') {
    state = { meta: { url: location.href, total: 0, updatedAt: 0 }, map: {} };
  }

  let saveTimer = null;
  function scheduleSave() {
    if (saveTimer) clearTimeout(saveTimer);
    saveTimer = setTimeout(() => {
      state.meta.updatedAt = Date.now();
      GM_setValue(PAGE_KEY, JSON.stringify(state));
      saveTimer = null;
    }, SAVE_DEBOUNCE_MS);
  }

  // --------------------------
  // Build checkbox keys
  // --------------------------
  const checkboxes = Array.from(document.querySelectorAll('input[type="checkbox"]'));

  const textCount = new Map();
  const keys = new Map(); // checkbox -> key

  for (const cb of checkboxes) {
    const text = getOptionText(cb) || '__EMPTY__';
    const cnt = (textCount.get(text) || 0) + 1;
    textCount.set(text, cnt);
    const key = `${text}@@${cnt}`;
    keys.set(cb, key);
    cb.dataset.tmKey = key;
  }

  // Meta(不弹窗)
  const prevTotal = state.meta.total || 0;
  state.meta.total = checkboxes.length;
  if (prevTotal && prevTotal !== checkboxes.length) {
    console.warn(`[CheckboxTracker] checkbox count changed: ${prevTotal} -> ${checkboxes.length}`);
  }

  // Restore(不弹窗)
  for (const cb of checkboxes) {
    const key = keys.get(cb);
    if (state.map[key] === true) cb.checked = true;
  }

  // --------------------------
  // UI Panel
  // --------------------------
  function countChecked() {
    let c = 0;
    for (const cb of checkboxes) if (cb.checked) c++;
    return c;
  }

  const panel = createEl('div', {
    style: `
      position: fixed; right: 14px; top: 14px; z-index: 999998;
      background: rgba(255,255,255,.92);
      border: 1px solid rgba(0,0,0,.15);
      border-radius: 12px;
      padding: 10px 10px 8px 10px;
      font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial;
      font-size: 12px; color: #111;
      box-shadow: 0 8px 30px rgba(0,0,0,.18);
      min-width: 180px;
      user-select: none;
    `
  });

  const title = createEl('div', { style: 'font-weight: 700; margin-bottom: 6px;' }, ['✅ 选项记录']);

  const progress = createEl('div', { style: 'margin-bottom: 8px; opacity: .9;' });
  function refreshProgress() {
    progress.textContent = `进度:${countChecked()} / ${checkboxes.length}`;
  }

  const btnRow = createEl('div', { style: 'display: flex; gap: 6px; flex-wrap: wrap;' });

  function mkBtn(text, onClick) {
    return createEl('button', {
      type: 'button',
      textContent: text,
      style: `
        padding: 5px 8px; border-radius: 10px;
        border: 1px solid rgba(0,0,0,.2);
        background: rgba(255,255,255,.9);
        cursor: pointer; font-size: 12px;
      `,
      onclick: onClick
    });
  }

  const btnClear = mkBtn('清空本页', () => {
    state.map = {};
    for (const cb of checkboxes) cb.checked = false;
    scheduleSave();
    refreshProgress();
    toast('已清空本页记录');
  });

  const btnHide = mkBtn('隐藏', () => {
    panel.style.display = 'none';
    GM_setValue(PANEL_HIDDEN_KEY, true);
    toast('面板已隐藏:Tampermonkey 菜单里可“显示面板”');
  });

  btnRow.append(btnClear, btnHide);
  panel.append(title, progress, btnRow);
  document.body.appendChild(panel);
  refreshProgress();

  const panelHidden = !!GM_getValue(PANEL_HIDDEN_KEY, false);
  if (panelHidden) {
    panel.style.display = 'none';
  }

  // --------------------------
  // Listen changes
  // --------------------------
  function onChanged(ev) {
    const cb = ev.target;
    if (!(cb instanceof HTMLInputElement) || cb.type !== 'checkbox') return;
    const key = cb.dataset.tmKey;
    if (!key) return;

    state.map[key] = cb.checked === true;
    scheduleSave(); // 仅保存,不提示
    refreshProgress();
  }

  document.addEventListener('change', onChanged, true);

  // --------------------------
  // Tampermonkey menu commands
  // --------------------------
  function showPanel() {
    panel.style.display = '';
    GM_setValue(PANEL_HIDDEN_KEY, false); // ✅ 取消隐藏记忆
  }

  GM_registerMenuCommand('显示面板', showPanel);
  GM_registerMenuCommand('清空本页记录', () => btnClear.click());
})();