Pages

The Thumbnails Plugin

The thumbnails plugin puts real pictures on a face's settings page as base64 data URLs. It does two jobs, each with its own generator and check. The first puts a picture of each panel on a face with a layout editor, so a drag and drop grid or a slot editor shows each panel the way it looks on the watch rather than as an emoji on a coloured tile. The second puts a picture of each theme beside the theme picker, so a wearer sees the palette before they choose it. Neither draws the pictures. They are made outside the framework and committed with the face.

The panel generator is embed-thumbnails.ts with the check check-thumbnails.ts, and the theme-preview generator is embed-theme-previews.ts with the check check-theme-previews.ts, all registered in the plugin's package.json. Both encode through data-url.ts. The lookup an editor runs for a panel picture is thumbs.ts. How the editors themselves are written and bundled is on The Settings Page.

Most of this page is about the panel pictures. Theme Previews at the end covers the second job, which is simpler.

Turning It On

A unit lists the plugin under plugins in its paf.config.json. It takes no options:

{
  "plugins": {
    "thumbnails": {}
  }
}

The plugin's paf key offers a thumbnails generator, with resources/thumbnails as its when, and adds its check to paf check. So paf gen <face> all runs the generator only for a face that has the folder, and paf gen <face> thumbnails runs it on its own. A family lists the plugin once, and each face in it that keeps a folder gets its own generated file. paf gen and Generators cover how the generators are found and ordered.

What a Face Provides

The Pictures. Each PNG is named <slug>-<size>.png. The name is split on its last dash, so a slug can hold dashes of its own and weekday-dots-1x2.png is the weekday-dots panel at 1x2. A size cannot hold a dash. A panel that fits several sizes has one picture for each, so a Battery panel at two sizes comes as battery-1x2.png and battery-2x2.png. The pictures are drawn at the size the editor shows them, so they stay small.

The Module List. The face's src/pkjs/clay/module-meta.ts is the label-keyed list its editors already read for each panel's emoji and tint. Each row's slug is the file name stem its pictures use, so the list of panels lives in one place. The same file exports thumbnailSizes, the sizes a picture may come in:

const moduleMeta: Record<string, ModuleMeta> = {
  'Battery': { icon: '🔋', blockColor: '#f0a500', slug: 'battery' },
  'Weather': { icon: '🌤', blockColor: '#1e88e5', slug: 'weather' },
};

export const thumbnailSizes = ['1x2', '2x2'];

export default moduleMeta;

Sizes Belong to the Face. Each face draws its own panel shapes, so the framework keeps no list of sizes. A grid face might offer 1x2 and 2x2 blocks, and a face with fixed slots a slot and a tall size. A face with a resources/thumbnails folder but no module-meta.ts, or one that does not export thumbnailSizes, stops the generator with a message naming what is missing.

What paf gen Writes

The generator reads every PNG in the folder, matches it to a row of the module list, and writes src/pkjs/clay/module-thumbnails.g.js in the face:

// generated by the thumbnails plugin
// do not edit by hand: run `paf gen <face> thumbnails` after changing the pictures
module.exports = {
  "Battery": { "1x2": "data:image/png;base64,iVBORw0KGgo…", "2x2": "data:image/png;base64,iVBORw0KGgo…" },
  "Weather": { "1x2": "data:image/png;base64,iVBORw0KGgo…" },
};

Keyed by Label. Each entry is filed under the panel's label, then its size. The editors match a panel's icon and tint by label too, so a picture stays with its panel even when the numbers behind the panels are renumbered. The slug only exists to name the files.

The Same File Every Run. Rows follow the order of the module list, and each row's sizes are sorted, so an unchanged folder writes the same file byte for byte. A diff of the committed file shows only the pictures that really changed.

Nothing Is Written on a Problem. Every PNG is matched before anything is written. A PNG whose slug no panel claims, or whose size the face does not list, stops the run. So does a panel with no picture at all, and so do two panels sharing one slug, which would hand both the same pictures and leave one of them blank. The run names each file or slug at fault, and for a PNG says whether its name matches no panel, has no size after the panel's name, or carries a size the face does not list. It leaves the committed file as it was, rather than replacing a good file with a broken one.

One Picture Is Enough. A panel counts as covered once it has a picture at any size. Most panels fit only one or two of the sizes a face offers, so asking for every size would fail every face. The cost is that a panel offered at a size it has no picture for is not caught here. The editor shows its emoji tile at that size instead.

How the Settings Page Reads Them

A Clay component cannot import anything, since Clay copies only its function bodies into the page as text (The Serialization Trap). So the face's config.ts imports the generated file and hands it to the editor on its page item as moduleThumbnails, beside the moduleOptions that carry the panel list:

import moduleThumbnails from './clay/module-thumbnails.g';

{
  type: 'gridEditor',
  messageKey: 'GRID',
  defaultValue: '1,0,0,2,2',
  moduleOptions: PANEL_OPTIONS,
  moduleThumbnails: moduleThumbnails,
},

The editor reads this.config.moduleThumbnails when it starts and looks a panel up by its label and the size it is being shown at. The framework's thumbByLabel builder piece does that lookup and returns null when there is no picture, and the editor then draws the panel's emoji on its tint from the module list. A face can file a picture under any label in the list.

