Skip to content

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.

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.

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.

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.

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 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.