Conversation
This was referenced Jul 31, 2026
moWerk
force-pushed
the
watchface-store-split
branch
from
July 31, 2026 22:26
ecc4240 to
5e619c2
Compare
moWerk
marked this pull request as draft
August 2, 2026 15:33
moWerk
force-pushed
the
watchface-store-split
branch
2 times, most recently
from
August 3, 2026 08:38
90fc378 to
8f2d396
Compare
moWerk
marked this pull request as ready for review
August 3, 2026 08:39
This was referenced Aug 4, 2026
moWerk
force-pushed
the
watchface-store-split
branch
from
August 5, 2026 01:56
8f2d396 to
76811df
Compare
moWerk
marked this pull request as draft
August 6, 2026 19:35
moWerk
force-pushed
the
watchface-store-split
branch
2 times, most recently
from
August 8, 2026 19:21
c0341d8 to
e2865f3
Compare
moWerk
marked this pull request as ready for review
August 8, 2026 20:51
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
force-pushed
the
watchface-store-split
branch
from
August 12, 2026 23:59
e2865f3 to
fcf9a13
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
WatchfaceHelperWatchfaceCatalogWatchfacePreviewsWatchfaceQmlProbeWatchfaceInstallerWatchfaceStoreModelThe four workers know nothing about list rows. They report by name and the model
decides what that means, through three signals:
namesChanged,previewReady(name), andprogress/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.