Action

review flashcards

Posted by dhaysom, Last update over 3 years ago

A simple system for reviewing notes in the form of spaced repetition flashcards.
Drafts forum post.

Tags

  • This action uses three types of tag:
    • flashcard: used to identify notes that are to be reviewed as flashcards
    • reviewYYYYMMDD: used to keep track of the date when flashcards are due
    • masteryX: used to calculate when the next review date will be

Making Flashcards

  • turn any note into a flashcard by adding the three tags listed above, e.g. flashcard, mastery1 and review20200716
  • the action convert to flashcard will add these tags automatically
  • use the line break (—-) to split the flashcards into different sides
    • sides will be revealed one at a time
    • there is no limit to the number of sides a flashcard can have

Reviewing Flashcards

  • when this action is run, it will find all notes with the flashcard tag and a review date tag that is on or before the current date
  • if you select “remembered”, the mastery count will be doubled
  • if you select “forgot”, the mastery count will be halved (down to a minimum of 1)
  • the new mastery count indicates the number of days before the next review
    • e.g. after remembering a card with the mastery2 tag on 20200716, the tag will be updated to mastery4, and the review date tag will be updated to review20200720
    • e.g. after forgetting a card with the mastery2 tag on 20200716, the tag will be updated the mastery1, and the review date tag will be updated to review20200717

Changes

1.01

  • added autoscroll on pressing the “flip” button
  • added dark mode
  • tweaked heading font sizes
  • divided CSS and functions into separate steps for clarity

