Blooket Themer

A sleek theme picker for Blooket. Press Alt+T (or Cmd+T on Mac) to open.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Blooket Themer
// @namespace    https://greasyfork.org/
// @version      1.0.4
// @description  A sleek theme picker for Blooket. Press Alt+T (or Cmd+T on Mac) to open.
// @author       You
// @match        https://*.blooket.com/*
// @run-at       document-end
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function () {
  'use strict';

  // ─── THEMES ────────────────────────────────────────────────────────────────
  // Each theme has:
  //   vars      — CSS vars for the GUI panel itself
  //   navbar    — Blooket top navbar background
  //   bg        — page background color
  //   bgPattern — blooket background pattern tint (usually same as bg but lighter)
  //   btn       — primary button color
  //   btnText   — button text color
  //   text      — general page text
  //   surface   — card/surface background

  const THEMES = [
    {
      id: 'default',
      name: 'Default',
      emoji: '⬜',
      description: 'Original Blooket look',
      navbar:    null, // null = remove override (restore original)
      bg:        null,
      btn:       null,
      btnText:   null,
      text:      null,
      surface:   null,
      vars: {
        '--bt-primary': '#4f46e5', '--bt-secondary': '#7c3aed',
        '--bt-accent': '#f59e0b', '--bt-bg': '#f8fafc',
        '--bt-surface': '#ffffff', '--bt-text': '#1e293b',
        '--bt-border': '#e2e8f0', '--bt-shadow': 'rgba(79,70,229,0.15)',
      },
    },
    {
      id: 'ocean',
      name: 'Ocean Blue',
      emoji: '🌊',
      description: 'Deep & calm blues',
      navbar:  '#0369a1',
      bg:      '#0f172a',
      btn:     '#0ea5e9',
      btnText: '#ffffff',
      text:    '#e0f2fe',
      surface: '#1e293b',
      vars: {
        '--bt-primary': '#0ea5e9', '--bt-secondary': '#0284c7',
        '--bt-accent': '#38bdf8', '--bt-bg': '#0f172a',
        '--bt-surface': '#1e293b', '--bt-text': '#e0f2fe',
        '--bt-border': '#334155', '--bt-shadow': 'rgba(14,165,233,0.25)',
      },
    },
    {
      id: 'crimson',
      name: 'Crimson',
      emoji: '🔴',
      description: 'Bold, fiery reds',
      navbar:  '#b91c1c',
      bg:      '#fef2f2',
      btn:     '#ef4444',
      btnText: '#ffffff',
      text:    '#450a0a',
      surface: '#ffffff',
      vars: {
        '--bt-primary': '#ef4444', '--bt-secondary': '#dc2626',
        '--bt-accent': '#fca5a5', '--bt-bg': '#fef2f2',
        '--bt-surface': '#ffffff', '--bt-text': '#450a0a',
        '--bt-border': '#fecaca', '--bt-shadow': 'rgba(239,68,68,0.2)',
      },
    },
    {
      id: 'shadow',
      name: 'Red & Black',
      emoji: '🖤',
      description: 'Dark with red accents',
      navbar:  '#1a0000',
      bg:      '#0a0a0a',
      btn:     '#e11d48',
      btnText: '#ffffff',
      text:    '#fafafa',
      surface: '#171717',
      vars: {
        '--bt-primary': '#e11d48', '--bt-secondary': '#be123c',
        '--bt-accent': '#fb7185', '--bt-bg': '#0a0a0a',
        '--bt-surface': '#171717', '--bt-text': '#fafafa',
        '--bt-border': '#3f0d1a', '--bt-shadow': 'rgba(225,29,72,0.3)',
      },
    },
    {
      id: 'forest',
      name: 'Forest',
      emoji: '🌿',
      description: 'Earthy greens',
      navbar:  '#166534',
      bg:      '#f0fdf4',
      btn:     '#16a34a',
      btnText: '#ffffff',
      text:    '#14532d',
      surface: '#ffffff',
      vars: {
        '--bt-primary': '#16a34a', '--bt-secondary': '#15803d',
        '--bt-accent': '#86efac', '--bt-bg': '#f0fdf4',
        '--bt-surface': '#ffffff', '--bt-text': '#14532d',
        '--bt-border': '#bbf7d0', '--bt-shadow': 'rgba(22,163,74,0.2)',
      },
    },
    {
      id: 'sunset',
      name: 'Sunset',
      emoji: '🌅',
      description: 'Warm oranges & pinks',
      navbar:  '#9a3412',
      bg:      '#1c0a00',
      btn:     '#f97316',
      btnText: '#ffffff',
      text:    '#fef3c7',
      surface: '#2d1206',
      vars: {
        '--bt-primary': '#f97316', '--bt-secondary': '#ec4899',
        '--bt-accent': '#fbbf24', '--bt-bg': '#1c0a00',
        '--bt-surface': '#2d1206', '--bt-text': '#fef3c7',
        '--bt-border': '#7c2d12', '--bt-shadow': 'rgba(249,115,22,0.3)',
      },
    },
    {
      id: 'candy',
      name: 'Candy',
      emoji: '🍬',
      description: 'Sweet pastel vibes',
      navbar:  '#a21caf',
      bg:      '#fdf4ff',
      btn:     '#d946ef',
      btnText: '#ffffff',
      text:    '#4a044e',
      surface: '#ffffff',
      vars: {
        '--bt-primary': '#d946ef', '--bt-secondary': '#a855f7',
        '--bt-accent': '#f0abfc', '--bt-bg': '#fdf4ff',
        '--bt-surface': '#ffffff', '--bt-text': '#4a044e',
        '--bt-border': '#f5d0fe', '--bt-shadow': 'rgba(217,70,239,0.2)',
      },
    },
    {
      id: 'midnight',
      name: 'Midnight',
      emoji: '🌙',
      description: 'Pure dark mode',
      navbar:  '#1e1b4b',
      bg:      '#030712',
      btn:     '#6366f1',
      btnText: '#ffffff',
      text:    '#f9fafb',
      surface: '#111827',
      vars: {
        '--bt-primary': '#6366f1', '--bt-secondary': '#8b5cf6',
        '--bt-accent': '#a5b4fc', '--bt-bg': '#030712',
        '--bt-surface': '#111827', '--bt-text': '#f9fafb',
        '--bt-border': '#1f2937', '--bt-shadow': 'rgba(99,102,241,0.25)',
      },
    },
  ];

  // ─── CSS INJECTION ──────────────────────────────────────────────────────────
  GM_addStyle(`
    :root {
      --bt-primary:   #4f46e5;
      --bt-secondary: #7c3aed;
      --bt-accent:    #f59e0b;
      --bt-bg:        #f8fafc;
      --bt-surface:   #ffffff;
      --bt-text:      #1e293b;
      --bt-border:    #e2e8f0;
      --bt-shadow:    rgba(79,70,229,0.15);
    }

    /* ── GUI Panel ── */
    #bt-panel {
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%) scale(0.92);
      z-index: 999999;
      width: 360px;
      background: var(--bt-surface);
      border: 1.5px solid var(--bt-border);
      border-radius: 18px;
      box-shadow: 0 24px 60px var(--bt-shadow), 0 4px 16px rgba(0,0,0,0.12);
      font-family: 'Segoe UI', 'SF Pro Display', system-ui, sans-serif;
      color: var(--bt-text);
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.22s cubic-bezier(.4,0,.2,1),
                  transform 0.22s cubic-bezier(.4,0,.2,1);
      overflow: hidden;
    }
    #bt-panel.bt-open {
      opacity: 1;
      pointer-events: all;
      transform: translate(-50%, -50%) scale(1);
    }

    /* Header */
    #bt-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 18px 20px 14px;
      border-bottom: 1.5px solid var(--bt-border);
      background: linear-gradient(135deg, var(--bt-primary), var(--bt-secondary));
    }
    #bt-header-left {
      display: flex;
      align-items: center;
      gap: 10px;
    }
    #bt-logo {
      width: 30px;
      height: 30px;
      border-radius: 8px;
      background: rgba(255,255,255,0.25);
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 16px;
    }
    #bt-title {
      font-size: 15px;
      font-weight: 700;
      color: #fff;
      letter-spacing: -0.3px;
    }
    #bt-subtitle {
      font-size: 11px;
      color: rgba(255,255,255,0.75);
      margin-top: 1px;
    }
    #bt-close {
      width: 28px;
      height: 28px;
      border-radius: 50%;
      background: rgba(255,255,255,0.18);
      border: none;
      cursor: pointer;
      color: #fff;
      font-size: 16px;
      display: flex;
      align-items: center;
      justify-content: center;
      line-height: 1;
      transition: background 0.15s;
    }
    #bt-close:hover { background: rgba(255,255,255,0.32); }

    /* Theme grid */
    #bt-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 10px;
      padding: 16px;
    }
    .bt-theme-card {
      border-radius: 12px;
      border: 2px solid var(--bt-border);
      padding: 12px;
      cursor: pointer;
      background: var(--bt-bg);
      transition: border-color 0.15s, transform 0.12s, box-shadow 0.15s;
      position: relative;
      overflow: hidden;
    }
    .bt-theme-card:hover {
      transform: translateY(-2px);
      box-shadow: 0 6px 20px var(--bt-shadow);
    }
    .bt-theme-card.bt-active {
      border-color: var(--bt-primary);
      box-shadow: 0 0 0 3px var(--bt-shadow);
    }
    .bt-theme-card.bt-active::after {
      content: '✓';
      position: absolute;
      top: 6px;
      right: 8px;
      font-size: 11px;
      font-weight: 800;
      color: var(--bt-primary);
    }
    .bt-card-swatch {
      height: 28px;
      border-radius: 7px;
      margin-bottom: 8px;
    }
    .bt-card-name {
      font-size: 12.5px;
      font-weight: 700;
      color: var(--bt-text);
    }
    .bt-card-desc {
      font-size: 10.5px;
      color: var(--bt-text);
      opacity: 0.55;
      margin-top: 2px;
    }
    .bt-card-emoji {
      font-size: 14px;
      margin-bottom: 4px;
      display: block;
    }

    /* Footer */
    #bt-footer {
      padding: 10px 16px 14px;
      text-align: center;
      font-size: 10.5px;
      color: var(--bt-text);
      opacity: 0.4;
      border-top: 1.5px solid var(--bt-border);
    }

    /* Overlay */
    #bt-overlay {
      position: fixed;
      inset: 0;
      z-index: 999998;
      background: rgba(0,0,0,0.35);
      backdrop-filter: blur(2px);
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.22s;
    }
    #bt-overlay.bt-open {
      opacity: 1;
      pointer-events: all;
    }

    /* Toast */
    #bt-toast {
      position: fixed;
      bottom: 28px;
      left: 50%;
      transform: translateX(-50%) translateY(16px);
      z-index: 9999999;
      background: var(--bt-primary);
      color: #fff;
      padding: 9px 20px;
      border-radius: 999px;
      font-size: 13px;
      font-weight: 600;
      font-family: 'Segoe UI', system-ui, sans-serif;
      box-shadow: 0 4px 18px var(--bt-shadow);
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.2s, transform 0.2s;
    }
    #bt-toast.bt-show {
      opacity: 1;
      transform: translateX(-50%) translateY(0);
    }
  `);

  // ─── BUILD UI ───────────────────────────────────────────────────────────────
  const overlay = document.createElement('div');
  overlay.id = 'bt-overlay';
  document.body.appendChild(overlay);

  const panel = document.createElement('div');
  panel.id = 'bt-panel';
  panel.innerHTML = `
    <div id="bt-header">
      <div id="bt-header-left">
        <div id="bt-logo">🎨</div>
        <div>
          <div id="bt-title">Blooket Themer</div>
          <div id="bt-subtitle">Pick your vibe</div>
        </div>
      </div>
      <button id="bt-close" title="Close (Alt+T)">✕</button>
    </div>
    <div id="bt-grid"></div>
    <div id="bt-footer">Alt+B / ⌘+B to toggle · Themes save automatically</div>
  `;
  document.body.appendChild(panel);

  const toast = document.createElement('div');
  toast.id = 'bt-toast';
  document.body.appendChild(toast);

  // ─── POPULATE GRID ─────────────────────────────────────────────────────────
  const grid = panel.querySelector('#bt-grid');
  let activeId = GM_getValue('bt-theme', 'default');

  function buildGrid() {
    grid.innerHTML = '';
    THEMES.forEach(theme => {
      const card = document.createElement('div');
      card.className = 'bt-theme-card' + (theme.id === activeId ? ' bt-active' : '');
      card.dataset.id = theme.id;

      // Build a mini gradient swatch from the theme vars
      const { '--bt-primary': p, '--bt-secondary': s, '--bt-accent': a } = theme.vars;
      card.innerHTML = `
        <span class="bt-card-emoji">${theme.emoji}</span>
        <div class="bt-card-swatch" style="background: linear-gradient(120deg, ${p}, ${s}, ${a});"></div>
        <div class="bt-card-name">${theme.name}</div>
        <div class="bt-card-desc">${theme.description}</div>
      `;
      card.addEventListener('click', () => applyTheme(theme.id));
      grid.appendChild(card);
    });
  }

  // ─── APPLY THEME ───────────────────────────────────────────────────────────
  function applyTheme(id) {
    const theme = THEMES.find(t => t.id === id);
    if (!theme) return;
    activeId = id;
    GM_setValue('bt-theme', id);

    const root = document.documentElement;
    Object.entries(theme.vars).forEach(([k, v]) => root.style.setProperty(k, v));

    buildGrid();
    showToast(`${theme.emoji} ${theme.name} applied!`);
  }

  // ─── TOAST ─────────────────────────────────────────────────────────────────
  let toastTimer;
  function showToast(msg) {
    toast.textContent = msg;
    toast.classList.add('bt-show');
    clearTimeout(toastTimer);
    toastTimer = setTimeout(() => toast.classList.remove('bt-show'), 2200);
  }

  // ─── OPEN / CLOSE ──────────────────────────────────────────────────────────
  function openPanel() {
    panel.classList.add('bt-open');
    overlay.classList.add('bt-open');
  }
  function closePanel() {
    panel.classList.remove('bt-open');
    overlay.classList.remove('bt-open');
  }
  function togglePanel() {
    panel.classList.contains('bt-open') ? closePanel() : openPanel();
  }

  panel.querySelector('#bt-close').addEventListener('click', closePanel);
  overlay.addEventListener('click', closePanel);

  // ─── KEYBOARD SHORTCUT ─────────────────────────────────────────────────────
  window.addEventListener('keydown', (e) => {
    const isMac = navigator.platform.toUpperCase().includes('MAC');
    const trigger = isMac
      ? (e.metaKey && !e.altKey && (e.key === 'b' || e.key === 'B'))
      : (e.altKey && (e.key === 'b' || e.key === 'B'));
    if (trigger) {
      e.preventDefault();
      e.stopPropagation();
      togglePanel();
    }
  }, true); // useCapture=true so it fires before React can swallow it

  // ─── INIT ───────────────────────────────────────────────────────────────────
  buildGrid();
  applyTheme(activeId); // restore saved theme on load

})();