NitroType Racer Info Tab (Deeper Polish)

Movable, resizable tab showing extra Nitro Type racer info with refined aesthetics and minimal fields

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         NitroType Racer Info Tab (Deeper Polish)
// @namespace    http://tampermonkey.net/
// @version      2.5
// @description  Movable, resizable tab showing extra Nitro Type racer info with refined aesthetics and minimal fields
// @match        https://www.nitrotype.com/racer/*
// @grant        none
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function() {
  'use strict';

  const LS_KEY = 'nt_racer_info_tab_prefs';
  const prefs = loadPrefs();

  function loadPrefs() {
    try {
      return JSON.parse(localStorage.getItem(LS_KEY)) || {
        x: null,
        y: null,
        width: 360,
        visible: true,
        theme: 'dark'
      };
    } catch {
      return { x: null, y: null, width: 360, visible: true, theme: 'dark' };
    }
  }
  function savePrefs() {
    localStorage.setItem(LS_KEY, JSON.stringify(prefs));
  }

  // Keyboard shortcut to toggle (Alt+R)
  document.addEventListener('keydown', (e) => {
    if (e.altKey && e.key.toLowerCase() === 'r') {
      prefs.visible = !prefs.visible;
      savePrefs();
      const panel = document.getElementById('nt-racer-panel');
      if (panel) panel.style.display = prefs.visible ? 'block' : 'none';
    }
  });

  // Toggle button (bottom-right)
  function createToggleButton() {
    if (document.getElementById('nt-racer-toggle')) return;
    const btn = document.createElement('button');
    btn.id = 'nt-racer-toggle';
    btn.textContent = 'Racer Info';
    Object.assign(btn.style, {
      position: 'fixed',
      bottom: '14px',
      right: '14px',
      padding: '8px 12px',
      borderRadius: '10px',
      border: 'none',
      fontFamily: 'system-ui, Arial, sans-serif',
      fontSize: '13px',
      cursor: 'pointer',
      zIndex: '99999',
      boxShadow: '0 10px 20px rgba(0,0,0,0.25)',
      transition: 'transform 120ms ease, box-shadow 150ms ease'
    });
    styleButtonTheme(btn);
    btn.addEventListener('mouseenter', () => {
      btn.style.transform = 'translateY(-1px)';
      btn.style.boxShadow = '0 14px 26px rgba(0,0,0,0.30)';
    });
    btn.addEventListener('mouseleave', () => {
      btn.style.transform = 'translateY(0)';
      btn.style.boxShadow = '0 10px 20px rgba(0,0,0,0.25)';
    });
    btn.addEventListener('click', () => {
      prefs.visible = !prefs.visible;
      savePrefs();
      const panel = document.getElementById('nt-racer-panel');
      if (panel) panel.style.display = prefs.visible ? 'block' : 'none';
    });
    document.body.appendChild(btn);
  }

  function stylePanelTheme(el) {
    const dark = prefs.theme === 'dark';
    el.style.background = dark
      ? 'linear-gradient(180deg, rgba(24,24,28,0.90), rgba(16,16,22,0.88))'
      : 'linear-gradient(180deg, rgba(255,255,255,0.96), rgba(248,248,250,0.94))';
    el.style.color = dark ? '#f2f2f2' : '#1a1a1a';
    el.style.border = dark ? '1px solid rgba(255,255,255,0.08)' : '1px solid rgba(0,0,0,0.08)';
    el.style.backdropFilter = 'blur(8px)';
  }
  function styleChip(el) {
    const dark = prefs.theme === 'dark';
    el.style.background = dark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)';
    el.style.color = dark ? '#e8e8e8' : '#2a2a2a';
    el.style.border = dark ? '1px solid rgba(255,255,255,0.10)' : '1px solid rgba(0,0,0,0.10)';
    el.style.borderRadius = '10px';
    el.style.padding = '6px 8px';
    el.style.fontSize = '12px';
  }
  function styleButtonTheme(el) {
    const dark = prefs.theme === 'dark';
    el.style.background = dark
      ? 'linear-gradient(180deg,#2b2b31,#23232a)'
      : 'linear-gradient(180deg,#f3f3f7,#e9e9ee)';
    el.style.color = dark ? '#fafafa' : '#1e1e1e';
  }

  function whenReady(cb) {
    const ntg = window.NTGLOBALS;
    if (ntg && ntg.RACER_INFO && ntg.RACER_INFO.username) {
      return cb(ntg.RACER_INFO);
    }
    setTimeout(() => whenReady(cb), 150);
  }

  function buildPanel(info) {
    // Compute unique garage IDs count (no list shown)
    const uniqueCount = Array.isArray(info.garage) ? new Set(info.garage).size : 0;

    // Create panel
    const panel = document.createElement('div');
    panel.id = 'nt-racer-panel';
    Object.assign(panel.style, {
      position: 'fixed',
      top: prefs.y || '80px',
      right: prefs.x ? 'auto' : '40px',
      left: prefs.x || 'auto',
      width: (prefs.width || 360) + 'px',
      minWidth: '280px',
      borderRadius: '14px',
      boxShadow: '0 18px 40px rgba(0,0,0,0.35)',
      fontFamily: 'system-ui, Arial, sans-serif',
      fontSize: '13px',
      lineHeight: '1.55em',
      zIndex: '99999',
      display: prefs.visible ? 'block' : 'none',
      transition: 'opacity 160ms ease, transform 160ms ease'
    });
    stylePanelTheme(panel);

    // Header (draggable)
    const header = document.createElement('div');
    header.style.display = 'flex';
    header.style.alignItems = 'center';
    header.style.justifyContent = 'space-between';
    header.style.padding = '12px 14px';
    header.style.cursor = 'move';
    header.style.userSelect = 'none';
    header.style.borderTopLeftRadius = '14px';
    header.style.borderTopRightRadius = '14px';
    header.style.background = 'rgba(255,255,255,0.06)';

    const title = document.createElement('div');
    title.textContent = 'Racer Info (Extras)';
    title.style.fontWeight = '800';
    title.style.letterSpacing = '0.2px';
    header.appendChild(title);

    // Actions: theme toggle + close
    const actions = document.createElement('div');
    actions.style.display = 'flex';
    actions.style.gap = '8px';

    const themeBtn = document.createElement('button');
    themeBtn.textContent = prefs.theme === 'dark' ? 'Light' : 'Dark';
    styleChip(themeBtn);
    themeBtn.title = 'Toggle theme';
    themeBtn.addEventListener('click', () => {
      prefs.theme = prefs.theme === 'dark' ? 'light' : 'dark';
      themeBtn.textContent = prefs.theme === 'dark' ? 'Light' : 'Dark';
      savePrefs();
      stylePanelTheme(panel);
      [themeBtn, closeBtn].forEach(el => styleChip(el));
    });

    const closeBtn = document.createElement('button');
    closeBtn.textContent = 'Hide';
    styleChip(closeBtn);
    closeBtn.title = 'Hide panel';
    closeBtn.addEventListener('click', () => {
      prefs.visible = false;
      savePrefs();
      panel.style.display = 'none';
    });

    actions.appendChild(themeBtn);
    actions.appendChild(closeBtn);
    header.appendChild(actions);
    panel.appendChild(header);

    // Body table (only extra fields, minimal)
    const body = document.createElement('div');
    body.style.padding = '10px 14px 6px 14px';

    const table = document.createElement('table');
    table.style.width = '100%';
    table.style.borderCollapse = 'collapse';

    function addRow(label, value) {
      const tr = document.createElement('tr');
      const td1 = document.createElement('td');
      const td2 = document.createElement('td');
      td1.textContent = label;
      td1.style.fontWeight = '700';
      td1.style.padding = '6px 8px 4px 0';
      td1.style.verticalAlign = 'top';
      td2.textContent = value;
      td2.style.padding = '6px 0 4px 0';
      tr.appendChild(td1);
      tr.appendChild(td2);
      table.appendChild(tr);
    }

    addRow('Username', safe(info.username));
    addRow('Cars owned (unique)', uniqueCount);
    addRow('Nitros', `${safe(info.nitros)} (Used: ${safe(info.nitrosUsed)})`);
    addRow('Longest session', safe(info.longestSession));
    addRow('Level', safe(info.level));
    addRow('Experience', safe(info.experience));
    addRow('Car ID (current)', safe(info.carID));
    addRow('Unique garage IDs', uniqueCount);

    body.appendChild(table);
    panel.appendChild(body);

    // Footer credit
    const footer = document.createElement('div');
    footer.innerHTML = `Made By <a href="https://www.youtube.com/@InternetTyper" target="_blank" rel="noopener noreferrer" style="color:#58a6ff; text-decoration:none; font-weight:700;">@InternetTyper On YouTube</a>`;
    Object.assign(footer.style, {
      textAlign: 'center',
      padding: '10px 12px',
      fontSize: '12px',
      borderTop: '1px solid rgba(255,255,255,0.1)'
    });
    panel.appendChild(footer);

    // Resize handle (visual + functional)
    const handle = document.createElement('div');
    Object.assign(handle.style, {
      position: 'absolute',
      width: '12px',
      height: '12px',
      right: '8px',
      bottom: '8px',
      borderRadius: '3px',
      cursor: 'nwse-resize'
    });
    styleChip(handle);
    panel.appendChild(handle);

    // Dragging logic
    let dragging = false, offsetX = 0, offsetY = 0;
    header.addEventListener('mousedown', (e) => {
      dragging = true;
      offsetX = e.clientX - panel.offsetLeft;
      offsetY = e.clientY - panel.offsetTop;
      document.addEventListener('mousemove', onDrag);
      document.addEventListener('mouseup', endDrag);
    });
    function onDrag(e) {
      if (!dragging) return;
      panel.style.left = (e.clientX - offsetX) + 'px';
      panel.style.top = (e.clientY - offsetY) + 'px';
      panel.style.right = 'auto';
      prefs.x = panel.style.left;
      prefs.y = panel.style.top;
      savePrefs();
    }
    function endDrag() {
      dragging = false;
      document.removeEventListener('mousemove', onDrag);
      document.removeEventListener('mouseup', endDrag);
    }

    // Resizing logic
    let resizing = false, startX = 0, startWidth = 0;
    handle.addEventListener('mousedown', (e) => {
      resizing = true;
      startX = e.clientX;
      startWidth = panel.offsetWidth;
      document.addEventListener('mousemove', onResize);
      document.addEventListener('mouseup', endResize);
    });
    function onResize(e) {
      if (!resizing) return;
      const delta = e.clientX - startX;
      const newW = Math.max(300, Math.min(520, startWidth + delta));
      panel.style.width = newW + 'px';
      prefs.width = newW;
      savePrefs();
    }
    function endResize() {
      resizing = false;
      document.removeEventListener('mousemove', onResize);
      document.removeEventListener('mouseup', endResize);
    }

    document.body.appendChild(panel);
  }

  function safe(v) {
    if (v === undefined || v === null) return '—';
    return String(v);
  }

  function init() {
    whenReady((info) => {
      // Build once
      if (!document.getElementById('nt-racer-panel')) {
        buildPanel(info);
      }
      // Provide toggle button
      createToggleButton();
    });

    // SPA-aware: re-check on DOM mutations in case profile content changes
    const obs = new MutationObserver(() => {
      const ntg = window.NTGLOBALS;
      if (!document.getElementById('nt-racer-panel') && ntg?.RACER_INFO?.username) {
        buildPanel(ntg.RACER_INFO);
      }
    });
    obs.observe(document.documentElement, { childList: true, subtree: true });
  }

  window.addEventListener('load', init);
})();