Steps

  • script

    // CSS
    
    let stylesheet = `
    <style>
    @charset "utf-8";
    
    html { 
    	font-size:150%;
    	font-family: Avenir, Helvetica, Arial, Freesans, clean, sans-serif;
    }
    body {
    	margin:0;
    	padding:1em;
    }
    @media (max-device-width: 480px) { 
    
    } 
    @media (min-device-width: 481px) { 
    	body {
    		margin:auto;
    		max-width:600px;
    	} 
    }
    
    h1, h2, h3, h4, h5, h6 {
        font-weight: bold;
    }
    
    h1 {
        font-size: 1rem;
        border-bottom: 1px solid #CCCCCC;
    }
    
    h2 {
        border-bottom: 1px solid #CCCCCC;
        color: #000000;
        font-size: 1.25rem;
    }
    
    h3 {
        font-size: 1.1rem;
    }
    
    h4 {
        font-size: 1rem;
    }
    
    h5 {
        font-size: 1rem;
    }
    
    h6 {
        color: #777777;
        background-color: inherit;
        font-size: 14px;
    }
    
    hr {
        height: 0.2em;
        border: 0;
        color: #CCCCCC;
        background-color: #CCCCCC;
    }
    
    p, blockquote, ul, ol, dl, li, table, pre {
        margin: 1em 0;
    }
    
    blockquote {
    	background:		#f9f9f9;
    	border-left:	10px solid #ccc;
    	margin:			1.5em 10px;
    	padding:		.5em 10px;
    }
    
    code, pre {
        border-radius: 3px;
        background-color: #F8F8F8;
        color: inherit;
    }
    
    code {
        border: 1px solid #EAEAEA;
        margin: 0 2px;
        padding: 0 5px;
    }
    
    pre {
        border: 1px solid #CCCCCC;
        line-height: 1.25em;
        overflow: auto;
        padding: 6px 10px;
    }
    
    pre > code {
        border: 0;
        margin: 0;
        padding: 0;
    }
    
    a, a:visited {
        color: #4183C4;
        background-color: inherit;
        text-decoration: none;
    }
    
    table {
      margin: 1em 0;
      border: 1px solid #aaa;
      border-collapse: collapse;
    }
    
    th {
      padding:.25em .5em;
      background: #efefef;
      border: 1px solid #ccc;  
    }
    
    td {
      padding:.25em .5em;
      border: 1px solid #ccc;
    }
    
    img {
        max-width: 90%;
    }
    
    .flipbutton {
    	background: #e7e7e7;
    	border: none;
    	padding: 1rem;
    	text-align: center;
    	text-decoration: none;
    	margin: auto;
    	width: 50%;
    	font-family:'Avenir',sans-serif;
    	font-size: 1rem;
    	border-radius: 6px;
    	display: block;
    	box-shadow: none;
    }
    
    .yesbutton {
    	background: #d2f8d2;
    	border: none;
    	padding: 1rem;
    	text-align: center;
    	text-decoration: none;
    	margin: auto;
    	width: 50%;
    	font-family:'Avenir',sans-serif;
    	font-size: 1rem;
    	border-radius: 6px;
    	display: block;
    	box-shadow: none;
    }
    
    .nobutton {
    	background: #f8d2d2;
    	border: none;
    	padding: 1rem;
    	text-align: center;
    	text-decoration: none;
    	margin: auto;
    	width: 50%;
    	font-family:'Avenir',sans-serif;
    	font-size: 1rem;
    	border-radius: 6px;
    	display: block;
    	box-shadow: none;
    }
    
    .content {
    	display: none;
    }
    
    #section1 {
    	display: block;
    }
    
    @media (prefers-color-scheme: dark) {
      body {
        color: #eee;
        background: #121212;
      }
    
      body a {
        color: #809fff;
      }
      
      blockquote {
    	background: #404040;
    	border-left: 10px solid #686868;
    	}
    	
    	th {
    	  background: #404040;   
    	}
      
      .flipbutton {
        background: #606060;
        color: #D3D3D3;
    	}
    	
    	.yesbutton {
    	  background: #093909;
    	  color: #D3D3D3;
    	}
    	
    	.nobutton {
    	  background: #6C1111;
    	  color: #D3D3D3;
    	}
    	
    }
    
    </style>
    
    <script>
    
    function yesButton() {
    	Drafts.send("cardCheckButton", "remembered");
    	Drafts.continue();
    }
    
    function noButton() {
    	Drafts.send("cardCheckButton", "forgot");
    	Drafts.continue()
    }
    
    function flipButton(divCount) {
    	let nextSection = "section"+(divCount+1);
    	let thisButton = "flipbutton"+divCount;
    	document.getElementById(thisButton).style.display = "none";
    	document.getElementById(nextSection).style.display = "block";
    	document.getElementById(nextSection).scrollIntoView();
    }
    
    </script>
    `
    
  • script

    let divCount = 1;
    
    // format and number the divisions of each card
    
    function divFormat(divContent) {
    	return `<div class = 'content' id = 'section${divCount}'>`+divContent+`<br>
    	<button id = 'flipbutton${divCount}' class = 'flipbutton' onclick = 'flipButton(${divCount})'>flip</button></div>`
    	
    //	<button onclick='document.getElementById("section${divCount+1}").style.display = "block"' class = 'flipbutton'>flip</button></div>`
    }
    
    // format the final division of the card with remembered/forgot buttons
    
    function finalDivFormat(finalDivContent) {
    	return `<div class = 'content' id = 'section${divCount}'>`+finalDivContent+`<br>
    	<button name = "yesbutton1" class="yesbutton" onclick="yesButton()">remembered</button><button name = "nobutton1" class="nobutton" onclick="noButton()">forgot</button></div>
    	`
    }
    
    // display card as HTML
    
    function displayCard(cardInput) {
    	let cardTitle = cardInput.title;
    	// process markdown to HTML
    	let cardContent = cardInput.processTemplate("%%[[body]]%%");
    	// divide card into sections by line break
    	let dividedCard = cardContent.split("<hr />");
    	
    	// assign div class and ID to each section
    	divCount = 1;
    	let dividedCardDivs = []
    	for (let z = 0; z < (dividedCard.length-1); z++) {
    		dividedCardDivs.push(divFormat(dividedCard[z]));
    		divCount++;
    	}
    	dividedCardDivs.push(finalDivFormat(dividedCard[dividedCard.length - 1]));
    	
    	// display as HTML
    	let cardHTML = "<html><head><title>" + cardTitle + "</title>" + stylesheet + "<body>" + dividedCardDivs.join("")+"</body>";
    	return cardHTML;
    }
    
    // search all drafts for flashcard tag
    let allCards = Draft.query("", "all", ["flashcard"], [], "modified", false, false);
    let dueCards = [];
    
    // get today's date
    
    var now = new Date();
    
    // format as YYYYMMDD
    
    function yyyymmdd(dateInput) {
        var y = dateInput.getFullYear();
        var m = dateInput.getMonth() + 1;
        var d = dateInput.getDate();
        var mm = m < 10 ? '0' + m : m;
        var dd = d < 10 ? '0' + d : d;
        return '' + y + mm + dd;
    }
    
    currentDate = yyyymmdd(now);
    
    // calculate the date for next review by adding mastery level to current date
    
    function getReviewDate(date, days) {
    	const copy = new Date(Number(date))
    	copy.setDate(date.getDate() + days)
    	return copy
    }
    
    for (let i = 0; i < allCards.length; i++) {
    	let cardDueDate;
    	let cardMastery;
    	allCardTags = allCards[i].tags;
    	// go through each tag of each card
    	for (let j = 0; j < allCardTags.length; j++) {
    		
    		// find tag containing string "review"
    		if (allCardTags[j].match(/review/) != null) {
    		
    			//identify card due date
    			let dueDateTag = allCardTags[j]; 
    			let dateMatch = dueDateTag.match(/(\d+)/);
    			cardDueDate = dateMatch[0];
    		
    			if (currentDate >= cardDueDate) {
    			
    				// if due, remove old due date tag
    				allCards[i].removeTag(allCardTags[j]);
    				allCards[i].update();
    
    				// if due, display card content
    				let html = displayCard(allCards[i])
    				let preview = HTMLPreview.create();
    				preview.show(html);
    				
    				for (let k = 0; k < allCardTags.length; k++) {
    					// find tag containing string "mastery"
    					if (allCardTags[k].match(/mastery/) != null) {
    						// identify mastery counter tag
    						let cardMasteryTag = allCardTags[k];
    						let cardMasteryMatch = cardMasteryTag.match(/(\d+)/);
    						cardMastery = parseInt(cardMasteryMatch[0]);
    						// remove old mastery counter tag
    						allCards[i].removeTag(allCardTags[k]);
    						allCards[i].update();
    					}
    				}
    				
    				// double card mastery counter if remembered, halve if forgotten
    								
    				let cardCheck = context.previewValues["cardCheckButton"];
    				
    				if (cardCheck === "remembered") {
    					cardMastery = cardMastery * 2;
    				}
    				
    				if ((cardCheck === "forgot") && (cardMastery > 1)) {
    					cardMastery = cardMastery / 2;
    				}
    				
    				
    				// update review date with new mastery counter
    				let nextReviewDate = getReviewDate(now, cardMastery);
    				let nextReviewDateFormatted = yyyymmdd(nextReviewDate);
    				// add new review date tag
    				allCards[i].addTag("review"+nextReviewDateFormatted);
    				// add new mastery counter tag
    				allCards[i].addTag("mastery"+cardMastery);
    				allCards[i].update();
    				
    			}
    		}
    	}	
    }
    

Options

  • After Success Default
    Notification Info
    Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.