├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets └── fletxible.png ├── requirements.txt ├── setup.py ├── src ├── __init__.py ├── config.py ├── core │ ├── __init__.py │ ├── base.py │ ├── drawer.py │ ├── header.py │ ├── left_panel.py │ ├── middle_panel.py │ ├── mobile_drop_down.py │ ├── mobile_navigation.py │ ├── navigation.py │ ├── repo_data.py │ └── right_panel.py ├── fx_material │ ├── __init__.py │ ├── annotation.py │ ├── block.py │ └── typography.py ├── main.py ├── pages │ ├── _error.py │ ├── index.py │ └── router.py ├── scripts │ ├── __init__.py │ ├── build.py │ └── create.py └── utilities │ ├── __init__.py │ ├── fx_cli.py │ ├── fx_config.py │ ├── fx_error.py │ ├── fx_main.py │ ├── fx_scratch.py │ ├── fx_sub_router.py │ ├── fx_sub_template.py │ └── fx_template.py ├── tests └── test_template.py └── tree.md /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: [3.8, 3.9, 3.10] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: 3.9 19 | - name: Install dependencies 20 | run: pip install -r requirements.txt 21 | - name: Run tests 22 | run: python -m unittest discover -s tests 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | dist 3 | build 4 | dev 5 | Fletxible.egg-info 6 | .DS_Store 7 | __pycache__ 8 | web 9 | command.py 10 | src/__pycache__ 11 | src/pages/about.py 12 | src/pages/__pycache__ 13 | fletxible -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LineIndent/fletxible/a9547703ae1ed7e707aaaf569c67d447911b2a43/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Seyed Ahmad Pour Hakimi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

fletxible.

3 | 4 | 5 | 6 |
7 | 8 | Build Status 9 | 10 | 13 | 16 | 17 | PyPI version 18 | 19 | 20 | PyPI downloads 21 | 22 |
23 | 24 |
25 | 26 |

27 | Fletxible is a Python web boilerplate project designed to provide a solid foundation for building web applications with Python and Flet. The project comes pre-configured with a range of tools and features to make it easy for developers to get started building their applications, without the need to spend time setting up infrastructure or configuring tools.

