Compare commits

..

No commits in common. "master" and "1.0" have entirely different histories.
master ... 1.0

7 changed files with 36 additions and 287 deletions

1
.gitignore vendored
View File

@ -1,2 +1 @@
*.sw? *.sw?
dist/

View File

@ -1,15 +0,0 @@
deploy: clean build
aws s3 sync dist s3://flashcards.jdbernard.com
rm -r dist
serve-local:
(cd dist && python -m SimpleHTTPServer &)
./makewatch build
build: flashcards.*
-mkdir dist
cp flashcards.* dist
git describe --always --dirty --tags | xargs --replace=INSERTED -- sed -i -e 's/%VERSION%/INSERTED/' dist/*
clean:
-rm -r dist

View File

@ -1,13 +0,0 @@
Simple Flashcards takes a set of text values, one value per line, and shows
them to the user one at a time. It has the following features:
* Support for prompts only or prompts and answers.
* Image URLs can be used as values for pictoral flashcards.
* Configurable timing between cards and answers.
* Configurable size of the text (small, medium, and large).
* In-order, reverse-order, and random-order traversal of the cards.
* Support for saving/loading settings and inputs (locally to the browser).
* Support for importing/exporting saved card sets as a file.
* Support for importing saved card sets from a URL.
Simple Flashcards is available at http://flashcards.jdbernard.com

View File

@ -57,12 +57,10 @@ button {
#settings .visibility-indicator { #settings .visibility-indicator {
display: inline-block; display: inline-block;
font-size: 80%;
transition: all linear 0.2s; transition: all linear 0.2s;
} }
#settings #adv-settings { #settings #adv-settings {
font-size: 80%;
max-height: 0; max-height: 0;
overflow: hidden; overflow: hidden;
transition: max-height linear 0.3s; transition: max-height linear 0.3s;
@ -70,7 +68,7 @@ button {
#settings.adv-settings-visible #adv-settings { #settings.adv-settings-visible #adv-settings {
max-height: 40%; max-height: 40%;
overflow: scroll; overflow-y: scroll;
} }
#settings.adv-settings-visible .visibility-indicator { #settings.adv-settings-visible .visibility-indicator {
@ -97,13 +95,6 @@ button {
min-width: 12em; min-width: 12em;
} }
#adv-settings label input[type="radio"] {
min-width: unset;
margin: 0 0.25em 0 1.5em;
}
#adv-settings label input[type="radio"]:first-of-type { margin-left: 0; }
#adv-settings button { #adv-settings button {
margin: 0 0.5em; margin: 0 0.5em;
} }
@ -118,58 +109,12 @@ input[name=importFileName] {
flex-direction: column; flex-direction: column;
flex-grow: 1; flex-grow: 1;
justify-content: space-around; justify-content: space-around;
} }
#card-input { #card-content { margin: auto; }
flex-grow: 2; #card-content.small-text{ font-size: 100%; }
display: flex; #card-content.medium-text{ font-size: 200%; }
flex-direction: row; #card-content.large-text{ font-size: 800%; }
}
#card-input label {
display: flex;
flex-direction: column;
align-items: stretch;
flex-grow: 1;
margin: 0 0.5em;
}
.card {
display: none;
flex-direction: column;
align-items: center;
width: 100%;
}
.card.current { display: flex; }
.card div {
display: flex;
flex-direction: column;
align-items: center;
}
.card img {
height: auto;
max-height: 100%;
max-width: 100%;
}
.answer { opacity: 0; }
.card.show-answer .answer { opacity: 1; }
.card.small img { width: 50%; }
.card.medium img { width: 75%; }
.card.large img { width: 100%; }
.card.small .prompt { font-size: 100%; }
.card.medium .prompt { font-size: 200%; }
.card.large .prompt { font-size: 800%; }
.card.small .answer { font-size: 75%; }
.card.medium .answer { font-size: 150%; }
.card.large .answer { font-size: 400%; }
/* Card View Visible */ /* Card View Visible */
#settings { #settings {
@ -185,14 +130,6 @@ input[name=importFileName] {
display: none; display: none;
} }
#card-input label.only-with-answers,
#adv-settings label.only-with-answers
{ display: none; }
.cards-have-answers #card-input label.only-with-answers,
.cards-have-answers #adv-settings label.only-with-answers
{ display: flex; }
/* Mobile View */ /* Mobile View */
@media ( max-width: 600px ) { @media ( max-width: 600px ) {
body { body {
@ -223,7 +160,7 @@ input[name=importFileName] {
height: calc(100vh - 2em); height: calc(100vh - 2em);
width: calc(100vw - 2em); width: calc(100vw - 2em);
} }
#settings > button, #cards > button { #settings > button, #cards > button {
font-size: 125%; font-size: 125%;
margin: 1em auto; margin: 1em auto;
@ -250,8 +187,10 @@ input[name=importFileName] {
} }
#debug { #debug {
display: none; /* display: none; */
position: absolute; position: absolute;
bottom: 0; bottom: 0;
right: 0; right: 0;
} }

