您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
cards hotkeys and counter
- // ==UserScript==
- // @name UCalgary Card & Collection Keyboard Shortcuts
- // @version 5.0
- // @description cards hotkeys and counter
- // @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*
- // @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});
- })();