diff --git a/.fvmrc b/.fvmrc index 214e6584ee..1670fb70a3 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.35.1" + "flutter": "3.35.5" } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5e67c8eb8..596c5a358c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "3.10", "3.11", "3.12", "3.13" ] + python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] steps: - name: Checkout repository uses: actions/checkout@v4 @@ -547,6 +547,90 @@ jobs: sdk/python/dist/*.whl sdk/python/dist/*.tar.gz + # ============================================ + # Build Flet extension Python packages + # ============================================ + build_flet_extensions: + name: Build ${{ matrix.package }} extension + runs-on: ubuntu-latest + needs: + - python_tests + - build_flet_package + env: + PYPI_VER: ${{ needs.build_flet_package.outputs.PYPI_VER }} + strategy: + fail-fast: false + matrix: + package: + - flet-ads + - flet-audio + - flet-audio-recorder + - flet-charts + - flet-datatable2 + - flet-flashlight + - flet-geolocator + - flet-lottie + - flet-map + - flet-permission-handler + - flet-rive + - flet-video + - flet-webview + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup uv + uses: astral-sh/setup-uv@v6 + + - name: Setup Flutter + uses: kuhnroyal/flutter-fvm-config-action/setup@v3 + with: + path: '.fvmrc' + cache: true + + - name: Analyze Flutter package with dart analyze + shell: bash + run: | + set -euo pipefail + PACKAGE="${{ matrix.package }}" + FLUTTER_PACKAGE="${PACKAGE//-/_}" + FLUTTER_DIR="${SDK_PYTHON}/packages/${PACKAGE}/src/flutter/${FLUTTER_PACKAGE}" + + if [[ ! -d "$FLUTTER_DIR" ]]; then + echo "Flutter directory $FLUTTER_DIR not found" + exit 1 + fi + + pushd "$FLUTTER_DIR" + flutter pub get + dart analyze + rm -f pubspec.lock + popd + + - name: Build Python package + shell: bash + working-directory: ${{ env.SDK_PYTHON }} + run: | + set -euo pipefail + PACKAGE="${{ matrix.package }}" + PYPROJECT="packages/${PACKAGE}/pyproject.toml" + + source "$SCRIPTS/common.sh" + patch_toml_versions "$PYPROJECT" "$PYPI_VER" + + rm -rf dist + uv build --package "$PACKAGE" --wheel + uv build --package "$PACKAGE" --sdist + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: python-extension-${{ matrix.package }} + if-no-files-found: error + path: | + sdk/python/dist/*.whl + sdk/python/dist/*.tar.gz + # ===================================== # Build flet, flet-cli and flet-desktop # ===================================== @@ -598,6 +682,7 @@ jobs: - build_macos - build_linux - build_web + - build_flet_extensions steps: - name: Setup uv uses: astral-sh/setup-uv@v6 @@ -613,7 +698,25 @@ jobs: # remove client to avoid glob conflicts with its contents rm -rf dist/client - for pkg in flet flet_cli flet_desktop flet_desktop_light flet_web; do + for pkg in \ + flet \ + flet_cli \ + flet_desktop \ + flet_desktop_light \ + flet_web \ + flet_ads \ + flet_audio \ + flet_audio_recorder \ + flet_charts \ + flet_datatable2 \ + flet_flashlight \ + flet_geolocator \ + flet_lottie \ + flet_map \ + flet_permission_handler \ + flet_rive \ + flet_video \ + flet_webview; do uv publish dist/**/${pkg}-* done diff --git a/README.md b/README.md index 1bf235936c..1855615bdd 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ Supported Python versions - - Build status + + Build status

@@ -51,7 +51,7 @@ with assets hosting and desktop clients. Flet UI is built with [Flutter](https://flutter.dev/), so your app looks professional and could be delivered to any platform. Flet simplifies the Flutter model by combining smaller "widgets" to ready-to-use "controls" -with an imperative programming model. +with an imperative programming model. ### πŸ“± Deliver to any device or platform diff --git a/client/pubspec.lock b/client/pubspec.lock index 17fbf16676..6f8197566b 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "3.6.1" + version: "4.0.7" args: dependency: transitive description: @@ -161,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + dart_polylabel2: + dependency: transitive + description: + name: dart_polylabel2 + sha256: "7eeab15ce72894e4bdba6a8765712231fc81be0bd95247de4ad9966abc57adc6" + url: "https://pub.dev" + source: hosted + version: "1.0.0" data_table_2: dependency: transitive description: @@ -193,22 +201,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.3" - dio: - dependency: transitive - description: - name: dio - sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 - url: "https://pub.dev" - source: hosted - version: "5.9.0" - dio_web_adapter: - dependency: transitive - description: - name: dio_web_adapter - sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" - url: "https://pub.dev" - source: hosted - version: "2.1.1" equatable: dependency: transitive description: @@ -261,10 +253,10 @@ packages: dependency: transitive description: name: fl_chart - sha256: "577aeac8ca414c25333334d7c4bb246775234c0e44b38b10a82b559dd4d764e7" + sha256: "7ca9a40f4eb85949190e54087be8b4d6ac09dc4c54238d782a34cf1f7c011de9" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" flet: dependency: "direct overridden" description: @@ -282,111 +274,87 @@ packages: flet_audio: dependency: "direct main" description: - path: "src/flutter/flet_audio" - ref: main - resolved-ref: "1e629a501a87b0c2c0a9956cf49552dc01efcbe5" - url: "https://github.com/flet-dev/flet-audio.git" - source: git - version: "0.2.0" + path: "../sdk/python/packages/flet-audio/src/flutter/flet_audio" + relative: true + source: path + version: "0.1.0" flet_audio_recorder: dependency: "direct main" description: - path: "src/flutter/flet_audio_recorder" - ref: main - resolved-ref: e29cd51dec917fdb8b9e03d7bf4a71cb2f75217b - url: "https://github.com/flet-dev/flet-audio-recorder.git" - source: git - version: "0.2.0" + path: "../sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder" + relative: true + source: path + version: "0.1.0" flet_charts: dependency: "direct main" description: - path: "src/flutter/flet_charts" - ref: main - resolved-ref: "6902aeb244d676cb85f14268126df9c4fe7af084" - url: "https://github.com/flet-dev/flet-charts.git" - source: git - version: "0.2.0" + path: "../sdk/python/packages/flet-charts/src/flutter/flet_charts" + relative: true + source: path + version: "0.1.0" flet_datatable2: dependency: "direct main" description: - path: "src/flutter/flet_datatable2" - ref: main - resolved-ref: b53fba432acf42e01505acba5898bc47d7d25797 - url: "https://github.com/flet-dev/flet-datatable2.git" - source: git + path: "../sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2" + relative: true + source: path version: "0.1.0" flet_flashlight: dependency: "direct main" description: - path: "src/flutter/flet_flashlight" - ref: main - resolved-ref: "0862f0324f4a0c1a405b2c1dc23e3857175b36cc" - url: "https://github.com/flet-dev/flet-flashlight.git" - source: git - version: "0.2.0" + path: "../sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight" + relative: true + source: path + version: "0.1.0" flet_geolocator: dependency: "direct main" description: - path: "src/flutter/flet_geolocator" - ref: main - resolved-ref: "8381f3605b09eec6cbf02c44abb8216aaedf22b2" - url: "https://github.com/flet-dev/flet-geolocator.git" - source: git - version: "0.25.2" + path: "../sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator" + relative: true + source: path + version: "0.1.0" flet_lottie: dependency: "direct main" description: - path: "src/flutter/flet_lottie" - ref: main - resolved-ref: c9e8db9bab3ae0054de4e7b3fcb3fe282f6d8ee6 - url: "https://github.com/flet-dev/flet-lottie.git" - source: git - version: "0.2.0" + path: "../sdk/python/packages/flet-lottie/src/flutter/flet_lottie" + relative: true + source: path + version: "0.1.0" flet_map: dependency: "direct main" description: - path: "src/flutter/flet_map" - ref: main - resolved-ref: "3575ca7dc80251e6b0ac7648ff2620adb84db412" - url: "https://github.com/flet-dev/flet-map.git" - source: git - version: "0.2.0" + path: "../sdk/python/packages/flet-map/src/flutter/flet_map" + relative: true + source: path + version: "0.1.0" flet_permission_handler: dependency: "direct main" description: - path: "src/flutter/flet_permission_handler" - ref: main - resolved-ref: "6c9ac6c2c5608e00603c542a13e545ffba637398" - url: "https://github.com/flet-dev/flet-permission-handler.git" - source: git - version: "0.2.0" + path: "../sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler" + relative: true + source: path + version: "0.1.0" flet_rive: dependency: "direct main" description: - path: "src/flutter/flet_rive" - ref: main - resolved-ref: "6c9164f842e2d2b7ef27f09ab3bec163b63f6ac8" - url: "https://github.com/flet-dev/flet-rive.git" - source: git - version: "0.2.0" + path: "../sdk/python/packages/flet-rive/src/flutter/flet_rive" + relative: true + source: path + version: "0.1.0" flet_video: dependency: "direct main" description: - path: "src/flutter/flet_video" - ref: main - resolved-ref: "9292cab1b40ce5ef81e9012421c05ce03d910133" - url: "https://github.com/flet-dev/flet-video.git" - source: git - version: "0.2.0" + path: "../sdk/python/packages/flet-video/src/flutter/flet_video" + relative: true + source: path + version: "0.1.0" flet_webview: dependency: "direct main" description: - path: "src/flutter/flet_webview" - ref: main - resolved-ref: "4ae806ad9019a9d3883d1c0c88e30ba61aacb696" - url: "https://github.com/flet-dev/flet-webview.git" - source: git - version: "0.2.0" + path: "../sdk/python/packages/flet-webview/src/flutter/flet_webview" + relative: true + source: path + version: "0.1.0" flutter: dependency: "direct main" description: flutter @@ -409,10 +377,10 @@ packages: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" url: "https://pub.dev" source: hosted - version: "0.13.1" + version: "0.14.4" flutter_lints: dependency: "direct dev" description: @@ -430,10 +398,10 @@ packages: dependency: transitive description: name: flutter_map - sha256: f7d0379477274f323c3f3bc12d369a2b42eb86d1e7bd2970ae1ea3cff782449a + sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8" url: "https://pub.dev" source: hosted - version: "8.1.1" + version: "8.2.2" flutter_map_animations: dependency: transitive description: @@ -442,14 +410,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.0" - flutter_map_cancellable_tile_provider: - dependency: transitive - description: - name: flutter_map_cancellable_tile_provider - sha256: "582df422f65c68216fcbc8f2d2c335ff3e8a014d9815382cfdde23ef772b4fb0" - url: "https://pub.dev" - source: hosted - version: "3.1.1" flutter_markdown: dependency: transitive description: @@ -462,18 +422,18 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab" + sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476 url: "https://pub.dev" source: hosted - version: "2.0.29" + version: "2.0.31" flutter_svg: dependency: transitive description: name: flutter_svg - sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1 + sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.1" flutter_test: dependency: "direct dev" description: flutter @@ -589,10 +549,10 @@ packages: dependency: transitive description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" http_parser: dependency: transitive description: @@ -605,10 +565,10 @@ packages: dependency: transitive description: name: image - sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.5.4" integration_test: dependency: "direct main" description: flutter @@ -642,10 +602,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "11.0.1" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: @@ -682,10 +642,10 @@ packages: dependency: transitive description: name: logger - sha256: "55d6c23a6c15db14920e037fe7e0dc32e7cdaf3b64b4b25df2d541b5b6b81c0c" + sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.2" logging: dependency: transitive description: @@ -698,10 +658,10 @@ packages: dependency: transitive description: name: lottie - sha256: "377d87b8dcef640c04717e93afb86a510f0e1117a399ab94dc4b3f39c85eaa87" + sha256: "8ae0be46dbd9e19641791dc12ee480d34e1fd3f84c749adc05f3ad9342b71b95" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.3.2" markdown: dependency: transitive description: @@ -730,18 +690,18 @@ packages: dependency: transitive description: name: media_kit - sha256: "48c10c3785df5d88f0eef970743f8c99b2e5da2b34b9d8f9876e598f62d9e776" + sha256: "52a8e989babc431db0aa242f32a4a08e55f60662477ea09759a105d7cd6410da" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" media_kit_libs_android_video: dependency: transitive description: name: media_kit_libs_android_video - sha256: adff9b571b8ead0867f9f91070f8df39562078c0eb3371d88b9029a2d547d7b7 + sha256: "3f6274e5ab2de512c286a25c327288601ee445ed8ac319e0ef0b66148bd8f76c" url: "https://pub.dev" source: hosted - version: "1.3.7" + version: "1.3.8" media_kit_libs_ios_video: dependency: transitive description: @@ -770,10 +730,10 @@ packages: dependency: transitive description: name: media_kit_libs_video - sha256: "958cc55e7065d9d01f52a2842dab2a0812a92add18489f1006d864fb5e42a3ef" + sha256: "2b235b5dac79c6020e01eef5022c6cc85fedc0df1738aadc6ea489daa12a92a9" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.7" media_kit_libs_windows_video: dependency: transitive description: @@ -786,10 +746,10 @@ packages: dependency: transitive description: name: media_kit_video - sha256: a656a9463298c1adc64c57f2d012874f7f2900f0c614d9545a3e7b8bb9e2137b + sha256: "813858c3fe84eb46679eb698695f60665e2bfbef757766fac4d2e683f926e15a" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" meta: dependency: transitive description: @@ -806,14 +766,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" msgpack_dart: dependency: transitive description: @@ -874,10 +826,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37" url: "https://pub.dev" source: hosted - version: "2.2.17" + version: "2.2.19" path_provider_foundation: dependency: transitive description: @@ -962,10 +914,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "7.0.1" platform: dependency: transitive description: @@ -982,14 +934,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - polylabel: + posix: dependency: transitive description: - name: polylabel - sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b" + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "6.0.3" process: dependency: transitive description: @@ -1018,26 +970,26 @@ packages: dependency: transitive description: name: record - sha256: daeb3f9b3fea9797094433fe6e49a879d8e4ca4207740bc6dc7e4a58764f0817 + sha256: "6bad72fb3ea6708d724cf8b6c97c4e236cf9f43a52259b654efeb6fd9b737f1f" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.2" record_android: dependency: transitive description: name: record_android - sha256: "854627cd78d8d66190377f98477eee06ca96ab7c9f2e662700daf33dbf7e6673" + sha256: f05677eeed074898327f890f232f9eb49cd99d1c20d0daaf22b5612f4b2301bb url: "https://pub.dev" source: hosted - version: "1.4.2" + version: "1.4.3" record_ios: dependency: transitive description: name: record_ios - sha256: "13e241ed9cbc220534a40ae6b66222e21288db364d96dd66fb762ebd3cb77c71" + sha256: "765b42ac1be019b1674ddd809b811fc721fe5a93f7bb1da7803f0d16772fd6d7" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.4" record_linux: dependency: transitive description: @@ -1050,10 +1002,10 @@ packages: dependency: transitive description: name: record_macos - sha256: "2849068bb59072f300ad63ed146e543d66afaef8263edba4de4834fc7c8d4d35" + sha256: "842ea4b7e95f4dd237aacffc686d1b0ff4277e3e5357865f8d28cd28bc18ed95" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" record_platform_interface: dependency: transitive description: @@ -1186,18 +1138,18 @@ packages: dependency: transitive description: name: shared_preferences - sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e" + sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713" url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.4.15" shared_preferences_foundation: dependency: transitive description: @@ -1359,18 +1311,18 @@ packages: dependency: transitive description: name: url_launcher - sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656" + sha256: "5c8b6c2d89a78f5a1cca70a73d9d5f86c701b36b42f9c9dac7bad592113c28e9" url: "https://pub.dev" source: hosted - version: "6.3.17" + version: "6.3.24" url_launcher_ios: dependency: transitive description: @@ -1447,10 +1399,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: ca81fdfaf62a5ab45d7296614aea108d2c7d0efca8393e96174bf4d51e6725b0 + sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc url: "https://pub.dev" source: hosted - version: "1.1.18" + version: "1.1.19" vector_math: dependency: transitive description: @@ -1535,10 +1487,10 @@ packages: dependency: transitive description: name: webview_flutter_android - sha256: "3c4eb4fcc252b40c2b5ce7be20d0481428b70f3ff589b0a8b8aaeb64c6bed701" + sha256: e5201c620eb2637dca88a756961fae4a7191bb30b4f2271e08b746405ffdf3fd url: "https://pub.dev" source: hosted - version: "4.10.2" + version: "4.10.5" webview_flutter_platform_interface: dependency: transitive description: @@ -1567,10 +1519,10 @@ packages: dependency: transitive description: name: win32 - sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e url: "https://pub.dev" source: hosted - version: "5.14.0" + version: "5.15.0" win32_registry: dependency: transitive description: @@ -1615,10 +1567,10 @@ packages: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.6.1" yaml: dependency: transitive description: diff --git a/client/pubspec.yaml b/client/pubspec.yaml index c1094c3fa5..ec66619c2c 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -37,74 +37,41 @@ dependencies: # --FAT_CLIENT_START-- flet_audio: - git: - url: https://github.com/flet-dev/flet-audio.git - ref: main - path: src/flutter/flet_audio + path: ../sdk/python/packages/flet-audio/src/flutter/flet_audio + flet_video: - git: - url: https://github.com/flet-dev/flet-video.git - ref: main - path: src/flutter/flet_video + path: ../sdk/python/packages/flet-video/src/flutter/flet_video # --FAT_CLIENT_END-- - flet_lottie: - git: - url: https://github.com/flet-dev/flet-lottie.git - ref: main - path: src/flutter/flet_lottie + flet_audio_recorder: + path: ../sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder - flet_map: - git: - url: https://github.com/flet-dev/flet-map.git - ref: main - path: src/flutter/flet_map + flet_datatable2: + path: ../sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2 - flet_rive: - git: - url: https://github.com/flet-dev/flet-rive.git - ref: main - path: src/flutter/flet_rive + flet_flashlight: + path: ../sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight - flet_audio_recorder: - git: - url: https://github.com/flet-dev/flet-audio-recorder.git - ref: main - path: src/flutter/flet_audio_recorder + flet_geolocator: + path: ../sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator + + flet_lottie: + path: ../sdk/python/packages/flet-lottie/src/flutter/flet_lottie + + flet_map: + path: ../sdk/python/packages/flet-map/src/flutter/flet_map flet_permission_handler: - git: - url: https://github.com/flet-dev/flet-permission-handler.git - ref: main - path: src/flutter/flet_permission_handler + path: ../sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler - flet_geolocator: - git: - url: https://github.com/flet-dev/flet-geolocator.git - ref: main - path: src/flutter/flet_geolocator + flet_rive: + path: ../sdk/python/packages/flet-rive/src/flutter/flet_rive flet_webview: - git: - url: https://github.com/flet-dev/flet-webview.git - ref: main - path: src/flutter/flet_webview - flet_flashlight: - git: - url: https://github.com/flet-dev/flet-flashlight.git - ref: main - path: src/flutter/flet_flashlight - flet_datatable2: - git: - url: https://github.com/flet-dev/flet-datatable2.git - ref: main - path: src/flutter/flet_datatable2 + path: ../sdk/python/packages/flet-webview/src/flutter/flet_webview flet_charts: - git: - url: https://github.com/flet-dev/flet-charts.git - ref: main - path: src/flutter/flet_charts + path: ../sdk/python/packages/flet-charts/src/flutter/flet_charts cupertino_icons: ^1.0.6 @@ -128,7 +95,7 @@ dev_dependencies: flutter_lints: ^1.0.0 # Docs: https://pub.dev/packages/flutter_launcher_icons - flutter_launcher_icons: "^0.13.1" + flutter_launcher_icons: "^0.14.4" # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/packages/flet/lib/src/controls/dropdownm2.dart b/packages/flet/lib/src/controls/dropdownm2.dart index a07e5dc6b7..718d41997a 100644 --- a/packages/flet/lib/src/controls/dropdownm2.dart +++ b/packages/flet/lib/src/controls/dropdownm2.dart @@ -70,11 +70,23 @@ class _DropdownM2ControlState extends State { var textStyle = widget.control .getTextStyle("text_style", Theme.of(context), const TextStyle())!; - if (textSize != null || color != null || focusedColor != null) { + + if (textSize != null) { + textStyle = textStyle.copyWith(fontSize: textSize); + } + + if (focusedColor != null) { textStyle = textStyle.copyWith( - fontSize: textSize, - color: (_focused ? (focusedColor ?? color) : color) ?? - Theme.of(context).colorScheme.onSurface); + color: _focused ? focusedColor : (color ?? textStyle.color)); + } + + if (color != null) { + textStyle = textStyle.copyWith(color: color); + } + + if (textStyle.color == null) { + textStyle = + textStyle.copyWith(color: Theme.of(context).colorScheme.onSurface); } var items = widget.control @@ -113,7 +125,7 @@ class _DropdownM2ControlState extends State { style: textStyle, autofocus: widget.control.getBool("autofocus", false)!, focusNode: _focusNode, - value: _value, + initialValue: _value, dropdownColor: widget.control.getColor("bgcolor", context), enableFeedback: widget.control.getBool("enable_feedback"), elevation: widget.control.getInt("elevation", 8)!, diff --git a/packages/flet/pubspec.yaml b/packages/flet/pubspec.yaml index 428a261f46..c18d34bcad 100644 --- a/packages/flet/pubspec.yaml +++ b/packages/flet/pubspec.yaml @@ -21,29 +21,28 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - - equatable: ^2.0.3 - provider: ^6.1.2 - web_socket_channel: ^3.0.2 - msgpack_dart: ^1.0.1 - window_manager: ^0.5.1 - window_to_front: ^0.0.3 collection: ^1.19.0 - flutter_svg: 2.1.0 - path: ^1.9.0 - url_launcher: 6.3.1 - http: 1.3.0 - shared_preferences: 2.5.2 device_info_plus: ^12.1.0 - flutter_markdown: 0.7.6+2 + equatable: ^2.0.3 + file_picker: ^10.3.3 flutter_highlight: 0.7.0 + flutter_markdown: 0.7.6+2 + flutter_svg: 2.2.1 + highlight: ^0.7.0 + http: 1.5.0 markdown: 7.3.0 - sensors_plus: ^6.1.1 - web: ^1.1.1 - file_picker: ^10.3.3 + msgpack_dart: ^1.0.1 + path: ^1.9.0 path_provider: ^2.1.5 + provider: ^6.1.2 screenshot: ^3.0.0 - highlight: ^0.7.0 + sensors_plus: ^6.1.1 + shared_preferences: 2.5.3 + url_launcher: 6.3.2 + web: ^1.1.1 + web_socket_channel: ^3.0.2 + window_manager: ^0.5.1 + window_to_front: ^0.0.3 dev_dependencies: flutter_test: diff --git a/sdk/python/examples/apps/declarative/counter_minimal.py b/sdk/python/examples/apps/declarative/counter_minimal.py new file mode 100644 index 0000000000..59129d8124 --- /dev/null +++ b/sdk/python/examples/apps/declarative/counter_minimal.py @@ -0,0 +1,16 @@ +import flet as ft + + +@ft.component +def App(): + count, set_count = ft.use_state(0) + + return ft.Row( + controls=[ + ft.Text(value=f"{count}"), + ft.Button("Add", on_click=lambda: set_count(count + 1)), + ], + ) + + +ft.run(lambda page: page.render(App)) diff --git a/sdk/python/examples/apps/declarative/todo.py b/sdk/python/examples/apps/declarative/todo.py index 5a18f00ad7..9dc8bebb99 100644 --- a/sdk/python/examples/apps/declarative/todo.py +++ b/sdk/python/examples/apps/declarative/todo.py @@ -1,6 +1,6 @@ import logging from dataclasses import dataclass, field -from typing import cast +from typing import Callable, cast import flet as ft @@ -15,12 +15,15 @@ class TaskItem: name: str completed: bool = False id: int = field(default_factory=TaskID) + on_status_changed: Callable | None = None def update_task(self, new_name: str): self.name = new_name def toggle_task_status(self): self.completed = not self.completed + if self.on_status_changed: + self.on_status_changed() @ft.observable @@ -49,8 +52,13 @@ def active_tasks_number(self) -> int: def status_changed(self, e: ft.Event[ft.Tabs]): self.status = self.statuses[e.control.selected_index] + def on_task_status_changed(self): + cast(ft.Observable, self).notify() + def add_task(self, new_task_event: str): - self.tasks.append(TaskItem(new_task_event)) + self.tasks.append( + TaskItem(new_task_event, on_status_changed=self.on_task_status_changed) + ) def delete_task(self, task: TaskItem): self.tasks.remove(task) diff --git a/sdk/python/examples/controls/audio/example_1.py b/sdk/python/examples/controls/audio/example_1.py new file mode 100644 index 0000000000..f4cb7e58c2 --- /dev/null +++ b/sdk/python/examples/controls/audio/example_1.py @@ -0,0 +1,73 @@ +import flet_audio as fta + +import flet as ft + + +def main(page: ft.Page): + url = "https://github.com/mdn/webaudio-examples/blob/main/audio-analyser/viper.mp3?raw=true" + + async def play(): + await audio.play() + + async def pause(): + await audio.pause() + + async def resume(): + await audio.resume() + + async def release(): + await audio.release() + + def set_volume(value: float): + audio.volume += value + + def set_balance(value: float): + audio.balance += value + + async def seek_2s(): + await audio.seek(ft.Duration(seconds=2)) + + async def get_duration(): + duration = await audio.get_duration() + print("Duration:", duration) + + async def on_get_current_position(): + position = await audio.get_current_position() + print("Current position:", position) + + audio = fta.Audio( + src=url, + autoplay=False, + volume=1, + balance=0, + on_loaded=lambda _: print("Loaded"), + on_duration_change=lambda e: print("Duration changed:", e.duration), + on_position_change=lambda e: print("Position changed:", e.position), + on_state_change=lambda e: print("State changed:", e.state), + on_seek_complete=lambda _: print("Seek complete"), + ) + + page.add( + ft.Button("Play", on_click=play), + ft.Button("Pause", on_click=pause), + ft.Button("Resume", on_click=resume), + ft.Button("Release", on_click=release), + ft.Button("Seek 2s", on_click=seek_2s), + ft.Row( + controls=[ + ft.Button("Volume down", on_click=lambda _: set_volume(-0.1)), + ft.Button("Volume up", on_click=lambda _: set_volume(0.1)), + ] + ), + ft.Row( + controls=[ + ft.Button("Balance left", on_click=lambda _: set_balance(-0.1)), + ft.Button("Balance right", on_click=lambda _: set_balance(0.1)), + ] + ), + ft.Button("Get duration", on_click=get_duration), + ft.Button("Get current position", on_click=on_get_current_position), + ) + + +ft.run(main) diff --git a/sdk/python/examples/controls/audio_recorder/example_1.py b/sdk/python/examples/controls/audio_recorder/example_1.py new file mode 100644 index 0000000000..b88cbd03d8 --- /dev/null +++ b/sdk/python/examples/controls/audio_recorder/example_1.py @@ -0,0 +1,72 @@ +import logging + +import flet_audio_recorder as far + +import flet as ft + +logging.basicConfig(level=logging.DEBUG) + + +def main(page: ft.Page): + page.horizontal_alignment = ft.CrossAxisAlignment.CENTER + page.appbar = ft.AppBar(title=ft.Text("Audio Recorder"), center_title=True) + + path = "test-audio-file.wav" + + def show_snackbar(message): + page.show_dialog(ft.SnackBar(ft.Text(message))) + + async def handle_recording_start(e: ft.Event[ft.Button]): + show_snackbar("Starting recording...") + await recorder.start_recording(output_path=path) + + async def handle_recording_stop(e: ft.Event[ft.Button]): + output_path = await recorder.stop_recording() + show_snackbar(f"Stopped recording. Output Path: {output_path}") + if page.web and output_path is not None: + await page.launch_url(output_path) + + async def handle_list_devices(e: ft.Event[ft.Button]): + o = await recorder.get_input_devices() + show_snackbar(f"Input Devices: {', '.join([f'{d.id}:{d.label}' for d in o])}") + + async def handle_has_permission(e: ft.Event[ft.Button]): + try: + status = await recorder.has_permission() + show_snackbar(f"Audio Recording Permission status: {status}") + except Exception as e: + show_snackbar(f"Error checking permission: {e}") + + async def handle_pause(e: ft.Event[ft.Button]): + print(f"isRecording: {await recorder.is_recording()}") + if await recorder.is_recording(): + await recorder.pause_recording() + + async def handle_resume(e: ft.Event[ft.Button]): + print(f"isPaused: {await recorder.is_paused()}") + if await recorder.is_paused(): + await recorder.resume_recording() + + async def handle_audio_encoder_test(e: ft.Event[ft.Button]): + print(await recorder.is_supported_encoder(far.AudioEncoder.WAV)) + + recorder = far.AudioRecorder( + configuration=far.AudioRecorderConfiguration(encoder=far.AudioEncoder.WAV), + on_state_change=lambda e: print(f"State Changed: {e.data}"), + ) + + page.add( + ft.Button(content="Start Audio Recorder", on_click=handle_recording_start), + ft.Button(content="Stop Audio Recorder", on_click=handle_recording_stop), + ft.Button(content="List Devices", on_click=handle_list_devices), + ft.Button(content="Pause Recording", on_click=handle_pause), + ft.Button(content="Resume Recording", on_click=handle_resume), + ft.Button(content="WAV Encoder Support", on_click=handle_audio_encoder_test), + ft.Button( + content="Get Audio Recording Permission Status", + on_click=handle_has_permission, + ), + ) + + +ft.run(main) diff --git a/sdk/python/examples/controls/charts/bar_chart/example_1.py b/sdk/python/examples/controls/charts/bar_chart/example_1.py new file mode 100644 index 0000000000..7fa3eb5802 --- /dev/null +++ b/sdk/python/examples/controls/charts/bar_chart/example_1.py @@ -0,0 +1,97 @@ +import flet_charts as fch + +import flet as ft + + +def main(page: ft.Page): + page.add( + fch.BarChart( + expand=True, + interactive=True, + max_y=110, + border=ft.Border.all(1, ft.Colors.GREY_400), + horizontal_grid_lines=fch.ChartGridLines( + color=ft.Colors.GREY_300, width=1, dash_pattern=[3, 3] + ), + tooltip=fch.BarChartTooltip( + bgcolor=ft.Colors.with_opacity(0.5, ft.Colors.GREY_300), + border_radius=ft.BorderRadius.all(20), + ), + left_axis=fch.ChartAxis( + label_size=40, title=ft.Text("Fruit supply"), title_size=40 + ), + right_axis=fch.ChartAxis(show_labels=False), + bottom_axis=fch.ChartAxis( + label_size=40, + labels=[ + fch.ChartAxisLabel( + value=0, label=ft.Container(ft.Text("Apple"), padding=10) + ), + fch.ChartAxisLabel( + value=1, label=ft.Container(ft.Text("Blueberry"), padding=10) + ), + fch.ChartAxisLabel( + value=2, label=ft.Container(ft.Text("Cherry"), padding=10) + ), + fch.ChartAxisLabel( + value=3, label=ft.Container(ft.Text("Orange"), padding=10) + ), + ], + ), + groups=[ + fch.BarChartGroup( + x=0, + rods=[ + fch.BarChartRod( + from_y=0, + to_y=40, + width=40, + color=ft.Colors.GREEN, + border_radius=0, + ), + ], + ), + fch.BarChartGroup( + x=1, + rods=[ + fch.BarChartRod( + from_y=0, + to_y=100, + width=40, + color=ft.Colors.BLUE, + tooltip=fch.BarChartRodTooltip("Blueberry"), + border_radius=0, + ), + ], + ), + fch.BarChartGroup( + x=2, + rods=[ + fch.BarChartRod( + from_y=0, + to_y=30, + width=40, + color=ft.Colors.RED, + border_radius=0, + ), + ], + ), + fch.BarChartGroup( + x=3, + rods=[ + fch.BarChartRod( + from_y=0, + to_y=60, + width=40, + color=ft.Colors.ORANGE, + tooltip=fch.BarChartRodTooltip("Orange"), + border_radius=0, + ), + ], + ), + ], + ) + ) + + +ft.run(main) diff --git a/sdk/python/examples/controls/charts/bar_chart/example_2.py b/sdk/python/examples/controls/charts/bar_chart/example_2.py new file mode 100644 index 0000000000..6acdc099b8 --- /dev/null +++ b/sdk/python/examples/controls/charts/bar_chart/example_2.py @@ -0,0 +1,73 @@ +import flet_charts as fch + +import flet as ft + + +class CustomRod(fch.BarChartRod): + def __init__(self, y: float, hovered: bool = False): + super().__init__() + self.hovered = hovered + self.y = y + self.width = 22 + self.color = ft.Colors.WHITE + self.bg_to_y = 20 + self.bg_color = ft.Colors.GREEN_300 + + def before_update(self): + super().before_update() + self.to_y = self.y + 0.5 if self.hovered else self.y + self.color = ft.Colors.YELLOW if self.hovered else ft.Colors.WHITE + self.border_side = ( + ft.BorderSide(width=1, color=ft.Colors.RED) + if self.hovered + else ft.BorderSide(width=1, color=ft.Colors.BLUE) + ) + + +def main(page: ft.Page): + def on_chart_event(e: fch.BarChartEvent): + if e.type == fch.ChartEventType.POINTER_HOVER: + for group_index, group in enumerate(chart.groups): + for rod_index, rod in enumerate(group.rods): + rod.hovered = ( + e.group_index == group_index and e.rod_index == rod_index + ) + chart.update() + + chart = fch.BarChart( + on_event=on_chart_event, + interactive=True, + groups=[ + fch.BarChartGroup(x=0, rods=[CustomRod(5)]), + fch.BarChartGroup(x=1, rods=[CustomRod(6.5)]), + fch.BarChartGroup(x=2, rods=[CustomRod(15)]), + fch.BarChartGroup(x=3, rods=[CustomRod(7.5)]), + fch.BarChartGroup(x=4, rods=[CustomRod(9)]), + fch.BarChartGroup(x=5, rods=[CustomRod(11.5)]), + fch.BarChartGroup(x=6, rods=[CustomRod(6)]), + ], + bottom_axis=fch.ChartAxis( + labels=[ + fch.ChartAxisLabel(value=0, label=ft.Text("M", color=ft.Colors.BLUE)), + fch.ChartAxisLabel(value=1, label=ft.Text("T", color=ft.Colors.YELLOW)), + fch.ChartAxisLabel(value=2, label=ft.Text("W", color=ft.Colors.BLUE)), + fch.ChartAxisLabel(value=3, label=ft.Text("T", color=ft.Colors.YELLOW)), + fch.ChartAxisLabel(value=4, label=ft.Text("F", color=ft.Colors.BLUE)), + fch.ChartAxisLabel(value=5, label=ft.Text("S", color=ft.Colors.YELLOW)), + fch.ChartAxisLabel(value=6, label=ft.Text("S", color=ft.Colors.BLUE)), + ], + ), + ) + + page.add( + ft.Container( + content=chart, + bgcolor=ft.Colors.GREEN_200, + padding=10, + border_radius=5, + expand=True, + ) + ) + + +ft.run(main) diff --git a/sdk/python/examples/controls/charts/bar_chart/media/example_1.png b/sdk/python/examples/controls/charts/bar_chart/media/example_1.png new file mode 100644 index 0000000000..f078a20f3c Binary files /dev/null and b/sdk/python/examples/controls/charts/bar_chart/media/example_1.png differ diff --git a/sdk/python/examples/controls/charts/bar_chart/media/example_2.gif b/sdk/python/examples/controls/charts/bar_chart/media/example_2.gif new file mode 100644 index 0000000000..e21fbc62ba Binary files /dev/null and b/sdk/python/examples/controls/charts/bar_chart/media/example_2.gif differ diff --git a/sdk/python/examples/controls/charts/candlestick_chart/example_1.py b/sdk/python/examples/controls/charts/candlestick_chart/example_1.py new file mode 100644 index 0000000000..f44d2e95a3 --- /dev/null +++ b/sdk/python/examples/controls/charts/candlestick_chart/example_1.py @@ -0,0 +1,129 @@ +import flet_charts as ftc + +import flet as ft + +CANDLE_DATA = [ + ("Mon", 24.8, 28.6, 23.9, 27.2), + ("Tue", 27.2, 30.1, 25.8, 28.4), + ("Wed", 28.4, 31.2, 26.5, 29.1), + ("Thu", 29.1, 32.4, 27.9, 31.8), + ("Fri", 31.8, 34.0, 29.7, 30.2), + ("Sat", 30.2, 33.6, 28.3, 32.7), + ("Sun", 32.7, 35.5, 30.1, 34.6), +] + + +def build_spots() -> list[ftc.CandlestickChartSpot]: + """Create candlestick spots from the static data.""" + spots: list[ftc.CandlestickChartSpot] = [] + for index, (label, open_, high, low, close) in enumerate(CANDLE_DATA): + spots.append( + ftc.CandlestickChartSpot( + x=float(index), + open=open_, + high=high, + low=low, + close=close, + selected=index == len(CANDLE_DATA) - 1, + tooltip=ftc.CandlestickChartSpotTooltip( + text=( + f"{label}\n" + f"Open: {open_:0.1f}\n" + f"High: {high:0.1f}\n" + f"Low : {low:0.1f}\n" + f"Close: {close:0.1f}" + ), + bottom_margin=12, + ), + ) + ) + return spots + + +def main(page: ft.Page): + page.title = "Candlestick chart" + page.padding = 24 + page.theme_mode = ft.ThemeMode.DARK + + info = ft.Text("Interact with the chart to see event details.") + + spots = build_spots() + min_x = -0.5 + max_x = len(spots) - 0.5 + min_y = min(low for _, _, _, low, _ in CANDLE_DATA) - 1 + max_y = max(high for _, _, _, _, high in CANDLE_DATA) + 1 + + def handle_event(e: ftc.CandlestickChartEvent): + if e.spot_index is not None and e.spot_index >= 0: + label, open_, high, low, close = CANDLE_DATA[e.spot_index] + info.value = ( + f"{e.type.value} β€’ {label}: " + f"O {open_:0.1f} H {high:0.1f} L {low:0.1f} C {close:0.1f}" + ) + else: + info.value = f"{e.type.value} β€’ outside candlesticks" + info.update() + + chart = ftc.CandlestickChart( + expand=True, + min_x=min_x, + max_x=max_x, + min_y=min_y, + max_y=max_y, + baseline_x=0, + baseline_y=min_y, + bgcolor=ft.Colors.with_opacity(0.2, ft.Colors.BLUE_GREY_900), + horizontal_grid_lines=ftc.ChartGridLines(interval=2, dash_pattern=[2, 2]), + vertical_grid_lines=ftc.ChartGridLines(interval=1, dash_pattern=[2, 2]), + left_axis=ftc.ChartAxis( + label_spacing=2, + label_size=60, + title=ft.Text("Price (k USD)", color=ft.Colors.GREY_300), + show_min=False, + ), + bottom_axis=ftc.ChartAxis( + labels=[ + ftc.ChartAxisLabel( + value=index, + label=ft.Text(name, color=ft.Colors.GREY_300), + ) + for index, (name, *_rest) in enumerate(CANDLE_DATA) + ], + label_spacing=1, + label_size=40, + show_min=False, + show_max=False, + ), + spots=spots, + tooltip=ftc.CandlestickChartTooltip( + bgcolor=ft.Colors.BLUE_GREY_800, + horizontal_alignment=ftc.HorizontalAlignment.CENTER, + fit_inside_horizontally=True, + ), + handle_built_in_touches=True, + on_event=handle_event, + ) + + page.add( + ft.Container( + expand=True, + border_radius=16, + padding=20, + content=ft.Column( + expand=True, + spacing=20, + controls=[ + ft.Text( + "Weekly OHLC snapshot (demo data)", + size=20, + weight=ft.FontWeight.BOLD, + ), + chart, + info, + ], + ), + ) + ) + + +ft.run(main) diff --git a/sdk/python/examples/controls/charts/candlestick_chart/media/example_1.png b/sdk/python/examples/controls/charts/candlestick_chart/media/example_1.png new file mode 100644 index 0000000000..2563a3ff1c Binary files /dev/null and b/sdk/python/examples/controls/charts/candlestick_chart/media/example_1.png differ diff --git a/sdk/python/examples/controls/charts/line_chart/example_1.py b/sdk/python/examples/controls/charts/line_chart/example_1.py new file mode 100644 index 0000000000..50b0f6f4b6 --- /dev/null +++ b/sdk/python/examples/controls/charts/line_chart/example_1.py @@ -0,0 +1,206 @@ +import flet_charts as fch + +import flet as ft + + +class State: + toggled = True + + +state = State() + + +def main(page: ft.Page): + data_1 = [ + fch.LineChartData( + stroke_width=8, + color=ft.Colors.LIGHT_GREEN, + curved=True, + rounded_stroke_cap=True, + points=[ + fch.LineChartDataPoint(1, 1), + fch.LineChartDataPoint(3, 1.5), + fch.LineChartDataPoint(5, 1.4), + fch.LineChartDataPoint(7, 3.4), + fch.LineChartDataPoint(10, 2), + fch.LineChartDataPoint(12, 2.2), + fch.LineChartDataPoint(13, 1.8), + ], + ), + fch.LineChartData( + color=ft.Colors.PINK, + below_line_bgcolor=ft.Colors.with_opacity(0, ft.Colors.PINK), + stroke_width=8, + curved=True, + rounded_stroke_cap=True, + points=[ + fch.LineChartDataPoint(1, 1), + fch.LineChartDataPoint(3, 2.8), + fch.LineChartDataPoint(7, 1.2), + fch.LineChartDataPoint(10, 2.8), + fch.LineChartDataPoint(12, 2.6), + fch.LineChartDataPoint(13, 3.9), + ], + ), + fch.LineChartData( + color=ft.Colors.CYAN, + stroke_width=8, + curved=True, + rounded_stroke_cap=True, + points=[ + fch.LineChartDataPoint(1, 2.8), + fch.LineChartDataPoint(3, 1.9), + fch.LineChartDataPoint(6, 3), + fch.LineChartDataPoint(10, 1.3), + fch.LineChartDataPoint(13, 2.5), + ], + ), + ] + + data_2 = [ + fch.LineChartData( + stroke_width=4, + color=ft.Colors.with_opacity(0.5, ft.Colors.LIGHT_GREEN), + rounded_stroke_cap=True, + points=[ + fch.LineChartDataPoint(1, 1), + fch.LineChartDataPoint(3, 4), + fch.LineChartDataPoint(5, 1.8), + fch.LineChartDataPoint(7, 5), + fch.LineChartDataPoint(10, 2), + fch.LineChartDataPoint(12, 2.2), + fch.LineChartDataPoint(13, 1.8), + ], + ), + fch.LineChartData( + color=ft.Colors.with_opacity(0.5, ft.Colors.PINK), + below_line_bgcolor=ft.Colors.with_opacity(0.2, ft.Colors.PINK), + stroke_width=4, + curved=True, + rounded_stroke_cap=True, + points=[ + fch.LineChartDataPoint(1, 1), + fch.LineChartDataPoint(3, 2.8), + fch.LineChartDataPoint(7, 1.2), + fch.LineChartDataPoint(10, 2.8), + fch.LineChartDataPoint(12, 2.6), + fch.LineChartDataPoint(13, 3.9), + ], + ), + fch.LineChartData( + color=ft.Colors.with_opacity(0.5, ft.Colors.CYAN), + stroke_width=4, + rounded_stroke_cap=True, + points=[ + fch.LineChartDataPoint(1, 3.8), + fch.LineChartDataPoint(3, 1.9), + fch.LineChartDataPoint(6, 5), + fch.LineChartDataPoint(10, 3.3), + fch.LineChartDataPoint(13, 4.5), + ], + ), + ] + + chart = fch.LineChart( + data_series=data_1, + border=ft.Border( + bottom=ft.BorderSide(4, ft.Colors.with_opacity(0.5, ft.Colors.ON_SURFACE)) + ), + tooltip=fch.LineChartTooltip( + bgcolor=ft.Colors.with_opacity(0.8, ft.Colors.BLUE_GREY) + ), + min_y=0, + max_y=4, + min_x=0, + max_x=14, + expand=True, + right_axis=fch.ChartAxis(show_labels=False), + left_axis=fch.ChartAxis( + label_size=40, + labels=[ + fch.ChartAxisLabel( + value=1, + label=ft.Text("1m", size=14, weight=ft.FontWeight.BOLD), + ), + fch.ChartAxisLabel( + value=2, + label=ft.Text("2m", size=14, weight=ft.FontWeight.BOLD), + ), + fch.ChartAxisLabel( + value=3, + label=ft.Text("3m", size=14, weight=ft.FontWeight.BOLD), + ), + fch.ChartAxisLabel( + value=4, + label=ft.Text("4m", size=14, weight=ft.FontWeight.BOLD), + ), + fch.ChartAxisLabel( + value=5, + label=ft.Text("5m", size=14, weight=ft.FontWeight.BOLD), + ), + fch.ChartAxisLabel( + value=6, + label=ft.Text("6m", size=14, weight=ft.FontWeight.BOLD), + ), + ], + ), + bottom_axis=fch.ChartAxis( + label_size=32, + labels=[ + fch.ChartAxisLabel( + value=2, + label=ft.Container( + margin=ft.Margin(top=10), + content=ft.Text( + value="SEP", + size=16, + weight=ft.FontWeight.BOLD, + color=ft.Colors.with_opacity(0.5, ft.Colors.ON_SURFACE), + ), + ), + ), + fch.ChartAxisLabel( + value=7, + label=ft.Container( + margin=ft.Margin(top=10), + content=ft.Text( + value="OCT", + size=16, + weight=ft.FontWeight.BOLD, + color=ft.Colors.with_opacity(0.5, ft.Colors.ON_SURFACE), + ), + ), + ), + fch.ChartAxisLabel( + value=12, + label=ft.Container( + margin=ft.Margin(top=10), + content=ft.Text( + value="DEC", + size=16, + weight=ft.FontWeight.BOLD, + color=ft.Colors.with_opacity(0.5, ft.Colors.ON_SURFACE), + ), + ), + ), + ], + ), + ) + + def toggle_data(e: ft.Event[ft.IconButton]): + if state.toggled: + chart.data_series = data_2 + chart.data_series[2].point = True + chart.max_y = 6 + chart.interactive = False + else: + chart.data_series = data_1 + chart.max_y = 4 + chart.interactive = True + state.toggled = not state.toggled + chart.update() + + page.add(ft.IconButton(ft.Icons.REFRESH, on_click=toggle_data), chart) + + +ft.run(main) diff --git a/sdk/python/examples/controls/charts/line_chart/example_2.py b/sdk/python/examples/controls/charts/line_chart/example_2.py new file mode 100644 index 0000000000..d7663e70f2 --- /dev/null +++ b/sdk/python/examples/controls/charts/line_chart/example_2.py @@ -0,0 +1,140 @@ +import flet_charts as fch + +import flet as ft + + +class State: + toggled = True + + +state = State() + + +def main(page: ft.Page): + data_1 = [ + fch.LineChartData( + stroke_width=5, + color=ft.Colors.CYAN, + curved=True, + rounded_stroke_cap=True, + points=[ + fch.LineChartDataPoint(0, 3), + fch.LineChartDataPoint(2.6, 2), + fch.LineChartDataPoint(4.9, 5), + fch.LineChartDataPoint(6.8, 3.1), + fch.LineChartDataPoint(8, 4), + fch.LineChartDataPoint(9.5, 3), + fch.LineChartDataPoint(11, 4), + ], + ) + ] + + data_2 = [ + fch.LineChartData( + stroke_width=5, + color=ft.Colors.CYAN, + curved=True, + rounded_stroke_cap=True, + points=[ + fch.LineChartDataPoint(0, 3.44), + fch.LineChartDataPoint(2.6, 3.44), + fch.LineChartDataPoint(4.9, 3.44), + fch.LineChartDataPoint(6.8, 3.44), + fch.LineChartDataPoint(8, 3.44), + fch.LineChartDataPoint(9.5, 3.44), + fch.LineChartDataPoint(11, 3.44), + ], + ) + ] + + chart = fch.LineChart( + expand=True, + data_series=data_1, + min_y=0, + max_y=6, + min_x=0, + max_x=11, + border=ft.Border.all(3, ft.Colors.with_opacity(0.2, ft.Colors.ON_SURFACE)), + horizontal_grid_lines=fch.ChartGridLines( + interval=1, color=ft.Colors.with_opacity(0.2, ft.Colors.ON_SURFACE), width=1 + ), + vertical_grid_lines=fch.ChartGridLines( + interval=1, color=ft.Colors.with_opacity(0.2, ft.Colors.ON_SURFACE), width=1 + ), + tooltip=fch.LineChartTooltip( + bgcolor=ft.Colors.with_opacity(0.8, ft.Colors.BLUE_GREY) + ), + left_axis=fch.ChartAxis( + label_size=40, + labels=[ + fch.ChartAxisLabel( + value=1, + label=ft.Text("10K", size=14, weight=ft.FontWeight.BOLD), + ), + fch.ChartAxisLabel( + value=3, + label=ft.Text("30K", size=14, weight=ft.FontWeight.BOLD), + ), + fch.ChartAxisLabel( + value=5, + label=ft.Text("50K", size=14, weight=ft.FontWeight.BOLD), + ), + ], + ), + bottom_axis=fch.ChartAxis( + label_size=32, + labels=[ + fch.ChartAxisLabel( + value=2, + label=ft.Container( + margin=ft.Margin(top=10), + content=ft.Text( + value="MAR", + size=16, + weight=ft.FontWeight.BOLD, + color=ft.Colors.with_opacity(0.5, ft.Colors.ON_SURFACE), + ), + ), + ), + fch.ChartAxisLabel( + value=5, + label=ft.Container( + margin=ft.Margin(top=10), + content=ft.Text( + value="JUN", + size=16, + weight=ft.FontWeight.BOLD, + color=ft.Colors.with_opacity(0.5, ft.Colors.ON_SURFACE), + ), + ), + ), + fch.ChartAxisLabel( + value=8, + label=ft.Container( + margin=ft.Margin(top=10), + content=ft.Text( + value="SEP", + size=16, + weight=ft.FontWeight.BOLD, + color=ft.Colors.with_opacity(0.5, ft.Colors.ON_SURFACE), + ), + ), + ), + ], + ), + ) + + def toggle_data(e: ft.Event[ft.ElevatedButton]): + if state.toggled: + chart.data_series = data_2 + chart.interactive = False + else: + chart.data_series = data_1 + chart.interactive = True + state.toggled = not state.toggled + chart.update() + + page.add(ft.ElevatedButton("avg", on_click=toggle_data), chart) + + +ft.run(main) diff --git a/sdk/python/examples/controls/charts/line_chart/media/example_1.gif b/sdk/python/examples/controls/charts/line_chart/media/example_1.gif new file mode 100644 index 0000000000..00f0f16d25 Binary files /dev/null and b/sdk/python/examples/controls/charts/line_chart/media/example_1.gif differ diff --git a/sdk/python/examples/controls/charts/line_chart/media/example_2.gif b/sdk/python/examples/controls/charts/line_chart/media/example_2.gif new file mode 100644 index 0000000000..38b70098d2 Binary files /dev/null and b/sdk/python/examples/controls/charts/line_chart/media/example_2.gif differ diff --git a/sdk/python/examples/controls/charts/matplotlib_chart/example_1.py b/sdk/python/examples/controls/charts/matplotlib_chart/example_1.py new file mode 100644 index 0000000000..578bca26af --- /dev/null +++ b/sdk/python/examples/controls/charts/matplotlib_chart/example_1.py @@ -0,0 +1,27 @@ +import flet_charts as fch +import matplotlib +import matplotlib.pyplot as plt + +import flet as ft + +matplotlib.use("svg") + + +def main(page: ft.Page): + fig, ax = plt.subplots() + + fruits = ["apple", "blueberry", "cherry", "orange"] + counts = [40, 100, 30, 55] + bar_labels = ["red", "blue", "_red", "orange"] + bar_colors = ["tab:red", "tab:blue", "tab:red", "tab:orange"] + + ax.bar(fruits, counts, label=bar_labels, color=bar_colors) + + ax.set_ylabel("fruit supply") + ax.set_title("Fruit supply by kind and color") + ax.legend(title="Fruit color") + + page.add(fch.MatplotlibChart(figure=fig, expand=True)) + + +ft.run(main) diff --git a/sdk/python/examples/controls/charts/matplotlib_chart/example_2.py b/sdk/python/examples/controls/charts/matplotlib_chart/example_2.py new file mode 100644 index 0000000000..6418bf402d --- /dev/null +++ b/sdk/python/examples/controls/charts/matplotlib_chart/example_2.py @@ -0,0 +1,39 @@ +import flet_charts as fch +import matplotlib +import matplotlib.pyplot as plt +import numpy as np + +import flet as ft + +matplotlib.use("svg") + + +def main(page: ft.Page): + # Fixing random state for reproducibility + np.random.seed(19680801) + + dt = 0.01 + t = np.arange(0, 30, dt) + nse1 = np.random.randn(len(t)) # white noise 1 + nse2 = np.random.randn(len(t)) # white noise 2 + + # Two signals with a coherent part at 10Hz and a random part + s1 = np.sin(2 * np.pi * 10 * t) + nse1 + s2 = np.sin(2 * np.pi * 10 * t) + nse2 + + fig, axs = plt.subplots(2, 1) + axs[0].plot(t, s1, t, s2) + axs[0].set_xlim(0, 2) + axs[0].set_xlabel("time") + axs[0].set_ylabel("s1 and s2") + axs[0].grid(True) + + cxy, f = axs[1].cohere(s1, s2, 256, 1.0 / dt) + axs[1].set_ylabel("coherence") + + fig.tight_layout() + + page.add(fch.MatplotlibChart(figure=fig, expand=True)) + + +ft.run(main) diff --git a/sdk/python/examples/controls/charts/matplotlib_chart/media/contour.png b/sdk/python/examples/controls/charts/matplotlib_chart/media/contour.png new file mode 100644 index 0000000000..ba8b25e02f Binary files /dev/null and b/sdk/python/examples/controls/charts/matplotlib_chart/media/contour.png differ diff --git a/sdk/python/examples/controls/charts/matplotlib_chart/media/example_1.png b/sdk/python/examples/controls/charts/matplotlib_chart/media/example_1.png new file mode 100644 index 0000000000..9c56e7130c Binary files /dev/null and b/sdk/python/examples/controls/charts/matplotlib_chart/media/example_1.png differ diff --git a/sdk/python/examples/controls/charts/matplotlib_chart/media/example_2.png b/sdk/python/examples/controls/charts/matplotlib_chart/media/example_2.png new file mode 100644 index 0000000000..1f0d05890d Binary files /dev/null and b/sdk/python/examples/controls/charts/matplotlib_chart/media/example_2.png differ diff --git a/sdk/python/examples/controls/charts/matplotlib_chart/media/scatter.png b/sdk/python/examples/controls/charts/matplotlib_chart/media/scatter.png new file mode 100644 index 0000000000..97aa751a2a Binary files /dev/null and b/sdk/python/examples/controls/charts/matplotlib_chart/media/scatter.png differ diff --git a/sdk/python/examples/controls/charts/pie_chart/example_1.py b/sdk/python/examples/controls/charts/pie_chart/example_1.py new file mode 100644 index 0000000000..b8acd8931a --- /dev/null +++ b/sdk/python/examples/controls/charts/pie_chart/example_1.py @@ -0,0 +1,53 @@ +import flet_charts as fch + +import flet as ft + + +def main(page: ft.Page): + normal_border = ft.BorderSide(0, ft.Colors.with_opacity(0, ft.Colors.WHITE)) + hovered_border = ft.BorderSide(6, ft.Colors.SECONDARY) + + def on_chart_event(e: fch.PieChartEvent): + for idx, section in enumerate(chart.sections): + section.border_side = ( + hovered_border if idx == e.section_index else normal_border + ) + chart.update() + + chart = fch.PieChart( + sections_space=1, + center_space_radius=0, + on_event=on_chart_event, + expand=True, + sections=[ + fch.PieChartSection( + value=25, + color=ft.Colors.BLUE, + radius=80, + border_side=normal_border, + ), + fch.PieChartSection( + value=25, + color=ft.Colors.YELLOW, + radius=65, + border_side=normal_border, + ), + fch.PieChartSection( + value=25, + color=ft.Colors.PINK, + radius=60, + border_side=normal_border, + ), + fch.PieChartSection( + value=25, + color=ft.Colors.GREEN, + radius=70, + border_side=normal_border, + ), + ], + ) + + page.add(chart) + + +ft.run(main) diff --git a/sdk/python/examples/controls/charts/pie_chart/example_2.py b/sdk/python/examples/controls/charts/pie_chart/example_2.py new file mode 100644 index 0000000000..9a776f470b --- /dev/null +++ b/sdk/python/examples/controls/charts/pie_chart/example_2.py @@ -0,0 +1,69 @@ +import flet_charts as fch + +import flet as ft + +NORMAL_RADIUS = 50 +HOVER_RADIUS = 60 +NORMAL_TITLE_STYLE = ft.TextStyle( + size=16, color=ft.Colors.WHITE, weight=ft.FontWeight.BOLD +) +HOVER_TITLE_STYLE = ft.TextStyle( + size=22, + color=ft.Colors.WHITE, + weight=ft.FontWeight.BOLD, + shadow=ft.BoxShadow(blur_radius=2, color=ft.Colors.BLACK54), +) + + +def main(page: ft.Page): + def on_chart_event(e: fch.PieChartEvent): + for idx, section in enumerate(chart.sections): + if idx == e.section_index: + section.radius = HOVER_RADIUS + section.title_style = HOVER_TITLE_STYLE + else: + section.radius = NORMAL_RADIUS + section.title_style = NORMAL_TITLE_STYLE + chart.update() + + chart = fch.PieChart( + expand=True, + sections_space=0, + center_space_radius=40, + on_event=on_chart_event, + sections=[ + fch.PieChartSection( + value=40, + title="40%", + title_style=NORMAL_TITLE_STYLE, + color=ft.Colors.BLUE, + radius=NORMAL_RADIUS, + ), + fch.PieChartSection( + value=30, + title="30%", + title_style=NORMAL_TITLE_STYLE, + color=ft.Colors.YELLOW, + radius=NORMAL_RADIUS, + ), + fch.PieChartSection( + value=15, + title="15%", + title_style=NORMAL_TITLE_STYLE, + color=ft.Colors.PURPLE, + radius=NORMAL_RADIUS, + ), + fch.PieChartSection( + value=15, + title="15%", + title_style=NORMAL_TITLE_STYLE, + color=ft.Colors.GREEN, + radius=NORMAL_RADIUS, + ), + ], + ) + + page.add(chart) + + +ft.run(main) diff --git a/sdk/python/examples/controls/charts/pie_chart/example_3.py b/sdk/python/examples/controls/charts/pie_chart/example_3.py new file mode 100644 index 0000000000..a0dde5dfa6 --- /dev/null +++ b/sdk/python/examples/controls/charts/pie_chart/example_3.py @@ -0,0 +1,91 @@ +import flet_charts as fch + +import flet as ft + +NORMAL_RADIUS = 100 +HOVER_RADIUS = 110 +NORMAL_TITLE_STYLE = ft.TextStyle( + size=12, color=ft.Colors.WHITE, weight=ft.FontWeight.BOLD +) +HOVER_TITLE_STYLE = ft.TextStyle( + size=16, + color=ft.Colors.WHITE, + weight=ft.FontWeight.BOLD, + shadow=ft.BoxShadow(blur_radius=2, color=ft.Colors.BLACK54), +) +NORMAL_BADGE_SIZE = 40 +HOVER_BADGE_SIZE = 50 + + +class SectionBadge(ft.Container): + def __init__(self, icon: ft.IconData, size: int = NORMAL_BADGE_SIZE): + super().__init__( + content=ft.Icon(icon), + width=size, + height=size, + border=ft.Border.all(1, ft.Colors.BROWN), + border_radius=size / 2, + bgcolor=ft.Colors.WHITE, + ) + + +def main(page: ft.Page): + def on_chart_event(e: fch.PieChartEvent): + for idx, section in enumerate(chart.sections): + if idx == e.section_index: + section.radius = HOVER_RADIUS + section.title_style = HOVER_TITLE_STYLE + else: + section.radius = NORMAL_RADIUS + section.title_style = NORMAL_TITLE_STYLE + chart.update() + + chart = fch.PieChart( + sections_space=0, + center_space_radius=0, + on_event=on_chart_event, + expand=True, + sections=[ + fch.PieChartSection( + value=40, + title="40%", + title_style=NORMAL_TITLE_STYLE, + color=ft.Colors.BLUE, + radius=NORMAL_RADIUS, + badge=SectionBadge(ft.Icons.AC_UNIT), + badge_position=0.98, + ), + fch.PieChartSection( + value=30, + title="30%", + title_style=NORMAL_TITLE_STYLE, + color=ft.Colors.YELLOW, + radius=NORMAL_RADIUS, + badge=SectionBadge(ft.Icons.ACCESS_ALARM), + badge_position=0.98, + ), + fch.PieChartSection( + value=15, + title="15%", + title_style=NORMAL_TITLE_STYLE, + color=ft.Colors.PURPLE, + radius=NORMAL_RADIUS, + badge=SectionBadge(ft.Icons.APPLE), + badge_position=0.98, + ), + fch.PieChartSection( + value=15, + title="15%", + title_style=NORMAL_TITLE_STYLE, + color=ft.Colors.GREEN, + radius=NORMAL_RADIUS, + badge=SectionBadge(ft.Icons.PEDAL_BIKE), + badge_position=0.98, + ), + ], + ) + + page.add(chart) + + +ft.run(main) diff --git a/sdk/python/examples/controls/charts/pie_chart/media/example_1.gif b/sdk/python/examples/controls/charts/pie_chart/media/example_1.gif new file mode 100644 index 0000000000..777f09c758 Binary files /dev/null and b/sdk/python/examples/controls/charts/pie_chart/media/example_1.gif differ diff --git a/sdk/python/examples/controls/charts/pie_chart/media/example_2.gif b/sdk/python/examples/controls/charts/pie_chart/media/example_2.gif new file mode 100644 index 0000000000..fb12fbcb47 Binary files /dev/null and b/sdk/python/examples/controls/charts/pie_chart/media/example_2.gif differ diff --git a/sdk/python/examples/controls/charts/pie_chart/media/example_3.gif b/sdk/python/examples/controls/charts/pie_chart/media/example_3.gif new file mode 100644 index 0000000000..148cb5402d Binary files /dev/null and b/sdk/python/examples/controls/charts/pie_chart/media/example_3.gif differ diff --git a/sdk/python/examples/controls/charts/plotly_chart/example_1.py b/sdk/python/examples/controls/charts/plotly_chart/example_1.py new file mode 100644 index 0000000000..78ad629ab6 --- /dev/null +++ b/sdk/python/examples/controls/charts/plotly_chart/example_1.py @@ -0,0 +1,14 @@ +import flet_charts as fch +import plotly.express as px + +import flet as ft + + +def main(page: ft.Page): + df = px.data.gapminder().query("continent=='Oceania'") + fig = px.line(df, x="year", y="lifeExp", color="country") + + page.add(fch.PlotlyChart(figure=fig, expand=True)) + + +ft.run(main) diff --git a/sdk/python/examples/controls/charts/plotly_chart/example_2.py b/sdk/python/examples/controls/charts/plotly_chart/example_2.py new file mode 100644 index 0000000000..b6e6c98816 --- /dev/null +++ b/sdk/python/examples/controls/charts/plotly_chart/example_2.py @@ -0,0 +1,22 @@ +import flet_charts as fch +import plotly.express as px + +import flet as ft + + +def main(page: ft.Page): + df = px.data.gapminder().query("continent == 'Oceania'") + fig = px.bar( + df, + x="year", + y="pop", + hover_data=["lifeExp", "gdpPercap"], + color="country", + labels={"pop": "population of Canada"}, + height=400, + ) + + page.add(fch.PlotlyChart(figure=fig, expand=True)) + + +ft.run(main) diff --git a/sdk/python/examples/controls/charts/plotly_chart/example_3.py b/sdk/python/examples/controls/charts/plotly_chart/example_3.py new file mode 100644 index 0000000000..c7f6bc8838 --- /dev/null +++ b/sdk/python/examples/controls/charts/plotly_chart/example_3.py @@ -0,0 +1,16 @@ +import flet_charts as fch +import plotly.graph_objects as go + +import flet as ft + + +def main(page: ft.Page): + labels = ["Oxygen", "Hydrogen", "Carbon_Dioxide", "Nitrogen"] + values = [4500, 2500, 1053, 500] + + fig = go.Figure(data=[go.Pie(labels=labels, values=values)]) + + page.add(fch.PlotlyChart(figure=fig, expand=True)) + + +ft.run(main) diff --git a/sdk/python/examples/controls/charts/plotly_chart/example_4.py b/sdk/python/examples/controls/charts/plotly_chart/example_4.py new file mode 100644 index 0000000000..c121a10370 --- /dev/null +++ b/sdk/python/examples/controls/charts/plotly_chart/example_4.py @@ -0,0 +1,58 @@ +import flet_charts as fch +import plotly.graph_objects as go + +import flet as ft + + +def main(page: ft.Page): + x = [ + "day 1", + "day 1", + "day 1", + "day 1", + "day 1", + "day 1", + "day 2", + "day 2", + "day 2", + "day 2", + "day 2", + "day 2", + ] + + fig = go.Figure() + + fig.add_trace( + go.Box( + y=[0.2, 0.2, 0.6, 1.0, 0.5, 0.4, 0.2, 0.7, 0.9, 0.1, 0.5, 0.3], + x=x, + name="kale", + marker_color="#3D9970", + ) + ) + fig.add_trace( + go.Box( + y=[0.6, 0.7, 0.3, 0.6, 0.0, 0.5, 0.7, 0.9, 0.5, 0.8, 0.7, 0.2], + x=x, + name="radishes", + marker_color="#FF4136", + ) + ) + fig.add_trace( + go.Box( + y=[0.1, 0.3, 0.1, 0.9, 0.6, 0.6, 0.9, 1.0, 0.3, 0.6, 0.8, 0.5], + x=x, + name="carrots", + marker_color="#FF851B", + ) + ) + + fig.update_layout( + yaxis_title="normalized moisture", + boxmode="group", # group together boxes of the different traces + ) + + page.add(fch.PlotlyChart(figure=fig, expand=True)) + + +ft.run(main) diff --git a/sdk/python/examples/controls/charts/plotly_chart/media/example_1.png b/sdk/python/examples/controls/charts/plotly_chart/media/example_1.png new file mode 100644 index 0000000000..9aedc455dc Binary files /dev/null and b/sdk/python/examples/controls/charts/plotly_chart/media/example_1.png differ diff --git a/sdk/python/examples/controls/charts/plotly_chart/media/example_2.png b/sdk/python/examples/controls/charts/plotly_chart/media/example_2.png new file mode 100644 index 0000000000..eef1a04561 Binary files /dev/null and b/sdk/python/examples/controls/charts/plotly_chart/media/example_2.png differ diff --git a/sdk/python/examples/controls/charts/plotly_chart/media/example_3.png b/sdk/python/examples/controls/charts/plotly_chart/media/example_3.png new file mode 100644 index 0000000000..c3ba2dadd5 Binary files /dev/null and b/sdk/python/examples/controls/charts/plotly_chart/media/example_3.png differ diff --git a/sdk/python/examples/controls/charts/plotly_chart/media/example_4.png b/sdk/python/examples/controls/charts/plotly_chart/media/example_4.png new file mode 100644 index 0000000000..de326cd2f5 Binary files /dev/null and b/sdk/python/examples/controls/charts/plotly_chart/media/example_4.png differ diff --git a/sdk/python/examples/controls/charts/scatter_chart/example_1.py b/sdk/python/examples/controls/charts/scatter_chart/example_1.py new file mode 100644 index 0000000000..91ac663159 --- /dev/null +++ b/sdk/python/examples/controls/charts/scatter_chart/example_1.py @@ -0,0 +1,160 @@ +import random + +import flet_charts as ftc + +import flet as ft + + +class MySpot(ftc.ScatterChartSpot): + def __init__( + self, + x: float, + y: float, + radius: float = 8.0, + color: ft.Colors = None, + show_tooltip: bool = False, + ): + super().__init__( + x=x, + y=y, + radius=radius, + color=color, + show_tooltip=show_tooltip, + ) + + +flutter_logo_spots = [ + MySpot(20, 14.5), + MySpot(20, 14.5), + MySpot(22, 16.5), + MySpot(24, 18.5), + MySpot(22, 12.5), + MySpot(24, 14.5), + MySpot(26, 16.5), + MySpot(24, 10.5), + MySpot(26, 12.5), + MySpot(28, 14.5), + MySpot(26, 8.5), + MySpot(28, 10.5), + MySpot(30, 12.5), + MySpot(28, 6.5), + MySpot(30, 8.5), + MySpot(32, 10.5), + MySpot(30, 4.5), + MySpot(32, 6.5), + MySpot(34, 8.5), + MySpot(34, 4.5), + MySpot(36, 6.5), + MySpot(38, 4.5), + # section 2 + MySpot(20, 14.5), + MySpot(22, 12.5), + MySpot(24, 10.5), + MySpot(22, 16.5), + MySpot(24, 14.5), + MySpot(26, 12.5), + MySpot(24, 18.5), + MySpot(26, 16.5), + MySpot(28, 14.5), + MySpot(26, 20.5), + MySpot(28, 18.5), + MySpot(30, 16.5), + MySpot(28, 22.5), + MySpot(30, 20.5), + MySpot(32, 18.5), + MySpot(30, 24.5), + MySpot(32, 22.5), + MySpot(34, 20.5), + MySpot(34, 24.5), + MySpot(36, 22.5), + MySpot(38, 24.5), + # section 3 + MySpot(10, 25), + MySpot(12, 23), + MySpot(14, 21), + MySpot(12, 27), + MySpot(14, 25), + MySpot(16, 23), + MySpot(14, 29), + MySpot(16, 27), + MySpot(18, 25), + MySpot(16, 31), + MySpot(18, 29), + MySpot(20, 27), + MySpot(18, 33), + MySpot(20, 31), + MySpot(22, 29), + MySpot(20, 35), + MySpot(22, 33), + MySpot(24, 31), + MySpot(22, 37), + MySpot(24, 35), + MySpot(26, 33), + MySpot(24, 39), + MySpot(26, 37), + MySpot(28, 35), + MySpot(26, 41), + MySpot(28, 39), + MySpot(30, 37), + MySpot(28, 43), + MySpot(30, 41), + MySpot(32, 39), + MySpot(30, 45), + MySpot(32, 43), + MySpot(34, 41), + MySpot(34, 45), + MySpot(36, 43), + MySpot(38, 45), +] + + +def get_random_spots(): + """Generates random spots for the scatter chart.""" + return [ + MySpot( + x=random.uniform(4, 50), + y=random.uniform(4, 50), + radius=random.uniform(4, 20), + ) + for _ in range(len(flutter_logo_spots)) + ] + + +def main(page: ft.Page): + page.horizontal_alignment = ft.CrossAxisAlignment.CENTER + + def handle_event(e: ftc.ScatterChartEvent): + if e.type == ftc.ChartEventType.TAP_DOWN: + e.control.spots = ( + flutter_logo_spots + if (e.control.spots != flutter_logo_spots) + else get_random_spots() + ) + + page.add( + ft.Text( + "Tap on the chart to toggle between random spots and Flutter logo spots." + ), + ftc.ScatterChart( + expand=True, + aspect_ratio=1.0, + min_x=0.0, + max_x=50.0, + min_y=0.0, + max_y=50.0, + left_axis=ftc.ChartAxis(show_labels=False), + right_axis=ftc.ChartAxis(show_labels=False), + top_axis=ftc.ChartAxis(show_labels=False), + bottom_axis=ftc.ChartAxis(show_labels=False), + show_tooltips_for_selected_spots_only=False, + on_event=handle_event, + animation=ft.Animation( + duration=ft.Duration(milliseconds=600), + curve=ft.AnimationCurve.FAST_OUT_SLOWIN, + ), + spots=flutter_logo_spots, + ), + ) + + +ft.run(main) diff --git a/sdk/python/examples/controls/charts/scatter_chart/media/example_1.png b/sdk/python/examples/controls/charts/scatter_chart/media/example_1.png new file mode 100644 index 0000000000..1647cf5d37 Binary files /dev/null and b/sdk/python/examples/controls/charts/scatter_chart/media/example_1.png differ diff --git a/sdk/python/examples/controls/datatable2/data.py b/sdk/python/examples/controls/datatable2/data.py new file mode 100644 index 0000000000..551406225e --- /dev/null +++ b/sdk/python/examples/controls/datatable2/data.py @@ -0,0 +1,47 @@ +from dataclasses import dataclass + + +@dataclass +class Dessert: + name: str + calories: float + fat: float + carbs: float + protein: float + sodium: float + calcium: float + iron: float + + +desserts = [ + Dessert("Frozen Yogurt", 159, 6.0, 24, 4.0, 87, 14, 1), + Dessert("Ice Cream Sandwich", 237, 9.0, 37, 4.3, 129, 8, 1), + Dessert("Eclair", 262, 16.0, 24, 6.0, 337, 6, 7), + Dessert("Cupcake", 305, 3.7, 67, 4.3, 413, 3, 8), + Dessert("Gingerbread", 356, 16.0, 49, 3.9, 327, 7, 16), + Dessert("Jelly Bean", 375, 0.0, 94, 0.0, 50, 0, 0), + Dessert("Lollipop", 392, 0.2, 98, 0.0, 38, 0, 2), + Dessert("Honeycomb", 408, 3.2, 87, 6.5, 562, 0, 45), + Dessert("Donut", 452, 25.0, 51, 4.9, 326, 2, 22), + Dessert("Apple Pie", 518, 26.0, 65, 7.0, 54, 12, 6), + Dessert("Frozen Yougurt with sugar", 168, 6.0, 26, 4.0, 87, 14, 1), + Dessert("Ice Cream Sandwich with sugar", 246, 9.0, 39, 4.3, 129, 8, 1), + Dessert("Eclair with sugar", 271, 16.0, 26, 6.0, 337, 6, 7), + Dessert("Cupcake with sugar", 314, 3.7, 69, 4.3, 413, 3, 8), + Dessert("Gingerbread with sugar", 345, 16.0, 51, 3.9, 327, 7, 16), + Dessert("Jelly Bean with sugar", 364, 0.0, 96, 0.0, 50, 0, 0), + Dessert("Lollipop with sugar", 401, 0.2, 100, 0.0, 38, 0, 2), + Dessert("Honeycomb with sugar", 417, 3.2, 89, 6.5, 562, 0, 45), + Dessert("Donut with sugar", 461, 25.0, 53, 4.9, 326, 2, 22), + Dessert("Apple pie with sugar", 527, 26.0, 67, 7.0, 54, 12, 6), + Dessert("Frozen yougurt with honey", 223, 6.0, 36, 4.0, 87, 14, 1), + Dessert("Ice Cream Sandwich with honey", 301, 9.0, 49, 4.3, 129, 8, 1), + Dessert("Eclair with honey", 326, 16.0, 36, 6.0, 337, 6, 7), + Dessert("Cupcake with honey", 369, 3.7, 79, 4.3, 413, 3, 8), + Dessert("Gingerbread with honey", 420, 16.0, 61, 3.9, 327, 7, 16), + Dessert("Jelly Bean with honey", 439, 0.0, 106, 0.0, 50, 0, 0), + Dessert("Lollipop with honey", 456, 0.2, 110, 0.0, 38, 0, 2), + Dessert("Honeycomb with honey", 472, 3.2, 99, 6.5, 562, 0, 45), + Dessert("Donut with honey", 516, 25.0, 63, 4.9, 326, 2, 22), + Dessert("Apple pie with honey", 582, 26.0, 77, 7.0, 54, 12, 6), +] diff --git a/sdk/python/examples/controls/datatable2/example_1.py b/sdk/python/examples/controls/datatable2/example_1.py new file mode 100644 index 0000000000..b5a3c56c3b --- /dev/null +++ b/sdk/python/examples/controls/datatable2/example_1.py @@ -0,0 +1,19 @@ +import flet_datatable2 as fdt + +import flet as ft + + +def main(page: ft.Page): + page.add( + fdt.DataTable2( + empty=ft.Text("This table is empty."), + columns=[ + fdt.DataColumn2(label=ft.Text("First name")), + fdt.DataColumn2(label=ft.Text("Last name")), + fdt.DataColumn2(label=ft.Text("Age"), numeric=True), + ], + ), + ) + + +ft.run(main) diff --git a/sdk/python/examples/controls/datatable2/example_2.py b/sdk/python/examples/controls/datatable2/example_2.py new file mode 100644 index 0000000000..2eda6cd9d0 --- /dev/null +++ b/sdk/python/examples/controls/datatable2/example_2.py @@ -0,0 +1,103 @@ +import flet_datatable2 as ftd +from data import desserts + +import flet as ft + + +def main(page: ft.Page): + page.vertical_alignment = ft.MainAxisAlignment.CENTER + page.horizontal_alignment = ft.CrossAxisAlignment.CENTER + + def handle_row_selection_change(e: ft.Event[ftd.DataRow2]): + e.control.selected = not e.control.selected + e.control.update() + + def sort_column(e: ft.DataColumnSortEvent): + print(f"Sorting column {e.column_index}, ascending={e.ascending}") + + def get_data_columns(): + data_columns = [ + ftd.DataColumn2( + label=ft.Text("Name"), + size=ftd.DataColumnSize.L, + on_sort=sort_column, + heading_row_alignment=ft.MainAxisAlignment.START, + ), + ftd.DataColumn2( + label=ft.Text("Calories"), + on_sort=sort_column, + numeric=True, + heading_row_alignment=ft.MainAxisAlignment.END, + ), + ftd.DataColumn2( + label=ft.Text("Fat"), + on_sort=sort_column, + numeric=True, + ), + ftd.DataColumn2( + label=ft.Text("Carbs"), + on_sort=sort_column, + numeric=True, + ), + ftd.DataColumn2( + label=ft.Text("Protein"), + on_sort=sort_column, + numeric=True, + ), + ftd.DataColumn2( + label=ft.Text("Sodium"), + on_sort=sort_column, + numeric=True, + ), + ftd.DataColumn2( + label=ft.Text("Calcium"), + on_sort=sort_column, + numeric=True, + ), + ftd.DataColumn2( + label=ft.Text("Iron"), + on_sort=sort_column, + numeric=True, + ), + ] + return data_columns + + def get_data_rows(desserts): + data_rows = [] + for dessert in desserts: + data_rows.append( + ftd.DataRow2( + specific_row_height=50, + on_select_change=handle_row_selection_change, + cells=[ + ft.DataCell(content=ft.Text(dessert.name)), + ft.DataCell(content=ft.Text(dessert.calories)), + ft.DataCell(content=ft.Text(dessert.fat)), + ft.DataCell(content=ft.Text(dessert.carbs)), + ft.DataCell(content=ft.Text(dessert.protein)), + ft.DataCell(content=ft.Text(dessert.sodium)), + ft.DataCell(content=ft.Text(dessert.calcium)), + ft.DataCell(content=ft.Text(dessert.iron)), + ], + ) + ) + return data_rows + + page.add( + ftd.DataTable2( + show_checkbox_column=True, + expand=True, + column_spacing=0, + heading_row_color=ft.Colors.SECONDARY_CONTAINER, + horizontal_margin=12, + sort_ascending=True, + bottom_margin=10, + min_width=600, + on_select_all=lambda e: print("All selected"), + columns=get_data_columns(), + rows=get_data_rows(desserts), + ), + ) + + +ft.run(main) diff --git a/sdk/python/examples/controls/datatable2/media/example_2.gif b/sdk/python/examples/controls/datatable2/media/example_2.gif new file mode 100644 index 0000000000..07fb5a645e Binary files /dev/null and b/sdk/python/examples/controls/datatable2/media/example_2.gif differ diff --git a/sdk/python/examples/controls/dropdown_m2/basic.py b/sdk/python/examples/controls/dropdown_m2/basic.py index 5b389e4187..9476e82c2a 100644 --- a/sdk/python/examples/controls/dropdown_m2/basic.py +++ b/sdk/python/examples/controls/dropdown_m2/basic.py @@ -1,8 +1,12 @@ +import logging + import flet as ft +logging.basicConfig(level=logging.DEBUG) + def main(page: ft.Page): - page.theme_mode = ft.ThemeMode.DARK + # page.theme_mode = ft.ThemeMode.DARK def handle_button_click(e): message.value = f"Dropdown value is: {dd.value}" @@ -11,6 +15,7 @@ def handle_button_click(e): page.add( dd := ft.DropdownM2( width=100, + value="Green", options=[ ft.dropdownm2.Option("Red"), ft.dropdownm2.Option("Green"), diff --git a/sdk/python/examples/controls/flashlight/example_1.py b/sdk/python/examples/controls/flashlight/example_1.py new file mode 100644 index 0000000000..2f1e7f3c16 --- /dev/null +++ b/sdk/python/examples/controls/flashlight/example_1.py @@ -0,0 +1,14 @@ +import flet_flashlight as ffl + +import flet as ft + + +def main(page: ft.Page): + async def toggle_flashlight(): + flashlight = ffl.Flashlight() + await flashlight.toggle() + + page.add(ft.TextButton("toggle", on_click=toggle_flashlight)) + + +ft.run(main) diff --git a/sdk/python/examples/controls/geolocator/example_1.py b/sdk/python/examples/controls/geolocator/example_1.py new file mode 100644 index 0000000000..2331d360d2 --- /dev/null +++ b/sdk/python/examples/controls/geolocator/example_1.py @@ -0,0 +1,118 @@ +from typing import Callable + +import flet_geolocator as ftg + +import flet as ft + + +async def main(page: ft.Page): + page.scroll = ft.ScrollMode.ADAPTIVE + page.appbar = ft.AppBar(title=ft.Text("Geolocator Tests")) + + def handle_position_change(e: ftg.GeolocatorPositionChangeEvent): + page.add(ft.Text(f"New position: {e.position.latitude} {e.position.longitude}")) + + def get_dialog(handler: Callable): + return ft.AlertDialog( + adaptive=True, + title="Opening Location Settings...", + content=ft.Text( + "You are about to be redirected to the location/app settings. " + "Please locate this app and grant it location permissions." + ), + actions=[ft.TextButton("Take me there", on_click=handler)], + actions_alignment=ft.MainAxisAlignment.CENTER, + ) + + def show_snackbar(message): + page.show_dialog(ft.SnackBar(ft.Text(message))) + + async def handle_permission_request(e: ft.Event[ft.OutlinedButton]): + p = await geo.request_permission(timeout=60) + page.add(ft.Text(f"request_permission: {p}")) + show_snackbar(f"Permission request sent: {p}") + + async def handle_get_permission_status(e: ft.Event[ft.OutlinedButton]): + p = await geo.get_permission_status() + show_snackbar(f"Permission status: {p}") + + async def handle_get_current_position(e: ft.Event[ft.OutlinedButton]): + p = await geo.get_current_position() + show_snackbar(f"Current position: ({p.latitude}, {p.longitude})") + + async def handle_get_last_known_position(e): + p = await geo.get_last_known_position() + show_snackbar(f"Last known position: ({p.latitude}, {p.longitude})") + + async def handle_location_service_enabled(e): + p = await geo.is_location_service_enabled() + show_snackbar(f"Location service enabled: {p}") + + async def handle_open_location_settings(e: ft.Event[ft.OutlinedButton]): + p = await geo.open_location_settings() + page.pop_dialog() + if p is True: + show_snackbar("Location settings opened successfully.") + else: + show_snackbar("Location settings could not be opened.") + + async def handle_open_app_settings(e: ft.Event[ft.OutlinedButton]): + p = await geo.open_app_settings() + page.pop_dialog() + if p: + show_snackbar("App settings opened successfully.") + else: + show_snackbar("App settings could not be opened.") + + location_settings_dlg = get_dialog(handle_open_location_settings) + app_settings_dlg = get_dialog(handle_open_app_settings) + + geo = ftg.Geolocator( + configuration=ftg.GeolocatorConfiguration( + accuracy=ftg.GeolocatorPositionAccuracy.LOW + ), + on_position_change=handle_position_change, + on_error=lambda e: page.add(ft.Text(f"Error: {e.data}")), + ) + + page.add( + ft.Row( + wrap=True, + controls=[ + ft.OutlinedButton( + content="Request Permission", + on_click=handle_permission_request, + ), + ft.OutlinedButton( + content="Get Permission Status", + on_click=handle_get_permission_status, + ), + ft.OutlinedButton( + content="Get Current Position", + on_click=handle_get_current_position, + ), + ft.OutlinedButton( + content="Get Last Known Position", + visible=not page.web, + on_click=handle_get_last_known_position, + ), + ft.OutlinedButton( + content="Is Location Service Enabled", + on_click=handle_location_service_enabled, + ), + ft.OutlinedButton( + content="Open Location Settings", + visible=not page.web, # (1)! + on_click=lambda e: page.show_dialog(location_settings_dlg), + ), + ft.OutlinedButton( + content="Open App Settings", + visible=not page.web, # (1)! + on_click=lambda e: page.show_dialog(app_settings_dlg), + ), + ], + ) + ) + + +ft.run(main) diff --git a/sdk/python/examples/controls/lottie/assets/sample.json b/sdk/python/examples/controls/lottie/assets/sample.json new file mode 100644 index 0000000000..afd268ea80 --- /dev/null +++ b/sdk/python/examples/controls/lottie/assets/sample.json @@ -0,0 +1 @@ +{"assets":[],"layers":[{"ddd":0,"ind":0,"ty":4,"nm":"B 2","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,-14.502],[14.502,0],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[32.279,-158.917],[6.297,-132.935],[-32.677,-132.935],[-32.677,-184.598],[6.297,-184.598]],"c":true}},"nm":"B"},{"ind":1,"ty":"sh","ks":{"k":{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[16.569,-41.995],[-32.677,-41.995],[-32.677,-93.658],[16.569,-93.658],[43.156,-67.676]],"c":true}},"nm":"B"},{"ty":"fl","fillEnabled":true,"c":{"k":[1,1,1,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":26,"op":37,"st":22,"bm":0,"sr":1},{"ddd":0,"ind":1,"ty":4,"nm":"B","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[21.149,8.762],[0,21.451],[37.766,0],[0,0],[0,0],[0,0],[0,41.089]],"o":[[15.106,-9.064],[0,-38.672],[0,0],[0,0],[0,0],[37.463,0],[0,-29.004]],"v":[[55.241,-119.037],[79.411,-164.355],[12.037,-226.593],[-79.507,-226.593],[-79.507,0],[23.518,0],[90.287,-63.144]],"c":true}},"nm":"B"},{"ty":"fl","fillEnabled":true,"c":{"k":[0,0.01,0.24,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":26,"op":37,"st":22,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"B blue layer 6","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[-1.324,-11.343],[2.439,-7.314],[0,0],[0,0],[0,0]],"o":[[1.603,13.739],[0,0],[0,0],[0,0],[3.612,-2.224]],"v":[[-55.603,-180.739],[-55.439,-147.686],[-63.981,-142.398],[-70.654,-190.988],[-63.612,-197.276]],"c":true}],"e":[{"i":[[-2.154,-14.341],[9.939,-11.314],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[9.112,-3.724]],"v":[[-32.103,-179.739],[-36.939,-146.686],[-53.481,-139.398],[-60.154,-190.488],[-47.112,-197.276]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[-2.154,-14.341],[9.939,-11.314],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[9.112,-3.724]],"v":[[-32.103,-179.739],[-36.939,-146.686],[-53.481,-139.398],[-60.154,-190.488],[-47.112,-197.276]],"c":true}],"e":[{"i":[[-2.295,-14.342],[14.615,-7.06],[0,0],[0,0],[0,0]],"o":[[2.247,14.043],[0,0],[0,0],[0,0],[12.13,-2.72]],"v":[[-11.249,-176.983],[-23.893,-143.184],[-46.891,-138.152],[-50.397,-189.494],[-30.535,-196.276]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[-2.295,-14.342],[14.615,-7.06],[0,0],[0,0],[0,0]],"o":[[2.247,14.043],[0,0],[0,0],[0,0],[12.13,-2.72]],"v":[[-11.249,-176.983],[-23.893,-143.184],[-46.891,-138.152],[-50.397,-189.494],[-30.535,-196.276]],"c":true}],"e":[{"i":[[-2.154,-14.341],[17.439,-2.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[13.612,-1.724]],"v":[[4.397,-174.239],[-14.439,-139.686],[-40.981,-136.898],[-44.654,-188.488],[-18.612,-193.276]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[-2.154,-14.341],[17.439,-2.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[13.612,-1.724]],"v":[[4.397,-174.239],[-14.439,-139.686],[-40.981,-136.898],[-44.654,-188.488],[-18.612,-193.276]],"c":true}],"e":[{"i":[[-1.534,-14.395],[17.58,-1.876],[0,0],[0,0],[0,0]],"o":[[1.502,14.095],[0,0],[0,0],[0,0],[14.855,-1.149]],"v":[[16.998,-170.132],[-5.273,-137.269],[-37.547,-135.91],[-39.662,-187.025],[-9.278,-190.383]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[-1.534,-14.395],[17.58,-1.876],[0,0],[0,0],[0,0]],"o":[[1.502,14.095],[0,0],[0,0],[0,0],[14.855,-1.149]],"v":[[16.998,-170.132],[-5.273,-137.269],[-37.547,-135.91],[-39.662,-187.025],[-9.278,-190.383]],"c":true}],"e":[{"i":[[-0.767,-14.448],[16.041,-0.938],[0,0],[0,0],[0,0]],"o":[[0.751,14.147],[0,0],[0,0],[0,0],[14.678,-0.575]],"v":[[24.639,-165.025],[-0.738,-135.852],[-35.862,-134.172],[-36.92,-187.061],[-2.991,-188.991]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[-0.767,-14.448],[16.041,-0.938],[0,0],[0,0],[0,0]],"o":[[0.751,14.147],[0,0],[0,0],[0,0],[14.678,-0.575]],"v":[[24.639,-165.025],[-0.738,-135.852],[-35.862,-134.172],[-36.92,-187.061],[-2.991,-188.991]],"c":true}],"e":[{"i":[[0,-14.502],[14.502,0],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[28.279,-161.917],[4.297,-134.435],[-34.177,-133.435],[-34.177,-187.098],[3.297,-187.598]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"n":"0_1_0p167_0p167","t":21.376,"s":[{"i":[[0,-14.502],[14.502,0],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[28.279,-161.917],[4.297,-134.435],[-34.177,-133.435],[-34.177,-187.098],[3.297,-187.598]],"c":true}],"e":[{"i":[[0,-14.502],[14.502,0],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[32.279,-158.917],[6.297,-132.935],[-32.677,-132.935],[-32.677,-184.598],[6.297,-184.598]],"c":true}]},{"t":24}]},"nm":"B"},{"ind":1,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[5.931,-1.505],[0,0],[0,0],[0,0],[-1.768,-14.461]],"o":[[0,0],[0,0],[0,0],[8.931,-2.842],[2.344,19.176]],"v":[[-44.931,-50.495],[-55.177,-48.495],[-60.677,-102.158],[-53.931,-106.658],[-42.844,-84.676]],"c":true}],"e":[{"i":[[13.431,-1.505],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[8.931,-2.842],[2.589,13.962]],"v":[[-27.431,-47.995],[-46.177,-46.495],[-51.177,-100.658],[-34.431,-104.658],[-18.344,-87.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[13.431,-1.505],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[8.931,-2.842],[2.589,13.962]],"v":[[-27.431,-47.995],[-46.177,-46.495],[-51.177,-100.658],[-34.431,-104.658],[-18.344,-87.176]],"c":true}],"e":[{"i":[[14.118,-0.752],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[11.868,-1.421],[2.589,13.962]],"v":[[-10.681,-46.995],[-40.427,-45.245],[-43.677,-98.908],[-17.181,-102.408],[3.406,-81.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[14.118,-0.752],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[11.868,-1.421],[2.589,13.962]],"v":[[-10.681,-46.995],[-40.427,-45.245],[-43.677,-98.908],[-17.181,-102.408],[3.406,-81.176]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[14.804,0],[2.589,13.962]],"v":[[-0.931,-44.995],[-36.677,-42.995],[-39.177,-97.158],[-3.931,-100.158],[20.156,-76.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[14.804,0],[2.589,13.962]],"v":[[-0.931,-44.995],[-36.677,-42.995],[-39.177,-97.158],[-3.931,-100.158],[20.156,-76.176]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-1.77,-14.283]],"o":[[0,0],[0,0],[0,0],[14.804,0],[1.726,14.041]],"v":[[6.902,-43.495],[-34.844,-42.662],[-35.844,-96.158],[5.902,-97.825],[29.989,-73.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-1.77,-14.283]],"o":[[0,0],[0,0],[0,0],[14.804,0],[1.726,14.041]],"v":[[6.902,-43.495],[-34.844,-42.662],[-35.844,-96.158],[5.902,-97.825],[29.989,-73.176]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-0.885,-14.241]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0.863,14.121]],"v":[[12.235,-42.745],[-33.761,-42.329],[-34.761,-95.908],[11.235,-96.242],[37.322,-69.926]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-0.885,-14.241]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0.863,14.121]],"v":[[12.235,-42.745],[-33.761,-42.329],[-34.761,-95.908],[11.235,-96.242],[37.322,-69.926]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[13.569,-41.995],[-32.677,-41.995],[-33.677,-95.658],[16.569,-94.658],[40.656,-68.676]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"n":"0_1_0p167_0p167","t":21.376,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[13.569,-41.995],[-32.677,-41.995],[-33.677,-95.658],[16.569,-94.658],[40.656,-68.676]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[16.569,-41.995],[-32.677,-41.995],[-32.677,-93.658],[16.569,-93.658],[43.156,-67.676]],"c":true}]},{"t":24}]},"nm":"B"},{"ty":"fl","fillEnabled":true,"c":{"k":[1,1,1,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":16,"op":26,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"B blue layer 5","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[5.759,27.537],[4.508,20.972],[7.96,-31.787],[0,0],[0,0],[-52.303,4.363],[12.713,79.144]],"o":[[-6.241,-30.463],[-18.411,-85.645],[-8.037,32.093],[0,0],[0,0],[17.982,-1.5],[-4.202,-26.16]],"v":[[-23.759,-157.537],[-41.089,-239.855],[-57.963,-260.093],[-97.507,-214.093],[-76.507,-0.5],[-13.982,-7.5],[-8.213,-83.644]],"c":true}],"e":[{"i":[[8.259,26.537],[3.138,21.22],[33.463,-28.907],[0,0],[0,0],[-52.485,0],[6.213,46.144]],"o":[[-3.241,-28.463],[-9.411,-63.645],[-29.686,25.644],[0,0],[0,0],[43.482,0],[-4.959,-36.833]],"v":[[21.241,-163.037],[14.911,-228.855],[-29.963,-255.093],[-95.007,-217.593],[-76.507,-0.5],[17.018,-3.5],[41.787,-87.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[8.259,26.537],[3.138,21.22],[33.463,-28.907],[0,0],[0,0],[-52.485,0],[6.213,46.144]],"o":[[-3.241,-28.463],[-9.411,-63.645],[-29.686,25.644],[0,0],[0,0],[43.482,0],[-4.959,-36.833]],"v":[[21.241,-163.037],[14.911,-228.855],[-29.963,-255.093],[-95.007,-217.593],[-76.507,-0.5],[17.018,-3.5],[41.787,-87.144]],"c":true}],"e":[{"i":[[15.759,21.037],[2.598,21.286],[37.463,-16.657],[0,0],[0,0],[-26.242,0],[5.463,51.394]],"o":[[3.509,-24.213],[-4.661,-61.895],[-35.537,14.843],[0,0],[0,0],[52.482,4],[-3.676,-32.795]],"v":[[46.741,-144.787],[49.661,-211.605],[-14.963,-245.343],[-91.757,-220.343],[-77.257,-0.75],[21.518,-2],[72.287,-76.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[15.759,21.037],[2.598,21.286],[37.463,-16.657],[0,0],[0,0],[-26.242,0],[5.463,51.394]],"o":[[3.509,-24.213],[-4.661,-61.895],[-35.537,14.843],[0,0],[0,0],[52.482,4],[-3.676,-32.795]],"v":[[46.741,-144.787],[49.661,-211.605],[-14.963,-245.343],[-91.757,-220.343],[-77.257,-0.75],[21.518,-2],[72.287,-76.144]],"c":true}],"e":[{"i":[[23.259,15.537],[2.058,21.352],[34.868,-8.794],[0,0],[0,0],[0,0],[4.713,56.644]],"o":[[10.259,-19.963],[-5.411,-56.145],[-38.037,9.593],[0,0],[0,0],[45.482,1],[-2.393,-28.758]],"v":[[56.241,-133.537],[68.411,-198.355],[-2.963,-239.593],[-88.507,-223.093],[-78.007,-1],[26.018,-0.5],[86.787,-72.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[23.259,15.537],[2.058,21.352],[34.868,-8.794],[0,0],[0,0],[0,0],[4.713,56.644]],"o":[[10.259,-19.963],[-5.411,-56.145],[-38.037,9.593],[0,0],[0,0],[45.482,1],[-2.393,-28.758]],"v":[[56.241,-133.537],[68.411,-198.355],[-2.963,-239.593],[-88.507,-223.093],[-78.007,-1],[26.018,-0.5],[86.787,-72.144]],"c":true}],"e":[{"i":[[24.259,15.287],[1.029,21.401],[36.665,-5.6],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[12.683,-14.513],[-2.705,-47.408],[-19.018,4.797],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[60.241,-127.787],[76.911,-186.105],[5.787,-235.593],[-86.007,-223.593],[-78.007,-1],[24.768,-0.25],[93.287,-66.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[24.259,15.287],[1.029,21.401],[36.665,-5.6],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[12.683,-14.513],[-2.705,-47.408],[-19.018,4.797],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[60.241,-127.787],[76.911,-186.105],[5.787,-235.593],[-86.007,-223.593],[-78.007,-1],[24.768,-0.25],[93.287,-66.644]],"c":true}],"e":[{"i":[[21.149,8.762],[0,21.451],[38.463,-2.407],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[15.106,-9.064],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[59.241,-124.037],[79.411,-176.855],[14.537,-231.593],[-83.507,-224.093],[-78.007,-1],[23.518,0],[94.787,-61.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[21.149,8.762],[0,21.451],[38.463,-2.407],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[15.106,-9.064],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[59.241,-124.037],[79.411,-176.855],[14.537,-231.593],[-83.507,-224.093],[-78.007,-1],[23.518,0],[94.787,-61.144]],"c":true}],"e":[{"i":[[21.149,8.762],[0.749,21.438],[38.114,-1.203],[0,0],[0,0],[0,0],[-0.152,41.088]],"o":[[15.106,-9.064],[-1.411,-40.395],[0,0],[0,0],[0,0],[41.473,0.5],[0.106,-28.93]],"v":[[56.741,-120.537],[79.911,-168.605],[13.287,-226.593],[-79.507,-225.843],[-78.757,-0.5],[21.518,-0.5],[92.037,-63.144]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"n":"0_1_0p167_0p167","t":22.253,"s":[{"i":[[21.149,8.762],[0.749,21.438],[38.114,-1.203],[0,0],[0,0],[0,0],[-0.152,41.088]],"o":[[15.106,-9.064],[-1.411,-40.395],[0,0],[0,0],[0,0],[41.473,0.5],[0.106,-28.93]],"v":[[56.741,-120.537],[79.911,-168.605],[13.287,-226.593],[-79.507,-225.843],[-78.757,-0.5],[21.518,-0.5],[92.037,-63.144]],"c":true}],"e":[{"i":[[21.149,8.762],[0,21.451],[37.766,0],[0,0],[0,0],[0,0],[0,41.089]],"o":[[15.106,-9.064],[0,-38.672],[0,0],[0,0],[0,0],[37.463,0],[0,-29.004]],"v":[[55.241,-119.037],[79.411,-164.355],[12.037,-226.593],[-79.507,-226.593],[-79.507,0],[23.518,0],[90.287,-63.144]],"c":true}]},{"t":24}]},"nm":"B"},{"ty":"fl","fillEnabled":true,"c":{"k":[0,0.01,0.24,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":16,"op":26,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":4,"ty":3,"nm":"Null smoke","parent":34,"ks":{"o":{"k":0},"r":{"k":9.771},"p":{"k":[{"i":{"x":0.601,"y":0.889},"o":{"x":0.239,"y":0.067},"n":"0p601_0p889_0p239_0p067","t":9,"s":[241.443,258.888,0],"e":[222.443,251.888,0],"to":[-0.5,-0.33333334326744,0],"ti":[-1.02731943130493,-0.37357068061829,0]},{"i":{"x":0.686,"y":1},"o":{"x":0.239,"y":0.304},"n":"0p686_1_0p239_0p304","t":11,"s":[222.443,251.888,0],"e":[242,259,0],"to":[5.54179763793945,2.01519918441772,0],"ti":[-3.09325051307678,-1.12481832504272,0]},{"t":20.3449832499027}]},"a":{"k":[0,0,0]},"s":{"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":8.999,"s":[-100,100,100],"e":[-128,128,100]},{"t":21.9237344563007}]}},"ao":0,"ip":9.99999961256981,"op":22.7584036290646,"st":-10.5559570491314,"bm":0,"sr":0.96000009775162},{"ddd":0,"ind":5,"ty":3,"nm":"Null smoke","parent":34,"ks":{"o":{"k":0},"r":{"k":-22.412},"p":{"k":[{"i":{"x":0.592,"y":0.891},"o":{"x":0.257,"y":0.068},"n":"0p592_0p891_0p257_0p068","t":9,"s":[225.656,310.504,0],"e":[206.656,303.504,0],"to":[-0.5,-0.33333334326744,0],"ti":[-1.516930103302,-0.31094110012054,0]},{"i":{"x":0.762,"y":1},"o":{"x":0.257,"y":0.3},"n":"0p762_1_0p257_0p3","t":11,"s":[206.656,303.504,0],"e":[228,308,0],"to":[6.19302797317505,1.26945006847382,0],"ti":[-0.32227402925491,0,0]},{"t":20.3640127778053}]},"a":{"k":[0,0,0]},"s":{"k":[-100,100,100]}},"ao":0,"ip":10.0000003576279,"op":22.5608477592468,"st":-9.35224944353104,"bm":0,"sr":0.95000010728836},{"ddd":0,"ind":6,"ty":3,"nm":"Null smoke","parent":34,"ks":{"o":{"k":0},"r":{"k":26.915},"p":{"k":[{"i":{"x":0.587,"y":0.894},"o":{"x":0.264,"y":0.068},"n":"0p587_0p894_0p264_0p068","t":10,"s":[228.667,205.657,0],"e":[209.667,198.657,0],"to":[-0.5,-0.33333334326744,0],"ti":[-1.19691395759583,-0.40347543358803,0]},{"i":{"x":0.713,"y":1},"o":{"x":0.264,"y":0.336},"n":"0p713_1_0p264_0p336","t":12,"s":[209.667,198.657,0],"e":[223,204,0],"to":[4.62041616439819,1.55752575397491,0],"ti":[-0.48471575975418,0,0]},{"t":19.0271503602465}]},"a":{"k":[0,0,0]},"s":{"k":[-100,100,100]}},"ao":0,"ip":11.0000006233652,"op":22.5608497237166,"st":-9.35224822411935,"bm":0,"sr":0.95000010728836},{"ddd":0,"ind":7,"ty":4,"nm":"B blue layer 4 shadow","parent":34,"ks":{"o":{"k":5},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[6,3],[35.446,17.154],[1.39,-24.214],[-7,-13],[-11,-18],[-9,-3],[0,0],[0,0]],"o":[[-6,-3],[-35.446,-17.154],[-1.585,27.613],[7,13],[11,18],[9,3],[0,0],[0,0]],"v":[[-140,-282],[-94,-266],[-98,-118],[-80,-5],[-119,27],[-97,40],[-59,31],[-74,-216]],"c":true}],"e":[{"i":[[6,3],[0,-5],[-5,-22],[-7,-13],[-11,-18],[-9,-3],[0,0],[0,0]],"o":[[-6,-3],[0,5],[5,22],[7,13],[11,18],[9,3],[0,0],[0,0]],"v":[[-140,-282],[-143,-270],[-133,-137],[-118,-11],[-119,27],[-97,40],[-59,31],[-74,-216]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[6,3],[0,-5],[-5,-22],[-7,-13],[-11,-18],[-9,-3],[0,0],[0,0]],"o":[[-6,-3],[0,5],[5,22],[7,13],[11,18],[9,3],[0,0],[0,0]],"v":[[-140,-282],[-143,-270],[-133,-137],[-118,-11],[-119,27],[-97,40],[-59,31],[-74,-216]],"c":true}],"e":[{"i":[[6,3],[0,-5],[-5,-22],[-7,-13],[-11,-18],[-9,-3],[0,0],[0,0]],"o":[[-6,-3],[0,5],[5,22],[7,13],[11,18],[9,3],[0,0],[0,0]],"v":[[-140,-282],[-178,-280],[-159,-145],[-141,4],[-119,27],[-97,40],[-59,31],[-74,-216]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[6,3],[0,-5],[-5,-22],[-7,-13],[-11,-18],[-9,-3],[0,0],[0,0]],"o":[[-6,-3],[0,5],[5,22],[7,13],[11,18],[9,3],[0,0],[0,0]],"v":[[-140,-282],[-178,-280],[-159,-145],[-141,4],[-119,27],[-97,40],[-59,31],[-74,-216]],"c":true}],"e":[{"i":[[6,3],[0,-5],[-7,-32],[-7,-13],[-11,-18],[-9,-3],[0,0],[0,0]],"o":[[-6,-3],[0,5],[6.414,29.323],[7,13],[11,18],[9,3],[0,0],[0,0]],"v":[[-140,-282],[-158,-284],[-149,-204],[-118,6],[-119,27],[-97,40],[-59,31],[-74,-216]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[{"i":[[6,3],[0,-5],[-7,-32],[-7,-13],[-11,-18],[-9,-3],[0,0],[0,0]],"o":[[-6,-3],[0,5],[6.414,29.323],[7,13],[11,18],[9,3],[0,0],[0,0]],"v":[[-140,-282],[-158,-284],[-149,-204],[-118,6],[-119,27],[-97,40],[-59,31],[-74,-216]],"c":true}],"e":[{"i":[[6,3],[0,-5],[-0.752,-22.548],[-7,-13],[-11,-18],[-9,-3],[0,0],[0,0]],"o":[[-6,-3],[0,5],[1,30],[7,13],[11,18],[9,3],[0,0],[0,0]],"v":[[-106,-286],[-115,-276],[-99,-138],[-79,20],[-119,27],[-97,40],[-59,31],[-74,-216]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[6,3],[0,-5],[-0.752,-22.548],[-7,-13],[-11,-18],[-9,-3],[0,0],[0,0]],"o":[[-6,-3],[0,5],[1,30],[7,13],[11,18],[9,3],[0,0],[0,0]],"v":[[-106,-286],[-115,-276],[-99,-138],[-79,20],[-119,27],[-97,40],[-59,31],[-74,-216]],"c":true}],"e":[{"i":[[6,3],[0,-5],[-0.752,-22.548],[-7,-13],[-11,-18],[-9,-3],[0,0],[0,0]],"o":[[-6,-3],[0,5],[1,30],[7,13],[11,18],[9,3],[0,0],[0,0]],"v":[[-388,-298],[-397,-288],[-381,-150],[-361,8],[-401,15],[-379,28],[-341,19],[-356,-228]],"c":true}]},{"t":15}]},"o":{"k":100},"x":{"k":0},"nm":"Mask 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[3.637,14.035],[-6.562,-18.555],[0,0],[0,0],[0,0],[1.944,58.843],[5.869,46.436],[6.789,29.064]],"o":[[-3.783,-14.599],[12.079,34.152],[0,0],[0,0],[0,0],[-0.327,-9.886],[-2.413,-19.093],[-6.444,-27.588]],"v":[[-100.137,-270.035],[-81.079,-226.152],[-69.301,-172.891],[-65.375,-112.805],[-61.507,1.5],[-65.673,-78.614],[-71.869,-138.936],[-83.556,-205.912]],"c":true}],"e":[{"i":[[1.179,13.587],[-16.316,-11.006],[0,0],[0,0],[0,0],[51.329,28.838],[-0.631,46.436],[1.219,29.821]],"o":[[-1.863,-21.465],[33.579,22.652],[0,0],[0,0],[0,0],[-23.827,-13.386],[0.145,-10.708],[-0.944,-23.088]],"v":[[-180.137,-248.535],[-116.079,-227.152],[-76.301,-169.391],[-71.875,-105.305],[-64.007,0.5],[-127.173,-60.614],[-176.869,-121.436],[-176.556,-181.412]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[1.179,13.587],[-16.316,-11.006],[0,0],[0,0],[0,0],[51.329,28.838],[-0.631,46.436],[1.219,29.821]],"o":[[-1.863,-21.465],[33.579,22.652],[0,0],[0,0],[0,0],[-23.827,-13.386],[0.145,-10.708],[-0.944,-23.088]],"v":[[-180.137,-248.535],[-116.079,-227.152],[-76.301,-169.391],[-71.875,-105.305],[-64.007,0.5],[-127.173,-60.614],[-176.869,-121.436],[-176.556,-181.412]],"c":true}],"e":[{"i":[[0.592,13.423],[-38.037,-14.907],[0,0],[0,0],[0,0],[62.482,13.5],[0.713,41.144],[-1.198,29.377]],"o":[[-0.911,-20.645],[31.01,12.153],[0,0],[0,0],[0,0],[-23.658,-5.112],[-0.183,-10.545],[1.875,-45.981]],"v":[[-210.589,-237.355],[-131.463,-229.593],[-83.507,-180.593],[-80.113,-142.106],[-67.007,-0.5],[-149.982,-38.5],[-213.713,-83.644],[-211.875,-154.519]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[0.592,13.423],[-38.037,-14.907],[0,0],[0,0],[0,0],[62.482,13.5],[0.713,41.144],[-1.198,29.377]],"o":[[-0.911,-20.645],[31.01,12.153],[0,0],[0,0],[0,0],[-23.658,-5.112],[-0.183,-10.545],[1.875,-45.981]],"v":[[-210.589,-237.355],[-131.463,-229.593],[-83.507,-180.593],[-80.113,-142.106],[-67.007,-0.5],[-149.982,-38.5],[-213.713,-83.644],[-211.875,-154.519]],"c":true}],"e":[{"i":[[0.592,13.423],[-38.037,-14.907],[0,0],[0,0],[0,0],[49.59,6.194],[2.062,42.216],[0.844,29.39]],"o":[[-0.911,-20.645],[31.01,12.153],[0,0],[0,0],[0,0],[-24.018,-3],[-0.514,-10.534],[-1.267,-44.142]],"v":[[-219.089,-243.355],[-134.463,-234.593],[-89.007,-187.093],[-84.613,-147.106],[-68.507,-0.5],[-150.482,-30.5],[-213.713,-68.644],[-215.875,-135.019]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[0.592,13.423],[-38.037,-14.907],[0,0],[0,0],[0,0],[49.59,6.194],[2.062,42.216],[0.844,29.39]],"o":[[-0.911,-20.645],[31.01,12.153],[0,0],[0,0],[0,0],[-24.018,-3],[-0.514,-10.534],[-1.267,-44.142]],"v":[[-219.089,-243.355],[-134.463,-234.593],[-89.007,-187.093],[-84.613,-147.106],[-68.507,-0.5],[-150.482,-30.5],[-213.713,-68.644],[-215.875,-135.019]],"c":true}],"e":[{"i":[[1.979,12.724],[-39.037,-17.407],[0,0],[0,0],[0,0],[49.482,7],[3.213,42.144],[3.102,33.18]],"o":[[-3.911,-25.145],[30.419,13.564],[0,0],[0,0],[0,0],[-13.966,-1.976],[-0.805,-10.559],[-4.66,-49.835]],"v":[[-212.089,-255.355],[-134.463,-244.093],[-94.007,-197.593],[-89.078,-155.355],[-71.007,-0.5],[-128.482,-21.5],[-193.213,-61.644],[-199.771,-135.061]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[{"i":[[1.979,12.724],[-39.037,-17.407],[0,0],[0,0],[0,0],[49.482,7],[3.213,42.144],[3.102,33.18]],"o":[[-3.911,-25.145],[30.419,13.564],[0,0],[0,0],[0,0],[-13.966,-1.976],[-0.805,-10.559],[-4.66,-49.835]],"v":[[-212.089,-255.355],[-134.463,-244.093],[-94.007,-197.593],[-89.078,-155.355],[-71.007,-0.5],[-128.482,-21.5],[-193.213,-61.644],[-199.771,-135.061]],"c":true}],"e":[{"i":[[2.758,12.578],[-32.825,-21.239],[0,0],[0,0],[0,0],[34.982,4],[4.713,44.644],[5.969,34.173]],"o":[[-8.911,-40.645],[27.963,18.093],[0,0],[0,0],[0,0],[-14.013,-1.602],[-1.112,-10.531],[-8.965,-51.327]],"v":[[-179.589,-262.855],[-130.463,-262.593],[-96.507,-222.593],[-91.042,-175.962],[-71.007,-5],[-104.982,-18.5],[-144.713,-64.144],[-156.854,-139.358]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[2.758,12.578],[-32.825,-21.239],[0,0],[0,0],[0,0],[34.982,4],[4.713,44.644],[5.969,34.173]],"o":[[-8.911,-40.645],[27.963,18.093],[0,0],[0,0],[0,0],[-14.013,-1.602],[-1.112,-10.531],[-8.965,-51.327]],"v":[[-179.589,-262.855],[-130.463,-262.593],[-96.507,-222.593],[-91.042,-175.962],[-71.007,-5],[-104.982,-18.5],[-144.713,-64.144],[-156.854,-139.358]],"c":true}],"e":[{"i":[[3.235,12.104],[-17.053,-18.911],[0,0],[3.05,20.138],[0,0],[9.544,-4.956],[7.213,61.144],[5.736,34.461]],"o":[[-17.411,-65.145],[14.963,16.593],[0,0],[-3.05,-20.138],[0,0],[-12.518,6.5],[-1.291,-10.944],[-8.055,-48.393]],"v":[[-114.589,-252.855],[-102.463,-279.593],[-83.507,-232.593],[-72.45,-171.862],[-51.007,-10.5],[-65.982,-2.5],[-81.213,-61.644],[-90.187,-141.969]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[3.235,12.104],[-17.053,-18.911],[0,0],[3.05,20.138],[0,0],[9.544,-4.956],[7.213,61.144],[5.736,34.461]],"o":[[-17.411,-65.145],[14.963,16.593],[0,0],[-3.05,-20.138],[0,0],[-12.518,6.5],[-1.291,-10.944],[-8.055,-48.393]],"v":[[-114.589,-252.855],[-102.463,-279.593],[-83.507,-232.593],[-72.45,-171.862],[-51.007,-10.5],[-65.982,-2.5],[-81.213,-61.644],[-90.187,-141.969]],"c":true}],"e":[{"i":[[2.706,12.59],[7.96,-31.787],[0,0],[0,0],[0,0],[-52.303,4.363],[12.713,79.144],[5.528,26.028]],"o":[[-18.411,-85.645],[-8.037,32.093],[0,0],[0,0],[0,0],[17.982,-1.5],[-1.68,-10.456],[-8.303,-39.094]],"v":[[-41.089,-239.855],[-57.963,-260.093],[-97.507,-214.093],[-93.006,-168.319],[-76.507,-0.5],[-13.982,-7.5],[-8.213,-83.644],[-20.289,-143.83]],"c":true}]},{"t":16}]},"nm":"B"},{"ty":"mm","mm":3,"nm":"Merge Paths 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0,0.01,0.24,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":10,"op":15,"st":0,"bm":10,"sr":1},{"ddd":0,"ind":8,"ty":4,"nm":"B blue layer 4","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[3.637,14.035],[-6.562,-18.555],[0,0],[0,0],[0,0],[1.944,58.843],[5.869,46.436],[6.789,29.064]],"o":[[-3.783,-14.599],[12.079,34.152],[0,0],[0,0],[0,0],[-0.327,-9.886],[-2.413,-19.093],[-6.444,-27.588]],"v":[[-100.137,-270.035],[-81.079,-226.152],[-69.301,-172.891],[-65.375,-112.805],[-61.507,1.5],[-65.673,-78.614],[-71.869,-138.936],[-83.556,-205.912]],"c":true}],"e":[{"i":[[1.179,13.587],[-16.316,-11.006],[0,0],[0,0],[0,0],[51.329,28.838],[-0.631,46.436],[1.219,29.821]],"o":[[-1.863,-21.465],[33.579,22.652],[0,0],[0,0],[0,0],[-23.827,-13.386],[0.145,-10.708],[-0.944,-23.088]],"v":[[-180.137,-248.535],[-116.079,-227.152],[-76.301,-169.391],[-71.875,-105.305],[-64.007,0.5],[-127.173,-60.614],[-176.869,-121.436],[-176.556,-181.412]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[1.179,13.587],[-16.316,-11.006],[0,0],[0,0],[0,0],[51.329,28.838],[-0.631,46.436],[1.219,29.821]],"o":[[-1.863,-21.465],[33.579,22.652],[0,0],[0,0],[0,0],[-23.827,-13.386],[0.145,-10.708],[-0.944,-23.088]],"v":[[-180.137,-248.535],[-116.079,-227.152],[-76.301,-169.391],[-71.875,-105.305],[-64.007,0.5],[-127.173,-60.614],[-176.869,-121.436],[-176.556,-181.412]],"c":true}],"e":[{"i":[[0.592,13.423],[-38.037,-14.907],[0,0],[0,0],[0,0],[62.482,13.5],[0.713,41.144],[-1.198,29.377]],"o":[[-0.911,-20.645],[31.01,12.153],[0,0],[0,0],[0,0],[-23.658,-5.112],[-0.183,-10.545],[1.875,-45.981]],"v":[[-210.589,-237.355],[-131.463,-229.593],[-83.507,-180.593],[-80.113,-142.106],[-67.007,-0.5],[-149.982,-38.5],[-213.713,-83.644],[-211.875,-154.519]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[0.592,13.423],[-38.037,-14.907],[0,0],[0,0],[0,0],[62.482,13.5],[0.713,41.144],[-1.198,29.377]],"o":[[-0.911,-20.645],[31.01,12.153],[0,0],[0,0],[0,0],[-23.658,-5.112],[-0.183,-10.545],[1.875,-45.981]],"v":[[-210.589,-237.355],[-131.463,-229.593],[-83.507,-180.593],[-80.113,-142.106],[-67.007,-0.5],[-149.982,-38.5],[-213.713,-83.644],[-211.875,-154.519]],"c":true}],"e":[{"i":[[0.592,13.423],[-38.037,-14.907],[0,0],[0,0],[0,0],[49.59,6.194],[2.062,42.216],[0.844,29.39]],"o":[[-0.911,-20.645],[31.01,12.153],[0,0],[0,0],[0,0],[-24.018,-3],[-0.514,-10.534],[-1.267,-44.142]],"v":[[-219.089,-243.355],[-134.463,-234.593],[-89.007,-187.093],[-84.613,-147.106],[-68.507,-0.5],[-150.482,-30.5],[-213.713,-68.644],[-215.875,-135.019]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[0.592,13.423],[-38.037,-14.907],[0,0],[0,0],[0,0],[49.59,6.194],[2.062,42.216],[0.844,29.39]],"o":[[-0.911,-20.645],[31.01,12.153],[0,0],[0,0],[0,0],[-24.018,-3],[-0.514,-10.534],[-1.267,-44.142]],"v":[[-219.089,-243.355],[-134.463,-234.593],[-89.007,-187.093],[-84.613,-147.106],[-68.507,-0.5],[-150.482,-30.5],[-213.713,-68.644],[-215.875,-135.019]],"c":true}],"e":[{"i":[[1.979,12.724],[-39.037,-17.407],[0,0],[0,0],[0,0],[49.482,7],[3.213,42.144],[3.102,33.18]],"o":[[-3.911,-25.145],[30.419,13.564],[0,0],[0,0],[0,0],[-13.966,-1.976],[-0.805,-10.559],[-4.66,-49.835]],"v":[[-212.089,-255.355],[-134.463,-244.093],[-94.007,-197.593],[-89.078,-155.355],[-71.007,-0.5],[-128.482,-21.5],[-193.213,-61.644],[-199.771,-135.061]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[{"i":[[1.979,12.724],[-39.037,-17.407],[0,0],[0,0],[0,0],[49.482,7],[3.213,42.144],[3.102,33.18]],"o":[[-3.911,-25.145],[30.419,13.564],[0,0],[0,0],[0,0],[-13.966,-1.976],[-0.805,-10.559],[-4.66,-49.835]],"v":[[-212.089,-255.355],[-134.463,-244.093],[-94.007,-197.593],[-89.078,-155.355],[-71.007,-0.5],[-128.482,-21.5],[-193.213,-61.644],[-199.771,-135.061]],"c":true}],"e":[{"i":[[2.758,12.578],[-32.825,-21.239],[0,0],[0,0],[0,0],[34.982,4],[4.713,44.644],[5.969,34.173]],"o":[[-8.911,-40.645],[27.963,18.093],[0,0],[0,0],[0,0],[-14.013,-1.602],[-1.112,-10.531],[-8.965,-51.327]],"v":[[-179.589,-262.855],[-130.463,-262.593],[-96.507,-222.593],[-91.042,-175.962],[-71.007,-5],[-104.982,-18.5],[-144.713,-64.144],[-156.854,-139.358]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[2.758,12.578],[-32.825,-21.239],[0,0],[0,0],[0,0],[34.982,4],[4.713,44.644],[5.969,34.173]],"o":[[-8.911,-40.645],[27.963,18.093],[0,0],[0,0],[0,0],[-14.013,-1.602],[-1.112,-10.531],[-8.965,-51.327]],"v":[[-179.589,-262.855],[-130.463,-262.593],[-96.507,-222.593],[-91.042,-175.962],[-71.007,-5],[-104.982,-18.5],[-144.713,-64.144],[-156.854,-139.358]],"c":true}],"e":[{"i":[[3.235,12.104],[-17.053,-18.911],[0,0],[-3.55,-45.138],[0,0],[4.732,-1.5],[7.213,61.144],[5.736,34.461]],"o":[[-17.411,-65.145],[14.963,16.593],[0,0],[3.168,40.285],[0,0],[-13.445,4.262],[-1.291,-10.944],[-8.055,-48.393]],"v":[[-114.589,-252.855],[-102.463,-279.593],[-83.507,-232.593],[-73.45,-164.862],[-61.507,-3],[-68.482,-0.25],[-81.213,-61.644],[-90.187,-141.969]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[3.235,12.104],[-17.053,-18.911],[0,0],[-3.55,-45.138],[0,0],[4.732,-1.5],[7.213,61.144],[5.736,34.461]],"o":[[-17.411,-65.145],[14.963,16.593],[0,0],[3.168,40.285],[0,0],[-13.445,4.262],[-1.291,-10.944],[-8.055,-48.393]],"v":[[-114.589,-252.855],[-102.463,-279.593],[-83.507,-232.593],[-73.45,-164.862],[-61.507,-3],[-68.482,-0.25],[-81.213,-61.644],[-90.187,-141.969]],"c":true}],"e":[{"i":[[2.706,12.59],[7.96,-31.787],[0,0],[0,0],[0,0],[-52.303,4.363],[12.713,79.144],[5.528,26.028]],"o":[[-18.411,-85.645],[-8.037,32.093],[0,0],[0,0],[0,0],[17.982,-1.5],[-1.68,-10.456],[-8.303,-39.094]],"v":[[-41.089,-239.855],[-57.963,-260.093],[-97.507,-214.093],[-93.006,-168.319],[-76.507,-0.5],[-13.982,-7.5],[-8.213,-83.644],[-20.289,-143.83]],"c":true}]},{"t":16}]},"nm":"B"},{"ty":"mm","mm":3,"nm":"Merge Paths 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0,0.01,0.24,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":9,"op":16,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":9,"ty":4,"nm":"B blue layer 3","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[20.982,16]],"o":[[0,0],[0,0],[-11.216,-8.553]],"v":[[-78.007,-37.093],[-72.507,0.5],[-84.482,-27.5]],"c":true}},"nm":"B"},{"ty":"fl","fillEnabled":true,"c":{"k":[0,0.01,0.24,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":14,"op":15,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":10,"ty":3,"nm":"Null smoke","parent":34,"ks":{"o":{"k":0},"r":{"k":26.875},"p":{"k":[{"i":{"x":0.663,"y":0.933},"o":{"x":0.334,"y":0.066},"n":"0p663_0p933_0p334_0p066","t":9,"s":[269.705,292.352,0],"e":[250.705,285.352,0],"to":[-0.5,-0.33333334326744,0],"ti":[-0.60055363178253,-0.48845085501671,0]},{"i":{"x":0.685,"y":1},"o":{"x":0.334,"y":0.228},"n":"0p685_1_0p334_0p228","t":11,"s":[250.705,285.352,0],"e":[276,308,0],"to":[9.00963115692139,7.32784128189087,0],"ti":[-0.48727434873581,0,0]},{"t":22.5016243755817}]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"ip":10.0000001490116,"op":24.4098640978336,"st":-11.1670814454556,"bm":0,"sr":0.99000012874603},{"ddd":0,"ind":11,"ty":3,"nm":"Null smoke","parent":34,"ks":{"o":{"k":0},"r":{"k":-21.596},"p":{"k":[{"i":{"x":0.58,"y":0.829},"o":{"x":0.278,"y":0.114},"n":"0p58_0p829_0p278_0p114","t":9,"s":[217.311,226.631,0],"e":[198.311,219.631,0],"to":[0,0,0],"ti":[-3.21461248397827,3.18686246871948,0]},{"i":{"x":0.726,"y":1},"o":{"x":0.278,"y":0.371},"n":"0p726_1_0p278_0p371","t":11,"s":[198.311,219.631,0],"e":[223,195,0],"to":[8.70546340942383,-8.63031387329102,0],"ti":[-1.45636057853699,0,0]},{"t":21.5996034443378}]},"a":{"k":[0,0,0]},"s":{"k":[100,-100,100]}},"ao":0,"ip":10.0000001490116,"op":24.4098640978336,"st":-11.1670814454556,"bm":0,"sr":0.99000012874603},{"ddd":0,"ind":12,"ty":3,"nm":"null smoke","parent":34,"ks":{"o":{"k":0},"r":{"k":16.774},"p":{"k":[{"i":{"x":0.577,"y":0.847},"o":{"x":0.284,"y":0.103},"n":"0p577_0p847_0p284_0p103","t":9,"s":[261.182,221.23,0],"e":[242.182,214.23,0],"to":[0,0,0],"ti":[-3.1457417011261,0.33704376220703,0]},{"i":{"x":0.697,"y":1},"o":{"x":0.284,"y":0.405},"n":"0p697_1_0p284_0p405","t":11,"s":[242.182,214.23,0],"e":[263,212,0],"to":[7.38616800308228,-0.79137516021729,0],"ti":[-3.2727952003479,0.3506566286087,0]},{"t":18.9003057777882}]},"a":{"k":[0,0,0]},"s":{"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":9.9,"s":[100,100,100],"e":[122,122,100]},{"t":20.7004822790623}]}},"ao":0,"ip":10.0000001490116,"op":24.4098640978336,"st":-11.1670814454556,"bm":0,"sr":0.99000012874603},{"ddd":0,"ind":13,"ty":3,"nm":"Null 26","parent":34,"ks":{"o":{"k":0},"r":{"k":-11.144},"p":{"k":[{"i":{"x":0.573,"y":0.757},"o":{"x":0.294,"y":0.167},"n":"0p573_0p757_0p294_0p167","t":9,"s":[267.556,276.269,0],"e":[248.556,269.269,0],"to":[0,0,0],"ti":[-4.61654806137085,2.50612592697144,0]},{"i":{"x":0.699,"y":1},"o":{"x":0.294,"y":0.426},"n":"0p699_1_0p294_0p426","t":11,"s":[248.556,269.269,0],"e":[273,256,0],"to":[9.04685020446777,-4.91114711761475,0],"ti":[-3.86238408088684,2.09672284126282,0]},{"t":17.4897499382496}]},"a":{"k":[0,0,0]},"s":{"k":[100,-100,100]}},"ao":0,"ip":10.0000001490116,"op":21.5510979294777,"st":-11.1670814454556,"bm":0,"sr":0.99000012874603},{"ddd":0,"ind":14,"ty":4,"nm":"B blue layer 2","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":1,"s":[{"i":[[-1.24,5.104],[0.198,1.892],[0,0],[0,0],[0,0],[-0.804,-2.124],[0.343,-5.77],[-1.462,6.091]],"o":[[1.049,-4.319],[-0.297,-2.847],[0,0],[0,0],[0,0],[0.386,1.02],[0.093,-2.77],[0.816,-3.399]],"v":[[-22.001,-21.919],[-20.415,-30.453],[-22.49,-31.96],[-27.759,-19.513],[-32.548,3.666],[-29.736,4.974],[-28.593,7.27],[-25.53,-6.238]],"c":true}],"e":[{"i":[[-1.032,7.042],[1.743,2.453],[0,0],[0,0],[0,0],[-3.84,-2.615],[-0.171,-9.84],[-1.176,8.399]],"o":[[0.873,-5.959],[-2.624,-3.692],[0,0],[0,0],[0,0],[1.845,1.256],[2.053,-13.363],[0.656,-4.688]],"v":[[-19.587,-23.915],[-19.439,-35.572],[-25.627,-39.562],[-30.78,-20.831],[-37.568,3.797],[-29.696,6.026],[-25.346,16.687],[-22.197,-8.804]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":2,"s":[{"i":[[-1.032,7.042],[1.743,2.453],[0,0],[0,0],[0,0],[-3.84,-2.615],[-0.171,-9.84],[-1.176,8.399]],"o":[[0.873,-5.959],[-2.624,-3.692],[0,0],[0,0],[0,0],[1.845,1.256],[2.053,-13.363],[0.656,-4.688]],"v":[[-19.587,-23.915],[-19.439,-35.572],[-25.627,-39.562],[-30.78,-20.831],[-37.568,3.797],[-29.696,6.026],[-25.346,16.687],[-22.197,-8.804]],"c":true}],"e":[{"i":[[-0.284,9.426],[4.036,2.935],[0,0],[0,0],[0,0],[-7.528,-4.291],[-1.775,-13.654],[-0.23,11.234]],"o":[[0.241,-7.977],[-6.074,-4.417],[0,0],[0,0],[0,0],[6.05,3.449],[0.725,-17.904],[0.128,-6.27]],"v":[[-11.741,-32.523],[-17.536,-48.935],[-30.445,-51.84],[-35.936,-26.366],[-41.832,3.676],[-25.8,5.301],[-13.725,26.404],[-12.128,-11.48]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":3,"s":[{"i":[[-0.284,9.426],[4.036,2.935],[0,0],[0,0],[0,0],[-7.528,-4.291],[-1.775,-13.654],[-0.23,11.234]],"o":[[0.241,-7.977],[-6.074,-4.417],[0,0],[0,0],[0,0],[6.05,3.449],[0.725,-17.904],[0.128,-6.27]],"v":[[-11.741,-32.523],[-17.536,-48.935],[-30.445,-51.84],[-35.936,-26.366],[-41.832,3.676],[-25.8,5.301],[-13.725,26.404],[-12.128,-11.48]],"c":true}],"e":[{"i":[[0.512,9.416],[12.036,5.935],[0,0],[0,0],[0,0],[-10.429,-3.711],[-2.775,-15.904],[1.665,20.58]],"o":[[-0.759,-13.977],[-8.794,-4.337],[0,0],[0,0],[0,0],[11.8,4.199],[-1.275,-18.904],[-0.872,-10.77]],"v":[[-0.741,-42.023],[-15.536,-66.935],[-34.945,-68.34],[-40.936,-29.866],[-44.332,2.676],[-17.3,1.801],[4.275,28.904],[1.372,-13.73]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":4,"s":[{"i":[[0.512,9.416],[12.036,5.935],[0,0],[0,0],[0,0],[-10.429,-3.711],[-2.775,-15.904],[1.665,20.58]],"o":[[-0.759,-13.977],[-8.794,-4.337],[0,0],[0,0],[0,0],[11.8,4.199],[-1.275,-18.904],[-0.872,-10.77]],"v":[[-0.741,-42.023],[-15.536,-66.935],[-34.945,-68.34],[-40.936,-29.866],[-44.332,2.676],[-17.3,1.801],[4.275,28.904],[1.372,-13.73]],"c":true}],"e":[{"i":[[2.118,9.189],[13.168,3.137],[0,0],[0,0],[0,0],[-21.716,-6.523],[-3.775,-12.404],[3.122,20.41]],"o":[[-4.259,-18.477],[-14.964,-3.565],[0,0],[0,0],[0,0],[22.3,6.699],[-2.775,-18.904],[-2.872,-18.77]],"v":[[12.259,-65.023],[-14.536,-90.935],[-40.945,-84.34],[-43.436,-51.366],[-48.332,2.176],[-5.8,-5.199],[26.275,28.404],[16.372,-36.73]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[2.118,9.189],[13.168,3.137],[0,0],[0,0],[0,0],[-21.716,-6.523],[-3.775,-12.404],[3.122,20.41]],"o":[[-4.259,-18.477],[-14.964,-3.565],[0,0],[0,0],[0,0],[22.3,6.699],[-2.775,-18.904],[-2.872,-18.77]],"v":[[12.259,-65.023],[-14.536,-90.935],[-40.945,-84.34],[-43.436,-51.366],[-48.332,2.176],[-5.8,-5.199],[26.275,28.404],[16.372,-36.73]],"c":true}],"e":[{"i":[[2.118,9.189],[13.533,0.289],[0,0],[0,0],[0,0],[-54.798,-20.458],[-2.775,-7.904],[4.128,20.23]],"o":[[-4.259,-18.477],[-26.464,-0.565],[0,0],[0,0],[0,0],[23.3,8.699],[-0.275,-14.904],[-3.009,-14.743]],"v":[[31.259,-89.523],[-7.536,-119.435],[-46.945,-102.34],[-48.936,-56.366],[-51.832,2.676],[27.2,-12.199],[54.275,22.904],[40.872,-40.73]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[2.118,9.189],[13.533,0.289],[0,0],[0,0],[0,0],[-54.798,-20.458],[-2.775,-7.904],[4.128,20.23]],"o":[[-4.259,-18.477],[-26.464,-0.565],[0,0],[0,0],[0,0],[23.3,8.699],[-0.275,-14.904],[-3.009,-14.743]],"v":[[31.259,-89.523],[-7.536,-119.435],[-46.945,-102.34],[-48.936,-56.366],[-51.832,2.676],[27.2,-12.199],[54.275,22.904],[40.872,-40.73]],"c":true}],"e":[{"i":[[2.741,9.023],[35.536,-9.065],[0,0],[0,0],[0,0],[-58.271,5.078],[7.345,30.067],[7.32,28.81]],"o":[[-6.003,-19.766],[-38.857,9.912],[0,0],[0,0],[0,0],[72.3,-6.301],[-3.275,-13.404],[-3.372,-13.27]],"v":[[77.259,-132.023],[-1.536,-158.935],[-53.945,-122.34],[-54.436,-66.866],[-54.832,1.176],[23.7,-42.699],[103.275,-31.596],[86.872,-91.23]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[2.741,9.023],[35.536,-9.065],[0,0],[0,0],[0,0],[-58.271,5.078],[7.345,30.067],[7.32,28.81]],"o":[[-6.003,-19.766],[-38.857,9.912],[0,0],[0,0],[0,0],[72.3,-6.301],[-3.275,-13.404],[-3.372,-13.27]],"v":[[77.259,-132.023],[-1.536,-158.935],[-53.945,-122.34],[-54.436,-66.866],[-54.832,1.176],[23.7,-42.699],[103.275,-31.596],[86.872,-91.23]],"c":true}],"e":[{"i":[[2.692,12.158],[14.536,-18.065],[0,0],[0,0],[0,0],[-45.886,36.274],[7.225,30.096],[8.494,27.268]],"o":[[-3.759,-16.977],[-25.14,31.242],[0,0],[0,0],[0,0],[35.8,-28.301],[-2.392,-9.964],[-7.872,-25.27]],"v":[[26.759,-245.023],[-23.036,-213.935],[-60.445,-143.84],[-59.936,-99.866],[-58.832,1.676],[-5.3,-69.199],[64.275,-130.596],[40.372,-201.73]],"c":true}]},{"t":8}]},"nm":"B"},{"ty":"mm","mm":2,"nm":"Merge Paths 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0,0.01,0.24,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":1,"op":9,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":15,"ty":4,"nm":"B orange layer 3 shadow 2","parent":34,"ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":16,"s":[5],"e":[40]},{"t":22}]},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3,10],[4,29]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[3,-10],[-4,-29]],"v":[[-99,-265],[-121,-264],[-133,-219],[-107,10],[-84,33],[-49,13],[-62,-108]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3,10],[4,29]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[3,-10],[-4,-29]],"v":[[-70,-275],[-92,-274],[-104,-229],[-78,0],[-55,23],[-20,3],[-33,-118]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3,10],[4,29]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[3,-10],[-4,-29]],"v":[[-70,-275],[-92,-274],[-104,-229],[-78,0],[-55,23],[-20,3],[-33,-118]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3,10],[4,29]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[3,-10],[-4,-29]],"v":[[-25.168,-278.566],[-92,-274],[-104,-229],[-78,0],[-55,23],[22.285,0.453],[-2.338,-146.717]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3,10],[4,29]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[3,-10],[-4,-29]],"v":[[-25.168,-278.566],[-92,-274],[-104,-229],[-78,0],[-55,23],[22.285,0.453],[-2.338,-146.717]],"c":true}],"e":[{"i":[[16.753,14.499],[0,0],[0,0],[0,0],[0,0],[-3,10],[21.96,31.036]],"o":[[-16.753,-14.499],[0,0],[0,0],[0,0],[0,0],[3,-10],[2.96,-38.964]],"v":[[19.753,-282.501],[-92,-274],[-104,-229],[-78,0],[-55,23],[64.682,-1.606],[39.04,-141.036]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17,"s":[{"i":[[16.753,14.499],[0,0],[0,0],[0,0],[0,0],[-3,10],[21.96,31.036]],"o":[[-16.753,-14.499],[0,0],[0,0],[0,0],[0,0],[3,-10],[2.96,-38.964]],"v":[[19.753,-282.501],[-92,-274],[-104,-229],[-78,0],[-55,23],[64.682,-1.606],[39.04,-141.036]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3,10],[24,29]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[3,-10],[12,-31]],"v":[[67,-296],[-92,-274],[-104,-229],[-78,0],[-55,23],[110,9],[57,-143]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3,10],[24,29]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[3,-10],[12,-31]],"v":[[67,-296],[-92,-274],[-104,-229],[-78,0],[-55,23],[110,9],[57,-143]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3,10],[4,29]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[3,-10],[-4,-29]],"v":[[60,-286],[-92,-274],[-104,-229],[-78,0],[-55,23],[104,4],[115,-139]],"c":true}]},{"t":19}]},"o":{"k":100},"x":{"k":0},"nm":"Mask 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[1.759,26.037],[0.589,9.355],[8.227,2.939],[0,0],[0,0],[-17.127,-8.576],[-4.287,-8.856]],"o":[[-1.241,-21.463],[-0.382,-6.06],[-9.537,-3.407],[0,0],[0,0],[11.982,6],[-1.787,-26.856]],"v":[[-12.759,-43.537],[-15.089,-87.355],[-25.963,-106.093],[-46.507,-102.093],[-51.007,3.5],[-21.482,0],[-8.713,17.856]],"c":true}],"e":[{"i":[[5.759,37.537],[2.589,11.855],[15.486,1.354],[0,0],[0,0],[-32.518,-11.5],[-4.287,-8.856]],"o":[[-6.241,-39.963],[-1.296,-5.932],[-27.537,-2.407],[0,0],[0,0],[21.489,7.6],[-3.287,-20.356]],"v":[[19.241,-49.037],[9.411,-111.855],[-17.463,-132.593],[-53.507,-120.593],[-55.507,2.5],[4.018,-9.5],[30.287,16.856]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[5.759,37.537],[2.589,11.855],[15.486,1.354],[0,0],[0,0],[-32.518,-11.5],[-4.287,-8.856]],"o":[[-6.241,-39.963],[-1.296,-5.932],[-27.537,-2.407],[0,0],[0,0],[21.489,7.6],[-3.287,-20.356]],"v":[[19.241,-49.037],[9.411,-111.855],[-17.463,-132.593],[-53.507,-120.593],[-55.507,2.5],[4.018,-9.5],[30.287,16.856]],"c":true}],"e":[{"i":[[7.259,32.537],[3.089,9.355],[44.113,-3.3],[0,0],[0,0],[-53.018,1],[3.713,19.144]],"o":[[-7.241,-38.463],[-1.904,-5.765],[-48.037,3.593],[0,0],[0,0],[60.505,-1.141],[-3.159,-16.288]],"v":[[83.741,-85.537],[72.411,-142.355],[4.537,-172.593],[-61.007,-138.593],[-59.007,1.5],[28.018,-33.5],[99.787,-17.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[7.259,32.537],[3.089,9.355],[44.113,-3.3],[0,0],[0,0],[-53.018,1],[3.713,19.144]],"o":[[-7.241,-38.463],[-1.904,-5.765],[-48.037,3.593],[0,0],[0,0],[60.505,-1.141],[-3.159,-16.288]],"v":[[83.741,-85.537],[72.411,-142.355],[4.537,-172.593],[-61.007,-138.593],[-59.007,1.5],[28.018,-33.5],[99.787,-17.644]],"c":true}],"e":[{"i":[[10.759,29.037],[4.471,5.31],[29.463,-40.407],[0,0],[0,0],[-43.018,23.5],[3.713,19.144]],"o":[[-7.241,-21.963],[-3.911,-4.645],[-7.99,10.959],[0,0],[0,0],[44.538,-24.331],[-3.159,-16.288]],"v":[[45.241,-195.037],[21.411,-260.355],[-37.963,-203.593],[-68.007,-155.093],[-61.507,3],[11.018,-60],[71.287,-111.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[10.759,29.037],[4.471,5.31],[29.463,-40.407],[0,0],[0,0],[-43.018,23.5],[3.713,19.144]],"o":[[-7.241,-21.963],[-3.911,-4.645],[-7.99,10.959],[0,0],[0,0],[44.538,-24.331],[-3.159,-16.288]],"v":[[45.241,-195.037],[21.411,-260.355],[-37.963,-203.593],[-68.007,-155.093],[-61.507,3],[11.018,-60],[71.287,-111.144]],"c":true}],"e":[{"i":[[4.259,17.037],[1.335,6.812],[2.963,-14.907],[0,0],[0,0],[-7.018,26.5],[3.713,19.144]],"o":[[-2.741,-12.463],[-0.911,-4.645],[-2.644,13.302],[0,0],[0,0],[9.673,-36.528],[-3.159,-16.288]],"v":[[-52.759,-171.537],[-67.089,-217.855],[-68.963,-189.593],[-76.007,-170.093],[-64.007,1.5],[-40.982,-50],[-39.713,-115.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[4.259,17.037],[1.335,6.812],[2.963,-14.907],[0,0],[0,0],[-7.018,26.5],[3.713,19.144]],"o":[[-2.741,-12.463],[-0.911,-4.645],[-2.644,13.302],[0,0],[0,0],[9.673,-36.528],[-3.159,-16.288]],"v":[[-52.759,-171.537],[-67.089,-217.855],[-68.963,-189.593],[-76.007,-170.093],[-64.007,1.5],[-40.982,-50],[-39.713,-115.644]],"c":true}],"e":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-73.259,-145.537],[-82.089,-178.355],[-90.963,-174.593],[-83.007,-159.593],[-66.507,1.5],[-58.482,-36],[-71.713,-94.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-73.259,-145.537],[-82.089,-178.355],[-90.963,-174.593],[-83.007,-159.593],[-66.507,1.5],[-58.482,-36],[-71.713,-94.644]],"c":true}],"e":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-79.259,-143.537],[-82.089,-178.355],[-90.963,-174.593],[-87.007,-159.593],[-69.007,-0.5],[-59.482,-33.5],[-71.713,-94.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-79.259,-143.537],[-82.089,-178.355],[-90.963,-174.593],[-87.007,-159.593],[-69.007,-0.5],[-59.482,-33.5],[-71.713,-94.644]],"c":true}],"e":[{"i":[[2.759,16.037],[1.089,6.855],[3.15,2.884],[0,0],[0,0],[0.024,23.358],[2.213,13.144]],"o":[[-0.741,-7.963],[-2.92,-18.378],[-1.537,-1.407],[0,0],[0,0],[-0.018,-17.5],[-2.755,-16.362]],"v":[[-74.759,-145.537],[-84.089,-185.855],[-90.463,-219.593],[-95.507,-208.093],[-71.507,0.5],[-51.482,-32],[-65.713,-94.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[{"i":[[2.759,16.037],[1.089,6.855],[3.15,2.884],[0,0],[0,0],[0.024,23.358],[2.213,13.144]],"o":[[-0.741,-7.963],[-2.92,-18.378],[-1.537,-1.407],[0,0],[0,0],[-0.018,-17.5],[-2.755,-16.362]],"v":[[-74.759,-145.537],[-84.089,-185.855],[-90.463,-219.593],[-95.507,-208.093],[-71.507,0.5],[-51.482,-32],[-65.713,-94.144]],"c":true}],"e":[{"i":[[8.509,31.287],[6.813,20.34],[4.963,-11.907],[0,0],[0,0],[-17.018,16],[9.213,50.737]],"o":[[-6.241,-30.463],[-5.911,-17.645],[-6.06,14.54],[0,0],[0,0],[9.992,-9.394],[-3.787,-20.856]],"v":[[-51.259,-158.537],[-71.589,-238.355],[-80.963,-225.093],[-96.007,-202.593],[-72.007,0],[-29.482,-25.5],[-34.213,-88.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[8.509,31.287],[6.813,20.34],[4.963,-11.907],[0,0],[0,0],[-17.018,16],[9.213,50.737]],"o":[[-6.241,-30.463],[-5.911,-17.645],[-6.06,14.54],[0,0],[0,0],[9.992,-9.394],[-3.787,-20.856]],"v":[[-51.259,-158.537],[-71.589,-238.355],[-80.963,-225.093],[-96.007,-202.593],[-72.007,0],[-29.482,-25.5],[-34.213,-88.644]],"c":true}],"e":[{"i":[[8.509,31.287],[4.772,20.913],[18.771,-35.562],[0,0],[0,0],[-35.294,7.846],[9.713,50.644]],"o":[[-6.241,-30.463],[-20.911,-91.645],[-14.037,26.593],[0,0],[0,0],[44.982,-10],[-4.991,-26.022]],"v":[[-6.759,-153.537],[-31.089,-246.855],[-59.463,-246.093],[-97.007,-208.093],[-74.007,0.5],[-18.482,-13.5],[11.287,-77.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[8.509,31.287],[4.772,20.913],[18.771,-35.562],[0,0],[0,0],[-35.294,7.846],[9.713,50.644]],"o":[[-6.241,-30.463],[-20.911,-91.645],[-14.037,26.593],[0,0],[0,0],[44.982,-10],[-4.991,-26.022]],"v":[[-6.759,-153.537],[-31.089,-246.855],[-59.463,-246.093],[-97.007,-208.093],[-74.007,0.5],[-18.482,-13.5],[11.287,-77.644]],"c":true}],"e":[{"i":[[9.759,27.037],[3.56,21.153],[32.297,-24.991],[0,0],[0,0],[-51.855,8.105],[16.213,81.144]],"o":[[-6.241,-30.463],[-13.911,-82.645],[-39.537,30.593],[0,0],[0,0],[47.982,-7.5],[-5.191,-25.982]],"v":[[27.241,-164.537],[14.411,-234.855],[-36.963,-250.093],[-97.507,-214.093],[-76.507,-0.5],[0.018,-9],[47.287,-86.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[9.759,27.037],[3.56,21.153],[32.297,-24.991],[0,0],[0,0],[-51.855,8.105],[16.213,81.144]],"o":[[-6.241,-30.463],[-13.911,-82.645],[-39.537,30.593],[0,0],[0,0],[47.982,-7.5],[-5.191,-25.982]],"v":[[27.241,-164.537],[14.411,-234.855],[-36.963,-250.093],[-97.507,-214.093],[-76.507,-0.5],[0.018,-9],[47.287,-86.144]],"c":true}],"e":[{"i":[[9.759,25.037],[3.589,22.355],[46.963,-21.907],[0,0],[0,0],[-52.444,2.078],[5.958,46.178]],"o":[[-1.241,-27.463],[-10.199,-63.523],[-35.55,16.583],[0,0],[0,0],[50.482,-2],[-5.787,-44.856]],"v":[[49.241,-157.537],[45.911,-224.855],[-24.963,-243.593],[-95.007,-217.593],[-76.507,-0.5],[33.018,-7],[72.287,-80.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[9.759,25.037],[3.589,22.355],[46.963,-21.907],[0,0],[0,0],[-52.444,2.078],[5.958,46.178]],"o":[[-1.241,-27.463],[-10.199,-63.523],[-35.55,16.583],[0,0],[0,0],[50.482,-2],[-5.787,-44.856]],"v":[[49.241,-157.537],[45.911,-224.855],[-24.963,-243.593],[-95.007,-217.593],[-76.507,-0.5],[33.018,-7],[72.287,-80.144]],"c":true}],"e":[{"i":[[15.759,21.037],[2.598,21.286],[45.963,-10.657],[0,0],[0,0],[-28.018,1],[5.463,51.394]],"o":[[3.509,-24.213],[-4.161,-64.895],[-38.537,12.343],[0,0],[0,0],[59.982,2.5],[-3.676,-32.795]],"v":[[59.241,-140.287],[64.661,-201.605],[-11.463,-241.343],[-91.757,-220.343],[-77.257,-0.75],[28.018,-4.5],[84.787,-81.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[15.759,21.037],[2.598,21.286],[45.963,-10.657],[0,0],[0,0],[-28.018,1],[5.463,51.394]],"o":[[3.509,-24.213],[-4.161,-64.895],[-38.537,12.343],[0,0],[0,0],[59.982,2.5],[-3.676,-32.795]],"v":[[59.241,-140.287],[64.661,-201.605],[-11.463,-241.343],[-91.757,-220.343],[-77.257,-0.75],[28.018,-4.5],[84.787,-81.144]],"c":true}],"e":[{"i":[[21.759,21.537],[2.058,21.352],[37.463,-5.907],[0,0],[0,0],[0,0],[4.713,56.644]],"o":[[10.259,-19.963],[-5.411,-56.145],[-38.749,6.11],[0,0],[0,0],[45.482,1],[-2.393,-28.758]],"v":[[62.241,-133.037],[74.911,-192.355],[-0.463,-236.593],[-88.507,-223.093],[-78.007,-1],[23.018,-2.5],[92.287,-70.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[21.759,21.537],[2.058,21.352],[37.463,-5.907],[0,0],[0,0],[0,0],[4.713,56.644]],"o":[[10.259,-19.963],[-5.411,-56.145],[-38.749,6.11],[0,0],[0,0],[45.482,1],[-2.393,-28.758]],"v":[[62.241,-133.037],[74.911,-192.355],[-0.463,-236.593],[-88.507,-223.093],[-78.007,-1],[23.018,-2.5],[92.287,-70.144]],"c":true}],"e":[{"i":[[24.259,15.287],[0.089,22.605],[44.213,-5.407],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[11.759,-13.713],[-2.705,-47.408],[-24.787,2.093],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[60.241,-127.787],[78.411,-180.605],[5.787,-233.593],[-86.007,-223.593],[-78.007,-1],[24.768,-0.25],[93.287,-66.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[24.259,15.287],[0.089,22.605],[44.213,-5.407],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[11.759,-13.713],[-2.705,-47.408],[-24.787,2.093],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[60.241,-127.787],[78.411,-180.605],[5.787,-233.593],[-86.007,-223.593],[-78.007,-1],[24.768,-0.25],[93.287,-66.644]],"c":true}],"e":[{"i":[[21.149,8.762],[0,21.451],[38.463,-2.407],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[15.106,-9.064],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[59.241,-124.037],[79.411,-176.855],[14.537,-231.593],[-83.507,-224.093],[-78.007,-1],[23.518,0],[94.787,-61.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[21.149,8.762],[0,21.451],[38.463,-2.407],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[15.106,-9.064],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[59.241,-124.037],[79.411,-176.855],[14.537,-231.593],[-83.507,-224.093],[-78.007,-1],[23.518,0],[94.787,-61.144]],"c":true}],"e":[{"i":[[21.149,8.762],[0.749,21.438],[38.114,-1.203],[0,0],[0,0],[0,0],[-0.152,41.088]],"o":[[15.106,-9.064],[-1.411,-40.395],[0,0],[0,0],[0,0],[41.473,0.5],[0.106,-28.93]],"v":[[56.741,-120.537],[79.911,-168.605],[13.287,-226.593],[-79.507,-225.843],[-78.757,-0.5],[21.518,-0.5],[92.037,-63.144]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":22.253,"s":[{"i":[[21.149,8.762],[0.749,21.438],[38.114,-1.203],[0,0],[0,0],[0,0],[-0.152,41.088]],"o":[[15.106,-9.064],[-1.411,-40.395],[0,0],[0,0],[0,0],[41.473,0.5],[0.106,-28.93]],"v":[[56.741,-120.537],[79.911,-168.605],[13.287,-226.593],[-79.507,-225.843],[-78.757,-0.5],[21.518,-0.5],[92.037,-63.144]],"c":true}],"e":[{"i":[[21.149,8.762],[0,21.451],[37.766,0],[0,0],[0,0],[0,0],[0,41.089]],"o":[[15.106,-9.064],[0,-38.672],[0,0],[0,0],[0,0],[37.463,0],[0,-29.004]],"v":[[55.241,-119.037],[79.411,-164.355],[12.037,-226.593],[-79.507,-226.593],[-79.507,0],[23.518,0],[90.287,-63.144]],"c":true}]},{"t":24}]},"nm":"B"},{"ind":1,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[-1.324,-11.343],[1.439,-10.814],[0,0],[0,0],[0,0]],"o":[[1.603,13.739],[0,0],[0,0],[0,0],[3.112,-1.724]],"v":[[-264.603,-181.739],[-262.939,-148.686],[-267.481,-144.898],[-273.154,-192.488],[-271.112,-195.276]],"c":true}],"e":[{"i":[[-1.324,-11.343],[1.439,-10.814],[0,0],[0,0],[0,0]],"o":[[1.603,13.739],[0,0],[0,0],[0,0],[3.112,-1.724]],"v":[[-60.603,-177.739],[-58.939,-144.686],[-63.481,-140.898],[-69.154,-188.488],[-67.112,-191.276]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[-1.324,-11.343],[1.439,-10.814],[0,0],[0,0],[0,0]],"o":[[1.603,13.739],[0,0],[0,0],[0,0],[3.112,-1.724]],"v":[[-60.603,-177.739],[-58.939,-144.686],[-63.481,-140.898],[-69.154,-188.488],[-67.112,-191.276]],"c":true}],"e":[{"i":[[-1.324,-11.343],[4.939,-8.814],[0,0],[0,0],[0,0]],"o":[[1.603,13.739],[0,0],[0,0],[0,0],[6.612,-3.724]],"v":[[-40.603,-178.239],[-44.439,-146.186],[-54.981,-139.898],[-61.654,-188.488],[-53.612,-193.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[-1.324,-11.343],[4.939,-8.814],[0,0],[0,0],[0,0]],"o":[[1.603,13.739],[0,0],[0,0],[0,0],[6.612,-3.724]],"v":[[-40.603,-178.239],[-44.439,-146.186],[-54.981,-139.898],[-61.654,-188.488],[-53.612,-193.776]],"c":true}],"e":[{"i":[[-0.897,-12.761],[7.439,-4.314],[0,0],[0,0],[0,0]],"o":[[1.049,14.93],[0,0],[0,0],[0,0],[11.112,-4.224]],"v":[[-22.103,-175.239],[-29.439,-144.686],[-49.481,-139.398],[-53.654,-189.988],[-40.112,-193.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[-0.897,-12.761],[7.439,-4.314],[0,0],[0,0],[0,0]],"o":[[1.049,14.93],[0,0],[0,0],[0,0],[11.112,-4.224]],"v":[[-22.103,-175.239],[-29.439,-144.686],[-49.481,-139.398],[-53.654,-189.988],[-40.112,-193.776]],"c":true}],"e":[{"i":[[-2.295,-14.342],[14.615,-7.06],[0,0],[0,0],[0,0]],"o":[[2.247,14.043],[0,0],[0,0],[0,0],[12.13,-2.72]],"v":[[-5.749,-174.483],[-19.893,-141.684],[-44.391,-137.152],[-47.397,-188.494],[-26.535,-193.276]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[-2.295,-14.342],[14.615,-7.06],[0,0],[0,0],[0,0]],"o":[[2.247,14.043],[0,0],[0,0],[0,0],[12.13,-2.72]],"v":[[-5.749,-174.483],[-19.893,-141.684],[-44.391,-137.152],[-47.397,-188.494],[-26.535,-193.276]],"c":true}],"e":[{"i":[[-2.154,-14.341],[17.439,-2.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[13.612,-1.724]],"v":[[7.397,-171.739],[-12.939,-138.686],[-40.981,-136.898],[-43.154,-187.488],[-17.112,-191.276]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[-2.154,-14.341],[17.439,-2.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[13.612,-1.724]],"v":[[7.397,-171.739],[-12.939,-138.686],[-40.981,-136.898],[-43.154,-187.488],[-17.112,-191.276]],"c":true}],"e":[{"i":[[-1.534,-14.395],[17.58,-1.876],[0,0],[0,0],[0,0]],"o":[[1.502,14.095],[0,0],[0,0],[0,0],[14.855,-1.149]],"v":[[16.498,-167.632],[-5.273,-137.269],[-37.547,-135.91],[-39.662,-187.025],[-9.278,-190.383]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[-1.534,-14.395],[17.58,-1.876],[0,0],[0,0],[0,0]],"o":[[1.502,14.095],[0,0],[0,0],[0,0],[14.855,-1.149]],"v":[[16.498,-167.632],[-5.273,-137.269],[-37.547,-135.91],[-39.662,-187.025],[-9.278,-190.383]],"c":true}],"e":[{"i":[[-0.767,-14.448],[16.041,-0.938],[0,0],[0,0],[0,0]],"o":[[0.751,14.147],[0,0],[0,0],[0,0],[14.678,-0.575]],"v":[[24.639,-165.025],[-0.738,-135.852],[-35.862,-134.172],[-36.92,-187.061],[-2.991,-188.991]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[-0.767,-14.448],[16.041,-0.938],[0,0],[0,0],[0,0]],"o":[[0.751,14.147],[0,0],[0,0],[0,0],[14.678,-0.575]],"v":[[24.639,-165.025],[-0.738,-135.852],[-35.862,-134.172],[-36.92,-187.061],[-2.991,-188.991]],"c":true}],"e":[{"i":[[0,-14.502],[14.502,0],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[28.279,-161.917],[4.297,-134.435],[-34.177,-133.435],[-34.177,-187.098],[3.297,-187.598]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":21.376,"s":[{"i":[[0,-14.502],[14.502,0],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[28.279,-161.917],[4.297,-134.435],[-34.177,-133.435],[-34.177,-187.098],[3.297,-187.598]],"c":true}],"e":[{"i":[[0,-14.502],[16.037,-0.065],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[31.613,-160.417],[4.963,-133.935],[-33.677,-133.268],[-33.677,-186.264],[4.297,-186.598]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":22.253,"s":[{"i":[[0,-14.502],[16.037,-0.065],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[31.613,-160.417],[4.963,-133.935],[-33.677,-133.268],[-33.677,-186.264],[4.297,-186.598]],"c":true}],"e":[{"i":[[0,-14.502],[14.502,0],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[32.279,-158.917],[6.297,-132.935],[-32.677,-132.935],[-32.677,-184.598],[6.297,-184.598]],"c":true}]},{"t":24}]},"nm":"B"},{"ind":2,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[2.931,-7.005],[0,0],[0,0],[0,0],[-1.768,-14.461]],"o":[[0,0],[0,0],[0,0],[2.931,0.658],[2.344,19.176]],"v":[[-262.931,-52.995],[-268.677,-50.995],[-274.177,-102.658],[-270.431,-105.658],[-264.344,-84.176]],"c":true}],"e":[{"i":[[2.931,-7.005],[0,0],[0,0],[0,0],[-1.768,-14.461]],"o":[[0,0],[0,0],[0,0],[2.931,0.658],[2.344,19.176]],"v":[[-46.431,-52.995],[-52.177,-50.995],[-57.677,-102.658],[-53.931,-105.658],[-47.844,-84.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[2.931,-7.005],[0,0],[0,0],[0,0],[-1.768,-14.461]],"o":[[0,0],[0,0],[0,0],[2.931,0.658],[2.344,19.176]],"v":[[-46.431,-52.995],[-52.177,-50.995],[-57.677,-102.658],[-53.931,-105.658],[-47.844,-84.176]],"c":true}],"e":[{"i":[[7.931,-3.505],[0,0],[0,0],[0,0],[-1.768,-14.461]],"o":[[0,0],[0,0],[0,0],[8.931,-2.842],[2.344,19.176]],"v":[[-32.931,-51.495],[-45.677,-48.995],[-51.177,-100.658],[-40.431,-104.658],[-26.844,-83.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[7.931,-3.505],[0,0],[0,0],[0,0],[-1.768,-14.461]],"o":[[0,0],[0,0],[0,0],[8.931,-2.842],[2.344,19.176]],"v":[[-32.931,-51.495],[-45.677,-48.995],[-51.177,-100.658],[-40.431,-104.658],[-26.844,-83.176]],"c":true}],"e":[{"i":[[13.431,-1.505],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[8.931,-2.842],[2.589,13.962]],"v":[[-19.931,-48.995],[-40.177,-47.495],[-45.177,-99.158],[-24.431,-103.158],[-6.344,-82.676]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[13.431,-1.505],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[8.931,-2.842],[2.589,13.962]],"v":[[-19.931,-48.995],[-40.177,-47.495],[-45.177,-99.158],[-24.431,-103.158],[-6.344,-82.676]],"c":true}],"e":[{"i":[[14.118,-0.752],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[15.681,-1.092],[2.589,13.962]],"v":[[-7.681,-46.495],[-37.927,-44.245],[-41.177,-97.908],[-14.681,-101.408],[8.906,-80.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[14.118,-0.752],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[15.681,-1.092],[2.589,13.962]],"v":[[-7.681,-46.495],[-37.927,-44.245],[-41.177,-97.908],[-14.681,-101.408],[8.906,-80.176]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[14.804,0],[2.589,13.962]],"v":[[-0.931,-44.995],[-36.677,-42.995],[-39.177,-97.158],[-3.931,-100.158],[21.656,-76.676]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[14.804,0],[2.589,13.962]],"v":[[-0.931,-44.995],[-36.677,-42.995],[-39.177,-97.158],[-3.931,-100.158],[21.656,-76.676]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-1.77,-14.283]],"o":[[0,0],[0,0],[0,0],[14.804,0],[1.726,14.041]],"v":[[7.902,-44.495],[-34.344,-44.162],[-35.844,-96.158],[5.902,-97.825],[29.989,-73.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-1.77,-14.283]],"o":[[0,0],[0,0],[0,0],[14.804,0],[1.726,14.041]],"v":[[7.902,-44.495],[-34.344,-44.162],[-35.844,-96.158],[5.902,-97.825],[29.989,-73.176]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-0.885,-14.241]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0.863,14.121]],"v":[[12.235,-42.745],[-33.761,-42.329],[-34.761,-95.908],[11.235,-96.242],[37.322,-69.926]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-0.885,-14.241]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0.863,14.121]],"v":[[12.235,-42.745],[-33.761,-42.329],[-34.761,-95.908],[11.235,-96.242],[37.322,-69.926]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[13.569,-41.995],[-32.677,-41.995],[-33.677,-95.658],[16.569,-94.658],[40.656,-68.676]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":21.376,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[13.569,-41.995],[-32.677,-41.995],[-33.677,-95.658],[16.569,-94.658],[40.656,-68.676]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[16.569,-41.995],[-32.677,-41.995],[-32.677,-93.658],[16.569,-94.658],[43.156,-67.676]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":22.253,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[16.569,-41.995],[-32.677,-41.995],[-32.677,-93.658],[16.569,-94.658],[43.156,-67.676]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[16.569,-41.995],[-32.677,-41.995],[-32.677,-93.658],[16.569,-93.658],[43.156,-67.676]],"c":true}]},{"t":24}]},"nm":"B"},{"ty":"mm","mm":1,"nm":"Merge Paths 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0.86,0.86,0.86,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":15,"op":23,"st":0,"bm":10,"sr":1},{"ddd":0,"ind":16,"ty":4,"nm":"B orange layer 3 shadow","parent":34,"ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":16,"s":[5],"e":[40]},{"t":22}]},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3,10],[4,29]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[3,-10],[-4,-29]],"v":[[-99,-265],[-121,-264],[-133,-219],[-107,10],[-84,33],[-49,13],[-62,-108]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3,10],[4,29]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[3,-10],[-4,-29]],"v":[[-70,-275],[-92,-274],[-104,-229],[-78,0],[-55,23],[-20,3],[-33,-118]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3,10],[4,29]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[3,-10],[-4,-29]],"v":[[-70,-275],[-92,-274],[-104,-229],[-78,0],[-55,23],[-20,3],[-33,-118]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3,10],[4,29]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[3,-10],[-4,-29]],"v":[[-25.168,-278.566],[-92,-274],[-104,-229],[-78,0],[-55,23],[22.285,0.453],[-2.338,-146.717]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3,10],[4,29]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[3,-10],[-4,-29]],"v":[[-25.168,-278.566],[-92,-274],[-104,-229],[-78,0],[-55,23],[22.285,0.453],[-2.338,-146.717]],"c":true}],"e":[{"i":[[16.753,14.499],[0,0],[0,0],[0,0],[0,0],[-3,10],[21.96,31.036]],"o":[[-16.753,-14.499],[0,0],[0,0],[0,0],[0,0],[3,-10],[2.96,-38.964]],"v":[[19.753,-282.501],[-92,-274],[-104,-229],[-78,0],[-55,23],[64.682,-1.606],[39.04,-141.036]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17,"s":[{"i":[[16.753,14.499],[0,0],[0,0],[0,0],[0,0],[-3,10],[21.96,31.036]],"o":[[-16.753,-14.499],[0,0],[0,0],[0,0],[0,0],[3,-10],[2.96,-38.964]],"v":[[19.753,-282.501],[-92,-274],[-104,-229],[-78,0],[-55,23],[64.682,-1.606],[39.04,-141.036]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3,10],[24,29]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[3,-10],[12,-31]],"v":[[67,-296],[-92,-274],[-104,-229],[-78,0],[-55,23],[110,9],[57,-143]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3,10],[24,29]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[3,-10],[12,-31]],"v":[[67,-296],[-92,-274],[-104,-229],[-78,0],[-55,23],[110,9],[57,-143]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3,10],[4,29]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[3,-10],[-4,-29]],"v":[[60,-286],[-92,-274],[-104,-229],[-78,0],[-55,23],[104,4],[115,-139]],"c":true}]},{"t":19}]},"o":{"k":100},"x":{"k":0},"nm":"Mask 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[1.759,26.037],[0.589,9.355],[8.227,2.939],[0,0],[0,0],[-17.127,-8.576],[-4.287,-8.856]],"o":[[-1.241,-21.463],[-0.382,-6.06],[-9.537,-3.407],[0,0],[0,0],[11.982,6],[-1.787,-26.856]],"v":[[-12.759,-43.537],[-15.089,-87.355],[-25.963,-106.093],[-46.507,-102.093],[-51.007,3.5],[-21.482,0],[-8.713,17.856]],"c":true}],"e":[{"i":[[5.759,37.537],[2.589,11.855],[15.486,1.354],[0,0],[0,0],[-32.518,-11.5],[-4.287,-8.856]],"o":[[-6.241,-39.963],[-1.296,-5.932],[-27.537,-2.407],[0,0],[0,0],[21.489,7.6],[-3.287,-20.356]],"v":[[19.241,-49.037],[9.411,-111.855],[-17.463,-132.593],[-53.507,-120.593],[-55.507,2.5],[4.018,-9.5],[30.287,16.856]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[5.759,37.537],[2.589,11.855],[15.486,1.354],[0,0],[0,0],[-32.518,-11.5],[-4.287,-8.856]],"o":[[-6.241,-39.963],[-1.296,-5.932],[-27.537,-2.407],[0,0],[0,0],[21.489,7.6],[-3.287,-20.356]],"v":[[19.241,-49.037],[9.411,-111.855],[-17.463,-132.593],[-53.507,-120.593],[-55.507,2.5],[4.018,-9.5],[30.287,16.856]],"c":true}],"e":[{"i":[[7.259,32.537],[3.089,9.355],[44.113,-3.3],[0,0],[0,0],[-53.018,1],[3.713,19.144]],"o":[[-7.241,-38.463],[-1.904,-5.765],[-48.037,3.593],[0,0],[0,0],[60.505,-1.141],[-3.159,-16.288]],"v":[[83.741,-85.537],[72.411,-142.355],[4.537,-172.593],[-61.007,-138.593],[-59.007,1.5],[28.018,-33.5],[99.787,-17.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[7.259,32.537],[3.089,9.355],[44.113,-3.3],[0,0],[0,0],[-53.018,1],[3.713,19.144]],"o":[[-7.241,-38.463],[-1.904,-5.765],[-48.037,3.593],[0,0],[0,0],[60.505,-1.141],[-3.159,-16.288]],"v":[[83.741,-85.537],[72.411,-142.355],[4.537,-172.593],[-61.007,-138.593],[-59.007,1.5],[28.018,-33.5],[99.787,-17.644]],"c":true}],"e":[{"i":[[10.759,29.037],[4.471,5.31],[29.463,-40.407],[0,0],[0,0],[-43.018,23.5],[3.713,19.144]],"o":[[-7.241,-21.963],[-3.911,-4.645],[-7.99,10.959],[0,0],[0,0],[44.538,-24.331],[-3.159,-16.288]],"v":[[45.241,-195.037],[21.411,-260.355],[-37.963,-203.593],[-68.007,-155.093],[-61.507,3],[11.018,-60],[71.287,-111.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[10.759,29.037],[4.471,5.31],[29.463,-40.407],[0,0],[0,0],[-43.018,23.5],[3.713,19.144]],"o":[[-7.241,-21.963],[-3.911,-4.645],[-7.99,10.959],[0,0],[0,0],[44.538,-24.331],[-3.159,-16.288]],"v":[[45.241,-195.037],[21.411,-260.355],[-37.963,-203.593],[-68.007,-155.093],[-61.507,3],[11.018,-60],[71.287,-111.144]],"c":true}],"e":[{"i":[[4.259,17.037],[1.335,6.812],[2.963,-14.907],[0,0],[0,0],[-7.018,26.5],[3.713,19.144]],"o":[[-2.741,-12.463],[-0.911,-4.645],[-2.644,13.302],[0,0],[0,0],[9.673,-36.528],[-3.159,-16.288]],"v":[[-52.759,-171.537],[-67.089,-217.855],[-68.963,-189.593],[-76.007,-170.093],[-64.007,1.5],[-40.982,-50],[-39.713,-115.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[4.259,17.037],[1.335,6.812],[2.963,-14.907],[0,0],[0,0],[-7.018,26.5],[3.713,19.144]],"o":[[-2.741,-12.463],[-0.911,-4.645],[-2.644,13.302],[0,0],[0,0],[9.673,-36.528],[-3.159,-16.288]],"v":[[-52.759,-171.537],[-67.089,-217.855],[-68.963,-189.593],[-76.007,-170.093],[-64.007,1.5],[-40.982,-50],[-39.713,-115.644]],"c":true}],"e":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-73.259,-145.537],[-82.089,-178.355],[-90.963,-174.593],[-83.007,-159.593],[-66.507,1.5],[-58.482,-36],[-71.713,-94.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-73.259,-145.537],[-82.089,-178.355],[-90.963,-174.593],[-83.007,-159.593],[-66.507,1.5],[-58.482,-36],[-71.713,-94.644]],"c":true}],"e":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-79.259,-143.537],[-82.089,-178.355],[-90.963,-174.593],[-87.007,-159.593],[-69.007,-0.5],[-59.482,-33.5],[-71.713,-94.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-79.259,-143.537],[-82.089,-178.355],[-90.963,-174.593],[-87.007,-159.593],[-69.007,-0.5],[-59.482,-33.5],[-71.713,-94.644]],"c":true}],"e":[{"i":[[2.759,16.037],[1.089,6.855],[3.15,2.884],[0,0],[0,0],[0.024,23.358],[2.213,13.144]],"o":[[-0.741,-7.963],[-2.92,-18.378],[-1.537,-1.407],[0,0],[0,0],[-0.018,-17.5],[-2.755,-16.362]],"v":[[-74.759,-145.537],[-84.089,-185.855],[-90.463,-219.593],[-95.507,-208.093],[-71.507,0.5],[-51.482,-32],[-65.713,-94.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[{"i":[[2.759,16.037],[1.089,6.855],[3.15,2.884],[0,0],[0,0],[0.024,23.358],[2.213,13.144]],"o":[[-0.741,-7.963],[-2.92,-18.378],[-1.537,-1.407],[0,0],[0,0],[-0.018,-17.5],[-2.755,-16.362]],"v":[[-74.759,-145.537],[-84.089,-185.855],[-90.463,-219.593],[-95.507,-208.093],[-71.507,0.5],[-51.482,-32],[-65.713,-94.144]],"c":true}],"e":[{"i":[[8.509,31.287],[6.813,20.34],[4.963,-11.907],[0,0],[0,0],[-17.018,16],[9.213,50.737]],"o":[[-6.241,-30.463],[-5.911,-17.645],[-6.06,14.54],[0,0],[0,0],[9.992,-9.394],[-3.787,-20.856]],"v":[[-51.259,-158.537],[-71.589,-238.355],[-80.963,-225.093],[-96.007,-202.593],[-72.007,0],[-29.482,-25.5],[-34.213,-88.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[8.509,31.287],[6.813,20.34],[4.963,-11.907],[0,0],[0,0],[-17.018,16],[9.213,50.737]],"o":[[-6.241,-30.463],[-5.911,-17.645],[-6.06,14.54],[0,0],[0,0],[9.992,-9.394],[-3.787,-20.856]],"v":[[-51.259,-158.537],[-71.589,-238.355],[-80.963,-225.093],[-96.007,-202.593],[-72.007,0],[-29.482,-25.5],[-34.213,-88.644]],"c":true}],"e":[{"i":[[8.509,31.287],[4.772,20.913],[18.771,-35.562],[0,0],[0,0],[-35.294,7.846],[9.713,50.644]],"o":[[-6.241,-30.463],[-20.911,-91.645],[-14.037,26.593],[0,0],[0,0],[44.982,-10],[-4.991,-26.022]],"v":[[-6.759,-153.537],[-31.089,-246.855],[-59.463,-246.093],[-97.007,-208.093],[-74.007,0.5],[-18.482,-13.5],[11.287,-77.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[8.509,31.287],[4.772,20.913],[18.771,-35.562],[0,0],[0,0],[-35.294,7.846],[9.713,50.644]],"o":[[-6.241,-30.463],[-20.911,-91.645],[-14.037,26.593],[0,0],[0,0],[44.982,-10],[-4.991,-26.022]],"v":[[-6.759,-153.537],[-31.089,-246.855],[-59.463,-246.093],[-97.007,-208.093],[-74.007,0.5],[-18.482,-13.5],[11.287,-77.644]],"c":true}],"e":[{"i":[[9.759,27.037],[3.56,21.153],[32.297,-24.991],[0,0],[0,0],[-51.855,8.105],[16.213,81.144]],"o":[[-6.241,-30.463],[-13.911,-82.645],[-39.537,30.593],[0,0],[0,0],[47.982,-7.5],[-5.191,-25.982]],"v":[[27.241,-164.537],[14.411,-234.855],[-36.963,-250.093],[-97.507,-214.093],[-76.507,-0.5],[0.018,-9],[47.287,-86.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[9.759,27.037],[3.56,21.153],[32.297,-24.991],[0,0],[0,0],[-51.855,8.105],[16.213,81.144]],"o":[[-6.241,-30.463],[-13.911,-82.645],[-39.537,30.593],[0,0],[0,0],[47.982,-7.5],[-5.191,-25.982]],"v":[[27.241,-164.537],[14.411,-234.855],[-36.963,-250.093],[-97.507,-214.093],[-76.507,-0.5],[0.018,-9],[47.287,-86.144]],"c":true}],"e":[{"i":[[9.759,25.037],[3.589,22.355],[46.963,-21.907],[0,0],[0,0],[-52.444,2.078],[5.958,46.178]],"o":[[-1.241,-27.463],[-10.199,-63.523],[-35.55,16.583],[0,0],[0,0],[50.482,-2],[-5.787,-44.856]],"v":[[49.241,-157.537],[45.911,-224.855],[-24.963,-243.593],[-95.007,-217.593],[-76.507,-0.5],[33.018,-7],[72.287,-80.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[9.759,25.037],[3.589,22.355],[46.963,-21.907],[0,0],[0,0],[-52.444,2.078],[5.958,46.178]],"o":[[-1.241,-27.463],[-10.199,-63.523],[-35.55,16.583],[0,0],[0,0],[50.482,-2],[-5.787,-44.856]],"v":[[49.241,-157.537],[45.911,-224.855],[-24.963,-243.593],[-95.007,-217.593],[-76.507,-0.5],[33.018,-7],[72.287,-80.144]],"c":true}],"e":[{"i":[[15.759,21.037],[2.598,21.286],[45.963,-10.657],[0,0],[0,0],[-28.018,1],[5.463,51.394]],"o":[[3.509,-24.213],[-4.161,-64.895],[-38.537,12.343],[0,0],[0,0],[59.982,2.5],[-3.676,-32.795]],"v":[[59.241,-140.287],[64.661,-201.605],[-11.463,-241.343],[-91.757,-220.343],[-77.257,-0.75],[28.018,-4.5],[84.787,-81.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[15.759,21.037],[2.598,21.286],[45.963,-10.657],[0,0],[0,0],[-28.018,1],[5.463,51.394]],"o":[[3.509,-24.213],[-4.161,-64.895],[-38.537,12.343],[0,0],[0,0],[59.982,2.5],[-3.676,-32.795]],"v":[[59.241,-140.287],[64.661,-201.605],[-11.463,-241.343],[-91.757,-220.343],[-77.257,-0.75],[28.018,-4.5],[84.787,-81.144]],"c":true}],"e":[{"i":[[21.759,21.537],[2.058,21.352],[37.463,-5.907],[0,0],[0,0],[0,0],[4.713,56.644]],"o":[[10.259,-19.963],[-5.411,-56.145],[-38.749,6.11],[0,0],[0,0],[45.482,1],[-2.393,-28.758]],"v":[[62.241,-133.037],[74.911,-192.355],[-0.463,-236.593],[-88.507,-223.093],[-78.007,-1],[23.018,-2.5],[92.287,-70.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[21.759,21.537],[2.058,21.352],[37.463,-5.907],[0,0],[0,0],[0,0],[4.713,56.644]],"o":[[10.259,-19.963],[-5.411,-56.145],[-38.749,6.11],[0,0],[0,0],[45.482,1],[-2.393,-28.758]],"v":[[62.241,-133.037],[74.911,-192.355],[-0.463,-236.593],[-88.507,-223.093],[-78.007,-1],[23.018,-2.5],[92.287,-70.144]],"c":true}],"e":[{"i":[[24.259,15.287],[0.089,22.605],[44.213,-5.407],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[11.759,-13.713],[-2.705,-47.408],[-24.787,2.093],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[60.241,-127.787],[78.411,-180.605],[5.787,-233.593],[-86.007,-223.593],[-78.007,-1],[24.768,-0.25],[93.287,-66.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[24.259,15.287],[0.089,22.605],[44.213,-5.407],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[11.759,-13.713],[-2.705,-47.408],[-24.787,2.093],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[60.241,-127.787],[78.411,-180.605],[5.787,-233.593],[-86.007,-223.593],[-78.007,-1],[24.768,-0.25],[93.287,-66.644]],"c":true}],"e":[{"i":[[21.149,8.762],[0,21.451],[38.463,-2.407],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[15.106,-9.064],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[59.241,-124.037],[79.411,-176.855],[14.537,-231.593],[-83.507,-224.093],[-78.007,-1],[23.518,0],[94.787,-61.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[21.149,8.762],[0,21.451],[38.463,-2.407],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[15.106,-9.064],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[59.241,-124.037],[79.411,-176.855],[14.537,-231.593],[-83.507,-224.093],[-78.007,-1],[23.518,0],[94.787,-61.144]],"c":true}],"e":[{"i":[[21.149,8.762],[0.749,21.438],[38.114,-1.203],[0,0],[0,0],[0,0],[-0.152,41.088]],"o":[[15.106,-9.064],[-1.411,-40.395],[0,0],[0,0],[0,0],[41.473,0.5],[0.106,-28.93]],"v":[[56.741,-120.537],[79.911,-168.605],[13.287,-226.593],[-79.507,-225.843],[-78.757,-0.5],[21.518,-0.5],[92.037,-63.144]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":22.253,"s":[{"i":[[21.149,8.762],[0.749,21.438],[38.114,-1.203],[0,0],[0,0],[0,0],[-0.152,41.088]],"o":[[15.106,-9.064],[-1.411,-40.395],[0,0],[0,0],[0,0],[41.473,0.5],[0.106,-28.93]],"v":[[56.741,-120.537],[79.911,-168.605],[13.287,-226.593],[-79.507,-225.843],[-78.757,-0.5],[21.518,-0.5],[92.037,-63.144]],"c":true}],"e":[{"i":[[21.149,8.762],[0,21.451],[37.766,0],[0,0],[0,0],[0,0],[0,41.089]],"o":[[15.106,-9.064],[0,-38.672],[0,0],[0,0],[0,0],[37.463,0],[0,-29.004]],"v":[[55.241,-119.037],[79.411,-164.355],[12.037,-226.593],[-79.507,-226.593],[-79.507,0],[23.518,0],[90.287,-63.144]],"c":true}]},{"t":24}]},"nm":"B"},{"ty":"fl","fillEnabled":true,"c":{"k":[0.86,0.86,0.86,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":14,"op":15,"st":0,"bm":10,"sr":1},{"ddd":0,"ind":17,"ty":4,"nm":"B orange layer 5","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[1.759,26.037],[0.589,9.355],[8.227,2.939],[0,0],[0,0],[-17.127,-8.576],[-4.287,-8.856]],"o":[[-1.241,-21.463],[-0.382,-6.06],[-9.537,-3.407],[0,0],[0,0],[11.982,6],[-1.787,-26.856]],"v":[[-12.759,-43.537],[-15.089,-87.355],[-25.963,-106.093],[-46.507,-102.093],[-51.007,3.5],[-21.482,0],[-8.713,17.856]],"c":true}],"e":[{"i":[[5.759,37.537],[2.589,11.855],[15.486,1.354],[0,0],[0,0],[-32.518,-11.5],[-4.287,-8.856]],"o":[[-6.241,-39.963],[-1.296,-5.932],[-27.537,-2.407],[0,0],[0,0],[21.489,7.6],[-3.287,-20.356]],"v":[[19.241,-49.037],[9.411,-111.855],[-17.463,-132.593],[-53.507,-120.593],[-55.507,2.5],[4.018,-9.5],[30.287,16.856]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[5.759,37.537],[2.589,11.855],[15.486,1.354],[0,0],[0,0],[-32.518,-11.5],[-4.287,-8.856]],"o":[[-6.241,-39.963],[-1.296,-5.932],[-27.537,-2.407],[0,0],[0,0],[21.489,7.6],[-3.287,-20.356]],"v":[[19.241,-49.037],[9.411,-111.855],[-17.463,-132.593],[-53.507,-120.593],[-55.507,2.5],[4.018,-9.5],[30.287,16.856]],"c":true}],"e":[{"i":[[7.259,32.537],[3.089,9.355],[44.113,-3.3],[0,0],[0,0],[-53.018,1],[3.713,19.144]],"o":[[-7.241,-38.463],[-1.904,-5.765],[-48.037,3.593],[0,0],[0,0],[60.505,-1.141],[-3.159,-16.288]],"v":[[83.741,-85.537],[72.411,-142.355],[4.537,-172.593],[-61.007,-138.593],[-59.007,1.5],[28.018,-33.5],[99.787,-17.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[7.259,32.537],[3.089,9.355],[44.113,-3.3],[0,0],[0,0],[-53.018,1],[3.713,19.144]],"o":[[-7.241,-38.463],[-1.904,-5.765],[-48.037,3.593],[0,0],[0,0],[60.505,-1.141],[-3.159,-16.288]],"v":[[83.741,-85.537],[72.411,-142.355],[4.537,-172.593],[-61.007,-138.593],[-59.007,1.5],[28.018,-33.5],[99.787,-17.644]],"c":true}],"e":[{"i":[[10.759,29.037],[4.471,5.31],[29.463,-40.407],[0,0],[0,0],[-43.018,23.5],[3.713,19.144]],"o":[[-7.241,-21.963],[-3.911,-4.645],[-7.99,10.959],[0,0],[0,0],[44.538,-24.331],[-3.159,-16.288]],"v":[[45.241,-195.037],[21.411,-260.355],[-37.963,-203.593],[-68.007,-155.093],[-61.507,3],[11.018,-60],[71.287,-111.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[10.759,29.037],[4.471,5.31],[29.463,-40.407],[0,0],[0,0],[-43.018,23.5],[3.713,19.144]],"o":[[-7.241,-21.963],[-3.911,-4.645],[-7.99,10.959],[0,0],[0,0],[44.538,-24.331],[-3.159,-16.288]],"v":[[45.241,-195.037],[21.411,-260.355],[-37.963,-203.593],[-68.007,-155.093],[-61.507,3],[11.018,-60],[71.287,-111.144]],"c":true}],"e":[{"i":[[4.259,17.037],[1.335,6.812],[2.963,-14.907],[0,0],[0,0],[-7.018,26.5],[3.713,19.144]],"o":[[-2.741,-12.463],[-0.911,-4.645],[-2.644,13.302],[0,0],[0,0],[9.673,-36.528],[-3.159,-16.288]],"v":[[-52.759,-171.537],[-67.089,-217.855],[-68.963,-189.593],[-76.007,-170.093],[-64.007,1.5],[-40.982,-50],[-39.713,-115.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[4.259,17.037],[1.335,6.812],[2.963,-14.907],[0,0],[0,0],[-7.018,26.5],[3.713,19.144]],"o":[[-2.741,-12.463],[-0.911,-4.645],[-2.644,13.302],[0,0],[0,0],[9.673,-36.528],[-3.159,-16.288]],"v":[[-52.759,-171.537],[-67.089,-217.855],[-68.963,-189.593],[-76.007,-170.093],[-64.007,1.5],[-40.982,-50],[-39.713,-115.644]],"c":true}],"e":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-73.259,-145.537],[-82.089,-178.355],[-90.963,-174.593],[-83.007,-159.593],[-66.507,1.5],[-58.482,-36],[-71.713,-94.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-73.259,-145.537],[-82.089,-178.355],[-90.963,-174.593],[-83.007,-159.593],[-66.507,1.5],[-58.482,-36],[-71.713,-94.644]],"c":true}],"e":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-79.259,-143.537],[-82.089,-178.355],[-90.963,-174.593],[-87.007,-159.593],[-69.007,-0.5],[-59.482,-33.5],[-71.713,-94.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-79.259,-143.537],[-82.089,-178.355],[-90.963,-174.593],[-87.007,-159.593],[-69.007,-0.5],[-59.482,-33.5],[-71.713,-94.644]],"c":true}],"e":[{"i":[[2.759,16.037],[1.089,6.855],[3.15,2.884],[0,0],[0,0],[0.024,23.358],[2.213,13.144]],"o":[[-0.741,-7.963],[-2.92,-18.378],[-1.537,-1.407],[0,0],[0,0],[-0.018,-17.5],[-2.755,-16.362]],"v":[[-74.759,-145.537],[-84.089,-185.855],[-90.463,-219.593],[-95.507,-208.093],[-71.507,0.5],[-51.482,-32],[-65.713,-94.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[{"i":[[2.759,16.037],[1.089,6.855],[3.15,2.884],[0,0],[0,0],[0.024,23.358],[2.213,13.144]],"o":[[-0.741,-7.963],[-2.92,-18.378],[-1.537,-1.407],[0,0],[0,0],[-0.018,-17.5],[-2.755,-16.362]],"v":[[-74.759,-145.537],[-84.089,-185.855],[-90.463,-219.593],[-95.507,-208.093],[-71.507,0.5],[-51.482,-32],[-65.713,-94.144]],"c":true}],"e":[{"i":[[8.509,31.287],[6.813,20.34],[4.963,-11.907],[0,0],[0,0],[-17.018,16],[9.213,50.737]],"o":[[-6.241,-30.463],[-5.911,-17.645],[-6.06,14.54],[0,0],[0,0],[9.992,-9.394],[-3.787,-20.856]],"v":[[-51.259,-158.537],[-71.589,-238.355],[-80.963,-225.093],[-96.007,-202.593],[-72.007,0],[-29.482,-25.5],[-34.213,-88.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[8.509,31.287],[6.813,20.34],[4.963,-11.907],[0,0],[0,0],[-17.018,16],[9.213,50.737]],"o":[[-6.241,-30.463],[-5.911,-17.645],[-6.06,14.54],[0,0],[0,0],[9.992,-9.394],[-3.787,-20.856]],"v":[[-51.259,-158.537],[-71.589,-238.355],[-80.963,-225.093],[-96.007,-202.593],[-72.007,0],[-29.482,-25.5],[-34.213,-88.644]],"c":true}],"e":[{"i":[[8.509,31.287],[4.772,20.913],[18.771,-35.562],[0,0],[0,0],[-35.294,7.846],[9.713,50.644]],"o":[[-6.241,-30.463],[-20.911,-91.645],[-14.037,26.593],[0,0],[0,0],[44.982,-10],[-4.991,-26.022]],"v":[[-6.759,-153.537],[-31.089,-246.855],[-59.463,-246.093],[-97.007,-208.093],[-74.007,0.5],[-18.482,-13.5],[11.287,-77.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[8.509,31.287],[4.772,20.913],[18.771,-35.562],[0,0],[0,0],[-35.294,7.846],[9.713,50.644]],"o":[[-6.241,-30.463],[-20.911,-91.645],[-14.037,26.593],[0,0],[0,0],[44.982,-10],[-4.991,-26.022]],"v":[[-6.759,-153.537],[-31.089,-246.855],[-59.463,-246.093],[-97.007,-208.093],[-74.007,0.5],[-18.482,-13.5],[11.287,-77.644]],"c":true}],"e":[{"i":[[9.759,27.037],[3.56,21.153],[32.297,-24.991],[0,0],[0,0],[-51.855,8.105],[16.213,81.144]],"o":[[-6.241,-30.463],[-13.911,-82.645],[-39.537,30.593],[0,0],[0,0],[47.982,-7.5],[-5.191,-25.982]],"v":[[27.241,-164.537],[14.411,-234.855],[-36.963,-250.093],[-97.507,-214.093],[-76.507,-0.5],[0.018,-9],[47.287,-86.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[9.759,27.037],[3.56,21.153],[32.297,-24.991],[0,0],[0,0],[-51.855,8.105],[16.213,81.144]],"o":[[-6.241,-30.463],[-13.911,-82.645],[-39.537,30.593],[0,0],[0,0],[47.982,-7.5],[-5.191,-25.982]],"v":[[27.241,-164.537],[14.411,-234.855],[-36.963,-250.093],[-97.507,-214.093],[-76.507,-0.5],[0.018,-9],[47.287,-86.144]],"c":true}],"e":[{"i":[[9.759,25.037],[3.589,22.355],[46.963,-21.907],[0,0],[0,0],[-52.444,2.078],[5.958,46.178]],"o":[[-1.241,-27.463],[-10.199,-63.523],[-35.55,16.583],[0,0],[0,0],[50.482,-2],[-5.787,-44.856]],"v":[[49.241,-157.537],[45.911,-224.855],[-24.963,-243.593],[-95.007,-217.593],[-76.507,-0.5],[33.018,-7],[72.287,-80.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[9.759,25.037],[3.589,22.355],[46.963,-21.907],[0,0],[0,0],[-52.444,2.078],[5.958,46.178]],"o":[[-1.241,-27.463],[-10.199,-63.523],[-35.55,16.583],[0,0],[0,0],[50.482,-2],[-5.787,-44.856]],"v":[[49.241,-157.537],[45.911,-224.855],[-24.963,-243.593],[-95.007,-217.593],[-76.507,-0.5],[33.018,-7],[72.287,-80.144]],"c":true}],"e":[{"i":[[15.759,21.037],[2.598,21.286],[45.963,-10.657],[0,0],[0,0],[-28.018,1],[5.463,51.394]],"o":[[3.509,-24.213],[-4.161,-64.895],[-38.537,12.343],[0,0],[0,0],[59.982,2.5],[-3.676,-32.795]],"v":[[59.241,-140.287],[64.661,-201.605],[-11.463,-241.343],[-91.757,-220.343],[-77.257,-0.75],[28.018,-4.5],[84.787,-81.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[15.759,21.037],[2.598,21.286],[45.963,-10.657],[0,0],[0,0],[-28.018,1],[5.463,51.394]],"o":[[3.509,-24.213],[-4.161,-64.895],[-38.537,12.343],[0,0],[0,0],[59.982,2.5],[-3.676,-32.795]],"v":[[59.241,-140.287],[64.661,-201.605],[-11.463,-241.343],[-91.757,-220.343],[-77.257,-0.75],[28.018,-4.5],[84.787,-81.144]],"c":true}],"e":[{"i":[[21.759,21.537],[2.058,21.352],[37.463,-5.907],[0,0],[0,0],[0,0],[4.713,56.644]],"o":[[10.259,-19.963],[-5.411,-56.145],[-38.749,6.11],[0,0],[0,0],[45.482,1],[-2.393,-28.758]],"v":[[62.241,-133.037],[74.911,-192.355],[-0.463,-236.593],[-88.507,-223.093],[-78.007,-1],[23.018,-2.5],[92.287,-70.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[21.759,21.537],[2.058,21.352],[37.463,-5.907],[0,0],[0,0],[0,0],[4.713,56.644]],"o":[[10.259,-19.963],[-5.411,-56.145],[-38.749,6.11],[0,0],[0,0],[45.482,1],[-2.393,-28.758]],"v":[[62.241,-133.037],[74.911,-192.355],[-0.463,-236.593],[-88.507,-223.093],[-78.007,-1],[23.018,-2.5],[92.287,-70.144]],"c":true}],"e":[{"i":[[24.259,15.287],[0.089,22.605],[44.213,-5.407],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[11.759,-13.713],[-2.705,-47.408],[-24.787,2.093],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[60.241,-127.787],[78.411,-180.605],[5.787,-233.593],[-86.007,-223.593],[-78.007,-1],[24.768,-0.25],[93.287,-66.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[24.259,15.287],[0.089,22.605],[44.213,-5.407],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[11.759,-13.713],[-2.705,-47.408],[-24.787,2.093],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[60.241,-127.787],[78.411,-180.605],[5.787,-233.593],[-86.007,-223.593],[-78.007,-1],[24.768,-0.25],[93.287,-66.644]],"c":true}],"e":[{"i":[[21.149,8.762],[0,21.451],[38.463,-2.407],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[15.106,-9.064],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[59.241,-124.037],[79.411,-176.855],[14.537,-231.593],[-83.507,-224.093],[-78.007,-1],[23.518,0],[94.787,-61.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[21.149,8.762],[0,21.451],[38.463,-2.407],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[15.106,-9.064],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[59.241,-124.037],[79.411,-176.855],[14.537,-231.593],[-83.507,-224.093],[-78.007,-1],[23.518,0],[94.787,-61.144]],"c":true}],"e":[{"i":[[21.149,8.762],[0.749,21.438],[38.114,-1.203],[0,0],[0,0],[0,0],[-0.152,41.088]],"o":[[15.106,-9.064],[-1.411,-40.395],[0,0],[0,0],[0,0],[41.473,0.5],[0.106,-28.93]],"v":[[56.741,-120.537],[79.911,-168.605],[13.287,-226.593],[-79.507,-225.843],[-78.757,-0.5],[21.518,-0.5],[92.037,-63.144]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":22.253,"s":[{"i":[[21.149,8.762],[0.749,21.438],[38.114,-1.203],[0,0],[0,0],[0,0],[-0.152,41.088]],"o":[[15.106,-9.064],[-1.411,-40.395],[0,0],[0,0],[0,0],[41.473,0.5],[0.106,-28.93]],"v":[[56.741,-120.537],[79.911,-168.605],[13.287,-226.593],[-79.507,-225.843],[-78.757,-0.5],[21.518,-0.5],[92.037,-63.144]],"c":true}],"e":[{"i":[[21.149,8.762],[0,21.451],[37.766,0],[0,0],[0,0],[0,0],[0,41.089]],"o":[[15.106,-9.064],[0,-38.672],[0,0],[0,0],[0,0],[37.463,0],[0,-29.004]],"v":[[55.241,-119.037],[79.411,-164.355],[12.037,-226.593],[-79.507,-226.593],[-79.507,0],[23.518,0],[90.287,-63.144]],"c":true}]},{"t":24}]},"nm":"B"},{"ind":1,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[-1.324,-11.343],[1.439,-10.814],[0,0],[0,0],[0,0]],"o":[[1.603,13.739],[0,0],[0,0],[0,0],[3.112,-1.724]],"v":[[-264.603,-181.739],[-262.939,-148.686],[-267.481,-144.898],[-273.154,-192.488],[-271.112,-195.276]],"c":true}],"e":[{"i":[[-1.324,-11.343],[1.439,-10.814],[0,0],[0,0],[0,0]],"o":[[1.603,13.739],[0,0],[0,0],[0,0],[3.112,-1.724]],"v":[[-60.603,-177.739],[-58.939,-144.686],[-63.481,-140.898],[-69.154,-188.488],[-67.112,-191.276]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[-1.324,-11.343],[1.439,-10.814],[0,0],[0,0],[0,0]],"o":[[1.603,13.739],[0,0],[0,0],[0,0],[3.112,-1.724]],"v":[[-60.603,-177.739],[-58.939,-144.686],[-63.481,-140.898],[-69.154,-188.488],[-67.112,-191.276]],"c":true}],"e":[{"i":[[-1.324,-11.343],[4.939,-8.814],[0,0],[0,0],[0,0]],"o":[[1.603,13.739],[0,0],[0,0],[0,0],[6.612,-3.724]],"v":[[-40.603,-178.239],[-44.439,-146.186],[-54.981,-139.898],[-61.654,-188.488],[-53.612,-193.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[-1.324,-11.343],[4.939,-8.814],[0,0],[0,0],[0,0]],"o":[[1.603,13.739],[0,0],[0,0],[0,0],[6.612,-3.724]],"v":[[-40.603,-178.239],[-44.439,-146.186],[-54.981,-139.898],[-61.654,-188.488],[-53.612,-193.776]],"c":true}],"e":[{"i":[[-0.897,-12.761],[7.439,-4.314],[0,0],[0,0],[0,0]],"o":[[1.049,14.93],[0,0],[0,0],[0,0],[11.112,-4.224]],"v":[[-22.103,-175.239],[-29.439,-144.686],[-49.481,-139.398],[-53.654,-189.988],[-40.112,-193.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[-0.897,-12.761],[7.439,-4.314],[0,0],[0,0],[0,0]],"o":[[1.049,14.93],[0,0],[0,0],[0,0],[11.112,-4.224]],"v":[[-22.103,-175.239],[-29.439,-144.686],[-49.481,-139.398],[-53.654,-189.988],[-40.112,-193.776]],"c":true}],"e":[{"i":[[-2.295,-14.342],[14.615,-7.06],[0,0],[0,0],[0,0]],"o":[[2.247,14.043],[0,0],[0,0],[0,0],[12.13,-2.72]],"v":[[-5.749,-174.483],[-19.893,-141.684],[-44.391,-137.152],[-47.397,-188.494],[-26.535,-193.276]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[-2.295,-14.342],[14.615,-7.06],[0,0],[0,0],[0,0]],"o":[[2.247,14.043],[0,0],[0,0],[0,0],[12.13,-2.72]],"v":[[-5.749,-174.483],[-19.893,-141.684],[-44.391,-137.152],[-47.397,-188.494],[-26.535,-193.276]],"c":true}],"e":[{"i":[[-2.154,-14.341],[17.439,-2.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[13.612,-1.724]],"v":[[7.397,-171.739],[-12.939,-138.686],[-40.981,-136.898],[-43.154,-187.488],[-17.112,-191.276]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[-2.154,-14.341],[17.439,-2.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[13.612,-1.724]],"v":[[7.397,-171.739],[-12.939,-138.686],[-40.981,-136.898],[-43.154,-187.488],[-17.112,-191.276]],"c":true}],"e":[{"i":[[-1.534,-14.395],[17.58,-1.876],[0,0],[0,0],[0,0]],"o":[[1.502,14.095],[0,0],[0,0],[0,0],[14.855,-1.149]],"v":[[16.498,-167.632],[-5.273,-137.269],[-37.547,-135.91],[-39.662,-187.025],[-9.278,-190.383]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[-1.534,-14.395],[17.58,-1.876],[0,0],[0,0],[0,0]],"o":[[1.502,14.095],[0,0],[0,0],[0,0],[14.855,-1.149]],"v":[[16.498,-167.632],[-5.273,-137.269],[-37.547,-135.91],[-39.662,-187.025],[-9.278,-190.383]],"c":true}],"e":[{"i":[[-0.767,-14.448],[16.041,-0.938],[0,0],[0,0],[0,0]],"o":[[0.751,14.147],[0,0],[0,0],[0,0],[14.678,-0.575]],"v":[[24.639,-165.025],[-0.738,-135.852],[-35.862,-134.172],[-36.92,-187.061],[-2.991,-188.991]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[-0.767,-14.448],[16.041,-0.938],[0,0],[0,0],[0,0]],"o":[[0.751,14.147],[0,0],[0,0],[0,0],[14.678,-0.575]],"v":[[24.639,-165.025],[-0.738,-135.852],[-35.862,-134.172],[-36.92,-187.061],[-2.991,-188.991]],"c":true}],"e":[{"i":[[0,-14.502],[14.502,0],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[28.279,-161.917],[4.297,-134.435],[-34.177,-133.435],[-34.177,-187.098],[3.297,-187.598]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":21.376,"s":[{"i":[[0,-14.502],[14.502,0],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[28.279,-161.917],[4.297,-134.435],[-34.177,-133.435],[-34.177,-187.098],[3.297,-187.598]],"c":true}],"e":[{"i":[[0,-14.502],[16.037,-0.065],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[31.613,-160.417],[4.963,-133.935],[-33.677,-133.268],[-33.677,-186.264],[4.297,-186.598]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":22.253,"s":[{"i":[[0,-14.502],[16.037,-0.065],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[31.613,-160.417],[4.963,-133.935],[-33.677,-133.268],[-33.677,-186.264],[4.297,-186.598]],"c":true}],"e":[{"i":[[0,-14.502],[14.502,0],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[32.279,-158.917],[6.297,-132.935],[-32.677,-132.935],[-32.677,-184.598],[6.297,-184.598]],"c":true}]},{"t":24}]},"nm":"B"},{"ind":2,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[2.931,-7.005],[0,0],[0,0],[0,0],[-1.768,-14.461]],"o":[[0,0],[0,0],[0,0],[2.931,0.658],[2.344,19.176]],"v":[[-262.931,-52.995],[-268.677,-50.995],[-274.177,-102.658],[-270.431,-105.658],[-264.344,-84.176]],"c":true}],"e":[{"i":[[2.931,-7.005],[0,0],[0,0],[0,0],[-1.768,-14.461]],"o":[[0,0],[0,0],[0,0],[2.931,0.658],[2.344,19.176]],"v":[[-46.431,-52.995],[-52.177,-50.995],[-57.677,-102.658],[-53.931,-105.658],[-47.844,-84.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[2.931,-7.005],[0,0],[0,0],[0,0],[-1.768,-14.461]],"o":[[0,0],[0,0],[0,0],[2.931,0.658],[2.344,19.176]],"v":[[-46.431,-52.995],[-52.177,-50.995],[-57.677,-102.658],[-53.931,-105.658],[-47.844,-84.176]],"c":true}],"e":[{"i":[[7.931,-3.505],[0,0],[0,0],[0,0],[-1.768,-14.461]],"o":[[0,0],[0,0],[0,0],[8.931,-2.842],[2.344,19.176]],"v":[[-32.931,-51.495],[-45.677,-48.995],[-51.177,-100.658],[-40.431,-104.658],[-26.844,-83.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[7.931,-3.505],[0,0],[0,0],[0,0],[-1.768,-14.461]],"o":[[0,0],[0,0],[0,0],[8.931,-2.842],[2.344,19.176]],"v":[[-32.931,-51.495],[-45.677,-48.995],[-51.177,-100.658],[-40.431,-104.658],[-26.844,-83.176]],"c":true}],"e":[{"i":[[13.431,-1.505],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[8.931,-2.842],[2.589,13.962]],"v":[[-19.931,-48.995],[-40.177,-47.495],[-45.177,-99.158],[-24.431,-103.158],[-6.344,-82.676]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[13.431,-1.505],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[8.931,-2.842],[2.589,13.962]],"v":[[-19.931,-48.995],[-40.177,-47.495],[-45.177,-99.158],[-24.431,-103.158],[-6.344,-82.676]],"c":true}],"e":[{"i":[[14.118,-0.752],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[15.681,-1.092],[2.589,13.962]],"v":[[-7.681,-46.495],[-37.927,-44.245],[-41.177,-97.908],[-14.681,-101.408],[8.906,-80.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[14.118,-0.752],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[15.681,-1.092],[2.589,13.962]],"v":[[-7.681,-46.495],[-37.927,-44.245],[-41.177,-97.908],[-14.681,-101.408],[8.906,-80.176]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[14.804,0],[2.589,13.962]],"v":[[-0.931,-44.995],[-36.677,-42.995],[-39.177,-97.158],[-3.931,-100.158],[21.656,-76.676]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[14.804,0],[2.589,13.962]],"v":[[-0.931,-44.995],[-36.677,-42.995],[-39.177,-97.158],[-3.931,-100.158],[21.656,-76.676]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-1.77,-14.283]],"o":[[0,0],[0,0],[0,0],[14.804,0],[1.726,14.041]],"v":[[7.902,-44.495],[-34.344,-44.162],[-35.844,-96.158],[5.902,-97.825],[29.989,-73.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-1.77,-14.283]],"o":[[0,0],[0,0],[0,0],[14.804,0],[1.726,14.041]],"v":[[7.902,-44.495],[-34.344,-44.162],[-35.844,-96.158],[5.902,-97.825],[29.989,-73.176]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-0.885,-14.241]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0.863,14.121]],"v":[[12.235,-42.745],[-33.761,-42.329],[-34.761,-95.908],[11.235,-96.242],[37.322,-69.926]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-0.885,-14.241]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0.863,14.121]],"v":[[12.235,-42.745],[-33.761,-42.329],[-34.761,-95.908],[11.235,-96.242],[37.322,-69.926]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[13.569,-41.995],[-32.677,-41.995],[-33.677,-95.658],[16.569,-94.658],[40.656,-68.676]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":21.376,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[13.569,-41.995],[-32.677,-41.995],[-33.677,-95.658],[16.569,-94.658],[40.656,-68.676]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[16.569,-41.995],[-32.677,-41.995],[-32.677,-93.658],[16.569,-94.658],[43.156,-67.676]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":22.253,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[16.569,-41.995],[-32.677,-41.995],[-32.677,-93.658],[16.569,-94.658],[43.156,-67.676]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[16.569,-41.995],[-32.677,-41.995],[-32.677,-93.658],[16.569,-93.658],[43.156,-67.676]],"c":true}]},{"t":24}]},"nm":"B"},{"ty":"mm","mm":1,"nm":"Merge Paths 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0.86,0.86,0.86,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":15,"op":23,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":18,"ty":4,"nm":"B orange layer 3","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[1.759,26.037],[0.589,9.355],[8.227,2.939],[0,0],[0,0],[-17.127,-8.576],[-4.287,-8.856]],"o":[[-1.241,-21.463],[-0.382,-6.06],[-9.537,-3.407],[0,0],[0,0],[11.982,6],[-1.787,-26.856]],"v":[[-12.759,-43.537],[-15.089,-87.355],[-25.963,-106.093],[-46.507,-102.093],[-51.007,3.5],[-21.482,0],[-8.713,17.856]],"c":true}],"e":[{"i":[[5.759,37.537],[2.589,11.855],[15.486,1.354],[0,0],[0,0],[-32.518,-11.5],[-4.287,-8.856]],"o":[[-6.241,-39.963],[-1.296,-5.932],[-27.537,-2.407],[0,0],[0,0],[21.489,7.6],[-3.287,-20.356]],"v":[[19.241,-49.037],[9.411,-111.855],[-17.463,-132.593],[-53.507,-120.593],[-55.507,2.5],[4.018,-9.5],[30.287,16.856]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[5.759,37.537],[2.589,11.855],[15.486,1.354],[0,0],[0,0],[-32.518,-11.5],[-4.287,-8.856]],"o":[[-6.241,-39.963],[-1.296,-5.932],[-27.537,-2.407],[0,0],[0,0],[21.489,7.6],[-3.287,-20.356]],"v":[[19.241,-49.037],[9.411,-111.855],[-17.463,-132.593],[-53.507,-120.593],[-55.507,2.5],[4.018,-9.5],[30.287,16.856]],"c":true}],"e":[{"i":[[7.259,32.537],[3.089,9.355],[44.113,-3.3],[0,0],[0,0],[-53.018,1],[3.713,19.144]],"o":[[-7.241,-38.463],[-1.904,-5.765],[-48.037,3.593],[0,0],[0,0],[60.505,-1.141],[-3.159,-16.288]],"v":[[83.741,-85.537],[72.411,-142.355],[4.537,-172.593],[-61.007,-138.593],[-59.007,1.5],[28.018,-33.5],[99.787,-17.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[7.259,32.537],[3.089,9.355],[44.113,-3.3],[0,0],[0,0],[-53.018,1],[3.713,19.144]],"o":[[-7.241,-38.463],[-1.904,-5.765],[-48.037,3.593],[0,0],[0,0],[60.505,-1.141],[-3.159,-16.288]],"v":[[83.741,-85.537],[72.411,-142.355],[4.537,-172.593],[-61.007,-138.593],[-59.007,1.5],[28.018,-33.5],[99.787,-17.644]],"c":true}],"e":[{"i":[[10.759,29.037],[4.471,5.31],[29.463,-40.407],[0,0],[0,0],[-43.018,23.5],[3.713,19.144]],"o":[[-7.241,-21.963],[-3.911,-4.645],[-7.99,10.959],[0,0],[0,0],[44.538,-24.331],[-3.159,-16.288]],"v":[[45.241,-195.037],[21.411,-260.355],[-37.963,-203.593],[-68.007,-155.093],[-61.507,3],[11.018,-60],[71.287,-111.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[10.759,29.037],[4.471,5.31],[29.463,-40.407],[0,0],[0,0],[-43.018,23.5],[3.713,19.144]],"o":[[-7.241,-21.963],[-3.911,-4.645],[-7.99,10.959],[0,0],[0,0],[44.538,-24.331],[-3.159,-16.288]],"v":[[45.241,-195.037],[21.411,-260.355],[-37.963,-203.593],[-68.007,-155.093],[-61.507,3],[11.018,-60],[71.287,-111.144]],"c":true}],"e":[{"i":[[4.259,17.037],[1.335,6.812],[2.963,-14.907],[0,0],[0,0],[-7.018,26.5],[3.713,19.144]],"o":[[-2.741,-12.463],[-0.911,-4.645],[-2.644,13.302],[0,0],[0,0],[9.673,-36.528],[-3.159,-16.288]],"v":[[-52.759,-171.537],[-67.089,-217.855],[-68.963,-189.593],[-76.007,-170.093],[-64.007,1.5],[-40.982,-50],[-39.713,-115.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[4.259,17.037],[1.335,6.812],[2.963,-14.907],[0,0],[0,0],[-7.018,26.5],[3.713,19.144]],"o":[[-2.741,-12.463],[-0.911,-4.645],[-2.644,13.302],[0,0],[0,0],[9.673,-36.528],[-3.159,-16.288]],"v":[[-52.759,-171.537],[-67.089,-217.855],[-68.963,-189.593],[-76.007,-170.093],[-64.007,1.5],[-40.982,-50],[-39.713,-115.644]],"c":true}],"e":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-73.259,-145.537],[-82.089,-178.355],[-90.963,-174.593],[-83.007,-159.593],[-66.507,1.5],[-58.482,-36],[-71.713,-94.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-73.259,-145.537],[-82.089,-178.355],[-90.963,-174.593],[-83.007,-159.593],[-66.507,1.5],[-58.482,-36],[-71.713,-94.644]],"c":true}],"e":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-79.259,-143.537],[-82.089,-178.355],[-90.963,-174.593],[-87.007,-159.593],[-69.007,-0.5],[-59.482,-33.5],[-71.713,-94.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-79.259,-143.537],[-82.089,-178.355],[-90.963,-174.593],[-87.007,-159.593],[-69.007,-0.5],[-59.482,-33.5],[-71.713,-94.644]],"c":true}],"e":[{"i":[[2.759,16.037],[1.089,6.855],[3.15,2.884],[0,0],[0,0],[0.024,23.358],[2.213,13.144]],"o":[[-0.741,-7.963],[-2.92,-18.378],[-1.537,-1.407],[0,0],[0,0],[-0.018,-17.5],[-2.755,-16.362]],"v":[[-74.759,-145.537],[-84.089,-185.855],[-90.463,-219.593],[-95.507,-208.093],[-71.507,0.5],[-51.482,-32],[-65.713,-94.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[{"i":[[2.759,16.037],[1.089,6.855],[3.15,2.884],[0,0],[0,0],[0.024,23.358],[2.213,13.144]],"o":[[-0.741,-7.963],[-2.92,-18.378],[-1.537,-1.407],[0,0],[0,0],[-0.018,-17.5],[-2.755,-16.362]],"v":[[-74.759,-145.537],[-84.089,-185.855],[-90.463,-219.593],[-95.507,-208.093],[-71.507,0.5],[-51.482,-32],[-65.713,-94.144]],"c":true}],"e":[{"i":[[8.509,31.287],[6.813,20.34],[4.963,-11.907],[0,0],[0,0],[-17.018,16],[9.213,50.737]],"o":[[-6.241,-30.463],[-5.911,-17.645],[-6.06,14.54],[0,0],[0,0],[9.992,-9.394],[-3.787,-20.856]],"v":[[-51.259,-158.537],[-71.589,-238.355],[-80.963,-225.093],[-96.007,-202.593],[-72.007,0],[-29.482,-25.5],[-34.213,-88.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[8.509,31.287],[6.813,20.34],[4.963,-11.907],[0,0],[0,0],[-17.018,16],[9.213,50.737]],"o":[[-6.241,-30.463],[-5.911,-17.645],[-6.06,14.54],[0,0],[0,0],[9.992,-9.394],[-3.787,-20.856]],"v":[[-51.259,-158.537],[-71.589,-238.355],[-80.963,-225.093],[-96.007,-202.593],[-72.007,0],[-29.482,-25.5],[-34.213,-88.644]],"c":true}],"e":[{"i":[[8.509,31.287],[4.772,20.913],[18.771,-35.562],[0,0],[0,0],[-35.294,7.846],[9.713,50.644]],"o":[[-6.241,-30.463],[-20.911,-91.645],[-14.037,26.593],[0,0],[0,0],[44.982,-10],[-4.991,-26.022]],"v":[[-6.759,-153.537],[-31.089,-246.855],[-59.463,-246.093],[-97.007,-208.093],[-74.007,0.5],[-18.482,-13.5],[11.287,-77.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[8.509,31.287],[4.772,20.913],[18.771,-35.562],[0,0],[0,0],[-35.294,7.846],[9.713,50.644]],"o":[[-6.241,-30.463],[-20.911,-91.645],[-14.037,26.593],[0,0],[0,0],[44.982,-10],[-4.991,-26.022]],"v":[[-6.759,-153.537],[-31.089,-246.855],[-59.463,-246.093],[-97.007,-208.093],[-74.007,0.5],[-18.482,-13.5],[11.287,-77.644]],"c":true}],"e":[{"i":[[9.759,27.037],[3.56,21.153],[32.297,-24.991],[0,0],[0,0],[-51.855,8.105],[16.213,81.144]],"o":[[-6.241,-30.463],[-13.911,-82.645],[-39.537,30.593],[0,0],[0,0],[47.982,-7.5],[-5.191,-25.982]],"v":[[27.241,-164.537],[14.411,-234.855],[-36.963,-250.093],[-97.507,-214.093],[-76.507,-0.5],[0.018,-9],[47.287,-86.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[9.759,27.037],[3.56,21.153],[32.297,-24.991],[0,0],[0,0],[-51.855,8.105],[16.213,81.144]],"o":[[-6.241,-30.463],[-13.911,-82.645],[-39.537,30.593],[0,0],[0,0],[47.982,-7.5],[-5.191,-25.982]],"v":[[27.241,-164.537],[14.411,-234.855],[-36.963,-250.093],[-97.507,-214.093],[-76.507,-0.5],[0.018,-9],[47.287,-86.144]],"c":true}],"e":[{"i":[[9.759,25.037],[3.589,22.355],[46.963,-21.907],[0,0],[0,0],[-52.444,2.078],[5.958,46.178]],"o":[[-1.241,-27.463],[-10.199,-63.523],[-35.55,16.583],[0,0],[0,0],[50.482,-2],[-5.787,-44.856]],"v":[[49.241,-157.537],[45.911,-224.855],[-24.963,-243.593],[-95.007,-217.593],[-76.507,-0.5],[33.018,-7],[72.287,-80.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[9.759,25.037],[3.589,22.355],[46.963,-21.907],[0,0],[0,0],[-52.444,2.078],[5.958,46.178]],"o":[[-1.241,-27.463],[-10.199,-63.523],[-35.55,16.583],[0,0],[0,0],[50.482,-2],[-5.787,-44.856]],"v":[[49.241,-157.537],[45.911,-224.855],[-24.963,-243.593],[-95.007,-217.593],[-76.507,-0.5],[33.018,-7],[72.287,-80.144]],"c":true}],"e":[{"i":[[15.759,21.037],[2.598,21.286],[45.963,-10.657],[0,0],[0,0],[-28.018,1],[5.463,51.394]],"o":[[3.509,-24.213],[-4.161,-64.895],[-38.537,12.343],[0,0],[0,0],[59.982,2.5],[-3.676,-32.795]],"v":[[59.241,-140.287],[64.661,-201.605],[-11.463,-241.343],[-91.757,-220.343],[-77.257,-0.75],[28.018,-4.5],[84.787,-81.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[15.759,21.037],[2.598,21.286],[45.963,-10.657],[0,0],[0,0],[-28.018,1],[5.463,51.394]],"o":[[3.509,-24.213],[-4.161,-64.895],[-38.537,12.343],[0,0],[0,0],[59.982,2.5],[-3.676,-32.795]],"v":[[59.241,-140.287],[64.661,-201.605],[-11.463,-241.343],[-91.757,-220.343],[-77.257,-0.75],[28.018,-4.5],[84.787,-81.144]],"c":true}],"e":[{"i":[[21.759,21.537],[2.058,21.352],[37.463,-5.907],[0,0],[0,0],[0,0],[4.713,56.644]],"o":[[10.259,-19.963],[-5.411,-56.145],[-38.749,6.11],[0,0],[0,0],[45.482,1],[-2.393,-28.758]],"v":[[62.241,-133.037],[74.911,-192.355],[-0.463,-236.593],[-88.507,-223.093],[-78.007,-1],[23.018,-2.5],[92.287,-70.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[21.759,21.537],[2.058,21.352],[37.463,-5.907],[0,0],[0,0],[0,0],[4.713,56.644]],"o":[[10.259,-19.963],[-5.411,-56.145],[-38.749,6.11],[0,0],[0,0],[45.482,1],[-2.393,-28.758]],"v":[[62.241,-133.037],[74.911,-192.355],[-0.463,-236.593],[-88.507,-223.093],[-78.007,-1],[23.018,-2.5],[92.287,-70.144]],"c":true}],"e":[{"i":[[24.259,15.287],[0.089,22.605],[44.213,-5.407],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[11.759,-13.713],[-2.705,-47.408],[-24.787,2.093],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[60.241,-127.787],[78.411,-180.605],[5.787,-233.593],[-86.007,-223.593],[-78.007,-1],[24.768,-0.25],[93.287,-66.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[24.259,15.287],[0.089,22.605],[44.213,-5.407],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[11.759,-13.713],[-2.705,-47.408],[-24.787,2.093],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[60.241,-127.787],[78.411,-180.605],[5.787,-233.593],[-86.007,-223.593],[-78.007,-1],[24.768,-0.25],[93.287,-66.644]],"c":true}],"e":[{"i":[[21.149,8.762],[0,21.451],[38.463,-2.407],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[15.106,-9.064],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[59.241,-124.037],[79.411,-176.855],[14.537,-231.593],[-83.507,-224.093],[-78.007,-1],[23.518,0],[94.787,-61.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[21.149,8.762],[0,21.451],[38.463,-2.407],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[15.106,-9.064],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[59.241,-124.037],[79.411,-176.855],[14.537,-231.593],[-83.507,-224.093],[-78.007,-1],[23.518,0],[94.787,-61.144]],"c":true}],"e":[{"i":[[21.149,8.762],[0.749,21.438],[38.114,-1.203],[0,0],[0,0],[0,0],[-0.152,41.088]],"o":[[15.106,-9.064],[-1.411,-40.395],[0,0],[0,0],[0,0],[41.473,0.5],[0.106,-28.93]],"v":[[56.741,-120.537],[79.911,-168.605],[13.287,-226.593],[-79.507,-225.843],[-78.757,-0.5],[21.518,-0.5],[92.037,-63.144]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":22.253,"s":[{"i":[[21.149,8.762],[0.749,21.438],[38.114,-1.203],[0,0],[0,0],[0,0],[-0.152,41.088]],"o":[[15.106,-9.064],[-1.411,-40.395],[0,0],[0,0],[0,0],[41.473,0.5],[0.106,-28.93]],"v":[[56.741,-120.537],[79.911,-168.605],[13.287,-226.593],[-79.507,-225.843],[-78.757,-0.5],[21.518,-0.5],[92.037,-63.144]],"c":true}],"e":[{"i":[[21.149,8.762],[0,21.451],[37.766,0],[0,0],[0,0],[0,0],[0,41.089]],"o":[[15.106,-9.064],[0,-38.672],[0,0],[0,0],[0,0],[37.463,0],[0,-29.004]],"v":[[55.241,-119.037],[79.411,-164.355],[12.037,-226.593],[-79.507,-226.593],[-79.507,0],[23.518,0],[90.287,-63.144]],"c":true}]},{"t":24}]},"nm":"B"},{"ty":"mm","mm":1,"nm":"Merge Paths 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0.86,0.86,0.86,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":11,"op":15,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":19,"ty":4,"nm":"B orange layer 2 shadow","parent":34,"ks":{"o":{"k":5},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[27,-20],[0,0],[-5,-11],[-20,3],[0,21],[-1,63]],"o":[[-10.811,8.008],[0,0],[5,11],[20,-3],[0,-21],[1,-63]],"v":[[-55,-172],[-86,-85],[-92,10],[-95,22],[0,21],[-15,-137]],"c":true}],"e":[{"i":[[27,-20],[0,0],[-5,-11],[-20,3],[0,21],[-1,63]],"o":[[-10.811,8.008],[0,0],[5,11],[20,-3],[0,-21],[1,-63]],"v":[[-55,-172],[-86,-85],[-92,10],[-58,40],[42,28],[20,-147]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[27,-20],[0,0],[-5,-11],[-20,3],[0,21],[-1,63]],"o":[[-10.811,8.008],[0,0],[5,11],[20,-3],[0,-21],[1,-63]],"v":[[-55,-172],[-86,-85],[-92,10],[-58,40],[42,28],[20,-147]],"c":true}],"e":[{"i":[[55,-29],[0,0],[-5,-11],[-20,3],[0,21],[28,61]],"o":[[-55,29],[0,0],[5,11],[20,-3],[0,-21],[-26.285,-57.263]],"v":[[-79,-179],[-124,-104],[-119,72],[-21,87],[50,-29],[4,-198]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[55,-29],[0,0],[-5,-11],[-20,3],[0,21],[28,61]],"o":[[-55,29],[0,0],[5,11],[20,-3],[0,-21],[-26.285,-57.263]],"v":[[-79,-179],[-124,-104],[-119,72],[-21,87],[50,-29],[4,-198]],"c":true}],"e":[{"i":[[29.478,-54.745],[0,0],[-5,-11],[-20,3],[0,21],[13,66]],"o":[[-7,13],[0,0],[5,11],[20,-3],[0,-21],[-12.177,-61.82]],"v":[[-115,-165],[-124,-104],[-107,16],[-80,39],[-19,5],[-54,-216]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[29.478,-54.745],[0,0],[-5,-11],[-20,3],[0,21],[13,66]],"o":[[-7,13],[0,0],[5,11],[20,-3],[0,-21],[-12.177,-61.82]],"v":[[-115,-165],[-124,-104],[-107,16],[-80,39],[-19,5],[-54,-216]],"c":true}],"e":[{"i":[[29.478,-54.745],[0,0],[-5,-11],[-20,3],[0,21],[13,66]],"o":[[-7,13],[0,0],[5,11],[20,-3],[0,-21],[-12.177,-61.82]],"v":[[-150,-165],[-159,-104],[-142,16],[-115,39],[-54,5],[-77,-199]],"c":true}]},{"t":10}]},"o":{"k":100},"x":{"k":0},"nm":"Mask 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":4,"s":[{"i":[[-0.993,13.755],[-0.576,6.238],[1.302,0.585],[0,0],[0,0],[0,0],[-2.275,-1.384],[-0.28,-3.435]],"o":[[1.757,-16.495],[0.509,-5.518],[-2.446,-1.099],[0,0],[0,0],[0,0],[1.508,0.917],[1.082,-12.356]],"v":[[-36.507,-32.755],[-33.509,-58.982],[-33.554,-66.651],[-36.609,-64.578],[-39.481,-45.472],[-46.5,1.214],[-41.725,1.384],[-40.332,4.606]],"c":true}],"e":[{"i":[[-0.741,19.537],[-0.161,8.855],[3.194,1.609],[0,0],[0,0],[0,0],[-8.518,-3.5],[-1.037,-4.856]],"o":[[-0.241,-13.963],[0.11,-6.071],[-3.787,-1.907],[0,0],[0,0],[0,0],[4.451,1.829],[0.963,-28.106]],"v":[[-29.259,-43.037],[-28.339,-74.105],[-32.213,-84.593],[-40.757,-84.843],[-44.677,-58.245],[-54.257,6.75],[-37.482,2.75],[-30.463,12.606]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[-0.741,19.537],[-0.161,8.855],[3.194,1.609],[0,0],[0,0],[0,0],[-8.518,-3.5],[-1.037,-4.856]],"o":[[-0.241,-13.963],[0.11,-6.071],[-3.787,-1.907],[0,0],[0,0],[0,0],[4.451,1.829],[0.963,-28.106]],"v":[[-29.259,-43.037],[-28.339,-74.105],[-32.213,-84.593],[-40.757,-84.843],[-44.677,-58.245],[-54.257,6.75],[-37.482,2.75],[-30.463,12.606]],"c":true}],"e":[{"i":[[1.759,26.037],[0.589,9.355],[8.227,2.939],[0,0],[0,0],[0,0],[-15.518,-7.5],[-1.787,-7.356]],"o":[[-1.241,-21.463],[-0.382,-6.06],[-9.537,-3.407],[0,0],[0,0],[0,0],[12.065,5.831],[-1.787,-26.856]],"v":[[-12.759,-43.537],[-15.089,-87.355],[-25.963,-106.093],[-47.257,-101.593],[-49.484,-64.148],[-53.507,3.5],[-22.232,0.5],[-8.713,17.856]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[1.759,26.037],[0.589,9.355],[8.227,2.939],[0,0],[0,0],[0,0],[-15.518,-7.5],[-1.787,-7.356]],"o":[[-1.241,-21.463],[-0.382,-6.06],[-9.537,-3.407],[0,0],[0,0],[0,0],[12.065,5.831],[-1.787,-26.856]],"v":[[-12.759,-43.537],[-15.089,-87.355],[-25.963,-106.093],[-47.257,-101.593],[-49.484,-64.148],[-53.507,3.5],[-22.232,0.5],[-8.713,17.856]],"c":true}],"e":[{"i":[[5.759,37.537],[1.35375,9.22725],[1.2945,5.92749999999999],[15.486,1.354],[0,0],[0,0],[-32.518,-11.5],[-4.287,-8.856]],"o":[[-3.1205,-19.9815],[-1.35375,-9.22725],[-1.296,-5.932],[-27.537,-2.407],[0,0],[0,0],[21.489,7.6],[-3.287,-20.356]],"v":[[19.241,-49.037],[12.9565,-90.9865],[9.411,-111.855],[-17.463,-132.593],[-53.507,-120.593],[-55.507,2.5],[4.018,-9.5],[30.287,16.856]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[5.759,37.537],[1.35375,9.22725],[1.2945,5.92749999999999],[15.486,1.354],[0,0],[0,0],[-32.518,-11.5],[-4.287,-8.856]],"o":[[-3.1205,-19.9815],[-1.35375,-9.22725],[-1.296,-5.932],[-27.537,-2.407],[0,0],[0,0],[21.489,7.6],[-3.287,-20.356]],"v":[[19.241,-49.037],[12.9565,-90.9865],[9.411,-111.855],[-17.463,-132.593],[-53.507,-120.593],[-55.507,2.5],[4.018,-9.5],[30.287,16.856]],"c":true}],"e":[{"i":[[7.259,32.537],[1.54124999999999,8.22725],[1.5445,4.67749999999998],[44.113,-3.3],[0,0],[0,0],[-53.018,1],[3.713,19.144]],"o":[[-3.62050000000001,-19.2315],[-1.54125000000001,-8.22725],[-1.904,-5.765],[-48.037,3.593],[0,0],[0,0],[60.505,-1.141],[-3.159,-16.288]],"v":[[83.741,-85.537],[76.519,-124.8615],[72.411,-142.355],[4.537,-172.593],[-61.007,-138.593],[-59.007,1.5],[28.018,-33.5],[99.787,-17.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[7.259,32.537],[1.54124999999999,8.22725],[1.5445,4.67749999999998],[44.113,-3.3],[0,0],[0,0],[-53.018,1],[3.713,19.144]],"o":[[-3.62050000000001,-19.2315],[-1.54125000000001,-8.22725],[-1.904,-5.765],[-48.037,3.593],[0,0],[0,0],[60.505,-1.141],[-3.159,-16.288]],"v":[[83.741,-85.537],[76.519,-124.8615],[72.411,-142.355],[4.537,-172.593],[-61.007,-138.593],[-59.007,1.5],[28.018,-33.5],[99.787,-17.644]],"c":true}],"e":[{"i":[[10.759,29.037],[4.4935,12.920375],[2.2355,2.65499999999997],[29.463,-40.407],[0,0],[0,0],[-43.018,23.5],[3.713,19.144]],"o":[[-3.6205,-10.9815],[-4.4935,-12.920375],[-3.911,-4.645],[-7.99,10.959],[0,0],[0,0],[44.538,-24.331],[-3.159,-16.288]],"v":[[45.241,-195.037],[32.28725,-233.940875],[21.411,-260.355],[-37.963,-203.593],[-68.007,-155.093],[-61.507,3],[11.018,-60],[71.287,-111.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[10.759,29.037],[4.4935,12.920375],[2.2355,2.65499999999997],[29.463,-40.407],[0,0],[0,0],[-43.018,23.5],[3.713,19.144]],"o":[[-3.6205,-10.9815],[-4.4935,-12.920375],[-3.911,-4.645],[-7.99,10.959],[0,0],[0,0],[44.538,-24.331],[-3.159,-16.288]],"v":[[45.241,-195.037],[32.28725,-233.940875],[21.411,-260.355],[-37.963,-203.593],[-68.007,-155.093],[-61.507,3],[11.018,-60],[71.287,-111.144]],"c":true}],"e":[{"i":[[4.259,17.037],[3.073,9.17012500000001],[0.66749999999999,3.40600000000001],[2.963,-14.907],[0,0],[0,0],[-7.018,26.5],[3.713,19.144]],"o":[[-1.3705,-6.23150000000001],[-3.07300000000001,-9.17012499999998],[-0.911,-4.645],[-2.644,13.302],[0,0],[0,0],[9.673,-36.528],[-3.159,-16.288]],"v":[[-52.759,-171.537],[-60.45125,-196.815125],[-67.089,-217.855],[-68.963,-189.593],[-76.007,-170.093],[-64.007,1.5],[-40.982,-50],[-39.713,-115.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[4.259,17.037],[3.073,9.17012500000001],[0.66749999999999,3.40600000000001],[2.963,-14.907],[0,0],[0,0],[-7.018,26.5],[3.713,19.144]],"o":[[-1.3705,-6.23150000000001],[-3.07300000000001,-9.17012499999998],[-0.911,-4.645],[-2.644,13.302],[0,0],[0,0],[9.673,-36.528],[-3.159,-16.288]],"v":[[-52.759,-171.537],[-60.45125,-196.815125],[-67.089,-217.855],[-68.963,-189.593],[-76.007,-170.093],[-64.007,1.5],[-40.982,-50],[-39.713,-115.644]],"c":true}],"e":[{"i":[[2.759,16.037],[0.545875,6.84424999999999],[0.27600000000001,3.45950000000002],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.37050000000001,-3.98150000000001],[-0.545875,-6.84424999999999],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-79.259,-143.537],[-80.744875,-161.3375],[-82.089,-178.355],[-90.963,-174.593],[-80.007,-159.593],[-66.507,1.5],[-58.482,-36],[-71.713,-94.644]],"c":true}]},{"t":11}]},"nm":"B"},{"ind":1,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":4,"s":[{"i":[[-0.993,13.755],[-0.576,6.238],[1.302,0.585],[0,0],[0,0],[-2.275,-1.384],[-0.28,-3.435]],"o":[[1.757,-16.495],[0.509,-5.518],[-2.446,-1.099],[0,0],[0,0],[1.508,0.917],[1.082,-12.356]],"v":[[-36.507,-32.755],[-33.509,-58.982],[-33.554,-66.651],[-36.609,-64.578],[-46.5,1.214],[-41.725,1.384],[-40.332,4.606]],"c":true}],"e":[{"i":[[-0.741,19.537],[-0.161,8.855],[3.194,1.609],[0,0],[0,0],[-8.518,-3.5],[-1.037,-4.856]],"o":[[-0.241,-13.963],[0.11,-6.071],[-3.787,-1.907],[0,0],[0,0],[4.451,1.829],[0.963,-28.106]],"v":[[-29.259,-43.037],[-28.339,-74.105],[-32.213,-84.593],[-40.757,-84.843],[-54.257,6.75],[-37.482,2.75],[-30.463,12.606]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[-0.741,19.537],[-0.161,8.855],[3.194,1.609],[0,0],[0,0],[-8.518,-3.5],[-1.037,-4.856]],"o":[[-0.241,-13.963],[0.11,-6.071],[-3.787,-1.907],[0,0],[0,0],[4.451,1.829],[0.963,-28.106]],"v":[[-29.259,-43.037],[-28.339,-74.105],[-32.213,-84.593],[-40.757,-84.843],[-54.257,6.75],[-37.482,2.75],[-30.463,12.606]],"c":true}],"e":[{"i":[[0.832,26.037],[0.279,9.355],[-0.34,-0.907],[0,0],[0,0],[-1.764,2.5],[-2,4.644]],"o":[[-0.587,-21.463],[-0.181,-6.06],[-2.641,3.629],[0,0],[0,0],[1.764,-2.5],[3,-26.606]],"v":[[-48.414,-64.537],[-46.516,-92.355],[-47.66,-93.593],[-51.128,-76.843],[-60.507,13.25],[-56.514,6],[-52.5,-2.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[0.832,26.037],[0.279,9.355],[-0.34,-0.907],[0,0],[0,0],[-1.764,2.5],[-2,4.644]],"o":[[-0.587,-21.463],[-0.181,-6.06],[-2.641,3.629],[0,0],[0,0],[1.764,-2.5],[3,-26.606]],"v":[[-48.414,-64.537],[-46.516,-92.355],[-47.66,-93.593],[-51.128,-76.843],[-60.507,13.25],[-56.514,6],[-52.5,-2.394]],"c":true}],"e":[{"i":[[5.759,37.537],[2.589,11.855],[15.486,1.354],[0,0],[0,0],[-32.518,-11.5],[-4.287,-8.856]],"o":[[-6.241,-39.963],[-1.296,-5.932],[-27.537,-2.407],[0,0],[0,0],[21.489,7.6],[-3.287,-20.356]],"v":[[19.241,-49.037],[9.411,-111.855],[-17.463,-132.593],[-53.507,-120.593],[-55.507,2.5],[4.018,-9.5],[30.287,16.856]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[5.759,37.537],[2.589,11.855],[15.486,1.354],[0,0],[0,0],[-32.518,-11.5],[-4.287,-8.856]],"o":[[-6.241,-39.963],[-1.296,-5.932],[-27.537,-2.407],[0,0],[0,0],[21.489,7.6],[-3.287,-20.356]],"v":[[19.241,-49.037],[9.411,-111.855],[-17.463,-132.593],[-53.507,-120.593],[-55.507,2.5],[4.018,-9.5],[30.287,16.856]],"c":true}],"e":[{"i":[[7.259,32.537],[3.089,9.355],[44.113,-3.3],[0,0],[0,0],[-53.018,1],[3.713,19.144]],"o":[[-7.241,-38.463],[-1.904,-5.765],[-48.037,3.593],[0,0],[0,0],[60.505,-1.141],[-3.159,-16.288]],"v":[[83.741,-85.537],[72.411,-142.355],[4.537,-172.593],[-61.007,-138.593],[-59.007,1.5],[28.018,-33.5],[99.787,-17.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[7.259,32.537],[3.089,9.355],[44.113,-3.3],[0,0],[0,0],[-53.018,1],[3.713,19.144]],"o":[[-7.241,-38.463],[-1.904,-5.765],[-48.037,3.593],[0,0],[0,0],[60.505,-1.141],[-3.159,-16.288]],"v":[[83.741,-85.537],[72.411,-142.355],[4.537,-172.593],[-61.007,-138.593],[-59.007,1.5],[28.018,-33.5],[99.787,-17.644]],"c":true}],"e":[{"i":[[10.759,29.037],[4.471,5.31],[29.463,-40.407],[0,0],[0,0],[-43.018,23.5],[3.713,19.144]],"o":[[-7.241,-21.963],[-3.911,-4.645],[-7.99,10.959],[0,0],[0,0],[44.538,-24.331],[-3.159,-16.288]],"v":[[45.241,-195.037],[21.411,-260.355],[-37.963,-203.593],[-68.007,-155.093],[-61.507,3],[11.018,-60],[71.287,-111.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[10.759,29.037],[4.471,5.31],[29.463,-40.407],[0,0],[0,0],[-43.018,23.5],[3.713,19.144]],"o":[[-7.241,-21.963],[-3.911,-4.645],[-7.99,10.959],[0,0],[0,0],[44.538,-24.331],[-3.159,-16.288]],"v":[[45.241,-195.037],[21.411,-260.355],[-37.963,-203.593],[-68.007,-155.093],[-61.507,3],[11.018,-60],[71.287,-111.144]],"c":true}],"e":[{"i":[[4.259,17.037],[1.335,6.812],[2.963,-14.907],[0,0],[0,0],[-7.018,26.5],[3.713,19.144]],"o":[[-2.741,-12.463],[-0.911,-4.645],[-2.644,13.302],[0,0],[0,0],[9.673,-36.528],[-3.159,-16.288]],"v":[[-52.759,-171.537],[-67.089,-217.855],[-68.963,-189.593],[-76.007,-170.093],[-64.007,1.5],[-40.982,-50],[-39.713,-115.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[4.259,17.037],[1.335,6.812],[2.963,-14.907],[0,0],[0,0],[-7.018,26.5],[3.713,19.144]],"o":[[-2.741,-12.463],[-0.911,-4.645],[-2.644,13.302],[0,0],[0,0],[9.673,-36.528],[-3.159,-16.288]],"v":[[-52.759,-171.537],[-67.089,-217.855],[-68.963,-189.593],[-76.007,-170.093],[-64.007,1.5],[-40.982,-50],[-39.713,-115.644]],"c":true}],"e":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-79.259,-143.537],[-82.089,-178.355],[-90.963,-174.593],[-80.007,-159.593],[-66.507,1.5],[-58.482,-36],[-71.713,-94.644]],"c":true}]},{"t":11}]},"nm":"B 2"},{"ty":"mm","mm":1,"nm":"Merge Paths 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0.86,0.86,0.86,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":6,"op":11,"st":0,"bm":10,"sr":1},{"ddd":0,"ind":20,"ty":4,"nm":"B orange layer 2","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":4,"s":[{"i":[[-0.993,13.755],[-0.576,6.238],[1.302,0.585],[0,0],[0,0],[0,0],[-2.275,-1.384],[-0.28,-3.435]],"o":[[1.757,-16.495],[0.509,-5.518],[-2.446,-1.099],[0,0],[0,0],[0,0],[1.508,0.917],[1.082,-12.356]],"v":[[-36.507,-32.755],[-33.509,-58.982],[-33.554,-66.651],[-36.609,-64.578],[-39.481,-45.472],[-46.5,1.214],[-41.725,1.384],[-40.332,4.606]],"c":true}],"e":[{"i":[[-0.741,19.537],[-0.161,8.855],[3.194,1.609],[0,0],[0,0],[0,0],[-8.518,-3.5],[-1.037,-4.856]],"o":[[-0.241,-13.963],[0.11,-6.071],[-3.787,-1.907],[0,0],[0,0],[0,0],[4.451,1.829],[0.963,-28.106]],"v":[[-29.259,-43.037],[-28.339,-74.105],[-32.213,-84.593],[-40.757,-84.843],[-44.677,-58.245],[-54.257,6.75],[-37.482,2.75],[-30.463,12.606]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[-0.741,19.537],[-0.161,8.855],[3.194,1.609],[0,0],[0,0],[0,0],[-8.518,-3.5],[-1.037,-4.856]],"o":[[-0.241,-13.963],[0.11,-6.071],[-3.787,-1.907],[0,0],[0,0],[0,0],[4.451,1.829],[0.963,-28.106]],"v":[[-29.259,-43.037],[-28.339,-74.105],[-32.213,-84.593],[-40.757,-84.843],[-44.677,-58.245],[-54.257,6.75],[-37.482,2.75],[-30.463,12.606]],"c":true}],"e":[{"i":[[1.759,26.037],[0.589,9.355],[8.227,2.939],[0,0],[0,0],[0,0],[-15.518,-7.5],[-1.787,-7.356]],"o":[[-1.241,-21.463],[-0.382,-6.06],[-9.537,-3.407],[0,0],[0,0],[0,0],[12.065,5.831],[-1.787,-26.856]],"v":[[-12.759,-43.537],[-15.089,-87.355],[-25.963,-106.093],[-47.257,-101.593],[-49.484,-64.148],[-53.507,3.5],[-22.232,0.5],[-8.713,17.856]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[1.759,26.037],[0.589,9.355],[8.227,2.939],[0,0],[0,0],[0,0],[-15.518,-7.5],[-1.787,-7.356]],"o":[[-1.241,-21.463],[-0.382,-6.06],[-9.537,-3.407],[0,0],[0,0],[0,0],[12.065,5.831],[-1.787,-26.856]],"v":[[-12.759,-43.537],[-15.089,-87.355],[-25.963,-106.093],[-47.257,-101.593],[-49.484,-64.148],[-53.507,3.5],[-22.232,0.5],[-8.713,17.856]],"c":true}],"e":[{"i":[[5.759,37.537],[1.35375,9.22725],[1.2945,5.92749999999999],[15.486,1.354],[0,0],[0,0],[-32.518,-11.5],[-4.287,-8.856]],"o":[[-3.1205,-19.9815],[-1.35375,-9.22725],[-1.296,-5.932],[-27.537,-2.407],[0,0],[0,0],[21.489,7.6],[-3.287,-20.356]],"v":[[19.241,-49.037],[12.9565,-90.9865],[9.411,-111.855],[-17.463,-132.593],[-53.507,-120.593],[-55.507,2.5],[4.018,-9.5],[30.287,16.856]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[5.759,37.537],[1.35375,9.22725],[1.2945,5.92749999999999],[15.486,1.354],[0,0],[0,0],[-32.518,-11.5],[-4.287,-8.856]],"o":[[-3.1205,-19.9815],[-1.35375,-9.22725],[-1.296,-5.932],[-27.537,-2.407],[0,0],[0,0],[21.489,7.6],[-3.287,-20.356]],"v":[[19.241,-49.037],[12.9565,-90.9865],[9.411,-111.855],[-17.463,-132.593],[-53.507,-120.593],[-55.507,2.5],[4.018,-9.5],[30.287,16.856]],"c":true}],"e":[{"i":[[7.259,32.537],[1.54124999999999,8.22725],[1.5445,4.67749999999998],[44.113,-3.3],[0,0],[0,0],[-53.018,1],[3.713,19.144]],"o":[[-3.62050000000001,-19.2315],[-1.54125000000001,-8.22725],[-1.904,-5.765],[-48.037,3.593],[0,0],[0,0],[60.505,-1.141],[-3.159,-16.288]],"v":[[83.741,-85.537],[76.519,-124.8615],[72.411,-142.355],[4.537,-172.593],[-61.007,-138.593],[-59.007,1.5],[28.018,-33.5],[99.787,-17.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[7.259,32.537],[1.54124999999999,8.22725],[1.5445,4.67749999999998],[44.113,-3.3],[0,0],[0,0],[-53.018,1],[3.713,19.144]],"o":[[-3.62050000000001,-19.2315],[-1.54125000000001,-8.22725],[-1.904,-5.765],[-48.037,3.593],[0,0],[0,0],[60.505,-1.141],[-3.159,-16.288]],"v":[[83.741,-85.537],[76.519,-124.8615],[72.411,-142.355],[4.537,-172.593],[-61.007,-138.593],[-59.007,1.5],[28.018,-33.5],[99.787,-17.644]],"c":true}],"e":[{"i":[[10.759,29.037],[4.4935,12.920375],[2.2355,2.65499999999997],[29.463,-40.407],[0,0],[0,0],[-43.018,23.5],[3.713,19.144]],"o":[[-3.6205,-10.9815],[-4.4935,-12.920375],[-3.911,-4.645],[-7.99,10.959],[0,0],[0,0],[44.538,-24.331],[-3.159,-16.288]],"v":[[45.241,-195.037],[32.28725,-233.940875],[21.411,-260.355],[-37.963,-203.593],[-68.007,-155.093],[-61.507,3],[11.018,-60],[71.287,-111.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[10.759,29.037],[4.4935,12.920375],[2.2355,2.65499999999997],[29.463,-40.407],[0,0],[0,0],[-43.018,23.5],[3.713,19.144]],"o":[[-3.6205,-10.9815],[-4.4935,-12.920375],[-3.911,-4.645],[-7.99,10.959],[0,0],[0,0],[44.538,-24.331],[-3.159,-16.288]],"v":[[45.241,-195.037],[32.28725,-233.940875],[21.411,-260.355],[-37.963,-203.593],[-68.007,-155.093],[-61.507,3],[11.018,-60],[71.287,-111.144]],"c":true}],"e":[{"i":[[4.259,17.037],[3.073,9.17012500000001],[0.66749999999999,3.40600000000001],[2.963,-14.907],[0,0],[0,0],[-7.018,26.5],[3.713,19.144]],"o":[[-1.3705,-6.23150000000001],[-3.07300000000001,-9.17012499999998],[-0.911,-4.645],[-2.644,13.302],[0,0],[0,0],[9.673,-36.528],[-3.159,-16.288]],"v":[[-52.759,-171.537],[-60.45125,-196.815125],[-67.089,-217.855],[-68.963,-189.593],[-76.007,-170.093],[-64.007,1.5],[-40.982,-50],[-39.713,-115.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[4.259,17.037],[3.073,9.17012500000001],[0.66749999999999,3.40600000000001],[2.963,-14.907],[0,0],[0,0],[-7.018,26.5],[3.713,19.144]],"o":[[-1.3705,-6.23150000000001],[-3.07300000000001,-9.17012499999998],[-0.911,-4.645],[-2.644,13.302],[0,0],[0,0],[9.673,-36.528],[-3.159,-16.288]],"v":[[-52.759,-171.537],[-60.45125,-196.815125],[-67.089,-217.855],[-68.963,-189.593],[-76.007,-170.093],[-64.007,1.5],[-40.982,-50],[-39.713,-115.644]],"c":true}],"e":[{"i":[[2.759,16.037],[0.545875,6.84424999999999],[0.27600000000001,3.45950000000002],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.37050000000001,-3.98150000000001],[-0.545875,-6.84424999999999],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-79.259,-143.537],[-80.744875,-161.3375],[-82.089,-178.355],[-90.963,-174.593],[-80.007,-159.593],[-66.507,1.5],[-58.482,-36],[-71.713,-94.644]],"c":true}]},{"t":11}]},"nm":"B"},{"ind":1,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":4,"s":[{"i":[[-0.993,13.755],[-0.576,6.238],[1.302,0.585],[0,0],[0,0],[-2.275,-1.384],[-0.28,-3.435]],"o":[[1.757,-16.495],[0.509,-5.518],[-2.446,-1.099],[0,0],[0,0],[1.508,0.917],[1.082,-12.356]],"v":[[-36.507,-32.755],[-33.509,-58.982],[-33.554,-66.651],[-36.609,-64.578],[-46.5,1.214],[-41.725,1.384],[-40.332,4.606]],"c":true}],"e":[{"i":[[-0.741,19.537],[-0.161,8.855],[3.194,1.609],[0,0],[0,0],[-8.518,-3.5],[-1.037,-4.856]],"o":[[-0.241,-13.963],[0.11,-6.071],[-3.787,-1.907],[0,0],[0,0],[4.451,1.829],[0.963,-28.106]],"v":[[-29.259,-43.037],[-28.339,-74.105],[-32.213,-84.593],[-40.757,-84.843],[-54.257,6.75],[-37.482,2.75],[-30.463,12.606]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[-0.741,19.537],[-0.161,8.855],[3.194,1.609],[0,0],[0,0],[-8.518,-3.5],[-1.037,-4.856]],"o":[[-0.241,-13.963],[0.11,-6.071],[-3.787,-1.907],[0,0],[0,0],[4.451,1.829],[0.963,-28.106]],"v":[[-29.259,-43.037],[-28.339,-74.105],[-32.213,-84.593],[-40.757,-84.843],[-54.257,6.75],[-37.482,2.75],[-30.463,12.606]],"c":true}],"e":[{"i":[[0.832,26.037],[0.279,9.355],[-0.34,-0.907],[0,0],[0,0],[-1.764,2.5],[-2,4.644]],"o":[[-0.587,-21.463],[-0.181,-6.06],[-2.641,3.629],[0,0],[0,0],[1.764,-2.5],[3,-26.606]],"v":[[-48.414,-64.537],[-46.516,-92.355],[-47.66,-93.593],[-51.128,-76.843],[-60.507,13.25],[-56.514,6],[-52.5,-2.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[0.832,26.037],[0.279,9.355],[-0.34,-0.907],[0,0],[0,0],[-1.764,2.5],[-2,4.644]],"o":[[-0.587,-21.463],[-0.181,-6.06],[-2.641,3.629],[0,0],[0,0],[1.764,-2.5],[3,-26.606]],"v":[[-48.414,-64.537],[-46.516,-92.355],[-47.66,-93.593],[-51.128,-76.843],[-60.507,13.25],[-56.514,6],[-52.5,-2.394]],"c":true}],"e":[{"i":[[5.759,37.537],[2.589,11.855],[15.486,1.354],[0,0],[0,0],[-32.518,-11.5],[-4.287,-8.856]],"o":[[-6.241,-39.963],[-1.296,-5.932],[-27.537,-2.407],[0,0],[0,0],[21.489,7.6],[-3.287,-20.356]],"v":[[19.241,-49.037],[9.411,-111.855],[-17.463,-132.593],[-53.507,-120.593],[-55.507,2.5],[4.018,-9.5],[30.287,16.856]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[5.759,37.537],[2.589,11.855],[15.486,1.354],[0,0],[0,0],[-32.518,-11.5],[-4.287,-8.856]],"o":[[-6.241,-39.963],[-1.296,-5.932],[-27.537,-2.407],[0,0],[0,0],[21.489,7.6],[-3.287,-20.356]],"v":[[19.241,-49.037],[9.411,-111.855],[-17.463,-132.593],[-53.507,-120.593],[-55.507,2.5],[4.018,-9.5],[30.287,16.856]],"c":true}],"e":[{"i":[[7.259,32.537],[3.089,9.355],[44.113,-3.3],[0,0],[0,0],[-53.018,1],[3.713,19.144]],"o":[[-7.241,-38.463],[-1.904,-5.765],[-48.037,3.593],[0,0],[0,0],[60.505,-1.141],[-3.159,-16.288]],"v":[[83.741,-85.537],[72.411,-142.355],[4.537,-172.593],[-61.007,-138.593],[-59.007,1.5],[28.018,-33.5],[99.787,-17.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[7.259,32.537],[3.089,9.355],[44.113,-3.3],[0,0],[0,0],[-53.018,1],[3.713,19.144]],"o":[[-7.241,-38.463],[-1.904,-5.765],[-48.037,3.593],[0,0],[0,0],[60.505,-1.141],[-3.159,-16.288]],"v":[[83.741,-85.537],[72.411,-142.355],[4.537,-172.593],[-61.007,-138.593],[-59.007,1.5],[28.018,-33.5],[99.787,-17.644]],"c":true}],"e":[{"i":[[10.759,29.037],[4.471,5.31],[29.463,-40.407],[0,0],[0,0],[-43.018,23.5],[3.713,19.144]],"o":[[-7.241,-21.963],[-3.911,-4.645],[-7.99,10.959],[0,0],[0,0],[44.538,-24.331],[-3.159,-16.288]],"v":[[45.241,-195.037],[21.411,-260.355],[-37.963,-203.593],[-68.007,-155.093],[-61.507,3],[11.018,-60],[71.287,-111.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[10.759,29.037],[4.471,5.31],[29.463,-40.407],[0,0],[0,0],[-43.018,23.5],[3.713,19.144]],"o":[[-7.241,-21.963],[-3.911,-4.645],[-7.99,10.959],[0,0],[0,0],[44.538,-24.331],[-3.159,-16.288]],"v":[[45.241,-195.037],[21.411,-260.355],[-37.963,-203.593],[-68.007,-155.093],[-61.507,3],[11.018,-60],[71.287,-111.144]],"c":true}],"e":[{"i":[[4.259,17.037],[1.335,6.812],[2.963,-14.907],[0,0],[0,0],[-7.018,26.5],[3.713,19.144]],"o":[[-2.741,-12.463],[-0.911,-4.645],[-2.644,13.302],[0,0],[0,0],[9.673,-36.528],[-3.159,-16.288]],"v":[[-52.759,-171.537],[-67.089,-217.855],[-68.963,-189.593],[-76.007,-170.093],[-64.007,1.5],[-40.982,-50],[-39.713,-115.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[4.259,17.037],[1.335,6.812],[2.963,-14.907],[0,0],[0,0],[-7.018,26.5],[3.713,19.144]],"o":[[-2.741,-12.463],[-0.911,-4.645],[-2.644,13.302],[0,0],[0,0],[9.673,-36.528],[-3.159,-16.288]],"v":[[-52.759,-171.537],[-67.089,-217.855],[-68.963,-189.593],[-76.007,-170.093],[-64.007,1.5],[-40.982,-50],[-39.713,-115.644]],"c":true}],"e":[{"i":[[2.759,16.037],[0.552,6.919],[3.15,2.884],[0,0],[0,0],[4.147,22.987],[2.213,13.144]],"o":[[-0.741,-7.963],[-0.411,-5.145],[-1.537,-1.407],[0,0],[0,0],[-3.518,-19.5],[-2.755,-16.362]],"v":[[-79.259,-143.537],[-82.089,-178.355],[-90.963,-174.593],[-80.007,-159.593],[-66.507,1.5],[-58.482,-36],[-71.713,-94.644]],"c":true}]},{"t":11}]},"nm":"B 2"},{"ty":"mm","mm":1,"nm":"Merge Paths 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0.86,0.86,0.86,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":4,"op":11,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":21,"ty":4,"nm":"B blue layer 1","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":1,"s":[{"i":[[0,0],[1.135,0.16],[0.519,-0.169],[0.368,-1.304],[0.165,-1.422],[-2.463,0.226],[-0.024,0.731],[0.1,1.443]],"o":[[0,0],[-0.591,-0.084],[-0.572,0.81],[-0.408,1.446],[-0.141,1.212],[2.637,-0.242],[-0.113,-1.414],[-0.065,-0.946]],"v":[[-29.39,-0.524],[-31.301,-0.669],[-32.486,-0.16],[-34.047,2.548],[-34.859,5.55],[-31.571,8.49],[-28.75,5.075],[-29.073,1.857]],"c":true}],"e":[{"i":[[0,0],[4.131,0.584],[1.89,-0.615],[1.34,-4.746],[0.602,-5.175],[-8.964,0.823],[-0.088,2.66],[0.363,5.252]],"o":[[0,0],[-2.152,-0.304],[-2.08,2.95],[-1.486,5.262],[-0.513,4.413],[9.6,-0.881],[-0.41,-5.148],[-0.238,-3.443]],"v":[[-27.079,-7.92],[-34.036,-8.446],[-38.349,-6.594],[-44.031,3.262],[-46.987,14.191],[-35.017,24.893],[-24.75,12.463],[-25.925,0.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":2,"s":[{"i":[[0,0],[4.131,0.584],[1.89,-0.615],[1.34,-4.746],[0.602,-5.175],[-8.964,0.823],[-0.088,2.66],[0.363,5.252]],"o":[[0,0],[-2.152,-0.304],[-2.08,2.95],[-1.486,5.262],[-0.513,4.413],[9.6,-0.881],[-0.41,-5.148],[-0.238,-3.443]],"v":[[-27.079,-7.92],[-34.036,-8.446],[-38.349,-6.594],[-44.031,3.262],[-46.987,14.191],[-35.017,24.893],[-24.75,12.463],[-25.925,0.75]],"c":true}],"e":[{"i":[[0,0],[8.324,1.188],[3.808,-1.25],[2.7,-9.654],[1.213,-10.525],[-18.063,1.674],[-0.178,5.41],[0.731,10.682]],"o":[[0,0],[-4.337,-0.619],[-4.192,6],[-2.994,10.703],[-1.034,8.975],[19.344,-1.793],[-0.826,-10.471],[-0.479,-7.002]],"v":[[-18.099,-21.695],[-32.116,-22.765],[-40.808,-19],[-52.256,1.047],[-58.213,23.275],[-34.094,45.043],[-13.405,19.761],[-15.773,-4.063]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":3,"s":[{"i":[[0,0],[8.324,1.188],[3.808,-1.25],[2.7,-9.654],[1.213,-10.525],[-18.063,1.674],[-0.178,5.41],[0.731,10.682]],"o":[[0,0],[-4.337,-0.619],[-4.192,6],[-2.994,10.703],[-1.034,8.975],[19.344,-1.793],[-0.826,-10.471],[-0.479,-7.002]],"v":[[-18.099,-21.695],[-32.116,-22.765],[-40.808,-19],[-52.256,1.047],[-58.213,23.275],[-34.094,45.043],[-13.405,19.761],[-15.773,-4.063]],"c":true}],"e":[{"i":[[0,0],[12.843,1.833],[4.559,8.938],[3.186,-15.134],[3.035,-13.004],[-27.952,1.396],[-0.275,8.346],[1.128,16.48]],"o":[[0,0],[-6.691,-0.955],[-2.441,12.438],[-4.015,19.071],[-3.168,13.574],[31.05,-1.551],[-1.275,-16.154],[-0.74,-10.803]],"v":[[-2.195,-42.59],[-24.593,-43.083],[-44.559,-47.688],[-56.436,-7.116],[-64.082,27.176],[-28.8,62.301],[4.275,27.154],[0.622,-14.23]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":4,"s":[{"i":[[0,0],[12.843,1.833],[4.559,8.938],[3.186,-15.134],[3.035,-13.004],[-27.952,1.396],[-0.275,8.346],[1.128,16.48]],"o":[[0,0],[-6.691,-0.955],[-2.441,12.438],[-4.015,19.071],[-3.168,13.574],[31.05,-1.551],[-1.275,-16.154],[-0.74,-10.803]],"v":[[-2.195,-42.59],[-24.593,-43.083],[-44.559,-47.688],[-56.436,-7.116],[-64.082,27.176],[-28.8,62.301],[4.275,27.154],[0.622,-14.23]],"c":true}],"e":[{"i":[[0,0],[30.844,0.507],[4.059,4.688],[1.936,-11.384],[-2.168,-13.176],[-27.451,0.398],[1.225,11.846],[3.429,20.36]],"o":[[0,0],[-10.157,-0.167],[-1.941,14.188],[-3.267,19.214],[1.6,9.723],[38.05,-0.551],[-1.275,-15.904],[-3.372,-20.02]],"v":[[12.805,-58.84],[-26.843,-38.833],[-49.309,-49.938],[-55.686,-8.616],[-60.582,43.176],[-20.55,70.051],[26.275,28.904],[17.122,-30.23]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[0,0],[30.844,0.507],[4.059,4.688],[1.936,-11.384],[-2.168,-13.176],[-27.451,0.398],[1.225,11.846],[3.429,20.36]],"o":[[0,0],[-10.157,-0.167],[-1.941,14.188],[-3.267,19.214],[1.6,9.723],[38.05,-0.551],[-1.275,-15.904],[-3.372,-20.02]],"v":[[12.805,-58.84],[-26.843,-38.833],[-49.309,-49.938],[-55.686,-8.616],[-60.582,43.176],[-20.55,70.051],[26.275,28.904],[17.122,-30.23]],"c":true}],"e":[{"i":[[0,0],[-0.089,-0.987],[-0.278,-3.396],[-0.525,-7.148],[-4.945,-5.803],[-9.866,11.148],[0.725,3.596],[6.837,19.482]],"o":[[0,0],[0.121,1.333],[0.323,3.936],[1.428,19.437],[2.832,3.324],[11.55,-13.051],[-1.275,-15.904],[-5.622,-16.02]],"v":[[10.305,-57.09],[10.44,-55.64],[11.04,-48.623],[12.314,-32.116],[21.668,57.426],[44.2,51.551],[54.275,22.904],[39.872,-29.73]],"c":true}]},{"t":6}]},"nm":"B"},{"ind":1,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":1,"s":[{"i":[[-0.404,3.747],[1.477,0.688],[1.74,-9.708],[-5.091,0.021],[-0.25,4.402]],"o":[[-1.673,-0.138],[-1.523,11.938],[-0.954,5.323],[3.195,-0.013],[0.819,-14.412]],"v":[[-23.513,-54.481],[-25.144,-53.423],[-32.713,-2.808],[-29.826,7.494],[-26.917,-1.136]],"c":true}],"e":[{"i":[[-0.404,3.747],[1.477,0.688],[1.74,-9.708],[-5.091,0.021],[-0.25,4.402]],"o":[[-1.673,-0.138],[-1.523,11.938],[-0.954,5.323],[3.195,-0.013],[0.819,-14.412]],"v":[[-31.013,-45.315],[-32.644,-44.256],[-40.213,6.359],[-37.326,16.661],[-34.417,8.03]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":3,"s":[{"i":[[-0.404,3.747],[1.477,0.688],[1.74,-9.708],[-5.091,0.021],[-0.25,4.402]],"o":[[-1.673,-0.138],[-1.523,11.938],[-0.954,5.323],[3.195,-0.013],[0.819,-14.412]],"v":[[-31.013,-45.315],[-32.644,-44.256],[-40.213,6.359],[-37.326,16.661],[-34.417,8.03]],"c":true}],"e":[{"i":[[-0.404,3.747],[1.477,0.688],[1.74,-9.708],[-5.091,0.021],[-0.25,4.402]],"o":[[-1.673,-0.138],[-1.523,11.938],[-0.954,5.323],[3.196,-0.013],[0.819,-14.412]],"v":[[-37.096,-52.997],[-38.727,-51.938],[-46.296,-1.323],[-43.159,7.979],[-40.5,0.348]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":4,"s":[{"i":[[-0.404,3.747],[1.477,0.688],[1.74,-9.708],[-5.091,0.021],[-0.25,4.402]],"o":[[-1.673,-0.138],[-1.523,11.938],[-0.954,5.323],[3.196,-0.013],[0.819,-14.412]],"v":[[-37.096,-52.997],[-38.727,-51.938],[-46.296,-1.323],[-43.159,7.979],[-40.5,0.348]],"c":true}],"e":[{"i":[[-0.103,4.085],[4.059,4.688],[1.686,-13.134],[-16.7,0.449],[-0.025,15.596]],"o":[[-5.407,-0.167],[-1.941,14.188],[-2.481,19.331],[7.341,-0.197],[-0.025,-17.404]],"v":[[-31.343,-55.333],[-44.309,-60.438],[-51.186,-11.116],[-42.05,21.551],[-30.225,2.654]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[-0.103,4.085],[4.059,4.688],[1.686,-13.134],[-16.7,0.449],[-0.025,15.596]],"o":[[-5.407,-0.167],[-1.941,14.188],[-2.481,19.331],[7.341,-0.197],[-0.025,-17.404]],"v":[[-31.343,-55.333],[-44.309,-60.438],[-51.186,-11.116],[-42.05,21.551],[-30.225,2.654]],"c":true}],"e":[{"i":[[-0.089,-0.987],[-0.278,-3.396],[-0.525,-7.148],[-9.866,11.148],[0.725,3.596]],"o":[[0.121,1.333],[0.323,3.936],[1.428,19.437],[11.55,-13.051],[-1.275,-15.904]],"v":[[5.773,-76.735],[6.373,-69.719],[7.647,-53.212],[39.533,30.455],[49.608,1.808]],"c":true}]},{"t":6}]},"nm":"B 2"},{"ty":"fl","fillEnabled":true,"c":{"k":[0,0.01,0.24,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":1,"op":7,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":22,"ty":4,"nm":"B white layer 2 shadow 2","parent":34,"ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":16,"s":[5],"e":[40]},{"t":22}]},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[0,0],[0,0],[0,0],[-4,0.01],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[51.196,-0.129],[0,0],[0,0]],"v":[[-38.124,-198.023],[-153.77,-179.155],[-179.51,0.476],[-152.785,32.892],[-44.446,28.81],[-41.27,-85.157]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-4,0.01],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[51.196,-0.129],[0,0],[0,0]],"v":[[-26.124,-205.023],[-141.77,-186.155],[-167.51,-6.524],[-140.785,25.892],[-10.446,30.81],[-18.323,-87.679]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[0,0],[0,0],[0,0],[-4,0.01],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[51.196,-0.129],[0,0],[0,0]],"v":[[-26.124,-205.023],[-141.77,-186.155],[-167.51,-6.524],[-140.785,25.892],[-10.446,30.81],[-18.323,-87.679]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-4,0.01],[-22.446,5.81],[5.009,56.785]],"o":[[0,0],[0,0],[0,0],[51.196,-0.129],[11.259,-2.914],[-4.977,-56.418]],"v":[[7.876,-190.023],[-107.77,-171.155],[-133.51,8.476],[-106.785,40.892],[16.554,39.31],[20.584,-78.085]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[0,0],[0,0],[0,0],[-4,0.01],[-22.446,5.81],[5.009,56.785]],"o":[[0,0],[0,0],[0,0],[51.196,-0.129],[11.259,-2.914],[-4.977,-56.418]],"v":[[7.876,-190.023],[-107.77,-171.155],[-133.51,8.476],[-106.785,40.892],[16.554,39.31],[20.584,-78.085]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-18.5,31.5],[9.358,52.957]],"o":[[0,0],[0,0],[0,0],[50,-11],[9.28,-15.801],[-9.297,-52.616]],"v":[[37.5,-220],[-101,-170],[-88,11],[-55,37],[65.5,7],[58.347,-118.825]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-18.5,31.5],[9.358,52.957]],"o":[[0,0],[0,0],[0,0],[50,-11],[9.28,-15.801],[-9.297,-52.616]],"v":[[37.5,-220],[-101,-170],[-88,11],[-55,37],[65.5,7],[58.347,-118.825]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-18.5,33],[14.123,54.022]],"o":[[0,0],[0,0],[0,0],[50,-11],[9.28,-16.553],[-14.032,-53.673]],"v":[[-38.5,-242],[-236,-164],[-88,11],[-55,36],[8.5,-10],[-8.199,-138.898]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-18.5,33],[14.123,54.022]],"o":[[0,0],[0,0],[0,0],[50,-11],[9.28,-16.553],[-14.032,-53.673]],"v":[[-38.5,-242],[-236,-164],[-88,11],[-55,36],[8.5,-10],[-8.199,-138.898]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-10.976,14.749],[13.055,55.474]],"o":[[0,0],[0,0],[0,0],[50,-11],[8.026,-10.785],[-12.971,-55.116]],"v":[[-61,-230],[-236,-164],[-109,24],[-60,28],[-17,2],[-33.126,-122.6]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-10.976,14.749],[13.055,55.474]],"o":[[0,0],[0,0],[0,0],[50,-11],[8.026,-10.785],[-12.971,-55.116]],"v":[[-61,-230],[-236,-164],[-109,24],[-60,28],[-17,2],[-33.126,-122.6]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-9,14.5],[13.551,58.865]],"o":[[0,0],[0,0],[0,0],[50,-11],[4.515,-7.273],[-13.463,-58.485]],"v":[[-61,-237],[-191,-176],[-109,24],[-76,30],[-11.5,5],[-33.006,-122.007]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-9,14.5],[13.551,58.865]],"o":[[0,0],[0,0],[0,0],[50,-11],[4.515,-7.273],[-13.463,-58.485]],"v":[[-61,-237],[-191,-176],[-109,24],[-76,30],[-11.5,5],[-33.006,-122.007]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-14,18],[17.568,58.925]],"o":[[0,0],[0,0],[0,0],[50,-11],[7.023,-9.029],[-17.454,-58.545]],"v":[[-56,-244],[-191,-176],[-109,24],[-76,30],[7,0],[-19.42,-129.321]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-14,18],[17.568,58.925]],"o":[[0,0],[0,0],[0,0],[50,-11],[7.023,-9.029],[-17.454,-58.545]],"v":[[-56,-244],[-191,-176],[-109,24],[-76,30],[7,0],[-19.42,-129.321]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-12,13],[17.065,63.067]],"o":[[0,0],[0,0],[0,0],[50,-11],[6.019,-6.521],[-16.954,-62.66]],"v":[[-39,-257],[-191,-176],[-109,24],[-76,30],[23,1],[-3.665,-133.486]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-12,13],[17.065,63.067]],"o":[[0,0],[0,0],[0,0],[50,-11],[6.019,-6.521],[-16.954,-62.66]],"v":[[-39,-257],[-191,-176],[-109,24],[-76,30],[23,1],[-3.665,-133.486]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-9,17],[17.689,64.569]],"o":[[0,0],[0,0],[0,0],[50,-11],[4.515,-8.528],[-17.575,-64.152]],"v":[[-13,-271],[-191,-176],[-109,24],[-76,30],[53,-5],[23.204,-145]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-9,17],[17.689,64.569]],"o":[[0,0],[0,0],[0,0],[50,-11],[4.515,-8.528],[-17.575,-64.152]],"v":[[-13,-271],[-191,-176],[-109,24],[-76,30],[53,-5],[23.204,-145]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-16,20],[23.562,61.869]],"o":[[0,0],[0,0],[0,0],[50,-11],[7.752,-9.689],[-8.438,-57.131]],"v":[[17,-282],[-191,-176],[-109,24],[-76,30],[71,-9],[50.438,-146.869]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-16,20],[23.562,61.869]],"o":[[0,0],[0,0],[0,0],[50,-11],[7.752,-9.689],[-8.438,-57.131]],"v":[[17,-282],[-191,-176],[-109,24],[-76,30],[71,-9],[50.438,-146.869]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-16,15],[24.604,34.177]],"o":[[0,0],[0,0],[0,0],[50,-11],[16,-15],[3.604,-39.823]],"v":[[46,-289],[-116,-244],[-109,24],[-51,19],[77,-7],[61.396,-150.177]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-16,15],[24.604,34.177]],"o":[[0,0],[0,0],[0,0],[50,-11],[16,-15],[3.604,-39.823]],"v":[[46,-289],[-116,-244],[-109,24],[-51,19],[77,-7],[61.396,-150.177]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-10.999,17.25],[28.431,29.796]],"o":[[0,0],[0,0],[0,0],[50,-11],[10.999,-17.25],[16.431,-29.204]],"v":[[65.252,-286.25],[-116,-244],[-109,24],[-51,19],[88.001,-9.75],[65.569,-132.796]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-10.999,17.25],[28.431,29.796]],"o":[[0,0],[0,0],[0,0],[50,-11],[10.999,-17.25],[16.431,-29.204]],"v":[[65.252,-286.25],[-116,-244],[-109,24],[-51,19],[88.001,-9.75],[65.569,-132.796]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[50,-11],[0,0],[0,0]],"v":[[92,-282],[-116,-244],[-109,24],[-51,19],[110,22],[100.956,-130.738]],"c":true}]},{"t":19.1474609375}]},"o":{"k":100},"x":{"k":0},"nm":"Mask 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[-1.282,17.269],[-0.661,17.73],[1.838,0.593],[0,0],[0,0],[-1.643,0],[-0.912,-1.481]],"o":[[1.259,-16.963],[0.075,-2.02],[-1.701,-0.549],[0,0],[0,0],[1.257,0],[0.338,-3.231]],"v":[[-41.009,-32.537],[-38.089,-79.48],[-39.588,-83.593],[-42.507,-82.468],[-50.507,4.375],[-47.107,4],[-44.213,5.856]],"c":true}],"e":[{"i":[[-0.008,17.316],[0.339,16.48],[3.281,1.942],[0,0],[0,0],[-6.268,-1],[-1.162,-9.106]],"o":[[0.009,-21.463],[-0.098,-4.771],[-4.912,-2.907],[0,0],[0,0],[5.666,0.904],[-0.412,-10.356]],"v":[[-34.509,-42.537],[-34.339,-91.23],[-37.588,-101.593],[-47.007,-102.468],[-59.132,7.5],[-45.982,2.75],[-34.588,14.106]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[-0.008,17.316],[0.339,16.48],[3.281,1.942],[0,0],[0,0],[-6.268,-1],[-1.162,-9.106]],"o":[[0.009,-21.463],[-0.098,-4.771],[-4.912,-2.907],[0,0],[0,0],[5.666,0.904],[-0.412,-10.356]],"v":[[-34.509,-42.537],[-34.339,-91.23],[-37.588,-101.593],[-47.007,-102.468],[-59.132,7.5],[-45.982,2.75],[-34.588,14.106]],"c":true}],"e":[{"i":[[1.009,17.287],[2.01,28.256],[7.354,3.812],[0,0],[0,0],[-11.66,-5.191],[-3.162,-10.856]],"o":[[-1.064,-18.224],[-0.411,-5.77],[-10.912,-5.657],[0,0],[0,0],[11.232,5],[-1.162,-18.356]],"v":[[-16.009,-48.037],[-20.089,-101.23],[-28.588,-120.343],[-52.757,-120.968],[-55.882,3.75],[-26.232,2.5],[-10.838,21.356]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[1.009,17.287],[2.01,28.256],[7.354,3.812],[0,0],[0,0],[-11.66,-5.191],[-3.162,-10.856]],"o":[[-1.064,-18.224],[-0.411,-5.77],[-10.912,-5.657],[0,0],[0,0],[11.232,5],[-1.162,-18.356]],"v":[[-16.009,-48.037],[-20.089,-101.23],[-28.588,-120.343],[-52.757,-120.968],[-55.882,3.75],[-26.232,2.5],[-10.838,21.356]],"c":true}],"e":[{"i":[[3.581,24.09],[2.339,28.23],[18.566,5.173],[0,0],[0,0],[-42.815,-12.244],[-3.162,-9.856]],"o":[[-4.491,-30.213],[-0.972,-11.734],[-34.662,-9.657],[0,0],[0,0],[19.232,5.5],[-2.412,-16.356]],"v":[[28.491,-50.787],[21.161,-112.48],[-7.588,-142.843],[-60.007,-138.968],[-58.382,3.5],[13.518,-2.5],[41.412,22.606]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[3.581,24.09],[2.339,28.23],[18.566,5.173],[0,0],[0,0],[-42.815,-12.244],[-3.162,-9.856]],"o":[[-4.491,-30.213],[-0.972,-11.734],[-34.662,-9.657],[0,0],[0,0],[19.232,5.5],[-2.412,-16.356]],"v":[[28.491,-50.787],[21.161,-112.48],[-7.588,-142.843],[-60.007,-138.968],[-58.382,3.5],[13.518,-2.5],[41.412,22.606]],"c":true}],"e":[{"i":[[6.259,23.537],[9.028,29.832],[35.338,-11.407],[0,0],[0,0],[-43.647,8.83],[7.838,30.644]],"o":[[-6.901,-25.951],[-3.411,-11.27],[-35.699,11.524],[0,0],[0,0],[66.732,-13.5],[-4.529,-17.709]],"v":[[98.991,-111.037],[77.161,-192.23],[3.912,-187.593],[-68.757,-153.968],[-60.882,2],[23.768,-27],[112.412,-55.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[6.259,23.537],[9.028,29.832],[35.338,-11.407],[0,0],[0,0],[-43.647,8.83],[7.838,30.644]],"o":[[-6.901,-25.951],[-3.411,-11.27],[-35.699,11.524],[0,0],[0,0],[66.732,-13.5],[-4.529,-17.709]],"v":[[98.991,-111.037],[77.161,-192.23],[3.912,-187.593],[-68.757,-153.968],[-60.882,2],[23.768,-27],[112.412,-55.894]],"c":true}],"e":[{"i":[[9.241,25.213],[14.339,35.48],[31.838,-35.407],[0,0],[0,0],[-37.642,23.794],[7.838,30.644]],"o":[[-9.241,-25.213],[-15.283,-37.816],[-16.668,18.536],[0,0],[0,0],[45.482,-28.75],[-4.529,-17.709]],"v":[[33.991,-189.537],[9.161,-252.48],[-36.838,-201.593],[-76.007,-167.218],[-63.882,2],[24.768,-48],[62.412,-113.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[9.241,25.213],[14.339,35.48],[31.838,-35.407],[0,0],[0,0],[-37.642,23.794],[7.838,30.644]],"o":[[-9.241,-25.213],[-15.283,-37.816],[-16.668,18.536],[0,0],[0,0],[45.482,-28.75],[-4.529,-17.709]],"v":[[33.991,-189.537],[9.161,-252.48],[-36.838,-201.593],[-76.007,-167.218],[-63.882,2],[24.768,-48],[62.412,-113.394]],"c":true}],"e":[{"i":[[9.241,25.213],[15.339,35.73],[24.588,-31.657],[0,0],[0,0],[-33.385,29.471],[8.088,31.144]],"o":[[-9.241,-25.213],[-19.116,-44.526],[-15.291,19.687],[0,0],[0,0],[38.232,-33.75],[-4.594,-17.692]],"v":[[1.491,-181.287],[-33.839,-268.23],[-51.588,-209.843],[-83.257,-178.968],[-66.382,1.5],[2.768,-41.25],[25.662,-113.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[9.241,25.213],[15.339,35.73],[24.588,-31.657],[0,0],[0,0],[-33.385,29.471],[8.088,31.144]],"o":[[-9.241,-25.213],[-19.116,-44.526],[-15.291,19.687],[0,0],[0,0],[38.232,-33.75],[-4.594,-17.692]],"v":[[1.491,-181.287],[-33.839,-268.23],[-51.588,-209.843],[-83.257,-178.968],[-66.382,1.5],[2.768,-41.25],[25.662,-113.144]],"c":true}],"e":[{"i":[[9.241,25.213],[14.589,37.98],[24.588,-31.657],[0,0],[0,0],[-36.476,25.546],[8.088,31.144]],"o":[[-9.241,-25.213],[-17.376,-45.234],[-15.291,19.687],[0,0],[0,0],[34.982,-24.5],[-4.594,-17.692]],"v":[[-0.759,-180.537],[-36.839,-272.73],[-56.338,-216.843],[-89.257,-186.468],[-69.382,1],[6.518,-38],[24.662,-104.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[9.241,25.213],[14.589,37.98],[24.588,-31.657],[0,0],[0,0],[-36.476,25.546],[8.088,31.144]],"o":[[-9.241,-25.213],[-17.376,-45.234],[-15.291,19.687],[0,0],[0,0],[34.982,-24.5],[-4.594,-17.692]],"v":[[-0.759,-180.537],[-36.839,-272.73],[-56.338,-216.843],[-89.257,-186.468],[-69.382,1],[6.518,-38],[24.662,-104.894]],"c":true}],"e":[{"i":[[8.009,25.037],[13.247,38.034],[35.338,-37.657],[0,0],[0,0],[-39.772,20.031],[8.088,31.144]],"o":[[-11.991,-32.463],[-17.16,-49.27],[-21.388,22.792],[0,0],[0,0],[36.732,-18.5],[-4.594,-17.692]],"v":[[11.241,-169.037],[-25.59,-271.98],[-60.088,-220.343],[-93.257,-193.718],[-71.382,1],[15.768,-34.75],[32.662,-102.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[{"i":[[8.009,25.037],[13.247,38.034],[35.338,-37.657],[0,0],[0,0],[-39.772,20.031],[8.088,31.144]],"o":[[-11.991,-32.463],[-17.16,-49.27],[-21.388,22.792],[0,0],[0,0],[36.732,-18.5],[-4.594,-17.692]],"v":[[11.241,-169.037],[-25.59,-271.98],[-60.088,-220.343],[-93.257,-193.718],[-71.382,1],[15.768,-34.75],[32.662,-102.144]],"c":true}],"e":[{"i":[[8.259,24.037],[10.641,35.776],[35.838,-35.157],[0,0],[0,0],[-42.018,14.75],[10.338,43.894]],"o":[[-9.241,-31.213],[-13.91,-46.77],[-22.312,21.888],[0,0],[0,0],[40.972,-14.383],[-7.971,-33.842]],"v":[[23.991,-176.037],[-5.09,-269.48],[-49.338,-232.593],[-95.757,-200.718],[-72.382,0.25],[27.268,-29],[47.662,-96.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[8.259,24.037],[10.641,35.776],[35.838,-35.157],[0,0],[0,0],[-42.018,14.75],[10.338,43.894]],"o":[[-9.241,-31.213],[-13.91,-46.77],[-22.312,21.888],[0,0],[0,0],[40.972,-14.383],[-7.971,-33.842]],"v":[[23.991,-176.037],[-5.09,-269.48],[-49.338,-232.593],[-95.757,-200.718],[-72.382,0.25],[27.268,-29],[47.662,-96.394]],"c":true}],"e":[{"i":[[7.009,23.287],[7.59,33.73],[50.58,-35.169],[0,0],[0,0],[0,0],[11.714,49.114]],"o":[[-7.241,-28.463],[-10.225,-45.445],[-25.662,17.843],[0,0],[0,0],[43.732,-7.5],[-10.162,-42.606]],"v":[[42.741,-173.287],[20.411,-260.73],[-42.588,-234.593],[-97.257,-206.968],[-73.882,0.25],[35.018,-20.5],[65.662,-88.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[7.009,23.287],[7.59,33.73],[50.58,-35.169],[0,0],[0,0],[0,0],[11.714,49.114]],"o":[[-7.241,-28.463],[-10.225,-45.445],[-25.662,17.843],[0,0],[0,0],[43.732,-7.5],[-10.162,-42.606]],"v":[[42.741,-173.287],[20.411,-260.73],[-42.588,-234.593],[-97.257,-206.968],[-73.882,0.25],[35.018,-20.5],[65.662,-88.144]],"c":true}],"e":[{"i":[[3.259,21.287],[6.044,31.942],[57.588,-23.657],[0,0],[0,0],[0,0],[10.088,66.644]],"o":[[-1.741,-23.963],[-8.661,-45.77],[-33.448,13.74],[0,0],[0,0],[43.732,-7.5],[-8.74,-57.737]],"v":[[54.991,-175.537],[44.161,-246.98],[-37.838,-233.593],[-96.757,-212.718],[-75.382,0],[38.518,-14.75],[79.662,-80.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[3.259,21.287],[6.044,31.942],[57.588,-23.657],[0,0],[0,0],[0,0],[10.088,66.644]],"o":[[-1.741,-23.963],[-8.661,-45.77],[-33.448,13.74],[0,0],[0,0],[43.732,-7.5],[-8.74,-57.737]],"v":[[54.991,-175.537],[44.161,-246.98],[-37.838,-233.593],[-96.757,-212.718],[-75.382,0],[38.518,-14.75],[79.662,-80.894]],"c":true}],"e":[{"i":[[3.259,21.287],[4.985,35.86],[31.838,-13.407],[0,0],[0,0],[0,0],[7.588,68.394]],"o":[[-1.491,-26.213],[-5.911,-42.52],[-31.691,13.345],[0,0],[0,0],[38.732,-3.75],[-6.443,-58.072]],"v":[[65.241,-152.287],[61.661,-221.73],[10.912,-247.343],[-94.007,-216.968],[-77.382,-0.75],[40.268,-9.5],[88.162,-79.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[3.259,21.287],[4.985,35.86],[31.838,-13.407],[0,0],[0,0],[0,0],[7.588,68.394]],"o":[[-1.491,-26.213],[-5.911,-42.52],[-31.691,13.345],[0,0],[0,0],[38.732,-3.75],[-6.443,-58.072]],"v":[[65.241,-152.287],[61.661,-221.73],[10.912,-247.343],[-94.007,-216.968],[-77.382,-0.75],[40.268,-9.5],[88.162,-79.394]],"c":true}],"e":[{"i":[[2.759,8.787],[5.839,35.73],[33.162,-9.093],[0,0],[0,0],[0,0],[2.478,49.663]],"o":[[-2.741,-7.463],[-6.377,-39.022],[-33.162,9.093],[0,0],[0,0],[38.732,-3.75],[-2.912,-58.356]],"v":[[66.241,-138.287],[71.911,-205.73],[16.162,-241.093],[-91.507,-220.468],[-77.382,-0.75],[41.768,-6.5],[92.912,-66.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[2.759,8.787],[5.839,35.73],[33.162,-9.093],[0,0],[0,0],[0,0],[2.478,49.663]],"o":[[-2.741,-7.463],[-6.377,-39.022],[-33.162,9.093],[0,0],[0,0],[38.732,-3.75],[-2.912,-58.356]],"v":[[66.241,-138.287],[71.911,-205.73],[16.162,-241.093],[-91.507,-220.468],[-77.382,-0.75],[41.768,-6.5],[92.912,-66.894]],"c":true}],"e":[{"i":[[27.259,18.287],[2.089,21.98],[37.838,-4.407],[0,0],[0,0],[0,0],[0.838,46.394]],"o":[[11.509,-17.213],[-3.742,-39.362],[0,0],[0,0],[0,0],[36.982,0],[-0.523,-28.962]],"v":[[63.241,-131.037],[77.161,-188.98],[16.162,-235.843],[-89.507,-222.468],[-78.882,-0.5],[31.768,-3.75],[95.162,-60.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[27.259,18.287],[2.089,21.98],[37.838,-4.407],[0,0],[0,0],[0,0],[0.838,46.394]],"o":[[11.509,-17.213],[-3.742,-39.362],[0,0],[0,0],[0,0],[36.982,0],[-0.523,-28.962]],"v":[[63.241,-131.037],[77.161,-188.98],[16.162,-235.843],[-89.507,-222.468],[-78.882,-0.5],[31.768,-3.75],[95.162,-60.894]],"c":true}],"e":[{"i":[[25.509,15.287],[1.964,22.98],[38.338,-2.657],[0,0],[0,0],[0,0],[-0.037,44.769]],"o":[[11.884,-14.088],[-3.365,-39.367],[0,0],[0,0],[0,0],[36.982,0],[0.026,-31.246]],"v":[[61.616,-125.787],[79.286,-179.98],[15.537,-232.218],[-86.382,-224.093],[-79.007,-0.375],[30.268,-2.75],[94.037,-62.519]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[25.509,15.287],[1.964,22.98],[38.338,-2.657],[0,0],[0,0],[0,0],[-0.037,44.769]],"o":[[11.884,-14.088],[-3.365,-39.367],[0,0],[0,0],[0,0],[36.982,0],[0.026,-31.246]],"v":[[61.616,-125.787],[79.286,-179.98],[15.537,-232.218],[-86.382,-224.093],[-79.007,-0.375],[30.268,-2.75],[94.037,-62.519]],"c":true}],"e":[{"i":[[23.759,12.287],[0.374,21.444],[38.838,-0.907],[0,0],[0,0],[0,0],[-0.076,41.089]],"o":[[12.259,-10.963],[-0.705,-39.533],[0,0],[0,0],[0,0],[36.982,0],[0.053,-28.967]],"v":[[59.491,-123.537],[79.911,-170.48],[17.412,-229.593],[-83.257,-225.718],[-79.132,-0.25],[28.768,-1.75],[93.412,-61.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[23.759,12.287],[0.374,21.444],[38.838,-0.907],[0,0],[0,0],[0,0],[-0.076,41.089]],"o":[[12.259,-10.963],[-0.705,-39.533],[0,0],[0,0],[0,0],[36.982,0],[0.053,-28.967]],"v":[[59.491,-123.537],[79.911,-170.48],[17.412,-229.593],[-83.257,-225.718],[-79.132,-0.25],[28.768,-1.75],[93.412,-61.644]],"c":true}],"e":[{"i":[[23.009,8.287],[0.374,21.444],[37.94,-0.602],[0,0],[0,0],[0,0],[-0.076,41.089]],"o":[[15.106,-9.064],[-0.705,-39.533],[0,0],[0,0],[0,0],[39.468,0.25],[0.053,-28.967]],"v":[[55.991,-119.287],[79.661,-166.48],[11.162,-226.593],[-79.507,-226.218],[-79.132,-0.25],[22.518,-0.25],[91.162,-62.644]],"c":true}]},{"t":22.2529296875}]},"nm":"B"},{"ind":1,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[-2.196,-15.458],[4.87,-2.756],[0,0],[0,0],[0,0]],"o":[[1.997,14.059],[0,0],[0,0],[0,0],[2.979,-1.252]],"v":[[-276.179,-132.292],[-277.37,-102.994],[-282.552,-101.351],[-288.355,-146.931],[-283.229,-149.748]],"c":true}],"e":[{"i":[[-2.196,-15.458],[4.87,-2.756],[0,0],[0,0],[0,0]],"o":[[1.997,14.059],[0,0],[0,0],[0,0],[2.979,-1.252]],"v":[[-45.179,-170.292],[-46.37,-140.994],[-51.552,-139.351],[-57.355,-184.931],[-52.229,-187.748]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[-2.196,-15.458],[4.87,-2.756],[0,0],[0,0],[0,0]],"o":[[1.997,14.059],[0,0],[0,0],[0,0],[2.979,-1.252]],"v":[[-45.179,-170.292],[-46.37,-140.994],[-51.552,-139.351],[-57.355,-184.931],[-52.229,-187.748]],"c":true}],"e":[{"i":[[-2.196,-15.458],[10.37,-3.756],[0,0],[0,0],[0,0]],"o":[[1.997,14.059],[0,0],[0,0],[0,0],[5.729,-1.752]],"v":[[-29.679,-174.292],[-36.62,-141.244],[-48.302,-138.851],[-53.605,-186.431],[-42.979,-189.748]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[-2.196,-15.458],[10.37,-3.756],[0,0],[0,0],[0,0]],"o":[[1.997,14.059],[0,0],[0,0],[0,0],[5.729,-1.752]],"v":[[-29.679,-174.292],[-36.62,-141.244],[-48.302,-138.851],[-53.605,-186.431],[-42.979,-189.748]],"c":true}],"e":[{"i":[[-2.196,-15.458],[11.62,-1.881],[0,0],[0,0],[0,0]],"o":[[1.997,14.059],[0,0],[0,0],[0,0],[12.104,-2.127]],"v":[[-14.179,-171.542],[-26.62,-140.744],[-44.802,-137.851],[-48.605,-186.931],[-32.479,-190.998]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[-2.196,-15.458],[11.62,-1.881],[0,0],[0,0],[0,0]],"o":[[1.997,14.059],[0,0],[0,0],[0,0],[12.104,-2.127]],"v":[[-14.179,-171.542],[-26.62,-140.744],[-44.802,-137.851],[-48.605,-186.931],[-32.479,-190.998]],"c":true}],"e":[{"i":[[-0.946,-15.083],[11.62,-1.881],[0,0],[0,0],[0,0]],"o":[[0.889,14.172],[0,0],[0,0],[0,0],[11.854,-1.752]],"v":[[-0.554,-168.667],[-16.12,-139.619],[-41.927,-136.351],[-45.105,-186.931],[-21.854,-190.998]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[-0.946,-15.083],[11.62,-1.881],[0,0],[0,0],[0,0]],"o":[[0.889,14.172],[0,0],[0,0],[0,0],[11.854,-1.752]],"v":[[-0.554,-168.667],[-16.12,-139.619],[-41.927,-136.351],[-45.105,-186.931],[-21.854,-190.998]],"c":true}],"e":[{"i":[[0,-14.502],[17.62,-0.131],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[16.104,-1.752]],"v":[[9.196,-166.167],[-14.62,-138.119],[-39.927,-136.351],[-41.855,-185.931],[-15.354,-189.498]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[0,-14.502],[17.62,-0.131],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[16.104,-1.752]],"v":[[9.196,-166.167],[-14.62,-138.119],[-39.927,-136.351],[-41.855,-185.931],[-15.354,-189.498]],"c":true}],"e":[{"i":[[0,-14.502],[18.37,-0.631],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[15.303,-0.876]],"v":[[18.196,-164.417],[-8.37,-136.494],[-37.552,-135.101],[-38.855,-185.806],[-8.479,-188.373]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[0,-14.502],[18.37,-0.631],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[15.303,-0.876]],"v":[[18.196,-164.417],[-8.37,-136.494],[-37.552,-135.101],[-38.855,-185.806],[-8.479,-188.373]],"c":true}],"e":[{"i":[[0,-14.502],[19.12,-1.131],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[24.196,-162.667],[-2.12,-134.869],[-35.177,-133.851],[-35.855,-185.681],[-1.604,-187.248]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[0,-14.502],[19.12,-1.131],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[24.196,-162.667],[-2.12,-134.869],[-35.177,-133.851],[-35.855,-185.681],[-1.604,-187.248]],"c":true}],"e":[{"i":[[0,-14.502],[18.37,0.935],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[30.446,-160.417],[1.88,-133.369],[-33.177,-133.101],[-33.105,-184.681],[4.646,-184.998]],"c":true}]},{"t":22.2529296875}]},"nm":"B"},{"ind":2,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[6.056,-1.63],[0,0],[0,0],[0,0],[-1.281,-9.949]],"o":[[0,0],[0,0],[0,0],[4.931,-1.967],[1.649,12.81]],"v":[[-287.306,-16.62],[-293.677,-14.62],[-299.802,-64.783],[-294.431,-67.033],[-284.719,-47.051]],"c":true}],"e":[{"i":[[6.056,-1.63],[0,0],[0,0],[0,0],[-1.281,-9.949]],"o":[[0,0],[0,0],[0,0],[4.931,-1.967],[1.649,12.81]],"v":[[-33.306,-52.62],[-39.677,-50.62],[-45.802,-100.783],[-40.431,-103.033],[-30.719,-83.051]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[6.056,-1.63],[0,0],[0,0],[0,0],[-1.281,-9.949]],"o":[[0,0],[0,0],[0,0],[4.931,-1.967],[1.649,12.81]],"v":[[-33.306,-52.62],[-39.677,-50.62],[-45.802,-100.783],[-40.431,-103.033],[-30.719,-83.051]],"c":true}],"e":[{"i":[[11.556,-3.13],[0,0],[0,0],[0,0],[-0.953,-10.127]],"o":[[0,0],[0,0],[0,0],[8.681,-1.967],[0.969,10.301]],"v":[[-21.806,-51.87],[-37.427,-49.12],[-43.552,-99.783],[-28.431,-103.533],[-13.719,-83.551]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[11.556,-3.13],[0,0],[0,0],[0,0],[-0.953,-10.127]],"o":[[0,0],[0,0],[0,0],[8.681,-1.967],[0.969,10.301]],"v":[[-21.806,-51.87],[-37.427,-49.12],[-43.552,-99.783],[-28.431,-103.533],[-13.719,-83.551]],"c":true}],"e":[{"i":[[13.181,-3.38],[0,0],[0,0],[0,0],[-0.953,-10.127]],"o":[[0,0],[0,0],[0,0],[14.306,-2.342],[0.969,10.301]],"v":[[-10.556,-49.87],[-36.177,-47.37],[-40.302,-98.783],[-18.181,-102.533],[2.031,-79.801]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[13.181,-3.38],[0,0],[0,0],[0,0],[-0.953,-10.127]],"o":[[0,0],[0,0],[0,0],[14.306,-2.342],[0.969,10.301]],"v":[[-10.556,-49.87],[-36.177,-47.37],[-40.302,-98.783],[-18.181,-102.533],[2.031,-79.801]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[-5.181,-47.745],[-34.927,-46.745],[-38.427,-97.908],[-7.931,-101.158],[14.156,-75.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[-5.181,-47.745],[-34.927,-46.745],[-38.427,-97.908],[-7.931,-101.158],[14.156,-75.176]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[2.819,-46.495],[-33.927,-44.495],[-36.427,-96.908],[0.069,-98.908],[23.656,-72.676]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[2.819,-46.495],[-33.927,-44.495],[-36.427,-96.908],[0.069,-98.908],[23.656,-72.676]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[16.556,-0.967],[0,14.2]],"v":[[6.819,-45.12],[-33.427,-44.245],[-34.802,-95.783],[5.694,-97.283],[31.781,-71.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[16.556,-0.967],[0,14.2]],"v":[[6.819,-45.12],[-33.427,-44.245],[-34.802,-95.783],[5.694,-97.283],[31.781,-71.176]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[10.819,-43.745],[-32.927,-43.995],[-33.177,-94.658],[11.319,-95.658],[36.406,-69.676]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[10.819,-43.745],[-32.927,-43.995],[-33.177,-94.658],[11.319,-95.658],[36.406,-69.676]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[14.819,-41.995],[-32.677,-41.995],[-32.677,-93.658],[16.569,-94.158],[41.656,-68.176]],"c":true}]},{"t":22.2529296875}]},"nm":"B"},{"ty":"mm","mm":1,"nm":"Merge Paths 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0,0.32,0.5,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":15,"op":23,"st":0,"bm":10,"sr":1},{"ddd":0,"ind":23,"ty":4,"nm":"B white layer 2 shadow","parent":34,"ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":16,"s":[5],"e":[40]},{"t":22}]},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[0,0],[0,0],[0,0],[-4,0.01],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[51.196,-0.129],[0,0],[0,0]],"v":[[-38.124,-198.023],[-153.77,-179.155],[-179.51,0.476],[-152.785,32.892],[-44.446,28.81],[-41.27,-85.157]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-4,0.01],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[51.196,-0.129],[0,0],[0,0]],"v":[[-26.124,-205.023],[-141.77,-186.155],[-167.51,-6.524],[-140.785,25.892],[-10.446,30.81],[-18.323,-87.679]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[0,0],[0,0],[0,0],[-4,0.01],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[51.196,-0.129],[0,0],[0,0]],"v":[[-26.124,-205.023],[-141.77,-186.155],[-167.51,-6.524],[-140.785,25.892],[-10.446,30.81],[-18.323,-87.679]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-4,0.01],[-22.446,5.81],[5.009,56.785]],"o":[[0,0],[0,0],[0,0],[51.196,-0.129],[11.259,-2.914],[-4.977,-56.418]],"v":[[7.876,-190.023],[-107.77,-171.155],[-133.51,8.476],[-106.785,40.892],[16.554,39.31],[20.584,-78.085]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[0,0],[0,0],[0,0],[-4,0.01],[-22.446,5.81],[5.009,56.785]],"o":[[0,0],[0,0],[0,0],[51.196,-0.129],[11.259,-2.914],[-4.977,-56.418]],"v":[[7.876,-190.023],[-107.77,-171.155],[-133.51,8.476],[-106.785,40.892],[16.554,39.31],[20.584,-78.085]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-18.5,31.5],[9.358,52.957]],"o":[[0,0],[0,0],[0,0],[50,-11],[9.28,-15.801],[-9.297,-52.616]],"v":[[37.5,-220],[-101,-170],[-88,11],[-55,37],[65.5,7],[58.347,-118.825]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-18.5,31.5],[9.358,52.957]],"o":[[0,0],[0,0],[0,0],[50,-11],[9.28,-15.801],[-9.297,-52.616]],"v":[[37.5,-220],[-101,-170],[-88,11],[-55,37],[65.5,7],[58.347,-118.825]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-18.5,33],[14.123,54.022]],"o":[[0,0],[0,0],[0,0],[50,-11],[9.28,-16.553],[-14.032,-53.673]],"v":[[-38.5,-242],[-236,-164],[-88,11],[-55,36],[8.5,-10],[-8.199,-138.898]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-18.5,33],[14.123,54.022]],"o":[[0,0],[0,0],[0,0],[50,-11],[9.28,-16.553],[-14.032,-53.673]],"v":[[-38.5,-242],[-236,-164],[-88,11],[-55,36],[8.5,-10],[-8.199,-138.898]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-10.976,14.749],[13.055,55.474]],"o":[[0,0],[0,0],[0,0],[50,-11],[8.026,-10.785],[-12.971,-55.116]],"v":[[-61,-230],[-236,-164],[-109,24],[-60,28],[-17,2],[-33.126,-122.6]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-10.976,14.749],[13.055,55.474]],"o":[[0,0],[0,0],[0,0],[50,-11],[8.026,-10.785],[-12.971,-55.116]],"v":[[-61,-230],[-236,-164],[-109,24],[-60,28],[-17,2],[-33.126,-122.6]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-9,14.5],[13.551,58.865]],"o":[[0,0],[0,0],[0,0],[50,-11],[4.515,-7.273],[-13.463,-58.485]],"v":[[-61,-237],[-191,-176],[-109,24],[-76,30],[-11.5,5],[-33.006,-122.007]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-9,14.5],[13.551,58.865]],"o":[[0,0],[0,0],[0,0],[50,-11],[4.515,-7.273],[-13.463,-58.485]],"v":[[-61,-237],[-191,-176],[-109,24],[-76,30],[-11.5,5],[-33.006,-122.007]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-14,18],[17.568,58.925]],"o":[[0,0],[0,0],[0,0],[50,-11],[7.023,-9.029],[-17.454,-58.545]],"v":[[-56,-244],[-191,-176],[-109,24],[-76,30],[7,0],[-19.42,-129.321]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-14,18],[17.568,58.925]],"o":[[0,0],[0,0],[0,0],[50,-11],[7.023,-9.029],[-17.454,-58.545]],"v":[[-56,-244],[-191,-176],[-109,24],[-76,30],[7,0],[-19.42,-129.321]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-12,13],[17.065,63.067]],"o":[[0,0],[0,0],[0,0],[50,-11],[6.019,-6.521],[-16.954,-62.66]],"v":[[-39,-257],[-191,-176],[-109,24],[-76,30],[23,1],[-3.665,-133.486]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-12,13],[17.065,63.067]],"o":[[0,0],[0,0],[0,0],[50,-11],[6.019,-6.521],[-16.954,-62.66]],"v":[[-39,-257],[-191,-176],[-109,24],[-76,30],[23,1],[-3.665,-133.486]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-9,17],[17.689,64.569]],"o":[[0,0],[0,0],[0,0],[50,-11],[4.515,-8.528],[-17.575,-64.152]],"v":[[-13,-271],[-191,-176],[-109,24],[-76,30],[53,-5],[23.204,-145]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-9,17],[17.689,64.569]],"o":[[0,0],[0,0],[0,0],[50,-11],[4.515,-8.528],[-17.575,-64.152]],"v":[[-13,-271],[-191,-176],[-109,24],[-76,30],[53,-5],[23.204,-145]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-16,20],[23.562,61.869]],"o":[[0,0],[0,0],[0,0],[50,-11],[7.752,-9.689],[-8.438,-57.131]],"v":[[17,-282],[-191,-176],[-109,24],[-76,30],[71,-9],[50.438,-146.869]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-16,20],[23.562,61.869]],"o":[[0,0],[0,0],[0,0],[50,-11],[7.752,-9.689],[-8.438,-57.131]],"v":[[17,-282],[-191,-176],[-109,24],[-76,30],[71,-9],[50.438,-146.869]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-16,15],[24.604,34.177]],"o":[[0,0],[0,0],[0,0],[50,-11],[16,-15],[3.604,-39.823]],"v":[[46,-289],[-116,-244],[-109,24],[-51,19],[77,-7],[61.396,-150.177]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-16,15],[24.604,34.177]],"o":[[0,0],[0,0],[0,0],[50,-11],[16,-15],[3.604,-39.823]],"v":[[46,-289],[-116,-244],[-109,24],[-51,19],[77,-7],[61.396,-150.177]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-10.999,17.25],[28.431,29.796]],"o":[[0,0],[0,0],[0,0],[50,-11],[10.999,-17.25],[16.431,-29.204]],"v":[[65.252,-286.25],[-116,-244],[-109,24],[-51,19],[88.001,-9.75],[65.569,-132.796]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18,"s":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[-10.999,17.25],[28.431,29.796]],"o":[[0,0],[0,0],[0,0],[50,-11],[10.999,-17.25],[16.431,-29.204]],"v":[[65.252,-286.25],[-116,-244],[-109,24],[-51,19],[88.001,-9.75],[65.569,-132.796]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0],[-3.907,0.859],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[50,-11],[0,0],[0,0]],"v":[[92,-282],[-116,-244],[-109,24],[-51,19],[110,22],[100.956,-130.738]],"c":true}]},{"t":19.1474609375}]},"o":{"k":100},"x":{"k":0},"nm":"Mask 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[-1.282,17.269],[-0.661,17.73],[1.838,0.593],[0,0],[0,0],[-1.643,0],[-0.912,-1.481]],"o":[[1.259,-16.963],[0.075,-2.02],[-1.701,-0.549],[0,0],[0,0],[1.257,0],[0.338,-3.231]],"v":[[-41.009,-32.537],[-38.089,-79.48],[-39.588,-83.593],[-42.507,-82.468],[-50.507,4.375],[-47.107,4],[-44.213,5.856]],"c":true}],"e":[{"i":[[-0.008,17.316],[0.339,16.48],[3.281,1.942],[0,0],[0,0],[-6.268,-1],[-1.162,-9.106]],"o":[[0.009,-21.463],[-0.098,-4.771],[-4.912,-2.907],[0,0],[0,0],[5.666,0.904],[-0.412,-10.356]],"v":[[-34.509,-42.537],[-34.339,-91.23],[-37.588,-101.593],[-47.007,-102.468],[-59.132,7.5],[-45.982,2.75],[-34.588,14.106]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[-0.008,17.316],[0.339,16.48],[3.281,1.942],[0,0],[0,0],[-6.268,-1],[-1.162,-9.106]],"o":[[0.009,-21.463],[-0.098,-4.771],[-4.912,-2.907],[0,0],[0,0],[5.666,0.904],[-0.412,-10.356]],"v":[[-34.509,-42.537],[-34.339,-91.23],[-37.588,-101.593],[-47.007,-102.468],[-59.132,7.5],[-45.982,2.75],[-34.588,14.106]],"c":true}],"e":[{"i":[[1.009,17.287],[2.01,28.256],[7.354,3.812],[0,0],[0,0],[-11.66,-5.191],[-3.162,-10.856]],"o":[[-1.064,-18.224],[-0.411,-5.77],[-10.912,-5.657],[0,0],[0,0],[11.232,5],[-1.162,-18.356]],"v":[[-16.009,-48.037],[-20.089,-101.23],[-28.588,-120.343],[-52.757,-120.968],[-55.882,3.75],[-26.232,2.5],[-10.838,21.356]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[1.009,17.287],[2.01,28.256],[7.354,3.812],[0,0],[0,0],[-11.66,-5.191],[-3.162,-10.856]],"o":[[-1.064,-18.224],[-0.411,-5.77],[-10.912,-5.657],[0,0],[0,0],[11.232,5],[-1.162,-18.356]],"v":[[-16.009,-48.037],[-20.089,-101.23],[-28.588,-120.343],[-52.757,-120.968],[-55.882,3.75],[-26.232,2.5],[-10.838,21.356]],"c":true}],"e":[{"i":[[3.581,24.09],[2.339,28.23],[18.566,5.173],[0,0],[0,0],[-42.815,-12.244],[-3.162,-9.856]],"o":[[-4.491,-30.213],[-0.972,-11.734],[-34.662,-9.657],[0,0],[0,0],[19.232,5.5],[-2.412,-16.356]],"v":[[28.491,-50.787],[21.161,-112.48],[-7.588,-142.843],[-60.007,-138.968],[-58.382,3.5],[13.518,-2.5],[41.412,22.606]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[3.581,24.09],[2.339,28.23],[18.566,5.173],[0,0],[0,0],[-42.815,-12.244],[-3.162,-9.856]],"o":[[-4.491,-30.213],[-0.972,-11.734],[-34.662,-9.657],[0,0],[0,0],[19.232,5.5],[-2.412,-16.356]],"v":[[28.491,-50.787],[21.161,-112.48],[-7.588,-142.843],[-60.007,-138.968],[-58.382,3.5],[13.518,-2.5],[41.412,22.606]],"c":true}],"e":[{"i":[[6.259,23.537],[9.028,29.832],[35.338,-11.407],[0,0],[0,0],[-43.647,8.83],[7.838,30.644]],"o":[[-6.901,-25.951],[-3.411,-11.27],[-35.699,11.524],[0,0],[0,0],[66.732,-13.5],[-4.529,-17.709]],"v":[[98.991,-111.037],[77.161,-192.23],[3.912,-187.593],[-68.757,-153.968],[-60.882,2],[23.768,-27],[112.412,-55.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[6.259,23.537],[9.028,29.832],[35.338,-11.407],[0,0],[0,0],[-43.647,8.83],[7.838,30.644]],"o":[[-6.901,-25.951],[-3.411,-11.27],[-35.699,11.524],[0,0],[0,0],[66.732,-13.5],[-4.529,-17.709]],"v":[[98.991,-111.037],[77.161,-192.23],[3.912,-187.593],[-68.757,-153.968],[-60.882,2],[23.768,-27],[112.412,-55.894]],"c":true}],"e":[{"i":[[9.241,25.213],[14.339,35.48],[31.838,-35.407],[0,0],[0,0],[-37.642,23.794],[7.838,30.644]],"o":[[-9.241,-25.213],[-15.283,-37.816],[-16.668,18.536],[0,0],[0,0],[45.482,-28.75],[-4.529,-17.709]],"v":[[33.991,-189.537],[9.161,-252.48],[-36.838,-201.593],[-76.007,-167.218],[-63.882,2],[24.768,-48],[62.412,-113.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[9.241,25.213],[14.339,35.48],[31.838,-35.407],[0,0],[0,0],[-37.642,23.794],[7.838,30.644]],"o":[[-9.241,-25.213],[-15.283,-37.816],[-16.668,18.536],[0,0],[0,0],[45.482,-28.75],[-4.529,-17.709]],"v":[[33.991,-189.537],[9.161,-252.48],[-36.838,-201.593],[-76.007,-167.218],[-63.882,2],[24.768,-48],[62.412,-113.394]],"c":true}],"e":[{"i":[[9.241,25.213],[15.339,35.73],[24.588,-31.657],[0,0],[0,0],[-33.385,29.471],[8.088,31.144]],"o":[[-9.241,-25.213],[-19.116,-44.526],[-15.291,19.687],[0,0],[0,0],[38.232,-33.75],[-4.594,-17.692]],"v":[[1.491,-181.287],[-33.839,-268.23],[-51.588,-209.843],[-83.257,-178.968],[-66.382,1.5],[2.768,-41.25],[25.662,-113.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[9.241,25.213],[15.339,35.73],[24.588,-31.657],[0,0],[0,0],[-33.385,29.471],[8.088,31.144]],"o":[[-9.241,-25.213],[-19.116,-44.526],[-15.291,19.687],[0,0],[0,0],[38.232,-33.75],[-4.594,-17.692]],"v":[[1.491,-181.287],[-33.839,-268.23],[-51.588,-209.843],[-83.257,-178.968],[-66.382,1.5],[2.768,-41.25],[25.662,-113.144]],"c":true}],"e":[{"i":[[9.241,25.213],[14.589,37.98],[24.588,-31.657],[0,0],[0,0],[-36.476,25.546],[8.088,31.144]],"o":[[-9.241,-25.213],[-17.376,-45.234],[-15.291,19.687],[0,0],[0,0],[34.982,-24.5],[-4.594,-17.692]],"v":[[-0.759,-180.537],[-36.839,-272.73],[-56.338,-216.843],[-89.257,-186.468],[-69.382,1],[6.518,-38],[24.662,-104.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[9.241,25.213],[14.589,37.98],[24.588,-31.657],[0,0],[0,0],[-36.476,25.546],[8.088,31.144]],"o":[[-9.241,-25.213],[-17.376,-45.234],[-15.291,19.687],[0,0],[0,0],[34.982,-24.5],[-4.594,-17.692]],"v":[[-0.759,-180.537],[-36.839,-272.73],[-56.338,-216.843],[-89.257,-186.468],[-69.382,1],[6.518,-38],[24.662,-104.894]],"c":true}],"e":[{"i":[[8.009,25.037],[13.247,38.034],[35.338,-37.657],[0,0],[0,0],[-39.772,20.031],[8.088,31.144]],"o":[[-11.991,-32.463],[-17.16,-49.27],[-21.388,22.792],[0,0],[0,0],[36.732,-18.5],[-4.594,-17.692]],"v":[[11.241,-169.037],[-25.59,-271.98],[-60.088,-220.343],[-93.257,-193.718],[-71.382,1],[15.768,-34.75],[32.662,-102.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[{"i":[[8.009,25.037],[13.247,38.034],[35.338,-37.657],[0,0],[0,0],[-39.772,20.031],[8.088,31.144]],"o":[[-11.991,-32.463],[-17.16,-49.27],[-21.388,22.792],[0,0],[0,0],[36.732,-18.5],[-4.594,-17.692]],"v":[[11.241,-169.037],[-25.59,-271.98],[-60.088,-220.343],[-93.257,-193.718],[-71.382,1],[15.768,-34.75],[32.662,-102.144]],"c":true}],"e":[{"i":[[8.259,24.037],[10.641,35.776],[35.838,-35.157],[0,0],[0,0],[-42.018,14.75],[10.338,43.894]],"o":[[-9.241,-31.213],[-13.91,-46.77],[-22.312,21.888],[0,0],[0,0],[40.972,-14.383],[-7.971,-33.842]],"v":[[23.991,-176.037],[-5.09,-269.48],[-49.338,-232.593],[-95.757,-200.718],[-72.382,0.25],[27.268,-29],[47.662,-96.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[8.259,24.037],[10.641,35.776],[35.838,-35.157],[0,0],[0,0],[-42.018,14.75],[10.338,43.894]],"o":[[-9.241,-31.213],[-13.91,-46.77],[-22.312,21.888],[0,0],[0,0],[40.972,-14.383],[-7.971,-33.842]],"v":[[23.991,-176.037],[-5.09,-269.48],[-49.338,-232.593],[-95.757,-200.718],[-72.382,0.25],[27.268,-29],[47.662,-96.394]],"c":true}],"e":[{"i":[[7.009,23.287],[7.59,33.73],[50.58,-35.169],[0,0],[0,0],[0,0],[11.714,49.114]],"o":[[-7.241,-28.463],[-10.225,-45.445],[-25.662,17.843],[0,0],[0,0],[43.732,-7.5],[-10.162,-42.606]],"v":[[42.741,-173.287],[20.411,-260.73],[-42.588,-234.593],[-97.257,-206.968],[-73.882,0.25],[35.018,-20.5],[65.662,-88.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[7.009,23.287],[7.59,33.73],[50.58,-35.169],[0,0],[0,0],[0,0],[11.714,49.114]],"o":[[-7.241,-28.463],[-10.225,-45.445],[-25.662,17.843],[0,0],[0,0],[43.732,-7.5],[-10.162,-42.606]],"v":[[42.741,-173.287],[20.411,-260.73],[-42.588,-234.593],[-97.257,-206.968],[-73.882,0.25],[35.018,-20.5],[65.662,-88.144]],"c":true}],"e":[{"i":[[3.259,21.287],[6.044,31.942],[57.588,-23.657],[0,0],[0,0],[0,0],[10.088,66.644]],"o":[[-1.741,-23.963],[-8.661,-45.77],[-33.448,13.74],[0,0],[0,0],[43.732,-7.5],[-8.74,-57.737]],"v":[[54.991,-175.537],[44.161,-246.98],[-37.838,-233.593],[-96.757,-212.718],[-75.382,0],[38.518,-14.75],[79.662,-80.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[3.259,21.287],[6.044,31.942],[57.588,-23.657],[0,0],[0,0],[0,0],[10.088,66.644]],"o":[[-1.741,-23.963],[-8.661,-45.77],[-33.448,13.74],[0,0],[0,0],[43.732,-7.5],[-8.74,-57.737]],"v":[[54.991,-175.537],[44.161,-246.98],[-37.838,-233.593],[-96.757,-212.718],[-75.382,0],[38.518,-14.75],[79.662,-80.894]],"c":true}],"e":[{"i":[[3.259,21.287],[4.985,35.86],[31.838,-13.407],[0,0],[0,0],[0,0],[7.588,68.394]],"o":[[-1.491,-26.213],[-5.911,-42.52],[-31.691,13.345],[0,0],[0,0],[38.732,-3.75],[-6.443,-58.072]],"v":[[65.241,-152.287],[61.661,-221.73],[10.912,-247.343],[-94.007,-216.968],[-77.382,-0.75],[40.268,-9.5],[88.162,-79.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[3.259,21.287],[4.985,35.86],[31.838,-13.407],[0,0],[0,0],[0,0],[7.588,68.394]],"o":[[-1.491,-26.213],[-5.911,-42.52],[-31.691,13.345],[0,0],[0,0],[38.732,-3.75],[-6.443,-58.072]],"v":[[65.241,-152.287],[61.661,-221.73],[10.912,-247.343],[-94.007,-216.968],[-77.382,-0.75],[40.268,-9.5],[88.162,-79.394]],"c":true}],"e":[{"i":[[2.759,8.787],[5.839,35.73],[33.162,-9.093],[0,0],[0,0],[0,0],[2.478,49.663]],"o":[[-2.741,-7.463],[-6.377,-39.022],[-33.162,9.093],[0,0],[0,0],[38.732,-3.75],[-2.912,-58.356]],"v":[[66.241,-138.287],[71.911,-205.73],[16.162,-241.093],[-91.507,-220.468],[-77.382,-0.75],[41.768,-6.5],[92.912,-66.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[2.759,8.787],[5.839,35.73],[33.162,-9.093],[0,0],[0,0],[0,0],[2.478,49.663]],"o":[[-2.741,-7.463],[-6.377,-39.022],[-33.162,9.093],[0,0],[0,0],[38.732,-3.75],[-2.912,-58.356]],"v":[[66.241,-138.287],[71.911,-205.73],[16.162,-241.093],[-91.507,-220.468],[-77.382,-0.75],[41.768,-6.5],[92.912,-66.894]],"c":true}],"e":[{"i":[[27.259,18.287],[2.089,21.98],[37.838,-4.407],[0,0],[0,0],[0,0],[0.838,46.394]],"o":[[11.509,-17.213],[-3.742,-39.362],[0,0],[0,0],[0,0],[36.982,0],[-0.523,-28.962]],"v":[[63.241,-131.037],[77.161,-188.98],[16.162,-235.843],[-89.507,-222.468],[-78.882,-0.5],[31.768,-3.75],[95.162,-60.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[27.259,18.287],[2.089,21.98],[37.838,-4.407],[0,0],[0,0],[0,0],[0.838,46.394]],"o":[[11.509,-17.213],[-3.742,-39.362],[0,0],[0,0],[0,0],[36.982,0],[-0.523,-28.962]],"v":[[63.241,-131.037],[77.161,-188.98],[16.162,-235.843],[-89.507,-222.468],[-78.882,-0.5],[31.768,-3.75],[95.162,-60.894]],"c":true}],"e":[{"i":[[25.509,15.287],[1.964,22.98],[38.338,-2.657],[0,0],[0,0],[0,0],[-0.037,44.769]],"o":[[11.884,-14.088],[-3.365,-39.367],[0,0],[0,0],[0,0],[36.982,0],[0.026,-31.246]],"v":[[61.616,-125.787],[79.286,-179.98],[15.537,-232.218],[-86.382,-224.093],[-79.007,-0.375],[30.268,-2.75],[94.037,-62.519]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[25.509,15.287],[1.964,22.98],[38.338,-2.657],[0,0],[0,0],[0,0],[-0.037,44.769]],"o":[[11.884,-14.088],[-3.365,-39.367],[0,0],[0,0],[0,0],[36.982,0],[0.026,-31.246]],"v":[[61.616,-125.787],[79.286,-179.98],[15.537,-232.218],[-86.382,-224.093],[-79.007,-0.375],[30.268,-2.75],[94.037,-62.519]],"c":true}],"e":[{"i":[[23.759,12.287],[0.374,21.444],[38.838,-0.907],[0,0],[0,0],[0,0],[-0.076,41.089]],"o":[[12.259,-10.963],[-0.705,-39.533],[0,0],[0,0],[0,0],[36.982,0],[0.053,-28.967]],"v":[[59.491,-123.537],[79.911,-170.48],[17.412,-229.593],[-83.257,-225.718],[-79.132,-0.25],[28.768,-1.75],[93.412,-61.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[23.759,12.287],[0.374,21.444],[38.838,-0.907],[0,0],[0,0],[0,0],[-0.076,41.089]],"o":[[12.259,-10.963],[-0.705,-39.533],[0,0],[0,0],[0,0],[36.982,0],[0.053,-28.967]],"v":[[59.491,-123.537],[79.911,-170.48],[17.412,-229.593],[-83.257,-225.718],[-79.132,-0.25],[28.768,-1.75],[93.412,-61.644]],"c":true}],"e":[{"i":[[23.009,8.287],[0.374,21.444],[37.94,-0.602],[0,0],[0,0],[0,0],[-0.076,41.089]],"o":[[15.106,-9.064],[-0.705,-39.533],[0,0],[0,0],[0,0],[39.468,0.25],[0.053,-28.967]],"v":[[55.991,-119.287],[79.661,-166.48],[11.162,-226.593],[-79.507,-226.218],[-79.132,-0.25],[22.518,-0.25],[91.162,-62.644]],"c":true}]},{"t":22.2529296875}]},"nm":"B"},{"ty":"fl","fillEnabled":true,"c":{"k":[0,0.32,0.5,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":6,"op":15,"st":0,"bm":10,"sr":1},{"ddd":0,"ind":24,"ty":4,"nm":"B white layer 3","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[-1.282,17.269],[-0.661,17.73],[1.838,0.593],[0,0],[0,0],[-1.643,0],[-0.912,-1.481]],"o":[[1.259,-16.963],[0.075,-2.02],[-1.701,-0.549],[0,0],[0,0],[1.257,0],[0.338,-3.231]],"v":[[-41.009,-32.537],[-38.089,-79.48],[-39.588,-83.593],[-42.507,-82.468],[-50.507,4.375],[-47.107,4],[-44.213,5.856]],"c":true}],"e":[{"i":[[-0.008,17.316],[0.339,16.48],[3.281,1.942],[0,0],[0,0],[-6.268,-1],[-1.162,-9.106]],"o":[[0.009,-21.463],[-0.098,-4.771],[-4.912,-2.907],[0,0],[0,0],[5.666,0.904],[-0.412,-10.356]],"v":[[-34.509,-42.537],[-34.339,-91.23],[-37.588,-101.593],[-47.007,-102.468],[-59.132,7.5],[-45.982,2.75],[-34.588,14.106]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[-0.008,17.316],[0.339,16.48],[3.281,1.942],[0,0],[0,0],[-6.268,-1],[-1.162,-9.106]],"o":[[0.009,-21.463],[-0.098,-4.771],[-4.912,-2.907],[0,0],[0,0],[5.666,0.904],[-0.412,-10.356]],"v":[[-34.509,-42.537],[-34.339,-91.23],[-37.588,-101.593],[-47.007,-102.468],[-59.132,7.5],[-45.982,2.75],[-34.588,14.106]],"c":true}],"e":[{"i":[[1.009,17.287],[2.01,28.256],[7.354,3.812],[0,0],[0,0],[-11.66,-5.191],[-3.162,-10.856]],"o":[[-1.064,-18.224],[-0.411,-5.77],[-10.912,-5.657],[0,0],[0,0],[11.232,5],[-1.162,-18.356]],"v":[[-16.009,-48.037],[-20.089,-101.23],[-28.588,-120.343],[-52.757,-120.968],[-55.882,3.75],[-26.232,2.5],[-10.838,21.356]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[1.009,17.287],[2.01,28.256],[7.354,3.812],[0,0],[0,0],[-11.66,-5.191],[-3.162,-10.856]],"o":[[-1.064,-18.224],[-0.411,-5.77],[-10.912,-5.657],[0,0],[0,0],[11.232,5],[-1.162,-18.356]],"v":[[-16.009,-48.037],[-20.089,-101.23],[-28.588,-120.343],[-52.757,-120.968],[-55.882,3.75],[-26.232,2.5],[-10.838,21.356]],"c":true}],"e":[{"i":[[3.581,24.09],[2.339,28.23],[18.566,5.173],[0,0],[0,0],[-42.815,-12.244],[-3.162,-9.856]],"o":[[-4.491,-30.213],[-0.972,-11.734],[-34.662,-9.657],[0,0],[0,0],[19.232,5.5],[-2.412,-16.356]],"v":[[28.491,-50.787],[21.161,-112.48],[-7.588,-142.843],[-60.007,-138.968],[-58.382,3.5],[13.518,-2.5],[41.412,22.606]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[3.581,24.09],[2.339,28.23],[18.566,5.173],[0,0],[0,0],[-42.815,-12.244],[-3.162,-9.856]],"o":[[-4.491,-30.213],[-0.972,-11.734],[-34.662,-9.657],[0,0],[0,0],[19.232,5.5],[-2.412,-16.356]],"v":[[28.491,-50.787],[21.161,-112.48],[-7.588,-142.843],[-60.007,-138.968],[-58.382,3.5],[13.518,-2.5],[41.412,22.606]],"c":true}],"e":[{"i":[[6.259,23.537],[9.028,29.832],[35.338,-11.407],[0,0],[0,0],[-43.647,8.83],[7.838,30.644]],"o":[[-6.901,-25.951],[-3.411,-11.27],[-35.699,11.524],[0,0],[0,0],[66.732,-13.5],[-4.529,-17.709]],"v":[[98.991,-111.037],[77.161,-192.23],[3.912,-187.593],[-68.757,-153.968],[-60.882,2],[23.768,-27],[112.412,-55.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[6.259,23.537],[9.028,29.832],[35.338,-11.407],[0,0],[0,0],[-43.647,8.83],[7.838,30.644]],"o":[[-6.901,-25.951],[-3.411,-11.27],[-35.699,11.524],[0,0],[0,0],[66.732,-13.5],[-4.529,-17.709]],"v":[[98.991,-111.037],[77.161,-192.23],[3.912,-187.593],[-68.757,-153.968],[-60.882,2],[23.768,-27],[112.412,-55.894]],"c":true}],"e":[{"i":[[9.241,25.213],[14.339,35.48],[31.838,-35.407],[0,0],[0,0],[-37.642,23.794],[7.838,30.644]],"o":[[-9.241,-25.213],[-15.283,-37.816],[-16.668,18.536],[0,0],[0,0],[45.482,-28.75],[-4.529,-17.709]],"v":[[33.991,-189.537],[9.161,-252.48],[-36.838,-201.593],[-76.007,-167.218],[-63.882,2],[24.768,-48],[62.412,-113.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[9.241,25.213],[14.339,35.48],[31.838,-35.407],[0,0],[0,0],[-37.642,23.794],[7.838,30.644]],"o":[[-9.241,-25.213],[-15.283,-37.816],[-16.668,18.536],[0,0],[0,0],[45.482,-28.75],[-4.529,-17.709]],"v":[[33.991,-189.537],[9.161,-252.48],[-36.838,-201.593],[-76.007,-167.218],[-63.882,2],[24.768,-48],[62.412,-113.394]],"c":true}],"e":[{"i":[[9.241,25.213],[15.339,35.73],[24.588,-31.657],[0,0],[0,0],[-33.385,29.471],[8.088,31.144]],"o":[[-9.241,-25.213],[-19.116,-44.526],[-15.291,19.687],[0,0],[0,0],[38.232,-33.75],[-4.594,-17.692]],"v":[[1.491,-181.287],[-33.839,-268.23],[-51.588,-209.843],[-83.257,-178.968],[-66.382,1.5],[2.768,-41.25],[25.662,-113.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[9.241,25.213],[15.339,35.73],[24.588,-31.657],[0,0],[0,0],[-33.385,29.471],[8.088,31.144]],"o":[[-9.241,-25.213],[-19.116,-44.526],[-15.291,19.687],[0,0],[0,0],[38.232,-33.75],[-4.594,-17.692]],"v":[[1.491,-181.287],[-33.839,-268.23],[-51.588,-209.843],[-83.257,-178.968],[-66.382,1.5],[2.768,-41.25],[25.662,-113.144]],"c":true}],"e":[{"i":[[9.241,25.213],[14.589,37.98],[24.588,-31.657],[0,0],[0,0],[-36.476,25.546],[8.088,31.144]],"o":[[-9.241,-25.213],[-17.376,-45.234],[-15.291,19.687],[0,0],[0,0],[34.982,-24.5],[-4.594,-17.692]],"v":[[-0.759,-180.537],[-36.839,-272.73],[-56.338,-216.843],[-89.257,-186.468],[-69.382,1],[6.518,-38],[24.662,-104.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[9.241,25.213],[14.589,37.98],[24.588,-31.657],[0,0],[0,0],[-36.476,25.546],[8.088,31.144]],"o":[[-9.241,-25.213],[-17.376,-45.234],[-15.291,19.687],[0,0],[0,0],[34.982,-24.5],[-4.594,-17.692]],"v":[[-0.759,-180.537],[-36.839,-272.73],[-56.338,-216.843],[-89.257,-186.468],[-69.382,1],[6.518,-38],[24.662,-104.894]],"c":true}],"e":[{"i":[[8.009,25.037],[13.247,38.034],[35.338,-37.657],[0,0],[0,0],[-39.772,20.031],[8.088,31.144]],"o":[[-11.991,-32.463],[-17.16,-49.27],[-21.388,22.792],[0,0],[0,0],[36.732,-18.5],[-4.594,-17.692]],"v":[[11.241,-169.037],[-25.59,-271.98],[-60.088,-220.343],[-93.257,-193.718],[-71.382,1],[15.768,-34.75],[32.662,-102.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[{"i":[[8.009,25.037],[13.247,38.034],[35.338,-37.657],[0,0],[0,0],[-39.772,20.031],[8.088,31.144]],"o":[[-11.991,-32.463],[-17.16,-49.27],[-21.388,22.792],[0,0],[0,0],[36.732,-18.5],[-4.594,-17.692]],"v":[[11.241,-169.037],[-25.59,-271.98],[-60.088,-220.343],[-93.257,-193.718],[-71.382,1],[15.768,-34.75],[32.662,-102.144]],"c":true}],"e":[{"i":[[8.259,24.037],[10.641,35.776],[35.838,-35.157],[0,0],[0,0],[-42.018,14.75],[10.338,43.894]],"o":[[-9.241,-31.213],[-13.91,-46.77],[-22.312,21.888],[0,0],[0,0],[40.972,-14.383],[-7.971,-33.842]],"v":[[23.991,-176.037],[-5.09,-269.48],[-49.338,-232.593],[-95.757,-200.718],[-72.382,0.25],[27.268,-29],[47.662,-96.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[8.259,24.037],[10.641,35.776],[35.838,-35.157],[0,0],[0,0],[-42.018,14.75],[10.338,43.894]],"o":[[-9.241,-31.213],[-13.91,-46.77],[-22.312,21.888],[0,0],[0,0],[40.972,-14.383],[-7.971,-33.842]],"v":[[23.991,-176.037],[-5.09,-269.48],[-49.338,-232.593],[-95.757,-200.718],[-72.382,0.25],[27.268,-29],[47.662,-96.394]],"c":true}],"e":[{"i":[[7.009,23.287],[7.59,33.73],[50.58,-35.169],[0,0],[0,0],[0,0],[11.714,49.114]],"o":[[-7.241,-28.463],[-10.225,-45.445],[-25.662,17.843],[0,0],[0,0],[43.732,-7.5],[-10.162,-42.606]],"v":[[42.741,-173.287],[20.411,-260.73],[-42.588,-234.593],[-97.257,-206.968],[-73.882,0.25],[35.018,-20.5],[65.662,-88.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[7.009,23.287],[7.59,33.73],[50.58,-35.169],[0,0],[0,0],[0,0],[11.714,49.114]],"o":[[-7.241,-28.463],[-10.225,-45.445],[-25.662,17.843],[0,0],[0,0],[43.732,-7.5],[-10.162,-42.606]],"v":[[42.741,-173.287],[20.411,-260.73],[-42.588,-234.593],[-97.257,-206.968],[-73.882,0.25],[35.018,-20.5],[65.662,-88.144]],"c":true}],"e":[{"i":[[3.259,21.287],[6.044,31.942],[57.588,-23.657],[0,0],[0,0],[0,0],[10.088,66.644]],"o":[[-1.741,-23.963],[-8.661,-45.77],[-33.448,13.74],[0,0],[0,0],[43.732,-7.5],[-8.74,-57.737]],"v":[[54.991,-175.537],[44.161,-246.98],[-37.838,-233.593],[-96.757,-212.718],[-75.382,0],[38.518,-14.75],[79.662,-80.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[3.259,21.287],[6.044,31.942],[57.588,-23.657],[0,0],[0,0],[0,0],[10.088,66.644]],"o":[[-1.741,-23.963],[-8.661,-45.77],[-33.448,13.74],[0,0],[0,0],[43.732,-7.5],[-8.74,-57.737]],"v":[[54.991,-175.537],[44.161,-246.98],[-37.838,-233.593],[-96.757,-212.718],[-75.382,0],[38.518,-14.75],[79.662,-80.894]],"c":true}],"e":[{"i":[[3.259,21.287],[4.985,35.86],[31.838,-13.407],[0,0],[0,0],[0,0],[7.588,68.394]],"o":[[-1.491,-26.213],[-5.911,-42.52],[-31.691,13.345],[0,0],[0,0],[38.732,-3.75],[-6.443,-58.072]],"v":[[65.241,-152.287],[61.661,-221.73],[10.912,-247.343],[-94.007,-216.968],[-77.382,-0.75],[40.268,-9.5],[88.162,-79.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[3.259,21.287],[4.985,35.86],[31.838,-13.407],[0,0],[0,0],[0,0],[7.588,68.394]],"o":[[-1.491,-26.213],[-5.911,-42.52],[-31.691,13.345],[0,0],[0,0],[38.732,-3.75],[-6.443,-58.072]],"v":[[65.241,-152.287],[61.661,-221.73],[10.912,-247.343],[-94.007,-216.968],[-77.382,-0.75],[40.268,-9.5],[88.162,-79.394]],"c":true}],"e":[{"i":[[2.759,8.787],[5.839,35.73],[33.162,-9.093],[0,0],[0,0],[0,0],[2.478,49.663]],"o":[[-2.741,-7.463],[-6.377,-39.022],[-33.162,9.093],[0,0],[0,0],[38.732,-3.75],[-2.912,-58.356]],"v":[[66.241,-138.287],[71.911,-205.73],[16.162,-241.093],[-91.507,-220.468],[-77.382,-0.75],[41.768,-6.5],[92.912,-66.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[2.759,8.787],[5.839,35.73],[33.162,-9.093],[0,0],[0,0],[0,0],[2.478,49.663]],"o":[[-2.741,-7.463],[-6.377,-39.022],[-33.162,9.093],[0,0],[0,0],[38.732,-3.75],[-2.912,-58.356]],"v":[[66.241,-138.287],[71.911,-205.73],[16.162,-241.093],[-91.507,-220.468],[-77.382,-0.75],[41.768,-6.5],[92.912,-66.894]],"c":true}],"e":[{"i":[[27.259,18.287],[2.089,21.98],[37.838,-4.407],[0,0],[0,0],[0,0],[0.838,46.394]],"o":[[11.509,-17.213],[-3.742,-39.362],[0,0],[0,0],[0,0],[36.982,0],[-0.523,-28.962]],"v":[[63.241,-131.037],[77.161,-188.98],[16.162,-235.843],[-89.507,-222.468],[-78.882,-0.5],[31.768,-3.75],[95.162,-60.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[27.259,18.287],[2.089,21.98],[37.838,-4.407],[0,0],[0,0],[0,0],[0.838,46.394]],"o":[[11.509,-17.213],[-3.742,-39.362],[0,0],[0,0],[0,0],[36.982,0],[-0.523,-28.962]],"v":[[63.241,-131.037],[77.161,-188.98],[16.162,-235.843],[-89.507,-222.468],[-78.882,-0.5],[31.768,-3.75],[95.162,-60.894]],"c":true}],"e":[{"i":[[25.509,15.287],[1.964,22.98],[38.338,-2.657],[0,0],[0,0],[0,0],[-0.037,44.769]],"o":[[11.884,-14.088],[-3.365,-39.367],[0,0],[0,0],[0,0],[36.982,0],[0.026,-31.246]],"v":[[61.616,-125.787],[79.286,-179.98],[15.537,-232.218],[-86.382,-224.093],[-79.007,-0.375],[30.268,-2.75],[94.037,-62.519]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[25.509,15.287],[1.964,22.98],[38.338,-2.657],[0,0],[0,0],[0,0],[-0.037,44.769]],"o":[[11.884,-14.088],[-3.365,-39.367],[0,0],[0,0],[0,0],[36.982,0],[0.026,-31.246]],"v":[[61.616,-125.787],[79.286,-179.98],[15.537,-232.218],[-86.382,-224.093],[-79.007,-0.375],[30.268,-2.75],[94.037,-62.519]],"c":true}],"e":[{"i":[[23.759,12.287],[0.374,21.444],[38.838,-0.907],[0,0],[0,0],[0,0],[-0.076,41.089]],"o":[[12.259,-10.963],[-0.705,-39.533],[0,0],[0,0],[0,0],[36.982,0],[0.053,-28.967]],"v":[[59.491,-123.537],[79.911,-170.48],[17.412,-229.593],[-83.257,-225.718],[-79.132,-0.25],[28.768,-1.75],[93.412,-61.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[23.759,12.287],[0.374,21.444],[38.838,-0.907],[0,0],[0,0],[0,0],[-0.076,41.089]],"o":[[12.259,-10.963],[-0.705,-39.533],[0,0],[0,0],[0,0],[36.982,0],[0.053,-28.967]],"v":[[59.491,-123.537],[79.911,-170.48],[17.412,-229.593],[-83.257,-225.718],[-79.132,-0.25],[28.768,-1.75],[93.412,-61.644]],"c":true}],"e":[{"i":[[23.009,8.287],[0.374,21.444],[37.94,-0.602],[0,0],[0,0],[0,0],[-0.076,41.089]],"o":[[15.106,-9.064],[-0.705,-39.533],[0,0],[0,0],[0,0],[39.468,0.25],[0.053,-28.967]],"v":[[55.991,-119.287],[79.661,-166.48],[11.162,-226.593],[-79.507,-226.218],[-79.132,-0.25],[22.518,-0.25],[91.162,-62.644]],"c":true}]},{"t":22.2529296875}]},"nm":"B"},{"ind":1,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[-2.196,-15.458],[4.87,-2.756],[0,0],[0,0],[0,0]],"o":[[1.997,14.059],[0,0],[0,0],[0,0],[2.979,-1.252]],"v":[[-276.179,-132.292],[-277.37,-102.994],[-282.552,-101.351],[-288.355,-146.931],[-283.229,-149.748]],"c":true}],"e":[{"i":[[-2.196,-15.458],[4.87,-2.756],[0,0],[0,0],[0,0]],"o":[[1.997,14.059],[0,0],[0,0],[0,0],[2.979,-1.252]],"v":[[-45.179,-170.292],[-46.37,-140.994],[-51.552,-139.351],[-57.355,-184.931],[-52.229,-187.748]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[-2.196,-15.458],[4.87,-2.756],[0,0],[0,0],[0,0]],"o":[[1.997,14.059],[0,0],[0,0],[0,0],[2.979,-1.252]],"v":[[-45.179,-170.292],[-46.37,-140.994],[-51.552,-139.351],[-57.355,-184.931],[-52.229,-187.748]],"c":true}],"e":[{"i":[[-2.196,-15.458],[10.37,-3.756],[0,0],[0,0],[0,0]],"o":[[1.997,14.059],[0,0],[0,0],[0,0],[5.729,-1.752]],"v":[[-29.679,-174.292],[-36.62,-141.244],[-48.302,-138.851],[-53.605,-186.431],[-42.979,-189.748]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[-2.196,-15.458],[10.37,-3.756],[0,0],[0,0],[0,0]],"o":[[1.997,14.059],[0,0],[0,0],[0,0],[5.729,-1.752]],"v":[[-29.679,-174.292],[-36.62,-141.244],[-48.302,-138.851],[-53.605,-186.431],[-42.979,-189.748]],"c":true}],"e":[{"i":[[-2.196,-15.458],[11.62,-1.881],[0,0],[0,0],[0,0]],"o":[[1.997,14.059],[0,0],[0,0],[0,0],[12.104,-2.127]],"v":[[-14.179,-171.542],[-26.62,-140.744],[-44.802,-137.851],[-48.605,-186.931],[-32.479,-190.998]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[-2.196,-15.458],[11.62,-1.881],[0,0],[0,0],[0,0]],"o":[[1.997,14.059],[0,0],[0,0],[0,0],[12.104,-2.127]],"v":[[-14.179,-171.542],[-26.62,-140.744],[-44.802,-137.851],[-48.605,-186.931],[-32.479,-190.998]],"c":true}],"e":[{"i":[[-0.946,-15.083],[11.62,-1.881],[0,0],[0,0],[0,0]],"o":[[0.889,14.172],[0,0],[0,0],[0,0],[11.854,-1.752]],"v":[[-0.554,-168.667],[-16.12,-139.619],[-41.927,-136.351],[-45.105,-186.931],[-21.854,-190.998]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[-0.946,-15.083],[11.62,-1.881],[0,0],[0,0],[0,0]],"o":[[0.889,14.172],[0,0],[0,0],[0,0],[11.854,-1.752]],"v":[[-0.554,-168.667],[-16.12,-139.619],[-41.927,-136.351],[-45.105,-186.931],[-21.854,-190.998]],"c":true}],"e":[{"i":[[0,-14.502],[17.62,-0.131],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[16.104,-1.752]],"v":[[9.196,-166.167],[-14.62,-138.119],[-39.927,-136.351],[-41.855,-185.931],[-15.354,-189.498]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[0,-14.502],[17.62,-0.131],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[16.104,-1.752]],"v":[[9.196,-166.167],[-14.62,-138.119],[-39.927,-136.351],[-41.855,-185.931],[-15.354,-189.498]],"c":true}],"e":[{"i":[[0,-14.502],[18.37,-0.631],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[15.303,-0.876]],"v":[[18.196,-164.417],[-8.37,-136.494],[-37.552,-135.101],[-38.855,-185.806],[-8.479,-188.373]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[0,-14.502],[18.37,-0.631],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[15.303,-0.876]],"v":[[18.196,-164.417],[-8.37,-136.494],[-37.552,-135.101],[-38.855,-185.806],[-8.479,-188.373]],"c":true}],"e":[{"i":[[0,-14.502],[19.12,-1.131],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[24.196,-162.667],[-2.12,-134.869],[-35.177,-133.851],[-35.855,-185.681],[-1.604,-187.248]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[0,-14.502],[19.12,-1.131],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[24.196,-162.667],[-2.12,-134.869],[-35.177,-133.851],[-35.855,-185.681],[-1.604,-187.248]],"c":true}],"e":[{"i":[[0,-14.502],[18.37,0.935],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[30.446,-160.417],[1.88,-133.369],[-33.177,-133.101],[-33.105,-184.681],[4.646,-184.998]],"c":true}]},{"t":22.2529296875}]},"nm":"B"},{"ind":2,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[6.056,-1.63],[0,0],[0,0],[0,0],[-1.281,-9.949]],"o":[[0,0],[0,0],[0,0],[4.931,-1.967],[1.649,12.81]],"v":[[-287.306,-16.62],[-293.677,-14.62],[-299.802,-64.783],[-294.431,-67.033],[-284.719,-47.051]],"c":true}],"e":[{"i":[[6.056,-1.63],[0,0],[0,0],[0,0],[-1.281,-9.949]],"o":[[0,0],[0,0],[0,0],[4.931,-1.967],[1.649,12.81]],"v":[[-33.306,-52.62],[-39.677,-50.62],[-45.802,-100.783],[-40.431,-103.033],[-30.719,-83.051]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[6.056,-1.63],[0,0],[0,0],[0,0],[-1.281,-9.949]],"o":[[0,0],[0,0],[0,0],[4.931,-1.967],[1.649,12.81]],"v":[[-33.306,-52.62],[-39.677,-50.62],[-45.802,-100.783],[-40.431,-103.033],[-30.719,-83.051]],"c":true}],"e":[{"i":[[11.556,-3.13],[0,0],[0,0],[0,0],[-0.953,-10.127]],"o":[[0,0],[0,0],[0,0],[8.681,-1.967],[0.969,10.301]],"v":[[-21.806,-51.87],[-37.427,-49.12],[-43.552,-99.783],[-28.431,-103.533],[-13.719,-83.551]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[11.556,-3.13],[0,0],[0,0],[0,0],[-0.953,-10.127]],"o":[[0,0],[0,0],[0,0],[8.681,-1.967],[0.969,10.301]],"v":[[-21.806,-51.87],[-37.427,-49.12],[-43.552,-99.783],[-28.431,-103.533],[-13.719,-83.551]],"c":true}],"e":[{"i":[[13.181,-3.38],[0,0],[0,0],[0,0],[-0.953,-10.127]],"o":[[0,0],[0,0],[0,0],[14.306,-2.342],[0.969,10.301]],"v":[[-10.556,-49.87],[-36.177,-47.37],[-40.302,-98.783],[-18.181,-102.533],[2.031,-79.801]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[13.181,-3.38],[0,0],[0,0],[0,0],[-0.953,-10.127]],"o":[[0,0],[0,0],[0,0],[14.306,-2.342],[0.969,10.301]],"v":[[-10.556,-49.87],[-36.177,-47.37],[-40.302,-98.783],[-18.181,-102.533],[2.031,-79.801]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[-5.181,-47.745],[-34.927,-46.745],[-38.427,-97.908],[-7.931,-101.158],[14.156,-75.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[-5.181,-47.745],[-34.927,-46.745],[-38.427,-97.908],[-7.931,-101.158],[14.156,-75.176]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[2.819,-46.495],[-33.927,-44.495],[-36.427,-96.908],[0.069,-98.908],[23.656,-72.676]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[2.819,-46.495],[-33.927,-44.495],[-36.427,-96.908],[0.069,-98.908],[23.656,-72.676]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[16.556,-0.967],[0,14.2]],"v":[[6.819,-45.12],[-33.427,-44.245],[-34.802,-95.783],[5.694,-97.283],[31.781,-71.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[16.556,-0.967],[0,14.2]],"v":[[6.819,-45.12],[-33.427,-44.245],[-34.802,-95.783],[5.694,-97.283],[31.781,-71.176]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[10.819,-43.745],[-32.927,-43.995],[-33.177,-94.658],[11.319,-95.658],[36.406,-69.676]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[10.819,-43.745],[-32.927,-43.995],[-33.177,-94.658],[11.319,-95.658],[36.406,-69.676]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[14.819,-41.995],[-32.677,-41.995],[-32.677,-93.658],[16.569,-94.158],[41.656,-68.176]],"c":true}]},{"t":22.2529296875}]},"nm":"B"},{"ty":"mm","mm":1,"nm":"Merge Paths 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0,0.32,0.5,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":15,"op":23,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":25,"ty":4,"nm":"B white layer 2","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[-1.282,17.269],[-0.661,17.73],[1.838,0.593],[0,0],[0,0],[-1.643,0],[-0.912,-1.481]],"o":[[1.259,-16.963],[0.075,-2.02],[-1.701,-0.549],[0,0],[0,0],[1.257,0],[0.338,-3.231]],"v":[[-41.009,-32.537],[-38.089,-79.48],[-39.588,-83.593],[-42.507,-82.468],[-50.507,4.375],[-47.107,4],[-44.213,5.856]],"c":true}],"e":[{"i":[[-0.008,17.316],[0.339,16.48],[3.281,1.942],[0,0],[0,0],[-6.268,-1],[-1.162,-9.106]],"o":[[0.009,-21.463],[-0.098,-4.771],[-4.912,-2.907],[0,0],[0,0],[5.666,0.904],[-0.412,-10.356]],"v":[[-34.509,-42.537],[-34.339,-91.23],[-37.588,-101.593],[-47.007,-102.468],[-59.132,7.5],[-45.982,2.75],[-34.588,14.106]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[-0.008,17.316],[0.339,16.48],[3.281,1.942],[0,0],[0,0],[-6.268,-1],[-1.162,-9.106]],"o":[[0.009,-21.463],[-0.098,-4.771],[-4.912,-2.907],[0,0],[0,0],[5.666,0.904],[-0.412,-10.356]],"v":[[-34.509,-42.537],[-34.339,-91.23],[-37.588,-101.593],[-47.007,-102.468],[-59.132,7.5],[-45.982,2.75],[-34.588,14.106]],"c":true}],"e":[{"i":[[1.009,17.287],[2.01,28.256],[7.354,3.812],[0,0],[0,0],[-11.66,-5.191],[-3.162,-10.856]],"o":[[-1.064,-18.224],[-0.411,-5.77],[-10.912,-5.657],[0,0],[0,0],[11.232,5],[-1.162,-18.356]],"v":[[-16.009,-48.037],[-20.089,-101.23],[-28.588,-120.343],[-52.757,-120.968],[-55.882,3.75],[-26.232,2.5],[-10.838,21.356]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[1.009,17.287],[2.01,28.256],[7.354,3.812],[0,0],[0,0],[-11.66,-5.191],[-3.162,-10.856]],"o":[[-1.064,-18.224],[-0.411,-5.77],[-10.912,-5.657],[0,0],[0,0],[11.232,5],[-1.162,-18.356]],"v":[[-16.009,-48.037],[-20.089,-101.23],[-28.588,-120.343],[-52.757,-120.968],[-55.882,3.75],[-26.232,2.5],[-10.838,21.356]],"c":true}],"e":[{"i":[[3.581,24.09],[2.339,28.23],[18.566,5.173],[0,0],[0,0],[-42.815,-12.244],[-3.162,-9.856]],"o":[[-4.491,-30.213],[-0.972,-11.734],[-34.662,-9.657],[0,0],[0,0],[19.232,5.5],[-2.412,-16.356]],"v":[[28.491,-50.787],[21.161,-112.48],[-7.588,-142.843],[-60.007,-138.968],[-58.382,3.5],[13.518,-2.5],[41.412,22.606]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[3.581,24.09],[2.339,28.23],[18.566,5.173],[0,0],[0,0],[-42.815,-12.244],[-3.162,-9.856]],"o":[[-4.491,-30.213],[-0.972,-11.734],[-34.662,-9.657],[0,0],[0,0],[19.232,5.5],[-2.412,-16.356]],"v":[[28.491,-50.787],[21.161,-112.48],[-7.588,-142.843],[-60.007,-138.968],[-58.382,3.5],[13.518,-2.5],[41.412,22.606]],"c":true}],"e":[{"i":[[6.259,23.537],[9.028,29.832],[35.338,-11.407],[0,0],[0,0],[-43.647,8.83],[7.838,30.644]],"o":[[-6.901,-25.951],[-3.411,-11.27],[-35.699,11.524],[0,0],[0,0],[66.732,-13.5],[-4.529,-17.709]],"v":[[98.991,-111.037],[77.161,-192.23],[3.912,-187.593],[-68.757,-153.968],[-60.882,2],[23.768,-27],[112.412,-55.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[6.259,23.537],[9.028,29.832],[35.338,-11.407],[0,0],[0,0],[-43.647,8.83],[7.838,30.644]],"o":[[-6.901,-25.951],[-3.411,-11.27],[-35.699,11.524],[0,0],[0,0],[66.732,-13.5],[-4.529,-17.709]],"v":[[98.991,-111.037],[77.161,-192.23],[3.912,-187.593],[-68.757,-153.968],[-60.882,2],[23.768,-27],[112.412,-55.894]],"c":true}],"e":[{"i":[[9.241,25.213],[14.339,35.48],[31.838,-35.407],[0,0],[0,0],[-37.642,23.794],[7.838,30.644]],"o":[[-9.241,-25.213],[-15.283,-37.816],[-16.668,18.536],[0,0],[0,0],[45.482,-28.75],[-4.529,-17.709]],"v":[[33.991,-189.537],[9.161,-252.48],[-36.838,-201.593],[-76.007,-167.218],[-63.882,2],[24.768,-48],[62.412,-113.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[9.241,25.213],[14.339,35.48],[31.838,-35.407],[0,0],[0,0],[-37.642,23.794],[7.838,30.644]],"o":[[-9.241,-25.213],[-15.283,-37.816],[-16.668,18.536],[0,0],[0,0],[45.482,-28.75],[-4.529,-17.709]],"v":[[33.991,-189.537],[9.161,-252.48],[-36.838,-201.593],[-76.007,-167.218],[-63.882,2],[24.768,-48],[62.412,-113.394]],"c":true}],"e":[{"i":[[9.241,25.213],[15.339,35.73],[24.588,-31.657],[0,0],[0,0],[-33.385,29.471],[8.088,31.144]],"o":[[-9.241,-25.213],[-19.116,-44.526],[-15.291,19.687],[0,0],[0,0],[38.232,-33.75],[-4.594,-17.692]],"v":[[1.491,-181.287],[-33.839,-268.23],[-51.588,-209.843],[-83.257,-178.968],[-66.382,1.5],[2.768,-41.25],[25.662,-113.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[9.241,25.213],[15.339,35.73],[24.588,-31.657],[0,0],[0,0],[-33.385,29.471],[8.088,31.144]],"o":[[-9.241,-25.213],[-19.116,-44.526],[-15.291,19.687],[0,0],[0,0],[38.232,-33.75],[-4.594,-17.692]],"v":[[1.491,-181.287],[-33.839,-268.23],[-51.588,-209.843],[-83.257,-178.968],[-66.382,1.5],[2.768,-41.25],[25.662,-113.144]],"c":true}],"e":[{"i":[[9.241,25.213],[14.589,37.98],[24.588,-31.657],[0,0],[0,0],[-36.476,25.546],[8.088,31.144]],"o":[[-9.241,-25.213],[-17.376,-45.234],[-15.291,19.687],[0,0],[0,0],[34.982,-24.5],[-4.594,-17.692]],"v":[[-0.759,-180.537],[-36.839,-272.73],[-56.338,-216.843],[-89.257,-186.468],[-69.382,1],[6.518,-38],[24.662,-104.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[9.241,25.213],[14.589,37.98],[24.588,-31.657],[0,0],[0,0],[-36.476,25.546],[8.088,31.144]],"o":[[-9.241,-25.213],[-17.376,-45.234],[-15.291,19.687],[0,0],[0,0],[34.982,-24.5],[-4.594,-17.692]],"v":[[-0.759,-180.537],[-36.839,-272.73],[-56.338,-216.843],[-89.257,-186.468],[-69.382,1],[6.518,-38],[24.662,-104.894]],"c":true}],"e":[{"i":[[8.009,25.037],[13.247,38.034],[35.338,-37.657],[0,0],[0,0],[-39.772,20.031],[8.088,31.144]],"o":[[-11.991,-32.463],[-17.16,-49.27],[-21.388,22.792],[0,0],[0,0],[36.732,-18.5],[-4.594,-17.692]],"v":[[11.241,-169.037],[-25.59,-271.98],[-60.088,-220.343],[-93.257,-193.718],[-71.382,1],[15.768,-34.75],[32.662,-102.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[{"i":[[8.009,25.037],[13.247,38.034],[35.338,-37.657],[0,0],[0,0],[-39.772,20.031],[8.088,31.144]],"o":[[-11.991,-32.463],[-17.16,-49.27],[-21.388,22.792],[0,0],[0,0],[36.732,-18.5],[-4.594,-17.692]],"v":[[11.241,-169.037],[-25.59,-271.98],[-60.088,-220.343],[-93.257,-193.718],[-71.382,1],[15.768,-34.75],[32.662,-102.144]],"c":true}],"e":[{"i":[[8.259,24.037],[10.641,35.776],[35.838,-35.157],[0,0],[0,0],[-42.018,14.75],[10.338,43.894]],"o":[[-9.241,-31.213],[-13.91,-46.77],[-22.312,21.888],[0,0],[0,0],[40.972,-14.383],[-7.971,-33.842]],"v":[[23.991,-176.037],[-5.09,-269.48],[-49.338,-232.593],[-95.757,-200.718],[-72.382,0.25],[27.268,-29],[47.662,-96.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[8.259,24.037],[10.641,35.776],[35.838,-35.157],[0,0],[0,0],[-42.018,14.75],[10.338,43.894]],"o":[[-9.241,-31.213],[-13.91,-46.77],[-22.312,21.888],[0,0],[0,0],[40.972,-14.383],[-7.971,-33.842]],"v":[[23.991,-176.037],[-5.09,-269.48],[-49.338,-232.593],[-95.757,-200.718],[-72.382,0.25],[27.268,-29],[47.662,-96.394]],"c":true}],"e":[{"i":[[7.009,23.287],[7.59,33.73],[50.58,-35.169],[0,0],[0,0],[0,0],[11.714,49.114]],"o":[[-7.241,-28.463],[-10.225,-45.445],[-25.662,17.843],[0,0],[0,0],[43.732,-7.5],[-10.162,-42.606]],"v":[[42.741,-173.287],[20.411,-260.73],[-42.588,-234.593],[-97.257,-206.968],[-73.882,0.25],[35.018,-20.5],[65.662,-88.144]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[7.009,23.287],[7.59,33.73],[50.58,-35.169],[0,0],[0,0],[0,0],[11.714,49.114]],"o":[[-7.241,-28.463],[-10.225,-45.445],[-25.662,17.843],[0,0],[0,0],[43.732,-7.5],[-10.162,-42.606]],"v":[[42.741,-173.287],[20.411,-260.73],[-42.588,-234.593],[-97.257,-206.968],[-73.882,0.25],[35.018,-20.5],[65.662,-88.144]],"c":true}],"e":[{"i":[[3.259,21.287],[6.044,31.942],[57.588,-23.657],[0,0],[0,0],[0,0],[10.088,66.644]],"o":[[-1.741,-23.963],[-8.661,-45.77],[-33.448,13.74],[0,0],[0,0],[43.732,-7.5],[-8.74,-57.737]],"v":[[54.991,-175.537],[44.161,-246.98],[-37.838,-233.593],[-96.757,-212.718],[-75.382,0],[38.518,-14.75],[79.662,-80.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[3.259,21.287],[6.044,31.942],[57.588,-23.657],[0,0],[0,0],[0,0],[10.088,66.644]],"o":[[-1.741,-23.963],[-8.661,-45.77],[-33.448,13.74],[0,0],[0,0],[43.732,-7.5],[-8.74,-57.737]],"v":[[54.991,-175.537],[44.161,-246.98],[-37.838,-233.593],[-96.757,-212.718],[-75.382,0],[38.518,-14.75],[79.662,-80.894]],"c":true}],"e":[{"i":[[3.259,21.287],[4.985,35.86],[31.838,-13.407],[0,0],[0,0],[0,0],[7.588,68.394]],"o":[[-1.491,-26.213],[-5.911,-42.52],[-31.691,13.345],[0,0],[0,0],[38.732,-3.75],[-6.443,-58.072]],"v":[[65.241,-152.287],[61.661,-221.73],[10.912,-247.343],[-94.007,-216.968],[-77.382,-0.75],[40.268,-9.5],[88.162,-79.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[3.259,21.287],[4.985,35.86],[31.838,-13.407],[0,0],[0,0],[0,0],[7.588,68.394]],"o":[[-1.491,-26.213],[-5.911,-42.52],[-31.691,13.345],[0,0],[0,0],[38.732,-3.75],[-6.443,-58.072]],"v":[[65.241,-152.287],[61.661,-221.73],[10.912,-247.343],[-94.007,-216.968],[-77.382,-0.75],[40.268,-9.5],[88.162,-79.394]],"c":true}],"e":[{"i":[[2.759,8.787],[5.839,35.73],[33.162,-9.093],[0,0],[0,0],[0,0],[2.478,49.663]],"o":[[-2.741,-7.463],[-6.377,-39.022],[-33.162,9.093],[0,0],[0,0],[38.732,-3.75],[-2.912,-58.356]],"v":[[66.241,-138.287],[71.911,-205.73],[16.162,-241.093],[-91.507,-220.468],[-77.382,-0.75],[41.768,-6.5],[92.912,-66.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[2.759,8.787],[5.839,35.73],[33.162,-9.093],[0,0],[0,0],[0,0],[2.478,49.663]],"o":[[-2.741,-7.463],[-6.377,-39.022],[-33.162,9.093],[0,0],[0,0],[38.732,-3.75],[-2.912,-58.356]],"v":[[66.241,-138.287],[71.911,-205.73],[16.162,-241.093],[-91.507,-220.468],[-77.382,-0.75],[41.768,-6.5],[92.912,-66.894]],"c":true}],"e":[{"i":[[27.259,18.287],[2.089,21.98],[37.838,-4.407],[0,0],[0,0],[0,0],[0.838,46.394]],"o":[[11.509,-17.213],[-3.742,-39.362],[0,0],[0,0],[0,0],[36.982,0],[-0.523,-28.962]],"v":[[63.241,-131.037],[77.161,-188.98],[16.162,-235.843],[-89.507,-222.468],[-78.882,-0.5],[31.768,-3.75],[95.162,-60.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[27.259,18.287],[2.089,21.98],[37.838,-4.407],[0,0],[0,0],[0,0],[0.838,46.394]],"o":[[11.509,-17.213],[-3.742,-39.362],[0,0],[0,0],[0,0],[36.982,0],[-0.523,-28.962]],"v":[[63.241,-131.037],[77.161,-188.98],[16.162,-235.843],[-89.507,-222.468],[-78.882,-0.5],[31.768,-3.75],[95.162,-60.894]],"c":true}],"e":[{"i":[[25.509,15.287],[1.964,22.98],[38.338,-2.657],[0,0],[0,0],[0,0],[-0.037,44.769]],"o":[[11.884,-14.088],[-3.365,-39.367],[0,0],[0,0],[0,0],[36.982,0],[0.026,-31.246]],"v":[[61.616,-125.787],[79.286,-179.98],[15.537,-232.218],[-86.382,-224.093],[-79.007,-0.375],[30.268,-2.75],[94.037,-62.519]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[25.509,15.287],[1.964,22.98],[38.338,-2.657],[0,0],[0,0],[0,0],[-0.037,44.769]],"o":[[11.884,-14.088],[-3.365,-39.367],[0,0],[0,0],[0,0],[36.982,0],[0.026,-31.246]],"v":[[61.616,-125.787],[79.286,-179.98],[15.537,-232.218],[-86.382,-224.093],[-79.007,-0.375],[30.268,-2.75],[94.037,-62.519]],"c":true}],"e":[{"i":[[23.759,12.287],[0.374,21.444],[38.838,-0.907],[0,0],[0,0],[0,0],[-0.076,41.089]],"o":[[12.259,-10.963],[-0.705,-39.533],[0,0],[0,0],[0,0],[36.982,0],[0.053,-28.967]],"v":[[59.491,-123.537],[79.911,-170.48],[17.412,-229.593],[-83.257,-225.718],[-79.132,-0.25],[28.768,-1.75],[93.412,-61.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[23.759,12.287],[0.374,21.444],[38.838,-0.907],[0,0],[0,0],[0,0],[-0.076,41.089]],"o":[[12.259,-10.963],[-0.705,-39.533],[0,0],[0,0],[0,0],[36.982,0],[0.053,-28.967]],"v":[[59.491,-123.537],[79.911,-170.48],[17.412,-229.593],[-83.257,-225.718],[-79.132,-0.25],[28.768,-1.75],[93.412,-61.644]],"c":true}],"e":[{"i":[[23.009,8.287],[0.374,21.444],[37.94,-0.602],[0,0],[0,0],[0,0],[-0.076,41.089]],"o":[[15.106,-9.064],[-0.705,-39.533],[0,0],[0,0],[0,0],[39.468,0.25],[0.053,-28.967]],"v":[[55.991,-119.287],[79.661,-166.48],[11.162,-226.593],[-79.507,-226.218],[-79.132,-0.25],[22.518,-0.25],[91.162,-62.644]],"c":true}]},{"t":22.2529296875}]},"nm":"B"},{"ty":"fl","fillEnabled":true,"c":{"k":[0,0.32,0.5,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":5,"op":15,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":26,"ty":4,"nm":"B light blue layer 3 shadow 2","parent":34,"ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":16,"s":[5],"e":[40]},{"t":22}]},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[0,0],[0,0],[-3.12,0.516],[4.596,42.859],[0,0]],"o":[[0,0],[0,0],[3.12,-0.516],[-5.931,-55.302],[0,0]],"v":[[-58.13,-221.16],[-111.991,-226.753],[-103.289,41.823],[-46.069,-81.698],[-54.696,-253.118]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3.12,0.516],[9.482,45.79],[0,0]],"o":[[0,0],[0,0],[3.12,-0.516],[-11.278,-54.464],[0,0]],"v":[[-9.13,-263.16],[-128.991,-241.753],[-103.289,41.823],[-0.569,37.802],[-54.696,-253.118]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[0,0],[0,0],[-3.12,0.516],[9.482,45.79],[0,0]],"o":[[0,0],[0,0],[3.12,-0.516],[-11.278,-54.464],[0,0]],"v":[[-9.13,-263.16],[-128.991,-241.753],[-103.289,41.823],[-0.569,37.802],[-54.696,-253.118]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3.12,0.516],[-3.803,42.936],[0,0]],"o":[[0,0],[0,0],[3.12,-0.516],[3.803,-42.936],[0,0]],"v":[[-9.13,-263.16],[-128.991,-241.753],[-103.289,41.823],[42.431,32.302],[11.804,-249.118]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[0,0],[0,0],[-3.12,0.516],[-3.803,42.936],[0,0]],"o":[[0,0],[0,0],[3.12,-0.516],[3.803,-42.936],[0,0]],"v":[[-9.13,-263.16],[-128.991,-241.753],[-103.289,41.823],[42.431,32.302],[11.804,-249.118]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3.12,0.516],[-3.803,42.936],[0,0]],"o":[[0,0],[0,0],[3.12,-0.516],[3.803,-42.936],[0,0]],"v":[[-9.13,-263.16],[-128.991,-241.753],[-103.289,41.823],[70.931,-10.698],[8.304,-239.118]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[0,0],[0,0],[-3.12,0.516],[-3.803,42.936],[0,0]],"o":[[0,0],[0,0],[3.12,-0.516],[3.803,-42.936],[0,0]],"v":[[-9.13,-263.16],[-128.991,-241.753],[-103.289,41.823],[70.931,-10.698],[8.304,-239.118]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3,1],[3,43],[0,0]],"o":[[0,0],[0,0],[3,-1],[-3,-43],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[59,30],[-33,-263]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[0,0],[0,0],[-3,1],[3,43],[0,0]],"o":[[0,0],[0,0],[3,-1],[-3,-43],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[59,30],[-33,-263]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3,1],[3,43],[0,0]],"o":[[0,0],[0,0],[3,-1],[-3,-43],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[41,8],[-35,-264]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[0,0],[0,0],[-3,1],[3,43],[0,0]],"o":[[0,0],[0,0],[3,-1],[-3,-43],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[41,8],[-35,-264]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3,1],[-1,39],[0,0]],"o":[[0,0],[0,0],[3,-1],[1.444,-56.302],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[70,-17],[3,-268]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[0,0],[0,0],[-3,1],[-1,39],[0,0]],"o":[[0,0],[0,0],[3,-1],[1.444,-56.302],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[70,-17],[3,-268]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3,1],[9.112,40.348],[0,0]],"o":[[0,0],[0,0],[3,-1],[-12.406,-54.937],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[96,-9],[28,-280]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[0,0],[0,0],[-3,1],[9.112,40.348],[0,0]],"o":[[0,0],[0,0],[3,-1],[-12.406,-54.937],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[96,-9],[28,-280]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3,1],[9.112,40.348],[0,0]],"o":[[0,0],[0,0],[3,-1],[-12.406,-54.937],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[96,-19],[50,-287]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[0,0],[0,0],[-3,1],[9.112,40.348],[0,0]],"o":[[0,0],[0,0],[3,-1],[-12.406,-54.937],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[96,-19],[50,-287]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3,1],[9.112,40.348],[0,0]],"o":[[0,0],[0,0],[3,-1],[-12.406,-54.937],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[116,-7],[70,-285]],"c":true}]},{"t":18}]},"o":{"k":100},"x":{"k":0},"nm":"Mask 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[-0.201,21.411],[-0.286,15.23],[2.474,0.203],[0,0],[0,0],[-1.893,-0.375],[-0.662,-1.856]],"o":[[0.134,-14.338],[0.036,-1.896],[-1.912,-0.157],[0,0],[0,0],[0.644,0.128],[1.338,-20.856]],"v":[[-46.509,-50.162],[-44.214,-98.23],[-45.963,-101.593],[-49.132,-100.593],[-55.007,4.75],[-50.857,4.25],[-48.588,6.856]],"c":true}],"e":[{"i":[[0.279,24.912],[0.464,22.605],[2.088,1.343],[0,0],[0,0],[-4.143,-2],[-0.787,-3.981]],"o":[[-0.241,-21.463],[-0.098,-4.771],[-4.242,-2.728],[0,0],[0,0],[3.51,1.694],[-0.037,-19.356]],"v":[[-40.884,-52.662],[-41.839,-108.48],[-44.463,-118.593],[-53.507,-120.718],[-54.757,3.5],[-45.357,4.875],[-39.963,12.856]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[0.279,24.912],[0.464,22.605],[2.088,1.343],[0,0],[0,0],[-4.143,-2],[-0.787,-3.981]],"o":[[-0.241,-21.463],[-0.098,-4.771],[-4.242,-2.728],[0,0],[0,0],[3.51,1.694],[-0.037,-19.356]],"v":[[-40.884,-52.662],[-41.839,-108.48],[-44.463,-118.593],[-53.507,-120.718],[-54.757,3.5],[-45.357,4.875],[-39.963,12.856]],"c":true}],"e":[{"i":[[2.509,24.787],[1.952,19.983],[11.463,8.593],[0,0],[0,0],[-8.268,-3.75],[-3.537,-7.856]],"o":[[-1.87,-18.476],[-0.161,-1.645],[-7.865,-5.896],[0,0],[0,0],[10.005,4.538],[-1.787,-20.356]],"v":[[-22.759,-58.037],[-27.839,-117.855],[-38.713,-134.843],[-60.257,-138.093],[-58.257,2.5],[-32.482,4.25],[-15.963,19.356]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[2.509,24.787],[1.952,19.983],[11.463,8.593],[0,0],[0,0],[-8.268,-3.75],[-3.537,-7.856]],"o":[[-1.87,-18.476],[-0.161,-1.645],[-7.865,-5.896],[0,0],[0,0],[10.005,4.538],[-1.787,-20.356]],"v":[[-22.759,-58.037],[-27.839,-117.855],[-38.713,-134.843],[-60.257,-138.093],[-58.257,2.5],[-32.482,4.25],[-15.963,19.356]],"c":true}],"e":[{"i":[[2.509,23.537],[1.952,19.983],[37.213,10.843],[0,0],[0,0],[-37.012,-3.318],[3.87,36.773]],"o":[[-1.969,-18.466],[-0.161,-1.645],[-39.595,-11.537],[0,0],[0,0],[52.982,4.75],[-2.037,-19.356]],"v":[[46.741,-67.787],[41.411,-115.355],[-2.713,-150.593],[-68.757,-153.593],[-61.507,2.25],[4.268,0.25],[56.037,7.356]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[2.509,23.537],[1.952,19.983],[37.213,10.843],[0,0],[0,0],[-37.012,-3.318],[3.87,36.773]],"o":[[-1.969,-18.466],[-0.161,-1.645],[-39.595,-11.537],[0,0],[0,0],[52.982,4.75],[-2.037,-19.356]],"v":[[46.741,-67.787],[41.411,-115.355],[-2.713,-150.593],[-68.757,-153.593],[-61.507,2.25],[4.268,0.25],[56.037,7.356]],"c":true}],"e":[{"i":[[5.869,20.677],[5.339,19.355],[49.963,-14.657],[0,0],[0,0],[-48.018,8.5],[14.463,52.894]],"o":[[-5.241,-18.463],[-6.854,-24.846],[-39.574,11.609],[0,0],[0,0],[56.175,-9.944],[-11.083,-40.532]],"v":[[79.741,-141.537],[64.661,-196.855],[-12.963,-182.843],[-76.257,-168.093],[-64.507,2],[54.518,-20],[98.287,-79.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[5.869,20.677],[5.339,19.355],[49.963,-14.657],[0,0],[0,0],[-48.018,8.5],[14.463,52.894]],"o":[[-5.241,-18.463],[-6.854,-24.846],[-39.574,11.609],[0,0],[0,0],[56.175,-9.944],[-11.083,-40.532]],"v":[[79.741,-141.537],[64.661,-196.855],[-12.963,-182.843],[-76.257,-168.093],[-64.507,2],[54.518,-20],[98.287,-79.394]],"c":true}],"e":[{"i":[[7.415,20.175],[10.589,29.855],[50.213,-22.407],[0,0],[0,0],[-48.907,15.377],[14.389,46.029]],"o":[[-8.991,-24.463],[-8.616,-24.291],[-37.662,16.806],[0,0],[0,0],[43.732,-13.75],[-12.537,-40.106]],"v":[[75.491,-152.537],[45.911,-232.355],[-25.463,-197.593],[-83.007,-177.843],[-66.757,2],[45.518,-26.75],[98.787,-83.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[7.415,20.175],[10.589,29.855],[50.213,-22.407],[0,0],[0,0],[-48.907,15.377],[14.389,46.029]],"o":[[-8.991,-24.463],[-8.616,-24.291],[-37.662,16.806],[0,0],[0,0],[43.732,-13.75],[-12.537,-40.106]],"v":[[75.491,-152.537],[45.911,-232.355],[-25.463,-197.593],[-83.007,-177.843],[-66.757,2],[45.518,-26.75],[98.787,-83.894]],"c":true}],"e":[{"i":[[6.732,20.413],[9.339,28.855],[50.213,-22.407],[0,0],[0,0],[-49.459,13.494],[14.043,47.445]],"o":[[-7.491,-22.713],[-7.937,-24.521],[-37.662,16.806],[0,0],[0,0],[43.982,-12],[-12.537,-42.356]],"v":[[73.491,-153.537],[43.411,-239.855],[-29.463,-206.343],[-89.007,-186.343],[-68.507,1.5],[45.518,-26.75],[96.287,-84.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[6.732,20.413],[9.339,28.855],[50.213,-22.407],[0,0],[0,0],[-49.459,13.494],[14.043,47.445]],"o":[[-7.491,-22.713],[-7.937,-24.521],[-37.662,16.806],[0,0],[0,0],[43.982,-12],[-12.537,-42.356]],"v":[[73.491,-153.537],[43.411,-239.855],[-29.463,-206.343],[-89.007,-186.343],[-68.507,1.5],[45.518,-26.75],[96.287,-84.644]],"c":true}],"e":[{"i":[[5.741,20.713],[9.339,28.855],[60.463,-27.657],[0,0],[0,0],[-49.459,13.494],[12.963,47.644]],"o":[[-5.741,-20.713],[-7.937,-24.521],[-37.505,17.155],[0,0],[0,0],[43.982,-12],[-11.586,-42.584]],"v":[[73.741,-153.787],[45.411,-241.855],[-29.713,-213.843],[-93.257,-193.343],[-71.257,0.5],[49.768,-26.5],[95.287,-81.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[{"i":[[5.741,20.713],[9.339,28.855],[60.463,-27.657],[0,0],[0,0],[-49.459,13.494],[12.963,47.644]],"o":[[-5.741,-20.713],[-7.937,-24.521],[-37.505,17.155],[0,0],[0,0],[43.982,-12],[-11.586,-42.584]],"v":[[73.741,-153.787],[45.411,-241.855],[-29.713,-213.843],[-93.257,-193.343],[-71.257,0.5],[49.768,-26.5],[95.287,-81.894]],"c":true}],"e":[{"i":[[5.741,20.713],[7.839,29.855],[63.713,-28.907],[0,0],[0,0],[-50.018,11.25],[12.963,53.394]],"o":[[-5.741,-20.713],[-6.546,-24.929],[-37.557,17.04],[0,0],[0,0],[46.058,-10.359],[-10.749,-44.276]],"v":[[75.741,-154.037],[52.411,-240.355],[-22.963,-222.593],[-96.007,-200.343],[-72.507,1.25],[48.768,-23.5],[95.037,-81.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[5.741,20.713],[7.839,29.855],[63.713,-28.907],[0,0],[0,0],[-50.018,11.25],[12.963,53.394]],"o":[[-5.741,-20.713],[-6.546,-24.929],[-37.557,17.04],[0,0],[0,0],[46.058,-10.359],[-10.749,-44.276]],"v":[[75.741,-154.037],[52.411,-240.355],[-22.963,-222.593],[-96.007,-200.343],[-72.507,1.25],[48.768,-23.5],[95.037,-81.894]],"c":true}],"e":[{"i":[[4.259,17.037],[7.839,29.855],[64.463,-23.407],[0,0],[0,0],[-49.518,7.25],[10.523,50.543]],"o":[[-5.991,-25.463],[-6.546,-24.929],[-38.765,14.076],[0,0],[0,0],[48.964,-7.169],[-9.287,-44.606]],"v":[[76.491,-154.787],[58.161,-239.105],[-23.213,-225.593],[-97.507,-206.343],[-74.007,1.25],[52.268,-20],[94.787,-82.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[4.259,17.037],[7.839,29.855],[64.463,-23.407],[0,0],[0,0],[-49.518,7.25],[10.523,50.543]],"o":[[-5.991,-25.463],[-6.546,-24.929],[-38.765,14.076],[0,0],[0,0],[48.964,-7.169],[-9.287,-44.606]],"v":[[76.491,-154.787],[58.161,-239.105],[-23.213,-225.593],[-97.507,-206.343],[-74.007,1.25],[52.268,-20],[94.787,-82.394]],"c":true}],"e":[{"i":[[4.009,18.287],[9.089,30.105],[63.963,-18.157],[0,0],[0,0],[-52.207,5.395],[4.963,38.144]],"o":[[-1.741,-23.713],[-7.236,-23.967],[-39.674,11.262],[0,0],[0,0],[67.732,-7],[-5.852,-44.98]],"v":[[74.491,-158.287],[64.411,-233.605],[-18.713,-229.093],[-96.757,-211.843],[-75.507,0.25],[45.268,-14.75],[94.287,-79.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[4.009,18.287],[9.089,30.105],[63.963,-18.157],[0,0],[0,0],[-52.207,5.395],[4.963,38.144]],"o":[[-1.741,-23.713],[-7.236,-23.967],[-39.674,11.262],[0,0],[0,0],[67.732,-7],[-5.852,-44.98]],"v":[[74.491,-158.287],[64.411,-233.605],[-18.713,-229.093],[-96.757,-211.843],[-75.507,0.25],[45.268,-14.75],[94.287,-79.394]],"c":true}],"e":[{"i":[[5.509,15.037],[12.589,49.605],[49.213,-8.907],[0,0],[0,0],[-52.29,4.52],[5.084,46.282]],"o":[[-2.241,-23.713],[-7.654,-30.16],[-40.583,7.345],[0,0],[0,0],[60.732,-5.25],[-5.037,-45.856]],"v":[[73.241,-141.287],[69.161,-222.855],[-16.963,-229.093],[-94.757,-216.843],[-76.507,-0.5],[42.768,-11.75],[94.037,-80.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[5.509,15.037],[12.589,49.605],[49.213,-8.907],[0,0],[0,0],[-52.29,4.52],[5.084,46.282]],"o":[[-2.241,-23.713],[-7.654,-30.16],[-40.583,7.345],[0,0],[0,0],[60.732,-5.25],[-5.037,-45.856]],"v":[[73.241,-141.287],[69.161,-222.855],[-16.963,-229.093],[-94.757,-216.843],[-76.507,-0.5],[42.768,-11.75],[94.037,-80.894]],"c":true}],"e":[{"i":[[15.759,20.037],[2.089,16.855],[46.963,-6.157],[0,0],[0,0],[-32.059,1.901],[3.213,53.144]],"o":[[4.509,-21.713],[-5.161,-61.645],[-32.938,4.318],[0,0],[0,0],[63.232,-3.75],[-3.787,-41.856]],"v":[[68.491,-134.287],[76.911,-187.605],[-8.463,-229.843],[-91.507,-219.843],[-77.257,-0.75],[22.518,-7.25],[94.037,-72.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[15.759,20.037],[2.089,16.855],[46.963,-6.157],[0,0],[0,0],[-32.059,1.901],[3.213,53.144]],"o":[[4.509,-21.713],[-5.161,-61.645],[-32.938,4.318],[0,0],[0,0],[63.232,-3.75],[-3.787,-41.856]],"v":[[68.491,-134.287],[76.911,-187.605],[-8.463,-229.843],[-91.507,-219.843],[-77.257,-0.75],[22.518,-7.25],[94.037,-72.894]],"c":true}],"e":[{"i":[[20.259,14.037],[1.766,21.378],[30.713,-3.907],[0,0],[0,0],[-39.232,1.5],[3.134,56.753]],"o":[[11.509,-19.963],[-4.411,-53.395],[-38.914,4.95],[0,0],[0,0],[39.232,-1.5],[-1.787,-32.356]],"v":[[63.741,-128.537],[77.411,-184.105],[3.537,-230.593],[-88.507,-223.093],[-78.007,-1],[25.018,-5.5],[93.537,-70.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[20.259,14.037],[1.766,21.378],[30.713,-3.907],[0,0],[0,0],[-39.232,1.5],[3.134,56.753]],"o":[[11.509,-19.963],[-4.411,-53.395],[-38.914,4.95],[0,0],[0,0],[39.232,-1.5],[-1.787,-32.356]],"v":[[63.741,-128.537],[77.411,-184.105],[3.537,-230.593],[-88.507,-223.093],[-78.007,-1],[25.018,-5.5],[93.537,-70.894]],"c":true}],"e":[{"i":[[24.259,15.287],[1.029,21.401],[37.213,-2.157],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[12.683,-14.513],[-3.911,-45.645],[-21.287,1.093],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[61.241,-124.787],[78.661,-178.355],[4.787,-229.343],[-86.007,-223.593],[-78.007,-1],[18.768,-3.5],[92.287,-66.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[24.259,15.287],[1.029,21.401],[37.213,-2.157],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[12.683,-14.513],[-3.911,-45.645],[-21.287,1.093],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[61.241,-124.787],[78.661,-178.355],[4.787,-229.343],[-86.007,-223.593],[-78.007,-1],[18.768,-3.5],[92.287,-66.644]],"c":true}],"e":[{"i":[[22.009,10.287],[0,21.451],[39.713,-1.157],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[14.509,-10.713],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[58.491,-122.287],[79.161,-171.855],[11.037,-228.593],[-83.507,-224.093],[-78.007,-1],[21.268,-2.5],[92.037,-63.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[22.009,10.287],[0,21.451],[39.713,-1.157],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[14.509,-10.713],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[58.491,-122.287],[79.161,-171.855],[11.037,-228.593],[-83.507,-224.093],[-78.007,-1],[21.268,-2.5],[92.037,-63.394]],"c":true}],"e":[{"i":[[21.149,8.762],[0.374,21.444],[35.338,-1.657],[0,0],[0,0],[0,0],[-0.227,41.088]],"o":[[15.106,-9.064],[-0.705,-39.533],[0,0],[0,0],[0,0],[43.478,0.75],[0.16,-28.893]],"v":[[56.741,-121.162],[79.411,-167.48],[13.912,-227.343],[-81.507,-224.968],[-78.382,-0.75],[19.518,-1.25],[91.287,-62.019]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":21.376,"s":[{"i":[[21.149,8.762],[0.374,21.444],[35.338,-1.657],[0,0],[0,0],[0,0],[-0.227,41.088]],"o":[[15.106,-9.064],[-0.705,-39.533],[0,0],[0,0],[0,0],[43.478,0.75],[0.16,-28.893]],"v":[[56.741,-121.162],[79.411,-167.48],[13.912,-227.343],[-81.507,-224.968],[-78.382,-0.75],[19.518,-1.25],[91.287,-62.019]],"c":true}],"e":[{"i":[[21.149,8.762],[0.749,21.438],[32.213,-0.907],[0,0],[0,0],[0,0],[-0.152,41.088]],"o":[[15.106,-9.064],[-1.411,-40.395],[0,0],[0,0],[0,0],[41.473,0.5],[0.106,-28.93]],"v":[[56.241,-119.787],[79.411,-168.105],[13.287,-226.593],[-79.507,-225.843],[-78.757,-0.5],[21.518,-0.5],[90.787,-62.894]],"c":true}]},{"t":22.2529296875}]},"nm":"B"},{"ind":1,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[-2.154,-14.341],[5.939,-1.064],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[3.862,-1.474]],"v":[[-268.103,-143.239],[-271.189,-115.936],[-276.731,-115.148],[-282.154,-159.988],[-277.112,-161.276]],"c":true}],"e":[{"i":[[-2.154,-14.341],[5.939,-1.064],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[3.862,-1.474]],"v":[[-36.103,-162.739],[-39.189,-135.436],[-44.731,-134.648],[-50.154,-179.488],[-45.112,-180.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[-2.154,-14.341],[5.939,-1.064],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[3.862,-1.474]],"v":[[-36.103,-162.739],[-39.189,-135.436],[-44.731,-134.648],[-50.154,-179.488],[-45.112,-180.776]],"c":true}],"e":[{"i":[[-2.154,-14.341],[7.939,-1.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[7.862,-1.724]],"v":[[-22.103,-167.239],[-29.189,-138.186],[-43.481,-135.648],[-49.154,-181.988],[-37.862,-184.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[-2.154,-14.341],[7.939,-1.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[7.862,-1.724]],"v":[[-22.103,-167.239],[-29.189,-138.186],[-43.481,-135.648],[-49.154,-181.988],[-37.862,-184.776]],"c":true}],"e":[{"i":[[-2.154,-14.341],[7.939,-1.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[10.862,0.026]],"v":[[-9.353,-166.739],[-20.189,-138.686],[-42.231,-135.648],[-46.654,-183.988],[-26.862,-187.276]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[-2.154,-14.341],[7.939,-1.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[10.862,0.026]],"v":[[-9.353,-166.739],[-20.189,-138.686],[-42.231,-135.648],[-46.654,-183.988],[-26.862,-187.276]],"c":true}],"e":[{"i":[[-1.634,-14.432],[15.643,-0.316],[0,0],[0,0],[0,0]],"o":[[1.499,13.233],[0,0],[0,0],[0,0],[12.13,-2.72]],"v":[[1.501,-165.483],[-19.143,-137.184],[-39.891,-135.152],[-43.647,-185.494],[-18.285,-187.276]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[-1.634,-14.432],[15.643,-0.316],[0,0],[0,0],[0,0]],"o":[[1.499,13.233],[0,0],[0,0],[0,0],[12.13,-2.72]],"v":[[1.501,-165.483],[-19.143,-137.184],[-39.891,-135.152],[-43.647,-185.494],[-18.285,-187.276]],"c":true}],"e":[{"i":[[-2.154,-14.341],[17.439,-2.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[13.612,-1.724]],"v":[[10.147,-167.739],[-8.939,-136.936],[-38.481,-134.898],[-40.154,-185.488],[-12.862,-186.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[-2.154,-14.341],[17.439,-2.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[13.612,-1.724]],"v":[[10.147,-167.739],[-8.939,-136.936],[-38.481,-134.898],[-40.154,-185.488],[-12.862,-186.776]],"c":true}],"e":[{"i":[[-1.534,-14.395],[14.273,-2.231],[0,0],[0,0],[0,0]],"o":[[1.502,14.095],[0,0],[0,0],[0,0],[14.778,1.383]],"v":[[17.998,-164.132],[-2.523,-136.269],[-36.047,-134.41],[-37.912,-185.525],[-5.028,-187.133]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[-1.534,-14.395],[14.273,-2.231],[0,0],[0,0],[0,0]],"o":[[1.502,14.095],[0,0],[0,0],[0,0],[14.778,1.383]],"v":[[17.998,-164.132],[-2.523,-136.269],[-36.047,-134.41],[-37.912,-185.525],[-5.028,-187.133]],"c":true}],"e":[{"i":[[-0.767,-14.448],[16.041,-0.938],[0,0],[0,0],[0,0]],"o":[[0.751,14.147],[0,0],[0,0],[0,0],[14.678,-0.575]],"v":[[23.139,-163.775],[0.762,-135.102],[-34.862,-134.172],[-35.92,-185.061],[-3.241,-186.241]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[-0.767,-14.448],[16.041,-0.938],[0,0],[0,0],[0,0]],"o":[[0.751,14.147],[0,0],[0,0],[0,0],[14.678,-0.575]],"v":[[23.139,-163.775],[0.762,-135.102],[-34.862,-134.172],[-35.92,-185.061],[-3.241,-186.241]],"c":true}],"e":[{"i":[[0,-14.502],[14.502,0],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[27.029,-160.917],[4.297,-134.435],[-33.677,-133.685],[-33.677,-184.598],[2.797,-185.348]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":21.376,"s":[{"i":[[0,-14.502],[14.502,0],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[27.029,-160.917],[4.297,-134.435],[-33.677,-133.685],[-33.677,-184.598],[2.797,-185.348]],"c":true}],"e":[{"i":[[0,-14.502],[16.037,-0.065],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[30.363,-160.417],[4.963,-133.935],[-32.927,-133.268],[-32.927,-184.014],[3.297,-184.848]],"c":true}]},{"t":22.2529296875}]},"nm":"B"},{"ind":2,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[7.181,0.245],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[5.681,-0.592],[2.589,13.962]],"v":[[-272.931,-26.995],[-280.427,-25.995],[-286.427,-75.158],[-279.181,-76.658],[-269.094,-56.926]],"c":true}],"e":[{"i":[[7.181,0.245],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[5.681,-0.592],[2.589,13.962]],"v":[[-25.931,-49.995],[-33.427,-48.995],[-39.427,-98.158],[-32.181,-99.658],[-22.094,-79.926]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[7.181,0.245],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[5.681,-0.592],[2.589,13.962]],"v":[[-25.931,-49.995],[-33.427,-48.995],[-39.427,-98.158],[-32.181,-99.658],[-22.094,-79.926]],"c":true}],"e":[{"i":[[13.431,-1.505],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[7.681,-0.842],[2.589,13.962]],"v":[[-17.681,-50.245],[-33.677,-48.495],[-39.427,-98.158],[-22.431,-100.908],[-7.094,-82.426]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[13.431,-1.505],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[7.681,-0.842],[2.589,13.962]],"v":[[-17.681,-50.245],[-33.677,-48.495],[-39.427,-98.158],[-22.431,-100.908],[-7.094,-82.426]],"c":true}],"e":[{"i":[[13.431,-1.505],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[11.931,0.158],[2.589,13.962]],"v":[[-9.181,-49.745],[-33.677,-47.745],[-37.927,-97.908],[-12.681,-100.908],[5.906,-78.676]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[13.431,-1.505],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[11.931,0.158],[2.589,13.962]],"v":[[-9.181,-49.745],[-33.677,-47.745],[-37.927,-97.908],[-12.681,-100.908],[5.906,-78.676]],"c":true}],"e":[{"i":[[14.118,-0.752],[0,0],[0,0],[0,0],[-1.406,-16.074]],"o":[[0,0],[0,0],[0,0],[11.868,-1.421],[1.237,14.146]],"v":[[-2.681,-47.745],[-34.177,-45.745],[-36.927,-96.908],[-7.181,-99.908],[16.656,-76.926]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[14.118,-0.752],[0,0],[0,0],[0,0],[-1.406,-16.074]],"o":[[0,0],[0,0],[0,0],[11.868,-1.421],[1.237,14.146]],"v":[[-2.681,-47.745],[-34.177,-45.745],[-36.927,-96.908],[-7.181,-99.908],[16.656,-76.926]],"c":true}],"e":[{"i":[[12.181,0.495],[0,0],[0,0],[0,0],[-0.906,-12.324]],"o":[[0,0],[0,0],[0,0],[14.804,0],[1.041,14.162]],"v":[[3.069,-47.245],[-32.927,-45.495],[-35.677,-96.408],[1.819,-98.408],[24.906,-73.926]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[12.181,0.495],[0,0],[0,0],[0,0],[-0.906,-12.324]],"o":[[0,0],[0,0],[0,0],[14.804,0],[1.041,14.162]],"v":[[3.069,-47.245],[-32.927,-45.495],[-35.677,-96.408],[1.819,-98.408],[24.906,-73.926]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-0.264,-12.571]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0.261,12.426]],"v":[[8.652,-45.745],[-33.344,-44.412],[-34.844,-95.658],[6.902,-97.325],[31.239,-71.926]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-0.264,-12.571]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0.261,12.426]],"v":[[8.652,-45.745],[-33.344,-44.412],[-34.844,-95.658],[6.902,-97.325],[31.239,-71.926]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-0.885,-14.241]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0.863,14.121]],"v":[[10.735,-44.495],[-33.261,-43.579],[-33.761,-94.908],[11.235,-95.742],[35.822,-70.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-0.885,-14.241]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0.863,14.121]],"v":[[10.735,-44.495],[-33.261,-43.579],[-33.761,-94.908],[11.235,-95.742],[35.822,-70.176]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[12.681,0.408],[0,14.2]],"v":[[14.319,-43.995],[-32.677,-43.245],[-33.177,-93.658],[16.569,-94.658],[39.156,-68.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":21.376,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[12.681,0.408],[0,14.2]],"v":[[14.319,-43.995],[-32.677,-43.245],[-33.177,-93.658],[16.569,-94.658],[39.156,-68.176]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[14.819,-42.745],[-32.677,-41.995],[-32.677,-93.658],[16.069,-94.158],[41.406,-67.926]],"c":true}]},{"t":22.2529296875}]},"nm":"B"},{"ty":"mm","mm":1,"nm":"Merge Paths 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0.15,0.8,0.55,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":15,"op":23,"st":0,"bm":10,"sr":1},{"ddd":0,"ind":27,"ty":4,"nm":"B light blue layer 3 shadow","parent":34,"ks":{"o":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":16,"s":[5],"e":[40]},{"t":22}]},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[0,0],[0,0],[-3.12,0.516],[4.596,42.859],[0,0]],"o":[[0,0],[0,0],[3.12,-0.516],[-5.931,-55.302],[0,0]],"v":[[-58.13,-221.16],[-111.991,-226.753],[-103.289,41.823],[-46.069,-81.698],[-54.696,-253.118]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3.12,0.516],[9.482,45.79],[0,0]],"o":[[0,0],[0,0],[3.12,-0.516],[-11.278,-54.464],[0,0]],"v":[[-9.13,-263.16],[-128.991,-241.753],[-103.289,41.823],[-0.569,37.802],[-54.696,-253.118]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[0,0],[0,0],[-3.12,0.516],[9.482,45.79],[0,0]],"o":[[0,0],[0,0],[3.12,-0.516],[-11.278,-54.464],[0,0]],"v":[[-9.13,-263.16],[-128.991,-241.753],[-103.289,41.823],[-0.569,37.802],[-54.696,-253.118]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3.12,0.516],[-3.803,42.936],[0,0]],"o":[[0,0],[0,0],[3.12,-0.516],[3.803,-42.936],[0,0]],"v":[[-9.13,-263.16],[-128.991,-241.753],[-103.289,41.823],[42.431,32.302],[11.804,-249.118]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[0,0],[0,0],[-3.12,0.516],[-3.803,42.936],[0,0]],"o":[[0,0],[0,0],[3.12,-0.516],[3.803,-42.936],[0,0]],"v":[[-9.13,-263.16],[-128.991,-241.753],[-103.289,41.823],[42.431,32.302],[11.804,-249.118]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3.12,0.516],[-3.803,42.936],[0,0]],"o":[[0,0],[0,0],[3.12,-0.516],[3.803,-42.936],[0,0]],"v":[[-9.13,-263.16],[-128.991,-241.753],[-103.289,41.823],[70.931,-10.698],[8.304,-239.118]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[0,0],[0,0],[-3.12,0.516],[-3.803,42.936],[0,0]],"o":[[0,0],[0,0],[3.12,-0.516],[3.803,-42.936],[0,0]],"v":[[-9.13,-263.16],[-128.991,-241.753],[-103.289,41.823],[70.931,-10.698],[8.304,-239.118]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3,1],[3,43],[0,0]],"o":[[0,0],[0,0],[3,-1],[-3,-43],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[59,30],[-33,-263]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[0,0],[0,0],[-3,1],[3,43],[0,0]],"o":[[0,0],[0,0],[3,-1],[-3,-43],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[59,30],[-33,-263]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3,1],[3,43],[0,0]],"o":[[0,0],[0,0],[3,-1],[-3,-43],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[41,8],[-35,-264]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[0,0],[0,0],[-3,1],[3,43],[0,0]],"o":[[0,0],[0,0],[3,-1],[-3,-43],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[41,8],[-35,-264]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3,1],[-1,39],[0,0]],"o":[[0,0],[0,0],[3,-1],[1.444,-56.302],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[70,-17],[3,-268]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[0,0],[0,0],[-3,1],[-1,39],[0,0]],"o":[[0,0],[0,0],[3,-1],[1.444,-56.302],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[70,-17],[3,-268]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3,1],[9.112,40.348],[0,0]],"o":[[0,0],[0,0],[3,-1],[-12.406,-54.937],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[96,-9],[28,-280]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[0,0],[0,0],[-3,1],[9.112,40.348],[0,0]],"o":[[0,0],[0,0],[3,-1],[-12.406,-54.937],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[96,-9],[28,-280]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3,1],[9.112,40.348],[0,0]],"o":[[0,0],[0,0],[3,-1],[-12.406,-54.937],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[96,-19],[50,-287]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[0,0],[0,0],[-3,1],[9.112,40.348],[0,0]],"o":[[0,0],[0,0],[3,-1],[-12.406,-54.937],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[96,-19],[50,-287]],"c":true}],"e":[{"i":[[0,0],[0,0],[-3,1],[9.112,40.348],[0,0]],"o":[[0,0],[0,0],[3,-1],[-12.406,-54.937],[0,0]],"v":[[-47,-270],[-162,-230],[-92,46],[116,-7],[70,-285]],"c":true}]},{"t":18}]},"o":{"k":100},"x":{"k":0},"nm":"Mask 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[-0.201,21.411],[-0.286,15.23],[2.474,0.203],[0,0],[0,0],[-1.893,-0.375],[-0.662,-1.856]],"o":[[0.134,-14.338],[0.036,-1.896],[-1.912,-0.157],[0,0],[0,0],[0.644,0.128],[1.338,-20.856]],"v":[[-46.509,-50.162],[-44.214,-98.23],[-45.963,-101.593],[-49.132,-100.593],[-55.007,4.75],[-50.857,4.25],[-48.588,6.856]],"c":true}],"e":[{"i":[[0.279,24.912],[0.464,22.605],[2.088,1.343],[0,0],[0,0],[-4.143,-2],[-0.787,-3.981]],"o":[[-0.241,-21.463],[-0.098,-4.771],[-4.242,-2.728],[0,0],[0,0],[3.51,1.694],[-0.037,-19.356]],"v":[[-40.884,-52.662],[-41.839,-108.48],[-44.463,-118.593],[-53.507,-120.718],[-54.757,3.5],[-45.357,4.875],[-39.963,12.856]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[0.279,24.912],[0.464,22.605],[2.088,1.343],[0,0],[0,0],[-4.143,-2],[-0.787,-3.981]],"o":[[-0.241,-21.463],[-0.098,-4.771],[-4.242,-2.728],[0,0],[0,0],[3.51,1.694],[-0.037,-19.356]],"v":[[-40.884,-52.662],[-41.839,-108.48],[-44.463,-118.593],[-53.507,-120.718],[-54.757,3.5],[-45.357,4.875],[-39.963,12.856]],"c":true}],"e":[{"i":[[2.509,24.787],[1.952,19.983],[11.463,8.593],[0,0],[0,0],[-8.268,-3.75],[-3.537,-7.856]],"o":[[-1.87,-18.476],[-0.161,-1.645],[-7.865,-5.896],[0,0],[0,0],[10.005,4.538],[-1.787,-20.356]],"v":[[-22.759,-58.037],[-27.839,-117.855],[-38.713,-134.843],[-60.257,-138.093],[-58.257,2.5],[-32.482,4.25],[-15.963,19.356]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[2.509,24.787],[1.952,19.983],[11.463,8.593],[0,0],[0,0],[-8.268,-3.75],[-3.537,-7.856]],"o":[[-1.87,-18.476],[-0.161,-1.645],[-7.865,-5.896],[0,0],[0,0],[10.005,4.538],[-1.787,-20.356]],"v":[[-22.759,-58.037],[-27.839,-117.855],[-38.713,-134.843],[-60.257,-138.093],[-58.257,2.5],[-32.482,4.25],[-15.963,19.356]],"c":true}],"e":[{"i":[[2.509,23.537],[1.952,19.983],[37.213,10.843],[0,0],[0,0],[-37.012,-3.318],[3.87,36.773]],"o":[[-1.969,-18.466],[-0.161,-1.645],[-39.595,-11.537],[0,0],[0,0],[52.982,4.75],[-2.037,-19.356]],"v":[[46.741,-67.787],[41.411,-115.355],[-2.713,-150.593],[-68.757,-153.593],[-61.507,2.25],[4.268,0.25],[56.037,7.356]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[2.509,23.537],[1.952,19.983],[37.213,10.843],[0,0],[0,0],[-37.012,-3.318],[3.87,36.773]],"o":[[-1.969,-18.466],[-0.161,-1.645],[-39.595,-11.537],[0,0],[0,0],[52.982,4.75],[-2.037,-19.356]],"v":[[46.741,-67.787],[41.411,-115.355],[-2.713,-150.593],[-68.757,-153.593],[-61.507,2.25],[4.268,0.25],[56.037,7.356]],"c":true}],"e":[{"i":[[5.869,20.677],[5.339,19.355],[49.963,-14.657],[0,0],[0,0],[-48.018,8.5],[14.463,52.894]],"o":[[-5.241,-18.463],[-6.854,-24.846],[-39.574,11.609],[0,0],[0,0],[56.175,-9.944],[-11.083,-40.532]],"v":[[79.741,-141.537],[64.661,-196.855],[-12.963,-182.843],[-76.257,-168.093],[-64.507,2],[54.518,-20],[98.287,-79.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[5.869,20.677],[5.339,19.355],[49.963,-14.657],[0,0],[0,0],[-48.018,8.5],[14.463,52.894]],"o":[[-5.241,-18.463],[-6.854,-24.846],[-39.574,11.609],[0,0],[0,0],[56.175,-9.944],[-11.083,-40.532]],"v":[[79.741,-141.537],[64.661,-196.855],[-12.963,-182.843],[-76.257,-168.093],[-64.507,2],[54.518,-20],[98.287,-79.394]],"c":true}],"e":[{"i":[[7.415,20.175],[10.589,29.855],[50.213,-22.407],[0,0],[0,0],[-48.907,15.377],[14.389,46.029]],"o":[[-8.991,-24.463],[-8.616,-24.291],[-37.662,16.806],[0,0],[0,0],[43.732,-13.75],[-12.537,-40.106]],"v":[[75.491,-152.537],[45.911,-232.355],[-25.463,-197.593],[-83.007,-177.843],[-66.757,2],[45.518,-26.75],[98.787,-83.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[7.415,20.175],[10.589,29.855],[50.213,-22.407],[0,0],[0,0],[-48.907,15.377],[14.389,46.029]],"o":[[-8.991,-24.463],[-8.616,-24.291],[-37.662,16.806],[0,0],[0,0],[43.732,-13.75],[-12.537,-40.106]],"v":[[75.491,-152.537],[45.911,-232.355],[-25.463,-197.593],[-83.007,-177.843],[-66.757,2],[45.518,-26.75],[98.787,-83.894]],"c":true}],"e":[{"i":[[6.732,20.413],[9.339,28.855],[50.213,-22.407],[0,0],[0,0],[-49.459,13.494],[14.043,47.445]],"o":[[-7.491,-22.713],[-7.937,-24.521],[-37.662,16.806],[0,0],[0,0],[43.982,-12],[-12.537,-42.356]],"v":[[73.491,-153.537],[43.411,-239.855],[-29.463,-206.343],[-89.007,-186.343],[-68.507,1.5],[45.518,-26.75],[96.287,-84.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[6.732,20.413],[9.339,28.855],[50.213,-22.407],[0,0],[0,0],[-49.459,13.494],[14.043,47.445]],"o":[[-7.491,-22.713],[-7.937,-24.521],[-37.662,16.806],[0,0],[0,0],[43.982,-12],[-12.537,-42.356]],"v":[[73.491,-153.537],[43.411,-239.855],[-29.463,-206.343],[-89.007,-186.343],[-68.507,1.5],[45.518,-26.75],[96.287,-84.644]],"c":true}],"e":[{"i":[[5.741,20.713],[9.339,28.855],[60.463,-27.657],[0,0],[0,0],[-49.459,13.494],[12.963,47.644]],"o":[[-5.741,-20.713],[-7.937,-24.521],[-37.505,17.155],[0,0],[0,0],[43.982,-12],[-11.586,-42.584]],"v":[[73.741,-153.787],[45.411,-241.855],[-29.713,-213.843],[-93.257,-193.343],[-71.257,0.5],[49.768,-26.5],[95.287,-81.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[{"i":[[5.741,20.713],[9.339,28.855],[60.463,-27.657],[0,0],[0,0],[-49.459,13.494],[12.963,47.644]],"o":[[-5.741,-20.713],[-7.937,-24.521],[-37.505,17.155],[0,0],[0,0],[43.982,-12],[-11.586,-42.584]],"v":[[73.741,-153.787],[45.411,-241.855],[-29.713,-213.843],[-93.257,-193.343],[-71.257,0.5],[49.768,-26.5],[95.287,-81.894]],"c":true}],"e":[{"i":[[5.741,20.713],[7.839,29.855],[63.713,-28.907],[0,0],[0,0],[-50.018,11.25],[12.963,53.394]],"o":[[-5.741,-20.713],[-6.546,-24.929],[-37.557,17.04],[0,0],[0,0],[46.058,-10.359],[-10.749,-44.276]],"v":[[75.741,-154.037],[52.411,-240.355],[-22.963,-222.593],[-96.007,-200.343],[-72.507,1.25],[48.768,-23.5],[95.037,-81.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[5.741,20.713],[7.839,29.855],[63.713,-28.907],[0,0],[0,0],[-50.018,11.25],[12.963,53.394]],"o":[[-5.741,-20.713],[-6.546,-24.929],[-37.557,17.04],[0,0],[0,0],[46.058,-10.359],[-10.749,-44.276]],"v":[[75.741,-154.037],[52.411,-240.355],[-22.963,-222.593],[-96.007,-200.343],[-72.507,1.25],[48.768,-23.5],[95.037,-81.894]],"c":true}],"e":[{"i":[[4.259,17.037],[7.839,29.855],[64.463,-23.407],[0,0],[0,0],[-49.518,7.25],[10.523,50.543]],"o":[[-5.991,-25.463],[-6.546,-24.929],[-38.765,14.076],[0,0],[0,0],[48.964,-7.169],[-9.287,-44.606]],"v":[[76.491,-154.787],[58.161,-239.105],[-23.213,-225.593],[-97.507,-206.343],[-74.007,1.25],[52.268,-20],[94.787,-82.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[4.259,17.037],[7.839,29.855],[64.463,-23.407],[0,0],[0,0],[-49.518,7.25],[10.523,50.543]],"o":[[-5.991,-25.463],[-6.546,-24.929],[-38.765,14.076],[0,0],[0,0],[48.964,-7.169],[-9.287,-44.606]],"v":[[76.491,-154.787],[58.161,-239.105],[-23.213,-225.593],[-97.507,-206.343],[-74.007,1.25],[52.268,-20],[94.787,-82.394]],"c":true}],"e":[{"i":[[4.009,18.287],[9.089,30.105],[63.963,-18.157],[0,0],[0,0],[-52.207,5.395],[4.963,38.144]],"o":[[-1.741,-23.713],[-7.236,-23.967],[-39.674,11.262],[0,0],[0,0],[67.732,-7],[-5.852,-44.98]],"v":[[74.491,-158.287],[64.411,-233.605],[-18.713,-229.093],[-96.757,-211.843],[-75.507,0.25],[45.268,-14.75],[94.287,-79.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[4.009,18.287],[9.089,30.105],[63.963,-18.157],[0,0],[0,0],[-52.207,5.395],[4.963,38.144]],"o":[[-1.741,-23.713],[-7.236,-23.967],[-39.674,11.262],[0,0],[0,0],[67.732,-7],[-5.852,-44.98]],"v":[[74.491,-158.287],[64.411,-233.605],[-18.713,-229.093],[-96.757,-211.843],[-75.507,0.25],[45.268,-14.75],[94.287,-79.394]],"c":true}],"e":[{"i":[[5.509,15.037],[12.589,49.605],[49.213,-8.907],[0,0],[0,0],[-52.29,4.52],[5.084,46.282]],"o":[[-2.241,-23.713],[-7.654,-30.16],[-40.583,7.345],[0,0],[0,0],[60.732,-5.25],[-5.037,-45.856]],"v":[[73.241,-141.287],[69.161,-222.855],[-16.963,-229.093],[-94.757,-216.843],[-76.507,-0.5],[42.768,-11.75],[94.037,-80.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[5.509,15.037],[12.589,49.605],[49.213,-8.907],[0,0],[0,0],[-52.29,4.52],[5.084,46.282]],"o":[[-2.241,-23.713],[-7.654,-30.16],[-40.583,7.345],[0,0],[0,0],[60.732,-5.25],[-5.037,-45.856]],"v":[[73.241,-141.287],[69.161,-222.855],[-16.963,-229.093],[-94.757,-216.843],[-76.507,-0.5],[42.768,-11.75],[94.037,-80.894]],"c":true}],"e":[{"i":[[15.759,20.037],[2.089,16.855],[46.963,-6.157],[0,0],[0,0],[-32.059,1.901],[3.213,53.144]],"o":[[4.509,-21.713],[-5.161,-61.645],[-32.938,4.318],[0,0],[0,0],[63.232,-3.75],[-3.787,-41.856]],"v":[[68.491,-134.287],[76.911,-187.605],[-8.463,-229.843],[-91.507,-219.843],[-77.257,-0.75],[22.518,-7.25],[94.037,-72.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[15.759,20.037],[2.089,16.855],[46.963,-6.157],[0,0],[0,0],[-32.059,1.901],[3.213,53.144]],"o":[[4.509,-21.713],[-5.161,-61.645],[-32.938,4.318],[0,0],[0,0],[63.232,-3.75],[-3.787,-41.856]],"v":[[68.491,-134.287],[76.911,-187.605],[-8.463,-229.843],[-91.507,-219.843],[-77.257,-0.75],[22.518,-7.25],[94.037,-72.894]],"c":true}],"e":[{"i":[[20.259,14.037],[1.766,21.378],[30.713,-3.907],[0,0],[0,0],[-39.232,1.5],[3.134,56.753]],"o":[[11.509,-19.963],[-4.411,-53.395],[-38.914,4.95],[0,0],[0,0],[39.232,-1.5],[-1.787,-32.356]],"v":[[63.741,-128.537],[77.411,-184.105],[3.537,-230.593],[-88.507,-223.093],[-78.007,-1],[25.018,-5.5],[93.537,-70.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[20.259,14.037],[1.766,21.378],[30.713,-3.907],[0,0],[0,0],[-39.232,1.5],[3.134,56.753]],"o":[[11.509,-19.963],[-4.411,-53.395],[-38.914,4.95],[0,0],[0,0],[39.232,-1.5],[-1.787,-32.356]],"v":[[63.741,-128.537],[77.411,-184.105],[3.537,-230.593],[-88.507,-223.093],[-78.007,-1],[25.018,-5.5],[93.537,-70.894]],"c":true}],"e":[{"i":[[24.259,15.287],[1.029,21.401],[37.213,-2.157],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[12.683,-14.513],[-3.911,-45.645],[-21.287,1.093],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[61.241,-124.787],[78.661,-178.355],[4.787,-229.343],[-86.007,-223.593],[-78.007,-1],[18.768,-3.5],[92.287,-66.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[24.259,15.287],[1.029,21.401],[37.213,-2.157],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[12.683,-14.513],[-3.911,-45.645],[-21.287,1.093],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[61.241,-124.787],[78.661,-178.355],[4.787,-229.343],[-86.007,-223.593],[-78.007,-1],[18.768,-3.5],[92.287,-66.644]],"c":true}],"e":[{"i":[[22.009,10.287],[0,21.451],[39.713,-1.157],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[14.509,-10.713],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[58.491,-122.287],[79.161,-171.855],[11.037,-228.593],[-83.507,-224.093],[-78.007,-1],[21.268,-2.5],[92.037,-63.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[22.009,10.287],[0,21.451],[39.713,-1.157],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[14.509,-10.713],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[58.491,-122.287],[79.161,-171.855],[11.037,-228.593],[-83.507,-224.093],[-78.007,-1],[21.268,-2.5],[92.037,-63.394]],"c":true}],"e":[{"i":[[21.149,8.762],[0.374,21.444],[35.338,-1.657],[0,0],[0,0],[0,0],[-0.227,41.088]],"o":[[15.106,-9.064],[-0.705,-39.533],[0,0],[0,0],[0,0],[43.478,0.75],[0.16,-28.893]],"v":[[56.741,-121.162],[79.411,-167.48],[13.912,-227.343],[-81.507,-224.968],[-78.382,-0.75],[19.518,-1.25],[91.287,-62.019]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":21.376,"s":[{"i":[[21.149,8.762],[0.374,21.444],[35.338,-1.657],[0,0],[0,0],[0,0],[-0.227,41.088]],"o":[[15.106,-9.064],[-0.705,-39.533],[0,0],[0,0],[0,0],[43.478,0.75],[0.16,-28.893]],"v":[[56.741,-121.162],[79.411,-167.48],[13.912,-227.343],[-81.507,-224.968],[-78.382,-0.75],[19.518,-1.25],[91.287,-62.019]],"c":true}],"e":[{"i":[[21.149,8.762],[0.749,21.438],[32.213,-0.907],[0,0],[0,0],[0,0],[-0.152,41.088]],"o":[[15.106,-9.064],[-1.411,-40.395],[0,0],[0,0],[0,0],[41.473,0.5],[0.106,-28.93]],"v":[[56.241,-119.787],[79.411,-168.105],[13.287,-226.593],[-79.507,-225.843],[-78.757,-0.5],[21.518,-0.5],[90.787,-62.894]],"c":true}]},{"t":22.2529296875}]},"nm":"B"},{"ty":"fl","fillEnabled":true,"c":{"k":[0.15,0.8,0.55,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":7,"op":15,"st":0,"bm":10,"sr":1},{"ddd":0,"ind":28,"ty":4,"nm":"B light blue layer 4","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[-0.201,21.411],[-0.286,15.23],[2.474,0.203],[0,0],[0,0],[-1.893,-0.375],[-0.662,-1.856]],"o":[[0.134,-14.338],[0.036,-1.896],[-1.912,-0.157],[0,0],[0,0],[0.644,0.128],[1.338,-20.856]],"v":[[-46.509,-50.162],[-44.214,-98.23],[-45.963,-101.593],[-49.132,-100.593],[-55.007,4.75],[-50.857,4.25],[-48.588,6.856]],"c":true}],"e":[{"i":[[0.279,24.912],[0.464,22.605],[2.088,1.343],[0,0],[0,0],[-4.143,-2],[-0.787,-3.981]],"o":[[-0.241,-21.463],[-0.098,-4.771],[-4.242,-2.728],[0,0],[0,0],[3.51,1.694],[-0.037,-19.356]],"v":[[-40.884,-52.662],[-41.839,-108.48],[-44.463,-118.593],[-53.507,-120.718],[-54.757,3.5],[-45.357,4.875],[-39.963,12.856]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[0.279,24.912],[0.464,22.605],[2.088,1.343],[0,0],[0,0],[-4.143,-2],[-0.787,-3.981]],"o":[[-0.241,-21.463],[-0.098,-4.771],[-4.242,-2.728],[0,0],[0,0],[3.51,1.694],[-0.037,-19.356]],"v":[[-40.884,-52.662],[-41.839,-108.48],[-44.463,-118.593],[-53.507,-120.718],[-54.757,3.5],[-45.357,4.875],[-39.963,12.856]],"c":true}],"e":[{"i":[[2.509,24.787],[1.952,19.983],[11.463,8.593],[0,0],[0,0],[-8.268,-3.75],[-3.537,-7.856]],"o":[[-1.87,-18.476],[-0.161,-1.645],[-7.865,-5.896],[0,0],[0,0],[10.005,4.538],[-1.787,-20.356]],"v":[[-22.759,-58.037],[-27.839,-117.855],[-38.713,-134.843],[-60.257,-138.093],[-58.257,2.5],[-32.482,4.25],[-15.963,19.356]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[2.509,24.787],[1.952,19.983],[11.463,8.593],[0,0],[0,0],[-8.268,-3.75],[-3.537,-7.856]],"o":[[-1.87,-18.476],[-0.161,-1.645],[-7.865,-5.896],[0,0],[0,0],[10.005,4.538],[-1.787,-20.356]],"v":[[-22.759,-58.037],[-27.839,-117.855],[-38.713,-134.843],[-60.257,-138.093],[-58.257,2.5],[-32.482,4.25],[-15.963,19.356]],"c":true}],"e":[{"i":[[2.509,23.537],[1.952,19.983],[37.213,10.843],[0,0],[0,0],[-37.012,-3.318],[3.87,36.773]],"o":[[-1.969,-18.466],[-0.161,-1.645],[-39.595,-11.537],[0,0],[0,0],[52.982,4.75],[-2.037,-19.356]],"v":[[46.741,-67.787],[41.411,-115.355],[-2.713,-150.593],[-68.757,-153.593],[-61.507,2.25],[4.268,0.25],[56.037,7.356]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[2.509,23.537],[1.952,19.983],[37.213,10.843],[0,0],[0,0],[-37.012,-3.318],[3.87,36.773]],"o":[[-1.969,-18.466],[-0.161,-1.645],[-39.595,-11.537],[0,0],[0,0],[52.982,4.75],[-2.037,-19.356]],"v":[[46.741,-67.787],[41.411,-115.355],[-2.713,-150.593],[-68.757,-153.593],[-61.507,2.25],[4.268,0.25],[56.037,7.356]],"c":true}],"e":[{"i":[[5.869,20.677],[5.339,19.355],[49.963,-14.657],[0,0],[0,0],[-48.018,8.5],[14.463,52.894]],"o":[[-5.241,-18.463],[-6.854,-24.846],[-39.574,11.609],[0,0],[0,0],[56.175,-9.944],[-11.083,-40.532]],"v":[[79.741,-141.537],[64.661,-196.855],[-12.963,-182.843],[-76.257,-168.093],[-64.507,2],[54.518,-20],[98.287,-79.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[5.869,20.677],[5.339,19.355],[49.963,-14.657],[0,0],[0,0],[-48.018,8.5],[14.463,52.894]],"o":[[-5.241,-18.463],[-6.854,-24.846],[-39.574,11.609],[0,0],[0,0],[56.175,-9.944],[-11.083,-40.532]],"v":[[79.741,-141.537],[64.661,-196.855],[-12.963,-182.843],[-76.257,-168.093],[-64.507,2],[54.518,-20],[98.287,-79.394]],"c":true}],"e":[{"i":[[7.415,20.175],[10.589,29.855],[50.213,-22.407],[0,0],[0,0],[-48.907,15.377],[14.389,46.029]],"o":[[-8.991,-24.463],[-8.616,-24.291],[-37.662,16.806],[0,0],[0,0],[43.732,-13.75],[-12.537,-40.106]],"v":[[75.491,-152.537],[45.911,-232.355],[-25.463,-197.593],[-83.007,-177.843],[-66.757,2],[45.518,-26.75],[98.787,-83.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[7.415,20.175],[10.589,29.855],[50.213,-22.407],[0,0],[0,0],[-48.907,15.377],[14.389,46.029]],"o":[[-8.991,-24.463],[-8.616,-24.291],[-37.662,16.806],[0,0],[0,0],[43.732,-13.75],[-12.537,-40.106]],"v":[[75.491,-152.537],[45.911,-232.355],[-25.463,-197.593],[-83.007,-177.843],[-66.757,2],[45.518,-26.75],[98.787,-83.894]],"c":true}],"e":[{"i":[[6.732,20.413],[9.339,28.855],[50.213,-22.407],[0,0],[0,0],[-49.459,13.494],[14.043,47.445]],"o":[[-7.491,-22.713],[-7.937,-24.521],[-37.662,16.806],[0,0],[0,0],[43.982,-12],[-12.537,-42.356]],"v":[[73.491,-153.537],[43.411,-239.855],[-29.463,-206.343],[-89.007,-186.343],[-68.507,1.5],[45.518,-26.75],[96.287,-84.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[6.732,20.413],[9.339,28.855],[50.213,-22.407],[0,0],[0,0],[-49.459,13.494],[14.043,47.445]],"o":[[-7.491,-22.713],[-7.937,-24.521],[-37.662,16.806],[0,0],[0,0],[43.982,-12],[-12.537,-42.356]],"v":[[73.491,-153.537],[43.411,-239.855],[-29.463,-206.343],[-89.007,-186.343],[-68.507,1.5],[45.518,-26.75],[96.287,-84.644]],"c":true}],"e":[{"i":[[5.741,20.713],[9.339,28.855],[60.463,-27.657],[0,0],[0,0],[-49.459,13.494],[12.963,47.644]],"o":[[-5.741,-20.713],[-7.937,-24.521],[-37.505,17.155],[0,0],[0,0],[43.982,-12],[-11.586,-42.584]],"v":[[73.741,-153.787],[45.411,-241.855],[-29.713,-213.843],[-93.257,-193.343],[-71.257,0.5],[49.768,-26.5],[95.287,-81.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[{"i":[[5.741,20.713],[9.339,28.855],[60.463,-27.657],[0,0],[0,0],[-49.459,13.494],[12.963,47.644]],"o":[[-5.741,-20.713],[-7.937,-24.521],[-37.505,17.155],[0,0],[0,0],[43.982,-12],[-11.586,-42.584]],"v":[[73.741,-153.787],[45.411,-241.855],[-29.713,-213.843],[-93.257,-193.343],[-71.257,0.5],[49.768,-26.5],[95.287,-81.894]],"c":true}],"e":[{"i":[[5.741,20.713],[7.839,29.855],[63.713,-28.907],[0,0],[0,0],[-50.018,11.25],[12.963,53.394]],"o":[[-5.741,-20.713],[-6.546,-24.929],[-37.557,17.04],[0,0],[0,0],[46.058,-10.359],[-10.749,-44.276]],"v":[[75.741,-154.037],[52.411,-240.355],[-22.963,-222.593],[-96.007,-200.343],[-72.507,1.25],[48.768,-23.5],[95.037,-81.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[5.741,20.713],[7.839,29.855],[63.713,-28.907],[0,0],[0,0],[-50.018,11.25],[12.963,53.394]],"o":[[-5.741,-20.713],[-6.546,-24.929],[-37.557,17.04],[0,0],[0,0],[46.058,-10.359],[-10.749,-44.276]],"v":[[75.741,-154.037],[52.411,-240.355],[-22.963,-222.593],[-96.007,-200.343],[-72.507,1.25],[48.768,-23.5],[95.037,-81.894]],"c":true}],"e":[{"i":[[4.259,17.037],[7.839,29.855],[64.463,-23.407],[0,0],[0,0],[-49.518,7.25],[10.523,50.543]],"o":[[-5.991,-25.463],[-6.546,-24.929],[-38.765,14.076],[0,0],[0,0],[48.964,-7.169],[-9.287,-44.606]],"v":[[76.491,-154.787],[58.161,-239.105],[-23.213,-225.593],[-97.507,-206.343],[-74.007,1.25],[52.268,-20],[94.787,-82.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[4.259,17.037],[7.839,29.855],[64.463,-23.407],[0,0],[0,0],[-49.518,7.25],[10.523,50.543]],"o":[[-5.991,-25.463],[-6.546,-24.929],[-38.765,14.076],[0,0],[0,0],[48.964,-7.169],[-9.287,-44.606]],"v":[[76.491,-154.787],[58.161,-239.105],[-23.213,-225.593],[-97.507,-206.343],[-74.007,1.25],[52.268,-20],[94.787,-82.394]],"c":true}],"e":[{"i":[[4.009,18.287],[9.089,30.105],[63.963,-18.157],[0,0],[0,0],[-52.207,5.395],[4.963,38.144]],"o":[[-1.741,-23.713],[-7.236,-23.967],[-39.674,11.262],[0,0],[0,0],[67.732,-7],[-5.852,-44.98]],"v":[[74.491,-158.287],[64.411,-233.605],[-18.713,-229.093],[-96.757,-211.843],[-75.507,0.25],[45.268,-14.75],[94.287,-79.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[4.009,18.287],[9.089,30.105],[63.963,-18.157],[0,0],[0,0],[-52.207,5.395],[4.963,38.144]],"o":[[-1.741,-23.713],[-7.236,-23.967],[-39.674,11.262],[0,0],[0,0],[67.732,-7],[-5.852,-44.98]],"v":[[74.491,-158.287],[64.411,-233.605],[-18.713,-229.093],[-96.757,-211.843],[-75.507,0.25],[45.268,-14.75],[94.287,-79.394]],"c":true}],"e":[{"i":[[5.509,15.037],[12.589,49.605],[49.213,-8.907],[0,0],[0,0],[-52.29,4.52],[5.084,46.282]],"o":[[-2.241,-23.713],[-7.654,-30.16],[-40.583,7.345],[0,0],[0,0],[60.732,-5.25],[-5.037,-45.856]],"v":[[73.241,-141.287],[69.161,-222.855],[-16.963,-229.093],[-94.757,-216.843],[-76.507,-0.5],[42.768,-11.75],[94.037,-80.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[5.509,15.037],[12.589,49.605],[49.213,-8.907],[0,0],[0,0],[-52.29,4.52],[5.084,46.282]],"o":[[-2.241,-23.713],[-7.654,-30.16],[-40.583,7.345],[0,0],[0,0],[60.732,-5.25],[-5.037,-45.856]],"v":[[73.241,-141.287],[69.161,-222.855],[-16.963,-229.093],[-94.757,-216.843],[-76.507,-0.5],[42.768,-11.75],[94.037,-80.894]],"c":true}],"e":[{"i":[[15.759,20.037],[2.089,16.855],[46.963,-6.157],[0,0],[0,0],[-32.059,1.901],[3.213,53.144]],"o":[[4.509,-21.713],[-5.161,-61.645],[-32.938,4.318],[0,0],[0,0],[63.232,-3.75],[-3.787,-41.856]],"v":[[68.491,-134.287],[76.911,-187.605],[-8.463,-229.843],[-91.507,-219.843],[-77.257,-0.75],[22.518,-7.25],[94.037,-72.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[15.759,20.037],[2.089,16.855],[46.963,-6.157],[0,0],[0,0],[-32.059,1.901],[3.213,53.144]],"o":[[4.509,-21.713],[-5.161,-61.645],[-32.938,4.318],[0,0],[0,0],[63.232,-3.75],[-3.787,-41.856]],"v":[[68.491,-134.287],[76.911,-187.605],[-8.463,-229.843],[-91.507,-219.843],[-77.257,-0.75],[22.518,-7.25],[94.037,-72.894]],"c":true}],"e":[{"i":[[20.259,14.037],[1.766,21.378],[30.713,-3.907],[0,0],[0,0],[-39.232,1.5],[3.134,56.753]],"o":[[11.509,-19.963],[-4.411,-53.395],[-38.914,4.95],[0,0],[0,0],[39.232,-1.5],[-1.787,-32.356]],"v":[[63.741,-128.537],[77.411,-184.105],[3.537,-230.593],[-88.507,-223.093],[-78.007,-1],[25.018,-5.5],[93.537,-70.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[20.259,14.037],[1.766,21.378],[30.713,-3.907],[0,0],[0,0],[-39.232,1.5],[3.134,56.753]],"o":[[11.509,-19.963],[-4.411,-53.395],[-38.914,4.95],[0,0],[0,0],[39.232,-1.5],[-1.787,-32.356]],"v":[[63.741,-128.537],[77.411,-184.105],[3.537,-230.593],[-88.507,-223.093],[-78.007,-1],[25.018,-5.5],[93.537,-70.894]],"c":true}],"e":[{"i":[[24.259,15.287],[1.029,21.401],[37.213,-2.157],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[12.683,-14.513],[-3.911,-45.645],[-21.287,1.093],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[61.241,-124.787],[78.661,-178.355],[4.787,-229.343],[-86.007,-223.593],[-78.007,-1],[18.768,-3.5],[92.287,-66.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[24.259,15.287],[1.029,21.401],[37.213,-2.157],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[12.683,-14.513],[-3.911,-45.645],[-21.287,1.093],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[61.241,-124.787],[78.661,-178.355],[4.787,-229.343],[-86.007,-223.593],[-78.007,-1],[18.768,-3.5],[92.287,-66.644]],"c":true}],"e":[{"i":[[22.009,10.287],[0,21.451],[39.713,-1.157],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[14.509,-10.713],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[58.491,-122.287],[79.161,-171.855],[11.037,-228.593],[-83.507,-224.093],[-78.007,-1],[21.268,-2.5],[92.037,-63.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[22.009,10.287],[0,21.451],[39.713,-1.157],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[14.509,-10.713],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[58.491,-122.287],[79.161,-171.855],[11.037,-228.593],[-83.507,-224.093],[-78.007,-1],[21.268,-2.5],[92.037,-63.394]],"c":true}],"e":[{"i":[[21.149,8.762],[0.374,21.444],[35.338,-1.657],[0,0],[0,0],[0,0],[-0.227,41.088]],"o":[[15.106,-9.064],[-0.705,-39.533],[0,0],[0,0],[0,0],[43.478,0.75],[0.16,-28.893]],"v":[[56.741,-121.162],[79.411,-167.48],[13.912,-227.343],[-81.507,-224.968],[-78.382,-0.75],[19.518,-1.25],[91.287,-62.019]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":21.376,"s":[{"i":[[21.149,8.762],[0.374,21.444],[35.338,-1.657],[0,0],[0,0],[0,0],[-0.227,41.088]],"o":[[15.106,-9.064],[-0.705,-39.533],[0,0],[0,0],[0,0],[43.478,0.75],[0.16,-28.893]],"v":[[56.741,-121.162],[79.411,-167.48],[13.912,-227.343],[-81.507,-224.968],[-78.382,-0.75],[19.518,-1.25],[91.287,-62.019]],"c":true}],"e":[{"i":[[21.149,8.762],[0.749,21.438],[32.213,-0.907],[0,0],[0,0],[0,0],[-0.152,41.088]],"o":[[15.106,-9.064],[-1.411,-40.395],[0,0],[0,0],[0,0],[41.473,0.5],[0.106,-28.93]],"v":[[56.241,-119.787],[79.411,-168.105],[13.287,-226.593],[-79.507,-225.843],[-78.757,-0.5],[21.518,-0.5],[90.787,-62.894]],"c":true}]},{"t":22.2529296875}]},"nm":"B"},{"ind":1,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[-2.154,-14.341],[5.939,-1.064],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[3.862,-1.474]],"v":[[-268.103,-143.239],[-271.189,-115.936],[-276.731,-115.148],[-282.154,-159.988],[-277.112,-161.276]],"c":true}],"e":[{"i":[[-2.154,-14.341],[5.939,-1.064],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[3.862,-1.474]],"v":[[-36.103,-162.739],[-39.189,-135.436],[-44.731,-134.648],[-50.154,-179.488],[-45.112,-180.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[-2.154,-14.341],[5.939,-1.064],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[3.862,-1.474]],"v":[[-36.103,-162.739],[-39.189,-135.436],[-44.731,-134.648],[-50.154,-179.488],[-45.112,-180.776]],"c":true}],"e":[{"i":[[-2.154,-14.341],[7.939,-1.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[7.862,-1.724]],"v":[[-22.103,-167.239],[-29.189,-138.186],[-43.481,-135.648],[-49.154,-181.988],[-37.862,-184.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[-2.154,-14.341],[7.939,-1.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[7.862,-1.724]],"v":[[-22.103,-167.239],[-29.189,-138.186],[-43.481,-135.648],[-49.154,-181.988],[-37.862,-184.776]],"c":true}],"e":[{"i":[[-2.154,-14.341],[7.939,-1.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[10.862,0.026]],"v":[[-9.353,-166.739],[-20.189,-138.686],[-42.231,-135.648],[-46.654,-183.988],[-26.862,-187.276]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[-2.154,-14.341],[7.939,-1.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[10.862,0.026]],"v":[[-9.353,-166.739],[-20.189,-138.686],[-42.231,-135.648],[-46.654,-183.988],[-26.862,-187.276]],"c":true}],"e":[{"i":[[-1.634,-14.432],[15.643,-0.316],[0,0],[0,0],[0,0]],"o":[[1.499,13.233],[0,0],[0,0],[0,0],[12.13,-2.72]],"v":[[1.501,-165.483],[-19.143,-137.184],[-39.891,-135.152],[-43.647,-185.494],[-18.285,-187.276]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[-1.634,-14.432],[15.643,-0.316],[0,0],[0,0],[0,0]],"o":[[1.499,13.233],[0,0],[0,0],[0,0],[12.13,-2.72]],"v":[[1.501,-165.483],[-19.143,-137.184],[-39.891,-135.152],[-43.647,-185.494],[-18.285,-187.276]],"c":true}],"e":[{"i":[[-2.154,-14.341],[17.439,-2.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[13.612,-1.724]],"v":[[10.147,-167.739],[-8.939,-136.936],[-38.481,-134.898],[-40.154,-185.488],[-12.862,-186.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[-2.154,-14.341],[17.439,-2.814],[0,0],[0,0],[0,0]],"o":[[2.109,14.042],[0,0],[0,0],[0,0],[13.612,-1.724]],"v":[[10.147,-167.739],[-8.939,-136.936],[-38.481,-134.898],[-40.154,-185.488],[-12.862,-186.776]],"c":true}],"e":[{"i":[[-1.534,-14.395],[14.273,-2.231],[0,0],[0,0],[0,0]],"o":[[1.502,14.095],[0,0],[0,0],[0,0],[14.778,1.383]],"v":[[17.998,-164.132],[-2.523,-136.269],[-36.047,-134.41],[-37.912,-185.525],[-5.028,-187.133]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[-1.534,-14.395],[14.273,-2.231],[0,0],[0,0],[0,0]],"o":[[1.502,14.095],[0,0],[0,0],[0,0],[14.778,1.383]],"v":[[17.998,-164.132],[-2.523,-136.269],[-36.047,-134.41],[-37.912,-185.525],[-5.028,-187.133]],"c":true}],"e":[{"i":[[-0.767,-14.448],[16.041,-0.938],[0,0],[0,0],[0,0]],"o":[[0.751,14.147],[0,0],[0,0],[0,0],[14.678,-0.575]],"v":[[23.139,-163.775],[0.762,-135.102],[-34.862,-134.172],[-35.92,-185.061],[-3.241,-186.241]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[-0.767,-14.448],[16.041,-0.938],[0,0],[0,0],[0,0]],"o":[[0.751,14.147],[0,0],[0,0],[0,0],[14.678,-0.575]],"v":[[23.139,-163.775],[0.762,-135.102],[-34.862,-134.172],[-35.92,-185.061],[-3.241,-186.241]],"c":true}],"e":[{"i":[[0,-14.502],[14.502,0],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[27.029,-160.917],[4.297,-134.435],[-33.677,-133.685],[-33.677,-184.598],[2.797,-185.348]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":21.376,"s":[{"i":[[0,-14.502],[14.502,0],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[27.029,-160.917],[4.297,-134.435],[-33.677,-133.685],[-33.677,-184.598],[2.797,-185.348]],"c":true}],"e":[{"i":[[0,-14.502],[16.037,-0.065],[0,0],[0,0],[0,0]],"o":[[0,14.2],[0,0],[0,0],[0,0],[14.502,0]],"v":[[30.363,-160.417],[4.963,-133.935],[-32.927,-133.268],[-32.927,-184.014],[3.297,-184.848]],"c":true}]},{"t":22.2529296875}]},"nm":"B"},{"ind":2,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[7.181,0.245],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[5.681,-0.592],[2.589,13.962]],"v":[[-272.931,-26.995],[-280.427,-25.995],[-286.427,-75.158],[-279.181,-76.658],[-269.094,-56.926]],"c":true}],"e":[{"i":[[7.181,0.245],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[5.681,-0.592],[2.589,13.962]],"v":[[-25.931,-49.995],[-33.427,-48.995],[-39.427,-98.158],[-32.181,-99.658],[-22.094,-79.926]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[7.181,0.245],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[5.681,-0.592],[2.589,13.962]],"v":[[-25.931,-49.995],[-33.427,-48.995],[-39.427,-98.158],[-32.181,-99.658],[-22.094,-79.926]],"c":true}],"e":[{"i":[[13.431,-1.505],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[7.681,-0.842],[2.589,13.962]],"v":[[-17.681,-50.245],[-33.677,-48.495],[-39.427,-98.158],[-22.431,-100.908],[-7.094,-82.426]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[13.431,-1.505],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[7.681,-0.842],[2.589,13.962]],"v":[[-17.681,-50.245],[-33.677,-48.495],[-39.427,-98.158],[-22.431,-100.908],[-7.094,-82.426]],"c":true}],"e":[{"i":[[13.431,-1.505],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[11.931,0.158],[2.589,13.962]],"v":[[-9.181,-49.745],[-33.677,-47.745],[-37.927,-97.908],[-12.681,-100.908],[5.906,-78.676]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[13.431,-1.505],[0,0],[0,0],[0,0],[-2.656,-14.324]],"o":[[0,0],[0,0],[0,0],[11.931,0.158],[2.589,13.962]],"v":[[-9.181,-49.745],[-33.677,-47.745],[-37.927,-97.908],[-12.681,-100.908],[5.906,-78.676]],"c":true}],"e":[{"i":[[14.118,-0.752],[0,0],[0,0],[0,0],[-1.406,-16.074]],"o":[[0,0],[0,0],[0,0],[11.868,-1.421],[1.237,14.146]],"v":[[-2.681,-47.745],[-34.177,-45.745],[-36.927,-96.908],[-7.181,-99.908],[16.656,-76.926]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[14.118,-0.752],[0,0],[0,0],[0,0],[-1.406,-16.074]],"o":[[0,0],[0,0],[0,0],[11.868,-1.421],[1.237,14.146]],"v":[[-2.681,-47.745],[-34.177,-45.745],[-36.927,-96.908],[-7.181,-99.908],[16.656,-76.926]],"c":true}],"e":[{"i":[[12.181,0.495],[0,0],[0,0],[0,0],[-0.906,-12.324]],"o":[[0,0],[0,0],[0,0],[14.804,0],[1.041,14.162]],"v":[[3.069,-47.245],[-32.927,-45.495],[-35.677,-96.408],[1.819,-98.408],[24.906,-73.926]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[12.181,0.495],[0,0],[0,0],[0,0],[-0.906,-12.324]],"o":[[0,0],[0,0],[0,0],[14.804,0],[1.041,14.162]],"v":[[3.069,-47.245],[-32.927,-45.495],[-35.677,-96.408],[1.819,-98.408],[24.906,-73.926]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-0.264,-12.571]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0.261,12.426]],"v":[[8.652,-45.745],[-33.344,-44.412],[-34.844,-95.658],[6.902,-97.325],[31.239,-71.926]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-0.264,-12.571]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0.261,12.426]],"v":[[8.652,-45.745],[-33.344,-44.412],[-34.844,-95.658],[6.902,-97.325],[31.239,-71.926]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-0.885,-14.241]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0.863,14.121]],"v":[[10.735,-44.495],[-33.261,-43.579],[-33.761,-94.908],[11.235,-95.742],[35.822,-70.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[-0.885,-14.241]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0.863,14.121]],"v":[[10.735,-44.495],[-33.261,-43.579],[-33.761,-94.908],[11.235,-95.742],[35.822,-70.176]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[12.681,0.408],[0,14.2]],"v":[[14.319,-43.995],[-32.677,-43.245],[-33.177,-93.658],[16.569,-94.658],[39.156,-68.176]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":21.376,"s":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[12.681,0.408],[0,14.2]],"v":[[14.319,-43.995],[-32.677,-43.245],[-33.177,-93.658],[16.569,-94.658],[39.156,-68.176]],"c":true}],"e":[{"i":[[14.804,0],[0,0],[0,0],[0,0],[0,-14.2]],"o":[[0,0],[0,0],[0,0],[14.804,0],[0,14.2]],"v":[[14.819,-42.745],[-32.677,-41.995],[-32.677,-93.658],[16.069,-94.158],[41.406,-67.926]],"c":true}]},{"t":22.2529296875}]},"nm":"B"},{"ty":"mm","mm":1,"nm":"Merge Paths 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0.15,0.8,0.55,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":15,"op":23,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":29,"ty":4,"nm":"B light blue layer 3","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[-0.201,21.411],[-0.286,15.23],[2.474,0.203],[0,0],[0,0],[-1.893,-0.375],[-0.662,-1.856]],"o":[[0.134,-14.338],[0.036,-1.896],[-1.912,-0.157],[0,0],[0,0],[0.644,0.128],[1.338,-20.856]],"v":[[-46.509,-50.162],[-44.214,-98.23],[-45.963,-101.593],[-49.132,-100.593],[-55.007,4.75],[-50.857,4.25],[-48.588,6.856]],"c":true}],"e":[{"i":[[0.279,24.912],[0.464,22.605],[2.088,1.343],[0,0],[0,0],[-4.143,-2],[-0.787,-3.981]],"o":[[-0.241,-21.463],[-0.098,-4.771],[-4.242,-2.728],[0,0],[0,0],[3.51,1.694],[-0.037,-19.356]],"v":[[-40.884,-52.662],[-41.839,-108.48],[-44.463,-118.593],[-53.507,-120.718],[-54.757,3.5],[-45.357,4.875],[-39.963,12.856]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[0.279,24.912],[0.464,22.605],[2.088,1.343],[0,0],[0,0],[-4.143,-2],[-0.787,-3.981]],"o":[[-0.241,-21.463],[-0.098,-4.771],[-4.242,-2.728],[0,0],[0,0],[3.51,1.694],[-0.037,-19.356]],"v":[[-40.884,-52.662],[-41.839,-108.48],[-44.463,-118.593],[-53.507,-120.718],[-54.757,3.5],[-45.357,4.875],[-39.963,12.856]],"c":true}],"e":[{"i":[[2.509,24.787],[1.952,19.983],[11.463,8.593],[0,0],[0,0],[-8.268,-3.75],[-3.537,-7.856]],"o":[[-1.87,-18.476],[-0.161,-1.645],[-7.865,-5.896],[0,0],[0,0],[10.005,4.538],[-1.787,-20.356]],"v":[[-22.759,-58.037],[-27.839,-117.855],[-38.713,-134.843],[-60.257,-138.093],[-58.257,2.5],[-32.482,4.25],[-15.963,19.356]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":8,"s":[{"i":[[2.509,24.787],[1.952,19.983],[11.463,8.593],[0,0],[0,0],[-8.268,-3.75],[-3.537,-7.856]],"o":[[-1.87,-18.476],[-0.161,-1.645],[-7.865,-5.896],[0,0],[0,0],[10.005,4.538],[-1.787,-20.356]],"v":[[-22.759,-58.037],[-27.839,-117.855],[-38.713,-134.843],[-60.257,-138.093],[-58.257,2.5],[-32.482,4.25],[-15.963,19.356]],"c":true}],"e":[{"i":[[2.509,23.537],[1.952,19.983],[37.213,10.843],[0,0],[0,0],[-37.012,-3.318],[3.87,36.773]],"o":[[-1.969,-18.466],[-0.161,-1.645],[-39.595,-11.537],[0,0],[0,0],[52.982,4.75],[-2.037,-19.356]],"v":[[46.741,-67.787],[41.411,-115.355],[-2.713,-150.593],[-68.757,-153.593],[-61.507,2.25],[4.268,0.25],[56.037,7.356]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[{"i":[[2.509,23.537],[1.952,19.983],[37.213,10.843],[0,0],[0,0],[-37.012,-3.318],[3.87,36.773]],"o":[[-1.969,-18.466],[-0.161,-1.645],[-39.595,-11.537],[0,0],[0,0],[52.982,4.75],[-2.037,-19.356]],"v":[[46.741,-67.787],[41.411,-115.355],[-2.713,-150.593],[-68.757,-153.593],[-61.507,2.25],[4.268,0.25],[56.037,7.356]],"c":true}],"e":[{"i":[[5.869,20.677],[5.339,19.355],[49.963,-14.657],[0,0],[0,0],[-48.018,8.5],[14.463,52.894]],"o":[[-5.241,-18.463],[-6.854,-24.846],[-39.574,11.609],[0,0],[0,0],[56.175,-9.944],[-11.083,-40.532]],"v":[[79.741,-141.537],[64.661,-196.855],[-12.963,-182.843],[-76.257,-168.093],[-64.507,2],[54.518,-20],[98.287,-79.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[{"i":[[5.869,20.677],[5.339,19.355],[49.963,-14.657],[0,0],[0,0],[-48.018,8.5],[14.463,52.894]],"o":[[-5.241,-18.463],[-6.854,-24.846],[-39.574,11.609],[0,0],[0,0],[56.175,-9.944],[-11.083,-40.532]],"v":[[79.741,-141.537],[64.661,-196.855],[-12.963,-182.843],[-76.257,-168.093],[-64.507,2],[54.518,-20],[98.287,-79.394]],"c":true}],"e":[{"i":[[7.415,20.175],[10.589,29.855],[50.213,-22.407],[0,0],[0,0],[-48.907,15.377],[14.389,46.029]],"o":[[-8.991,-24.463],[-8.616,-24.291],[-37.662,16.806],[0,0],[0,0],[43.732,-13.75],[-12.537,-40.106]],"v":[[75.491,-152.537],[45.911,-232.355],[-25.463,-197.593],[-83.007,-177.843],[-66.757,2],[45.518,-26.75],[98.787,-83.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":11,"s":[{"i":[[7.415,20.175],[10.589,29.855],[50.213,-22.407],[0,0],[0,0],[-48.907,15.377],[14.389,46.029]],"o":[[-8.991,-24.463],[-8.616,-24.291],[-37.662,16.806],[0,0],[0,0],[43.732,-13.75],[-12.537,-40.106]],"v":[[75.491,-152.537],[45.911,-232.355],[-25.463,-197.593],[-83.007,-177.843],[-66.757,2],[45.518,-26.75],[98.787,-83.894]],"c":true}],"e":[{"i":[[6.732,20.413],[9.339,28.855],[50.213,-22.407],[0,0],[0,0],[-49.459,13.494],[14.043,47.445]],"o":[[-7.491,-22.713],[-7.937,-24.521],[-37.662,16.806],[0,0],[0,0],[43.982,-12],[-12.537,-42.356]],"v":[[73.491,-153.537],[43.411,-239.855],[-29.463,-206.343],[-89.007,-186.343],[-68.507,1.5],[45.518,-26.75],[96.287,-84.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[{"i":[[6.732,20.413],[9.339,28.855],[50.213,-22.407],[0,0],[0,0],[-49.459,13.494],[14.043,47.445]],"o":[[-7.491,-22.713],[-7.937,-24.521],[-37.662,16.806],[0,0],[0,0],[43.982,-12],[-12.537,-42.356]],"v":[[73.491,-153.537],[43.411,-239.855],[-29.463,-206.343],[-89.007,-186.343],[-68.507,1.5],[45.518,-26.75],[96.287,-84.644]],"c":true}],"e":[{"i":[[5.741,20.713],[9.339,28.855],[60.463,-27.657],[0,0],[0,0],[-49.459,13.494],[12.963,47.644]],"o":[[-5.741,-20.713],[-7.937,-24.521],[-37.505,17.155],[0,0],[0,0],[43.982,-12],[-11.586,-42.584]],"v":[[73.741,-153.787],[45.411,-241.855],[-29.713,-213.843],[-93.257,-193.343],[-71.257,0.5],[49.768,-26.5],[95.287,-81.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[{"i":[[5.741,20.713],[9.339,28.855],[60.463,-27.657],[0,0],[0,0],[-49.459,13.494],[12.963,47.644]],"o":[[-5.741,-20.713],[-7.937,-24.521],[-37.505,17.155],[0,0],[0,0],[43.982,-12],[-11.586,-42.584]],"v":[[73.741,-153.787],[45.411,-241.855],[-29.713,-213.843],[-93.257,-193.343],[-71.257,0.5],[49.768,-26.5],[95.287,-81.894]],"c":true}],"e":[{"i":[[5.741,20.713],[7.839,29.855],[63.713,-28.907],[0,0],[0,0],[-50.018,11.25],[12.963,53.394]],"o":[[-5.741,-20.713],[-6.546,-24.929],[-37.557,17.04],[0,0],[0,0],[46.058,-10.359],[-10.749,-44.276]],"v":[[75.741,-154.037],[52.411,-240.355],[-22.963,-222.593],[-96.007,-200.343],[-72.507,1.25],[48.768,-23.5],[95.037,-81.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[{"i":[[5.741,20.713],[7.839,29.855],[63.713,-28.907],[0,0],[0,0],[-50.018,11.25],[12.963,53.394]],"o":[[-5.741,-20.713],[-6.546,-24.929],[-37.557,17.04],[0,0],[0,0],[46.058,-10.359],[-10.749,-44.276]],"v":[[75.741,-154.037],[52.411,-240.355],[-22.963,-222.593],[-96.007,-200.343],[-72.507,1.25],[48.768,-23.5],[95.037,-81.894]],"c":true}],"e":[{"i":[[4.259,17.037],[7.839,29.855],[64.463,-23.407],[0,0],[0,0],[-49.518,7.25],[10.523,50.543]],"o":[[-5.991,-25.463],[-6.546,-24.929],[-38.765,14.076],[0,0],[0,0],[48.964,-7.169],[-9.287,-44.606]],"v":[[76.491,-154.787],[58.161,-239.105],[-23.213,-225.593],[-97.507,-206.343],[-74.007,1.25],[52.268,-20],[94.787,-82.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[{"i":[[4.259,17.037],[7.839,29.855],[64.463,-23.407],[0,0],[0,0],[-49.518,7.25],[10.523,50.543]],"o":[[-5.991,-25.463],[-6.546,-24.929],[-38.765,14.076],[0,0],[0,0],[48.964,-7.169],[-9.287,-44.606]],"v":[[76.491,-154.787],[58.161,-239.105],[-23.213,-225.593],[-97.507,-206.343],[-74.007,1.25],[52.268,-20],[94.787,-82.394]],"c":true}],"e":[{"i":[[4.009,18.287],[9.089,30.105],[63.963,-18.157],[0,0],[0,0],[-52.207,5.395],[4.963,38.144]],"o":[[-1.741,-23.713],[-7.236,-23.967],[-39.674,11.262],[0,0],[0,0],[67.732,-7],[-5.852,-44.98]],"v":[[74.491,-158.287],[64.411,-233.605],[-18.713,-229.093],[-96.757,-211.843],[-75.507,0.25],[45.268,-14.75],[94.287,-79.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16,"s":[{"i":[[4.009,18.287],[9.089,30.105],[63.963,-18.157],[0,0],[0,0],[-52.207,5.395],[4.963,38.144]],"o":[[-1.741,-23.713],[-7.236,-23.967],[-39.674,11.262],[0,0],[0,0],[67.732,-7],[-5.852,-44.98]],"v":[[74.491,-158.287],[64.411,-233.605],[-18.713,-229.093],[-96.757,-211.843],[-75.507,0.25],[45.268,-14.75],[94.287,-79.394]],"c":true}],"e":[{"i":[[5.509,15.037],[12.589,49.605],[49.213,-8.907],[0,0],[0,0],[-52.29,4.52],[5.084,46.282]],"o":[[-2.241,-23.713],[-7.654,-30.16],[-40.583,7.345],[0,0],[0,0],[60.732,-5.25],[-5.037,-45.856]],"v":[[73.241,-141.287],[69.161,-222.855],[-16.963,-229.093],[-94.757,-216.843],[-76.507,-0.5],[42.768,-11.75],[94.037,-80.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":16.963,"s":[{"i":[[5.509,15.037],[12.589,49.605],[49.213,-8.907],[0,0],[0,0],[-52.29,4.52],[5.084,46.282]],"o":[[-2.241,-23.713],[-7.654,-30.16],[-40.583,7.345],[0,0],[0,0],[60.732,-5.25],[-5.037,-45.856]],"v":[[73.241,-141.287],[69.161,-222.855],[-16.963,-229.093],[-94.757,-216.843],[-76.507,-0.5],[42.768,-11.75],[94.037,-80.894]],"c":true}],"e":[{"i":[[15.759,20.037],[2.089,16.855],[46.963,-6.157],[0,0],[0,0],[-32.059,1.901],[3.213,53.144]],"o":[[4.509,-21.713],[-5.161,-61.645],[-32.938,4.318],[0,0],[0,0],[63.232,-3.75],[-3.787,-41.856]],"v":[[68.491,-134.287],[76.911,-187.605],[-8.463,-229.843],[-91.507,-219.843],[-77.257,-0.75],[22.518,-7.25],[94.037,-72.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17.93,"s":[{"i":[[15.759,20.037],[2.089,16.855],[46.963,-6.157],[0,0],[0,0],[-32.059,1.901],[3.213,53.144]],"o":[[4.509,-21.713],[-5.161,-61.645],[-32.938,4.318],[0,0],[0,0],[63.232,-3.75],[-3.787,-41.856]],"v":[[68.491,-134.287],[76.911,-187.605],[-8.463,-229.843],[-91.507,-219.843],[-77.257,-0.75],[22.518,-7.25],[94.037,-72.894]],"c":true}],"e":[{"i":[[20.259,14.037],[1.766,21.378],[30.713,-3.907],[0,0],[0,0],[-39.232,1.5],[3.134,56.753]],"o":[[11.509,-19.963],[-4.411,-53.395],[-38.914,4.95],[0,0],[0,0],[39.232,-1.5],[-1.787,-32.356]],"v":[[63.741,-128.537],[77.411,-184.105],[3.537,-230.593],[-88.507,-223.093],[-78.007,-1],[25.018,-5.5],[93.537,-70.894]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18.756,"s":[{"i":[[20.259,14.037],[1.766,21.378],[30.713,-3.907],[0,0],[0,0],[-39.232,1.5],[3.134,56.753]],"o":[[11.509,-19.963],[-4.411,-53.395],[-38.914,4.95],[0,0],[0,0],[39.232,-1.5],[-1.787,-32.356]],"v":[[63.741,-128.537],[77.411,-184.105],[3.537,-230.593],[-88.507,-223.093],[-78.007,-1],[25.018,-5.5],[93.537,-70.894]],"c":true}],"e":[{"i":[[24.259,15.287],[1.029,21.401],[37.213,-2.157],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[12.683,-14.513],[-3.911,-45.645],[-21.287,1.093],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[61.241,-124.787],[78.661,-178.355],[4.787,-229.343],[-86.007,-223.593],[-78.007,-1],[18.768,-3.5],[92.287,-66.644]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.63,"s":[{"i":[[24.259,15.287],[1.029,21.401],[37.213,-2.157],[0,0],[0,0],[0,0],[2.205,48.866]],"o":[[12.683,-14.513],[-3.911,-45.645],[-21.287,1.093],[0,0],[0,0],[45.482,1],[-1.09,-28.807]],"v":[[61.241,-124.787],[78.661,-178.355],[4.787,-229.343],[-86.007,-223.593],[-78.007,-1],[18.768,-3.5],[92.287,-66.644]],"c":true}],"e":[{"i":[[22.009,10.287],[0,21.451],[39.713,-1.157],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[14.509,-10.713],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[58.491,-122.287],[79.161,-171.855],[11.037,-228.593],[-83.507,-224.093],[-78.007,-1],[21.268,-2.5],[92.037,-63.394]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20.505,"s":[{"i":[[22.009,10.287],[0,21.451],[39.713,-1.157],[0,0],[0,0],[0,0],[-0.303,41.088]],"o":[[14.509,-10.713],[0,-38.672],[0,0],[0,0],[0,0],[45.482,1],[0.213,-28.856]],"v":[[58.491,-122.287],[79.161,-171.855],[11.037,-228.593],[-83.507,-224.093],[-78.007,-1],[21.268,-2.5],[92.037,-63.394]],"c":true}],"e":[{"i":[[21.149,8.762],[0.374,21.444],[35.338,-1.657],[0,0],[0,0],[0,0],[-0.227,41.088]],"o":[[15.106,-9.064],[-0.705,-39.533],[0,0],[0,0],[0,0],[43.478,0.75],[0.16,-28.893]],"v":[[56.741,-121.162],[79.411,-167.48],[13.912,-227.343],[-81.507,-224.968],[-78.382,-0.75],[19.518,-1.25],[91.287,-62.019]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":21.376,"s":[{"i":[[21.149,8.762],[0.374,21.444],[35.338,-1.657],[0,0],[0,0],[0,0],[-0.227,41.088]],"o":[[15.106,-9.064],[-0.705,-39.533],[0,0],[0,0],[0,0],[43.478,0.75],[0.16,-28.893]],"v":[[56.741,-121.162],[79.411,-167.48],[13.912,-227.343],[-81.507,-224.968],[-78.382,-0.75],[19.518,-1.25],[91.287,-62.019]],"c":true}],"e":[{"i":[[21.149,8.762],[0.749,21.438],[32.213,-0.907],[0,0],[0,0],[0,0],[-0.152,41.088]],"o":[[15.106,-9.064],[-1.411,-40.395],[0,0],[0,0],[0,0],[41.473,0.5],[0.106,-28.93]],"v":[[56.241,-119.787],[79.411,-168.105],[13.287,-226.593],[-79.507,-225.843],[-78.757,-0.5],[21.518,-0.5],[90.787,-62.894]],"c":true}]},{"t":22.2529296875}]},"nm":"B"},{"ty":"fl","fillEnabled":true,"c":{"k":[0.15,0.8,0.55,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":6,"op":15,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":30,"ty":4,"nm":"B light blue layer 2","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":{"i":[[-0.899,27.404],[-1.161,21.73],[-2.043,1.082],[0,0],[0,0],[1.78,-1.142],[1.963,-3.606]],"o":[[0.634,-19.338],[0.161,-3.017],[1.713,-0.907],[0,0],[0,0],[-2.143,1.375],[-0.037,-20.106]],"v":[[-64.884,-51.412],[-61.589,-110.48],[-56.963,-116.843],[-53.507,-117.468],[-55.257,2.75],[-61.857,5.625],[-66.463,11.356]],"c":true}},"nm":"B"},{"ty":"mm","mm":1,"nm":"Merge Paths 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0.15,0.8,0.55,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":7,"op":8,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":31,"ty":4,"nm":"B light blue layer 1","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[0,0],[2.067,0.035],[0.272,0.321],[0.13,-0.779],[-0.356,-1.134],[-1.495,0.021],[0.012,0.775],[0.067,1.41]],"o":[[0,0],[-0.681,-0.011],[-0.197,0.927],[-0.219,1.314],[0.201,0.642],[2.094,-0.03],[0.082,-1.953],[-0.078,-1.64]],"v":[[-48.938,-1.125],[-52.016,0.387],[-54.014,-0.086],[-54.792,2.812],[-54.769,6.713],[-51.876,8.407],[-48.457,5.952],[-48.648,1.19]],"c":true}],"e":[{"i":[[0,0],[8.009,0.121],[1.054,1.117],[0.503,-2.712],[-1.38,-3.952],[-5.796,0.074],[0.046,2.698],[0.26,4.913]],"o":[[0,0],[-2.637,-0.04],[-0.765,3.23],[-0.848,4.577],[0.781,2.235],[8.117,-0.104],[0.318,-6.802],[-0.303,-5.714]],"v":[[-41.806,-11.476],[-53.736,-6.21],[-62.84,-11.105],[-65.858,1.238],[-65.495,16.577],[-54.51,22.479],[-39.943,14.677],[-40.685,-3.661]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[0,0],[8.009,0.121],[1.054,1.117],[0.503,-2.712],[-1.38,-3.952],[-5.796,0.074],[0.046,2.698],[0.26,4.913]],"o":[[0,0],[-2.637,-0.04],[-0.765,3.23],[-0.848,4.577],[0.781,2.235],[8.117,-0.104],[0.318,-6.802],[-0.303,-5.714]],"v":[[-41.806,-11.476],[-53.736,-6.21],[-62.84,-11.105],[-65.858,1.238],[-65.495,16.577],[-54.51,22.479],[-39.943,14.677],[-40.685,-3.661]],"c":true}],"e":[{"i":[[0,0],[18.114,0.262],[4.357,-2.3],[0.602,-17.382],[-1.273,-6.802],[-19.282,0.006],[0.47,7.998],[1.175,10.637]],"o":[[0,0],[-5.965,-0.086],[-1.14,7.324],[-0.349,10.097],[0.939,5.019],[22.347,-0.007],[-0.749,-8.21],[-1.405,-12.724]],"v":[[-32.13,-94.796],[-52.413,-95.468],[-69.857,-95.45],[-70.102,-37.118],[-69.977,24.12],[-51.218,42.744],[-15.72,20.252],[-22.595,-20.776]],"c":true}]},{"t":8}]},"nm":"B"},{"ty":"mm","mm":3,"nm":"Merge Paths 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0.15,0.8,0.55,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":6,"op":9,"st":2,"bm":0,"sr":1},{"ddd":0,"ind":32,"ty":4,"nm":"B white layer 1","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[0,0],[2.067,0.035],[0.272,0.321],[0.13,-0.779],[-0.356,-1.134],[-1.495,0.021],[0.012,0.775],[0.067,1.41]],"o":[[0,0],[-0.681,-0.011],[-0.197,0.927],[-0.219,1.314],[0.201,0.642],[2.094,-0.03],[0.082,-1.953],[-0.078,-1.64]],"v":[[-45.188,-1.125],[-48.266,0.387],[-50.264,-0.086],[-51.042,2.812],[-51.019,6.713],[-48.126,8.407],[-44.707,5.952],[-44.898,1.19]],"c":true}],"e":[{"i":[[0,0],[7.348,0.121],[0.967,1.117],[0.461,-2.712],[-1.266,-3.952],[-5.317,0.074],[0.042,2.698],[0.239,4.913]],"o":[[0,0],[-2.42,-0.04],[-0.702,3.23],[-0.778,4.577],[0.716,2.235],[7.447,-0.104],[0.292,-6.802],[-0.278,-5.714]],"v":[[-36.501,-11.351],[-47.446,-6.085],[-55.798,-10.98],[-58.567,1.363],[-58.234,15.952],[-47.697,21.854],[-34.792,13.302],[-35.472,-3.536]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[0,0],[7.348,0.121],[0.967,1.117],[0.461,-2.712],[-1.266,-3.952],[-5.317,0.074],[0.042,2.698],[0.239,4.913]],"o":[[0,0],[-2.42,-0.04],[-0.702,3.23],[-0.778,4.577],[0.716,2.235],[7.447,-0.104],[0.292,-6.802],[-0.278,-5.714]],"v":[[-36.501,-11.351],[-47.446,-6.085],[-55.798,-10.98],[-58.567,1.363],[-58.234,15.952],[-47.697,21.854],[-34.792,13.302],[-35.472,-3.536]],"c":true}],"e":[{"i":[[0,0],[18.114,0.262],[2.384,2.42],[1.102,-14.882],[-1.273,-6.802],[-19.282,0.006],[0.47,7.998],[1.175,10.637]],"o":[[0,0],[-5.965,-0.086],[-1.14,7.324],[-0.746,10.075],[0.939,5.019],[22.347,-0.007],[-0.749,-8.211],[-1.405,-12.724]],"v":[[-20.38,-97.546],[-49.163,-91.968],[-61.857,-100.7],[-65.602,-41.118],[-67.977,23.12],[-40.968,39.244],[-11.22,17.752],[-14.595,-21.526]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[0,0],[18.114,0.262],[2.384,2.42],[1.102,-14.882],[-1.273,-6.802],[-19.282,0.006],[0.47,7.998],[1.175,10.637]],"o":[[0,0],[-5.965,-0.086],[-1.14,7.324],[-0.746,10.075],[0.939,5.019],[22.347,-0.007],[-0.749,-8.211],[-1.405,-12.724]],"v":[[-20.38,-97.546],[-49.163,-91.968],[-61.857,-100.7],[-65.602,-41.118],[-67.977,23.12],[-40.968,39.244],[-11.22,17.752],[-14.595,-21.526]],"c":true}],"e":[{"i":[[0,0],[5.836,-0.538],[0.9,1.062],[-1.035,-10.477],[-0.481,-2.985],[-4.731,0.661],[-0.012,3.839],[1.64,5.168]],"o":[[0,0],[-2.242,0.207],[0.323,6.451],[0.436,4.412],[0.355,2.203],[8.337,-1.165],[-0.012,-6.693],[-1.375,-4.332]],"v":[[33.321,-6.126],[25.664,-9.212],[19,-11.513],[22.285,15.477],[25.087,31.97],[31.697,39.589],[41.03,25.38],[38.496,8.259]],"c":true}]},{"t":8}]},"nm":"B"},{"ind":1,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[-0.007,0.279],[0.272,0.321],[0.079,-0.813],[-1.102,-0.004],[0.001,1.067]],"o":[[-0.362,-0.011],[-0.13,0.97],[-0.13,1.326],[0.541,0.002],[-0.002,-1.19]],"v":[[-254.943,4.83],[-255.945,4.464],[-256.48,7.854],[-255.801,10.105],[-255.008,8.796]],"c":true}],"e":[{"i":[[-0.025,0.973],[0.967,1.117],[0.283,-2.831],[-3.919,-0.012],[0.005,3.715]],"o":[[-1.288,-0.04],[-0.462,3.38],[-0.461,4.62],[1.925,0.006],[-0.006,-4.146]],"v":[[-49.268,-10.266],[-52.833,-11.541],[-54.733,0.268],[-52.319,8.11],[-49.502,3.548]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[-0.025,0.973],[0.967,1.117],[0.283,-2.831],[-3.919,-0.012],[0.005,3.715]],"o":[[-1.288,-0.04],[-0.462,3.38],[-0.461,4.62],[1.925,0.006],[-0.006,-4.146]],"v":[[-49.268,-10.266],[-52.833,-11.541],[-54.733,0.268],[-52.319,8.11],[-49.502,3.548]],"c":true}],"e":[{"i":[[-0.103,2.475],[4.052,2.841],[0.919,-10.247],[-10.422,0.015],[0.022,12.118]],"o":[[-5.362,-2.495],[-1.938,8.597],[-0.804,8.962],[8.067,-0.012],[-0.019,-10.546]],"v":[[-44.638,-94.255],[-60.829,-101],[-66.196,3.288],[-56.078,21.235],[-40.272,8.382]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[{"i":[[-0.103,2.475],[4.052,2.841],[0.919,-10.247],[-10.422,0.015],[0.022,12.118]],"o":[[-5.362,-2.495],[-1.938,8.597],[-0.804,8.962],[8.067,-0.012],[-0.019,-10.546]],"v":[[-44.638,-94.255],[-60.829,-101],[-66.196,3.288],[-56.078,21.235],[-40.272,8.382]],"c":true}],"e":[{"i":[[-0.061,2.109],[3.595,-2.75],[0.696,-6.135],[-9.66,-0.026],[0.013,8.051]],"o":[[-3.175,-0.086],[-1.14,7.324],[-1.137,10.012],[4.745,0.013],[-0.015,-8.985]],"v":[[-56.056,-36.486],[-75.595,-26.5],[-76.296,12.591],[-66.594,33.585],[-52.9,17.2]],"c":true}]},{"t":8}]},"nm":"B 2"},{"ty":"mm","mm":3,"nm":"Merge Paths 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0,0.32,0.5,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":5,"op":9,"st":1,"bm":0,"sr":1},{"ddd":0,"ind":33,"ty":4,"nm":"B orange layer 1","parent":34,"ks":{"o":{"k":100},"r":{"k":0},"p":{"k":[250,400,0]},"a":{"k":[0,0,0]},"s":{"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":4,"s":[{"i":[[0,0],[2.067,0.035],[0.272,0.321],[0.13,-0.779],[-0.356,-1.134],[-1.495,0.021],[0.012,0.775],[0.067,1.41]],"o":[[0,0],[-0.681,-0.011],[-0.197,0.927],[-0.219,1.314],[0.201,0.642],[2.094,-0.03],[0.082,-1.953],[-0.078,-1.64]],"v":[[-40.938,-1],[-44.016,0.512],[-46.014,0.039],[-46.792,2.937],[-46.769,6.838],[-43.876,9.032],[-40.457,6.077],[-40.648,1.315]],"c":true}],"e":[{"i":[[0,0],[7.348,0.121],[0.967,1.117],[0.461,-2.712],[-1.266,-3.952],[-5.317,0.074],[0.042,2.698],[0.239,4.913]],"o":[[0,0],[-2.42,-0.04],[-0.702,3.23],[-0.778,4.577],[0.716,2.235],[7.447,-0.104],[0.292,-6.802],[-0.278,-5.714]],"v":[[-32.501,-11.351],[-43.446,-6.085],[-50.548,-7.73],[-53.317,2.363],[-53.734,15.952],[-42.697,22.604],[-30.292,13.552],[-31.472,-3.286]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[0,0],[7.348,0.121],[0.967,1.117],[0.461,-2.712],[-1.266,-3.952],[-5.317,0.074],[0.042,2.698],[0.239,4.913]],"o":[[0,0],[-2.42,-0.04],[-0.702,3.23],[-0.778,4.577],[0.716,2.235],[7.447,-0.104],[0.292,-6.802],[-0.278,-5.714]],"v":[[-32.501,-11.351],[-43.446,-6.085],[-50.548,-7.73],[-53.317,2.363],[-53.734,15.952],[-42.697,22.604],[-30.292,13.552],[-31.472,-3.286]],"c":true}],"e":[{"i":[[0,0],[18.114,0.262],[2.384,2.42],[1.602,-8.382],[-1.273,-6.802],[-16.121,0.205],[0.47,7.998],[2.014,10.511]],"o":[[0,0],[-5.965,-0.086],[-1.14,7.324],[-1.896,9.923],[0.939,5.019],[22.345,-0.285],[-0.749,-8.211],[-1.98,-10.335]],"v":[[-16.38,-29.046],[-39.663,-18.718],[-54.607,-25.2],[-57.602,-5.368],[-59.727,23.12],[-35.218,37.244],[-9.72,15.252],[-13.845,-14.276]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[0,0],[18.114,0.262],[2.384,2.42],[1.602,-8.382],[-1.273,-6.802],[-16.121,0.205],[0.47,7.998],[2.014,10.511]],"o":[[0,0],[-5.965,-0.086],[-1.14,7.324],[-1.896,9.923],[0.939,5.019],[22.345,-0.285],[-0.749,-8.211],[-1.98,-10.335]],"v":[[-16.38,-29.046],[-39.663,-18.718],[-54.607,-25.2],[-57.602,-5.368],[-59.727,23.12],[-35.218,37.244],[-9.72,15.252],[-13.845,-14.276]],"c":true}],"e":[{"i":[[0,0],[18.114,0.262],[2.384,2.42],[0.352,-20.882],[-1.273,-6.802],[-12.532,1.506],[-0.03,8.748],[4.345,11.776]],"o":[[0,0],[-5.965,-0.086],[0.857,14.7],[-0.17,10.101],[0.939,5.019],[22.085,-2.655],[-0.03,-15.252],[-3.642,-9.873]],"v":[[7.12,-52.546],[-13.163,-60.718],[-24.857,-45.45],[-24.102,2.382],[-21.977,37.12],[-4.468,50.494],[29.53,19.252],[22.155,-15.776]],"c":true}]},{"t":7}]},"nm":"B"},{"ind":1,"ty":"sh","ks":{"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":4,"s":[{"i":[[-0.007,0.279],[0.272,0.321],[0.079,-0.813],[-1.102,-0.004],[0.001,1.067]],"o":[[-0.362,-0.011],[-0.13,0.97],[-0.13,1.326],[0.541,0.002],[-0.002,-1.19]],"v":[[-254.943,4.83],[-255.945,4.464],[-256.48,7.854],[-255.801,10.105],[-255.008,8.796]],"c":true}],"e":[{"i":[[-0.025,0.973],[0.967,1.117],[0.283,-2.831],[-3.919,-0.012],[0.005,3.715]],"o":[[-1.288,-0.04],[-0.462,3.38],[-0.461,4.62],[1.925,0.006],[-0.006,-4.146]],"v":[[-44.518,-9.766],[-48.083,-11.041],[-49.983,0.768],[-47.319,8.11],[-44.752,2.798]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[-0.025,0.973],[0.967,1.117],[0.283,-2.831],[-3.919,-0.012],[0.005,3.715]],"o":[[-1.288,-0.04],[-0.462,3.38],[-0.461,4.62],[1.925,0.006],[-0.006,-4.146]],"v":[[-44.518,-9.766],[-48.083,-11.041],[-49.983,0.768],[-47.319,8.11],[-44.752,2.798]],"c":true}],"e":[{"i":[[-0.103,2.475],[4.052,2.841],[1.184,-7.201],[-11.422,1.515],[0.022,9.45]],"o":[[-5.398,-0.101],[-1.938,8.597],[-1.932,11.751],[7.997,-1.061],[-0.025,-10.546]],"v":[[-35.638,-26.505],[-50.579,-29.75],[-56.696,0.288],[-45.828,20.985],[-35.272,6.382]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[{"i":[[-0.103,2.475],[4.052,2.841],[1.184,-7.201],[-11.422,1.515],[0.022,9.45]],"o":[[-5.398,-0.101],[-1.938,8.597],[-1.932,11.751],[7.997,-1.061],[-0.025,-10.546]],"v":[[-35.638,-26.505],[-50.579,-29.75],[-56.696,0.288],[-45.828,20.985],[-35.272,6.382]],"c":true}],"e":[{"i":[[-0.061,2.109],[3.595,-2.75],[0.696,-6.135],[-9.66,-0.026],[0.013,8.051]],"o":[[-3.175,-0.086],[-1.14,7.324],[-1.137,10.012],[4.745,0.013],[-0.015,-8.985]],"v":[[-15.306,-36.486],[-34.095,-25],[-34.796,14.091],[-25.094,35.085],[-12.4,18.95]],"c":true}]},{"t":7}]},"nm":"B 2"},{"ty":"mm","mm":3,"nm":"Merge Paths 1"},{"ty":"fl","fillEnabled":true,"c":{"k":[0.86,0.86,0.86,1]},"o":{"k":100},"nm":"Fill 1"},{"ty":"tr","p":{"k":[0,0],"ix":2},"a":{"k":[0,0],"ix":1},"s":{"k":[100,100],"ix":3},"r":{"k":0,"ix":6},"o":{"k":100,"ix":7},"sk":{"k":0,"ix":4},"sa":{"k":0,"ix":5},"nm":"Transform"}],"nm":"B"}],"ip":4,"op":8,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":34,"ty":1,"nm":"ResizerTemp","parent":35,"ks":{"o":{"k":0},"r":{"k":0},"p":{"k":[74,102,0]},"a":{"k":[250,300,0]},"s":{"k":[30,30,100]}},"ao":0,"sw":500,"sh":600,"sc":"#ffffff","ip":0,"op":37,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":35,"ty":1,"nm":"White Solid 8","ks":{"o":{"k":0},"r":{"k":0},"p":{"k":[40,50,0]},"a":{"k":[70,100,0]},"s":{"k":[57.143,50,100]}},"ao":0,"sw":140,"sh":200,"sc":"#ffffff","ip":0,"op":37,"st":0,"bm":0,"sr":1}],"v":"4.4.26","ddd":0,"ip":0,"op":37,"fr":25,"w":80,"h":100} diff --git a/sdk/python/examples/controls/lottie/example_1.py b/sdk/python/examples/controls/lottie/example_1.py new file mode 100644 index 0000000000..2479cce046 --- /dev/null +++ b/sdk/python/examples/controls/lottie/example_1.py @@ -0,0 +1,27 @@ +import flet_lottie as ftl + +import flet as ft + + +def main(page: ft.Page): + page.add( + ftl.Lottie( + src="https://raw.githubusercontent.com/xvrh/lottie-flutter/master/example/assets/Mobilo/A.json", + reverse=False, + animate=True, + error_content=ft.Placeholder(ft.Text("Error loading Lottie")), + on_error=lambda e: print(f"Error loading Lottie: {e.data}"), + ), + ftl.Lottie( + src="sample.json", + reverse=False, + animate=True, + enable_merge_paths=True, + enable_layers_opacity=True, + error_content=ft.Placeholder(ft.Text("Error loading Lottie")), + on_error=lambda e: print(f"Error loading Lottie: {e.data}"), + ), + ) + + +ft.run(main) diff --git a/sdk/python/examples/controls/map/example_1.py b/sdk/python/examples/controls/map/example_1.py new file mode 100644 index 0000000000..450e37ceae --- /dev/null +++ b/sdk/python/examples/controls/map/example_1.py @@ -0,0 +1,138 @@ +import random + +import flet_map as ftm + +import flet as ft + + +def main(page: ft.Page): + marker_layer_ref = ft.Ref[ftm.MarkerLayer]() + circle_layer_ref = ft.Ref[ftm.CircleLayer]() + + def handle_tap(e: ftm.MapTapEvent): + if e.name == "tap": + marker_layer_ref.current.markers.append( + ftm.Marker( + content=ft.Icon( + ft.Icons.LOCATION_ON, color=ft.CupertinoColors.DESTRUCTIVE_RED + ), + coordinates=e.coordinates, + ) + ) + elif e.name == "secondary_tap": + circle_layer_ref.current.circles.append( + ftm.CircleMarker( + radius=random.randint(5, 10), + coordinates=e.coordinates, + color=ft.Colors.random(), + border_color=ft.Colors.random(), + border_stroke_width=4, + ) + ) + page.update() + + page.add( + ft.Text("Click anywhere to add a Marker, right-click to add a CircleMarker."), + ftm.Map( + expand=True, + initial_center=ftm.MapLatitudeLongitude(15, 10), + initial_zoom=4.2, + interaction_configuration=ftm.InteractionConfiguration( + flags=ftm.InteractionFlag.ALL + ), + on_tap=handle_tap, + on_secondary_tap=handle_tap, + on_long_press=handle_tap, + on_event=print, + layers=[ + ftm.TileLayer( + url_template="https://tile.openstreetmap.org/{z}/{x}/{y}.png", + on_image_error=lambda e: print("TileLayer Error"), + ), + ftm.RichAttribution( + attributions=[ + ftm.TextSourceAttribution( + text="OpenStreetMap Contributors", + on_click=lambda e: e.page.launch_url( + "https://www.openstreetmap.org/copyright" + ), + ), + ftm.TextSourceAttribution( + text="Flet", + on_click=lambda e: e.page.launch_url("https://flet.dev"), + ), + ] + ), + ftm.SimpleAttribution( + text="Flet", + alignment=ft.Alignment.TOP_RIGHT, + on_click=lambda e: print("Clicked SimpleAttribution"), + ), + ftm.MarkerLayer( + ref=marker_layer_ref, + markers=[ + ftm.Marker( + content=ft.Icon(ft.Icons.LOCATION_ON), + coordinates=ftm.MapLatitudeLongitude(30, 15), + ), + ftm.Marker( + content=ft.Icon(ft.Icons.LOCATION_ON), + coordinates=ftm.MapLatitudeLongitude(10, 10), + ), + ftm.Marker( + content=ft.Icon(ft.Icons.LOCATION_ON), + coordinates=ftm.MapLatitudeLongitude(25, 45), + ), + ], + ), + ftm.CircleLayer( + ref=circle_layer_ref, + circles=[ + ftm.CircleMarker( + radius=10, + coordinates=ftm.MapLatitudeLongitude(16, 24), + color=ft.Colors.RED, + border_color=ft.Colors.BLUE, + border_stroke_width=4, + ), + ], + ), + ftm.PolygonLayer( + polygons=[ + ftm.PolygonMarker( + label="Popular Touristic Area", + label_text_style=ft.TextStyle( + color=ft.Colors.BLACK, + size=15, + weight=ft.FontWeight.BOLD, + ), + color=ft.Colors.with_opacity(0.3, ft.Colors.BLUE), + coordinates=[ + ftm.MapLatitudeLongitude(10, 10), + ftm.MapLatitudeLongitude(30, 15), + ftm.MapLatitudeLongitude(25, 45), + ], + ), + ], + ), + ftm.PolylineLayer( + polylines=[ + ftm.PolylineMarker( + border_stroke_width=3, + border_color=ft.Colors.RED, + gradient_colors=[ft.Colors.BLACK, ft.Colors.BLACK], + color=ft.Colors.with_opacity(0.6, ft.Colors.GREEN), + coordinates=[ + ftm.MapLatitudeLongitude(10, 10), + ftm.MapLatitudeLongitude(30, 15), + ftm.MapLatitudeLongitude(25, 45), + ], + ), + ], + ), + ], + ), + ) + + +ft.run(main) diff --git a/sdk/python/examples/controls/permission_handler/example_1.py b/sdk/python/examples/controls/permission_handler/example_1.py new file mode 100644 index 0000000000..534431b596 --- /dev/null +++ b/sdk/python/examples/controls/permission_handler/example_1.py @@ -0,0 +1,35 @@ +import flet_permission_handler as fph + +import flet as ft + + +def main(page: ft.Page): + page.appbar = ft.AppBar(title="PermissionHandler Playground") + + def show_snackbar(message: str): + page.show_dialog(ft.SnackBar(ft.Text(message))) + + async def get_permission_status(e: ft.Event[ft.OutlinedButton]): + status = await ph.get_status(fph.Permission.MICROPHONE) + show_snackbar(f"Microphone permission status: {status.name}") + + async def request_permission(e: ft.Event[ft.OutlinedButton]): + status = await ph.request(fph.Permission.MICROPHONE) + show_snackbar(f"Requested microphone permission: {status.name}") + + async def open_app_settings(e: ft.Event[ft.OutlinedButton]): + show_snackbar("Opening app settings...") + await ph.open_app_settings() + + ph = fph.PermissionHandler() + + page.add( + ft.OutlinedButton("Open app settings", on_click=open_app_settings), + ft.OutlinedButton("Request Microphone permission", on_click=request_permission), + ft.OutlinedButton( + "Get Microphone permission status", on_click=get_permission_status + ), + ) + + +ft.run(main) diff --git a/sdk/python/examples/controls/rive/assets/vehicles.riv b/sdk/python/examples/controls/rive/assets/vehicles.riv new file mode 100644 index 0000000000..5574a91f25 Binary files /dev/null and b/sdk/python/examples/controls/rive/assets/vehicles.riv differ diff --git a/sdk/python/examples/controls/rive/example_1.py b/sdk/python/examples/controls/rive/example_1.py new file mode 100644 index 0000000000..e1b1447e4a --- /dev/null +++ b/sdk/python/examples/controls/rive/example_1.py @@ -0,0 +1,23 @@ +import flet_rive as ftr + +import flet as ft + + +def main(page: ft.Page): + page.add( + ftr.Rive( + src="https://cdn.rive.app/animations/vehicles.riv", + placeholder=ft.ProgressBar(), + width=300, + height=200, + ), + ftr.Rive( + src="vehicles.riv", + placeholder=ft.ProgressBar(), + width=300, + height=200, + ), + ) + + +ft.run(main) diff --git a/sdk/python/examples/controls/video/example_1.py b/sdk/python/examples/controls/video/example_1.py new file mode 100644 index 0000000000..7dbb0eb459 --- /dev/null +++ b/sdk/python/examples/controls/video/example_1.py @@ -0,0 +1,142 @@ +import random + +import flet_video as ftv + +import flet as ft + + +def main(page: ft.Page): + page.theme_mode = ft.ThemeMode.LIGHT + page.title = "TheEthicalVideo" + page.window.always_on_top = True + page.spacing = 20 + page.horizontal_alignment = ft.CrossAxisAlignment.CENTER + + async def handle_pause(e): + await video.pause() + print("Video.pause()") + + async def handle_play_or_pause(e): + await video.play_or_pause() + print("Video.play_or_pause()") + + async def handle_play(e): + await video.play() + print("Video.play()") + + async def handle_stop(e): + await video.stop() + print("Video.stop()") + + async def handle_next(e): + await video.next() + print("Video.next()") + + async def handle_previous(e): + await video.previous() + print("Video.previous()") + + def handle_volume_change(e): + video.volume = e.control.value + print(f"Video.volume = {e.control.value}") + + def handle_playback_rate_change(e): + video.playback_rate = e.control.value + print(f"Video.playback_rate = {e.control.value}") + + async def handle_seek(e): + await video.seek(10000) + print("Video.seek(10000)") + + async def handle_add_media(e): + await video.playlist_add(random.choice(sample_media)) + print("Video.playlist_add(random.choice(sample_media))") + + async def handle_remove_media(e): + r = random.randint(0, len(video.playlist) - 1) + await video.playlist_remove(r) + print(f"Popped Item at index: {r} (position {r + 1})") + + async def handle_jump(e): + print("Video.jump_to(0)") + await video.jump_to(0) + + sample_media = [ + ftv.VideoMedia( + "https://user-images.githubusercontent.com/28951144/229373720-14d69157-1a56-4a78-a2f4-d7a134d7c3e9.mp4" + ), + ftv.VideoMedia( + "https://user-images.githubusercontent.com/28951144/229373718-86ce5e1d-d195-45d5-baa6-ef94041d0b90.mp4" + ), + ftv.VideoMedia( + "https://user-images.githubusercontent.com/28951144/229373716-76da0a4e-225a-44e4-9ee7-3e9006dbc3e3.mp4" + ), + ftv.VideoMedia( + "https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4" + ), + ftv.VideoMedia( + "https://user-images.githubusercontent.com/28951144/229373709-603a7a89-2105-4e1b-a5a5-a6c3567c9a59.mp4", + extras={ + "artist": "Thousand Foot Krutch", + "album": "The End Is Where We Begin", + }, + http_headers={ + "Foo": "Bar", + "Accept": "*/*", + }, + ), + ] + + page.add( + video := ftv.Video( + expand=True, + playlist=sample_media[0:2], + playlist_mode=ftv.PlaylistMode.LOOP, + fill_color=ft.Colors.BLUE_400, + aspect_ratio=16 / 9, + volume=100, + autoplay=False, + filter_quality=ft.FilterQuality.HIGH, + muted=False, + on_load=lambda e: print("Video loaded successfully!"), + on_enter_fullscreen=lambda e: print("Video entered fullscreen!"), + on_exit_fullscreen=lambda e: print("Video exited fullscreen!"), + ), + ft.Row( + wrap=True, + alignment=ft.MainAxisAlignment.CENTER, + controls=[ + ft.Button("Play", on_click=handle_play), + ft.Button("Pause", on_click=handle_pause), + ft.Button("Play Or Pause", on_click=handle_play_or_pause), + ft.Button("Stop", on_click=handle_stop), + ft.Button("Next", on_click=handle_next), + ft.Button("Previous", on_click=handle_previous), + ft.Button("Seek s=10", on_click=handle_seek), + ft.Button("Jump to first Media", on_click=handle_jump), + ft.Button("Add Random Media", on_click=handle_add_media), + ft.Button("Remove Random Media", on_click=handle_remove_media), + ], + ), + ft.Slider( + min=0, + value=100, + max=100, + label="Volume = {value}%", + divisions=10, + width=400, + on_change=handle_volume_change, + ), + ft.Slider( + min=1, + value=1, + max=3, + label="PlaybackRate = {value}X", + divisions=6, + width=400, + on_change=handle_playback_rate_change, + ), + ) + + +ft.run(main) diff --git a/sdk/python/examples/controls/webview/example_1.py b/sdk/python/examples/controls/webview/example_1.py new file mode 100644 index 0000000000..32495aa5ed --- /dev/null +++ b/sdk/python/examples/controls/webview/example_1.py @@ -0,0 +1,18 @@ +import flet_webview as fwv + +import flet as ft + + +def main(page: ft.Page): + page.add( + fwv.WebView( + url="https://flet.dev", + on_page_started=lambda _: print("Page started"), + on_page_ended=lambda _: print("Page ended"), + on_web_resource_error=lambda e: print("WebView error:", e.data), + expand=True, + ) + ) + + +ft.run(main) diff --git a/sdk/python/examples/pyproject.toml b/sdk/python/examples/pyproject.toml index 95e833a776..9d821b4a5c 100644 --- a/sdk/python/examples/pyproject.toml +++ b/sdk/python/examples/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "Sample apps for Flet" license = "Apache-2.0" readme = "README.md" -requires-python = ">=3.10,<3.14" +requires-python = ">=3.10,<3.15" dependencies = [ "flet>=0.70.0.dev0", "flet-desktop>=0.70.0.dev0", diff --git a/sdk/python/packages/flet-ads/.gitignore b/sdk/python/packages/flet-ads/.gitignore deleted file mode 100644 index 373c0fed1a..0000000000 --- a/sdk/python/packages/flet-ads/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!lib/ diff --git a/sdk/python/packages/flet-ads/README.md b/sdk/python/packages/flet-ads/README.md index 607b232cdb..d6628e2491 100644 --- a/sdk/python/packages/flet-ads/README.md +++ b/sdk/python/packages/flet-ads/README.md @@ -2,7 +2,7 @@ [![pypi](https://img.shields.io/pypi/v/flet-ads.svg)](https://pypi.python.org/pypi/flet-ads) [![downloads](https://static.pepy.tech/badge/flet-ads/month)](https://pepy.tech/project/flet-ads) -[![license](https://img.shields.io/github/license/flet-dev/flet-ads.svg)](https://github.com/flet-dev/flet/blob/main/sdk/python/packages//flet-ads/LICENSE) +[![license](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-ads.svg)](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-ads/LICENSE) Display Google Ads in [Flet](https://flet.dev) apps. @@ -37,4 +37,4 @@ To install the `flet-ads` package and add it to your project dependencies: ### Examples -For examples, see [these](../../examples/controls/ads). +For examples, see [these](https://github.com/flet-dev/flet/tree/main/sdk/python/examples/controls/ads). diff --git a/sdk/python/packages/flet-ads/pyproject.toml b/sdk/python/packages/flet-ads/pyproject.toml index ce9eac566f..e430698a3c 100644 --- a/sdk/python/packages/flet-ads/pyproject.toml +++ b/sdk/python/packages/flet-ads/pyproject.toml @@ -12,9 +12,9 @@ dependencies = [ [project.urls] Homepage = "https://flet.dev" -Documentation = "https://flet-dev.github.io/flet-ads" -Repository = "https://github.com/flet-dev/flet-ads" -Issues = "https://github.com/flet-dev/flet-ads/issues" +Documentation = "https://docs.flet.dev/ads" +Repository = "https://github.com/flet-dev/flet/tree/main/sdk/python/packages/flet-ads" +Issues = "https://github.com/flet-dev/flet/issues" [tool.setuptools.package-data] "flutter.flet_ads" = ["**/*"] diff --git a/sdk/python/packages/flet-ads/src/flet_ads/.gitignore b/sdk/python/packages/flet-ads/src/flet_ads/.gitignore deleted file mode 100644 index c10c4529c6..0000000000 --- a/sdk/python/packages/flet-ads/src/flet_ads/.gitignore +++ /dev/null @@ -1,131 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# VS Code -.vscode/ - -# mac -.DS_store diff --git a/sdk/python/packages/flet-ads/src/flutter/flet_ads/.gitignore b/sdk/python/packages/flet-ads/src/flutter/flet_ads/.gitignore index 93ae689a61..ed7794f2ab 100644 --- a/sdk/python/packages/flet-ads/src/flutter/flet_ads/.gitignore +++ b/sdk/python/packages/flet-ads/src/flutter/flet_ads/.gitignore @@ -29,3 +29,6 @@ migrate_working_dir/ build/ .flutter-plugins .flutter-plugins-dependencies + +# override parent rules +!lib/ diff --git a/sdk/python/packages/flet-ads/src/flutter/flet_ads/pubspec.yaml b/sdk/python/packages/flet-ads/src/flutter/flet_ads/pubspec.yaml index a6b9f1f640..9af437ab1b 100644 --- a/sdk/python/packages/flet-ads/src/flutter/flet_ads/pubspec.yaml +++ b/sdk/python/packages/flet-ads/src/flutter/flet_ads/pubspec.yaml @@ -1,8 +1,5 @@ name: flet_ads description: Display Google Ads in Flet apps. -homepage: https://flet.dev -repository: https://github.com/flet-dev/flet-ads/src/flutter/flet_ads -issue_tracker: https://github.com/flet-dev/flet-ads/issues version: 0.1.0 publish_to: none @@ -15,7 +12,7 @@ dependencies: sdk: flutter collection: ^1.16.0 - google_mobile_ads: ^6.0.0 + google_mobile_ads: 6.0.0 flet: path: ../../../../../../../packages/flet diff --git a/sdk/python/packages/flet-audio-recorder/CHANGELOG.md b/sdk/python/packages/flet-audio-recorder/CHANGELOG.md new file mode 100644 index 0000000000..3ebf471d49 --- /dev/null +++ b/sdk/python/packages/flet-audio-recorder/CHANGELOG.md @@ -0,0 +1,50 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [0.2.0] - 2025-06-26 + +### Added + +- Deployed online documentation: https://docs.flet.dev/audio-recorder/ +- `AudioRecorder` control new property: `configuration` +- New dataclasses: + - `AudioRecorderConfiguration` + - `AndroidRecorderConfiguration` + - `IosRecorderConfiguration` + - `InputDevice` +- New enums: + - `AndroidAudioSource` + - `IosAudioCategoryOption` + +### Changed + +- Refactored `AudioRecorder` control to use `@ft.control` dataclass-style definition and switched to `Service` control type + +#### Breaking Changes + +- `AudioRecorder` must now be added to `Page.services` instead of `Page.overlay`. +- Recording configuration properties (`audio_encoder`, `suppress_noise`, `cancel_echo`, `auto_gain`, `channels_num`, `sample_rate`, `bit_rate`) are now grouped under `configuration: AudioRecorderConfiguration` +- Event `on_state_changed` renamed to `on_state_change` +- In all methods, parameter `wait_timeout` was renamed to `timeout`. +- The following `AudioRecorder` sync methods were made [`async`](https://docs.python.org/3/library/asyncio.html): + - `is_recording` + - `stop_recording` + - `cancel_recording` + - `resume_recording` + - `pause_recording` + - `is_paused` + - `is_supported_encoder` + - `get_input_devices` + - `has_permission` + +## [0.1.0] - 2025-01-15 + +Initial release. + + +[0.2.0]: https://github.com/flet-dev/flet-audio-recorder/compare/0.1.0...0.2.0 +[0.1.0]: https://github.com/flet-dev/flet-audio-recorder/releases/tag/0.1.0 diff --git a/sdk/python/packages/flet-audio-recorder/LICENSE b/sdk/python/packages/flet-audio-recorder/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/sdk/python/packages/flet-audio-recorder/LICENSE @@ -0,0 +1,201 @@ + 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 such Contributor that are necessarily infringed 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 consequential 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-audio-recorder/README.md b/sdk/python/packages/flet-audio-recorder/README.md new file mode 100644 index 0000000000..87eee3e93a --- /dev/null +++ b/sdk/python/packages/flet-audio-recorder/README.md @@ -0,0 +1,44 @@ +# flet-audio-recorder + +[![pypi](https://img.shields.io/pypi/v/flet-audio-recorder.svg)](https://pypi.python.org/pypi/flet-audio-recorder) +[![downloads](https://static.pepy.tech/badge/flet-audio-recorder/month)](https://pepy.tech/project/flet-audio-recorder) +[![license](https://img.shields.io/github/license/flet-dev/flet.svg)](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-audio-recorder/LICENSE) + +Adds audio recording support to [Flet](https://flet.dev) apps. + +It is based on the [record](https://pub.dev/packages/record) Flutter package. + +## Documentation + +Detailed documentation to this package can be found [here](https://docs.flet.dev/audio-recorder/). + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | + +## Usage + +### Installation + +To install the `flet-audio-recorder` package and add it to your project dependencies: + +- Using `uv`: + ```bash + uv add flet-audio-recorder + ``` + +- Using `pip`: + ```bash + pip install flet-audio-recorder + ``` + After this, you will have to manually add this package to your `requirements.txt` or `pyproject.toml`. + + +> [!NOTE] +> On Linux, encoding is provided by [fmedia](https://stsaz.github.io/fmedia/) which must be installed separately. + +### Examples + +For examples, see [these](https://github.com/flet-dev/flet/tree/main/sdk/python/examples/controls/audio_recorder). diff --git a/sdk/python/packages/flet-audio-recorder/pyproject.toml b/sdk/python/packages/flet-audio-recorder/pyproject.toml new file mode 100644 index 0000000000..180b444312 --- /dev/null +++ b/sdk/python/packages/flet-audio-recorder/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "flet-audio-recorder" +version = "0.1.0" +description = "Adds audio recording support to 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://docs.flet.dev/audio-recorder" +Repository = "https://github.com/flet-dev/flet/tree/main/sdk/python/packages/flet-audio-recorder" +Issues = "https://github.com/flet-dev/flet/issues" + +[tool.setuptools.package-data] +"flutter.flet_audio_recorder" = ["**/*"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/sdk/python/packages/flet-audio-recorder/src/flet_audio_recorder/__init__.py b/sdk/python/packages/flet-audio-recorder/src/flet_audio_recorder/__init__.py new file mode 100644 index 0000000000..ca5413e68c --- /dev/null +++ b/sdk/python/packages/flet-audio-recorder/src/flet_audio_recorder/__init__.py @@ -0,0 +1,25 @@ +from .audio_recorder import AudioRecorder +from .types import ( + AndroidAudioSource, + AndroidRecorderConfiguration, + AudioEncoder, + AudioRecorderConfiguration, + AudioRecorderState, + AudioRecorderStateChangeEvent, + InputDevice, + IosAudioCategoryOption, + IosRecorderConfiguration, +) + +__all__ = [ + "AndroidAudioSource", + "AndroidRecorderConfiguration", + "AudioEncoder", + "AudioRecorder", + "AudioRecorderConfiguration", + "AudioRecorderState", + "AudioRecorderStateChangeEvent", + "InputDevice", + "IosAudioCategoryOption", + "IosRecorderConfiguration", +] diff --git a/sdk/python/packages/flet-audio-recorder/src/flet_audio_recorder/audio_recorder.py b/sdk/python/packages/flet-audio-recorder/src/flet_audio_recorder/audio_recorder.py new file mode 100644 index 0000000000..49e86270be --- /dev/null +++ b/sdk/python/packages/flet-audio-recorder/src/flet_audio_recorder/audio_recorder.py @@ -0,0 +1,147 @@ +from dataclasses import field +from typing import Optional + +import flet as ft + +from .types import ( + AudioEncoder, + AudioRecorderConfiguration, + AudioRecorderStateChangeEvent, + InputDevice, +) + +__all__ = ["AudioRecorder"] + + +@ft.control("AudioRecorder") +class AudioRecorder(ft.Service): + """ + A control that allows you to record audio from your device. + + This control can record audio using different + audio encoders and also allows configuration + of various audio recording parameters such as + noise suppression, echo cancellation, and more. + """ + + configuration: AudioRecorderConfiguration = field( + default_factory=lambda: AudioRecorderConfiguration() + ) + """ + The default configuration of the audio recorder. + """ + + on_state_change: Optional[ft.EventHandler[AudioRecorderStateChangeEvent]] = None + """ + Event handler that is called when the state of the audio recorder changes. + """ + + async def start_recording( + self, + output_path: Optional[str] = None, + configuration: Optional[AudioRecorderConfiguration] = None, + ) -> bool: + """ + Starts recording audio and saves it to the specified output path. + + If not on the web, the `output_path` parameter must be provided. + + Args: + output_path: The file path where the audio will be saved. + It must be specified if not on web. + configuration: The configuration for the audio recorder. + If `None`, the `AudioRecorder.configuration` will be used. + + Returns: + `True` if recording was successfully started, `False` otherwise. + """ + assert self.page.web or output_path, ( + "output_path must be provided on platforms other than web" + ) + return await self._invoke_method( + method_name="start_recording", + arguments={ + "output_path": output_path, + "configuration": configuration + if configuration is not None + else self.configuration, + }, + ) + + async def is_recording(self) -> bool: + """ + Checks whether the audio recorder is currently recording. + + Returns: + `True` if the recorder is currently recording, `False` otherwise. + """ + return await self._invoke_method("is_recording") + + async def stop_recording(self) -> Optional[str]: + """ + Stops the audio recording and optionally returns the path to the saved file. + + Returns: + The file path where the audio was saved or `None` if not applicable. + """ + return await self._invoke_method("stop_recording") + + async def cancel_recording(self): + """ + Cancels the current audio recording. + """ + await self._invoke_method("cancel_recording") + + async def resume_recording(self): + """ + Resumes a paused audio recording. + """ + await self._invoke_method("resume_recording") + + async def pause_recording(self): + """ + Pauses the ongoing audio recording. + """ + await self._invoke_method("pause_recording") + + async def is_paused(self) -> bool: + """ + Checks whether the audio recorder is currently paused. + + Returns: + `True` if the recorder is paused, `False` otherwise. + """ + return await self._invoke_method("is_paused") + + async def is_supported_encoder(self, encoder: AudioEncoder) -> bool: + """ + Checks if the given audio encoder is supported by the recorder. + + Args: + encoder: The audio encoder to check. + + Returns: + `True` if the encoder is supported, `False` otherwise. + """ + return await self._invoke_method("is_supported_encoder", {"encoder": encoder}) + + async def get_input_devices(self) -> list[InputDevice]: + """ + Retrieves the available input devices for recording. + + Returns: + A list of available input devices. + """ + r = await self._invoke_method("get_input_devices") + return [ + InputDevice(id=device_id, label=label) for device_id, label in r.items() + ] + + async def has_permission(self) -> bool: + """ + Checks if the app has permission to record audio. + + Returns: + `True` if the app has permission, `False` otherwise. + """ + return await self._invoke_method("has_permission") diff --git a/sdk/python/packages/flet-audio-recorder/src/flet_audio_recorder/types.py b/sdk/python/packages/flet-audio-recorder/src/flet_audio_recorder/types.py new file mode 100644 index 0000000000..8f81a27240 --- /dev/null +++ b/sdk/python/packages/flet-audio-recorder/src/flet_audio_recorder/types.py @@ -0,0 +1,353 @@ +from dataclasses import dataclass, field +from enum import Enum +from typing import TYPE_CHECKING, Optional + +import flet as ft + +if TYPE_CHECKING: + from .audio_recorder import AudioRecorder # noqa + +__all__ = [ + "AndroidAudioSource", + "AndroidRecorderConfiguration", + "AudioEncoder", + "AudioRecorderConfiguration", + "AudioRecorderState", + "AudioRecorderStateChangeEvent", + "InputDevice", + "IosAudioCategoryOption", + "IosRecorderConfiguration", +] + + +class AudioRecorderState(Enum): + """State of the audio recorder.""" + + STOPPED = "stopped" + """The audio recorder is stopped and not recording.""" + + RECORDING = "recording" + """The audio recorder is currently recording audio.""" + + PAUSED = "paused" + """The audio recorder is paused and can be resumed.""" + + +@dataclass +class AudioRecorderStateChangeEvent(ft.Event["AudioRecorder"]): + state: AudioRecorderState + """The new state of the audio recorder.""" + + +class AudioEncoder(Enum): + """ + Represents the different audio encoders for audio recording. + """ + + AACLC = "aacLc" + """ + Advanced Audio Codec Low Complexity. + A commonly used encoder for streaming and general audio recording. + """ + + AACELD = "aacEld" + """ + Advanced Audio Codec Enhanced Low Delay. + Suitable for low-latency applications like VoIP. + """ + + AACHE = "aacHe" + """ + Advanced Audio Codec High Efficiency. + Optimized for high-quality audio at lower bit rates. + """ + + AMRNB = "amrNb" + """ + Adaptive Multi-Rate Narrow Band. + Used for speech audio in mobile communication. + """ + + AMRWB = "amrWb" + """ + Adaptive Multi-Rate Wide Band. + Used for higher-quality speech audio. + """ + + OPUS = "opus" + """ + A codec designed for both speech and audio applications, + known for its versatility. + """ + + FLAC = "flac" + """ + Free Lossless Audio Codec. + Provides high-quality lossless audio compression. + """ + + WAV = "wav" + """ + Standard audio format used for raw, uncompressed audio data. + """ + + PCM16BITS = "pcm16bits" + """ + Pulse Code Modulation with 16-bit depth, used for high-fidelity audio. + """ + + +class AndroidAudioSource(Enum): + """Android-specific audio source types.""" + + DEFAULT_SOURCE = "defaultSource" + """Default audio source.""" + + MIC = "mic" + """Microphone audio source.""" + + VOICE_UPLINK = "voiceUplink" + """Voice call uplink (Tx) audio source.""" + + VOICE_DOWNLINK = "voiceDownlink" + """Voice call downlink (Rx) audio source.""" + + VOICE_CALL = "voiceCall" + """Voice call uplink + downlink audio source.""" + + CAMCORDER = "camcorder" + """ + Microphone audio source tuned for video recording, + with the same orientation as the camera, if available. + """ + + VOICE_RECOGNITION = "voiceRecognition" + """Microphone audio source tuned for voice recognition.""" + + VOICE_COMMUNICATION = "voiceCommunication" + """Microphone audio source tuned for voice communications such as VoIP.""" + + REMOTE_SUBMIX = "remoteSubMix" + """Audio source for a submix of audio streams to be presented remotely.""" + + UNPROCESSED = "unprocessed" + """ + Microphone audio source tuned for unprocessed (raw) sound if available, + behaves like `DEFAULT_SOURCE` otherwise. + """ + + VOICE_PERFORMANCE = "voicePerformance" + """ + Source for capturing audio meant to be processed in real time + and played back for live performance (e.g karaoke). + """ + + +@dataclass +class AndroidRecorderConfiguration: + """Android specific configuration for recording.""" + + use_legacy: bool = False + """ + Whether to use the Android MediaRecorder. + + While advanced recorder (the default) unlocks additionnal features, + the legacy recorder is stability oriented. + """ + + mute_audio: bool = False + """ + Whether to mute all audio streams like alarms, music, ring, etc. + + This is useful when you want to record audio without any background noise. + The streams are restored to their previous state after recording is stopped + and will stay at current state on pause/resume. + """ + + manage_bluetooth: bool = True + """ + Try to start a bluetooth audio connection to a headset (Bluetooth SCO). + """ + + audio_source: AndroidAudioSource = AndroidAudioSource.DEFAULT_SOURCE + """ + Defines the audio source. + + An audio source defines both a default physical source of audio signal, + and a recording configuration. + Some effects are available or not depending on this source. + + Most of the time, you should use + [`AndroidAudioSource.DEFAULT_SOURCE`][(p).] or + [`AndroidAudioSource.MIC`][(p).]. + """ + + +class IosAudioCategoryOption(Enum): + """ + Audio behaviors. + + Each option is valid only for specific audio session categories. + """ + + MIX_WITH_OTHERS = "mixWithOthers" + """ + Whether audio from this session mixes with audio + from active sessions in other audio apps. + """ + + DUCK_OTHERS = "duckOthers" + """ + Reduces the volume of other audio sessions while audio from this session plays. + """ + + ALLOW_BLUETOOTH = "allowBluetooth" + """ + Bluetooth hands-free devices appear as available input routes. + """ + + DEFAULT_TO_SPEAKER = "defaultToSpeaker" + """ + Audio from the session defaults to the built-in speaker instead of the receiver. + """ + + INTERRUPT_SPOKEN_AUDIO_AND_MIX_WITH_OTHERS = "interruptSpokenAudioAndMixWithOthers" + """ + Pause spoken audio content from other sessions when your app plays its audio. + + Available from iOS 9.0. + """ + + ALLOW_BLUETOOTH_A2DP = "allowBluetoothA2DP" + """ + Stream audio from this session to Bluetooth devices + that support the Advanced Audio Distribution Profile (A2DP). + + Note: + Available from iOS 10.0. + """ + + ALLOW_AIRPLAY = "allowAirPlay" + """ + Stream audio from this session to AirPlay devices. + + Note: + Available from iOS 10.0. + """ + + OVERRIDE_MUTED_MICROPHONE_INTERRUPTION = "overrideMutedMicrophoneInterruption" + """ + System interrupts the audio session when it mutes the built-in microphone. + + Note: + Available from iOS 14.5. + """ + + +@dataclass +class IosRecorderConfiguration: + """iOS specific configuration for recording.""" + + options: list[IosAudioCategoryOption] = field( + default_factory=lambda: [ + IosAudioCategoryOption.DEFAULT_TO_SPEAKER, + IosAudioCategoryOption.ALLOW_BLUETOOTH, + IosAudioCategoryOption.ALLOW_BLUETOOTH_A2DP, + ] + ) + """ + Optional audio behaviors. + """ + + manage_audio_session: bool = True + """ + Whether to manage the shared AVAudioSession. + + Set this to `False` if another plugin is + already managing the AVAudioSession. + """ + + +@dataclass +class InputDevice: + """An audio input device.""" + + id: str + """The ID used to select the device on the platform.""" + + label: str + """The label text representation.""" + + +@dataclass +class AudioRecorderConfiguration: + """Recording configuration.""" + + encoder: AudioEncoder = AudioEncoder.WAV + """ + The requested output format through this given encoder. + """ + + suppress_noise: bool = False + """ + The recorder will try to negate the input + noise (if available on the device). + + Recording volume may be lowered by using this. + """ + + cancel_echo: bool = False + """ + The recorder will try to reduce echo (if available on the device). + + Recording volume may be lowered by using this. + """ + + auto_gain: bool = False + """ + The recorder will try to auto adjust recording volume in a + limited range (if available on the device). + + Recording volume may be lowered by using this. + """ + + channels: int = 2 + """ + The numbers of channels for the recording. + + - `1` for mono + - `2` for stereo + + Most platforms only accept at most 2 channels. + """ + + sample_rate: int = 44100 + """ + The sample rate for audio in samples per second if applicable. + """ + + bit_rate: ft.Number = 128000 + """ + The audio encoding bit rate in bits per second if applicable. + """ + + device: Optional[InputDevice] = None + """ + The device to be used for recording. + + If `None`, default device will be selected. + """ + + android_configuration: AndroidRecorderConfiguration = field( + default_factory=lambda: AndroidRecorderConfiguration() + ) + """ + Android specific configuration. + """ + + ios_configuration: IosRecorderConfiguration = field( + default_factory=lambda: IosRecorderConfiguration() + ) + """ + iOS specific configuration. + """ diff --git a/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/.gitignore b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/.gitignore new file mode 100644 index 0000000000..ed7794f2ab --- /dev/null +++ b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/.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-audio-recorder/src/flutter/flet_audio_recorder/CHANGELOG.md b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/CHANGELOG.md new file mode 100644 index 0000000000..010a321b90 --- /dev/null +++ b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/CHANGELOG.md @@ -0,0 +1,3 @@ +# 0.1.0 + +Initial release of the package. diff --git a/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/LICENSE b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/LICENSE @@ -0,0 +1,201 @@ + 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 such Contributor that are necessarily infringed 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 consequential 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-audio-recorder/src/flutter/flet_audio_recorder/README.md b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/README.md new file mode 100644 index 0000000000..c0da60f9da --- /dev/null +++ b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/README.md @@ -0,0 +1,3 @@ +# Flet `AudioRecorder` control + +`AudioRecorder` control to use in Flet apps. diff --git a/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/analysis_options.yaml b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/analysis_options.yaml new file mode 100644 index 0000000000..a5744c1cfb --- /dev/null +++ b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/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-audio-recorder/src/flutter/flet_audio_recorder/lib/flet_audio_recorder.dart b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/lib/flet_audio_recorder.dart new file mode 100644 index 0000000000..345e980435 --- /dev/null +++ b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/lib/flet_audio_recorder.dart @@ -0,0 +1,3 @@ +library flet_video; + +export "src/extension.dart" show Extension; diff --git a/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/lib/src/audio_recorder.dart b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/lib/src/audio_recorder.dart new file mode 100644 index 0000000000..5ce7f09679 --- /dev/null +++ b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/lib/src/audio_recorder.dart @@ -0,0 +1,91 @@ +import 'dart:async'; + +import 'package:flet/flet.dart'; +import 'package:flutter/widgets.dart'; +import 'package:record/record.dart'; + +import 'utils/audio_recorder.dart'; + +class AudioRecorderService extends FletService { + AudioRecorderService({required super.control}); + + AudioRecorder? recorder; + StreamSubscription? _onStateChangedSubscription; + + @override + void init() { + super.init(); + debugPrint("AudioRecorder.init($hashCode)"); + control.addInvokeMethodListener(_invokeMethod); + + recorder = AudioRecorder(); + + _onStateChangedSubscription = recorder!.onStateChanged().listen((state) { + _onStateChanged.call(state); + }); + } + + void _onStateChanged(RecordState state) { + var stateMap = { + RecordState.record: "recording", + RecordState.pause: "paused", + RecordState.stop: "stopped", + }; + control.triggerEvent("state_change", stateMap[state]); + } + + Future _invokeMethod(String name, dynamic args) async { + debugPrint("AudioRecorder.$name($args)"); + switch (name) { + case "start_recording": + final config = parseRecordConfig(args["configuration"]); + if (config != null && await recorder!.hasPermission()) { + final out = control.backend.getAssetSource(args["output_path"] ?? ""); + if (!isWebPlatform() && !out.isFile) { + // on non-web/IO platforms, the output path must be a valid file path + return false; + } + + await recorder!.start(config, path: out.path); + return true; + } + return false; + case "stop_recording": + return await recorder!.stop(); + case "cancel_recording": + await recorder!.cancel(); + case "resume_recording": + await recorder!.resume(); + case "pause_recording": + await recorder!.pause(); + case "is_supported_encoder": + var encoder = parseAudioEncoder(args["encoder"]); + if (encoder != null) { + return await recorder!.isEncoderSupported(encoder); + } + break; + case "is_paused": + return await recorder!.isPaused(); + case "is_recording": + return await recorder!.isRecording(); + case "has_permission": + return await recorder!.hasPermission(); + case "get_input_devices": + List devices = await recorder!.listInputDevices(); + return devices.asMap().map((k, v) { + return MapEntry(v.id, v.label); + }); + default: + throw Exception("Unknown AudioRecorder method: $name"); + } + } + + @override + void dispose() { + debugPrint("AudioRecorder(${control.id}).dispose()"); + _onStateChangedSubscription?.cancel(); + recorder?.dispose(); + control.removeInvokeMethodListener(_invokeMethod); + super.dispose(); + } +} diff --git a/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/lib/src/extension.dart b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/lib/src/extension.dart new file mode 100644 index 0000000000..a7b9865ad9 --- /dev/null +++ b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/lib/src/extension.dart @@ -0,0 +1,15 @@ +import 'package:flet/flet.dart'; + +import 'audio_recorder.dart'; + +class Extension extends FletExtension { + @override + FletService? createService(Control control) { + switch (control.type) { + case "AudioRecorder": + return AudioRecorderService(control: control); + default: + return null; + } + } +} diff --git a/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/lib/src/utils/audio_recorder.dart b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/lib/src/utils/audio_recorder.dart new file mode 100644 index 0000000000..9f65d339df --- /dev/null +++ b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/lib/src/utils/audio_recorder.dart @@ -0,0 +1,73 @@ +import 'package:collection/collection.dart'; +import 'package:flet/flet.dart'; +import 'package:record/record.dart'; + +AudioEncoder? parseAudioEncoder(String? value, [AudioEncoder? defaultValue]) { + if (value == null) return defaultValue; + return AudioEncoder.values.firstWhereOrNull( + (e) => e.name.toLowerCase() == value.toLowerCase()) ?? + defaultValue; +} + +InputDevice? parseInputDevice(dynamic value, [InputDevice? defaultValue]) { + if (value == null) return defaultValue; + return InputDevice(id: value["id"], label: value["label"]); +} + +AndroidAudioSource? parseAndroidAudioSource(String? value, + [AndroidAudioSource? defaultValue]) { + if (value == null) return defaultValue; + return AndroidAudioSource.values.firstWhereOrNull( + (e) => e.name.toLowerCase() == value.toLowerCase()) ?? + defaultValue; +} + +AndroidRecordConfig? parseAndroidRecordConfig(dynamic value, + [AndroidRecordConfig? defaultValue]) { + if (value == null) return defaultValue; + return AndroidRecordConfig( + audioSource: parseAndroidAudioSource( + value["audio_source"], AndroidAudioSource.defaultSource)!, + manageBluetooth: parseBool(value["manage_bluetooth"], true)!, + muteAudio: parseBool(value["mute_audio"], false)!, + useLegacy: parseBool(value["use_legacy"], false)!); +} + +IosAudioCategoryOption? parseIosAudioCategoryOption(String? value, + [IosAudioCategoryOption? defaultValue]) { + if (value == null) return defaultValue; + return IosAudioCategoryOption.values.firstWhereOrNull( + (e) => e.name.toLowerCase() == value.toLowerCase()) ?? + defaultValue; +} + +IosRecordConfig? parseIosRecordConfig(dynamic value, + [IosRecordConfig? defaultValue]) { + if (value == null) return defaultValue; + var options = (value["options"] as List) + .map((o) => parseIosAudioCategoryOption(o)) + .nonNulls + .toList(); + return IosRecordConfig( + manageAudioSession: parseBool(value["manage_audio_session"], true)!, + categoryOptions: options, + ); +} + +RecordConfig? parseRecordConfig(dynamic value, [RecordConfig? defaultValue]) { + if (value == null) return defaultValue; + return RecordConfig( + autoGain: parseBool(value["auto_gain"], false)!, + bitRate: parseInt(value["bit_rate"], 128000)!, + encoder: parseAudioEncoder(value["encoder"], AudioEncoder.wav)!, + echoCancel: parseBool(value["cancel_echo"], false)!, + noiseSuppress: parseBool(value["suppress_noise"], false)!, + numChannels: parseInt(value["channels"], 2)!, + device: parseInputDevice(value["device"]), + sampleRate: parseInt(value["sample_rate"], 44100)!, + androidConfig: parseAndroidRecordConfig( + value["android_configuration"], const AndroidRecordConfig())!, + iosConfig: parseIosRecordConfig( + value["ios_configuration"], const IosRecordConfig())!, + ); +} diff --git a/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/pubspec.yaml b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/pubspec.yaml new file mode 100644 index 0000000000..77e8cbd3b2 --- /dev/null +++ b/sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder/pubspec.yaml @@ -0,0 +1,23 @@ +name: flet_audio_recorder +description: Flet AudioRecorder control +version: 0.1.0 +publish_to: none + +environment: + sdk: '>=3.2.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + collection: ^1.16.0 + record: 6.1.2 + + flet: + path: ../../../../../../../packages/flet + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 diff --git a/sdk/python/packages/flet-audio/CHANGELOG.md b/sdk/python/packages/flet-audio/CHANGELOG.md new file mode 100644 index 0000000000..91e7bdc486 --- /dev/null +++ b/sdk/python/packages/flet-audio/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [0.2.0] - 2025-06-26 + +## Added + +- Deployed online documentation: https://flet-dev.github.io/flet-audio/ + +### Changed + +- Refactored `Audio` control to use `@ft.control` dataclass-style definition and switched to `Service` control type. + +### Breaking Changes + +- `Audio` must now be added to `Page.services` instead of `Page.overlay`. +- The following properties were renamed: + - `on_state_changed` β†’ `on_state_change` + - `on_duration_changed` β†’ `on_duration_change` + - `on_position_changed` β†’ `on_position_change` +- Method `Audio.play()` now accepts an optional `position` parameter for specifying start position. +- The following sync methods were made [`async`](https://docs.python.org/3/library/asyncio.html): + - `get_duration()` + - `get_current_position()` + +## [0.1.0] - 2025-01-15 + +Initial release. + + +[0.2.0]: https://github.com/flet-dev/flet-audio/compare/0.1.0...0.2.0 +[0.1.0]: https://github.com/flet-dev/flet-audio/releases/tag/0.1.0 diff --git a/sdk/python/packages/flet-audio/LICENSE b/sdk/python/packages/flet-audio/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/sdk/python/packages/flet-audio/LICENSE @@ -0,0 +1,201 @@ + 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 such Contributor that are necessarily infringed 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 consequential 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-audio/README.md b/sdk/python/packages/flet-audio/README.md new file mode 100644 index 0000000000..d2c818eae2 --- /dev/null +++ b/sdk/python/packages/flet-audio/README.md @@ -0,0 +1,40 @@ +# flet-audio + +[![pypi](https://img.shields.io/pypi/v/flet-audio.svg)](https://pypi.python.org/pypi/flet-audio) +[![downloads](https://static.pepy.tech/badge/flet-audio/month)](https://pepy.tech/project/flet-audio) +[![license](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-audio.svg)](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-audio/LICENSE) + +A [Flet](https://flet.dev) extension package for playing audio. + +It is based on the [audioplayers](https://pub.dev/packages/audioplayers) Flutter package. + +## Documentation + +Detailed documentation to this package can be found [here](https://docs.flet.dev/audio/). + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | + +## Usage + +### Installation + +To install the `flet-audio` package and add it to your project dependencies: + +- Using `uv`: + ```bash + uv add flet-audio + ``` + +- Using `pip`: + ```bash + pip install flet-audio + ``` + 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/controls/audio). diff --git a/sdk/python/packages/flet-audio/pyproject.toml b/sdk/python/packages/flet-audio/pyproject.toml new file mode 100644 index 0000000000..62492affed --- /dev/null +++ b/sdk/python/packages/flet-audio/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "flet-audio" +version = "0.1.0" +description = "Provides audio integration and playback in 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://docs.flet.dev/audio" +Repository = "https://github.com/flet-dev/flet/tree/main/sdk/python/packages/flet-audio" +Issues = "https://github.com/flet-dev/flet/issues" + +[tool.setuptools.package-data] +"flutter.flet_audio" = ["**/*"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/sdk/python/packages/flet-audio/src/flet_audio/__init__.py b/sdk/python/packages/flet-audio/src/flet_audio/__init__.py new file mode 100644 index 0000000000..0161798244 --- /dev/null +++ b/sdk/python/packages/flet-audio/src/flet_audio/__init__.py @@ -0,0 +1,17 @@ +from flet_audio.audio import Audio +from flet_audio.types import ( + AudioDurationChangeEvent, + AudioPositionChangeEvent, + AudioState, + AudioStateChangeEvent, + ReleaseMode, +) + +__all__ = [ + "Audio", + "AudioDurationChangeEvent", + "AudioPositionChangeEvent", + "AudioState", + "AudioStateChangeEvent", + "ReleaseMode", +] diff --git a/sdk/python/packages/flet-audio/src/flet_audio/audio.py b/sdk/python/packages/flet-audio/src/flet_audio/audio.py new file mode 100644 index 0000000000..a3a29d9d5a --- /dev/null +++ b/sdk/python/packages/flet-audio/src/flet_audio/audio.py @@ -0,0 +1,193 @@ +from typing import Optional + +import flet as ft +from flet_audio.types import ( + AudioDurationChangeEvent, + AudioPositionChangeEvent, + AudioStateChangeEvent, + ReleaseMode, +) + + +@ft.control("Audio") +class Audio(ft.Service): + """ + A control to simultaneously play multiple audio sources. + """ + + src: Optional[str] = None + """ + The audio source. + Can be a URL or a local [asset file](https://docs.flet.dev/cookbook/assets). + + Note: + - At least one of `src` or [`src_base64`][flet_audio.Audio.src_base64] must be + provided, with `src_base64` having priority if both are provided. + - [Here](https://github.com/bluefireteam/audioplayers/blob/main/troubleshooting.md#supported-formats--encodings) + is a list of supported audio formats. + + Raises: + AssertionError: If both [`src`][(c).] and [`src_base64`][(c).] are `None`. + """ + + src_base64: Optional[str] = None + """ + Defines the contents of audio file encoded in base-64 format. + + Note: + - At least one of [`src`][flet_audio.Audio.src] or `src_base64` must be + provided, with `src_base64` having priority if both are provided. + - [Here](https://github.com/bluefireteam/audioplayers/blob/main/troubleshooting.md#supported-formats--encodings) + is a list of supported audio formats. + + Raises: + AssertionError: If both [`src`][(c).] and [`src_base64`][(c).] are `None`. + """ + + autoplay: bool = False + """ + Starts playing audio as soon as audio control is added to a page. + + Note: + Autoplay works in desktop, mobile apps and Safari browser, + but doesn't work in Chrome/Edge. + """ + + volume: ft.Number = 1.0 + """ + Sets the volume (amplitude). + It's value ranges between `0.0` (mute) and `1.0` (maximum volume). + Intermediate values are linearly interpolated. + """ + + balance: ft.Number = 0.0 + """ + Defines the stereo balance. + + * `-1` - The left channel is at full volume; the right channel is silent. + * `1` - The right channel is at full volume; the left channel is silent. + * `0` - Both channels are at the same volume. + """ + + playback_rate: ft.Number = 1.0 + """ + Defines the playback rate. + + Should ideally be set when creating the constructor. + + Note: + - iOS and macOS have limits between `0.5x` and `2x`. + - Android SDK version should be 23 or higher. + """ + + release_mode: ReleaseMode = ReleaseMode.RELEASE + """ + Defines the release mode. + """ + + on_loaded: Optional[ft.ControlEventHandler["Audio"]] = None + """ + Fires when an audio is loaded/buffered. + """ + + on_duration_change: Optional[ft.EventHandler[AudioDurationChangeEvent]] = None + """ + Fires as soon as audio duration is available + (it might take a while to download or buffer it). + """ + + on_state_change: Optional[ft.EventHandler[AudioStateChangeEvent]] = None + """ + Fires when audio player state changes. + """ + + on_position_change: Optional[ft.EventHandler[AudioPositionChangeEvent]] = None + """ + Fires when audio position is changed. + Will continuously update the position of the playback + every 1 second if the status is playing. + + Can be used for a progress bar. + """ + + on_seek_complete: Optional[ft.ControlEventHandler["Audio"]] = None + """ + Fires on seek completions. + An event is going to be sent as soon as the audio seek is finished. + """ + + def before_update(self): + super().before_update() + assert self.src or self.src_base64, "either src or src_base64 must be provided" + + async def play(self, position: ft.DurationValue = 0): + """ + Starts playing audio from the specified `position`. + + Args: + position: The position to start playback from. + """ + await self._invoke_method( + method_name="play", + arguments={"position": position}, + ) + + async def pause(self): + """ + Pauses the audio that is currently playing. + + If you call [`resume()`][flet_audio.Audio.resume] later, + the audio will resume from the point that it has been paused. + """ + await self._invoke_method("pause") + + async def resume(self): + """ + Resumes the audio that has been paused or stopped. + """ + await self._invoke_method("resume") + + async def release(self): + """ + Releases the resources associated with this media player. + These are going to be fetched or buffered again as soon as + you change the source or call [`resume()`][flet_audio.Audio.resume]. + """ + await self._invoke_method("release") + + async def seek(self, position: ft.DurationValue): + """ + Moves the cursor to the desired position. + + Args: + position: The position to seek/move to. + """ + await self._invoke_method( + method_name="seek", + arguments={"position": position}, + ) + + async def get_duration(self) -> Optional[ft.Duration]: + """ + Get audio duration of the audio playback. + + It will be available as soon as the audio duration is available + (it might take a while to download or buffer it if file is not local). + + Returns: + The duration of audio playback. + """ + return await self._invoke_method( + method_name="get_duration", + ) + + async def get_current_position(self) -> Optional[ft.Duration]: + """ + Get the current position of the audio playback. + + Returns: + The current position of the audio playback. + """ + return await self._invoke_method( + method_name="get_current_position", + ) diff --git a/sdk/python/packages/flet-audio/src/flet_audio/types.py b/sdk/python/packages/flet-audio/src/flet_audio/types.py new file mode 100644 index 0000000000..d9220db4ad --- /dev/null +++ b/sdk/python/packages/flet-audio/src/flet_audio/types.py @@ -0,0 +1,94 @@ +from dataclasses import dataclass +from enum import Enum +from typing import TYPE_CHECKING + +import flet as ft + +if TYPE_CHECKING: + from flet_audio.audio import Audio # noqa + +__all__ = [ + "AudioDurationChangeEvent", + "AudioPositionChangeEvent", + "AudioState", + "AudioStateChangeEvent", + "ReleaseMode", +] + + +class ReleaseMode(Enum): + """The behavior of Audio player when an audio is finished or stopped.""" + + RELEASE = "release" + """ + Releases all resources, just like calling release method. + + Info: + - On Android, the media player is quite resource-intensive, and this will + let it go. Data will be buffered again when needed (if it's a remote file, + it will be downloaded again). + - On iOS and macOS, works just like + [`Audio.release()`][flet_audio.Audio.release] method. + """ + + LOOP = "loop" + """ + Keeps buffered data and plays again after completion, creating a loop. + Notice that calling stop method is not enough to release the resources + when this mode is being used. + """ + + STOP = "stop" + """ + Stops audio playback but keep all resources intact. + Use this if you intend to play again later. + """ + + +class AudioState(Enum): + """The state of the audio player.""" + + STOPPED = "stopped" + """The audio player is stopped.""" + + PLAYING = "playing" + """The audio player is currently playing audio.""" + + PAUSED = "paused" + """The audio player is paused and can be resumed.""" + + COMPLETED = "completed" + """The audio player has successfully reached the end of the audio.""" + + DISPOSED = "disposed" + """The audio player has been disposed of and should not be used anymore.""" + + +@dataclass +class AudioStateChangeEvent(ft.Event["Audio"]): + """ + Event triggered when the audio playback state changes. + """ + + state: AudioState + """The current state of the audio player.""" + + +@dataclass +class AudioPositionChangeEvent(ft.Event["Audio"]): + """ + Event triggered when the audio playback position changes. + """ + + position: int + """The current playback position in milliseconds.""" + + +@dataclass +class AudioDurationChangeEvent(ft.Event["Audio"]): + """ + Event triggered when the audio duration changes. + """ + + duration: ft.Duration + """The duration of the audio.""" diff --git a/sdk/python/packages/flet-audio/src/flutter/flet_audio/.gitignore b/sdk/python/packages/flet-audio/src/flutter/flet_audio/.gitignore new file mode 100644 index 0000000000..ed7794f2ab --- /dev/null +++ b/sdk/python/packages/flet-audio/src/flutter/flet_audio/.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-audio/src/flutter/flet_audio/.metadata b/sdk/python/packages/flet-audio/src/flutter/flet_audio/.metadata new file mode 100644 index 0000000000..07d8623a38 --- /dev/null +++ b/sdk/python/packages/flet-audio/src/flutter/flet_audio/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "2e9cb0aa71a386a91f73f7088d115c0d96654829" + channel: "stable" + +project_type: package diff --git a/sdk/python/packages/flet-audio/src/flutter/flet_audio/analysis_options.yaml b/sdk/python/packages/flet-audio/src/flutter/flet_audio/analysis_options.yaml new file mode 100644 index 0000000000..a5744c1cfb --- /dev/null +++ b/sdk/python/packages/flet-audio/src/flutter/flet_audio/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-audio/src/flutter/flet_audio/lib/flet_audio.dart b/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/flet_audio.dart new file mode 100644 index 0000000000..60323733ba --- /dev/null +++ b/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/flet_audio.dart @@ -0,0 +1,3 @@ +library flet_audio; + +export "src/extension.dart" show Extension; diff --git a/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/src/audio.dart b/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/src/audio.dart new file mode 100644 index 0000000000..4d3bc3eb8a --- /dev/null +++ b/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/src/audio.dart @@ -0,0 +1,174 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:audioplayers/audioplayers.dart'; +import 'package:flet/flet.dart'; +import 'package:flutter/foundation.dart'; + +import 'utils/audio.dart'; + +class AudioService extends FletService { + AudioService({required super.control}); + + AudioPlayer player = AudioPlayer(); + Duration? _duration; + int _position = -1; + StreamSubscription? _onDurationChangedSubscription; + StreamSubscription? _onStateChangedSubscription; + StreamSubscription? _onPositionChangedSubscription; + StreamSubscription? _onSeekCompleteSubscription; + + String? _src; + String? _srcBase64; + ReleaseMode? _releaseMode; + double? _volume; + double? _balance; + double? _playbackRate; + + @override + void init() { + super.init(); + debugPrint("Audio(${control.id}).init: ${control.properties}"); + control.addInvokeMethodListener(_invokeMethod); + + _onDurationChangedSubscription = + player.onDurationChanged.listen((duration) { + control.triggerEvent("duration_change", {"duration": duration}); + _duration = duration; + }); + + _onStateChangedSubscription = + player.onPlayerStateChanged.listen((PlayerState state) { + control.triggerEvent("state_change", {"state": state.name}); + }); + + _onPositionChangedSubscription = + player.onPositionChanged.listen((position) { + int posMs = (position.inMilliseconds / 1000).round() * 1000; + if (posMs != _position) { + _position = posMs; + } else if (position.inMilliseconds == _duration?.inMilliseconds) { + _position = _duration!.inMilliseconds; + } else { + return; + } + control.triggerEvent("position_change", {"position": posMs}); + }); + + _onSeekCompleteSubscription = player.onSeekComplete.listen((event) { + control.triggerEvent("seek_complete"); + }); + + update(); + } + + @override + void update() { + debugPrint("Audio(${control.id}).update: ${control.properties}"); + + var src = control.getString("src", "")!; + var srcBase64 = control.getString("src_base64", "")!; + if (src == "" && srcBase64 == "") { + throw Exception( + "Audio must have either \"src\" or \"src_base64\" specified."); + } + var autoplay = control.getBool("autoplay", false)!; + var volume = control.getDouble("volume", 1.0)!; + var balance = control.getDouble("balance", 0.0)!; + var playbackRate = control.getDouble("playback_rate", 1)!; + var releaseMode = parseReleaseMode(control.getString("release_mode")); + + () async { + bool srcChanged = false; + if (src != "" && src != _src) { + _src = src; + srcChanged = true; + + // URL or file? + var assetSrc = control.backend.getAssetSource(src); + if (assetSrc.isFile) { + await player.setSourceDeviceFile(assetSrc.path); + } else { + await player.setSourceUrl(assetSrc.path); + } + } else if (srcBase64 != "" && srcBase64 != _srcBase64) { + _srcBase64 = srcBase64; + srcChanged = true; + await player.setSourceBytes(base64Decode(srcBase64)); + } + + if (srcChanged) { + control.triggerEvent("loaded"); + } + + if (releaseMode != null && releaseMode != _releaseMode) { + _releaseMode = releaseMode; + await player.setReleaseMode(releaseMode); + } + + if (volume != _volume && volume >= 0 && volume <= 1) { + _volume = volume; + await player.setVolume(volume); + } + + if (playbackRate != _playbackRate) { + _playbackRate = playbackRate; + await player.setPlaybackRate(playbackRate); + } + + if (!kIsWeb && balance != _balance && balance >= -1 && balance <= 1) { + _balance = balance; + await player.setBalance(balance); + } + + if (srcChanged && autoplay) { + await player.resume(); + } + }(); + } + + Future _invokeMethod(String name, dynamic args) async { + debugPrint("Audio.$name($args)"); + switch (name) { + case "play": + final position = parseDuration(args["position"]); + if (position != null) { + await player.seek(position); + } + await player.resume(); + break; + case "resume": + await player.resume(); + break; + case "pause": + await player.pause(); + break; + case "release": + await player.release(); + break; + case "seek": + final position = parseDuration(args["position"]); + if (position != null) { + await player.seek(position); + } + break; + case "get_duration": + return await player.getDuration(); + case "get_current_position": + return await player.getCurrentPosition(); + default: + throw Exception("Unknown Audio method: $name"); + } + } + + @override + void dispose() { + debugPrint("Audio(${control.id}).dispose()"); + control.removeInvokeMethodListener(_invokeMethod); + _onDurationChangedSubscription?.cancel(); + _onStateChangedSubscription?.cancel(); + _onPositionChangedSubscription?.cancel(); + _onSeekCompleteSubscription?.cancel(); + super.dispose(); + } +} diff --git a/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/src/extension.dart b/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/src/extension.dart new file mode 100644 index 0000000000..392ae16161 --- /dev/null +++ b/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/src/extension.dart @@ -0,0 +1,15 @@ +import 'package:flet/flet.dart'; + +import 'audio.dart'; + +class Extension extends FletExtension { + @override + FletService? createService(Control control) { + switch (control.type) { + case "Audio": + return AudioService(control: control); + default: + return null; + } + } +} diff --git a/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/src/utils/audio.dart b/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/src/utils/audio.dart new file mode 100644 index 0000000000..aa6ef27e61 --- /dev/null +++ b/sdk/python/packages/flet-audio/src/flutter/flet_audio/lib/src/utils/audio.dart @@ -0,0 +1,9 @@ +import 'package:audioplayers/audioplayers.dart'; +import 'package:collection/collection.dart'; + +ReleaseMode? parseReleaseMode(String? value, [ReleaseMode? defaultValue]) { + if (value == null) return defaultValue; + return ReleaseMode.values.firstWhereOrNull( + (e) => e.name.toLowerCase() == value.toLowerCase()) ?? + defaultValue; +} diff --git a/sdk/python/packages/flet-audio/src/flutter/flet_audio/pubspec.yaml b/sdk/python/packages/flet-audio/src/flutter/flet_audio/pubspec.yaml new file mode 100644 index 0000000000..3d74b9ea27 --- /dev/null +++ b/sdk/python/packages/flet-audio/src/flutter/flet_audio/pubspec.yaml @@ -0,0 +1,23 @@ +name: flet_audio +description: Flet Audio control +version: 0.1.0 +publish_to: none + +environment: + sdk: '>=3.2.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + collection: ^1.16.0 + audioplayers: 6.5.1 + + flet: + path: ../../../../../../../packages/flet + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 diff --git a/sdk/python/packages/flet-charts/CHANGELOG.md b/sdk/python/packages/flet-charts/CHANGELOG.md new file mode 100644 index 0000000000..594bb78176 --- /dev/null +++ b/sdk/python/packages/flet-charts/CHANGELOG.md @@ -0,0 +1,75 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [0.2.0] - 2025-06-26 + +Initial release. + +### Added + +- Deployed online documentation: https://docs.flet.dev/charts/ +- New Chart control: `ScatterChart` +- New Chart control: `CandlestickChart` + +#### BarChart + +- New property: `tooltip` +- New enum: `BarChartTooltipDirection` +- In `BarChartRod`: new property `tooltip` + +#### LineChart + +- New property: `tooltip` +- In `LineChartDataPoint`: new property `tooltip` (now accepts a `LineChartDataPointTooltip` instance) + +### Changed + +All chart controls have been refactored to use `@ft.control` dataclass-style definition + +#### BarChart + +- Renamed properties: + - `bar_groups` β†’ `groups` + - `groups_space` β†’ `spacing` + - `animate` β†’ `animation` + - `on_chart_event` β†’ `on_event` +- Tooltip configuration has been redesigned: + - Removed properties: `tooltip_bgcolor`, `tooltip_rounded_radius`, `tooltip_margin`, `tooltip_padding`, `tooltip_max_content_width`, `tooltip_rotate_angle`, `tooltip_horizontal_offset`, `tooltip_border_side`, `tooltip_fit_inside_horizontally`, `tooltip_fit_inside_vertically`, `tooltip_direction` + - use the new `tooltip` property of type `BarChartTooltip` +- In `BarChartGroup`: + - Renamed properties: + - `bar_rods` β†’ `rods` + - `bars_space` β†’ `spacing` +- In `BarChartRod`: + - Renamed properties: + - `rod_stack_items` β†’ `stack_items` + - `bg_color` β†’ `bgcolor` + - `bg_gradient` β†’ `background_gradient` + +#### LineChart + +- Renamed properties: + - `animate` β†’ `animation` + - `on_chart_event` β†’ `on_event` +- `LineChart` Tooltip configuration has been redesigned: + - Removed properties: `tooltip_bgcolor`, `tooltip_rounded_radius`, `tooltip_margin`, `tooltip_padding`, `tooltip_max_content_width`, `tooltip_rotate_angle`, `tooltip_horizontal_offset`, `tooltip_border_side`, `tooltip_fit_inside_horizontally`, `tooltip_fit_inside_vertically`, `tooltip_show_on_top_of_chart_box_area` + - use the new `tooltip` property of type `LineChartTooltip` +- In `LineChartData`: + - Renamed properties: `data_points` β†’ `points`, `stroke_cap_round` β†’ `rounded_stroke_cap` + - Removed properties: `above_line_bgcolor`, `below_line_bgcolor` + - Renamed property: `selected_below_line` +- In `LineChartDataPoint`: + - Removed properties: `tooltip_align`, `tooltip_style` - use `tooltip` property instead which is now of type `LineChartDataPointTooltip` + +#### PieChart + +- Renamed properties: + - `animate` β†’ `animation` + - `on_chart_event` β†’ `on_event` + + +[0.2.0]: https://github.com/flet-dev/flet-charts/releases/tag/0.2.0 diff --git a/sdk/python/packages/flet-charts/LICENSE b/sdk/python/packages/flet-charts/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/sdk/python/packages/flet-charts/LICENSE @@ -0,0 +1,201 @@ + 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 such Contributor that are necessarily infringed 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 consequential 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-charts/README.md b/sdk/python/packages/flet-charts/README.md new file mode 100644 index 0000000000..14e9ec60a6 --- /dev/null +++ b/sdk/python/packages/flet-charts/README.md @@ -0,0 +1,50 @@ +# flet-charts + +[![pypi](https://img.shields.io/pypi/v/flet-charts.svg)](https://pypi.python.org/pypi/flet-charts) +[![downloads](https://static.pepy.tech/badge/flet-charts/month)](https://pepy.tech/project/flet-charts) +[![license](https://img.shields.io/github/license/flet-dev/flet.svg)](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-charts/LICENSE) + +A [Flet](https://flet.dev) extension for creating interactive charts and graphs. + +It is based on the [fl_chart](https://pub.dev/packages/fl_chart) Flutter package. + +## Documentation + +Detailed documentation to this package can be found [here](https://docs.flet.dev/charts/). + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | + +## Usage + +### Installation + +To install the `flet-charts` package and add it to your project dependencies: + +- Using `uv`: + ```bash + uv add flet-charts + ``` + +- Using `pip`: + ```bash + pip install flet-charts + ``` + 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/controls/charts). + +### Available charts + +- `BarChart` +- `CandlestickChart` +- `LineChart` +- `MatplotlibChart` +- `PieChart` +- `PlotlyChart` +- `ScatterChart` diff --git a/sdk/python/packages/flet-charts/pyproject.toml b/sdk/python/packages/flet-charts/pyproject.toml new file mode 100644 index 0000000000..d6410a7075 --- /dev/null +++ b/sdk/python/packages/flet-charts/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "flet-charts" +version = "0.1.0" +description = "Interactive chart controls 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://docs.flet.dev/charts" +Repository = "https://github.com/flet-dev/flet/tree/main/sdk/python/packages/flet-charts" +Issues = "https://github.com/flet-dev/flet/issues" + +[tool.setuptools.package-data] +"flutter.flet_charts" = ["**/*"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/sdk/python/packages/flet-charts/src/flet_charts/__init__.py b/sdk/python/packages/flet-charts/src/flet_charts/__init__.py new file mode 100644 index 0000000000..1dc2f4a2f9 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/__init__.py @@ -0,0 +1,103 @@ +from flet_charts.bar_chart import ( + BarChart, + BarChartEvent, + BarChartTooltip, + BarChartTooltipDirection, +) +from flet_charts.bar_chart_group import BarChartGroup +from flet_charts.bar_chart_rod import BarChartRod, BarChartRodTooltip +from flet_charts.bar_chart_rod_stack_item import BarChartRodStackItem +from flet_charts.candlestick_chart import ( + CandlestickChart, + CandlestickChartEvent, + CandlestickChartTooltip, +) +from flet_charts.candlestick_chart_spot import ( + CandlestickChartSpot, + CandlestickChartSpotTooltip, +) +from flet_charts.chart_axis import ChartAxis, ChartAxisLabel +from flet_charts.line_chart import ( + LineChart, + LineChartEvent, + LineChartEventSpot, + LineChartTooltip, +) +from flet_charts.line_chart_data import LineChartData +from flet_charts.line_chart_data_point import ( + LineChartDataPoint, + LineChartDataPointTooltip, +) +from flet_charts.matplotlib_chart import ( + MatplotlibChart, + MatplotlibChartMessageEvent, + MatplotlibChartToolbarButtonsUpdateEvent, +) +from flet_charts.matplotlib_chart_with_toolbar import MatplotlibChartWithToolbar +from flet_charts.pie_chart import PieChart, PieChartEvent +from flet_charts.pie_chart_section import PieChartSection +from flet_charts.plotly_chart import PlotlyChart +from flet_charts.scatter_chart import ( + ScatterChart, + ScatterChartEvent, + ScatterChartTooltip, +) +from flet_charts.scatter_chart_spot import ScatterChartSpot, ScatterChartSpotTooltip +from flet_charts.types import ( + ChartCirclePoint, + ChartCrossPoint, + ChartDataPointTooltip, + ChartEventType, + ChartGridLines, + ChartPointLine, + ChartPointShape, + ChartSquarePoint, + HorizontalAlignment, +) + +__all__ = [ + "BarChart", + "BarChartEvent", + "BarChartGroup", + "BarChartRod", + "BarChartRodStackItem", + "BarChartRodTooltip", + "BarChartTooltip", + "BarChartTooltipDirection", + "CandlestickChart", + "CandlestickChartEvent", + "CandlestickChartSpot", + "CandlestickChartSpotTooltip", + "CandlestickChartTooltip", + "ChartAxis", + "ChartAxisLabel", + "ChartCirclePoint", + "ChartCrossPoint", + "ChartDataPointTooltip", + "ChartEventType", + "ChartGridLines", + "ChartPointLine", + "ChartPointShape", + "ChartSquarePoint", + "HorizontalAlignment", + "LineChart", + "LineChartData", + "LineChartDataPoint", + "LineChartDataPointTooltip", + "LineChartEvent", + "LineChartEventSpot", + "LineChartTooltip", + "MatplotlibChart", + "MatplotlibChartMessageEvent", + "MatplotlibChartToolbarButtonsUpdateEvent", + "MatplotlibChartWithToolbar", + "PieChart", + "PieChartEvent", + "PieChartSection", + "PlotlyChart", + "ScatterChart", + "ScatterChartEvent", + "ScatterChartSpot", + "ScatterChartSpotTooltip", + "ScatterChartTooltip", +] diff --git a/sdk/python/packages/flet-charts/src/flet_charts/bar_chart.py b/sdk/python/packages/flet-charts/src/flet_charts/bar_chart.py new file mode 100644 index 0000000000..bd7416f84e --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/bar_chart.py @@ -0,0 +1,274 @@ +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Optional + +import flet as ft +from flet_charts.bar_chart_group import BarChartGroup +from flet_charts.chart_axis import ChartAxis +from flet_charts.types import ChartEventType, ChartGridLines, HorizontalAlignment + +__all__ = [ + "BarChart", + "BarChartEvent", + "BarChartTooltip", + "BarChartTooltipDirection", +] + + +class BarChartTooltipDirection(Enum): + """Controls showing tooltip on top or bottom.""" + + AUTO = "auto" + """Tooltip shows on top if value is positive, on bottom if value is negative.""" + + TOP = "top" + """Tooltip always shows on top.""" + + BOTTOM = "bottom" + """Tooltip always shows on bottom.""" + + +@dataclass +class BarChartTooltip: + """Configuration of the tooltip for [`BarChart`][(p).]s.""" + + bgcolor: ft.ColorValue = ft.Colors.SECONDARY + """ + Background color of tooltips. + """ + + border_radius: Optional[ft.BorderRadiusValue] = None + """ + The border radius of the tooltip. + """ + + margin: ft.Number = 16 + """ + Applies a bottom margin for showing tooltip on top of rods. + """ + + padding: ft.PaddingValue = field( + default_factory=lambda: ft.Padding.symmetric(vertical=8, horizontal=16) + ) + """ + Applies a padding for showing contents inside the tooltip. + """ + + max_width: Optional[ft.Number] = None + """ + Restricts the tooltip's width. + """ + + rotation: ft.Number = 0.0 + """ + The rotation angle of the tooltip. + """ + + horizontal_offset: ft.Number = 0.0 + """ + The horizontal offset of this tooltip. + """ + + border_side: Optional[ft.BorderSide] = None + """ + The tooltip border side. + """ + + fit_inside_horizontally: bool = False + """ + Forces the tooltip to shift horizontally inside the chart, if overflow happens. + """ + + fit_inside_vertically: bool = False + """ + Forces the tooltip to shift vertically inside the chart, if overflow happens. + """ + + direction: BarChartTooltipDirection = BarChartTooltipDirection.AUTO + """ + Defines the direction of this tooltip. + """ + + horizontal_alignment: HorizontalAlignment = HorizontalAlignment.CENTER + """ + Defines the horizontal alignment of this tooltip. + """ + + def copy( + self, + *, + bgcolor: Optional[ft.ColorValue] = None, + border_radius: Optional[ft.BorderRadiusValue] = None, + margin: Optional[ft.Number] = None, + padding: Optional[ft.PaddingValue] = None, + max_width: Optional[ft.Number] = None, + rotation: Optional[ft.Number] = None, + horizontal_offset: Optional[ft.Number] = None, + border_side: Optional[ft.BorderSide] = None, + fit_inside_horizontally: Optional[bool] = None, + fit_inside_vertically: Optional[bool] = None, + direction: Optional[BarChartTooltipDirection] = None, + horizontal_alignment: Optional[HorizontalAlignment] = None, + ) -> "BarChartTooltip": + """ + Returns a copy of this object with the specified properties overridden. + """ + return BarChartTooltip( + bgcolor=bgcolor if bgcolor is not None else self.bgcolor, + border_radius=border_radius + if border_radius is not None + else self.border_radius, + margin=margin if margin is not None else self.margin, + padding=padding if padding is not None else self.padding, + max_width=max_width if max_width is not None else self.max_width, + rotation=rotation if rotation is not None else self.rotation, + horizontal_offset=horizontal_offset + if horizontal_offset is not None + else self.horizontal_offset, + border_side=border_side if border_side is not None else self.border_side, + fit_inside_horizontally=fit_inside_horizontally + if fit_inside_horizontally is not None + else self.fit_inside_horizontally, + fit_inside_vertically=fit_inside_vertically + if fit_inside_vertically is not None + else self.fit_inside_vertically, + direction=direction if direction is not None else self.direction, + horizontal_alignment=horizontal_alignment + if horizontal_alignment is not None + else self.horizontal_alignment, + ) + + +@dataclass +class BarChartEvent(ft.Event["BarChart"]): + type: ChartEventType + """ + The type of event that occurred on the chart. + """ + + group_index: Optional[int] = None + """ + Bar's index or `-1` if chart is hovered or clicked outside of any bar. + """ + + rod_index: Optional[int] = None + """ + Rod's index or `-1` if chart is hovered or clicked outside of any bar. + """ + + stack_item_index: Optional[int] = None + """ + Stack item's index or `-1` if chart is hovered or clicked outside of any bar. + """ + + +@ft.control("BarChart") +class BarChart(ft.LayoutControl): + """ + Draws a bar chart. + """ + + groups: list[BarChartGroup] = field(default_factory=list) + """ + The list of [`BarChartGroup`][(p).]s to draw. + """ + + group_spacing: ft.Number = 16.0 + """ + A amount of space between bar [`groups`][..]. + """ + + group_alignment: ft.MainAxisAlignment = ft.MainAxisAlignment.SPACE_EVENLY + """ + A alignment of the bar [`groups`][..] within this chart. + + If set to [`MainAxisAlignment.CENTER`][flet.MainAxisAlignment.CENTER], + the space between the `groups` can be specified using [`group_spacing`][..]. + """ + + animation: ft.AnimationValue = field( + default_factory=lambda: ft.Animation( + duration=ft.Duration(milliseconds=150), curve=ft.AnimationCurve.LINEAR + ) + ) + """ + Controls chart implicit animation. + """ + + interactive: bool = True + """ + Enables automatic tooltips when hovering chart bars. + """ + + bgcolor: Optional[ft.ColorValue] = None + """ + Background color of the chart. + """ + + border: Optional[ft.Border] = None + """ + The border around the chart. + """ + + horizontal_grid_lines: Optional[ChartGridLines] = None + """ + Controls drawing of chart's horizontal lines. + """ + + vertical_grid_lines: Optional[ChartGridLines] = None + """ + Controls drawing of chart's vertical lines. + """ + + left_axis: Optional[ChartAxis] = None + """ + The appearance of the left axis, its title and labels. + """ + + top_axis: Optional[ChartAxis] = None + """ + The appearance of the top axis, its title and labels. + """ + + right_axis: Optional[ChartAxis] = None + """ + The appearance of the right axis, its title and labels. + """ + + bottom_axis: Optional[ChartAxis] = None + """ + The appearance of the bottom axis, its title and labels. + """ + + baseline_y: Optional[ft.Number] = None + """ + Baseline value for Y axis. + """ + + min_y: Optional[ft.Number] = None + """ + The minimum displayed value for Y axis. + """ + + max_y: Optional[ft.Number] = None + """ + The maximum displayed value for Y axis. + """ + + tooltip: Optional[BarChartTooltip] = field( + default_factory=lambda: BarChartTooltip() + ) + """ + The tooltip configuration for this chart. + + If set to `None`, tooltips will not shown throughout this chart. + """ + + on_event: Optional[ft.EventHandler[BarChartEvent]] = None + """ + Called when an event occurs on this chart, such as a click or hover. + """ + + def __post_init__(self, ref: Optional[ft.Ref[Any]]): + super().__post_init__(ref) + self._internals["skip_properties"] = ["tooltip"] diff --git a/sdk/python/packages/flet-charts/src/flet_charts/bar_chart_group.py b/sdk/python/packages/flet-charts/src/flet_charts/bar_chart_group.py new file mode 100644 index 0000000000..630ea5756b --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/bar_chart_group.py @@ -0,0 +1,32 @@ +from dataclasses import field +from typing import Optional + +import flet as ft +from flet_charts.bar_chart_rod import BarChartRod + +__all__ = ["BarChartGroup"] + + +@ft.control("BarChartGroup") +class BarChartGroup(ft.BaseControl): + x: int = 0 + """ + Group position on X axis. + """ + + rods: list[BarChartRod] = field(default_factory=list) + """ + The list of [`BarChartRod`][(p).] + objects to display in the group. + """ + + group_vertically: bool = False + """ + If set to `True` bar rods are drawn on top of each other; otherwise bar rods + are drawn next to each other. + """ + + spacing: Optional[ft.Number] = None + """ + The amount of space between bar rods. + """ diff --git a/sdk/python/packages/flet-charts/src/flet_charts/bar_chart_rod.py b/sdk/python/packages/flet-charts/src/flet_charts/bar_chart_rod.py new file mode 100644 index 0000000000..4486e06dd3 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/bar_chart_rod.py @@ -0,0 +1,136 @@ +from dataclasses import dataclass, field +from typing import Optional, Union + +import flet as ft +from flet_charts.bar_chart_rod_stack_item import BarChartRodStackItem +from flet_charts.types import ChartDataPointTooltip + +__all__ = ["BarChartRod", "BarChartRodTooltip"] + + +@dataclass +class BarChartRodTooltip(ChartDataPointTooltip): + """ + Tooltip configuration for the [`BarChartRod`][(p).]. + """ + + text: Optional[str] = None + """ + The text to display in the tooltip. + + When `None`, defaults to [`BarChartRod.to_y`][(p).]. + """ + + def copy( + self, + *, + text: Optional[str] = None, + text_style: Optional[ft.TextStyle] = None, + text_align: Optional[ft.TextAlign] = None, + text_spans: Optional[list[ft.TextSpan]] = None, + rtl: Optional[bool] = None, + ) -> "BarChartRodTooltip": + """ + Returns a copy of this object with the specified properties overridden. + """ + return BarChartRodTooltip( + text=text if text is not None else self.text, + text_style=text_style if text_style is not None else self.text_style, + text_align=text_align if text_align is not None else self.text_align, + text_spans=text_spans.copy() + if text_spans is not None + else (self.text_spans.copy() if self.text_spans is not None else None), + rtl=rtl if rtl is not None else self.rtl, + ) + + +@ft.control("BarChartRod") +class BarChartRod(ft.BaseControl): + """A bar rod in a [`BarChartGroup`][(p).].""" + + stack_items: list[BarChartRodStackItem] = field(default_factory=list) + """ + Optional list of [`BarChartRodStackItem`][(p).] objects to draw a stacked bar. + """ + + from_y: ft.Number = 0 + """ + Specifies a starting position of this rod on Y axis. + """ + + to_y: Optional[ft.Number] = None + """ + Specifies an ending position of this rod on Y axis. + """ + + width: Optional[ft.Number] = None + """ + The width of this rod. + """ + + color: Optional[ft.ColorValue] = None + """ + Rod color. + """ + + gradient: Optional[ft.Gradient] = None + """ + Gradient to draw rod's background. + """ + + border_radius: Optional[ft.BorderRadiusValue] = None + """ + Border radius of a bar rod. + """ + + border_side: Optional[ft.BorderSide] = None + """ + Border to draw around rod. + """ + + bg_from_y: Optional[ft.Number] = None + """ + An optional starting position of a background behind this rod. + """ + + bg_to_y: Optional[ft.Number] = None + """ + An optional ending position of a background behind this rod. + """ + + bgcolor: Optional[ft.ColorValue] = None + """ + An optional color of a background behind + this rod. + """ + + background_gradient: Optional[ft.Gradient] = None + """ + An optional gradient to draw a background with. + """ + + selected: bool = False + """ + If set to `True` a tooltip is always shown on top of the bar when + [`BarChart.interactive`][(p).] is set to `False`. + """ + + tooltip: Union[BarChartRodTooltip, str] = field( + default_factory=lambda: BarChartRodTooltip() + ) + """ + The rod's tooltip configuration for this rod. + """ + + show_tooltip: bool = True + """ + Whether a tooltip should be shown on top of hovered bar. + """ + + def before_update(self): + super().before_update() + self._internals["tooltip"] = ( + BarChartRodTooltip(text=self.tooltip) + if isinstance(self.tooltip, str) + else self.tooltip + ) diff --git a/sdk/python/packages/flet-charts/src/flet_charts/bar_chart_rod_stack_item.py b/sdk/python/packages/flet-charts/src/flet_charts/bar_chart_rod_stack_item.py new file mode 100644 index 0000000000..f8471bfe30 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/bar_chart_rod_stack_item.py @@ -0,0 +1,29 @@ +from dataclasses import field +from typing import Optional + +import flet as ft + +__all__ = ["BarChartRodStackItem"] + + +@ft.control("BarChartRodStackItem") +class BarChartRodStackItem(ft.BaseControl): + from_y: Optional[ft.Number] = None + """ + The starting position of this item inside a bar rod. + """ + + to_y: ft.Number = 0 + """ + The ending position of this item inside a bar rod. + """ + + color: Optional[ft.ColorValue] = None + """ + The color of this item. + """ + + border_side: ft.BorderSide = field(default_factory=lambda: ft.BorderSide.none()) + """ + A border around this item. + """ diff --git a/sdk/python/packages/flet-charts/src/flet_charts/candlestick_chart.py b/sdk/python/packages/flet-charts/src/flet_charts/candlestick_chart.py new file mode 100644 index 0000000000..d9e2ab324a --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/candlestick_chart.py @@ -0,0 +1,266 @@ +from dataclasses import dataclass, field +from typing import Any, Optional + +import flet as ft +from flet_charts.candlestick_chart_spot import CandlestickChartSpot +from flet_charts.chart_axis import ChartAxis +from flet_charts.types import ChartEventType, ChartGridLines, HorizontalAlignment + +__all__ = [ + "CandlestickChart", + "CandlestickChartEvent", + "CandlestickChartTooltip", +] + + +@dataclass +class CandlestickChartTooltip: + """Configuration of the tooltip for [`CandlestickChart`][(p).]s.""" + + bgcolor: ft.ColorValue = "#FF607D8B" + """ + Background color applied to the tooltip bubble. + """ + + border_radius: Optional[ft.BorderRadiusValue] = None + """ + Corner radius of the tooltip bubble. + """ + + padding: ft.PaddingValue = field( + default_factory=lambda: ft.Padding.symmetric(vertical=8, horizontal=16) + ) + """ + Padding inside the tooltip bubble. + """ + + max_width: ft.Number = 120 + """ + Maximum width of the tooltip bubble. + """ + + rotation: ft.Number = 0.0 + """ + Rotation angle (in degrees) applied to the tooltip bubble. + """ + + horizontal_offset: ft.Number = 0 + """ + Horizontal offset applied to the tooltip bubble. + """ + + horizontal_alignment: HorizontalAlignment = HorizontalAlignment.CENTER + """ + Horizontal alignment of the tooltip relative to the tapped candlestick. + """ + + border_side: ft.BorderSide = field(default_factory=lambda: ft.BorderSide.none()) + """ + The tooltip bubble border. + """ + + fit_inside_horizontally: bool = False + """ + Forces the tooltip bubble to remain inside the chart horizontally. + """ + + fit_inside_vertically: bool = False + """ + Forces the tooltip bubble to remain inside the chart vertically. + """ + + show_on_top_of_chart_box_area: bool = False + """ + When set to `True`, the tooltip is drawn at the top of the chart box. + """ + + def copy( + self, + *, + bgcolor: Optional[ft.ColorValue] = None, + border_radius: Optional[ft.BorderRadiusValue] = None, + padding: Optional[ft.PaddingValue] = None, + max_width: Optional[ft.Number] = None, + rotation: Optional[ft.Number] = None, + horizontal_offset: Optional[ft.Number] = None, + horizontal_alignment: Optional[HorizontalAlignment] = None, + border_side: Optional[ft.BorderSide] = None, + fit_inside_horizontally: Optional[bool] = None, + fit_inside_vertically: Optional[bool] = None, + show_on_top_of_chart_box_area: Optional[bool] = None, + ) -> "CandlestickChartTooltip": + """ + Returns a copy of this object with the specified properties overridden. + """ + return CandlestickChartTooltip( + bgcolor=bgcolor if bgcolor is not None else self.bgcolor, + border_radius=border_radius + if border_radius is not None + else self.border_radius, + padding=padding if padding is not None else self.padding, + max_width=max_width if max_width is not None else self.max_width, + rotation=rotation if rotation is not None else self.rotation, + horizontal_offset=horizontal_offset + if horizontal_offset is not None + else self.horizontal_offset, + horizontal_alignment=horizontal_alignment + if horizontal_alignment is not None + else self.horizontal_alignment, + border_side=border_side if border_side is not None else self.border_side, + fit_inside_horizontally=fit_inside_horizontally + if fit_inside_horizontally is not None + else self.fit_inside_horizontally, + fit_inside_vertically=fit_inside_vertically + if fit_inside_vertically is not None + else self.fit_inside_vertically, + show_on_top_of_chart_box_area=show_on_top_of_chart_box_area + if show_on_top_of_chart_box_area is not None + else self.show_on_top_of_chart_box_area, + ) + + +@dataclass +class CandlestickChartEvent(ft.Event["CandlestickChart"]): + """Event raised for interactions with a [`CandlestickChart`][(p).].""" + + type: ChartEventType + """ + Type of pointer gesture that triggered the event. + """ + + spot_index: Optional[int] = None + """ + Index of the candlestick that was interacted with; `None` if none. + """ + + +@ft.control("CandlestickChart") +class CandlestickChart(ft.LayoutControl): + """ + Draws a candlestick chart representing OHLC values. + """ + + spots: list[CandlestickChartSpot] = field(default_factory=list) + """ + Candlesticks to display on the chart. + """ + + animation: ft.AnimationValue = field( + default_factory=lambda: ft.Animation( + duration=ft.Duration(milliseconds=150), curve=ft.AnimationCurve.LINEAR + ) + ) + """ + Controls chart implicit animations. + """ + + interactive: bool = True + """ + Enables automatic tooltips and highlighting when hovering the chart. + """ + + handle_built_in_touches: bool = True + """ + Allows the chart to manage tooltip visibility automatically. + """ + + long_press_duration: Optional[ft.DurationValue] = None + """ + The duration of a long press on the chart. + """ + + touch_spot_threshold: Optional[ft.Number] = None + """ + The distance threshold to consider a touch near a candlestick. + """ + + bgcolor: Optional[ft.ColorValue] = None + """ + Background color of the chart. + """ + + border: Optional[ft.Border] = None + """ + Border drawn around the chart. + """ + + horizontal_grid_lines: Optional[ChartGridLines] = None + """ + Horizontal grid lines configuration. + """ + + vertical_grid_lines: Optional[ChartGridLines] = None + """ + Vertical grid lines configuration. + """ + + left_axis: Optional[ChartAxis] = None + """ + Appearance of the left axis, its title and labels. + """ + + top_axis: Optional[ChartAxis] = None + """ + Appearance of the top axis, its title and labels. + """ + + right_axis: Optional[ChartAxis] = None + """ + Appearance of the right axis, its title and labels. + """ + + bottom_axis: Optional[ChartAxis] = None + """ + Appearance of the bottom axis, its title and labels. + """ + + baseline_x: Optional[ft.Number] = None + """ + Baseline value on the X axis. + """ + + min_x: Optional[ft.Number] = None + """ + Minimum value displayed on the X axis. + """ + + max_x: Optional[ft.Number] = None + """ + Maximum value displayed on the X axis. + """ + + baseline_y: Optional[ft.Number] = None + """ + Baseline value on the Y axis. + """ + + min_y: Optional[ft.Number] = None + """ + Minimum value displayed on the Y axis. + """ + + max_y: Optional[ft.Number] = None + """ + Maximum value displayed on the Y axis. + """ + + rotation_quarter_turns: ft.Number = 0 + """ + Number of quarter turns (90-degree increments) to rotate the chart. + """ + + tooltip: Optional[CandlestickChartTooltip] = field( + default_factory=lambda: CandlestickChartTooltip() + ) + """ + Tooltip configuration for the chart. + """ + + on_event: Optional[ft.EventHandler[CandlestickChartEvent]] = None + """ + Called when an event occurs on this chart. + """ + + def __post_init__(self, ref: Optional[ft.Ref[Any]]): + super().__post_init__(ref) + self._internals["skip_properties"] = ["tooltip"] diff --git a/sdk/python/packages/flet-charts/src/flet_charts/candlestick_chart_spot.py b/sdk/python/packages/flet-charts/src/flet_charts/candlestick_chart_spot.py new file mode 100644 index 0000000000..ce6e841bbc --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/candlestick_chart_spot.py @@ -0,0 +1,98 @@ +from dataclasses import dataclass, field +from typing import Optional, Union + +import flet as ft +from flet_charts.types import ChartDataPointTooltip + +__all__ = ["CandlestickChartSpot", "CandlestickChartSpotTooltip"] + + +@dataclass +class CandlestickChartSpotTooltip(ChartDataPointTooltip): + """Tooltip configuration for the [`CandlestickChartSpot`][(p).].""" + + bottom_margin: ft.Number = 8 + """ + Space between the tooltip bubble and the candlestick. + """ + + def copy( + self, + *, + text: Optional[str] = None, + text_style: Optional[ft.TextStyle] = None, + text_align: Optional[ft.TextAlign] = None, + text_spans: Optional[list[ft.TextSpan]] = None, + rtl: Optional[bool] = None, + bottom_margin: Optional[ft.Number] = None, + ) -> "CandlestickChartSpotTooltip": + """ + Returns a copy of this object with the specified properties overridden. + """ + return CandlestickChartSpotTooltip( + text=text if text is not None else self.text, + text_style=text_style if text_style is not None else self.text_style, + text_align=text_align if text_align is not None else self.text_align, + text_spans=text_spans.copy() + if text_spans is not None + else (self.text_spans.copy() if self.text_spans is not None else None), + rtl=rtl if rtl is not None else self.rtl, + bottom_margin=bottom_margin + if bottom_margin is not None + else self.bottom_margin, + ) + + +@ft.control("CandlestickChartSpot") +class CandlestickChartSpot(ft.BaseControl): + """Represents a candlestick rendered on a [`CandlestickChart`][(p).].""" + + x: ft.Number + """ + The position of the candlestick on the X axis. + """ + + open: ft.Number + """ + The open value of the candlestick. + """ + + high: ft.Number + """ + The high value of the candlestick. + """ + + low: ft.Number + """ + The low value of the candlestick. + """ + + close: ft.Number + """ + The close value of the candlestick. + """ + + selected: bool = False + """ + Whether to treat this candlestick as selected. + """ + + tooltip: Union[CandlestickChartSpotTooltip, str] = field( + default_factory=lambda: CandlestickChartSpotTooltip() + ) + """ + Tooltip configuration for this candlestick. + """ + + show_tooltip: bool = True + """ + Whether the tooltip should be shown when this candlestick is highlighted. + """ + + def before_update(self): + super().before_update() + self._internals["tooltip"] = ( + CandlestickChartSpotTooltip(text=self.tooltip) + if isinstance(self.tooltip, str) + else self.tooltip + ) diff --git a/sdk/python/packages/flet-charts/src/flet_charts/chart_axis.py b/sdk/python/packages/flet-charts/src/flet_charts/chart_axis.py new file mode 100644 index 0000000000..0bcd696e61 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/chart_axis.py @@ -0,0 +1,84 @@ +from dataclasses import field +from typing import Optional + +import flet as ft + +__all__ = ["ChartAxis", "ChartAxisLabel"] + + +@ft.control("ChartAxisLabel") +class ChartAxisLabel(ft.BaseControl): + """ + Configures a custom label for specific value. + """ + + value: Optional[ft.Number] = None + """ + A value to draw label for. + """ + + label: Optional[ft.StrOrControl] = None + """ + The label to display for the specified [`value`][..]. + """ + + +@ft.control("ChartAxis") +class ChartAxis(ft.BaseControl): + """ + Configures chart axis. + """ + + title: Optional[ft.Control] = None + """ + A `Control` to display as axis title. + """ + + title_size: ft.Number = 16 + """ + The size of title area. + """ + + show_labels: bool = True + """ + Whether to display the [`labels`][..] along the axis. + If `labels` is empty then automatic labels are displayed. + """ + + labels: list[ChartAxisLabel] = field(default_factory=list) + """ + The list of [`ChartAxisLabel`][(p).] + objects to set custom axis labels for only specific values. + """ + + label_spacing: Optional[ft.Number] = None + """ + The spacing/interval between labels. + + If a value is not set, a suitable value + will be automatically calculated and used. + """ + + label_size: ft.Number = 22 + """ + The maximum space for each label in [`labels`][..]. + + Each label will stretch to fit this space. + """ + + show_min: bool = True + """ + Whether to display a label for the minimum value + independent of the sampling interval. + """ + + show_max: bool = True + """ + Whether to display a label for the maximum value + independent of the sampling interval. + """ + + def before_update(self): + super().before_update() + if self.label_spacing == 0: + raise ValueError("label_spacing cannot be 0") diff --git a/sdk/python/packages/flet-charts/src/flet_charts/line_chart.py b/sdk/python/packages/flet-charts/src/flet_charts/line_chart.py new file mode 100644 index 0000000000..8543dd44e3 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/line_chart.py @@ -0,0 +1,293 @@ +from dataclasses import dataclass, field +from typing import Any, Optional + +import flet as ft +from flet_charts.chart_axis import ChartAxis +from flet_charts.line_chart_data import LineChartData +from flet_charts.types import ChartEventType, ChartGridLines, HorizontalAlignment + +__all__ = [ + "LineChart", + "LineChartEvent", + "LineChartEventSpot", + "LineChartTooltip", +] + + +@dataclass +class LineChartEventSpot: + bar_index: int + """ + The line's index or `-1` if no line was hovered. + """ + + spot_index: int + """ + The line's point index or `-1` if no point was hovered. + """ + + def copy( + self, + *, + bar_index: Optional[int] = None, + spot_index: Optional[int] = None, + ) -> "LineChartEventSpot": + """ + Returns a copy of this object with the specified properties overridden. + """ + return LineChartEventSpot( + bar_index=bar_index if bar_index is not None else self.bar_index, + spot_index=spot_index if spot_index is not None else self.spot_index, + ) + + +@dataclass +class LineChartEvent(ft.Event["LineChart"]): + type: ChartEventType + """ + The type of event that occured. + """ + + spots: list[LineChartEventSpot] + """ + Spots on which the event occurred. + """ + + +@dataclass +class LineChartTooltip: + """Configuration of the tooltip for [`LineChart`][(p).]s.""" + + bgcolor: ft.ColorValue = "#FF607D8B" + """ + Background color of tooltip. + """ + + border_radius: Optional[ft.BorderRadiusValue] = None + """ + The tooltip's border radius. + """ + + margin: ft.Number = 16 + """ + Applies a bottom margin for showing tooltip on top of rods. + """ + + padding: ft.PaddingValue = field( + default_factory=lambda: ft.Padding.symmetric(vertical=8, horizontal=16) + ) + """ + Applies a padding for showing contents inside the tooltip. + """ + + max_width: ft.Number = 120 + """ + Restricts the tooltip's width. + """ + + rotation: ft.Number = 0.0 + """ + The tooltip's rotation angle in degrees. + """ + + horizontal_offset: ft.Number = 0.0 + """ + Applies horizontal offset for showing tooltip. + """ + + border_side: ft.BorderSide = field(default_factory=lambda: ft.BorderSide.none()) + """ + Defines the borders of this tooltip. + """ + + fit_inside_horizontally: bool = False + """ + Forces the tooltip to shift horizontally inside the chart, if overflow happens. + """ + + fit_inside_vertically: bool = False + """ + Forces the tooltip to shift vertically inside the chart, if overflow happens. + """ + + show_on_top_of_chart_box_area: bool = False + """ + Whether to force the tooltip container to top of the line. + """ + + horizontal_alignment: HorizontalAlignment = HorizontalAlignment.CENTER + """ + The horizontal alignment of this tooltip. + """ + + def copy( + self, + *, + bgcolor: Optional[ft.ColorValue] = None, + border_radius: Optional[ft.BorderRadiusValue] = None, + margin: Optional[ft.Number] = None, + padding: Optional[ft.PaddingValue] = None, + max_width: Optional[ft.Number] = None, + rotation: Optional[ft.Number] = None, + horizontal_offset: Optional[ft.Number] = None, + border_side: Optional[ft.BorderSide] = None, + fit_inside_horizontally: Optional[bool] = None, + fit_inside_vertically: Optional[bool] = None, + show_on_top_of_chart_box_area: Optional[bool] = None, + ) -> "LineChartTooltip": + """ + Returns a copy of this object with the specified properties overridden. + """ + return LineChartTooltip( + bgcolor=bgcolor if bgcolor is not None else self.bgcolor, + border_radius=border_radius + if border_radius is not None + else self.border_radius, + margin=margin if margin is not None else self.margin, + padding=padding if padding is not None else self.padding, + max_width=max_width if max_width is not None else self.max_width, + rotation=rotation if rotation is not None else self.rotation, + horizontal_offset=horizontal_offset + if horizontal_offset is not None + else self.horizontal_offset, + border_side=border_side if border_side is not None else self.border_side, + fit_inside_horizontally=fit_inside_horizontally + if fit_inside_horizontally is not None + else self.fit_inside_horizontally, + fit_inside_vertically=fit_inside_vertically + if fit_inside_vertically is not None + else self.fit_inside_vertically, + show_on_top_of_chart_box_area=show_on_top_of_chart_box_area + if show_on_top_of_chart_box_area is not None + else self.show_on_top_of_chart_box_area, + ) + + +@ft.control("LineChart") +class LineChart(ft.LayoutControl): + """ + Draws a line chart. + """ + + data_series: list[LineChartData] = field(default_factory=list) + """ + A list of [`LineChartData`][(p).] + controls drawn as separate lines on a chart. + """ + + animation: ft.AnimationValue = field( + default_factory=lambda: ft.Animation( + duration=ft.Duration(milliseconds=150), curve=ft.AnimationCurve.LINEAR + ) + ) + """ + Controls chart implicit animation. + """ + + interactive: bool = True + """ + Enables automatic tooltips and points highlighting when hovering over the chart. + """ + + point_line_start: Optional[ft.Number] = None + """ + The start of the vertical line drawn under the selected point. + + Defaults to chart's bottom edge. + """ + + point_line_end: Optional[ft.Number] = None + """ + The end of the vertical line drawn at selected point position. + + Defaults to data point's `y` value. + """ + + bgcolor: Optional[ft.ColorValue] = None + """ + Background color of the chart. + """ + + border: Optional[ft.Border] = None + """ + The border around the chart. + """ + + horizontal_grid_lines: Optional[ChartGridLines] = None + """ + Controls drawing of chart's horizontal lines. + """ + + vertical_grid_lines: Optional[ChartGridLines] = None + """ + Controls drawing of chart's vertical lines. + """ + + left_axis: Optional[ChartAxis] = None + """ + Defines the appearance of the left axis, its title and labels. + """ + + top_axis: Optional[ChartAxis] = None + """ + Defines the appearance of the top axis, its title and labels. + """ + + right_axis: Optional[ChartAxis] = None + """ + Defines the appearance of the right axis, its title and labels. + """ + + bottom_axis: Optional[ChartAxis] = None + """ + Defines the appearance of the bottom axis, its title and labels. + """ + + baseline_x: Optional[ft.Number] = None + """ + Baseline value for X axis. + """ + + min_x: Optional[ft.Number] = None + """ + Defines the minimum displayed value for X axis. + """ + + max_x: Optional[ft.Number] = None + """ + Defines the maximum displayed value for X axis. + """ + + baseline_y: Optional[ft.Number] = None + """ + Baseline value for Y axis. + """ + + min_y: Optional[ft.Number] = None + """ + Defines the minimum displayed value for Y axis. + """ + + max_y: Optional[ft.Number] = None + """ + Defines the maximum displayed value for Y axis. + """ + + tooltip: Optional[LineChartTooltip] = field( + default_factory=lambda: LineChartTooltip() + ) + """ + The tooltip configuration for this chart. + + If set to `None`, no tooltips will be shown throughout this chart. + """ + + on_event: Optional[ft.EventHandler[LineChartEvent]] = None + """ + Fires when a chart line is hovered or clicked. + """ + + def __post_init__(self, ref: Optional[ft.Ref[Any]]): + super().__post_init__(ref) + self._internals["skip_properties"] = ["tooltip"] + self._internals["skip_inherited_notifier"] = True diff --git a/sdk/python/packages/flet-charts/src/flet_charts/line_chart_data.py b/sdk/python/packages/flet-charts/src/flet_charts/line_chart_data.py new file mode 100644 index 0000000000..11689d84ab --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/line_chart_data.py @@ -0,0 +1,155 @@ +from dataclasses import field +from typing import Optional, Union + +import flet as ft +from flet_charts.line_chart_data_point import LineChartDataPoint +from flet_charts.types import ChartPointLine, ChartPointShape + +__all__ = ["LineChartData"] + + +@ft.control("LineChartData") +class LineChartData(ft.BaseControl): + points: list[LineChartDataPoint] = field(default_factory=list) + """ + A list of points (dots) of [`LineChartDataPoint`][(p).] + type representing a single chart line. + """ + + curved: bool = False + """ + Whether to draw this chart line as a curve. + """ + + color: ft.ColorValue = ft.Colors.CYAN + """ + A color of chart line. + """ + + gradient: Optional[ft.Gradient] = None + """ + Gradient to draw line's background. + """ + + stroke_width: ft.Number = 2.0 + """ + The width of a chart line. + """ + + rounded_stroke_cap: bool = False + """ + Whether to draw rounded line caps. + """ + + prevent_curve_over_shooting: bool = False + """ + Whether to prevent overshooting when draw curve line on linear sequence spots. + """ + + prevent_curve_over_shooting_threshold: ft.Number = 10.0 + """ + Threshold for [`prevent_curve_over_shooting`][..] algorithm. + """ + + dash_pattern: Optional[list[int]] = None + """ + Defines dash effect of the line. The value is a circular list of dash offsets + and lengths. For example, the list `[5, 10]` would result in dashes 5 pixels + long followed by blank spaces 10 pixels long. By default, a solid line is + drawn. + """ + + shadow: ft.BoxShadow = field( + default_factory=lambda: ft.BoxShadow(color=ft.Colors.TRANSPARENT) + ) + """ + Shadow to drop by a chart line. + """ + + above_line_bgcolor: Optional[ft.ColorValue] = None + """ + Fill the area above chart line with the specified + color. + """ + + above_line_gradient: Optional[ft.Gradient] = None + """ + Fill the area above chart line with the specified gradient. + """ + + above_line_cutoff_y: Optional[ft.Number] = None + """ + Cut off filled area above line chart at specific Y value. + """ + + above_line: Optional[ChartPointLine] = None + """ + A vertical line drawn between a line point and the top edge of the chart. + """ + + below_line_bgcolor: Optional[ft.ColorValue] = None + """ + Fill the area below chart line with the specified + color. + """ + + below_line_gradient: Optional[ft.Gradient] = None + """ + Fill the area below chart line with the specified gradient. + """ + + below_line_cutoff_y: Optional[ft.Number] = None + """ + Cut off filled area below line chart at specific Y value. + """ + + below_line: Optional[ChartPointLine] = None + """ + A vertical line drawn between a line point and the bottom edge of the chart. + """ + + selected_below_line: Union[None, bool, ChartPointLine] = None + """ + A vertical line drawn between selected line point and the bottom adge of the + chart. + + Setting this property to `True` will draw a line with default style. + """ + + point: Union[None, bool, ChartPointShape] = None + """ + Defines the appearance and shape of a line point (dot). + + Setting this property to `True` will draw a point with default style. + """ + + selected_point: Union[None, bool, ChartPointShape] = None + """ + Defines the appearance and shape of a selected line point. + """ + + curve_smoothness: ft.Number = 0.35 + """ + Defines the smoothness of a curve line, + when [`curved`][..] is set to `True`. + """ + + rounded_stroke_join: bool = False + """ + Whether to draw rounded line joins. + """ + + step_direction: Optional[ft.Number] = None + """ + Determines the direction of each step. + + If not `None`, this chart will be drawn as a + [Step Line Chart](https://docs.anychart.com/Basic_Charts/Step_Line_Chart). + + Below are some typical values: + + - `0.0`: Go to the next spot directly, with the current point's y value. + - `0.5`: Go to the half with the current spot y, and with the next spot y + for the rest. + - `1.0`: Go to the next spot y and direct line to the next spot. + """ diff --git a/sdk/python/packages/flet-charts/src/flet_charts/line_chart_data_point.py b/sdk/python/packages/flet-charts/src/flet_charts/line_chart_data_point.py new file mode 100644 index 0000000000..355930522f --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/line_chart_data_point.py @@ -0,0 +1,111 @@ +from dataclasses import dataclass, field +from typing import Optional, Union + +import flet as ft +from flet_charts.types import ChartDataPointTooltip, ChartPointLine, ChartPointShape + +__all__ = ["LineChartDataPoint", "LineChartDataPointTooltip"] + + +@dataclass +class LineChartDataPointTooltip(ChartDataPointTooltip): + """Tooltip configuration for the [`LineChartDataPoint`][(p).].""" + + text: Optional[str] = None + """ + The text to display in the tooltip. + + When `None`, defaults to [`LineChartDataPoint.y`][(p).]. + """ + + def copy( + self, + *, + text: Optional[str] = None, + text_style: Optional[ft.TextStyle] = None, + text_align: Optional[ft.TextAlign] = None, + text_spans: Optional[list[ft.TextSpan]] = None, + rtl: Optional[bool] = None, + ) -> "LineChartDataPointTooltip": + """ + Returns a copy of this object with the specified properties overridden. + """ + return LineChartDataPointTooltip( + text=text if text is not None else self.text, + text_style=text_style if text_style is not None else self.text_style, + text_align=text_align if text_align is not None else self.text_align, + text_spans=text_spans.copy() + if text_spans is not None + else (self.text_spans.copy() if self.text_spans is not None else None), + rtl=rtl if rtl is not None else self.rtl, + ) + + +@ft.control("LineChartDataPoint") +class LineChartDataPoint(ft.BaseControl): + """A [`LineChartData`][(p).] point.""" + + x: ft.Number + """ + The position of a point on `X` axis. + """ + + y: ft.Number + """ + The position of a point on `Y` axis. + """ + + selected: bool = False + """ + Draw the point as selected when [`LineChart.interactive`][(p).] + is set to `False`. + """ + + point: Union[None, bool, ChartPointShape] = None + """ + Defines the appearance and shape of a line point. + """ + + selected_point: Union[None, bool, ChartPointShape] = None + """ + Defines the appearance and shape of a selected line point. + """ + + show_above_line: bool = True + """ + Whether to display a line above data point. + """ + + show_below_line: bool = True + """ + Whether to display a line below data point. + """ + + selected_below_line: Union[None, bool, ChartPointLine] = None + """ + A vertical line drawn between selected line point and the bottom adge of the chart. + + The value is either `True` - draw a line with default style, `False` - do not draw a + line under selected point, or an instance of [`ChartPointLine`][(p).] class to + specify line style to draw. + """ + + tooltip: Union[LineChartDataPointTooltip, str] = field( + default_factory=lambda: LineChartDataPointTooltip() + ) + """ + Configuration of the tooltip for this data point. + """ + + show_tooltip: bool = True + """ + Whether the [`tooltip`][..] should be shown when this data point is hovered over. + """ + + def before_update(self): + super().before_update() + self._internals["tooltip"] = ( + LineChartDataPointTooltip(text=self.tooltip) + if isinstance(self.tooltip, str) + else self.tooltip + ) diff --git a/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_backends/backend_flet_agg.py b/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_backends/backend_flet_agg.py new file mode 100644 index 0000000000..8e2cc30267 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_backends/backend_flet_agg.py @@ -0,0 +1,16 @@ +from matplotlib import _api +from matplotlib.backends import backend_webagg_core + + +class FigureCanvasFletAgg(backend_webagg_core.FigureCanvasWebAggCore): + manager_class = _api.classproperty(lambda cls: FigureManagerFletAgg) + supports_blit = False + + +class FigureManagerFletAgg(backend_webagg_core.FigureManagerWebAgg): + _toolbar2_class = backend_webagg_core.NavigationToolbar2WebAgg + + +FigureCanvas = FigureCanvasFletAgg +FigureManager = FigureManagerFletAgg +interactive = True diff --git a/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_chart.py b/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_chart.py new file mode 100644 index 0000000000..bf917c23a9 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_chart.py @@ -0,0 +1,382 @@ +import asyncio +import logging +from dataclasses import dataclass, field +from io import BytesIO +from typing import Optional + +import flet as ft +import flet.canvas as fc + +try: + import matplotlib + from matplotlib.figure import Figure +except ImportError as e: + raise Exception( + 'Install "matplotlib" Python package to use MatplotlibChart control.' + ) from e + +__all__ = [ + "MatplotlibChart", + "MatplotlibChartMessageEvent", + "MatplotlibChartToolbarButtonsUpdateEvent", +] + +logger = logging.getLogger("flet-charts.matplotlib") + +matplotlib.use("module://flet_charts.matplotlib_backends.backend_flet_agg") + +figure_cursors = { + "default": None, + "pointer": ft.MouseCursor.CLICK, + "crosshair": ft.MouseCursor.PRECISE, + "move": ft.MouseCursor.MOVE, + "wait": ft.MouseCursor.WAIT, + "ew-resize": ft.MouseCursor.RESIZE_LEFT_RIGHT, + "ns-resize": ft.MouseCursor.RESIZE_UP_DOWN, +} + + +@dataclass +class MatplotlibChartMessageEvent(ft.Event["MatplotlibChart"]): + message: str + """ + Message text. + """ + + +@dataclass +class MatplotlibChartToolbarButtonsUpdateEvent(ft.Event["MatplotlibChart"]): + back_enabled: bool + """ + Whether Back button is enabled or not. + """ + forward_enabled: bool + """ + Whether Forward button is enabled or not. + """ + + +@ft.control(kw_only=True, isolated=True) +class MatplotlibChart(ft.GestureDetector): + """ + Displays a [Matplotlib](https://matplotlib.org/) chart. + + Warning: + This control requires the [`matplotlib`](https://matplotlib.org/) + Python package to be installed. + + See this [installation guide](index.md#installation) for more information. + """ + + figure: Figure = field(metadata={"skip": True}) + """ + Matplotlib figure to draw - an instance of + [`matplotlib.figure.Figure`](https://matplotlib.org/stable/api/_as_gen/matplotlib.figure.Figure.html#matplotlib.figure.Figure). + """ + + on_message: Optional[ft.EventHandler[MatplotlibChartMessageEvent]] = None + """ + The event is triggered on figure message update. + """ + + on_toolbar_buttons_update: Optional[ + ft.EventHandler[MatplotlibChartToolbarButtonsUpdateEvent] + ] = None + """ + Triggers when toolbar buttons status is updated. + """ + + def build(self): + self.mouse_cursor = ft.MouseCursor.WAIT + self.__started = False + self.__dpr = self.page.media.device_pixel_ratio + logger.debug(f"DPR: {self.__dpr}") + self.__image_mode = "full" + + self.canvas = fc.Canvas( + # resize_interval=10, + on_resize=self.on_canvas_resize, + expand=True, + ) + self.keyboard_listener = ft.KeyboardListener( + self.canvas, + autofocus=True, + on_key_down=self._on_key_down, + on_key_up=self._on_key_up, + ) + self.content = self.keyboard_listener + self.on_enter = self._on_enter + self.on_hover = self._on_hover + self.on_exit = self._on_exit + self.on_pan_start = self._pan_start + self.on_pan_update = self._pan_update + self.on_pan_end = self._pan_end + self.on_right_pan_start = self._right_pan_start + self.on_right_pan_update = self._right_pan_update + self.on_right_pan_end = self._right_pan_end + self.img_count = 1 + self._receive_queue = asyncio.Queue() + self._main_loop = asyncio.get_event_loop() + self._width = 0 + self._height = 0 + self._waiting = False + + def _on_key_down(self, e): + logger.debug(f"ON KEY DOWN: {e}") + + def _on_key_up(self, e): + logger.debug(f"ON KEY UP: {e}") + + def _on_enter(self, e: ft.HoverEvent): + logger.debug(f"_on_enter: {e.local_position.x}, {e.local_position.y}") + self.send_message( + { + "type": "figure_enter", + "x": e.local_position.x * self.__dpr, + "y": e.local_position.y * self.__dpr, + "button": 0, + "buttons": 0, + "modifiers": [], + } + ) + + def _on_hover(self, e: ft.HoverEvent): + logger.debug(f"_on_hover: {e.local_position.x}, {e.local_position.y}") + self.send_message( + { + "type": "motion_notify", + "x": e.local_position.x * self.__dpr, + "y": e.local_position.y * self.__dpr, + "button": 0, + "buttons": 0, + "modifiers": [], + } + ) + + def _on_exit(self, e: ft.HoverEvent): + logger.debug(f"_on_exit: {e.local_position.x}, {e.local_position.y}") + self.send_message( + { + "type": "figure_leave", + "x": e.local_position.x * self.__dpr, + "y": e.local_position.y * self.__dpr, + "button": 0, + "buttons": 0, + "modifiers": [], + } + ) + + def _pan_start(self, e: ft.DragStartEvent): + logger.debug(f"_pan_start: {e.local_position.x}, {e.local_position.y}") + asyncio.create_task(self.keyboard_listener.focus()) + self.send_message( + { + "type": "button_press", + "x": e.local_position.x * self.__dpr, + "y": e.local_position.y * self.__dpr, + "button": 0, + "buttons": 1, + "modifiers": [], + } + ) + + def _pan_update(self, e: ft.DragUpdateEvent): + logger.debug(f"_pan_update: {e.local_position.x}, {e.local_position.y}") + self.send_message( + { + "type": "motion_notify", + "x": e.local_position.x * self.__dpr, + "y": e.local_position.y * self.__dpr, + "button": 0, + "buttons": 1, + "modifiers": [], + } + ) + + def _pan_end(self, e: ft.DragEndEvent): + logger.debug(f"_pan_end: {e.local_position.x}, {e.local_position.y}") + self.send_message( + { + "type": "button_release", + "x": e.local_position.x * self.__dpr, + "y": e.local_position.y * self.__dpr, + "button": 0, + "buttons": 0, + "modifiers": [], + } + ) + + def _right_pan_start(self, e: ft.PointerEvent): + logger.debug(f"_pan_start: {e.local_position.x}, {e.local_position.y}") + self.send_message( + { + "type": "button_press", + "x": e.local_position.x * self.__dpr, + "y": e.local_position.y * self.__dpr, + "button": 2, + "buttons": 2, + "modifiers": [], + } + ) + + def _right_pan_update(self, e: ft.PointerEvent): + logger.debug(f"_pan_update: {e.local_position.x}, {e.local_position.y}") + self.send_message( + { + "type": "motion_notify", + "x": e.local_position.x * self.__dpr, + "y": e.local_position.y * self.__dpr, + "button": 0, + "buttons": 2, + "modifiers": [], + } + ) + + def _right_pan_end(self, e: ft.PointerEvent): + logger.debug(f"_pan_end: {e.local_position.x}, {e.local_position.y}") + self.send_message( + { + "type": "button_release", + "x": e.local_position.x * self.__dpr, + "y": e.local_position.y * self.__dpr, + "button": 2, + "buttons": 0, + "modifiers": [], + } + ) + + def will_unmount(self): + self.figure.canvas.manager.remove_web_socket(self) + + def home(self): + logger.debug("home)") + self.send_message({"type": "toolbar_button", "name": "home"}) + + def back(self): + logger.debug("back()") + self.send_message({"type": "toolbar_button", "name": "back"}) + + def forward(self): + logger.debug("forward)") + self.send_message({"type": "toolbar_button", "name": "forward"}) + + def pan(self): + logger.debug("pan()") + self.send_message({"type": "toolbar_button", "name": "pan"}) + + def zoom(self): + logger.debug("zoom()") + self.send_message({"type": "toolbar_button", "name": "zoom"}) + + def download(self, format): + logger.debug(f"Download in format: {format}") + buff = BytesIO() + self.figure.savefig(buff, format=format, dpi=self.figure.dpi * self.__dpr) + return buff.getvalue() + + async def _receive_loop(self): + while True: + is_binary, content = await self._receive_queue.get() + if is_binary: + logger.debug(f"receive_binary({len(content)})") + if self.__image_mode == "full": + await self.canvas.clear_capture() + + self.canvas.shapes = [ + fc.Image( + src_bytes=content, + x=0, + y=0, + width=self.figure.bbox.size[0] / self.__dpr, + height=self.figure.bbox.size[1] / self.__dpr, + ) + ] + ft.context.disable_auto_update() + self.canvas.update() + await self.canvas.capture() + self.img_count += 1 + self._waiting = False + else: + logger.debug(f"receive_json({content})") + if content["type"] == "image_mode": + self.__image_mode = content["mode"] + elif content["type"] == "cursor": + self.mouse_cursor = figure_cursors[content["cursor"]] + self.update() + elif content["type"] == "draw" and not self._waiting: + self._waiting = True + self.send_message({"type": "draw"}) + elif content["type"] == "rubberband": + if len(self.canvas.shapes) == 2: + self.canvas.shapes.pop() + if ( + content["x0"] != -1 + and content["y0"] != -1 + and content["x1"] != -1 + and content["y1"] != -1 + ): + x0 = content["x0"] / self.__dpr + y0 = self._height - content["y0"] / self.__dpr + x1 = content["x1"] / self.__dpr + y1 = self._height - content["y1"] / self.__dpr + self.canvas.shapes.append( + fc.Rect( + x=x0, + y=y0, + width=x1 - x0, + height=y1 - y0, + paint=ft.Paint( + stroke_width=1, style=ft.PaintingStyle.STROKE + ), + ) + ) + self.canvas.update() + elif content["type"] == "resize": + self.send_message({"type": "refresh"}) + elif content["type"] == "message": + await self._trigger_event( + "message", {"message": content["message"]} + ) + elif content["type"] == "history_buttons": + await self._trigger_event( + "toolbar_buttons_update", + { + "back_enabled": content["Back"], + "forward_enabled": content["Forward"], + }, + ) + + def send_message(self, message): + logger.debug(f"send_message({message})") + manager = self.figure.canvas.manager + if manager is not None: + manager.handle_json(message) + + def send_json(self, content): + logger.debug(f"send_json: {content}") + self._main_loop.call_soon_threadsafe( + lambda: self._receive_queue.put_nowait((False, content)) + ) + + def send_binary(self, blob): + self._main_loop.call_soon_threadsafe( + lambda: self._receive_queue.put_nowait((True, blob)) + ) + + async def on_canvas_resize(self, e: fc.CanvasResizeEvent): + logger.debug(f"on_canvas_resize: {e.width}, {e.height}") + + if not self.__started: + self.__started = True + asyncio.create_task(self._receive_loop()) + self.figure.canvas.manager.add_web_socket(self) + self.send_message({"type": "send_image_mode"}) + self.send_message( + {"type": "set_device_pixel_ratio", "device_pixel_ratio": self.__dpr} + ) + self.send_message({"type": "refresh"}) + self._width = e.width + self._height = e.height + self.send_message( + {"type": "resize", "width": self._width, "height": self._height} + ) diff --git a/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_chart_with_toolbar.py b/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_chart_with_toolbar.py new file mode 100644 index 0000000000..6f2f0fd657 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_chart_with_toolbar.py @@ -0,0 +1,110 @@ +from dataclasses import field + +from matplotlib.figure import Figure + +import flet as ft +import flet_charts + +_download_formats = [ + "eps", + "jpeg", + "pgf", + "pdf", + "png", + "ps", + "raw", + "svg", + "tif", + "webp", +] + + +@ft.control(kw_only=True, isolated=True) +class MatplotlibChartWithToolbar(ft.Column): + figure: Figure = field(metadata={"skip": True}) + """ + Matplotlib figure to draw - an instance of + [`matplotlib.figure.Figure`](https://matplotlib.org/stable/api/_as_gen/matplotlib.figure.Figure.html#matplotlib.figure.Figure). + """ + + def build(self): + self.mpl = flet_charts.MatplotlibChart( + figure=self.figure, + expand=True, + on_message=self.on_message, + on_toolbar_buttons_update=self.on_toolbar_update, + ) + self.home_btn = ft.IconButton(ft.Icons.HOME, on_click=lambda: self.mpl.home()) + self.back_btn = ft.IconButton( + ft.Icons.ARROW_BACK_ROUNDED, on_click=lambda: self.mpl.back() + ) + self.fwd_btn = ft.IconButton( + ft.Icons.ARROW_FORWARD_ROUNDED, on_click=lambda: self.mpl.forward() + ) + self.pan_btn = ft.IconButton( + ft.Icons.OPEN_WITH, + selected_icon=ft.Icons.OPEN_WITH, + selected_icon_color=ft.Colors.AMBER_800, + on_click=self.pan_click, + ) + self.zoom_btn = ft.IconButton( + ft.Icons.ZOOM_IN, + selected_icon=ft.Icons.ZOOM_IN, + selected_icon_color=ft.Colors.AMBER_800, + on_click=self.zoom_click, + ) + self.download_btn = ft.IconButton( + ft.Icons.DOWNLOAD, on_click=self.download_click + ) + self.download_fmt = ft.Dropdown( + value="png", + options=[ft.DropdownOption(fmt) for fmt in _download_formats], + ) + self.msg = ft.Text() + self.controls = [ + ft.Row( + [ + self.home_btn, + self.back_btn, + self.fwd_btn, + self.pan_btn, + self.zoom_btn, + self.download_btn, + self.download_fmt, + self.msg, + ] + ), + self.mpl, + ] + if not self.expand: + if not self.height: + self.height = self.figure.bbox.height + if not self.width: + self.width = self.figure.bbox.width + + def on_message(self, e: flet_charts.MatplotlibChartMessageEvent): + self.msg.value = e.message + self.msg.update() + + def on_toolbar_update( + self, e: flet_charts.MatplotlibChartToolbarButtonsUpdateEvent + ): + self.back_btn.disabled = not e.back_enabled + self.fwd_btn.disabled = not e.forward_enabled + self.update() + + def pan_click(self): + self.mpl.pan() + self.pan_btn.selected = not self.pan_btn.selected + self.zoom_btn.selected = False + + def zoom_click(self): + self.mpl.zoom() + self.pan_btn.selected = False + self.zoom_btn.selected = not self.zoom_btn.selected + + async def download_click(self): + fmt = self.download_fmt.value + buffer = self.mpl.download(fmt) + title = self.figure.canvas.manager.get_window_title() + await ft.FilePicker().save_file(file_name=f"{title}.{fmt}", src_bytes=buffer) diff --git a/sdk/python/packages/flet-charts/src/flet_charts/pie_chart.py b/sdk/python/packages/flet-charts/src/flet_charts/pie_chart.py new file mode 100644 index 0000000000..9d22229fc0 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/pie_chart.py @@ -0,0 +1,80 @@ +from dataclasses import dataclass, field +from typing import Optional + +import flet as ft +from flet_charts.pie_chart_section import PieChartSection +from flet_charts.types import ChartEventType + +__all__ = ["PieChart", "PieChartEvent"] + + +@dataclass +class PieChartEvent(ft.Event["PieChart"]): + type: ChartEventType + """ + Type of the event. + """ + + section_index: Optional[int] = None + """ + Section's index or `-1` if no section was hovered. + """ + + local_x: Optional[float] = None + """ + X coordinate of the local position where the event occurred. + """ + + local_y: Optional[float] = None + """ + Y coordinate of the local position where the event occurred. + """ + + +@ft.control("PieChart") +class PieChart(ft.LayoutControl): + """ + A pie chart control displaying multiple sections as slices of a circle. + """ + + sections: list[PieChartSection] = field(default_factory=list) + """ + A list of [`PieChartSection`][(p).] + controls drawn in a circle. + """ + + center_space_color: Optional[ft.ColorValue] = None + """ + Free space color in the middle of a chart. + """ + + center_space_radius: Optional[ft.Number] = None + """ + Free space radius in the middle of a chart. + """ + + sections_space: Optional[ft.Number] = None + """ + A gap between `sections`. + """ + + start_degree_offset: Optional[ft.Number] = None + """ + By default, `sections` are drawn from zero degree (right side of the circle) + clockwise. You can change the starting point by setting `start_degree_offset` + (in degrees). + """ + + animation: ft.AnimationValue = field( + default_factory=lambda: ft.Animation( + duration=ft.Duration(milliseconds=150), curve=ft.AnimationCurve.LINEAR + ) + ) + """ + Controls chart implicit animation. + """ + + on_event: Optional[ft.EventHandler[PieChartEvent]] = None + """ + Fires when a chart section is hovered or clicked. + """ diff --git a/sdk/python/packages/flet-charts/src/flet_charts/pie_chart_section.py b/sdk/python/packages/flet-charts/src/flet_charts/pie_chart_section.py new file mode 100644 index 0000000000..3b2e1749a6 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/pie_chart_section.py @@ -0,0 +1,91 @@ +from dataclasses import field +from typing import Optional + +import flet as ft + +__all__ = ["PieChartSection"] + + +@ft.control("PieChartSection") +class PieChartSection(ft.BaseControl): + """ + Configures a [PieChart][(p).] section. + + Raises: + AssertionError: If [`title_position`][(c).] or + [`badge_position`][(c).] is not between `0.0` and `1.0` inclusive. + """ + + value: ft.Number + """ + Determines how much the section should occupy. This depends on sum of all sections, + each section should occupy (`value` / sum of all values) * `360` degrees. + """ + + radius: Optional[ft.Number] = None + """ + External radius of the section. + """ + + color: Optional[ft.ColorValue] = None + """ + Background color of the section. + """ + + border_side: ft.BorderSide = field(default_factory=lambda: ft.BorderSide.none()) + """ + The border around section shape. + """ + + title: Optional[str] = None + """ + A title drawn at the center of the section. + """ + + title_style: Optional[ft.TextStyle] = None + """ + The style to draw `title` with. + """ + + title_position: Optional[ft.Number] = None + """ + The position/offset of the title relative to the section's center. + + By default the title is drawn in the middle of the section. + + Note: + Must be between `0.0` (near the center) + and `1.0`(near the outside of the chart) inclusive. + """ + + badge: Optional[ft.Control] = None + """ + An optional `Control` drawn in the middle of a section. + """ + + badge_position: Optional[ft.Number] = None + """ + The position/offset of the badge relative to the section's center. + + By default the badge is drawn in the middle of the section. + + Note: + Must be between `0.0` (near the center) + and `1.0`(near the outside of the chart) inclusive. + """ + + gradient: Optional[ft.Gradient] = None + """ + Defines the gradient of section. If specified, overrides the color setting. + """ + + def before_update(self): + super().before_update() + assert self.title_position is None or (0.0 <= self.title_position <= 1.0), ( + f"title_position must be between 0.0 and 1.0 inclusive, " + f"got {self.title_position}" + ) + assert self.badge_position is None or (0.0 <= self.badge_position <= 1.0), ( + f"badge_position must be between 0.0 and 1.0 inclusive, " + f"got {self.badge_position}" + ) diff --git a/sdk/python/packages/flet-charts/src/flet_charts/plotly_chart.py b/sdk/python/packages/flet-charts/src/flet_charts/plotly_chart.py new file mode 100644 index 0000000000..c370c7b83b --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/plotly_chart.py @@ -0,0 +1,58 @@ +import re +import xml.etree.ElementTree as ET +from dataclasses import field + +import flet as ft + +try: + from plotly.graph_objects import Figure +except ImportError as e: + raise Exception( + 'Install "plotly" Python package to use PlotlyChart control.' + ) from e + +__all__ = ["PlotlyChart"] + + +@ft.control(kw_only=True) +class PlotlyChart(ft.Container): + """ + Displays a [Plotly](https://plotly.com/python/) chart. + + Warning: + This control requires the [`plotly`](https://plotly.com/python/) Python + package to be installed. + + See this [installation guide](index.md#installation) for more information. + """ + + figure: Figure = field(metadata={"skip": True}) + """ + Plotly figure to draw. + + The value is an instance of [`plotly.graph_objects.Figure`](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Figure.html). + """ + + original_size: bool = False + """ + Whether to display this chart in original size. + + Set to `False` for it to fit it's configured bounds. + """ + + def init(self): + self.alignment = ft.Alignment.CENTER + self.__img = ft.Image(fit=ft.BoxFit.FILL) + self.content = self.__img + + def before_update(self): + super().before_update() + if self.figure is not None: + svg = self.figure.to_image(format="svg").decode("utf-8") + + if not self.original_size: + root = ET.fromstring(svg) + w = float(re.findall(r"\d+", root.attrib["width"])[0]) + h = float(re.findall(r"\d+", root.attrib["height"])[0]) + self.__img.aspect_ratio = w / h + self.__img.src = svg diff --git a/sdk/python/packages/flet-charts/src/flet_charts/scatter_chart.py b/sdk/python/packages/flet-charts/src/flet_charts/scatter_chart.py new file mode 100644 index 0000000000..01c24c89e8 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/scatter_chart.py @@ -0,0 +1,250 @@ +from dataclasses import dataclass, field +from typing import Any, Optional + +import flet as ft +from flet_charts.chart_axis import ChartAxis +from flet_charts.scatter_chart_spot import ScatterChartSpot +from flet_charts.types import ChartEventType, ChartGridLines, HorizontalAlignment + +__all__ = ["ScatterChart", "ScatterChartEvent", "ScatterChartTooltip"] + + +@dataclass +class ScatterChartTooltip: + """Configuration of the tooltip for [`ScatterChart`][(p).]s.""" + + bgcolor: ft.ColorValue = "#FF607D8B" + """ + The tooltip's background color. + """ + + border_radius: Optional[ft.BorderRadiusValue] = None + """ + The tooltip's border radius. + """ + + padding: ft.PaddingValue = field( + default_factory=lambda: ft.Padding.symmetric(vertical=8, horizontal=16) + ) + """ + Applies a padding for showing contents inside the tooltip. + """ + + max_width: ft.Number = 120 + """ + Restricts the tooltip's width. + """ + + rotation: ft.Number = 0.0 + """ + The tooltip's rotation angle in degrees. + """ + + horizontal_offset: ft.Number = 0 + """ + Applies horizontal offset for showing tooltip. + """ + + horizontal_alignment: HorizontalAlignment = HorizontalAlignment.CENTER + """ + The tooltip's horizontal alignment. + """ + + border_side: ft.BorderSide = field(default_factory=lambda: ft.BorderSide.none()) + """ + The tooltip's border side. + """ + + fit_inside_horizontally: bool = False + """ + Forces the tooltip to shift horizontally inside the chart, if overflow happens. + """ + + fit_inside_vertically: bool = False + """ + Forces the tooltip to shift vertically inside the chart, if overflow happens. + """ + + def copy( + self, + *, + bgcolor: Optional[ft.ColorValue] = None, + border_radius: Optional[ft.BorderRadiusValue] = None, + padding: Optional[ft.PaddingValue] = None, + max_width: Optional[ft.Number] = None, + rotation: Optional[ft.Number] = None, + horizontal_offset: Optional[ft.Number] = None, + horizontal_alignment: Optional[HorizontalAlignment] = None, + border_side: Optional[ft.BorderSide] = None, + fit_inside_horizontally: Optional[bool] = None, + fit_inside_vertically: Optional[bool] = None, + ) -> "ScatterChartTooltip": + """ + Returns a copy of this object with the specified properties overridden. + """ + return ScatterChartTooltip( + bgcolor=bgcolor if bgcolor is not None else self.bgcolor, + border_radius=border_radius + if border_radius is not None + else self.border_radius, + padding=padding if padding is not None else self.padding, + max_width=max_width if max_width is not None else self.max_width, + rotation=rotation if rotation is not None else self.rotation, + horizontal_offset=horizontal_offset + if horizontal_offset is not None + else self.horizontal_offset, + horizontal_alignment=horizontal_alignment + if horizontal_alignment is not None + else self.horizontal_alignment, + border_side=border_side if border_side is not None else self.border_side, + fit_inside_horizontally=fit_inside_horizontally + if fit_inside_horizontally is not None + else self.fit_inside_horizontally, + fit_inside_vertically=fit_inside_vertically + if fit_inside_vertically is not None + else self.fit_inside_vertically, + ) + + +@dataclass +class ScatterChartEvent(ft.Event["ScatterChart"]): + type: ChartEventType + """ + The type of the event that occurred. + """ + + spot_index: Optional[int] = None + """ + The index of the touched spot, if any. + """ + + +@ft.control("ScatterChart") +class ScatterChart(ft.LayoutControl): + """ + A scatter chart control. + + ScatterChart draws some points in a square space, + points are defined by [`ScatterChartSpot`][(p).]s. + """ + + spots: list[ScatterChartSpot] = field(default_factory=list) + """ + List of [`ScatterChartSpot`][(p).]s to show on the chart. + """ + + animation: ft.AnimationValue = field( + default_factory=lambda: ft.Animation( + duration=ft.Duration(milliseconds=150), curve=ft.AnimationCurve.LINEAR + ) + ) + """ + Controls chart implicit animation. + """ + + interactive: bool = True + """ + Enables automatic tooltips when hovering chart bars. + """ + + long_press_duration: Optional[ft.DurationValue] = None + """ + The duration of a long press on the chart. + """ + + bgcolor: Optional[ft.ColorValue] = None + """ + The chart's background color. + """ + + border: Optional[ft.Border] = None + """ + The border around the chart. + """ + + horizontal_grid_lines: Optional[ChartGridLines] = None + """ + Controls drawing of chart's horizontal lines. + """ + + vertical_grid_lines: Optional[ChartGridLines] = None + """ + Controls drawing of chart's vertical lines. + """ + + left_axis: Optional[ChartAxis] = None + """ + Configures the appearance of the left axis, its title and labels. + """ + + top_axis: Optional[ChartAxis] = None + """ + Configures the appearance of the top axis, its title and labels. + """ + + right_axis: Optional[ChartAxis] = None + """ + Configures the appearance of the right axis, its title and labels. + """ + + bottom_axis: Optional[ChartAxis] = None + """ + Configures the appearance of the bottom axis, its title and labels. + """ + + baseline_x: Optional[ft.Number] = None + """ + The baseline value for X axis. + """ + + min_x: Optional[ft.Number] = None + """ + The minimum displayed value for X axis. + """ + + max_x: Optional[ft.Number] = None + """ + The maximum displayed value for X axis. + """ + + baseline_y: Optional[ft.Number] = None + """ + Baseline value for Y axis. + """ + + min_y: Optional[ft.Number] = None + """ + The minimum displayed value for Y axis. + """ + + max_y: Optional[ft.Number] = None + """ + The maximum displayed value for Y axis. + """ + + tooltip: ScatterChartTooltip = field(default_factory=lambda: ScatterChartTooltip()) + """ + The tooltip configuration for the chart. + """ + + show_tooltips_for_selected_spots_only: bool = False + """ + Whether to permanently and only show the tooltips of spots with their + [`selected`][(p).ScatterChartSpot.selected] property set to `True`. + """ + + rotation_quarter_turns: ft.Number = 0 + """ + Number of quarter turns (90-degree increments) to rotate the chart. + Ex: `1` rotates the chart `90` degrees clockwise, + `2` rotates `180` degrees and `0` for no rotation. + """ + + on_event: Optional[ft.EventHandler[ScatterChartEvent]] = None + """ + Called when an event occurs on this chart. + """ + + def __post_init__(self, ref: Optional[ft.Ref[Any]]): + super().__post_init__(ref) + self._internals["skip_properties"] = ["tooltip"] diff --git a/sdk/python/packages/flet-charts/src/flet_charts/scatter_chart_spot.py b/sdk/python/packages/flet-charts/src/flet_charts/scatter_chart_spot.py new file mode 100644 index 0000000000..940208af50 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/scatter_chart_spot.py @@ -0,0 +1,141 @@ +from dataclasses import dataclass, field +from typing import Any, Optional, Union + +import flet as ft +from flet_charts.types import ChartDataPointTooltip, ChartPointShape + +__all__ = ["ScatterChartSpot", "ScatterChartSpotTooltip"] + + +@dataclass +class ScatterChartSpotTooltip(ChartDataPointTooltip): + """ + Tooltip configuration for the [`ScatterChartSpot`][(p).]. + """ + + text: Optional[str] = None + """ + The text to display in the tooltip. + + When `None`, defaults to [`ScatterChartSpot.y`][(p).]. + """ + + bottom_margin: ft.Number = 8 + """ + The bottom space from the spot. + """ + + def copy( + self, + *, + text: Optional[str] = None, + text_style: Optional[ft.TextStyle] = None, + text_align: Optional[ft.TextAlign] = None, + text_spans: Optional[list[ft.TextSpan]] = None, + rtl: Optional[bool] = None, + bottom_margin: Optional[float] = None, + ) -> "ScatterChartSpotTooltip": + """ + Returns a copy of this object with the specified properties overridden. + """ + return ScatterChartSpotTooltip( + text=text if text is not None else self.text, + text_style=text_style if text_style is not None else self.text_style, + text_align=text_align if text_align is not None else self.text_align, + text_spans=text_spans.copy() + if text_spans is not None + else (self.text_spans.copy() if self.text_spans is not None else None), + rtl=rtl if rtl is not None else self.rtl, + bottom_margin=bottom_margin + if bottom_margin is not None + else self.bottom_margin, + ) + + +@ft.control("ScatterChartSpot") +class ScatterChartSpot(ft.BaseControl): + """A spot on a scatter chart.""" + + x: Optional[ft.Number] = None + """ + The position of a spot on `X` axis. + """ + + y: Optional[ft.Number] = None + """ + The position of a spot on `Y` axis. + """ + + visible: bool = True + """ + Determines wether to show or hide the spot. + """ + + radius: Optional[ft.Number] = None + """ + Radius of a spot. + """ + + color: Optional[ft.ColorValue] = None + """ + Color of a spot. + """ + + render_priority: ft.Number = 0 + """ + Sort by this to manage overlap. + """ + + x_error: Optional[Any] = None + """ + Determines the error range of the data point using + [FlErrorRange](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#flerrorrange) + (which contains lowerBy and upperValue) for the `X` axis. + """ + + y_error: Optional[Any] = None + """ + Determines the error range of the data point using + [FlErrorRange](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#flerrorrange) + (which contains lowerBy and upperValue) for the `Y` axis. + """ + + selected: bool = False + """ + TBD + """ + + tooltip: Union[ScatterChartSpotTooltip, str] = field( + default_factory=lambda: ScatterChartSpotTooltip() + ) + """ + Tooltip configuration for this spot. + """ + + show_tooltip: bool = True + """ + Wether to show the tooltip. + """ + + label_text: str = "" + """ + TBD + """ + + label_text_style: ft.TextStyle = field(default_factory=lambda: ft.TextStyle()) + """ + TBD + """ + + point: Union[None, bool, ChartPointShape] = None + """ + TBD + """ + + def before_update(self): + super().before_update() + self._internals["tooltip"] = ( + ScatterChartSpotTooltip(text=self.tooltip) + if isinstance(self.tooltip, str) + else self.tooltip + ) diff --git a/sdk/python/packages/flet-charts/src/flet_charts/types.py b/sdk/python/packages/flet-charts/src/flet_charts/types.py new file mode 100644 index 0000000000..e9781be8fb --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flet_charts/types.py @@ -0,0 +1,427 @@ +from dataclasses import dataclass, field +from enum import Enum +from typing import Optional + +import flet as ft + +__all__ = [ + "ChartCirclePoint", + "ChartCrossPoint", + "ChartDataPointTooltip", + "ChartEventType", + "ChartGridLines", + "ChartPointLine", + "ChartPointShape", + "ChartSquarePoint", + "HorizontalAlignment", +] + + +@dataclass +class ChartGridLines: + """ + Configures the appearance of horizontal and vertical grid lines within the chart. + """ + + interval: Optional[ft.Number] = None + """ + The interval between grid lines. + """ + + color: Optional[ft.ColorValue] = None + """ + The color of a grid line. + """ + + width: ft.Number = 2.0 + """ + The width of a grid line. + """ + + dash_pattern: Optional[list[int]] = None + """ + Defines dash effect of the line. The value is a circular list of dash offsets + and lengths. For example, the list `[5, 10]` would result in dashes 5 pixels long + followed by blank spaces 10 pixels long. By default, a solid line is drawn. + """ + + def copy( + self, + *, + interval: Optional[ft.Number] = None, + color: Optional[ft.ColorValue] = None, + width: Optional[ft.Number] = None, + dash_pattern: Optional[list[int]] = None, + ) -> "ChartGridLines": + """ + Returns a copy of this object with the specified properties overridden. + """ + return ChartGridLines( + interval=interval if interval is not None else self.interval, + color=color if color is not None else self.color, + width=width if width is not None else self.width, + dash_pattern=dash_pattern.copy() + if dash_pattern is not None + else (self.dash_pattern.copy() if self.dash_pattern is not None else None), + ) + + +@dataclass +class ChartPointShape: + """ + Base class for chart point shapes. + + See usable subclasses: + + * [`ChartCirclePoint`][(p).] + * [`ChartCrossPoint`][(p).] + * [`ChartSquarePoint`][(p).] + """ + + _type: Optional[str] = field(init=False, repr=False, compare=False, default=None) + + +@dataclass +class ChartCirclePoint(ChartPointShape): + """Draws a circle.""" + + color: Optional[ft.ColorValue] = None + """ + The fill color to use for the circle. + """ + + radius: Optional[ft.Number] = None + """ + The radius of the circle. + """ + + stroke_color: Optional[ft.ColorValue] = None + """ + The stroke color to use for the circle + """ + + stroke_width: ft.Number = 0 + """ + The stroke width to use for the circle. + """ + + def __post_init__(self): + self._type = "ChartCirclePoint" + + def copy( + self, + *, + color: Optional[ft.ColorValue] = None, + radius: Optional[ft.Number] = None, + stroke_color: Optional[ft.ColorValue] = None, + stroke_width: Optional[ft.Number] = None, + ) -> "ChartCirclePoint": + """ + Returns a copy of this object with the specified properties overridden. + """ + return ChartCirclePoint( + color=color if color is not None else self.color, + radius=radius if radius is not None else self.radius, + stroke_color=stroke_color + if stroke_color is not None + else self.stroke_color, + stroke_width=stroke_width + if stroke_width is not None + else self.stroke_width, + ) + + +@dataclass +class ChartSquarePoint(ChartPointShape): + """Draws a square.""" + + color: Optional[ft.ColorValue] = None + """ + The fill color to use for the square. + """ + + size: ft.Number = 4.0 + """ + The size of the square. + """ + + stroke_color: Optional[ft.ColorValue] = None + """ + The stroke color to use for the square. + """ + + stroke_width: ft.Number = 1.0 + """ + The stroke width to use for the square. + """ + + def __post_init__(self): + self._type = "ChartSquarePoint" + + def copy( + self, + *, + color: Optional[ft.ColorValue] = None, + size: Optional[ft.Number] = None, + stroke_color: Optional[ft.ColorValue] = None, + stroke_width: Optional[ft.Number] = None, + ) -> "ChartSquarePoint": + """ + Returns a copy of this object with the specified properties overridden. + """ + return ChartSquarePoint( + color=color if color is not None else self.color, + size=size if size is not None else self.size, + stroke_color=stroke_color + if stroke_color is not None + else self.stroke_color, + stroke_width=stroke_width + if stroke_width is not None + else self.stroke_width, + ) + + +@dataclass +class ChartCrossPoint(ChartPointShape): + """Draws a cross-mark (X).""" + + color: Optional[ft.ColorValue] = None + """ + The fill color to use for the + cross-mark(X). + """ + + size: ft.Number = 8.0 + """ + The size of the cross-mark. + """ + + width: ft.Number = 2.0 + """ + The thickness of the cross-mark. + """ + + def __post_init__(self): + self._type = "ChartCrossPoint" + + def copy( + self, + *, + color: Optional[ft.ColorValue] = None, + size: Optional[ft.Number] = None, + width: Optional[ft.Number] = None, + ) -> "ChartCrossPoint": + """ + Returns a copy of this object with the specified properties overridden. + """ + return ChartCrossPoint( + color=color if color is not None else self.color, + size=size if size is not None else self.size, + width=width if width is not None else self.width, + ) + + +@dataclass +class ChartPointLine: + """Defines style of a line.""" + + color: Optional[ft.ColorValue] = None + """ + The line's color. + """ + + width: ft.Number = 2 + """ + The line's width. + """ + + dash_pattern: Optional[list[int]] = None + """ + The line's dash pattern. + """ + + gradient: Optional[ft.Gradient] = None + """ + The line's gradient. + """ + + def copy( + self, + *, + color: Optional[ft.ColorValue] = None, + width: Optional[ft.Number] = None, + dash_pattern: Optional[list[int]] = None, + gradient: Optional[ft.Gradient] = None, + ) -> "ChartPointLine": + """ + Returns a copy of this object with the specified properties overridden. + """ + return ChartPointLine( + color=color if color is not None else self.color, + width=width if width is not None else self.width, + dash_pattern=dash_pattern.copy() + if dash_pattern is not None + else self.dash_pattern.copy() + if self.dash_pattern is not None + else None, + gradient=gradient if gradient is not None else self.gradient, + ) + + +class ChartEventType(Enum): + """The type of event that occurred on the chart.""" + + PAN_END = "panEnd" + """ + When a pointer that was previously in contact with + the screen and moving is no longer in contact with the screen. + """ + + PAN_CANCEL = "panCancel" + """ + When the pointer that previously triggered a pan-start did not complete. + """ + + POINTER_EXIT = "pointerExit" + """ + The pointer has moved with respect to the device while the + pointer is or is not in contact with the device, and exited our chart. + """ + + LONG_PRESS_END = "longPressEnd" + """ + When a pointer stops contacting the screen after a long press + gesture was detected. Also reports the position where the + pointer stopped contacting the screen. + """ + + TAP_UP = "tapUp" + """ + When a pointer that will trigger a tap has stopped contacting the screen. + """ + + TAP_CANCEL = "tapCancel" + """ + When the pointer that previously triggered a tap-down will not end up causing a tap. + """ + + POINTER_ENTER = "pointerEnter" + """ + The pointer has moved with respect to the device while the pointer is or is + not in contact with the device, and it has entered our chart. + """ + + POINTER_HOVER = "pointerHover" + """ + The pointer has moved with respect to the device while the pointer is not + in contact with the device. + """ + + PAN_DOWN = "panDown" + """ + When a pointer has contacted the screen and might begin to move + """ + + PAN_START = "panStart" + """ + When a pointer has contacted the screen and has begun to move. + """ + + PAN_UPDATE = "panUpdate" + """ + When a pointer that is in contact with the screen and moving + has moved again. + """ + + LONG_PRESS_MOVE_UPDATE = "longPressMoveUpdate" + """ + When a pointer is moving after being held in contact at the same + location for a long period of time. Reports the new position and its offset + from the original down position. + """ + + LONG_PRESS_START = "longPressStart" + """ + When a pointer has remained in contact with the screen at the + same location for a long period of time. + """ + + TAP_DOWN = "tapDown" + """ + When a pointer that might cause a tap has contacted the + screen. + """ + + UNDEFINED = "undefined" + """ + An undefined event. + """ + + +@dataclass +class ChartDataPointTooltip: + """ + Configuration of the tooltip for data points in charts. + """ + + text: Optional[str] = None + """ + The text to display in this tooltip. + """ + + text_style: ft.TextStyle = field(default_factory=lambda: ft.TextStyle()) + """ + A text style to display tooltip with. + """ + + text_align: ft.TextAlign = ft.TextAlign.CENTER + """ + The text alignment of the tooltip. + """ + + text_spans: Optional[list[ft.TextSpan]] = None + """ + Additional text spans to show on this tooltip. + """ + + rtl: bool = False + """ + Whether the text is right-to-left. + """ + + def copy( + self, + *, + text: Optional[str] = None, + text_style: Optional[ft.TextStyle] = None, + text_align: Optional[ft.TextAlign] = None, + text_spans: Optional[list[ft.TextSpan]] = None, + rtl: Optional[bool] = None, + ) -> "ChartDataPointTooltip": + """ + Returns a copy of this object with the specified properties overridden. + """ + return ChartDataPointTooltip( + text=text if text is not None else self.text, + text_style=text_style if text_style is not None else self.text_style, + text_align=text_align if text_align is not None else self.text_align, + text_spans=text_spans.copy() + if text_spans is not None + else self.text_spans.copy() + if self.text_spans is not None + else None, + rtl=rtl if rtl is not None else self.rtl, + ) + + +class HorizontalAlignment(Enum): + """Defines an element's horizontal alignment to given point.""" + + LEFT = "left" + """Element shown on the left side of the given point.""" + + CENTER = "center" + """Element shown horizontally center aligned to a given point.""" + + RIGHT = "right" + """Element shown on the right side of the given point.""" diff --git a/sdk/python/packages/flet-charts/src/flutter/flet_charts/.gitignore b/sdk/python/packages/flet-charts/src/flutter/flet_charts/.gitignore new file mode 100644 index 0000000000..ed7794f2ab --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/.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-charts/src/flutter/flet_charts/CHANGELOG.md b/sdk/python/packages/flet-charts/src/flutter/flet_charts/CHANGELOG.md new file mode 100644 index 0000000000..2a5a5dfd04 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/CHANGELOG.md @@ -0,0 +1,3 @@ +# 0.2.0 + +Initial release of the package. diff --git a/sdk/python/packages/flet-charts/src/flutter/flet_charts/LICENSE b/sdk/python/packages/flet-charts/src/flutter/flet_charts/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/LICENSE @@ -0,0 +1,201 @@ + 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 such Contributor that are necessarily infringed 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 consequential 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-charts/src/flutter/flet_charts/analysis_options.yaml b/sdk/python/packages/flet-charts/src/flutter/flet_charts/analysis_options.yaml new file mode 100644 index 0000000000..8df683425b --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/analysis_options.yaml @@ -0,0 +1,5 @@ +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-charts/src/flutter/flet_charts/lib/flet_charts.dart b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/flet_charts.dart new file mode 100644 index 0000000000..42992b0a97 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/flet_charts.dart @@ -0,0 +1,3 @@ +library flet_flashlight; + +export 'src/extension.dart' show Extension; diff --git a/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/bar_chart.dart b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/bar_chart.dart new file mode 100644 index 0000000000..199f876fb7 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/bar_chart.dart @@ -0,0 +1,97 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +import 'utils/bar_chart.dart'; +import 'utils/charts.dart'; + +class BarChartControl extends StatefulWidget { + final Control control; + + BarChartControl({Key? key, required this.control}) + : super(key: ValueKey("control_${control.id}")); + + @override + State createState() => _BarChartControlState(); +} + +class _BarChartControlState extends State { + BarChartEventData? _eventData; + + @override + Widget build(BuildContext context) { + debugPrint("BarChart build: ${widget.control.id}"); + final theme = Theme.of(context); + + var animation = widget.control.getAnimation( + "animation", + ImplicitAnimationDetails( + duration: const Duration(milliseconds: 150), + curve: Curves.linear))!; + var border = widget.control.getBorder("border", theme); + var leftTitles = parseAxisTitles(widget.control.child("left_axis")); + var topTitles = parseAxisTitles(widget.control.child("top_axis")); + var rightTitles = parseAxisTitles(widget.control.child("right_axis")); + var bottomTitles = parseAxisTitles(widget.control.child("bottom_axis")); + var interactive = widget.control.getBool("interactive", true)!; + + List barGroups = widget.control + .children("groups") + .map((group) => parseBarChartGroupData(group, interactive, context)) + .toList(); + + var chart = BarChart( + BarChartData( + backgroundColor: widget.control.getColor("bgcolor", context), + minY: widget.control.getDouble("min_y"), + maxY: widget.control.getDouble("max_y"), + baselineY: widget.control.getDouble("baseline_y"), + titlesData: FlTitlesData( + show: (leftTitles.sideTitles.showTitles || + topTitles.sideTitles.showTitles || + rightTitles.sideTitles.showTitles || + bottomTitles.sideTitles.showTitles), + leftTitles: leftTitles, + topTitles: topTitles, + rightTitles: rightTitles, + bottomTitles: bottomTitles, + ), + borderData: FlBorderData(show: border != null, border: border), + alignment: parseBarChartAlignment( + widget.control.getMainAxisAlignment("group_alignment")?.name), + gridData: parseChartGridData( + widget.control.get("horizontal_grid_lines"), + widget.control.get("vertical_grid_lines"), + theme), + groupsSpace: widget.control.getDouble("spacing"), + barGroups: barGroups, + barTouchData: BarTouchData( + enabled: interactive, + touchTooltipData: parseBarTouchTooltipData(context, widget.control), + touchCallback: widget.control.getBool("on_event", false)! + ? (FlTouchEvent evt, BarTouchResponse? resp) { + var eventData = BarChartEventData.fromDetails(evt, resp); + if (eventData != _eventData) { + _eventData = eventData; + widget.control.triggerEvent("event", eventData.toMap()); + } + } + : null, + ), + ), + duration: animation.duration, // Optional + curve: animation.curve, + ); + + return ConstrainedControl( + control: widget.control, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return (constraints.maxHeight == double.infinity) + ? ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: chart) + : chart; + })); + } +} diff --git a/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/candlestick_chart.dart b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/candlestick_chart.dart new file mode 100644 index 0000000000..9f44a517be --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/candlestick_chart.dart @@ -0,0 +1,136 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +import 'utils/candlestick_chart.dart'; +import 'utils/charts.dart'; + +class CandlestickChartControl extends StatefulWidget { + final Control control; + + CandlestickChartControl({Key? key, required this.control}) + : super(key: ValueKey("control_${control.id}")); + + @override + State createState() => + _CandlestickChartControlState(); +} + +class _CandlestickChartControlState extends State { + CandlestickChartEventData? _eventData; + + @override + Widget build(BuildContext context) { + debugPrint("CandlestickChart build: ${widget.control.id}"); + + final theme = Theme.of(context); + final animation = widget.control.getAnimation( + "animation", + ImplicitAnimationDetails( + duration: const Duration(milliseconds: 150), + curve: Curves.linear))!; + final border = widget.control.getBorder("border", theme); + + final leftTitles = parseAxisTitles(widget.control.child("left_axis")); + final topTitles = parseAxisTitles(widget.control.child("top_axis")); + final rightTitles = parseAxisTitles(widget.control.child("right_axis")); + final bottomTitles = parseAxisTitles(widget.control.child("bottom_axis")); + + final interactive = widget.control.getBool("interactive", true)!; + final handleBuiltInTouches = + widget.control.getBool("handle_built_in_touches", true)!; + final touchSpotThreshold = widget.control.getDouble("touch_spot_threshold"); + + final spotControls = widget.control.children("spots"); + final candlestickSpots = spotControls.map((spot) { + spot.notifyParent = true; + return CandlestickSpot( + x: spot.getDouble("x", 0)!, + open: spot.getDouble("open", 0)!, + high: spot.getDouble("high", 0)!, + low: spot.getDouble("low", 0)!, + close: spot.getDouble("close", 0)!, + show: spot.visible, + ); + }).toList(); + + final selectedIndicators = spotControls + .asMap() + .entries + .where((entry) => entry.value.getBool("selected", false)!) + .map((entry) => entry.key) + .toList(); + + final showingIndicators = + (!interactive || !handleBuiltInTouches) ? selectedIndicators : []; + + final candlestickTouchData = CandlestickTouchData( + enabled: interactive && !widget.control.disabled, + handleBuiltInTouches: handleBuiltInTouches, + longPressDuration: widget.control.getDuration("long_press_duration"), + touchSpotThreshold: touchSpotThreshold, + touchTooltipData: parseCandlestickTouchTooltipData( + context, + widget.control, + spotControls, + ), + touchCallback: widget.control.getBool("on_event", false)! + ? (event, response) { + final eventData = + CandlestickChartEventData.fromDetails(event, response); + if (eventData != _eventData) { + _eventData = eventData; + widget.control.triggerEvent("event", eventData.toMap()); + } + } + : null, + ); + + final chart = CandlestickChart( + CandlestickChartData( + candlestickSpots: candlestickSpots, + backgroundColor: widget.control.getColor("bgcolor", context), + minX: widget.control.getDouble("min_x"), + maxX: widget.control.getDouble("max_x"), + baselineX: widget.control.getDouble("baseline_x"), + minY: widget.control.getDouble("min_y"), + maxY: widget.control.getDouble("max_y"), + baselineY: widget.control.getDouble("baseline_y"), + titlesData: FlTitlesData( + show: (leftTitles.sideTitles.showTitles || + topTitles.sideTitles.showTitles || + rightTitles.sideTitles.showTitles || + bottomTitles.sideTitles.showTitles), + leftTitles: leftTitles, + topTitles: topTitles, + rightTitles: rightTitles, + bottomTitles: bottomTitles, + ), + borderData: FlBorderData(show: border != null, border: border), + gridData: parseChartGridData( + widget.control.get("horizontal_grid_lines"), + widget.control.get("vertical_grid_lines"), + theme), + candlestickTouchData: candlestickTouchData, + showingTooltipIndicators: showingIndicators, + rotationQuarterTurns: + widget.control.getInt("rotation_quarter_turns", 0)!, + ), + duration: animation.duration, + curve: animation.curve, + ); + + return ConstrainedControl( + control: widget.control, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return (constraints.maxHeight == double.infinity) + ? ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: chart, + ) + : chart; + }), + ); + } +} diff --git a/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/extension.dart b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/extension.dart new file mode 100644 index 0000000000..dc35644867 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/extension.dart @@ -0,0 +1,28 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +import 'bar_chart.dart'; +import 'candlestick_chart.dart'; +import 'line_chart.dart'; +import 'pie_chart.dart'; +import 'scatter_chart.dart'; + +class Extension extends FletExtension { + @override + Widget? createWidget(Key? key, Control control) { + switch (control.type) { + case "BarChart": + return BarChartControl(key: key, control: control); + case "CandlestickChart": + return CandlestickChartControl(key: key, control: control); + case "LineChart": + return LineChartControl(key: key, control: control); + case "PieChart": + return PieChartControl(key: key, control: control); + case "ScatterChart": + return ScatterChartControl(key: key, control: control); + default: + return null; + } + } +} diff --git a/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/line_chart.dart b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/line_chart.dart new file mode 100644 index 0000000000..7d9d35173c --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/line_chart.dart @@ -0,0 +1,236 @@ +import 'package:collection/collection.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +import 'utils/charts.dart'; +import 'utils/line_chart.dart'; + +class LineChartControl extends StatefulWidget { + final Control control; + + LineChartControl({Key? key, required this.control}) + : super(key: ValueKey("control_${control.id}")); + + @override + State createState() => _LineChartControlState(); +} + +class _LineChartControlState extends State { + LineChartEventData? _eventData; + + final Map> _barSpots = {}; + + @override + void initState() { + super.initState(); + widget.control.addListener(_chartUpdated); + _chartUpdated(); + } + + @override + void dispose() { + widget.control.removeListener(_chartUpdated); + super.dispose(); + } + + _chartUpdated() { + setState(() { + for (var lineBar in widget.control.children("data_series")) { + lineBar.notifyParent = true; + List spots = []; + if (_barSpots.containsKey(lineBar.id)) { + spots = _barSpots[lineBar.id]!; + } else { + _barSpots[lineBar.id] = spots; + } + + spots.clear(); + for (var spot in lineBar.children("points")) { + spot.notifyParent = true; + spots.add(FlSpot(spot.getDouble("x")!, spot.getDouble("y")!)); + } + } + + // removed data series + for (var lineBarId in _barSpots.keys.toList()) { + if (!widget.control + .children("data_series") + .any((bar) => bar.id == lineBarId)) { + _barSpots.remove(lineBarId); + } + } + }); + } + + @override + Widget build(BuildContext context) { + debugPrint("LineChart build: ${widget.control.id}"); + final theme = Theme.of(context); + var animation = widget.control.getAnimation( + "animation", + ImplicitAnimationDetails( + duration: const Duration(milliseconds: 150), + curve: Curves.linear))!; + var border = widget.control.getBorder("border", theme); + var leftTitles = parseAxisTitles(widget.control.child("left_axis")); + var topTitles = parseAxisTitles(widget.control.child("top_axis")); + var rightTitles = parseAxisTitles(widget.control.child("right_axis")); + var bottomTitles = parseAxisTitles(widget.control.child("bottom_axis")); + var interactive = widget.control.getBool("interactive", true)!; + var pointLineStart = widget.control.getDouble("point_line_start"); + var pointLineEnd = widget.control.getDouble("point_line_end"); + + List barsData = []; + List selectedPoints = []; + + var barIndex = 0; + for (var ds in widget.control.children("data_series")) { + var barData = parseLineChartBarData( + widget.control, ds, interactive, context, _barSpots); + barsData.add(barData); + + var spotIndex = 0; + for (var p in ds.children("points")) { + if (!interactive && p.getBool("selected", false)!) { + selectedPoints + .add(LineBarSpot(barData, barIndex, barData.spots[spotIndex])); + } + spotIndex++; + } + + barIndex++; + } + + var chart = LineChart( + LineChartData( + backgroundColor: widget.control.getColor("bgcolor", context), + minX: widget.control.getDouble("min_x"), + maxX: widget.control.getDouble("max_x"), + minY: widget.control.getDouble("min_y"), + maxY: widget.control.getDouble("max_y"), + baselineX: widget.control.getDouble("baseline_x"), + baselineY: widget.control.getDouble("baseline_y"), + showingTooltipIndicators: groupBy(selectedPoints, (p) => p.x) + .values + .map((e) => ShowingTooltipIndicators(e)) + .toList(), + titlesData: FlTitlesData( + show: (leftTitles.sideTitles.showTitles || + topTitles.sideTitles.showTitles || + rightTitles.sideTitles.showTitles || + bottomTitles.sideTitles.showTitles), + leftTitles: leftTitles, + topTitles: topTitles, + rightTitles: rightTitles, + bottomTitles: bottomTitles, + ), + borderData: FlBorderData(show: border != null, border: border), + gridData: parseChartGridData( + widget.control.get("horizontal_grid_lines"), + widget.control.get("vertical_grid_lines"), + theme), + lineBarsData: barsData, + lineTouchData: LineTouchData( + enabled: interactive, + getTouchLineStart: pointLineStart != null + ? (barData, spotIndex) => pointLineStart + : defaultGetTouchLineStart, + getTouchLineEnd: pointLineEnd != null + ? (barData, spotIndex) => pointLineEnd + : defaultGetTouchLineEnd, + getTouchedSpotIndicator: + (LineChartBarData barData, List spotIndexes) { + var barIndex = interactive + ? barsData.indexWhere( + (b) => b == barData.copyWith(showingIndicators: [])) + : barsData.indexWhere((b) => b == barData); + + return spotIndexes.map((index) { + if (barIndex == -1) return null; + + var allDotsLine = parseSelectedFlLine( + widget.control + .children("data_series")[barIndex] + .get("selected_below_line"), + theme, + barData.color, + barData.gradient); + + FlLine? dotLine = parseSelectedFlLine( + widget.control + .children("data_series")[barIndex] + .children("points")[index] + .get("selected_below_line"), + theme, + barData.color, + barData.gradient); + + return TouchedSpotIndicatorData( + dotLine ?? + allDotsLine ?? + FlLine( + color: getDefaultPointColor( + 0, barData.color, barData.gradient), + strokeWidth: 3), + FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + var allDotsPainter = parseChartDotPainter( + widget.control + .children("data_series")[barIndex] + .get("selected_point"), + theme, + percent, + barData.color, + barData.gradient, + selected: true); + var dotPainter = parseChartDotPainter( + widget.control + .children("data_series")[barIndex] + .children("points")[index] + .get("selected_point"), + theme, + percent, + barData.color, + barData.gradient, + selected: true); + return dotPainter ?? + allDotsPainter ?? + getDefaultDotPainter( + percent, barData.color, barData.gradient, + selected: true); + }, + ), + ); + }).toList(); + }, + touchTooltipData: parseLineTouchTooltipData( + context, widget.control, const LineTouchTooltipData())!, + touchCallback: widget.control.getBool("on_event", false)! + ? (evt, resp) { + var eventData = LineChartEventData.fromDetails(evt, resp); + if (eventData != _eventData) { + _eventData = eventData; + widget.control.triggerEvent("event", eventData.toMap()); + } + } + : null, + )), + duration: animation.duration, // Optional + curve: animation.curve, + ); + + return ConstrainedControl( + control: widget.control, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return (constraints.maxHeight == double.infinity) + ? ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: chart, + ) + : chart; + })); + } +} diff --git a/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/pie_chart.dart b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/pie_chart.dart new file mode 100644 index 0000000000..c1d841340e --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/pie_chart.dart @@ -0,0 +1,71 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +import 'utils/pie_chart.dart'; + +class PieChartControl extends StatefulWidget { + final Control control; + + PieChartControl({Key? key, required this.control}) + : super(key: ValueKey("control_${control.id}")); + + @override + State createState() => _PieChartControlState(); +} + +class _PieChartControlState extends State { + PieChartEventData? _eventData; + + @override + Widget build(BuildContext context) { + debugPrint("PieChart build: ${widget.control.id}"); + + var animation = widget.control.getAnimation( + "animation", + ImplicitAnimationDetails( + duration: const Duration(milliseconds: 150), + curve: Curves.linear))!; + + List sections = widget.control + .children("sections") + .map((section) => parsePieChartSectionData(section, context)) + .toList(); + + Widget chart = PieChart( + PieChartData( + centerSpaceColor: + widget.control.getColor("center_space_color", context), + centerSpaceRadius: widget.control.getDouble("center_space_radius"), + sectionsSpace: widget.control.getDouble("sections_space"), + startDegreeOffset: widget.control.getDouble("start_degree_offset"), + pieTouchData: PieTouchData( + enabled: true, + touchCallback: widget.control.getBool("on_event", false)! + ? (FlTouchEvent evt, PieTouchResponse? resp) { + var eventData = PieChartEventData.fromDetails(evt, resp); + if (eventData != _eventData) { + _eventData = eventData; + widget.control.triggerEvent("event", eventData.toMap()); + } + } + : null, + ), + sections: sections, + ), + duration: animation.duration, // Optional + curve: animation.curve, + ); + + return ConstrainedControl( + control: widget.control, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return (constraints.maxHeight == double.infinity) + ? ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: chart) + : chart; + })); + } +} diff --git a/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/scatter_chart.dart b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/scatter_chart.dart new file mode 100644 index 0000000000..c4072ab9a8 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/scatter_chart.dart @@ -0,0 +1,141 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +import 'utils/charts.dart'; +import 'utils/scatter_chart.dart'; + +class ScatterChartControl extends StatefulWidget { + final Control control; + + ScatterChartControl({Key? key, required this.control}) + : super(key: ValueKey("control_${control.id}")); + + @override + State createState() => _ScatterChartControlState(); +} + +class _ScatterChartControlState extends State { + @override + Widget build(BuildContext context) { + debugPrint("ScatterChart build: ${widget.control.id}"); + + final theme = Theme.of(context); + var animation = widget.control.getAnimation( + "animation", + ImplicitAnimationDetails( + duration: const Duration(milliseconds: 150), + curve: Curves.linear))!; + var border = widget.control.getBorder("border", theme); + + var leftTitles = parseAxisTitles(widget.control.child("left_axis")); + var topTitles = parseAxisTitles(widget.control.child("top_axis")); + var rightTitles = parseAxisTitles(widget.control.child("right_axis")); + var bottomTitles = parseAxisTitles(widget.control.child("bottom_axis")); + + var interactive = widget.control.getBool("interactive", true)!; + + // Build list of ScatterSpotData + final spotsAsControls = widget.control.children('spots'); + final spots = spotsAsControls.map((spot) { + var x = spot.getDouble('x', 0)!; + var y = spot.getDouble('y', 0)!; + return ScatterSpot(x, y, + show: spot.visible, + renderPriority: spot.getInt('render_priority', 0)!, + xError: spot.get('x_error'), + yError: spot.get('y_error'), + dotPainter: spot.get("point") != null + ? parseChartDotPainter(spot.get("point"), theme, 0, null, null) + : FlDotCirclePainter( + radius: spot.getDouble("radius"), + color: spot.getColor( + "color", + context, + Colors.primaries[ + ((x * y) % Colors.primaries.length).toInt()])!, + )); + }).toList(); + + final chart = ScatterChart( + ScatterChartData( + scatterSpots: spots, + backgroundColor: widget.control.getColor("bgcolor", context), + minX: widget.control.getDouble("min_x"), + maxX: widget.control.getDouble("max_x"), + minY: widget.control.getDouble("min_y"), + maxY: widget.control.getDouble("max_y"), + baselineX: widget.control.getDouble("baseline_x"), + baselineY: widget.control.getDouble("baseline_y"), + titlesData: FlTitlesData( + show: (leftTitles.sideTitles.showTitles || + topTitles.sideTitles.showTitles || + rightTitles.sideTitles.showTitles || + bottomTitles.sideTitles.showTitles), + leftTitles: leftTitles, + topTitles: topTitles, + rightTitles: rightTitles, + bottomTitles: bottomTitles, + ), + borderData: FlBorderData(show: border != null, border: border), + gridData: parseChartGridData( + widget.control.get("horizontal_grid_lines"), + widget.control.get("vertical_grid_lines"), + theme), + scatterTouchData: ScatterTouchData( + enabled: interactive && !widget.control.disabled, + touchCallback: widget.control.getBool("on_event", false)! + ? (evt, resp) { + var eventData = ScatterChartEventData.fromDetails(evt, resp); + widget.control.triggerEvent("event", eventData.toMap()); + } + : null, + longPressDuration: widget.control.getDuration("long_press_duration"), + handleBuiltInTouches: !widget.control + .getBool("show_tooltips_for_selected_spots_only", false)!, + touchTooltipData: + parseScatterTouchTooltipData(context, widget.control, spots), + ), + scatterLabelSettings: ScatterLabelSettings( + showLabel: true, + getLabelFunction: (spotIndex, spot) { + var dp = spotsAsControls[spotIndex]; + return dp.getString("label_text", "")!; + }, + getLabelTextStyleFunction: (spotIndex, spot) { + var dp = spotsAsControls[spotIndex]; + var labelStyle = + dp.getTextStyle("label_text_style", theme, const TextStyle())!; + if (labelStyle.color == null) { + labelStyle = + labelStyle.copyWith(color: spot.dotPainter.mainColor); + } + return labelStyle; + }, + ), + showingTooltipIndicators: spotsAsControls + .asMap() + .entries + .where((e) => e.value.getBool("selected", false)!) + .map((e) => e.key) + .toList(), + rotationQuarterTurns: + widget.control.getInt('rotation_quarter_turns', 0)!, + //errorIndicatorData: widget.control.get('error_indicator_data'), + ), + duration: animation.duration, + curve: animation.curve, + ); + + return ConstrainedControl( + control: widget.control, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return (constraints.maxHeight == double.infinity) + ? ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: chart) + : chart; + })); + } +} diff --git a/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/bar_chart.dart b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/bar_chart.dart new file mode 100644 index 0000000000..d13d1aa4b9 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/bar_chart.dart @@ -0,0 +1,247 @@ +import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +import 'charts.dart'; + +class BarChartEventData extends Equatable { + final String eventType; + final int? groupIndex; + final int? rodIndex; + final int? stackItemIndex; + + const BarChartEventData({ + required this.eventType, + required this.groupIndex, + required this.rodIndex, + required this.stackItemIndex, + }); + + factory BarChartEventData.fromDetails( + FlTouchEvent event, + BarTouchResponse? response, + ) { + return BarChartEventData( + eventType: eventMap[event.runtimeType.toString()] ?? "undefined", + groupIndex: response != null && response.spot != null + ? response.spot!.touchedBarGroupIndex + : null, + rodIndex: response != null && response.spot != null + ? response.spot!.touchedRodDataIndex + : null, + stackItemIndex: response != null && response.spot != null + ? response.spot!.touchedStackItemIndex + : null, + ); + } + + Map toMap() => { + 'type': eventType, + 'group_index': groupIndex, + 'rod_index': rodIndex, + 'stack_item_index': stackItemIndex, + }; + + @override + List get props => [eventType, groupIndex, rodIndex, stackItemIndex]; +} + +TooltipDirection? parseTooltipDirection( + String? value, [ + TooltipDirection? defaultValue, +]) { + if (value == null) return defaultValue; + return TooltipDirection.values.firstWhereOrNull( + (e) => e.name.toLowerCase() == value.toLowerCase(), + ) ?? + defaultValue; +} + +BarTouchTooltipData? parseBarTouchTooltipData( + BuildContext context, + Control control, [ + BarTouchTooltipData? defaultValue, +]) { + var tooltip = control.get("tooltip"); + if (tooltip == null) return defaultValue; + + final theme = Theme.of(context); + + return BarTouchTooltipData( + getTooltipColor: (BarChartGroupData group) => + parseColor(tooltip["bgcolor"], theme, theme.colorScheme.secondary)!, + tooltipBorderRadius: parseBorderRadius(tooltip["border_radius"]), + tooltipMargin: parseDouble(tooltip["margin"], 16)!, + tooltipPadding: parsePadding( + tooltip["padding"], + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + )!, + maxContentWidth: parseDouble(tooltip["max_width"]), + rotateAngle: parseDouble(tooltip["rotation"], 0.0)!, + tooltipHorizontalOffset: parseDouble(tooltip["horizontal_offset"], 0)!, + tooltipBorder: parseBorderSide(tooltip["border_side"], theme), + fitInsideHorizontally: parseBool( + tooltip["fit_inside_horizontally"], + false, + )!, + fitInsideVertically: parseBool(tooltip["fit_inside_vertically"], false)!, + direction: parseTooltipDirection( + tooltip["direction"], + TooltipDirection.auto, + )!, + tooltipHorizontalAlignment: parseFLHorizontalAlignment( + tooltip["horizontal_alignment"], + FLHorizontalAlignment.center, + )!, + getTooltipItem: (group, groupIndex, rod, rodIndex) { + var rod = control + .children("groups")[groupIndex] + .children("rods")[rodIndex]; + return parseBarTooltipItem(rod, context); + }, + ); +} + +BarTooltipItem? parseBarTooltipItem(Control rod, BuildContext context) { + if (!rod.getBool("show_tooltip", true)!) return null; + + var tooltip = rod.internals?["tooltip"]; + if (tooltip == null) return null; + + final theme = Theme.of(context); + var tooltipTextStyle = parseTextStyle( + tooltip["text_style"], + theme, + const TextStyle(), + )!; + if (tooltipTextStyle.color == null) { + tooltipTextStyle = tooltipTextStyle.copyWith( + color: + rod.getGradient("gradient", theme)?.colors.first ?? + rod.getColor("color", context, Colors.blueGrey)!, + ); + } + return BarTooltipItem( + tooltip["text"] ?? rod.getDouble("to_y", 0)!.toString(), + tooltipTextStyle, + textAlign: parseTextAlign(tooltip["text_align"], TextAlign.center)!, + textDirection: parseBool(tooltip["rtl"], false)! + ? TextDirection.rtl + : TextDirection.ltr, + children: tooltip["text_spans"] != null + ? parseTextSpans(tooltip["text_spans"], theme, ( + s, + eventName, [ + eventData, + ]) { + s.triggerEvent(eventName, eventData); + }) + : null, + ); +} + +BarChartGroupData parseBarChartGroupData( + Control group, + bool interactiveChart, + BuildContext context, +) { + group.notifyParent = true; + return BarChartGroupData( + x: group.getInt("x", 0)!, + barsSpace: group.getDouble("spacing"), + groupVertically: group.getBool("group_vertically", false)!, + showingTooltipIndicators: group + .children("rods") + .asMap() + .entries + .where( + (rod) => !interactiveChart && rod.value.getBool("selected", false)!, + ) + .map((rod) => rod.key) + .toList(), + barRods: group + .children("rods") + .map((rod) => parseBarChartRodData(rod, interactiveChart, context)) + .toList(), + ); +} + +BarChartRodData parseBarChartRodData( + Control rod, + bool interactiveChart, + BuildContext context, +) { + rod.notifyParent = true; + + final theme = Theme.of(context); + var bgFromY = rod.getDouble("bg_from_y"); + var bgToY = rod.getDouble("bg_to_y"); + var bgcolor = rod.getColor("bgcolor", context); + var backgroundGradient = rod.getGradient("background_gradient", theme); + + return BarChartRodData( + fromY: rod.getDouble("from_y"), + toY: rod.getDouble("to_y", 0)!, + width: rod.getDouble("width"), + color: rod.getColor("color", context), + gradient: rod.getGradient("gradient", theme), + borderRadius: rod.getBorderRadius("border_radius"), + borderSide: rod.getBorderSide( + "border_side", + theme, + defaultValue: BorderSide.none, + ), + backDrawRodData: BackgroundBarChartRodData( + show: + (bgFromY != null || + bgToY != null || + bgcolor != null || + backgroundGradient != null), + fromY: bgFromY, + toY: bgToY, + color: bgcolor, + gradient: backgroundGradient, + ), + rodStackItems: rod + .children("stack_items") + .map( + (rodStackItem) => parseBarChartRodStackItem( + rodStackItem, + interactiveChart, + context, + ), + ) + .toList(), + ); +} + +BarChartRodStackItem parseBarChartRodStackItem( + Control rodStackItem, + bool interactiveChart, + BuildContext context, +) { + rodStackItem.notifyParent = true; + return BarChartRodStackItem( + rodStackItem.getDouble("from_y")!, + rodStackItem.getDouble("to_y", 0)!, + rodStackItem.getColor("color", context)!, + borderSide: rodStackItem.getBorderSide( + "border_side", + Theme.of(context), + defaultValue: BorderSide.none, + )!, + ); +} + +BarChartAlignment? parseBarChartAlignment( + String? value, [ + BarChartAlignment? defaultValue, +]) { + if (value == null) return defaultValue; + return BarChartAlignment.values.firstWhereOrNull( + (e) => e.name.toLowerCase() == value.toLowerCase(), + ) ?? + defaultValue; +} diff --git a/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/candlestick_chart.dart b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/candlestick_chart.dart new file mode 100644 index 0000000000..ce5ce38217 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/candlestick_chart.dart @@ -0,0 +1,117 @@ +import 'package:equatable/equatable.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +import 'charts.dart'; + +class CandlestickChartEventData extends Equatable { + final String eventType; + final int? spotIndex; + + const CandlestickChartEventData({ + required this.eventType, + required this.spotIndex, + }); + + factory CandlestickChartEventData.fromDetails( + FlTouchEvent event, + CandlestickTouchResponse? response, + ) { + return CandlestickChartEventData( + eventType: eventMap[event.runtimeType.toString()] ?? "undefined", + spotIndex: response?.touchedSpot?.spotIndex, + ); + } + + Map toMap() => { + "type": eventType, + "spot_index": spotIndex, + }; + + @override + List get props => [eventType, spotIndex]; +} + +CandlestickTouchTooltipData parseCandlestickTouchTooltipData( + BuildContext context, Control control, List spotControls) { + final tooltip = control.get("tooltip") ?? {}; + final theme = Theme.of(context); + + return CandlestickTouchTooltipData( + tooltipBorder: + parseBorderSide(tooltip["border_side"], theme, defaultValue: BorderSide.none)!, + rotateAngle: parseDouble(tooltip["rotation"], 0.0)!, + tooltipBorderRadius: parseBorderRadius(tooltip["border_radius"]), + tooltipPadding: parsePadding( + tooltip["padding"], const EdgeInsets.symmetric(horizontal: 16, vertical: 8))!, + tooltipHorizontalAlignment: parseFLHorizontalAlignment( + tooltip["horizontal_alignment"], FLHorizontalAlignment.center)!, + tooltipHorizontalOffset: parseDouble(tooltip["horizontal_offset"], 0)!, + maxContentWidth: parseDouble(tooltip["max_width"], 120)!, + fitInsideHorizontally: + parseBool(tooltip["fit_inside_horizontally"], false)!, + fitInsideVertically: + parseBool(tooltip["fit_inside_vertically"], false)!, + showOnTopOfTheChartBoxArea: + parseBool(tooltip["show_on_top_of_chart_box_area"], false)!, + getTooltipColor: (spot) => parseColor( + tooltip["bgcolor"], theme, const Color.fromRGBO(96, 125, 139, 1))!, + getTooltipItems: (painter, touchedSpot, spotIndex) { + if (spotIndex < 0 || spotIndex >= spotControls.length) { + return null; + } + return parseCandlestickTooltipItem( + spotControls[spotIndex], + painter, + touchedSpot, + spotIndex, + context, + ); + }, + ); +} + +CandlestickTooltipItem? parseCandlestickTooltipItem( + Control spotControl, + FlCandlestickPainter painter, + CandlestickSpot touchedSpot, + int spotIndex, + BuildContext context, +) { + if (!spotControl.getBool("show_tooltip", true)!) { + return null; + } + + final tooltip = spotControl.internals?["tooltip"]; + if (tooltip == null) { + return null; + } + + final theme = Theme.of(context); + var textStyle = + parseTextStyle(tooltip["text_style"], theme, const TextStyle())!; + if (textStyle.color == null) { + textStyle = textStyle.copyWith( + color: painter.getMainColor( + spot: touchedSpot, + spotIndex: spotIndex, + ), + ); + } + + return CandlestickTooltipItem( + tooltip["text"] ?? "", + textStyle: textStyle, + bottomMargin: parseDouble(tooltip["bottom_margin"], 8)!, + textAlign: parseTextAlign(tooltip["text_align"], TextAlign.center)!, + textDirection: + parseBool(tooltip["rtl"], false)! ? TextDirection.rtl : TextDirection.ltr, + children: tooltip["text_spans"] != null + ? parseTextSpans(tooltip["text_spans"], theme, (s, eventName, + [eventData]) { + s.triggerEvent(eventName, eventData); + }) + : null, + ); +} diff --git a/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/charts.dart b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/charts.dart new file mode 100644 index 0000000000..a6ea656cc4 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/charts.dart @@ -0,0 +1,185 @@ +import 'package:collection/collection.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +FlDotPainter invisibleDotPainter = + FlDotCirclePainter(radius: 0, strokeWidth: 0); +FlLine invisibleLine = const FlLine(strokeWidth: 0); + +FlGridData parseChartGridData( + dynamic horizontal, dynamic vertical, ThemeData theme) { + if (horizontal == null && vertical == null) { + return const FlGridData(show: false); + } + + var hLine = parseFlLine(horizontal, theme); + var vLine = parseFlLine(vertical, theme); + + return FlGridData( + show: true, + drawHorizontalLine: horizontal != null, + horizontalInterval: + horizontal != null ? parseDouble(horizontal["interval"]) : null, + getDrawingHorizontalLine: + hLine == null ? defaultGridLine : (value) => hLine, + drawVerticalLine: vertical != null, + verticalInterval: + vertical != null ? parseDouble(vertical["interval"]) : null, + getDrawingVerticalLine: vLine == null ? defaultGridLine : (value) => vLine, + ); +} + +FlLine? parseFlLine(dynamic value, ThemeData theme, [FlLine? defaultValue]) { + if (value == null || + (value['color'] == null && + value['width'] == null && + value['gradient'] == null && + value['dash_pattern'] == null)) { + return defaultValue; + } + + return FlLine( + color: parseColor(value['color'], theme, Colors.black)!, + strokeWidth: parseDouble(value['width'], 2)!, + gradient: parseGradient(value['gradient'], theme), + dashArray: (value['dash_pattern'] as List?) + ?.map((e) => parseInt(e)) + .nonNulls + .toList()); +} + +FlLine? parseSelectedFlLine( + dynamic value, ThemeData theme, Color? color, Gradient? gradient, + [FlLine? defaultValue]) { + if (value == null) return defaultValue; + + if (value == false) { + return invisibleLine; + } else if (value == true) { + return FlLine( + color: getDefaultPointColor(0, color, gradient), strokeWidth: 3); + } + + return parseFlLine(value, theme, defaultValue)?.copyWith( + color: parseColor( + value['color'], theme, defaultGetDotStrokeColor(0, color, gradient))); +} + +FlDotPainter? parseChartDotPainter(dynamic value, ThemeData theme, + double percentage, Color? barColor, Gradient? barGradient, + {FlDotPainter? defaultValue, bool selected = false}) { + if (value == null) { + return defaultValue; + } else if (value == false) { + return invisibleDotPainter; + } else if (value == true) { + return getDefaultDotPainter(percentage, barColor, barGradient, + selected: selected); + } + var type = value["_type"]; + var strokeWidth = parseDouble(value["stroke_width"]); + var size = parseDouble(value["size"]); + var color = parseColor(value['color'], theme); + var strokeColor = parseColor(value['stroke_color'], theme, + defaultGetDotStrokeColor(percentage, barColor, barGradient))!; + + if (type == "ChartCirclePoint") { + return FlDotCirclePainter( + color: color ?? getDefaultPointColor(percentage, barColor, barGradient), + radius: parseDouble(value["radius"]), + strokeColor: strokeColor, + strokeWidth: strokeWidth ?? 0.0); + } else if (type == "ChartSquarePoint") { + return FlDotSquarePainter( + color: color ?? getDefaultPointColor(percentage, barColor, barGradient), + size: size ?? 4.0, + strokeColor: strokeColor, + strokeWidth: strokeWidth ?? 1.0); + } else if (type == "ChartCrossPoint") { + return FlDotCrossPainter( + color: + color ?? defaultGetDotStrokeColor(percentage, barColor, barGradient), + size: size ?? 8.0, + width: parseDouble(value["width"], 2.0)!, + ); + } + return defaultValue; +} + +FlDotPainter getDefaultDotPainter( + double percentage, Color? barColor, Gradient? barGradient, + {bool selected = false}) { + return FlDotCirclePainter( + radius: selected ? 8 : 4, + strokeWidth: selected ? 2 : 1, + color: getDefaultPointColor(percentage, barColor, barGradient), + strokeColor: defaultGetDotStrokeColor(percentage, barColor, barGradient), + ); +} + +Color getDefaultPointColor( + double percentage, Color? barColor, Gradient? barGradient) { + if (barGradient != null && barGradient is LinearGradient) { + return lerpGradient( + barGradient.colors, barGradient.getSafeColorStops(), percentage / 100); + } + return barGradient?.colors.first ?? barColor ?? Colors.blueGrey; +} + +Color defaultGetDotStrokeColor(double percentage, + [Color? barColor, Gradient? barGradient]) { + Color color = getDefaultPointColor(percentage, barColor, barGradient); + return color.darken(); +} + +AxisTitles parseAxisTitles(Control? control) { + if (control == null) { + return const AxisTitles(sideTitles: SideTitles(showTitles: false)); + } + + return AxisTitles( + axisNameWidget: control.buildWidget("title"), + axisNameSize: control.getDouble("title_size", 16)!, + sideTitles: SideTitles( + showTitles: control.getBool("show_labels", true)!, + reservedSize: control.getDouble("label_size", 22)!, + interval: control.getDouble("label_spacing"), + minIncluded: control.getBool("show_min", true)!, + maxIncluded: control.getBool("show_max", true)!, + getTitlesWidget: control.children("labels").isEmpty + ? defaultGetTitle + : (double value, TitleMeta meta) { + var label = control + .children("labels") + .firstWhereOrNull((l) => l.getDouble("value") == value); + return label?.buildTextOrWidget("label") ?? + const SizedBox.shrink(); + }, + )); +} + +FLHorizontalAlignment? parseFLHorizontalAlignment(String? value, + [FLHorizontalAlignment? defaultValue]) { + if (value == null) return defaultValue; + return FLHorizontalAlignment.values.firstWhereOrNull( + (e) => e.name.toLowerCase() == value.toLowerCase()) ?? + defaultValue; +} + +const eventMap = { + "FlPointerEnterEvent": "pointerEnter", + "FlPointerExitEvent": "pointerExit", + "FlPointerHoverEvent": "pointerHover", + "FlPanCancelEvent": "panCancel", + "FlPanDownEvent": "panDown", + "FlPanEndEvent": "panEnd", + "FlPanStartEvent": "panStart", + "FlPanUpdateEvent": "panUpdate", + "FlLongPressEnd": "longPressEnd", + "FlLongPressMoveUpdate": "longPressMoveUpdate", + "FlLongPressStart": "longPressStart", + "FlTapCancelEvent": "tapCancel", + "FlTapDownEvent": "tapDown", + "FlTapUpEvent": "tapUp", +}; diff --git a/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/line_chart.dart b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/line_chart.dart new file mode 100644 index 0000000000..1174674c84 --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/line_chart.dart @@ -0,0 +1,220 @@ +import 'package:equatable/equatable.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +import 'charts.dart'; + +class LineChartEventData extends Equatable { + final String eventType; + final List barSpots; + + const LineChartEventData({required this.eventType, required this.barSpots}); + + factory LineChartEventData.fromDetails( + FlTouchEvent event, LineTouchResponse? response) { + return LineChartEventData( + eventType: eventMap[event.runtimeType.toString()] ?? "undefined", + barSpots: response != null && response.lineBarSpots != null + ? response.lineBarSpots! + .map((bs) => LineChartEventDataSpot( + barIndex: bs.barIndex, spotIndex: bs.spotIndex)) + .toList() + : []); + } + + Map toMap() => { + 'type': eventType, + 'spots': barSpots, + }; + + @override + List get props => [eventType, barSpots]; +} + +class LineChartEventDataSpot extends Equatable { + final int barIndex; + final int spotIndex; + + const LineChartEventDataSpot( + {required this.barIndex, required this.spotIndex}); + + Map toMap() => { + 'bar_index': barIndex, + 'spot_index': spotIndex, + }; + + @override + List get props => [barIndex, spotIndex]; +} + +LineTooltipItem? parseLineTooltipItem( + Control dataPoint, LineBarSpot spot, BuildContext context) { + if (!dataPoint.getBool("show_tooltip", true)!) return null; + + var tooltip = dataPoint.internals?["tooltip"]; + if (tooltip == null) return null; + + final theme = Theme.of(context); + var style = parseTextStyle(tooltip["text_style"], theme, const TextStyle())!; + if (style.color == null) { + style = style.copyWith( + color: spot.bar.gradient?.colors.first ?? + spot.bar.color ?? + Colors.blueGrey); + } + return LineTooltipItem( + tooltip["text"] ?? dataPoint.getDouble("y", 0)!.toString(), style, + textAlign: parseTextAlign(tooltip["text_align"], TextAlign.center)!, + textDirection: parseBool(tooltip["rtl"], false)! + ? TextDirection.rtl + : TextDirection.ltr, + children: tooltip["text_spans"] != null + ? parseTextSpans(tooltip["text_spans"], theme, (s, eventName, + [eventData]) { + s.triggerEvent(eventName, eventData); + }) + : null); +} + +LineTouchTooltipData? parseLineTouchTooltipData( + BuildContext context, Control control, + [LineTouchTooltipData? defaultValue]) { + final tooltip = control.get("tooltip"); + if (tooltip == null) return defaultValue; + + final theme = Theme.of(context); + + return LineTouchTooltipData( + getTooltipColor: (LineBarSpot spot) => parseColor( + tooltip["bgcolor"], theme, const Color.fromRGBO(96, 125, 139, 1))!, + tooltipBorderRadius: parseBorderRadius(tooltip["border_radius"]), + tooltipMargin: parseDouble(tooltip["margin"], 16)!, + tooltipPadding: parsePadding(tooltip["padding"], + const EdgeInsets.symmetric(horizontal: 16, vertical: 8))!, + maxContentWidth: parseDouble(tooltip["max_width"], 120)!, + rotateAngle: parseDouble(tooltip["rotation"], 0.0)!, + tooltipHorizontalOffset: parseDouble(tooltip["horizontal_offset"], 0)!, + tooltipBorder: parseBorderSide(tooltip["border_side"], theme, + defaultValue: BorderSide.none)!, + fitInsideHorizontally: + parseBool(tooltip["fit_inside_horizontally"], false)!, + fitInsideVertically: parseBool(tooltip["fit_inside_vertically"], false)!, + showOnTopOfTheChartBoxArea: + parseBool(tooltip["show_on_top_of_chart_box_area"], false)!, + tooltipHorizontalAlignment: parseFLHorizontalAlignment( + tooltip["horizontal_alignment"], FLHorizontalAlignment.center)!, + getTooltipItems: (List touchedSpots) { + return touchedSpots + .map((LineBarSpot spot) => parseLineTooltipItem( + control + .children("data_series")[spot.barIndex] + .children("points")[spot.spotIndex], + spot, + context)) + .nonNulls + .toList(); + }, + ); +} + +LineChartBarData parseLineChartBarData( + Control parent, + Control chartData, + bool interactiveChart, + BuildContext context, + Map> barSpots) { + final theme = Theme.of(context); + + var aboveLineBgcolor = chartData.getColor("above_line_bgcolor", context); + var aboveLineGradient = chartData.getGradient("above_line_gradient", theme); + var belowLineBgcolor = chartData.getColor("below_line_bgcolor", context); + var belowLineGradient = chartData.getGradient("below_line_gradient", theme); + var dashPattern = chartData.get("dash_pattern"); + var barColor = chartData.getColor("color", context, Colors.cyan)!; + var barGradient = chartData.getGradient("gradient", theme); + var aboveLine = parseFlLine(chartData.get("above_line"), Theme.of(context)); + var belowLine = parseFlLine(chartData.get("below_line"), Theme.of(context)); + var aboveLineCutoffY = chartData.getDouble("above_line_cutoff_y"); + var belowLineCutoffY = chartData.getDouble("below_line_cutoff_y"); + var stepDirection = chartData.getDouble("step_direction"); + + Map spots = { + for (var e in chartData.children("points")) + FlSpot(e.getDouble("x", 0)!, e.getDouble("y", 0)!): e + }; + return LineChartBarData( + preventCurveOverShooting: + chartData.getBool("prevent_curve_over_shooting", false)!, + preventCurveOvershootingThreshold: + chartData.getDouble("prevent_curve_over_shooting_threshold", 10.0)!, + spots: barSpots[chartData.id] ?? [], + curveSmoothness: chartData.getDouble("curve_smoothness", 0.35)!, + show: chartData.visible, + isStepLineChart: stepDirection != null, + lineChartStepData: LineChartStepData(stepDirection: stepDirection ?? 0.5), + showingIndicators: chartData + .children("points") + .asMap() + .entries + .where( + (dp) => !interactiveChart && dp.value.getBool("selected", false)!) + .map((e) => e.key) + .toList(), + isCurved: chartData.getBool("curved", false)!, + isStrokeCapRound: chartData.getBool("rounded_stroke_cap", false)!, + isStrokeJoinRound: chartData.getBool("rounded_stroke_join", false)!, + barWidth: chartData.getDouble("stroke_width", 2.0)!, + dashArray: dashPattern != null + ? (dashPattern as List).map((e) => parseInt(e)).nonNulls.toList() + : null, + shadow: parseBoxShadow(chartData.get("shadow"), Theme.of(context)) ?? + const Shadow(color: Colors.transparent), + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + var allDotsPainter = parseChartDotPainter( + chartData.get("point"), theme, percent, barColor, barGradient); + var dotPainter = parseChartDotPainter( + chartData.children("points")[index].get("point"), + theme, + percent, + barColor, + barGradient); + return dotPainter ?? allDotsPainter ?? invisibleDotPainter; + }), + aboveBarData: aboveLineBgcolor != null || + aboveLineGradient != null || + aboveLine != null + ? BarAreaData( + show: true, + color: aboveLineBgcolor, + gradient: aboveLineGradient, + applyCutOffY: aboveLineCutoffY != null, + cutOffY: aboveLineCutoffY ?? 0, + spotsLine: BarAreaSpotsLine( + show: aboveLine != null, + flLineStyle: aboveLine ?? const FlLine(), + checkToShowSpotLine: (spot) => + spots[spot]!.getBool("show_above_line", true)!, + )) + : null, + belowBarData: belowLineBgcolor != null || + belowLineGradient != null || + belowLine != null + ? BarAreaData( + show: true, + color: belowLineBgcolor, + gradient: belowLineGradient, + applyCutOffY: belowLineCutoffY != null, + cutOffY: belowLineCutoffY ?? 0, + spotsLine: BarAreaSpotsLine( + show: belowLine != null, + flLineStyle: belowLine ?? const FlLine(), + checkToShowSpotLine: (spot) => + spots[spot]!.getBool("show_below_line", true)!, + )) + : null, + color: barColor, + gradient: barGradient); +} diff --git a/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/pie_chart.dart b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/pie_chart.dart new file mode 100644 index 0000000000..e00584528b --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/pie_chart.dart @@ -0,0 +1,57 @@ +import 'package:equatable/equatable.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +import 'charts.dart'; + +class PieChartEventData extends Equatable { + final String eventType; + final int? sectionIndex; + final Offset? localPosition; + + const PieChartEventData( + {required this.eventType, + required this.sectionIndex, + this.localPosition}); + + factory PieChartEventData.fromDetails( + FlTouchEvent event, PieTouchResponse? response) { + return PieChartEventData( + eventType: eventMap[event.runtimeType.toString()] ?? "undefined", + sectionIndex: response?.touchedSection?.touchedSectionIndex, + localPosition: event.localPosition, + ); + } + + Map toMap() => { + 'type': eventType, + 'section_index': sectionIndex, + "local_x": localPosition?.dx, + "local_y": localPosition?.dy + }; + + @override + List get props => [eventType, sectionIndex]; +} + +PieChartSectionData parsePieChartSectionData( + Control section, BuildContext context) { + section.notifyParent = true; + var theme = Theme.of(context); + var title = section.getString("title"); + return PieChartSectionData( + value: section.getDouble("value"), + color: section.getColor("color", context), + radius: section.getDouble("radius"), + showTitle: title != null, + title: title, + gradient: section.getGradient("gradient", theme), + titleStyle: section.getTextStyle("title_style", theme), + borderSide: section.getBorderSide("border_side", theme, + defaultValue: BorderSide.none)!, + titlePositionPercentageOffset: section.getDouble("title_position"), + badgeWidget: section.buildWidget("badge_content"), + badgePositionPercentageOffset: section.getDouble("badge_position"), + ); +} diff --git a/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/scatter_chart.dart b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/scatter_chart.dart new file mode 100644 index 0000000000..1c244eedcc --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/scatter_chart.dart @@ -0,0 +1,86 @@ +import 'package:equatable/equatable.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +import 'charts.dart'; + +class ScatterChartEventData extends Equatable { + final String eventType; + final int? spotIndex; + + const ScatterChartEventData({required this.eventType, this.spotIndex}); + + factory ScatterChartEventData.fromDetails( + FlTouchEvent event, ScatterTouchResponse? response) { + return ScatterChartEventData( + eventType: eventMap[event.runtimeType.toString()] ?? "undefined", + spotIndex: response?.touchedSpot?.spotIndex); + } + + Map toMap() => {'type': eventType, 'spot_index': spotIndex}; + + @override + List get props => [eventType, spotIndex]; +} + +ScatterTouchTooltipData parseScatterTouchTooltipData( + BuildContext context, Control control, List spots) { + var tooltip = control.get("tooltip") ?? {}; + + final theme = Theme.of(context); + + return ScatterTouchTooltipData( + tooltipBorder: parseBorderSide(tooltip["border_side"], theme, + defaultValue: BorderSide.none)!, + rotateAngle: parseDouble(tooltip["rotation"], 0.0)!, + maxContentWidth: parseDouble(tooltip["max_width"], 120)!, + tooltipPadding: parsePadding(tooltip["padding"], + const EdgeInsets.symmetric(horizontal: 16, vertical: 8))!, + tooltipHorizontalAlignment: parseFLHorizontalAlignment( + tooltip["horizontal_alignment"], FLHorizontalAlignment.center)!, + tooltipHorizontalOffset: parseDouble(tooltip["horizontal_offset"], 0), + tooltipBorderRadius: parseBorderRadius(tooltip["border_radius"]), + fitInsideHorizontally: + parseBool(tooltip["fit_inside_horizontally"], false)!, + fitInsideVertically: parseBool(tooltip["fit_inside_vertically"], false)!, + getTooltipColor: (ScatterSpot touchedSpot) { + return parseColor( + tooltip["bgcolor"], theme, const Color.fromRGBO(96, 125, 139, 1))!; + }, + getTooltipItems: (ScatterSpot touchedSpot) { + var spotIndex = spots.indexWhere( + (spot) => spot.x == touchedSpot.x && spot.y == touchedSpot.y); + return parseScatterTooltipItem( + control.children("spots")[spotIndex], touchedSpot, context); + }, + ); +} + +ScatterTooltipItem? parseScatterTooltipItem( + Control dataPoint, ScatterSpot spot, BuildContext context) { + if (!dataPoint.getBool("show_tooltip", true)!) return null; + + var tooltip = dataPoint.internals?["tooltip"]; + if (tooltip == null) return null; + + final theme = Theme.of(context); + var style = parseTextStyle(tooltip["text_style"], theme, const TextStyle())!; + if (style.color == null) { + style = style.copyWith(color: spot.dotPainter.mainColor); + } + return ScatterTooltipItem( + tooltip["text"] ?? dataPoint.getDouble("y").toString(), + textStyle: style, + textAlign: parseTextAlign(tooltip["text_align"], TextAlign.center)!, + textDirection: parseBool(tooltip["rtl"], false)! + ? TextDirection.rtl + : TextDirection.ltr, + bottomMargin: parseDouble(tooltip["bottom_margin"]), + children: tooltip["text_spans"] != null + ? parseTextSpans(tooltip["text_spans"], theme, (s, eventName, + [eventData]) { + s.triggerEvent(eventName, eventData); + }) + : null); +} diff --git a/sdk/python/packages/flet-charts/src/flutter/flet_charts/pubspec.yaml b/sdk/python/packages/flet-charts/src/flutter/flet_charts/pubspec.yaml new file mode 100644 index 0000000000..86c058e89a --- /dev/null +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/pubspec.yaml @@ -0,0 +1,25 @@ +name: flet_charts +description: A Flet extension for creating interactive charts and graphs. +version: 0.1.0 +publish_to: none + +environment: + sdk: '>=3.2.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + + collection: ^1.16.0 + equatable: ^2.0.3 + fl_chart: 1.1.1 + + flet: + path: ../../../../../../../packages/flet + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/build.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/build.py index 0ea8d08e1a..99e4032602 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/build.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/build.py @@ -36,7 +36,7 @@ PYODIDE_ROOT_URL = "https://cdn.jsdelivr.net/pyodide/v0.27.7/full" DEFAULT_TEMPLATE_URL = "gh:flet-dev/flet-build-template" -MINIMAL_FLUTTER_VERSION = version.Version("3.35.1") +MINIMAL_FLUTTER_VERSION = version.Version("3.35.5") no_rich_output = get_bool_env_var("FLET_CLI_NO_RICH_OUTPUT") diff --git a/sdk/python/packages/flet-datatable2/CHANGELOG.md b/sdk/python/packages/flet-datatable2/CHANGELOG.md new file mode 100644 index 0000000000..886bb01094 --- /dev/null +++ b/sdk/python/packages/flet-datatable2/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [0.2.0] - 2025-06-26 + +## Added + +- Deployed online documentation: https://docs.flet.dev/datatable2/ +- New enums: `DataColumnSize` + +### Changed + +- Refactored all controls to use `@flet.control` dataclass-style definition. +- Additionally, they are now all based on their flet counterparts: + - `DataTable2` is now based on `flet.DataTable` + - `DataColumn2` is now based on `flet.DataColumn` + - `DataRow2` is now based on `flet.DataRow` + +## [0.1.0] - 2025-03-16 + +Initial release. + + +[0.2.0]: https://github.com/flet-dev/flet-datatable2/compare/0.1.0...0.2.0 +[0.1.0]: https://github.com/flet-dev/flet-datatable2/releases/tag/0.1.0 diff --git a/sdk/python/packages/flet-datatable2/LICENSE b/sdk/python/packages/flet-datatable2/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/sdk/python/packages/flet-datatable2/LICENSE @@ -0,0 +1,201 @@ + 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 such Contributor that are necessarily infringed 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 consequential 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-datatable2/README.md b/sdk/python/packages/flet-datatable2/README.md new file mode 100644 index 0000000000..a1c2fbf7a7 --- /dev/null +++ b/sdk/python/packages/flet-datatable2/README.md @@ -0,0 +1,53 @@ +# flet-datatable2 + +[![pypi](https://img.shields.io/pypi/v/flet-datatable2.svg)](https://pypi.python.org/pypi/flet-datatable2) +[![downloads](https://static.pepy.tech/badge/flet-datatable2/month)](https://pepy.tech/project/flet-datatable2) +[![license](https://img.shields.io/github/license/flet-dev/flet.svg)](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-datatable2/LICENSE) + +An enhanced data table for [Flet](https://flet.dev) apps that builds on the built-in component by adding sticky headers, +fixed top rows, and fixed left columns while preserving all core features. + +It is based on [data_table_2](https://pub.dev/packages/data_table_2) Flutter package. + +## Documentation + +You can find its documentation [here](https://docs.flet.dev/datatable2/). + +## Platform Support + +This package supports the following platforms: + +| Platform | Supported | +|----------|:---------:| +| Windows | βœ… | +| macOS | βœ… | +| Linux | βœ… | +| iOS | βœ… | +| Android | βœ… | +| Web | βœ… | + +## Usage + +### Installation + +To install the `flet-datatable2` package and add it to your project dependencies: + +- Using `uv`: + ```bash + uv add flet-datatable2 + ``` + +- Using `pip`: + ```bash + pip install flet-datatable2 + ``` + After this, you will have to manually add this package to your `requirements.txt` or `pyproject.toml`. + +- Using `poetry`: + ```bash + poetry add flet-datatable2 + ``` + +### Examples + +For examples, see [these](https://github.com/flet-dev/flet/tree/main/examples/controls/datatable2). diff --git a/sdk/python/packages/flet-datatable2/pyproject.toml b/sdk/python/packages/flet-datatable2/pyproject.toml new file mode 100644 index 0000000000..040d0b46c5 --- /dev/null +++ b/sdk/python/packages/flet-datatable2/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "flet-datatable2" +version = "0.1.0" +description = "Enhanced data table widgets 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://docs.flet.dev/datatable2" +Repository = "https://github.com/flet-dev/flet/tree/main/sdk/python/packages/flet-datatable2" +Issues = "https://github.com/flet-dev/flet/issues" + +[tool.setuptools.package-data] +"flutter.flet_datatable2" = ["**/*"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/sdk/python/packages/flet-datatable2/src/flet_datatable2/__init__.py b/sdk/python/packages/flet-datatable2/src/flet_datatable2/__init__.py new file mode 100644 index 0000000000..59d4d2e7cf --- /dev/null +++ b/sdk/python/packages/flet-datatable2/src/flet_datatable2/__init__.py @@ -0,0 +1,10 @@ +from flet_datatable2.datacolumn2 import DataColumn2, DataColumnSize +from flet_datatable2.datarow2 import DataRow2 +from flet_datatable2.datatable2 import DataTable2 + +__all__ = [ + "DataColumn2", + "DataColumnSize", + "DataRow2", + "DataTable2", +] diff --git a/sdk/python/packages/flet-datatable2/src/flet_datatable2/datacolumn2.py b/sdk/python/packages/flet-datatable2/src/flet_datatable2/datacolumn2.py new file mode 100644 index 0000000000..e2f55cde0f --- /dev/null +++ b/sdk/python/packages/flet-datatable2/src/flet_datatable2/datacolumn2.py @@ -0,0 +1,44 @@ +from enum import Enum +from typing import Optional + +import flet as ft + +__all__ = ["DataColumn2", "DataColumnSize"] + + +class DataColumnSize(Enum): + """ + Relative size of a column determines the share of total table + width allocated to each individual column. + + When determining column widths, ratios between `S`, `M` and `L` + columns are kept (i.e. Large columns are set to 1.2x width of Medium ones). + + See [`DataTable2.sm_ratio`][(p).], [`DataTable2.lm_ratio`][(p).]. + """ + + S = "s" + M = "m" + L = "l" + + +@ft.control("DataColumn2") +class DataColumn2(ft.DataColumn): + """ + Extends [`flet.DataColumn`][flet.DataColumn], + adding the ability to set relative column size and fixed column width. + + Meant to be used as an item of [`DataTable2.columns`][(p).]. + """ + + fixed_width: Optional[ft.Number] = None + """ + Defines absolute width of the column in pixels + (as opposed to relative [`size`][..] used by default). + """ + + size: Optional[DataColumnSize] = DataColumnSize.S + """ + Column sizes are determined based on available width by distributing + it to individual columns accounting for their relative sizes. + """ diff --git a/sdk/python/packages/flet-datatable2/src/flet_datatable2/datarow2.py b/sdk/python/packages/flet-datatable2/src/flet_datatable2/datarow2.py new file mode 100644 index 0000000000..419405e1a6 --- /dev/null +++ b/sdk/python/packages/flet-datatable2/src/flet_datatable2/datarow2.py @@ -0,0 +1,83 @@ +from typing import Optional + +import flet as ft + +__all__ = ["DataRow2"] + + +@ft.control("DataRow2") +class DataRow2(ft.DataRow): + """ + Extends [`flet.DataRow`][flet.DataRow], adding row-level `tap` events. + + There are also [`on_secondary_tap`][(c).] and [`on_secondary_tap_down`][(c).], + which are not available in [`DataCell`][flet.DataCell]s and can be useful in + desktop settings to handle right-click actions. + """ + + decoration: Optional[ft.BoxDecoration] = None + """ + Decoration to be applied to this row. + + Note: + If provided, [`DataTable2.divider_thickness`][(p).] has no effect. + """ + + specific_row_height: Optional[ft.Number] = None + """ + Specific row height. + + Falls back to [`DataTable2.data_row_height`][(p).] if not set. + """ + + on_double_tap: Optional[ft.ControlEventHandler["DataRow2"]] = None + """ + Fires when the row is double-tapped. + + Note: + Won't be called if tapped cell has any tap event handlers + ([`on_tap`][flet.DataCell.on_tap], + [`on_double_tap`][flet.DataCell.on_double_tap], + [`on_long_press`][flet.DataCell.on_long_press], + [`on_tap_cancel`][flet.DataCell.on_tap_cancel], + [`on_tap_down`][flet.DataCell.on_tap_down]) set. + """ + + on_secondary_tap: Optional[ft.ControlEventHandler["DataRow2"]] = None + """ + Fires when the row is right-clicked (secondary tap). + + Note: + Won't be called if tapped cell has any tap event handlers + ([`on_tap`][flet.DataCell.on_tap], + [`on_double_tap`][flet.DataCell.on_double_tap], + [`on_long_press`][flet.DataCell.on_long_press], + [`on_tap_cancel`][flet.DataCell.on_tap_cancel], + [`on_tap_down`][flet.DataCell.on_tap_down]) set. + """ + + on_secondary_tap_down: Optional[ft.ControlEventHandler["DataRow2"]] = None + """ + Fires when the row is right-clicked (secondary tap down). + + Note: + Won't be called if tapped cell has any tap event handlers + ([`on_tap`][flet.DataCell.on_tap], + [`on_double_tap`][flet.DataCell.on_double_tap], + [`on_long_press`][flet.DataCell.on_long_press], + [`on_tap_cancel`][flet.DataCell.on_tap_cancel], + [`on_tap_down`][flet.DataCell.on_tap_down]) set. + """ + + on_tap: Optional[ft.EventHandler[ft.TapEvent["DataRow2"]]] = None + """ + Fires when the row is tapped. + + Note: + Won't be called if tapped cell has any tap event handlers + ([`on_tap`][flet.DataCell.on_tap], + [`on_double_tap`][flet.DataCell.on_double_tap], + [`on_long_press`][flet.DataCell.on_long_press], + [`on_tap_cancel`][flet.DataCell.on_tap_cancel], + [`on_tap_down`][flet.DataCell.on_tap_down]) set. + """ diff --git a/sdk/python/packages/flet-datatable2/src/flet_datatable2/datatable2.py b/sdk/python/packages/flet-datatable2/src/flet_datatable2/datatable2.py new file mode 100644 index 0000000000..33d681a8b8 --- /dev/null +++ b/sdk/python/packages/flet-datatable2/src/flet_datatable2/datatable2.py @@ -0,0 +1,141 @@ +from dataclasses import field +from typing import Optional, Union + +import flet as ft +from flet_datatable2.datacolumn2 import DataColumn2 +from flet_datatable2.datarow2 import DataRow2 + +__all__ = ["DataTable2"] + + +@ft.control("DataTable2") +class DataTable2(ft.DataTable): + """ + Provides sticky header row, scrollable data rows, + and additional layout flexibility with [`DataColumn2`][(p).] + and [`DataRow2`][(p).]. + + Note: + `DataTable2` doesn't support + [`DataTable.data_row_min_height`][flet.DataTable.data_row_min_height] + and [`DataTable.data_row_max_height`][flet.DataTable.data_row_max_height] + properties present in the parent [`DataTable`][flet.DataTable]. + Use [`data_row_height`][(c).] instead. + """ + + columns: list[Union[DataColumn2, ft.DataColumn]] + """ + A list of table columns. + """ + + rows: list[Union[ft.DataRow, DataRow2]] = field(default_factory=list) + """ + A list of table rows. + """ + + empty: Optional[ft.Control] = None + """ + Placeholder control shown when there are no data rows. + """ + + bottom_margin: Optional[ft.Number] = None + """ + Adds space after the last row if set. + """ + + lm_ratio: ft.Number = 1.2 + """ + Ratio of Large column width to Medium. + """ + + sm_ratio: ft.Number = 0.67 + """ + Ratio of Small column width to Medium. + """ + + fixed_left_columns: int = 0 + """ + Number of sticky columns on the left. Includes checkbox column, if present. + """ + + fixed_top_rows: int = 1 + """ + Number of sticky rows from the top. Includes heading row by default. + """ + + fixed_columns_color: Optional[ft.ColorValue] = None + """ + Background color for sticky left columns. + """ + + fixed_corner_color: Optional[ft.ColorValue] = None + """ + Background color of the fixed top-left corner cell. + """ + + sort_arrow_icon_color: Optional[ft.ColorValue] = None + """ + When set always overrides/preceeds default arrow icon color. + """ + + min_width: Optional[ft.Number] = None + """ + Minimum table width before horizontal scrolling kicks in. + """ + + show_heading_checkbox: bool = True + """ + Controls visibility of the heading checkbox. + """ + + heading_checkbox_theme: Optional[ft.CheckboxTheme] = None + """ + Overrides theme of the heading checkbox. + """ + + data_row_checkbox_theme: Optional[ft.CheckboxTheme] = None + """ + Overrides theme of checkboxes in each data row. + """ + + sort_arrow_icon: ft.IconData = ft.Icons.ARROW_UPWARD + """ + Icon shown when sorting is applied. + """ + + sort_arrow_animation_duration: ft.DurationValue = field( + default_factory=lambda: ft.Duration(milliseconds=150) + ) + """ + Duration of sort arrow animation. + """ + + visible_horizontal_scroll_bar: Optional[bool] = None + """ + Determines visibility of the horizontal scrollbar. + """ + + visible_vertical_scroll_bar: Optional[bool] = None + """ + Determines visibility of the vertical scrollbar. + """ + + checkbox_alignment: ft.Alignment = field( + default_factory=lambda: ft.Alignment.CENTER + ) + """ + Alignment of the checkbox. + """ + + data_row_height: Optional[ft.Number] = None + """ + Height of each data row. + """ + + # present in parent (DataTable) but of no use in DataTable2 + data_row_min_height: None = field( + init=False, repr=False, compare=False, metadata={"skip": True} + ) + data_row_max_height: None = field( + init=False, repr=False, compare=False, metadata={"skip": True} + ) diff --git a/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/.gitignore b/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/.gitignore new file mode 100644 index 0000000000..ed7794f2ab --- /dev/null +++ b/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/.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-datatable2/src/flutter/flet_datatable2/lib/flet_datatable2.dart b/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/lib/flet_datatable2.dart new file mode 100644 index 0000000000..77c3be7cfc --- /dev/null +++ b/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/lib/flet_datatable2.dart @@ -0,0 +1,5 @@ +library flet_datatable2; + +//export "../src/create_control.dart" show createControl, ensureInitialized; + +export "src/extension.dart" show Extension; diff --git a/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/lib/src/data_sources.dart b/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/lib/src/data_sources.dart new file mode 100644 index 0000000000..afaf60bf56 --- /dev/null +++ b/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/lib/src/data_sources.dart @@ -0,0 +1,692 @@ +// ignore_for_file: avoid_print + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:data_table_2/data_table_2.dart'; + +// Copyright 2019 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// The file was extracted from GitHub: https://github.com/flutter/gallery +// Changes and modifications by Maxim Saplin, 2021 + +/// Keeps track of selected rows, feed the data into DesertsDataSource +class RestorableDessertSelections extends RestorableProperty> { + Set _dessertSelections = {}; + + /// Returns whether or not a dessert row is selected by index. + bool isSelected(int index) => _dessertSelections.contains(index); + + /// Takes a list of [Dessert]s and saves the row indices of selected rows + /// into a [Set]. + void setDessertSelections(List desserts) { + final updatedSet = {}; + for (var i = 0; i < desserts.length; i += 1) { + var dessert = desserts[i]; + if (dessert.selected) { + updatedSet.add(i); + } + } + _dessertSelections = updatedSet; + notifyListeners(); + } + + @override + Set createDefaultValue() => _dessertSelections; + + @override + Set fromPrimitives(Object? data) { + final selectedItemIndices = data as List; + _dessertSelections = { + ...selectedItemIndices.map((dynamic id) => id as int), + }; + return _dessertSelections; + } + + @override + void initWithValue(Set value) { + _dessertSelections = value; + } + + @override + Object toPrimitives() => _dessertSelections.toList(); +} + +int _idCounter = 0; + +/// Domain model entity +class Dessert { + Dessert( + this.name, + this.calories, + this.fat, + this.carbs, + this.protein, + this.sodium, + this.calcium, + this.iron, + ); + + final int id = _idCounter++; + + final String name; + final int calories; + final double fat; + final int carbs; + final double protein; + final int sodium; + final int calcium; + final int iron; + bool selected = false; +} + +/// Data source implementing standard Flutter's DataTableSource abstract class +/// which is part of DataTable and PaginatedDataTable synchronous data fecthin API. +/// This class uses static collection of deserts as a data store, projects it into +/// DataRows, keeps track of selected items, provides sprting capability +class DessertDataSource extends DataTableSource { + DessertDataSource.empty(this.context) { + desserts = []; + } + + DessertDataSource(this.context, + [sortedByCalories = false, + this.hasRowTaps = false, + this.hasRowHeightOverrides = false, + this.hasZebraStripes = false]) { + desserts = _desserts; + if (sortedByCalories) { + sort((d) => d.calories, true); + } + } + + final BuildContext context; + late List desserts; + // Add row tap handlers and show snackbar + bool hasRowTaps = false; + // Override height values for certain rows + bool hasRowHeightOverrides = false; + // Color each Row by index's parity + bool hasZebraStripes = false; + + void sort(Comparable Function(Dessert d) getField, bool ascending) { + desserts.sort((a, b) { + final aValue = getField(a); + final bValue = getField(b); + return ascending + ? Comparable.compare(aValue, bValue) + : Comparable.compare(bValue, aValue); + }); + notifyListeners(); + } + + void updateSelectedDesserts(RestorableDessertSelections selectedRows) { + _selectedCount = 0; + for (var i = 0; i < desserts.length; i += 1) { + var dessert = desserts[i]; + if (selectedRows.isSelected(i)) { + dessert.selected = true; + _selectedCount += 1; + } else { + dessert.selected = false; + } + } + notifyListeners(); + } + + @override + DataRow2 getRow(int index, [Color? color]) { + final format = NumberFormat.decimalPercentPattern( + locale: 'en', + decimalDigits: 0, + ); + assert(index >= 0); + if (index >= desserts.length) throw 'index > _desserts.length'; + final dessert = desserts[index]; + return DataRow2.byIndex( + index: index, + selected: dessert.selected, + color: color != null + ? WidgetStateProperty.all(color) + : (hasZebraStripes && index.isEven + ? WidgetStateProperty.all(Theme.of(context).highlightColor) + : null), + onSelectChanged: (value) { + if (dessert.selected != value) { + _selectedCount += value! ? 1 : -1; + assert(_selectedCount >= 0); + dessert.selected = value; + notifyListeners(); + } + }, + onTap: hasRowTaps + ? () => _showSnackbar(context, 'Tapped on row ${dessert.name}') + : null, + onDoubleTap: hasRowTaps + ? () => _showSnackbar(context, 'Double Tapped on row ${dessert.name}') + : null, + onLongPress: hasRowTaps + ? () => _showSnackbar(context, 'Long pressed on row ${dessert.name}') + : null, + onSecondaryTap: hasRowTaps + ? () => _showSnackbar(context, 'Right clicked on row ${dessert.name}') + : null, + onSecondaryTapDown: hasRowTaps + ? (d) => + _showSnackbar(context, 'Right button down on row ${dessert.name}') + : null, + specificRowHeight: + hasRowHeightOverrides && dessert.fat >= 25 ? 100 : null, + cells: [ + DataCell(Text(dessert.name)), + DataCell(Text('${dessert.calories}'), + onTap: () => _showSnackbar(context, + 'Tapped on a cell with "${dessert.calories}"', Colors.red)), + DataCell(Text(dessert.fat.toStringAsFixed(1))), + DataCell(Text('${dessert.carbs}')), + DataCell(Text(dessert.protein.toStringAsFixed(1))), + DataCell(Text('${dessert.sodium}')), + DataCell(Text(format.format(dessert.calcium / 100))), + DataCell(Text(format.format(dessert.iron / 100))), + ], + ); + } + + @override + int get rowCount => desserts.length; + + @override + bool get isRowCountApproximate => false; + + @override + int get selectedRowCount => _selectedCount; + + void selectAll(bool? checked) { + for (final dessert in desserts) { + dessert.selected = checked ?? false; + } + _selectedCount = (checked ?? false) ? desserts.length : 0; + notifyListeners(); + } +} + +/// Async datasource for AsynPaginatedDataTabke2 example. Based on AsyncDataTableSource which +/// is an extension to Flutter's DataTableSource and aimed at solving +/// saync data fetching scenarious by paginated table (such as using Web API) +class DessertDataSourceAsync extends AsyncDataTableSource { + DessertDataSourceAsync() { + print('DessertDataSourceAsync created'); + } + + DessertDataSourceAsync.empty() { + _empty = true; + print('DessertDataSourceAsync.empty created'); + } + + DessertDataSourceAsync.error() { + _errorCounter = 0; + print('DessertDataSourceAsync.error created'); + } + + bool _empty = false; + int? _errorCounter; + + RangeValues? _caloriesFilter; + + RangeValues? get caloriesFilter => _caloriesFilter; + set caloriesFilter(RangeValues? calories) { + _caloriesFilter = calories; + refreshDatasource(); + } + + final DesertsFakeWebService _repo = DesertsFakeWebService(); + + String _sortColumn = "name"; + bool _sortAscending = true; + + void sort(String columnName, bool ascending) { + _sortColumn = columnName; + _sortAscending = ascending; + refreshDatasource(); + } + + Future getTotalRecords() { + return Future.delayed( + const Duration(milliseconds: 0), () => _empty ? 0 : _dessertsX3.length); + } + + @override + Future getRows(int startIndex, int count) async { + print('getRows($startIndex, $count)'); + if (_errorCounter != null) { + _errorCounter = _errorCounter! + 1; + + if (_errorCounter! % 2 == 1) { + await Future.delayed(const Duration(milliseconds: 1000)); + throw 'Error #${((_errorCounter! - 1) / 2).round() + 1} has occured'; + } + } + + final format = NumberFormat.decimalPercentPattern( + locale: 'en', + decimalDigits: 0, + ); + assert(startIndex >= 0); + + // List returned will be empty is there're fewer items than startingAt + var x = _empty + ? await Future.delayed(const Duration(milliseconds: 2000), + () => DesertsFakeWebServiceResponse(0, [])) + : await _repo.getData( + startIndex, count, _caloriesFilter, _sortColumn, _sortAscending); + + var r = AsyncRowsResponse( + x.totalRecords, + x.data.map((dessert) { + return DataRow( + key: ValueKey(dessert.id), + //selected: dessert.selected, + onSelectChanged: (value) { + if (value != null) { + setRowSelection(ValueKey(dessert.id), value); + } + }, + cells: [ + DataCell(Text(dessert.name)), + DataCell(Text('${dessert.calories}')), + DataCell(Text(dessert.fat.toStringAsFixed(1))), + DataCell(Text('${dessert.carbs}')), + DataCell(Text(dessert.protein.toStringAsFixed(1))), + DataCell(Text('${dessert.sodium}')), + DataCell(Text(format.format(dessert.calcium / 100))), + DataCell(Text(format.format(dessert.iron / 100))), + ], + ); + }).toList()); + + return r; + } +} + +class DesertsFakeWebServiceResponse { + DesertsFakeWebServiceResponse(this.totalRecords, this.data); + + /// THe total ammount of records on the server, e.g. 100 + final int totalRecords; + + /// One page, e.g. 10 reocrds + final List data; +} + +class DesertsFakeWebService { + int Function(Dessert, Dessert)? _getComparisonFunction( + String column, bool ascending) { + var coef = ascending ? 1 : -1; + switch (column) { + case 'name': + return (Dessert d1, Dessert d2) => coef * d1.name.compareTo(d2.name); + case 'calories': + return (Dessert d1, Dessert d2) => coef * (d1.calories - d2.calories); + case 'fat': + return (Dessert d1, Dessert d2) => coef * (d1.fat - d2.fat).round(); + case 'carbs': + return (Dessert d1, Dessert d2) => coef * (d1.carbs - d2.carbs); + case 'protein': + return (Dessert d1, Dessert d2) => + coef * (d1.protein - d2.protein).round(); + case 'sodium': + return (Dessert d1, Dessert d2) => coef * (d1.sodium - d2.sodium); + case 'calcium': + return (Dessert d1, Dessert d2) => coef * (d1.calcium - d2.calcium); + case 'iron': + return (Dessert d1, Dessert d2) => coef * (d1.iron - d2.iron); + } + + return null; + } + + Future getData(int startingAt, int count, + RangeValues? caloriesFilter, String sortedBy, bool sortedAsc) async { + return Future.delayed( + Duration( + milliseconds: startingAt == 0 + ? 2650 + : startingAt < 20 + ? 2000 + : 400), () { + var result = _dessertsX3; + + if (caloriesFilter != null) { + result = result + .where((e) => + e.calories >= caloriesFilter.start && + e.calories <= caloriesFilter.end) + .toList(); + } + + result.sort(_getComparisonFunction(sortedBy, sortedAsc)); + return DesertsFakeWebServiceResponse( + result.length, result.skip(startingAt).take(count).toList()); + }); + } +} + +int _selectedCount = 0; + +List _desserts = [ + Dessert( + 'Frozen Yogurt', + 159, + 6.0, + 24, + 4.0, + 87, + 14, + 1, + ), + Dessert( + 'Ice Cream Sandwich', + 237, + 9.0, + 37, + 4.3, + 129, + 8, + 1, + ), + Dessert( + 'Eclair', + 262, + 16.0, + 24, + 6.0, + 337, + 6, + 7, + ), + Dessert( + 'Cupcake', + 305, + 3.7, + 67, + 4.3, + 413, + 3, + 8, + ), + Dessert( + 'Gingerbread', + 356, + 16.0, + 49, + 3.9, + 327, + 7, + 16, + ), + Dessert( + 'Jelly Bean', + 375, + 0.0, + 94, + 0.0, + 50, + 0, + 0, + ), + Dessert( + 'Lollipop', + 392, + 0.2, + 98, + 0.0, + 38, + 0, + 2, + ), + Dessert( + 'Honeycomb', + 408, + 3.2, + 87, + 6.5, + 562, + 0, + 45, + ), + Dessert( + 'Donut', + 452, + 25.0, + 51, + 4.9, + 326, + 2, + 22, + ), + Dessert( + 'Apple Pie', + 518, + 26.0, + 65, + 7.0, + 54, + 12, + 6, + ), + Dessert( + 'Frozen Yougurt with sugar', + 168, + 6.0, + 26, + 4.0, + 87, + 14, + 1, + ), + Dessert( + 'Ice Cream Sandwich with sugar', + 246, + 9.0, + 39, + 4.3, + 129, + 8, + 1, + ), + Dessert( + 'Eclair with sugar', + 271, + 16.0, + 26, + 6.0, + 337, + 6, + 7, + ), + Dessert( + 'Cupcake with sugar', + 314, + 3.7, + 69, + 4.3, + 413, + 3, + 8, + ), + Dessert( + 'Gingerbread with sugar', + 345, + 16.0, + 51, + 3.9, + 327, + 7, + 16, + ), + Dessert( + 'Jelly Bean with sugar', + 364, + 0.0, + 96, + 0.0, + 50, + 0, + 0, + ), + Dessert( + 'Lollipop with sugar', + 401, + 0.2, + 100, + 0.0, + 38, + 0, + 2, + ), + Dessert( + 'Honeycomd with sugar', + 417, + 3.2, + 89, + 6.5, + 562, + 0, + 45, + ), + Dessert( + 'Donut with sugar', + 461, + 25.0, + 53, + 4.9, + 326, + 2, + 22, + ), + Dessert( + 'Apple pie with sugar', + 527, + 26.0, + 67, + 7.0, + 54, + 12, + 6, + ), + Dessert( + 'Forzen yougurt with honey', + 223, + 6.0, + 36, + 4.0, + 87, + 14, + 1, + ), + Dessert( + 'Ice Cream Sandwich with honey', + 301, + 9.0, + 49, + 4.3, + 129, + 8, + 1, + ), + Dessert( + 'Eclair with honey', + 326, + 16.0, + 36, + 6.0, + 337, + 6, + 7, + ), + Dessert( + 'Cupcake with honey', + 369, + 3.7, + 79, + 4.3, + 413, + 3, + 8, + ), + Dessert( + 'Gignerbread with hone', + 420, + 16.0, + 61, + 3.9, + 327, + 7, + 16, + ), + Dessert( + 'Jelly Bean with honey', + 439, + 0.0, + 106, + 0.0, + 50, + 0, + 0, + ), + Dessert( + 'Lollipop with honey', + 456, + 0.2, + 110, + 0.0, + 38, + 0, + 2, + ), + Dessert( + 'Honeycomd with honey', + 472, + 3.2, + 99, + 6.5, + 562, + 0, + 45, + ), + Dessert( + 'Donut with honey', + 516, + 25.0, + 63, + 4.9, + 326, + 2, + 22, + ), + Dessert( + 'Apple pie with honey', + 582, + 26.0, + 77, + 7.0, + 54, + 12, + 6, + ), +]; + +List _dessertsX3 = _desserts.toList() + ..addAll(_desserts.map((i) => Dessert('${i.name} x2', i.calories, i.fat, + i.carbs, i.protein, i.sodium, i.calcium, i.iron))) + ..addAll(_desserts.map((i) => Dessert('${i.name} x3', i.calories, i.fat, + i.carbs, i.protein, i.sodium, i.calcium, i.iron))); + +_showSnackbar(BuildContext context, String text, [Color? color]) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + backgroundColor: color, + duration: const Duration(seconds: 1), + content: Text(text), + )); +} diff --git a/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/lib/src/datatable2.dart b/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/lib/src/datatable2.dart new file mode 100644 index 0000000000..118a7c6d84 --- /dev/null +++ b/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/lib/src/datatable2.dart @@ -0,0 +1,195 @@ +import 'package:data_table_2/data_table_2.dart'; +import 'package:flet/flet.dart' as ft; +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +import 'utils/datatable.dart'; + +class DataTable2Control extends StatefulWidget { + final Control control; + + const DataTable2Control({ + super.key, + required this.control, + }); + + @override + State createState() => _DataTable2ControlState(); +} + +class _DataTable2ControlState extends State { + //final ScrollController _horizontalController = ScrollController(); + //final ScrollController _controller = ScrollController(); + + // @override + // void dispose() { + // _horizontalController.dispose(); + // _controller.dispose(); + // super.dispose(); + // } + + @override + Widget build(BuildContext context) { + debugPrint("DataTable2Control build: ${widget.control.id}"); + + var bgColor = widget.control.getString("bgcolor"); + var border = widget.control.getBorder("border", Theme.of(context)); + var borderRadius = widget.control.getBorderRadius("border_radius"); + var gradient = widget.control.getGradient("gradient", Theme.of(context)); + var horizontalLines = + widget.control.getBorderSide("horizontal_lines", Theme.of(context)); + var verticalLines = + widget.control.getBorderSide("vertical_lines", Theme.of(context)); + var defaultDecoration = + Theme.of(context).dataTableTheme.decoration ?? const BoxDecoration(); + + BoxDecoration? decoration; + if (bgColor != null || + border != null || + borderRadius != null || + gradient != null) { + decoration = (defaultDecoration as BoxDecoration).copyWith( + color: parseColor(bgColor, Theme.of(context)), + border: border, + borderRadius: borderRadius, + gradient: gradient); + } + + var datatable2 = DataTable2( + // scrollController: _controller, + // horizontalScrollController: _horizontalController, + decoration: decoration, + border: (horizontalLines != null || verticalLines != null) + ? TableBorder( + horizontalInside: horizontalLines ?? BorderSide.none, + verticalInside: verticalLines ?? BorderSide.none) + : null, + clipBehavior: widget.control.getClipBehavior("clip_behavior", Clip.none)!, + checkboxHorizontalMargin: + widget.control.getDouble("checkbox_horizontal_margin"), + columnSpacing: widget.control.getDouble("column_spacing"), + minWidth: widget.control.getDouble("min_width"), + bottomMargin: widget.control.getDouble("bottom_margin"), + empty: widget.control.buildWidget("empty"), + isHorizontalScrollBarVisible: + widget.control.getBool("visible_horizontal_scroll_bar"), + isVerticalScrollBarVisible: + widget.control.getBool("visible_vertical_scroll_bar"), + fixedLeftColumns: widget.control.getInt("fixed_left_columns", 0)!, + fixedTopRows: widget.control.getInt("fixed_top_rows", 1)!, + fixedColumnsColor: + widget.control.getColor("fixed_columns_color", context), + fixedCornerColor: widget.control.getColor("fixed_corner_color", context), + smRatio: widget.control.getDouble("sm_ratio", 0.67)!, + lmRatio: widget.control.getDouble("lm_ratio", 1.2)!, + sortArrowIcon: + widget.control.getIconData("sort_arrow_icon") ?? Icons.arrow_upward, + sortArrowAnimationDuration: widget.control.getDuration( + "sort_arrow_animation_duration", Duration(microseconds: 150))!, + checkboxAlignment: + widget.control.getAlignment("checkbox_alignment", Alignment.center)!, + headingCheckboxTheme: widget.control + .getCheckboxTheme("heading_checkbox_theme", Theme.of(context)), + datarowCheckboxTheme: widget.control + .getCheckboxTheme("data_row_checkbox_theme", Theme.of(context)), + showHeadingCheckBox: + widget.control.getBool("show_heading_checkbox", true)!, + dataRowColor: widget.control + .getWidgetStateColor("data_row_color", Theme.of(context)), + dataRowHeight: widget.control.getDouble("data_row_height"), + sortArrowIconColor: + widget.control.getColor("sort_arrow_icon_color", context), + dataTextStyle: + widget.control.getTextStyle("data_text_style", Theme.of(context)), + headingRowColor: widget.control + .getWidgetStateColor("heading_row_color", Theme.of(context)), + headingRowHeight: widget.control.getDouble("heading_row_height"), + headingTextStyle: + widget.control.getTextStyle("heading_text_style", Theme.of(context)), + headingRowDecoration: + widget.control.getBoxDecoration("heading_row_decoration", context), + dividerThickness: widget.control.getDouble("divider_thickness"), + horizontalMargin: widget.control.getDouble("horizontal_margin"), + showBottomBorder: widget.control.getBool("show_bottom_border", false)!, + showCheckboxColumn: + widget.control.getBool("show_checkbox_column", false)!, + sortAscending: widget.control.getBool("sort_ascending", false)!, + sortColumnIndex: widget.control.getInt("sort_column_index"), + onSelectAll: widget.control.getBool("on_select_all", false)! + ? (bool? selected) => + widget.control.triggerEvent("select_all", selected) + : null, + columns: widget.control.children("columns").map((column) { + column.notifyParent = true; + var tooltip = + parseTooltip(column.get("tooltip"), context, const Placeholder()); + return DataColumn2( + size: parseColumnSize(column.getString("size"), ColumnSize.S)!, + fixedWidth: column.getDouble("fixed_width"), + numeric: column.getBool("numeric", false)!, + tooltip: tooltip?.message, + headingRowAlignment: + column.getMainAxisAlignment("heading_row_alignment"), + onSort: column.getBool("on_sort", false)! + ? (columnIndex, ascending) => column + .triggerEvent("sort", {"ci": columnIndex, "asc": ascending}) + : null, + label: column.buildTextOrWidget("label")!); + }).toList(), + rows: widget.control.children("rows").map((row) { + row.notifyParent = true; + return DataRow2( + key: ValueKey(row.id), + selected: row.getBool("selected", false)!, + color: row.getWidgetStateColor("color", Theme.of(context)), + specificRowHeight: row.getDouble("specific_row_height"), + decoration: row.getBoxDecoration("decoration", context), + onSelectChanged: row.getBool("on_select_change", false)! + ? (selected) => row.triggerEvent("select_change", selected) + : null, + onLongPress: row.getBool("on_long_press", false)! + ? () => row.triggerEvent("long_press") + : null, + onDoubleTap: row.getBool("on_double_tap", false)! + ? () => row.triggerEvent("double_tap") + : null, + onTap: row.getBool("on_tap", false)! + ? () => row.triggerEvent("tap") + : null, + onSecondaryTap: row.getBool("on_secondary_tap", false)! + ? () => row.triggerEvent("secondary_tap") + : null, + onSecondaryTapDown: row.getBool("on_secondary_tap_down", false)! + ? (details) => + row.triggerEvent("secondary_tap_down", details.toMap()) + : null, + cells: row.children("cells").map((cell) { + cell.notifyParent = true; + return DataCell( + cell.buildWidget("content")!, + placeholder: cell.getBool("placeholder", false)!, + showEditIcon: cell.getBool("show_edit_icon", false)!, + onDoubleTap: cell.getBool("on_double_tap", false)! + ? () => cell.triggerEvent("double_tap") + : null, + onLongPress: cell.getBool("on_long_press", false)! + ? () => cell.triggerEvent("long_press") + : null, + onTap: cell.getBool("on_tap", false)! + ? () => cell.triggerEvent("tap") + : null, + onTapCancel: cell.getBool("on_tap_cancel", false)! + ? () => cell.triggerEvent("tap_cancel") + : null, + onTapDown: cell.getBool("on_tap_down", false)! + ? (details) => cell.triggerEvent("tap_down", details.toMap()) + : null, + ); + }).toList(), + ); + }).toList(), + ); + + return ConstrainedControl(control: widget.control, child: datatable2); + } +} diff --git a/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/lib/src/extension.dart b/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/lib/src/extension.dart new file mode 100644 index 0000000000..2f0172dd19 --- /dev/null +++ b/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/lib/src/extension.dart @@ -0,0 +1,16 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/widgets.dart'; + +import 'datatable2.dart'; + +class Extension extends FletExtension { + @override + Widget? createWidget(Key? key, Control control) { + switch (control.type) { + case "DataTable2": + return DataTable2Control(key: key, control: control); + default: + return null; + } + } +} diff --git a/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/lib/src/utils/datatable.dart b/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/lib/src/utils/datatable.dart new file mode 100644 index 0000000000..4e602981d0 --- /dev/null +++ b/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/lib/src/utils/datatable.dart @@ -0,0 +1,11 @@ +import 'package:collection/collection.dart'; +import 'package:data_table_2/data_table_2.dart'; + +ColumnSize? parseColumnSize(String? size, [ColumnSize? defValue]) { + if (size == null) { + return defValue; + } + return ColumnSize.values.firstWhereOrNull( + (e) => e.name.toLowerCase() == size.toLowerCase()) ?? + defValue; +} diff --git a/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/pubspec.yaml b/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/pubspec.yaml new file mode 100644 index 0000000000..1128910c74 --- /dev/null +++ b/sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2/pubspec.yaml @@ -0,0 +1,22 @@ +name: flet_datatable2 +description: Flet DataTable2 control +version: 0.1.0 +publish_to: none + +environment: + sdk: '>=3.3.0 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + data_table_2: 2.6.0 + + flet: + path: ../../../../../../../packages/flet + + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 diff --git a/sdk/python/packages/flet-flashlight/CHANGELOG.md b/sdk/python/packages/flet-flashlight/CHANGELOG.md new file mode 100644 index 0000000000..837ad16e40 --- /dev/null +++ b/sdk/python/packages/flet-flashlight/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [0.2.0] - 2025-06-26 + +## Added + +- Deployed online documentation: https://docs.flet.dev/flashlight/ +- `Flashlight` control new properties: `on`, `on_error` +- `Flashlight` control new methods: `is_available` +- New exception classes: + - `FlashlightException` + - `FlashlightEnableExistentUserException` + - `FlashlightEnableNotAvailableException` + - `FlashlightEnableException` + - `FlashlightDisableExistentUserException` + - `FlashlightDisableNotAvailableException` + - `FlashlightDisableException` + +### Changed + +- Refactored `Flashlight` control to use `@ft.control` dataclass-style definition and switched to `Service` control type. +- `Flashlight` must now be added to `Page.services` instead of `Page.overlay` due to control type change. + +## [0.1.0] - 2025-01-15 + +Initial release. + + +[0.2.0]: https://github.com/flet-dev/flet-flashlight/compare/0.1.0...0.2.0 +[0.1.0]: https://github.com/flet-dev/flet-flashlight/releases/tag/0.1.0 diff --git a/sdk/python/packages/flet-flashlight/LICENSE b/sdk/python/packages/flet-flashlight/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/sdk/python/packages/flet-flashlight/LICENSE @@ -0,0 +1,201 @@ + 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 such Contributor that are necessarily infringed 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 consequential 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-flashlight/README.md b/sdk/python/packages/flet-flashlight/README.md new file mode 100644 index 0000000000..1d7df6249b --- /dev/null +++ b/sdk/python/packages/flet-flashlight/README.md @@ -0,0 +1,42 @@ +# flet-flashlight + +[![pypi](https://img.shields.io/pypi/v/flet-flashlight.svg)](https://pypi.python.org/pypi/flet-flashlight) +[![downloads](https://static.pepy.tech/badge/flet-flashlight/month)](https://pepy.tech/project/flet-flashlight) +[![license](https://img.shields.io/github/license/flet-dev/flet.svg)](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-flashlight/LICENSE) + +A [Flet](https://flet.dev) extension to manage the device torch/flashlight. + +It is based on the [flashlight](https://pub.dev/packages/flashlight) Flutter package. + +> **Important:** Add `Flashlight` instances to `page.services` before calling toggle or other methods. + +## Documentation + +Detailed documentation to this package can be found [here](https://docs.flet.dev/flashlight/). + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| ❌ | ❌ | ❌ | βœ… | βœ… | ❌ | + +## Usage + +### Installation + +To install the `flet-flashlight` package and add it to your project dependencies: + +- Using `uv`: + ```bash + uv add flet-flashlight + ``` + +- Using `pip`: + ```bash + pip install flet-flashlight + ``` + 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/examples/controls/flashlight). diff --git a/sdk/python/packages/flet-flashlight/pyproject.toml b/sdk/python/packages/flet-flashlight/pyproject.toml new file mode 100644 index 0000000000..d1695c08e2 --- /dev/null +++ b/sdk/python/packages/flet-flashlight/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "flet-flashlight" +version = "0.1.0" +description = "Control device torch/flashlight from 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://docs.flet.dev/flashlight" +Repository = "https://github.com/flet-dev/flet/tree/main/sdk/python/packages/flet-flashlight" +Issues = "https://github.com/flet-dev/flet/issues" + +[tool.setuptools.package-data] +"flutter.flet_flashlight" = ["**/*"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/sdk/python/packages/flet-flashlight/src/flet_flashlight/__init__.py b/sdk/python/packages/flet-flashlight/src/flet_flashlight/__init__.py new file mode 100644 index 0000000000..43dae39cf8 --- /dev/null +++ b/sdk/python/packages/flet-flashlight/src/flet_flashlight/__init__.py @@ -0,0 +1,21 @@ +from .exceptions import ( + FlashlightDisableException, + FlashlightDisableExistentUserException, + FlashlightDisableNotAvailableException, + FlashlightEnableException, + FlashlightEnableExistentUserException, + FlashlightEnableNotAvailableException, + FlashlightException, +) +from .flashlight import Flashlight + +__all__ = [ + "Flashlight", + "FlashlightDisableException", + "FlashlightDisableExistentUserException", + "FlashlightDisableNotAvailableException", + "FlashlightEnableException", + "FlashlightEnableExistentUserException", + "FlashlightEnableNotAvailableException", + "FlashlightException", +] diff --git a/sdk/python/packages/flet-flashlight/src/flet_flashlight/exceptions.py b/sdk/python/packages/flet-flashlight/src/flet_flashlight/exceptions.py new file mode 100644 index 0000000000..ae0b7e87ad --- /dev/null +++ b/sdk/python/packages/flet-flashlight/src/flet_flashlight/exceptions.py @@ -0,0 +1,65 @@ +__all__ = [ + "FlashlightDisableException", + "FlashlightDisableExistentUserException", + "FlashlightDisableNotAvailableException", + "FlashlightEnableException", + "FlashlightEnableExistentUserException", + "FlashlightEnableNotAvailableException", + "FlashlightException", +] + + +class FlashlightException(Exception): + """ + Base class for all [`Flashlight`][(p).] exceptions. + + See these subclasses: + - [`FlashlightEnableExistentUserException`][(p).] + - [`FlashlightEnableNotAvailableException`][(p).] + - [`FlashlightEnableException`][(p).] + - [`FlashlightDisableExistentUserException`][(p).] + - [`FlashlightDisableNotAvailableException`][(p).] + - [`FlashlightDisableException`][(p).] + """ + + +class FlashlightEnableExistentUserException(FlashlightException): + """ + An attempt was made to turn on the torch + but it was detected that the camera was being used by another process. + This means that the torch cannot be controlled. + """ + + +class FlashlightEnableNotAvailableException(FlashlightException): + """ + An attempt was made to turn on the torch + but it was detected that the device does not have one equipped. + """ + + +class FlashlightEnableException(FlashlightException): + """ + An error occurred while trying to turn on the device torch. + """ + + +class FlashlightDisableExistentUserException(FlashlightException): + """ + An attempt was made to turn off the torch + but it was detected that the camera was being used by another process. + This means that the torch cannot be controlled. + """ + + +class FlashlightDisableNotAvailableException(FlashlightException): + """ + An attempt was made to turn off the torch, + but it was detected that the device does not have one equipped. + """ + + +class FlashlightDisableException(FlashlightException): + """ + An error occurred while trying to turn off the device torch. + """ diff --git a/sdk/python/packages/flet-flashlight/src/flet_flashlight/flashlight.py b/sdk/python/packages/flet-flashlight/src/flet_flashlight/flashlight.py new file mode 100644 index 0000000000..feffac5a6c --- /dev/null +++ b/sdk/python/packages/flet-flashlight/src/flet_flashlight/flashlight.py @@ -0,0 +1,89 @@ +from typing import Optional + +import flet as ft + +from .exceptions import ( + FlashlightDisableException, + FlashlightDisableExistentUserException, + FlashlightDisableNotAvailableException, + FlashlightEnableException, + FlashlightEnableExistentUserException, + FlashlightEnableNotAvailableException, + FlashlightException, +) + +__all__ = ["Flashlight"] + + +@ft.control("Flashlight") +class Flashlight(ft.Service): + """ + A control to use FlashLight. Works on iOS and Android. + """ + + on = False + """ + Whether the flashlight is currently turned on. + """ + + on_error: Optional[ft.ControlEventHandler["Flashlight"]] = None + """ + Fires when an error occurs. + + The [`data`][flet.Event.data] property of the event handler argument + contains information on the error. + """ + + async def turn_on(self): + """ + Turns the flashlight on. + """ + r = await self._invoke_method("on") + if r is True: + self.on = True + else: # error occured + error_type = r.get("error_type") + error_msg = r.get("error_msg") + if error_type == "EnableTorchExistentUserException": + raise FlashlightEnableExistentUserException(error_msg) + elif error_type == "EnableTorchNotAvailableException": + raise FlashlightEnableNotAvailableException(error_msg) + else: + raise FlashlightEnableException(error_msg) + + async def turn_off(self): + """ + Turns the flashlight off. + """ + r = await self._invoke_method("off") + if r is True: + self.on = False + else: # error occured + error_type = r.get("error_type") + error_msg = r.get("error_msg") + if error_type == "DisableTorchExistentUserException": + raise FlashlightDisableExistentUserException(error_msg) + elif error_type == "DisableTorchNotAvailableException": + raise FlashlightDisableNotAvailableException(error_msg) + else: + raise FlashlightDisableException(error_msg) + + async def toggle(self): + """ + Toggles the flashlight on and off. + """ + if self.on: + await self.turn_off() + else: + await self.turn_on() + + async def is_available(self): + """ + Checks if the flashlight is available on the device. + """ + r = await self._invoke_method("is_available") + if isinstance(r, bool): + return r + else: # error occured + error_msg = r.get("error_msg") + raise FlashlightException(error_msg) diff --git a/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/.gitignore b/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/.gitignore new file mode 100644 index 0000000000..ed7794f2ab --- /dev/null +++ b/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/.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-flashlight/src/flutter/flet_flashlight/analysis_options.yaml b/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/analysis_options.yaml new file mode 100644 index 0000000000..8df683425b --- /dev/null +++ b/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/analysis_options.yaml @@ -0,0 +1,5 @@ +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-flashlight/src/flutter/flet_flashlight/lib/flet_flashlight.dart b/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/lib/flet_flashlight.dart new file mode 100644 index 0000000000..42992b0a97 --- /dev/null +++ b/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/lib/flet_flashlight.dart @@ -0,0 +1,3 @@ +library flet_flashlight; + +export 'src/extension.dart' show Extension; diff --git a/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/lib/src/extension.dart b/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/lib/src/extension.dart new file mode 100644 index 0000000000..215b5db8d2 --- /dev/null +++ b/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/lib/src/extension.dart @@ -0,0 +1,15 @@ +import 'package:flet/flet.dart'; + +import 'flashlight.dart'; + +class Extension extends FletExtension { + @override + FletService? createService(Control control) { + switch (control.type) { + case "Flashlight": + return FlashlightControl(control: control); + default: + return null; + } + } +} diff --git a/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/lib/src/flashlight.dart b/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/lib/src/flashlight.dart new file mode 100644 index 0000000000..627442782d --- /dev/null +++ b/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/lib/src/flashlight.dart @@ -0,0 +1,101 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/widgets.dart'; +import 'package:torch_light/torch_light.dart'; + +class FlashlightControl extends FletService { + FlashlightControl({required super.control}); + + @override + void init() { + super.init(); + debugPrint("Flashlight(${control.id}).init: ${control.properties}"); + control.addInvokeMethodListener(_invokeMethod); + } + + Future _invokeMethod(String name, dynamic args) async { + debugPrint("Flashlight.$name($args)"); + if (isMobilePlatform()) { + Map? errorInfo; + switch (name) { + case "on": + try { + await TorchLight.enableTorch(); + return true; + } catch (e) { + if (e is EnableTorchExistentUserException) { + errorInfo = { + "error_type": "EnableTorchExistentUserException", + "error_msg": e.message + }; + } else if (e is EnableTorchNotAvailableException) { + errorInfo = { + "error_type": "EnableTorchNotAvailableException", + "error_msg": e.message + }; + } else { + errorInfo = { + "error_type": "EnableTorchException", + "error_msg": (e as EnableTorchException).message + }; + } + control.triggerEvent("error", errorInfo); + debugPrint( + "Error enabling Flashlight: ${errorInfo["error_type"]}(${errorInfo["error_msg"]})"); + return errorInfo; + } + case "off": + try { + await TorchLight.disableTorch(); + return true; + } catch (e) { + if (e is DisableTorchExistentUserException) { + errorInfo = { + "error_type": "DisableTorchExistentUserException", + "error_msg": e.message + }; + } else if (e is DisableTorchNotAvailableException) { + errorInfo = { + "error_type": "DisableTorchNotAvailableException", + "error_msg": e.message + }; + } else { + errorInfo = { + "error_type": "DisableTorchException", + "error_msg": (e as DisableTorchException).message + }; + } + control.triggerEvent("error", errorInfo); + debugPrint( + "Error disabling Flashlight: ${errorInfo["error_type"]}(${errorInfo["error_msg"]})"); + return errorInfo; + } + case "is_available": + try { + final available = await TorchLight.isTorchAvailable(); + return available; + } on EnableTorchException catch (e) { + errorInfo = { + "error_type": "EnableTorchException", + "error_msg": e.message + }; + control.triggerEvent("error", errorInfo); + debugPrint( + "Error checking Flashlight availability: EnableTorchException(${e.message})"); + return errorInfo; + } + default: + throw Exception("Unknown Flashlight method: $name"); + } + } else { + throw Exception( + "Flashlight control is supported only on Android and iOS devices."); + } + } + + @override + void dispose() { + debugPrint("Flashlight(${control.id}).dispose"); + control.removeInvokeMethodListener(_invokeMethod); + super.dispose(); + } +} 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 new file mode 100644 index 0000000000..1ca7a12bdf --- /dev/null +++ b/sdk/python/packages/flet-flashlight/src/flutter/flet_flashlight/pubspec.yaml @@ -0,0 +1,23 @@ +name: flet_flashlight +description: Flet Flashlight control +version: 0.1.0 +publish_to: none + +environment: + sdk: '>=3.2.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + collection: ^1.16.0 + torch_light: 1.1.0 + + flet: + path: ../../../../../../../packages/flet + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 diff --git a/sdk/python/packages/flet-geolocator/CHANGELOG.md b/sdk/python/packages/flet-geolocator/CHANGELOG.md new file mode 100644 index 0000000000..c9bdadbf29 --- /dev/null +++ b/sdk/python/packages/flet-geolocator/CHANGELOG.md @@ -0,0 +1,50 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [0.2.0] - 2025-06-26 + +### Added + +- Deployed online documentation: https://docs.flet.dev/geolocator/ +- `Geolocator` control new methods: `distance_between` +- `Geolocator` control new properties: `position`, `configuration` +- New dataclasses: + - `GeolocatorConfiguration` + - `GeolocatorWebConfiguration` + - `GeolocatorIosConfiguration` + - `GeolocatorAndroidConfiguration` + - `ForegroundNotificationConfiguration` + +### Changed + +- Refactored `Geolocator` control to use `@ft.control` dataclass-style definition and switched to `Service` control type + +#### Breaking Changes + +- `Geolocator` must now be added to `Page.services` instead of `Page.overlay`. +- `Geolocator` method `get_current_position_async` parameters changed: + - removed `accuracy` + - `location_settings` renamed to `configuration` (type changed) + - `wait_timeout` renamed to `timeout` +- In all `Geolocator` methods, parameter `wait_timeout` renamed to `timeout`. +- The following `Geolocator` sync methods were made [`async`](https://docs.python.org/3/library/asyncio.html): + - `get_current_position` + - `get_last_known_position` + - `get_permission_status` + - `request_permission` + - `is_location_service_enabled` + - `open_app_settings` + - `open_location_settings` +- Enum `GeolocatorActivityType` renamed to `GeolocatorIosActivityType` + +## [0.1.0] - 2025-01-15 + +Initial release. + + +[0.2.0]: https://github.com/flet-dev/flet-geolocator/compare/0.1.0...0.2.0 +[0.1.0]: https://github.com/flet-dev/flet-geolocator/releases/tag/0.1.0 diff --git a/sdk/python/packages/flet-geolocator/LICENSE b/sdk/python/packages/flet-geolocator/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/sdk/python/packages/flet-geolocator/LICENSE @@ -0,0 +1,201 @@ + 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 such Contributor that are necessarily infringed 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 consequential 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-geolocator/README.md b/sdk/python/packages/flet-geolocator/README.md new file mode 100644 index 0000000000..16dcc448e2 --- /dev/null +++ b/sdk/python/packages/flet-geolocator/README.md @@ -0,0 +1,48 @@ +# flet-geolocator + +[![pypi](https://img.shields.io/pypi/v/flet-geolocator.svg)](https://pypi.python.org/pypi/flet-geolocator) +[![downloads](https://static.pepy.tech/badge/flet-geolocator/month)](https://pepy.tech/project/flet-geolocator) +[![license](https://img.shields.io/github/license/flet-dev/flet.svg)](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-geolocator/LICENSE) + +Adds geolocation capabilities to your [Flet](https://flet.dev) apps. + +Features include: +- Get the last known location; +- Get the current location of the device; +- Get continuous location updates; +- Check if location services are enabled on the device. + +It is based on the [geolocator](https://pub.dev/packages/geolocator) Flutter package. + +> **Important:** Add the `Geolocator` instance to `page.services` before invoking its methods. + +## Documentation + +Detailed documentation to this package can be found [here](https://docs.flet.dev/geolocator/). + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | + +## Usage + +### Installation + +To install the `flet-geolocator` package and add it to your project dependencies: + +- Using `uv`: + ```bash + uv add flet-geolocator + ``` + +- Using `pip`: + ```bash + pip install flet-geolocator + ``` + 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/examples/controls/geolocator). diff --git a/sdk/python/packages/flet-geolocator/pyproject.toml b/sdk/python/packages/flet-geolocator/pyproject.toml new file mode 100644 index 0000000000..484c5a66ea --- /dev/null +++ b/sdk/python/packages/flet-geolocator/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "flet-geolocator" +version = "0.1.0" +description = "Adds geolocation capabilities to your 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://docs.flet.dev/geolocator" +Repository = "https://github.com/flet-dev/flet/tree/main/sdk/python/packages/flet-geolocator" +Issues = "https://github.com/flet-dev/flet/issues" + +[tool.setuptools.package-data] +"flutter.flet_geolocator" = ["**/*"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/sdk/python/packages/flet-geolocator/src/flet_geolocator/__init__.py b/sdk/python/packages/flet-geolocator/src/flet_geolocator/__init__.py new file mode 100644 index 0000000000..d7d0fc12a4 --- /dev/null +++ b/sdk/python/packages/flet-geolocator/src/flet_geolocator/__init__.py @@ -0,0 +1,27 @@ +from flet_geolocator.geolocator import Geolocator +from flet_geolocator.types import ( + ForegroundNotificationConfiguration, + GeolocatorAndroidConfiguration, + GeolocatorConfiguration, + GeolocatorIosActivityType, + GeolocatorIosConfiguration, + GeolocatorPermissionStatus, + GeolocatorPosition, + GeolocatorPositionAccuracy, + GeolocatorPositionChangeEvent, + GeolocatorWebConfiguration, +) + +__all__ = [ + "ForegroundNotificationConfiguration", + "Geolocator", + "GeolocatorAndroidConfiguration", + "GeolocatorConfiguration", + "GeolocatorIosActivityType", + "GeolocatorIosConfiguration", + "GeolocatorPermissionStatus", + "GeolocatorPosition", + "GeolocatorPositionAccuracy", + "GeolocatorPositionChangeEvent", + "GeolocatorWebConfiguration", +] diff --git a/sdk/python/packages/flet-geolocator/src/flet_geolocator/geolocator.py b/sdk/python/packages/flet-geolocator/src/flet_geolocator/geolocator.py new file mode 100644 index 0000000000..45dfa60c97 --- /dev/null +++ b/sdk/python/packages/flet-geolocator/src/flet_geolocator/geolocator.py @@ -0,0 +1,196 @@ +from dataclasses import field +from typing import Optional + +import flet as ft +from flet_geolocator.types import ( + GeolocatorConfiguration, + GeolocatorPermissionStatus, + GeolocatorPosition, + GeolocatorPositionChangeEvent, +) + +__all__ = ["Geolocator"] + + +@ft.control("Geolocator") +class Geolocator(ft.Service): + """ + A control that allows you to fetch GPS data from your device. + """ + + configuration: Optional[GeolocatorConfiguration] = None + """ + Some additional configuration. + """ + + on_position_change: Optional[ft.EventHandler[GeolocatorPositionChangeEvent]] = None + """ + Fires when the position of the device changes. + """ + + on_error: Optional[ft.ControlEventHandler["Geolocator"]] = None + """ + Fires when an error occurs. + + The [`data`][flet.Event.data] property of the event + handler argument contains information on the error. + """ + + position: Optional[GeolocatorPosition] = field( + default=None, init=False + ) # TODO: make this property readonly + """ + The current position of the device. (read-only) + + Starts as `None` and will be updated when the position changes. + """ + + async def get_current_position( + self, + configuration: Optional[GeolocatorConfiguration] = None, + ) -> GeolocatorPosition: + """ + Gets the current position of the device with the desired accuracy and settings. + + Note: + Depending on the availability of different location services, + this can take several seconds. It is recommended to call the + [`get_last_known_position`][..] method first to receive a + known/cached position and update it with the result of the + [`get_current_position`][..] method. + + Args: + configuration: Additional configuration for the location request. + If not specified, then the [`Geolocator.configuration`][(p).] + property is used. + + Returns: + The current position of the device as a [`GeolocatorPosition`][(p).]. + """ + r = await self._invoke_method( + method_name="get_current_position", + arguments={"configuration": configuration or self.configuration}, + ) + return GeolocatorPosition(**r) + + async def get_last_known_position(self) -> GeolocatorPosition: + """ + Gets the last known position stored on the user's device. + The accuracy can be defined using the + [`Geolocator.configuration`][(p).] property. + + Note: + This method is not supported on web platform. + + Returns: + The last known position of the device as a [`GeolocatorPosition`][(p).]. + + Raises: + AssertionError: If invoked on a web platform. + """ + assert not self.page.web, "get_last_known_position is not supported on web" + r = await self._invoke_method( + "get_last_known_position", + ) + return GeolocatorPosition(**r) + + async def get_permission_status(self) -> GeolocatorPermissionStatus: + """ + Gets which permission the app has been granted to access the device's location. + + Returns: + The status of the permission. + """ + r = await self._invoke_method( + "get_permission_status", + ) + return GeolocatorPermissionStatus(r) + + async def request_permission(self) -> GeolocatorPermissionStatus: + """ + Requests the device for access to the device's location. + + Returns: + The status of the permission request. + """ + r = await self._invoke_method( + "request_permission", + ) + return GeolocatorPermissionStatus(r) + + async def is_location_service_enabled(self) -> bool: + """ + Checks if location service is enabled. + + Returns: + `True` if location service is enabled, `False` otherwise. + """ + return await self._invoke_method("is_location_service_enabled") + + async def open_app_settings(self) -> bool: + """ + Attempts to open the app's settings. + + Note: + This method is not supported on web platform. + + Returns: + `True` if the app's settings were opened successfully, `False` otherwise. + + Raises: + AssertionError: If invoked on a web platform. + """ + assert not self.page.web, "open_app_settings is not supported on web" + return await self._invoke_method( + "open_app_settings", + ) + + async def open_location_settings(self) -> bool: + """ + Attempts to open the device's location settings. + + Note: + This method is not supported on web platform. + + Returns: + `True` if the device's settings were opened successfully, `False` otherwise. + + Raises: + AssertionError: If invoked on a web platform. + """ + assert not self.page.web, "open_location_settings is not supported on web" + return await self._invoke_method( + "open_location_settings", + ) + + async def distance_between( + self, + start_latitude: ft.Number, + start_longitude: ft.Number, + end_latitude: ft.Number, + end_longitude: ft.Number, + ) -> ft.Number: + """ + Calculates the distance between the supplied coordinates in meters. + + The distance between the coordinates is calculated using the + Haversine formula (see https://en.wikipedia.org/wiki/Haversine_formula). + + Args: + start_latitude: The latitude of the starting point, in degrees. + start_longitude: The longitude of the starting point, in degrees. + end_latitude: The latitude of the ending point, in degrees. + end_longitude: The longitude of the ending point, in degrees. + + Returns: + The distance between the coordinates in meters. + """ + return await self._invoke_method( + method_name="distance_between", + arguments={ + "start_latitude": start_latitude, + "start_longitude": start_longitude, + "end_latitude": end_latitude, + "end_longitude": end_longitude, + }, + ) diff --git a/sdk/python/packages/flet-geolocator/src/flet_geolocator/types.py b/sdk/python/packages/flet-geolocator/src/flet_geolocator/types.py new file mode 100644 index 0000000000..0223ab4930 --- /dev/null +++ b/sdk/python/packages/flet-geolocator/src/flet_geolocator/types.py @@ -0,0 +1,420 @@ +import datetime +from dataclasses import dataclass, field +from enum import Enum +from typing import TYPE_CHECKING, Optional + +import flet as ft + +if TYPE_CHECKING: + from flet_geolocator.geolocator import Geolocator # noqa + +__all__ = [ + "ForegroundNotificationConfiguration", + "GeolocatorAndroidConfiguration", + "GeolocatorConfiguration", + "GeolocatorIosActivityType", + "GeolocatorIosConfiguration", + "GeolocatorPermissionStatus", + "GeolocatorPosition", + "GeolocatorPositionAccuracy", + "GeolocatorPositionChangeEvent", + "GeolocatorWebConfiguration", +] + + +class GeolocatorPositionAccuracy(Enum): + """Represent the possible location accuracy values.""" + + LOWEST = "lowest" + """ + Location is accurate within a distance of 3000m on iOS and 500m on Android. + + On Android, corresponds to + [PRIORITY_PASSIVE](https://developers.google.com/android/reference/com/google/android/gms/location/Priority#public-static-final-int-priority_passive). + """ + + LOW = "low" + """ + Location is accurate within a distance of 1000m on iOS and 500m on Android. + + On Android, corresponds to + [PRIORITY_LOW_POWER](https://developers.google.com/android/reference/com/google/android/gms/location/Priority#public-static-final-int-priority_low_power). + """ + + MEDIUM = "medium" + """ + Location is accurate within a distance of 100m on iOS and between 100m and + 500m on Android. + + On Android, corresponds to + [PRIORITY_BALANCED_POWER_ACCURACY](https://developers.google.com/android/reference/com/google/android/gms/location/Priority#public-static-final-int-priority_balanced_power_accuracy). + """ + + HIGH = "high" + """ + Location is accurate within a distance of 10m on iOS and between 0m and + 100m on Android. + + On Android, corresponds to + [PRIORITY_HIGH_ACCURACY](https://developers.google.com/android/reference/com/google/android/gms/location/Priority#public-static-final-int-priority_high_accuracy). + """ + + BEST = "best" + """ + Location is accurate within a distance of ~0m on iOS and between 0m and + 100m on Android. + + On Android, corresponds to + [PRIORITY_HIGH_ACCURACY](https://developers.google.com/android/reference/com/google/android/gms/location/Priority#public-static-final-int-priority_high_accuracy). + """ + + BEST_FOR_NAVIGATION = "bestForNavigation" + """ + Location accuracy is optimized for navigation on iOS and matches the + `GeolocatorPositionAccuracy.BEST` on Android. + + On Android, corresponds to + [PRIORITY_HIGH_ACCURACY](https://developers.google.com/android/reference/com/google/android/gms/location/Priority#public-static-final-int-priority_high_accuracy). + """ + + REDUCED = "reduced" + """ + Location accuracy is reduced for iOS 14+ devices. Matches + `GeolocatorPositionAccuracy.LOWEST` on iOS 13 and below and all other platforms. + + On Android, corresponds to + [PRIORITY_PASSIVE](https://developers.google.com/android/reference/com/google/android/gms/location/Priority#public-static-final-int-priority_passive). + """ + + +class GeolocatorPermissionStatus(Enum): + """Represent the possible location permissions.""" + + DENIED = "denied" + """ + Permission to access the device's location is denied. + + The app should try to request permission using the + [`Geolocator.request_permission`][(p).] method. + """ + + DENIED_FOREVER = "deniedForever" + """ + Permission to access the device's location is permanently denied. + + When requesting permissions, the permission dialog will not be shown until the + user updates the permission in the app settings. + """ + + WHILE_IN_USE = "whileInUse" + """ + Permission to access the device's location is allowed only while the app is in use. + """ + + ALWAYS = "always" + """ + Permission to access the device's location is allowed even when the app is + running in the background. + """ + + UNABLE_TO_DETERMINE = "unableToDetermine" + """ + Permission status cannot be determined. + + This status is only returned by the [`Geolocator.request_permission`][(p).] method + on the web platform for browsers that did not implement the Permissions API. + See: https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API + """ + + +class GeolocatorIosActivityType(Enum): + """Represents the possible iOS activity types.""" + + AUTOMOTIVE_NAVIGATION = "automotiveNavigation" + """ + The location manager is being used specifically during vehicular + navigation to track location changes to the automobile. + """ + + FITNESS = "fitness" + """ + The location manager is being used to track fitness activities such as + walking, running, cycling, and so on. + """ + + OTHER_NAVIGATION = "otherNavigation" + """ + The location manager is being used to track movements for other types of + vehicular navigation that are not automobile related. + """ + + AIRBORNE = "airborne" + """ + The location manager is being used specifically during + airborne activities. + """ + + OTHER = "other" + """ + The location manager is being used for an unknown activity. + """ + + +@dataclass +class GeolocatorPosition: + """Detailed location information.""" + + latitude: Optional[ft.Number] = None + """ + The latitude of this position in degrees normalized to the interval -90.0 + to +90.0 (both inclusive). + """ + + longitude: Optional[ft.Number] = None + """ + The longitude of the position in degrees normalized to the interval -180 + (exclusive) to +180 (inclusive). + """ + + speed: Optional[ft.Number] = None + """ + The speed at which the device is traveling in meters per second over ground. + + The speed is not available on all devices. + In these cases the value is `0.0`. + """ + + altitude: Optional[ft.Number] = None + """ + The altitude of the device in meters. + + The altitude is not available on all devices. + In these cases the returned value is `0.0`. + """ + + timestamp: datetime.datetime = None + """ + The time at which this position was determined. + """ + + accuracy: Optional[ft.Number] = None + """ + The estimated horizontal accuracy of the position in meters. + + The accuracy is not available on all devices. + In these cases the value is `0.0`. + """ + + altitude_accuracy: Optional[ft.Number] = None + """ + The estimated vertical accuracy of the position in meters. + + The accuracy is not available on all devices. + In these cases the value is `0.0`. + """ + + heading: Optional[ft.Number] = None + """ + The heading in which the device is traveling in degrees. + + The heading is not available on all devices. + In these cases the value is `0.0`. + """ + + heading_accuracy: Optional[ft.Number] = None + """ + The estimated heading accuracy of the position in degrees. + + The heading accuracy is not available on all devices. + In these cases the value is `0.0`. + """ + + speed_accuracy: Optional[ft.Number] = None + """ + The estimated speed accuracy of this position, in meters per second. + + The speed accuracy is not available on all devices. + In these cases the value is `0.0`. + """ + + floor: Optional[int] = None + """ + The floor specifies the floor of the building on which the device is + located. + + The floor property is only available on iOS + and only when the information is available. + In all other cases this value will be `None`. + """ + + mocked: Optional[bool] = None + """ + Will be `True` on Android (starting from API level 18) when the location came + from the mocked provider. + + On iOS this value will always be `False`. + """ + + +@dataclass +class GeolocatorConfiguration: + accuracy: GeolocatorPositionAccuracy = GeolocatorPositionAccuracy.BEST + """ + Defines the desired accuracy that should be used to determine the location data. + """ + + distance_filter: int = 0 + """ + The minimum distance (measured in meters) a device must move + horizontally before an update event is generated. + + Set to `0` when you want to be notified of all movements. + """ + + time_limit: ft.DurationValue = None + """ + Specifies a timeout interval. + + For no time limit, set to `None`. + """ + + +@dataclass +class GeolocatorWebConfiguration(GeolocatorConfiguration): + """Web specific settings.""" + + maximum_age: ft.DurationValue = field(default_factory=lambda: ft.Duration()) + """ + A value indicating the maximum age of a possible cached + position that is acceptable to return. If set to 0, it means + that the device cannot use a cached position and must + attempt to retrieve the real current position. + """ + + +@dataclass +class GeolocatorIosConfiguration(GeolocatorConfiguration): + """iOS specific settings.""" + + activity_type: GeolocatorIosActivityType = GeolocatorIosActivityType.OTHER + """ + The location manager uses the information in this property as a cue + to determine when location updates may be automatically paused. + """ + + pause_location_updates_automatically: bool = False + """ + Allows the location manager to pause updates to improve battery life + on the target device without sacrificing location data. + When this property is set to `True`, the location manager pauses updates + (and powers down the appropriate hardware) at times when the + location data is unlikely to change. + """ + + show_background_location_indicator: bool = False + """ + Flag to ask the Apple OS to show the background location indicator (iOS only) + if app starts up and background and requests the users location. + + For this setting to work and for the location to be retrieved the user must + have granted "always" permissions for location retrieval. + """ + + allow_background_location_updates: bool = True + """ + Flag to allow the app to receive location updates in the background (iOS only) + + Note: + For this setting to work `Info.plist` should contain the following keys: + - UIBackgroundModes and the value should contain "location" + - NSLocationAlwaysUsageDescription + """ + + +@dataclass +class ForegroundNotificationConfiguration: + notification_title: str + """ + The title used for the foreground service notification. + """ + + notification_text: str + """ + The body used for the foreground service notification. + """ + + notification_channel_name: str = "Background Location" + """ + The user visible name of the notification channel. + + The notification channel name will be displayed in the system settings. + The maximum recommended length is 40 characters, the name might be + truncated if it is to long. Default value: "Background Location". + """ + + notification_enable_wake_lock: bool = False + """ + When enabled, a Wakelock is acquired when background execution is started. + + If this is false then the system can still sleep and all location + events will be received at once when the system wakes up again. + + Wake lock permissions should be obtained first by using a permissions library. + """ + + notification_enable_wifi_lock: bool = False + """ + When enabled, a WifiLock is acquired when background execution is started. + This allows the application to keep the Wi-Fi radio awake, even when the + user has not used the device in a while + (e.g. for background network communications). + + Wifi lock permissions should be obtained first by using a permissions library. + """ + + notification_set_ongoing: bool = False + """ + When enabled, the displayed notification is persistent and + the user cannot dismiss it. + """ + + # foreground_notification_color: Optional[ft.ColorValue] = None + + +@dataclass +class GeolocatorAndroidConfiguration(GeolocatorConfiguration): + """Android specific settings.""" + + interval_duration: ft.DurationValue = field( + default_factory=lambda: ft.Duration(milliseconds=5000) + ) + """ + The desired interval for active location updates. + """ + + use_msl_altitude: bool = False + """ + Whether altitude should be calculated as MSL (EGM2008) from NMEA messages + and reported as the altitude instead of using the geoidal height (WSG84). Setting + this property true will help to align Android altitude to that of iOS which + uses MSL. + + If the NMEA message is empty then the altitude reported will still be + the standard WSG84 altitude from the GPS receiver. + + MSL Altitude is only available starting from Android N and not all devices support + NMEA message returning $GPGGA sequences. + + This property only works with position stream updates and has no effect when + getting the current position or last known position. + """ + + foreground_notification_config: Optional[ForegroundNotificationConfiguration] = None + + +@dataclass +class GeolocatorPositionChangeEvent(ft.Event["Geolocator"]): + position: GeolocatorPosition + """ + The current/new position of the device. + """ diff --git a/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/.gitignore b/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/.gitignore new file mode 100644 index 0000000000..ed7794f2ab --- /dev/null +++ b/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/.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-geolocator/src/flutter/flet_geolocator/analysis_options.yaml b/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/analysis_options.yaml new file mode 100644 index 0000000000..a5744c1cfb --- /dev/null +++ b/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/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-geolocator/src/flutter/flet_geolocator/lib/flet_geolocator.dart b/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/lib/flet_geolocator.dart new file mode 100644 index 0000000000..39799a4a71 --- /dev/null +++ b/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/lib/flet_geolocator.dart @@ -0,0 +1,3 @@ +library flet_geolocator; + +export "src/extension.dart" show Extension; diff --git a/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/lib/src/extension.dart b/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/lib/src/extension.dart new file mode 100644 index 0000000000..6a282e4915 --- /dev/null +++ b/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/lib/src/extension.dart @@ -0,0 +1,15 @@ +import 'package:flet/flet.dart'; + +import 'geolocator.dart'; + +class Extension extends FletExtension { + @override + FletService? createService(Control control) { + switch (control.type) { + case "Geolocator": + return GeolocatorService(control: control); + default: + return null; + } + } +} diff --git a/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/lib/src/geolocator.dart b/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/lib/src/geolocator.dart new file mode 100644 index 0000000000..696a7a0703 --- /dev/null +++ b/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/lib/src/geolocator.dart @@ -0,0 +1,112 @@ +import 'dart:async'; + +import 'package:flet/flet.dart'; +import 'package:flutter/foundation.dart'; +import 'package:geolocator/geolocator.dart'; + +import 'utils/geolocator.dart'; + +class GeolocatorService extends FletService { + GeolocatorService({required super.control}); + + StreamSubscription? _onPositionChangedSubscription; + + @override + void init() { + super.init(); + debugPrint("Geolocator(${control.id}).init: ${control.properties}"); + control.addInvokeMethodListener(_invokeMethod); + registerEvents(); + } + + @override + void update() { + debugPrint("Geolocator(${control.id}).update: ${control.properties}"); + registerEvents(); + } + + void registerEvents() { + _onPositionChangedSubscription = Geolocator.getPositionStream( + locationSettings: parseLocationSettings( + control.get("configuration"), + // Theme.of(context), + ), + ).listen( + (Position? position) { + if (position != null) { + control.updateProperties({"position": position.toMap()}); + control + .triggerEvent("position_change", {"position": position.toMap()}); + } + }, + onError: (Object error, StackTrace stackTrace) { + control.triggerEvent("error", error.toString()); + }, + onDone: () { + // done + }, + ); + } + + Future _invokeMethod(String name, dynamic args) async { + debugPrint("Geolocator.$name($args)"); + switch (name) { + case "request_permission": + var permission = await Geolocator.requestPermission(); + return permission.name; + case "get_permission_status": + var permission = await Geolocator.checkPermission(); + return permission.name; + case "is_location_service_enabled": + var serviceEnabled = await Geolocator.isLocationServiceEnabled(); + return serviceEnabled; + case "open_app_settings": + if (!kIsWeb) { + return await Geolocator.openAppSettings(); + } + break; + case "open_location_settings": + if (!kIsWeb) { + return await Geolocator.openLocationSettings(); + } + break; + case "get_last_known_position": + if (!kIsWeb) { + return (await Geolocator.getLastKnownPosition())?.toMap(); + } + break; + case "get_current_position": + try { + Position currentPosition = await Geolocator.getCurrentPosition( + locationSettings: parseLocationSettings(args["settings"]), + ); + return currentPosition.toMap(); + } catch (error) { + control.triggerEvent("error", error.toString()); + break; + } + case "distance_between": + var p = [ + args["start_latitude"], + args["start_longitude"], + args["end_latitude"], + args["end_longitude"] + ]; + + if (p.every((e) => e != null)) { + return Geolocator.distanceBetween(p[0], p[1], p[2], p[3]); + } + break; + default: + throw Exception("Unknown Geolocator method: $name"); + } + } + + @override + void dispose() { + debugPrint("Geolocator(${control.id}).dispose()"); + control.removeInvokeMethodListener(_invokeMethod); + _onPositionChangedSubscription?.cancel(); + super.dispose(); + } +} diff --git a/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/lib/src/utils/geolocator.dart b/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/lib/src/utils/geolocator.dart new file mode 100644 index 0000000000..d1056a6aa6 --- /dev/null +++ b/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/lib/src/utils/geolocator.dart @@ -0,0 +1,102 @@ +import 'package:collection/collection.dart'; +import 'package:flet/flet.dart'; +import 'package:flutter/foundation.dart'; +import 'package:geolocator/geolocator.dart'; + +LocationAccuracy? parseLocationAccuracy(String? value, + [LocationAccuracy? defaultValue]) { + if (value == null) return defaultValue; + return LocationAccuracy.values.firstWhereOrNull( + (e) => e.name.toLowerCase() == value.toLowerCase()) ?? + defaultValue; +} + +extension PositionExtension on Position { + Map toMap() => { + "latitude": latitude, + "longitude": longitude, + "speed": speed, + "altitude": altitude, + "timestamp": timestamp, + "accuracy": accuracy, + "altitude_accuracy": altitudeAccuracy, + "heading": heading, + "heading_accuracy": headingAccuracy, + "speed_accuracy": speedAccuracy, + "floor": floor, + "mocked": isMocked, + }; +} + +ActivityType? parseActivityType(String? value, [ActivityType? defaultValue]) { + if (value == null) return defaultValue; + return ActivityType.values.firstWhereOrNull( + (e) => e.name.toLowerCase() == value.toLowerCase()) ?? + defaultValue; +} + +LocationSettings? parseLocationSettings(dynamic value, + [LocationSettings? defaultValue]) { + if (value == null) return defaultValue; + + var distanceFilter = parseInt(value["distance_filter"], 0)!; + var accuracy = + parseLocationAccuracy(value["accuracy"], LocationAccuracy.best)!; + var timeLimit = parseDuration(value["time_limit"]); + if (kIsWeb) { + return WebSettings( + accuracy: accuracy, + distanceFilter: distanceFilter, + timeLimit: timeLimit, + maximumAge: parseDuration(value["maximum_age"], Duration.zero)!, + ); + } else if (isAndroidMobile()) { + return AndroidSettings( + accuracy: accuracy, + distanceFilter: distanceFilter, + timeLimit: timeLimit, + intervalDuration: parseDuration( + value["interval_duration"], const Duration(milliseconds: 5000))!, + useMSLAltitude: parseBool(value["use_msl_altitude"], false)!, + // Needed to prevet background to stop working when app goes in background + foregroundNotificationConfig: (value["foreground_notification_text"] != + null || + value["foreground_notification_title"] != null) + ? ForegroundNotificationConfig( + notificationText: + value["foreground_notification_text"] ?? "Location Updates", + notificationTitle: value["foreground_notification_title"] ?? + "Running in Background", + enableWakeLock: parseBool( + value["foreground_notification_enable_wake_lock"], false)!, + enableWifiLock: parseBool( + value["foreground_notification_enable_wifi_lock"], false)!, + // color: + // parseColor(value["foreground_notification_color"], theme), + notificationChannelName: + value["foreground_notification_channel_name"] ?? + 'Background Location', + setOngoing: parseBool( + value["foreground_notification_set_ongoing"], true)!) + : null); + } else if (isApplePlatform()) { + return AppleSettings( + accuracy: accuracy, + distanceFilter: distanceFilter, + timeLimit: timeLimit, + activityType: + parseActivityType(value["activity_type"], ActivityType.other)!, + pauseLocationUpdatesAutomatically: + parseBool(value["pause_location_updates_automatically"], false)!, + showBackgroundLocationIndicator: + parseBool(value["show_background_location_indicator"], false)!, + allowBackgroundLocationUpdates: + parseBool(value["allow_background_location_updates"], true)!, + ); + } else { + return LocationSettings( + accuracy: accuracy, + distanceFilter: distanceFilter, + timeLimit: timeLimit); + } +} diff --git a/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/pubspec.yaml b/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/pubspec.yaml new file mode 100644 index 0000000000..026d3bad9b --- /dev/null +++ b/sdk/python/packages/flet-geolocator/src/flutter/flet_geolocator/pubspec.yaml @@ -0,0 +1,23 @@ +name: flet_geolocator +description: Flet Geolocator control +version: 0.1.0 +publish_to: none + +environment: + sdk: '>=3.2.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + geolocator: 14.0.2 + collection: ^1.16.0 + + flet: + path: ../../../../../../../packages/flet + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 diff --git a/sdk/python/packages/flet-lottie/CHANGELOG.md b/sdk/python/packages/flet-lottie/CHANGELOG.md new file mode 100644 index 0000000000..41e9b79df2 --- /dev/null +++ b/sdk/python/packages/flet-lottie/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [0.2.0] - 2025-06-26 + +## Added + +- `Lottie` control new properties: `enable_merge_paths`, `enable_layers_opacity`, `headers`, `error_content`. +- Deployed online documentation: https://docs.flet.dev/lottie/ + +### Changed + +- Refactored `Lottie` control to use `@ft.control` dataclass-style definition. + +## [0.1.0] - 2025-01-15 + +Initial release. + + +[0.2.0]: https://github.com/flet-dev/flet-lottie/compare/0.1.0...0.2.0 +[0.1.0]: https://github.com/flet-dev/flet-lottie/releases/tag/0.1.0 diff --git a/sdk/python/packages/flet-lottie/LICENSE b/sdk/python/packages/flet-lottie/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/sdk/python/packages/flet-lottie/LICENSE @@ -0,0 +1,201 @@ + 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 such Contributor that are necessarily infringed 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 consequential 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-lottie/README.md b/sdk/python/packages/flet-lottie/README.md new file mode 100644 index 0000000000..54a5580c1b --- /dev/null +++ b/sdk/python/packages/flet-lottie/README.md @@ -0,0 +1,40 @@ +# flet-lottie + +[![pypi](https://img.shields.io/pypi/v/flet-lottie.svg)](https://pypi.python.org/pypi/flet-lottie) +[![downloads](https://static.pepy.tech/badge/flet-lottie/month)](https://pepy.tech/project/flet-lottie) +[![license](https://img.shields.io/github/license/flet-dev/flet.svg)](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-lottie/LICENSE) + +A [Flet](https://flet.dev) extension package for displaying Lottie animations. + +It is based on the [lottie](https://pub.dev/packages/lottie) Flutter package. + +## Documentation + +Detailed documentation to this package can be found [here](https://docs.flet.dev/lottie/). + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | + +## Usage + +### Installation + +To install the `flet-lottie` package and add it to your project dependencies: + +- Using `uv`: + ```bash + uv add flet-lottie + ``` + +- Using `pip`: + ```bash + pip install flet-lottie + ``` + 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/controls/lottie). diff --git a/sdk/python/packages/flet-lottie/pyproject.toml b/sdk/python/packages/flet-lottie/pyproject.toml new file mode 100644 index 0000000000..72abf88504 --- /dev/null +++ b/sdk/python/packages/flet-lottie/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "flet-lottie" +version = "0.1.0" +description = "Display Lottie animations in 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://docs.flet.dev/lottie" +Repository = "https://github.com/flet-dev/flet/tree/main/sdk/python/packages/flet-lottie" +Issues = "https://github.com/flet-dev/flet/issues" + +[tool.setuptools.package-data] +"flutter.flet_lottie" = ["**/*"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/sdk/python/packages/flet-lottie/src/flet_lottie/__init__.py b/sdk/python/packages/flet-lottie/src/flet_lottie/__init__.py new file mode 100644 index 0000000000..c6cf2c653a --- /dev/null +++ b/sdk/python/packages/flet-lottie/src/flet_lottie/__init__.py @@ -0,0 +1,3 @@ +from flet_lottie.lottie import Lottie + +__all__ = ["Lottie"] diff --git a/sdk/python/packages/flet-lottie/src/flet_lottie/lottie.py b/sdk/python/packages/flet-lottie/src/flet_lottie/lottie.py new file mode 100644 index 0000000000..cfb2ab7e90 --- /dev/null +++ b/sdk/python/packages/flet-lottie/src/flet_lottie/lottie.py @@ -0,0 +1,115 @@ +from typing import Optional + +import flet as ft + +__all__ = ["Lottie"] + + +@ft.control("Lottie") +class Lottie(ft.LayoutControl): + """ + Displays lottie animations. + """ + + src: Optional[str] = None + """ + The source of the Lottie file. + + Can be a URL or a local [asset file](https://flet.dev/docs/cookbook/assets). + + Note: + If both `src` and [`src_base64`][..] are provided, + `src_base64` will be prioritized/used. + + Raises: + AssertionError: If neither [`src`][(c).] nor + [`src_base64`][(c).] is provided. + """ + + src_base64: Optional[str] = None + """ + The base64 encoded string of the Lottie file. + + Note: + If both `src_base64` and [`src`][..] are provided, + `src_base64` will be prioritized/used. + + Raises: + AssertionError: If neither [`src`][(c).] nor + [`src_base64`][(c).] is provided. + """ + + repeat: bool = True + """ + Whether the animation should repeat in a loop. + + Note: + Has no effect if [`animate`][..] is `False`. + """ + + reverse: bool = False + """ + Whether the animation should be played in reverse + (from start to end and then continuously from end to start). + + Note: + Has no effect if [`animate`][..] or [`repeat`][..] is `False`. + """ + + animate: bool = True + """ + Whether the animation should be played automatically. + """ + + enable_merge_paths: bool = False + """ + Whether to enable merge path support. + """ + + enable_layers_opacity: bool = False + """ + Whether to enable layer-level opacity. + """ + + background_loading: Optional[bool] = None + """ + Whether the animation should be loaded in the background. + """ + + filter_quality: ft.FilterQuality = ft.FilterQuality.LOW + """ + The quality of the image layer. + """ + + fit: Optional[ft.BoxFit] = None + """ + Defines how to inscribe the Lottie composition + into the space allocated during layout. + """ + + headers: Optional[dict[str, str]] = None + """ + Headers for network requests. + """ + + error_content: Optional[ft.Control] = None + """ + A control to display when an error occurs + while loading the Lottie animation. + + For more information on the error, see [`on_error`][..]. + """ + + on_error: Optional[ft.ControlEventHandler["Lottie"]] = None + """ + Fires when an error occurs while loading the Lottie animation. + + The [`data`][flet.Event.data] property of the event handler argument + contains information on the error. + """ + + def before_update(self): + super().before_update() + assert self.src or self.src_base64, ( + "at least one of src and src_base64 must be provided" + ) diff --git a/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/.gitignore b/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/.gitignore new file mode 100644 index 0000000000..ed7794f2ab --- /dev/null +++ b/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/.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-lottie/src/flutter/flet_lottie/.metadata b/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/.metadata new file mode 100644 index 0000000000..07d8623a38 --- /dev/null +++ b/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "2e9cb0aa71a386a91f73f7088d115c0d96654829" + channel: "stable" + +project_type: package diff --git a/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/analysis_options.yaml b/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/analysis_options.yaml new file mode 100644 index 0000000000..a5744c1cfb --- /dev/null +++ b/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/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-lottie/src/flutter/flet_lottie/lib/flet_lottie.dart b/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/lib/flet_lottie.dart new file mode 100644 index 0000000000..ca99abed23 --- /dev/null +++ b/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/lib/flet_lottie.dart @@ -0,0 +1,3 @@ +library flet_lottie; + +export "src/extension.dart" show Extension; diff --git a/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/lib/src/extension.dart b/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/lib/src/extension.dart new file mode 100644 index 0000000000..ed479da13c --- /dev/null +++ b/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/lib/src/extension.dart @@ -0,0 +1,16 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/cupertino.dart'; + +import 'lottie.dart'; + +class Extension extends FletExtension { + @override + Widget? createWidget(Key? key, Control control) { + switch (control.type) { + case "Lottie": + return LottieControl(control: control); + default: + return null; + } + } +} diff --git a/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/lib/src/lottie.dart b/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/lib/src/lottie.dart new file mode 100644 index 0000000000..288b96ad87 --- /dev/null +++ b/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/lib/src/lottie.dart @@ -0,0 +1,123 @@ +import 'dart:convert'; + +import 'package:flet/flet.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:lottie/lottie.dart'; + +class LottieControl extends StatefulWidget { + final Control control; + + const LottieControl({super.key, required this.control}); + + @override + State createState() => _LottieControlState(); +} + +class _LottieControlState extends State { + @override + Widget build(BuildContext context) { + debugPrint( + "Lottie build: ${widget.control.id} (${widget.control.hashCode})"); + + var src = widget.control.getString("src"); + var srcBase64 = widget.control.getString("src_base64"); + + if (src == null && srcBase64 == null) { + return const ErrorControl( + "Lottie must have either \"src\" or \"src_base64\" specified."); + } + + var repeat = widget.control.getBool("repeat", true)!; + var backgroundLoading = widget.control.getBool("background_loading"); + var reverse = widget.control.getBool("reverse", false)!; + var animate = widget.control.getBool("animate", true)!; + var fit = widget.control.getBoxFit("fit"); + var alignment = widget.control.getAlignment("alignment"); + var filterQuality = widget.control.getFilterQuality("filter_quality"); + var errorContent = widget.control.buildWidget("error_content"); + var options = LottieOptions( + enableMergePaths: widget.control.getBool("enable_merge_paths", false)!, + enableApplyingOpacityToLayers: + widget.control.getBool("enable_layers_opacity", false)!, + ); + void onError(String value) { + if (widget.control.getBool("on_error", false)!) { + widget.control.triggerEvent("error", value); + } + } + + void onLoad(LottieComposition composition) { + if (widget.control.getBool("on_load", false)!) { + widget.control.triggerEvent("load"); + } + } + + Widget errorBuilder(context, error, stackTrace) { + onError(error.toString()); + return errorContent ?? + ErrorControl("Error loading Lottie", description: error.toString()); + } + + Widget? lottie; + + if (srcBase64 != null) { + try { + Uint8List bytes = base64Decode(srcBase64); + lottie = Lottie.memory( + bytes, + repeat: repeat, + reverse: reverse, + animate: animate, + alignment: alignment, + fit: fit, + filterQuality: filterQuality, + options: options, + backgroundLoading: backgroundLoading, + errorBuilder: errorBuilder, + onLoaded: onLoad, + onWarning: onError, + ); + } catch (ex) { + onError(ex.toString()); + return errorContent ?? + ErrorControl("Error decoding src_base64", + description: ex.toString()); + } + } else { + var assetSrc = widget.control.backend.getAssetSource(src!); + if (assetSrc.isFile) { + // Local File + lottie = Lottie.asset(assetSrc.path, + repeat: repeat, + reverse: reverse, + animate: animate, + alignment: alignment, + options: options, + fit: fit, + filterQuality: filterQuality, + backgroundLoading: backgroundLoading, + errorBuilder: errorBuilder, + onLoaded: onLoad, + onWarning: onError); + } else { + // URL + lottie = Lottie.network(assetSrc.path, + repeat: repeat, + reverse: reverse, + animate: animate, + alignment: alignment, + fit: fit, + options: options, + filterQuality: filterQuality, + backgroundLoading: backgroundLoading, + headers: widget.control.get("headers")?.cast(), + errorBuilder: errorBuilder, + onLoaded: onLoad, + onWarning: onError); + } + } + + return ConstrainedControl(control: widget.control, child: lottie); + } +} diff --git a/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/pubspec.yaml b/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/pubspec.yaml new file mode 100644 index 0000000000..f233e842db --- /dev/null +++ b/sdk/python/packages/flet-lottie/src/flutter/flet_lottie/pubspec.yaml @@ -0,0 +1,23 @@ +name: flet_lottie +description: Flet Lottie control +version: 0.1.0 +publish_to: none + +environment: + sdk: '>=3.2.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + collection: ^1.16.0 + lottie: 3.3.2 + + flet: + path: ../../../../../../../packages/flet + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 diff --git a/sdk/python/packages/flet-map/CHANGELOG.md b/sdk/python/packages/flet-map/CHANGELOG.md new file mode 100644 index 0000000000..e2cea07998 --- /dev/null +++ b/sdk/python/packages/flet-map/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.2.0] - 2025-06-26 + +- Added configuration helpers for cameras, interaction flags, and stroke patterns. +- Introduced attribution controls and additional layer types for circles, polygons, and polylines. +- Published hosted documentation: https://docs.flet.dev/map/ + +## [0.1.0] - 2025-01-15 + +- Initial release. diff --git a/sdk/python/packages/flet-map/LICENSE b/sdk/python/packages/flet-map/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/sdk/python/packages/flet-map/LICENSE @@ -0,0 +1,201 @@ + 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 such Contributor that are necessarily infringed 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 consequential 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-map/README.md b/sdk/python/packages/flet-map/README.md new file mode 100644 index 0000000000..f839a6bc47 --- /dev/null +++ b/sdk/python/packages/flet-map/README.md @@ -0,0 +1,40 @@ +# flet-map + +[![pypi](https://img.shields.io/pypi/v/flet-map.svg)](https://pypi.python.org/pypi/flet-map) +[![downloads](https://static.pepy.tech/badge/flet-map/month)](https://pepy.tech/project/flet-map) +[![license](https://img.shields.io/github/license/flet-dev/flet.svg)](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-map/LICENSE) + +A [Flet](https://flet.dev) extension for displaying interactive maps. + +It is based on the [flutter_map](https://pub.dev/packages/flutter_map) Flutter package. + +## Documentation + +Detailed documentation to this package can be found [here](https://docs.flet.dev/map/). + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | + +## Usage + +### Installation + +To install the `flet-map` package and add it to your project dependencies: + +- Using `uv`: + ```bash + uv add flet-map + ``` + +- Using `pip`: + ```bash + pip install flet-map + ``` + 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/controls/map). diff --git a/sdk/python/packages/flet-map/pyproject.toml b/sdk/python/packages/flet-map/pyproject.toml new file mode 100644 index 0000000000..daa7c809a9 --- /dev/null +++ b/sdk/python/packages/flet-map/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "flet-map" +version = "0.2.0" +description = "Interactive map controls 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://docs.flet.dev/map" +Repository = "https://github.com/flet-dev/flet/tree/main/sdk/python/packages/flet-map" +Issues = "https://github.com/flet-dev/flet/issues" + +[tool.setuptools.package-data] +"flutter.flet_map" = ["**/*"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/sdk/python/packages/flet-map/src/flet_map/__init__.py b/sdk/python/packages/flet-map/src/flet_map/__init__.py new file mode 100644 index 0000000000..c11c07dc12 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flet_map/__init__.py @@ -0,0 +1,87 @@ +from flet_map.circle_layer import CircleLayer, CircleMarker +from flet_map.map import Map +from flet_map.map_layer import MapLayer +from flet_map.marker_layer import Marker, MarkerLayer +from flet_map.polygon_layer import PolygonLayer, PolygonMarker +from flet_map.polyline_layer import PolylineLayer, PolylineMarker +from flet_map.rich_attribution import RichAttribution +from flet_map.simple_attribution import SimpleAttribution +from flet_map.source_attribution import ( + ImageSourceAttribution, + SourceAttribution, + TextSourceAttribution, +) +from flet_map.tile_layer import TileLayer +from flet_map.types import ( + AttributionAlignment, + Camera, + CameraFit, + CursorKeyboardRotationConfiguration, + CursorRotationBehaviour, + DashedStrokePattern, + DottedStrokePattern, + FadeInTileDisplay, + InstantaneousTileDisplay, + InteractionConfiguration, + InteractionFlag, + KeyboardConfiguration, + MapEvent, + MapEventSource, + MapHoverEvent, + MapLatitudeLongitude, + MapLatitudeLongitudeBounds, + MapPointerEvent, + MapPositionChangeEvent, + MapTapEvent, + MultiFingerGesture, + PatternFit, + SolidStrokePattern, + StrokePattern, + TileDisplay, + TileLayerEvictErrorTileStrategy, +) + +__all__ = [ + "AttributionAlignment", + "Camera", + "CameraFit", + "CircleLayer", + "CircleMarker", + "CursorKeyboardRotationConfiguration", + "CursorRotationBehaviour", + "DashedStrokePattern", + "DottedStrokePattern", + "FadeInTileDisplay", + "ImageSourceAttribution", + "InstantaneousTileDisplay", + "InteractionConfiguration", + "InteractionFlag", + "KeyboardConfiguration", + "Map", + "MapEvent", + "MapEventSource", + "MapHoverEvent", + "MapLatitudeLongitude", + "MapLatitudeLongitudeBounds", + "MapLayer", + "MapPointerEvent", + "MapPositionChangeEvent", + "MapTapEvent", + "Marker", + "MarkerLayer", + "MultiFingerGesture", + "PatternFit", + "PolygonLayer", + "PolygonMarker", + "PolylineLayer", + "PolylineMarker", + "RichAttribution", + "SimpleAttribution", + "SolidStrokePattern", + "SourceAttribution", + "StrokePattern", + "TextSourceAttribution", + "TileDisplay", + "TileLayer", + "TileLayerEvictErrorTileStrategy", +] diff --git a/sdk/python/packages/flet-map/src/flet_map/circle_layer.py b/sdk/python/packages/flet-map/src/flet_map/circle_layer.py new file mode 100644 index 0000000000..56035ee8e0 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flet_map/circle_layer.py @@ -0,0 +1,66 @@ +from typing import Optional + +import flet as ft +from flet_map.map_layer import MapLayer +from flet_map.types import MapLatitudeLongitude + +__all__ = ["CircleLayer", "CircleMarker"] + + +@ft.control("CircleMarker") +class CircleMarker(ft.Control): + """ + A circular marker displayed on the Map at the specified + location through the [`CircleLayer`][(p).]. + + Raises: + AssertionError: If the [`border_stroke_width`][(c).] is negative. + """ + + radius: ft.Number + """The radius of the circle""" + + coordinates: MapLatitudeLongitude + """The center coordinates of the circle""" + + color: Optional[ft.ColorValue] = None + """The color of the circle area.""" + + border_color: Optional[ft.ColorValue] = None + """ + The color of the circle border line. + + Note: + [`border_stroke_width`][..] must to be greater than + `0.0` in order for this color to be visible. + """ + + border_stroke_width: ft.Number = 0.0 + """ + The stroke width for the circle border. + + Note: + Must be non-negative. + """ + + use_radius_in_meter: bool = False + """ + Whether the [`radius`][..] should use the unit meters. + """ + + def before_update(self): + super().before_update() + assert self.border_stroke_width >= 0, ( + f"border_stroke_width must be greater than or equal to 0, " + f"got {self.border_stroke_width}" + ) + + +@ft.control("CircleLayer") +class CircleLayer(MapLayer): + """ + A layer to display [`CircleMarker`][(p).]s. + """ + + circles: list[CircleMarker] + """A list of [`CircleMarker`][(p).]s to display.""" diff --git a/sdk/python/packages/flet-map/src/flet_map/map.py b/sdk/python/packages/flet-map/src/flet_map/map.py new file mode 100644 index 0000000000..aa6973f22b --- /dev/null +++ b/sdk/python/packages/flet-map/src/flet_map/map.py @@ -0,0 +1,367 @@ +from dataclasses import field +from typing import Optional + +import flet as ft +from flet_map.map_layer import MapLayer +from flet_map.types import ( + CameraFit, + InteractionConfiguration, + MapEvent, + MapHoverEvent, + MapLatitudeLongitude, + MapPointerEvent, + MapPositionChangeEvent, + MapTapEvent, +) + +__all__ = ["Map"] + + +@ft.control("Map") +class Map(ft.LayoutControl): + """ + An interactive map that displays various layers. + """ + + layers: list[MapLayer] + """ + A list of layers to be displayed (stack-like) on the map. + """ + + initial_center: MapLatitudeLongitude = field( + default_factory=lambda: MapLatitudeLongitude(latitude=50.5, longitude=30.51) + ) + """ + The initial center of the map. + """ + + initial_rotation: ft.Number = 0.0 + """ + The rotation (in degrees) when the map is first loaded. + """ + + initial_zoom: ft.Number = 13.0 + """ + The zoom when the map is first loaded. + If initial_camera_fit is defined this has no effect. + """ + + interaction_configuration: InteractionConfiguration = field( + default_factory=lambda: InteractionConfiguration() + ) + """ + The interaction configuration. + """ + + bgcolor: ft.ColorValue = ft.Colors.GREY_300 + """ + The background color of this control. + """ + + keep_alive: bool = False + """ + Whether to enable the built in keep-alive functionality. + + If the map is within a complex layout, such as a [`ListView`][flet.ListView], + the map will reset to it's inital position after it appears back into view. + To ensure this doesn't happen, enable this flag to prevent it from rebuilding. + """ + + max_zoom: Optional[ft.Number] = None + """ + The maximum (highest) zoom level of every layer. + Each layer can specify additional zoom level restrictions. + """ + + min_zoom: Optional[ft.Number] = None + """ + The minimum (smallest) zoom level of every layer. + Each layer can specify additional zoom level restrictions. + """ + + animation_curve: ft.AnimationCurve = ft.AnimationCurve.FAST_OUT_SLOWIN + """ + The default animation curve to be used for map-animations + when calling instance methods like [`zoom_in()`][(c).zoom_in], + [`rotate_from()`][(c).rotate_from], + [`move_to()`][(c).move_to] etc. + """ + + animation_duration: ft.DurationValue = field( + default_factory=lambda: ft.Duration(milliseconds=500) + ) + """ + The default animation duration to be used for map-animations + when calling instance methods like [`zoom_in()`][(c).zoom_in], + [`rotate_from()`][(c).rotate_from], + [`move_to()`][(c).move_to] etc. + """ + + initial_camera_fit: Optional[CameraFit] = None + """ + Defines the visible bounds when the map is first loaded. + Takes precedence over [`initial_center`][..]/[`initial_zoom`][..]. + """ + + on_init: Optional[ft.ControlEventHandler["Map"]] = None + """ + Fires when the map is initialized. + """ + + on_tap: Optional[ft.EventHandler[MapTapEvent]] = None + """ + Fires when a tap event occurs. + """ + + on_hover: Optional[ft.EventHandler[MapHoverEvent]] = None + """ + Fires when a hover event occurs. + """ + + on_secondary_tap: Optional[ft.EventHandler[MapTapEvent]] = None + """ + Fires when a secondary tap event occurs. + """ + + on_long_press: Optional[ft.EventHandler[MapTapEvent]] = None + """ + Fires when a long press event occurs. + """ + + on_event: Optional[ft.EventHandler[MapEvent]] = None + """ + Fires when any map events occurs. + """ + + on_position_change: Optional[ft.EventHandler[MapPositionChangeEvent]] = None + """ + Fires when the map position changes. + """ + + on_pointer_down: Optional[ft.EventHandler[MapPointerEvent]] = None + """ + Fires when a pointer down event occurs. + """ + + on_pointer_cancel: Optional[ft.EventHandler[MapPointerEvent]] = None + """ + Fires when a pointer cancel event occurs. + """ + + on_pointer_up: Optional[ft.EventHandler[MapPointerEvent]] = None + """ + Fires when a pointer up event occurs. + """ + + async def rotate_from( + self, + degree: ft.Number, + animation_curve: Optional[ft.AnimationCurve] = None, + animation_duration: Optional[ft.DurationValue] = None, + cancel_ongoing_animations: bool = False, + ) -> None: + """ + Applies a rotation of `degree` to the current rotation. + + Args: + degree: The number of degrees to increment to the current rotation. + animation_curve: The curve of the animation. If None (the default), + [`Map.animation_curve`][(p).] will be used. + animation_duration: The duration of the animation. + If None (the default), [`Map.animation_duration`][(p).] will be used. + cancel_ongoing_animations: Whether to cancel/stop all + ongoing map-animations before starting this new one. + """ + await self._invoke_method( + method_name="rotate_from", + arguments={ + "degree": degree, + "curve": animation_curve or self.animation_curve, + "duration": animation_duration or self.animation_duration, + "cancel_ongoing_animations": cancel_ongoing_animations, + }, + ) + + async def reset_rotation( + self, + animation_curve: Optional[ft.AnimationCurve] = None, + animation_duration: Optional[ft.DurationValue] = None, + cancel_ongoing_animations: bool = False, + ) -> None: + """ + Resets the map's rotation to 0 degrees. + + Args: + animation_curve: The curve of the animation. If None (the default), + [`Map.animation_curve`][(p).] will be used. + animation_duration: The duration of the animation. + If None (the default), [`Map.animation_duration`][(p).] will be used. + cancel_ongoing_animations: Whether to cancel/stop all + ongoing map-animations before starting this new one. + """ + await self._invoke_method( + method_name="reset_rotation", + arguments={ + "curve": animation_curve or self.animation_curve, + "duration": animation_duration or self.animation_duration, + "cancel_ongoing_animations": cancel_ongoing_animations, + }, + ) + + async def zoom_in( + self, + animation_curve: Optional[ft.AnimationCurve] = None, + animation_duration: Optional[ft.DurationValue] = None, + cancel_ongoing_animations: bool = False, + ) -> None: + """ + Zooms in by one zoom-level from the current one. + + Args: + animation_curve: The curve of the animation. If None (the default), + [`Map.animation_curve`][(p).] will be used. + animation_duration: The duration of the animation. + If None (the default), [`Map.animation_duration`][(p).] will be used. + cancel_ongoing_animations: Whether to cancel/stop all + ongoing map-animations before starting this new one. + """ + await self._invoke_method( + method_name="zoom_in", + arguments={ + "curve": animation_curve or self.animation_curve, + "duration": animation_duration or self.animation_duration, + "cancel_ongoing_animations": cancel_ongoing_animations, + }, + ) + + async def zoom_out( + self, + animation_curve: Optional[ft.AnimationCurve] = None, + animation_duration: Optional[ft.DurationValue] = None, + cancel_ongoing_animations: bool = False, + ) -> None: + """ + Zooms out by one zoom-level from the current one. + + Args: + animation_curve: The curve of the animation. If None (the default), + [`Map.animation_curve`][(p).] will be used. + animation_duration: The duration of the animation. + If None (the default), [`Map.animation_duration`][(p).] will be used. + cancel_ongoing_animations: Whether to cancel/stop all + ongoing map-animations before starting this new one. + """ + await self._invoke_method( + method_name="zoom_out", + arguments={ + "curve": animation_curve or self.animation_curve, + "duration": animation_duration or self.animation_duration, + "cancel_ongoing_animations": cancel_ongoing_animations, + }, + ) + + async def zoom_to( + self, + zoom: ft.Number, + animation_curve: Optional[ft.AnimationCurve] = None, + animation_duration: Optional[ft.DurationValue] = None, + cancel_ongoing_animations: bool = False, + ) -> None: + """ + Zoom the map to a specific zoom level. + + Args: + zoom: The zoom level to zoom to. + animation_curve: The curve of the animation. If None (the default), + [`Map.animation_curve`][(p).] will be used. + animation_duration: The duration of the animation. + If None (the default), [`Map.animation_duration`][(p).] will be used. + cancel_ongoing_animations: Whether to cancel/stop all + ongoing map-animations before starting this new one. + """ + await self._invoke_method( + method_name="zoom_to", + arguments={ + "zoom": zoom, + "curve": animation_curve or self.animation_curve, + "duration": animation_duration or self.animation_duration, + "cancel_ongoing_animations": cancel_ongoing_animations, + }, + ) + + async def move_to( + self, + destination: Optional[MapLatitudeLongitude] = None, + zoom: Optional[ft.Number] = None, + rotation: Optional[ft.Number] = None, + animation_curve: Optional[ft.AnimationCurve] = None, + animation_duration: Optional[ft.DurationValue] = None, + offset: ft.OffsetValue = (0, 0), + cancel_ongoing_animations: bool = False, + ) -> None: + """ + Moves to a specific location. + + Args: + destination: The destination point to move to. + zoom: The zoom level to be applied. If provided, + must be greater than or equal to `0.0`. + rotation: Rotation (in degrees) to be applied. + offset: The offset to be used. Only works when `rotation` is `None`. + animation_curve: The curve of the animation. If None (the default), + [`Map.animation_curve`][(p).] will be used. + animation_duration: The duration of the animation. + If None (the default), [`Map.animation_duration`][(p).] will be used. + cancel_ongoing_animations: Whether to cancel/stop all + ongoing map-animations before starting this new one. + + Raises: + AssertionError: If `zoom` is not `None` and is negative. + """ + assert zoom is None or zoom >= 0, ( + f"zoom must be greater than or equal to zero, got {zoom}" + ) + await self._invoke_method( + method_name="move_to", + arguments={ + "destination": destination, + "zoom": zoom, + "offset": offset, + "rotation": rotation, + "curve": animation_curve or self.animation_curve, + "duration": animation_duration or self.animation_duration, + "cancel_ongoing_animations": cancel_ongoing_animations, + }, + ) + + async def center_on( + self, + point: MapLatitudeLongitude, + zoom: Optional[ft.Number], + animation_curve: Optional[ft.AnimationCurve] = None, + animation_duration: Optional[ft.DurationValue] = None, + cancel_ongoing_animations: bool = False, + ) -> None: + """ + Centers the map on the given point. + + Args: + point: The point on which to center the map. + zoom: The zoom level to be applied. + animation_curve: The curve of the animation. If `None` (the default), + [`Map.animation_curve`][(p).] will be used. + animation_duration: The duration of the animation. + If None (the default), [`Map.animation_duration`][(p).] will be used. + cancel_ongoing_animations: Whether to cancel/stop all + ongoing map-animations before starting this new one. + """ + await self._invoke_method( + method_name="center_on", + arguments={ + "point": point, + "zoom": zoom, + "curve": animation_curve or self.animation_curve, + "duration": animation_duration or self.animation_duration, + "cancel_ongoing_animations": cancel_ongoing_animations, + }, + ) diff --git a/sdk/python/packages/flet-map/src/flet_map/map_layer.py b/sdk/python/packages/flet-map/src/flet_map/map_layer.py new file mode 100644 index 0000000000..aed7d7fb44 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flet_map/map_layer.py @@ -0,0 +1,20 @@ +import flet as ft + +__all__ = ["MapLayer"] + + +@ft.control("MapLayer") +class MapLayer(ft.Control): + """ + Abstract class for all map layers. + + The following layers are available: + + - [`CircleLayer`][(p).] + - [`MarkerLayer`][(p).] + - [`PolygonLayer`][(p).] + - [`PolylineLayer`][(p).] + - [`RichAttribution`][(p).] + - [`SimpleAttribution`][(p).] + - [`TileLayer`][(p).] + """ diff --git a/sdk/python/packages/flet-map/src/flet_map/marker_layer.py b/sdk/python/packages/flet-map/src/flet_map/marker_layer.py new file mode 100644 index 0000000000..e9aa6a9738 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flet_map/marker_layer.py @@ -0,0 +1,109 @@ +from dataclasses import field +from typing import Optional + +import flet as ft +from flet_map.map_layer import MapLayer +from flet_map.types import MapLatitudeLongitude + +__all__ = ["Marker", "MarkerLayer"] + + +@ft.control("Marker") +class Marker(ft.Control): + """ + A marker displayed on the Map at the specified location + through the [`MarkerLayer`][(p).]. + + Raises: + AssertionError: If the [`content`][(c).] is not visible, or + if [`height`][(c).] or [`width`][(c).] are negative. + """ + + content: ft.Control + """ + The content to be displayed at [`coordinates`][..]. + + Note: + Must be provided and visible. + """ + + coordinates: MapLatitudeLongitude + """ + The coordinates of the marker. + + This will be the center of the marker, + if [`alignment`][..] is [`Alignment.CENTER`][flet.Alignment.CENTER]. + """ + + rotate: Optional[bool] = None + """ + Whether to counter rotate this marker to the map's rotation, + to keep a fixed orientation. + So, when `True`, this marker will always appear upright and + vertical from the user's perspective. + + If `None`, defaults to the value of the parent [`MarkerLayer.rotate`][(p).]. + + Note: + This is not used to apply a custom rotation in degrees to this marker. + + """ + + height: ft.Number = 30.0 + """ + The height of the [`content`][..] Control. + + Note: + Must be non-negative. + """ + + width: ft.Number = 30.0 + """ + The width of the [`content`][..] Control. + + Note: + Must be non-negative. + """ + + alignment: Optional[ft.Alignment] = None + """ + Alignment of the marker relative to the normal center at [`coordinates`][..]. + + Defaults to the value of the parent [`MarkerLayer.alignment`][(p).]. + """ + + def before_update(self): + super().before_update() + assert self.content.visible, "content must be visible" + assert self.height >= 0, ( + f"height must be greater than or equal to 0, got {self.height}" + ) + assert self.width >= 0, ( + f"width must be greater than or equal to 0, got {self.width}" + ) + + +@ft.control("MarkerLayer") +class MarkerLayer(MapLayer): + """ + A layer to display Markers. + """ + + markers: list[Marker] + """ + A list of [`Marker`][(m).]s to display. + """ + + alignment: Optional[ft.Alignment] = field( + default_factory=lambda: ft.Alignment.CENTER + ) + """ + The alignment of each marker relative to its normal center at + [`Marker.coordinates`][(m).]. + """ + + rotate: bool = False + """ + Whether to counter-rotate `markers` to the map's rotation, + to keep a fixed orientation. + """ diff --git a/sdk/python/packages/flet-map/src/flet_map/polygon_layer.py b/sdk/python/packages/flet-map/src/flet_map/polygon_layer.py new file mode 100644 index 0000000000..240a0fedc8 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flet_map/polygon_layer.py @@ -0,0 +1,131 @@ +from typing import Optional + +import flet as ft +from flet_map.map_layer import MapLayer +from flet_map.types import MapLatitudeLongitude + +__all__ = ["PolygonLayer", "PolygonMarker"] + + +@ft.control("PolygonMarker") +class PolygonMarker(ft.Control): + """ + A marker for the [`PolygonLayer`][(p).]. + """ + + coordinates: list[MapLatitudeLongitude] + """ + The points for the outline of this polygon. + """ + + label: Optional[str] = None + """ + An optional label for this polygon. + + Note: specifying a label will reduce performance, as the internal + canvas must be drawn to and 'saved' more frequently to ensure the proper + stacking order is maintained. This can be avoided, potentially at the + expense of appearance, by setting [`PolygonLayer.draw_labels_last`][(p).]. + """ + + label_text_style: Optional[ft.TextStyle] = None + """ + The text style for the label. + """ + + border_color: ft.ColorValue = ft.Colors.GREEN + """ + The color of the border outline. + """ + + color: ft.ColorValue = ft.Colors.GREEN + """ + The color of the polygon. + """ + + border_stroke_width: ft.Number = 0.0 + """ + The width of the border outline. + + Note: + Must be non-negative. + """ + + disable_holes_border: bool = False + """ + Whether holes should have borders. + """ + + rotate_label: bool = False + """ + Whether to rotate the label counter to the camera's rotation, + to ensure it remains upright. + """ + + stroke_cap: ft.StrokeCap = ft.StrokeCap.ROUND + """ + Style to use for line endings. + """ + + stroke_join: ft.StrokeJoin = ft.StrokeJoin.ROUND + """ + Style to use for line segment joins. + """ + + def before_update(self): + super().before_update() + assert self.border_stroke_width >= 0, ( + f"border_stroke_width must be greater than or equal to 0, " + f"got {self.border_stroke_width}" + ) + + +@ft.control("PolygonLayer") +class PolygonLayer(MapLayer): + """ + A layer to display PolygonMarkers. + """ + + polygons: list[PolygonMarker] + """ + A list of [`PolygonMarker`][(p).]s to display. + """ + + polygon_culling: bool = True + """ + Whether to cull polygons and polygon sections that are outside of the viewport. + """ + + polygon_labels: bool = True + """ + Whether to draw per-polygon labels. + """ + + draw_labels_last: bool = False + """ + Whether to draw labels last and thus over all the polygons. + """ + + simplification_tolerance: ft.Number = 0.3 + """ + The tolerance value used to simplify polygon outlines before rendering. + + Higher values will result in polygons with fewer points, which can improve + rendering performance at the cost of reduced geometric accuracy. Lower values + preserve more detail but may decrease performance, especially with complex polygons. + + Set to 0 to disable simplification. + """ + + use_alternative_rendering: bool = False + """ + Whether to use an alternative rendering pathway to draw polygons onto the + underlying `Canvas`, which can be more performant in 'some' circumstances. + + This will not always improve performance, and there are other important + considerations before enabling it. It is intended for use when prior + profiling indicates more performance is required after other methods are + already in use. For example, it may worsen performance when there are a + huge number of polygons to triangulate - and so this is best used in + conjunction with simplification, not as a replacement. + """ diff --git a/sdk/python/packages/flet-map/src/flet_map/polyline_layer.py b/sdk/python/packages/flet-map/src/flet_map/polyline_layer.py new file mode 100644 index 0000000000..7a515ac19c --- /dev/null +++ b/sdk/python/packages/flet-map/src/flet_map/polyline_layer.py @@ -0,0 +1,121 @@ +from dataclasses import field +from typing import Optional + +import flet as ft +from flet_map.map_layer import MapLayer +from flet_map.types import MapLatitudeLongitude, SolidStrokePattern, StrokePattern + +__all__ = ["PolylineLayer", "PolylineMarker"] + + +@ft.control("PolylineMarker") +class PolylineMarker(ft.Control): + """ + A marker for the [`PolylineLayer`][(p).]. + """ + + coordinates: list[MapLatitudeLongitude] + """ + The list of coordinates for the polyline. + """ + + colors_stop: Optional[list[ft.Number]] = None + """ + The stops for the [`gradient_colors`][..]. + """ + + gradient_colors: Optional[list[ft.ColorValue]] = None + """ + The List of colors in case a gradient should get used. + """ + + border_color: ft.ColorValue = ft.Colors.YELLOW + """ + The border's color. + """ + + color: ft.ColorValue = ft.Colors.YELLOW + """ + The color of the line stroke. + """ + + stroke_width: ft.Number = 1.0 + """ + The width of the stroke. + + Note: + Must be non-negative. + """ + + border_stroke_width: ft.Number = 0.0 + """ + The width of the stroke with of the line border. + + Note: + Must be non-negative. + """ + + use_stroke_width_in_meter: bool = False + """ + Whether the stroke's width should have meters as unit. + """ + + stroke_pattern: StrokePattern = field(default_factory=lambda: SolidStrokePattern()) + """ + Determines whether the line should be solid, dotted, or dashed, and the + exact characteristics of each. + """ + + stroke_cap: ft.StrokeCap = ft.StrokeCap.ROUND + """ + Style to use for line endings. + """ + + stroke_join: ft.StrokeJoin = ft.StrokeJoin.ROUND + """ + Style to use for line segment joins. + """ + + def before_update(self): + super().before_update() + assert self.border_stroke_width >= 0, ( + f"border_stroke_width must be greater than or equal to 0, " + f"got {self.border_stroke_width}" + ) + assert self.stroke_width >= 0, ( + f"stroke_width must be greater than or equal to 0, got {self.stroke_width}" + ) + + +@ft.control("PolylineLayer") +class PolylineLayer(MapLayer): + """ + A layer to display [`PolylineMarker`][(p).]s. + """ + + polylines: list[PolylineMarker] + """ + List of [`PolylineMarker`][(p).]s to be drawn. + """ + + culling_margin: ft.Number = 10.0 + """ + Acceptable extent outside of viewport before culling polyline segments. + """ + + min_hittable_radius: ft.Number = 10.0 + """ + The minimum radius of the hittable area around each polyline in logical pixels. + + The entire visible area is always hittable, but if the visible area is + smaller than this, then this will be the hittable area. + """ + + simplification_tolerance: ft.Number = 0.3 + """ + The tolerance (in map units) used to simplify polylines for rendering. + + Higher values result in more aggressive simplification, + which can improve performance but may reduce the accuracy of + the displayed polyline. + """ diff --git a/sdk/python/packages/flet-map/src/flet_map/rich_attribution.py b/sdk/python/packages/flet-map/src/flet_map/rich_attribution.py new file mode 100644 index 0000000000..f887aec5df --- /dev/null +++ b/sdk/python/packages/flet-map/src/flet_map/rich_attribution.py @@ -0,0 +1,65 @@ +from dataclasses import field +from typing import Optional + +import flet as ft +from flet_map.map_layer import MapLayer +from flet_map.source_attribution import SourceAttribution +from flet_map.types import AttributionAlignment + +__all__ = ["RichAttribution"] + + +@ft.control("RichAttribution") +class RichAttribution(MapLayer): + """ + An animated and interactive attribution layer that supports both images and text + (displayed in a popup controlled by an icon button adjacent to the images). + """ + + attributions: list[SourceAttribution] + """ + List of attributions to display. + + [`TextSourceAttribution`][(p).]s are shown in a popup box, + unlike [`ImageSourceAttribution`][(p).], which are visible permanently. + """ + + alignment: Optional[AttributionAlignment] = None + """ + The position in which to anchor this attribution control. + """ + + popup_bgcolor: Optional[ft.ColorValue] = ft.Colors.SURFACE + """ + The color to use as the popup box's background color. + """ + + popup_border_radius: Optional[ft.BorderRadiusValue] = None + """ + The radius of the edges of the popup box. + """ + + popup_initial_display_duration: ft.DurationValue = field( + default_factory=lambda: ft.Duration() + ) + """ + The popup box will be open by default and be hidden this + long after the map is initialised. + + This is useful with certain sources/tile servers that make immediate + attribution mandatory and are not attributed with a permanently + visible [`ImageSourceAttribution`][(p).]. + """ + + permanent_height: ft.Number = 24.0 + """ + The height of the permanent row in which is found the popup menu toggle button. + Also determines spacing between the items within the row. + """ + + show_flutter_map_attribution: bool = True + """ + Whether to add an additional attribution logo and text + for [`flutter-map`](https://docs.fleaflet.dev/), + on which 'flet-map' package is based for map-renderings. + """ diff --git a/sdk/python/packages/flet-map/src/flet_map/simple_attribution.py b/sdk/python/packages/flet-map/src/flet_map/simple_attribution.py new file mode 100644 index 0000000000..f6495c744f --- /dev/null +++ b/sdk/python/packages/flet-map/src/flet_map/simple_attribution.py @@ -0,0 +1,32 @@ +from dataclasses import field +from typing import Optional, Union + +import flet as ft +from flet_map.map_layer import MapLayer + +__all__ = ["SimpleAttribution"] + + +@ft.control("SimpleAttribution") +class SimpleAttribution(MapLayer): + """ + A simple attribution layer displayed on the Map. + """ + + text: Union[str, ft.Text] + """ + The attribution message to be displayed. + """ + + alignment: ft.Alignment = field(default_factory=lambda: ft.Alignment.BOTTOM_RIGHT) + """ + The alignment of this attribution on the map. + """ + + bgcolor: ft.ColorValue = ft.Colors.SURFACE + """ + The color of the box containing the [`text`][..]. + """ + + on_click: Optional[ft.ControlEventHandler["SimpleAttribution"]] = None + """Fired when this attribution is clicked/pressed.""" diff --git a/sdk/python/packages/flet-map/src/flet_map/source_attribution.py b/sdk/python/packages/flet-map/src/flet_map/source_attribution.py new file mode 100644 index 0000000000..4cc81fb5c9 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flet_map/source_attribution.py @@ -0,0 +1,77 @@ +from dataclasses import dataclass +from typing import Optional + +import flet as ft + +__all__ = ["ImageSourceAttribution", "SourceAttribution", "TextSourceAttribution"] + + +@dataclass +class SourceAttribution(ft.BaseControl): + """ + Abstract class for source attribution controls: + + - [`ImageSourceAttribution`][(p).] + - [`TextSourceAttribution`][(p).] + """ + + +@ft.control("ImageSourceAttribution") +class ImageSourceAttribution(SourceAttribution): + """ + An image attribution permanently displayed adjacent to the + open/close icon of a [`RichAttribution`][(p).] control. + For it to be displayed, it should be part of a + [`RichAttribution.attributions`][(p).] list. + + Raises: + AssertionError: If the [`image`][(c).] is not visible. + """ + + image: ft.Image + """ + The `Image` to be displayed. + + Note: + Must be provided and visible. + """ + + height: ft.Number = 24.0 + """ + The height of the image. + Should be the same as [`RichAttribution.permanent_height`][(p).], + otherwise layout issues may occur. + """ + + tooltip: Optional[str] = None + """Tooltip text to be displayed when the image is hovered over.""" + + on_click: Optional[ft.ControlEventHandler["ImageSourceAttribution"]] = None + """Fired when this attribution is clicked/pressed.""" + + def before_update(self): + super().before_update() + assert self.image.visible, "image must be visible" + + +@ft.control("TextSourceAttribution") +class TextSourceAttribution(SourceAttribution): + """ + A text source attribution displayed on the Map. + For it to be displayed, it should be part of a + [`RichAttribution.attributions`][(p).] list. + """ + + text: str + """The text to display as attribution, styled with [`text_style`][..].""" + + text_style: Optional[ft.TextStyle] = None + """Style used to display the [`text`][..].""" + + prepend_copyright: bool = True + """ + Whether to add the 'Β©' character to the start of [`text`][..] automatically. + """ + + on_click: Optional[ft.ControlEventHandler["TextSourceAttribution"]] = None + """Fired when this attribution is clicked/pressed.""" diff --git a/sdk/python/packages/flet-map/src/flet_map/tile_layer.py b/sdk/python/packages/flet-map/src/flet_map/tile_layer.py new file mode 100644 index 0000000000..d73ae23198 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flet_map/tile_layer.py @@ -0,0 +1,245 @@ +from dataclasses import field +from typing import Optional + +import flet as ft +from flet_map.map_layer import MapLayer +from flet_map.types import ( + FadeInTileDisplay, + MapLatitudeLongitudeBounds, + TileDisplay, + TileLayerEvictErrorTileStrategy, +) + +__all__ = ["TileLayer"] + + +@ft.control("TileLayer") +class TileLayer(MapLayer): + """ + Displays square raster images in a continuous grid, + sourced from the provided [`url_template`][(c).] and [`fallback_url`][(c).]. + + Typically the first layer to be added to a [`Map`][(p).], + as it provides the tiles on which + other layers are displayed. + + Raises: + AssertionError: If one or more of the following is negative: + [`tile_size`][(c).], [`min_native_zoom`][(c).], + [`max_native_zoom`][(c).], [`zoom_offset`][(c).], + [`max_zoom`][(c).], [`min_zoom`][(c).] + """ + + url_template: str + """ + The URL template is a string that contains placeholders, + which, when filled in, create a URL/URI to a specific tile. + """ + + fallback_url: Optional[str] = None + """ + Fallback URL template, used if an error occurs when fetching tiles from + the [`url_template`][..]. + + Note that specifying this (non-none) will result in tiles not being cached + in memory. This is to avoid issues where the [`url_template`][..] is flaky, to + prevent different tilesets being displayed at the same time. + + It is expected that this follows the same retina support behaviour + as [`url_template`][..]. + """ + + subdomains: list[str] = field(default_factory=lambda: ["a", "b", "c"]) + """ + List of subdomains used in the URL template. + + For example, if [`subdomains`][..] is set to `["a", "b", "c"]` and the + `url_template` is `"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"`, + the resulting tile URLs will be: + + - `"https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"` + - `"https://b.tile.openstreetmap.org/{z}/{x}/{y}.png"` + - `"https://c.tile.openstreetmap.org/{z}/{x}/{y}.png"` + """ + + tile_bounds: Optional[MapLatitudeLongitudeBounds] = None + """ + Defines the bounds of the map. + Only tiles that fall within these bounds will be loaded. + """ + + tile_size: int = 256 + """ + The size in pixels of each tile image. + Should be a positive power of 2. + + Note: + Must be greater than or equal to `0.0`. + """ + + min_native_zoom: int = 0 + """ + Minimum zoom level supported by the tile source. + + Tiles from below this zoom level will not be displayed, instead tiles at + this zoom level will be displayed and scaled. + + This should usually be 0 (as default), as most tile sources will support + zoom levels onwards from this. + + Note: + Must be greater than or equal to `0.0`. + """ + + max_native_zoom: int = 19 + """ + Maximum zoom number supported by the tile source has available. + + Tiles from above this zoom level will not be displayed, instead tiles at + this zoom level will be displayed and scaled. + + Most tile servers support up to zoom level `19`, which is the default. + Otherwise, this should be specified. + + Note: + Must be greater than or equal to `0.0`. + """ + + zoom_reverse: bool = False + """ + Whether the zoom number used in tile URLs will be reversed + (`max_zoom - zoom` instead of `zoom`). + """ + + zoom_offset: ft.Number = 0.0 + """ + The zoom number used in tile URLs will be offset with this value. + + Note: + Must be greater than or equal to `0.0`. + """ + + keep_buffer: int = 2 + """ + When panning the map, keep this many rows and columns of + tiles before unloading them. + """ + + pan_buffer: int = 1 + """ + When loading tiles only visible tiles are loaded by default. This option + increases the loaded tiles by the given number on both axis which can help + prevent the user from seeing loading tiles whilst panning. Setting the + pan buffer too high can impact performance, typically this is set to `0` or `1`. + """ + + enable_tms: bool = False + """ + Whether to inverse Y-axis numbering for tiles. + Turn this on for [TMS](https://en.wikipedia.org/wiki/Tile_Map_Service) services. + """ + + enable_retina_mode: bool = False + """ + Whether to enable retina mode. + Retina mode improves the resolution of map tiles, particularly on + high density displays. + """ + + additional_options: dict[str, str] = field(default_factory=dict) + """ + Static information that should replace placeholders in the [`url_template`][..]. + Applying API keys, for example, is a good usecase of this parameter. + + Example: + ```python + TileLayer( + url_template="https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}{r}.png?access_token={accessToken}", + additional_options={ + 'accessToken': '', + 'id': 'mapbox.streets', + }, + ), + ``` + """ + + max_zoom: ft.Number = float("inf") + """ + The maximum zoom level up to which this layer will be displayed (inclusive). + The main usage for this property is to display a different `TileLayer` + when zoomed far in. + + Prefer [`max_native_zoom`][..] for setting the maximum zoom level supported by the + tile source. + + Typically set to infinity so that there are tiles always displayed. + + Note: + Must be greater than or equal to `0.0`. + """ + + min_zoom: ft.Number = 0.0 + """ + The minimum zoom level at which this layer is displayed (inclusive). + Typically `0.0`. + + Note: + Must be greater than or equal to `0.0`. + """ + + error_image_src: Optional[str] = None + """ + The source of the tile image to show in place of the tile that failed to load. + + See [`on_image_error`][..] property for details on the error. + """ + + evict_error_tile_strategy: Optional[TileLayerEvictErrorTileStrategy] = ( + TileLayerEvictErrorTileStrategy.NONE + ) + """ + If a tile was loaded with error, + the tile provider will be asked to evict the image based on this strategy. + """ + + display_mode: TileDisplay = field(default_factory=lambda: FadeInTileDisplay()) + """ + + Defines how tiles are displayed on the map. + """ + + user_agent_package_name: str = "unknown" + """ + The package name of the user agent. + """ + + on_image_error: Optional[ft.ControlEventHandler["TileLayer"]] = None + """ + Fires if an error occurs when fetching the tiles. + + Event handler argument [`data`][flet.Event.data] property contains + information about the error. + """ + + def before_update(self): + super().before_update() + assert self.tile_size >= 0, ( + f"tile_size must be greater than or equal to 0, got {self.tile_size}" + ) + assert self.min_native_zoom >= 0, ( + f"min_native_zoom must be greater than or equal to 0, " + f"got {self.min_native_zoom}" + ) + assert self.max_native_zoom >= 0, ( + f"max_native_zoom must be greater than or equal to 0, " + f"got {self.max_native_zoom}" + ) + assert self.zoom_offset >= 0, ( + f"zoom_offset must be greater than or equal to 0, got {self.zoom_offset}" + ) + assert self.max_zoom >= 0, ( + f"max_zoom must be greater than or equal to 0, got {self.max_zoom}" + ) + assert self.min_zoom >= 0, ( + f"min_zoom must be greater than or equal to 0, got {self.min_zoom}" + ) diff --git a/sdk/python/packages/flet-map/src/flet_map/types.py b/sdk/python/packages/flet-map/src/flet_map/types.py new file mode 100644 index 0000000000..d63a93742c --- /dev/null +++ b/sdk/python/packages/flet-map/src/flet_map/types.py @@ -0,0 +1,1016 @@ +from dataclasses import dataclass, field +from enum import Enum, IntFlag +from typing import TYPE_CHECKING, Optional + +import flet as ft +from flet.controls.animation import AnimationCurve + +if TYPE_CHECKING: + from flet_map.map import Map # noqa + +__all__ = [ + "AttributionAlignment", + "Camera", + "CameraFit", + "CursorKeyboardRotationConfiguration", + "CursorRotationBehaviour", + "DashedStrokePattern", + "DottedStrokePattern", + "FadeInTileDisplay", + "InstantaneousTileDisplay", + "InteractionConfiguration", + "InteractionFlag", + "KeyboardConfiguration", + "MapEvent", + "MapEventSource", + "MapHoverEvent", + "MapLatitudeLongitude", + "MapLatitudeLongitudeBounds", + "MapPointerEvent", + "MapPositionChangeEvent", + "MapTapEvent", + "MultiFingerGesture", + "PatternFit", + "SolidStrokePattern", + "StrokePattern", + "TileDisplay", + "TileLayerEvictErrorTileStrategy", +] + + +class TileLayerEvictErrorTileStrategy(Enum): + """Strategies on how to handle tile errors.""" + + NONE = "none" + """Never evict images for tiles which failed to load.""" + + DISPOSE = "dispose" + """Evict images for tiles which failed to load when they are pruned.""" + + NOT_VISIBLE = "notVisible" + """ + Evict images for tiles which failed to load and: + - do not belong to the current zoom level AND/OR + - are not visible + """ + + NOT_VISIBLE_RESPECT_MARGIN = "notVisibleRespectMargin" + """ + Evict images for tiles which failed to load and: + - do not belong to the current zoom level AND/OR + - are not visible, respecting the pruning buffer + (the maximum of the `keep_buffer` and `pan_buffer`). + """ + + +class AttributionAlignment(Enum): + """Position to anchor [`RichAttribution`][(p).] control relative to the map.""" + + BOTTOM_LEFT = "bottomLeft" + """The bottom left corner.""" + + BOTTOM_RIGHT = "bottomRight" + """The bottom right corner.""" + + +class PatternFit(Enum): + """ + Determines how a non-solid [`StrokePattern`][(p).] should be fit to a line + when their lengths are not equal or multiples + """ + + NONE = "none" + """ + Don't apply any specific fit to the pattern - repeat exactly as specified, + and stop when the last point is reached. + + Not recommended, as it may leave a gap between the final segment and the last + point, making it unclear where the line ends. + """ + + SCALE_DOWN = "scaleDown" + """ + Scale the pattern to ensure it fits an integer number of times into the + polyline (smaller version regarding rounding, cf. [`SCALE_UP`][..]). + """ + + SCALE_UP = "scaleUp" + """ + Scale the pattern to ensure it fits an integer number of times into the + polyline (bigger version regarding rounding, cf. [`SCALE_DOWN`][..]). + """ + + APPEND_DOT = "appendDot" + """ + Uses the pattern exactly, truncating the final dash if it does not fit, or + adding a single dot at the last point if the final dash does not reach the + last point (there is a gap at that location). + """ + + EXTEND_FINAL_DASH = "extendFinalDash" + """ + Uses the pattern exactly, truncating the final dash if it does not fit, or + extending the final dash to the last point if it would not normally reach + that point (there is a gap at that location). + + Only useful when working with [`DashedStrokePattern`][(p).]. + Similar to `APPEND_DOT` for `DottedStrokePattern`. + """ + + +@dataclass +class Camera: + center: "MapLatitudeLongitude" + """ + The center of this camera. + """ + + zoom: ft.Number + """ + Defines how far this camera is zoomed. + """ + + min_zoom: ft.Number + """ + The minimum allowed zoom level. + """ + + max_zoom: ft.Number + """ + The maximum allowed zoom level. + """ + + rotation: ft.Number + """ + The rotation (in degrees) of the camera. + """ + + +@dataclass +class StrokePattern: + """ + Determines whether a stroke should be solid, dotted, or dashed, + and the exact characteristics of each. + + This is an abstract class and shouldn't be used directly. + + See usable derivatives: + - [`SolidStrokePattern`][(p).] + - [`DashedStrokePattern`][(p).] + - [`DottedStrokePattern`][(p).] + """ + + _type: Optional[str] = field(init=False, repr=False, compare=False, default=None) + + +@dataclass +class SolidStrokePattern(StrokePattern): + """A solid/unbroken stroke pattern.""" + + def __post_init__(self): + self._type = "solid" + + +@dataclass +class DashedStrokePattern(StrokePattern): + """ + A stroke pattern of alternating dashes and gaps, defined by [`segments`][(c).]. + + Raises: + AssertionError: If [`segments`][(c).] does not contain at least two items, + or has an odd length. + """ + + segments: list[ft.Number] = field(default_factory=list) + """ + A list of alternating dash and gap lengths, in pixels. + + Note: + Must contain at least two items, and have an even length. + """ + pattern_fit: PatternFit = PatternFit.SCALE_UP + """ + Determines how this stroke pattern should be fit to a line when their lengths + are not equal or multiples. + """ + + def __post_init__(self): + assert len(self.segments) >= 2, ( + f"segments must contain at least two items, got {len(self.segments)}" + ) + assert len(self.segments) % 2 == 0, "segments must have an even length" + self._type = "dashed" + + +@dataclass +class DottedStrokePattern(StrokePattern): + """ + A stroke pattern of circular dots, spaced with [`spacing_factor`][(c).]. + + Raises: + AssertionError: If [`spacing_factor`][(c).] is negative. + """ + + spacing_factor: ft.Number = 1.5 + """ + The multiplier used to calculate the spacing between dots in a dotted polyline, + with respect to `Polyline.stroke_width` / `Polygon.border_stroke_width`. + A value of `1.0` will result in spacing equal to the `stroke_width`. + Increasing the value increases the spacing with the same scaling. + + May also be scaled by the use of [`PatternFit.SCALE_UP`][(p).]. + + Note: + Must be non-negative. + """ + pattern_fit: PatternFit = PatternFit.SCALE_UP + """ + Determines how this stroke pattern should be fit to a line when their + lengths are not equal or multiples. + """ + + def __post_init__(self): + assert self.spacing_factor > 0, ( + f"spacing_factor must be greater than or equal to 0.0, " + f"got {self.spacing_factor}" + ) + self._type = "dotted" + + +@dataclass +class MapLatitudeLongitude: + """Map coordinates in degrees.""" + + latitude: ft.Number + """The latitude point of this coordinate.""" + + longitude: ft.Number + """The longitude point of this coordinate.""" + + +@dataclass +class MapLatitudeLongitudeBounds: + """ + Both corners have to be on opposite sites, but it doesn't matter + which opposite corners or in what order the corners are provided. + """ + + corner_1: MapLatitudeLongitude + """The corner 1.""" + + corner_2: MapLatitudeLongitude + """The corner 2.""" + + +class InteractionFlag(IntFlag): + """ + Flags to enable/disable certain interaction events on the map. + + Example: + - [`InteractionFlag.ALL`][(p).] to enable all events + - [`InteractionFlag.NONE`][(p).] to disable all events + """ + + NONE = 0 + """No interaction.""" + + DRAG = 1 << 0 + """Panning with a single finger or cursor.""" + + FLING_ANIMATION = 1 << 1 + """Fling animation after panning if velocity is great enough.""" + + PINCH_MOVE = 1 << 2 + """Panning with multiple fingers.""" + + PINCH_ZOOM = 1 << 3 + """Zooming with a multi-finger pinch gesture.""" + + DOUBLE_TAP_ZOOM = 1 << 4 + """Zooming with a single-finger double tap gesture.""" + + DOUBLE_TAP_DRAG_ZOOM = 1 << 5 + """Zooming with a single-finger double-tap-drag gesture.""" + + SCROLL_WHEEL_ZOOM = 1 << 6 + """Zooming with a mouse scroll wheel.""" + + ROTATE = 1 << 7 + """Rotation with two-finger twist gesture.""" + + ALL = ( + (1 << 0) + | (1 << 1) + | (1 << 2) + | (1 << 3) + | (1 << 4) + | (1 << 5) + | (1 << 6) + | (1 << 7) + ) + """All available interactive flags.""" + + @staticmethod + def has_flag(left_flags: int, right_flags: int) -> bool: + """ + Returns: + `True` if `left_flags` has at least one member + in `right_flags` (intersection). + """ + return left_flags & right_flags != 0 + + @staticmethod + def has_multi_finger(flags: int) -> bool: + """ + Returns: + `True` if any multi-finger gesture flags + ([`MultiFingerGesture.PINCH_MOVE`][(p).], + [`MultiFingerGesture.PINCH_ZOOM`][(p).], + [`MultiFingerGesture.ROTATE`][(p).]) are enabled. + """ + return InteractionFlag.has_flag( + flags, + ( + MultiFingerGesture.PINCH_MOVE + | MultiFingerGesture.PINCH_ZOOM + | MultiFingerGesture.ROTATE + ), + ) + + @staticmethod + def has_drag(flags: int) -> bool: + """ + Returns: + `True` if the [`DRAG`][flet_map.InteractionFlag.DRAG] interaction + flag is enabled. + """ + return InteractionFlag.has_flag(flags, InteractionFlag.DRAG) + + @staticmethod + def has_fling_animation(flags: int) -> bool: + """ + Returns: + `True` if the [`FLING_ANIMATION`][flet_map.InteractionFlag.FLING_ANIMATION] + interaction flag is enabled. + """ + return InteractionFlag.has_flag(flags, InteractionFlag.FLING_ANIMATION) + + @staticmethod + def has_pinch_move(flags: int) -> bool: + """ + Returns: + `True` if the [`PINCH_MOVE`][flet_map.InteractionFlag.PINCH_MOVE] + interaction flag is enabled. + """ + return InteractionFlag.has_flag(flags, InteractionFlag.PINCH_MOVE) + + @staticmethod + def has_fling_pinch_zoom(flags: int) -> bool: + """ + Returns: + `True` if the [`PINCH_ZOOM`][flet_map.InteractionFlag.PINCH_ZOOM] + interaction flag is enabled. + """ + return InteractionFlag.has_flag(flags, InteractionFlag.PINCH_ZOOM) + + @staticmethod + def has_double_tap_drag_zoom(flags: int) -> bool: + """ + Returns: + `True` if the + [`DOUBLE_TAP_DRAG_ZOOM`][flet_map.InteractionFlag.DOUBLE_TAP_DRAG_ZOOM] + interaction flag is enabled. + """ + return InteractionFlag.has_flag(flags, InteractionFlag.DOUBLE_TAP_DRAG_ZOOM) + + @staticmethod + def has_double_tap_zoom(flags: int) -> bool: + """ + Returns: + `True` if the [`DOUBLE_TAP_ZOOM`][flet_map.InteractionFlag.DOUBLE_TAP_ZOOM] + interaction flag is enabled. + """ + return InteractionFlag.has_flag(flags, InteractionFlag.DOUBLE_TAP_ZOOM) + + @staticmethod + def has_rotate(flags: int) -> bool: + """ + Returns: + `True` if the [`ROTATE`][..] interactive flag is enabled. + """ + return InteractionFlag.has_flag(flags, InteractionFlag.ROTATE) + + @staticmethod + def has_scroll_wheel_zoom(flags: int) -> bool: + """ + Returns: + `True` if the [`SCROLL_WHEEL_ZOOM`][..] interaction flag is enabled. + """ + return InteractionFlag.has_flag(flags, InteractionFlag.SCROLL_WHEEL_ZOOM) + + +class MultiFingerGesture(IntFlag): + """Flags to enable/disable certain multi-finger gestures on the map.""" + + NONE = 0 + """No multi-finger gesture.""" + + PINCH_MOVE = 1 << 0 + """Pinch move gesture, which allows moving the map by dragging with two fingers.""" + + PINCH_ZOOM = 1 << 1 + """ + Pinch zoom gesture, which allows zooming in and out by pinching with two fingers. + """ + + ROTATE = 1 << 2 + """Rotate gesture, which allows rotating the map by twisting two fingers.""" + + ALL = (1 << 0) | (1 << 1) | (1 << 2) + """All multi-finger gestures defined in this enum.""" + + +@dataclass +class InteractionConfiguration: + enable_multi_finger_gesture_race: bool = False + """ + If `True`, then [`rotation_threshold`][..] and [`pinch_zoom_threshold`][..] + and [`pinch_move_threshold`][..] will race. + If multiple gestures win at the same time, then precedence: + [`pinch_zoom_win_gestures`][..] > [`rotation_win_gestures`][..] > + [`pinch_move_win_gestures`][..] + """ + + pinch_move_threshold: ft.Number = 40.0 + """ + Map starts to move when `pinch_move_threshold` has been achieved + or another multi finger gesture wins which allows + [`MultiFingerGesture.PINCH_MOVE`][(p).]. + + Note: + If [`InteractionConfiguration.flags`][(p).] doesn't contain + [`InteractionFlag.PINCH_MOVE`][(p).] + or [`enable_multi_finger_gesture_race`][..] is false then pinch move cannot win. + """ + + scroll_wheel_velocity: ft.Number = 0.005 + """ + The used velocity how fast the map should zoom in or out by scrolling + with the scroll wheel of a mouse. + """ + + pinch_zoom_threshold: ft.Number = 0.5 + """ + Map starts to zoom when `pinch_zoom_threshold` has been achieved or + another multi finger gesture wins which allows + [`MultiFingerGesture.PINCH_ZOOM`][(p).]. + + Note: + If [`InteractionConfiguration.flags`][(p).] + doesn't contain [`InteractionFlag.PINCH_ZOOM`][(p).] + or [`enable_multi_finger_gesture_race`][..] is false then zoom cannot win. + """ + + rotation_threshold: ft.Number = 20.0 + """ + Map starts to rotate when `rotation_threshold` has been achieved or + another multi finger gesture wins which allows [`MultiFingerGesture.ROTATE`][(p).]. + + Note: + If [`InteractionConfiguration.flags`][(p).] + doesn't contain [`InteractionFlag.ROTATE`][(p).] + or [`enable_multi_finger_gesture_race`][..] is false then rotate cannot win. + """ + + flags: InteractionFlag = InteractionFlag.ALL + """ + Defines the map events to be enabled/disabled. + """ + + rotation_win_gestures: MultiFingerGesture = MultiFingerGesture.ROTATE + """ + When [`rotation_threshold`][..] wins over [`pinch_zoom_threshold`][..] and + [`pinch_move_threshold`][..] then `rotation_win_gestures` gestures will be used. + """ + + pinch_move_win_gestures: MultiFingerGesture = ( + MultiFingerGesture.PINCH_ZOOM | MultiFingerGesture.PINCH_MOVE + ) + """ + When [`pinch_move_threshold`][..] wins over [`rotation_threshold`][..] + and [`pinch_zoom_threshold`][..] then `pinch_move_win_gestures` gestures + will be used. + + By default [`MultiFingerGesture.PINCH_MOVE`][(p).] + and [`MultiFingerGesture.PINCH_ZOOM`][(p).] + gestures will take effect see [`MultiFingerGesture`][(p).] for custom settings. + """ + + pinch_zoom_win_gestures: MultiFingerGesture = ( + MultiFingerGesture.PINCH_ZOOM | MultiFingerGesture.PINCH_MOVE + ) + """ + When [`pinch_zoom_threshold`][..] wins over [`rotation_threshold`][..] + and [`pinch_move_threshold`][..] + then `pinch_zoom_win_gestures` gestures will be used. + + By default [`MultiFingerGesture.PINCH_ZOOM`][(p).] + and [`MultiFingerGesture.PINCH_MOVE`][(p).] + gestures will take effect see `MultiFingerGesture` for custom settings. + """ + + keyboard_configuration: "KeyboardConfiguration" = field( + default_factory=lambda: KeyboardConfiguration() + ) + """ + Options to configure how keyboard keys may be used to control the map. + + Keyboard movements using the arrow keys are enabled by default. + """ + + cursor_keyboard_rotation_configuration: "CursorKeyboardRotationConfiguration" = ( + field(default_factory=lambda: CursorKeyboardRotationConfiguration()) + ) + """ + Options to control the keyboard and mouse cursor being used together + to rotate the map. + """ + + +class MapEventSource(Enum): + """Defines the source of a [`MapEvent`][(p).].""" + + MAP_CONTROLLER = "mapController" + """The `MapEvent` is caused programmatically by the `MapController`.""" + + TAP = "tap" + """The `MapEvent` is caused by a tap gesture.""" + + SECONDARY_TAP = "secondaryTap" + """The `MapEvent` is caused by a secondary tap gesture.""" + + LONG_PRESS = "longPress" + """The `MapEvent` is caused by a long press gesture.""" + + DOUBLE_TAP = "doubleTap" + """The `MapEvent` is caused by a double tap gesture.""" + + DOUBLE_TAP_HOLD = "doubleTapHold" + """The `MapEvent` is caused by a double tap and hold gesture.""" + + DRAG_START = "dragStart" + """The `MapEvent` is caused by the start of a drag gesture.""" + + ON_DRAG = "onDrag" + """The `MapEvent` is caused by a drag update gesture.""" + + DRAG_END = "dragEnd" + """The `MapEvent` is caused by the end of a drag gesture.""" + + MULTI_FINGER_GESTURE_START = "multiFingerGestureStart" + """The `MapEvent` is caused by the start of a two finger gesture.""" + + ON_MULTI_FINGER = "onMultiFinger" + """The `MapEvent` is caused by a two finger gesture update.""" + + MULTI_FINGER_GESTURE_END = "multiFingerEnd" + """The `MapEvent` is caused by a the end of a two finger gesture.""" + + FLING_ANIMATION_CONTROLLER = "flingAnimationController" + """ + The `MapEvent` is caused by the `AnimationController` while + performing the fling gesture. + """ + + DOUBLE_TAP_ZOOM_ANIMATION_CONTROLLER = "doubleTapZoomAnimationController" + """ + The `MapEvent` is caused by the `AnimationController` + while performing the double tap zoom in animation. + """ + + INTERACTIVE_FLAGS_CHANGED = "InteractionFlagsChanged" + """The `MapEvent` is caused by a change of the interactive flags.""" + + FIT_CAMERA = "fitCamera" + """The `MapEvent` is caused by calling fit_camera.""" + + CUSTOM = "custom" + """The `MapEvent` is caused by a custom source.""" + + SCROLL_WHEEL = "scrollWheel" + """The `MapEvent` is caused by a scroll wheel zoom gesture.""" + + NON_ROTATED_SIZE_CHANGE = "nonRotatedSizeChange" + """The `MapEvent` is caused by a size change of the `Map` constraints.""" + + CURSOR_KEYBOARD_ROTATION = "cursorKeyboardRotation" + """The `MapEvent` is caused by a 'CTRL + drag' rotation gesture.""" + + KEYBOARD = "keyboard" + """ + The `MapEvent` is caused by a keyboard key. + See [`KeyboardConfiguration`][(p).] for details. + """ + + +@dataclass +class CameraFit: + """ + Defines how the camera should fit the bounds or coordinates, + depending on which one was provided. + + Raises: + AssertionError: If both [`bounds`][(c).] and [`coordinates`][(c).] + are `None` or not `None`. + """ + + bounds: Optional[MapLatitudeLongitudeBounds] = None + """ + The bounds which the camera should contain once it is fitted. + + Note: + If this is not `None`, [`coordinates`][..] should be `None`, and vice versa. + """ + + coordinates: Optional[list[MapLatitudeLongitude]] = None + """ + The coordinates which the camera should contain once it is fitted. + + Note: + If this is not `None`, [`bounds`][..] should be `None`, and vice versa. + """ + + max_zoom: Optional[ft.Number] = None + """ + The inclusive upper zoom limit used for the resulting fit. + + If the zoom level calculated for the fit exceeds the `max_zoom` value, + `max_zoom` will be used instead. + """ + + min_zoom: ft.Number = 0.0 + """ + """ + + padding: ft.PaddingValue = field(default_factory=lambda: ft.Padding.zero()) + """ + Adds a constant/pixel-based padding to the normal fit. + """ + + force_integer_zoom_level: bool = False + """ + Whether the zoom level of the resulting fit should be rounded to the + nearest integer level. + """ + + def __post_init__(self): + assert (self.bounds and not self.coordinates) or ( + self.coordinates and not self.bounds + ), "only one of bounds or coordinates must be provided, not both" + + +@dataclass +class MapTapEvent(ft.TapEvent["Map"]): + coordinates: MapLatitudeLongitude + """Coordinates of the point at which the tap occured.""" + + +@dataclass +class MapHoverEvent(ft.HoverEvent["Map"]): + coordinates: MapLatitudeLongitude + + +@dataclass +class MapPositionChangeEvent(ft.Event["Map"]): + coordinates: MapLatitudeLongitude + camera: Camera + has_gesture: bool + + +@dataclass +class MapPointerEvent(ft.PointerEvent["Map"]): + coordinates: MapLatitudeLongitude + """Coordinates of the point at which the tap occured.""" + + +@dataclass +class MapEvent(ft.Event["Map"]): + source: MapEventSource + """Who/what issued the event.""" + + camera: Camera + """The map camera after the event.""" + + +@dataclass +class TileDisplay: + """ + Defines how the tile should get displayed on the map. + + This is an abstract class and shouldn't be used directly. + + See usable derivatives: + - `InstantaneousTileDisplay` + - `FadeInTileDisplay` + """ + + _type: Optional[str] = field(init=False, repr=False, compare=False, default=None) + + +@dataclass +class InstantaneousTileDisplay(TileDisplay): + """A `TileDisplay` that should get instantaneously displayed.""" + + opacity: ft.Number = 1.0 + """ + The optional opacity of the tile. + """ + + def __post_init__(self): + assert 0.0 <= self.opacity <= 1.0, ( + f"opacity must be between 0.0 and 1.0 inclusive, got {self.opacity}" + ) + self._type = "instantaneous" + + +@dataclass +class FadeInTileDisplay(TileDisplay): + """A `TileDisplay` that should get faded in.""" + + duration: ft.DurationValue = field( + default_factory=lambda: ft.Duration(milliseconds=100) + ) + """ + The duration of the fade in animation. + """ + + start_opacity: ft.Number = 0.0 + """ + Opacity start value when a tile is faded in. + """ + + reload_start_opacity: ft.Number = 0.0 + """ + Opacity start value when a tile is reloaded. + """ + + def __post_init__(self): + assert 0.0 <= self.start_opacity <= 1.0, ( + f"start_opacity must be between 0.0 and 1.0 inclusive, " + f"got {self.start_opacity}" + ) + assert 0.0 <= self.reload_start_opacity <= 1.0, ( + f"reload_start_opacity must be between 0.0 and 1.0 inclusive, " + f"got {self.reload_start_opacity}" + ) + self._type = "fadein" + + +@dataclass +class KeyboardConfiguration: + """ + Options to configure how keyboard keys may be used to control the map. + When a key is pushed down, an animation starts, consisting of a curved + portion which takes the animation to its maximum velocity, an indefinitely + long animation at maximum velocity, then ended on the key up with another + curved portion. + + If a key is pressed and released quickly, it might trigger a short animation + called a 'leap'. The leap consists of a part of the curved portion, and also + scales the velocity of the concerned gesture. + + Info: + See [`CursorKeyboardRotationConfiguration`][(p).] for options + to control the keyboard and + mouse cursor being used together to rotate the map. + """ + + autofocus: bool = True + """ + Whether to request focus as soon as the map control appears + (and to enable keyboard controls). + """ + + animation_curve_duration: ft.DurationValue = field( + default_factory=lambda: ft.Duration(milliseconds=450) + ) + """ + Duration of the curved ([`AnimationCurve.EASE_IN`][flet.AnimationCurve.EASE_IN]) + portion of the animation occuring + after a key down event (and after a key up event if + [`animation_curve_reverse_duration`][..] is `None`) + """ + + animation_curve_reverse_duration: Optional[ft.DurationValue] = field( + default_factory=lambda: ft.Duration(milliseconds=600) + ) + """ + Duration of the curved (reverse + [`AnimationCurve.EASE_IN`][flet.AnimationCurve.EASE_IN]) + portion of the animation occuring after a key up event. + + Set to `None` to use [`animation_curve_duration`][..]. + """ + + animation_curve_curve: AnimationCurve = AnimationCurve.EASE_IN_OUT + """ + Curve of the curved portion of the animation occuring after + key down and key up events. + """ + + enable_arrow_keys_panning: bool = True + """ + Whether to allow arrow keys to pan the map (in their respective directions). + """ + + enable_qe_rotating: bool = True + """ + Whether to allow the `Q` & `E` keys (*) to rotate the map (`Q` rotates + anticlockwise, `E` rotates clockwise). + + QE are only the physical and logical keys on QWERTY keyboards. + On non- QWERTY keyboards, such as AZERTY, + the keys in the same position as on the QWERTY keyboard is used (ie. AE on AZERTY). + """ + + enable_rf_zooming: bool = True + """ + Whether to allow the `R` & `F` keys to zoom the map (`R` zooms IN + (increases zoom level), `F` zooms OUT (decreases zoom level)). + + RF are only the physical and logical keys on QWERTY keyboards. + On non- QWERTY keyboards, such as AZERTY, + the keys in the same position as on the QWERTY keyboard is used (ie. RF on AZERTY). + """ + + enable_wasd_panning: bool = True + """ + Whether to allow the `W`, `A`, `S`, `D` keys (*) to pan the map + (in the directions UP, LEFT, DOWN, RIGHT respectively). + + WASD are only the physical and logical keys on QWERTY keyboards. + On non- QWERTY keyboards, such as AZERTY, + the keys in the same position as on the QWERTY keyboard is + used (ie. ZQSD on AZERTY). + + If enabled, it is recommended to enable `enable_arrow_keys_panning` + to provide panning functionality easily for left handed users. + """ + + leap_max_of_curve_component: ft.Number = 0.6 + """ + The percentage (0.0 - 1.0) of the curve animation component that is driven + to (from 0), then in reverse from (to 0). + + Reducing means the leap occurs quicker (assuming a consistent curve + animation duration). Also see `*_leap_velocity_multiplier` properties to + change the distance of the leap assuming a consistent leap duration. + + For example, if set to 1, then the leap will take + `animation_curve_duration + animation_curve_reverse_duration` + to complete. + + Must be greater than 0 and less than or equal to 1. + To disable leaping, or change the maximum length of the key press + that will trigger a leap, see [`perform_leap_trigger_duration`][..]. + """ + + max_rotate_velocity: ft.Number = 3 + """ + The maximum angular difference to apply per frame to the camera's rotation + during a rotation animation. + + Measured in degrees. Negative numbers will flip the standard rotation keys. + """ + + max_zoom_velocity: ft.Number = 0.03 + """ + The maximum zoom level difference to apply per frame to the camera's zoom + level during a zoom animation. + + Measured in zoom levels. Negative numbers will flip the standard zoom keys. + """ + + pan_leap_velocity_multiplier: ft.Number = 5 + """ + The amount to scale the panning offset velocity by during a leap animation. + + The larger the number, the larger the movement during a leap. + To change the duration of a leap, see [`leap_max_of_curve_component`][..]. + """ + + rotate_leap_velocity_multiplier: ft.Number = 3 + """ + The amount to scale the rotation velocity by during a leap animation + + The larger the number, the larger the rotation difference during a leap. + To change the duration of a leap, see [`leap_max_of_curve_component`][..]. + + This may cause the pan velocity to exceed [`max_rotate_velocity`][..]. + """ + + zoom_leap_velocity_multiplier: ft.Number = 3 + """ + The amount to scale the zooming velocity by during a leap animation. + + The larger the number, the larger the zoom difference during a leap. To + change the duration of a leap, see [`leap_max_of_curve_component`][..]. + + This may cause the pan velocity to exceed [`max_zoom_velocity`][..]. + """ + + perform_leap_trigger_duration: Optional[ft.DurationValue] = field( + default_factory=lambda: ft.Duration(milliseconds=100) + ) + """ + Maximum duration between the key down and key up events of an animation + which will trigger a 'leap'. + + To customize the leap itself, see the [`leap_max_of_curve_component`][..] & + `*leap_velocity_multiplier` ([`zoom_leap_velocity_multiplier`][..], + [`pan_leap_velocity_multiplier`][..] and [`rotate_leap_velocity_multiplier`][..]) + properties. + + Set to `None` to disable leaping. + """ + + @classmethod + def disabled(cls) -> "KeyboardConfiguration": + """ + Disable keyboard control of the map. + + Info: + [`CursorKeyboardRotationConfiguration`][(p).] may still be active, + and is not disabled if this is disabled. + """ + return KeyboardConfiguration( + enable_arrow_keys_panning=False, + perform_leap_trigger_duration=None, + autofocus=False, + ) + + +class CursorRotationBehaviour(Enum): + """ + The behaviour of the cursor/keyboard rotation function in terms of the angle + that the map is rotated to. + + Does not disable cursor/keyboard rotation, or adjust its triggers: see + `CursorKeyboardRotationConfiguration.is_key_trriger`. + """ + + OFFSET = "offset" + """ + Offset the current rotation of the map to the angle at which the + user drags their cursor. + """ + + SET_NORTH = "setNorth" + """ + Set the North of the map to the angle at which the user drags their cursor. + """ + + +@dataclass +class CursorKeyboardRotationConfiguration: + """ + Options to configure cursor/keyboard rotation. + + Cursor/keyboard rotation is designed for desktop platforms, + and allows the cursor to be used to set the rotation of the map + whilst a keyboard key is held down (as triggered by `is_key_trriger`). + """ + + set_north_on_click: bool = True + """ + Whether to set the North of the map to the clicked angle, + when the user clicks their mouse without dragging + (a `on_pointer_down` event followed by `on_pointer_up` + without a change in rotation). + """ + + behavior: CursorRotationBehaviour = CursorRotationBehaviour.OFFSET + """ + The behaviour of the cursor/keyboard rotation function in terms of the + angle that the map is rotated to. + + Does not disable cursor/keyboard rotation, or + adjust its triggers: see `is_key_trriger`. + """ + + # TODO + trigger_keys: list = field( + default_factory=lambda: [ + # ft.LogicalKeyboardKey.CONTROL, + # ft.LogicalKeyboardKey.CONTROL_LEFT, + # ft.LogicalKeyboardKey.CONTROL_RIGHT, + ] + ) + """ + List of keys that will trigger cursor/keyboard rotation, when pressed. + """ + + @classmethod + def disabled(cls) -> "CursorKeyboardRotationConfiguration": + """A disabled `CursorKeyboardRotationConfiguration`.""" + return CursorKeyboardRotationConfiguration(trigger_keys=[]) diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/.gitignore b/sdk/python/packages/flet-map/src/flutter/flet_map/.gitignore new file mode 100644 index 0000000000..ed7794f2ab --- /dev/null +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/.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-map/src/flutter/flet_map/.metadata b/sdk/python/packages/flet-map/src/flutter/flet_map/.metadata new file mode 100644 index 0000000000..07d8623a38 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "2e9cb0aa71a386a91f73f7088d115c0d96654829" + channel: "stable" + +project_type: package diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/analysis_options.yaml b/sdk/python/packages/flet-map/src/flutter/flet_map/analysis_options.yaml new file mode 100644 index 0000000000..a5744c1cfb --- /dev/null +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/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-map/src/flutter/flet_map/lib/flet_map.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/flet_map.dart new file mode 100644 index 0000000000..e569f53f80 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/flet_map.dart @@ -0,0 +1,3 @@ +library flet_map; + +export "src/extension.dart" show Extension; diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/circle_layer.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/circle_layer.dart new file mode 100644 index 0000000000..8f138df69f --- /dev/null +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/circle_layer.dart @@ -0,0 +1,33 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart'; + +import 'utils/map.dart'; + +class CircleLayerControl extends StatelessWidget with FletStoreMixin { + final Control control; + + const CircleLayerControl({super.key, required this.control}); + + @override + Widget build(BuildContext context) { + debugPrint("CircleLayerControl build: ${control.id}"); + + var circles = control + .children("circles") + .where((c) => c.type == "CircleMarker") + .map((circle) { + return CircleMarker( + point: parseLatLng(circle.get("coordinates"))!, + color: circle.getColor("color", context, const Color(0xFF00FF00))!, + borderColor: circle.getColor( + "border_color", context, const Color(0xFFFFFF00))!, + borderStrokeWidth: circle.getDouble("border_stroke_width", 0.0)!, + useRadiusInMeter: circle.getBool("use_radius_in_meter", false)!, + radius: circle.getDouble("radius", 10)!); + }).toList(); + + return CircleLayer(circles: circles); + } +} diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/extension.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/extension.dart new file mode 100644 index 0000000000..02e25d3902 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/extension.dart @@ -0,0 +1,37 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/cupertino.dart'; + +import 'circle_layer.dart'; +import 'map.dart'; +import 'marker_layer.dart'; +import 'polygon_layer.dart'; +import 'polyline_layer.dart'; +import 'rich_attribution.dart'; +import 'simple_attribution.dart'; +import 'tile_layer.dart'; + +class Extension extends FletExtension { + @override + Widget? createWidget(Key? key, Control control) { + switch (control.type) { + case "Map": + return MapControl(key: key, control: control); + case "RichAttribution": + return RichAttributionControl(key: key, control: control); + case "SimpleAttribution": + return SimpleAttributionControl(key: key, control: control); + case "TileLayer": + return TileLayerControl(key: key, control: control); + case "MarkerLayer": + return MarkerLayerControl(key: key, control: control); + case "CircleLayer": + return CircleLayerControl(key: key, control: control); + case "PolygonLayer": + return PolygonLayerControl(key: key, control: control); + case "PolylineLayer": + return PolylineLayerControl(key: key, control: control); + default: + return null; + } + } +} diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/map.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/map.dart new file mode 100644 index 0000000000..30de3914c0 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/map.dart @@ -0,0 +1,128 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_animations/flutter_map_animations.dart'; + +import 'utils/map.dart'; + +class MapControl extends StatefulWidget { + final Control control; + + const MapControl({super.key, required this.control}); + + @override + State createState() => _MapControlState(); +} + +class _MapControlState extends State + with FletStoreMixin, TickerProviderStateMixin { + late final _animatedMapController = AnimatedMapController(vsync: this); + + @override + void initState() { + super.initState(); + widget.control.addInvokeMethodListener(_invokeMethod); + } + + Future _invokeMethod(String name, dynamic args) async { + debugPrint("Map.$name($args)"); + var defaultAnimationCurve = + widget.control.getCurve("animation_curve", Curves.fastOutSlowIn); + var defaultAnimationDuration = widget.control + .getDuration("animation_duration", const Duration(milliseconds: 500))!; + var animationCurve = parseCurve(args["curve"], defaultAnimationCurve); + var animationDuration = + parseDuration(args["duration"], defaultAnimationDuration); + var cancelPreviousAnimations = parseBool(args["cancel_ongoing_animations"]); + var zoom = parseDouble(args["zoom"]); + switch (name) { + case "rotate_from": + var degree = parseDouble(args["degree"]); + if (degree != null) { + await _animatedMapController.animatedRotateFrom( + degree, + curve: animationCurve, + duration: animationDuration, + cancelPreviousAnimations: cancelPreviousAnimations, + ); + } + break; + case "reset_rotation": + await _animatedMapController.animatedRotateReset( + curve: animationCurve, + duration: animationDuration, + cancelPreviousAnimations: cancelPreviousAnimations, + ); + break; + case "zoom_in": + await _animatedMapController.animatedZoomIn( + curve: animationCurve, + duration: animationDuration, + cancelPreviousAnimations: cancelPreviousAnimations, + ); + break; + case "zoom_out": + await _animatedMapController.animatedZoomOut( + curve: animationCurve, + duration: animationDuration, + cancelPreviousAnimations: cancelPreviousAnimations, + ); + break; + case "zoom_to": + if (zoom != null) { + await _animatedMapController.animatedZoomTo( + zoom, + curve: animationCurve, + duration: animationDuration, + cancelPreviousAnimations: cancelPreviousAnimations, + ); + } + break; + case "move_to": + await _animatedMapController.animateTo( + zoom: zoom, + curve: animationCurve, + rotation: parseDouble(args["rotation"]), + duration: animationDuration, + dest: parseLatLng(args["destination"]), + offset: parseOffset(args["offset"], Offset.zero)!, + cancelPreviousAnimations: cancelPreviousAnimations, + ); + break; + case "center_on": + var point = parseLatLng(args["point"]); + if (point != null) { + await _animatedMapController.centerOnPoint( + point, + zoom: zoom, + curve: animationCurve, + duration: animationDuration, + cancelPreviousAnimations: cancelPreviousAnimations, + ); + } + break; + default: + throw Exception("Unknown Map method: $name"); + } + } + + @override + void dispose() { + _animatedMapController.dispose(); + widget.control.removeInvokeMethodListener(_invokeMethod); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("Map build: ${widget.control.id} (${widget.control.hashCode})"); + + Widget map = FlutterMap( + mapController: _animatedMapController.mapController, + options: parseConfiguration(widget.control, context, const MapOptions())!, + children: widget.control.buildWidgets("layers"), + ); + + return ConstrainedControl(control: widget.control, child: map); + } +} diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/marker_layer.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/marker_layer.dart new file mode 100644 index 0000000000..32b3bafa60 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/marker_layer.dart @@ -0,0 +1,38 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_map_animations/flutter_map_animations.dart'; + +import 'utils/map.dart'; + +class MarkerLayerControl extends StatelessWidget with FletStoreMixin { + final Control control; + + const MarkerLayerControl({super.key, required this.control}); + + @override + Widget build(BuildContext context) { + debugPrint("MarkerLayerControl build: ${control.id}"); + var markers = control + .children("markers") + .where((c) => c.type == "Marker") + .map((marker) { + return AnimatedMarker( + point: parseLatLng(marker.get("coordinates"))!, + rotate: marker.getBool("rotate"), + height: marker.getDouble("height", 30.0)!, + width: marker.getDouble("width", 30.0)!, + alignment: marker.getAlignment("alignment"), + builder: (BuildContext context, Animation animation) { + return marker.buildWidget("content") ?? + const ErrorControl("content must be provided and visible"); + }); + }).toList(); + + return AnimatedMarkerLayer( + markers: markers, + rotate: control.getBool("rotate", false)!, + alignment: control.getAlignment("alignment", Alignment.center)!, + ); + } +} diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/polygon_layer.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/polygon_layer.dart new file mode 100644 index 0000000000..db8f310073 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/polygon_layer.dart @@ -0,0 +1,48 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; + +import 'utils/map.dart'; + +class PolygonLayerControl extends StatelessWidget with FletStoreMixin { + final Control control; + + const PolygonLayerControl({super.key, required this.control}); + + @override + Widget build(BuildContext context) { + debugPrint("PolygonLayerControl build: ${control.id}"); + + var polygons = control + .children("polygons") + .where((c) => c.type == "PolygonMarker") + .map((polygon) { + return Polygon( + borderStrokeWidth: polygon.getDouble("border_stroke_width", 0)!, + borderColor: polygon.getColor("border_color", context, Colors.green)!, + color: polygon.getColor("color", context, Colors.green)!, + disableHolesBorder: polygon.getBool("disable_holes_border", false)!, + rotateLabel: polygon.getBool("rotate_label", false)!, + label: polygon.getString("label"), + labelStyle: polygon.getTextStyle( + "label_text_style", Theme.of(context), const TextStyle())!, + strokeCap: polygon.getStrokeCap("stroke_cap", StrokeCap.round)!, + strokeJoin: polygon.getStrokeJoin("stroke_join", StrokeJoin.round)!, + points: polygon + .get("coordinates", [])! + .map((c) => parseLatLng(c)) + .nonNulls + .toList()); + }).toList(); + + return PolygonLayer( + polygons: polygons, + polygonCulling: control.getBool("polygon_culling", true)!, + polygonLabels: control.getBool("polygon_labels", true)!, + drawLabelsLast: control.getBool("draw_labels_last", false)!, + simplificationTolerance: + control.getDouble("simplification_tolerance", 0.3)!, + useAltRendering: control.getBool("use_alternative_rendering", false)!, + ); + } +} diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/polyline_layer.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/polyline_layer.dart new file mode 100644 index 0000000000..f19a8bece2 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/polyline_layer.dart @@ -0,0 +1,57 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; + +import 'utils/map.dart'; + +class PolylineLayerControl extends StatelessWidget with FletStoreMixin { + final Control control; + + const PolylineLayerControl({super.key, required this.control}); + + @override + Widget build(BuildContext context) { + debugPrint("PolylineLayerControl build: ${control.id}"); + + var polylines = control + .children("polygons") + .where((c) => c.type == "PolygonMarker") + .map((polyline) { + return Polyline( + borderStrokeWidth: polyline.getDouble("border_stroke_width", 0)!, + borderColor: + polyline.getColor("border_color", context, Colors.yellow)!, + color: polyline.getColor("color", context, Colors.yellow)!, + pattern: parseStrokePattern( + polyline.get("stroke_pattern"), const StrokePattern.solid())!, + strokeCap: polyline.getStrokeCap("stroke_cap", StrokeCap.round)!, + strokeJoin: polyline.getStrokeJoin("stroke_join", StrokeJoin.round)!, + strokeWidth: polyline.getDouble("stroke_width", 1.0)!, + useStrokeWidthInMeter: + polyline.getBool("use_stroke_width_in_meter", false)!, + colorsStop: polyline + .get("colors_stop", [])! + .map((e) => parseDouble(e)) + .nonNulls + .toList(), + gradientColors: polyline + .get("gradient_colors", [])! + .map((e) => parseColor(e, Theme.of(context))) + .nonNulls + .toList(), + points: polyline + .get("coordinates", [])! + .map((c) => parseLatLng(c)) + .nonNulls + .toList()); + }).toList(); + + return PolylineLayer( + polylines: polylines, + cullingMargin: control.getDouble("culling_margin", 10.0)!, + minimumHitbox: control.getDouble("min_hittable_radius", 10.0)!, + simplificationTolerance: + control.getDouble("simplification_tolerance", 0.3)!, + ); + } +} diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/rich_attribution.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/rich_attribution.dart new file mode 100644 index 0000000000..2cfc87d473 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/rich_attribution.dart @@ -0,0 +1,61 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; + +import 'utils/attribution_alignment.dart'; + +class RichAttributionControl extends StatefulWidget { + final Control control; + + const RichAttributionControl({super.key, required this.control}); + + @override + State createState() => _RichAttributionControlState(); +} + +class _RichAttributionControlState extends State + with FletStoreMixin { + @override + Widget build(BuildContext context) { + debugPrint("RichAttributionControl build: ${widget.control.id}"); + + var attributions = widget.control + .children("attributions") + .map((Control c) { + if (c.type == "TextSourceAttribution") { + return TextSourceAttribution( + c.getString("text", "Placeholder Text")!, + textStyle: c.getTextStyle("text_style", Theme.of(context)), + onTap: () => c.triggerEvent("click"), + prependCopyright: c.getBool("prepend_copyright", true)!, + ); + } else if (c.type == "ImageSourceAttribution") { + var image = c.buildWidget("image"); + if (image == null) return null; + return LogoSourceAttribution( + image, + height: c.getDouble("height", 24.0)!, + tooltip: c.getString("tooltip"), + onTap: () => c.triggerEvent("click"), + ); + } + }) + .nonNulls + .toList(); + + return RichAttributionWidget( + attributions: attributions, + permanentHeight: widget.control.getDouble("permanent_height", 24.0)!, + popupBackgroundColor: widget.control.getColor( + "popup_bgcolor", context, Theme.of(context).colorScheme.surface), + showFlutterMapAttribution: + widget.control.getBool("show_flutter_map_attribution", true)!, + alignment: parseAttributionAlignment( + widget.control.getString("alignment"), + AttributionAlignment.bottomRight)!, + popupBorderRadius: + widget.control.getBorderRadius("popup_border_radius"), + popupInitialDisplayDuration: widget.control + .getDuration("popup_initial_display_duration", Duration.zero)!); + } +} diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/simple_attribution.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/simple_attribution.dart new file mode 100644 index 0000000000..3153cbeedd --- /dev/null +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/simple_attribution.dart @@ -0,0 +1,23 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; + +class SimpleAttributionControl extends StatelessWidget { + final Control control; + + const SimpleAttributionControl({super.key, required this.control}); + + @override + Widget build(BuildContext context) { + debugPrint("SimpleAttributionControl build: ${control.id}"); + var text = control.buildTextOrWidget("text"); + + return SimpleAttributionWidget( + source: text is Text ? text : const Text("Placeholder Text"), + onTap: () => control.triggerEvent("click"), + backgroundColor: control.getColor( + "bgcolor", context, Theme.of(context).colorScheme.surface)!, + alignment: control.getAlignment("alignment", Alignment.bottomRight)!, + ); + } +} diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/tile_layer.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/tile_layer.dart new file mode 100644 index 0000000000..5d0eaa0a41 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/tile_layer.dart @@ -0,0 +1,66 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart'; + +import './utils/map.dart'; + +class TileLayerControl extends StatelessWidget { + final Control control; + + const TileLayerControl({super.key, required this.control}); + + @override + Widget build(BuildContext context) { + debugPrint("TileLayerControl build: ${control.id}"); + + var errorImageSrc = control.getString("errorImageSrc"); + ImageProvider? errorImage; + + if (errorImageSrc != null) { + var assetSrc = control.backend.getAssetSource(errorImageSrc); + if (assetSrc.isFile) { + // from File + errorImage = AssetImage(assetSrc.path); + } else { + // URL + errorImage = NetworkImage(assetSrc.path); + } + } + Widget tileLayer = TileLayer( + urlTemplate: control.getString("url_template"), + fallbackUrl: control.getString("fallback_url"), + subdomains: control + .get("subdomains") + ?.map((e) => e.toString()) + .toList() ?? + ['a', 'b', 'c'], + tileProvider: NetworkTileProvider(), + tileDisplay: parseTileDisplay( + control.get("display_mode"), const TileDisplay.fadeIn())!, + tileDimension: control.getInt("tile_size", 256)!, + userAgentPackageName: + control.getString("user_agent_package_name", 'unknown')!, + minNativeZoom: control.getInt("min_native_zoom", 0)!, + maxNativeZoom: control.getInt("max_native_zoom", 19)!, + zoomReverse: control.getBool("zoom_reverse", false)!, + zoomOffset: control.getDouble("zoom_offset", 0)!, + keepBuffer: control.getInt("keep_buffer", 2)!, + panBuffer: control.getInt("pan_buffer", 1)!, + tms: control.getBool("enable_tms", false)!, + tileBounds: parseLatLngBounds(control.get("tile_bounds")), + retinaMode: control.getBool("enable_retina_mode"), + maxZoom: control.getDouble("max_zoom", double.infinity)!, + minZoom: control.getDouble("min_zoom", 0)!, + evictErrorTileStrategy: parseEvictErrorTileStrategy( + control.getString("evict_error_tile_strategy"), + EvictErrorTileStrategy.none)!, + errorImage: errorImage, + errorTileCallback: (TileImage t, Object o, StackTrace? s) { + control.triggerEvent("image_error", o.toString()); + }, + additionalOptions: control.get("additional_options", {})!); + + return ConstrainedControl(control: control, child: tileLayer); + } +} diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/utils/attribution_alignment.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/utils/attribution_alignment.dart new file mode 100644 index 0000000000..25f79d9a43 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/utils/attribution_alignment.dart @@ -0,0 +1,10 @@ +import 'package:collection/collection.dart'; +import 'package:flutter_map/flutter_map.dart'; + +AttributionAlignment? parseAttributionAlignment(String? value, + [AttributionAlignment? defValue]) { + if (value == null) return defValue; + return AttributionAlignment.values.firstWhereOrNull( + (e) => e.name.toLowerCase() == value.toLowerCase()) ?? + defValue; +} diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/utils/map.dart b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/utils/map.dart new file mode 100644 index 0000000000..a5448acb67 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/lib/src/utils/map.dart @@ -0,0 +1,336 @@ +import 'package:collection/collection.dart'; +import 'package:flet/flet.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; + +LatLng? parseLatLng(dynamic value, [LatLng? defaultValue]) { + if (value == null) return defaultValue; + + return LatLng( + parseDouble(value['latitude'], 0)!, parseDouble(value['longitude'], 0)!); +} + +LatLngBounds? parseLatLngBounds(dynamic value, [LatLngBounds? defaultValue]) { + if (value == null || + value['corner_1'] == null || + value['corner_2'] == null || + parseLatLng(value['corner_1']) == null || + parseLatLng(value['corner_2']) == null) { + return defaultValue; + } + return LatLngBounds( + parseLatLng(value['corner_1'])!, parseLatLng(value['corner_2'])!); +} + +PatternFit? parsePatternFit(String? value, [PatternFit? defaultValue]) { + if (value == null) return defaultValue; + return PatternFit.values.firstWhereOrNull( + (e) => e.name.toLowerCase() == value.toLowerCase()) ?? + defaultValue; +} + +StrokePattern? parseStrokePattern(dynamic value, + [StrokePattern? defaultValue]) { + if (value == null) return defaultValue; + final type = value['_type']; + if (type == 'dotted') { + return StrokePattern.dotted( + spacingFactor: parseDouble(value['spacing_factor'], 1.5)!, + patternFit: parsePatternFit(value['pattern_fit'], PatternFit.scaleUp)!, + ); + } else if (type == 'solid') { + return const StrokePattern.solid(); + } else if (type == 'dashed') { + var segments = value['segments'] ?? []; + return StrokePattern.dashed( + patternFit: parsePatternFit(value['pattern_fit'], PatternFit.scaleUp)!, + segments: segments.map((e) => parseDouble(e)).nonNulls.toList(), + ); + } + return defaultValue; +} + +TileDisplay? parseTileDisplay(dynamic value, [TileDisplay? defaultValue]) { + if (value == null) return defaultValue; + final type = value['_type']; + if (type == 'instantaneous') { + return TileDisplay.instantaneous( + opacity: parseDouble(value['opacity'], 1.0)!, + ); + } else if (type == 'fadein') { + return TileDisplay.fadeIn( + startOpacity: parseDouble(value['start_opacity'], 1.0)!, + reloadStartOpacity: parseDouble(value['reload_start_opacity'], 1.0)!, + duration: + parseDuration(value['duration'], const Duration(milliseconds: 100))!, + ); + } + return defaultValue; +} + +InteractionOptions? parseInteractionOptions(dynamic value, + [InteractionOptions? defaultValue]) { + if (value == null) return defaultValue; + return InteractionOptions( + enableMultiFingerGestureRace: + parseBool(value["enable_multi_finger_gesture_race"], false)!, + pinchMoveThreshold: parseDouble( + value["pinch_move_threshold"], + )!, + scrollWheelVelocity: parseDouble(value["scroll_wheel_velocity"], 0.005)!, + pinchZoomThreshold: parseDouble(value["pinch_zoom_threshold"], 0.5)!, + rotationThreshold: parseDouble(value["rotation_threshold"], 20.0)!, + flags: parseInt(value["flags"], InteractiveFlag.all)!, + rotationWinGestures: + parseInt(value["rotation_win_gestures"], MultiFingerGesture.rotate)!, + pinchMoveWinGestures: parseInt(value["pinch_move_win_gestures"], + MultiFingerGesture.pinchZoom | MultiFingerGesture.pinchMove)!, + pinchZoomWinGestures: parseInt(value["pinch_zoom_win_gestures"], + MultiFingerGesture.pinchZoom | MultiFingerGesture.pinchMove)!, + keyboardOptions: parseKeyboardOptions( + value["keyboard_configuration"], const KeyboardOptions())!, + cursorKeyboardRotationOptions: parseCursorKeyboardRotationOptions( + value["cursor_keyboard_rotation_configuration"], + const CursorKeyboardRotationOptions())!, + ); +} + +CameraFit? parseCameraFit(dynamic value, [CameraFit? defaultValue]) { + if (value == null) return defaultValue; + + final bounds = parseLatLngBounds(value["bounds"]); + final coordinates = (value["coordinates"] as List?) + ?.map((c) => parseLatLng(c)) + .nonNulls + .toList(); + if (bounds == null && coordinates == null) return defaultValue; + + final forceIntegerZoomLevel = + parseBool(value["force_integer_zoom_level"], false)!; + final maxZoom = parseDouble(value["max_zoom"]); + final minZoom = parseDouble(value["min_zoom"], 0)!; + final padding = parsePadding(value["padding"], EdgeInsets.zero)!; + if (bounds != null) { + return CameraFit.insideBounds( + bounds: bounds, + forceIntegerZoomLevel: forceIntegerZoomLevel, + maxZoom: maxZoom, + minZoom: minZoom, + padding: padding, + ); + } else { + return CameraFit.coordinates( + coordinates: coordinates!, + forceIntegerZoomLevel: forceIntegerZoomLevel, + maxZoom: maxZoom, + minZoom: minZoom, + padding: padding, + ); + } +} + +KeyboardOptions? parseKeyboardOptions(dynamic value, + [KeyboardOptions? defaultValue]) { + if (value == null) return defaultValue; + return KeyboardOptions( + autofocus: parseBool(value["autofocus"], true)!, + animationCurveDuration: parseDuration(value["animation_curve_duration"], + const Duration(milliseconds: 450))!, + animationCurveCurve: + parseCurve(value["animation_curve_curve"], Curves.easeInOut)!, + enableArrowKeysPanning: + parseBool(value["enable_arrow_keys_panning"], true)!, + enableQERotating: parseBool(value["enable_qe_rotating"], true)!, + enableRFZooming: parseBool(value["enable_rf_zooming"], true)!, + enableWASDPanning: parseBool(value["enable_wasd_panning"], true)!, + leapMaxOfCurveComponent: + parseDouble(value["leap_max_of_curve_component"], 0.6)!, + // maxPanVelocity: , + maxRotateVelocity: parseDouble(value["max_rotate_velocity"], 3)!, + maxZoomVelocity: parseDouble(value["max_zoom_velocity"], 0.03)!, + panLeapVelocityMultiplier: + parseDouble(value["pan_leap_velocity_multiplier"], 5)!, + rotateLeapVelocityMultiplier: + parseDouble(value["rotate_leap_velocity_multiplier"], 3)!, + zoomLeapVelocityMultiplier: + parseDouble(value["zoom_leap_velocity_multiplier"], 3)!, + performLeapTriggerDuration: + parseDuration(value["perform_leap_trigger_duration"]), + animationCurveReverseDuration: + parseDuration(value["animation_curve_reverse_duration"])); +} + +CursorRotationBehaviour? parseCursorRotationBehaviour(String? value, + [CursorRotationBehaviour? defValue]) { + if (value == null) return defValue; + return CursorRotationBehaviour.values.firstWhereOrNull( + (e) => e.name.toLowerCase() == value.toLowerCase()) ?? + defValue; +} + +CursorKeyboardRotationOptions? parseCursorKeyboardRotationOptions(dynamic value, + [CursorKeyboardRotationOptions? defaultValue]) { + if (value == null) return defaultValue; + return CursorKeyboardRotationOptions( + setNorthOnClick: parseBool(value["set_north_on_click"], true)!, + behaviour: parseCursorRotationBehaviour( + value["behaviour"], CursorRotationBehaviour.offset)!, + isKeyTrigger: (LogicalKeyboardKey key) { + return (value["trigger_keys"] as List).contains(key); + }); +} + +// Crs? parseCrs(dynamic value, [Crs? defaultValue]) { +// if (value == null) return defaultValue; +// return Crs(); +// } + +// MapCamera? parseMapCamera(dynamic value, [MapCamera? defaultValue]) { +// if (value == null) return defaultValue; +// return MapCamera( +// crs: Crs(), +// center: parseLatLng(value["center"])!, +// zoom: parseDouble(value["zoom"], 0)!, +// minZoom: parseDouble(value["min_zoom"], 0)!, +// maxZoom: parseDouble(value["max_zoom"], 0)!, +// rotation: parseDouble(value["rotation"], 0)!, +// bounds: parseLatLngBounds(value["bounds"]), +// ); +// } + +EvictErrorTileStrategy? parseEvictErrorTileStrategy(String? value, + [EvictErrorTileStrategy? defaultValue]) { + if (value == null) return defaultValue; + return EvictErrorTileStrategy.values.firstWhereOrNull( + (e) => e.name.toLowerCase() == value.toLowerCase()) ?? + defaultValue; +} + +extension TapPositionExtension on TapPosition { + Map toMap() => { + "gx": global.dx, + "gy": global.dy, + "lx": relative?.dx, + "ly": relative?.dy, + }; +} + +extension LatLngExtension on LatLng { + Map toMap() => { + "latitude": latitude, + "longitude": longitude, + }; +} + +extension LatLngBoundsExtension on LatLngBounds { + // TODO + // Map toMap() => { + // + // }; +} + +extension MapCameraExtension on MapCamera { + Map toMap() => { + "center": center.toMap(), + "zoom": zoom, + "min_zoom": minZoom, + "max_zoom": maxZoom, + "rotation": rotation, + }; +} + +extension MapEventExtension on MapEvent { + Map toMap() => { + "source": source.name, + "camera": camera.toMap(), + }; +} + +MapOptions? parseConfiguration(Control control, BuildContext context, + [MapOptions? defaultValue]) { + return MapOptions( + initialCenter: + parseLatLng(control.get("initial_center"), const LatLng(50.5, 30.51))!, + interactionOptions: parseInteractionOptions( + control.get("interaction_configuration"), const InteractionOptions())!, + backgroundColor: control.getColor("bgcolor", context, Colors.grey[300])!, + initialRotation: control.getDouble("initial_rotation", 0.0)!, + initialZoom: control.getDouble("initial_zoom", 13.0)!, + keepAlive: control.getBool("keep_alive", false)!, + maxZoom: control.getDouble("max_zoom"), + minZoom: control.getDouble("min_zoom"), + initialCameraFit: parseCameraFit(control.get("initial_camera_fit")), + onPointerHover: control.getBool("on_hover", false)! + ? (PointerHoverEvent e, LatLng latlng) { + control.triggerEvent("hover", { + "coordinates": latlng.toMap(), + ...e.toMap(), + }); + } + : null, + onTap: control.getBool("on_tap", false)! + ? (TapPosition pos, LatLng latlng) { + control.triggerEvent("tap", { + "coordinates": latlng.toMap(), + ...pos.toMap(), + }); + } + : null, + onLongPress: control.getBool("on_long_press", false)! + ? (TapPosition pos, LatLng latlng) { + control.triggerEvent("long_press", { + "coordinates": latlng.toMap(), + ...pos.toMap(), + }); + } + : null, + onPositionChanged: control.getBool("on_position_change", false)! + ? (MapCamera camera, bool hasGesture) { + control.triggerEvent("position_change", { + "coordinates": camera.center.toMap(), + "has_gesture": hasGesture, + "camera": camera.toMap() + }); + } + : null, + onPointerDown: control.getBool("on_pointer_down", false)! + ? (PointerDownEvent e, LatLng latlng) { + control.triggerEvent("pointer_down", { + "coordinates": latlng.toMap(), + ...e.toMap(), + }); + } + : null, + onPointerCancel: control.getBool("on_pointer_cancel", false)! + ? (PointerCancelEvent e, LatLng latlng) { + control.triggerEvent("pointer_cancel", { + "coordinates": latlng.toMap(), + ...e.toMap(), + }); + } + : null, + onPointerUp: control.getBool("on_pointer_up", false)! + ? (PointerUpEvent e, LatLng latlng) { + control.triggerEvent( + "pointer_up", {"coordinates": latlng.toMap(), ...e.toMap()}); + } + : null, + onSecondaryTap: control.getBool("on_secondary_tap", false)! + ? (TapPosition pos, LatLng latlng) { + control.triggerEvent("secondary_tap", { + "coordinates": latlng.toMap(), + ...pos.toMap(), + }); + } + : null, + onMapEvent: control.getBool("on_event", false)! + ? (MapEvent e) => control.triggerEvent("event", e.toMap()) + : null, + onMapReady: control.getBool("on_init", false)! + ? () => control.triggerEvent("init") + : null, + ); +} diff --git a/sdk/python/packages/flet-map/src/flutter/flet_map/pubspec.yaml b/sdk/python/packages/flet-map/src/flutter/flet_map/pubspec.yaml new file mode 100644 index 0000000000..4a04f656d5 --- /dev/null +++ b/sdk/python/packages/flet-map/src/flutter/flet_map/pubspec.yaml @@ -0,0 +1,25 @@ +name: flet_map +description: Flet Map control +version: 0.1.0 +publish_to: none + +environment: + sdk: '>=3.2.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + collection: ^1.16.0 + flutter_map: 8.2.2 + flutter_map_animations: 0.9.0 + latlong2: 0.9.1 + + flet: + path: ../../../../../../../packages/flet + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 diff --git a/sdk/python/packages/flet-permission-handler/CHANGELOG.md b/sdk/python/packages/flet-permission-handler/CHANGELOG.md new file mode 100644 index 0000000000..7dac888feb --- /dev/null +++ b/sdk/python/packages/flet-permission-handler/CHANGELOG.md @@ -0,0 +1,45 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [0.2.0] - 2025-06-26 + +## Added + +- Deployed online documentation: https://docs.flet.dev/permission-handler/ +- `PermissionHandler` control new methods: + - `get_status_async` + - `request_async` + - `open_app_settings_async` + +### Changed + +- Refactored `PermissionHandler` control to use `@ft.control` dataclass-style definition and switched to `Service` control type + +### Breaking Changes + +- Enum `PermissionType` renamed to `Permission` +- `PermissionHandler` method `check_permission_async` renamed to `get_status_async`, with parameters changed: + - `of` β†’ `permission` (type: `PermissionType` β†’ `Permission`) + - `wait_timeout` β†’ `timeout` +- `PermissionHandler` method `request_permission_async` renamed to `request_async`, with parameters changed: + - `of` β†’ `permission` (type: `PermissionType` β†’ `Permission`) + - `wait_timeout` β†’ `timeout` +- `PermissionHandler` method `open_app_settings_async` parameter `wait_timeout` renamed to `timeout` (type: `Optional[float]` β†’ `int`) +- Removed sync methods from `PermissionHandler`: + - `check_permission` β†’ use `get_status_async` instead + - `request_permission` β†’ use `request_async` instead + - `open_app_settings` β†’ use `open_app_settings_async` instead +- `PermissionHandler` must now be added to `Page.services` before being used instead of `Page.overlay`. +- `PermissionHandler` can now only be used on the following platforms: Windows, iOS, Android, and Web. A `FletUnimplementedPlatformEception` will be raised if used on unsupported platforms. + +## [0.1.0] - 2025-01-15 + +Initial release. + + +[0.2.0]: https://github.com/flet-dev/flet-permission-handler/compare/0.1.0...0.2.0 +[0.1.0]: https://github.com/flet-dev/flet-permission-handler/releases/tag/0.1.0 diff --git a/sdk/python/packages/flet-permission-handler/LICENSE b/sdk/python/packages/flet-permission-handler/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/sdk/python/packages/flet-permission-handler/LICENSE @@ -0,0 +1,201 @@ + 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 such Contributor that are necessarily infringed 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 consequential 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-permission-handler/README.md b/sdk/python/packages/flet-permission-handler/README.md new file mode 100644 index 0000000000..5d2b220ed5 --- /dev/null +++ b/sdk/python/packages/flet-permission-handler/README.md @@ -0,0 +1,45 @@ +# flet-permission-handler + +[![pypi](https://img.shields.io/pypi/v/flet-permission-handler.svg)](https://pypi.python.org/pypi/flet-permission-handler) +[![downloads](https://static.pepy.tech/badge/flet-permission-handler/month)](https://pepy.tech/project/flet-permission-handler) +[![license](https://img.shields.io/github/license/flet-dev/flet.svg)](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-permission-handler/LICENSE) + +A [Flet](https://flet.dev) extension that simplifies working with device permissions. + +It is based on the [permission_handler](https://pub.dev/packages/permission_handler) Flutter package +and brings similar functionality to Flet, including: + +- Requesting permissions at runtime +- Checking the current permission status (e.g., granted, denied) +- Redirecting users to system settings to manually grant permissions + +## Documentation + +Detailed documentation to this package can be found [here](https://docs.flet.dev/permission-handler/). + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | ❌ | ❌ | βœ… | βœ… | βœ… | + +## Usage + +### Installation + +To install the `flet-permission-handler` package and add it to your project dependencies: + +- Using `uv`: + ```bash + uv add flet-permission-handler + ``` + +- Using `pip`: + ```bash + pip install flet-permission-handler + ``` + 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/controls/permission_handler). diff --git a/sdk/python/packages/flet-permission-handler/pyproject.toml b/sdk/python/packages/flet-permission-handler/pyproject.toml new file mode 100644 index 0000000000..1c2dda525f --- /dev/null +++ b/sdk/python/packages/flet-permission-handler/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "flet-permission-handler" +version = "0.1.0" +description = "Manage runtime permissions in 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://docs.flet.dev/permission-handler" +Repository = "https://github.com/flet-dev/flet/tree/main/sdk/python/packages/flet-permission-handler" +Issues = "https://github.com/flet-dev/flet/issues" + +[tool.setuptools.package-data] +"flutter.flet_permission_handler" = ["**/*"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/sdk/python/packages/flet-permission-handler/src/flet_permission_handler/__init__.py b/sdk/python/packages/flet-permission-handler/src/flet_permission_handler/__init__.py new file mode 100644 index 0000000000..af48b31356 --- /dev/null +++ b/sdk/python/packages/flet-permission-handler/src/flet_permission_handler/__init__.py @@ -0,0 +1,8 @@ +from flet_permission_handler.permission_handler import PermissionHandler +from flet_permission_handler.types import Permission, PermissionStatus + +__all__ = [ + "Permission", + "PermissionHandler", + "PermissionStatus", +] diff --git a/sdk/python/packages/flet-permission-handler/src/flet_permission_handler/permission_handler.py b/sdk/python/packages/flet-permission-handler/src/flet_permission_handler/permission_handler.py new file mode 100644 index 0000000000..06f8d7a169 --- /dev/null +++ b/sdk/python/packages/flet-permission-handler/src/flet_permission_handler/permission_handler.py @@ -0,0 +1,82 @@ +from typing import Optional + +import flet as ft +from flet_permission_handler.types import Permission, PermissionStatus + +__all__ = ["PermissionHandler"] + + +@ft.control("PermissionHandler") +class PermissionHandler(ft.Service): + """ + Manages permissions for the application. + + Danger: Platform support + Currently only supported on Android, iOS, Windows, and Web platforms. + + Raises: + FletUnsupportedPlatformException: If the platform is not supported. + """ + + def before_update(self): + super().before_update() + + # validate platform + if not ( + self.page.web + or self.page.platform + in [ + ft.PagePlatform.ANDROID, + ft.PagePlatform.IOS, + ft.PagePlatform.WINDOWS, + ] + ): + raise ft.FletUnsupportedPlatformException( + "PermissionHandler is currently only supported on Android, iOS, " + "Windows, and Web platforms." + ) + + async def get_status(self, permission: Permission) -> Optional[PermissionStatus]: + """ + Gets the current status of the given `permission`. + + Args: + permission: The `Permission` to check the status for. + + Returns: + A `PermissionStatus` if the status is known, otherwise `None`. + """ + status = await self._invoke_method( + method_name="get_status", + arguments={"permission": permission}, + ) + return PermissionStatus(status) if status is not None else None + + async def request(self, permission: Permission) -> Optional[PermissionStatus]: + """ + Request the user for access to the `permission` if access hasn't already been + granted access before. + + Args: + permission: The `Permission` to request. + + Returns: + The new `PermissionStatus` after the request, or `None` if the request + was not successful. + """ + r = await self._invoke_method( + method_name="request", + arguments={"permission": permission}, + ) + return PermissionStatus(r) if r is not None else None + + async def open_app_settings(self) -> bool: + """ + Opens the app settings page. + + Returns: + `True` if the app settings page could be opened, otherwise `False`. + """ + return await self._invoke_method( + method_name="open_app_settings", + ) diff --git a/sdk/python/packages/flet-permission-handler/src/flet_permission_handler/types.py b/sdk/python/packages/flet-permission-handler/src/flet_permission_handler/types.py new file mode 100644 index 0000000000..a4f918e501 --- /dev/null +++ b/sdk/python/packages/flet-permission-handler/src/flet_permission_handler/types.py @@ -0,0 +1,425 @@ +from enum import Enum + +__all__ = [ + "Permission", + "PermissionStatus", +] + + +class PermissionStatus(Enum): + """Defines the state of a [`Permission`][(p).].""" + + GRANTED = "granted" + """ + The user granted access to the requested feature. + """ + + DENIED = "denied" + """ + The user denied access to the requested feature, permission needs to be asked first. + """ + + PERMANENTLY_DENIED = "permanentlyDenied" + """ + Permission to the requested feature is permanently denied, + the permission dialog will not be shown when requesting this permission. + The user may still change the permission status in the settings. + + Note: + - On Android: + - Android 11+ (API 30+): whether the user denied the permission + for a second time. + - Below Android 11 (API 30): whether the user denied access + to the requested feature and selected to never again show a request. + - On iOS: If the user has denied access to the requested feature. + """ + + LIMITED = "limited" + """ + The user has authorized this application for limited access. + So far this is only relevant for the Photo Library picker. + + Note: + Only supported on iOS (iOS14+) and Android (Android 14+). + """ + + PROVISIONAL = "provisional" + """ + The application is provisionally authorized to post non-interruptive + user notifications. + + Note: + Only supported on iOS (iOS 12+). + """ + + RESTRICTED = "restricted" + """ + The OS denied access to the requested feature. The user cannot change + this app's status, possibly due to active restrictions such as parental + controls being in place. + + Note: + Only supported on iOS. + """ + + +# todo: show how pyproject config for each could look like for each permission +# (what exactly is needed in manifest, plist, etc.) + + +class Permission(Enum): + """Defines the permissions which can be checked and requested.""" + + ACCESS_MEDIA_LOCATION = "accessMediaLocation" + """ + Permission for accessing the device's media library. + + Allows an application to access any geographic locations persisted in the + user's shared collection. + + Note: + Only supported on Android 10+ (API 29+) only. + """ + + ACCESS_NOTIFICATION_POLICY = "accessNotificationPolicy" + """ + Permission for accessing the device's notification policy. + + Allows the user to access the notification policy of the phone. + Example: Allows app to turn on and off do-not-disturb. + + Note: + Only supported on Android Marshmallow+ (API 23+) only. + """ + + ACTIVITY_RECOGNITION = "activityRecognition" + """ + Permission for accessing the activity recognition. + + Note: + Only supported on Android 10+ (API 29+) only. + """ + + APP_TRACKING_TRANSPARENCY = "appTrackingTransparency" + """ + Permission for accessing the device's tracking state. + Allows user to accept that your app collects data about end users and + shares it with other companies for purposes of tracking across apps and + websites. + + Note: + Only supported on iOS only. + """ + + ASSISTANT = "assistant" + """ + Info: + - Android: Nothing + - iOS: SiriKit + """ + + AUDIO = "audio" + """ + Permission for accessing the device's audio files from external storage. + + Note: + Only supported on Android 13+ (API 33+) only. + """ + + BACKGROUND_REFRESH = "backgroundRefresh" + """ + Permission for reading the current background refresh status. + + Note: + Only supported on iOS only. + """ + + BLUETOOTH = "bluetooth" + """ + Permission for accessing the device's bluetooth adapter state. + + Depending on the platform and version, the requirements are slightly different: + + Info: + - Android: always allowed. + - iOS: + - 13 and above: The authorization state of Core Bluetooth manager. + - below 13: always allowed. + """ + + BLUETOOTH_ADVERTISE = "bluetoothAdvertise" + """ + Permission for advertising Bluetooth devices + Allows the user to make this device discoverable to other Bluetooth devices. + + Note: + Only supported on Android 12+ (API 31+) only. + """ + + BLUETOOTH_CONNECT = "bluetoothConnect" + """ + Permission for connecting to Bluetooth devices. + Allows the user to connect with already paired Bluetooth devices. + + Note: + Only supported on Android 12+ (API 31+) only. + """ + + BLUETOOTH_SCAN = "bluetoothScan" + """ + Permission for scanning for Bluetooth devices. + + Note: + Only supported on Android 12+ (API 31+) only. + """ + + CALENDAR_FULL_ACCESS = "calendarFullAccess" + """ + Permission for reading from and writing to the device's calendar. + """ + + CALENDAR_WRITE_ONLY = "calendarWriteOnly" + """ + Permission for writing to the device's calendar. + + On iOS 16 and lower, this permission is identical to + [`CALENDAR_FULL_ACCESS`][..]. + """ + + CAMERA = "camera" + """ + Permission for accessing the device's camera. + + Info: + - Android: Camera + - iOS: Photos (Camera Roll and Camera) + """ + + CONTACTS = "contacts" + """ + Permission for accessing the device's contacts. + + Info: + - Android: Contacts + - iOS: AddressBook + """ + + CRITICAL_ALERTS = "criticalAlerts" + """ + Permission for sending critical alerts. + Allow for sending notifications that override the ringer. + + Note: + Only supported on iOS only. + """ + + IGNORE_BATTERY_OPTIMIZATIONS = "ignoreBatteryOptimizations" + """ + Permission for accessing ignore battery optimizations. + + Note: + Only supported on Android only. + """ + + LOCATION = "location" + """ + Permission for accessing the device's location. + + Info: + - Android: Fine and Coarse Location + - iOS: CoreLocation (Always and WhenInUse) + """ + + LOCATION_ALWAYS = "locationAlways" + """ + Info: + iOS: CoreLocation (Always) + """ + + LOCATION_WHEN_IN_USE = "locationWhenInUse" + """ + Permission for accessing the device's location when the app is + running in the foreground. + + Info: + - Android: Fine and Coarse Location + - iOS: CoreLocation - WhenInUse + """ + + MANAGE_EXTERNAL_STORAGE = "manageExternalStorage" + """ + Permission for accessing the device's external storage. + Allows an application a broad access to external storage in scoped storage. + + You should request this permission only when your app cannot + effectively make use of the more privacy-friendly APIs. + For more information: + https://developer.android.com/training/data-storage/manage-all-files + + Info: + When the privacy-friendly APIs (i.e. [Storage Access Framework](https://developer.android.com/guide/topics/providers/document-provider) + or the[MediaStore](https://developer.android.com/training/data-storage/shared/media) APIs) + is all your app needs, the [PermissionGroup.storage] are the only + permissions you need to request. + + If the usage of this permission is needed, you have to fill out + the Permission Declaration Form upon submitting your app to the + Google Play Store. + More details: + https://support.google.com/googleplay/android-developer/answer/9214102#zippy= + + Note: + Only supported on Android 11+ (API 30+) only. + """ # noqa: E501 + + MEDIA_LIBRARY = "mediaLibrary" + """ + Permission for accessing the device's media library. + + Note: + Only supported on iOS 9.3+ only + """ + + MICROPHONE = "microphone" + """ + Permission for accessing the device's microphone. + """ + + NEARBY_WIFI_DEVICES = "nearbyWifiDevices" + """ + Permission for connecting to nearby devices via Wi-Fi. + + Note: + Only supported on Android 13+ (API 33+) only. + """ + + NOTIFICATION = "notification" + """ + Permission for pushing notifications. + """ + + PHONE = "phone" + """ + Permission for accessing the device's phone state. + + Note: + Only supported on Android only. + """ + + PHOTOS = "photos" + """ + Permission for accessing (read & write) the device's photos. + + If you only want to add photos, you can use + the `PHOTOS_ADD_ONLY` permission instead (iOS only). + """ + + PHOTOS_ADD_ONLY = "photosAddOnly" + """ + Permission for adding photos to the device's photo library (iOS only). + + If you want to read them as well, use the `Permission.PHOTOS` permission instead. + + Info: + iOS: Photos (14+ read & write access level) + """ + + REMINDERS = "reminders" + """ + Permission for accessing the device's reminders. + + Note: + Only supported on iOS only. + """ + + REQUEST_INSTALL_PACKAGES = "requestInstallPackages" + """ + Permission for requesting installing packages. + + Note: + Only supported on Android Marshmallow+ (API 23+) only. + """ + + SCHEDULE_EXACT_ALARM = "scheduleExactAlarm" + """ + Permission for scheduling exact alarms. + + Note: + Only supported on Android 12+ (API 31+) only. + """ + + SENSORS = "sensors" + """ + Permission for accessing the device's sensors. + + Info: + - Android: Body Sensors + - iOS: CoreMotion + """ + + SENSORS_ALWAYS = "sensorsAlways" + """ + Permission for accessing the device's sensors in background. + + Note: + Only supported on Android 13+ (API 33+) only. + """ + + SMS = "sms" + """ + Permission for sending and reading SMS messages (Android only). + """ + + SPEECH = "speech" + """ + Permission for accessing speech recognition. + + Info: + - Android: Requests access to microphone + (identical to requesting [`MICROPHONE`][..]). + - iOS: Requests speech access (different from requesting + [`MICROPHONE`][..]). + """ + + STORAGE = "storage" + """ + Permission for accessing external storage. + + Depending on the platform and version, the requirements are slightly different: + + Info: + - Android: + - On Android 13 (API 33) and above, this permission is deprecated and + always returns `PermissionStatus.denied`. Instead use `Permission.PHOTOS`, + `Permission.VIDEO`, `Permission.AUDIO` or + `Permission.MANAGE_EXTERNAL_STORAGE`. + For more information see + [this](https://pub.dev/packages/permission_handler#faq). + + - Below Android 13 (API 33), the `READ_EXTERNAL_STORAGE` and + `WRITE_EXTERNAL_STORAGE` permissions are requested (depending on the + definitions in the AndroidManifest.xml) file. + - iOS: Access to folders like `Documents` or `Downloads`. Implicitly granted. + """ + + SYSTEM_ALERT_WINDOW = "systemAlertWindow" + """ + Permission for creating system alert window. + Allows an app to create windows shown on top of all other apps. + + Note: + Only supported on Android only. + """ + + UNKNOWN = "unknown" + """ + The unknown only used for return type, never requested. + """ + + VIDEOS = "videos" + """ + Permission for accessing the device's video files from external storage. + + Note: + Only supported on Android 13+ (API 33+) only. + """ diff --git a/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/.gitignore b/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/.gitignore new file mode 100644 index 0000000000..ed7794f2ab --- /dev/null +++ b/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/.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-permission-handler/src/flutter/flet_permission_handler/analysis_options.yaml b/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/analysis_options.yaml new file mode 100644 index 0000000000..a5744c1cfb --- /dev/null +++ b/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/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-permission-handler/src/flutter/flet_permission_handler/lib/flet_permission_handler.dart b/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/lib/flet_permission_handler.dart new file mode 100644 index 0000000000..076a1f34a0 --- /dev/null +++ b/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/lib/flet_permission_handler.dart @@ -0,0 +1,3 @@ +library flet_permission_handler; + +export "src/extension.dart" show Extension; diff --git a/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/lib/src/extension.dart b/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/lib/src/extension.dart new file mode 100644 index 0000000000..35ec302865 --- /dev/null +++ b/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/lib/src/extension.dart @@ -0,0 +1,15 @@ +import 'package:flet/flet.dart'; + +import 'permission_handler.dart'; + +class Extension extends FletExtension { + @override + FletService? createService(Control control) { + switch (control.type) { + case "PermissionHandler": + return PermissionHandlerService(control: control); + default: + return null; + } + } +} diff --git a/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/lib/src/permission_handler.dart b/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/lib/src/permission_handler.dart new file mode 100644 index 0000000000..523ac12837 --- /dev/null +++ b/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/lib/src/permission_handler.dart @@ -0,0 +1,46 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/widgets.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import 'utils/permission_handler.dart'; + +class PermissionHandlerService extends FletService { + PermissionHandlerService({required super.control}); + + @override + void init() { + super.init(); + debugPrint("PermissionHandler(${control.id}).init: ${control.properties}"); + control.addInvokeMethodListener(_invokeMethod); + } + + Future _invokeMethod(String name, dynamic args) async { + debugPrint("PermissionHandler.$name($args)"); + switch (name) { + case "get_status": + return await parsePermission(args['permission'])?.status.then((value) { + return value.name; + }); + case "request": + var permission = parsePermission(args['permission']); + if (permission != null) { + Future status = permission.request(); + return await status.then((value) async { + return value.name; + }); + } + break; + case "open_app_settings": + return await openAppSettings(); + default: + throw Exception("Unknown PermissionHandler method: $name"); + } + } + + @override + void dispose() { + debugPrint("PermissionHandler(${control.id}).dispose()"); + control.removeInvokeMethodListener(_invokeMethod); + super.dispose(); + } +} diff --git a/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/lib/src/utils/permission_handler.dart b/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/lib/src/utils/permission_handler.dart new file mode 100644 index 0000000000..6ff8f0a7aa --- /dev/null +++ b/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/lib/src/utils/permission_handler.dart @@ -0,0 +1,11 @@ +import "package:collection/collection.dart"; +import "package:permission_handler/permission_handler.dart"; + +Permission? parsePermission(String? value, [Permission? defaultValue]) { + if (value == null) return defaultValue; + return Permission.values.firstWhereOrNull( + (Permission p) => + p.toString().split('.').last.toLowerCase() == value.toLowerCase(), + ) ?? + defaultValue; +} diff --git a/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/pubspec.yaml b/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/pubspec.yaml new file mode 100644 index 0000000000..ee7f793a14 --- /dev/null +++ b/sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler/pubspec.yaml @@ -0,0 +1,23 @@ +name: flet_permission_handler +description: Flet Permission Handler control +version: 0.1.0 +publish_to: none + +environment: + sdk: '>=3.2.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + collection: ^1.16.0 + permission_handler: 12.0.1 + + flet: + path: ../../../../../../../packages/flet + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 diff --git a/sdk/python/packages/flet-rive/CHANGELOG.md b/sdk/python/packages/flet-rive/CHANGELOG.md new file mode 100644 index 0000000000..e8fa3a3507 --- /dev/null +++ b/sdk/python/packages/flet-rive/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.2.0] - 2025-06-26 + +- Published documentation at https://docs.flet.dev/rive/ +- Added async-friendly examples and workspace integration for the `Rive` control. + +## [0.1.0] - 2025-01-15 + +- Initial release. diff --git a/sdk/python/packages/flet-rive/LICENSE b/sdk/python/packages/flet-rive/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/sdk/python/packages/flet-rive/LICENSE @@ -0,0 +1,201 @@ + 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 such Contributor that are necessarily infringed 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 consequential 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-rive/README.md b/sdk/python/packages/flet-rive/README.md new file mode 100644 index 0000000000..3ba3e37b6a --- /dev/null +++ b/sdk/python/packages/flet-rive/README.md @@ -0,0 +1,40 @@ +# flet-rive + +[![pypi](https://img.shields.io/pypi/v/flet-rive.svg)](https://pypi.python.org/pypi/flet-rive) +[![downloads](https://static.pepy.tech/badge/flet-rive/month)](https://pepy.tech/project/flet-rive) +[![license](https://img.shields.io/github/license/flet-dev/flet.svg)](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-rive/LICENSE) + +A cross-platform [Flet](https://flet.dev) extension for displaying [Rive](https://rive.app/) animations. + +It is based on the [rive](https://pub.dev/packages/rive) Flutter package. + +## Documentation + +Detailed documentation to this package can be found [here](https://docs.flet.dev/rive/). + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | + +## Usage + +### Installation + +To install the `flet-rive` package and add it to your project dependencies: + +- Using `uv`: + ```bash + uv add flet-rive + ``` + +- Using `pip`: + ```bash + pip install flet-rive + ``` + 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/controls/rive). diff --git a/sdk/python/packages/flet-rive/pyproject.toml b/sdk/python/packages/flet-rive/pyproject.toml new file mode 100644 index 0000000000..b0f5292c4e --- /dev/null +++ b/sdk/python/packages/flet-rive/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "flet-rive" +version = "0.1.0" +description = "Display Rive animations in 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://docs.flet.dev/rive" +Repository = "https://github.com/flet-dev/flet/tree/main/sdk/python/packages/flet-rive" +Issues = "https://github.com/flet-dev/flet/issues" + +[tool.setuptools.package-data] +"flutter.flet_rive" = ["**/*"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/sdk/python/packages/flet-rive/src/flet_rive/__init__.py b/sdk/python/packages/flet-rive/src/flet_rive/__init__.py new file mode 100644 index 0000000000..1c676ffb41 --- /dev/null +++ b/sdk/python/packages/flet-rive/src/flet_rive/__init__.py @@ -0,0 +1,3 @@ +from flet_rive.rive import Rive as Rive + +__all__ = ["Rive"] diff --git a/sdk/python/packages/flet-rive/src/flet_rive/rive.py b/sdk/python/packages/flet-rive/src/flet_rive/rive.py new file mode 100644 index 0000000000..e660e54be0 --- /dev/null +++ b/sdk/python/packages/flet-rive/src/flet_rive/rive.py @@ -0,0 +1,82 @@ +from dataclasses import field +from typing import Optional + +import flet as ft + +__all__ = ["Rive"] + + +@ft.control("Rive") +class Rive(ft.LayoutControl): + """ + Displays rive animations. + """ + + src: str + """ + The source of your rive animation. + + Can either be a URL or a path to a local asset file. + """ + + placeholder: Optional[ft.Control] = None + """ + Control displayed while the Rive is loading. + """ + + artboard: Optional[str] = None + """ + The name of the artboard to use. + If not specified, the default artboard of the provided `src` is used. + """ + + alignment: Optional[ft.Alignment] = None + """ + Alignment for the animation in the Rive control. + """ + + enable_antialiasing: bool = True + """ + Whether to enable anti-aliasing when rendering. + """ + + use_artboard_size: bool = False + """ + Determines whether to use the inherent size of the artboard, + i.e. the absolute size defined by the artboard, + or size the control based on the available constraints only (sized by parent). + """ + + fit: Optional[ft.BoxFit] = None + """ + The animation's fit. + """ + + speed_multiplier: ft.Number = 1.0 + """ + A multiplier for controlling the speed of the Rive animation playback. + """ + + animations: list[str] = field(default_factory=list) + """ + List of animations to play; default animation is played if empty. + """ + + state_machines: list[str] = field(default_factory=list) + """ + List of state machines to play; none will play if empty. + """ + + headers: Optional[dict[str, str]] = None + """ + Headers for network requests. + """ + + clip_rect: Optional[ft.Rect] = None + """ + Clip the artboard to this rect. + + If not supplied it'll default to the constraint size provided by the parent + control. + Unless the Artboard has clipping disabled, then no clip will be applied. + """ diff --git a/sdk/python/packages/flet-rive/src/flutter/flet_rive/.gitignore b/sdk/python/packages/flet-rive/src/flutter/flet_rive/.gitignore new file mode 100644 index 0000000000..ed7794f2ab --- /dev/null +++ b/sdk/python/packages/flet-rive/src/flutter/flet_rive/.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-rive/src/flutter/flet_rive/.metadata b/sdk/python/packages/flet-rive/src/flutter/flet_rive/.metadata new file mode 100644 index 0000000000..07d8623a38 --- /dev/null +++ b/sdk/python/packages/flet-rive/src/flutter/flet_rive/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "2e9cb0aa71a386a91f73f7088d115c0d96654829" + channel: "stable" + +project_type: package diff --git a/sdk/python/packages/flet-rive/src/flutter/flet_rive/analysis_options.yaml b/sdk/python/packages/flet-rive/src/flutter/flet_rive/analysis_options.yaml new file mode 100644 index 0000000000..a5744c1cfb --- /dev/null +++ b/sdk/python/packages/flet-rive/src/flutter/flet_rive/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-rive/src/flutter/flet_rive/lib/flet_rive.dart b/sdk/python/packages/flet-rive/src/flutter/flet_rive/lib/flet_rive.dart new file mode 100644 index 0000000000..6f81f1a43b --- /dev/null +++ b/sdk/python/packages/flet-rive/src/flutter/flet_rive/lib/flet_rive.dart @@ -0,0 +1,3 @@ +library flet_rive; + +export "src/extension.dart" show Extension; diff --git a/sdk/python/packages/flet-rive/src/flutter/flet_rive/lib/src/extension.dart b/sdk/python/packages/flet-rive/src/flutter/flet_rive/lib/src/extension.dart new file mode 100644 index 0000000000..96be5a8ff4 --- /dev/null +++ b/sdk/python/packages/flet-rive/src/flutter/flet_rive/lib/src/extension.dart @@ -0,0 +1,16 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/cupertino.dart'; + +import 'rive.dart'; + +class Extension extends FletExtension { + @override + Widget? createWidget(Key? key, Control control) { + switch (control.type) { + case "Rive": + return RiveControl(control: control); + default: + return null; + } + } +} diff --git a/sdk/python/packages/flet-rive/src/flutter/flet_rive/lib/src/rive.dart b/sdk/python/packages/flet-rive/src/flutter/flet_rive/lib/src/rive.dart new file mode 100644 index 0000000000..6e81b9e484 --- /dev/null +++ b/sdk/python/packages/flet-rive/src/flutter/flet_rive/lib/src/rive.dart @@ -0,0 +1,75 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/widgets.dart'; +import 'package:rive/rive.dart'; + +class RiveControl extends StatefulWidget { + final Control control; + + const RiveControl({super.key, required this.control}); + + @override + State createState() => _RiveControlState(); +} + +class _RiveControlState extends State { + @override + Widget build(BuildContext context) { + debugPrint("Rive build: ${widget.control.id} (${widget.control.hashCode})"); + var src = widget.control.getString("src"); + if (src == null) { + return const ErrorControl("Rive must have \"src\" specified."); + } + + var artBoard = widget.control.getString("art_board"); + var antiAliasing = widget.control.getBool("enable_anti_aliasing", true)!; + var useArtBoardSize = widget.control.getBool("use_art_board_size", false)!; + var fit = widget.control.getBoxFit("fit"); + var alignment = widget.control.getAlignment("alignment"); + var placeholder = widget.control.buildWidget("placeholder"); + var speedMultiplier = widget.control.getDouble("speed_multiplier", 1)!; + var animations = widget.control.get>("animations", const [])!; + var stateMachines = + widget.control.get>("state_machines", const [])!; + var headers = widget.control.get("headers")?.cast(); + var clipRect = widget.control.getRect("clip_rect"); + + Widget? rive; + + var assetSrc = widget.control.backend.getAssetSource(src); + if (assetSrc.isFile) { + // Local File + rive = RiveAnimation.file( + assetSrc.path, + artboard: artBoard, + fit: fit, + antialiasing: antiAliasing, + useArtboardSize: useArtBoardSize, + alignment: alignment, + placeHolder: placeholder, + speedMultiplier: speedMultiplier, + animations: animations, + stateMachines: stateMachines, + clipRect: clipRect, + ); + } else { + // URL + rive = RiveAnimation.network( + assetSrc.path, + fit: fit, + artboard: artBoard, + alignment: alignment, + antialiasing: antiAliasing, + useArtboardSize: useArtBoardSize, + placeHolder: placeholder, + speedMultiplier: speedMultiplier, + animations: animations, + stateMachines: stateMachines, + headers: headers, + clipRect: clipRect, + // onInit: _onInit, + ); + } + + return ConstrainedControl(control: widget.control, child: rive); + } +} diff --git a/sdk/python/packages/flet-rive/src/flutter/flet_rive/pubspec.yaml b/sdk/python/packages/flet-rive/src/flutter/flet_rive/pubspec.yaml new file mode 100644 index 0000000000..8195edac84 --- /dev/null +++ b/sdk/python/packages/flet-rive/src/flutter/flet_rive/pubspec.yaml @@ -0,0 +1,23 @@ +name: flet_rive +description: Flet Rive control +version: 0.1.0 +publish_to: none + +environment: + sdk: '>=3.2.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + collection: ^1.16.0 + rive: 0.13.20 + + flet: + path: ../../../../../../../packages/flet + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 diff --git a/sdk/python/packages/flet-video/CHANGELOG.md b/sdk/python/packages/flet-video/CHANGELOG.md new file mode 100644 index 0000000000..5934adbdc3 --- /dev/null +++ b/sdk/python/packages/flet-video/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [0.2.0] - 2025-06-26 + +### Added + +- Deployed online documentation: https://docs.flet.dev/video/ +- `Video` new property: `subtitle_track` +- `VideoConfiguration` new properties: `width`, `height`, `scale` + +### Changed + +- Refactored `Video` control to use `@flet.control` dataclass-style definition. +- Renamed `Video` event handler properties: + - `on_loaded` β†’ `on_load` + - `on_completed` β†’ `on_complete` + - `on_track_changed` β†’ `on_track_change` + +## [0.1.0] - 2025-01-15 + +Initial release. + + +[0.2.0]: https://github.com/flet-dev/flet-video/compare/0.1.0...0.2.0 +[0.1.0]: https://github.com/flet-dev/flet-video/releases/tag/0.1.0 diff --git a/sdk/python/packages/flet-video/LICENSE b/sdk/python/packages/flet-video/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/sdk/python/packages/flet-video/LICENSE @@ -0,0 +1,201 @@ + 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 such Contributor that are necessarily infringed 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 consequential 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-video/README.md b/sdk/python/packages/flet-video/README.md new file mode 100644 index 0000000000..5a478372f0 --- /dev/null +++ b/sdk/python/packages/flet-video/README.md @@ -0,0 +1,40 @@ +# flet-video + +[![pypi](https://img.shields.io/pypi/v/flet-video.svg)](https://pypi.python.org/pypi/flet-video) +[![downloads](https://static.pepy.tech/badge/flet-video/month)](https://pepy.tech/project/flet-video) +[![license](https://img.shields.io/github/license/flet-dev/flet.svg)](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-video/LICENSE) + +A cross-platform video player for [Flet](https://flet.dev) apps. + +It is based on the [media_kit](https://pub.dev/packages/media_kit) Flutter package. + +## Documentation + +Detailed documentation to this package can be found [here](https://docs.flet.dev/video/). + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | + +## Usage + +### Installation + +To install the `flet-video` package and add it to your project dependencies: + +- Using `uv`: + ```bash + uv add flet-video + ``` + +- Using `pip`: + ```bash + pip install flet-video + ``` + 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/controls/video). diff --git a/sdk/python/packages/flet-video/pyproject.toml b/sdk/python/packages/flet-video/pyproject.toml new file mode 100644 index 0000000000..c34352d080 --- /dev/null +++ b/sdk/python/packages/flet-video/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "flet-video" +version = "0.1.0" +description = "Cross-platform video playback 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://docs.flet.dev/video" +Repository = "https://github.com/flet-dev/flet/tree/main/sdk/python/packages/flet-video" +Issues = "https://github.com/flet-dev/flet/issues" + +[tool.setuptools.package-data] +"flutter.flet_video" = ["**/*"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/sdk/python/packages/flet-video/src/flet_video/__init__.py b/sdk/python/packages/flet-video/src/flet_video/__init__.py new file mode 100644 index 0000000000..05476ff85e --- /dev/null +++ b/sdk/python/packages/flet-video/src/flet_video/__init__.py @@ -0,0 +1,21 @@ +""" +Public exports for the flet-video package. +""" + +from flet_video.types import ( + PlaylistMode, + VideoConfiguration, + VideoMedia, + VideoSubtitleConfiguration, + VideoSubtitleTrack, +) +from flet_video.video import Video + +__all__ = [ + "PlaylistMode", + "Video", + "VideoConfiguration", + "VideoMedia", + "VideoSubtitleConfiguration", + "VideoSubtitleTrack", +] diff --git a/sdk/python/packages/flet-video/src/flet_video/types.py b/sdk/python/packages/flet-video/src/flet_video/types.py new file mode 100644 index 0000000000..4661adb7ea --- /dev/null +++ b/sdk/python/packages/flet-video/src/flet_video/types.py @@ -0,0 +1,217 @@ +""" +Type definitions and configuration objects for flet-video. +""" + +from dataclasses import dataclass, field +from enum import Enum +from typing import Optional + +import flet as ft + +__all__ = [ + "PlaylistMode", + "VideoConfiguration", + "VideoMedia", + "VideoSubtitleConfiguration", + "VideoSubtitleTrack", +] + + +class PlaylistMode(Enum): + """Defines the playback mode for the video playlist.""" + + NONE = "none" + """End playback once end of the playlist is reached.""" + + SINGLE = "single" + """Indefinitely loop over the currently playing file in the playlist.""" + + LOOP = "loop" + """Loop over the playlist & restart it from beginning once end is reached.""" + + +@dataclass +class VideoMedia: + """Represents a media resource for video playback.""" + + resource: str + """URI of the media resource.""" + + http_headers: Optional[dict[str, str]] = None + """HTTP headers to be used for the media resource.""" + + extras: Optional[dict[str, str]] = None + """Additional metadata for the media resource.""" + + +@dataclass +class VideoConfiguration: + """Additional configuration for video playback.""" + + output_driver: Optional[str] = None + """ + Sets the [--vo](https://mpv.io/manual/stable/#options-vo) property + on native backend. + + The default value is platform dependent: + - Windows, GNU/Linux, macOS & iOS : `"libmpv"` + - Android: `"gpu"` + """ + + hardware_decoding_api: Optional[str] = None + """ + Sets the [--hwdec](https://mpv.io/manual/stable/#options-hwdec) + property on native backend. + + The default value is platform dependent: + - Windows, GNU/Linux, macOS & iOS : `"auto"` + - Android: `"auto-safe"` + """ + + enable_hardware_acceleration: bool = True + """ + Whether to enable hardware acceleration. + When disabled, may cause battery drain, device heating, and high CPU usage. + """ + + width: Optional[ft.Number] = None + """ + The fixed width for the video output. + """ + + height: Optional[ft.Number] = None + """ + The fixed height for the video output. + """ + + scale: ft.Number = 1.0 + """ + The scale for the video output. + Specifying this option will cause [`width`][..] & [`height`][..] to be ignored. + """ + + +@dataclass +class VideoSubtitleTrack: + """Represents a subtitle track for a video.""" + + src: str + """ + The subtitle source. + + Supported values: + - A URL (e.g. "https://example.com/subs.srt" or "www.example.com/sub.vtt") + - An absolute local file path (not supported on the web platform) + - A raw subtitle text string (e.g. the full contents of an SRT/VTT file) + """ + + title: Optional[str] = None + """The title of the subtitle track, e.g. 'English'.""" + + language: Optional[str] = None + """The language of the subtitle track, e.g. 'en'.""" + + channels_count: Optional[int] = None + """ + The number of audio channels detected in the media. + """ + + channels: Optional[str] = None + """ + Channel layout string describing the spatial arrangement of channels. + """ + + sample_rate: Optional[int] = None + """ + Audio sampling rate in hertz. + """ + + fps: Optional[ft.Number] = None + """ + Video frames per second. + """ + + bitrate: Optional[int] = None + """ + Overall media bitrate in bits per second. + """ + + rotate: Optional[int] = None + """ + Rotation metadata in degrees to apply when rendering the video. + """ + + par: Optional[ft.Number] = None + """ + Pixel aspect ratio value. + """ + + audio_channels: Optional[int] = None + """ + Explicit audio channel count override. + """ + + album_art: Optional[bool] = None + """ + Whether the track represents album art rather than timed media. + """ + + codec: Optional[str] = None + """ + Codec identifier for the media stream. + """ + + decoder: Optional[str] = None + """ + Decoder name used to process the media stream. + """ + + @classmethod + def none(cls) -> "VideoSubtitleTrack": + """No subtitle track. Disables subtitle output.""" + return VideoSubtitleTrack(src="none") + + @classmethod + def auto(cls) -> "VideoSubtitleTrack": + """Default subtitle track. Selects the first subtitle track.""" + return VideoSubtitleTrack(src="auto") + + +@dataclass +class VideoSubtitleConfiguration: + """Represents the configuration for video subtitles.""" + + text_style: ft.TextStyle = field( + default_factory=lambda: ft.TextStyle( + height=1.4, + size=32.0, + letter_spacing=0.0, + word_spacing=0.0, + color=ft.Colors.WHITE, + weight=ft.FontWeight.NORMAL, + bgcolor=ft.Colors.BLACK54, + ) + ) + """The text style to be used for the subtitles.""" + + text_scale_factor: ft.Number = 1.0 + """ + Defines the scale factor for the subtitle text. + """ + + text_align: ft.TextAlign = ft.TextAlign.CENTER + """ + The text alignment to be used for the subtitles. + """ + + padding: ft.PaddingValue = field( + default_factory=lambda: ft.Padding(left=16.0, top=0.0, right=16.0, bottom=24.0) + ) + """ + The padding to be used for the subtitles. + """ + + visible: bool = True + """ + Whether the subtitles should be visible or not. + """ diff --git a/sdk/python/packages/flet-video/src/flet_video/video.py b/sdk/python/packages/flet-video/src/flet_video/video.py new file mode 100644 index 0000000000..606a6bfa27 --- /dev/null +++ b/sdk/python/packages/flet-video/src/flet_video/video.py @@ -0,0 +1,280 @@ +""" +Video control definition for the flet-video package. +""" + +from dataclasses import field +from typing import Optional + +import flet as ft +from flet_video.types import ( + PlaylistMode, + VideoConfiguration, + VideoMedia, + VideoSubtitleConfiguration, + VideoSubtitleTrack, +) + +__all__ = ["Video"] + + +@ft.control("Video") +class Video(ft.LayoutControl): + """ + A control that displays a video from a playlist. + """ + + playlist: list[VideoMedia] = field(default_factory=list) + """ + A list of `VideoMedia`s representing the video files to be played. + """ + + title: str = "flet-video" + """ + Defines the name of the underlying window & process for native backend. + This is visible inside the windows' volume mixer. + """ + + fit: ft.BoxFit = ft.BoxFit.CONTAIN + """ + The box fit to use for the video. + """ + + fill_color: ft.ColorValue = ft.Colors.BLACK + """ + Defines the color used to fill the video background. + """ + + wakelock: bool = True + """ + Whether to acquire wake lock while playing the video. + When `True`, device's display will not go to standby/sleep while + the video is playing. + """ + + autoplay: bool = False + """ + Whether the video should start playing automatically. + """ + + show_controls: bool = True + """ + Whether to show the video player controls. + """ + + muted: bool = False + """ + Defines whether the video player should be started in muted state. + """ + + playlist_mode: Optional[PlaylistMode] = None + """ + Represents the mode of playback for the playlist. + """ + + shuffle_playlist: bool = False + """ + Defines whether the playlist should be shuffled. + """ + + volume: ft.Number = 100.0 + """ + Defines the volume of the video player. + + Note: + It's value ranges between `0.0` to `100.0` (inclusive), where `0.0` + is muted and `100.0` is the maximum volume. + An exception will be raised if the value is outside this range. + + Raises: + AssertionError: If the [`volume`][(c).] is not between + `0.0` and `100.0` (inclusive). + """ + + playback_rate: ft.Number = 1.0 + """ + Defines the playback rate of the video player. + """ + + alignment: ft.Alignment = field(default_factory=lambda: ft.Alignment.CENTER) + """ + Defines the Alignment of the viewport. + """ + + filter_quality: ft.FilterQuality = ft.FilterQuality.LOW + """ + Filter quality of the texture used to render the video output. + + Note: + Android was reported to show blurry images when using + [`FilterQuality.HIGH`][flet.FilterQuality.HIGH]. + Prefer the usage of [`FilterQuality.MEDIUM`][flet.FilterQuality.MEDIUM] + on this platform. + """ + + pause_upon_entering_background_mode: bool = True + """ + Whether to pause the video when application enters background mode. + """ + + resume_upon_entering_foreground_mode: bool = False + """ + Whether to resume the video when application enters foreground mode. + Has effect only if [`pause_upon_entering_background_mode`][..] is also set to + `True`. + """ + + pitch: ft.Number = 1.0 + """ + Defines the relative pitch of the video player. + """ + + configuration: VideoConfiguration = field( + default_factory=lambda: VideoConfiguration() + ) + """ + Additional configuration for the video player. + """ + + subtitle_configuration: VideoSubtitleConfiguration = field( + default_factory=lambda: VideoSubtitleConfiguration() + ) + """ + Defines the subtitle configuration for the video player. + """ + + subtitle_track: Optional[VideoSubtitleTrack] = None + """ + Defines the subtitle track for the video player. + """ + + on_load: Optional[ft.ControlEventHandler["Video"]] = None + """Fires when the video player is initialized and ready for playback.""" + + on_enter_fullscreen: Optional[ft.ControlEventHandler["Video"]] = None + """Fires when the video player enters fullscreen.""" + + on_exit_fullscreen: Optional[ft.ControlEventHandler["Video"]] = None + """Fires when the video player exits fullscreen""" + + on_error: Optional[ft.ControlEventHandler["Video"]] = None + """ + Fires when an error occurs. + + Event handler argument's [`data`][flet.Event.data] property contains + information about the error. + """ + + on_complete: Optional[ft.ControlEventHandler["Video"]] = None + """Fires when a video player completes.""" + + on_track_change: Optional[ft.ControlEventHandler["Video"]] = None + """ + Fires when a video track changes. + + Event handler argument's [`data`][flet.Event.data] property contains + the index of the new track. + """ + + def before_update(self): + super().before_update() + assert 0 <= self.volume <= 100, ( + f"volume must be between 0 and 100 inclusive, got {self.volume}" + ) + + async def play(self): + """Starts playing the video.""" + await self._invoke_method("play") + + async def pause(self): + """Pauses the video player.""" + await self._invoke_method("pause") + + async def play_or_pause(self): + """ + Cycles between play and pause states of the video player, + i.e., plays if paused and pauses if playing. + """ + await self._invoke_method("play_or_pause") + + async def stop(self): + """Stops the video player.""" + await self._invoke_method("stop") + + async def next(self): + """Jumps to the next `VideoMedia` in the [`playlist`][..].""" + await self._invoke_method("next") + + async def previous(self): + """Jumps to the previous `VideoMedia` in the [`playlist`][..].""" + await self._invoke_method("previous") + + async def seek(self, position: ft.DurationValue): + """ + Seeks the currently playing `VideoMedia` from the + [`playlist`][..] at the specified `position`. + """ + await self._invoke_method( + "seek", + {"position": position}, + ) + + async def jump_to(self, media_index: int): + """ + Jumps to the `VideoMedia` at the specified `media_index` + in the [`playlist`][..]. + """ + assert self.playlist[media_index], "media_index is out of range" + if media_index < 0: + # dart doesn't support negative indexes + media_index = len(self.playlist) + media_index + await self._invoke_method( + method_name="jump_to", + arguments={"media_index": media_index}, + ) + + async def playlist_add(self, media: VideoMedia): + """Appends/Adds the provided `media` to the `playlist`.""" + assert media.resource, "media has no resource" + await self._invoke_method( + method_name="playlist_add", + arguments={"media": media}, + ) + self.playlist.append(media) + + async def playlist_remove(self, media_index: int): + """Removes the provided `media` from the `playlist`.""" + assert self.playlist[media_index], "index out of range" + await self._invoke_method( + method_name="playlist_remove", + arguments={"media_index": media_index}, + ) + self.playlist.pop(media_index) + + async def is_playing(self) -> bool: + """ + Returns: + `True` if the video player is currently playing, `False` otherwise. + """ + return await self._invoke_method("is_playing") + + async def is_completed(self) -> bool: + """ + Returns: + `True` if video player has reached the end of + the currently playing media, `False` otherwise. + """ + return await self._invoke_method("is_completed") + + async def get_duration(self) -> ft.Duration: + """ + Returns: + The duration of the currently playing media. + """ + return await self._invoke_method("get_duration") + + async def get_current_position(self) -> ft.Duration: + """ + Returns: + The current position of the currently playing media. + """ + return await self._invoke_method("get_current_position") diff --git a/sdk/python/packages/flet-video/src/flutter/flet_video/.gitignore b/sdk/python/packages/flet-video/src/flutter/flet_video/.gitignore new file mode 100644 index 0000000000..ed7794f2ab --- /dev/null +++ b/sdk/python/packages/flet-video/src/flutter/flet_video/.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-video/src/flutter/flet_video/LICENSE b/sdk/python/packages/flet-video/src/flutter/flet_video/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/sdk/python/packages/flet-video/src/flutter/flet_video/LICENSE @@ -0,0 +1,201 @@ + 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 such Contributor that are necessarily infringed 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 consequential 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-video/src/flutter/flet_video/analysis_options.yaml b/sdk/python/packages/flet-video/src/flutter/flet_video/analysis_options.yaml new file mode 100644 index 0000000000..a5744c1cfb --- /dev/null +++ b/sdk/python/packages/flet-video/src/flutter/flet_video/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-video/src/flutter/flet_video/lib/flet_video.dart b/sdk/python/packages/flet-video/src/flutter/flet_video/lib/flet_video.dart new file mode 100644 index 0000000000..345e980435 --- /dev/null +++ b/sdk/python/packages/flet-video/src/flutter/flet_video/lib/flet_video.dart @@ -0,0 +1,3 @@ +library flet_video; + +export "src/extension.dart" show Extension; diff --git a/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/extension.dart b/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/extension.dart new file mode 100644 index 0000000000..278ece6f43 --- /dev/null +++ b/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/extension.dart @@ -0,0 +1,22 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:media_kit/media_kit.dart'; + +import 'video.dart'; + +class Extension extends FletExtension { + @override + void ensureInitialized() { + MediaKit.ensureInitialized(); + } + + @override + Widget? createWidget(Key? key, Control control) { + switch (control.type) { + case "Video": + return VideoControl(key: key, control: control); + default: + return null; + } + } +} diff --git a/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/utils/file_utils_io.dart b/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/utils/file_utils_io.dart new file mode 100644 index 0000000000..fb8568dd6e --- /dev/null +++ b/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/utils/file_utils_io.dart @@ -0,0 +1,9 @@ +import 'dart:io'; + +/// Only available on non‐web platforms. +/// Returns the file's contents if it exists, or null otherwise. +String? readFileAsStringIfExists(String path) { + final file = File(path); + if (file.existsSync()) return file.readAsStringSync(); + return null; +} diff --git a/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/utils/file_utils_web.dart b/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/utils/file_utils_web.dart new file mode 100644 index 0000000000..ef494e019f --- /dev/null +++ b/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/utils/file_utils_web.dart @@ -0,0 +1,4 @@ +/// On web, local‐path reading isn’t supported. Always returns null. +String? readFileAsStringIfExists(String path) { + return null; +} diff --git a/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/utils/video.dart b/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/utils/video.dart new file mode 100644 index 0000000000..68cc4dc7c7 --- /dev/null +++ b/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/utils/video.dart @@ -0,0 +1,135 @@ +import 'package:collection/collection.dart'; +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; +import 'package:media_kit/media_kit.dart'; +import 'package:media_kit_video/media_kit_video.dart'; + +import "file_utils_web.dart" if (dart.library.io) 'file_utils_io.dart'; + +Media? parseVideoMedia(dynamic value, [Media? defaultValue]) { + if (value == null || value["resource"] == null) return defaultValue; + + final extras = (value["extras"] as Map?)?.map( + (key, val) => MapEntry(key.toString(), val.toString()), + ); + + final httpHeaders = (value["http_headers"] as Map?)?.map( + (key, val) => MapEntry(key.toString(), val.toString()), + ); + + return Media(value["resource"], extras: extras, httpHeaders: httpHeaders); +} + +List? parseVideoMedias(dynamic value, [List? defaultValue]) { + if (value == null) return defaultValue; + + if (value is List) { + return value.map((e) => parseVideoMedia(e)).nonNulls.toList(); + } + + final media = parseVideoMedia(value); + return media != null ? [media] : defaultValue; +} + +SubtitleViewConfiguration? parseSubtitleConfiguration( + dynamic value, ThemeData theme, + [SubtitleViewConfiguration? defaultValue]) { + if (value == null) return defaultValue; + + return SubtitleViewConfiguration( + style: parseTextStyle( + value["text_style"], + theme, + const TextStyle( + height: 1.4, + fontSize: 32.0, + letterSpacing: 0.0, + wordSpacing: 0.0, + color: Color(0xffffffff), + fontWeight: FontWeight.normal, + backgroundColor: Color(0xaa000000)))!, + visible: parseBool(value["visible"], true)!, + textScaler: TextScaler.linear(parseDouble(value["text_scale_factor"], 1)!), + textAlign: parseTextAlign(value["text_align"], TextAlign.center)!, + padding: parsePadding( + value["padding"], const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 24.0))!, + ); +} + +bool isUrl(String value) { + final urlPattern = RegExp(r'^(http:\/\/|https:\/\/|www\.)'); + return urlPattern.hasMatch(value); +} + +SubtitleTrack? parseSubtitleTrack( + dynamic value, + BuildContext context, [ + SubtitleTrack? defaultValue, +]) { + if (value == null) return defaultValue; + + String src; + final String rawSrc = value["src"] as String; + if (rawSrc == "none") return SubtitleTrack.no(); + if (rawSrc == "auto") return SubtitleTrack.auto(); + + bool uri = false; + + if (isUrl(rawSrc)) { + uri = true; + src = rawSrc; + } else { + // Non-URL: on non-web platforms, try reading it as a file path + String? fileContents; + if (!isWebPlatform()) { + // todo: add support for relative paths to assets-dir + fileContents = readFileAsStringIfExists(rawSrc); + } + + // If reading succeeded, use the file’s contents; + // otherwise assume rawSrc is already subtitle text + src = fileContents ?? rawSrc; + uri = false; + } + + return SubtitleTrack( + src, + value["title"], + value["language"], + channelscount: parseInt(value["channels_count"]), + channels: value["channels"], + samplerate: parseInt(value["sample_rate"]), + fps: parseDouble(value["fps"]), + bitrate: parseInt(value["bitrate"]), + rotate: parseInt(value["rotate"]), + par: parseDouble(value["par"]), + audiochannels: parseInt(value["audio_channels"]), + albumart: parseBool(value["album_art"]), + codec: value["codec"], + decoder: value["decoder"], + data: !uri, + // true when providing raw subtitle text + uri: uri, // true when providing a URL + ); +} + +VideoControllerConfiguration? parseControllerConfiguration(dynamic value, + [VideoControllerConfiguration? defaultValue]) { + if (value == null) return defaultValue; + return VideoControllerConfiguration( + vo: value["output_driver"], + hwdec: value["hardware_decoding_api"], + enableHardwareAcceleration: + parseBool(value["enable_hardware_acceleration"], true)!, + width: value["width"], + height: value["height"], + scale: parseDouble(value["scale"], 1.0)!, + ); +} + +PlaylistMode? parsePlaylistMode(String? value, [PlaylistMode? defaultValue]) { + if (value == null) return defaultValue; + return PlaylistMode.values.firstWhereOrNull( + (e) => e.name.toLowerCase() == value.toLowerCase()) ?? + defaultValue; +} diff --git a/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/video.dart b/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/video.dart new file mode 100644 index 0000000000..dec64f86f7 --- /dev/null +++ b/sdk/python/packages/flet-video/src/flutter/flet_video/lib/src/video.dart @@ -0,0 +1,218 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; +import 'package:media_kit/media_kit.dart'; +import 'package:media_kit_video/media_kit_video.dart'; + +import 'utils/video.dart'; + +class VideoControl extends StatefulWidget { + final Control control; + + const VideoControl({super.key, required this.control}); + + @override + State createState() => _VideoControlState(); +} + +class _VideoControlState extends State with FletStoreMixin { + late final playerConfig = PlayerConfiguration( + title: widget.control.getString("title", "flet-video")!, + muted: widget.control.getBool("muted", false)!, + pitch: widget.control.getDouble("pitch") != null ? true : false, + ready: widget.control.getBool("on_loaded", false)! + ? () => widget.control.triggerEvent("loaded") + : null, + ); + + late final Player player = Player(configuration: playerConfig); + late final videoControllerConfiguration = parseControllerConfiguration( + widget.control.get("configuration"), + const VideoControllerConfiguration())!; + late final controller = + VideoController(player, configuration: videoControllerConfiguration); + + @override + void initState() { + super.initState(); + widget.control.addInvokeMethodListener(_invokeMethod); + player.open(Playlist(parseVideoMedias(widget.control.get("playlist"), [])!), + play: widget.control.getBool("autoplay", false)!); + } + + @override + void dispose() { + widget.control.removeInvokeMethodListener(_invokeMethod); + player.dispose(); + super.dispose(); + } + + void _onError(String? message) { + if (widget.control.getBool("on_error", false)!) { + widget.control.triggerEvent("error", message); + } + } + + Future _invokeMethod(String name, dynamic args) async { + debugPrint("Video.$name($args)"); + switch (name) { + case "play": + await player.play(); + break; + case "pause": + await player.pause(); + break; + case "play_or_pause": + await player.playOrPause(); + break; + case "stop": + await player.stop(); + player.open( + Playlist(parseVideoMedias(widget.control.get("playlist"), [])!), + play: false); + break; + case "seek": + var position = parseDuration(args["position"]); + if (position != null) await player.seek(position); + break; + case "next": + await player.next(); + break; + case "previous": + await player.previous(); + break; + case "jump_to": + final mediaIndex = parseInt(args["media_index"]); + if (mediaIndex != null) await player.jump(mediaIndex); + break; + case "playlist_add": + var media = parseVideoMedia(args["media"]); + if (media != null) await player.add(media); + break; + case "playlist_remove": + final mediaIndex = parseInt(args["media_index"]); + if (mediaIndex != null) await player.remove(mediaIndex); + break; + case "is_playing": + return player.state.playing; + case "is_completed": + return player.state.completed; + case "get_duration": + return player.state.duration; + case "get_current_position": + return player.state.position; + default: + throw Exception("Unknown Video method: $name"); + } + } + + @override + Widget build(BuildContext context) { + debugPrint("Video XXX build: ${widget.control.id}"); + + var subtitleConfiguration = parseSubtitleConfiguration( + widget.control.get("subtitle_configuration"), + Theme.of(context), + const SubtitleViewConfiguration())!; + var subtitleTrack = + parseSubtitleTrack(widget.control.get("subtitle_track"), context); + + var volume = widget.control.getDouble("volume"); + var pitch = widget.control.getDouble("pitch"); + var playbackRate = widget.control.getDouble("playback_rate"); + var shufflePlaylist = widget.control.getBool("shuffle_playlist"); + var showControls = widget.control.getBool("show_controls", true)!; + var playlistMode = + parsePlaylistMode(widget.control.getString("playlist_mode")); + + // previous values + final prevVolume = widget.control.getDouble("_volume"); + final prevPitch = widget.control.getDouble("_pitch"); + final prevPlaybackRate = widget.control.getDouble("_playback_rate"); + final prevShufflePlaylist = widget.control.getBool("_shuffle_playlist"); + final PlaylistMode? prevPlaylistMode = widget.control.get("_playlist_mode"); + final SubtitleTrack? prevSubtitleTrack = + widget.control.get("_subtitleTrack"); + + Video video = Video( + controller: controller, + wakelock: widget.control.getBool("wakelock", true)!, + controls: showControls ? AdaptiveVideoControls : null, + pauseUponEnteringBackgroundMode: + widget.control.getBool("pause_upon_entering_background_mode", true)!, + resumeUponEnteringForegroundMode: widget.control + .getBool("resume_upon_entering_foreground_mode", false)!, + alignment: widget.control.getAlignment("alignment", Alignment.center)!, + fit: widget.control.getBoxFit("fit", BoxFit.contain)!, + filterQuality: + widget.control.getFilterQuality("filter_quality", FilterQuality.low)!, + subtitleViewConfiguration: subtitleConfiguration, + fill: widget.control.getColor("fill_color", context, Colors.black)!, + onEnterFullscreen: widget.control.getBool("on_enter_fullscreen", false)! + ? () async => widget.control.triggerEvent("enter_fullscreen") + : defaultEnterNativeFullscreen, + onExitFullscreen: widget.control.getBool("on_exit_fullscreen", false)! + ? () async => widget.control.triggerEvent("exit_fullscreen") + : defaultExitNativeFullscreen, + ); + + () async { + if (volume != null && + volume != prevVolume && + volume >= 0 && + volume <= 100) { + widget.control.updateProperties({"_volume": volume}, python: false); + await player.setVolume(volume); + } + + if (pitch != null && pitch != prevPitch) { + widget.control.updateProperties({"_pitch": pitch}, python: false); + await player.setPitch(pitch); + } + + if (playbackRate != null && playbackRate != prevPlaybackRate) { + widget.control + .updateProperties({"_playbackRate": playbackRate}, python: false); + await player.setRate(playbackRate); + } + + if (shufflePlaylist != null && shufflePlaylist != prevShufflePlaylist) { + widget.control.updateProperties({"_shufflePlaylist": shufflePlaylist}, + python: false); + await player.setShuffle(shufflePlaylist); + } + + if (playlistMode != null && playlistMode != prevPlaylistMode) { + widget.control + .updateProperties({"_playlistMode": playlistMode}, python: false); + await player.setPlaylistMode(playlistMode); + } + + if (subtitleTrack != null && subtitleTrack != prevSubtitleTrack) { + widget.control + .updateProperties({"_subtitleTrack": subtitleTrack}, python: false); + await player.setSubtitleTrack(subtitleTrack); + } + }(); + + // listen to errors + player.stream.error.listen((event) { + _onError(event); + }); + + // listen to completion + player.stream.completed.listen((event) { + if (widget.control.getBool("on_complete", false)!) { + widget.control.triggerEvent("complete", event); + } + }); + + // listen to track changes + player.stream.playlist.listen((event) { + if (widget.control.getBool("on_track_change", false)!) { + widget.control.triggerEvent("track_change", event.index); + } + }); + + return ConstrainedControl(control: widget.control, child: video); + } +} diff --git a/sdk/python/packages/flet-video/src/flutter/flet_video/pubspec.yaml b/sdk/python/packages/flet-video/src/flutter/flet_video/pubspec.yaml new file mode 100644 index 0000000000..e31c510793 --- /dev/null +++ b/sdk/python/packages/flet-video/src/flutter/flet_video/pubspec.yaml @@ -0,0 +1,25 @@ +name: flet_video +description: Flet Video control +version: 0.1.0 +publish_to: none + +environment: + sdk: '>=3.2.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + + collection: ^1.16.0 + media_kit: 1.2.1 + media_kit_video: 1.3.1 + media_kit_libs_video: 1.0.7 + + flet: + path: ../../../../../../../packages/flet + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 diff --git a/sdk/python/packages/flet-webview/CHANGELOG.md b/sdk/python/packages/flet-webview/CHANGELOG.md new file mode 100644 index 0000000000..87a123c36e --- /dev/null +++ b/sdk/python/packages/flet-webview/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [0.2.0] - 2025-06-26 + +### Added + +- Deployed online documentation: https://docs.flet.dev/webview/ + +### Changed + +- Refactored all controls to use `@flet.control` dataclass-style definition. + +## [0.1.0] - 2025-01-15 + +Initial release. + + +[0.2.0]: https://github.com/flet-dev/flet-lottie/compare/0.1.0...0.2.0 +[0.1.0]: https://github.com/flet-dev/flet-lottie/releases/tag/0.1.0 diff --git a/sdk/python/packages/flet-webview/LICENSE b/sdk/python/packages/flet-webview/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/sdk/python/packages/flet-webview/LICENSE @@ -0,0 +1,201 @@ + 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 such Contributor that are necessarily infringed 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 consequential 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-webview/README.md b/sdk/python/packages/flet-webview/README.md new file mode 100644 index 0000000000..feffa8b3fb --- /dev/null +++ b/sdk/python/packages/flet-webview/README.md @@ -0,0 +1,43 @@ +# flet-webview + +[![pypi](https://img.shields.io/pypi/v/flet-webview.svg)](https://pypi.python.org/pypi/flet-webview) +[![downloads](https://static.pepy.tech/badge/flet-webview/month)](https://pepy.tech/project/flet-webview) +[![license](https://img.shields.io/github/license/flet-dev/flet.svg)](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-webview/LICENSE) + +A [Flet](https://flet.dev) extension for displaying web content in a WebView. + +It is based on the [webview_flutter](https://pub.dev/packages/webview_flutter) +and [webview_flutter_web](https://pub.dev/packages/webview_flutter_web) Flutter packages. + +> **Important:** WebView requires platform-specific configuration (e.g., enabling webview on iOS). Consult Flutter's platform setup guides. + +## Documentation + +Detailed documentation to this package can be found [here](https://docs.flet.dev/webview/). + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| ❌ | βœ… | ❌ | βœ… | βœ… | βœ… | + +## Usage + +### Installation + +To install the `flet-webview` package and add it to your project dependencies: + +- Using `uv`: + ```bash + uv add flet-webview + ``` + +- Using `pip`: + ```bash + pip install flet-webview + ``` + 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/examples/controls/webview). diff --git a/sdk/python/packages/flet-webview/pyproject.toml b/sdk/python/packages/flet-webview/pyproject.toml new file mode 100644 index 0000000000..7a1ce6422a --- /dev/null +++ b/sdk/python/packages/flet-webview/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "flet-webview" +version = "0.1.0" +description = "Embed web content inside Flet apps via WebView." +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://docs.flet.dev/webview" +Repository = "https://github.com/flet-dev/flet/tree/main/sdk/python/packages/flet-webview" +Issues = "https://github.com/flet-dev/flet/issues" + +[tool.setuptools.package-data] +"flutter.flet_webview" = ["**/*"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/sdk/python/packages/flet-webview/src/flet_webview/__init__.py b/sdk/python/packages/flet-webview/src/flet_webview/__init__.py new file mode 100644 index 0000000000..4601ba14fd --- /dev/null +++ b/sdk/python/packages/flet-webview/src/flet_webview/__init__.py @@ -0,0 +1,19 @@ +from flet_webview.types import ( + JavaScriptMode, + LogLevelSeverity, + RequestMethod, + WebViewConsoleMessageEvent, + WebViewJavaScriptEvent, + WebViewScrollEvent, +) +from flet_webview.webview import WebView + +__all__ = [ + "JavaScriptMode", + "LogLevelSeverity", + "RequestMethod", + "WebView", + "WebViewConsoleMessageEvent", + "WebViewJavaScriptEvent", + "WebViewScrollEvent", +] diff --git a/sdk/python/packages/flet-webview/src/flet_webview/types.py b/sdk/python/packages/flet-webview/src/flet_webview/types.py new file mode 100644 index 0000000000..925fa3b19d --- /dev/null +++ b/sdk/python/packages/flet-webview/src/flet_webview/types.py @@ -0,0 +1,92 @@ +from dataclasses import dataclass +from enum import Enum +from typing import TYPE_CHECKING + +import flet as ft + +if TYPE_CHECKING: + from flet_webview.webview import WebView # noqa + +__all__ = [ + "JavaScriptMode", + "LogLevelSeverity", + "RequestMethod", + "WebViewConsoleMessageEvent", + "WebViewJavaScriptEvent", + "WebViewScrollEvent", +] + + +class RequestMethod(Enum): + """Defines the supported HTTP methods for loading a page in a `WebView`.""" + + GET = "get" + """HTTP GET method.""" + + POST = "post" + """HTTP POST method.""" + + +class LogLevelSeverity(Enum): + """Represents the severity of a JavaScript log message.""" + + ERROR = "error" + """ + Indicates an error message was logged via an "error" event of the + `console.error` method. + """ + + WARNING = "warning" + """Indicates a warning message was logged using the `console.warning` method.""" + + DEBUG = "debug" + """Indicates a debug message was logged using the `console.debug` method.""" + + INFO = "info" + """Indicates an informational message was logged using the `console.info` method.""" + + LOG = "log" + """Indicates a log message was logged using the `console.log` method.""" + + +class JavaScriptMode(Enum): + """Defines the state of JavaScript support in the `WebView`.""" + + UNRESTRICTED = "unrestricted" + """JavaScript execution is unrestricted.""" + + DISABLED = "disabled" + """JavaScript execution is disabled.""" + + +@dataclass +class WebViewScrollEvent(ft.Event["WebView"]): + x: float + """ + The value of the horizontal offset with the origin being at the + leftmost of the `WebView`. + """ + + y: float + """ + The value of the vertical offset with the origin being at the + topmost of the `WebView`. + """ + + +@dataclass +class WebViewConsoleMessageEvent(ft.Event["WebView"]): + message: str + """The message written to the console.""" + + severity_level: LogLevelSeverity + """The severity of a JavaScript log message.""" + + +@dataclass +class WebViewJavaScriptEvent(ft.Event["WebView"]): + message: str + """The message to be displayed in the window.""" + + url: str + """The URL of the page requesting the dialog.""" diff --git a/sdk/python/packages/flet-webview/src/flet_webview/webview.py b/sdk/python/packages/flet-webview/src/flet_webview/webview.py new file mode 100644 index 0000000000..5c25b22379 --- /dev/null +++ b/sdk/python/packages/flet-webview/src/flet_webview/webview.py @@ -0,0 +1,383 @@ +from typing import Optional + +import flet as ft +from flet_webview.types import ( + JavaScriptMode, + RequestMethod, + WebViewConsoleMessageEvent, + WebViewJavaScriptEvent, + WebViewScrollEvent, +) + +__all__ = ["WebView"] + + +@ft.control("WebView") +class WebView(ft.LayoutControl): + """ + Easily load webpages while allowing user interaction. + + Note: + Works only on the following platforms: iOS, Android, macOS and Web. + """ + + url: str + """The URL of the web page to load.""" + + prevent_links: Optional[list[str]] = None + """List of url-prefixes that should not be followed/loaded/downloaded.""" + + bgcolor: Optional[ft.ColorValue] = None + """Defines the background color of the WebView.""" + + on_page_started: Optional[ft.ControlEventHandler["WebView"]] = None + """ + Fires soon as the first loading process of the webview page is started. + + Event handler argument's [`data`][flet.Event.data] property is of type + `str` and contains the URL. + + Note: + Works only on the following platforms: iOS, Android and macOS. + """ + + on_page_ended: Optional[ft.ControlEventHandler["WebView"]] = None + """ + Fires when all the webview page loading processes are ended. + + Event handler argument's [`data`][flet.Event.data] property is of type `str` + and contains the URL. + + Note: + Works only on the following platforms: iOS, Android and macOS. + """ + + on_web_resource_error: Optional[ft.ControlEventHandler["WebView"]] = None + """ + Fires when there is error with loading a webview page resource. + + Event handler argument's [`data`][flet.Event.data] property is of type + `str` and contains the error message. + + Note: + Works only on the following platforms: iOS, Android and macOS. + """ + + on_progress: Optional[ft.ControlEventHandler["WebView"]] = None + """ + Fires when the progress of the webview page loading is changed. + + Event handler argument's [`data`][flet.Event.data] property is of type + `int` and contains the progress value. + + Note: + Works only on the following platforms: iOS, Android and macOS. + """ + + on_url_change: Optional[ft.ControlEventHandler["WebView"]] = None + """ + Fires when the URL of the webview page is changed. + + Event handler argument's [`data`][flet.Event.data] property is of type + `str` and contains the new URL. + + Note: + Works only on the following platforms: iOS, Android and macOS. + """ + + on_scroll: Optional[ft.EventHandler[WebViewScrollEvent]] = None + """ + Fires when the web page's scroll position changes. + + Note: + Works only on the following platforms: iOS, Android and macOS. + """ + + on_console_message: Optional[ft.EventHandler[WebViewConsoleMessageEvent]] = None + """ + Fires when a log message is written to the JavaScript console. + + Note: + Works only on the following platforms: iOS, Android and macOS. + """ + + on_javascript_alert_dialog: Optional[ft.EventHandler[WebViewJavaScriptEvent]] = None + """ + Fires when the web page attempts to display a JavaScript alert() dialog. + + Note: + Works only on the following platforms: iOS, Android and macOS. + """ + + def _check_mobile_or_mac_platform(self): + """ + Checks/Validates support for the current platform (iOS, Android, or macOS). + """ + assert self.page is not None, "WebView must be added to page first." + if self.page.web or self.page.platform not in [ + ft.PagePlatform.ANDROID, + ft.PagePlatform.IOS, + ft.PagePlatform.MACOS, + ]: + raise ft.FletUnsupportedPlatformException( + "This method is supported on Android, iOS and macOS platforms only." + ) + + async def reload(self): + """ + Reloads the current URL. + + Note: + Works only on the following platforms: iOS, Android and macOS. + """ + self._check_mobile_or_mac_platform() + await self._invoke_method("reload") + + async def can_go_back(self) -> bool: + """ + Whether there's a back history item. + + Note: + Works only on the following platforms: iOS, Android and macOS. + + Returns: + `True` if there is a back history item, `False` otherwise. + """ + self._check_mobile_or_mac_platform() + return await self._invoke_method("can_go_back") + + async def can_go_forward(self) -> bool: + """ + Whether there's a forward history item. + + Note: + Works only on the following platforms: iOS, Android and macOS. + + Returns: + `True` if there is a forward history item, `False` otherwise. + """ + self._check_mobile_or_mac_platform() + return await self._invoke_method("can_go_forward") + + async def go_back(self): + """ + Goes back in the history of the webview, if `can_go_back()` is `True`. + + Note: + Works only on the following platforms: iOS, Android and macOS. + """ + self._check_mobile_or_mac_platform() + await self._invoke_method("go_back") + + async def go_forward(self): + """ + Goes forward in the history of the webview, + if [`can_go_forward()`][(c).can_go_forward] is `True`. + + Note: + Works only on the following platforms: iOS, Android and macOS. + """ + self._check_mobile_or_mac_platform() + await self._invoke_method("go_forward") + + async def enable_zoom(self): + """ + Enables zooming using the on-screen zoom controls and gestures. + + Note: + Works only on the following platforms: iOS, Android and macOS. + """ + self._check_mobile_or_mac_platform() + await self._invoke_method("enable_zoom") + + async def disable_zoom(self): + """ + Disables zooming using the on-screen zoom controls and gestures. + + Note: + Works only on the following platforms: iOS, Android and macOS. + """ + self._check_mobile_or_mac_platform() + await self._invoke_method("disable_zoom") + + async def clear_cache(self): + """ + Clears all caches used by the WebView. + + The following caches are cleared: + - Browser HTTP Cache + - Cache API caches. Service workers tend to use this cache. + - Application cache + + Note: + Works only on the following platforms: iOS, Android and macOS. + """ + self._check_mobile_or_mac_platform() + await self._invoke_method("clear_cache") + + async def clear_local_storage(self): + """ + Clears the local storage used by the WebView. + + Note: + Works only on the following platforms: iOS, Android and macOS. + """ + self._check_mobile_or_mac_platform() + await self._invoke_method("clear_local_storage") + + async def get_current_url(self) -> Optional[str]: + """ + Gets the current URL that the WebView is displaying or `None` + if no URL was ever loaded. + + Note: + Works only on the following platforms: iOS, Android and macOS. + + Returns: + The current URL that the WebView is displaying or `None` + if no URL was ever loaded. + """ + self._check_mobile_or_mac_platform() + return await self._invoke_method("get_current_url") + + async def get_title(self) -> Optional[str]: + """ + Get the title of the currently loaded page. + + Note: + Works only on the following platforms: iOS, Android and macOS. + + Returns: + The title of the currently loaded page. + """ + self._check_mobile_or_mac_platform() + return await self._invoke_method("get_title") + + async def get_user_agent(self) -> Optional[str]: + """ + Get the value used for the HTTP `User-Agent:` request header. + + Note: + Works only on the following platforms: iOS, Android and macOS. + + Returns: + The value used for the HTTP `User-Agent:` request header. + """ + self._check_mobile_or_mac_platform() + return await self._invoke_method("get_user_agent") + + async def load_file(self, path: str): + """ + Loads the provided local file. + + Note: + Works only on the following platforms: iOS, Android and macOS. + + Args: + path: The absolute path to the file. + """ + self._check_mobile_or_mac_platform() + await self._invoke_method( + method_name="load_file", + arguments={"path": path}, + ) + + async def load_request(self, url: str, method: RequestMethod = RequestMethod.GET): + """ + Makes an HTTP request and loads the response in the webview. + + Args: + url: The URL to load. + method: The HTTP method to use. + + Note: + Works only on the following platforms: iOS, Android and macOS. + """ + self._check_mobile_or_mac_platform() + await self._invoke_method( + "load_request", arguments={"url": url, "method": method} + ) + + async def run_javascript(self, value: str): + """ + Runs the given JavaScript in the context of the current page. + + Args: + value: The JavaScript code to run. + + Note: + Works only on the following platforms: iOS, Android and macOS. + """ + self._check_mobile_or_mac_platform() + await self._invoke_method( + method_name="run_javascript", + arguments={"value": value}, + ) + + async def load_html(self, value: str, base_url: Optional[str] = None): + """ + Loads the provided HTML string. + + Note: + Works only on the following platforms: iOS, Android and macOS. + + Args: + value: The HTML string to load. + base_url: The base URL to use when resolving relative URLs within the value. + """ + self._check_mobile_or_mac_platform() + await self._invoke_method( + "load_html", arguments={"value": value, "base_url": base_url} + ) + + async def scroll_to(self, x: int, y: int): + """ + Scrolls to the provided position of webview pixels. + + Note: + Works only on the following platforms: iOS, Android and macOS. + + Args: + x: The x-coordinate of the scroll position. + y: The y-coordinate of the scroll position. + """ + self._check_mobile_or_mac_platform() + await self._invoke_method( + method_name="scroll_to", + arguments={"x": x, "y": y}, + ) + + async def scroll_by(self, x: int, y: int): + """ + Scrolls by the provided number of webview pixels. + + Note: + Works only on the following platforms: iOS, Android and macOS. + + Args: + x: The number of pixels to scroll by on the x-axis. + y: The number of pixels to scroll by on the y-axis. + """ + self._check_mobile_or_mac_platform() + await self._invoke_method( + method_name="scroll_by", + arguments={"x": x, "y": y}, + ) + + async def set_javascript_mode(self, mode: JavaScriptMode): + """ + Sets the JavaScript mode of the WebView. + + Note: + - Works only on the following platforms: iOS, Android and macOS. + - Disabling the JavaScript execution on the page may result to + unexpected web page behaviour. + + Args: + mode: The JavaScript mode to set. + """ + self._check_mobile_or_mac_platform() + await self._invoke_method( + method_name="set_javascript_mode", + arguments={"mode": mode}, + ) diff --git a/sdk/python/packages/flet-webview/src/flutter/flet_webview/.gitignore b/sdk/python/packages/flet-webview/src/flutter/flet_webview/.gitignore new file mode 100644 index 0000000000..ed7794f2ab --- /dev/null +++ b/sdk/python/packages/flet-webview/src/flutter/flet_webview/.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-webview/src/flutter/flet_webview/analysis_options.yaml b/sdk/python/packages/flet-webview/src/flutter/flet_webview/analysis_options.yaml new file mode 100644 index 0000000000..a5744c1cfb --- /dev/null +++ b/sdk/python/packages/flet-webview/src/flutter/flet_webview/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-webview/src/flutter/flet_webview/lib/flet_webview.dart b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/flet_webview.dart new file mode 100644 index 0000000000..0816f961c3 --- /dev/null +++ b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/flet_webview.dart @@ -0,0 +1,3 @@ +library flet_webview; + +export "src/extension.dart" show Extension; diff --git a/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/extension.dart b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/extension.dart new file mode 100644 index 0000000000..e716a2ab23 --- /dev/null +++ b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/extension.dart @@ -0,0 +1,16 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/cupertino.dart'; + +import 'webview.dart'; + +class Extension extends FletExtension { + @override + Widget? createWidget(Key? key, Control control) { + switch (control.type) { + case "WebView": + return WebViewControl(control: control); + default: + return null; + } + } +} diff --git a/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/utils/webview.dart b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/utils/webview.dart new file mode 100644 index 0000000000..60919e2f5a --- /dev/null +++ b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/utils/webview.dart @@ -0,0 +1,18 @@ +import 'package:collection/collection.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +LoadRequestMethod? parseLoadRequestMethod(String? value, + [LoadRequestMethod? defaultValue]) { + if (value == null) return defaultValue; + return LoadRequestMethod.values.firstWhereOrNull( + (e) => e.name.toLowerCase() == value.toLowerCase()) ?? + defaultValue; +} + +JavaScriptMode? parseJavaScriptMode(String? value, + [JavaScriptMode? defaultValue]) { + if (value == null) return defaultValue; + return JavaScriptMode.values.firstWhereOrNull( + (e) => e.name.toLowerCase() == value.toLowerCase()) ?? + defaultValue; +} diff --git a/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview.dart b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview.dart new file mode 100644 index 0000000000..e6b9d639ef --- /dev/null +++ b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview.dart @@ -0,0 +1,29 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +import 'webview_mobile_and_mac.dart'; +import 'webview_web.dart' if (dart.library.io) "webview_web_vain.dart"; +import 'webview_windows_and_linux.dart' + if (dart.library.html) "webview_windows_and_linux_vain.dart"; + +class WebViewControl extends StatelessWidget { + final Control control; + + const WebViewControl({super.key, required this.control}); + + @override + Widget build(BuildContext context) { + debugPrint("WebViewControl build: ${control.id}"); + Widget view = + const ErrorControl("Webview is not yet supported on this platform."); + if (isWebPlatform()) { + view = WebviewWeb(control: control); + } else if (isMobilePlatform() || isMacOSDesktop()) { + view = WebviewMobileAndMac(control: control); + } else if (isWindowsDesktop() || isLinuxDesktop()) { + view = const WebviewDesktop(); + } + + return ConstrainedControl(control: control, child: view); + } +} diff --git a/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview_mobile_and_mac.dart b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview_mobile_and_mac.dart new file mode 100644 index 0000000000..5378215db7 --- /dev/null +++ b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview_mobile_and_mac.dart @@ -0,0 +1,185 @@ +import 'package:flet/flet.dart'; +import 'package:flet_webview/src/utils/webview.dart'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class WebviewMobileAndMac extends StatefulWidget { + final Control control; + + const WebviewMobileAndMac({super.key, required this.control}); + + @override + State createState() => _WebviewMobileAndMacState(); +} + +class _WebviewMobileAndMacState extends State { + late WebViewController controller; + + @override + void initState() { + super.initState(); + widget.control.addInvokeMethodListener(_invokeMethod); + + var params = const PlatformWebViewControllerCreationParams(); + controller = WebViewController.fromPlatformCreationParams(params); + + controller.setNavigationDelegate( + NavigationDelegate( + onProgress: (int progress) { + widget.control.triggerEvent("progress", progress); + }, + onUrlChange: (UrlChange url) { + widget.control.triggerEvent("url_change", url.url); + }, + onPageStarted: (String url) { + widget.control.triggerEvent("page_started", url); + }, + onPageFinished: (String url) { + widget.control.triggerEvent("page_ended", url); + }, + onWebResourceError: (WebResourceError error) { + widget.control.triggerEvent("web_resource_error", error.description); + }, + onNavigationRequest: (NavigationRequest request) { + var links = widget.control.get("prevent_link"); + var prevent = links is List && + links.isNotEmpty && + links.any((l) => request.url.startsWith(l)); + return prevent + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + ), + ); + + // request + controller.loadRequest( + Uri.parse(widget.control.getString("url", "https://flet.dev")!), + method: parseLoadRequestMethod( + widget.control.getString("method"), LoadRequestMethod.get)!); + + // scroll + if (!isMacOSDesktop()) { + controller.setOnScrollPositionChange((ScrollPositionChange position) { + widget.control + .triggerEvent("scroll", {"x": position.x, "y": position.y}); + }); + } + + // console + controller.setOnConsoleMessage((JavaScriptConsoleMessage message) { + widget.control.triggerEvent("console_message", + {"message": message.message, "level": message.level.name}); + }); + + // alert + controller.setOnJavaScriptAlertDialog( + (JavaScriptAlertDialogRequest request) async { + widget.control.triggerEvent("javascript_alert_dialog", + {"message": request.message, "url": request.url}); + }); + } + + Future _invokeMethod(String name, dynamic args) async { + debugPrint("WebView.$name($args)"); + switch (name) { + case "reload": + await controller.reload(); + break; + case "can_go_back": + return controller.canGoBack().toString(); + case "can_go_forward": + return controller.canGoForward().toString(); + case "go_back": + if (await controller.canGoBack()) { + await controller.goBack(); + } + break; + case "go_forward": + if (await controller.canGoForward()) { + await controller.goForward(); + } + break; + case "enable_zoom": + await controller.enableZoom(true); + break; + case "disable_zoom": + await controller.enableZoom(false); + break; + case "clear_cache": + await controller.clearCache(); + break; + case "clear_local_storage": + await controller.clearLocalStorage(); + break; + case "get_current_url": + return await controller.currentUrl(); + case "get_title": + return await controller.getTitle(); + case "get_user_agent": + return await controller.getUserAgent(); + case "load_file": + await controller.loadFile(args["path"]); + break; + case "load_html": + await controller.loadHtmlString(args["value"], + baseUrl: args["base_url"]); + break; + case "load_request": + var url = args["url"]; + if (url != null) { + await controller.loadRequest(Uri.parse(url), + method: parseLoadRequestMethod( + args["method"], LoadRequestMethod.get)!); + } + break; + case "run_javascript": + var javascript = args["value"]; + if (javascript != null) { + await controller.runJavaScript(javascript); + } + break; + case "scroll_to": + var x = parseInt(args["x"]); + var y = parseInt(args["y"]); + if (x != null && y != null) { + await controller.scrollTo(x, y); + } + break; + case "scroll_by": + var x = parseInt(args["x"]); + var y = parseInt(args["y"]); + if (x != null && y != null) { + await controller.scrollBy(x, y); + } + break; + case "set_javascript_mode": + var mode = parseJavaScriptMode(args["mode"]); + if (mode != null) { + await controller.setJavaScriptMode(mode); + } + break; + default: + throw Exception("Unknown WebView method: $name"); + } + } + + @override + void dispose() { + debugPrint("WebViewControl dispose: ${widget.control.id}"); + widget.control.removeInvokeMethodListener(_invokeMethod); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("WebViewControl build: ${widget.control.id}"); + + var bgcolor = widget.control.getColor("bgcolor", context); + + if (bgcolor != null) { + controller.setBackgroundColor(bgcolor); + } + return WebViewWidget(controller: controller); + } +} diff --git a/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview_web.dart b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview_web.dart new file mode 100644 index 0000000000..fff692b2eb --- /dev/null +++ b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview_web.dart @@ -0,0 +1,37 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_web/webview_flutter_web.dart'; + +class WebviewWeb extends StatefulWidget { + final Control control; + + const WebviewWeb({super.key, required this.control}); + + @override + State createState() => _WebviewWebState(); +} + +class _WebviewWebState extends State { + late PlatformWebViewController controller; + @override + void initState() { + super.initState(); + WebViewPlatform.instance = WebWebViewPlatform(); + + controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + )..loadRequest( + LoadRequestParams( + uri: Uri.parse( + widget.control.getString("url", "https://flet.dev")!)), + ); + } + + @override + Widget build(BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + } +} diff --git a/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview_web_vain.dart b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview_web_vain.dart new file mode 100644 index 0000000000..a93c40a1a4 --- /dev/null +++ b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview_web_vain.dart @@ -0,0 +1,12 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +class WebviewWeb extends StatelessWidget { + final Control control; + + const WebviewWeb({super.key, required this.control}); + @override + Widget build(BuildContext context) { + return const ErrorControl("Webview is not yet supported on this platform."); + } +} diff --git a/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview_windows_and_linux.dart b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview_windows_and_linux.dart new file mode 100644 index 0000000000..8510a8ee4b --- /dev/null +++ b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview_windows_and_linux.dart @@ -0,0 +1,11 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +class WebviewDesktop extends StatelessWidget { + const WebviewDesktop({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const ErrorControl("Webview is not yet supported on this Platform."); + } +} diff --git a/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview_windows_and_linux_vain.dart b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview_windows_and_linux_vain.dart new file mode 100644 index 0000000000..2b03b80508 --- /dev/null +++ b/sdk/python/packages/flet-webview/src/flutter/flet_webview/lib/src/webview_windows_and_linux_vain.dart @@ -0,0 +1,16 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +class WebviewDesktop extends StatefulWidget { + const WebviewDesktop({Key? key}) : super(key: key); + + @override + State createState() => _WebviewDesktopState(); +} + +class _WebviewDesktopState extends State { + @override + Widget build(BuildContext context) { + return const ErrorControl("Webview is not yet supported on this Platform."); + } +} diff --git a/sdk/python/packages/flet-webview/src/flutter/flet_webview/pubspec.yaml b/sdk/python/packages/flet-webview/src/flutter/flet_webview/pubspec.yaml new file mode 100644 index 0000000000..58ff2de61d --- /dev/null +++ b/sdk/python/packages/flet-webview/src/flutter/flet_webview/pubspec.yaml @@ -0,0 +1,27 @@ +name: flet_webview +description: Flet WebView control +version: 0.1.0 +publish_to: none + +environment: + sdk: '>=3.2.3 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + + collection: ^1.16.0 + webview_flutter: 4.13.0 + webview_flutter_web: 0.2.3+4 + webview_flutter_platform_interface: 2.14.0 + + flet: + path: ../../../../../../../packages/flet + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 diff --git a/sdk/python/packages/flet/docs/audio/index.md b/sdk/python/packages/flet/docs/audio/index.md new file mode 100644 index 0000000000..2ef002bb72 --- /dev/null +++ b/sdk/python/packages/flet/docs/audio/index.md @@ -0,0 +1,56 @@ +--- +class_name: flet_audio.Audio +examples: ../../examples/controls/audio +--- + +# Audio + +Allows playing audio in [Flet](https://flet.dev) apps. + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | + +## Usage + +To use `Audio` control add `flet-audio` package to your project dependencies: + +/// tab | uv +```bash +uv add flet-audio +``` + +/// +/// tab | pip +```bash +pip install flet-audio # (1)! +``` + +1. After this, you will have to manually add this package to your `requirements.txt` or `pyproject.toml`. +/// + +## Example + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +/// admonition | Windows Subsystem for Linux (WSL) + type: note +On WSL, you need to install [`GStreamer`](https://github.com/GStreamer/gstreamer) library. + +If you receive `error while loading shared libraries: libgstapp-1.0.so.0`, +it means `GStreamer` is not installed in your WSL environment. + +To install it, run the following command: + +```bash +apt install -y libgstreamer1.0-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-tools +``` +/// + +## Description + +{{ class_all_options(class_name) }} diff --git a/sdk/python/packages/flet/docs/audio/types/audio_duration_change_event.md b/sdk/python/packages/flet/docs/audio/types/audio_duration_change_event.md new file mode 100644 index 0000000000..b20c6f48d3 --- /dev/null +++ b/sdk/python/packages/flet/docs/audio/types/audio_duration_change_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_audio.AudioDurationChangeEvent") }} diff --git a/sdk/python/packages/flet/docs/audio/types/audio_position_change_event.md b/sdk/python/packages/flet/docs/audio/types/audio_position_change_event.md new file mode 100644 index 0000000000..3a43e3e77d --- /dev/null +++ b/sdk/python/packages/flet/docs/audio/types/audio_position_change_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_audio.AudioPositionChangeEvent") }} diff --git a/sdk/python/packages/flet/docs/audio/types/audio_state.md b/sdk/python/packages/flet/docs/audio/types/audio_state.md new file mode 100644 index 0000000000..befb3c0435 --- /dev/null +++ b/sdk/python/packages/flet/docs/audio/types/audio_state.md @@ -0,0 +1 @@ +{{ class_all_options("flet_audio.AudioState", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/audio/types/audio_state_change_event.md b/sdk/python/packages/flet/docs/audio/types/audio_state_change_event.md new file mode 100644 index 0000000000..a9aacbf8d5 --- /dev/null +++ b/sdk/python/packages/flet/docs/audio/types/audio_state_change_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_audio.AudioStateChangeEvent") }} diff --git a/sdk/python/packages/flet/docs/audio/types/release_mode.md b/sdk/python/packages/flet/docs/audio/types/release_mode.md new file mode 100644 index 0000000000..bc95c7f3d9 --- /dev/null +++ b/sdk/python/packages/flet/docs/audio/types/release_mode.md @@ -0,0 +1 @@ +{{ class_all_options("flet_audio.ReleaseMode", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/audio_recorder/index.md b/sdk/python/packages/flet/docs/audio_recorder/index.md new file mode 100644 index 0000000000..3755780964 --- /dev/null +++ b/sdk/python/packages/flet/docs/audio_recorder/index.md @@ -0,0 +1,47 @@ +--- +class_name: flet_audio_recorder.AudioRecorder +examples: ../../examples/controls/audio_recorder +--- + +# Audio Recorder + +Allows recording audio in [Flet](https://flet.dev) apps. + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | + +## Usage + +To use `AudioRecorder` service add `flet-audio-recorder` package to your project dependencies: + +/// tab | uv +```bash +uv add flet-audio-recorder +``` + +/// +/// tab | pip +```bash +pip install flet-audio-recorder # (1)! +``` + +1. After this, you will have to manually add this package to your `requirements.txt` or `pyproject.toml`. +/// + +/// admonition | Linux + type: note +Audio encoding on Linux is provided by [fmedia](https://stsaz.github.io/fmedia/) and must be installed separately. +/// + +## Example + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +## Description + +{{ class_all_options(class_name) }} diff --git a/sdk/python/packages/flet/docs/audio_recorder/types/android_audio_source.md b/sdk/python/packages/flet/docs/audio_recorder/types/android_audio_source.md new file mode 100644 index 0000000000..f582fad5f6 --- /dev/null +++ b/sdk/python/packages/flet/docs/audio_recorder/types/android_audio_source.md @@ -0,0 +1 @@ +{{ class_all_options("flet_audio_recorder.AndroidAudioSource", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/audio_recorder/types/android_recorder_configuration.md b/sdk/python/packages/flet/docs/audio_recorder/types/android_recorder_configuration.md new file mode 100644 index 0000000000..e5047f7259 --- /dev/null +++ b/sdk/python/packages/flet/docs/audio_recorder/types/android_recorder_configuration.md @@ -0,0 +1 @@ +{{ class_all_options("flet_audio_recorder.AndroidRecorderConfiguration") }} diff --git a/sdk/python/packages/flet/docs/audio_recorder/types/audio_encoder.md b/sdk/python/packages/flet/docs/audio_recorder/types/audio_encoder.md new file mode 100644 index 0000000000..6d178e335b --- /dev/null +++ b/sdk/python/packages/flet/docs/audio_recorder/types/audio_encoder.md @@ -0,0 +1 @@ +{{ class_all_options("flet_audio_recorder.AudioEncoder", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/audio_recorder/types/audio_recorder_configuration.md b/sdk/python/packages/flet/docs/audio_recorder/types/audio_recorder_configuration.md new file mode 100644 index 0000000000..21dd1f1543 --- /dev/null +++ b/sdk/python/packages/flet/docs/audio_recorder/types/audio_recorder_configuration.md @@ -0,0 +1 @@ +{{ class_all_options("flet_audio_recorder.AudioRecorderConfiguration") }} diff --git a/sdk/python/packages/flet/docs/audio_recorder/types/audio_recorder_state.md b/sdk/python/packages/flet/docs/audio_recorder/types/audio_recorder_state.md new file mode 100644 index 0000000000..24411e1576 --- /dev/null +++ b/sdk/python/packages/flet/docs/audio_recorder/types/audio_recorder_state.md @@ -0,0 +1 @@ +{{ class_all_options("flet_audio_recorder.AudioRecorderState", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/audio_recorder/types/audio_recorder_state_change_event.md b/sdk/python/packages/flet/docs/audio_recorder/types/audio_recorder_state_change_event.md new file mode 100644 index 0000000000..de0b9ebc3f --- /dev/null +++ b/sdk/python/packages/flet/docs/audio_recorder/types/audio_recorder_state_change_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_audio_recorder.AudioRecorderStateChangeEvent") }} diff --git a/sdk/python/packages/flet/docs/audio_recorder/types/input_device.md b/sdk/python/packages/flet/docs/audio_recorder/types/input_device.md new file mode 100644 index 0000000000..470d5fb4f5 --- /dev/null +++ b/sdk/python/packages/flet/docs/audio_recorder/types/input_device.md @@ -0,0 +1 @@ +{{ class_all_options("flet_audio_recorder.InputDevice") }} diff --git a/sdk/python/packages/flet/docs/audio_recorder/types/ios_audio_category_option.md b/sdk/python/packages/flet/docs/audio_recorder/types/ios_audio_category_option.md new file mode 100644 index 0000000000..9db51dbe72 --- /dev/null +++ b/sdk/python/packages/flet/docs/audio_recorder/types/ios_audio_category_option.md @@ -0,0 +1 @@ +{{ class_all_options("flet_audio_recorder.IosAudioCategoryOption", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/audio_recorder/types/ios_recorder_configuration.md b/sdk/python/packages/flet/docs/audio_recorder/types/ios_recorder_configuration.md new file mode 100644 index 0000000000..6068fc92a6 --- /dev/null +++ b/sdk/python/packages/flet/docs/audio_recorder/types/ios_recorder_configuration.md @@ -0,0 +1 @@ +{{ class_all_options("flet_audio_recorder.IosRecorderConfiguration") }} diff --git a/sdk/python/packages/flet/docs/charts/assets/bar-chart-diagram.svg b/sdk/python/packages/flet/docs/charts/assets/bar-chart-diagram.svg new file mode 100644 index 0000000000..36c9fa4e73 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/assets/bar-chart-diagram.svg @@ -0,0 +1,614 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Left Axis +
+
+
+
+ + Left Axis + +
+
+ + + + + +
+
+
+ Top Axis +
+
+
+
+ + Top Axis + +
+
+ + + + + +
+
+
+ Right Axis +
+
+
+
+ + Right Axis + +
+
+ + + +
+
+
+ Bottom Axis +
+
+
+
+ + Bottom Axis + +
+
+ + + + + + +
+
+
+ Labels +
+
+
+
+ + Labels + +
+
+ + + + + + +
+
+
+ Title +
+
+
+
+ + Title + +
+
+ + + + + + + + + +
+
+
+ 10 +
+
+
+
+ + 10 + +
+
+ + + +
+
+
+ 20 +
+
+
+
+ + 20 + +
+
+ + + +
+
+
+ 30 +
+
+
+
+ 30 + +
+
+ + + +
+
+
+ Left Axis Title +
+
+
+
+ Left Axis Title + +
+
+ + + +
+
+
+ Group A +
+
+
+
+ Group + A + +
+
+ + + +
+
+
+ 0 +
+
+
+
+ 0 + +
+
+ + + +
+
+
+ Group B +
+
+
+
+ Group + B + +
+
+ + + +
+
+
+ Group C +
+
+
+
+ Group + C + +
+
+ + + +
+
+
+ Bottom Axis Title +
+
+
+
+ Bottom Axis Title + +
+
+ + + +
+
+
+ Border +
+
+
+
+ + Border + +
+
+ + + + + + + + +
+
+
+ Grid lines +
+
+
+
+ Grid + lines + +
+
+ + + +
+
+
+ Vertical +
+
+
+
+ + Vertical + +
+
+ + + +
+
+
+ Horizontal +
+
+
+
+ + Horizontal + +
+
+ + + + + + + + + +
+
+
+ BarChartGroup +
+
+
+
+ + BarChartGr... + +
+
+ + + + + + + + + +
+
+
+ BarChartRodStackItem +
+
+
+
+ + BarChartRo... + +
+
+ + + + + + + +
+
+
+ BarChartRod +
+
+
+
+ + BarChartRod + +
+
+ + + + + + + + + + + +
+
+
+ BarChartGroup +
+
+
+
+ + BarChartGr... + +
+
+ + + + + +
+
+
+ group_vertically +
+
+
+
+ + group_vertically + +
+
+ + + +
+
+
+ Text +
+
+
+
+ + Text + +
+
+
+ + + + Text is not SVG - cannot display + + +
diff --git a/sdk/python/packages/flet/docs/charts/assets/line-chart-diagram.svg b/sdk/python/packages/flet/docs/charts/assets/line-chart-diagram.svg new file mode 100644 index 0000000000..968087bcc9 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/assets/line-chart-diagram.svg @@ -0,0 +1,609 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Left Axis +
+
+
+
+ + Left Axis + +
+
+ + + + + +
+
+
+ Top Axis +
+
+
+
+ + Top Axis + +
+
+ + + + + +
+
+
+ Right Axis +
+
+
+
+ + Right Axis + +
+
+ + + +
+
+
+ Bottom Axis +
+
+
+
+ + Bottom Axis + +
+
+ + + + + + +
+
+
+ Labels +
+
+
+
+ + Labels + +
+
+ + + + + + +
+
+
+ Title +
+
+
+
+ + Title + +
+
+ + + + + + + + + +
+
+
+ Cold +
+
+
+
+ + Cold + +
+
+ + + +
+
+
+ Warm +
+
+
+
+ + Warm + +
+
+ + + +
+
+
+ Hot +
+
+
+
+ Hot + +
+
+ + + +
+
+
+ Temp +
+
+
+
+ Temp + +
+
+ + + +
+
+
+ 1 +
+
+
+
+ 1 + +
+
+ + + +
+
+
+ 0 +
+
+
+
+ 0 + +
+
+ + + +
+
+
+ 2 +
+
+
+
+ 2 + +
+
+ + + +
+
+
+ 3 +
+
+
+
+ 3 + +
+
+ + + +
+
+
+ 4 +
+
+
+
+ 4 + +
+
+ + + +
+
+
+ Time +
+
+
+
+ Time + +
+
+ + + +
+
+
+ Border +
+
+
+
+ + Border + +
+
+ + + + + + + + + + +
+
+
+ Points +
+
+
+
+ + Points + +
+
+ + + + + + + + + + + + + + +
+
+
+ Grid lines +
+
+
+
+ Grid + lines + +
+
+ + + +
+
+
+ Vertical +
+
+
+
+ + Vertical + +
+
+ + + + +
+
+
+ Horizontal +
+
+
+
+ + Horizontal + +
+
+ + + + + + + +
+
+
+ Data series (LineChartData) +
+
+
+
+ Data + series (LineChartData) + +
+
+ + + + + + + +
+
+
+ curved +
+
+
+
+ curved + +
+
+ + + +
+
+
+ below line +
+
+
+
+ below + line + +
+
+ + + +
+
+
+ above line +
+
+
+
+ above + line + +
+
+
+ + + + Text is not SVG - cannot display + + +
diff --git a/sdk/python/packages/flet/docs/charts/assets/pie-chart-diagram.svg b/sdk/python/packages/flet/docs/charts/assets/pie-chart-diagram.svg new file mode 100644 index 0000000000..648302f6f4 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/assets/pie-chart-diagram.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + +
+
+
+ Badge +
+
+
+
+ Badge + +
+
+ + + + + + +
+
+
+ PieChartSection +
+
+
+
+ + PieChartSe... + +
+
+ + + + + + + + +
+
+
+ Center space +
+
+
+
+ + Center spa... + +
+
+ + + + + + + + + +
+
+
+ Radius +
+
+
+
+ + Radius + +
+
+ + +
+ + + + Text is not SVG - cannot display + + +
diff --git a/sdk/python/packages/flet/docs/charts/bar_chart.md b/sdk/python/packages/flet/docs/charts/bar_chart.md new file mode 100644 index 0000000000..ebc71360cd --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/bar_chart.md @@ -0,0 +1,28 @@ +--- +class_name: flet_charts.bar_chart.BarChart +examples: ../../examples/controls/charts/bar_chart +example_images: ../examples/controls/charts/bar_chart/media +diagram: assets/bar-chart-diagram.svg +--- + +{{ class_summary(class_name, image_url=diagram, image_width="100%") }} + +## Examples + +### Example 1 + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +{{ image(example_images + "/example_1.png", width="80%") }} + +### Example 2 + +```python +--8<-- "{{ examples }}/example_2.py" +``` + +{{ image(example_images + "/example_2.gif", width="80%") }} + +{{ class_members(class_name) }} diff --git a/sdk/python/packages/flet/docs/charts/candlestick_chart.md b/sdk/python/packages/flet/docs/charts/candlestick_chart.md new file mode 100644 index 0000000000..0e0d7c3587 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/candlestick_chart.md @@ -0,0 +1,19 @@ +--- +class_name: flet_charts.candlestick_chart.CandlestickChart +examples: ../../examples/controls/charts/candlestick_chart +example_images: ../examples/controls/charts/candlestick_chart/media +--- + +{{ class_summary(class_name) }} + +## Examples + +### Basic Candlestick Chart + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +{{ image(example_images + "/example_1.png", width="80%") }} + +{{ class_members(class_name) }} diff --git a/sdk/python/packages/flet/docs/charts/index.md b/sdk/python/packages/flet/docs/charts/index.md new file mode 100644 index 0000000000..14cb56ba43 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/index.md @@ -0,0 +1,48 @@ +--- +examples: ../../examples/controls/charts +--- + +# Charts + +Interactive chart controls powered by [flet-charts](https://pypi.org/project/flet-charts/) let you display data as bar, line, pie, scatter and plotly visualisations directly in your Flet apps. + +It is built on top of the [fl_chart](https://pub.dev/packages/fl_chart) Flutter package and ships with helper classes for axis labels, tooltips and more. + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | + +## Usage + +Add `flet-charts` to your project dependencies: + +/// tab | uv +```bash +uv add flet-charts +``` + +/// +/// tab | pip +```bash +pip install flet-charts # (1)! +``` + +1. After this, you will have to manually add this package to your `requirements.txt` or `pyproject.toml`. +/// + +> [!TIP] +> All chart services are regular controlsβ€”simply instantiate them and add to the page or to a layout container. + +## Available Charts + +- [`BarChart`](bar_chart.md) +- [`CandlestickChart`](candlestick_chart.md) +- [`LineChart`](line_chart.md) +- [`MatplotlibChart`](matplotlib_chart.md) +- [`PieChart`](pie_chart.md) +- [`PlotlyChart`](plotly_chart.md) +- [`ScatterChart`](scatter_chart.md) + +Each chart page provides ready-to-run examples from `{{ examples }}`. diff --git a/sdk/python/packages/flet/docs/charts/line_chart.md b/sdk/python/packages/flet/docs/charts/line_chart.md new file mode 100644 index 0000000000..639813ff0e --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/line_chart.md @@ -0,0 +1,28 @@ +--- +class_name: flet_charts.line_chart.LineChart +examples: ../../examples/controls/charts/line_chart +example_images: ../examples/controls/charts/line_chart/media +diagram: assets/line-chart-diagram.svg +--- + +{{ class_summary(class_name, image_url=diagram, image_width="100%") }} + +## Examples + +### Example 1 + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +{{ image(example_images + "/example_1.gif", width="80%") }} + +### Example 2 + +```python +--8<-- "{{ examples }}/example_2.py" +``` + +{{ image(example_images + "/example_2.gif", width="80%") }} + +{{ class_members(class_name) }} diff --git a/sdk/python/packages/flet/docs/charts/matplotlib_chart.md b/sdk/python/packages/flet/docs/charts/matplotlib_chart.md new file mode 100644 index 0000000000..9801caa7f5 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/matplotlib_chart.md @@ -0,0 +1,31 @@ +--- +class_name: flet_charts.matplotlib_chart.MatplotlibChart +examples: ../../examples/controls/charts/matplotlib_chart +example_images: ../examples/controls/charts/matplotlib_chart/media +--- + +{{ class_summary(class_name) }} + +## Examples + +### Example 1 + +Based on an official [Matplotlib example](https://matplotlib.org/stable/gallery/lines_bars_and_markers/bar_colors.html#sphx-glr-gallery-lines-bars-and-markers-bar-colors-py). + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +{{ image(example_images + "/example_1.png", width="80%") }} + +### Example 2 + +Based on an official [Matplotlib example](https://matplotlib.org/stable/gallery/lines_bars_and_markers/cohere.html#sphx-glr-gallery-lines-bars-and-markers-cohere-py). + +```python +--8<-- "{{ examples }}/example_2.py" +``` + +{{ image(example_images + "/example_2.png", width="80%") }} + +{{ class_members(class_name) }} diff --git a/sdk/python/packages/flet/docs/charts/pie_chart.md b/sdk/python/packages/flet/docs/charts/pie_chart.md new file mode 100644 index 0000000000..2f05455289 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/pie_chart.md @@ -0,0 +1,36 @@ +--- +class_name: flet_charts.pie_chart.PieChart +examples: ../../examples/controls/charts/pie_chart +example_images: ../examples/controls/charts/pie_chart/media +diagram: assets/pie-chart-diagram.svg +--- + +{{ class_summary(class_name, image_url=diagram, image_width="100%") }} + +## Examples + +### Example 1 + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +{{ image(example_images + "/example_1.gif", width="80%") }} + +### Example 2 + +```python +--8<-- "{{ examples }}/example_2.py" +``` + +{{ image(example_images + "/example_2.gif", width="80%") }} + +### Example 3 + +```python +--8<-- "{{ examples }}/example_3.py" +``` + +{{ image(example_images + "/example_3.gif", width="80%") }} + +{{ class_members(class_name) }} diff --git a/sdk/python/packages/flet/docs/charts/plotly_chart.md b/sdk/python/packages/flet/docs/charts/plotly_chart.md new file mode 100644 index 0000000000..0a9b983396 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/plotly_chart.md @@ -0,0 +1,51 @@ +--- +class_name: flet_charts.plotly_chart.PlotlyChart +examples: ../../examples/controls/charts/plotly_chart +example_images: ../examples/controls/charts/plotly_chart/media +--- + +{{ class_summary(class_name) }} + +## Examples + +### Example 1 + +Based on an official [Plotly example](https://plotly.com/python/line-charts). + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +{{ image(example_images + "/example_1.png", width="80%") }} + +### Example 2 + +Based on an official [Plotly example](https://plotly.com/python/bar-charts). + +```python +--8<-- "{{ examples }}/example_2.py" +``` + +{{ image(example_images + "/example_2.png", width="80%") }} + +### Example 3 + +Based on an official [Plotly example](https://plotly.com/python/pie-charts). + +```python +--8<-- "{{ examples }}/example_3.py" +``` + +{{ image(example_images + "/example_3.png", width="80%") }} + +### Example 4 + +Based on an official [Plotly example](https://plotly.com/python/box-plots). + +```python +--8<-- "{{ examples }}/example_4.py" +``` + +{{ image(example_images + "/example_4.png", width="80%") }} + +{{ class_members(class_name) }} diff --git a/sdk/python/packages/flet/docs/charts/scatter_chart.md b/sdk/python/packages/flet/docs/charts/scatter_chart.md new file mode 100644 index 0000000000..c9d9dac678 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/scatter_chart.md @@ -0,0 +1,19 @@ +--- +class_name: flet_charts.scatter_chart.ScatterChart +examples: ../../examples/controls/charts/scatter_chart +example_images: ../examples/controls/charts/scatter_chart/media +--- + +{{ class_summary(class_name) }} + +## Examples + +### Basic Scatter Chart + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +{{ image(example_images + "/example_1.png", width="80%") }} + +{{ class_members(class_name) }} diff --git a/sdk/python/packages/flet/docs/charts/types/bar_chart_event.md b/sdk/python/packages/flet/docs/charts/types/bar_chart_event.md new file mode 100644 index 0000000000..e6f081a436 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/bar_chart_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.bar_chart.BarChartEvent") }} diff --git a/sdk/python/packages/flet/docs/charts/types/bar_chart_group.md b/sdk/python/packages/flet/docs/charts/types/bar_chart_group.md new file mode 100644 index 0000000000..d3c319c04b --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/bar_chart_group.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.bar_chart_group.BarChartGroup") }} diff --git a/sdk/python/packages/flet/docs/charts/types/bar_chart_rod.md b/sdk/python/packages/flet/docs/charts/types/bar_chart_rod.md new file mode 100644 index 0000000000..436c794814 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/bar_chart_rod.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.bar_chart_rod.BarChartRod") }} diff --git a/sdk/python/packages/flet/docs/charts/types/bar_chart_rod_stack_item.md b/sdk/python/packages/flet/docs/charts/types/bar_chart_rod_stack_item.md new file mode 100644 index 0000000000..35dba67b2d --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/bar_chart_rod_stack_item.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.bar_chart_rod_stack_item.BarChartRodStackItem") }} diff --git a/sdk/python/packages/flet/docs/charts/types/bar_chart_rod_tooltip.md b/sdk/python/packages/flet/docs/charts/types/bar_chart_rod_tooltip.md new file mode 100644 index 0000000000..950b5869e5 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/bar_chart_rod_tooltip.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.bar_chart_rod.BarChartRodTooltip") }} diff --git a/sdk/python/packages/flet/docs/charts/types/bar_chart_tooltip.md b/sdk/python/packages/flet/docs/charts/types/bar_chart_tooltip.md new file mode 100644 index 0000000000..1f2684febf --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/bar_chart_tooltip.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.bar_chart.BarChartTooltip") }} diff --git a/sdk/python/packages/flet/docs/charts/types/bar_chart_tooltip_direction.md b/sdk/python/packages/flet/docs/charts/types/bar_chart_tooltip_direction.md new file mode 100644 index 0000000000..0ec4c8555d --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/bar_chart_tooltip_direction.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.bar_chart.BarChartTooltipDirection", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/charts/types/candlestick_chart_event.md b/sdk/python/packages/flet/docs/charts/types/candlestick_chart_event.md new file mode 100644 index 0000000000..c0e06a4778 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/candlestick_chart_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.candlestick_chart.CandlestickChartEvent") }} diff --git a/sdk/python/packages/flet/docs/charts/types/candlestick_chart_spot.md b/sdk/python/packages/flet/docs/charts/types/candlestick_chart_spot.md new file mode 100644 index 0000000000..b9c9725906 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/candlestick_chart_spot.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.candlestick_chart_spot.CandlestickChartSpot") }} diff --git a/sdk/python/packages/flet/docs/charts/types/candlestick_chart_spot_tooltip.md b/sdk/python/packages/flet/docs/charts/types/candlestick_chart_spot_tooltip.md new file mode 100644 index 0000000000..90498462dc --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/candlestick_chart_spot_tooltip.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.candlestick_chart_spot.CandlestickChartSpotTooltip") }} diff --git a/sdk/python/packages/flet/docs/charts/types/candlestick_chart_tooltip.md b/sdk/python/packages/flet/docs/charts/types/candlestick_chart_tooltip.md new file mode 100644 index 0000000000..eb12f54b95 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/candlestick_chart_tooltip.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.candlestick_chart.CandlestickChartTooltip") }} diff --git a/sdk/python/packages/flet/docs/charts/types/chart_axis.md b/sdk/python/packages/flet/docs/charts/types/chart_axis.md new file mode 100644 index 0000000000..1384497e8d --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/chart_axis.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.chart_axis.ChartAxis") }} diff --git a/sdk/python/packages/flet/docs/charts/types/chart_axis_label.md b/sdk/python/packages/flet/docs/charts/types/chart_axis_label.md new file mode 100644 index 0000000000..271a969d03 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/chart_axis_label.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.chart_axis.ChartAxisLabel") }} diff --git a/sdk/python/packages/flet/docs/charts/types/chart_circle_point.md b/sdk/python/packages/flet/docs/charts/types/chart_circle_point.md new file mode 100644 index 0000000000..6ece9124f4 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/chart_circle_point.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.types.ChartCirclePoint") }} diff --git a/sdk/python/packages/flet/docs/charts/types/chart_cross_point.md b/sdk/python/packages/flet/docs/charts/types/chart_cross_point.md new file mode 100644 index 0000000000..fc1a6a2d7d --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/chart_cross_point.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.types.ChartCrossPoint") }} diff --git a/sdk/python/packages/flet/docs/charts/types/chart_data_point_tooltip.md b/sdk/python/packages/flet/docs/charts/types/chart_data_point_tooltip.md new file mode 100644 index 0000000000..229a18bca3 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/chart_data_point_tooltip.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.types.ChartDataPointTooltip") }} diff --git a/sdk/python/packages/flet/docs/charts/types/chart_event_type.md b/sdk/python/packages/flet/docs/charts/types/chart_event_type.md new file mode 100644 index 0000000000..38402e84f6 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/chart_event_type.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.types.ChartEventType", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/charts/types/chart_grid_lines.md b/sdk/python/packages/flet/docs/charts/types/chart_grid_lines.md new file mode 100644 index 0000000000..3a6b174205 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/chart_grid_lines.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.types.ChartGridLines") }} diff --git a/sdk/python/packages/flet/docs/charts/types/chart_point_line.md b/sdk/python/packages/flet/docs/charts/types/chart_point_line.md new file mode 100644 index 0000000000..a2f44e74ff --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/chart_point_line.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.types.ChartPointLine") }} diff --git a/sdk/python/packages/flet/docs/charts/types/chart_point_shape.md b/sdk/python/packages/flet/docs/charts/types/chart_point_shape.md new file mode 100644 index 0000000000..4948e89ec2 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/chart_point_shape.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.types.ChartPointShape") }} diff --git a/sdk/python/packages/flet/docs/charts/types/chart_square_point.md b/sdk/python/packages/flet/docs/charts/types/chart_square_point.md new file mode 100644 index 0000000000..b1f72ea2f5 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/chart_square_point.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.types.ChartSquarePoint") }} diff --git a/sdk/python/packages/flet/docs/charts/types/horizontal_alignment.md b/sdk/python/packages/flet/docs/charts/types/horizontal_alignment.md new file mode 100644 index 0000000000..23ac51771c --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/horizontal_alignment.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.types.HorizontalAlignment", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/charts/types/line_chart_data.md b/sdk/python/packages/flet/docs/charts/types/line_chart_data.md new file mode 100644 index 0000000000..4cedac9ad8 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/line_chart_data.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.line_chart_data.LineChartData") }} diff --git a/sdk/python/packages/flet/docs/charts/types/line_chart_data_point.md b/sdk/python/packages/flet/docs/charts/types/line_chart_data_point.md new file mode 100644 index 0000000000..b32a5784c4 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/line_chart_data_point.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.line_chart_data_point.LineChartDataPoint") }} diff --git a/sdk/python/packages/flet/docs/charts/types/line_chart_data_point_tooltip.md b/sdk/python/packages/flet/docs/charts/types/line_chart_data_point_tooltip.md new file mode 100644 index 0000000000..b6c41ac0e0 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/line_chart_data_point_tooltip.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.line_chart_data_point.LineChartDataPointTooltip") }} diff --git a/sdk/python/packages/flet/docs/charts/types/line_chart_event.md b/sdk/python/packages/flet/docs/charts/types/line_chart_event.md new file mode 100644 index 0000000000..0d9afc89f8 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/line_chart_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.line_chart.LineChartEvent") }} diff --git a/sdk/python/packages/flet/docs/charts/types/line_chart_event_spot.md b/sdk/python/packages/flet/docs/charts/types/line_chart_event_spot.md new file mode 100644 index 0000000000..ee43470d85 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/line_chart_event_spot.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.line_chart.LineChartEventSpot") }} diff --git a/sdk/python/packages/flet/docs/charts/types/line_chart_tooltip.md b/sdk/python/packages/flet/docs/charts/types/line_chart_tooltip.md new file mode 100644 index 0000000000..d295182ba1 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/line_chart_tooltip.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.line_chart.LineChartTooltip") }} diff --git a/sdk/python/packages/flet/docs/charts/types/pie_chart_event.md b/sdk/python/packages/flet/docs/charts/types/pie_chart_event.md new file mode 100644 index 0000000000..c5f963b3c2 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/pie_chart_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.pie_chart.PieChartEvent") }} diff --git a/sdk/python/packages/flet/docs/charts/types/pie_chart_section.md b/sdk/python/packages/flet/docs/charts/types/pie_chart_section.md new file mode 100644 index 0000000000..d9155ef07e --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/pie_chart_section.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.pie_chart_section.PieChartSection") }} diff --git a/sdk/python/packages/flet/docs/charts/types/scatter_chart_event.md b/sdk/python/packages/flet/docs/charts/types/scatter_chart_event.md new file mode 100644 index 0000000000..1729bfc0a3 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/scatter_chart_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.scatter_chart.ScatterChartEvent") }} diff --git a/sdk/python/packages/flet/docs/charts/types/scatter_chart_spot.md b/sdk/python/packages/flet/docs/charts/types/scatter_chart_spot.md new file mode 100644 index 0000000000..0cb85edd56 --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/scatter_chart_spot.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.scatter_chart_spot.ScatterChartSpot") }} diff --git a/sdk/python/packages/flet/docs/charts/types/scatter_chart_spot_tooltip.md b/sdk/python/packages/flet/docs/charts/types/scatter_chart_spot_tooltip.md new file mode 100644 index 0000000000..9b7d839dbf --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/scatter_chart_spot_tooltip.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.scatter_chart_spot.ScatterChartSpotTooltip") }} diff --git a/sdk/python/packages/flet/docs/charts/types/scatter_chart_tooltip.md b/sdk/python/packages/flet/docs/charts/types/scatter_chart_tooltip.md new file mode 100644 index 0000000000..022751a36e --- /dev/null +++ b/sdk/python/packages/flet/docs/charts/types/scatter_chart_tooltip.md @@ -0,0 +1 @@ +{{ class_all_options("flet_charts.scatter_chart.ScatterChartTooltip") }} diff --git a/sdk/python/packages/flet/docs/datatable2/datacolumn2.md b/sdk/python/packages/flet/docs/datatable2/datacolumn2.md new file mode 100644 index 0000000000..62c373dfe6 --- /dev/null +++ b/sdk/python/packages/flet/docs/datatable2/datacolumn2.md @@ -0,0 +1 @@ +{{ class_all_options("flet_datatable2.DataColumn2") }} diff --git a/sdk/python/packages/flet/docs/datatable2/datarow2.md b/sdk/python/packages/flet/docs/datatable2/datarow2.md new file mode 100644 index 0000000000..af7a95ceed --- /dev/null +++ b/sdk/python/packages/flet/docs/datatable2/datarow2.md @@ -0,0 +1 @@ +{{ class_all_options("flet_datatable2.DataRow2") }} diff --git a/sdk/python/packages/flet/docs/datatable2/index.md b/sdk/python/packages/flet/docs/datatable2/index.md new file mode 100644 index 0000000000..655d574e9a --- /dev/null +++ b/sdk/python/packages/flet/docs/datatable2/index.md @@ -0,0 +1,49 @@ +--- +examples: ../../examples/controls/datatable2 +--- + +# DataTable2 + +Enhanced data table for [Flet](https://flet.dev) that adds sticky headers, fixed rows/columns, and other UX improvements via the `flet-datatable2` extension. + +It wraps the Flutter [`data_table_2`](https://pub.dev/packages/data_table_2) package. + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | + +## Usage + +Add `flet-datatable2` to your project dependencies: + +/// tab | uv +```bash +uv add flet-datatable2 +``` + +/// +/// tab | pip +```bash +pip install flet-datatable2 # (1)! +``` + +1. After this, you will have to manually add this package to your `requirements.txt` or `pyproject.toml`. +/// + +## Example + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +```python +--8<-- "{{ examples }}/example_2.py" +``` + +![DataTable2 example]({{ examples }}/media/example_2.gif) + +## Description + +{{ class_all_options("flet_datatable2.DataTable2") }} diff --git a/sdk/python/packages/flet/docs/datatable2/types/data_column_size.md b/sdk/python/packages/flet/docs/datatable2/types/data_column_size.md new file mode 100644 index 0000000000..ac7063780e --- /dev/null +++ b/sdk/python/packages/flet/docs/datatable2/types/data_column_size.md @@ -0,0 +1 @@ +{{ class_all_options("flet_datatable2.DataColumnSize", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/extras/macros/__init__.py b/sdk/python/packages/flet/docs/extras/macros/__init__.py index 1ba4229ce2..76225c60a4 100644 --- a/sdk/python/packages/flet/docs/extras/macros/__init__.py +++ b/sdk/python/packages/flet/docs/extras/macros/__init__.py @@ -1,3 +1,7 @@ +import os +from urllib.parse import urlparse + + def define_env(env): def format_value(value): if isinstance(value, bool): @@ -48,7 +52,14 @@ def class_all_options( @env.macro def image(src, alt=None, width=None, caption=None, link=None): - alt_text = alt or "" + if alt is None: + parsed_src = urlparse(src) + path = parsed_src.path or src + filename = os.path.basename(path.rstrip("/")) + alt_text = filename or src + else: + alt_text = alt + alt_text = str(alt_text) body = f"![{alt_text}]({src})" if width: body += f'{{width="{width}"}}' diff --git a/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_disable_exception.md b/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_disable_exception.md new file mode 100644 index 0000000000..ef132ee650 --- /dev/null +++ b/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_disable_exception.md @@ -0,0 +1 @@ +{{ class_all_options("flet_flashlight.FlashlightDisableException") }} diff --git a/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_disable_existent_user_exception.md b/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_disable_existent_user_exception.md new file mode 100644 index 0000000000..499f7a6d6d --- /dev/null +++ b/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_disable_existent_user_exception.md @@ -0,0 +1 @@ +{{ class_all_options("flet_flashlight.FlashlightDisableExistentUserException") }} diff --git a/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_disable_not_available_exception.md b/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_disable_not_available_exception.md new file mode 100644 index 0000000000..6350d09f63 --- /dev/null +++ b/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_disable_not_available_exception.md @@ -0,0 +1 @@ +{{ class_all_options("flet_flashlight.FlashlightDisableNotAvailableException") }} diff --git a/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_enable_exception.md b/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_enable_exception.md new file mode 100644 index 0000000000..07e62e7c06 --- /dev/null +++ b/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_enable_exception.md @@ -0,0 +1 @@ +{{ class_all_options("flet_flashlight.FlashlightEnableException") }} diff --git a/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_enable_existent_user_exception.md b/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_enable_existent_user_exception.md new file mode 100644 index 0000000000..7dd830b491 --- /dev/null +++ b/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_enable_existent_user_exception.md @@ -0,0 +1 @@ +{{ class_all_options("flet_flashlight.FlashlightEnableExistentUserException") }} diff --git a/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_enable_not_available_exception.md b/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_enable_not_available_exception.md new file mode 100644 index 0000000000..c046f91c56 --- /dev/null +++ b/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_enable_not_available_exception.md @@ -0,0 +1 @@ +{{ class_all_options("flet_flashlight.FlashlightEnableNotAvailableException") }} diff --git a/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_exception.md b/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_exception.md new file mode 100644 index 0000000000..4d6baef2b8 --- /dev/null +++ b/sdk/python/packages/flet/docs/flashlight/exceptions/flashlight_exception.md @@ -0,0 +1 @@ +{{ class_all_options("flet_flashlight.FlashlightException") }} diff --git a/sdk/python/packages/flet/docs/flashlight/index.md b/sdk/python/packages/flet/docs/flashlight/index.md new file mode 100644 index 0000000000..1d67f516ae --- /dev/null +++ b/sdk/python/packages/flet/docs/flashlight/index.md @@ -0,0 +1,47 @@ +--- +class_name: flet_flashlight.Flashlight +examples: ../../examples/controls/flashlight +--- + +# Flashlight + +Control the device torch/flashlight in your [Flet](https://flet.dev) app via the `flet-flashlight` extension, built on top of Flutter's [`flashlight`](https://pub.dev/packages/flashlight) package. + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| ❌ | ❌ | ❌ | βœ… | βœ… | ❌ | + +## Usage + +Add `flet-flashlight` to your project dependencies: + +/// tab | uv +```bash +uv add flet-flashlight +``` + +/// +/// tab | pip +```bash +pip install flet-flashlight # (1)! +``` + +1. After this, you will have to manually add this package to your `requirements.txt` or `pyproject.toml`. +/// + +## Example + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +## Description + +{{ class_all_options(class_name) }} + +See also: +- [`FlashlightException`](exceptions/flashlight_exception.md) +- [`FlashlightEnableException`](exceptions/flashlight_enable_exception.md) +- [`FlashlightDisableException`](exceptions/flashlight_disable_exception.md) diff --git a/sdk/python/packages/flet/docs/geolocator/index.md b/sdk/python/packages/flet/docs/geolocator/index.md new file mode 100644 index 0000000000..6e43890908 --- /dev/null +++ b/sdk/python/packages/flet/docs/geolocator/index.md @@ -0,0 +1,47 @@ +--- +class_name: flet_geolocator.Geolocator +examples: ../../examples/controls/geolocator +--- + +# Geolocator + +Access device location services in your [Flet](https://flet.dev) app using the `flet-geolocator` extension. The control wraps Flutter's [`geolocator`](https://pub.dev/packages/geolocator) package and exposes async helpers for permission checks and position streams. + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | + +## Usage + +Add `flet-geolocator` to your project dependencies: + +/// tab | uv +```bash +uv add flet-geolocator +``` + +/// +/// tab | pip +```bash +pip install flet-geolocator # (1)! +``` + +1. After this, you will have to manually add this package to your `requirements.txt` or `pyproject.toml`. +/// + +/// admonition | Important + type: note +Request permissions with `request_permission` or `get_permission_status` before relying on location data. +/// + +## Example + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +## Description + +{{ class_all_options(class_name) }} diff --git a/sdk/python/packages/flet/docs/geolocator/types/foreground_notification_configuration.md b/sdk/python/packages/flet/docs/geolocator/types/foreground_notification_configuration.md new file mode 100644 index 0000000000..85af15d606 --- /dev/null +++ b/sdk/python/packages/flet/docs/geolocator/types/foreground_notification_configuration.md @@ -0,0 +1 @@ +{{ class_all_options("flet_geolocator.ForegroundNotificationConfiguration", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/geolocator/types/geolocator_android_configuration.md b/sdk/python/packages/flet/docs/geolocator/types/geolocator_android_configuration.md new file mode 100644 index 0000000000..8265ad41e8 --- /dev/null +++ b/sdk/python/packages/flet/docs/geolocator/types/geolocator_android_configuration.md @@ -0,0 +1 @@ +{{ class_all_options("flet_geolocator.GeolocatorAndroidConfiguration", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/geolocator/types/geolocator_configuration.md b/sdk/python/packages/flet/docs/geolocator/types/geolocator_configuration.md new file mode 100644 index 0000000000..99158b166b --- /dev/null +++ b/sdk/python/packages/flet/docs/geolocator/types/geolocator_configuration.md @@ -0,0 +1 @@ +{{ class_all_options("flet_geolocator.GeolocatorConfiguration", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/geolocator/types/geolocator_ios_activity_type.md b/sdk/python/packages/flet/docs/geolocator/types/geolocator_ios_activity_type.md new file mode 100644 index 0000000000..41ef1146a5 --- /dev/null +++ b/sdk/python/packages/flet/docs/geolocator/types/geolocator_ios_activity_type.md @@ -0,0 +1 @@ +{{ class_all_options("flet_geolocator.GeolocatorIosActivityType", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/geolocator/types/geolocator_ios_configuration.md b/sdk/python/packages/flet/docs/geolocator/types/geolocator_ios_configuration.md new file mode 100644 index 0000000000..3993457ae7 --- /dev/null +++ b/sdk/python/packages/flet/docs/geolocator/types/geolocator_ios_configuration.md @@ -0,0 +1 @@ +{{ class_all_options("flet_geolocator.GeolocatorIosConfiguration", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/geolocator/types/geolocator_permission_status.md b/sdk/python/packages/flet/docs/geolocator/types/geolocator_permission_status.md new file mode 100644 index 0000000000..7222fe1014 --- /dev/null +++ b/sdk/python/packages/flet/docs/geolocator/types/geolocator_permission_status.md @@ -0,0 +1 @@ +{{ class_all_options("flet_geolocator.GeolocatorPermissionStatus", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/geolocator/types/geolocator_position.md b/sdk/python/packages/flet/docs/geolocator/types/geolocator_position.md new file mode 100644 index 0000000000..96345214b0 --- /dev/null +++ b/sdk/python/packages/flet/docs/geolocator/types/geolocator_position.md @@ -0,0 +1 @@ +{{ class_all_options("flet_geolocator.GeolocatorPosition", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/geolocator/types/geolocator_position_accuracy.md b/sdk/python/packages/flet/docs/geolocator/types/geolocator_position_accuracy.md new file mode 100644 index 0000000000..99899f18b4 --- /dev/null +++ b/sdk/python/packages/flet/docs/geolocator/types/geolocator_position_accuracy.md @@ -0,0 +1 @@ +{{ class_all_options("flet_geolocator.GeolocatorPositionAccuracy", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/geolocator/types/geolocator_position_change_event.md b/sdk/python/packages/flet/docs/geolocator/types/geolocator_position_change_event.md new file mode 100644 index 0000000000..36c8852ac5 --- /dev/null +++ b/sdk/python/packages/flet/docs/geolocator/types/geolocator_position_change_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_geolocator.GeolocatorPositionChangeEvent", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/geolocator/types/geolocator_web_configuration.md b/sdk/python/packages/flet/docs/geolocator/types/geolocator_web_configuration.md new file mode 100644 index 0000000000..9b772c5d4d --- /dev/null +++ b/sdk/python/packages/flet/docs/geolocator/types/geolocator_web_configuration.md @@ -0,0 +1 @@ +{{ class_all_options("flet_geolocator.GeolocatorWebConfiguration", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/lottie/index.md b/sdk/python/packages/flet/docs/lottie/index.md new file mode 100644 index 0000000000..ef9ce1996d --- /dev/null +++ b/sdk/python/packages/flet/docs/lottie/index.md @@ -0,0 +1,47 @@ +--- +class_name: flet_lottie.Lottie +examples: ../../examples/controls/lottie +--- + +# Lottie + +Render rich [Lottie](https://airbnb.design/lottie/) animations inside your [Flet](https://flet.dev) apps with a simple control. + +It is backed by the [lottie](https://pub.dev/packages/lottie) Flutter package. + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | + +## Usage + +Add the `flet-lottie` package to your project dependencies: + +/// tab | uv +```bash +uv add flet-lottie +``` + +/// +/// tab | pip +```bash +pip install flet-lottie # (1)! +``` + +1. After this, you will have to manually add this package to your `requirements.txt` or `pyproject.toml`. +/// + +> [!TIP] +> `Lottie` is a visual controlβ€”place it wherever any other widget goes and configure playback via its properties. + +## Example + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +## Description + +{{ class_all_options(class_name) }} diff --git a/sdk/python/packages/flet/docs/map/circle_layer.md b/sdk/python/packages/flet/docs/map/circle_layer.md new file mode 100644 index 0000000000..ab1ddc9887 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/circle_layer.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.CircleLayer") }} diff --git a/sdk/python/packages/flet/docs/map/circle_marker.md b/sdk/python/packages/flet/docs/map/circle_marker.md new file mode 100644 index 0000000000..42a37c7b81 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/circle_marker.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.CircleMarker") }} diff --git a/sdk/python/packages/flet/docs/map/image_source_attribution.md b/sdk/python/packages/flet/docs/map/image_source_attribution.md new file mode 100644 index 0000000000..dc27ffc894 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/image_source_attribution.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.ImageSourceAttribution") }} diff --git a/sdk/python/packages/flet/docs/map/index.md b/sdk/python/packages/flet/docs/map/index.md new file mode 100644 index 0000000000..cb4b0333e0 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/index.md @@ -0,0 +1,48 @@ +--- +examples: ../../examples/controls/map +--- + +# Map + +Display interactive maps in your [Flet](https://flet.dev) apps with markers, overlays, and rich attributions provided by the `flet-map` extension. The control is built on top of [`flutter_map`](https://pub.dev/packages/flutter_map) and supports multiple tile providers and layers. + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | + +## Usage + +Add the `flet-map` package to your project dependencies: + +/// tab | uv +```bash +uv add flet-map +``` + +/// +/// tab | pip +```bash +pip install flet-map # (1)! +``` + +1. After this, you will have to manually add this package to your `requirements.txt` or `pyproject.toml`. +/// + +> Different tile providers have their own usage policies. Make sure you comply with their attribution and rate limits. + +## Example + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +## Reference + +- [`Map`](map.md) +- Layers: [`TileLayer`](tile_layer.md), [`MarkerLayer`](marker_layer.md), [`CircleLayer`](circle_layer.md), [`PolygonLayer`](polygon_layer.md), [`PolylineLayer`](polyline_layer.md) +- Markers and overlays: [`Marker`](marker.md), [`CircleMarker`](circle_marker.md), [`PolygonMarker`](polygon_marker.md), [`PolylineMarker`](polyline_marker.md) +- Attributions: [`SimpleAttribution`](simple_attribution.md), [`RichAttribution`](rich_attribution.md), [`SourceAttribution`](source_attribution.md) + +See the [types](types/attribution_alignment.md) section for additional configuration helpers. diff --git a/sdk/python/packages/flet/docs/map/map.md b/sdk/python/packages/flet/docs/map/map.md new file mode 100644 index 0000000000..3cc15074c6 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/map.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.Map") }} diff --git a/sdk/python/packages/flet/docs/map/map_layer.md b/sdk/python/packages/flet/docs/map/map_layer.md new file mode 100644 index 0000000000..d850640595 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/map_layer.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.MapLayer") }} diff --git a/sdk/python/packages/flet/docs/map/marker.md b/sdk/python/packages/flet/docs/map/marker.md new file mode 100644 index 0000000000..42a2ee29ab --- /dev/null +++ b/sdk/python/packages/flet/docs/map/marker.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.Marker") }} diff --git a/sdk/python/packages/flet/docs/map/marker_layer.md b/sdk/python/packages/flet/docs/map/marker_layer.md new file mode 100644 index 0000000000..4e41f80c34 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/marker_layer.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.MarkerLayer") }} diff --git a/sdk/python/packages/flet/docs/map/polygon_layer.md b/sdk/python/packages/flet/docs/map/polygon_layer.md new file mode 100644 index 0000000000..e3147ba249 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/polygon_layer.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.PolygonLayer") }} diff --git a/sdk/python/packages/flet/docs/map/polygon_marker.md b/sdk/python/packages/flet/docs/map/polygon_marker.md new file mode 100644 index 0000000000..b6b1e9c759 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/polygon_marker.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.PolygonMarker") }} diff --git a/sdk/python/packages/flet/docs/map/polyline_layer.md b/sdk/python/packages/flet/docs/map/polyline_layer.md new file mode 100644 index 0000000000..ff8a17e699 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/polyline_layer.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.PolylineLayer") }} diff --git a/sdk/python/packages/flet/docs/map/polyline_marker.md b/sdk/python/packages/flet/docs/map/polyline_marker.md new file mode 100644 index 0000000000..ebf8351aee --- /dev/null +++ b/sdk/python/packages/flet/docs/map/polyline_marker.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.PolylineMarker") }} diff --git a/sdk/python/packages/flet/docs/map/rich_attribution.md b/sdk/python/packages/flet/docs/map/rich_attribution.md new file mode 100644 index 0000000000..a8f57de0cc --- /dev/null +++ b/sdk/python/packages/flet/docs/map/rich_attribution.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.RichAttribution") }} diff --git a/sdk/python/packages/flet/docs/map/simple_attribution.md b/sdk/python/packages/flet/docs/map/simple_attribution.md new file mode 100644 index 0000000000..1194771a0f --- /dev/null +++ b/sdk/python/packages/flet/docs/map/simple_attribution.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.SimpleAttribution") }} diff --git a/sdk/python/packages/flet/docs/map/source_attribution.md b/sdk/python/packages/flet/docs/map/source_attribution.md new file mode 100644 index 0000000000..6090fdead1 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/source_attribution.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.SourceAttribution") }} diff --git a/sdk/python/packages/flet/docs/map/text_source_attribution.md b/sdk/python/packages/flet/docs/map/text_source_attribution.md new file mode 100644 index 0000000000..42e29ceac9 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/text_source_attribution.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.TextSourceAttribution") }} diff --git a/sdk/python/packages/flet/docs/map/tile_layer.md b/sdk/python/packages/flet/docs/map/tile_layer.md new file mode 100644 index 0000000000..58fd43e222 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/tile_layer.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.TileLayer") }} diff --git a/sdk/python/packages/flet/docs/map/types/attribution_alignment.md b/sdk/python/packages/flet/docs/map/types/attribution_alignment.md new file mode 100644 index 0000000000..4ee547a972 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/attribution_alignment.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.AttributionAlignment", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/map/types/camera.md b/sdk/python/packages/flet/docs/map/types/camera.md new file mode 100644 index 0000000000..3aab5e9dc5 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/camera.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.Camera", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/camera_fit.md b/sdk/python/packages/flet/docs/map/types/camera_fit.md new file mode 100644 index 0000000000..5f6ad698dc --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/camera_fit.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.CameraFit", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/cursor_keyboard_rotation_configuration.md b/sdk/python/packages/flet/docs/map/types/cursor_keyboard_rotation_configuration.md new file mode 100644 index 0000000000..fb04140092 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/cursor_keyboard_rotation_configuration.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.CursorKeyboardRotationConfiguration", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/cursor_rotation_behaviour.md b/sdk/python/packages/flet/docs/map/types/cursor_rotation_behaviour.md new file mode 100644 index 0000000000..eeb916100d --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/cursor_rotation_behaviour.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.CursorRotationBehaviour", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/map/types/dashed_stroke_pattern.md b/sdk/python/packages/flet/docs/map/types/dashed_stroke_pattern.md new file mode 100644 index 0000000000..dcbe55d61d --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/dashed_stroke_pattern.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.DashedStrokePattern", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/dotted_stroke_pattern.md b/sdk/python/packages/flet/docs/map/types/dotted_stroke_pattern.md new file mode 100644 index 0000000000..7b9d2e6d51 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/dotted_stroke_pattern.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.DottedStrokePattern", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/fade_in_tile_display.md b/sdk/python/packages/flet/docs/map/types/fade_in_tile_display.md new file mode 100644 index 0000000000..6eca71d24d --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/fade_in_tile_display.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.FadeInTileDisplay", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/instantaneous_tile_display.md b/sdk/python/packages/flet/docs/map/types/instantaneous_tile_display.md new file mode 100644 index 0000000000..8c5205d8a5 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/instantaneous_tile_display.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.InstantaneousTileDisplay", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/interaction_configuration.md b/sdk/python/packages/flet/docs/map/types/interaction_configuration.md new file mode 100644 index 0000000000..2070e70add --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/interaction_configuration.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.InteractionConfiguration", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/interaction_flag.md b/sdk/python/packages/flet/docs/map/types/interaction_flag.md new file mode 100644 index 0000000000..be40e5bd0c --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/interaction_flag.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.InteractionFlag", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/map/types/keyboard_configuration.md b/sdk/python/packages/flet/docs/map/types/keyboard_configuration.md new file mode 100644 index 0000000000..99ff8af178 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/keyboard_configuration.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.KeyboardConfiguration", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/map_event.md b/sdk/python/packages/flet/docs/map/types/map_event.md new file mode 100644 index 0000000000..713f83497e --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/map_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.MapEvent", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/map_event_source.md b/sdk/python/packages/flet/docs/map/types/map_event_source.md new file mode 100644 index 0000000000..0e3e35c0e4 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/map_event_source.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.MapEventSource", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/map/types/map_hover_event.md b/sdk/python/packages/flet/docs/map/types/map_hover_event.md new file mode 100644 index 0000000000..17a9865bbb --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/map_hover_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.MapHoverEvent", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/map_latitude_longitude.md b/sdk/python/packages/flet/docs/map/types/map_latitude_longitude.md new file mode 100644 index 0000000000..627ecbc4d0 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/map_latitude_longitude.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.MapLatitudeLongitude", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/map_latitude_longitude_bounds.md b/sdk/python/packages/flet/docs/map/types/map_latitude_longitude_bounds.md new file mode 100644 index 0000000000..358f0e97bd --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/map_latitude_longitude_bounds.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.MapLatitudeLongitudeBounds", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/map_pointer_event.md b/sdk/python/packages/flet/docs/map/types/map_pointer_event.md new file mode 100644 index 0000000000..9159f1df9a --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/map_pointer_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.MapPointerEvent", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/map_position_change_event.md b/sdk/python/packages/flet/docs/map/types/map_position_change_event.md new file mode 100644 index 0000000000..8d2db7c93a --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/map_position_change_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.MapPositionChangeEvent", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/map_tap_event.md b/sdk/python/packages/flet/docs/map/types/map_tap_event.md new file mode 100644 index 0000000000..5696923938 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/map_tap_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.MapTapEvent", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/multi_finger_gesture.md b/sdk/python/packages/flet/docs/map/types/multi_finger_gesture.md new file mode 100644 index 0000000000..bf9a063d69 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/multi_finger_gesture.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.MultiFingerGesture", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/map/types/pattern_fit.md b/sdk/python/packages/flet/docs/map/types/pattern_fit.md new file mode 100644 index 0000000000..4663071a26 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/pattern_fit.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.PatternFit", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/map/types/solid_stroke_pattern.md b/sdk/python/packages/flet/docs/map/types/solid_stroke_pattern.md new file mode 100644 index 0000000000..8ecb695f9f --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/solid_stroke_pattern.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.SolidStrokePattern", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/stroke_pattern.md b/sdk/python/packages/flet/docs/map/types/stroke_pattern.md new file mode 100644 index 0000000000..a426f900f9 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/stroke_pattern.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.StrokePattern", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/tile_display.md b/sdk/python/packages/flet/docs/map/types/tile_display.md new file mode 100644 index 0000000000..200f036b50 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/tile_display.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.TileDisplay", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/map/types/tile_layer_evict_error_tile_strategy.md b/sdk/python/packages/flet/docs/map/types/tile_layer_evict_error_tile_strategy.md new file mode 100644 index 0000000000..52e3463261 --- /dev/null +++ b/sdk/python/packages/flet/docs/map/types/tile_layer_evict_error_tile_strategy.md @@ -0,0 +1 @@ +{{ class_all_options("flet_map.TileLayerEvictErrorTileStrategy", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/permission_handler/index.md b/sdk/python/packages/flet/docs/permission_handler/index.md new file mode 100644 index 0000000000..c3c5d8b4c6 --- /dev/null +++ b/sdk/python/packages/flet/docs/permission_handler/index.md @@ -0,0 +1,47 @@ +--- +class_name: flet_permission_handler.PermissionHandler +examples: ../../examples/controls/permission_handler +--- + +# Permission Handler + +Manage runtime permissions in your [Flet](https://flet.dev) apps using the `flet-permission-handler` extension, powered by Flutter's [`permission_handler`](https://pub.dev/packages/permission_handler). + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | ❌ | ❌ | βœ… | βœ… | βœ… | + +## Usage + +Add `flet-permission-handler` to your project dependencies: + +/// tab | uv +```bash +uv add flet-permission-handler +``` + +/// +/// tab | pip +```bash +pip install flet-permission-handler # (1)! +``` + +1. After this, you will have to manually add this package to your `requirements.txt` or `pyproject.toml`. +/// + +/// admonition | Note + type: note +On mobile platforms you must also declare permissions in the native project files. See [Flet publish docs](https://flet.dev/docs/publish#permissions). +/// + +## Example + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +## Description + +{{ class_all_options(class_name) }} diff --git a/sdk/python/packages/flet/docs/permission_handler/types/__init__.py b/sdk/python/packages/flet/docs/permission_handler/types/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk/python/packages/flet/docs/permission_handler/types/permission.md b/sdk/python/packages/flet/docs/permission_handler/types/permission.md new file mode 100644 index 0000000000..e2b21ddab9 --- /dev/null +++ b/sdk/python/packages/flet/docs/permission_handler/types/permission.md @@ -0,0 +1 @@ +{{ class_all_options("flet_permission_handler.Permission", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/permission_handler/types/permission_status.md b/sdk/python/packages/flet/docs/permission_handler/types/permission_status.md new file mode 100644 index 0000000000..1f069e4dc9 --- /dev/null +++ b/sdk/python/packages/flet/docs/permission_handler/types/permission_status.md @@ -0,0 +1 @@ +{{ class_all_options("flet_permission_handler.PermissionStatus", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/rive/index.md b/sdk/python/packages/flet/docs/rive/index.md new file mode 100644 index 0000000000..f7f4f152a1 --- /dev/null +++ b/sdk/python/packages/flet/docs/rive/index.md @@ -0,0 +1,47 @@ +--- +class_name: flet_rive.Rive +examples: ../../examples/controls/rive +--- + +# Rive + +Render [Rive](https://rive.app/) animations in your [Flet](https://flet.dev) app with the `flet-rive` extension. + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | + +## Usage + +Add `flet-rive` to your project dependencies: + +/// tab | uv +```bash +uv add flet-rive +``` + +/// +/// tab | pip +```bash +pip install flet-rive # (1)! +``` + +1. After this, you will have to manually add this package to your `requirements.txt` or `pyproject.toml`. +/// + +/// admonition | Hosting Rive files + type: tip +Host `.riv` files locally or load them from a CDN. Use `placeholder` to keep layouts responsive while animations load. +/// + +## Example + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +## Description + +{{ class_all_options(class_name) }} diff --git a/sdk/python/packages/flet/docs/video/index.md b/sdk/python/packages/flet/docs/video/index.md new file mode 100644 index 0000000000..d5e44f49b7 --- /dev/null +++ b/sdk/python/packages/flet/docs/video/index.md @@ -0,0 +1,56 @@ +--- +class_name: flet_video.Video +examples: ../../examples/controls/video +--- + +# Video + +Embed a full-featured video player in your [Flet](https://flet.dev) app with playlist support, hardware acceleration controls, and subtitle configuration. + +It is powered by the [media_kit](https://pub.dev/packages/media_kit) Flutter package. + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| βœ… | βœ… | βœ… | βœ… | βœ… | βœ… | + +## Usage + +Add the `flet-video` package to your project dependencies: + +/// tab | uv +```bash +uv add flet-video +``` + +/// +/// tab | pip +```bash +pip install flet-video # (1)! +``` + +1. After this, you will have to manually add this package to your `requirements.txt` or `pyproject.toml`. +/// + +/// admonition | Windows Subsystem for Linux (WSL) + type: note +Install the [`libmpv`](https://github.com/mpv-player/mpv) library when running on WSL. +If you encounter `libmpv.so.1` load errors, run: + +```bash +sudo apt update +sudo apt install libmpv-dev libmpv2 +sudo ln -s /usr/lib/x86_64-linux-gnu/libmpv.so /usr/lib/libmpv.so.1 +``` +/// + +## Example + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +## Description + +{{ class_all_options(class_name) }} diff --git a/sdk/python/packages/flet/docs/video/types/playlist_mode.md b/sdk/python/packages/flet/docs/video/types/playlist_mode.md new file mode 100644 index 0000000000..8afb391a35 --- /dev/null +++ b/sdk/python/packages/flet/docs/video/types/playlist_mode.md @@ -0,0 +1 @@ +{{ class_all_options("flet_video.PlaylistMode", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/video/types/video_configuration.md b/sdk/python/packages/flet/docs/video/types/video_configuration.md new file mode 100644 index 0000000000..d8a02f8725 --- /dev/null +++ b/sdk/python/packages/flet/docs/video/types/video_configuration.md @@ -0,0 +1 @@ +{{ class_all_options("flet_video.VideoConfiguration") }} diff --git a/sdk/python/packages/flet/docs/video/types/video_media.md b/sdk/python/packages/flet/docs/video/types/video_media.md new file mode 100644 index 0000000000..8ba23c2174 --- /dev/null +++ b/sdk/python/packages/flet/docs/video/types/video_media.md @@ -0,0 +1 @@ +{{ class_all_options("flet_video.VideoMedia") }} diff --git a/sdk/python/packages/flet/docs/video/types/video_subtitle_configuration.md b/sdk/python/packages/flet/docs/video/types/video_subtitle_configuration.md new file mode 100644 index 0000000000..22c2919371 --- /dev/null +++ b/sdk/python/packages/flet/docs/video/types/video_subtitle_configuration.md @@ -0,0 +1 @@ +{{ class_all_options("flet_video.VideoSubtitleConfiguration") }} diff --git a/sdk/python/packages/flet/docs/video/types/video_subtitle_track.md b/sdk/python/packages/flet/docs/video/types/video_subtitle_track.md new file mode 100644 index 0000000000..bd1a3eec9c --- /dev/null +++ b/sdk/python/packages/flet/docs/video/types/video_subtitle_track.md @@ -0,0 +1 @@ +{{ class_all_options("flet_video.VideoSubtitleTrack") }} diff --git a/sdk/python/packages/flet/docs/webview/index.md b/sdk/python/packages/flet/docs/webview/index.md new file mode 100644 index 0000000000..e9c15be50c --- /dev/null +++ b/sdk/python/packages/flet/docs/webview/index.md @@ -0,0 +1,49 @@ +--- +class_name: flet_webview.WebView +examples: ../../examples/controls/webview +--- + +# WebView + +Display web content inside your [Flet](https://flet.dev) app using the `flet-webview` extension, which wraps Flutter's [`webview_flutter`](https://pub.dev/packages/webview_flutter) package. + +## Platform Support + +| Platform | Windows | macOS | Linux | iOS | Android | Web | +|----------|---------|-------|-------|-----|---------|-----| +| Supported| ❌ | βœ… | ❌ | βœ… | βœ… | βœ… | + +## Usage + +Add `flet-webview` to your project dependencies: + +/// tab | uv +```bash +uv add flet-webview +``` + +/// +/// tab | pip +```bash +pip install flet-webview # (1)! +``` + +1. After this, you will have to manually add this package to your `requirements.txt` or `pyproject.toml`. +/// + +## Example + +```python +--8<-- "{{ examples }}/example_1.py" +``` + +## Description + +{{ class_all_options(class_name) }} + +See also types: +- [`RequestMethod`](types/request_method.md) +- [`LogLevelSeverity`](types/log_level_severity.md) +- [`WebViewConsoleMessageEvent`](types/webview_console_message_event.md) +- [`WebViewJavaScriptEvent`](types/webview_javascript_event.md) +- [`WebViewScrollEvent`](types/webview_scroll_event.md) diff --git a/sdk/python/packages/flet/docs/webview/types/log_level_severity.md b/sdk/python/packages/flet/docs/webview/types/log_level_severity.md new file mode 100644 index 0000000000..0f44fbb04c --- /dev/null +++ b/sdk/python/packages/flet/docs/webview/types/log_level_severity.md @@ -0,0 +1 @@ +{{ class_all_options("flet_webview.LogLevelSeverity", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/webview/types/request_method.md b/sdk/python/packages/flet/docs/webview/types/request_method.md new file mode 100644 index 0000000000..3eac0ea96c --- /dev/null +++ b/sdk/python/packages/flet/docs/webview/types/request_method.md @@ -0,0 +1 @@ +{{ class_all_options("flet_webview.RequestMethod", separate_signature=False) }} diff --git a/sdk/python/packages/flet/docs/webview/types/webview_console_message_event.md b/sdk/python/packages/flet/docs/webview/types/webview_console_message_event.md new file mode 100644 index 0000000000..2c6686c2ae --- /dev/null +++ b/sdk/python/packages/flet/docs/webview/types/webview_console_message_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_webview.WebViewConsoleMessageEvent", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/webview/types/webview_javascript_event.md b/sdk/python/packages/flet/docs/webview/types/webview_javascript_event.md new file mode 100644 index 0000000000..595fd5d0e7 --- /dev/null +++ b/sdk/python/packages/flet/docs/webview/types/webview_javascript_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_webview.WebViewJavaScriptEvent", separate_signature=True) }} diff --git a/sdk/python/packages/flet/docs/webview/types/webview_scroll_event.md b/sdk/python/packages/flet/docs/webview/types/webview_scroll_event.md new file mode 100644 index 0000000000..a4b5571f9a --- /dev/null +++ b/sdk/python/packages/flet/docs/webview/types/webview_scroll_event.md @@ -0,0 +1 @@ +{{ class_all_options("flet_webview.WebViewScrollEvent", separate_signature=True) }} diff --git a/sdk/python/packages/flet/mkdocs.yml b/sdk/python/packages/flet/mkdocs.yml index 1123fc8905..2ecc83b086 100644 --- a/sdk/python/packages/flet/mkdocs.yml +++ b/sdk/python/packages/flet/mkdocs.yml @@ -253,7 +253,7 @@ nav: - Extending Flet: - Creating an Extension: extend/user-extensions.md - Built-in Extensions: extend/built-in-extensions.md - - Reference: + - API Reference: - Controls: - Overview: controls/index.md - Ads: @@ -262,6 +262,17 @@ nav: - BaseAd: ads/basead.md - InterstitialAd: ads/interstitialad.md # - NativeAd: ads/nativead.md + - Audio: audio/index.md + - AudioRecorder: audio_recorder/index.md + - Charts: + - Overview: charts/index.md + - BarChart: charts/bar_chart.md + - CandlestickChart: charts/candlestick_chart.md + - LineChart: charts/line_chart.md + - MatplotlibChart: charts/matplotlib_chart.md + - PieChart: charts/pie_chart.md + - PlotlyChart: charts/plotly_chart.md + - ScatterChart: charts/scatter_chart.md - AlertDialog: controls/alertdialog.md - AnimatedSwitcher: controls/animatedswitcher.md - AppBar: controls/appbar.md @@ -308,6 +319,7 @@ nav: - CupertinoDatePicker: controls/cupertinodatepicker.md - CupertinoDialogAction: controls/cupertinodialogaction.md - CupertinoFilledButton: controls/cupertinofilledbutton.md + - CupertinoListTile: controls/cupertinolisttile.md - CupertinoNavigationBar: controls/cupertinonavigationbar.md - CupertinoPicker: controls/cupertinopicker.md - CupertinoRadio: controls/cupertinoradio.md @@ -318,7 +330,6 @@ nav: - CupertinoTextField: controls/cupertinotextfield.md - CupertinoTimerPicker: controls/cupertinotimerpicker.md - CupertinoTintedButton: controls/cupertinotintedbutton.md - - CupertinoListTile: controls/cupertinolisttile.md - DatePicker: controls/datepicker.md - DateRangePicker: controls/daterangepicker.md - DataTable: @@ -326,6 +337,10 @@ nav: - DataCell: controls/datacell.md - DataColumn: controls/datacolumn.md - DataRow: controls/datarow.md + - DataTable2: + - Overview: datatable2/index.md + - DataColumn2: datatable2/datacolumn2.md + - DataRow2: datatable2/datarow2.md - Dismissible: controls/dismissible.md - Divider: controls/divider.md - DragTarget: controls/dragtarget.md @@ -342,9 +357,11 @@ nav: - FilledIconButton: controls/fillediconbutton.md - FilledTonalButton: controls/filledtonalbutton.md - FilledTonalIconButton: controls/filledtonaliconbutton.md + - Flashlight: flashlight/index.md - FletApp: controls/fletapp.md - FloatingActionButton: controls/floatingactionbutton.md - GestureDetector: controls/gesturedetector.md + - Geolocator: geolocator/index.md - GridView: controls/gridview.md - HapticFeedback: controls/hapticfeedback.md - Icon: controls/icon.md @@ -354,6 +371,28 @@ nav: - KeyboardListener: controls/keyboardlistener.md - ListTile: controls/listtile.md - ListView: controls/listview.md + - Lottie: lottie/index.md + - Map: + - Overview: map/index.md + - Map: map/map.md + - Layers: + - MapLayer: map/map_layer.md + - TileLayer: map/tile_layer.md + - MarkerLayer: map/marker_layer.md + - CircleLayer: map/circle_layer.md + - PolygonLayer: map/polygon_layer.md + - PolylineLayer: map/polyline_layer.md + - Markers: + - Marker: map/marker.md + - CircleMarker: map/circle_marker.md + - PolygonMarker: map/polygon_marker.md + - PolylineMarker: map/polyline_marker.md + - Attributions: + - SourceAttribution: map/source_attribution.md + - SimpleAttribution: map/simple_attribution.md + - RichAttribution: map/rich_attribution.md + - TextSourceAttribution: map/text_source_attribution.md + - ImageSourceAttribution: map/image_source_attribution.md - Markdown: controls/markdown.md - MenuBar: controls/menubar.md - MenuItemButton: controls/menuitembutton.md @@ -374,6 +413,7 @@ nav: - Pagelet: controls/pagelet.md - Placeholder: controls/placeholder.md - PopupMenuButton: controls/popupmenubutton.md + - PermissionHandler: permission_handler/index.md - ProgressBar: controls/progressbar.md - ProgressRing: controls/progressring.md - Radio: controls/radio.md @@ -383,6 +423,7 @@ nav: - ReorderableListView: controls/reorderablelistview.md - ResponsiveRow: controls/responsiverow.md - Row: controls/row.md + - Rive: rive/index.md - SafeArea: controls/safearea.md - SearchBar: controls/searchbar.md - SegmentedButton: controls/segmentedbutton.md @@ -408,7 +449,9 @@ nav: - TimePicker: controls/timepicker.md - TransparentPointer: controls/transparentpointer.md - VerticalDivider: controls/verticaldivider.md + - Video: video/index.md - View: controls/view.md + - WebView: webview/index.md - WindowDragArea: controls/windowdragarea.md - CLI: - Overview: cli/index.md @@ -439,6 +482,22 @@ nav: - AndroidBuildVersion: types/androidbuildversion.md - Animation: types/animation.md - AnimationStyle: types/animationstyle.md + - Audio: + - AudioDurationChangeEvent: audio/types/audio_duration_change_event.md + - AudioPositionChangeEvent: audio/types/audio_position_change_event.md + - AudioStateChangeEvent: audio/types/audio_state_change_event.md + - AudioState: audio/types/audio_state.md + - ReleaseMode: audio/types/release_mode.md + - AudioRecorder: + - AndroidAudioSource: audio_recorder/types/android_audio_source.md + - AndroidRecorderConfiguration: audio_recorder/types/android_recorder_configuration.md + - AudioEncoder: audio_recorder/types/audio_encoder.md + - AudioRecorderConfiguration: audio_recorder/types/audio_recorder_configuration.md + - AudioRecorderStateChangeEvent: audio_recorder/types/audio_recorder_state_change_event.md + - AudioRecorderState: audio_recorder/types/audio_recorder_state.md + - InputDevice: audio_recorder/types/input_device.md + - IosAudioCategoryOption: audio_recorder/types/ios_audio_category_option.md + - IosRecorderConfiguration: audio_recorder/types/ios_recorder_configuration.md - AutoCompleteSuggestion: types/autocompletesuggestion.md - Blur: types/blur.md - Border: types/border.md @@ -449,6 +508,41 @@ nav: - BoxShadow: types/boxshadow.md - BrowserContextMenu: types/browsercontextmenu.md - ButtonStyle: types/buttonstyle.md + - Charts: + - BarChartEvent: charts/types/bar_chart_event.md + - BarChartGroup: charts/types/bar_chart_group.md + - BarChartRod: charts/types/bar_chart_rod.md + - BarChartRodStackItem: charts/types/bar_chart_rod_stack_item.md + - BarChartRodTooltip: charts/types/bar_chart_rod_tooltip.md + - BarChartTooltip: charts/types/bar_chart_tooltip.md + - BarChartTooltipDirection: charts/types/bar_chart_tooltip_direction.md + - CandlestickChartEvent: charts/types/candlestick_chart_event.md + - CandlestickChartSpot: charts/types/candlestick_chart_spot.md + - CandlestickChartSpotTooltip: charts/types/candlestick_chart_spot_tooltip.md + - CandlestickChartTooltip: charts/types/candlestick_chart_tooltip.md + - ChartAxis: charts/types/chart_axis.md + - ChartAxisLabel: charts/types/chart_axis_label.md + - ChartCirclePoint: charts/types/chart_circle_point.md + - ChartCrossPoint: charts/types/chart_cross_point.md + - ChartDataPointTooltip: charts/types/chart_data_point_tooltip.md + - ChartEventType: charts/types/chart_event_type.md + - ChartGridLines: charts/types/chart_grid_lines.md + - ChartPointLine: charts/types/chart_point_line.md + - ChartPointShape: charts/types/chart_point_shape.md + - ChartSquarePoint: charts/types/chart_square_point.md + - HorizontalAlignment: charts/types/horizontal_alignment.md + - LineChartData: charts/types/line_chart_data.md + - LineChartDataPoint: charts/types/line_chart_data_point.md + - LineChartDataPointTooltip: charts/types/line_chart_data_point_tooltip.md + - LineChartEvent: charts/types/line_chart_event.md + - LineChartEventSpot: charts/types/line_chart_event_spot.md + - LineChartTooltip: charts/types/line_chart_tooltip.md + - PieChartEvent: charts/types/pie_chart_event.md + - PieChartSection: charts/types/pie_chart_section.md + - ScatterChartEvent: charts/types/scatter_chart_event.md + - ScatterChartSpot: charts/types/scatter_chart_spot.md + - ScatterChartSpotTooltip: charts/types/scatter_chart_spot_tooltip.md + - ScatterChartTooltip: charts/types/scatter_chart_tooltip.md - Clipboard: controls/clipboard.md - ColorFilter: types/colorfilter.md - Context: types/context.md @@ -467,6 +561,25 @@ nav: - FilePickerUploadFile: types/filepickeruploadfile.md - Finder: types/finder.md - FletTestApp: types/flettestapp.md + - Flashlight: + - FlashlightException: flashlight/exceptions/flashlight_exception.md + - FlashlightEnableException: flashlight/exceptions/flashlight_enable_exception.md + - FlashlightEnableNotAvailableException: flashlight/exceptions/flashlight_enable_not_available_exception.md + - FlashlightEnableExistentUserException: flashlight/exceptions/flashlight_enable_existent_user_exception.md + - FlashlightDisableException: flashlight/exceptions/flashlight_disable_exception.md + - FlashlightDisableNotAvailableException: flashlight/exceptions/flashlight_disable_not_available_exception.md + - FlashlightDisableExistentUserException: flashlight/exceptions/flashlight_disable_existent_user_exception.md + - Geolocator: + - ForegroundNotificationConfiguration: geolocator/types/foreground_notification_configuration.md + - GeolocatorAndroidConfiguration: geolocator/types/geolocator_android_configuration.md + - GeolocatorConfiguration: geolocator/types/geolocator_configuration.md + - GeolocatorIosActivityType: geolocator/types/geolocator_ios_activity_type.md + - GeolocatorIosConfiguration: geolocator/types/geolocator_ios_configuration.md + - GeolocatorPermissionStatus: geolocator/types/geolocator_permission_status.md + - GeolocatorPosition: geolocator/types/geolocator_position.md + - GeolocatorPositionAccuracy: geolocator/types/geolocator_position_accuracy.md + - GeolocatorPositionChangeEvent: geolocator/types/geolocator_position_change_event.md + - GeolocatorWebConfiguration: geolocator/types/geolocator_web_configuration.md - Gradient: - Gradient: types/gradient/index.md - LinearGradient: types/lineargradient.md @@ -481,6 +594,33 @@ nav: - ValueKey: types/valuekey.md - Locale: types/locale.md - LocaleConfiguration: types/localeconfiguration.md + - Map: + - AttributionAlignment: map/types/attribution_alignment.md + - Camera: map/types/camera.md + - CameraFit: map/types/camera_fit.md + - CursorKeyboardRotationConfiguration: map/types/cursor_keyboard_rotation_configuration.md + - CursorRotationBehaviour: map/types/cursor_rotation_behaviour.md + - DashedStrokePattern: map/types/dashed_stroke_pattern.md + - DottedStrokePattern: map/types/dotted_stroke_pattern.md + - FadeInTileDisplay: map/types/fade_in_tile_display.md + - InstantaneousTileDisplay: map/types/instantaneous_tile_display.md + - InteractionConfiguration: map/types/interaction_configuration.md + - InteractionFlag: map/types/interaction_flag.md + - KeyboardConfiguration: map/types/keyboard_configuration.md + - MapEvent: map/types/map_event.md + - MapEventSource: map/types/map_event_source.md + - MapHoverEvent: map/types/map_hover_event.md + - MapLatitudeLongitude: map/types/map_latitude_longitude.md + - MapLatitudeLongitudeBounds: map/types/map_latitude_longitude_bounds.md + - MapPointerEvent: map/types/map_pointer_event.md + - MapPositionChangeEvent: map/types/map_position_change_event.md + - MapTapEvent: map/types/map_tap_event.md + - MultiFingerGesture: map/types/multi_finger_gesture.md + - PatternFit: map/types/pattern_fit.md + - SolidStrokePattern: map/types/solid_stroke_pattern.md + - StrokePattern: map/types/stroke_pattern.md + - TileDisplay: map/types/tile_display.md + - TileLayerEvictErrorTileStrategy: map/types/tile_layer_evict_error_tile_strategy.md - Margin: types/margin.md - MarkdownCustomCodeTheme: types/markdowncustomcodetheme.md - MarkdownStyleSheet: types/markdownstylesheet.md @@ -505,6 +645,9 @@ nav: - PaintLinearGradient: types/paintlineargradient.md - PaintRadialGradient: types/paintradialgradient.md - PaintSweepGradient: types/paintsweepgradient.md + - PermissionHandler: + - Permission: permission_handler/types/permission.md + - PermissionStatus: permission_handler/types/permission_status.md - PubSubClient: types/pubsub/pubsubclient.md - PubSubHub: types/pubsub/pubsubhub.md - Rect: types/rect.md @@ -573,6 +716,18 @@ nav: - Url: types/url.md - UrlLauncher: controls/urllauncher.md - UnderlineTabIndicator: types/underlinetabindicator.md + - Video: + - PlaylistMode: video/types/playlist_mode.md + - VideoConfiguration: video/types/video_configuration.md + - VideoMedia: video/types/video_media.md + - VideoSubtitleConfiguration: video/types/video_subtitle_configuration.md + - VideoSubtitleTrack: video/types/video_subtitle_track.md + - WebView: + - RequestMethod: webview/types/request_method.md + - LogLevelSeverity: webview/types/log_level_severity.md + - WebViewConsoleMessageEvent: webview/types/webview_console_message_event.md + - WebViewJavaScriptEvent: webview/types/webview_javascript_event.md + - WebViewScrollEvent: webview/types/webview_scroll_event.md - Decorators: - component: types/component.md - control: types/control.md diff --git a/sdk/python/packages/flet/pyproject.toml b/sdk/python/packages/flet/pyproject.toml index 1b8d49cd85..eed18c1968 100644 --- a/sdk/python/packages/flet/pyproject.toml +++ b/sdk/python/packages/flet/pyproject.toml @@ -33,6 +33,18 @@ flet = "flet.cli:main" [dependency-groups] extensions = [ "flet-ads", + "flet-audio", + "flet-audio-recorder", + "flet-charts", + "flet-datatable2", + "flet-flashlight", + "flet-geolocator", + "flet-lottie", + "flet-map", + "flet-permission-handler", + "flet-rive", + "flet-video", + "flet-webview", ] test = [ "pytest >=7.2.0", diff --git a/sdk/python/packages/flet/src/flet/components/observable.py b/sdk/python/packages/flet/src/flet/components/observable.py index 69b4dcd141..b1c43bb220 100644 --- a/sdk/python/packages/flet/src/flet/components/observable.py +++ b/sdk/python/packages/flet/src/flet/components/observable.py @@ -127,6 +127,12 @@ def _notify(self, field: str | None): for fn in list(self.__listeners): fn(self, field) + def notify(self): + """ + Manually notify listeners that something changed. + """ + self._notify(None) + # collection wrapping def _wrap_if_collection(self, name: str, value: Any) -> Any: if isinstance(value, list) and not isinstance(value, ObservableList): diff --git a/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py b/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py index 65e5aa451f..c273271380 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py +++ b/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py @@ -1,4 +1,3 @@ -from dataclasses import field from enum import Enum from typing import Optional, Union @@ -31,7 +30,7 @@ class FormFieldControl(LayoutControl): Text size in virtual pixels. """ - text_style: TextStyle = field(default_factory=lambda: TextStyle()) + text_style: Optional[TextStyle] = None """ The [`TextStyle`][flet.] to use for the text being edited. diff --git a/sdk/python/packages/flet/src/flet/controls/services/browser_context_menu.py b/sdk/python/packages/flet/src/flet/controls/services/browser_context_menu.py index d2f2d0f07a..1b95cf8677 100644 --- a/sdk/python/packages/flet/src/flet/controls/services/browser_context_menu.py +++ b/sdk/python/packages/flet/src/flet/controls/services/browser_context_menu.py @@ -1,5 +1,3 @@ -from typing import Optional - from flet.controls.base_control import control from flet.controls.services.service import Service @@ -12,12 +10,12 @@ def __post_init__(self, ref): super().__post_init__(ref) self.__disabled = False - async def enable(self, timeout: Optional[float] = None): - await self._invoke_method("enable_menu", timeout=timeout) + async def enable(self): + await self._invoke_method("enable_menu") self.__disabled = False - async def disable(self, timeout: Optional[float] = None): - await self._invoke_method("disable_menu", timeout=timeout) + async def disable(self): + await self._invoke_method("disable_menu") self.__disabled = True @property diff --git a/sdk/python/packages/flet/src/flet/controls/services/clipboard.py b/sdk/python/packages/flet/src/flet/controls/services/clipboard.py index 7148a7f0e8..86d0b1d978 100644 --- a/sdk/python/packages/flet/src/flet/controls/services/clipboard.py +++ b/sdk/python/packages/flet/src/flet/controls/services/clipboard.py @@ -2,14 +2,13 @@ from flet.controls.base_control import control from flet.controls.services.service import Service -from flet.controls.types import Number __all__ = ["Clipboard"] @control("Clipboard") class Clipboard(Service): - async def set(self, value: str, timeout: Optional[Number] = None) -> None: + async def set(self, value: str) -> None: """ Set clipboard data on a client side (user's web browser or a desktop). @@ -21,9 +20,9 @@ async def set(self, value: str, timeout: Optional[Number] = None) -> None: ``` /// """ - await self._invoke_method("set", {"data": value}, timeout=timeout) + await self._invoke_method("set", {"data": value}) - async def get(self, timeout: Optional[Number] = None) -> Optional[str]: + async def get(self) -> Optional[str]: """ Set clipboard data on a client side (user's web browser or a desktop). @@ -35,4 +34,4 @@ async def get(self, timeout: Optional[Number] = None) -> Optional[str]: ``` /// """ - return await self._invoke_method("get", timeout=timeout) + return await self._invoke_method("get") diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index fca1fe0dd6..e505726ada 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -4,13 +4,25 @@ version = "0.1.0" description = "" authors = [{name = "Appveyor Systems Inc.", email ="hello@flet.dev"}] license = "Apache-2.0" -requires-python = ">=3.10,<3.14" +requires-python = ">=3.10,<3.15" dependencies = [ "flet", "flet-cli", "flet-desktop", "flet-web", "flet-ads", + "flet-audio", + "flet-audio-recorder", + "flet-charts", + "flet-datatable2", + "flet-flashlight", + "flet-geolocator", + "flet-lottie", + "flet-map", + "flet-permission-handler", + "flet-rive", + "flet-video", + "flet-webview" ] [tool.uv.sources] @@ -19,6 +31,18 @@ flet-cli = { workspace = true } flet-desktop = { workspace = true } flet-web = { workspace = true } flet-ads = { workspace = true } +flet-audio = { workspace = true } +flet-audio-recorder = { workspace = true } +flet-charts = { workspace = true } +flet-datatable2 = { workspace = true } +flet-flashlight = { workspace = true } +flet-geolocator = { workspace = true } +flet-lottie = { workspace = true } +flet-map = { workspace = true } +flet-permission-handler = { workspace = true } +flet-rive = { workspace = true } +flet-video = { workspace = true } +flet-webview = { workspace = true } mkdocs-external-images = { git = "https://github.com/flet-dev/mkdocs-external-images", tag = "v0.2.0" } [tool.uv.workspace]