View File

@ -3,8 +3,6 @@
<head> <head>
<title>Simple Flashcards</title> <title>Simple Flashcards</title>
<meta charset=utf-8> <meta charset=utf-8>
<meta name=viewport content='width=device-width,initial-scale=1'>
<meta name=application-name content='Simple Flashcards'>
<link rel=stylesheet href=flashcards.css type="text/css"> <link rel=stylesheet href=flashcards.css type="text/css">
<script src='flashcards.js'></script> <script src='flashcards.js'></script>
@ -13,30 +11,15 @@
<body class=settings-visible> <body class=settings-visible>
<h1 class=title>Simple Flashcards</h1> <h1 class=title>Simple Flashcards</h1>
<form id=settings action=#> <form id=settings action=#>
<div id=card-input> <label>Items</label>
<label>Cards <textarea id=items-input></textarea>
<textarea id=prompts-input></textarea>
</label>
<label class="only-with-answers">Answers
<textarea id=answers-input></textarea>
</label>
</div>
<label class=toggle-adv-settings>Advanced Settings <label class=toggle-adv-settings>Advanced Settings
<span class=visibility-indicator></span> <span class=visibility-indicator></span>
</label> </label>
<div id=adv-settings> <div id=adv-settings>
<label> <label>
<span>Cards have answers:</span> <span>Seconds per slide:</span>
<label><input type=radio name=hasAnswers value="yes">yes</label> <input type=number name=slidePeriod value=3>
<label><input type=radio name=hasAnswers value="no" checked>no</label>
</label>
<label>
<span>Seconds per prompt:</span>
<input type=number name=promptPeriod value=3>
</label>
<label class="only-with-answers">
<span>Seconds per answer:</span>
<input type=number name=answerPeriod value=3>
</label> </label>
<label> <label>
<span>Show cards </span> <span>Show cards </span>
@ -72,19 +55,14 @@
<button id=import>Import Cards</button> <button id=import>Import Cards</button>
<input name=importFileName type=file accept='.json'> <input name=importFileName type=file accept='.json'>
</label> </label>
<label>
<span>Import from URL: </span>
<input name=importURL type=text>
<button id=importURL>Import</button>
</label>
</div> </div>
<button class=btn-primary id=start-button>Go!</button> <button class=btn-primary id=start-button>Go!</button>
</form> </form>
<div id=cards> <div id=cards>
<div id=card-content></div>
<button id=stop-button>Stop</button> <button id=stop-button>Stop</button>
</div> </div>
<div id=debug> <div id=debug>
Version %VERSION%
<span class=small-only>small</span> <span class=small-only>small</span>
<span class=medium-only>medium</span> <span class=medium-only>medium</span>
<span class=large-only>large</span> <span class=large-only>large</span>

View File