Why Data URLs. The page is one data URL, as The Serialization Trap covers, so it has no way to reach a file in the face. A picture written into the page as a data URL needs nothing else to load.

Theme Previews

A face whose settings page has a theme picker can show a picture of each theme under it. The picker keeps its dropdown, and the chosen theme's picture is drawn beneath it. A theme with more than one picture, such as a day and a night shot, gets them all, side by side. This is the second generator in the plugin, and it is simpler than the panel pictures because a theme has no sizes and no module list to match: it is a map from a theme's value to its pictures.

The Manifest. The face keeps a resources/theme-previews.json mapping each theme's option value to a picture path, or a list of paths, relative to the face folder. A path may climb out of the face with .., since the whole repo is checked out when the generator runs:

{
  "0": ["previews/day.png", "previews/night.png"],
  "1": "previews/one.png"
}

A value is the number the theme carries in the picker's options. A single path is treated as a one-item list. An absolute path or an empty path stops the run with a message naming the key, as does a value that is none of the shapes this section describes.

One Theme, One Watch's Shots. A face built for more than one watch can show each watch its own shots. In place of a path or a list, a value takes a map keyed by platform, each key a path or a list the same as before:

{
  "0": {
    "emery": ["previews/emery-day.png", "previews/emery-night.png"],
    "gabbro": ["previews/gabbro-day.png", "previews/gabbro-night.png"]
  }
}

The picker reads the watch the settings page was opened from and draws that platform's shots, so a round watch shows its round ones and a rectangular one shows its. A platform name has to be one Pebble builds for (emery, gabbro, chalk, basalt, aplite, diorite, flint), so a typo stops the run rather than showing nothing on that watch. A theme whose map leaves out the watch in hand falls back to any shots the map does carry, so the row is never blank while the theme has pictures. A flat value still works, and a face can mix the two, flat for a theme that looks the same on every watch and a map for one that does not.

What paf gen Writes. paf gen <face> theme-previews reads the manifest, base64-encodes each picture, and writes src/pkjs/clay/theme-previews.g.js. The rows come out with the integer keys smallest first and any other key in the order the manifest lists it, so an unchanged manifest writes the same file byte for byte. A flat value writes a list, and a platform-keyed one writes a map of lists in the same shape:

// generated by the thumbnails plugin
// do not edit by hand: run `paf gen <face> theme-previews` after changing the pictures
module.exports = {
  "0": { "emery": ["data:image/png;base64,iVBORw0KGgo…"], "gabbro": ["data:image/png;base64,iVBORw0KGgo…"] },
  "1": ["data:image/png;base64,iVBORw0KGgo…"],
};

As with the panel pictures, nothing is written on a problem: a path that is missing or is not a PNG stops the run and leaves the committed file as it was.

How the Page Reads It. The face's config.ts imports the generated file and passes it to buildConfig as the theme section's previews, beside the theme options:

import themePreviews from './clay/theme-previews.g';

buildConfig({
  theme: { label: 'Theme', options: THEME_OPTIONS, previews: themePreviews },
  // …
});

buildConfig then builds the picker from the framework's themePreview component instead of the plain select. It still saves under APPEARANCE_THEME. A face that passes no previews gets the plain dropdown, unchanged. See The Theme Picker with Pictures.

What paf check Catches. The theme-preview check mirrors the panel one: a committed theme-previews.g.js that differs from what the generator would write now, a path that is missing or is not a PNG, and a committed file in a face with no manifest to build it from.

What It Costs

Base64 writes every three bytes as four characters, so the generated file is about a third larger than the PNGs it holds. On Gridlock, 73 pictures come to about 51 KB and the file to about 72 KB.

Theme previews run larger, since each is a whole-watch shot and a theme can carry a day and a night one. A set of eight themes with both comes to roughly 80 to 100 KB. A face that carries a set per platform ships every platform's shots in the one file, so two platforms roughly double it. The whole settings page goes to the phone as one data URL, so a face that finds the page slow or refusing to open can drop to a single shot per theme, which about halves it.

In the Phone Code. The generated file is part of the face's phone code. It ships in every download of the face and loads every time the phone code starts, whether or not the wearer opens the settings page. None of it reaches the watch, since the PNGs are not listed as watch resources and the watch never runs the phone code.

In the Page. Clay writes the whole page description into the page each time the settings page opens, and an object handed to two items is written out twice. A face that gives the pictures to two editors carries them twice.

What paf check Catches

The check runs the generator in memory for every face in the unit and compares the result with the committed file, without writing anything. It reports, one line per face and problem:

  • a committed file that differs from what the generator would write now, such as after a new picture, a new panel, a renamed label, or a changed order, with the paf gen <face> thumbnails line that fixes it
  • a PNG that names no panel, one named for a panel with no size after it, or one at a size the face does not list, which names the size and the sizes the face does list
  • a panel with no picture at any size, unless a PNG above already names it, since then the fix is renaming that PNG
  • a committed module-thumbnails.g.js in a face with no resources/thumbnails folder to build it from, since the face would still ship it
  • a face that cannot be checked at all, such as one missing its module-meta.ts or its thumbnailSizes, with the reason

The build never runs the generator, so the committed file is all the page ever sees. A re-shot picture without a fresh paf gen ships the old one, and this check is what catches it.

$ paf check
== check: pebble-watchface-lcars ==
Clay components: every face is current
icons: every face is current
thumbnails: every face is current