// ==UserScript== // @name Knowt Flashcards - Printable Double-Sided // @namespace https://github.com/nousresearch // @version 1.2.0 // @description Extract flashcards from Knowt and generate a printable double-sided layout with terms on front and definitions/images on back. // @author Hermes Agent // @match https://knowt.com/flashcards/* // @icon https://knowt.com/favicon.ico // @grant GM_addStyle // @run-at document-idle // @downloadURL https://update.greasyfork.org/scripts/584409/Knowt%20Flashcards%20-%20Printable%20Double-Sided.user.js // @updateURL https://update.greasyfork.org/scripts/584409/Knowt%20Flashcards%20-%20Printable%20Double-Sided.meta.js // ==/UserScript== // Source: https://github.com/thepick/userscripts (function () { "use strict"; var DEBUG = true; function log() { if (!DEBUG) return; var args = Array.prototype.slice.call(arguments); args.unshift("[Knowt Print]"); console.log.apply(console, args); } // ------------------------------------------------------------ // Floating button styles // ------------------------------------------------------------ GM_addStyle(` #kt-print-btn { position: fixed !important; bottom: 24px !important; right: 24px !important; z-index: 2147483647 !important; padding: 12px 20px !important; background: #4F46E5 !important; color: #fff !important; border: none !important; border-radius: 8px !important; font-size: 15px !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; font-weight: 700 !important; cursor: pointer !important; box-shadow: 0 4px 14px rgba(79,70,229,0.4) !important; transition: transform 0.15s, box-shadow 0.15s !important; display: flex !important; align-items: center !important; gap: 8px !important; opacity: 1 !important; pointer-events: auto !important; } #kt-print-btn:hover { transform: translateY(-2px) !important; box-shadow: 0 6px 20px rgba(79,70,229,0.5) !important; } #kt-print-btn:active { transform: translateY(0) !important; } #kt-print-btn:disabled { opacity: 0.6 !important; cursor: not-allowed !important; transform: none !important; } `); // ------------------------------------------------------------ // Text helpers // ------------------------------------------------------------ function getCleanText(el) { if (!el) return ""; return String(el.textContent || "") .replace(/\s+/g, " ") .trim(); } function escapeHTML(str) { var div = document.createElement("div"); div.textContent = String(str || ""); return div.innerHTML; } function escapeAttr(str) { return String(str || "") .replace(/&/g, "&") .replace(/"/g, """) .replace(//g, ">"); } // ------------------------------------------------------------ // DOM scraping // ------------------------------------------------------------ // Knowt virtualizes its editor: scrolling replaces the mounted cards. // Use row identity and explicit sides, never visibility or text deduplication. function findAllCardContainers() { return Array.from(document.querySelectorAll('[id^="flashcardCard-"][data-card-index]')); } function extractCardData(container) { function side(name) { var editor = container.querySelector('[data-testid="flashcard-' + name + '-editor"]'); if (!editor) throw new Error("A card editor has not rendered yet. Please try again."); var area = editor; // The image is a sibling of the text column within this side's row. while (area.parentElement && area.parentElement !== container && area.parentElement.querySelectorAll('[data-testid$="-editor"]').length === 1) { area = area.parentElement; } var img = area.querySelector('img'); return {text: getCleanText(editor), image: img ? img.currentSrc || img.src || "" : ""}; } var term = side("term"); var definition = side("definition"); return { id: container.id, index: Number(container.dataset.cardIndex), term: term.text, definition: definition.text, termImageUrl: term.image, imageUrl: definition.image }; } async function collectAllCards(btn) { var list = document.querySelector('[data-testid="virtuoso-item-list"]'); if (!list) throw new Error("Open the flashcard set's Edit page, then try Print Flashcards again."); var viewport = list.closest('[data-viewport-type]'); var scroller = viewport && viewport.dataset.viewportType === "window" ? document.scrollingElement : list.closest('[data-virtuoso-scroller]'); if (!scroller) throw new Error("Could not find the flashcard scroll area."); var originalTop = scroller.scrollTop; var originalBehavior = scroller.style.scrollBehavior; var route = location.href; var cards = new Map(); var pause = function () { return new Promise(function (resolve) { setTimeout(resolve, 300); }); }; var observer = new MutationObserver(capture); function capture() { findAllCardContainers().forEach(function (row) { if (!row.querySelector('[data-testid="flashcard-term-editor"]') || !row.querySelector('[data-testid="flashcard-definition-editor"]')) return; var card = extractCardData(row); if (Number.isInteger(card.index) && card.index >= 0) cards.set(card.index, card); }); } scroller.style.scrollBehavior = "auto"; observer.observe(list, {childList: true, subtree: true, attributes: true, characterData: true}); try { scroller.scrollTop = 0; var stableBottom = 0; var previousSize = 0; var previousHeight = 0; var deadline = Date.now() + 180000; while (stableBottom < 5) { if (Date.now() > deadline) throw new Error("Collection timed out. Try again after the page finishes loading."); if (location.href !== route || !list.isConnected) throw new Error("The set changed during collection. Please try again."); await pause(); capture(); btn.textContent = "Collecting " + cards.size + " cards..."; var height = scroller.scrollHeight; var atBottom = scroller.scrollTop + scroller.clientHeight >= height - 2; stableBottom = atBottom && cards.size === previousSize && height === previousHeight ? stableBottom + 1 : 0; previousSize = cards.size; previousHeight = height; if (!atBottom) scroller.scrollTop += Math.max(100, Math.floor(scroller.clientHeight * 0.65)); } var result = Array.from(cards.values()).sort(function (a, b) { return a.index - b.index; }); if (!result.length) throw new Error("No cards were found. Open the set's Edit page and try again."); var missing = []; for (var i = 0; i <= result[result.length - 1].index; i++) { if (!cards.has(i)) missing.push(i + 1); } // Remaining virtual padding means the final rows have not mounted yet. var remaining = parseFloat(getComputedStyle(list).paddingBottom) || 0; if (missing.length || remaining > 2) { throw new Error("The set did not fully render. " + (missing.length ? "Missing card numbers: " + missing.join(", ") + ". " : "") + "Please try again and leave the page open while it scrolls."); } log("Collected", result.length, "cards in set order"); return result; } finally { observer.disconnect(); scroller.scrollTop = originalTop; scroller.style.scrollBehavior = originalBehavior; } } // ------------------------------------------------------------ // Print HTML // ------------------------------------------------------------ function getBackIndex(frontIndex, columns) { var row = Math.floor(frontIndex / columns); var col = frontIndex % columns; return row * columns + (columns - 1 - col); } function buildCardCellHTML(card, pageCardNumber, side, columns) { var html = ""; if (side === "front") { html += '
'; if (card) { if (card.termImageUrl) { html += '
'; } html += '
' + escapeHTML(card.term) + "
"; } html += "
"; return html; } var extraClass = card && card.imageUrl ? "has-image" : "no-image"; html += '
'; if (card) { if (card.imageUrl) { html += '
'; html += ''; html += "
"; } html += '
' + escapeHTML(card.definition) + "
"; } html += "
"; return html; } function buildPrintPageHTML(pageCards, pageNumber, side, perPage, columns) { var html = ""; html += '"; return html; } function buildPrintHTML(cards, perPage, columns) { var totalSheets = Math.ceil(cards.length / perPage); var pagesHTML = ""; for (var p = 0; p < totalSheets; p++) { var pageCards = cards.slice(p * perPage, (p + 1) * perPage); pagesHTML += buildPrintPageHTML(pageCards, p + 1, "front", perPage, columns); pagesHTML += buildPrintPageHTML(pageCards, p + 1, "back", perPage, columns); } return ` Knowt Flashcards - Print

Knowt Flashcards - Printable Double-Sided Cards

Best option: Click "Print all duplex", then choose double-sided printing and "Flip on long edge" in your printer dialog.

Manual option: Print fronts only first. Put the paper back in the printer, then print backs only.

The back pages are mirrored left-to-right so the definitions line up with the matching terms after duplex printing.

Important print settings: Paper size A4, scale 100, margins none, pages per sheet 1, headers and footers off.

${pagesHTML} `; } function openPrintWindow(cards, w) { if (w.closed) throw new Error("The print preview was closed. Please try again."); var html = buildPrintHTML(cards, 8, 2); w.document.open(); w.document.write(html); w.document.close(); } // ------------------------------------------------------------ // Button creation and persistence // ------------------------------------------------------------ var ktButtonObserver = null; var ktButtonInterval = null; function resetButton(btn) { btn.innerHTML = "Print Flashcards"; btn.disabled = false; } function createPrintButton() { var btn = document.createElement("button"); btn.id = "kt-print-btn"; btn.type = "button"; btn.innerHTML = "Print Flashcards"; btn.addEventListener("click", async function () { // Open during the click so popup blockers do not reject the async result. var w = window.open("", "_blank", "width=1000,height=800"); if (!w) { alert("Please allow popups for Knowt, then try again."); return; } w.document.body.textContent = "Collecting flashcards. Keep the Knowt page open and let it scroll automatically."; btn.disabled = true; try { var cards = await collectAllCards(btn); openPrintWindow(cards, w); } catch (error) { console.error("[Knowt Print]", error); if (!w.closed) w.close(); alert("Error: " + error.message); } finally { resetButton(btn); } }); return btn; } function ensurePrintButton() { if (!document.body) return; var existing = document.getElementById("kt-print-btn"); if (existing && document.body.contains(existing)) { return; } var btn = createPrintButton(); document.body.appendChild(btn); log("Print button added or restored"); } function init() { log("Knowt Flashcard Print userscript v1.2.0 loaded"); ensurePrintButton(); if (!ktButtonObserver) { ktButtonObserver = new MutationObserver(function () { ensurePrintButton(); }); ktButtonObserver.observe(document.documentElement, { childList: true, subtree: true }); } if (!ktButtonInterval) { ktButtonInterval = setInterval(function () { ensurePrintButton(); }, 1000); } } if (document.readyState === "complete" || document.readyState === "interactive") { init(); } else { window.addEventListener("DOMContentLoaded", init); } })();