I have tried all the "flashcards" apps. Nothing has stuck with me. This is my final attemp which seems to finally work (for now :D). I just take screenshots of things I find interesting, move them to "flashcards" folder (using Keyboard maestro macro), and use Scriptkit to "spaced-repeat" them.
https://github.com/user-attachments/assets/515132a8-a039-4e49-b8f6-e4fda71a5846
// Name: Flashcards// Description: Spaced-repeat images, managed just by filesystem// Shortcut: shift+ctrl+option+cmd+oimport '@johnlindquist/kit'const flashcardsDir = kenvPath('db', 'flashcards')await ensureDir(flashcardsDir)const today = new Date()let files = await readdir(flashcardsDir)let renamed = 0for (const file of files) {if (!file.endsWith('.png')) continueconst parsed = parseFilename(file)if (!parsed) {renamed++const max = currentMaxForDate(formatDate(today))const newName = formatFilename(formatDate(today), max + 1, 0)await rename(path.join(flashcardsDir, file), path.join(flashcardsDir, newName))}}if (renamed > 0) {notify(`Flashcards: Renamed ${renamed} files to match the flashcard format`)files = await readdir(flashcardsDir) // refresh files after rename}let cards = files.filter(file => file.endsWith('.png')).map(file => {const parsed = parseFilename(file)!return {filename: file,...parsed,fullPath: path.join(flashcardsDir, file),}})if (cards.length === 0) {open(`file://${flashcardsDir}`)await div(md(`## No flashcards found in ${flashcardsDir}Just add images (must be pngs!) there (I've opened the folder for you).Don't worry about the filename, it will be renamed automatically on the next run.After you are done, just rerun this script`))exit()}cards.sort((a, b) => {if (a.nextReviewDate < formatDate(today) && b.nextReviewDate >= formatDate(today)) return -1if (a.nextReviewDate >= formatDate(today) && b.nextReviewDate < formatDate(today)) return 1return a.orderInBucket - b.orderInBucket})const reviewDays = [1, 2, 4, 7, 14, 30]const shortcuts = {"remember": 'r',"forgot": 'f',"delete": 'd',}const cardsToReview = cards.filter(card => {const reviewDate = new Date(card.nextReviewDate)return reviewDate <= today})const { size } = await getActiveScreen()for (const card of cardsToReview) {const days = reviewDays[Math.min(card.numberOfReviewsDone, reviewDays.length - 1)]const nextReview = new Date()nextReview.setDate(today.getDate() + days)await div({x: 50,y: 50,height: size.height - 100,width: size.width - 100,enter: "Close",hint: `${cardsToReview.length} cards to review`,html: `<imgsrc="file://${card.fullPath}"style="width: 100%; height: 100%; object-fit: contain"/>`,className: "h-full w-full", // Otherwise the image could grow too much and cause y scrollshortcuts: [// Right bar{name: "Remember",key: "Space",bar: "right",onPress: async () => {const newFilename = formatFilename(formatDate(nextReview),card.orderInBucket,card.numberOfReviewsDone + 1)await rename(card.fullPath,path.join(flashcardsDir, newFilename))submit(null)},},// Left bar{name: "Forgot",key: shortcuts.forgot,bar: "left",onPress: async () => {const lastCard = cards.reduce((max, card) => Math.max(max, card.orderInBucket), 0)const newFilename = formatFilename(formatDate(today), lastCard + 1, 0)await rename(card.fullPath, path.join(flashcardsDir, newFilename))submit(null)},},{name: "Delete",key: shortcuts.delete,bar: "left",onPress: async () => {await trash(card.fullPath)submit(null)},},{name: "Reveal in Finder",key: "r",bar: "left",onPress: async () => {// -R reveals the file in Finder instead of opening itawait exec(`open -R "${card.fullPath}"`)},},],})}say(`Time flows like water,Knowledge grows with each review,Mind blooms day by day`)await div(md(`# That's all folks!All flashcards have been reviewed.See ya next time!`))// Helpers// ===function parseFilename(filename: string) {const match = filename.match(/^CARD_(\d{4}-\d{2}-\d{2})_(\d+)_(\d+)\.png$/)if (!match) return nullreturn {nextReviewDate: match[1],orderInBucket: parseInt(match[2], 10),numberOfReviewsDone: parseInt(match[3], 10),}}function formatFilename(nextReviewDate: string,orderInBucket: number,numberOfReviewsDone: number) {return `CARD_${nextReviewDate}_${orderInBucket}_${numberOfReviewsDone}.png`}function currentMaxForDate(date: string) {const files = readdirSync(flashcardsDir)return files.filter(file => file.startsWith(`CARD_${date}_`)).map(file => parseFilename(file)!.orderInBucket).reduce((max, order) => Math.max(max, order), 0)}function formatDate(date: Date) {return date.toISOString().slice(0, 10)}