Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, Swift, BASIC, and whatever else I happen to feel like hacking at.

Flashcards for iPad using Pythonista

Jerry Stratton, November 21, 2018

Since I’ve started using Duolingo, I occasionally save a phrase that I think might be worth remembering. I save it very simply: by taking a screenshot. I do this because I think it will be helpful, for the learning process, to memorize some phrases as well as run through the app normally.

What I’ve been doing is occasionally going into the Photos app on the iPad and scanning through the “flashcards” I have thus created. As the number of phrases increases, it has occurred to me that a random flashcard app might be useful.

This is, of course, easy to create in Pythonista.

In the interface builder, two elements are required, a button to show the image and a button to choose the album to get the images from.

button namex, ysizeflexborderradiusborder colortitleaction
flasher6, 12756, 880WH20#6e6e6eTap To StartshowCard
albumChooser166, 900440, 30WLRT18#6e6e6eNo Albums FoundchooseAlbum

The overall view I’ve set to 768x960. The two titles—Tap To Start and No Albums Found—are defaults; the image where the flashcards go will initially not have an image in it and will say “Tap To Start”. Once an image is chosen, that title will go away, by setting its title to an empty string.

PhraseCard view Inspector: Building the view for PhraseCard.; Pythonista

The overall view.

PhraseCard flasher Inspector: Building the flashcard image holder button for PhraseCard.; Pythonista

The button that holds the displayed image.

PhraseCard chooser button inspector: Building the chooser button for PhraseCard.; Pythonista

The button that lets you choose which album to use.

The very first function that it runs is:

[toggle code]

  • def getAlbums():
    • albums = {}
    • for album in photos.get_albums():
      • if 'phrases' in album.title:
        • albums[album.title] = album
    • return albums

This creates a dictionary of album names, from all albums that have the word “phrases” in their title. In my case, these are:

  • French phrases
  • French phrases (Good)
  • French phrases (Very Good)
  • Italian phrases
  • Italian phrases (Good)
  • Italian phrases (Very Good)
  • Italian phrases (Great)

On my Mac, these albums are smart albums: I mark all Duolingo screenshots as either French or Italian and as flashcards using keywords. I also use ratings keywords for 1 star, 2 star, 3 star, 4 star, and 5 star. The latter three correspond to Good, Very Good, and Great, which means I can focus in on those phrases I most want to learn.

Il faudra voir le résultat: Il faudra voir le résultat. (We will have to see the result.); French; Duolingo

Always good to be able to speak vaguely but wisely.

The drop-down (or, in this case, pop-up) menu for choosing which album to pull flashcards from is simple. When setting up the view, it creates an albumDrop and sets its hidden to True, so that it isn’t visible. When I press the button to choose a new album, it just unhides it:

[toggle code]

  • def chooseAlbum(chooser):
    • albumDrop.selected_row = 0, albumList.items.index(view['albumChooser'].title)
    • albumDrop.hidden = False

The first line selects the current album, and the second line unhides the drop-down. When an album is chosen, the delegate hides the drop-down again.

A new flashcard is displayed whenever I tap on the flashcard display area. That’s why I made it a ui.Button rather than a ui.ImageView.

[toggle code]

  • def showCard(flasher):
    • album = albums[view['albumChooser'].title]
    • card = random.choice(album.assets)
    • #if there are more than one card in this album, make sure we aren't repeating the current card
    • if view.currentCard and len(album.assets) > 1:
      • while card == view.currentCard:
        • card = random.choice(album.assets)
    • view.newCard(flasher, card)
    • flasher.title = ''

First, it gets the album, using the title of the albumChooser button as the key for the albums dictionary. Then, it chooses an image at random from that album’s assets. If (as should be the case in a flashcard album) there are more than one image in the album, it also makes sure that the new image isn’t the same as the old image. Finally, it asks the view to replace the image with the new card and makes sure that the flashcard display area no longer has a title.

Here is the full code:

