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