Copy HTML to Anki (Refresh Before Each Copy)

Always refresh the page before copying HTML to Anki. You can choose normal or random ID via floating menu or keyboard shortcut. After reload, it auto-copies once then clears the flag so you can repeat when needed.

  1. // ==UserScript==
  2. // @name Copy HTML to Anki (Refresh Before Each Copy)
  3. // @namespace http://tampermonkey.net/
  4. // @version 5.0
  5. // @description Always refresh the page before copying HTML to Anki. You can choose normal or random ID via floating menu or keyboard shortcut. After reload, it auto-copies once then clears the flag so you can repeat when needed.
  6. // @author nabe
  7. // @match https://cards.ucalgary.ca/card/*
  8. // @grant GM_xmlhttpRequest
  9. // @connect localhost
  10. // @run-at document-end
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // LOCALSTORAGE KEY used to store whether we want to run "copyHtmlToAnki"
  18. // after the page reloads, and whether we want random ID or not.
  19. const REFRESH_COPY_KEY = 'doRefreshCopyMode';
  20. // possible values: "normal" or "random" (or null/undefined if not set)
  21.  
  22. /********************************************************
  23. * 1) Check localStorage on load. If set => do the copy.
  24. ********************************************************/
  25. const pendingMode = localStorage.getItem(REFRESH_COPY_KEY);
  26. if (pendingMode) {
  27. console.log('[RefreshAnki] Detected pendingMode =', pendingMode);
  28.  
  29. // Clear so we only do it once
  30. localStorage.removeItem(REFRESH_COPY_KEY);
  31.  
  32. // Actually do the copy with or without random ID
  33. if (pendingMode === 'random') {
  34. copyHtmlToAnki(true);
  35. } else {
  36. copyHtmlToAnki(false);
  37. }
  38. }
  39.  
  40. /********************************************************
  41. * 2) MAIN FUNCTION - Copy HTML to Anki
  42. ********************************************************/
  43. function copyHtmlToAnki(generateRandomID = false) {
  44. console.log(`[RefreshAnki] copyHtmlToAnki() called. generateRandomID = ${generateRandomID}`);
  45.  
  46. // Helper: convert relative URLs to absolute
  47. function makeAbsolute(url) {
  48. return new URL(url, document.baseURI).href;
  49. }
  50.  
  51. // Helper: random numeric string
  52. function generateRandomString(length) {
  53. let result = '';
  54. const characters = '0123456789';
  55. for (let i = 0; i < length; i++) {
  56. result += characters.charAt(Math.floor(Math.random() * characters.length));
  57. }
  58. return result;
  59. }
  60.  
  61. // Clone the entire document
  62. let docClone = document.documentElement.cloneNode(true);
  63.  
  64. // Convert all [src] and [href] to absolute
  65. let elements = docClone.querySelectorAll('[src], [href]');
  66. elements.forEach(el => {
  67. if (el.hasAttribute('src')) {
  68. el.setAttribute('src', makeAbsolute(el.getAttribute('src')));
  69. }
  70. if (el.hasAttribute('href')) {
  71. el.setAttribute('href', makeAbsolute(el.getAttribute('href')));
  72. }
  73. });
  74.  
  75. // Example extractions (adjust selectors as needed):
  76. let frontElement = docClone.querySelector('.container.card');
  77. let frontField = frontElement ? frontElement.innerHTML : '';
  78.  
  79. let frontImageUrl = docClone.querySelector('.group.img a.img-fill')?.getAttribute('href') || '';
  80.  
  81. let photoQuestionElement = docClone.querySelector('.solution.container .photo-question');
  82. let photoQuestion = photoQuestionElement ? photoQuestionElement.outerHTML : '';
  83.  
  84. let questionField = docClone.querySelector('form.question h3')?.innerText.trim() || '';
  85.  
  86. let optionField = Array.from(docClone.querySelectorAll('.options .option'))
  87. .map(opt => opt.innerText.trim())
  88. .filter(txt => txt)
  89. .map(txt => `<li>${txt}</li>`)
  90. .join('') || '';
  91.  
  92. let backField = Array.from(docClone.querySelectorAll('.options .option.correct'))
  93. .map(opt => opt.innerText.trim())
  94. .filter(txt => txt)
  95. .map(txt => `<li>${txt}</li>`)
  96. .join('') || '';
  97.  
  98. let extraElement = docClone.querySelector('.results.container.collected .feedback-container .text');
  99. let extraField = extraElement ? extraElement.innerHTML : '';
  100. let extraFieldText = extraElement ? extraElement.innerText.trim() : '';
  101.  
  102. let webpageURL = window.location.href;
  103.  
  104. // Build unique ID
  105. let uniqueID = generateRandomID
  106. ? generateRandomString(10)
  107. : questionField + backField + extraField + photoQuestion;
  108.  
  109. // Construct note fields
  110. let noteFields = {
  111. "Front": frontField + "<br>" + photoQuestion,
  112. "Question": questionField,
  113. "Options": `<div class="psol">${optionField}</div>`,
  114. "Back": `<div class="psol">${backField}</div>`,
  115. "Feedback": extraFieldText,
  116. "Extra": extraField,
  117. "Link": `<a href="${webpageURL}">Link To Card</a>`,
  118. "UniqueID": uniqueID,
  119. "Front Image": `<img src="${frontImageUrl}">`
  120. };
  121.  
  122. console.log('[RefreshAnki] Sending note fields to AnkiConnect:', noteFields);
  123.  
  124. // Send to AnkiConnect
  125. GM_xmlhttpRequest({
  126. method: "POST",
  127. url: "http://localhost:8765",
  128. data: JSON.stringify({
  129. action: "addNote",
  130. version: 6,
  131. params: {
  132. note: {
  133. deckName: "Default",
  134. modelName: "uofcCard",
  135. fields: noteFields,
  136. tags: [webpageURL]
  137. }
  138. }
  139. }),
  140. headers: { "Content-Type": "application/json" },
  141. onload: function(response) {
  142. console.log("Response from AnkiConnect:", response);
  143. if (response.status === 200) {
  144. console.log("Successfully added note to Anki!");
  145. } else {
  146. console.error("Failed to send note fields to Anki.");
  147. }
  148. }
  149. });
  150. }
  151.  
  152. /********************************************************
  153. * 3) REFRESH-THEN-COPY HELPER
  154. ********************************************************/
  155. // Instead of copying immediately, we set localStorage
  156. // so after reload the script calls copyHtmlToAnki().
  157.  
  158. function refreshThenCopy(generateRandomID) {
  159. const mode = generateRandomID ? 'random' : 'normal';
  160. console.log(`[RefreshAnki] refreshThenCopy() was called, setting doRefreshCopyMode=${mode} then reloading...`);
  161. localStorage.setItem(REFRESH_COPY_KEY, mode);
  162. location.reload();
  163. }
  164.  
  165. /********************************************************
  166. * 4) OPTIONAL KEYBOARD SHORTCUT => Refresh & Copy
  167. ********************************************************/
  168. // If you prefer a direct copy (without refresh) on the shortcut,
  169. // then just call copyHtmlToAnki(true). But you specifically want
  170. // a refresh each time, so let's do it that way:
  171. document.addEventListener('keydown', function(e) {
  172. if (e.ctrlKey && e.shiftKey && e.code === 'KeyY') {
  173. // Refresh page, then copy with random ID
  174. refreshThenCopy(true);
  175. }
  176. });
  177.  
  178. /********************************************************
  179. * 5) FLOATING MENU => Refresh & Copy
  180. ********************************************************/
  181. const menuContainer = document.createElement('div');
  182. menuContainer.style.position = 'fixed';
  183. menuContainer.style.top = '10px';
  184. menuContainer.style.left = '10px';
  185. menuContainer.style.zIndex = 9999;
  186. menuContainer.style.backgroundColor = 'rgba(255, 255, 255, 0.9)';
  187. menuContainer.style.border = '1px solid #ccc';
  188. menuContainer.style.padding = '5px';
  189. menuContainer.style.borderRadius = '5px';
  190. menuContainer.style.fontFamily = 'sans-serif';
  191. menuContainer.style.fontSize = '14px';
  192.  
  193. const title = document.createElement('div');
  194. title.textContent = 'Anki Refresh Menu';
  195. title.style.fontWeight = 'bold';
  196. title.style.marginBottom = '5px';
  197. menuContainer.appendChild(title);
  198.  
  199. // Button 1: Refresh + Copy (No Random ID)
  200. const btnNormal = document.createElement('button');
  201. btnNormal.textContent = 'Refresh & Copy (No Random ID)';
  202. btnNormal.style.display = 'block';
  203. btnNormal.style.marginBottom = '5px';
  204. btnNormal.onclick = () => refreshThenCopy(false);
  205. menuContainer.appendChild(btnNormal);
  206.  
  207. // Button 2: Refresh + Copy (Random ID)
  208. const btnRandom = document.createElement('button');
  209. btnRandom.textContent = 'Refresh & Copy (Random ID)';
  210. btnRandom.style.display = 'block';
  211. btnRandom.onclick = () => refreshThenCopy(true);
  212. menuContainer.appendChild(btnRandom);
  213.  
  214. document.body.appendChild(menuContainer);
  215.  
  216. })();