[toggle code]

  • import ui, photos
  • import random
  • class FlashView(ui.View):
    • def __init__(self):
      • self.currentCard = None
    • def draw(self):
      • self.frameCard()
    • def resetFlasher(self, flasher):
      • flasher.x = 6
      • flasher.y = 12
      • flasher.width = self.width - 12
      • flasher.height = self.height - 80
    • def frameCard(self):
      • card = self.currentCard
      • if not card:
        • return
      • flasher = self['flasher']
      • self.resetFlasher(flasher)
      • if card.pixel_width/card.pixel_height > flasher.width/flasher.height:
        • #need to reduce the height of the flashcard display
        • oldHeight = flasher.height
        • newHeight = flasher.width*card.pixel_height/card.pixel_width
        • flasher.height = newHeight
        • flasher.y += (oldHeight-newHeight)/2
      • elif card.pixel_width/card.pixel_height < flasher.width/flasher.height:
        • #need to reduce the width of the flashcard display
        • oldWidth = flasher.width
        • newWidth = flasher.height*card.pixel_width/card.pixel_height
        • flasher.width = newWidth
        • flasher.x += (oldWidth-newWidth)/2
    • def newCard(self, flasher, card):
      • view.currentCard = card
      • flasher.background_image = card.get_ui_image()
      • self.frameCard()
  • def getAlbums():
    • albums = {}
    • for album in photos.get_albums():
      • if 'phrases' in album.title:
        • albums[album.title] = album
    • return albums
  • def chooseAlbum(chooser):
    • albumDrop.selected_row = 0, albumList.items.index(view['albumChooser'].title)
    • albumDrop.hidden = False
  • class AlbumSelector():
    • def tableview_did_select(self, tableview, section, row):
      • #highlight the selection
      • tableview.reload()
      • #change the button text
      • view['albumChooser'].title = albumList.items[row]
      • #hide the drop-down again
      • albumDrop.hidden = True
  • def showCard(flasher):
    • album = albums[view['albumChooser'].title]
    • card = random.choice(album.assets)
    • #if there are more than one card in this album, make sure we aren't repeating the current card
    • if view.currentCard and len(album.assets) > 1:
      • while card == view.currentCard:
        • card = random.choice(album.assets)
    • view.newCard(flasher, card)
    • flasher.title = ''
  • #populate list of albums
  • albums = getAlbums()
  • #drop-down for choosing an album
  • albumList = ui.ListDataSource(sorted(albums.keys()))
  • albumList.delete_enabled = False
  • albumDrop = ui.TableView()
  • albumDrop.delegate = AlbumSelector()
  • albumDrop.hidden = True
  • #display app
  • view = ui.load_view()
  • view.add_subview(albumDrop)
  • #set default album as the first one in the list
  • view['albumChooser'].title = albumList.items[0]
  • view.present()
  • #align album list dropdown to button
  • albumDrop.width = view['albumChooser'].width*1.8
  • albumDrop.row_height = view['albumChooser'].height
  • albumDrop.height = albumDrop.row_height*(len(albums))
  • albumDrop.x = view['albumChooser'].x
  • albumDrop.y = view['albumChooser'].y - albumDrop.height
  • albumDrop.border_width = view['albumChooser'].border_width
  • albumDrop.border_color = view['albumChooser'].border_color
  • albumDrop.data_source = albumList
  • albumDrop.reload()
Le gouvernement a fait un mauvais choix: Le gouvernement a fait un mauvais choix. (The government has made a wrong choice.); government; French; Duolingo

A phrase that never goes out of style, though in some countries it may be prudent to whisper it…

You can, of course, download the zip file and install it yourself in Pythonista (Zip file, 2.9 KB) if you wish.

This code can, of course, be used to display the photos from any album at random. If you’re using it for something other than phrases, you’ll need to change the way that the app collects names of albums from your photo albums. As long as getAlbums returns a dictionary of albums, keyed off of the album title, the rest of the app won’t care.

I had a strong sense of nostalgia while building this app. The very first program that I wrote for public consumption was a Spanish flashcard program in BASIC on the TRS-80 Model I. It was “RS-80Tay, Aysay, Hatway?”1 in the February, 1982 issue of 80 microcomputing. It was the same basic idea, albeit with scoring and quizzing: input phrases, and then have the computer quiz you on them.

Pythonista elicits, for me, a lot of the same sense of wonder, excitement, and immediacy that programming with BASIC did in the eighties, and programming with HyperCard did in the nineties. Whenever I think of something really useful that I ought to be able to do with the iPad, and there’s no app for that yet, it’s fun to make it in Pythonista. Whether it’s on-the-fly data collection and analysis or augmenting everyday blog reading, I can think of a potential solution, fool around with it without hurting anything, and see useful results immediately.

Well, we’ve come full circle, Lord; I’d like to think there’s some higher meaning to all this. It would certainly reflect well on you. — Matthew Broderick (Ladyhawke)

  1. No, I did not choose the title. I wanted to call it “La Computadora Internacional”.

  1. <- Ascify Comments
  2. Model 100 poet ->