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?
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 {
display: inline-block;
font-size: 80%;
transition: all linear 0.2s;
}
#settings #adv-settings {
font-size: 80%;
max-height: 0;
overflow: hidden;
transition: max-height linear 0.3s;
@ -70,7 +68,7 @@ button {
#settings.adv-settings-visible #adv-settings {
max-height: 40%;
overflow: scroll;
overflow-y: scroll;
}
#settings.adv-settings-visible .visibility-indicator {
@ -97,13 +95,6 @@ button {
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 {
margin: 0 0.5em;
}
@ -118,58 +109,12 @@ input[name=importFileName] {
flex-direction: column;
flex-grow: 1;
justify-content: space-around;
}
#card-input {
flex-grow: 2;
display: flex;
flex-direction: row;
}
#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-content { margin: auto; }
#card-content.small-text{ font-size: 100%; }
#card-content.medium-text{ font-size: 200%; }
#card-content.large-text{ font-size: 800%; }
/* Card View Visible */
#settings {
@ -185,14 +130,6 @@ input[name=importFileName] {
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 */
@media ( max-width: 600px ) {
body {
@ -223,7 +160,7 @@ input[name=importFileName] {
height: calc(100vh - 2em);
width: calc(100vw - 2em);
}
#settings > button, #cards > button {
font-size: 125%;
margin: 1em auto;
@ -250,8 +187,10 @@ input[name=importFileName] {
}
#debug {
display: none;
/* display: none; */
position: absolute;
bottom: 0;
right: 0;
}

View File

@ -3,8 +3,6 @@
<head>
<title>Simple Flashcards</title>
<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">
<script src='flashcards.js'></script>
@ -13,30 +11,15 @@
<body class=settings-visible>
<h1 class=title>Simple Flashcards</h1>
<form id=settings action=#>
<div id=card-input>
<label>Cards
<textarea id=prompts-input></textarea>
</label>
<label class="only-with-answers">Answers
<textarea id=answers-input></textarea>
</label>
</div>
<label>Items</label>
<textarea id=items-input></textarea>
<label class=toggle-adv-settings>Advanced Settings
<span class=visibility-indicator></span>
</label>
<div id=adv-settings>
<label>
<span>Cards have answers:</span>
<label><input type=radio name=hasAnswers value="yes">yes</label>
<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>
<span>Seconds per slide:</span>
<input type=number name=slidePeriod value=3>
</label>
<label>
<span>Show cards </span>
@ -72,19 +55,14 @@
<button id=import>Import Cards</button>
<input name=importFileName type=file accept='.json'>
</label>
<label>
<span>Import from URL: </span>
<input name=importURL type=text>
<button id=importURL>Import</button>
</label>
</div>
<button class=btn-primary id=start-button>Go!</button>
</form>
<div id=cards>
<div id=card-content></div>
<button id=stop-button>Stop</button>
</div>
<div id=debug>
Version %VERSION%
<span class=small-only>small</span>
<span class=medium-only>medium</span>
<span class=large-only>large</span>

View File

@ -1,18 +1,13 @@
(function() {
const FC = {
currentSet: [],
cards: [],
curCardEls: [], // array of card HTMLElements
items: [],
nextCardIdx: 0,
cardOrder: [],
savedSets: [], // elements are objects: { name: 'abc', cards: 'xyz' }
$: document.querySelector.bind(document),
version: "%VERSION%"
cardOrder: [], // elements are objects: { name: 'abc', cards: 'xyz' }
savedSets: [],
$: document.querySelector.bind(document)
};
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) {
/* http://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
* modified to be a pure function, not mutating it's input */
@ -39,10 +34,6 @@
return outArray;
};
FC.zipItems = function(prompts, answers) {
return prompts.map((prompt, idx) => ({ prompt, answer: answers[idx] || null }));
}
FC.isRunning = function() {
return !FC.bodyEl.classList.contains('settings-visible');
};
@ -55,12 +46,11 @@
if (ev && ev.preventDefault) ev.preventDefault();
const newSet = {
name: FC.$('input[name=saveSetName]').value || 'new set',
prompts: FC.promptsInputEl.value,
answers: FC.answersInputEl.value,
cardsHaveAnswers: FC.$('input[name=hasAnswers]:checked')?.value === 'yes',
promptPeriod: parseInt(FC.$('input[name=promptPeriod]').value) || 3,
answerPeriod: parseInt(FC.$('input[name=answerPeriod]').value) || 3,
name: (FC.currentSet || {}).name ||
FC.$('input[name=saveSetName]').value ||
'new set',
cards: FC.itemsEl.value,
slidePeriod: parseInt(FC.$('input[name=slidePeriod]').value) || 3,
textSize: FC.$('select[name=textSize]').value || 'small',
sortOrder: FC.$('select[name=cardOrder]').value || 'in-order'
};
@ -70,15 +60,10 @@
FC.populateSettingsFromSet = function(set) {
FC.$('input[name=saveSetName]').value = set.name;
FC.promptsInputEl.value = set.prompts;
FC.answersInputEl.value = set.answers;
FC.$('input[name=promptPeriod]').value = set.promptPeriod;
FC.$('input[name=answerPeriod]').value = set.answerPeriod;
FC.itemsEl.value = set.cards;
FC.$('input[name=slidePeriod]').value = set.slidePeriod;
FC.$('select[name=cardOrder]').value = set.sortOrder;
FC.$('select[name=textSize]').value = set.textSize;
FC.$('input[name=hasAnswers][value="' +
(set.cardsHaveAnswers?'yes':'no') + '"]').checked = true;
FC.setCardsHaveAnswers();
};
FC.startCards = function(ev) {
@ -87,18 +72,15 @@
ev.preventDefault();
FC.currentSet = FC.makeSetFromSettings();
FC.cards = FC.zipItems(
FC.currentSet.prompts.split('\n'),
FC.currentSet.answers?.split('\n')
);
FC.items = FC.currentSet.cards.split('\n');
FC.curCardEls = FC.cards.map(FC.makeCard);
FC.curCardEls.forEach(cardEl => FC.cardsEl.prepend(cardEl));
FC.cardContentEl.classList.remove(
'small-text', 'medium-text', 'large-text');
FC.cardContentEl.classList.add(FC.currentSet.textSize + '-text');
FC.nextCardIdx = 0;
const orderedIndices = [...Array(FC.cards.length).keys()];
const orderedIndices = [...Array(FC.items.length).keys()];
switch (FC.currentSet.sortOrder) {
default:
case 'in-order':
@ -111,64 +93,20 @@
FC.cardOrder = FC.shuffle(orderedIndices);
}
FC.$('html').requestFullscreen();
FC.showNextCard();
const fullInterval = (FC.currentSet.promptPeriod * 1000) +
(FC.currentSet.cardsHaveAnswers ? FC.currentSet.answerPeriod * 1000 : 0);
FC.runningInterval = setInterval(FC.showNextCard, fullInterval);
FC.runningInterval = setInterval(FC.showNextCard, FC.currentSet.slidePeriod * 1000);
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) {
clearInterval(FC.runningInterval);
document.exitFullscreen();
FC.bodyEl.classList.add('settings-visible');
document.querySelectorAll('.card').forEach(el => el.remove());
};
FC.showNextCard = function() {
FC.$('.card.current')?.classList?.remove('current', 'show-answer');
const curCardEl = FC.$('.card[data-index="' + FC.cardOrder[FC.nextCardIdx] + '"]');
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
);
}
const curItem = FC.items[FC.cardOrder[FC.nextCardIdx]];
FC.cardContentEl.innerHTML = curItem;
FC.nextCardIdx = (FC.nextCardIdx + 1) % FC.items.length;
};
FC.handleLoadSet = function(ev) {
@ -273,39 +211,13 @@
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() {
// Cached element references
FC.settingsEl = FC.$('#settings');
FC.promptsInputEl = FC.$('#prompts-input');
FC.answersInputEl = FC.$('#answers-input');
FC.itemsEl = FC.$('#items-input');
FC.bodyEl = FC.$('body');
FC.cardsEl = FC.$('#cards');
FC.cardContentEl = FC.$('#card-content');
// Event handlers
FC.$('.toggle-adv-settings').addEventListener('click', FC.toggleAdvSettings);
@ -313,11 +225,8 @@
FC.$('#stop-button').addEventListener('click', FC.stopCards);
FC.$('#export').addEventListener('click', FC.exportCardSet);
FC.$('#import').addEventListener('click', FC.importCardSet);
FC.$('#importURL').addEventListener('click', FC.importCardSetFromURL);
FC.$('input[name=importFileName]')
.addEventListener('change', FC.handleImport);
document.querySelectorAll('input[name=hasAnswers]').forEach(inpEl =>
inpEl.addEventListener('change', FC.setCardsHaveAnswers));
FC.$('#save-set').addEventListener('click', FC.handleSaveSet);
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