Themes
This page explains how built-in themes are implemented and distributed in this repository. For the package format and publishing instructions, see Theme Package Authoring. For the user-facing guide to choosing and installing themes, see Themes. Upstream palette sources and their attributions are listed in the authoring guide.
Built-in themes
Section titled “Built-in themes”BuiltinTheme (lib/data/model/app/builtin_theme.dart) lists the packages
bundled with this build: only what the app needs without a network. Default
is a Dart constant and does not read an asset; its picker label is localized
(fl_lib defaultLabel). AMOLED, which legacy AMOLED theme modes migrate
to, has a source folder under assets/themes/amoled/ containing a
manifest.toml, registered in pubspec.yaml. Every other official theme is in
the theme store (see Official themes); a saved preset that
names a bundled theme this build no longer carries falls back to Default
(ThemePackages.reconcileSelection).
Bundled folders use the same installer as imported folders and .fsbt
archives, so built-in themes support the same fields as installed themes. The
folders are committed as source and bundled directly by Flutter; no archive or
other binary asset is committed for them.
A new official theme goes to the store, not here. Bundle one only if the app
must have it offline, by creating its folder, registering it in pubspec.yaml,
and adding a BuiltinTheme case.
Loading
Section titled “Loading”BuiltinThemeLoader (lib/core/service/theme_package.dart) loads themes on
demand. _loaded stores completed results and _pending tracks active loads,
so simultaneous requests for the same theme share one parse. Failed loads are
not cached and can be retried.
Opening the picker does not load theme files. A folder is read when selected or at startup if it was the saved selection. Built-in assets use a separate runtime cache and do not appear in the user-installed theme list.
The parser and the editor schema
Section titled “The parser and the editor schema”Three files define the manifest grammar:
theme_package.dart (top-level tables, archive entries, the schema range),
theme_components.dart (component fields, states, and every numeric range) and
theme_palette.dart (the non-deprecated ColorScheme roles).
docs/schemas/fsbt-manifest.schema.json mirrors these definitions so editors
can report errors while a file is being edited. The schema is derived from the
parser definitions, not from this documentation.
The schema is as strict as the installer except for one rule it cannot express:
the installer rejects an icons.colors key without a matching icons.images
entry. This check spans two tables and cannot be represented in JSON Schema.
The CI docs job validates every checked-in manifest, including the bundled
folders and docs/examples/aurora/, against the schema.
test/unit/theme_schema_test.dart checks the schema against the parser, so the
fields, enums, and bounds offered by editors match what the installer accepts.
Theme store
Section titled “Theme store”ThemeRepo (lib/core/service/theme_repo.dart) reads a catalog of repositories
and then each repository’s tree. assets/catalog/repos.toml provides the
initial catalog when no network is available. When Urls.themeCatalog
responds, the app reads that catalog instead.
Repository URLs must use HTTPS and resolve to a tarball. Git repositories are
fetched from <address>/archive/HEAD.tar.gz, which follows the repository’s
default branch without assuming its name. ThemePackages.download rejects URLs
containing credentials and follows at most three redirects, checking that each
target still uses HTTPS. Plain HTTP is rejected because the repository
determines which bytes are installed.
ThemeRepo enforces these limits: at most 100 repositories and 1 MiB per
catalog; 16 MiB compressed and 64 MiB unpacked per repository tree; and 8 MiB
per entry. Unknown sections are skipped, allowing one tree to contain both
themes/ and plugins/ for the app and plugin feature.
The store page is in lib/view/page/theme_store/. Its listing is persisted
between runs in SettingStore.themeStoreCache using ThemeStore.toJson() with
updateLastModified: false. The key is included in
SettingStore.deviceLocalKeys because this cache records catalog contents; it
is not user data to sync or restore on another device. Cached items do not
include repository files (ThemeStoreItem.index is null), so installing a
version stored in a repository fetches its tarball again. The digest is checked
in either case.
Official themes
Section titled “Official themes”Official themes live in store/ in this repository: store/repo.toml, and
for each theme a listing store/themes/<id>.toml beside its source folder
store/themes/<id>/. test/unit/theme_store_tree_test.dart reads the folder
the way the store does, so a listing the reader would drop fails a test rather
than disappearing from the store.
The app does not download this repository. The website build runs
scripts/store-tarball.sh, which writes store/ at HEAD with git archive
to public/store.tar.gz, and the catalog lists
https://serverbox.lollipopkit.com/store.tar.gz. A change to store/ reaches
the store when the website is deployed: the Cloudflare Pages project builds on
changes under store/ as well as website/ and docs/. The same build lists
the themes on the site (website/store-data.js). git archive is used rather than tar
because macOS tar writes binary xattr records the reader refuses.
scripts/publish-themes.py publishes every theme that changed since its newest
recorded version, in one run. It runs the serverbox-theme skill’s
scripts/publish.py with store/ as the theme repository — the same publisher
a third-party author runs in their own repository, where their themes are
released; only official themes are released from this repository. It packs each folder and compares the package’s
digest with the newest version in the listing: the same digest means nothing
changed, and the theme is skipped. A changed theme gets the next patch version
(--bump minor|major for another part, <id>=<version> for an exact one, 1.0.0
for a theme with no version yet). --dry-run shows the plan; naming ids limits
the run to those themes.
Comparing digests works because the package is deterministic: entries sorted, one fixed timestamp and permission, and stored rather than deflated, so the bytes depend on the files alone and not on a checkout’s modification times or a machine’s zlib.
All new packages are uploaded together, as <id>-<version>.fsbt, to one
release of this repository tagged themes; then each listing gets a
[[version]] block with the digest and size, and the listings are committed
afterwards. One release holds every package: the app’s update check reads this
repository’s releases, a tag without a build number is skipped there, and a
release per theme version would push the app’s releases off the first page. The
release is a pre-release, created with --latest=false, and is never this
repository’s Latest: GitHub does not mark a pre-release Latest, so Latest is
always an app release, and the script checks that before it uploads anything.
The script keeps two ordering and integrity rules. It uploads packages before updating the listings, so a listing never points to a missing asset. It never replaces an uploaded asset — one left by an interrupted run is accepted only if its bytes are the same — and never records a version number for a second set of bytes.