Skip to content

[WatchfacePage] On-device watchface store with direct install - #145

Open
moWerk wants to merge 13 commits into
AsteroidOS:masterfrom
moWerk:watchface-store-split
Open

moWerk wants to merge 13 commits into
AsteroidOS:masterfrom
moWerk:watchface-store-split

Conversation

@moWerk

@moWerk moWerk commented Jul 31, 2026

Copy link
Copy Markdown
Member

What this adds

An on-device watchface store in the watchface settings page. The page lists the
stock faces and the community faces already installed. Behind an explicit "get
more" it also lists what the unofficial-watchfaces repository offers, and a face
can be installed, activated and removed without leaving the watch.

A face that imports a QML module missing on the device is listed greyed with the
missing module named, rather than offered, because it would render blank.

Removing the face currently shown hands activation to the nearest face still
installed, so the homescreen is never left pointing at a file that is gone.

How it is structured

The work is split so each piece can be judged on its own. The list model no
longer contains the install mechanics, the network layer or the preview
handling:

unit what it owns
WatchfaceHelper the user watchface, asset, font and cache paths, and removing a face's files
WatchfaceCatalog the repository listings, ETag caching, reachability
WatchfacePreviews which image a tile shows, and fetching one when nothing resolves
WatchfaceQmlProbe what a face imports and whether it declares a settings page
WatchfaceInstaller the download transaction
WatchfaceStoreModel rows, activation, and the import gate

The four workers know nothing about list rows. They report by name and the model
decides what that means, through three signals: namesChanged,
previewReady(name), and progress / finished(name, ok, wallpaper).

Reviewing it

Every commit is one concern, and each one configures on its own, so the series
bisects. The licence header conversion and the qmlformat pass are separate
commits from any functional change, so no functional diff carries incidental
reordering or spacing churn.

Reviewing commit by commit is the intended path. Accepting or rejecting an
individual unit does not require taking or rejecting the rest.

Rate limiting

An install enumerates a face's files from one cached recursive repository tree
and pulls each file from raw. The contents API allows 60 calls an hour per IP,
and per-directory calls exhausted that budget after a couple of installs, which
silently dropped bundled fonts. Both listings are fetched conditionally, so a
revalidation that changes nothing costs a 304.

Downloaded files are written atomically, so no observer, the launcher's folder
watchers, its font registration, or the store's own disk enumeration, can see a
half-written face. Paths taken from the tree are refused if they would climb out
of the destination folder, since the listing is remote data.

Testing

Built and run on two panels with different sizes, a beluga and a sawfish, from a
full image on each. Exercised: opening the page offline, browsing the catalog,
installing a face including its bundled font and wallpaper, installing a face
with an unmet import, removing the active face, and preview regeneration on
activation.

Related

Part of the set tracked in unofficial-watchfaces#44, alongside
asteroid-launcher#288, #293 and #295. The launcher pieces are independent of
this one: the store renders no previews itself, it shows whatever the launcher
produced, and falls back through shipped and downloaded images when there is
none.

Credits

The backend restructuring follows review from @dodoradio, whose reading of the
model as several separate concerns is what this shape came from. Thanks also to
Ed Beroset for the commit and review discipline this series is built to.

Claude was used as a coding and review assistant throughout.

moWerk and others added 13 commits August 13, 2026 01:54
Same licence, GPL-3.0-or-later, expressed in the SPDX form the project is
converting to on touch rather than as a full boilerplate block.

Only the oldest copyright line is kept per author, so no duplicate entry
appears for a contributor who has since changed their handle.
No behaviour change. Kept separate so the functional commit that follows
carries no incidental reordering, spacing or literal churn.
Single source for the user watchface, asset, font and cache paths, and for
removing a face's files. Everything else in the store defers to it so no
path logic is duplicated.
Fetches and caches what the repository offers: the catalog of face
directories and one recursive tree of every file in it. Both are conditional
on an ETag, so a revalidation that changes nothing costs a 304.

The tree exists so an install can enumerate a face's files locally. The
contents API allows 60 calls an hour per IP, and per-directory calls
exhausted that after a couple of installs, which silently dropped fonts.

Knows nothing about rows or installs: it reports that the name list changed
and the caller decides what that means.
Finds the image a tile should show, walking from the preview the launcher
grabbed for this device down to a downloaded gallery thumbnail, and fetches
one when nothing resolves. Results are memoized including misses, because a
tile asks once per row and again on every refresh.

Owns no list state. A finished download is reported by name and the caller
decides which row that is.
Answers the two questions the store asks about a face before showing it: do
the modules it imports resolve on this device, and does it declare a settings
page. Both are read from the face's own QML and cached.

A face importing a module that is missing here renders blank, so the answer
is needed before the face is offered.
Downloads a face's QML plus its optional assets, scripts, images, bundled
wallpaper and fonts, as one transaction. Files are enumerated from the cached
repository tree and pulled from raw, so an install makes no rate-limited API
call.

Writes are atomic, so no observer, the launcher's folder watchers, its font
registration or the store's own disk enumeration, can see a half-written
face. Only the QML is essential; a missing asset does not fail the install.

Paths from the tree are refused if they would climb out of the destination:
the listing is remote data.
The list QML binds to: stock faces, installed community faces and, once the
user asks to browse, what the catalog offers. Turns the catalog, the
installer, the preview resolver and the QML probe into rows, and owns what
their results mean for a row, for activation and for the import gate.

Removing the face currently shown hands activation to the nearest face still
installed, so the homescreen is never left pointing at a file that is gone.

Construction lists only what is already on disk, so opening the page costs
two directory scans; every catalog cost waits for an explicit request.
One instance each, shared by every page that needs them. The model is
constructed before an engine exists, so the factory hands one over for the
import probe.
The selector lists stock and installed faces as before and, behind an
explicit "get more", what the repository offers. A face installs, activates
and removes in place.

A face whose imports are unmet on this device is shown greyed with the
missing module named rather than offered, since it would render blank.
Hosts the settings page a face declares, so a face can carry its own options
without the selector knowing anything about them.

Co-authored-by: Arseniy Movshev <dodoradio@outlook.com>
Merge the user wallpaper folder, where the store installs a face's bundled
wallpaper, into the grid alongside the stock set. The folder location comes
from the lightweight helper singleton: a page that needs one path must not
construct the store backend.

Each entry carries its thumbnail and its .qml activation sibling precomputed,
so delegates do no filesystem checks or string work while flicking, and
images decode at cell size instead of full resolution.
The same two-clause comparison decided both the dimming overlay and the
checkmark, written out at each use. It is now one readonly property used by
both.

Those clauses were joined with the bitwise & and |, not && and ||. That
happens to work on booleans but is not what was meant.
@moWerk
moWerk force-pushed the watchface-store-split branch from e2865f3 to fcf9a13 Compare August 12, 2026 23:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant