您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Card pages: number keys to answer, Enter/~/etc., floating counter (click badge or ⌃⌥R to reset).
当前为
// ==UserScript== // @name UCalgary Card & Collection Keyboard Shortcuts // @description Card pages: number keys to answer, Enter/~/etc., floating counter (click badge or ⌃⌥R to reset). // @version 4.3 // @author you // @namespace https://greasyfork.org/users/1331386 // @match https://cards.ucalgary.ca/card/* // @match https://cards.ucalgary.ca/collection/* // @match https://cards.ucalgary.ca/collection* // @grant GM_registerMenuCommand // @run-at document-end // ==/UserScript== (() => { 'use strict'; /* ─────────────── helpers ─────────────── */ const isCardPage = location.pathname.startsWith('/card/'); const isCollectionPage = location.pathname.startsWith('/collection/') && location.pathname !== '/collection'; const isCollectionRootPage = location.pathname === '/collection'; // keyboard 0-19 index (Shift+1-0 → 10-19) const key2index = { Digit1: 0, Digit2: 1, Digit3: 2, Digit4: 3, Digit5: 4, Digit6: 5, Digit7: 6, Digit8: 7, Digit9: 8, Digit0: 9, }; const getOptionIndex = e => e.code in key2index ? key2index[e.code] + (e.shiftKey ? 10 : 0) : null; /* ════════════════════════════════════════ Card counter (and shared storage) ════════════════════════════════════════ */ const STORAGE_KEY = 'ucalgaryVisitedCards'; // Set<string> of card IDs const TOTAL_KEY = 'ucalgaryDeckTotal'; // Number: deck size const visited = new Set(JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')); let totalCards = Number(localStorage.getItem(TOTAL_KEY) || 0); function saveVisited() { localStorage.setItem(STORAGE_KEY, JSON.stringify([...visited])); } /* ---- badge UI ---- */ let badge = null; function ensureBadge() { if (badge) return; badge = document.createElement('div'); Object.assign(badge.style, { position:'fixed', top:'8px', right:'8px', padding:'4px 10px', background:'#333', color:'#fff', font:'14px/1.2 sans-serif', borderRadius:'4px', cursor:'pointer', zIndex:10000, opacity:0.9, userSelect:'none', }); badge.addEventListener('click', resetCounter); document.body.appendChild(badge); } function renderBadge() { if (!badge) return; badge.textContent = totalCards > 0 ? `${visited.size} / ${totalCards}` : `Cards: ${visited.size}`; } function resetCounter() { localStorage.setItem(STORAGE_KEY, '[]'); // clear IDs visited.clear(); if (isCardPage && cardID) visited.add(cardID); // restart with current card saveVisited(); renderBadge(); badge?.animate([{opacity:0.4},{opacity:0.9}], {duration:300}); // keep TOTAL_KEY as-is (manual reset shouldn't forget deck size) } /* ---- storage sync across tabs ---- */ window.addEventListener('storage', e => { if (e.key === STORAGE_KEY) { visited.clear(); JSON.parse(e.newValue || '[]').forEach(id => visited.add(id)); renderBadge(); } else if (e.key === TOTAL_KEY) { totalCards = Number(e.newValue || 0); renderBadge(); } }); /* ---- install badge & counting only on /card/ ---- */ let cardID = null; if (isCardPage) { const m = location.pathname.match(/\/card\/(\d+)/); if (m) { cardID = m[1]; visited.add(cardID); saveVisited(); } ensureBadge(); renderBadge(); } /* ════════════════════════════════════════ Shortcut logic (card & collection pages) ════════════════════════════════════════ */ /* ---- /card/ helpers ---- */ function addCardHints() { document.querySelectorAll('form.question').forEach(form => { form.querySelectorAll('.option label').forEach((label, i) => { if (label.dataset.hinted) return; label.insertAdjacentHTML( 'afterbegin', `<span style="font-weight:600;margin-right:4px;">(${i+1})</span>` ); label.dataset.hinted = 'true'; }); }); } function clearSelections() { document.querySelectorAll( 'form.question input[type="radio"], form.question input[type="checkbox"]' ).forEach(inp => (inp.checked = false)); } function handleEnter() { const submitBtn = document.querySelector('form.question .submit button'); const nextBtn = document.querySelector('#next'); const reviewBtn = document.querySelector('div.actions span.review-buttons a.save'); if (submitBtn && submitBtn.offsetParent) submitBtn.click(); else if (nextBtn && nextBtn.offsetParent) nextBtn.click(); else if (reviewBtn&& reviewBtn.offsetParent) reviewBtn.click(); } /* ---- /collection/ helpers ---- */ function addCollectionHints() { document.querySelectorAll('table.table-striped tbody tr').forEach((row, i) => { const name = row.querySelector('a.deck-name'); if (!name || name.dataset.hinted) return; name.insertAdjacentHTML( 'afterbegin', `<span style="font-weight:600;margin-right:4px;">(${i+1})</span>` ); name.dataset.hinted = 'true'; }); } function playDeck(index) { const buttons = document.querySelectorAll('a.btn.action.save'); const btn = buttons[index + 1]; // index+1 because first is “Details” if (!btn) return; /* -- read "x / y" label in the same row and keep the denominator -- */ const label = btn.closest('tr')?.querySelector('span.label'); const m = label?.textContent.trim().match(/\/\s*(\d+)/); // “6 / 13” → 13 totalCards = m ? Number(m[1]) : 0; localStorage.setItem(TOTAL_KEY, String(totalCards)); /* reset IDs BEFORE the first card loads */ resetCounter(); /* open deck in centred window */ const w=1000,h=800,l=Math.round((screen.width-w)/2),t=Math.round((screen.height-h)/2); const feats=[ 'noopener','noreferrer','scrollbars=yes','resizable=yes', `width=${w}`,`height=${h}`,`left=${l}`,`top=${t}` ].join(','); const win = window.open(btn.href,'_blank',feats); if (win) win.focus(); } /* /collection root – Enter opens first visible “Details” bag */ function openFirstBagDetails() { const bags = document.querySelectorAll('.bag'); for (const bag of bags) { if (bag.offsetParent === null) continue; // hidden by filter const detailsBtn = bag.querySelector('a.btn.deck-details'); if (detailsBtn) { window.open(detailsBtn.href, '_blank', 'noopener'); break; } } } /* ---- global key handler ---- */ document.addEventListener('keydown', e => { const index = getOptionIndex(e); if (isCollectionRootPage && e.key === 'Enter') { openFirstBagDetails(); e.preventDefault(); return; } if (isCardPage) { if (index !== null) { const radios = document.querySelectorAll('form.question input[type="radio"]'); const checks = document.querySelectorAll('form.question input[type="checkbox"]'); if (radios[index]) { radios[index].checked = true; radios[index].scrollIntoView({behavior:'smooth',block:'center'}); } if (checks[index]) { checks[index].checked = !checks[index].checked; checks[index].scrollIntoView({behavior:'smooth',block:'center'}); } return; } if (e.key === 'Enter') handleEnter(); if (e.key === '~') clearSelections(); } if (isCollectionPage && index !== null) { playDeck(index); } }); /* ---- observers / init ---- */ function initHints() { if (isCardPage) addCardHints(); if (isCollectionPage) addCollectionHints(); } window.addEventListener('load', initHints); document.addEventListener('DOMContentLoaded', initHints); new MutationObserver(initHints).observe(document.body, {childList:true,subtree:true}); })();