├── .appveyor.yml ├── .ci └── patch_toml.py ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── flet_contrib ├── audio_player │ ├── README.md │ ├── __init__.py │ ├── examples │ │ └── audio_player_example.py │ ├── media │ │ └── audio_player_example_screenshot.png │ └── src │ │ ├── audio_player.py │ │ └── utils.py ├── color_picker │ ├── README.md │ ├── __init__.py │ ├── examples │ │ ├── color_picker_dialog.py │ │ ├── color_picker_dialog_async.py │ │ ├── hue_slider_example.py │ │ ├── palette_color_picker.py │ │ └── update_color_property.py │ ├── media │ │ └── color_picker.png │ └── src │ │ ├── color_picker.py │ │ ├── hue_slider.py │ │ └── utils.py ├── flet_map │ ├── README.md │ ├── __init__.py │ ├── assets │ │ └── screen_shot.png │ ├── src │ │ └── flet_map.py │ └── tests │ │ └── flet_map_show_map.py ├── flexible_slider │ ├── README.md │ ├── __init__.py │ ├── examples │ │ ├── horizontal_slider_example.py │ │ └── vertical_slider_example.py │ └── src │ │ └── flexible_slider.py ├── shimmer │ ├── README.md │ ├── __init__.py │ ├── examples │ │ ├── shimmer_async_common_shimmer.py │ │ ├── shimmer_async_individual_ctrl_effect.py │ │ └── shimmer_sync_mode_common_effect.py │ ├── media │ │ └── shimmer_common_async.mp4 │ └── src │ │ └── shimmer.py └── vertical_splitter │ ├── README.md │ ├── __init__.py │ ├── examples │ ├── vertical_splitter_with_containers.py │ └── vertical_splitter_with_navigationrail_and_text.py │ └── src │ └── vertical_splitter.py ├── poetry.lock └── pyproject.toml /.appveyor.yml: -------------------------------------------------------------------------------- 1 | image: ubuntu2004 2 | 3 | skip_branch_with_pr: true 4 | 5 | skip_commits: 6 | files: 7 | - "*.md" 8 | 9 | environment: 10 | TWINE_USERNAME: __token__ 11 | TWINE_PASSWORD: 12 | secure: 174ncAbF5IjSIkmioPt62jeSnzmTlRNchUkE4QdjDWH8xK1olYtySXLJpo2q95HcP7lWJky1hv4APESiRRHnBWoY0XRFafzM/mbCDMzG1tZXiXZmpP1qzHAtRP2QSCIg18xh1TMktraUdTi7sbJnjjRhqzgbW1k0kLBxKw79MPFBhYQ/TiGcmaYWZbWVZNY3HCUCb6Dt7bG1OE2Ul9rD1gvs55xwO9Oq9FOVA1VnMYw= 13 | TWINE_NON_INTERACTIVE: true 14 | 15 | stack: 16 | - python 3.10 17 | 18 | install: 19 | - ps: | 20 | if ($env:APPVEYOR_REPO_TAG_NAME) { 21 | $env:PYPI_VER = $env:APPVEYOR_REPO_TAG_NAME.replace("v", "") 22 | } else { 23 | $env:PYPI_VER = "$($env:APPVEYOR_BUILD_VERSION).dev0" 24 | } 25 | Update-AppveyorBuild -Version $PYPI_VER 26 | - python --version 27 | - pip install --upgrade setuptools wheel twine poetry tomlkit virtualenv 28 | - poetry install 29 | 30 | build_script: 31 | # patch version 32 | - python3 .ci/patch_toml.py pyproject.toml $PYPI_VER 33 | 34 | # build package 35 | - poetry build 36 | 37 | # publish package 38 | - sh: | 39 | if [[ ("$APPVEYOR_REPO_BRANCH" == "main" || "$APPVEYOR_REPO_TAG_NAME" != "") && "$APPVEYOR_PULL_REQUEST_NUMBER" == "" ]]; then 40 | twine upload dist/* 41 | fi 42 | 43 | artifacts: 44 | - path: dist/* 45 | 46 | test: off -------------------------------------------------------------------------------- /.ci/patch_toml.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import sys 4 | 5 | import tomlkit 6 | 7 | if len(sys.argv) < 3: 8 | print("Specify toml file and version to patch") 9 | sys.exit(1) 10 | 11 | current_dir = pathlib.Path(os.getcwd()) 12 | toml_path = current_dir.joinpath(current_dir, sys.argv[1]) 13 | ver = sys.argv[2] 14 | print(f"Patching TOML file {toml_path} to {ver}") 15 | 16 | # read 17 | with open(toml_path, "r") as f: 18 | t = tomlkit.parse(f.read()) 19 | 20 | # patch version 21 | t["tool"]["poetry"]["version"] = ver 22 | 23 | # save 24 | with open(toml_path, "w") as f: 25 | f.write(tomlkit.dumps(t)) 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | 154 | .vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flet controls in Python by the community 2 | 3 | `flet-contrib` package includes reusable Flet controls written in Python only and using existing Flet primitives. 4 | 5 | ## Controls 6 | 7 | * [ColorPicker](flet_contrib/color_picker) ([Live demo](https://flet-controls-gallery.fly.dev/contrib/colorpicker)) 8 | 9 | ## Usage 10 | 11 | To install `flet-contrib` package: 12 | 13 | ``` 14 | pip install flet-contrib 15 | ``` 16 | 17 | To use in your app: 18 | 19 | ```python 20 | from flet_contrib.color_picker import ColorPicker 21 | 22 | picker = ColorPicker(...) 23 | ``` 24 | 25 | ## How to contribute 26 | 27 | Contributions are welcome! 28 | 29 | Fork this repo. 30 | 31 | Create a new directory inside `flet_contrib` for your control(s) - that will be control's module name, for example `flet_contrib.my_control`. 32 | 33 | Control directory structure: 34 | 35 | * `README.md` - control description, usage, examples, support information. 36 | * `/src` - control implementation. 37 | * `/media` - images, multimedia files, databases and other files required by control to function or used in `README.md`. 38 | * `/examples` - one or more examples of usage of your control. 39 | * `__init__.py` - classes and functions exported to users of your control. 40 | 41 | See [ColorPicker](flet_contrib/color_picker) for folder structure example. 42 | 43 | See [FletMap](flet_contrib/flet_map) for folder structure example. 44 | ![FletMapScreenShot](./flet_contrib/flet_map/assets/screen_shot.png) 45 | 46 | Submit Pull Request (PR) with your changes. 47 | 48 | Once your PR is merged into `main` a new "dev" package will be released [here](https://pypi.org/project/flet-contrib/) which can be installed with `pip install flet-contrib --pre`. 49 | 50 | When the contribution is tested by Flet team/community a new `flet-contrib` release will be published. -------------------------------------------------------------------------------- /flet_contrib/audio_player/README.md: -------------------------------------------------------------------------------- 1 | # AudioPlayer 2 | 3 | A minimal audio player for your app. 4 | 5 | ![Screenshot](https://github.com/taaaf11/flet-contrib/assets/109919009/bb3d31c2-fe1d-4005-bdde-46b246dd1b89) 6 | -------------------------------------------------------------------------------- /flet_contrib/audio_player/__init__.py: -------------------------------------------------------------------------------- 1 | from flet_contrib.audio_player.src.audio_player import AudioPlayer 2 | -------------------------------------------------------------------------------- /flet_contrib/audio_player/examples/audio_player_example.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | from flet_contrib.audio_player import AudioPlayer 4 | 5 | 6 | def main(page: ft.Page): 7 | page.vertical_alignment = ft.MainAxisAlignment.CENTER 8 | page.horizontal_alignment = ft.CrossAxisAlignment.CENTER 9 | 10 | page.add( 11 | AudioPlayer( 12 | page=page, 13 | src="https://github.com/mdn/webaudio-examples/blob/main/audio-analyser/viper.mp3?raw=true", 14 | width=page.width / 2, 15 | ) 16 | ) 17 | 18 | 19 | ft.app(main) 20 | -------------------------------------------------------------------------------- /flet_contrib/audio_player/media/audio_player_example_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flet-dev/flet-contrib/ee0c6666769fa8c60f894eea77d13e2eda6b4207/flet_contrib/audio_player/media/audio_player_example_screenshot.png -------------------------------------------------------------------------------- /flet_contrib/audio_player/src/audio_player.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import timedelta 3 | 4 | import flet as ft 5 | 6 | from .utils import format_timedelta_str_ms 7 | 8 | 9 | # from inside, this control is just a Column control, 10 | # so, asking about vertical and horizontal alignments makes sense 11 | class AudioPlayer(ft.Container): 12 | def __init__( 13 | self, 14 | page: ft.Page, 15 | src_dir: str | None = None, 16 | src: str | None = None, 17 | curr_idx: int = 0, 18 | font_family: str | None = None, 19 | controls_vertical_alignment: ft.MainAxisAlignment = ft.MainAxisAlignment.NONE, 20 | controls_horizontal_alignment: ft.CrossAxisAlignment = ft.CrossAxisAlignment.NONE, 21 | *args, 22 | **kwargs, 23 | ): 24 | """ 25 | Arguments to constructor: 26 | page: page 27 | src_dir: Path to directory where audio files rest 28 | src: Path to the audio file, if src_dir is not to be given 29 | curr_idx: The index number of file the control should use when it is just added 30 | font_family: Font family to be used in the textual controls 31 | controls_vertical_alignment: From inside, AudioPlayer is just a Column control, 32 | so these control_..._alignment is for the Column control 33 | controls_horizontal_alignment: ... 34 | """ 35 | super().__init__(*args, **kwargs) 36 | self.page_ = page 37 | self.font_family = font_family 38 | 39 | self.curr_idx = curr_idx 40 | 41 | if src_dir is None: 42 | self.src_dir = "" 43 | self.src_dir_contents = [src] 44 | else: 45 | self.src_dir = src_dir 46 | self.src_dir_contents = [ 47 | os.path.join(self.src_dir, folder_content) 48 | for folder_content in os.listdir(self.src_dir) 49 | if folder_content.split(".")[-1] == "mp3" 50 | and not os.path.isdir(os.path.join(self.src_dir, folder_content)) 51 | ] 52 | 53 | self.curr_song_name = self.src_dir_contents[self.curr_idx] 54 | self.seek_bar = ft.ProgressBar(width=self.width) 55 | 56 | # for elapsed time and duration 57 | self.times_row = ft.Row( 58 | alignment=ft.MainAxisAlignment.SPACE_BETWEEN, 59 | # width=self.width, 60 | ) 61 | 62 | # play pause next buttons 63 | self.play_controls = ft.Container( 64 | ft.Column( 65 | [ 66 | ft.Row( 67 | [ 68 | # ft.Text(), # placeholder, nothing to be placed here 69 | ft.IconButton( 70 | icon=ft.icons.SKIP_PREVIOUS_SHARP, 71 | data="prev", 72 | on_click=self.prev_next_music, 73 | ), 74 | play_pause_btn := ft.IconButton( 75 | icon=ft.icons.PLAY_ARROW, on_click=self.play_pause 76 | ), 77 | ft.IconButton( 78 | icon=ft.icons.SKIP_NEXT_SHARP, 79 | data="next", 80 | on_click=self.prev_next_music, 81 | ), 82 | ], 83 | alignment=ft.MainAxisAlignment.CENTER, 84 | spacing=0, 85 | ), 86 | ft.Container( 87 | ft.Row( 88 | [ 89 | ft.IconButton( 90 | icon=ft.icons.ADD, 91 | data="inc", 92 | on_click=self.change_vol, 93 | icon_size=18, 94 | ), 95 | ft.IconButton( 96 | icon=ft.icons.REMOVE, 97 | data="dec", 98 | on_click=self.change_vol, 99 | icon_size=18, 100 | ), 101 | ], 102 | spacing=0, 103 | # wrap=True, 104 | alignment=ft.MainAxisAlignment.CENTER, 105 | ), 106 | # border=ft.border.all(2, ft.Colors.PINK), 107 | ), 108 | ], 109 | spacing=0, 110 | # horizontal_alignment=ft.CrossAxisAlignment.CENTER, 111 | ), 112 | width=page.width, 113 | alignment=ft.alignment.center, 114 | # border=ft.border.all(2, ft.Colors.PURPLE), 115 | margin=0, 116 | ) 117 | 118 | self.contents = [self.seek_bar, self.times_row, self.play_controls] 119 | 120 | self.content = ft.Column( 121 | self.contents, 122 | alignment=controls_vertical_alignment, 123 | horizontal_alignment=controls_horizontal_alignment, 124 | ) 125 | 126 | self.audio = ft.Audio( 127 | src=self.src_dir_contents[self.curr_idx], 128 | volume=1, 129 | on_loaded=self._show_controls, 130 | on_state_changed=lambda _: setattr( 131 | self, "curr_state", _.data 132 | ), # equivalent of self.curr_state = _.data 133 | on_position_changed=self._update_controls, 134 | ) 135 | self.page_.overlay.append(self.audio) 136 | self.page_.update() 137 | 138 | self.play_pause_btn = play_pause_btn 139 | self.playing = False 140 | 141 | # self.border = ft.border.all(2, ft.Colors.PURPLE) 142 | 143 | # contents = ft.Column([self.seek_bar]) 144 | 145 | def play_pause(self, e): 146 | if self.playing: 147 | self.audio.pause() 148 | self.playing = False 149 | self.play_pause_btn.icon = ft.icons.PLAY_ARROW 150 | else: 151 | self.audio.resume() 152 | self.playing = True 153 | self.play_pause_btn.icon = ft.icons.PAUSE 154 | self.page_.update() 155 | 156 | def prev_next_music(self, e): 157 | old_audio_src = self.audio.src 158 | try: 159 | old_audio_state = self.curr_state 160 | except: # when user has not changed the state, that is, control is just added to the page 161 | old_audio_state = "paused" 162 | self.audio.pause() 163 | if e.control.data == "next": 164 | idx = self.curr_idx + 1 165 | if idx >= len(self.src_dir_contents): 166 | idx = len(self.src_dir_contents) - 1 167 | elif e.control.data == "prev": 168 | idx = self.curr_idx - 1 169 | if idx <= 0: 170 | idx = 0 171 | self.curr_idx = idx 172 | 173 | new_path = os.path.join(self.src_dir, self.src_dir_contents[self.curr_idx]) 174 | self.curr_song_name = self.src_dir_contents[self.curr_idx] 175 | 176 | # if it is the same song as the old one, resume the audo and bail out 177 | if old_audio_src == new_path: 178 | if old_audio_state == "playing": 179 | self.audio.resume() 180 | return 181 | 182 | self.audio.src = new_path 183 | self.duration = self.audio.get_duration() 184 | 185 | if old_audio_state == "playing": 186 | self.play_pause_btn.icon = ft.icons.PAUSE 187 | # too hacky way 188 | self.audio.autoplay = True 189 | elif old_audio_state == "paused": 190 | self.play_pause_btn.icon = ft.icons.PLAY_ARROW 191 | 192 | self.page_.update() 193 | self.audio.autoplay = False 194 | 195 | def change_vol(self, e): 196 | if e.control.data == "inc": 197 | self.audio.volume += 0.1 198 | elif e.control.data == "dec": 199 | self.audio.volume -= 0.1 200 | self.audio.update() 201 | 202 | # executed when audio is loaded 203 | def _show_controls(self, e): 204 | self.seek_bar.value = 0 205 | self.duration = self.audio.get_duration() 206 | 207 | elapsed_time, duration = self._calculate_formatted_times(0) 208 | 209 | self._update_times_row(elapsed_time, duration) 210 | 211 | # updating the progressbar and times_row 212 | def _update_controls(self, e): 213 | if e.data == "0": # completed 214 | self.play_pause_btn.icon = ft.icons.PLAY_ARROW 215 | self.playing = False 216 | self.page_.update() 217 | return 218 | curr_time = int(e.data) # the elapsed time 219 | try: 220 | self.seek_bar.value = curr_time / self.duration 221 | except AttributeError: 222 | self.duration = self.audio.get_duration() 223 | finally: 224 | self.seek_bar.value = curr_time / self.duration 225 | 226 | elapsed_time, duration = self._calculate_formatted_times(curr_time) 227 | 228 | self._update_times_row(elapsed_time, duration) 229 | 230 | def _calculate_formatted_times(self, elapsed_time: int): 231 | formatted_elapsed_time = format_timedelta_str_ms( 232 | str(timedelta(milliseconds=elapsed_time)) 233 | ) 234 | formatted_time_duration = format_timedelta_str_ms( 235 | str(timedelta(milliseconds=self.duration)) 236 | ) 237 | 238 | return formatted_elapsed_time, formatted_time_duration 239 | 240 | def _update_times_row(self, elapsed_time, time_duration): 241 | self.times_row.controls = [ 242 | ft.Text(elapsed_time, font_family=self.font_family), 243 | ft.Text(time_duration, font_family=self.font_family), 244 | ] 245 | 246 | self.page_.update() 247 | -------------------------------------------------------------------------------- /flet_contrib/audio_player/src/utils.py: -------------------------------------------------------------------------------- 1 | # request this only when you have done timedelta(milliseconds=...) 2 | def format_timedelta_str_ms(timedelta_milliseconds_str: str): 3 | """ 4 | Examples: 5 | format_timedelta_str("0:00:40.399000") -> "00:40" 6 | format_timedelta_str("0:00:40.600000") -> "00:41" 7 | """ 8 | time_ = timedelta_milliseconds_str.split(":") # the whole split string 9 | seconds_field = time_[-1] 10 | seconds = seconds_field.split(".")[0] # situation: 0:03:40.something 11 | try: 12 | microseconds = seconds_field.split(".")[1] # if it exists 13 | except: 14 | pass # if the microseconds field doesn't exist 15 | else: 16 | # basically, this is the process of rounding 17 | # first, convert the parts into numbers 18 | # process 19 | # round the output and convert into string 20 | time_[-1] = str(round(float(eval(seconds + "." + microseconds)))) 21 | 22 | # the hours place 23 | if len(time_[0]) == 1 and time_[0] == "0": 24 | del time_[0] 25 | 26 | return ":".join(time_) 27 | -------------------------------------------------------------------------------- /flet_contrib/color_picker/README.md: -------------------------------------------------------------------------------- 1 | # ColorPicker 2 | 3 | `ColorPicker` control is used for picking a color from color map in hex (rgb) format. 4 | 5 | `ColorPicker` inherits from [`Column`](https://flet.dev/docs/controls/column) and can be used as a content for [`AlertDialog`](https://flet.dev/docs/controls/alertdialog) or other control or placed directly on a page. 6 | 7 | ## Examples 8 | 9 | [Live example](https://flet-controls-gallery.fly.dev/contrib/colorpicker) 10 | 11 | ### ColorPicker dialog 12 | 13 | 14 | 15 | ```python 16 | import flet as ft 17 | 18 | from flet_contrib.color_picker import ColorPicker 19 | 20 | def main(page: ft.Page): 21 | def open_color_picker(e): 22 | d.open = True 23 | page.update() 24 | 25 | color_picker = ColorPicker(color="#c8df6f", width=300) 26 | color_icon = ft.IconButton(icon=ft.icons.BRUSH, on_click=open_color_picker) 27 | 28 | def change_color(e): 29 | color_icon.icon_color = color_picker.color 30 | d.open = False 31 | page.update() 32 | 33 | def close_dialog(e): 34 | d.open = False 35 | d.update() 36 | 37 | d = ft.AlertDialog( 38 | content=color_picker, 39 | actions=[ 40 | ft.TextButton("OK", on_click=change_color), 41 | ft.TextButton("Cancel", on_click=close_dialog), 42 | ], 43 | actions_alignment=ft.MainAxisAlignment.END, 44 | on_dismiss=change_color, 45 | ) 46 | page.dialog = d 47 | 48 | page.add(color_icon) 49 | 50 | ft.app(target=main) 51 | ``` 52 | 53 | ## Properties 54 | 55 | ### `color` 56 | 57 | [Color](https://flet.dev/docs/guides/python/colors#hex-value) in hex value format. The default value is `#000000`. 58 | 59 | ### `width` 60 | 61 | Width of `ColorPicker` in virtual pixels that can be specified when creating a `ColorPicker` instance. The default value is `340`. 62 | -------------------------------------------------------------------------------- /flet_contrib/color_picker/__init__.py: -------------------------------------------------------------------------------- 1 | from flet_contrib.color_picker.src.color_picker import ColorPicker 2 | from flet_contrib.color_picker.src.hue_slider import HueSlider 3 | 4 | # from flet_contrib.color_picker.src.palette_color_picker import PaletteColorPicker 5 | -------------------------------------------------------------------------------- /flet_contrib/color_picker/examples/color_picker_dialog.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | from flet_contrib.color_picker import ColorPicker 4 | 5 | 6 | def main(page: ft.Page): 7 | def open_color_picker(e): 8 | d.open = True 9 | page.update() 10 | 11 | color_picker = ColorPicker(color="#c8df6f", width=300) 12 | color_icon = ft.IconButton(icon=ft.icons.BRUSH, on_click=open_color_picker) 13 | 14 | def change_color(e): 15 | color_icon.icon_color = color_picker.color 16 | d.open = False 17 | page.update() 18 | 19 | def close_dialog(e): 20 | d.open = False 21 | d.update() 22 | 23 | d = ft.AlertDialog( 24 | content=color_picker, 25 | actions=[ 26 | ft.TextButton("OK", on_click=change_color), 27 | ft.TextButton("Cancel", on_click=close_dialog), 28 | ], 29 | actions_alignment=ft.MainAxisAlignment.END, 30 | on_dismiss=change_color, 31 | ) 32 | page.dialog = d 33 | 34 | page.add(color_icon) 35 | 36 | 37 | ft.app(target=main) 38 | -------------------------------------------------------------------------------- /flet_contrib/color_picker/examples/color_picker_dialog_async.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | from flet_contrib.color_picker import ColorPicker 4 | 5 | 6 | async def main(page: ft.Page): 7 | def open_color_picker(e): 8 | d.open = True 9 | page.update() 10 | 11 | color_picker = ColorPicker(color="#c8df6f", width=300) 12 | color_icon = ft.IconButton(icon=ft.icons.BRUSH, on_click=open_color_picker) 13 | 14 | async def change_color(e): 15 | color_icon.icon_color = color_picker.color 16 | d.open = False 17 | page.update() 18 | 19 | async def close_dialog(e): 20 | d.open = False 21 | d.update() 22 | 23 | d = ft.AlertDialog( 24 | content=color_picker, 25 | actions=[ 26 | ft.TextButton("OK", on_click=change_color), 27 | ft.TextButton("Cancel", on_click=close_dialog), 28 | ], 29 | actions_alignment=ft.MainAxisAlignment.END, 30 | on_dismiss=change_color, 31 | ) 32 | page.dialog = d 33 | page.add(color_icon) 34 | 35 | 36 | ft.app(main) 37 | -------------------------------------------------------------------------------- /flet_contrib/color_picker/examples/hue_slider_example.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | from flet_contrib.color_picker import HueSlider 4 | 5 | 6 | def main(page: ft.Page): 7 | def hue_changed(): 8 | print(hue_slider.hue) 9 | 10 | hue_slider = HueSlider(on_change_hue=hue_changed) 11 | 12 | page.add(hue_slider) 13 | 14 | new_hue = ft.TextField(label="Value between 0 and 1, e.g. 0.5") 15 | 16 | def update_hue(e): 17 | hue_slider.hue = float(new_hue.value) 18 | # hue_slider.update_hue_slider(new_hue.value) 19 | hue_slider.update() 20 | 21 | page.add(new_hue, ft.FilledButton("Update hue", on_click=update_hue)) 22 | 23 | 24 | ft.app(target=main) 25 | -------------------------------------------------------------------------------- /flet_contrib/color_picker/examples/palette_color_picker.py: -------------------------------------------------------------------------------- 1 | # from color_picker import CustomColorPicker 2 | import flet as ft 3 | 4 | 5 | class ColorSwatch: 6 | def __init__(self, name, display_name, accent=True): 7 | self.name = name 8 | self.display_name = display_name 9 | self.accent = accent 10 | 11 | 12 | class Color: 13 | def __init__(self, swatch, shade="", accent=False): 14 | if shade == "": 15 | self.name = swatch.name 16 | self.display_name = swatch.display_name 17 | else: 18 | if not accent: 19 | self.name = f"{swatch.name}{shade}" 20 | self.display_name = f"{swatch.display_name}_{shade}" 21 | else: 22 | self.name = f"{swatch.name}accent{shade}" 23 | self.display_name = f"{swatch.display_name}_ACCENT_{shade}" 24 | 25 | 26 | SHADES = ["50", "100", "200", "300", "400", "500", "600", "700", "800", "900"] 27 | ACCENT_SHADES = ["100", "200", "400", "700"] 28 | WHITE_SHADES = ["10", "12", "24", "30", "38", "54", "70"] 29 | BLACK_SHADES = ["12", "26", "38", "45", "54", "87"] 30 | 31 | 32 | class PaletteColorPicker(ft.Row): 33 | def __init__(self, color="black"): 34 | super().__init__() 35 | self.tight = True 36 | self.color = color 37 | self.spacing = 1 38 | self.generate_color_matrix() 39 | 40 | def generate_color_matrix(self): 41 | swatches = [ 42 | ColorSwatch(name="red", display_name="RED"), 43 | ColorSwatch(name="pink", display_name="PINK"), 44 | ColorSwatch(name="purple", display_name="PURPLE"), 45 | ColorSwatch(name="deeppurple", display_name="DEEP_PURPLE"), 46 | ColorSwatch(name="indigo", display_name="INDIGO"), 47 | ColorSwatch(name="blue", display_name="BLUE"), 48 | ColorSwatch(name="lightblue", display_name="LIGHT_BLUE"), 49 | ColorSwatch(name="cyan", display_name="CYAN"), 50 | ColorSwatch(name="teal", display_name="TEAL"), 51 | ColorSwatch(name="green", display_name="GREEN"), 52 | ColorSwatch(name="lightgreen", display_name="LIGHT_GREEN"), 53 | ColorSwatch(name="lime", display_name="LIME"), 54 | ColorSwatch(name="yellow", display_name="YELLOW"), 55 | ColorSwatch(name="amber", display_name="AMBER"), 56 | ColorSwatch(name="orange", display_name="ORANGE"), 57 | ColorSwatch(name="deeporange", display_name="DEEP_ORANGE"), 58 | ColorSwatch(name="brown", display_name="BROWN", accent=False), 59 | ColorSwatch(name="grey", display_name="GREY", accent=False), 60 | ColorSwatch(name="bluegrey", display_name="BLUE_GREY", accent=False), 61 | ColorSwatch(name="white", display_name="WHITE"), 62 | ColorSwatch(name="black", display_name="BLACK"), 63 | ] 64 | 65 | def generate_color_names(swatch): 66 | colors = [] 67 | base_color = Color(swatch=swatch) 68 | colors.append(base_color) 69 | if swatch.name == "white": 70 | for shade in WHITE_SHADES: 71 | color = Color(swatch=swatch, shade=shade) 72 | colors.append(color) 73 | return colors 74 | if swatch.name == "black": 75 | for shade in BLACK_SHADES: 76 | color = Color(swatch=swatch, shade=shade) 77 | colors.append(color) 78 | return colors 79 | for shade in SHADES: 80 | color = Color(swatch=swatch, shade=shade) 81 | colors.append(color) 82 | if swatch.accent: 83 | for shade in ACCENT_SHADES: 84 | color = Color(swatch=swatch, shade=shade, accent=True) 85 | colors.append(color) 86 | return colors 87 | 88 | def color_clicked(e): 89 | self.color = e.control.bgcolor 90 | print(self.color) 91 | 92 | for swatch in swatches: 93 | swatch_colors = ft.Column(spacing=1, controls=[]) 94 | for color in generate_color_names(swatch): 95 | swatch_colors.controls.append( 96 | ft.Container( 97 | height=20, 98 | width=20, 99 | border_radius=20, 100 | bgcolor=color.name, 101 | on_click=color_clicked, 102 | ) 103 | ) 104 | self.controls.append(swatch_colors) 105 | 106 | 107 | def main(page: ft.Page): 108 | color_picker = PaletteColorPicker() 109 | 110 | def dialog_closed(e): 111 | text_icon.icon_color = e.control.content.color 112 | text_icon.update() 113 | 114 | # color_picker = PaletteColorPicker() 115 | d = ft.AlertDialog(content=color_picker, on_dismiss=dialog_closed) 116 | 117 | page.dialog = d 118 | 119 | def open_color_picker(e): 120 | d.open = True 121 | page.update() 122 | 123 | text_icon = ft.IconButton( 124 | icon=ft.icons.FORMAT_COLOR_TEXT, on_click=open_color_picker 125 | ) 126 | 127 | page.add(text_icon) 128 | 129 | 130 | ft.app(main) 131 | -------------------------------------------------------------------------------- /flet_contrib/color_picker/examples/update_color_property.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | from flet_contrib.color_picker import ColorPicker 4 | 5 | 6 | def main(page: ft.Page): 7 | color_picker = ColorPicker(color="#c8df6f") 8 | new_color = ft.TextField(width=100) 9 | 10 | def change_color(e): 11 | color_picker.color = new_color.value 12 | color_picker.update() 13 | 14 | page.add( 15 | color_picker, new_color, ft.FilledButton("Change color", on_click=change_color) 16 | ) 17 | 18 | 19 | ft.app(target=main) 20 | -------------------------------------------------------------------------------- /flet_contrib/color_picker/media/color_picker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flet-dev/flet-contrib/ee0c6666769fa8c60f894eea77d13e2eda6b4207/flet_contrib/color_picker/media/color_picker.png -------------------------------------------------------------------------------- /flet_contrib/color_picker/src/color_picker.py: -------------------------------------------------------------------------------- 1 | import colorsys 2 | 3 | import flet as ft 4 | 5 | from .hue_slider import HueSlider 6 | from .utils import * 7 | 8 | COLOR_MATRIX_WIDTH = 340 9 | CIRCLE_SIZE = 20 10 | 11 | 12 | class ColorPicker(ft.Column): 13 | def __init__(self, color="#000000", width=COLOR_MATRIX_WIDTH): 14 | super().__init__() 15 | self.tight = True 16 | self.width = width 17 | self.__color = color 18 | self.hue_slider = HueSlider( 19 | on_change_hue=self.update_color_picker_on_hue_change, 20 | hue=hex2hsv(self.color)[0], 21 | ) 22 | self.generate_color_map() 23 | self.generate_selected_color_view() 24 | 25 | # color 26 | @property 27 | def color(self): 28 | return self.__color 29 | 30 | @color.setter 31 | def color(self, value): 32 | self.__color = value 33 | 34 | def before_update(self): 35 | super().before_update() 36 | # called every time on self.update() 37 | self.hue_slider.hue = hex2hsv(self.color)[0] 38 | self.update_circle_position() 39 | self.update_color_map() 40 | self.update_selected_color_view_values() 41 | 42 | def update_circle_position(self): 43 | hsv_color = hex2hsv(self.color) 44 | self.thumb.left = hsv_color[1] * self.color_map.width # s * width 45 | self.thumb.top = (1 - hsv_color[2]) * self.color_map.height # (1-v)*height 46 | 47 | def find_color(self, x, y): 48 | h = self.hue_slider.hue 49 | s = x / self.color_map.width 50 | v = (self.color_map.height - y) / self.color_map.height 51 | self.color = rgb2hex(colorsys.hsv_to_rgb(h, s, v)) 52 | 53 | def generate_selected_color_view(self): 54 | rgb = hex2rgb(self.color) 55 | 56 | def on_hex_submit(e): 57 | self.color = e.control.value 58 | self.update() 59 | 60 | def __on_rgb_submit(): 61 | rgb = ( 62 | int(self.r.value) / 255, 63 | int(self.g.value) / 255, 64 | int(self.b.value) / 255, 65 | ) 66 | self.color = rgb2hex(rgb) 67 | 68 | def on_rgb_submit(e): 69 | __on_rgb_submit() 70 | self.update() 71 | 72 | self.hex = ft.TextField( 73 | label="Hex", 74 | text_size=12, 75 | value=self.__color, 76 | height=40, 77 | width=90, 78 | on_submit=on_hex_submit, 79 | on_blur=on_hex_submit, 80 | ) 81 | self.r = ft.TextField( 82 | label="R", 83 | height=40, 84 | width=55, 85 | value=rgb[0], 86 | text_size=12, 87 | on_submit=on_rgb_submit, 88 | on_blur=on_rgb_submit, 89 | ) 90 | self.g = ft.TextField( 91 | label="G", 92 | height=40, 93 | width=55, 94 | value=rgb[1], 95 | text_size=12, 96 | on_submit=on_rgb_submit, 97 | on_blur=on_rgb_submit, 98 | ) 99 | self.b = ft.TextField( 100 | label="B", 101 | height=40, 102 | width=55, 103 | value=rgb[2], 104 | text_size=12, 105 | on_submit=on_rgb_submit, 106 | on_blur=on_rgb_submit, 107 | ) 108 | self.selected_color_view = ft.Column( 109 | spacing=20, 110 | controls=[ 111 | ft.Row( 112 | alignment=ft.MainAxisAlignment.SPACE_AROUND, 113 | controls=[ 114 | ft.Container( 115 | width=30, height=30, border_radius=30, bgcolor=self.__color 116 | ), 117 | self.hue_slider, 118 | ], 119 | ), 120 | ft.Row( 121 | alignment=ft.MainAxisAlignment.SPACE_AROUND, 122 | controls=[ 123 | self.hex, 124 | self.r, 125 | self.g, 126 | self.b, 127 | ], 128 | ), 129 | ], 130 | ) 131 | 132 | self.controls.append(self.selected_color_view) 133 | 134 | def update_selected_color_view_values(self): 135 | rgb = hex2rgb(self.color) 136 | self.selected_color_view.controls[0].controls[ 137 | 0 138 | ].bgcolor = self.color # Colored circle 139 | self.hex.value = self.__color # Hex 140 | self.r.value = rgb[0] # R 141 | self.g.value = rgb[1] # G 142 | self.b.value = rgb[2] # B 143 | self.thumb.bgcolor = self.color # Color matrix circle 144 | 145 | def generate_color_map(self): 146 | def __move_circle(x, y): 147 | self.thumb.top = max( 148 | 0, 149 | min( 150 | y - CIRCLE_SIZE / 2, 151 | self.color_map.height, 152 | ), 153 | ) 154 | self.thumb.left = max( 155 | 0, 156 | min( 157 | x - CIRCLE_SIZE / 2, 158 | self.color_map.width, 159 | ), 160 | ) 161 | self.find_color(x=self.thumb.left, y=self.thumb.top) 162 | self.update_selected_color_view_values() 163 | 164 | def on_pan_update(e: ft.DragStartEvent): 165 | __move_circle(x=e.local_x, y=e.local_y) 166 | self.selected_color_view.update() 167 | self.thumb.update() 168 | 169 | self.color_map_container = ft.GestureDetector( 170 | content=ft.Stack( 171 | width=self.width, 172 | height=int(self.width * 3 / 5), 173 | ), 174 | on_pan_start=on_pan_update, 175 | on_pan_update=on_pan_update, 176 | ) 177 | 178 | saturation_container = ft.Container( 179 | gradient=ft.LinearGradient( 180 | begin=ft.alignment.center_left, 181 | end=ft.alignment.center_right, 182 | colors=[ft.Colors.WHITE, ft.Colors.RED], 183 | ), 184 | width=self.color_map_container.content.width - CIRCLE_SIZE, 185 | height=self.color_map_container.content.height - CIRCLE_SIZE, 186 | border_radius=5, 187 | ) 188 | 189 | self.color_map = ft.ShaderMask( 190 | top=CIRCLE_SIZE / 2, 191 | left=CIRCLE_SIZE / 2, 192 | content=saturation_container, 193 | blend_mode=ft.BlendMode.MULTIPLY, 194 | shader=ft.LinearGradient( 195 | begin=ft.alignment.top_center, 196 | end=ft.alignment.bottom_center, 197 | colors=[ft.Colors.WHITE, ft.Colors.BLACK], 198 | ), 199 | border_radius=5, 200 | width=saturation_container.width, 201 | height=saturation_container.height, 202 | ) 203 | 204 | self.thumb = ft.Container( 205 | width=CIRCLE_SIZE, 206 | height=CIRCLE_SIZE, 207 | border_radius=CIRCLE_SIZE, 208 | border=ft.border.all(width=2, color="white"), 209 | ) 210 | 211 | self.color_map_container.content.controls.append(self.color_map) 212 | self.color_map_container.content.controls.append(self.thumb) 213 | self.controls.append(self.color_map_container) 214 | 215 | def update_color_map(self): 216 | h = self.hue_slider.hue 217 | s = hex2hsv(self.color)[1] 218 | v = hex2hsv(self.color)[2] 219 | container_gradient_colors = [ 220 | rgb2hex(colorsys.hsv_to_rgb(h, 0, 1)), 221 | rgb2hex(colorsys.hsv_to_rgb(h, 1, 1)), 222 | ] 223 | 224 | self.color_map.content.gradient.colors = container_gradient_colors 225 | 226 | self.color = rgb2hex(colorsys.hsv_to_rgb(h, s, v)) 227 | 228 | def update_color_picker_on_hue_change(self): 229 | self.update_color_map() 230 | self.update_selected_color_view_values() 231 | self.selected_color_view.update() 232 | self.color_map_container.update() 233 | -------------------------------------------------------------------------------- /flet_contrib/color_picker/src/hue_slider.py: -------------------------------------------------------------------------------- 1 | import colorsys 2 | 3 | import flet as ft 4 | 5 | from .utils import * 6 | 7 | SLIDER_WIDTH = 180 8 | CIRCLE_SIZE = 16 9 | 10 | 11 | class HueSlider(ft.GestureDetector): 12 | def __init__(self, on_change_hue, hue=1): 13 | super().__init__() 14 | self.__hue = hue 15 | self.__number_of_hues = 10 16 | self.content = ft.Stack(height=CIRCLE_SIZE, width=SLIDER_WIDTH) 17 | self.generate_slider() 18 | self.on_change_hue = on_change_hue 19 | self.on_pan_start = self.drag_start 20 | self.on_pan_update = self.drag_update 21 | 22 | # hue 23 | @property 24 | def hue(self) -> float: 25 | return self.__hue 26 | 27 | @hue.setter 28 | def hue(self, value: float): 29 | if isinstance(value, float): 30 | self.__hue = value 31 | if value < 0 or value > 1: 32 | raise Exception("Hue value should be between 0 and 1") 33 | else: 34 | raise Exception("Hue value should be a float number") 35 | 36 | def _before_build_command(self): 37 | super()._before_build_command() 38 | # called every time on self.update() 39 | self.thumb.left = self.__hue * self.track.width 40 | self.thumb.bgcolor = rgb2hex(colorsys.hsv_to_rgb(self.__hue, 1, 1)) 41 | 42 | def __update_selected_hue(self, x): 43 | self.__hue = max(0, min((x - CIRCLE_SIZE / 2) / self.track.width, 1)) 44 | self.thumb.left = self.__hue * self.track.width 45 | self.thumb.bgcolor = rgb2hex(colorsys.hsv_to_rgb(self.__hue, 1, 1)) 46 | 47 | def update_selected_hue(self, x): 48 | self.__update_selected_hue(x) 49 | self.thumb.update() 50 | self.on_change_hue() 51 | 52 | def drag_start(self, e: ft.DragStartEvent): 53 | self.update_selected_hue(x=e.local_x) 54 | 55 | def drag_update(self, e: ft.DragUpdateEvent): 56 | self.update_selected_hue(x=e.local_x) 57 | 58 | def generate_gradient_colors(self): 59 | colors = [] 60 | for i in range(0, self.__number_of_hues + 1): 61 | color = rgb2hex(colorsys.hsv_to_rgb(i / self.__number_of_hues, 1, 1)) 62 | colors.append(color) 63 | return colors 64 | 65 | def generate_slider(self): 66 | self.track = ft.Container( 67 | gradient=ft.LinearGradient( 68 | begin=ft.alignment.center_left, 69 | end=ft.alignment.center_right, 70 | colors=self.generate_gradient_colors(), 71 | ), 72 | width=SLIDER_WIDTH - CIRCLE_SIZE, 73 | height=CIRCLE_SIZE / 2, 74 | border_radius=5, 75 | top=CIRCLE_SIZE / 4, 76 | left=CIRCLE_SIZE / 2, 77 | ) 78 | 79 | self.thumb = ft.Container( 80 | width=CIRCLE_SIZE, 81 | height=CIRCLE_SIZE, 82 | border_radius=CIRCLE_SIZE, 83 | border=ft.border.all(width=2, color="white"), 84 | ) 85 | 86 | self.content.controls.append(self.track) 87 | self.content.controls.append(self.thumb) 88 | -------------------------------------------------------------------------------- /flet_contrib/color_picker/src/utils.py: -------------------------------------------------------------------------------- 1 | import colorsys 2 | 3 | 4 | def rgb2hex(rgb): 5 | return "#{:02x}{:02x}{:02x}".format( 6 | int(rgb[0] * 255.0), int(rgb[1] * 255.0), int(rgb[2] * 255.0) 7 | ) 8 | 9 | 10 | def hex2rgb(value): 11 | value = value.lstrip("#") 12 | lv = len(value) 13 | return tuple(int(value[i : i + lv // 3], 16) for i in range(0, lv, lv // 3)) 14 | 15 | 16 | def hex2hsv(value): 17 | rgb_color = hex2rgb(value) 18 | return colorsys.rgb_to_hsv( 19 | rgb_color[0] / 255, rgb_color[1] / 255, rgb_color[2] / 255 20 | ) 21 | -------------------------------------------------------------------------------- /flet_contrib/flet_map/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flet-dev/flet-contrib/ee0c6666769fa8c60f894eea77d13e2eda6b4207/flet_contrib/flet_map/README.md -------------------------------------------------------------------------------- /flet_contrib/flet_map/__init__.py: -------------------------------------------------------------------------------- 1 | from flet_contrib.flet_map.src.flet_map import FletMap -------------------------------------------------------------------------------- /flet_contrib/flet_map/assets/screen_shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flet-dev/flet-contrib/ee0c6666769fa8c60f894eea77d13e2eda6b4207/flet_contrib/flet_map/assets/screen_shot.png -------------------------------------------------------------------------------- /flet_contrib/flet_map/src/flet_map.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, Union 2 | import math 3 | from flet import alignment 4 | import flet as ft 5 | from flet.container import Container 6 | from flet.control import OptionalNumber 7 | from flet.image import Image 8 | from flet.ref import Ref 9 | from flet.row import Row 10 | from flet.column import Column 11 | from flet.types import ( 12 | AnimationValue, 13 | OffsetValue, 14 | ResponsiveNumber, 15 | RotateValue, 16 | ScaleValue, 17 | ) 18 | 19 | 20 | class FletMap(Container): 21 | def __init__( 22 | self, 23 | ref: Optional[Ref] = None, 24 | expand: Union[None, bool, int] = None, 25 | col: Optional[ResponsiveNumber] = None, 26 | opacity: OptionalNumber = None, 27 | rotate: RotateValue = None, 28 | scale: ScaleValue = None, 29 | offset: OffsetValue = None, 30 | aspect_ratio: OptionalNumber = None, 31 | animate_opacity: AnimationValue = None, 32 | animate_size: AnimationValue = None, 33 | animate_position: AnimationValue = None, 34 | animate_rotation: AnimationValue = None, 35 | animate_scale: AnimationValue = None, 36 | animate_offset: AnimationValue = None, 37 | on_animation_end=None, 38 | tooltip: Optional[str] = None, 39 | visible: Optional[bool] = None, 40 | disabled: Optional[bool] = None, 41 | data: Any = None, 42 | # 43 | # Specific 44 | # 45 | screenView: Optional[list] = [], 46 | latitude: Optional[float] = 0.0, 47 | longtitude: Optional[float] = 0.0, 48 | delta_lat: Optional[float] = 0.0, 49 | delta_long: Optional[float] = 0.0, 50 | zoom: Optional[float] = 0.0, 51 | isolated: bool = False, 52 | original_size: bool = False, 53 | transparent: bool = False, 54 | ): 55 | 56 | Container.__init__( 57 | self, 58 | ref=ref, 59 | expand=expand, 60 | col=col, 61 | opacity=opacity, 62 | rotate=rotate, 63 | scale=scale, 64 | offset=offset, 65 | aspect_ratio=aspect_ratio, 66 | animate_opacity=animate_opacity, 67 | animate_size=animate_size, 68 | animate_position=animate_position, 69 | animate_rotation=animate_rotation, 70 | animate_scale=animate_scale, 71 | animate_offset=animate_offset, 72 | on_animation_end=on_animation_end, 73 | tooltip=tooltip, 74 | visible=visible, 75 | disabled=disabled, 76 | data=data, 77 | ) 78 | 79 | self.latitude = latitude 80 | self.longtitude = longtitude 81 | self.delta_lat = delta_lat 82 | self.delta_long = delta_long 83 | self.zoom = zoom 84 | self.screenView = screenView 85 | self.isolated = isolated 86 | self.original_size = original_size 87 | self.transparent = transparent 88 | 89 | def _is_isolated(self): 90 | return self.__isolated 91 | 92 | def _build(self): 93 | self.alignment = alignment.center 94 | self.__row = Row() 95 | self.__col = Column() 96 | # self.content 97 | # self.content = self.__img 98 | 99 | def _get_image_cluster(self): 100 | index = 0 101 | smurl = r"http://a.tile.openstreetmap.org/{0}/{1}/{2}.png" 102 | 103 | xmin, ymax = self.deg2num(self.latitude, self.longtitude, self.zoom) 104 | xmax, ymin = self.deg2num( 105 | self.latitude + self.delta_lat, self.longtitude + self.delta_long, self.zoom) 106 | self.__row = Row(alignment=ft.MainAxisAlignment.CENTER, 107 | spacing=0, auto_scroll=True) 108 | 109 | for xtile in range(xmin, xmax+self.screenView[0]): 110 | self.__col = Column( 111 | alignment=ft.MainAxisAlignment.CENTER, spacing=0, auto_scroll=True) 112 | xtile 113 | for ytile in range(ymin, ymax+self.screenView[1]): 114 | try: 115 | imgurl = smurl.format(self.zoom, xtile, ytile) 116 | self.__col.controls.append( 117 | Image(src=imgurl, 118 | )) 119 | except: 120 | print("Couldn't download image") 121 | self.__row.controls.append(self.__col) 122 | 123 | self.content = self.__row 124 | 125 | def _before_build_command(self): 126 | super()._before_build_command() 127 | # self.delta_lat = 0.5 128 | # self.delta_lon = 0.5 129 | self._get_image_cluster() 130 | # print(self.__src_content) 131 | # self.__img.src = self.__src_content 132 | 133 | def deg2num(self, lat_deg, lon_deg, zoom): 134 | lat_rad = math.radians(lat_deg) 135 | n = 2.0 ** zoom 136 | xtile = int((lon_deg + 180.0) / 360.0 * n) 137 | ytile = int((1.0 - math.log(math.tan(lat_rad) + 138 | (1 / math.cos(lat_rad))) / math.pi) / 2.0 * n) 139 | return (xtile, ytile) 140 | 141 | def num2deg(self, xtile, ytile, zoom): 142 | n = 2.0 ** zoom 143 | lon_deg = xtile / n * 360.0 - 180.0 144 | lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n))) 145 | lat_deg = math.degrees(lat_rad) 146 | return (lat_deg, lon_deg) 147 | 148 | # zoom 149 | @property 150 | def zoom(self): 151 | return self.__zoom 152 | 153 | @zoom.setter 154 | def zoom(self, value): 155 | self.__zoom = value 156 | 157 | # delta_long 158 | @property 159 | def delta_long(self): 160 | return self.__delta_long 161 | 162 | @delta_long.setter 163 | def delta_long(self, value): 164 | self.__delta_long = value 165 | 166 | # delta_lat 167 | @property 168 | def delta_lat(self): 169 | return self.__delta_lat 170 | 171 | @delta_lat.setter 172 | def delta_lat(self, value): 173 | self.__delta_lat = value 174 | 175 | # latitude 176 | @property 177 | def latitude(self): 178 | return self.__latitude 179 | 180 | @latitude.setter 181 | def latitude(self, value): 182 | self.__latitude = value 183 | 184 | # longtitude 185 | @property 186 | def longtitude(self): 187 | return self.__longtitude 188 | 189 | @longtitude.setter 190 | def longtitude(self, value): 191 | self.__longtitude = value 192 | 193 | # original_size 194 | @property 195 | def original_size(self): 196 | return self.__original_size 197 | 198 | @original_size.setter 199 | def original_size(self, value): 200 | self.__original_size = value 201 | 202 | # isolated 203 | @property 204 | def isolated(self): 205 | return self.__isolated 206 | 207 | @isolated.setter 208 | def isolated(self, value): 209 | self.__isolated = value 210 | 211 | # maintain_aspect_ratio 212 | @property 213 | def maintain_aspect_ratio(self) -> bool: 214 | return self.__maintain_aspect_ratio 215 | 216 | @maintain_aspect_ratio.setter 217 | def maintain_aspect_ratio(self, value: bool): 218 | self.__maintain_aspect_ratio = value 219 | 220 | # transparent 221 | @property 222 | def transparent(self) -> bool: 223 | return self.__transparent 224 | 225 | @transparent.setter 226 | def transparent(self, value: bool): 227 | self.__transparent = value 228 | -------------------------------------------------------------------------------- /flet_contrib/flet_map/tests/flet_map_show_map.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | from flet_contrib.flet_map import FletMap 4 | 5 | def main(page: ft.Page): 6 | 7 | page.add( 8 | ft.ListView( 9 | expand=True, 10 | controls=[ 11 | FletMap(expand=True, latitude=40.766666, 12 | longtitude=29.916668,zoom=15,screenView = [8,4],) 13 | ] 14 | )) 15 | 16 | # page.add(ft.FletMap(expand=True)) 17 | 18 | 19 | if __name__ == '__main__': 20 | # FletMap() 21 | ft.app(target=main) 22 | -------------------------------------------------------------------------------- /flet_contrib/flexible_slider/README.md: -------------------------------------------------------------------------------- 1 | # VerticalSlider 2 | 3 | `FlexibleSlider` can be configured to be horizontal or vertical. 4 | 5 | `FlexibleSlider` inherits from [`GestureDetector`](https://flet.dev/docs/controls/gesturedetector). 6 | 7 | ## Examples 8 | 9 | [Live example](https://flet-controls-gallery.fly.dev/contrib/flexibleslider) 10 | 11 | ### Vertical slider example 12 | 13 | ```python 14 | import flet as ft 15 | 16 | from flet_contrib.flexible_slider import FlexibleSlider 17 | 18 | 19 | def main(page: ft.Page): 20 | def vertical_slider_changed(): 21 | print(vertical_slider.value) 22 | 23 | vertical_slider = FlexibleSlider( 24 | vertical=True, 25 | divisions=10, 26 | min=100, 27 | max=600, 28 | value=500, 29 | on_change=vertical_slider_changed, 30 | ) 31 | 32 | page.add( 33 | vertical_slider, 34 | ) 35 | 36 | 37 | ft.app(target=main) 38 | ``` 39 | 40 | ## Properties 41 | 42 | ### `vertical` 43 | 44 | If `vertical` property is `False`, the slider will be displayed horuzontally and if it set to `True`, the slider will be displated vertically. 45 | 46 | The detault value is `False`. 47 | 48 | ### `length` 49 | 50 | Length of the slider track in virtual pixels. 51 | 52 | The default value is `200`. 53 | 54 | ### `thickness` 55 | 56 | Thickness of the slider track in virtual pixels. 57 | 58 | The default value is `5`. 59 | 60 | ### `active_color` 61 | 62 | The [color](https://flet.dev/docs/guides/python/colors/) to use for the portion of the slider track that is active. 63 | 64 | The "active" side of the slider is the side between the thumb and the minimum value. 65 | 66 | The default value is `ft.Colors.PRIMARY`. 67 | 68 | ### `inactive_color` 69 | 70 | The [color](https://flet.dev/docs/guides/python/colors/) for the inactive portion of the slider track. 71 | 72 | The "inactive" side of the slider is the side between the thumb and the maximum value. 73 | 74 | The default value is `ft.Colors.OUTLINE_VARIANT`. 75 | 76 | ### `divisions` 77 | 78 | The number of discrete divisions. 79 | 80 | If not set, the slider is continuous. 81 | 82 | ### `division_active_color` 83 | 84 | The [color](https://flet.dev/docs/guides/python/colors/) to use for the division shapes displayed on the slider track that is active. 85 | 86 | The default value is `ft.Colors.OUTLINE`. 87 | 88 | ### `division_inactive_color` 89 | 90 | The [color](https://flet.dev/docs/guides/python/colors/) to use for the division shapes displayed on the slider track that is inactive. 91 | 92 | The default value is `ft.Colors.PRIMARY_CONTAINER`. 93 | 94 | ### `thumb_radius` 95 | 96 | Thumb radius in virtual pixels. 97 | 98 | The default value is `10`. 99 | 100 | 101 | ### `thumb_color` 102 | 103 | The color of the thumb. 104 | 105 | The default value is `ft.Colors.PRIMARY`. 106 | 107 | ### `value` 108 | 109 | The currently selected value for this slider. 110 | 111 | The slider's thumb is drawn at a position that corresponds to this value. 112 | 113 | ### `min` 114 | 115 | The minimum value the user can select. 116 | 117 | Defaults to `0.0`. Must be less than or equal to max. 118 | 119 | ### `max` 120 | 121 | The maximum value the user can select. 122 | 123 | Defaults to `1.0`. Must be greater than or equal to min. 124 | 125 | ## Events 126 | 127 | ### `on_change` 128 | 129 | Fires when the value of the Slider is changed. -------------------------------------------------------------------------------- /flet_contrib/flexible_slider/__init__.py: -------------------------------------------------------------------------------- 1 | from flet_contrib.flexible_slider.src.flexible_slider import ( 2 | FlexibleSlider, 3 | ) 4 | -------------------------------------------------------------------------------- /flet_contrib/flexible_slider/examples/horizontal_slider_example.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | from flet_contrib.flexible_slider import FlexibleSlider 4 | 5 | 6 | def main(page: ft.Page): 7 | def horizontal_slider_changed(): 8 | print(horizontal_slider.value) 9 | 10 | horizontal_slider = FlexibleSlider( 11 | min=100, 12 | max=600, 13 | value=250, 14 | thickness=10, 15 | length=200, 16 | active_color=ft.Colors.BLUE_500, 17 | inactive_color=ft.Colors.YELLOW_300, 18 | thumb_color=ft.Colors.GREEN, 19 | thumb_radius=20, 20 | on_change=horizontal_slider_changed, 21 | ) 22 | # horizontal_slider.content.bgcolor = ft.Colors.AMBER 23 | page.add( 24 | horizontal_slider, 25 | ) 26 | 27 | 28 | ft.app(target=main) 29 | -------------------------------------------------------------------------------- /flet_contrib/flexible_slider/examples/vertical_slider_example.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | from flet_contrib.flexible_slider import FlexibleSlider 4 | 5 | 6 | async def main(page: ft.Page): 7 | def vertical_slider_changed(): 8 | print(vertical_slider.value) 9 | 10 | vertical_slider = FlexibleSlider( 11 | vertical=True, 12 | divisions=5, 13 | on_change=vertical_slider_changed, 14 | ) 15 | 16 | page.add( 17 | vertical_slider, 18 | ) 19 | 20 | 21 | ft.app(target=main) 22 | -------------------------------------------------------------------------------- /flet_contrib/flexible_slider/src/flexible_slider.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | import flet.canvas as cv 3 | 4 | 5 | class FlexibleSlider(ft.GestureDetector): 6 | def __init__( 7 | self, 8 | on_change=None, 9 | vertical=False, 10 | length=200, 11 | thickness=5, 12 | value=0.0, 13 | min=0.0, 14 | max=1.0, 15 | thumb_radius=10, 16 | thumb_color=ft.Colors.PRIMARY, 17 | divisions=None, 18 | inactive_color=ft.Colors.OUTLINE_VARIANT, 19 | active_color=ft.Colors.PRIMARY, 20 | division_inactive_color=ft.Colors.PRIMARY_CONTAINER, 21 | division_active_color=ft.Colors.OUTLINE, 22 | ): 23 | super().__init__() 24 | self.value = value 25 | self.on_change = on_change 26 | self.vertical = vertical 27 | self.min = min 28 | self.max = max 29 | self.thickness = thickness 30 | self.length = length 31 | self.thumb_radius = thumb_radius 32 | self.thumb_color = thumb_color 33 | self.divisions = divisions 34 | self.track_color = inactive_color 35 | self.selected_track_color = active_color 36 | self.division_color_on_track = division_inactive_color 37 | self.division_color_on_selected = division_active_color 38 | self.content = self.generate_slider() 39 | self.on_hover = self.change_cursor 40 | self.on_pan_start = self.change_value_on_click 41 | self.on_pan_update = self.change_value_on_drag 42 | 43 | def generate_slider(self): 44 | c = ft.Container(content=cv.Canvas(shapes=self.generate_shapes())) 45 | if self.vertical: 46 | c.height = self.length + self.thumb_radius * 2 47 | c.width = max(self.thickness, self.thumb.radius * 2) 48 | else: 49 | c.width = self.length + self.thumb_radius * 2 50 | c.height = max(self.thickness, self.thumb_radius * 2) 51 | return c 52 | 53 | def generate_shapes(self): 54 | # thumb 55 | self.thumb = cv.Circle( 56 | radius=self.thumb_radius, 57 | paint=ft.Paint(color=self.thumb_color), 58 | ) 59 | 60 | self.track = cv.Rect( 61 | border_radius=self.thickness / 2, 62 | paint=ft.Paint(color=self.track_color), 63 | ) 64 | self.selected_track = cv.Rect( 65 | border_radius=self.thickness / 2, 66 | paint=ft.Paint(color=self.selected_track_color), 67 | ) 68 | 69 | if self.vertical: 70 | self.thumb.x = self.thumb_radius 71 | self.thumb.y = self.get_position(self.value) 72 | self.track.x = self.thumb_radius - self.thickness / 2 73 | self.track.y = self.thumb_radius 74 | self.track.width = self.thickness 75 | self.track.height = self.length 76 | self.selected_track.x = self.thumb_radius - self.thickness / 2 77 | self.selected_track.y = self.thumb.y 78 | self.selected_track.width = self.thickness 79 | self.selected_track.height = self.length + self.thumb_radius - self.thumb.y 80 | 81 | else: 82 | self.thumb.x = self.get_position(self.value) 83 | self.thumb.y = self.thumb_radius 84 | self.track.x = self.thumb_radius 85 | self.track.y = self.thumb_radius - self.thickness / 2 86 | self.track.width = self.length 87 | self.track.height = self.thickness 88 | self.selected_track.x = self.thumb_radius 89 | self.selected_track.y = self.thumb_radius - self.thickness / 2 90 | self.selected_track.width = ( 91 | self.value * self.length / (self.max - self.min) + self.thumb_radius 92 | ) 93 | self.selected_track.height = self.thickness 94 | 95 | self.generate_divisions() 96 | shapes = [self.track, self.selected_track] + self.division_shapes + [self.thumb] 97 | return shapes 98 | 99 | def generate_divisions(self): 100 | self.division_shapes = [] 101 | if self.divisions == None: 102 | return 103 | else: 104 | if self.divisions > 1: 105 | for i in range(1, self.divisions): 106 | if self.vertical: 107 | y = ( 108 | self.length 109 | + self.thumb_radius 110 | - (self.length / self.divisions) * i 111 | ) 112 | if y > self.get_position(self.value): 113 | color = self.division_color_on_selected 114 | else: 115 | color = self.division_color_on_track 116 | self.division_shapes.append( 117 | cv.Circle( 118 | y=y, 119 | x=self.thumb_radius, 120 | radius=self.thickness / 4, 121 | paint=ft.Paint(color=color), 122 | ) 123 | ) 124 | else: 125 | x = (self.length / self.divisions) * i + self.thumb_radius 126 | if x < self.selected_track.width + self.thumb_radius: 127 | color = self.division_color_on_selected 128 | else: 129 | color = self.division_color_on_track 130 | self.division_shapes.append( 131 | cv.Circle( 132 | x=x, 133 | y=self.thumb_radius, 134 | radius=self.thickness / 4, 135 | paint=ft.Paint(color=color), 136 | ) 137 | ) 138 | 139 | def update_divisions(self): 140 | for division_shape in self.division_shapes: 141 | if self.vertical: 142 | if ( 143 | division_shape.y 144 | < self.length - self.selected_track.height + self.thumb_radius 145 | ): 146 | color = self.division_color_on_track 147 | else: 148 | color = self.division_color_on_selected 149 | else: 150 | if division_shape.x < self.selected_track.width + self.thumb_radius: 151 | color = self.division_color_on_selected 152 | else: 153 | color = self.division_color_on_track 154 | division_shape.paint.color = color 155 | 156 | def find_closest_division_shape_position(self, position): 157 | if self.vertical: 158 | previous_y = self.thumb_radius + self.length 159 | for division_shape in self.division_shapes: 160 | if position < division_shape.y: 161 | previous_y = division_shape.y 162 | else: 163 | if abs(previous_y - position) < abs(division_shape.y - position): 164 | return previous_y 165 | else: 166 | return division_shape.y 167 | 168 | if abs(self.thumb_radius - position) < abs(previous_y - position): 169 | return self.thumb_radius 170 | else: 171 | return previous_y 172 | else: 173 | previous_x = self.thumb_radius 174 | for division_shape in self.division_shapes: 175 | if position > division_shape.x: 176 | previous_x = division_shape.x 177 | else: 178 | if abs(position - previous_x) < abs(division_shape.x - position): 179 | return previous_x 180 | else: 181 | return division_shape.x 182 | if abs(previous_x - position) < abs( 183 | self.length + self.thumb_radius - position 184 | ): 185 | return previous_x 186 | else: 187 | return self.length + self.thumb_radius 188 | 189 | def get_value(self, position): 190 | if self.vertical: 191 | return self.max - ( 192 | (position - self.thumb_radius) * (self.max - self.min) / self.length 193 | ) 194 | else: 195 | return (position - self.thumb_radius) * ( 196 | self.max - self.min 197 | ) / self.length + self.min 198 | 199 | def get_position(self, value): 200 | if self.vertical: 201 | return self.thumb_radius + ((self.max - value) * self.length) / ( 202 | self.max - self.min 203 | ) 204 | else: 205 | return value * self.length / (self.max - self.min) + self.thumb_radius 206 | 207 | def change_cursor(self, e: ft.HoverEvent): 208 | e.control.mouse_cursor = ft.MouseCursor.CLICK 209 | e.control.update() 210 | 211 | def move_thumb(self, position): 212 | self.value = self.get_value(position) 213 | # print(f"Value: {self.value}") 214 | if self.vertical: 215 | self.selected_track.y = position 216 | self.selected_track.height = ( 217 | self.track.height - position + self.thumb_radius 218 | ) 219 | self.thumb.y = position 220 | else: 221 | self.selected_track.width = position - self.thumb_radius 222 | self.thumb.x = position 223 | 224 | def update_slider(self, position): 225 | if self.divisions == None: 226 | self.move_thumb(position) 227 | else: 228 | discrete_position = self.find_closest_division_shape_position(position) 229 | self.move_thumb(discrete_position) 230 | self.update_divisions() 231 | if self.on_change is not None: 232 | self.on_change() 233 | self.update() 234 | 235 | def change_value_on_click(self, e: ft.DragStartEvent): 236 | if self.vertical: 237 | position = max( 238 | self.thumb_radius, min(e.local_y, self.length + self.thumb_radius) 239 | ) 240 | else: 241 | position = max( 242 | self.thumb_radius, min(e.local_x, self.length + self.thumb_radius) 243 | ) 244 | self.update_slider(position) 245 | self.update() 246 | 247 | def change_value_on_drag(self, e: ft.DragUpdateEvent): 248 | if self.vertical: 249 | position = max( 250 | self.thumb_radius, 251 | min(e.local_y + e.delta_y, self.length + self.thumb_radius), 252 | ) 253 | else: 254 | position = max( 255 | self.thumb_radius, 256 | min(e.local_x + e.delta_x, self.length + self.thumb_radius), 257 | ) 258 | self.update_slider(position) 259 | self.update() 260 | -------------------------------------------------------------------------------- /flet_contrib/shimmer/README.md: -------------------------------------------------------------------------------- 1 | # Shimmer Loading Effect 2 | `Shimmer` control is used to create shimmer loading effect. 3 | 4 | Also this control has the ability to auto generate dummy boxes that appears in the shimmer effect. To achieve it we have pass `auto_generate = True` to Shimmer and set `data = 'shimmer_load` to all those controls for which we want to create dummy boxes. 5 | 6 | This control can create shimmer effect either individually for one control or commonly for whole page. 7 | 8 | This control can also be assigned to `page.splash`. 9 | 10 | This control also accept custom dummy boxes. We just need to pass a custom created dummy box to `control` parameter and set `auto_generate = False` 11 | 12 | When height and width for shimmer effect is not given, Shimmer takes the size of given control. 13 | 14 | Use params `color1` and `color2` to create dual color shimmer effect. 15 | Use param `color` to create effect with variants of same root color. 16 | 17 | ## Example - Async, Common effect 18 | 19 | ```python 20 | import asyncio 21 | import flet as ft 22 | from flet_contrib.shimmer import Shimmer 23 | async def main(page: ft.Page): 24 | page.vertical_alignment = ft.MainAxisAlignment.CENTER 25 | page.horizontal_alignment = ft.CrossAxisAlignment.CENTER 26 | 27 | holder = ft.Container() 28 | await page.add_async(holder) 29 | lt = ft.ListTile( 30 | leading = ft.Icon(ft.icons.ALBUM, data = 'shimmer_load'), # data = 'shimmer_load' inform the Shimmer class to create dummy for this control 31 | title = ft.Text("The Enchanted Nightingale", data = 'shimmer_load'), 32 | subtitle = ft.Text("Music by Julie Gable. Lyrics by Sidney Stein.", data = 'shimmer_load') 33 | ) 34 | row = ft.Row( 35 | alignment = ft.MainAxisAlignment.END, 36 | controls= [ 37 | ft.TextButton("Buy tickets", data = 'shimmer_load'), 38 | ft.TextButton("Listen", data = 'shimmer_load') 39 | ] 40 | ) 41 | column = ft.Column( 42 | controls = [lt, row] 43 | ) 44 | container = ft.Container( 45 | content = column, 46 | height = 130, 47 | width = 400, 48 | padding = 10 49 | ) 50 | card = ft.Card( 51 | content = container 52 | ) 53 | ctrl = ft.Column([card for i in range(5)]) 54 | 55 | dummy = Shimmer(control=ctrl, auto_generate= True) # passing ctrl to Shimmer 56 | holder.content = dummy # can also use page.splash in place of holder 57 | await holder.update_async() 58 | await asyncio.sleep(3) # assume this to be any data fetching task 59 | holder.content = ctrl 60 | await holder.update_async() 61 | 62 | ft.app(target=main) 63 | ``` 64 | 65 | ## Output of above code 66 | https://github.com/lekshmanmj/flet-contrib/assets/35882740/9226586f-2c46-4483-8ba1-d29fe5ec427b 67 | 68 | 69 | -------------------------------------------------------------------------------- /flet_contrib/shimmer/__init__.py: -------------------------------------------------------------------------------- 1 | from flet_contrib.shimmer.src.shimmer import Shimmer 2 | -------------------------------------------------------------------------------- /flet_contrib/shimmer/examples/shimmer_async_common_shimmer.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import flet as ft 3 | from flet_contrib.shimmer import Shimmer 4 | 5 | 6 | async def main(page: ft.Page): 7 | page.vertical_alignment = ft.MainAxisAlignment.CENTER 8 | page.horizontal_alignment = ft.CrossAxisAlignment.CENTER 9 | 10 | holder = ft.Container() 11 | page.add(holder) 12 | lt = ft.ListTile( 13 | leading=ft.Icon( 14 | ft.icons.ALBUM, data="shimmer_load" 15 | ), # data = 'shimmer_load' inform the Shimmer class to create dummy for this control 16 | title=ft.Text("The Enchanted Nightingale", data="shimmer_load"), 17 | subtitle=ft.Text( 18 | "Music by Julie Gable. Lyrics by Sidney Stein.", data="shimmer_load" 19 | ), 20 | ) 21 | row = ft.Row( 22 | alignment=ft.MainAxisAlignment.END, 23 | controls=[ 24 | ft.TextButton("Buy tickets", data="shimmer_load"), 25 | ft.TextButton("Listen", data="shimmer_load"), 26 | ], 27 | ) 28 | column = ft.Column(controls=[lt, row]) 29 | container = ft.Container(content=column, height=130, width=400, padding=10) 30 | card = ft.Card(content=container) 31 | ctrl = ft.Column([card for i in range(5)]) 32 | 33 | dummy = Shimmer(control=ctrl, auto_generate=True) # passing ctrl to Shimmer 34 | holder.content = dummy # can also use page.splash in place of holder 35 | holder.update() 36 | await asyncio.sleep(3) # assume this to be any data fetching task 37 | holder.content = ctrl 38 | holder.update() 39 | 40 | 41 | ft.app(target=main) 42 | -------------------------------------------------------------------------------- /flet_contrib/shimmer/examples/shimmer_async_individual_ctrl_effect.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import flet as ft 3 | from flet_contrib.shimmer import Shimmer 4 | 5 | 6 | async def main(page: ft.Page): 7 | page.vertical_alignment = ft.MainAxisAlignment.CENTER 8 | page.horizontal_alignment = ft.CrossAxisAlignment.CENTER 9 | page.theme_mode = ft.ThemeMode.LIGHT 10 | 11 | holder = ft.Container() 12 | page.add(holder) 13 | lt = ft.ListTile( 14 | leading=ft.Icon(ft.icons.ALBUM, data="shimmer_load"), 15 | title=ft.Text("The Enchanted Nightingale", data="shimmer_load"), 16 | subtitle=ft.Text( 17 | "Music by Julie Gable. Lyrics by Sidney Stein.", data="shimmer_load" 18 | ), 19 | ) 20 | row = ft.Row( 21 | alignment=ft.MainAxisAlignment.END, 22 | controls=[ 23 | ft.TextButton("Buy tickets", data="shimmer_load"), 24 | ft.TextButton("Listen", data="shimmer_load"), 25 | ], 26 | ) 27 | column = ft.Column(controls=[lt, row]) 28 | container = ft.Container(content=column, height=130, width=400, padding=10) 29 | ctrl = ft.Card(content=container) 30 | 31 | temp = [] 32 | for i in range(5): # individual mode shimmer effect 33 | temp.append( 34 | Shimmer( 35 | ref=ft.Ref[ft.ShaderMask](), 36 | control=ctrl, 37 | height=ctrl.height, 38 | width=ctrl.width, 39 | auto_generate=True, 40 | ) 41 | ) 42 | holder.content = ft.Column(temp) 43 | holder.update() 44 | 45 | await asyncio.sleep(6) # assume this to be some data fetching task 46 | 47 | holder.content = ft.Column([ctrl for each in range(5)]) 48 | holder.update() 49 | 50 | 51 | ft.app(target=main) 52 | -------------------------------------------------------------------------------- /flet_contrib/shimmer/examples/shimmer_sync_mode_common_effect.py: -------------------------------------------------------------------------------- 1 | import time 2 | import flet as ft 3 | from flet_contrib.shimmer import Shimmer 4 | 5 | 6 | def main(page: ft.Page): 7 | page.vertical_alignment = ft.MainAxisAlignment.CENTER 8 | page.horizontal_alignment = ft.CrossAxisAlignment.CENTER 9 | page.theme_mode = ft.ThemeMode.LIGHT 10 | 11 | holder = ft.Container() 12 | page.add(holder) 13 | lt = ft.ListTile( 14 | leading=ft.Icon(ft.icons.ALBUM, data="shimmer_load"), 15 | title=ft.Text("The Enchanted Nightingale", data="shimmer_load"), 16 | subtitle=ft.Text( 17 | "Music by Julie Gable. Lyrics by Sidney Stein.", data="shimmer_load" 18 | ), 19 | ) 20 | row = ft.Row( 21 | alignment=ft.MainAxisAlignment.END, 22 | controls=[ 23 | ft.TextButton("Buy tickets", data="shimmer_load"), 24 | ft.TextButton("Listen", data="shimmer_load"), 25 | ], 26 | ) 27 | column = ft.Column(controls=[lt, row]) 28 | container = ft.Container(content=column, height=130, width=400, padding=10) 29 | ctrl = ft.Card(content=container) 30 | dummy = Shimmer(control=ctrl, auto_generate=True) 31 | 32 | holder.content = dummy 33 | holder.update() 34 | time.sleep(6) 35 | holder.content = ctrl 36 | holder.update() 37 | 38 | 39 | ft.app(target=main) 40 | -------------------------------------------------------------------------------- /flet_contrib/shimmer/media/shimmer_common_async.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flet-dev/flet-contrib/ee0c6666769fa8c60f894eea77d13e2eda6b4207/flet_contrib/shimmer/media/shimmer_common_async.mp4 -------------------------------------------------------------------------------- /flet_contrib/shimmer/src/shimmer.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import threading 3 | import time 4 | 5 | import flet as ft 6 | 7 | 8 | class Shimmer(ft.Container): 9 | def __init__( 10 | self, 11 | ref=None, 12 | control=None, 13 | color=None, 14 | color1=None, 15 | color2=None, 16 | height=None, 17 | width=None, 18 | auto_generate: bool = False, 19 | ) -> None: 20 | super().__init__() 21 | 22 | self.color = color 23 | self.color1 = color1 24 | self.color2 = color2 25 | self.height = height 26 | self.width = width 27 | 28 | if ref is None: 29 | self.ref = ft.Ref[ft.ShaderMask]() 30 | else: 31 | self.ref = ref 32 | 33 | if self.color1 is None and self.color2 is None and self.color is None: 34 | self.__color1 = ft.Colors.BACKGROUND 35 | self.__color2 = ft.Colors.with_opacity(0.5, ft.Colors.BACKGROUND) 36 | elif self.color is not None: 37 | self.__color1 = self.color 38 | self.__color2 = ft.Colors.with_opacity(0.5, self.color) 39 | elif self.color1 is not None and self.color2 is not None: 40 | self.__color1 = self.color1 41 | self.__color2 = ft.Colors.with_opacity(0.5, self.color2) 42 | if auto_generate: 43 | self.control = self.create_dummy(control) 44 | else: 45 | self.control = control 46 | 47 | self.__stop_shine = False 48 | 49 | self.i = -0.1 50 | self.gap = 0.075 51 | 52 | def build(self): 53 | gradient = ft.LinearGradient( 54 | colors=[self.__color2, self.__color1, self.__color2], 55 | stops=[ 56 | 0 + self.i - self.gap, 57 | self.i, 58 | self.gap + self.i, 59 | ], 60 | begin=ft.alignment.top_left, 61 | end=ft.alignment.bottom_right, 62 | ) 63 | 64 | self.__shadermask = ft.ShaderMask( 65 | ref=self.ref, 66 | content=self.control, 67 | blend_mode=ft.BlendMode.DST_IN, 68 | height=self.height, 69 | width=self.width, 70 | shader=gradient, 71 | ) 72 | 73 | self.content = self.__shadermask 74 | self.bgcolor = self.__color1 75 | 76 | async def shine_async(self): 77 | try: 78 | while self.i <= 5: 79 | gradient = ft.LinearGradient( 80 | colors=[self.__color2, self.__color1, self.__color2], 81 | stops=[ 82 | 0 + self.i - self.gap, 83 | self.i, 84 | self.gap + self.i, 85 | ], 86 | begin=ft.alignment.top_left, 87 | end=ft.alignment.bottom_right, 88 | ) 89 | self.ref.current.shader = gradient 90 | self.ref.current.update() 91 | self.i += 0.02 92 | if self.i >= 1.1: 93 | self.i = -0.1 94 | await asyncio.sleep(0.4) 95 | await asyncio.sleep(0.01) 96 | except: 97 | pass 98 | 99 | def create_dummy(self, target=None): 100 | opacity = 0.1 101 | color = ft.Colors.ON_PRIMARY_CONTAINER 102 | circle = lambda size=60: ft.Container( 103 | height=size, 104 | width=size, 105 | bgcolor=ft.Colors.with_opacity(opacity, color), 106 | border_radius=size, 107 | ) 108 | rectangle = lambda height, content=None: ft.Container( 109 | content=content, 110 | height=height, 111 | width=height * 2.5, 112 | bgcolor=ft.Colors.with_opacity(opacity, color), 113 | border_radius=20, 114 | alignment=ft.alignment.bottom_center, 115 | padding=20, 116 | ) 117 | tube = lambda width: ft.Container( 118 | height=10, 119 | width=width, 120 | bgcolor=ft.Colors.with_opacity(opacity, color), 121 | border_radius=20, 122 | expand=0, 123 | ) 124 | 125 | if target is None: 126 | target = self.control 127 | controls, content, title, subtitle, leading, trailing = ( 128 | False, 129 | False, 130 | False, 131 | False, 132 | False, 133 | False, 134 | ) 135 | ctrl_name = target._get_control_name() 136 | for key in list(ft.__dict__.keys())[::-1]: 137 | if key.lower() == ctrl_name and key != ctrl_name: 138 | dummy = ft.__dict__[key]() 139 | 140 | if ctrl_name in ["text"] and target.data == "shimmer_load": 141 | dummy = tube(len(target.__dict__["_Control__attrs"]["value"][0]) * 7.5) 142 | elif ctrl_name in ["textbutton"] and target.data == "shimmer_load": 143 | dummy = rectangle(40) 144 | elif ctrl_name in ["icon"] and target.data == "shimmer_load": 145 | dummy = circle(30) 146 | elif ctrl_name in ["image"] and target.data == "shimmer_load": 147 | dummy = ft.Container( 148 | bgcolor=ft.Colors.with_opacity(opacity, color), expand=True 149 | ) 150 | elif ctrl_name in ["image"]: 151 | dummy = ft.Container(expand=True) 152 | 153 | for key in list(target.__dict__.keys())[::-1]: 154 | if ( 155 | key.lower().split("__")[-1] == "controls" 156 | and target.__dict__[key] is not None 157 | ): 158 | controls = True 159 | elif ( 160 | key.lower().split("__")[-1] == "content" 161 | and target.__dict__[key] is not None 162 | ): 163 | content = True 164 | elif ( 165 | key.lower().split("__")[-1] == "title" 166 | and target.__dict__[key] is not None 167 | ): 168 | title = True 169 | elif ( 170 | key.lower().split("__")[-1] == "subtitle" 171 | and target.__dict__[key] is not None 172 | ): 173 | subtitle = True 174 | elif ( 175 | key.lower().split("__")[-1] == "leading" 176 | and target.__dict__[key] is not None 177 | ): 178 | leading = True 179 | elif ( 180 | key.lower().split("__")[-1] == "trailing" 181 | and target.__dict__[key] is not None 182 | ): 183 | trailing = True 184 | 185 | ctrl_attrs = target.__dict__["_Control__attrs"] 186 | if ctrl_attrs is not None: 187 | for each_pos in ctrl_attrs.keys(): 188 | if each_pos not in [ 189 | "text", 190 | "value", 191 | "label", 192 | "foregroundimageurl", 193 | "bgcolor", 194 | "name", 195 | "color", 196 | "icon", 197 | "src", 198 | "src_base64", 199 | ]: 200 | try: 201 | dummy._set_attr(each_pos, ctrl_attrs[each_pos][0]) 202 | except Exception as e: 203 | print("EXCEPTION", e, ctrl_name, each_pos) 204 | 205 | for each_pos in target.__dict__: 206 | if target.__dict__[each_pos] is not None: 207 | pos = each_pos.split("__")[-1] 208 | if pos == "rotate": 209 | dummy.rotate = target.__dict__[each_pos] 210 | elif pos == "scale": 211 | dummy.scale = target.__dict__[each_pos] 212 | elif pos == "border_radius": 213 | dummy.border_radius = target.__dict__[each_pos] 214 | elif pos == "alignment": 215 | dummy.alignment = target.__dict__[each_pos] 216 | elif pos == "padding": 217 | dummy.padding = target.__dict__[each_pos] 218 | elif pos == "horizontal_alignment": 219 | dummy.horizontal_alignment = target.__dict__[each_pos] 220 | elif pos == "vertical_alignment": 221 | dummy.vertical_alignment = target.__dict__[each_pos] 222 | elif pos == "top": 223 | dummy.top = target.__dict__[each_pos] 224 | elif pos == "bottom": 225 | dummy.bottom = target.__dict__[each_pos] 226 | elif pos == "left": 227 | dummy.left = target.__dict__[each_pos] 228 | elif pos == "right": 229 | dummy.right = target.__dict__[each_pos] 230 | elif pos == "rows": 231 | dummy.rows = [ 232 | ft.DataRow( 233 | [ 234 | ( 235 | ft.DataCell(tube(100)) 236 | if each_col.content.data == "shimmer_load" 237 | else ft.DataCell(ft.Text()) 238 | ) 239 | for each_col in each_control.cells 240 | ] 241 | ) 242 | for each_control in target.__dict__[each_pos] 243 | ] 244 | elif pos == "columns": 245 | dummy.columns = [ 246 | ( 247 | ft.DataColumn(tube(100)) 248 | if each_control.label.data == "shimmer_load" 249 | else ft.DataColumn(ft.Text()) 250 | ) 251 | for each_control in target.__dict__[each_pos] 252 | ] 253 | 254 | if content: 255 | dummy.content = self.create_dummy(target.content) 256 | if title: 257 | dummy.title = self.create_dummy(target.title) 258 | if subtitle: 259 | dummy.subtitle = self.create_dummy(target.subtitle) 260 | if leading: 261 | dummy.leading = self.create_dummy(target.leading) 262 | if trailing: 263 | dummy.trailing = self.create_dummy(target.trailing) 264 | if controls: 265 | try: 266 | dummy.controls = [ 267 | self.create_dummy(each_control) for each_control in target.controls 268 | ] 269 | except Exception as e: 270 | print(e) 271 | temp = [] 272 | for each_control in target.controls: 273 | try: 274 | temp.append(self.create_dummy(each_control)) 275 | except Exception as e: 276 | pass 277 | dummy.controls = temp 278 | 279 | if target.data == "shimmer_load": 280 | dummy.bgcolor = ft.Colors.with_opacity(opacity, color) 281 | return ft.Container(ft.Stack([dummy]), bgcolor=self.__color1) 282 | 283 | def did_mount(self): 284 | self.task = self.page.run_task(self.shine_async) 285 | 286 | def will_unmount(self): 287 | self.task.cancel() 288 | -------------------------------------------------------------------------------- /flet_contrib/vertical_splitter/README.md: -------------------------------------------------------------------------------- 1 | # VerticalSplitter 2 | 3 | `VerticalSplitter` control is used for building layout with left and right panes divided by a vertical line that can be dragged in the left and/or right direction. 4 | 5 | `VerticalSplitter` inherits from [`Row`](https://flet.dev/docs/controls/row). 6 | 7 | ## Examples 8 | 9 | [Live example](https://flet-controls-gallery.fly.dev/contrib/verticalsplitter) 10 | 11 | ### VerticalSplitter example 12 | 13 | ```python 14 | import flet as ft 15 | 16 | from flet_contrib.vertical_splitter import VerticalSplitter, FixedPane 17 | 18 | 19 | def main(page: ft.Page): 20 | c_left = ft.Container(bgcolor=ft.Colors.BLUE_400) 21 | 22 | c_right = ft.Container(bgcolor=ft.Colors.YELLOW_400) 23 | 24 | vertical_splitter = VerticalSplitter( 25 | # height=400, 26 | expand=True, 27 | right_pane=c_right, 28 | left_pane=c_left, 29 | fixed_pane_min_width=200, 30 | fixed_pane_width=300, 31 | fixed_pane_max_width=400, 32 | fixed_pane=FixedPane.RIGHT, 33 | ) 34 | 35 | page.add(vertical_splitter) 36 | 37 | 38 | ft.app(target=main) 39 | ``` 40 | 41 | ## Properties 42 | 43 | ### `left_pane` 44 | 45 | A child Control contained by the left pane of the vertical splitter. 46 | 47 | ### `right_pane` 48 | 49 | A child Control contained by the right pane of the vertical splitter. 50 | 51 | ### `fixed_pane` 52 | 53 | Configures which pane will have a `fixed_pane_width`, `fixed_pane_minumum_width` and `fixed_pane_maximum_width` properties, while the other pane will have `expand` property set to `True` and will take up the remainer of the VerticalSpliitter width. The value must be an instance of the `FixedPane` class: 54 | 55 | ``` 56 | vertical_splitter.fixed_pane = FixedPane.RIGHT 57 | ``` 58 | The default value is `FixedPane.LEFT`. 59 | 60 | ### `fixed_pane_width` 61 | 62 | Width in virtual pixels of `left_pane` or `right_pane` container, depending on the `fixed_pane` property. 63 | 64 | The default value is `100`. 65 | 66 | ### `fixed_pane_min_width` 67 | 68 | Minimum width in virtual pixels of `left_pane` or `right_pane` container when dragging the splitter, depending on the `fixed_pane` property. 69 | 70 | The default value is `50`. 71 | 72 | 73 | ### `fixed_pane_max_width` 74 | 75 | Maximum width in virtual pixels of `left_pane` or `right_pane` container when dragging the splitter, depending on the `fixed_pane` property. 76 | 77 | The default value is `200`. -------------------------------------------------------------------------------- /flet_contrib/vertical_splitter/__init__.py: -------------------------------------------------------------------------------- 1 | from flet_contrib.vertical_splitter.src.vertical_splitter import ( 2 | VerticalSplitter, 3 | FixedPane, 4 | ) 5 | -------------------------------------------------------------------------------- /flet_contrib/vertical_splitter/examples/vertical_splitter_with_containers.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | from flet_contrib.vertical_splitter import FixedPane, VerticalSplitter 4 | 5 | 6 | def main(page: ft.Page): 7 | c_left = ft.Container(bgcolor=ft.Colors.BLUE_400) 8 | 9 | c_right = ft.Container(bgcolor=ft.Colors.YELLOW_400) 10 | 11 | vertical_splitter = VerticalSplitter( 12 | # height=400, 13 | expand=True, 14 | right_pane=c_right, 15 | left_pane=c_left, 16 | fixed_pane_min_width=200, 17 | fixed_pane_width=300, 18 | fixed_pane_max_width=400, 19 | fixed_pane=FixedPane.RIGHT, 20 | ) 21 | 22 | page.add(vertical_splitter) 23 | 24 | 25 | ft.app(target=main) 26 | -------------------------------------------------------------------------------- /flet_contrib/vertical_splitter/examples/vertical_splitter_with_navigationrail_and_text.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | from flet_contrib.vertical_splitter import FixedPane, VerticalSplitter 4 | 5 | 6 | def main(page: ft.Page): 7 | c_left = ft.NavigationRail( 8 | selected_index=0, 9 | label_type=ft.NavigationRailLabelType.ALL, 10 | leading=ft.FloatingActionButton(icon=ft.icons.CREATE, text="Add"), 11 | group_alignment=-0.9, 12 | destinations=[ 13 | ft.NavigationRailDestination( 14 | icon=ft.icons.FAVORITE_BORDER, 15 | selected_icon=ft.icons.FAVORITE, 16 | label="First", 17 | ), 18 | ft.NavigationRailDestination( 19 | icon_content=ft.Icon(ft.icons.BOOKMARK_BORDER), 20 | selected_icon_content=ft.Icon(ft.icons.BOOKMARK), 21 | label="Second", 22 | ), 23 | ft.NavigationRailDestination( 24 | icon=ft.icons.SETTINGS_OUTLINED, 25 | selected_icon_content=ft.Icon(ft.icons.SETTINGS), 26 | label_content=ft.Text("Settings"), 27 | ), 28 | ], 29 | on_change=lambda e: print("Selected destination:", e.control.selected_index), 30 | ) 31 | 32 | c_right = ft.Column( 33 | controls=[ 34 | ft.Text("Display Large", style=ft.TextThemeStyle.DISPLAY_LARGE), 35 | ft.Text("Display Medium", style=ft.TextThemeStyle.DISPLAY_MEDIUM), 36 | ft.Text("Display Small", style=ft.TextThemeStyle.DISPLAY_SMALL), 37 | ft.Text("Headline Large", style=ft.TextThemeStyle.HEADLINE_LARGE), 38 | ft.Text("Headline Medium", style=ft.TextThemeStyle.HEADLINE_MEDIUM), 39 | ft.Text("Headline Small", style=ft.TextThemeStyle.HEADLINE_MEDIUM), 40 | ft.Text("Title Large", style=ft.TextThemeStyle.TITLE_LARGE), 41 | ft.Text("Title Medium", style=ft.TextThemeStyle.TITLE_MEDIUM), 42 | ft.Text("Title Small", style=ft.TextThemeStyle.TITLE_SMALL), 43 | ft.Text("Label Large", style=ft.TextThemeStyle.LABEL_LARGE), 44 | ft.Text("Label Medium", style=ft.TextThemeStyle.LABEL_MEDIUM), 45 | ft.Text("Label Small", style=ft.TextThemeStyle.LABEL_SMALL), 46 | ft.Text("Body Large", style=ft.TextThemeStyle.BODY_LARGE), 47 | ft.Text("Body Medium", style=ft.TextThemeStyle.BODY_MEDIUM), 48 | ft.Text("Body Small", style=ft.TextThemeStyle.BODY_SMALL), 49 | ] 50 | ) 51 | 52 | vertical_splitter = VerticalSplitter( 53 | # height=400, 54 | expand=True, 55 | right_pane=c_right, 56 | left_pane=c_left, 57 | fixed_pane_min_width=70 58 | # fixed_pane=FixedPane.RIGHT, 59 | ) 60 | 61 | page.add(vertical_splitter) 62 | 63 | 64 | ft.app(target=main) 65 | -------------------------------------------------------------------------------- /flet_contrib/vertical_splitter/src/vertical_splitter.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Optional 3 | 4 | import flet as ft 5 | 6 | 7 | class FixedPane(Enum): 8 | RIGHT = "right" 9 | LEFT = "left" 10 | 11 | 12 | class VerticalSplitter(ft.Row): 13 | def __init__( 14 | self, 15 | right_pane: Optional[ft.Control], 16 | left_pane: Optional[ft.Control], 17 | spacing=0, 18 | fixed_pane_min_width=50, 19 | fixed_pane_max_width=200, 20 | fixed_pane_width=100, 21 | fixed_pane: FixedPane = FixedPane.LEFT, 22 | width=None, 23 | height=400, 24 | expand=False, 25 | ): 26 | super().__init__(width=width, height=height, spacing=spacing, expand=expand) 27 | self.fixed_pane_min_width = fixed_pane_min_width 28 | self.fixed_pane_max_width = fixed_pane_max_width 29 | self.fixed_pane_width = fixed_pane_width 30 | self.fixed_pane = fixed_pane 31 | self.splitter = ft.GestureDetector( 32 | content=ft.VerticalDivider(), 33 | drag_interval=10, 34 | on_pan_update=self.move_vertical_splitter, 35 | on_hover=self.show_draggable_cursor, 36 | ) 37 | self.generate_layout(left_pane, right_pane) 38 | 39 | def generate_layout(self, left_pane, right_pane): 40 | self.left_container = ft.Container(content=left_pane) 41 | self.right_container = ft.Container(content=right_pane) 42 | if self.fixed_pane == FixedPane.LEFT: 43 | self.left_container.width = self.fixed_pane_width 44 | self.right_container.expand = 1 45 | 46 | elif self.fixed_pane == FixedPane.RIGHT: 47 | self.right_container.width = self.fixed_pane_width 48 | self.left_container.expand = 1 49 | 50 | self.controls = [ 51 | self.left_container, 52 | self.splitter, 53 | self.right_container, 54 | ] 55 | 56 | def move_vertical_splitter(self, e: ft.DragUpdateEvent): 57 | if self.fixed_pane == FixedPane.LEFT: 58 | if e.control.mouse_cursor == ft.MouseCursor.RESIZE_LEFT_RIGHT and ( 59 | ( 60 | e.delta_x > 0 61 | and self.left_container.width + e.delta_x 62 | < self.fixed_pane_max_width 63 | ) 64 | or ( 65 | e.delta_x < 0 66 | and self.left_container.width + e.delta_x 67 | > self.fixed_pane_min_width 68 | ) 69 | ): 70 | self.left_container.width += e.delta_x 71 | self.left_container.update() 72 | 73 | if self.fixed_pane == FixedPane.RIGHT: 74 | if ( 75 | e.delta_x > 0 76 | and self.right_container.width - e.delta_x > self.fixed_pane_min_width 77 | ) or ( 78 | e.delta_x < 0 79 | and self.right_container.width - e.delta_x < self.fixed_pane_max_width 80 | ): 81 | self.right_container.width -= e.delta_x 82 | self.right_container.update() 83 | 84 | def show_draggable_cursor(self, e: ft.HoverEvent): 85 | e.control.mouse_cursor = ft.MouseCursor.RESIZE_LEFT_RIGHT 86 | e.control.update() 87 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "anyio" 5 | version = "4.5.2" 6 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, 11 | {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, 12 | ] 13 | 14 | [package.dependencies] 15 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 16 | idna = ">=2.8" 17 | sniffio = ">=1.1" 18 | typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} 19 | 20 | [package.extras] 21 | doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 22 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] 23 | trio = ["trio (>=0.26.1)"] 24 | 25 | [[package]] 26 | name = "black" 27 | version = "23.12.1" 28 | description = "The uncompromising code formatter." 29 | optional = false 30 | python-versions = ">=3.8" 31 | files = [ 32 | {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, 33 | {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, 34 | {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, 35 | {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, 36 | {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, 37 | {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, 38 | {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, 39 | {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, 40 | {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, 41 | {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, 42 | {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, 43 | {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, 44 | {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, 45 | {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, 46 | {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, 47 | {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, 48 | {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, 49 | {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, 50 | {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, 51 | {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, 52 | {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, 53 | {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, 54 | ] 55 | 56 | [package.dependencies] 57 | click = ">=8.0.0" 58 | mypy-extensions = ">=0.4.3" 59 | packaging = ">=22.0" 60 | pathspec = ">=0.9.0" 61 | platformdirs = ">=2" 62 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 63 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 64 | 65 | [package.extras] 66 | colorama = ["colorama (>=0.4.3)"] 67 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 68 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 69 | uvloop = ["uvloop (>=0.15.2)"] 70 | 71 | [[package]] 72 | name = "certifi" 73 | version = "2024.8.30" 74 | description = "Python package for providing Mozilla's CA Bundle." 75 | optional = false 76 | python-versions = ">=3.6" 77 | files = [ 78 | {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, 79 | {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, 80 | ] 81 | 82 | [[package]] 83 | name = "click" 84 | version = "8.1.7" 85 | description = "Composable command line interface toolkit" 86 | optional = false 87 | python-versions = ">=3.7" 88 | files = [ 89 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 90 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 91 | ] 92 | 93 | [package.dependencies] 94 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 95 | 96 | [[package]] 97 | name = "colorama" 98 | version = "0.4.6" 99 | description = "Cross-platform colored terminal text." 100 | optional = false 101 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 102 | files = [ 103 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 104 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 105 | ] 106 | 107 | [[package]] 108 | name = "exceptiongroup" 109 | version = "1.2.2" 110 | description = "Backport of PEP 654 (exception groups)" 111 | optional = false 112 | python-versions = ">=3.7" 113 | files = [ 114 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 115 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 116 | ] 117 | 118 | [package.extras] 119 | test = ["pytest (>=6)"] 120 | 121 | [[package]] 122 | name = "flet" 123 | version = "0.25.1" 124 | description = "Flet for Python - easily build interactive multi-platform apps in Python" 125 | optional = false 126 | python-versions = "<4.0,>=3.8" 127 | files = [ 128 | {file = "flet-0.25.1-py3-none-any.whl", hash = "sha256:4a11db637ebd0074bcb6197e1d0626e1f28839f7e021e91b0fcda057c2fb8739"}, 129 | ] 130 | 131 | [package.dependencies] 132 | httpx = {version = "*", markers = "platform_system != \"Pyodide\""} 133 | oauthlib = {version = ">=3.2.2,<4.0.0", markers = "platform_system != \"Pyodide\""} 134 | repath = ">=0.9.0,<0.10.0" 135 | 136 | [package.extras] 137 | all = ["flet-cli (==0.25.1)", "flet-desktop (==0.25.1)", "flet-desktop-light (==0.25.1)", "flet-web (==0.25.1)"] 138 | cli = ["flet-cli (==0.25.1)"] 139 | desktop = ["flet-desktop (==0.25.1)", "flet-desktop-light (==0.25.1)"] 140 | web = ["flet-web (==0.25.1)"] 141 | 142 | [[package]] 143 | name = "h11" 144 | version = "0.14.0" 145 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 146 | optional = false 147 | python-versions = ">=3.7" 148 | files = [ 149 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 150 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 151 | ] 152 | 153 | [[package]] 154 | name = "httpcore" 155 | version = "1.0.7" 156 | description = "A minimal low-level HTTP client." 157 | optional = false 158 | python-versions = ">=3.8" 159 | files = [ 160 | {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, 161 | {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, 162 | ] 163 | 164 | [package.dependencies] 165 | certifi = "*" 166 | h11 = ">=0.13,<0.15" 167 | 168 | [package.extras] 169 | asyncio = ["anyio (>=4.0,<5.0)"] 170 | http2 = ["h2 (>=3,<5)"] 171 | socks = ["socksio (==1.*)"] 172 | trio = ["trio (>=0.22.0,<1.0)"] 173 | 174 | [[package]] 175 | name = "httpx" 176 | version = "0.28.0" 177 | description = "The next generation HTTP client." 178 | optional = false 179 | python-versions = ">=3.8" 180 | files = [ 181 | {file = "httpx-0.28.0-py3-none-any.whl", hash = "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc"}, 182 | {file = "httpx-0.28.0.tar.gz", hash = "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0"}, 183 | ] 184 | 185 | [package.dependencies] 186 | anyio = "*" 187 | certifi = "*" 188 | httpcore = "==1.*" 189 | idna = "*" 190 | 191 | [package.extras] 192 | brotli = ["brotli", "brotlicffi"] 193 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 194 | http2 = ["h2 (>=3,<5)"] 195 | socks = ["socksio (==1.*)"] 196 | zstd = ["zstandard (>=0.18.0)"] 197 | 198 | [[package]] 199 | name = "idna" 200 | version = "3.10" 201 | description = "Internationalized Domain Names in Applications (IDNA)" 202 | optional = false 203 | python-versions = ">=3.6" 204 | files = [ 205 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 206 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 207 | ] 208 | 209 | [package.extras] 210 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 211 | 212 | [[package]] 213 | name = "mypy-extensions" 214 | version = "1.0.0" 215 | description = "Type system extensions for programs checked with the mypy type checker." 216 | optional = false 217 | python-versions = ">=3.5" 218 | files = [ 219 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 220 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 221 | ] 222 | 223 | [[package]] 224 | name = "oauthlib" 225 | version = "3.2.2" 226 | description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" 227 | optional = false 228 | python-versions = ">=3.6" 229 | files = [ 230 | {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, 231 | {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, 232 | ] 233 | 234 | [package.extras] 235 | rsa = ["cryptography (>=3.0.0)"] 236 | signals = ["blinker (>=1.4.0)"] 237 | signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] 238 | 239 | [[package]] 240 | name = "packaging" 241 | version = "24.2" 242 | description = "Core utilities for Python packages" 243 | optional = false 244 | python-versions = ">=3.8" 245 | files = [ 246 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 247 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 248 | ] 249 | 250 | [[package]] 251 | name = "pathspec" 252 | version = "0.12.1" 253 | description = "Utility library for gitignore style pattern matching of file paths." 254 | optional = false 255 | python-versions = ">=3.8" 256 | files = [ 257 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 258 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 259 | ] 260 | 261 | [[package]] 262 | name = "platformdirs" 263 | version = "4.3.6" 264 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 265 | optional = false 266 | python-versions = ">=3.8" 267 | files = [ 268 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 269 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 270 | ] 271 | 272 | [package.extras] 273 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 274 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 275 | type = ["mypy (>=1.11.2)"] 276 | 277 | [[package]] 278 | name = "repath" 279 | version = "0.9.0" 280 | description = "Generate regular expressions form ExpressJS path patterns" 281 | optional = false 282 | python-versions = "*" 283 | files = [ 284 | {file = "repath-0.9.0-py3-none-any.whl", hash = "sha256:ee079d6c91faeb843274d22d8f786094ee01316ecfe293a1eb6546312bb6a318"}, 285 | {file = "repath-0.9.0.tar.gz", hash = "sha256:8292139bac6a0e43fd9d70605d4e8daeb25d46672e484ed31a24c7ce0aef0fb7"}, 286 | ] 287 | 288 | [package.dependencies] 289 | six = ">=1.9.0" 290 | 291 | [[package]] 292 | name = "six" 293 | version = "1.16.0" 294 | description = "Python 2 and 3 compatibility utilities" 295 | optional = false 296 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 297 | files = [ 298 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 299 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 300 | ] 301 | 302 | [[package]] 303 | name = "sniffio" 304 | version = "1.3.1" 305 | description = "Sniff out which async library your code is running under" 306 | optional = false 307 | python-versions = ">=3.7" 308 | files = [ 309 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 310 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 311 | ] 312 | 313 | [[package]] 314 | name = "tomli" 315 | version = "2.2.1" 316 | description = "A lil' TOML parser" 317 | optional = false 318 | python-versions = ">=3.8" 319 | files = [ 320 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 321 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 322 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 323 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 324 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 325 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 326 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 327 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 328 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 329 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 330 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 331 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 332 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 333 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 334 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 335 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 336 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 337 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 338 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 339 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 340 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 341 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 342 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 343 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 344 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 345 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 346 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 347 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 348 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 349 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 350 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 351 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 352 | ] 353 | 354 | [[package]] 355 | name = "typing-extensions" 356 | version = "4.12.2" 357 | description = "Backported and Experimental Type Hints for Python 3.8+" 358 | optional = false 359 | python-versions = ">=3.8" 360 | files = [ 361 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 362 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 363 | ] 364 | 365 | [metadata] 366 | lock-version = "2.0" 367 | python-versions = "^3.8" 368 | content-hash = "0158a7aef53cca538cf206dc5cb02bf3683b8145260f4508f073f37997e1fbab" 369 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "flet-contrib" 3 | version = "0.1.0" 4 | description = "Flet controls by the community" 5 | authors = ["Appveyor Systems Inc. "] 6 | license = "Apache-2.0" 7 | readme = "README.md" 8 | packages = [{include = "flet_contrib"}] 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.8" 12 | flet = "^0" 13 | # flet-core = { path = "../flet/sdk/python/packages/flet-core", develop = true } 14 | 15 | [tool.poetry.group.dev.dependencies] 16 | black = "^23.3.0" 17 | flet = "^0" 18 | # flet = { path = "../flet/sdk/python/packages/flet", develop = true } 19 | # flet-runtime = { path = "../flet/sdk/python/packages/flet-runtime", develop = true } 20 | 21 | [build-system] 22 | requires = ["poetry-core"] 23 | build-backend = "poetry.core.masonry.api" 24 | --------------------------------------------------------------------------------