Sleazy Fork is available in English.

Seiya-saiga Checkbox Tracker

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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());
})();