28 | 29 | 30 | 31 | 32 | ## Installation 33 | 34 | To use Fletxible, you need to have the following installed: 35 | 36 | - Latest version of Flet 37 | - Python 3.5+ 38 | 39 | If you don't have Flet installed, installing Fletxible automatically installs it for you. You can install Fletxible using the following command: 40 | ```py 41 | $ pip install Fletxible 42 | ``` 43 | 44 | 45 | 46 | ## Application Setup 47 | 48 | After installing Fletxible, you can test if it's working properly by running the following command: 49 | 50 | ```py 51 | $ fx-init 52 | ``` 53 | 54 | If the package was installed correctly, a folder called ```src``` will be generated inside the root directory. Other directories and files will also be generated. 55 | 56 | ## 3. Quick Start 57 | 58 | Open the ```config.py``` file inside the ```src``` folder and configure the document as needed. Change the site name, repository link, as well as any theme-related settings. You can also add/remove the navigation section as needed. 59 | 60 | When you're ready, change directories to the source folder, ```cd src```, and then run the following command to generate your files/pages: 61 | ```py 62 | python3 scripts/build.py 63 | ``` 64 | 65 | If successful, the script should generate the files inside the ```pages``` folder that correspond to the ```config.py``` navigation map. 66 | 67 | You can then run the following command to see your application: 68 | 69 | ```py 70 | python3 main.py 71 | ``` 72 | 73 | If the setup has no error, you can start customizing your pages by adding in your personal layout directly within the generated pages inside the ```pages``` directory. 74 | 75 | 76 | ## Current Algorithm Functions 77 | 78 | This algorithm is a script that loads and processes data from a YAML file ```flet_config.yml``` that contains navigation information for a web application. The script then updates and creates various files and directories necessary for the application to function. 79 | 80 | Here is a summary of what the algorithm does (v0.2.0): 81 | 82 | 1. Import necessary libraries and functions 83 | 2. Define a dictionary variable to hold route keys 84 | 85 | 3. Define several functions to perform various tasks: 86 | 1. open_yaml_script(): Loads data from the "fx_config.yml" file. 87 | 2. check_pages_directory_script(): Checks if a "pages" directory exists and creates one if not. 88 | 3. update_pages_directory_script(docs: dict): Loops over the files in the "pages" directory and deletes any files that are not listed in the navigation information. 89 | 4. handle_navigation_routing_script(docs: dict): Loops over the navigation information and writes route strings to a temporary file. 90 | 5. set_application_routing_script(docs: dict): Reads the temporary file created in the previous step, creates a route.py file with the appropriate routes, and deletes the temporary file. 91 | 6. set_default_methods_script(docs: dict): Loops over the navigation information and creates default pages for each page listed, and creates a route.pickle file with information about the modules used in the application. 92 | 7. map_yaml(yaml_file_path, output_file_path): Reads a YAML file and writes its contents to a Python file with a specified filename. 93 | 8. script(page: ft.Page): Main function that calls the other functions to process the data and set up the application. 94 | 95 | Overall, the script is part of a larger application development process that involves reading and processing data from a YAML file, creating and updating various files and directories, and setting up routing information for a web application. 96 | 97 | ## Contributing 98 | 99 | Contributions are highly encouraged and welcomed. 100 | 101 | 102 | ## License 103 | 104 | Fletxible is open-source and licensed under the [MIT License](LICENSE). -------------------------------------------------------------------------------- /assets/fletxible.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LineIndent/fletxible/a9547703ae1ed7e707aaaf569c67d447911b2a43/assets/fletxible.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click>=8.1.3 2 | flet>=0.7.4 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name="Fletxible", 5 | version="0.7.1", 6 | author="S. Ahmad P. Hakimi", 7 | author_email="pourhakimi@pm.me", 8 | description="Web Boilerplate for Flet Library", 9 | long_description="Fletxible is a Python web boilerplate project designed to provide a solid foundation for building web applications with Python and Flet. The project comes pre-configured with a range of tools and features to make it easy for developers to get started building their applications, without the need to spend time setting up infrastructure or configuring tools.", # noqa: E501 10 | long_description_content_type="text/markdown", 11 | url="https://github.com/LineIndent/fletxible", 12 | packages=find_packages("fletxible"), 13 | package_dir={"": "fletxible"}, 14 | install_requires=[ 15 | "click>=8.1.3", 16 | "flet>=0.9.0", 17 | "beautifulsoup4>=4.12.2", 18 | ], 19 | classifiers=[ 20 | "Programming Language :: Python :: 3", 21 | "License :: OSI Approved :: MIT License", 22 | "Operating System :: OS Independent", 23 | ], 24 | entry_points={ 25 | "console_scripts": [ 26 | "fx-init=scripts.create:create", 27 | ], 28 | }, 29 | keywords=["python web template", "web application", "theme"], 30 | ) 31 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | from src.config import config # noqa: F401 2 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | config: dict = { 2 | "site-name": "fletxible.", 3 | "repo-url": "https://github.com/LineIndent/fletxible", 4 | "repo-name": "LineIndent/fletxible", 5 | "theme": { 6 | "bgcolor": "teal", 7 | "primary": "teal700", 8 | }, 9 | "navigation": { 10 | "index": "index.py", 11 | "about": "about.py", 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LineIndent/fletxible/a9547703ae1ed7e707aaaf569c67d447911b2a43/src/core/__init__.py -------------------------------------------------------------------------------- /src/core/base.py: -------------------------------------------------------------------------------- 1 | from core.mobile_drop_down import MobileDropDownNavigation # noqa: F401 2 | from core.mobile_navigation import MobileNavigation # noqa: F401 3 | from core.middle_panel import MiddlePanel # noqa: F401 4 | from core.right_panel import RightPanel # noqa: F401 5 | from core.navigation import Navigation # noqa: F401 6 | from core.left_panel import LeftPanel # noqa: F401 7 | from core.header import Header # noqa: F401 8 | from core.drawer import Drawer # noqa: F401 9 | 10 | import flet as ft 11 | 12 | 13 | class FxBaseView(ft.View): 14 | def __init__( 15 | self, 16 | page: ft.Page, 17 | docs: dict, 18 | components: list, 19 | nav_rail: list[list], 20 | route: str, 21 | sub_nav=None, 22 | padding=0, 23 | ): 24 | self.page = page 25 | self.docs = docs 26 | self.components = components 27 | self.nav_rail = nav_rail 28 | self.sub_nav = sub_nav 29 | 30 | self.fx_stack = ft.Stack(expand=True) 31 | self.fx_row = ft.Row(expand=True, spacing=2) 32 | 33 | self.fx_drawer = Drawer(docs=self.docs, page=self.page) 34 | 35 | self.fx_max_nav = Navigation(page=self.page) 36 | self.fx_min_nav = MobileNavigation(on_click=lambda e: self.set_fx_drawer(e)) 37 | 38 | self.fx_header = Header( 39 | page=self.page, 40 | docs=self.docs, 41 | full_nav=self.fx_max_nav, 42 | mobile_nav=self.fx_min_nav, 43 | ) 44 | 45 | self.fx_left = LeftPanel( 46 | page=self.page, 47 | routes=self.sub_nav, 48 | ) 49 | self.fx_middle = MiddlePanel( 50 | components=self.components, 51 | function=[ 52 | self.set_fx_header, 53 | self.set_header_navigation_row, 54 | self.fx_header.set_header_name, 55 | ], 56 | page=self.page, 57 | header_name=self.fx_header, 58 | ) 59 | self.fx_right = RightPanel( 60 | docs=self.docs, middle_panel=self.fx_middle, fx_rail=self.nav_rail 61 | ) 62 | 63 | self.fx_drop_down = MobileDropDownNavigation( 64 | "On this page ...", len(self.nav_rail), self.nav_rail, self.fx_middle 65 | ) 66 | self.fx_middle.components.insert(1, self.fx_drop_down) 67 | 68 | super().__init__( 69 | padding=padding, 70 | route=route, 71 | ) 72 | 73 | self.fx_row.controls = [self.fx_left, self.fx_middle, self.fx_right] 74 | self.fx_stack.controls = [self.fx_row, self.fx_header, self.fx_drawer] 75 | 76 | self.controls = [self.fx_stack] 77 | 78 | # Method: Responsive method to set the UI for 'mobile/tablet' screens ... 79 | def set_application_to_mobile(self): 80 | self.set_fx_max_nav(0, False) 81 | self.set_fx_left(False) 82 | self.set_fx_right(False) 83 | 84 | self.set_fx_min_nav(True) 85 | 86 | if self.fx_drop_down.max_height != 0: 87 | self.set_fx_drop_down(True) 88 | else: 89 | self.set_fx_drop_down(False) 90 | 91 | self.set_fx_header(60) 92 | self.set_header_repo_opacity(0, False) 93 | self.set_header_navigation_row(0, False) 94 | 95 | self.update() 96 | 97 | # Method: Responsive method to set the UI for 'desktop' screens ... 98 | def set_application_to_desktop(self): 99 | self.set_fx_left(True) 100 | self.set_fx_right(True) 101 | self.set_fx_max_nav(1, True) 102 | 103 | self.set_fx_min_nav(False) 104 | self.set_fx_drop_down(False) 105 | 106 | self.set_fx_header(90) 107 | self.set_header_repo_opacity(1, True) 108 | self.set_header_navigation_row(1, True) 109 | 110 | self.update() 111 | 112 | # Method: sets the state of the header with animations ... 113 | def set_header_navigation_row(self, value: int, state: bool): 114 | self.fx_header.navigation.opacity = value 115 | self.fx_header.navigation.visible = state 116 | self.fx_header.navigation.update() 117 | 118 | def set_header_repo_opacity(self, value: int, state: bool): 119 | self.fx_header.repo.controls[1].opacity = value 120 | self.fx_header.repo.controls[1].visible = state 121 | self.fx_header.repo.update() 122 | 123 | def set_fx_header(self, height: int): 124 | self.fx_header.height = height 125 | self.fx_header.update() 126 | 127 | def set_fx_drop_down(self, state: bool): 128 | self.fx_drop_down.visible = state 129 | 130 | def set_fx_max_nav(self, value: int, state: bool): 131 | self.fx_max_nav.opacity = value 132 | self.fx_max_nav.update() 133 | 134 | self.fx_max_nav.visible = state 135 | self.fx_max_nav.update() 136 | 137 | def set_fx_min_nav(self, state: bool): 138 | self.fx_min_nav.visible = state 139 | self.fx_min_nav.update() 140 | 141 | def set_fx_left(self, state: bool): 142 | self.fx_left.visible = state 143 | self.fx_left.update() 144 | 145 | def set_fx_right(self, state: bool): 146 | self.fx_right.visible = state 147 | self.fx_right.update() 148 | 149 | def set_fx_drawer(self, e): 150 | if self.fx_drawer.width != 220: 151 | self.show_fx_drawer() 152 | else: 153 | self.hide_fx_drawer() 154 | 155 | def show_fx_drawer(self): 156 | self.fx_drawer.width = 220 157 | self.fx_drawer.shadow = ft.BoxShadow( 158 | blur_radius=15, 159 | spread_radius=10, 160 | color=ft.colors.with_opacity(0.25, "black"), 161 | offset=(4, 4), 162 | ) 163 | self.fx_drawer.update() 164 | 165 | self.fx_drawer.content.opacity = 1 166 | self.fx_drawer.update() 167 | 168 | def hide_fx_drawer(self): 169 | self.fx_drawer.content.opacity = 0 170 | self.fx_drawer.update() 171 | 172 | self.fx_drawer.width = 0 173 | self.fx_drawer.shadow = None 174 | self.fx_drawer.update() 175 | -------------------------------------------------------------------------------- /src/core/drawer.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | from core.repo_data import RepoData 3 | 4 | 5 | class Drawer(ft.Container): 6 | def __init__( 7 | self, 8 | docs: dict, 9 | page: ft.Page, 10 | expand=True, 11 | width=0, 12 | bgcolor="#23262d", 13 | shadow=None, 14 | animate=ft.Animation(550, "ease"), 15 | content=ft.Column( 16 | expand=True, 17 | # opacity=0, 18 | spacing=0, 19 | animate_opacity=ft.Animation(100, "ease"), 20 | ), 21 | ): 22 | self.page = page 23 | self.docs = docs 24 | self.repo = RepoData(self.docs) 25 | 26 | name = self.docs.get("repo-name", "") 27 | url = self.docs.get("repo-url", "") 28 | background_color = self.docs.get("theme", "").get("bgcolor", "") 29 | primary = self.docs.get("theme", "").get("primary", "") 30 | 31 | super().__init__( 32 | expand=expand, 33 | width=width, 34 | bgcolor=bgcolor, 35 | shadow=shadow, 36 | animate=animate, 37 | content=content, 38 | ) 39 | 40 | self.content.controls = [ 41 | ft.Container( 42 | bgcolor=background_color, 43 | height=60, 44 | padding=ft.padding.only(left=14), 45 | content=ft.Row( 46 | alignment="start", 47 | controls=[ 48 | ft.Text( 49 | # start # 50 | name, # end # 51 | size=19, 52 | weight="w700", 53 | color="white", 54 | ) 55 | ], 56 | ), 57 | ), 58 | ft.Container( 59 | padding=ft.padding.only(left=14), 60 | bgcolor=primary, 61 | height=45, 62 | on_click=lambda __: self.page.launch_url(url=url), 63 | content=ft.Tooltip( 64 | padding=10, 65 | vertical_offset=30, 66 | message="Go to repository", 67 | bgcolor="#20222c", 68 | text_style=ft.TextStyle(color="white", size=9), 69 | content=self.repo, 70 | ), 71 | ), 72 | ] 73 | -------------------------------------------------------------------------------- /src/core/header.py: -------------------------------------------------------------------------------- 1 | from core.repo_data import RepoData 2 | import flet as ft 3 | import time 4 | 5 | 6 | class Header(ft.Container): 7 | def __init__( 8 | self, 9 | page: ft.Page, 10 | docs: dict, 11 | full_nav: ft.Row, 12 | mobile_nav: ft.IconButton, 13 | height=90, 14 | padding=ft.padding.only(left=60, right=60), 15 | shadow=ft.BoxShadow( 16 | spread_radius=2, 17 | blur_radius=4, 18 | color=ft.colors.with_opacity(0.25, "black"), 19 | offset=ft.Offset(3, 3), 20 | ), 21 | animate=ft.Animation(500, "ease"), 22 | clip_behavior=ft.ClipBehavior.HARD_EDGE, 23 | ): 24 | self.page = page 25 | self.docs = docs 26 | self.repo_data = RepoData(self.docs) 27 | 28 | url = self.docs.get("repo-url", "") 29 | 30 | self.name = self.docs.get("site-name", "") 31 | self.background_color = self.docs.get("theme", "").get("bgcolor", "") 32 | 33 | self.full_nav = full_nav 34 | self.mobile_nav = mobile_nav 35 | 36 | self.navigation = ft.Row( 37 | alignment="start", 38 | opacity=1, 39 | animate_opacity=ft.Animation(500, "ease"), 40 | vertical_alignment="start", 41 | controls=[ 42 | self.full_nav, 43 | ], 44 | ) 45 | self.repo = ft.Row( 46 | alignment="end", 47 | controls=[ 48 | self.mobile_nav, 49 | ft.Column( 50 | opacity=1, 51 | animate_opacity=ft.Animation(500, "ease"), 52 | alignment="center", 53 | horizontal_alignment="start", 54 | spacing=5, 55 | controls=[ 56 | ft.Container( 57 | on_click=lambda __: self.page.launch_url(url=url), 58 | content=ft.Tooltip( 59 | padding=10, 60 | vertical_offset=25, 61 | message="Go to repository", 62 | bgcolor=ft.colors.with_opacity(0.85, "#20222c"), 63 | text_style=ft.TextStyle(color="white", size=9), 64 | content=self.repo_data, 65 | ), 66 | ), 67 | ], 68 | ), 69 | ], 70 | ) 71 | 72 | self.title = ft.Text( 73 | # start # 74 | "fletxible.", # end # 75 | size=21, 76 | color="white", 77 | weight="w700", 78 | opacity=1, 79 | offset=ft.transform.Offset(0, 0), 80 | animate_opacity=ft.Animation(100, "ease"), 81 | animate_offset=ft.Animation(100, "ease"), 82 | ) 83 | 84 | super().__init__( 85 | height=height, 86 | padding=padding, 87 | shadow=shadow, 88 | animate=animate, 89 | clip_behavior=clip_behavior, 90 | ) 91 | 92 | self.bgcolor = self.background_color 93 | 94 | self.content = ft.Column( 95 | alignment="center", 96 | spacing=20, 97 | controls=[ 98 | # 99 | ft.Row( 100 | alignment="spaceBetween", 101 | vertical_alignment="center", 102 | controls=[ 103 | self.title, 104 | ft.Row( 105 | spacing=1, 106 | alignment="end", 107 | vertical_alignment="center", 108 | controls=[ 109 | ft.IconButton( 110 | icon_size=14, 111 | icon=ft.icons.DARK_MODE_ROUNDED, 112 | icon_color="white", 113 | on_click=lambda e: self.toggle_theme(e), 114 | ), 115 | self.repo, 116 | ], 117 | ), 118 | ], 119 | ), 120 | self.navigation, 121 | ], 122 | ) 123 | 124 | def set_header_name(self, name: str): 125 | if name != self.title.value: 126 | self.title.opacity = 0 127 | self.title.offset = ft.transform.Offset(-0.25, 0) 128 | self.title.update() 129 | time.sleep(0.1) 130 | self.title.value = name 131 | self.title.offset = ft.transform.Offset(0, 0) 132 | self.title.opacity = 1 133 | self.title.update() 134 | 135 | def toggle_theme(self, e): 136 | if e.control.icon == ft.icons.LIGHT_MODE_ROUNDED: 137 | self.page.theme_mode = ft.ThemeMode.DARK 138 | e.control.icon = ft.icons.DARK_MODE_ROUNDED 139 | 140 | else: 141 | self.page.theme_mode = ft.ThemeMode.LIGHT 142 | e.control.icon = ft.icons.LIGHT_MODE_ROUNDED 143 | 144 | self.page.update() 145 | -------------------------------------------------------------------------------- /src/core/left_panel.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | 4 | class LeftPanel(ft.Container): 5 | def __init__( 6 | self, 7 | page: ft.Page, 8 | routes: list[list], 9 | expand=1, 10 | padding=ft.padding.only(top=65), 11 | content=ft.Column( 12 | expand=True, alignment="start", horizontal_alignment="center" 13 | ), 14 | ): 15 | self.page = page 16 | self.routes = routes 17 | super().__init__(expand=expand, padding=padding, content=content) 18 | self.route_links = self.generate_sub_routes(self.routes) 19 | 20 | self.content.controls = self.route_links 21 | 22 | def generate_sub_routes(self, route_list: list[list]): 23 | route_links = [ 24 | ft.Divider(height=35, color="transparent"), 25 | ft.Divider(height=25, color="transparent"), 26 | ] 27 | if route_list is not None: 28 | for route in route_list: 29 | route_links.append(self.route(title=route[0], route_to=route[1])) 30 | 31 | return route_links 32 | 33 | def route(self, title: str, route_to: str) -> ft.Control: 34 | return ft.Text( 35 | size=11, 36 | weight="bold", 37 | spans=[ft.TextSpan(title, on_click=lambda __: self.page.go(route_to))], 38 | ) 39 | -------------------------------------------------------------------------------- /src/core/middle_panel.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | 4 | class MiddlePanel(ft.Container): 5 | def __init__( 6 | self, 7 | components: list, 8 | function: list[callable], 9 | page: ft.Page, 10 | header_name: str, 11 | expand=5, 12 | padding=ft.padding.only(top=65, right=15, left=15), 13 | alignment=ft.alignment.top_center, 14 | ): 15 | self.page = page 16 | self.components = components 17 | self.header_name = header_name 18 | self.function = function 19 | 20 | self.main_column = ft.Column( 21 | expand=True, alignment="start", scroll="hidden", spacing=0 22 | ) 23 | self.main_column.controls = self.components 24 | self.main_column.on_scroll = lambda e: self.get_scroll(e) 25 | super().__init__(expand=expand, padding=padding, alignment=alignment) 26 | self.content = self.main_column 27 | 28 | def get_scroll(self, e: ft.OnScrollEvent) -> None: 29 | if e.pixels >= float(2.0): 30 | self.function[0](60) 31 | self.function[1](0, False) 32 | self.function[2](self.page.route.replace("/", "").capitalize()) 33 | 34 | if e.pixels <= float(1.9): 35 | self.function[2](self.header_name.name) 36 | if self.page.width >= 850: 37 | self.function[1](1, True) 38 | self.function[0](90) 39 | -------------------------------------------------------------------------------- /src/core/mobile_drop_down.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | 4 | class MobileDropDownNavigation(ft.Container): 5 | def __init__( 6 | self, 7 | title: str, 8 | max_height: int, 9 | drop_rail: list[list], 10 | middle_panel: ft.Container, 11 | visible=False, 12 | height=45, 13 | bgcolor=ft.colors.with_opacity(0.95, "#20222c"), 14 | border=ft.border.all(0.85, "white24"), 15 | border_radius=6, 16 | clip_behavior=ft.ClipBehavior.HARD_EDGE, 17 | animate=ft.Animation(300, "decelerate"), 18 | alignment=ft.alignment.top_left, 19 | shadow=ft.BoxShadow( 20 | spread_radius=2, 21 | blur_radius=4, 22 | color=ft.colors.with_opacity(0.25, "black"), 23 | offset=ft.Offset(4, 4), 24 | ), 25 | ): 26 | self.title = title 27 | self.middle_panel = middle_panel 28 | self.drop_rail = drop_rail 29 | 30 | self.max_height = max_height 31 | if self.max_height != 0: 32 | self.max_height = (max_height * 30) + 60 33 | 34 | self.drop_rail = self.generate_right_rail_logic(self.drop_rail) 35 | 36 | super().__init__( 37 | visible=visible, 38 | height=height, 39 | bgcolor=bgcolor, 40 | border=border, 41 | border_radius=border_radius, 42 | shadow=shadow, 43 | clip_behavior=clip_behavior, 44 | animate=animate, 45 | alignment=alignment, 46 | ) 47 | 48 | self.content = ft.Column( 49 | expand=True, 50 | alignment="start", 51 | spacing=0, 52 | controls=[ 53 | ft.Container( 54 | bgcolor="#20222c", 55 | padding=ft.padding.only(left=20), 56 | content=ft.Row( 57 | height=42, 58 | alignment="spaceBetween", 59 | controls=[ 60 | ft.Row( 61 | vertical_alignment="center", 62 | alignment="start", 63 | spacing=10, 64 | controls=[ 65 | ft.Text( 66 | self.title.capitalize(), 67 | size=11, 68 | weight="w700", 69 | color="white", 70 | ), 71 | ], 72 | ), 73 | ft.IconButton( 74 | icon=ft.icons.ADD, 75 | icon_size=15, 76 | icon_color="white24", 77 | rotate=ft.Rotate(0, ft.alignment.center), 78 | animate_rotation=ft.Animation(400, "easeOutBack"), 79 | on_click=lambda e: self.resize_admonition(e), 80 | ), 81 | ], 82 | ), 83 | ), 84 | ft.Container( 85 | padding=ft.padding.only(left=20, right=20, bottom=10, top=15), 86 | expand=True, 87 | content=ft.Column( 88 | expand=True, 89 | alignment="spaceEven", 90 | horizontal_alignment="start", 91 | controls=self.drop_rail, 92 | ), 93 | ), 94 | ], 95 | ) 96 | 97 | def resize_admonition(self, e): 98 | if self.height != self.max_height: 99 | self.height = self.max_height 100 | e.control.rotate = ft.Rotate(0.75, ft.alignment.center) 101 | else: 102 | self.height = 45 103 | e.control.rotate = ft.Rotate(0, ft.alignment.center) 104 | 105 | self.update() 106 | 107 | def rail_hover_color(self, e): 108 | if e.data == "true": 109 | e.control.content.color = "white" 110 | 111 | else: 112 | e.control.content.color = ft.colors.with_opacity(0.55, "white10") 113 | 114 | e.control.content.update() 115 | 116 | def generate_right_rail_logic(self, fx_rail_list): 117 | nav_rail = [] 118 | if len(fx_rail_list) != 0: 119 | for item in fx_rail_list: 120 | key = item[0] 121 | nav_rail.append( 122 | ft.Container( 123 | content=ft.Text( 124 | item[1], 125 | size=12, 126 | color=ft.colors.with_opacity(0.55, "white10"), 127 | ), 128 | on_hover=lambda e,: self.rail_hover_color(e), 129 | on_click=lambda _, key=key: self.scroll_to_key(key), 130 | ) 131 | ) 132 | 133 | return nav_rail 134 | 135 | def scroll_to_key(self, key): 136 | self.middle_panel.main_column.scroll_to(key=str(key), duration=500) 137 | -------------------------------------------------------------------------------- /src/core/mobile_navigation.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | 4 | class MobileNavigation(ft.IconButton): 5 | def __init__( 6 | self, 7 | icon=ft.icons.MENU_SHARP, 8 | visible=False, 9 | icon_size=14, 10 | icon_color="white", 11 | on_click=callable, 12 | ): 13 | super().__init__( 14 | icon=icon, 15 | visible=visible, 16 | icon_size=icon_size, 17 | icon_color=icon_color, 18 | on_click=on_click, 19 | ) 20 | -------------------------------------------------------------------------------- /src/core/navigation.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | 4 | class Navigation(ft.Row): 5 | def __init__( 6 | self, 7 | page: ft.Page, 8 | alignment="center", 9 | ): 10 | self.page = page 11 | 12 | super().__init__(alignment=alignment) 13 | self.controls = [ 14 | self.route("Home", "/index"), 15 | self.route("About", "/about"), 16 | self.route("Contact", "/contact/index"), 17 | ] 18 | 19 | def route(self, title: str, route_to: str) -> ft.Control: 20 | return ft.Text( 21 | size=11, 22 | weight="bold", 23 | color="white", 24 | spans=[ft.TextSpan(title, on_click=lambda __: self.page.go(route_to))], 25 | ) 26 | -------------------------------------------------------------------------------- /src/core/repo_data.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | from bs4 import BeautifulSoup 3 | import httpx 4 | import asyncio 5 | from math import pi 6 | 7 | 8 | class RepoData(ft.Row): 9 | def __init__( 10 | self, 11 | docs: dict, 12 | alignment="start", 13 | vertical_alignment="center", 14 | ): 15 | self.docs = docs 16 | self.repo_name = self.docs.get("repo-name", "") 17 | self.repo = self.docs.get("repo-url", "") 18 | 19 | self.repo_data = asyncio.run(self.get_repo_data()) 20 | 21 | super().__init__(alignment=alignment, vertical_alignment=vertical_alignment) 22 | 23 | self.controls = [ 24 | ft.Column( 25 | opacity=1, 26 | animate_opacity=ft.Animation(500, "ease"), 27 | alignment="center", 28 | horizontal_alignment="start", 29 | spacing=2.5, 30 | controls=[ 31 | ft.Text( 32 | self.repo_name, 33 | size=11, 34 | color="white", 35 | weight="w700", 36 | ), 37 | ft.Row( 38 | alignment="center", 39 | vertical_alignment="center", 40 | controls=self.repo_data, 41 | ), 42 | ], 43 | ), 44 | ] 45 | 46 | # Method: gets the repo details based on the input repo URL ... 47 | async def get_repo_data(self): 48 | controls_list: list = [] 49 | 50 | icon_elements = ["LABEL_OUTLINED", "STAR_BORDER_SHARP", "CALL_SPLIT_SHARP"] 51 | 52 | span_elements: list = [ 53 | "css-truncate css-truncate-target text-bold mr-2", 54 | "Counter js-social-count", 55 | "Counter", 56 | ] 57 | 58 | async with httpx.AsyncClient() as client: 59 | response = await client.get(self.repo) 60 | data = response.content 61 | 62 | soup = BeautifulSoup(data, "html.parser") 63 | 64 | for i, span in enumerate(span_elements): 65 | span_element = soup.find("span", span) 66 | if span_element is not None: 67 | text_content = span_element.text.strip() 68 | 69 | if i == 0: 70 | icon = ft.Icon( 71 | name=icon_elements[i], 72 | size=10, 73 | rotate=ft.Rotate(pi / 4), 74 | color="white", 75 | ) 76 | else: 77 | icon = ft.Icon( 78 | name=icon_elements[i], 79 | size=10, 80 | color="white", 81 | ) 82 | 83 | controls_list.append( 84 | ft.Row( 85 | alignment="center", 86 | spacing=0, 87 | controls=[ 88 | icon, 89 | ft.Text( 90 | text_content, 91 | size=10, 92 | weight="w200", 93 | color="white", 94 | ), 95 | ], 96 | ) 97 | ) 98 | 99 | else: 100 | pass 101 | 102 | return controls_list 103 | -------------------------------------------------------------------------------- /src/core/right_panel.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | 4 | class RightPanel(ft.Container): 5 | def __init__( 6 | self, 7 | docs: dict, 8 | middle_panel: ft.Container, 9 | fx_rail: list, 10 | expand=1, 11 | padding=ft.padding.only(top=65, left=10), 12 | ): 13 | self.docs = docs 14 | self.highlight = self.docs.get("theme", "").get("bgcolor", "") 15 | 16 | self.middle_panel = middle_panel 17 | 18 | self.fx_rail = fx_rail 19 | self.adjusted_nav_rail = self.generate_right_rail_logic(self.fx_rail) 20 | 21 | self.rail_controls = ft.Column( 22 | expand=True, alignment="start", controls=self.adjusted_nav_rail 23 | ) 24 | 25 | super().__init__(expand=expand, padding=padding) 26 | self.content = self.rail_controls 27 | 28 | def generate_right_rail_logic(self, fx_rail_list): 29 | nav_rail = [ 30 | ft.Divider(height=35, color="transparent"), 31 | ft.Divider(height=25, color="transparent"), 32 | ] 33 | 34 | if len(fx_rail_list) != 0: 35 | for item in fx_rail_list: 36 | key = item[0] 37 | nav_rail.append( 38 | ft.Container( 39 | content=ft.Text( 40 | item[1], 41 | size=12, 42 | weight="w500", 43 | ), 44 | on_hover=lambda e,: self.rail_hover_color(e), 45 | on_click=lambda _, key=key: self.scroll_to_key(key), 46 | ) 47 | ) 48 | 49 | return nav_rail 50 | 51 | def scroll_to_key(self, key): 52 | self.middle_panel.main_column.scroll_to(key=str(key), duration=500) 53 | 54 | def rail_hover_color(self, e): 55 | if e.data == "true": 56 | e.control.content.color = self.highlight 57 | 58 | else: 59 | e.control.content.color = "" 60 | 61 | e.control.content.update() 62 | -------------------------------------------------------------------------------- /src/fx_material/__init__.py: -------------------------------------------------------------------------------- 1 | from fx_material.annotation import Annotations # noqa: F401 2 | from fx_material.block import CodeBlock # noqa: F401 3 | from fx_material.typography import heading, subtitle, paragraph # noqa: F401 4 | -------------------------------------------------------------------------------- /src/fx_material/annotation.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | 4 | class Annotations(ft.Container): 5 | def __init__( 6 | self, 7 | annotations_msg: str, 8 | *args, 9 | **kwargs, 10 | ): 11 | self.annotations_msg = annotations_msg 12 | 13 | self.annotation = ft.Tooltip( 14 | padding=10, 15 | vertical_offset=20, 16 | message=self.annotations_msg, 17 | bgcolor="#20222c", 18 | text_style=ft.TextStyle(color="white"), 19 | content=ft.Icon( 20 | name=ft.icons.ADD, 21 | size=15, 22 | rotate=ft.Rotate(0, ft.alignment.center), 23 | animate_rotation=ft.Animation(400, "easeOutBack"), 24 | ), 25 | ) 26 | 27 | kwargs.setdefault("width", 21) 28 | kwargs.setdefault("height", 21) 29 | kwargs.setdefault("bgcolor", "white24") 30 | kwargs.setdefault("shape", ft.BoxShape("circle")) 31 | kwargs.setdefault("alignment", ft.alignment.center) 32 | kwargs.setdefault("content", self.annotation) 33 | kwargs.setdefault("animate", 400) 34 | kwargs.setdefault("on_hover", lambda e: self.change_rotation(e)) 35 | super().__init__(*args, **kwargs) 36 | 37 | def change_rotation(self, e): 38 | if e.data == "true": 39 | self.bgcolor = "#dd6058" 40 | self.content.content.rotate = ft.Rotate(0.75, ft.alignment.center) 41 | 42 | else: 43 | self.bgcolor = "white24" 44 | self.content.content.rotate = ft.Rotate(0, ft.alignment.center) 45 | 46 | self.update() 47 | -------------------------------------------------------------------------------- /src/fx_material/block.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | import asyncio 3 | 4 | 5 | class CodeBlock(ft.UserControl): 6 | def __init__(self, title): 7 | # 8 | self.title = title 9 | 10 | # 11 | self._hovered: bool | None = None 12 | 13 | self.copy_box = ft.Container( 14 | width=28, 15 | height=28, 16 | border=ft.border.all(1, "transparent"), 17 | right=1, 18 | top=1, 19 | border_radius=7, 20 | scale=ft.Scale(1), 21 | animate=ft.Animation(400, "ease"), 22 | alignment=ft.alignment.center, 23 | content=ft.Icon( 24 | name=ft.icons.COPY, 25 | size=14, 26 | color="white12", 27 | opacity=0, 28 | animate_opacity=ft.Animation(420, "ease"), 29 | ), 30 | on_click=lambda e: asyncio.run(self.get_copy_box_content(e)), 31 | ) 32 | 33 | super().__init__() 34 | 35 | async def get_copy_box_content(self, e): 36 | self.title = self.title.replace("`", "") 37 | self.title = self.title.replace("python", "") 38 | e.page.set_clipboard(self.title) 39 | 40 | while self._hovered: 41 | self.copy_box.disabled = True 42 | self.copy_box.update() 43 | 44 | self.copy_box.content.opacity = 0 45 | self.copy_box.content.name = ft.icons.CHECK 46 | self.copy_box.update() 47 | 48 | await asyncio.sleep(0.25) 49 | 50 | self.copy_box.content.opacity = 1 51 | self.copy_box.content.color = "teal" 52 | self.copy_box.update() 53 | 54 | await asyncio.sleep(1) 55 | 56 | self.copy_box.content.opacity = 0 57 | self.copy_box.content.name = ft.icons.COPY 58 | self.copy_box.content.color = "white12" 59 | self.copy_box.update() 60 | 61 | self.copy_box.disabled = False 62 | self.copy_box.update() 63 | 64 | break 65 | 66 | if self._hovered is True: 67 | self.copy_box.content.opacity = 1 68 | 69 | else: 70 | self.copy_box.content.opacity = 0 71 | 72 | self.copy_box.content.update() 73 | 74 | def show_copy_box(self, e): 75 | if e.data == "true": 76 | self.copy_box.border = ft.border.all(0.95, "white10") 77 | self.copy_box.content.opacity = 1 78 | self._hovered = True 79 | 80 | else: 81 | self.copy_box.content.opacity = 0 82 | self.copy_box.border = ft.border.all(0.95, "transparent") 83 | self._hovered = False 84 | 85 | self.copy_box.update() 86 | 87 | def build(self): 88 | return ft.Row( 89 | alignment="start", 90 | vertical_alignment="center", 91 | controls=[ 92 | ft.Container( 93 | expand=True, 94 | padding=8, 95 | border_radius=7, 96 | bgcolor="#282b33", 97 | on_hover=lambda e: self.show_copy_box(e), 98 | content=ft.Stack( 99 | controls=[ 100 | ft.Markdown( 101 | value=self.title, 102 | selectable=True, 103 | extension_set="gitHubWeb", 104 | code_theme="atom-one-dark-reasonable", 105 | code_style=ft.TextStyle(size=12), 106 | ), 107 | self.copy_box, 108 | ], 109 | ), 110 | ) 111 | ], 112 | ) 113 | -------------------------------------------------------------------------------- /src/fx_material/typography.py: -------------------------------------------------------------------------------- 1 | from flet_core import Text 2 | 3 | 4 | def heading(title): 5 | return Text(value=title, size=21, weight="bold") 6 | 7 | 8 | def subtitle(title, key=None): 9 | return Text(value=title, size=17, weight="w700", key=key) 10 | 11 | 12 | def paragraph(title): 13 | return Text(value=title, size=15, weight="w500") 14 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | from config import config 3 | 4 | from pages._error import Error404 5 | import importlib 6 | import gc 7 | import os 8 | 9 | 10 | def get_list_of_pages_from_directory() -> list: 11 | pages_list = set() 12 | 13 | def loop_over_sub_folders(path: str): 14 | for item in os.listdir(path): 15 | item_path = os.path.join(path, item) 16 | if item == "__pycache__": 17 | continue 18 | if os.path.isfile(item_path) and item_path.endswith(".py"): 19 | pages_list.add(item_path) 20 | elif os.path.isdir(item_path): 21 | loop_over_sub_folders(item_path) 22 | 23 | for root, folders, files in os.walk("pages"): 24 | for folder in folders: 25 | path = os.path.join(root, folder) 26 | loop_over_sub_folders(path) 27 | 28 | for file in files: 29 | item_path = os.path.join(root, file) 30 | if os.path.isfile(item_path) and item_path.endswith(".py"): 31 | pages_list.add(item_path) 32 | 33 | return pages_list 34 | 35 | 36 | def main(page: ft.Page): 37 | page.theme_mode = ft.ThemeMode.DARK 38 | theme = ft.Theme( 39 | scrollbar_theme=ft.ScrollbarTheme( 40 | thickness=4, 41 | radius=10, 42 | main_axis_margin=5, 43 | cross_axis_margin=-10, 44 | ), 45 | ) 46 | theme.page_transitions.macos = ft.PageTransitionTheme.NONE 47 | theme.page_transitions.windows = ft.PageTransitionTheme.NONE 48 | theme.page_transitions.ios = ft.PageTransitionTheme.NONE 49 | page.theme = theme 50 | 51 | docs: dict = config 52 | list_of_pages = get_list_of_pages_from_directory() 53 | 54 | def generate_view_as_instance(route): 55 | for file in list_of_pages: 56 | filename = file.split("pages", 1)[1].split(".")[0] 57 | filepath = file 58 | file_length = len(file.split("/")) 59 | if filename == route: 60 | module_spec = importlib.util.spec_from_file_location(filename, filepath) 61 | module = importlib.util.module_from_spec(module_spec) 62 | module_spec.loader.exec_module(module) 63 | try: 64 | if file_length >= 3: 65 | return module.FxSubView(page, docs) 66 | 67 | else: 68 | return module.FxView(page, docs) 69 | 70 | except AttributeError: 71 | return Error404(page, docs) 72 | 73 | return Error404(page, docs) 74 | 75 | def change_route(route): 76 | page.views.clear() 77 | gc.collect() 78 | view = generate_view_as_instance(page.route) 79 | page.views.append(view) 80 | 81 | page.update() 82 | 83 | def resize_applications(event): 84 | for view in page.views[:]: 85 | if view.route is not None: 86 | if page.width <= 850: 87 | view.set_application_to_mobile() 88 | else: 89 | view.set_application_to_desktop() 90 | 91 | index = generate_view_as_instance("/index") 92 | page.views.append(index) 93 | 94 | page.on_route_change = change_route 95 | page.on_resize = resize_applications 96 | 97 | page.update() 98 | 99 | 100 | ft.app(target=main, view="web_browser") 101 | -------------------------------------------------------------------------------- /src/pages/_error.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | from core.base import FxBaseView 3 | import fx_material as fx 4 | 5 | 6 | class Error404(FxBaseView): 7 | def __init__( 8 | self, 9 | page: ft.Page, 10 | docs: dict, 11 | route="/error_404", 12 | ): 13 | self.components = self.fx_controls() 14 | self.nav_rail = self.fx_rail() 15 | 16 | super().__init__( 17 | page=page, 18 | docs=docs, 19 | components=self.components, 20 | nav_rail=self.nav_rail, 21 | route=route, 22 | ) 23 | 24 | def fx_rail(self) -> list[list]: 25 | return [] 26 | 27 | def fx_controls(self) -> list: 28 | return [ 29 | ft.Divider(height=35, color="transparent"), 30 | ft.Divider(height=25, color="transparent"), 31 | # start your layout design here ... 32 | fx.heading("404 - Page Not Found!"), 33 | ft.Divider(height=25, color="transparent"), 34 | fx.paragraph("Demo 404 page for bad URLs."), 35 | # end your layout design here ... 36 | ft.Divider(height=15, color="transparent"), 37 | ] 38 | -------------------------------------------------------------------------------- /src/pages/index.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | from core.base import FxBaseView 3 | import fx_material as fx 4 | 5 | intro = """ 6 | Fletxible is a Python web boilerplate project designed to provide a solid foundation for building web applications with Python and Flet. The project comes pre-configured with a range of tools and features to make it easy for developers to get started building their applications, without the need to spend time setting up infrastructure or configuring tools. 7 | """ 8 | 9 | pip = """ 10 | Fletxible is published as a Python package and can be installed with pip, ideally by using a virtual environment. Open up a terminal and install Fletxible using the following command: 11 | """ 12 | 13 | pip_command = """```python 14 | pip3 install Fletxible 15 | ``` 16 | """ 17 | 18 | pip_outro = """ 19 | This will automatically install compatible versions of all dependencies including Flet. 20 | """ 21 | 22 | app_intro = """ 23 | After installing Fletxible, you can test if it's working properly by running the following command: 24 | """ 25 | 26 | app_command = """ 27 | ```python 28 | fletxible-init 29 | ``` 30 | """ 31 | 32 | app_outro = """ 33 | If the package was installed correctly, you should see the following directories: 34 | - core 35 | - components 36 | - utilities 37 | 38 | As well as the following files: 39 | - main.py 40 | - script.py 41 | - base.py 42 | - fx_tempalte.py 43 | - fx_config.yml 44 | """ 45 | 46 | script = """ 47 | First, open up your fx_config.yml file and make sure the following are present: 48 | """ 49 | 50 | config = """```yaml 51 | site-name: "fletxible." 52 | repo-url: "https://github.com/LineIndent/fletxible" 53 | 54 | theme: 55 | - bgcolor: "teal" 56 | 57 | navigation: 58 | - Home: "index.py" 59 | - About: "about.py 60 | ``` 61 | """ 62 | 63 | change_config = """ 64 | You can replace the default configuration with your own data. The navigation header will generate files with names correpsonding to the python file. 65 | """ 66 | 67 | script_two = """ 68 | When you're set with your config file (fx_config.yml), navigate to your terminal and run the following command to generate your files inside a directory called web: 69 | """ 70 | 71 | script_cmd = """```python 72 | python3 script.py 73 | ``` 74 | """ 75 | 76 | conclusion = """ 77 | That's it! You now have your pages set up along with the necessary routing and layout. 78 | You can open the web directory and start creating your pages immediately! 79 | """ 80 | 81 | 82 | class FxView(FxBaseView): 83 | def __init__( 84 | self, 85 | page: ft.Page, 86 | docs: dict, 87 | route="/index", 88 | ): 89 | self.components = self.fx_controls() 90 | self.nav_rail = self.fx_rail() 91 | 92 | super().__init__( 93 | page=page, 94 | docs=docs, 95 | components=self.components, 96 | nav_rail=self.nav_rail, 97 | route=route, 98 | ) 99 | 100 | def fx_rail(self) -> list[list]: 101 | return [ 102 | [1, "Installation"], 103 | [2, "Application Setup"], 104 | [3, "Configuration"], 105 | ] 106 | 107 | def fx_controls(self) -> list: 108 | return [ 109 | ft.Divider(height=35, color="transparent"), 110 | ft.Divider(height=25, color="transparent"), 111 | # start your layout design here ... 112 | fx.heading("Getting Started - INDEX PAGE"), 113 | fx.paragraph(intro), 114 | fx.subtitle("Installation"), 115 | ft.Divider(height=5, color="transparent"), 116 | fx.subtitle("with PIP", key=1), 117 | fx.paragraph(pip), 118 | fx.CodeBlock(pip_command), 119 | fx.paragraph(pip_outro), 120 | fx.subtitle("Application Setup", key=2), 121 | fx.paragraph(app_intro), 122 | fx.CodeBlock(app_command), 123 | fx.paragraph(app_outro), 124 | fx.subtitle("Configure YAML File", key=3), 125 | fx.paragraph(script), 126 | fx.CodeBlock(config), 127 | fx.paragraph(change_config), 128 | fx.paragraph(script_two), 129 | fx.CodeBlock(script_cmd), 130 | fx.paragraph(conclusion), 131 | # end your layout design here ... 132 | ft.Divider(height=15, color="transparent"), 133 | ] 134 | -------------------------------------------------------------------------------- /src/pages/router.py: -------------------------------------------------------------------------------- 1 | """ 2 | Interpage Routing File: 3 | 4 | Example Method: 5 | 6 | 1. 7 | def folder_name_navigation() -> list[list]: 8 | return [ 9 | ["Title: str", "Route Path: str"], 10 | ] 11 | 12 | 2. 13 | Import inside relevant sub_dir_files inside pages folder: 14 | from pages.router import folder_name_navigation 15 | 16 | 3. 17 | Call the imported method inside the class method: 18 | def fx_sub_navigation(self) -> list[list]: 19 | return folder_name_navigation() 20 | 21 | """ 22 | -------------------------------------------------------------------------------- /src/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LineIndent/fletxible/a9547703ae1ed7e707aaaf569c67d447911b2a43/src/scripts/__init__.py -------------------------------------------------------------------------------- /src/scripts/build.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | 5 | def check_if_pages_directory_exists() -> None: 6 | pages_directory = None 7 | for root, dirs, __ in os.walk("."): 8 | if "pages" in dirs: 9 | pages_directory = os.path.join(root, "pages") 10 | break 11 | 12 | if not pages_directory: 13 | pages_directory = os.path.join(os.getcwd(), "pages") 14 | os.mkdir(pages_directory) 15 | 16 | 17 | def get_list_of_pages_from_directory() -> list: 18 | pages_list = set() 19 | dirs_list: list = [] 20 | 21 | def loop_over_sub_folders(path: str): 22 | for item in os.listdir(path): 23 | item_path = os.path.join(path, item) 24 | if item == "__pycache__": 25 | continue 26 | if os.path.isfile(item_path) and item_path.endswith(".py"): 27 | pages_list.add(item_path) 28 | elif os.path.isdir(item_path): 29 | dirs_list.append(item_path) 30 | loop_over_sub_folders(item_path) 31 | 32 | for root, folders, files in os.walk("pages"): 33 | for folder in folders: 34 | path = os.path.join(root, folder) 35 | dirs_list.append(path) 36 | loop_over_sub_folders(path) 37 | 38 | for file in files: 39 | item_path = os.path.join(root, file) 40 | if os.path.isfile(item_path) and item_path.endswith(".py"): 41 | pages_list.add(item_path) 42 | 43 | return dirs_list, pages_list 44 | 45 | 46 | def get_list_of_pages_from_config_file(docs: dict, parent_path: str = "pages"): 47 | pages_list: list = [] 48 | dirs_list: list = [] 49 | 50 | def loop_over_nested_dict(docs: dict, current_path: str): 51 | if docs.get("navigation") is not None: 52 | docs = docs.get("navigation").items() 53 | else: 54 | docs = docs.items() 55 | 56 | for key, value in docs: 57 | new_path = os.path.join(current_path, key) 58 | if isinstance(value, dict): 59 | dirs_list.append(new_path) 60 | loop_over_nested_dict(value, new_path) 61 | else: 62 | file_path = new_path + ".py" 63 | pages_list.append(file_path) 64 | 65 | loop_over_nested_dict(docs, parent_path) 66 | 67 | return dirs_list, pages_list 68 | 69 | 70 | def synchronize_directories(docs: dict): 71 | pages_dirs, pages_files = get_list_of_pages_from_directory() 72 | dict_dirs, dict_files = get_list_of_pages_from_config_file(docs) 73 | 74 | for pages_dir in pages_dirs: 75 | try: 76 | if pages_dir not in dict_dirs: 77 | shutil.rmtree(pages_dir) 78 | except: # noqa: E722 79 | pass 80 | 81 | for dict_dir in dict_dirs: 82 | if not os.path.exists(dict_dir): 83 | os.makedirs(dict_dir) 84 | 85 | for file_path in pages_files: 86 | try: 87 | if file_path not in dict_files and file_path == "pages/_error": 88 | os.remove(file_path) 89 | except: # noqa: E722 90 | pass 91 | 92 | with open("utilities/fx_template.py", "r") as file: 93 | view = file.read() 94 | 95 | with open("utilities/fx_sub_template.py", "r") as file: 96 | sub_view = file.read() 97 | 98 | for file in dict_files: 99 | if not os.path.exists(file): 100 | file_length = len(file.split("/")) 101 | with open(file, "w") as file: 102 | if file_length >= 3: 103 | file.write(sub_view) 104 | else: 105 | file.write(view) 106 | 107 | 108 | def set_sub_directory_router_file(): 109 | with open("utilities/fx_error.py", "r") as file: 110 | error = file.read() 111 | 112 | error_path = os.path.join("pages" + "/_error.py") 113 | if not os.path.exists(error_path): 114 | with open(error_path, "w") as file: 115 | file.write(error) 116 | 117 | with open("utilities/fx_sub_router.py", "r") as file: 118 | router = file.read() 119 | 120 | for root, dirs, __ in os.walk("pages"): 121 | for dir in dirs: 122 | router_path = os.path.join(root + "/" + dir + "/" + "router.py") 123 | if not os.path.exists(router_path): 124 | with open(router_path, "w") as file: 125 | file.write(router) 126 | 127 | 128 | # NOT USED # 129 | def create_navigation_links_from_keys(): 130 | nav_list: list = [] 131 | 132 | __, list_of_pages = get_list_of_pages_from_directory() 133 | 134 | for file in list_of_pages: 135 | filename = file.split("pages", 1)[1].split(".")[0] 136 | title = file.split("/")[-1].split(".")[0] 137 | if filename == "/_error": 138 | continue 139 | nav_list.append(f"self.route('{title.capitalize()}', '{filename}'),") 140 | 141 | with open("core/navigation.py", "r") as file: 142 | data = file.read() 143 | 144 | start_index = data.index("# start #") 145 | stop_index = data.index("# end #") 146 | 147 | new_nav_list = "\n".join(nav_list) 148 | 149 | new_data = ( 150 | data[:start_index] + "# start #\n" + new_nav_list + "\n" + data[stop_index:] 151 | ) 152 | 153 | with open("core/navigation.py", "w") as file: 154 | file.write(new_data) 155 | 156 | 157 | def build(docs: dict): 158 | check_if_pages_directory_exists() 159 | synchronize_directories(docs) 160 | set_sub_directory_router_file() 161 | 162 | 163 | if __name__ == "__main__": 164 | import sys 165 | 166 | sys.path.append("../") 167 | from src.config import config 168 | 169 | build(config) 170 | -------------------------------------------------------------------------------- /src/scripts/create.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | import subprocess 4 | import click 5 | 6 | 7 | file_structure = { 8 | "core": [ 9 | "__init__.py", 10 | "base.py", 11 | "drawer.py", 12 | "header.py", 13 | "left_panel.py", 14 | "middle_panel.py", 15 | "mobile_drop_down.py", 16 | "mobile_navigation.py", 17 | "navigation.py", 18 | "repo_data.py", 19 | "right_panel.py", 20 | ], 21 | "fx_material": ["__init__.py", "annotation.py", "block.py", "typography.py"], 22 | "scripts": ["__init__.py", "create.py", "build.py"], 23 | "utilities": [ 24 | "__init__.py", 25 | "fx_cli.py", 26 | "fx_config.py", 27 | "fx_error.py", 28 | "fx_scratch.py", 29 | "fx_sub_router.py", 30 | "fx_sub_template.py", 31 | "fx_template.py", 32 | ], 33 | "config.py": None, 34 | "main.py": None, 35 | } 36 | 37 | 38 | def create_src_directory(): 39 | for __ in os.listdir("."): 40 | if not os.path.exists("./" + "src"): 41 | os.makedirs("src") 42 | else: 43 | continue 44 | 45 | 46 | def create_src_file_structure(): 47 | source = Path(__file__).parent.parent 48 | dublicate = Path("./src") 49 | fx_file_src = Path(source, "utilities") 50 | 51 | def create_dir_file_structure(dir_path: str, key: str, file: str): 52 | source_path = os.path.join(source, key, file) 53 | dublicate_path = os.path.join(dir_path, file) 54 | 55 | with open(source_path, "r") as src_file, open(dublicate_path, "w") as dst_file: 56 | dst_file.write(src_file.read()) 57 | 58 | for key, value in file_structure.items(): 59 | if value is not None: 60 | dir_path = os.path.join(dublicate, key) 61 | os.mkdir(dir_path) 62 | if isinstance(value, list): 63 | for file in value: 64 | create_dir_file_structure(dir_path, key, file) 65 | else: 66 | source_path = os.path.join(fx_file_src, "fx_" + key) 67 | dublicate_path = os.path.join(dublicate, key) 68 | with open(source_path, "r") as src_file, open( 69 | dublicate_path, "w" 70 | ) as dst_file: 71 | dst_file.write(src_file.read()) 72 | 73 | 74 | @click.command() 75 | def create(): 76 | click.echo("Creating source directory...") 77 | create_src_directory() 78 | create_src_file_structure() 79 | click.echo("Source directory created successfully.") 80 | 81 | os.chdir("src") 82 | 83 | click.echo("Running build scripts...") 84 | if os.path.basename(os.getcwd()) == "src": 85 | # Run twine upload * 86 | subprocess.run( 87 | ["python3", "scripts/build.py"], capture_output=True, text=True, bufsize=0 88 | ) 89 | click.echo("Build ran successfully.") 90 | 91 | 92 | @click.group() 93 | def flexible(): 94 | pass 95 | 96 | 97 | flexible.add_command(create) 98 | 99 | if __name__ == "__main__": 100 | flexible() 101 | -------------------------------------------------------------------------------- /src/utilities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LineIndent/fletxible/a9547703ae1ed7e707aaaf569c67d447911b2a43/src/utilities/__init__.py -------------------------------------------------------------------------------- /src/utilities/fx_cli.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import click 3 | import os 4 | from utilities.config import create_yaml_file 5 | from pathlib import Path 6 | 7 | 8 | @click.command() 9 | def init(): 10 | main: list = [] 11 | source = Path(__file__).parent.parent 12 | 13 | code = create_yaml_file() 14 | with open("fx_config.yml", "w") as f: 15 | f.write(code) 16 | 17 | root: list = ["main.py", "script.py", "base.py", "fx_template.py"] 18 | 19 | pos = 0 20 | for file_name in root: 21 | file_path = Path(".") / file_name 22 | file_path.touch() 23 | 24 | source_path = source / "fletxible" / root[pos] 25 | with open(source_path, "r") as source_file: 26 | source_contents = source_file.read() 27 | 28 | with open(file_path, "w") as new_file: 29 | new_file.write(source_contents) 30 | 31 | main.append(file_name) 32 | pos += 1 33 | 34 | paths: list = ["components", "core", "utilities"] 35 | for path in paths: 36 | Path(path).mkdir(exist_ok=True) 37 | 38 | dirs: list = ["components", "core", "utilities"] 39 | 40 | index = 0 41 | for dir in dirs: 42 | file_names: list = [] 43 | path = os.path.join(source, dir) 44 | for file in os.listdir(path): 45 | if file.endswith(".py"): 46 | main.append(file) 47 | file_names.append(file) 48 | 49 | for file_name in file_names: 50 | file_path = Path(dir) / file_name 51 | file_path.touch() 52 | 53 | source_path = source / dir / file_name 54 | with open(source_path, "r") as source_file: 55 | source_contents = source_file.read() 56 | 57 | with open(file_path, "w") as new_file: 58 | new_file.write(source_contents) 59 | 60 | index += 1 61 | 62 | click.echo() 63 | click.echo(f"Generated {len(main) + 1} files in the 'root' directory:") 64 | click.echo("Status: OK") 65 | click.echo() 66 | 67 | click.echo("Running script file ...") 68 | subprocess.run(["python3", "script.py"], capture_output=True, text=True, bufsize=0) 69 | subprocess.run(["python3", "main.py"], capture_output=True, text=True, bufsize=0) 70 | click.echo("Status: OK") 71 | 72 | 73 | @click.group() 74 | def flet_template(): 75 | pass 76 | 77 | 78 | flet_template.add_command(init) 79 | 80 | if __name__ == "__main__": 81 | flet_template() 82 | -------------------------------------------------------------------------------- /src/utilities/fx_config.py: -------------------------------------------------------------------------------- 1 | config: dict = { 2 | "site-name": "fletxible.", 3 | "repo-url": "https://github.com/LineIndent/fletxible", 4 | "repo-name": "LineIndent/fletxible", 5 | "theme": { 6 | "bgcolor": "teal", 7 | "primary": "teal700", 8 | }, 9 | "navigation": { 10 | "index": "index.py", 11 | "about": "about.py", 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/utilities/fx_error.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | from core.base import FxBaseView 3 | import fx_material as fx 4 | 5 | 6 | class Error404(FxBaseView): 7 | def __init__( 8 | self, 9 | page: ft.Page, 10 | docs: dict, 11 | route="/error_404", 12 | ): 13 | self.components = self.fx_controls() 14 | self.nav_rail = self.fx_rail() 15 | 16 | super().__init__( 17 | page=page, 18 | docs=docs, 19 | components=self.components, 20 | nav_rail=self.nav_rail, 21 | route=route, 22 | ) 23 | 24 | def fx_rail(self) -> list[list]: 25 | return [] 26 | 27 | def fx_controls(self) -> list: 28 | return [ 29 | ft.Divider(height=35, color="transparent"), 30 | ft.Divider(height=25, color="transparent"), 31 | # start your layout design here ... 32 | fx.heading("404 - Page Not Found!"), 33 | ft.Divider(height=25, color="transparent"), 34 | fx.paragraph("Demo 404 page for bad URLs."), 35 | # end your layout design here ... 36 | ft.Divider(height=15, color="transparent"), 37 | ] 38 | -------------------------------------------------------------------------------- /src/utilities/fx_main.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | from config import config 3 | 4 | from pages._error import Error404 5 | import importlib 6 | import gc 7 | import os 8 | 9 | 10 | def get_list_of_pages_from_directory() -> list: 11 | pages_list = set() 12 | 13 | def loop_over_sub_folders(path: str): 14 | for item in os.listdir(path): 15 | item_path = os.path.join(path, item) 16 | if item == "__pycache__": 17 | continue 18 | if os.path.isfile(item_path) and item_path.endswith(".py"): 19 | pages_list.add(item_path) 20 | elif os.path.isdir(item_path): 21 | loop_over_sub_folders(item_path) 22 | 23 | for root, folders, files in os.walk("pages"): 24 | for folder in folders: 25 | path = os.path.join(root, folder) 26 | loop_over_sub_folders(path) 27 | 28 | for file in files: 29 | item_path = os.path.join(root, file) 30 | if os.path.isfile(item_path) and item_path.endswith(".py"): 31 | pages_list.add(item_path) 32 | 33 | return pages_list 34 | 35 | 36 | def main(page: ft.Page): 37 | page.theme_mode = ft.ThemeMode.DARK 38 | theme = ft.Theme( 39 | scrollbar_theme=ft.ScrollbarTheme( 40 | thickness=4, 41 | radius=10, 42 | main_axis_margin=5, 43 | cross_axis_margin=-10, 44 | ), 45 | ) 46 | theme.page_transitions.macos = ft.PageTransitionTheme.NONE 47 | theme.page_transitions.windows = ft.PageTransitionTheme.NONE 48 | theme.page_transitions.ios = ft.PageTransitionTheme.NONE 49 | page.theme = theme 50 | 51 | docs: dict = config 52 | list_of_pages = get_list_of_pages_from_directory() 53 | 54 | def generate_view_as_instance(route): 55 | for file in list_of_pages: 56 | filename = file.split("pages", 1)[1].split(".")[0] 57 | filepath = file 58 | file_length = len(file.split("/")) 59 | if filename == route: 60 | module_spec = importlib.util.spec_from_file_location(filename, filepath) 61 | module = importlib.util.module_from_spec(module_spec) 62 | module_spec.loader.exec_module(module) 63 | try: 64 | if file_length >= 3: 65 | return module.FxSubView(page, docs) 66 | 67 | else: 68 | return module.FxView(page, docs) 69 | 70 | except AttributeError: 71 | return Error404(page, docs) 72 | 73 | return Error404(page, docs) 74 | 75 | def change_route(route): 76 | page.views.clear() 77 | gc.collect() 78 | view = generate_view_as_instance(page.route) 79 | page.views.append(view) 80 | 81 | page.update() 82 | 83 | def resize_applications(event): 84 | for view in page.views[:]: 85 | if view.route is not None: 86 | if page.width <= 850: 87 | view.set_application_to_mobile() 88 | else: 89 | view.set_application_to_desktop() 90 | 91 | index = generate_view_as_instance("/index") 92 | page.views.append(index) 93 | 94 | page.on_route_change = change_route 95 | page.on_resize = resize_applications 96 | 97 | page.update() 98 | 99 | 100 | ft.app(target=main, view="web_browser") 101 | -------------------------------------------------------------------------------- /src/utilities/fx_scratch.py: -------------------------------------------------------------------------------- 1 | # ## Original code block for returning views as instances ... 2 | 3 | # for file in os.listdir("pages"): 4 | # if os.path.isfile(f"pages/{file}"): 5 | # filename = os.path.splitext(file)[0] 6 | # if "/" + filename == route: 7 | # filepath = os.path.join("pages", file) 8 | # module_spec = importlib.util.spec_from_file_location( 9 | # filename, filepath 10 | # ) 11 | # module = importlib.util.module_from_spec(module_spec) 12 | # module_spec.loader.exec_module(module) 13 | # return module.FxView(page, docs) 14 | -------------------------------------------------------------------------------- /src/utilities/fx_sub_router.py: -------------------------------------------------------------------------------- 1 | """ 2 | Interpage Routing File: 3 | 4 | Example Method: 5 | 6 | 1. 7 | def folder_name_navigation() -> list[list]: 8 | return [ 9 | ["Title: str", "Route Path: str"], 10 | ] 11 | 12 | 2. 13 | Import inside relevant sub_dir_files inside pages folder: 14 | from pages.router import folder_name_navigation 15 | 16 | 3. 17 | Call the imported method inside the class method: 18 | def fx_sub_navigation(self) -> list[list]: 19 | return folder_name_navigation() 20 | 21 | """ 22 | -------------------------------------------------------------------------------- /src/utilities/fx_sub_template.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | from core.base import FxBaseView 3 | import fx_material as fx # noqa: F401 4 | 5 | 6 | class FxSubView(FxBaseView): 7 | def __init__( 8 | self, 9 | page: ft.Page, 10 | docs: dict, 11 | route="", # place route here ... 12 | ): 13 | self.components = self.fx_controls() 14 | self.nav_rail = self.fx_rail() 15 | self.sub_nav = self.fx_sub_navigation() 16 | 17 | super().__init__( 18 | page=page, 19 | docs=docs, 20 | components=self.components, 21 | nav_rail=self.nav_rail, 22 | sub_nav=self.sub_nav, 23 | route=route, 24 | ) 25 | 26 | def fx_sub_navigation(self) -> list: 27 | ... 28 | 29 | def fx_rail(self) -> list[list]: 30 | return [] # page navigation here ... 31 | 32 | def fx_controls(self) -> list: 33 | return [ 34 | ft.Divider(height=35, color="transparent"), 35 | ft.Divider(height=25, color="transparent"), 36 | # Start your layout below # 37 | # End your layout above # 38 | ft.Divider(height=15, color="transparent"), 39 | ] 40 | -------------------------------------------------------------------------------- /src/utilities/fx_template.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | from core.base import FxBaseView 3 | import fx_material as fx # noqa: F401 4 | 5 | 6 | class FxView(FxBaseView): 7 | def __init__( 8 | self, 9 | page: ft.Page, 10 | docs: dict, 11 | route="", # place route here ... 12 | ): 13 | self.components = self.fx_controls() 14 | self.nav_rail = self.fx_rail() 15 | 16 | super().__init__( 17 | page=page, 18 | docs=docs, 19 | components=self.components, 20 | nav_rail=self.nav_rail, 21 | route=route, 22 | ) 23 | 24 | def fx_rail(self) -> list[list]: 25 | return [] # page navigation here ... 26 | 27 | def fx_controls(self) -> list: 28 | return [ 29 | ft.Divider(height=35, color="transparent"), 30 | ft.Divider(height=25, color="transparent"), 31 | # Start your layout below # 32 | # End your layout above # 33 | ft.Divider(height=15, color="transparent"), 34 | ] 35 | -------------------------------------------------------------------------------- /tests/test_template.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /tree.md: -------------------------------------------------------------------------------- 1 | ├── .github/ 2 | │ │ 3 | │ └── workflows/ 4 | │ └── build.yml 5 | │ 6 | ├── assets/ 7 | │ └── fletxible.png 8 | │ 9 | ├── src/ 10 | │ │ 11 | │ ├── core/ 12 | │ │ ├── __init__.py 13 | │ │ ├── base.py 14 | │ │ ├── drawer.py 15 | │ │ ├── header.py 16 | │ │ ├── left_panel.py 17 | │ │ ├── middle_panel.py 18 | │ │ ├── mobile_drop_down.py 19 | │ │ ├── mobile_navigation.py 20 | │ │ ├── navigation.py 21 | │ │ ├── repo_data.py 22 | │ │ └── right_panel.py 23 | │ │ 24 | │ ├── fx_material/ 25 | │ │ ├── __init__.py 26 | │ │ ├── annotation.py 27 | │ │ ├── block.py 28 | │ │ └── typography.py 29 | │ │ 30 | │ ├── scripts/ 31 | │ │ ├── __init__.py 32 | │ │ ├── create.py 33 | │ │ └── build.py 34 | │ │ 35 | │ │ 36 | │ ├── utilities/ 37 | │ │ ├── __init__.py 38 | │ │ ├── fx_cli.py 39 | │ │ ├── fx_config.py 40 | │ │ ├── fx_error.py 41 | │ │ ├── fx_scratch.py 42 | │ │ ├── fx_sub_router.py 43 | │ │ ├── fx_sub_template.py 44 | │ │ └── fx_template.py 45 | │ │ 46 | │ ├── __init__.py 47 | │ ├── config.py 48 | │ └── main.py 49 | │ 50 | │ 51 | ├── tests/ 52 | │ └── test_template.py 53 | │ 54 | ├── .gitignore 55 | ├── CHANGELOG.md 56 | ├── LICENSE 57 | ├── README.md 58 | ├── requirements.txt 59 | ├── setup.py 60 | └── tree.md --------------------------------------------------------------------------------