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