Skip to content

Themes

This document covers the built-in themes 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 (packages/fl_lib/lib/src/theme/builtin.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). The AMOLED theme source is in packages/fl_lib/assets/themes/amoled/, with a manifest.toml registered in fl_lib’s 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 fl_lib’s pubspec.yaml, and adding a BuiltinTheme case.

BuiltinThemeLoader (packages/fl_lib/lib/src/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 in packages/fl_lib/lib/src/theme/ define the manifest grammar: package.dart (top-level tables, archive entries, the schema range), components.dart (component fields, states, and every numeric range) and 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 (packages/fl_lib/lib/src/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 packages/fl_lib/lib/src/theme/view/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.

A listing’s description is a string or a table of language tags (ThemeText); the page resolves it with the app’s locale, and the cache keeps the table. A row’s preview (packages/fl_lib/lib/src/theme/view/store/preview.dart) is built only while the row is expanded. It renders real widgets under the ThemeData that buildAppTheme (packages/fl_lib/lib/src/theme/view/app_theme.dart) makes from the package, the same function the app uses for itself with AppThemeSource.current(). An uninstalled theme is installed for the preview under <app name>_theme_preview in the system’s temporary directory, apart from the user’s themes, and that directory is deleted when the page is disposed.

A package with [variants] installs once; each variant is written as a complete, normalized theme directory under variants/<key>/ of the installation, so ThemePackages.installed(id, variant: key) reads it with the same code as a package without variants. The preset carries the variant as package:<installation id>#<key>; appThemePackage keeps the installation id alone.

No store theme ships inside the app: a .fsbt in the repository is a binary git cannot diff. A backup made while one did (bundled in ThemeBackup) is restored from the store, by the release whose digest is the installation id.

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>/, and for each icon pack the same under store/icons/. test/unit/theme_store_tree_test.dart checks this folder with the store’s reader. A rejected listing fails the test instead of silently disappearing from the store.

The app does not download this repository. The website build runs scripts/store-tarball.sh, which packs store/ as committed at HEAD into 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). Images and theme packages are Git LFS objects, and git archive would write their pointers, so the script packs the working tree: it refuses a store/ that differs from HEAD or still holds pointers, and writes the archive with Python’s tarfile (sorted, no xattr records, which macOS tar adds and 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.

The package is deterministic, so its digest reflects file contents alone: entries are sorted, timestamps and permissions are fixed, and files are stored without compression. Checkout modification times and the machine’s zlib do not affect the result.

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.