@ -1,18 +1,13 @@
(function() { (function() {
const FC = { const FC = {
currentSet: [], currentSet: [],
cards: [], items: [],
curCardEls: [], // array of card HTMLElements
nextCardIdx: 0, nextCardIdx: 0,
cardOrder: [], cardOrder: [], // elements are objects: { name: 'abc', cards: 'xyz' }
savedSets: [], // elements are objects: { name: 'abc', cards: 'xyz' } savedSets: [],
$: document.querySelector.bind(document), $: document.querySelector.bind(document)
version: "%VERSION%"
}; };
const IMG_REGEX = /(http|https|file):\/\/(.*)\.(jpg|jpeg|gif|png|svg)/;
const URL_REGEX = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/;
FC.shuffle = function(inArray) { FC.shuffle = function(inArray) {
/* http://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array /* http://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
* modified to be a pure function, not mutating it's input */ * modified to be a pure function, not mutating it's input */
@ -39,10 +34,6 @@
return outArray; return outArray;
}; };
FC.zipItems = function(prompts, answers) {
return prompts.map((prompt, idx) => ({ prompt, answer: answers[idx] || null }));
}
FC.isRunning = function() { FC.isRunning = function() {
return !FC.bodyEl.classList.contains('settings-visible'); return !FC.bodyEl.classList.contains('settings-visible');
}; };
@ -55,12 +46,11 @@
if (ev && ev.preventDefault) ev.preventDefault(); if (ev && ev.preventDefault) ev.preventDefault();
const newSet = { const newSet = {
name: FC.$('input[name=saveSetName]').value || 'new set', name: (FC.currentSet || {}).name ||
prompts: FC.promptsInputEl.value, FC.$('input[name=saveSetName]').value ||
answers: FC.answersInputEl.value, 'new set',
cardsHaveAnswers: FC.$('input[name=hasAnswers]:checked')?.value === 'yes', cards: FC.itemsEl.value,
promptPeriod: parseInt(FC.$('input[name=promptPeriod]').value) || 3, slidePeriod: parseInt(FC.$('input[name=slidePeriod]').value) || 3,
answerPeriod: parseInt(FC.$('input[name=answerPeriod]').value) || 3,
textSize: FC.$('select[name=textSize]').value || 'small', textSize: FC.$('select[name=textSize]').value || 'small',
sortOrder: FC.$('select[name=cardOrder]').value || 'in-order' sortOrder: FC.$('select[name=cardOrder]').value || 'in-order'
}; };
@ -70,15 +60,10 @@
FC.populateSettingsFromSet = function(set) { FC.populateSettingsFromSet = function(set) {
FC.$('input[name=saveSetName]').value = set.name; FC.$('input[name=saveSetName]').value = set.name;
FC.promptsInputEl.value = set.prompts; FC.itemsEl.value = set.cards;
FC.answersInputEl.value = set.answers; FC.$('input[name=slidePeriod]').value = set.slidePeriod;
FC.$('input[name=promptPeriod]').value = set.promptPeriod;
FC.$('input[name=answerPeriod]').value = set.answerPeriod;
FC.$('select[name=cardOrder]').value = set.sortOrder; FC.$('select[name=cardOrder]').value = set.sortOrder;
FC.$('select[name=textSize]').value = set.textSize; FC.$('select[name=textSize]').value = set.textSize;
FC.$('input[name=hasAnswers][value="' +
(set.cardsHaveAnswers?'yes':'no') + '"]').checked = true;
FC.setCardsHaveAnswers();
}; };
FC.startCards = function(ev) { FC.startCards = function(ev) {
@ -87,18 +72,15 @@
ev.preventDefault(); ev.preventDefault();
FC.currentSet = FC.makeSetFromSettings(); FC.currentSet = FC.makeSetFromSettings();
FC.cards = FC.zipItems( FC.items = FC.currentSet.cards.split('\n');
FC.currentSet.prompts.split('\n'),
FC.currentSet.answers?.split('\n')
);
FC.curCardEls = FC.cards.map(FC.makeCard); FC.cardContentEl.classList.remove(
'small-text', 'medium-text', 'large-text');
FC.curCardEls.forEach(cardEl => FC.cardsEl.prepend(cardEl)); FC.cardContentEl.classList.add(FC.currentSet.textSize + '-text');
FC.nextCardIdx = 0; FC.nextCardIdx = 0;
const orderedIndices = [...Array(FC.cards.length).keys()]; const orderedIndices = [...Array(FC.items.length).keys()];
switch (FC.currentSet.sortOrder) { switch (FC.currentSet.sortOrder) {
default: default:
case 'in-order': case 'in-order':
@ -111,64 +93,20 @@
FC.cardOrder = FC.shuffle(orderedIndices); FC.cardOrder = FC.shuffle(orderedIndices);
} }
FC.$('html').requestFullscreen();
FC.showNextCard(); FC.showNextCard();
const fullInterval = (FC.currentSet.promptPeriod * 1000) + FC.runningInterval = setInterval(FC.showNextCard, FC.currentSet.slidePeriod * 1000);
(FC.currentSet.cardsHaveAnswers ? FC.currentSet.answerPeriod * 1000 : 0);
FC.runningInterval = setInterval(FC.showNextCard, fullInterval);
FC.bodyEl.classList.remove('settings-visible'); FC.bodyEl.classList.remove('settings-visible');
}; };
FC.makeCard = function(item, idx) {
const newCardDiv = document.createElement("div");
newCardDiv.classList.add('card', FC.currentSet.textSize);
newCardDiv.dataset.index = idx;
const promptDiv = document.createElement("div");
promptDiv.classList.add('prompt');
promptDiv.innerHTML = FC.transformContent(item.prompt);
newCardDiv.appendChild(promptDiv);
if (item.answer) {
const answerDiv = document.createElement("div");
answerDiv.classList.add('answer');
answerDiv.innerHTML = FC.transformContent(item.answer);
newCardDiv.appendChild(answerDiv);
}
return newCardDiv;
};
FC.transformContent = function(content) {
const matchesImg = content.toLowerCase().trim().match(IMG_REGEX);
if (matchesImg) {
return '<img src="' + content.trim() + '"/>';
} else {
return content;
}
};
FC.stopCards = function(ev) { FC.stopCards = function(ev) {
clearInterval(FC.runningInterval); clearInterval(FC.runningInterval);
document.exitFullscreen();
FC.bodyEl.classList.add('settings-visible'); FC.bodyEl.classList.add('settings-visible');
document.querySelectorAll('.card').forEach(el => el.remove());
}; };
FC.showNextCard = function() { FC.showNextCard = function() {
FC.$('.card.current')?.classList?.remove('current', 'show-answer'); const curItem = FC.items[FC.cardOrder[FC.nextCardIdx]];
FC.cardContentEl.innerHTML = curItem;
const curCardEl = FC.$('.card[data-index="' + FC.cardOrder[FC.nextCardIdx] + '"]'); FC.nextCardIdx = (FC.nextCardIdx + 1) % FC.items.length;
curCardEl.classList.add('current');
FC.nextCardIdx = (FC.nextCardIdx + 1) % FC.cards.length;
if (FC.currentSet.cardsHaveAnswers) {
setTimeout(
() => { curCardEl.classList.add('show-answer') },
FC.currentSet.promptPeriod * 1000
);
}
}; };
FC.handleLoadSet = function(ev) { FC.handleLoadSet = function(ev) {
@ -273,39 +211,13 @@
reader.readAsText(fileToImport); reader.readAsText(fileToImport);
}; };
FC.importCardSetFromURL = function(ev) {
ev.preventDefault();
fetch(FC.$('input[name=importURL]').value)
.then(resp => resp.json())
.then(set => {
if (!set.name) throw "Invalid set definition file (missing name)";
FC.saveSet(set);
FC.storeSets(FC.savedSets);
FC.updateSetList(FC.savedSets);
FC.populateSettingsFromSet(set);
})
.catch(err => {
alert("Unable to load set from URL: " + err);
console.error(err);
});
};
FC.setCardsHaveAnswers = function(ev) {
if (FC.$('input[name=hasAnswers]:checked')?.value === 'yes') {
FC.bodyEl.classList.add('cards-have-answers');
} else {
FC.bodyEl.classList.remove('cards-have-answers');
}
};
window.onload = function() { window.onload = function() {
// Cached element references // Cached element references
FC.settingsEl = FC.$('#settings'); FC.settingsEl = FC.$('#settings');
FC.promptsInputEl = FC.$('#prompts-input'); FC.itemsEl = FC.$('#items-input');
FC.answersInputEl = FC.$('#answers-input');
FC.bodyEl = FC.$('body'); FC.bodyEl = FC.$('body');
FC.cardsEl = FC.$('#cards'); FC.cardsEl = FC.$('#cards');
FC.cardContentEl = FC.$('#card-content');
// Event handlers // Event handlers
FC.$('.toggle-adv-settings').addEventListener('click', FC.toggleAdvSettings); FC.$('.toggle-adv-settings').addEventListener('click', FC.toggleAdvSettings);
@ -313,11 +225,8 @@
FC.$('#stop-button').addEventListener('click', FC.stopCards); FC.$('#stop-button').addEventListener('click', FC.stopCards);
FC.$('#export').addEventListener('click', FC.exportCardSet); FC.$('#export').addEventListener('click', FC.exportCardSet);
FC.$('#import').addEventListener('click', FC.importCardSet); FC.$('#import').addEventListener('click', FC.importCardSet);
FC.$('#importURL').addEventListener('click', FC.importCardSetFromURL);
FC.$('input[name=importFileName]') FC.$('input[name=importFileName]')
.addEventListener('change', FC.handleImport); .addEventListener('change', FC.handleImport);
document.querySelectorAll('input[name=hasAnswers]').forEach(inpEl =>
inpEl.addEventListener('change', FC.setCardsHaveAnswers));
FC.$('#save-set').addEventListener('click', FC.handleSaveSet); FC.$('#save-set').addEventListener('click', FC.handleSaveSet);
FC.$('#load-set').addEventListener('click', FC.handleLoadSet); FC.$('#load-set').addEventListener('click', FC.handleLoadSet);

View File

@ -1,48 +0,0 @@
#!/bin/bash -e
# Don Marti <dmarti@zgp.org>
# (If you need a license on this in order to use it,
# mail me and I'll put a license on it. Otherwise,
# de minimis non curat lex.)
# re-run make, with the supplied arguments, when a
# Makefile prerequisite changes. Example:
# makewatch test
# Fun to use with Auto Reload (Firefox) or Tincr (Chrome) and Pandoc
# https://addons.mozilla.org/en-US/firefox/addon/auto-reload/
# http://tin.cr/
# http://johnmacfarlane.net/pandoc/
# Requires inotifywait. Probably requires GNU Make to
# get the right "-dnr" output. Not tested with other
# "make" implementations
# If $MAKEWATCH is set to an existing file or
# space-separated list of files, also checks those.
make_prereqs() {
# Make "make" figure out what files it's interested in.
echo "Makefile"
find $MAKEWATCH
gmake -dnr $* | tr ' ' '\n' | \
grep ".*'.$" | grep -o '\w.*\b'
}
prereq_files() {
# prerequisites mentioned in a Makefile
# that are extant files
echo ' '
for f in `make_prereqs $* | sort -u`; do
[ -e $f ] && echo -n "$f ";
done
echo
}
make $*
while true; do
fl=$(prereq_files $*)
ev=$(inotifywait --quiet --format %e $fl)
if [ "xOPEN" != "x$ev" ]; then
sleep 1
make $*
fi
done