您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Each browser tab has its own independent card counter, always starting at 1. No hotkeys, no totals.
// ==UserScript== // @name UCalgary Card Counter // @version 5.1 // @description Each browser tab has its own independent card counter, always starting at 1. No hotkeys, no totals. // @match https://cards.ucalgary.ca/card/* // @run-at document-end // @namespace https://greasyfork.org/users/1331386 // ==/UserScript== (() => { 'use strict'; /* ===== Per-tab unique storage ===== */ const TAB_ID = sessionStorage.getItem('ucalgaryTabId') || (crypto?.randomUUID?.() || Math.random().toString(36).slice(2)); sessionStorage.setItem('ucalgaryTabId', TAB_ID); const STORAGE_KEY = `ucalgaryVisitedCards_${TAB_ID}`; const visited = new Set(JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '[]')); const saveVisited = () => sessionStorage.setItem(STORAGE_KEY, JSON.stringify([...visited])); const isCardPage = () => /^\/card\/[^/]+/i.test(location.pathname); const getCardIdFromUrl = () => { const m = location.pathname.match(/^\/card\/([^/]+)/i); return m ? decodeURIComponent(m[1]) : null; }; /* ===== Seed counter at 1 when a tab first opens ===== */ if (isCardPage()) { const id = getCardIdFromUrl(); if (id && !visited.has(id)) { visited.add(id); saveVisited(); } } /* ===== Badge UI ===== */ let badge; function ensureBadge() { if (badge) return; badge = document.createElement('div'); Object.assign(badge.style, { position:'fixed', top:'8px', right:'8px', padding:'6px 10px', background:'#222', color:'#fff', font:'14px/1.2 system-ui, -apple-system, Segoe UI, Roboto, sans-serif', borderRadius:'6px', cursor:'pointer', zIndex:10000, opacity:0.92, boxShadow:'0 2px 6px rgba(0,0,0,0.25)', userSelect:'none', }); badge.title = 'Click to reset this tab’s counter (resets to 1)'; //badge.addEventListener('click', resetCounter); document.body.appendChild(badge); } const renderBadge = () => { if (badge) badge.textContent = `Cards: ${visited.size}`; }; const flashBadge = () => badge?.animate?.([{opacity:0.5},{opacity:0.92}], {duration:200}); function resetCounter() { visited.clear(); const id = getCardIdFromUrl(); if (id) visited.add(id); // restart at 1 saveVisited(); renderBadge(); flashBadge(); } /* ===== Count on navigation within this tab ===== */ let lastPath = ''; function onRouteChange() { ensureBadge(); if (isCardPage()) { const id = getCardIdFromUrl(); if (id && !visited.has(id)) { visited.add(id); saveVisited(); flashBadge(); } } renderBadge(); } const _ps = history.pushState; const _rs = history.replaceState; history.pushState = function() { const r = _ps.apply(this, arguments); queueRouteCheck(); return r; }; history.replaceState = function() { const r = _rs.apply(this, arguments); queueRouteCheck(); return r; }; window.addEventListener('popstate', queueRouteCheck); window.addEventListener('load', queueRouteCheck); let routeTimer; function queueRouteCheck() { const p = location.pathname + location.search; if (p === lastPath) return; lastPath = p; clearTimeout(routeTimer); routeTimer = setTimeout(onRouteChange, 100); } // Initial queueRouteCheck(); })();