From 2f42e197328c8a394668142be14f69782c6ed27c Mon Sep 17 00:00:00 2001 From: Federico Rao <157750791+Federicorao@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:17:28 +0200 Subject: [PATCH 1/8] Update CONTRIBUTING.md hello.py example to use modern `ft.run()` and `ft.Page`/`ft.Text` instead of deprecated `flet.app (#6582) * Fix issue #6579 * docs: use fvm for Flutter commands --- CONTRIBUTING.md | 130 +++++++++++++++++++++++++----------------------- 1 file changed, 69 insertions(+), 61 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60f68e536e..63cd7b2f47 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,13 +70,12 @@ uv sync Create `hello.py` file with a minimal Flet program: ```python -import flet -from flet import Page, Text +import flet as ft -def main(page: Page): - page.add(Text("Hello, world!")) +def main(page: ft.Page): + page.add(ft.Text("Hello, world!")) -flet.app(target=main) +ft.run(main) ``` and then run it: @@ -169,12 +168,12 @@ First, run `printenv | grep FLET` (or `gci env:* | findstr FLET` on Windows) in - To build the Flutter client for MacOS, run: ``` - flutter build macos + fvm flutter build macos ``` When the build is complete, you should see the Flet bundle in the `FLET_VIEW_PATH`. (Running it will open a blank window.) - To build the Flutter client for Web, run the below command: ``` - flutter build web + fvm flutter build web --wasm ``` When the build is complete, a directory `client/build/web` will be created. @@ -257,84 +256,93 @@ For patches to the current stable release, branch directly from `main`, fix, ope * Keep the `## {version}` section in `packages/flet/CHANGELOG.md` in sync with the root `CHANGELOG.md` before tagging the release. * Ensure every merged PR on `release/v{version}` added a new record to the active root `CHANGELOG.md` section. -* Open terminal in `client` directory and run `flutter pub get` to update Flet dependency versions in `client/pubspec.lock`. -* Templates are in `sdk/python/templates/` and automatically packaged as zip artifacts with the GitHub Release. No manual branch creation in external repos is needed. +* Open terminal in `client` directory and run `fvm flutter clean; fvm flutter pub get`. +* Increment version in `packages/flet/pubspec.yaml`. +* Review and update `CHANGELOG.md` for the release version. +* Update version in `packages/flet/pubspec.yaml` to the release version if not already done. +* Run `fvm flutter build macos`, `fvm flutter build linux`, `fvm flutter build windows`, `fvm flutter build web --wasm` to ensure everything compiles. +* Run `uv run pytest` in `sdk/python` to ensure all tests pass. +* If any test fails, fix the issue before proceeding. +* Once all checks pass, commit the final changes (version bumps, changelog updates) to the release branch. ## New macOS environment for Flet developer -* **Homebrew**: https://brew.sh/ +This section outlines how to prepare a fresh macOS environment for Flet development. -After installing homebrew, install xz libraries with it: -``` -brew install xz -``` +### Prerequisites -* **Pyenv**. Install with `brew`: https://github.com/pyenv/pyenv?tab=readme-ov-file#unixmacos - * Install and switch to the latest Python 3.12: -``` -pyenv install 3.12.6 -pyenv global 3.12.6 -``` +* **uv**: https://docs.astral.sh/uv/getting-started/installation/ +* **git**: https://git-scm.com/downloads +* **Flutter**: https://docs.flutter.dev/get-started/install/macos +* **Xcode**: Install from the Mac App Store, then open it, agree to license, and install command line tools with `xcode-select --install`. +* **Android Studio** (optional for Android development): https://developer.android.com/studio +* **FVM** - Flutter Version Manager: https://fvm.app/documentation/getting-started/installation +* **CocoaPods**: `sudo gem install cocoapods` +* **Visual Studio Code**: https://code.visualstudio.com/ -Setup your shell environment: https://github.com/pyenv/pyenv#set-up-your-shell-environment-for-pyenv +### Clone the repository -``` -echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zprofile -echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zprofile -echo 'eval "$(pyenv init -)"' >> ~/.zprofile +```bash +git clone https://github.com/flet-dev/flet.git +cd flet ``` -Ensure Python version is 3.12.6 and location is `/Users/{user}/.pyenv/shims/python`: +### Install Flutter -``` -python --version -which python -``` +Follow the [official guide](https://docs.flutter.dev/get-started/install/macos) to install Flutter. Ensure Flutter is in your PATH and run `fvm flutter doctor` to verify. -* **Rbenv**. Install with `brew`: https://github.com/rbenv/rbenv?tab=readme-ov-file#homebrew - * Install and switch to the latest Ruby: -``` -rbenv install 3.3.5 -rbenv global 3.3.5 +### Install uv + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh ``` -Ensure Ruby version is 3.3.5 and location is `/Users/{user}/.rbenv/shims/ruby`: +### Set up Python SDK -``` -ruby --version -which ruby +```bash +cd sdk/python +uv sync ``` -* **VS Code**. Install "Apple silicon" release: https://code.visualstudio.com/download +### Set up Flutter client -* **GitHub Desktop**: https://desktop.github.com/download/ -Open GitHub Desktop app, install Rosetta. +```bash +cd client +fvm flutter pub get +``` -* **uv**: https://docs.astral.sh/uv/getting-started/installation/ +### Set environment variables -After installing uv, set PATH: -``` -echo 'export PATH=$HOME/.local/bin:$PATH' >> ~/.zprofile +Add the following to your shell profile (e.g., `~/.zshrc`): + +```bash +export FLET_VIEW_PATH="$HOME/flet/client/build/macos/Build/Products/Release" +export FLET_WEB_PATH="$HOME/flet/client/build/web" ``` -Check `uv` version and make sure it's in PATH: +Then reload: `source ~/.zshrc` -``` -uv --version -``` +### Build Flutter client -* **Android Studio** for Android SDK required by Flutter: https://developer.android.com/studio -* **XCode** for macOS and iOS SDKs: https://apps.apple.com/ca/app/xcode/id497799835?mt=12 -* **FVM** - Flutter Version Manager: https://fvm.app/documentation/getting-started/installation -Install flutter with fvm: -``` -fvm install 3.24.3 -fvm global 3.24.3 +```bash +cd client +fvm flutter build macos +fvm flutter build web --wasm ``` -Set PATH: -``` -echo 'export PATH=$HOME/fvm/default/bin:$PATH' >> ~/.zprofile +### Verify installation + +Create a `hello.py` file with the updated example and run it with `uv run python hello.py`. You should see a window with "Hello, world!". + +### Running tests + +```bash +cd sdk/python +uv run pytest ``` -* **cocoapods**: https://guides.cocoapods.org/using/getting-started.html#installation +### Additional notes + +* For Android development, set up an Android emulator or connect a physical device. +* For iOS development, you need Xcode and a Mac with Apple Silicon or an Intel Mac. +* Refer to the official Flutter documentation for any platform-specific setup. From c2b5ba8c3fab9497e915b62deba1ddb915a09d0a Mon Sep 17 00:00:00 2001 From: InesaFitsner Date: Thu, 18 Jun 2026 08:21:12 -0700 Subject: [PATCH 2/8] feat: add flet-spinkit extension package wrapping flutter_spinkit ^5.2.2 (#6596) * feat(flet-spinkit): add flet-spinkit extension package wrapping flutter_spinkit ^5.2.2 Adds 31 separate spinner animation controls (SpinKitRotatingCircle, SpinKitWave, SpinKitRipple, etc.) following the same structure as flet-color-pickers. Includes a spinkit_showcase example. * fix(flet-spinkit): use local path source for flet-spinkit in example * fix(flet-spinkit): use ft.Alignment.CENTER instead of ft.alignment.center * fix(flet-spinkit): use ft.Border.all instead of ft.border.all * refactor(flet-spinkit): simplify example to a single SpinKitRotatingCircle * feat(flet-spinkit): register flet_spinkit extension in client runner * fix(flet-spinkit): remove SpinKitPumpCurve/RingCurve (Curve helpers, not widgets), add SpinKitWaveSpinner * refactor(flet-spinkit): strip SpinKit prefix from all Python class names Python API is now fsk.RotatingCircle, fsk.Wave, fsk.WaveType etc. Flutter control type strings (@ft.control decorator values) are unchanged. * feat(flet-spinkit): expand example to a full 30-spinner grid showcase * feat(flet-spinkit): add spinkit_props example; use system theme in both examples * docs(flet-spinkit): add single-page SpinKit docs and sidebar entry * docs(flet-spinkit): register flet_spinkit in crocodocs; remove image refs from docs * ci(flet-spinkit): add flet-spinkit to build and publish pipeline; remove local path source overrides from examples * test(flet-spinkit): add unit tests; fix stale showcase description * refactor(flet-spinkit): rename import alias from fsk to spins in examples and tests * feat(flet-spinkit): complete integration and cleanup - Register flet-spinkit in sdk/python/pyproject.toml (dependencies + uv.sources) - Register flet-spinkit in sdk/python/packages/flet/pyproject.toml (extensions group) - Register flet-spinkit in flet_build_test/pyproject.toml (dependencies, uv.sources, dev_packages) - Replace custom _parseWaveType() helper with parseEnum() in spinkit.dart - Update implement-flet-extension SKILL.md with lessons learned from this implementation * docs(flet-spinkit): split into per-control pages, add WaveType page Replace single-page spinkit docs with individual pages per control, consistent with flet-color-pickers and other extensions. Add WaveType enum page to fix unresolved reST cross-reference on the Wave page. * test(flet-spinkit): rewrite as proper integration tests using ftt.FletTestApp Replace pure Python unit tests with integration tests that render each control in a real Flutter app. Use tester.pump() instead of pump_and_settle() to advance animation clock without waiting for continuous animations to settle. Assert finder.count == 1 to verify each control is mounted in the Flutter widget tree. * docs(flet-spinkit): reformat README to match other extension packages --- .../skills/implement-flet-extension/SKILL.md | 25 +- .github/workflows/ci.yml | 2 + client/lib/main.dart | 2 + client/pubspec.lock | 15 + client/pubspec.yaml | 3 + .../apps/flet_build_test/pyproject.toml | 3 + .../extensions/spinkit/spinkit_props/main.py | 250 +++++++++ .../spinkit/spinkit_props/pyproject.toml | 29 + .../spinkit/spinkit_showcase/main.py | 82 +++ .../spinkit/spinkit_showcase/pyproject.toml | 33 ++ sdk/python/packages/flet-spinkit/CHANGELOG.md | 5 + sdk/python/packages/flet-spinkit/LICENSE | 200 +++++++ sdk/python/packages/flet-spinkit/README.md | 42 ++ .../packages/flet-spinkit/pyproject.toml | 27 + .../flet-spinkit/src/flet_spinkit/__init__.py | 67 +++ .../flet-spinkit/src/flet_spinkit/spinkit.py | 495 ++++++++++++++++++ .../src/flutter/flet_spinkit/.gitignore | 34 ++ .../flet_spinkit/analysis_options.yaml | 4 + .../flet_spinkit/lib/flet_spinkit.dart | 3 + .../flet_spinkit/lib/src/extension.dart | 45 ++ .../flutter/flet_spinkit/lib/src/spinkit.dart | 260 +++++++++ .../src/flutter/flet_spinkit/pubspec.yaml | 22 + .../extensions/spinkit/test_spinkit.py | 148 ++++++ sdk/python/packages/flet/pyproject.toml | 1 + sdk/python/pyproject.toml | 2 + tools/crocodocs/pyproject.toml | 1 + website/docs/controls/spinkit/chasingdots.md | 10 + website/docs/controls/spinkit/circle.md | 10 + website/docs/controls/spinkit/cubegrid.md | 10 + .../docs/controls/spinkit/dancingsquare.md | 10 + website/docs/controls/spinkit/doublebounce.md | 10 + website/docs/controls/spinkit/dualring.md | 10 + website/docs/controls/spinkit/fadingcircle.md | 10 + website/docs/controls/spinkit/fadingcube.md | 10 + website/docs/controls/spinkit/fadingfour.md | 10 + website/docs/controls/spinkit/fadinggrid.md | 10 + website/docs/controls/spinkit/foldingcube.md | 10 + website/docs/controls/spinkit/hourglass.md | 10 + website/docs/controls/spinkit/index.md | 78 +++ website/docs/controls/spinkit/pianowave.md | 10 + .../docs/controls/spinkit/pouringhourglass.md | 10 + .../spinkit/pouringhourglassrefined.md | 10 + website/docs/controls/spinkit/pulse.md | 10 + website/docs/controls/spinkit/pulsinggrid.md | 10 + website/docs/controls/spinkit/pumpingheart.md | 10 + website/docs/controls/spinkit/ring.md | 10 + website/docs/controls/spinkit/ripple.md | 10 + .../docs/controls/spinkit/rotatingcircle.md | 10 + .../docs/controls/spinkit/rotatingplain.md | 10 + .../docs/controls/spinkit/spinningcircle.md | 10 + .../docs/controls/spinkit/spinninglines.md | 10 + website/docs/controls/spinkit/squarecircle.md | 10 + website/docs/controls/spinkit/threebounce.md | 10 + website/docs/controls/spinkit/threeinout.md | 10 + .../docs/controls/spinkit/wanderingcubes.md | 10 + website/docs/controls/spinkit/wave.md | 10 + website/docs/controls/spinkit/wavespinner.md | 10 + website/docs/controls/spinkit/wavetype.md | 10 + website/sidebars.js | 166 ++++++ website/sidebars.yml | 33 ++ 60 files changed, 2380 insertions(+), 7 deletions(-) create mode 100644 sdk/python/examples/extensions/spinkit/spinkit_props/main.py create mode 100644 sdk/python/examples/extensions/spinkit/spinkit_props/pyproject.toml create mode 100644 sdk/python/examples/extensions/spinkit/spinkit_showcase/main.py create mode 100644 sdk/python/examples/extensions/spinkit/spinkit_showcase/pyproject.toml create mode 100644 sdk/python/packages/flet-spinkit/CHANGELOG.md create mode 100644 sdk/python/packages/flet-spinkit/LICENSE create mode 100644 sdk/python/packages/flet-spinkit/README.md create mode 100644 sdk/python/packages/flet-spinkit/pyproject.toml create mode 100644 sdk/python/packages/flet-spinkit/src/flet_spinkit/__init__.py create mode 100644 sdk/python/packages/flet-spinkit/src/flet_spinkit/spinkit.py create mode 100644 sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/.gitignore create mode 100644 sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/analysis_options.yaml create mode 100644 sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/lib/flet_spinkit.dart create mode 100644 sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/lib/src/extension.dart create mode 100644 sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/lib/src/spinkit.dart create mode 100644 sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/pubspec.yaml create mode 100644 sdk/python/packages/flet/integration_tests/extensions/spinkit/test_spinkit.py create mode 100644 website/docs/controls/spinkit/chasingdots.md create mode 100644 website/docs/controls/spinkit/circle.md create mode 100644 website/docs/controls/spinkit/cubegrid.md create mode 100644 website/docs/controls/spinkit/dancingsquare.md create mode 100644 website/docs/controls/spinkit/doublebounce.md create mode 100644 website/docs/controls/spinkit/dualring.md create mode 100644 website/docs/controls/spinkit/fadingcircle.md create mode 100644 website/docs/controls/spinkit/fadingcube.md create mode 100644 website/docs/controls/spinkit/fadingfour.md create mode 100644 website/docs/controls/spinkit/fadinggrid.md create mode 100644 website/docs/controls/spinkit/foldingcube.md create mode 100644 website/docs/controls/spinkit/hourglass.md create mode 100644 website/docs/controls/spinkit/index.md create mode 100644 website/docs/controls/spinkit/pianowave.md create mode 100644 website/docs/controls/spinkit/pouringhourglass.md create mode 100644 website/docs/controls/spinkit/pouringhourglassrefined.md create mode 100644 website/docs/controls/spinkit/pulse.md create mode 100644 website/docs/controls/spinkit/pulsinggrid.md create mode 100644 website/docs/controls/spinkit/pumpingheart.md create mode 100644 website/docs/controls/spinkit/ring.md create mode 100644 website/docs/controls/spinkit/ripple.md create mode 100644 website/docs/controls/spinkit/rotatingcircle.md create mode 100644 website/docs/controls/spinkit/rotatingplain.md create mode 100644 website/docs/controls/spinkit/spinningcircle.md create mode 100644 website/docs/controls/spinkit/spinninglines.md create mode 100644 website/docs/controls/spinkit/squarecircle.md create mode 100644 website/docs/controls/spinkit/threebounce.md create mode 100644 website/docs/controls/spinkit/threeinout.md create mode 100644 website/docs/controls/spinkit/wanderingcubes.md create mode 100644 website/docs/controls/spinkit/wave.md create mode 100644 website/docs/controls/spinkit/wavespinner.md create mode 100644 website/docs/controls/spinkit/wavetype.md diff --git a/.agents/skills/implement-flet-extension/SKILL.md b/.agents/skills/implement-flet-extension/SKILL.md index 01584b2c9d..457354e264 100644 --- a/.agents/skills/implement-flet-extension/SKILL.md +++ b/.agents/skills/implement-flet-extension/SKILL.md @@ -44,7 +44,7 @@ Implement a Flet extension around an external Flutter package using existing `fl ## Flutter-side Rules -- Use `parseEnum()` for enums. +- Use `parseEnum()` for enums — it's exported from `package:flet/flet.dart`. Call it as `parseEnum(MyEnum.values, widget.control.getString("attr"), MyEnum.defaultVal)!`. Do NOT write a custom `_parseXxx()` switch helper. - For control attributes, prefer `widget.control.getBool()/getDouble()/...` accessors. - For non-control parsing, use shared `parseSomething()` helpers. - Do not add one-off private parser utilities when standard helpers exist. @@ -65,22 +65,33 @@ Without matching defaults, Dart receives `null` and either crashes or silently u ## Integration Checklist -- Add dependency and registration to Flet client app. -- Add extension package to `sdk/python/pyproject.toml`. -- Add extension to `sdk/python/packages/flet/pyproject.toml`. -- Add extension to `sdk/python/examples/apps/flet_build_test/pyproject.toml`. +- Register in the Flet client app — two files: + 1. `client/pubspec.yaml`: add `flet_: path: ../sdk/python/packages/flet-/src/flutter/flet_` under `dependencies`. + 2. `client/lib/main.dart`: add `import 'package:flet_/flet_.dart' as flet_;` and `flet_.Extension()` to the `extensions` list. +- Add extension package to `sdk/python/pyproject.toml` in **two places**: the `dependencies` list and `[tool.uv.sources]` as `{ workspace = true }`. +- Add extension to `sdk/python/packages/flet/pyproject.toml` in the `[dependency-groups] extensions` list. +- Add extension to `sdk/python/examples/apps/flet_build_test/pyproject.toml` in **three places**: `[project] dependencies`, `[tool.uv.sources]` as `{ path = "...", editable = true }`, and `[tool.flet.dev_packages]` as a relative path string. +- Add extension to `tools/crocodocs/pyproject.toml` under `[tool.crocodocs.packages]` as ` = "../../sdk/python/packages//src"`. This fixes "Missing API entry" errors in the docs. Note: `api-data.json` is gitignored (generated at build time); only the `pyproject.toml` change needs committing. - Add extension to `.github/workflows/ci.yml` in both places: - `build_flet_extensions` -> `PACKAGES` list. - `py_publish` -> `for pkg in ...` publish loop. - Add `.gitignore` to Flutter extension project if missing. +- Remove `[tool.uv.sources]` local path overrides from example `pyproject.toml` files before opening a PR (they are development-only conveniences). ## Docs, Examples, Tests - Add control/service docs under `website/docs/controls/` for controls and `website/docs/services/` for services. +- Always create one doc page per control, even for extensions with many similar controls. Use `index.md` for the overview (install instructions, examples, list of links) and individual `.md` files for each control — consistent with `flet-color-pickers` and other extensions. +- Use `` and `` JSX from `@site/src/components/crocodocs` to render API docs. Do NOT add `image=`, `imageCaption=`, or `imageWidth=` props to `` when no screenshots exist yet. +- In the `## Examples` section, do NOT add `###` subtitles above `` blocks — titles are injected automatically from the example file itself. - Add all custom enums/types docs and update `website/sidebars.yml` navigation. - Use markdown filenames without underscores (`codeeditor.md`, not `code_editor.md`). -- Add examples under `sdk/python/examples` in the appropriate category for control vs service. -- Add integration tests under `packages/flet/integration_tests` in the appropriate category for control vs service. +- Add examples under `sdk/python/examples/extensions//` for extension controls. +- Use `import flet_ as ` in examples (e.g., `import flet_spinkit as spins`). Keep alias short but readable. +- Use `ft.Colors.SURFACE_CONTAINER_HIGHEST` for card/cell backgrounds in showcase examples — it adapts to light and dark system themes automatically. +- Do NOT set an explicit dark theme in examples; let the app use system theme (no `page.theme_mode`). +- Add integration tests under `packages/flet/integration_tests/extensions//` — **not** inside the extension package's own directory (no `tests/` folder in the package itself, matching the pattern of `flet-code-editor`, `flet-color-pickers`, etc.). +- For controls with continuously-running animations, do NOT use `assert_control_screenshot` or `pump_and_settle` — they will timeout waiting for animations to settle. Instead use `await flet_app.tester.pump(duration=ft.Duration(milliseconds=500))` which advances the clock by a fixed amount. This still runs real Flutter rendering and catches crashes, without screenshot comparison. - Ensure generated screenshots are suitable for docs usage when visual examples are added. ## Upgrade and Compatibility Guardrails diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1dbfd39668..0abc7379ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -681,6 +681,7 @@ jobs: flet-permission-handler flet-rive flet-secure-storage + flet-spinkit flet-video flet-webview ) @@ -845,6 +846,7 @@ jobs: flet_permission_handler \ flet_rive \ flet_secure_storage \ + flet_spinkit \ flet_video \ flet_webview; do uv publish dist/**/${pkg}-* diff --git a/client/lib/main.dart b/client/lib/main.dart index 131338e54a..1e5e1b6abb 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -24,6 +24,7 @@ import 'package:flet_rive/flet_rive.dart' as flet_rive; // --FAT_CLIENT_END-- import 'package:flet_secure_storage/flet_secure_storage.dart' as flet_secure_storage; +import 'package:flet_spinkit/flet_spinkit.dart' as flet_spinkit; // --FAT_CLIENT_START-- import 'package:flet_video/flet_video.dart' as flet_video; // --FAT_CLIENT_END-- @@ -59,6 +60,7 @@ void main([List? args]) async { flet_map.Extension(), flet_permission_handler.Extension(), flet_secure_storage.Extension(), + flet_spinkit.Extension(), flet_webview.Extension(), // --FAT_CLIENT_START-- diff --git a/client/pubspec.lock b/client/pubspec.lock index e095af5265..7a78d53a3d 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -465,6 +465,13 @@ packages: relative: true source: path version: "0.1.0" + flet_spinkit: + dependency: "direct main" + description: + path: "../sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit" + relative: true + source: path + version: "0.1.0" flet_video: dependency: "direct main" description: @@ -631,6 +638,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + flutter_spinkit: + dependency: transitive + description: + name: flutter_spinkit + sha256: "77850df57c00dc218bfe96071d576a8babec24cf58b2ed121c83cca4a2fdce7f" + url: "https://pub.dev" + source: hosted + version: "5.2.2" flutter_svg: dependency: transitive description: diff --git a/client/pubspec.yaml b/client/pubspec.yaml index 79ebf5030c..b70a83270a 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -82,6 +82,9 @@ dependencies: flet_secure_storage: path: ../sdk/python/packages/flet-secure-storage/src/flutter/flet_secure_storage + flet_spinkit: + path: ../sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit + flet_webview: path: ../sdk/python/packages/flet-webview/src/flutter/flet_webview diff --git a/sdk/python/examples/apps/flet_build_test/pyproject.toml b/sdk/python/examples/apps/flet_build_test/pyproject.toml index aeba0ac742..256622f74b 100644 --- a/sdk/python/examples/apps/flet_build_test/pyproject.toml +++ b/sdk/python/examples/apps/flet_build_test/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "flet-permission-handler", "flet-rive", "flet-secure-storage", + "flet-spinkit", "flet-video", "flet-webview", ] @@ -51,6 +52,7 @@ flet-map = { path = "../../../packages/flet-map", editable = true } flet-permission-handler = { path = "../../../packages/flet-permission-handler", editable = true } flet-rive = { path = "../../../packages/flet-rive", editable = true } flet-secure-storage = { path = "../../../packages/flet-secure-storage", editable = true } +flet-spinkit = { path = "../../../packages/flet-spinkit", editable = true } flet-video = { path = "../../../packages/flet-video", editable = true } flet-webview = { path = "../../../packages/flet-webview", editable = true } @@ -69,6 +71,7 @@ flet-map = "../../../packages/flet-map" flet-permission-handler = "../../../packages/flet-permission-handler" flet-rive = "../../../packages/flet-rive" flet-secure-storage = "../../../packages/flet-secure-storage" +flet-spinkit = "../../../packages/flet-spinkit" flet-video = "../../../packages/flet-video" flet-webview = "../../../packages/flet-webview" diff --git a/sdk/python/examples/extensions/spinkit/spinkit_props/main.py b/sdk/python/examples/extensions/spinkit/spinkit_props/main.py new file mode 100644 index 0000000000..4fbb386c7a --- /dev/null +++ b/sdk/python/examples/extensions/spinkit/spinkit_props/main.py @@ -0,0 +1,250 @@ +import flet_spinkit as spins + +import flet as ft + + +def section(title: str, controls: list) -> ft.Column: + return ft.Column( + [ + ft.Text(title, size=14, weight=ft.FontWeight.BOLD), + ft.Row(controls, spacing=24, wrap=True), + ], + spacing=12, + ) + + +def labeled(spinner, label: str) -> ft.Column: + return ft.Column( + [ + ft.Container( + content=spinner, + alignment=ft.Alignment.CENTER, + width=80, + height=80, + ), + ft.Text( + label, + size=10, + text_align=ft.TextAlign.CENTER, + ), + ], + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + spacing=4, + ) + + +def main(page: ft.Page): + page.title = "SpinKit Properties" + page.scroll = ft.ScrollMode.AUTO + page.padding = 24 + + page.add( + ft.Text("SpinKit Properties", size=24, weight=ft.FontWeight.BOLD), + ft.Divider(height=16, color=ft.Colors.TRANSPARENT), + # Colors + section( + "color", + [ + labeled(spins.RotatingCircle(color=ft.Colors.BLUE, size=50), "BLUE"), + labeled(spins.RotatingCircle(color=ft.Colors.RED, size=50), "RED"), + labeled(spins.RotatingCircle(color=ft.Colors.GREEN, size=50), "GREEN"), + labeled( + spins.RotatingCircle(color=ft.Colors.ORANGE, size=50), "ORANGE" + ), + labeled( + spins.RotatingCircle(color=ft.Colors.PURPLE, size=50), "PURPLE" + ), + labeled(spins.RotatingCircle(color=ft.Colors.TEAL, size=50), "TEAL"), + ], + ), + ft.Divider(height=8), + # Sizes + section( + "size", + [ + labeled(spins.ThreeBounce(color=ft.Colors.BLUE, size=20), "20"), + labeled(spins.ThreeBounce(color=ft.Colors.BLUE, size=30), "30"), + labeled(spins.ThreeBounce(color=ft.Colors.BLUE, size=40), "40"), + labeled(spins.ThreeBounce(color=ft.Colors.BLUE, size=50), "50"), + labeled(spins.ThreeBounce(color=ft.Colors.BLUE, size=70), "70"), + ], + ), + ft.Divider(height=8), + # Durations + section( + "duration", + [ + labeled( + spins.Pulse( + color=ft.Colors.BLUE, + size=50, + duration=ft.Duration(milliseconds=400), + ), + "400ms", + ), + labeled( + spins.Pulse( + color=ft.Colors.BLUE, + size=50, + duration=ft.Duration(milliseconds=700), + ), + "700ms", + ), + labeled( + spins.Pulse( + color=ft.Colors.BLUE, + size=50, + duration=ft.Duration(milliseconds=1000), + ), + "1000ms", + ), + labeled( + spins.Pulse( + color=ft.Colors.BLUE, + size=50, + duration=ft.Duration(milliseconds=2000), + ), + "2000ms", + ), + labeled( + spins.Pulse( + color=ft.Colors.BLUE, + size=50, + duration=ft.Duration(milliseconds=4000), + ), + "4000ms", + ), + ], + ), + ft.Divider(height=8), + # Wave — wave_type + section( + "Wave · wave_type", + [ + labeled( + spins.Wave( + color=ft.Colors.BLUE, size=50, wave_type=spins.WaveType.START + ), + "START", + ), + labeled( + spins.Wave( + color=ft.Colors.BLUE, size=50, wave_type=spins.WaveType.CENTER + ), + "CENTER", + ), + labeled( + spins.Wave( + color=ft.Colors.BLUE, size=50, wave_type=spins.WaveType.END + ), + "END", + ), + ], + ), + ft.Divider(height=8), + # Wave — item_count + section( + "Wave · item_count", + [ + labeled(spins.Wave(color=ft.Colors.BLUE, size=50, item_count=3), "3"), + labeled(spins.Wave(color=ft.Colors.BLUE, size=50, item_count=5), "5"), + labeled(spins.Wave(color=ft.Colors.BLUE, size=50, item_count=7), "7"), + labeled(spins.Wave(color=ft.Colors.BLUE, size=50, item_count=10), "10"), + ], + ), + ft.Divider(height=8), + # Ring — line_width + section( + "Ring · line_width", + [ + labeled(spins.Ring(color=ft.Colors.BLUE, size=50, line_width=2), "2"), + labeled(spins.Ring(color=ft.Colors.BLUE, size=50, line_width=5), "5"), + labeled(spins.Ring(color=ft.Colors.BLUE, size=50, line_width=8), "8"), + labeled(spins.Ring(color=ft.Colors.BLUE, size=50, line_width=12), "12"), + ], + ), + ft.Divider(height=8), + # DualRing — line_width + section( + "DualRing · line_width", + [ + labeled( + spins.DualRing(color=ft.Colors.BLUE, size=50, line_width=2), "2" + ), + labeled( + spins.DualRing(color=ft.Colors.BLUE, size=50, line_width=5), "5" + ), + labeled( + spins.DualRing(color=ft.Colors.BLUE, size=50, line_width=8), "8" + ), + labeled( + spins.DualRing(color=ft.Colors.BLUE, size=50, line_width=12), "12" + ), + ], + ), + ft.Divider(height=8), + # Ripple — border_width + section( + "Ripple · border_width", + [ + labeled( + spins.Ripple(color=ft.Colors.BLUE, size=50, border_width=2), "2" + ), + labeled( + spins.Ripple(color=ft.Colors.BLUE, size=50, border_width=4), "4" + ), + labeled( + spins.Ripple(color=ft.Colors.BLUE, size=50, border_width=8), "8" + ), + labeled( + spins.Ripple(color=ft.Colors.BLUE, size=50, border_width=14), "14" + ), + ], + ), + ft.Divider(height=8), + # SpinningLines — line_width + section( + "SpinningLines · line_width", + [ + labeled( + spins.SpinningLines(color=ft.Colors.BLUE, size=50, line_width=1), + "1", + ), + labeled( + spins.SpinningLines(color=ft.Colors.BLUE, size=50, line_width=2), + "2", + ), + labeled( + spins.SpinningLines(color=ft.Colors.BLUE, size=50, line_width=4), + "4", + ), + labeled( + spins.SpinningLines(color=ft.Colors.BLUE, size=50, line_width=6), + "6", + ), + ], + ), + ft.Divider(height=8), + # PianoWave — item_count + section( + "PianoWave · item_count", + [ + labeled( + spins.PianoWave(color=ft.Colors.BLUE, size=50, item_count=3), "3" + ), + labeled( + spins.PianoWave(color=ft.Colors.BLUE, size=50, item_count=5), "5" + ), + labeled( + spins.PianoWave(color=ft.Colors.BLUE, size=50, item_count=8), "8" + ), + labeled( + spins.PianoWave(color=ft.Colors.BLUE, size=50, item_count=12), "12" + ), + ], + ), + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/sdk/python/examples/extensions/spinkit/spinkit_props/pyproject.toml b/sdk/python/examples/extensions/spinkit/spinkit_props/pyproject.toml new file mode 100644 index 0000000000..3d7405e92b --- /dev/null +++ b/sdk/python/examples/extensions/spinkit/spinkit_props/pyproject.toml @@ -0,0 +1,29 @@ +[project] +name = "spinkit-props" +version = "1.0.0" +description = "Demonstrates SpinKit control properties: color, size, duration, line_width, border_width, wave_type, item_count." +requires-python = ">=3.10" +keywords = ["spinkit", "loading", "spinner", "animation", "properties"] +authors = [{ name = "Flet team", email = "hello@flet.dev" }] +dependencies = ["flet", "flet-spinkit"] + +[dependency-groups] +dev = ["flet-cli", "flet-desktop", "flet-web"] + +[tool.flet.gallery] +categories = ["Animation/SpinKit"] + +[tool.flet.metadata] +title = "SpinKit Properties" +controls = [ + "RotatingCircle", "ThreeBounce", "Pulse", "Wave", + "Ring", "DualRing", "Ripple", "SpinningLines", "PianoWave", +] +layout_pattern = "sections" +complexity = "beginner" +features = ["color", "size", "duration", "wave_type", "item_count", "line_width", "border_width"] + +[tool.flet] +org = "dev.flet" +company = "Flet" +copyright = "Copyright (C) 2023-2026 by Flet" diff --git a/sdk/python/examples/extensions/spinkit/spinkit_showcase/main.py b/sdk/python/examples/extensions/spinkit/spinkit_showcase/main.py new file mode 100644 index 0000000000..8ee70b1b1b --- /dev/null +++ b/sdk/python/examples/extensions/spinkit/spinkit_showcase/main.py @@ -0,0 +1,82 @@ +import flet_spinkit as spins + +import flet as ft + +SPINNERS = [ + ("RotatingPlain", spins.RotatingPlain), + ("DoubleBounce", spins.DoubleBounce), + ("Wave", spins.Wave), + ("WanderingCubes", spins.WanderingCubes), + ("FadingFour", spins.FadingFour), + ("FadingCube", spins.FadingCube), + ("Pulse", spins.Pulse), + ("ChasingDots", spins.ChasingDots), + ("ThreeBounce", spins.ThreeBounce), + ("Circle", spins.Circle), + ("CubeGrid", spins.CubeGrid), + ("FadingCircle", spins.FadingCircle), + ("RotatingCircle", spins.RotatingCircle), + ("FoldingCube", spins.FoldingCube), + ("PumpingHeart", spins.PumpingHeart), + ("HourGlass", spins.HourGlass), + ("PouringHourGlass", spins.PouringHourGlass), + ("PouringHourGlassRefined", spins.PouringHourGlassRefined), + ("FadingGrid", spins.FadingGrid), + ("Ring", spins.Ring), + ("Ripple", spins.Ripple), + ("DualRing", spins.DualRing), + ("SpinningCircle", spins.SpinningCircle), + ("SpinningLines", spins.SpinningLines), + ("SquareCircle", spins.SquareCircle), + ("ThreeInOut", spins.ThreeInOut), + ("DancingSquare", spins.DancingSquare), + ("PianoWave", spins.PianoWave), + ("PulsingGrid", spins.PulsingGrid), + ("WaveSpinner", spins.WaveSpinner), +] + + +def main(page: ft.Page): + page.title = "SpinKit Showcase" + page.scroll = ft.ScrollMode.AUTO + page.padding = 20 + + color = ft.Colors.BLUE + + page.add( + ft.GridView( + runs_count=4, + max_extent=160, + child_aspect_ratio=0.9, + spacing=12, + run_spacing=12, + controls=[ + ft.Container( + content=ft.Column( + [ + ft.Container( + content=cls(color=color, size=50), + alignment=ft.Alignment.CENTER, + expand=True, + ), + ft.Text( + name, + size=11, + text_align=ft.TextAlign.CENTER, + ), + ], + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + spacing=8, + ), + bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST, + border_radius=12, + padding=12, + ) + for name, cls in SPINNERS + ], + ) + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/sdk/python/examples/extensions/spinkit/spinkit_showcase/pyproject.toml b/sdk/python/examples/extensions/spinkit/spinkit_showcase/pyproject.toml new file mode 100644 index 0000000000..caaf23184f --- /dev/null +++ b/sdk/python/examples/extensions/spinkit/spinkit_showcase/pyproject.toml @@ -0,0 +1,33 @@ +[project] +name = "spinkit-showcase" +version = "1.0.0" +description = "Showcases all 30 SpinKit loading animations." +requires-python = ">=3.10" +keywords = ["spinkit", "loading", "spinner", "animation", "indicator"] +authors = [{ name = "Flet team", email = "hello@flet.dev" }] +dependencies = ["flet", "flet-spinkit"] + +[dependency-groups] +dev = ["flet-cli", "flet-desktop", "flet-web"] + +[tool.flet.gallery] +categories = ["Animation/SpinKit"] + +[tool.flet.metadata] +title = "SpinKit Showcase" +controls = [ + "SafeArea", "Column", "Row", "GridView", "Container", "Text", + "Slider", "SegmentedButton", "ButtonSegment", + "SpinKitRotatingCircle", "SpinKitWave", "SpinKitThreeBounce", + "SpinKitFadingCircle", "SpinKitPulse", "SpinKitRing", + "SpinKitRipple", "SpinKitDualRing", "SpinKitCubeGrid", + "SpinKitFoldingCube", "SpinKitHourGlass", "SpinKitPianoWave", +] +layout_pattern = "grid" +complexity = "intermediate" +features = ["spinner grid", "color selection", "size slider", "all 31 spinner types"] + +[tool.flet] +org = "dev.flet" +company = "Flet" +copyright = "Copyright (C) 2023-2026 by Flet" diff --git a/sdk/python/packages/flet-spinkit/CHANGELOG.md b/sdk/python/packages/flet-spinkit/CHANGELOG.md new file mode 100644 index 0000000000..621a0dbdaa --- /dev/null +++ b/sdk/python/packages/flet-spinkit/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.0 + +- Initial release. diff --git a/sdk/python/packages/flet-spinkit/LICENSE b/sdk/python/packages/flet-spinkit/LICENSE new file mode 100644 index 0000000000..94edbe67ce --- /dev/null +++ b/sdk/python/packages/flet-spinkit/LICENSE @@ -0,0 +1,200 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by their Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or exemplary damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/sdk/python/packages/flet-spinkit/README.md b/sdk/python/packages/flet-spinkit/README.md new file mode 100644 index 0000000000..acbe47ca0a --- /dev/null +++ b/sdk/python/packages/flet-spinkit/README.md @@ -0,0 +1,42 @@ +# flet-spinkit + +[![pypi](https://img.shields.io/pypi/v/flet-spinkit.svg)](https://pypi.python.org/pypi/flet-spinkit) +[![downloads](https://static.pepy.tech/badge/flet-spinkit/month)](https://pepy.tech/project/flet-spinkit) +[![python](https://img.shields.io/badge/python-%3E%3D3.10-%2334D058)](https://pypi.org/project/flet-spinkit) +[![docstring coverage](https://flet.dev/docs/assets/badges/docs-coverage/flet-spinkit.svg)](https://flet.dev/docs/assets/badges/docs-coverage/flet-spinkit.svg) +[![license](https://img.shields.io/badge/License-Apache_2.0-green.svg)](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-spinkit/LICENSE) + +A [Flet](https://flet.dev) extension package with 30 animated loading spinner controls. + +It is based on the [flutter_spinkit](https://pub.dev/packages/flutter_spinkit) Flutter package. + +## Documentation + +Detailed documentation to this package can be found [here](https://flet.dev/docs/controls/spinkit/). + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | + +## Usage + +### Installation + +To install the `flet-spinkit` package and add it to your project dependencies: + +- Using `uv`: + ```bash + uv add flet-spinkit + ``` + +- Using `pip`: + ```bash + pip install flet-spinkit + ``` + After this, you will have to manually add this package to your `requirements.txt` or `pyproject.toml`. + +### Examples + +For examples, see [these](https://github.com/flet-dev/flet/tree/main/sdk/python/examples/extensions/spinkit). diff --git a/sdk/python/packages/flet-spinkit/pyproject.toml b/sdk/python/packages/flet-spinkit/pyproject.toml new file mode 100644 index 0000000000..ecc20d0d27 --- /dev/null +++ b/sdk/python/packages/flet-spinkit/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "flet-spinkit" +version = "0.1.0" +description = "Loading spinner animations for Flet apps." +readme = "README.md" +authors = [{ name = "Flet contributors", email = "hello@flet.dev" }] +license = "Apache-2.0" +requires-python = ">=3.10" +dependencies = [ + "flet", +] + +[project.urls] +Homepage = "https://flet.dev" +Documentation = "https://flet.dev/docs/controls/spinkit" +Repository = "https://github.com/flet-dev/flet/tree/main/sdk/python/packages/flet-spinkit" +Issues = "https://github.com/flet-dev/flet/issues" + +[tool.setuptools.package-data] +"flutter.flet_spinkit" = [ + "pubspec.yaml", + "lib/**", +] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/sdk/python/packages/flet-spinkit/src/flet_spinkit/__init__.py b/sdk/python/packages/flet-spinkit/src/flet_spinkit/__init__.py new file mode 100644 index 0000000000..80969d6e34 --- /dev/null +++ b/sdk/python/packages/flet-spinkit/src/flet_spinkit/__init__.py @@ -0,0 +1,67 @@ +from flet_spinkit.spinkit import ( + ChasingDots, + Circle, + CubeGrid, + DancingSquare, + DoubleBounce, + DualRing, + FadingCircle, + FadingCube, + FadingFour, + FadingGrid, + FoldingCube, + HourGlass, + PianoWave, + PouringHourGlass, + PouringHourGlassRefined, + Pulse, + PulsingGrid, + PumpingHeart, + Ring, + Ripple, + RotatingCircle, + RotatingPlain, + SpinningCircle, + SpinningLines, + SquareCircle, + ThreeBounce, + ThreeInOut, + WanderingCubes, + Wave, + WaveSpinner, + WaveType, +) + +__all__ = [ + "ChasingDots", + "Circle", + "CubeGrid", + "DancingSquare", + "DoubleBounce", + "DualRing", + "FadingCircle", + "FadingCube", + "FadingFour", + "FadingGrid", + "FoldingCube", + "HourGlass", + "PianoWave", + "PouringHourGlass", + "PouringHourGlassRefined", + "Pulse", + "PulsingGrid", + "PumpingHeart", + "Ring", + "Ripple", + "RotatingCircle", + "RotatingPlain", + "SpinningCircle", + "SpinningLines", + "SquareCircle", + "ThreeBounce", + "ThreeInOut", + "WanderingCubes", + "Wave", + "WaveSpinner", + "WaveType", +] diff --git a/sdk/python/packages/flet-spinkit/src/flet_spinkit/spinkit.py b/sdk/python/packages/flet-spinkit/src/flet_spinkit/spinkit.py new file mode 100644 index 0000000000..6da8056525 --- /dev/null +++ b/sdk/python/packages/flet-spinkit/src/flet_spinkit/spinkit.py @@ -0,0 +1,495 @@ +from enum import Enum +from typing import Optional + +import flet as ft + +__all__ = [ + "ChasingDots", + "Circle", + "CubeGrid", + "DancingSquare", + "DoubleBounce", + "DualRing", + "FadingCircle", + "FadingCube", + "FadingFour", + "FadingGrid", + "FoldingCube", + "HourGlass", + "PianoWave", + "PouringHourGlass", + "PouringHourGlassRefined", + "Pulse", + "PulsingGrid", + "PumpingHeart", + "Ring", + "Ripple", + "RotatingCircle", + "RotatingPlain", + "SpinningCircle", + "SpinningLines", + "SquareCircle", + "ThreeBounce", + "ThreeInOut", + "WanderingCubes", + "Wave", + "WaveSpinner", + "WaveType", +] + + +class WaveType(Enum): + """Controls how the wave animation is aligned.""" + + START = "start" + """Wave animation starts from the first item.""" + + CENTER = "center" + """Wave animation radiates from the center.""" + + END = "end" + """Wave animation starts from the last item.""" + + +@ft.control("SpinKitRotatingPlain") +class RotatingPlain(ft.LayoutControl): + """A plain rotating square spinner.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitDoubleBounce") +class DoubleBounce(ft.LayoutControl): + """Two overlapping circles that bounce in and out.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitWave") +class Wave(ft.LayoutControl): + """A row of bars that animate in a wave pattern.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + wave_type: Optional[WaveType] = None + """Controls the wave propagation direction. + + Defaults to :attr:`WaveType.START`. + """ + + item_count: Optional[int] = None + """Number of bars in the wave. Defaults to 5.""" + + +@ft.control("SpinKitWanderingCubes") +class WanderingCubes(ft.LayoutControl): + """Two cubes that rotate and wander around each other.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitFadingFour") +class FadingFour(ft.LayoutControl): + """Four dots arranged in a square that fade in sequence.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitFadingCube") +class FadingCube(ft.LayoutControl): + """A cube made of four smaller cubes that fade and rotate.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitPulse") +class Pulse(ft.LayoutControl): + """A circle that pulses in and out.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitChasingDots") +class ChasingDots(ft.LayoutControl): + """Two dots that chase each other in a circular path.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitThreeBounce") +class ThreeBounce(ft.LayoutControl): + """Three dots that bounce sequentially.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitCircle") +class Circle(ft.LayoutControl): + """A ring of dots that fade sequentially around a circle.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitCubeGrid") +class CubeGrid(ft.LayoutControl): + """A 3x3 grid of cubes that scale and fade in a wave.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitFadingCircle") +class FadingCircle(ft.LayoutControl): + """A ring of dots that fade sequentially.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitRotatingCircle") +class RotatingCircle(ft.LayoutControl): + """A circle that rotates with a color gradient effect.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitFoldingCube") +class FoldingCube(ft.LayoutControl): + """Four cubes that fold and unfold in sequence.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitPumpingHeart") +class PumpingHeart(ft.LayoutControl): + """A heart shape that pumps in and out.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitHourGlass") +class HourGlass(ft.LayoutControl): + """An hourglass shape that rotates.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitPouringHourGlass") +class PouringHourGlass(ft.LayoutControl): + """An hourglass that pours dots from top to bottom.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitPouringHourGlassRefined") +class PouringHourGlassRefined(ft.LayoutControl): + """A refined version of the pouring hourglass with smoother animation.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitFadingGrid") +class FadingGrid(ft.LayoutControl): + """A 3x3 grid of dots that fade in a spiral sequence.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitRing") +class Ring(ft.LayoutControl): + """A ring that spins with a gap.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + line_width: Optional[ft.Number] = None + """The stroke width of the ring. Defaults to 7.""" + + +@ft.control("SpinKitRipple") +class Ripple(ft.LayoutControl): + """Two concentric circles that expand outward like a ripple.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + border_width: Optional[ft.Number] = None + """The stroke width of the ripple rings. Defaults to 6.""" + + +@ft.control("SpinKitDualRing") +class DualRing(ft.LayoutControl): + """Two concentric rings that spin in opposite directions.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + line_width: Optional[ft.Number] = None + """The stroke width of the rings. Defaults to 7.""" + + +@ft.control("SpinKitSpinningCircle") +class SpinningCircle(ft.LayoutControl): + """A circle that spins continuously.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitSpinningLines") +class SpinningLines(ft.LayoutControl): + """Multiple lines radiating from the center that spin.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + line_width: Optional[ft.Number] = None + """The stroke width of the lines. Defaults to 2.""" + + +@ft.control("SpinKitSquareCircle") +class SquareCircle(ft.LayoutControl): + """A square and a circle that morph between shapes.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitThreeInOut") +class ThreeInOut(ft.LayoutControl): + """Three dots that move in and out of view.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitDancingSquare") +class DancingSquare(ft.LayoutControl): + """A square that dances by rotating and scaling.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitPianoWave") +class PianoWave(ft.LayoutControl): + """A row of bars that animate like piano keys.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + item_count: Optional[int] = None + """Number of bars. Defaults to 5.""" + + +@ft.control("SpinKitPulsingGrid") +class PulsingGrid(ft.LayoutControl): + """A 3x3 grid of dots that pulse in sequence.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" + + +@ft.control("SpinKitWaveSpinner") +class WaveSpinner(ft.LayoutControl): + """A circular wave spinner.""" + + color: Optional[ft.ColorValue] = None + """The color of the spinner.""" + + size: Optional[ft.Number] = None + """The size of the spinner in pixels. Defaults to 50.""" + + duration: Optional[ft.DurationValue] = None + """The duration of one animation cycle.""" diff --git a/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/.gitignore b/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/.gitignore new file mode 100644 index 0000000000..ed7794f2ab --- /dev/null +++ b/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/.gitignore @@ -0,0 +1,34 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ +.flutter-plugins +.flutter-plugins-dependencies + +# override parent rules +!lib/ diff --git a/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/analysis_options.yaml b/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/analysis_options.yaml new file mode 100644 index 0000000000..a5744c1cfb --- /dev/null +++ b/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/lib/flet_spinkit.dart b/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/lib/flet_spinkit.dart new file mode 100644 index 0000000000..a5cb6448f4 --- /dev/null +++ b/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/lib/flet_spinkit.dart @@ -0,0 +1,3 @@ +library flet_spinkit; + +export "src/extension.dart" show Extension; diff --git a/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/lib/src/extension.dart b/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/lib/src/extension.dart new file mode 100644 index 0000000000..50f1e02500 --- /dev/null +++ b/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/lib/src/extension.dart @@ -0,0 +1,45 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/widgets.dart'; + +import 'spinkit.dart'; + +class Extension extends FletExtension { + @override + Widget? createWidget(Key? key, Control control) { + switch (control.type) { + case "SpinKitRotatingPlain": + case "SpinKitDoubleBounce": + case "SpinKitWave": + case "SpinKitWanderingCubes": + case "SpinKitFadingFour": + case "SpinKitFadingCube": + case "SpinKitPulse": + case "SpinKitChasingDots": + case "SpinKitThreeBounce": + case "SpinKitCircle": + case "SpinKitCubeGrid": + case "SpinKitFadingCircle": + case "SpinKitRotatingCircle": + case "SpinKitFoldingCube": + case "SpinKitPumpingHeart": + case "SpinKitHourGlass": + case "SpinKitPouringHourGlass": + case "SpinKitPouringHourGlassRefined": + case "SpinKitFadingGrid": + case "SpinKitRing": + case "SpinKitRipple": + case "SpinKitDualRing": + case "SpinKitSpinningCircle": + case "SpinKitSpinningLines": + case "SpinKitSquareCircle": + case "SpinKitThreeInOut": + case "SpinKitDancingSquare": + case "SpinKitPianoWave": + case "SpinKitPulsingGrid": + case "SpinKitWaveSpinner": + return SpinKitControl(key: key, control: control); + default: + return null; + } + } +} diff --git a/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/lib/src/spinkit.dart b/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/lib/src/spinkit.dart new file mode 100644 index 0000000000..e1d93a8753 --- /dev/null +++ b/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/lib/src/spinkit.dart @@ -0,0 +1,260 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; + +class SpinKitControl extends StatefulWidget { + final Control control; + + const SpinKitControl({super.key, required this.control}); + + @override + State createState() => _SpinKitControlState(); +} + +class _SpinKitControlState extends State { + @override + Widget build(BuildContext context) { + debugPrint("SpinKitControl build: ${widget.control.id} (${widget.control.type})"); + + final color = + widget.control.getColor("color", context) ?? Theme.of(context).primaryColor; + final size = widget.control.getDouble("size", 50.0)!; + final duration = widget.control.getDuration("duration"); + + final lineWidth = widget.control.getDouble("line_width"); + final borderWidth = widget.control.getDouble("border_width"); + final itemCount = widget.control.getInt("item_count"); + final waveType = parseEnum(SpinKitWaveType.values, + widget.control.getString("wave_type"), SpinKitWaveType.start)!; + + Widget spinner; + + switch (widget.control.type) { + case "SpinKitRotatingPlain": + spinner = SpinKitRotatingPlain( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + ); + break; + case "SpinKitDoubleBounce": + spinner = SpinKitDoubleBounce( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 2000), + ); + break; + case "SpinKitWave": + spinner = SpinKitWave( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + itemCount: itemCount ?? 5, + type: waveType, + ); + break; + case "SpinKitWanderingCubes": + spinner = SpinKitWanderingCubes( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1800), + ); + break; + case "SpinKitFadingFour": + spinner = SpinKitFadingFour( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + ); + break; + case "SpinKitFadingCube": + spinner = SpinKitFadingCube( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + ); + break; + case "SpinKitPulse": + spinner = SpinKitPulse( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1000), + ); + break; + case "SpinKitChasingDots": + spinner = SpinKitChasingDots( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 2000), + ); + break; + case "SpinKitThreeBounce": + spinner = SpinKitThreeBounce( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1400), + ); + break; + case "SpinKitCircle": + spinner = SpinKitCircle( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + ); + break; + case "SpinKitCubeGrid": + spinner = SpinKitCubeGrid( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + ); + break; + case "SpinKitFadingCircle": + spinner = SpinKitFadingCircle( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + ); + break; + case "SpinKitRotatingCircle": + spinner = SpinKitRotatingCircle( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + ); + break; + case "SpinKitFoldingCube": + spinner = SpinKitFoldingCube( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 2400), + ); + break; + case "SpinKitPumpingHeart": + spinner = SpinKitPumpingHeart( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1000), + ); + break; + case "SpinKitHourGlass": + spinner = SpinKitHourGlass( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + ); + break; + case "SpinKitPouringHourGlass": + spinner = SpinKitPouringHourGlass( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 2400), + ); + break; + case "SpinKitPouringHourGlassRefined": + spinner = SpinKitPouringHourGlassRefined( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 2400), + ); + break; + case "SpinKitFadingGrid": + spinner = SpinKitFadingGrid( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + ); + break; + case "SpinKitRing": + spinner = SpinKitRing( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + lineWidth: lineWidth ?? 7.0, + ); + break; + case "SpinKitRipple": + spinner = SpinKitRipple( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1800), + borderWidth: borderWidth ?? 6.0, + ); + break; + case "SpinKitDualRing": + spinner = SpinKitDualRing( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + lineWidth: lineWidth ?? 7.0, + ); + break; + case "SpinKitSpinningCircle": + spinner = SpinKitSpinningCircle( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + ); + break; + case "SpinKitSpinningLines": + spinner = SpinKitSpinningLines( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + lineWidth: lineWidth ?? 2.0, + ); + break; + case "SpinKitSquareCircle": + spinner = SpinKitSquareCircle( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 500), + ); + break; + case "SpinKitThreeInOut": + spinner = SpinKitThreeInOut( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1500), + ); + break; + case "SpinKitDancingSquare": + spinner = SpinKitDancingSquare( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + ); + break; + case "SpinKitPianoWave": + spinner = SpinKitPianoWave( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + itemCount: itemCount ?? 5, + ); + break; + case "SpinKitPulsingGrid": + spinner = SpinKitPulsingGrid( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + ); + break; + case "SpinKitWaveSpinner": + spinner = SpinKitWaveSpinner( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + ); + break; + default: + spinner = SpinKitRotatingCircle( + color: color, + size: size, + duration: duration ?? const Duration(milliseconds: 1200), + ); + } + + return LayoutControl(control: widget.control, child: spinner); + } +} diff --git a/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/pubspec.yaml b/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/pubspec.yaml new file mode 100644 index 0000000000..df490172cb --- /dev/null +++ b/sdk/python/packages/flet-spinkit/src/flutter/flet_spinkit/pubspec.yaml @@ -0,0 +1,22 @@ +name: flet_spinkit +description: Flet SpinKit controls +version: 0.1.0 +publish_to: none + +environment: + sdk: '>=3.2.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + flutter_spinkit: ^5.2.2 + + flet: + path: ../../../../../../../packages/flet + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 diff --git a/sdk/python/packages/flet/integration_tests/extensions/spinkit/test_spinkit.py b/sdk/python/packages/flet/integration_tests/extensions/spinkit/test_spinkit.py new file mode 100644 index 0000000000..dc9c2d2c5c --- /dev/null +++ b/sdk/python/packages/flet/integration_tests/extensions/spinkit/test_spinkit.py @@ -0,0 +1,148 @@ +import flet_spinkit as spins +import pytest + +import flet as ft +import flet.testing as ftt + + +@pytest.mark.asyncio(loop_scope="module") +async def test_rotating_circle(flet_app: ftt.FletTestApp): + flet_app.page.clean() + flet_app.page.add(spins.RotatingCircle(color=ft.Colors.BLUE, size=50, key="ctrl")) + await flet_app.tester.pump(duration=ft.Duration(milliseconds=500)) + assert (await flet_app.tester.find_by_key("ctrl")).count == 1 + + +@pytest.mark.asyncio(loop_scope="module") +async def test_wave_default(flet_app: ftt.FletTestApp): + flet_app.page.clean() + flet_app.page.add(spins.Wave(color=ft.Colors.BLUE, size=50, key="ctrl")) + await flet_app.tester.pump(duration=ft.Duration(milliseconds=500)) + assert (await flet_app.tester.find_by_key("ctrl")).count == 1 + + +@pytest.mark.asyncio(loop_scope="module") +async def test_wave_type(flet_app: ftt.FletTestApp): + flet_app.page.clean() + flet_app.page.add( + spins.Wave( + color=ft.Colors.BLUE, + size=50, + wave_type=spins.WaveType.CENTER, + item_count=7, + key="ctrl", + ) + ) + await flet_app.tester.pump(duration=ft.Duration(milliseconds=500)) + assert (await flet_app.tester.find_by_key("ctrl")).count == 1 + + +@pytest.mark.asyncio(loop_scope="module") +async def test_ring_line_width(flet_app: ftt.FletTestApp): + flet_app.page.clean() + flet_app.page.add( + spins.Ring(color=ft.Colors.BLUE, size=50, line_width=10, key="ctrl") + ) + await flet_app.tester.pump(duration=ft.Duration(milliseconds=500)) + assert (await flet_app.tester.find_by_key("ctrl")).count == 1 + + +@pytest.mark.asyncio(loop_scope="module") +async def test_dual_ring_line_width(flet_app: ftt.FletTestApp): + flet_app.page.clean() + flet_app.page.add( + spins.DualRing(color=ft.Colors.BLUE, size=50, line_width=4, key="ctrl") + ) + await flet_app.tester.pump(duration=ft.Duration(milliseconds=500)) + assert (await flet_app.tester.find_by_key("ctrl")).count == 1 + + +@pytest.mark.asyncio(loop_scope="module") +async def test_ripple_border_width(flet_app: ftt.FletTestApp): + flet_app.page.clean() + flet_app.page.add( + spins.Ripple(color=ft.Colors.BLUE, size=50, border_width=8, key="ctrl") + ) + await flet_app.tester.pump(duration=ft.Duration(milliseconds=500)) + assert (await flet_app.tester.find_by_key("ctrl")).count == 1 + + +@pytest.mark.asyncio(loop_scope="module") +async def test_spinning_lines_line_width(flet_app: ftt.FletTestApp): + flet_app.page.clean() + flet_app.page.add( + spins.SpinningLines(color=ft.Colors.BLUE, size=50, line_width=3, key="ctrl") + ) + await flet_app.tester.pump(duration=ft.Duration(milliseconds=500)) + assert (await flet_app.tester.find_by_key("ctrl")).count == 1 + + +@pytest.mark.asyncio(loop_scope="module") +async def test_piano_wave_item_count(flet_app: ftt.FletTestApp): + flet_app.page.clean() + flet_app.page.add( + spins.PianoWave(color=ft.Colors.BLUE, size=50, item_count=8, key="ctrl") + ) + await flet_app.tester.pump(duration=ft.Duration(milliseconds=500)) + assert (await flet_app.tester.find_by_key("ctrl")).count == 1 + + +@pytest.mark.asyncio(loop_scope="module") +async def test_custom_duration(flet_app: ftt.FletTestApp): + flet_app.page.clean() + flet_app.page.add( + spins.Pulse( + color=ft.Colors.BLUE, + size=50, + duration=ft.Duration(milliseconds=400), + key="ctrl", + ) + ) + await flet_app.tester.pump(duration=ft.Duration(milliseconds=500)) + assert (await flet_app.tester.find_by_key("ctrl")).count == 1 + + +@pytest.mark.asyncio(loop_scope="module") +async def test_all_controls_render(flet_app: ftt.FletTestApp): + controls = [ + spins.ChasingDots, + spins.Circle, + spins.CubeGrid, + spins.DancingSquare, + spins.DoubleBounce, + spins.DualRing, + spins.FadingCircle, + spins.FadingCube, + spins.FadingFour, + spins.FadingGrid, + spins.FoldingCube, + spins.HourGlass, + spins.PianoWave, + spins.PouringHourGlass, + spins.PouringHourGlassRefined, + spins.Pulse, + spins.PulsingGrid, + spins.PumpingHeart, + spins.Ring, + spins.Ripple, + spins.RotatingCircle, + spins.RotatingPlain, + spins.SpinningCircle, + spins.SpinningLines, + spins.SquareCircle, + spins.ThreeBounce, + spins.ThreeInOut, + spins.WanderingCubes, + spins.Wave, + spins.WaveSpinner, + ] + flet_app.page.clean() + flet_app.page.add( + ft.Row( + wrap=True, + key="row", + controls=[cls(color=ft.Colors.BLUE, size=40) for cls in controls], + ) + ) + await flet_app.tester.pump(duration=ft.Duration(milliseconds=500)) + assert (await flet_app.tester.find_by_key("row")).count == 1 diff --git a/sdk/python/packages/flet/pyproject.toml b/sdk/python/packages/flet/pyproject.toml index c6a20b80f2..55539efe6e 100644 --- a/sdk/python/packages/flet/pyproject.toml +++ b/sdk/python/packages/flet/pyproject.toml @@ -53,6 +53,7 @@ extensions = [ "flet-permission-handler", "flet-rive", "flet-secure-storage", + "flet-spinkit", "flet-video", "flet-webview", ] diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index 8a805efb35..66bed0b632 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "flet-permission-handler", "flet-rive", "flet-secure-storage", + "flet-spinkit", "flet-video", "flet-webview", ] @@ -47,6 +48,7 @@ flet-map = { workspace = true } flet-permission-handler = { workspace = true } flet-rive = { workspace = true } flet-secure-storage = { workspace = true } +flet-spinkit = { workspace = true } flet-video = { workspace = true } flet-webview = { workspace = true } flet-code-editor = { workspace = true } diff --git a/tools/crocodocs/pyproject.toml b/tools/crocodocs/pyproject.toml index 5d89833ce9..34bfeba0e4 100644 --- a/tools/crocodocs/pyproject.toml +++ b/tools/crocodocs/pyproject.toml @@ -74,6 +74,7 @@ flet_map = "../../sdk/python/packages/flet-map/src" flet_permission_handler = "../../sdk/python/packages/flet-permission-handler/src" flet_rive = "../../sdk/python/packages/flet-rive/src" flet_secure_storage = "../../sdk/python/packages/flet-secure-storage/src" +flet_spinkit = "../../sdk/python/packages/flet-spinkit/src" flet_video = "../../sdk/python/packages/flet-video/src" flet_web = "../../sdk/python/packages/flet-web/src" flet_webview = "../../sdk/python/packages/flet-webview/src" diff --git a/website/docs/controls/spinkit/chasingdots.md b/website/docs/controls/spinkit/chasingdots.md new file mode 100644 index 0000000000..49563c4b65 --- /dev/null +++ b/website/docs/controls/spinkit/chasingdots.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.ChasingDots" +title: "ChasingDots" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/circle.md b/website/docs/controls/spinkit/circle.md new file mode 100644 index 0000000000..42a9a773c1 --- /dev/null +++ b/website/docs/controls/spinkit/circle.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.Circle" +title: "Circle" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/cubegrid.md b/website/docs/controls/spinkit/cubegrid.md new file mode 100644 index 0000000000..8a09a76e04 --- /dev/null +++ b/website/docs/controls/spinkit/cubegrid.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.CubeGrid" +title: "CubeGrid" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/dancingsquare.md b/website/docs/controls/spinkit/dancingsquare.md new file mode 100644 index 0000000000..2a2d57ba60 --- /dev/null +++ b/website/docs/controls/spinkit/dancingsquare.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.DancingSquare" +title: "DancingSquare" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/doublebounce.md b/website/docs/controls/spinkit/doublebounce.md new file mode 100644 index 0000000000..559e4b4891 --- /dev/null +++ b/website/docs/controls/spinkit/doublebounce.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.DoubleBounce" +title: "DoubleBounce" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/dualring.md b/website/docs/controls/spinkit/dualring.md new file mode 100644 index 0000000000..aa1f304d4d --- /dev/null +++ b/website/docs/controls/spinkit/dualring.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.DualRing" +title: "DualRing" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/fadingcircle.md b/website/docs/controls/spinkit/fadingcircle.md new file mode 100644 index 0000000000..f4d7b73dbb --- /dev/null +++ b/website/docs/controls/spinkit/fadingcircle.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.FadingCircle" +title: "FadingCircle" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/fadingcube.md b/website/docs/controls/spinkit/fadingcube.md new file mode 100644 index 0000000000..1cfa5a9a56 --- /dev/null +++ b/website/docs/controls/spinkit/fadingcube.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.FadingCube" +title: "FadingCube" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/fadingfour.md b/website/docs/controls/spinkit/fadingfour.md new file mode 100644 index 0000000000..3c0267ebde --- /dev/null +++ b/website/docs/controls/spinkit/fadingfour.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.FadingFour" +title: "FadingFour" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/fadinggrid.md b/website/docs/controls/spinkit/fadinggrid.md new file mode 100644 index 0000000000..c08a637ffc --- /dev/null +++ b/website/docs/controls/spinkit/fadinggrid.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.FadingGrid" +title: "FadingGrid" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/foldingcube.md b/website/docs/controls/spinkit/foldingcube.md new file mode 100644 index 0000000000..ebd16fba28 --- /dev/null +++ b/website/docs/controls/spinkit/foldingcube.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.FoldingCube" +title: "FoldingCube" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/hourglass.md b/website/docs/controls/spinkit/hourglass.md new file mode 100644 index 0000000000..4ccefe1673 --- /dev/null +++ b/website/docs/controls/spinkit/hourglass.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.HourGlass" +title: "HourGlass" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/index.md b/website/docs/controls/spinkit/index.md new file mode 100644 index 0000000000..a48356dc91 --- /dev/null +++ b/website/docs/controls/spinkit/index.md @@ -0,0 +1,78 @@ +--- +examples: "extensions/spinkit" +title: "SpinKit" +--- + +import TabItem from '@theme/TabItem'; +import Tabs from '@theme/Tabs'; +import {CodeExample} from '@site/src/components/crocodocs'; + +# SpinKit + +30 animated loading spinner controls built on Flutter's +[`flutter_spinkit`](https://pub.dev/packages/flutter_spinkit) package. + +## Usage + +Add `flet-spinkit` to your project dependencies: + + + +```bash +uv add flet-spinkit +``` + + + +```bash +pip install flet-spinkit # (1)! +``` + +1. After this, you will have to manually add this package to your `requirements.txt` or `pyproject.toml`. + + + +Then import the package in your Flet app: + +```python +import flet_spinkit as spins +``` + +## Examples + + + + + +## Available controls + +- [ChasingDots](chasingdots.md) +- [Circle](circle.md) +- [CubeGrid](cubegrid.md) +- [DancingSquare](dancingsquare.md) +- [DoubleBounce](doublebounce.md) +- [DualRing](dualring.md) +- [FadingCircle](fadingcircle.md) +- [FadingCube](fadingcube.md) +- [FadingFour](fadingfour.md) +- [FadingGrid](fadinggrid.md) +- [FoldingCube](foldingcube.md) +- [HourGlass](hourglass.md) +- [PianoWave](pianowave.md) +- [PouringHourGlass](pouringhourglass.md) +- [PouringHourGlassRefined](pouringhourglassrefined.md) +- [Pulse](pulse.md) +- [PulsingGrid](pulsinggrid.md) +- [PumpingHeart](pumpingheart.md) +- [Ring](ring.md) +- [Ripple](ripple.md) +- [RotatingCircle](rotatingcircle.md) +- [RotatingPlain](rotatingplain.md) +- [SpinningCircle](spinningcircle.md) +- [SpinningLines](spinninglines.md) +- [SquareCircle](squarecircle.md) +- [ThreeBounce](threebounce.md) +- [ThreeInOut](threeinout.md) +- [WanderingCubes](wanderingcubes.md) +- [Wave](wave.md) +- [WaveSpinner](wavespinner.md) diff --git a/website/docs/controls/spinkit/pianowave.md b/website/docs/controls/spinkit/pianowave.md new file mode 100644 index 0000000000..dd16b1ae40 --- /dev/null +++ b/website/docs/controls/spinkit/pianowave.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.PianoWave" +title: "PianoWave" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/pouringhourglass.md b/website/docs/controls/spinkit/pouringhourglass.md new file mode 100644 index 0000000000..3472120af1 --- /dev/null +++ b/website/docs/controls/spinkit/pouringhourglass.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.PouringHourGlass" +title: "PouringHourGlass" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/pouringhourglassrefined.md b/website/docs/controls/spinkit/pouringhourglassrefined.md new file mode 100644 index 0000000000..9ec760daa4 --- /dev/null +++ b/website/docs/controls/spinkit/pouringhourglassrefined.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.PouringHourGlassRefined" +title: "PouringHourGlassRefined" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/pulse.md b/website/docs/controls/spinkit/pulse.md new file mode 100644 index 0000000000..222ad5bf8f --- /dev/null +++ b/website/docs/controls/spinkit/pulse.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.Pulse" +title: "Pulse" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/pulsinggrid.md b/website/docs/controls/spinkit/pulsinggrid.md new file mode 100644 index 0000000000..843e12f962 --- /dev/null +++ b/website/docs/controls/spinkit/pulsinggrid.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.PulsingGrid" +title: "PulsingGrid" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/pumpingheart.md b/website/docs/controls/spinkit/pumpingheart.md new file mode 100644 index 0000000000..1c542ab4ad --- /dev/null +++ b/website/docs/controls/spinkit/pumpingheart.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.PumpingHeart" +title: "PumpingHeart" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/ring.md b/website/docs/controls/spinkit/ring.md new file mode 100644 index 0000000000..a6367979ad --- /dev/null +++ b/website/docs/controls/spinkit/ring.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.Ring" +title: "Ring" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/ripple.md b/website/docs/controls/spinkit/ripple.md new file mode 100644 index 0000000000..424b0b8559 --- /dev/null +++ b/website/docs/controls/spinkit/ripple.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.Ripple" +title: "Ripple" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/rotatingcircle.md b/website/docs/controls/spinkit/rotatingcircle.md new file mode 100644 index 0000000000..522c6d31e3 --- /dev/null +++ b/website/docs/controls/spinkit/rotatingcircle.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.RotatingCircle" +title: "RotatingCircle" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/rotatingplain.md b/website/docs/controls/spinkit/rotatingplain.md new file mode 100644 index 0000000000..919a482e30 --- /dev/null +++ b/website/docs/controls/spinkit/rotatingplain.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.RotatingPlain" +title: "RotatingPlain" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/spinningcircle.md b/website/docs/controls/spinkit/spinningcircle.md new file mode 100644 index 0000000000..6cf1c0e336 --- /dev/null +++ b/website/docs/controls/spinkit/spinningcircle.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.SpinningCircle" +title: "SpinningCircle" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/spinninglines.md b/website/docs/controls/spinkit/spinninglines.md new file mode 100644 index 0000000000..8ea7031d48 --- /dev/null +++ b/website/docs/controls/spinkit/spinninglines.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.SpinningLines" +title: "SpinningLines" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/squarecircle.md b/website/docs/controls/spinkit/squarecircle.md new file mode 100644 index 0000000000..31f04276df --- /dev/null +++ b/website/docs/controls/spinkit/squarecircle.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.SquareCircle" +title: "SquareCircle" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/threebounce.md b/website/docs/controls/spinkit/threebounce.md new file mode 100644 index 0000000000..1e9d35b88b --- /dev/null +++ b/website/docs/controls/spinkit/threebounce.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.ThreeBounce" +title: "ThreeBounce" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/threeinout.md b/website/docs/controls/spinkit/threeinout.md new file mode 100644 index 0000000000..a9a3aa2774 --- /dev/null +++ b/website/docs/controls/spinkit/threeinout.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.ThreeInOut" +title: "ThreeInOut" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/wanderingcubes.md b/website/docs/controls/spinkit/wanderingcubes.md new file mode 100644 index 0000000000..142f08cb4e --- /dev/null +++ b/website/docs/controls/spinkit/wanderingcubes.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.WanderingCubes" +title: "WanderingCubes" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/wave.md b/website/docs/controls/spinkit/wave.md new file mode 100644 index 0000000000..6ee58341be --- /dev/null +++ b/website/docs/controls/spinkit/wave.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.Wave" +title: "Wave" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/wavespinner.md b/website/docs/controls/spinkit/wavespinner.md new file mode 100644 index 0000000000..e90a96ccae --- /dev/null +++ b/website/docs/controls/spinkit/wavespinner.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.WaveSpinner" +title: "WaveSpinner" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/docs/controls/spinkit/wavetype.md b/website/docs/controls/spinkit/wavetype.md new file mode 100644 index 0000000000..cd70f80a56 --- /dev/null +++ b/website/docs/controls/spinkit/wavetype.md @@ -0,0 +1,10 @@ +--- +class_name: "flet_spinkit.WaveType" +title: "WaveType" +--- + +import {ClassMembers, ClassSummary} from '@site/src/components/crocodocs'; + + + + diff --git a/website/sidebars.js b/website/sidebars.js index 4118a47cc4..b052322b52 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -1344,6 +1344,172 @@ module.exports = { "id": "controls/shimmer", "label": "Shimmer" }, + { + "type": "category", + "label": "SpinKit", + "collapsed": true, + "items": [ + { + "type": "doc", + "id": "controls/spinkit/chasingdots", + "label": "ChasingDots" + }, + { + "type": "doc", + "id": "controls/spinkit/circle", + "label": "Circle" + }, + { + "type": "doc", + "id": "controls/spinkit/cubegrid", + "label": "CubeGrid" + }, + { + "type": "doc", + "id": "controls/spinkit/dancingsquare", + "label": "DancingSquare" + }, + { + "type": "doc", + "id": "controls/spinkit/doublebounce", + "label": "DoubleBounce" + }, + { + "type": "doc", + "id": "controls/spinkit/dualring", + "label": "DualRing" + }, + { + "type": "doc", + "id": "controls/spinkit/fadingcircle", + "label": "FadingCircle" + }, + { + "type": "doc", + "id": "controls/spinkit/fadingcube", + "label": "FadingCube" + }, + { + "type": "doc", + "id": "controls/spinkit/fadingfour", + "label": "FadingFour" + }, + { + "type": "doc", + "id": "controls/spinkit/fadinggrid", + "label": "FadingGrid" + }, + { + "type": "doc", + "id": "controls/spinkit/foldingcube", + "label": "FoldingCube" + }, + { + "type": "doc", + "id": "controls/spinkit/hourglass", + "label": "HourGlass" + }, + { + "type": "doc", + "id": "controls/spinkit/pianowave", + "label": "PianoWave" + }, + { + "type": "doc", + "id": "controls/spinkit/pouringhourglass", + "label": "PouringHourGlass" + }, + { + "type": "doc", + "id": "controls/spinkit/pouringhourglassrefined", + "label": "PouringHourGlassRefined" + }, + { + "type": "doc", + "id": "controls/spinkit/pulse", + "label": "Pulse" + }, + { + "type": "doc", + "id": "controls/spinkit/pulsinggrid", + "label": "PulsingGrid" + }, + { + "type": "doc", + "id": "controls/spinkit/pumpingheart", + "label": "PumpingHeart" + }, + { + "type": "doc", + "id": "controls/spinkit/ring", + "label": "Ring" + }, + { + "type": "doc", + "id": "controls/spinkit/ripple", + "label": "Ripple" + }, + { + "type": "doc", + "id": "controls/spinkit/rotatingcircle", + "label": "RotatingCircle" + }, + { + "type": "doc", + "id": "controls/spinkit/rotatingplain", + "label": "RotatingPlain" + }, + { + "type": "doc", + "id": "controls/spinkit/spinningcircle", + "label": "SpinningCircle" + }, + { + "type": "doc", + "id": "controls/spinkit/spinninglines", + "label": "SpinningLines" + }, + { + "type": "doc", + "id": "controls/spinkit/squarecircle", + "label": "SquareCircle" + }, + { + "type": "doc", + "id": "controls/spinkit/threebounce", + "label": "ThreeBounce" + }, + { + "type": "doc", + "id": "controls/spinkit/threeinout", + "label": "ThreeInOut" + }, + { + "type": "doc", + "id": "controls/spinkit/wanderingcubes", + "label": "WanderingCubes" + }, + { + "type": "doc", + "id": "controls/spinkit/wave", + "label": "Wave" + }, + { + "type": "doc", + "id": "controls/spinkit/wavespinner", + "label": "WaveSpinner" + }, + { + "type": "doc", + "id": "controls/spinkit/wavetype", + "label": "WaveType" + } + ], + "link": { + "type": "doc", + "id": "controls/spinkit/index" + } + }, { "type": "doc", "id": "controls/slider", diff --git a/website/sidebars.yml b/website/sidebars.yml index 10ad4f70da..f37ebbfcf0 100644 --- a/website/sidebars.yml +++ b/website/sidebars.yml @@ -272,6 +272,39 @@ docs: Screenshot: controls/screenshot.md ShaderMask: controls/shadermask.md Shimmer: controls/shimmer.md + SpinKit: + _index: controls/spinkit/index.md + ChasingDots: controls/spinkit/chasingdots.md + Circle: controls/spinkit/circle.md + CubeGrid: controls/spinkit/cubegrid.md + DancingSquare: controls/spinkit/dancingsquare.md + DoubleBounce: controls/spinkit/doublebounce.md + DualRing: controls/spinkit/dualring.md + FadingCircle: controls/spinkit/fadingcircle.md + FadingCube: controls/spinkit/fadingcube.md + FadingFour: controls/spinkit/fadingfour.md + FadingGrid: controls/spinkit/fadinggrid.md + FoldingCube: controls/spinkit/foldingcube.md + HourGlass: controls/spinkit/hourglass.md + PianoWave: controls/spinkit/pianowave.md + PouringHourGlass: controls/spinkit/pouringhourglass.md + PouringHourGlassRefined: controls/spinkit/pouringhourglassrefined.md + Pulse: controls/spinkit/pulse.md + PulsingGrid: controls/spinkit/pulsinggrid.md + PumpingHeart: controls/spinkit/pumpingheart.md + Ring: controls/spinkit/ring.md + Ripple: controls/spinkit/ripple.md + RotatingCircle: controls/spinkit/rotatingcircle.md + RotatingPlain: controls/spinkit/rotatingplain.md + SpinningCircle: controls/spinkit/spinningcircle.md + SpinningLines: controls/spinkit/spinninglines.md + SquareCircle: controls/spinkit/squarecircle.md + ThreeBounce: controls/spinkit/threebounce.md + ThreeInOut: controls/spinkit/threeinout.md + WanderingCubes: controls/spinkit/wanderingcubes.md + Wave: controls/spinkit/wave.md + WaveSpinner: controls/spinkit/wavespinner.md + WaveType: controls/spinkit/wavetype.md Slider: controls/slider.md SnackBar: controls/snackbar.md Stack: controls/stack.md From 2030eb92f54bdfe9d081658b78457c81a3c36f18 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Sat, 20 Jun 2026 12:39:20 -0700 Subject: [PATCH 3/8] Lift app packaging into serious_python: unpacked bundle + reworked storage dirs App Python sources now ship unpacked inside the app bundle (next to stdlib/site-packages) on macOS/iOS/Windows/Linux instead of being extracted from app.zip on first launch; Android ships a stored app.zip asset unpacked once. The app dir is read-only, so the process cwd moves to a writable, app-private data dir. - build_base.py: stage the app via SERIOUS_PYTHON_APP for native platforms; gate the app.zip existence check to Emscripten/web. - templates main.dart / native_runtime.dart: ask serious_python for the app dir (prepareApp/getAppDir) instead of extracting app.zip; set cwd + FLET_APP_STORAGE_DATA to /data, add FLET_APP_STORAGE_CACHE, repoint FLET_APP_STORAGE_TEMP to the OS temp dir; pubspec pins serious_python 4.0.0 and makes app.zip a web-only asset. - run.py: dev-mode storage under a hidden, self-gitignored /.flet/; cwd = .flet/storage/data, redirect TMPDIR, set the three FLET_APP_STORAGE_* vars; hide .flet/ on Windows; write a README explaining the dirs. - docs: new breaking-change guide + updated environment-variables, read-and-write-files, storagepaths, sidebars. --- CHANGELOG.md | 3 +- .../src/flet_cli/commands/build_base.py | 24 +++- .../flet-cli/src/flet_cli/commands/run.py | 92 ++++++++++++- .../app/{{cookiecutter.out_dir}}/.gitignore | 2 +- .../{{cookiecutter.out_dir}}/lib/main.dart | 40 +++--- .../lib/native_runtime.dart | 10 +- .../lib/native_runtime_stub.dart | 4 +- .../{{cookiecutter.out_dir}}/pubspec.yaml | 13 +- website/docs/cookbook/read-and-write-files.md | 22 +++- .../docs/reference/environment-variables.md | 30 ++++- website/docs/services/storagepaths.md | 10 ++ ...6-0-app-files-unpacked-read-only-bundle.md | 121 ++++++++++++++++++ website/sidebars.yml | 1 + 13 files changed, 325 insertions(+), 47 deletions(-) create mode 100644 website/docs/updates/breaking-changes/v0-86-0-app-files-unpacked-read-only-bundle.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 35380e6d0f..01c6b9eb88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### New features -* Multi-version bundled CPython support in `flet build` and `flet publish`. Pick the runtime your app ships with via the new `--python-version` flag (3.12 / 3.13 / 3.14), or let it be derived from `[project].requires-python` in your `pyproject.toml`; defaults to the latest supported stable (currently 3.14). The matching CPython-standalone build, Pyodide release (0.27.7 / 0.29.4 / 314.0.0), and Emscripten wheel platform tag are all resolved from `flet-dev/python-build`'s date-keyed manifest. Adding a future pre-release CPython line (e.g. 3.15 beta) is a one-row append with `prerelease=True` — opt-in only via an explicit `--python-version 3.15` or `requires-python = "==3.15.*"`, never the auto-resolved default. Requires `serious_python` >= 3.0.0, now pinned in the `flet build` template. See the new [Choosing a Python version](https://flet.dev/docs/publish#choosing-a-python-version) docs section ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. +* Multi-version bundled CPython support in `flet build` and `flet publish`. Pick the runtime your app ships with via the new `--python-version` flag (3.12 / 3.13 / 3.14), or let it be derived from `[project].requires-python` in your `pyproject.toml`; defaults to the latest supported stable (currently 3.14). The matching CPython-standalone build, Pyodide release (0.27.7 / 0.29.4 / 314.0.0), and Emscripten wheel platform tag are all resolved from `flet-dev/python-build`'s date-keyed manifest. Adding a future pre-release CPython line (e.g. 3.15 beta) is a one-row append with `prerelease=True` — opt-in only via an explicit `--python-version 3.15` or `requires-python = "==3.15.*"`, never the auto-resolved default. Requires `serious_python` >= 4.0.0, now pinned in the `flet build` template. See the new [Choosing a Python version](https://flet.dev/docs/publish#choosing-a-python-version) docs section ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. * Add `ft.DataChannel`: dedicated byte channels for widgets that move bulk binary data (image frames, audio buffers, ML tensors) between Dart and Python, bypassing the MsgPack control protocol. The Dart side opens a channel via `FletBackend.of(context).openDataChannel()` and announces it to Python by firing a `data_channel_open` control event with `{channel_name, channel_id}`; the Python side declares `on_data_channel_open: Optional[ft.EventHandler[ft.DataChannelOpenEvent]]` and captures the channel via `self.get_data_channel(e.channel_id)`. Backed by a dedicated `PythonBridge` per channel in embedded native mode (4–7 GiB/s on M2 Pro) and by the default `ProtocolMuxedDataChannelFactory` in dev / web modes (raw-byte frames muxed over the active Flet protocol transport with a 1-byte type discriminator). Pyodide gets zero-copy outbound sends via `postMessage` Transferable ArrayBuffer. First consumer: `flet-charts` `MatplotlibChartCanvas`, migrated from `_invoke_method` PNG dispatch to a 1-byte-opcode data channel by @FeodorFitsner. * **In-process Python transport (`dart_bridge` FFI).** `package:flet` gains a third protocol transport alongside the UDS / TCP socket servers: it can run Flet's MsgPack protocol over an in-process `dart_bridge` byte channel via a `FletApp(channelBuilder: …)` seam (the `flet` package stays Python-independent — it doesn't depend on `serious_python` or know about `PythonBridge`; the embedder wires the channel). `serious_python` >= 3.0.0 uses this seam to embed the Python interpreter **in-process** instead of talking to it over a localhost socket, and the `flet build` template migrates from sockets to the FFI transport. On Android, where the OS may keep the process alive across a back-button quit and restart only the Dart VM, the transport rebinds to the new VM's `dart_bridge` ports on a session-restart signal (`libdart_bridge` >= 1.3.0) — the Python process and its in-memory state are preserved and the Flet session is rebuilt from `REGISTER_CLIENT` by @FeodorFitsner. * Add `flet clean` command that deletes the `build` directory of a Flet app — the Flutter bootstrap project, cached artifacts, and generated output — in a single step ([#6233](https://github.com/flet-dev/flet/issues/6233)) by @ndonkoHenri. @@ -22,6 +22,7 @@ ### Breaking changes +* **App files now ship unpacked in a read-only bundle, and the storage directories were reworked** (requires `serious_python` >= 4.0.0, now pinned in the `flet build` template). Your Python sources ship **unpacked inside the app bundle** next to the stdlib/site-packages (no first-launch `app.zip` extraction) on macOS/iOS/Windows/Linux; on Android they ship as a *stored* `app.zip` asset unpacked once on first launch; web is unchanged. The app directory is now **read-only**, so the Python program's **working directory** moved to a writable, app-private data dir. `FLET_APP_STORAGE_DATA` now maps to the OS *application support* dir (a `data` subdir) instead of the user's Documents folder and is the cwd; `FLET_APP_STORAGE_TEMP` now points to the OS temp dir (was the cache dir) and a new `FLET_APP_STORAGE_CACHE` exposes the cache dir. `flet run` sets the dev cwd to a hidden, git-ignored `/.flet/storage/data`. Relative **reads** of bundled files (`open("seed.json")`) must move to `__file__`/`importlib.resources` or `assets/`. See the [app files unpacked / storage dirs](/docs/updates/breaking-changes/v0-86-0-app-files-unpacked-read-only-bundle) guide by @FeodorFitsner. * `flet build` and `flet publish` now bundle CPython 3.14 by default (previously 3.12, implicit via the old single-version `serious_python`). Existing apps that depend on native wheels without 3.14 binaries should pin explicitly with `--python-version 3.12` (CLI), `requires-python = ">=3.12,<3.13"` (pyproject), or `SERIOUS_PYTHON_VERSION=3.12` in the build environment ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. * Android builds now include only the ABIs the bundled Python supports: for Python 3.13+, `armeabi-v7a` is no longer supported nor packaged by default. An explicit `--arch armeabi-v7a` fails with a clear error unless combined with `--python-version 3.12`, which is the only Python version supporting it. The per-version ABI set is sourced from python-build's manifest (`pythons..android_abis`), not hardcoded in flet ([#6578](https://github.com/flet-dev/flet/pull/6578)) by @ndonkoHenri. * `flet build` / `flet publish` now **compile your app and packages to `.pyc` by default** (previously off). This removes per-launch bytecode recompilation — a significant cold-start win, especially on mobile where pure Python is imported from a stored zip (`zipimport`) and can't cache bytecode back to disk, so every module would otherwise recompile from source on each launch. The CLI flags gain `--no-compile-app` / `--no-compile-packages` (via `argparse.BooleanOptionalAction`; the existing `--compile-app` / `--compile-packages` still work), and `[tool.flet.compile].app` / `.packages` now default to `true`. Pass `--no-compile-*` or set them to `false` to restore the old behavior (faster iterative builds, or keeping `.py` source in the bundle). Compiled web builds were verified to load in Pyodide (bundled CPython and Pyodide share the same minor version). See the [compile-on-by-default](/docs/updates/breaking-changes/v0-86-0-compile-on-by-default) guide ([#6598](https://github.com/flet-dev/flet/pull/6598)) by @FeodorFitsner. diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py index 529dd2e804..ac7709776a 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py @@ -2088,6 +2088,10 @@ def package_python_app(self): package_env["SERIOUS_PYTHON_SITE_PACKAGES"] = str( self.build_dir / "site-packages" ) + # app staging dir: serious_python's `package` places the processed + # app here (no app.zip on native); the platform native build copies + # it into the bundle (Android zips it as a stored asset). + package_env["SERIOUS_PYTHON_APP"] = str(self.build_dir / "python-app") # flutter-packages variable if self.flutter_packages_temp_dir.exists(): @@ -2234,10 +2238,18 @@ def package_python_app(self): hash.commit() - # make sure app/app.zip exists - app_zip_path = self.flutter_dir.joinpath("app", "app.zip") - if not os.path.exists(app_zip_path): - self.cleanup(1, "Flet app package app/app.zip was not created.") + # verify the package output: web ships app/app.zip; native platforms + # stage the unpacked app to build/app for the native build to bundle. + if self.package_platform == "Emscripten": + app_zip_path = self.flutter_dir.joinpath("app", "app.zip") + if not os.path.exists(app_zip_path): + self.cleanup(1, "Flet app package app/app.zip was not created.") + else: + app_staging_dir = self.build_dir / "python-app" + if not app_staging_dir.exists(): + self.cleanup( + 1, f"Flet app package was not staged to {app_staging_dir}." + ) console.log(f"Packaged Python app {self.emojis['checkmark']}") @@ -2337,6 +2349,10 @@ def _run_flutter_command(self): build_env["SERIOUS_PYTHON_SITE_PACKAGES"] = str( self.build_dir / "site-packages" ) + # app staging dir: read by the platform native build (CMake / + # podspec / Android Gradle) at `flutter build` time to place the + # unpacked app into the bundle. + build_env["SERIOUS_PYTHON_APP"] = str(self.build_dir / "python-app") # Path-hungry packages to ship extracted to disk: consumed by the # serious_python_android Gradle split during `flutter build`. diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/run.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/run.py index 2be23587a1..e0217aa2e2 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/run.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/run.py @@ -220,8 +220,67 @@ def handle(self, options: argparse.Namespace) -> None: else [] ) - flet_app_data_dir = project_dir / "storage" / "data" - flet_app_temp_dir = project_dir / "storage" / "temp" + # Dev-mode app storage under a hidden, Flet-namespaced `.flet/` dir so + # it stays out of the way and is git-ignored. Mirrors a built app: the + # data dir becomes the process cwd; data/cache/temp map to the three + # FLET_APP_STORAGE_* env vars. + flet_storage_dir = project_dir / ".flet" / "storage" + flet_app_data_dir = flet_storage_dir / "data" + flet_app_cache_dir = flet_storage_dir / "cache" + flet_app_temp_dir = flet_storage_dir / "temp" + for d in (flet_app_data_dir, flet_app_cache_dir, flet_app_temp_dir): + d.mkdir(parents=True, exist_ok=True) + # Self-ignore the whole `.flet/` dir in any repo, independent of the + # project's root .gitignore. + flet_dir = project_dir / ".flet" + flet_gitignore = flet_dir / ".gitignore" + if not flet_gitignore.exists(): + flet_gitignore.write_text("*\n", encoding="utf-8") + # Drop a short note explaining what this dir is, next to .gitignore. + flet_readme = flet_dir / "README.md" + if not flet_readme.exists(): + flet_readme.write_text( + "# `.flet/`\n" + "\n" + "This directory is created by `flet run` to hold per-project " + "development state. It is local to your machine, safe to delete " + "(it will be recreated on the next run), and git-ignored via the " + "`.gitignore` next to this file.\n" + "\n" + "While `flet run` is active, your app's working directory is " + "`storage/data` and the three storage locations are exposed to " + "your Python code through the `FLET_APP_STORAGE_*` environment " + "variables — matching how a packaged app behaves on a device.\n" + "\n" + "## `storage/`\n" + "\n" + "- **`data/`** (`FLET_APP_STORAGE_DATA`) — durable application " + "data: databases, settings, user files. This is also the " + "process current working directory, so relative paths " + '(e.g. `sqlite3.connect("my.db")`) land here. Persists across ' + "runs; the equivalent of this dir is preserved across app " + "updates on a device.\n" + "- **`cache/`** (`FLET_APP_STORAGE_CACHE`) — regenerable cache " + "data. The OS may purge the equivalent dir on a device under " + "storage pressure, so only store things you can rebuild.\n" + "- **`temp/`** (`FLET_APP_STORAGE_TEMP`) — throwaway scratch " + "space. Python's `tempfile` is pointed here too. May be cleared " + "at any time.\n", + encoding="utf-8", + ) + # On Windows a leading dot doesn't hide a dir (that's a POSIX + # convention); set the FILE_ATTRIBUTE_HIDDEN attribute explicitly so + # `.flet/` stays out of Explorer / file pickers like it does elsewhere. + if is_windows(): + try: + import ctypes + + FILE_ATTRIBUTE_HIDDEN = 0x02 + ctypes.windll.kernel32.SetFileAttributesW( + str(flet_dir), FILE_ATTRIBUTE_HIDDEN + ) + except Exception: + pass my_event_handler = Handler( args=[sys.executable, "-u"] @@ -240,6 +299,7 @@ def handle(self, options: argparse.Namespace) -> None: assets_dir=assets_dir, ignore_dirs=ignore_dirs, flet_app_data_dir=str(flet_app_data_dir), + flet_app_cache_dir=str(flet_app_cache_dir), flet_app_temp_dir=str(flet_app_temp_dir), ) @@ -284,6 +344,7 @@ def __init__( assets_dir, ignore_dirs, flet_app_data_dir, + flet_app_cache_dir, flet_app_temp_dir, ) -> None: super().__init__() @@ -307,6 +368,7 @@ def __init__( self.page_url_prefix = f"PAGE_URL_{time.time()}" self.page_url = None self.flet_app_data_dir = flet_app_data_dir + self.flet_app_cache_dir = flet_app_cache_dir self.flet_app_temp_dir = flet_app_temp_dir self.terminate = threading.Event() self.start_process() @@ -339,13 +401,37 @@ def start_process(self): p_env["FLET_DISPLAY_URL_PREFIX"] = self.page_url_prefix p_env["FLET_APP_STORAGE_DATA"] = self.flet_app_data_dir + p_env["FLET_APP_STORAGE_CACHE"] = self.flet_app_cache_dir p_env["FLET_APP_STORAGE_TEMP"] = self.flet_app_temp_dir + # Point Python's temp machinery at the project-local temp dir so + # tempfile.gettempdir() agrees with FLET_APP_STORAGE_TEMP and dev scratch + # stays inside the git-ignored .flet/. + p_env["TMPDIR"] = self.flet_app_temp_dir + p_env["TEMP"] = self.flet_app_temp_dir + p_env["TMP"] = self.flet_app_temp_dir + + # We launch the app with cwd = the data dir (matches a built app, where + # the app bundle is read-only and writes go to app-support). Keep the + # app importable by putting the original working directory — where + # `flet run` was invoked and where `-m` modules resolve — on PYTHONPATH. + invocation_cwd = os.getcwd() + existing_pythonpath = p_env.get("PYTHONPATH", "") + p_env["PYTHONPATH"] = ( + invocation_cwd + os.pathsep + existing_pythonpath + if existing_pythonpath + else invocation_cwd + ) + p_env["PYTHONIOENCODING"] = "utf-8" p_env["PYTHONWARNINGS"] = "default::DeprecationWarning" self.p = subprocess.Popen( - self.args, env=p_env, stdout=subprocess.PIPE, encoding="utf-8" + self.args, + env=p_env, + cwd=self.flet_app_data_dir, + stdout=subprocess.PIPE, + encoding="utf-8", ) self.is_running = True diff --git a/sdk/python/templates/app/app/{{cookiecutter.out_dir}}/.gitignore b/sdk/python/templates/app/app/{{cookiecutter.out_dir}}/.gitignore index adb31b9c42..13e37d0cb1 100644 --- a/sdk/python/templates/app/app/{{cookiecutter.out_dir}}/.gitignore +++ b/sdk/python/templates/app/app/{{cookiecutter.out_dir}}/.gitignore @@ -160,4 +160,4 @@ cython_debug/ #.idea/ # Flet -storage/ +.flet/ diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/main.dart b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/main.dart index 73b8557f6d..0701306297 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/main.dart +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/main.dart @@ -6,7 +6,6 @@ import 'package:flet/flet.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:package_info_plus/package_info_plus.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart' as path_provider; import 'package:flutter_web_plugins/url_strategy.dart'; @@ -50,7 +49,6 @@ hide_window_on_start: {{ hide_window_on_start }} const bool isRelease = bool.fromEnvironment('dart.vm.product'); -const assetPath = "app/app.zip"; const pythonModuleName = "{{ cookiecutter.python_module_name }}"; final showAppBootScreen = bool.tryParse("{{ show_boot_screen }}".toLowerCase()) ?? false; const appBootScreenMessage = '{{ boot_screen_message | default("Preparing the app for its first launch…", true) }}'; @@ -183,35 +181,35 @@ Future prepareApp() async { } } else { // production mode - // extract app from asset - appDir = await nrt.extractAppAssets(assetPath, checkHash: true); - - // set current directory to app path - Directory.current = appDir; + // resolve the app dir from the bundle (Android unpacks app.zip on first launch) + appDir = await nrt.getAppDir(); assetsDir = path.join(appDir, "assets"); - // configure apps DATA and TEMP directories + // configure the app's storage directories WidgetsFlutterBinding.ensureInitialized(); - var appTempPath = (await path_provider.getApplicationCacheDirectory()).path; - var appDataPath = - (await path_provider.getApplicationDocumentsDirectory()).path; - - if (defaultTargetPlatform != TargetPlatform.iOS && - defaultTargetPlatform != TargetPlatform.android) { - // append app name to the path and create dir - PackageInfo packageInfo = await PackageInfo.fromPlatform(); - appDataPath = path.join(appDataPath, "flet", packageInfo.packageName); - if (!await Directory(appDataPath).exists()) { - await Directory(appDataPath).create(recursive: true); - } + // FLET_APP_STORAGE_DATA — durable, app-private; also the cwd. Lives under + // the OS application-support dir (NOT the app bundle, which is read-only), + // so relative file writes / SQLite work and persist across app updates. + var appDataPath = path.join( + (await path_provider.getApplicationSupportDirectory()).path, "data"); + if (!await Directory(appDataPath).exists()) { + await Directory(appDataPath).create(recursive: true); } + Directory.current = appDataPath; + + // FLET_APP_STORAGE_CACHE — regenerable; the OS may purge it. + var appCachePath = (await path_provider.getApplicationCacheDirectory()).path; + // FLET_APP_STORAGE_TEMP — volatile OS temp; may vanish between launches. + var appTempPath = (await path_provider.getTemporaryDirectory()).path; environmentVariables.putIfAbsent("FLET_APP_STORAGE_DATA", () => appDataPath); + environmentVariables.putIfAbsent( + "FLET_APP_STORAGE_CACHE", () => appCachePath); environmentVariables.putIfAbsent("FLET_APP_STORAGE_TEMP", () => appTempPath); - outLogFilename = path.join(appTempPath, "console.log"); + outLogFilename = path.join(appCachePath, "console.log"); environmentVariables.putIfAbsent("FLET_APP_CONSOLE", () => outLogFilename); environmentVariables.putIfAbsent( diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart index ab281e4901..edb09ff6a9 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart @@ -70,11 +70,11 @@ bool get bridgesActive => _bridge != null; /// process, and false when running against pre-1.3.0 libdart_bridge. bool get pythonAlreadyRunning => DartBridge.instance.isPythonInitialized; -/// Extract the bundled app.zip into a fresh app directory and return its -/// path. Wraps `serious_python.extractAssetZip` so main.dart doesn't have -/// to import the FFI-touching package directly. -Future extractAppAssets(String assetPath, {bool checkHash = false}) => - extractAssetZip(assetPath, checkHash: checkHash); +/// Resolve the bundled app directory (Android unpacks `app.zip` on first +/// launch; desktop/iOS read it in place from the bundle). Wraps +/// `SeriousPython.prepareApp()` so main.dart doesn't have to import the +/// FFI-touching package directly. +Future getAppDir() => SeriousPython.prepareApp(); /// FletApp's `channelBuilder` for the embedded-Python protocol — wraps the /// in-process [PythonBridge] in a [FletBackendChannel]. Returns null until diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime_stub.dart b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime_stub.dart index 95cacb5acc..dbfc67f390 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime_stub.dart +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime_stub.dart @@ -17,8 +17,8 @@ bool get bridgesActive => false; /// process-reuse check compiles for the web target. bool get pythonAlreadyRunning => false; -Future extractAppAssets(String assetPath, {bool checkHash = false}) => - throw UnsupportedError("Asset extraction not available on web"); +Future getAppDir() => + throw UnsupportedError("App directory not available on web"); FletBackendChannelBuilder? get channelBuilder => null; diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml b/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml index 22472a5acd..055be6bdfa 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml @@ -17,7 +17,13 @@ dependencies: flet: path: ../../../../../packages/flet - serious_python: 3.0.0 + # Track the in-development serious_python (4.0.0) from the app-package branch + # until it's published to pub.dev. Pinned by the lifted app-packaging work. + serious_python: + git: + url: https://github.com/flet-dev/serious-python.git + ref: app-package + path: src/serious_python # MsgPack codec used by the dart_bridge FletBackendChannel implementation # in lib/main.dart — matches the wire format flet's existing socket @@ -47,9 +53,14 @@ flutter: uses-material-design: true +# On native platforms the app ships unpacked inside the bundle (placed by +# serious_python's platform build), so there is no app.zip Flutter asset. Only +# the web (Pyodide) build still loads app/app.zip. +#{% if cookiecutter.options.config_platform == "web" %} assets: - app/app.zip - app/app.zip.hash +#{% endif %} # dart run flutter_launcher_icons flutter_launcher_icons: diff --git a/website/docs/cookbook/read-and-write-files.md b/website/docs/cookbook/read-and-write-files.md index 330f231a1f..e2eb53cf1d 100644 --- a/website/docs/cookbook/read-and-write-files.md +++ b/website/docs/cookbook/read-and-write-files.md @@ -9,19 +9,33 @@ Flet makes it easy to work with files and directories on the mobile/desktop devi ### Storage Paths -Flet provides two directory paths for data storage, available as environment variables: -[`FLET_APP_STORAGE_DATA`](../reference/environment-variables.md#flet_app_storage_data) and -[`FLET_APP_STORAGE_TEMP`](../reference/environment-variables.md#flet_app_storage_temp). +Flet provides three pre-created, app-private directory paths as environment variables: -Their values can be gotten as follows: +- [`FLET_APP_STORAGE_DATA`](../reference/environment-variables.md#flet_app_storage_data) — **durable** + data (databases, state, config). Preserved across app updates and backed up; the OS never deletes it. + This is also the program's current working directory, so relative writes land here. +- [`FLET_APP_STORAGE_CACHE`](../reference/environment-variables.md#flet_app_storage_cache) — + **regenerable** cache. The OS may purge it; only store things you can rebuild. +- [`FLET_APP_STORAGE_TEMP`](../reference/environment-variables.md#flet_app_storage_temp) — **throwaway** + scratch (the OS temp dir). May vanish between launches. + +Use `FLET_APP_STORAGE_DATA` for anything you must keep — putting a database in cache or temp risks +silent data loss. + +Their values can be read as follows: ```python import os app_data_path = os.getenv("FLET_APP_STORAGE_DATA") +app_cache_path = os.getenv("FLET_APP_STORAGE_CACHE") app_temp_path = os.getenv("FLET_APP_STORAGE_TEMP") ``` +Because `FLET_APP_STORAGE_DATA` is the working directory, a relative path like `"counter.txt"` resolves +inside it; use `os.path.join(app_data_path, ...)` if you want to be explicit. The same behavior applies +under `flet run` (the dev working directory is `/.flet/storage/data`). + ### Writing to a File To write data to a new/existing file, you can use the built-in [`open`](https://docs.python.org/3/library/functions.html#open) function. diff --git a/website/docs/reference/environment-variables.md b/website/docs/reference/environment-variables.md index d85c18f22f..a8e223d90a 100644 --- a/website/docs/reference/environment-variables.md +++ b/website/docs/reference/environment-variables.md @@ -9,19 +9,39 @@ Any other value will be interpreted as `False`. ### `FLET_APP_CONSOLE` -The path to the application's console log file (`console.log`) in the temporary storage directory. +The path to the application's console log file (`console.log`) in the cache storage directory +([`FLET_APP_STORAGE_CACHE`](#flet_app_storage_cache)). Its value is set in production mode. ### `FLET_APP_STORAGE_DATA` -A directory for the storage of persistent application data that is preserved between app updates. -It is already pre-created and its location depends on the platform the app is running on. +A directory for **durable** application data — databases, state, config — that is preserved between +app updates, backed up by the OS, and never auto-deleted. It is pre-created, app-private, and its +location depends on the platform (it maps to the platform's *application support* directory: +`%APPDATA%\\\data` on Windows, `~/Library/Application Support//data` on +macOS, `~/.local/share//data` on Linux, the app-sandboxed support dir on iOS/Android). + +In production this directory is also the Python program's **current working directory**, so relative +file writes (e.g. `open("app.db", "w")`, `sqlite3.connect("app.db")`) land here. In `flet run` (dev +mode) it is `/.flet/storage/data`. + +### `FLET_APP_STORAGE_CACHE` + +A directory for **regenerable** cached data. The OS may purge it under storage pressure (and the +platform "clear cache" action wipes it), so only store things you can rebuild. Pre-created and +app-private; it maps to the platform's *caches* directory (`%LOCALAPPDATA%\\` on +Windows, `~/Library/Caches/` on macOS, `~/.cache/` on Linux, the app cache dir on +iOS/Android). In `flet run` it is `/.flet/storage/cache`. ### `FLET_APP_STORAGE_TEMP` -A directory for the storage of temporary application files, i.e. cache. -It is already pre-created and its location depends on the platform the app is running on. +A directory for **throwaway** temporary files — the OS temporary directory (`getTemporaryDirectory()`). +It is the most volatile of the three and may be cleared between launches, so don't rely on its contents +persisting. (On Android it resolves to the same directory as +[`FLET_APP_STORAGE_CACHE`](#flet_app_storage_cache).) In `flet run` it is `/.flet/storage/temp`, +and Python's `tempfile` is pointed here too (via `TMPDIR`). Equivalent to Python's +`tempfile.gettempdir()`. ### `FLET_APP_USER_MODEL_ID` diff --git a/website/docs/services/storagepaths.md b/website/docs/services/storagepaths.md index 9e8e7c2670..ee6f3ef20b 100644 --- a/website/docs/services/storagepaths.md +++ b/website/docs/services/storagepaths.md @@ -8,6 +8,16 @@ import {ClassMembers, ClassSummary, CodeExample} from '@site/src/components/croc +:::note Relationship to the `FLET_APP_STORAGE_*` environment variables +`StoragePaths` exposes the raw platform directories. The pre-created +[`FLET_APP_STORAGE_*`](../reference/environment-variables.md#flet_app_storage_data) env vars map onto +them: `FLET_APP_STORAGE_CACHE` = `get_application_cache_directory()`, `FLET_APP_STORAGE_TEMP` = +`get_temporary_directory()`, and `FLET_APP_STORAGE_DATA` = a flet-owned **`data` subdirectory** of +`get_application_support_directory()` (which is also the app's current working directory). Prefer the +env vars for app storage; use `StoragePaths` when you need a different platform directory (e.g. +Documents or Downloads). +::: + ## Examples diff --git a/website/docs/updates/breaking-changes/v0-86-0-app-files-unpacked-read-only-bundle.md b/website/docs/updates/breaking-changes/v0-86-0-app-files-unpacked-read-only-bundle.md new file mode 100644 index 0000000000..8ca4d4867a --- /dev/null +++ b/website/docs/updates/breaking-changes/v0-86-0-app-files-unpacked-read-only-bundle.md @@ -0,0 +1,121 @@ +--- +title: "App files ship unpacked in a read-only bundle; storage dirs reworked" +--- + +# App files ship unpacked in a read-only bundle; storage dirs reworked + +:::note +This guide is accurate as of Flet 0.86.0. Later releases might add new APIs or +additional migration paths. + +The [breaking changes and deprecations index](.) lists the guides created for each release. +::: + +## Summary + +Flet 0.86.0 (with `serious_python` 4.0.0) changes how a packaged app's Python files are shipped and +where the app reads and writes data: + +- **App files ship unpacked inside the app bundle**, next to the Python standard library and + site-packages, on macOS / iOS / Windows / Linux. There is no first-launch `app.zip` extraction. On + **Android** the app ships as a *stored* `app.zip` asset inside the APK and is unpacked once + (version-keyed) to the app's files directory on the first launch after an install/update. Web + (Pyodide) is unchanged. +- The app directory is now **read-only** (signed `.app`, iOS bundle, `Program Files`, the APK). +- The Python program's **current working directory** moved from the app directory to a writable, + app-private data directory, exposed as `FLET_APP_STORAGE_DATA`. +- `FLET_APP_STORAGE_DATA` now maps to the OS *application support* directory (a `data` subdir), + **not** the user's Documents folder. +- The storage env vars are now a clean three-tier set: `FLET_APP_STORAGE_DATA` (durable), + the new **`FLET_APP_STORAGE_CACHE`** (regenerable), and `FLET_APP_STORAGE_TEMP` (now the OS temp + dir, previously the cache dir). +- `flet run` (dev mode) now sets the working directory to a hidden, git-ignored + `/.flet/storage/data`, mirroring a built app. + +## Background + +Previously your Python sources were zipped into `app/app.zip`, shipped as a Flutter asset, and +extracted at runtime on first launch — while the stdlib and site-packages already shipped unpacked +inside the bundle. Lifting app-file handling into `serious_python` removes the first-launch unzip and +the hash bookkeeping, and puts your code in the same place as the stdlib. Because that place is +read-only, the working directory has to move somewhere writable — and the natural home for app data +is the OS *application support* directory, which is app-private on every platform (no Documents +clutter, no manual scoping). + +## Migration guide + +### Write data to the storage dirs, not next to your code + +The app directory is read-only. Relative writes resolve against the **current working directory**, +which is now `FLET_APP_STORAGE_DATA` (a writable, durable, app-private dir). `open("app.db", "w")` and +`sqlite3.connect("app.db")` keep working — the file just lands in the data dir instead of next to your +`.py` files, and now persists across app updates. + +### Read shipped files via `__file__`, not relative paths + +Relative **reads** of files you bundle with your app (e.g. `open("seed.json")`) no longer resolve +against your app folder. Read bundled data relative to your module instead: + +```python +from pathlib import Path + +SEED = Path(__file__).parent / "seed.json" +data = SEED.read_text() +``` + +or use `importlib.resources`, or place the file under your Flet `assets/` directory and resolve it via +`FLET_ASSETS_DIR`. + +### Storage environment variables + +| Variable | Meaning | Lifetime | +| --- | --- | --- | +| `FLET_APP_STORAGE_DATA` | Durable app data (= the working directory) | Never auto-deleted; backed up | +| `FLET_APP_STORAGE_CACHE` | Regenerable cache (**new**) | OS may purge under storage pressure | +| `FLET_APP_STORAGE_TEMP` | OS temp / scratch (**changed** — was the cache dir) | Most volatile; may vanish between launches | + +If you relied on `FLET_APP_STORAGE_TEMP` for data that should survive (it used to be the cache dir), +switch to `FLET_APP_STORAGE_CACHE` (or `FLET_APP_STORAGE_DATA` for must-keep data). + +### Physical locations + +`FLET_APP_STORAGE_DATA` (= the working directory): + +| Platform | Location | +| --- | --- | +| Windows | `%APPDATA%\\\data` (e.g. `C:\Users\\AppData\Roaming\\\data`) | +| macOS | `~/Library/Application Support//data` (sandboxed apps: redirected into the app container) | +| Linux | `${XDG_DATA_HOME:-~/.local/share}//data` | +| iOS | `/Library/Application Support//data` | +| Android | `/data/data//files/data` | +| `flet run` (dev) | `/.flet/storage/data` | + +`FLET_APP_STORAGE_CACHE` and `FLET_APP_STORAGE_TEMP`: + +| Platform | `FLET_APP_STORAGE_CACHE` | `FLET_APP_STORAGE_TEMP` | +| --- | --- | --- | +| Windows | `%LOCALAPPDATA%\\` | `%TEMP%` (`…\AppData\Local\Temp`) | +| macOS | `~/Library/Caches/` | `$TMPDIR` (e.g. `/var/folders/.../T/`) | +| Linux | `${XDG_CACHE_HOME:-~/.cache}/` | system temp (`$TMPDIR`, fallback `/tmp`) | +| iOS | `/Library/Caches/` | `/tmp` | +| Android | `getCacheDir()` (`/data/data//cache`) | same as `CACHE` (`getCacheDir()`) | +| `flet run` (dev) | `/.flet/storage/cache` | `/.flet/storage/temp` | + +### `flet run` dev storage + +`flet run` now creates a hidden `/.flet/` directory (with `storage/{data,cache,temp}`) and +runs your app with the working directory set to `.flet/storage/data`, so dev behaves like a built app +(including the read-shipped-files change above). The directory writes its own `.gitignore` and is +ignored by Git automatically; the old visible `storage/` directory is no longer used. + +## Timeline + +- Changed in: `0.86.0` + +## References + +- Environment variables: [`FLET_APP_STORAGE_DATA`](../../reference/environment-variables.md#flet_app_storage_data), + [`FLET_APP_STORAGE_CACHE`](../../reference/environment-variables.md#flet_app_storage_cache), + [`FLET_APP_STORAGE_TEMP`](../../reference/environment-variables.md#flet_app_storage_temp) +- Cookbook: [Read and write files](../../cookbook/read-and-write-files.md) +- Release notes: [Flet 0.86.0](../release-notes.md) diff --git a/website/sidebars.yml b/website/sidebars.yml index 361f2e80af..056f812703 100644 --- a/website/sidebars.yml +++ b/website/sidebars.yml @@ -69,6 +69,7 @@ docs: Breaking changes and deprecations: _index: updates/breaking-changes/index.md v0.86.0: + App files ship unpacked in a read-only bundle; storage dirs reworked: updates/breaking-changes/v0-86-0-app-files-unpacked-read-only-bundle.md Default bundled Python version is now 3.14: updates/breaking-changes/v0-86-0-default-bundled-python-3-14.md App and packages are compiled to .pyc by default: updates/breaking-changes/v0-86-0-compile-on-by-default.md flet.version.pyodide_version and PYODIDE_VERSION removed: updates/breaking-changes/v0-86-0-removed-pyodide-version-export.md From 5eddf3c320891cb74cb07932a0bd02d5129d915e Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Sat, 20 Jun 2026 12:39:30 -0700 Subject: [PATCH 4/8] Bump pasteboard to ^0.5.0 (and refresh transitive deps in client lockfile) --- client/pubspec.lock | 24 ++++++++++++------------ packages/flet/pubspec.yaml | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/client/pubspec.lock b/client/pubspec.lock index 79e83f043d..ae600eb608 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -333,10 +333,10 @@ packages: dependency: transitive description: name: file_picker - sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343" + sha256: f13a03000d942e476bc1ff0a736d2e9de711d2f89a95cd4c1d88f861c3348387 url: "https://pub.dev" source: hosted - version: "10.3.10" + version: "11.0.2" fixnum: dependency: transitive description: @@ -587,26 +587,26 @@ packages: dependency: transitive description: name: flutter_secure_storage - sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40 + sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.3.1" flutter_secure_storage_darwin: dependency: transitive description: name: flutter_secure_storage_darwin - sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3" + sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.3.2" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" + sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" flutter_secure_storage_platform_interface: dependency: transitive description: @@ -619,10 +619,10 @@ packages: dependency: transitive description: name: flutter_secure_storage_web - sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3" + sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" flutter_secure_storage_windows: dependency: transitive description: @@ -1087,10 +1087,10 @@ packages: dependency: transitive description: name: pasteboard - sha256: "9ff73ada33f79a59ff91f6c01881fd4ed0a0031cfc4ae2d86c0384471525fca1" + sha256: fedbe8da188d2f713aa8b01260737342e6e1087534a3ab26e1a719f8d3e8f32f url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.5.0" path: dependency: transitive description: diff --git a/packages/flet/pubspec.yaml b/packages/flet/pubspec.yaml index 257f6f9c97..51967a08a4 100644 --- a/packages/flet/pubspec.yaml +++ b/packages/flet/pubspec.yaml @@ -39,7 +39,7 @@ dependencies: package_info_plus: ^9.0.0 path: ^1.9.0 path_provider: ^2.1.5 - pasteboard: ^0.4.0 + pasteboard: ^0.5.0 provider: ^6.1.2 screenshot: ^3.0.0 screen_brightness: ^2.1.11 From f2b59c0c135f408d521ceda9e1f3cd39bdea9216 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Sun, 21 Jun 2026 14:41:49 -0700 Subject: [PATCH 5/8] flet build: Swift Package Manager by default for iOS/macOS (#6607) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * flet build: use Swift Package Manager for iOS/macOS by default Drives the new serious_python_darwin SPM build path. SPM is Flet's default darwin integration (CocoaPods remains the fallback); Flet auto-enables it only on its own managed Flutter, never a Flutter found on PATH. - flutter_base.py: install_flutter() now enables Swift Package Manager on the Flet-managed Flutter (`flutter config --enable-swift-package-manager`) and sets flutter_installed_by_flet. - build_base.py: _darwin_spm_active() reports whether the darwin build uses SPM (true on Flet's managed Flutter; otherwise honors the user's flutter config via `flutter config --machine`). When active, the package step gets SERIOUS_PYTHON_DARWIN_SPM (so serious_python does the host-side SPM staging the podspec prepare_command can't) + SERIOUS_PYTHON_SPM_KEY_FILE, and the flutter build env gets SP_NATIVE_SET read back from that key file so SwiftPM re-resolves when the staged native set changes. Requires serious_python >= the SPM-capable release. * flet build: SPM-by-default plumbing + client plugin migration + stale flutter-packages fix - build_base.py: set SERIOUS_PYTHON_DARWIN_SPM explicitly (true/false) and pass SP_NATIVE_SET key file through to serious_python's package step; --swift-package-manager / pyproject opt-out with flet-video auto-fallback. - build_base.py: register_flutter_extensions now always clears the permanent flutter-packages dir before staging this build's set, so an extension removed since the previous build (e.g. dropping flet-video) no longer lingers in the built app. - flutter_base.py: drop the global 'flutter config' SPM toggle (flet must not mutate Flutter's machine-wide config). - client: migrate off non-SPM plugins for the default desktop client — window_to_front -> windowManager.show(); pin screen_retriever 0.2.1 (SPM); regenerate plugin registrants + Runner SPM project wiring. - typos: exclude generated *.pbxproj (pre-commit hook + _typos.toml). - templates/build pubspec + CHANGELOG. --- CHANGELOG.md | 1 + .../flutter/generated_plugin_registrant.cc | 4 - client/linux/flutter/generated_plugins.cmake | 1 - .../Flutter/GeneratedPluginRegistrant.swift | 2 - client/macos/Runner.xcodeproj/project.pbxproj | 22 +++++ .../xcshareddata/xcschemes/Runner.xcscheme | 18 ++++ client/pubspec.lock | 18 +--- client/pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 - .../windows/flutter/generated_plugins.cmake | 1 - packages/flet/lib/src/utils/desktop.dart | 5 +- packages/flet/pubspec.yaml | 1 - sdk/python/.pre-commit-config.yaml | 4 +- sdk/python/_typos.toml | 5 + .../src/flet_cli/commands/build_base.py | 91 ++++++++++++++++++- .../{{cookiecutter.out_dir}}/pubspec.yaml | 5 +- 16 files changed, 150 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01c6b9eb88..b5e0e0f885 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Improvements +* **Swift Package Manager for iOS/macOS builds (on by default).** `flet build` / `flet debug` now integrate the embedded Python runtime via SPM instead of CocoaPods (CocoaPods goes read-only in December 2026). Flet **auto-falls back to CocoaPods** when the app depends on a package that isn't SPM-ready — currently `flet-video` (media_kit) — since Flutter then builds the whole app with CocoaPods. Force CocoaPods for other non-SPM packages with `--no-swift-package-manager` (or `swift_package_manager = false` under `[tool.flet]`). Flet does **not** change Flutter's global SPM configuration; the setting only selects how `serious_python` stages the runtime to match. When SPM is used (it has no `pod install` hook), `flet build` sets `SERIOUS_PYTHON_DARWIN_SPM` so `serious_python`'s `package` step stages the runtime (Python/dart_bridge xcframeworks, the iOS native extensions, and the stdlib/site-packages/app resources) into the plugin's `Package.swift` layout on the host before `flutter build`, and exports the `SP_NATIVE_SET` cache-bust key into the build. Requires the SPM-capable `serious_python` release. * **Smaller Android apps with no native-packaging config.** `flet build apk`/`aab` consume serious_python's new Android packaging: Python extension modules load **memory-mapped directly from the APK** (no extraction to disk), and pure Python ships in stored asset zips read via `zipimport`, so the standard library is no longer duplicated per ABI. Apps no longer need `useLegacyPackaging` / `keepDebugSymbols` — the `flet build` Android template drops them; just use `minSdk 23+`. New `--android-extract-packages` flag and `[tool.flet.android].extract_packages` ship "path-hungry" packages — those that read bundled data via `__file__` / `pkg_resources` instead of `importlib.resources` — extracted to disk instead of inside the zip (most packages, including `certifi`, are zip-safe and need no entry). Requires `serious_python` with the native-mmap packaging (dart_bridge 1.4.0). * Pyodide is no longer pre-baked into the `flet build` template. Each `flet build web` / `flet publish` run downloads the matching `pyodide-core-.tar.bz2` (plus the runtime `micropip` and `packaging` wheels) into a per-version cache at `~/.flet/pyodide//` and copies the files into the build output. Subsequent builds reuse the cache; the older `0.27.5` bundle previously shipped in the cookiecutter template is gone ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. * The supported Python / Pyodide / dart_bridge versions are loaded on demand from `flet-dev/python-build`'s date-keyed `manifest.json` (fetched once and cached under `~/.flet/cache`), the single source of truth shared with `serious_python` — replacing flet's hand-mirrored version table. `flet build` forwards only `SERIOUS_PYTHON_VERSION` and lets `serious_python` derive the full version / build date / dart_bridge version from its own committed snapshot. The module exposes `get_supported_python_versions()` / `get_default_python_version()` (the previous `SUPPORTED_PYTHON_VERSIONS` / `DEFAULT_PYTHON_VERSION` constants are removed) ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. diff --git a/client/linux/flutter/generated_plugin_registrant.cc b/client/linux/flutter/generated_plugin_registrant.cc index 486c5f4f54..49c3757b48 100644 --- a/client/linux/flutter/generated_plugin_registrant.cc +++ b/client/linux/flutter/generated_plugin_registrant.cc @@ -16,7 +16,6 @@ #include #include #include -#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = @@ -49,7 +48,4 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) window_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); window_manager_plugin_register_with_registrar(window_manager_registrar); - g_autoptr(FlPluginRegistrar) window_to_front_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "WindowToFrontPlugin"); - window_to_front_plugin_register_with_registrar(window_to_front_registrar); } diff --git a/client/linux/flutter/generated_plugins.cmake b/client/linux/flutter/generated_plugins.cmake index 692e9fcd42..b30e4c610a 100644 --- a/client/linux/flutter/generated_plugins.cmake +++ b/client/linux/flutter/generated_plugins.cmake @@ -13,7 +13,6 @@ list(APPEND FLUTTER_PLUGIN_LIST screen_retriever_linux url_launcher_linux window_manager - window_to_front ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/client/macos/Flutter/GeneratedPluginRegistrant.swift b/client/macos/Flutter/GeneratedPluginRegistrant.swift index 62e3cfcb94..634e2b35b4 100644 --- a/client/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/client/macos/Flutter/GeneratedPluginRegistrant.swift @@ -26,7 +26,6 @@ import url_launcher_macos import wakelock_plus import webview_flutter_wkwebview import window_manager -import window_to_front func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) @@ -50,5 +49,4 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) - WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin")) } diff --git a/client/macos/Runner.xcodeproj/project.pbxproj b/client/macos/Runner.xcodeproj/project.pbxproj index 7f045dcd00..2cccb3d5db 100644 --- a/client/macos/Runner.xcodeproj/project.pbxproj +++ b/client/macos/Runner.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; 4C5C567116C0F37A5F3A163A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BB9EEDA013737D623D55462A /* Pods_Runner.framework */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -74,6 +75,7 @@ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; BB9EEDA013737D623D55462A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EDAD244E5F1A9F15957004F8 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -81,6 +83,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 4C5C567116C0F37A5F3A163A /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -132,6 +135,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, @@ -175,6 +179,9 @@ /* Begin PBXNativeTarget section */ 33CC10EC2044A3C60003C045 /* Runner */ = { + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( @@ -200,6 +207,9 @@ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; @@ -647,6 +657,18 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } diff --git a/client/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/client/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 16d4c22e3f..d1579dfd00 100644 --- a/client/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/client/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + #include #include -#include void RegisterPlugins(flutter::PluginRegistry* registry) { AudioplayersWindowsPluginRegisterWithRegistrar( @@ -57,6 +56,4 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("UrlLauncherWindows")); WindowManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowManagerPlugin")); - WindowToFrontPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("WindowToFrontPlugin")); } diff --git a/client/windows/flutter/generated_plugins.cmake b/client/windows/flutter/generated_plugins.cmake index c04e69f80e..1aab211945 100644 --- a/client/windows/flutter/generated_plugins.cmake +++ b/client/windows/flutter/generated_plugins.cmake @@ -19,7 +19,6 @@ list(APPEND FLUTTER_PLUGIN_LIST share_plus url_launcher_windows window_manager - window_to_front ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/flet/lib/src/utils/desktop.dart b/packages/flet/lib/src/utils/desktop.dart index 34d44b696b..fba99767c2 100644 --- a/packages/flet/lib/src/utils/desktop.dart +++ b/packages/flet/lib/src/utils/desktop.dart @@ -1,7 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:window_to_front/window_to_front.dart'; import '../models/window_state.dart'; import 'platform.dart'; @@ -243,7 +242,9 @@ Future focusWindow() async { Future windowToFront() async { if (isDesktopPlatform()) { - await WindowToFront.activate(); + // Bring the window to the front, activating the app over other apps + // (window_manager.show() calls NSApp.activate(ignoringOtherApps: true)). + await windowManager.show(); } } diff --git a/packages/flet/pubspec.yaml b/packages/flet/pubspec.yaml index 51967a08a4..41ca2cded8 100644 --- a/packages/flet/pubspec.yaml +++ b/packages/flet/pubspec.yaml @@ -52,7 +52,6 @@ dependencies: web: ^1.1.1 web_socket_channel: ^3.0.2 window_manager: ^0.5.1 - window_to_front: ^0.0.3 wakelock_plus: ^1.4.0 dev_dependencies: diff --git a/sdk/python/.pre-commit-config.yaml b/sdk/python/.pre-commit-config.yaml index 9a0bd650d0..cb79e5c469 100644 --- a/sdk/python/.pre-commit-config.yaml +++ b/sdk/python/.pre-commit-config.yaml @@ -28,4 +28,6 @@ repos: hooks: - id: typos args: [ --config, sdk/python/_typos.toml ] - exclude: /templates/ + # Generated *.pbxproj files contain random hex UUIDs that typos + # misreads as misspelled words. + exclude: (/templates/|\.pbxproj$) diff --git a/sdk/python/_typos.toml b/sdk/python/_typos.toml index b503150fa2..8092cfbc0d 100644 --- a/sdk/python/_typos.toml +++ b/sdk/python/_typos.toml @@ -1,3 +1,8 @@ +[files] +# Generated Xcode project files contain random hex UUIDs that typos +# misreads as misspelled words. +extend-exclude = ["*.pbxproj"] + [default.extend-words] # AAC High Efficiency audio codec enum member name AACHE = "AACHE" diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py index ac7709776a..ddf7771c1c 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py @@ -1,4 +1,5 @@ import argparse +import contextlib import copy import glob import os @@ -9,6 +10,7 @@ import yaml from packaging.requirements import Requirement +from packaging.utils import canonicalize_name from rich.panel import Panel from rich.table import Column, Table @@ -486,6 +488,18 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: help="Pre-compile site packages' `.py` files to `.pyc` (on by default; " "use --no-compile-packages to disable)", ) + parser.add_argument( + "--swift-package-manager", + dest="swift_package_manager", + action=argparse.BooleanOptionalAction, + default=None, + help="Integrate the embedded Python runtime via Swift Package Manager " + "(default) or CocoaPods for iOS/macOS builds. On by default; Flet " + "automatically falls back to CocoaPods when the app uses `flet-video` " + "(media_kit, not yet SPM-compatible). Use --no-swift-package-manager " + "(or `swift_package_manager = false` under [tool.flet]) if your app " + "uses other packages that don't support SPM.", + ) parser.add_argument( "--cleanup-app", dest="cleanup_app", @@ -1507,10 +1521,15 @@ def register_flutter_extensions(self): assert self.template_data assert self.build_dir + # Replace the permanent flutter-packages copy with this build's set. The + # temp dir is populated by serious_python's package step and is ABSENT + # when the app has no Flutter extensions — so always clear the old copy + # first, otherwise an extension removed since the previous build (e.g. + # dropping flet-video) would linger here and stay in the built app. + if self.flutter_packages_dir.exists(): + shutil.rmtree(self.flutter_packages_dir, ignore_errors=True) if self.flutter_packages_temp_dir.exists(): # copy packages from temp to permanent location - if self.flutter_packages_dir.exists(): - shutil.rmtree(self.flutter_packages_dir, ignore_errors=True) shutil.move(self.flutter_packages_temp_dir, self.flutter_packages_dir) if self.flutter_packages_dir.exists(): @@ -1972,6 +1991,45 @@ def fallback_image(self, pubspec, yaml_path: str, images: list, images_dir: str) d[pp[-1]] = f"{images_dir}/{image}" return + # Flet packages that don't yet support Swift Package Manager. If an app uses + # one, Flutter builds the whole iOS/macOS app with CocoaPods, so serious_python + # must stage for CocoaPods too. Names are canonicalized for comparison. + _NON_SPM_PLUGINS = frozenset({canonicalize_name("flet-video")}) + + def _references_non_spm_plugins(self, toml_dependencies, requirements_txt): + """Whether the app depends on any package that forces a CocoaPods build.""" + names = set() + for dep in toml_dependencies or []: + with contextlib.suppress(Exception): + names.add(canonicalize_name(Requirement(dep).name)) + if requirements_txt.exists(): + for line in requirements_txt.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith(("#", "-")): + continue + with contextlib.suppress(Exception): + names.add(canonicalize_name(Requirement(line).name)) + return bool(names & self._NON_SPM_PLUGINS) + + def _darwin_spm_active(self) -> bool: + """Whether to stage serious_python for Swift Package Manager (vs CocoaPods). + + On by default. Falls back to CocoaPods when the app uses a package that + isn't SPM-ready (e.g. `flet-video`/media_kit) — Flutter then builds the + whole app with CocoaPods, so serious_python must stage to match. A user + can also force CocoaPods with `--no-swift-package-manager` (or + `swift_package_manager = false` under `[tool.flet]`) when they use other + non-SPM packages. Flet does not change Flutter's global SPM configuration — + only which way it stages the Python runtime. + """ + if self.package_platform not in ("iOS", "Darwin"): + return False + if not self.get_bool_setting( + self.options.swift_package_manager, "swift_package_manager", True + ): + return False + return not getattr(self, "_app_uses_non_spm_plugin", False) + def package_python_app(self): """ Package Python app and dependencies into Flutter-consumable app archive. @@ -2036,6 +2094,14 @@ def package_python_app(self): if platform_dependencies: toml_dependencies.extend(platform_dependencies) + # Detect packages that don't support Swift Package Manager and so force + # Flutter to build the whole iOS/macOS app with CocoaPods (currently only + # `flet-video`, which bundles media_kit). serious_python then has to stage + # for CocoaPods to match — see `_darwin_spm_active`. + self._app_uses_non_spm_plugin = self._references_non_spm_plugins( + toml_dependencies, requirements_txt + ) + dev_packages_configured = False if len(toml_dependencies) > 0: dev_packages = ( @@ -2093,6 +2159,19 @@ def package_python_app(self): # it into the bundle (Android zips it as a stored asset). package_env["SERIOUS_PYTHON_APP"] = str(self.build_dir / "python-app") + # Swift Package Manager (darwin): tell serious_python's package command to + # do the host-side SPM staging (the podspec prepare_command doesn't run + # under SPM) and write the SP_NATIVE_SET cache-bust key to this file. + # serious_python defaults to SPM staging, so be explicit either way — set + # it false for the CocoaPods cases (e.g. an app using flet-video). + if self.package_platform in ("iOS", "Darwin"): + spm = self._darwin_spm_active() + package_env["SERIOUS_PYTHON_DARWIN_SPM"] = "true" if spm else "false" + if spm: + package_env["SERIOUS_PYTHON_SPM_KEY_FILE"] = str( + self.build_dir / ".serious_python_spm_key" + ) + # flutter-packages variable if self.flutter_packages_temp_dir.exists(): shutil.rmtree(self.flutter_packages_temp_dir) @@ -2354,6 +2433,14 @@ def _run_flutter_command(self): # unpacked app into the bundle. build_env["SERIOUS_PYTHON_APP"] = str(self.build_dir / "python-app") + # Swift Package Manager (darwin): export the cache-bust key the package + # step computed so the plugin's Package.swift re-resolves when the staged + # native set changes (SwiftPM caches its graph on manifest text + env). + if self._darwin_spm_active(): + spm_key_file = self.build_dir / ".serious_python_spm_key" + if spm_key_file.exists(): + build_env["SP_NATIVE_SET"] = spm_key_file.read_text().strip() + # Path-hungry packages to ship extracted to disk: consumed by the # serious_python_android Gradle split during `flutter build`. if self.package_platform == "Android" and self.android_extract_packages: diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml b/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml index 055be6bdfa..87f93b193c 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml @@ -17,12 +17,12 @@ dependencies: flet: path: ../../../../../packages/flet - # Track the in-development serious_python (4.0.0) from the app-package branch + # Track the in-development serious_python (4.0.0) from the main branch # until it's published to pub.dev. Pinned by the lifted app-packaging work. serious_python: git: url: https://github.com/flet-dev/serious-python.git - ref: app-package + ref: main path: src/serious_python # MsgPack codec used by the dart_bridge FletBackendChannel implementation @@ -43,6 +43,7 @@ dependencies: dependency_overrides: flet: path: ../../../../../packages/flet + screen_retriever: 0.2.1 # this one migrated to SPM, but 0.2.0 is used by some other packages dev_dependencies: flutter_launcher_icons: ^0.14.1 From 681f8c6846ac1ae6227b40f396b6623da7929d01 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Sun, 21 Jun 2026 16:02:06 -0700 Subject: [PATCH 6/8] flet build: always stage serious_python for SPM (drop flet-video CocoaPods fallback) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flet-video auto-fallback assumed that an app with a non-SPM plugin builds entirely with CocoaPods, so serious_python was staged for CocoaPods. That's wrong for Flutter 3.44+ (SPM on by default): Flutter builds hybrid — plugins with a Package.swift (incl. serious_python_darwin) use SPM while non-SPM plugins (media_kit/flet-video, torch_light) use CocoaPods in the same build. Staging serious_python for CocoaPods while Flutter linked it via its Package.swift left dart_bridge unlinked -> 'Undefined symbol: _DartBridge_*/_serious_python_run' on macOS and iOS. Always stage for SPM (Flutter's default); other non-SPM plugins ride CocoaPods via Flutter's hybrid mode independently. Keep --no-swift-package-manager / swift_package_manager=false only for when SPM is disabled in Flutter itself. Removes _references_non_spm_plugins/_NON_SPM_PLUGINS/_app_uses_non_spm_plugin (and the now-unused canonicalize_name/contextlib imports). --- client/pubspec.lock | 8 +-- .../src/flet_cli/commands/build_base.py | 62 +++++-------------- .../src/flutter/flet_flashlight/pubspec.yaml | 2 +- 3 files changed, 21 insertions(+), 51 deletions(-) diff --git a/client/pubspec.lock b/client/pubspec.lock index ebaf398bf7..54e7ce9773 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -1675,10 +1675,10 @@ packages: dependency: transitive description: name: torch_light - sha256: a1397443a375c6991151547cb77361085df6cf8aa59999292e683db7385a0d15 + sha256: "6a2659cf413d11b838540d15f2a18ead0e7736250cecdacb153cff654beb5c4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "2.0.0" tuple: dependency: transitive description: @@ -1984,5 +1984,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.11.0 <4.0.0" - flutter: ">=3.41.0" + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py index ddf7771c1c..2bc6d2ebd9 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py @@ -1,5 +1,4 @@ import argparse -import contextlib import copy import glob import os @@ -10,7 +9,6 @@ import yaml from packaging.requirements import Requirement -from packaging.utils import canonicalize_name from rich.panel import Panel from rich.table import Column, Table @@ -494,11 +492,11 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: action=argparse.BooleanOptionalAction, default=None, help="Integrate the embedded Python runtime via Swift Package Manager " - "(default) or CocoaPods for iOS/macOS builds. On by default; Flet " - "automatically falls back to CocoaPods when the app uses `flet-video` " - "(media_kit, not yet SPM-compatible). Use --no-swift-package-manager " - "(or `swift_package_manager = false` under [tool.flet]) if your app " - "uses other packages that don't support SPM.", + "(default) or CocoaPods for iOS/macOS builds. On by default, matching " + "Flutter 3.44+ which uses SPM by default (other non-SPM plugins still " + "build with CocoaPods alongside it). Use --no-swift-package-manager (or " + "`swift_package_manager = false` under [tool.flet]) only if you've " + "disabled Swift Package Manager in Flutter.", ) parser.add_argument( "--cleanup-app", @@ -1991,44 +1989,24 @@ def fallback_image(self, pubspec, yaml_path: str, images: list, images_dir: str) d[pp[-1]] = f"{images_dir}/{image}" return - # Flet packages that don't yet support Swift Package Manager. If an app uses - # one, Flutter builds the whole iOS/macOS app with CocoaPods, so serious_python - # must stage for CocoaPods too. Names are canonicalized for comparison. - _NON_SPM_PLUGINS = frozenset({canonicalize_name("flet-video")}) - - def _references_non_spm_plugins(self, toml_dependencies, requirements_txt): - """Whether the app depends on any package that forces a CocoaPods build.""" - names = set() - for dep in toml_dependencies or []: - with contextlib.suppress(Exception): - names.add(canonicalize_name(Requirement(dep).name)) - if requirements_txt.exists(): - for line in requirements_txt.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not line or line.startswith(("#", "-")): - continue - with contextlib.suppress(Exception): - names.add(canonicalize_name(Requirement(line).name)) - return bool(names & self._NON_SPM_PLUGINS) - def _darwin_spm_active(self) -> bool: """Whether to stage serious_python for Swift Package Manager (vs CocoaPods). - On by default. Falls back to CocoaPods when the app uses a package that - isn't SPM-ready (e.g. `flet-video`/media_kit) — Flutter then builds the - whole app with CocoaPods, so serious_python must stage to match. A user - can also force CocoaPods with `--no-swift-package-manager` (or - `swift_package_manager = false` under `[tool.flet]`) when they use other - non-SPM packages. Flet does not change Flutter's global SPM configuration — - only which way it stages the Python runtime. + On by default, matching Flutter 3.44+ (SPM enabled by default). Because + `serious_python_darwin` ships a `Package.swift`, Flutter always builds it + as an SPM plugin when SPM is enabled — even in a hybrid app where other, + non-SPM plugins (e.g. `flet-video`/media_kit) build with CocoaPods at the + same time. So serious_python must stage for SPM to match; it is NOT tied + to whether the app also pulls in non-SPM plugins. Users force CocoaPods + with `--no-swift-package-manager` (or `swift_package_manager = false` under + `[tool.flet]`) only when they've disabled SPM in Flutter itself. Flet does + not change Flutter's global SPM configuration. """ if self.package_platform not in ("iOS", "Darwin"): return False - if not self.get_bool_setting( + return self.get_bool_setting( self.options.swift_package_manager, "swift_package_manager", True - ): - return False - return not getattr(self, "_app_uses_non_spm_plugin", False) + ) def package_python_app(self): """ @@ -2094,14 +2072,6 @@ def package_python_app(self): if platform_dependencies: toml_dependencies.extend(platform_dependencies) - # Detect packages that don't support Swift Package Manager and so force - # Flutter to build the whole iOS/macOS app with CocoaPods (currently only - # `flet-video`, which bundles media_kit). serious_python then has to stage - # for CocoaPods to match — see `_darwin_spm_active`. - self._app_uses_non_spm_plugin = self._references_non_spm_plugins( - toml_dependencies, requirements_txt - ) - dev_packages_configured = False if len(toml_dependencies) > 0: dev_packages = ( diff --git a/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/pubspec.yaml b/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/pubspec.yaml index 1ca7a12bdf..83045edaf0 100644 --- a/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/pubspec.yaml +++ b/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: sdk: flutter collection: ^1.16.0 - torch_light: 1.1.0 + torch_light: 2.0.0 flet: path: ../../../../../../../packages/flet From 3fed154a91da8b60d98d861329aacf341c8141d6 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Sun, 21 Jun 2026 19:32:22 -0700 Subject: [PATCH 7/8] Add example deps and modernize typing imports Add flet-code-editor and flet-color-pickers to the flet_build_test example (pyproject and example imports) and register flet_spinkit import in main.py. Modernize typing usage across packages: add conditional typing-extensions dependency for flet-webview, use typing.Self on Python >=3.11 with a fallback to typing_extensions.Self, and simplify/remove try/except fallbacks by importing Protocol, TypeVar and ParamSpec directly from typing. These changes simplify type annotations and ensure compatibility with older Python via typing-extensions. --- sdk/python/examples/apps/flet_build_test/pyproject.toml | 6 ++++++ sdk/python/examples/apps/flet_build_test/src/main.py | 3 +++ sdk/python/packages/flet-webview/pyproject.toml | 1 + .../packages/flet-webview/src/flet_webview/webview.py | 6 +++++- .../packages/flet/src/flet/components/hooks/use_context.py | 7 +------ sdk/python/packages/flet/src/flet/controls/page.py | 7 +------ 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/sdk/python/examples/apps/flet_build_test/pyproject.toml b/sdk/python/examples/apps/flet_build_test/pyproject.toml index 256622f74b..9ba03599b6 100644 --- a/sdk/python/examples/apps/flet_build_test/pyproject.toml +++ b/sdk/python/examples/apps/flet_build_test/pyproject.toml @@ -14,6 +14,8 @@ dependencies = [ "flet-audio-recorder", "flet-camera", "flet-charts", + "flet-code-editor", + "flet-color-pickers", "flet-datatable2", "flet-flashlight", "flet-geolocator", @@ -44,6 +46,8 @@ flet-audio = { path = "../../../packages/flet-audio", editable = true } flet-audio-recorder = { path = "../../../packages/flet-audio-recorder", editable = true } flet-camera = { path = "../../../packages/flet-camera", editable = true } flet-charts = { path = "../../../packages/flet-charts", editable = true } +flet-code-editor = { path = "../../../packages/flet-code-editor", editable = true } +flet-color-pickers = { path = "../../../packages/flet-color-pickers", editable = true } flet-datatable2 = { path = "../../../packages/flet-datatable2", editable = true } flet-flashlight = { path = "../../../packages/flet-flashlight", editable = true } flet-geolocator = { path = "../../../packages/flet-geolocator", editable = true } @@ -63,6 +67,8 @@ flet-audio = "../../../packages/flet-audio" flet-audio-recorder = "../../../packages/flet-audio-recorder" flet-camera = "../../../packages/flet-camera" flet-charts = "../../../packages/flet-charts" +flet-code-editor = "../../../packages/flet-code-editor" +flet-color-pickers = "../../../packages/flet-color-pickers" flet-datatable2 = "../../../packages/flet-datatable2" flet-flashlight = "../../../packages/flet-flashlight" flet-geolocator = "../../../packages/flet-geolocator" diff --git a/sdk/python/examples/apps/flet_build_test/src/main.py b/sdk/python/examples/apps/flet_build_test/src/main.py index 5eb53b1e05..63c35edd28 100644 --- a/sdk/python/examples/apps/flet_build_test/src/main.py +++ b/sdk/python/examples/apps/flet_build_test/src/main.py @@ -1,5 +1,7 @@ import sys +import flet_code_editor # noqa: F401 +import flet_spinkit # noqa: F401 import numpy import PIL from modules.utils import greet @@ -10,6 +12,7 @@ import flet_audio # noqa: F401 import flet_audio_recorder # noqa: F401 import flet_charts # noqa: F401 +import flet_color_pickers # noqa: F401 import flet_datatable2 # noqa: F401 import flet_flashlight # noqa: F401 import flet_geolocator # noqa: F401 diff --git a/sdk/python/packages/flet-webview/pyproject.toml b/sdk/python/packages/flet-webview/pyproject.toml index e9b72025a1..4f0d34a9ad 100644 --- a/sdk/python/packages/flet-webview/pyproject.toml +++ b/sdk/python/packages/flet-webview/pyproject.toml @@ -8,6 +8,7 @@ license = "Apache-2.0" requires-python = ">=3.10" dependencies = [ "flet", + "typing-extensions; python_version < '3.11'", ] [project.urls] diff --git a/sdk/python/packages/flet-webview/src/flet_webview/webview.py b/sdk/python/packages/flet-webview/src/flet_webview/webview.py index 91da17d8df..c35fbbda3e 100644 --- a/sdk/python/packages/flet-webview/src/flet_webview/webview.py +++ b/sdk/python/packages/flet-webview/src/flet_webview/webview.py @@ -1,6 +1,10 @@ +import sys from typing import Optional -from typing_extensions import Self +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self import flet as ft from flet_webview.types import ( diff --git a/sdk/python/packages/flet/src/flet/components/hooks/use_context.py b/sdk/python/packages/flet/src/flet/components/hooks/use_context.py index db1830e342..bc6a1449b3 100644 --- a/sdk/python/packages/flet/src/flet/components/hooks/use_context.py +++ b/sdk/python/packages/flet/src/flet/components/hooks/use_context.py @@ -1,11 +1,6 @@ from collections.abc import Callable from dataclasses import dataclass -from typing import TypeVar, cast - -try: - from typing import Protocol -except ImportError: - from typing_extensions import Protocol +from typing import Protocol, TypeVar, cast from flet.components.hooks.hook import Hook from flet.components.observable import Observable diff --git a/sdk/python/packages/flet/src/flet/controls/page.py b/sdk/python/packages/flet/src/flet/controls/page.py index 977ec72f04..fa813abf1c 100644 --- a/sdk/python/packages/flet/src/flet/controls/page.py +++ b/sdk/python/packages/flet/src/flet/controls/page.py @@ -13,6 +13,7 @@ Any, Callable, Optional, + ParamSpec, TypeVar, Union, ) @@ -83,12 +84,6 @@ def _default_authorization_impl() -> "type[Authorization]": return Authorization -try: - from typing import ParamSpec -except ImportError: - from typing_extensions import ParamSpec - - logger = logging.getLogger("flet") From ab3640b5b74b26311845fbc27bb576a3b3c2a51b Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 22 Jun 2026 11:45:54 -0700 Subject: [PATCH 8/8] Pin serious_python to 4.0.0 Commented out the Shizuku Android provider entries in sdk/python/examples/apps/flet_build_test/pyproject.toml to disable that provider by default. Replaced the git-based serious_python dependency in sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml with a pinned version (4.0.0) to stop tracking the main branch and use the published package. --- sdk/python/examples/apps/flet_build_test/pyproject.toml | 4 ++-- .../build/{{cookiecutter.out_dir}}/pubspec.yaml | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/sdk/python/examples/apps/flet_build_test/pyproject.toml b/sdk/python/examples/apps/flet_build_test/pyproject.toml index 9ba03599b6..ffdda16f17 100644 --- a/sdk/python/examples/apps/flet_build_test/pyproject.toml +++ b/sdk/python/examples/apps/flet_build_test/pyproject.toml @@ -103,8 +103,8 @@ path = "src" [tool.flet.android.meta_data] "com.google.android.gms.ads.APPLICATION_ID" = "ca-app-pub-3940256099942544~3347511713" -[tool.flet.android.provider] -"rikka.shizuku.ShizukuProvider" = { authorities = "${applicationId}.shizuku", multiprocess = "false", enabled = "true", exported = "true", permission = "android.permission.INTERACT_ACROSS_USERS_FULL" } +# [tool.flet.android.provider] +# "rikka.shizuku.ShizukuProvider" = { authorities = "${applicationId}.shizuku", multiprocess = "false", enabled = "true", exported = "true", permission = "android.permission.INTERACT_ACROSS_USERS_FULL" } [tool.flet.ios.info] GADApplicationIdentifier = "ca-app-pub-3940256099942544~3347511713" diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml b/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml index 87f93b193c..441db56117 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml @@ -17,13 +17,8 @@ dependencies: flet: path: ../../../../../packages/flet - # Track the in-development serious_python (4.0.0) from the main branch - # until it's published to pub.dev. Pinned by the lifted app-packaging work. - serious_python: - git: - url: https://github.com/flet-dev/serious-python.git - ref: main - path: src/serious_python + + serious_python: 4.0.0 # MsgPack codec used by the dart_bridge FletBackendChannel implementation # in lib/main.dart — matches the wire format flet's existing socket