Add drawer navigation and searchable icon catalog
This commit is contained in:
parent
6e647fed4b
commit
b812ed21c5
6 changed files with 147 additions and 47 deletions
|
|
@ -8,6 +8,9 @@
|
||||||
типографика, карточки, списки, адаптивные layouts, навигация, таблицы, progress,
|
типографика, карточки, списки, адаптивные layouts, навигация, таблицы, progress,
|
||||||
dialogs, snackbar и bottom sheet. Постоянная кнопка настроек позволяет менять
|
dialogs, snackbar и bottom sheet. Постоянная кнопка настроек позволяет менять
|
||||||
светлую/тёмную/системную тему, Material 3 accent color и видимость кнопок кода.
|
светлую/тёмную/системную тему, Material 3 accent color и видимость кнопок кода.
|
||||||
|
Разделы открываются через выдвижное меню в AppBar. Отдельный каталог всех иконок
|
||||||
|
Flet поддерживает поиск по имени и копирует готовое выражение `ft.Icon(ft.Icons.HOME)`
|
||||||
|
при нажатии.
|
||||||
|
|
||||||
## Запуск
|
## Запуск
|
||||||
|
|
||||||
|
|
@ -42,6 +45,7 @@ ARMv8 в Android соответствует ABI `arm64-v8a`:
|
||||||
uv run flet build apk src --arch arm64-v8a --yes \
|
uv run flet build apk src --arch arm64-v8a --yes \
|
||||||
--project flet_component_gallery \
|
--project flet_component_gallery \
|
||||||
--product "Flet Material Catalog" \
|
--product "Flet Material Catalog" \
|
||||||
|
--build-version 0.2.0 \
|
||||||
--org su.snw
|
--org su.snw
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "flet-component-gallery"
|
name = "flet-component-gallery"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "Composer-inspired interactive gallery of Flet controls"
|
description = "Composer-inspired interactive gallery of Flet controls"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import flet as ft
|
import flet as ft
|
||||||
|
|
||||||
from gallery.examples import ALL_EXAMPLES
|
from gallery.examples import ALL_EXAMPLES
|
||||||
|
from gallery.icon_catalog import ICON_NAMES, PAGE_SIZE, icon_snippet, matching_icons
|
||||||
from gallery.models import Example, GallerySettings
|
from gallery.models import Example, GallerySettings
|
||||||
from gallery.source_links import source_url
|
from gallery.source_links import source_url
|
||||||
|
|
||||||
|
|
@ -18,11 +19,13 @@ class GalleryApp:
|
||||||
self.page = page
|
self.page = page
|
||||||
self.settings = GallerySettings()
|
self.settings = GallerySettings()
|
||||||
self.selected_category = "Обзор"
|
self.selected_category = "Обзор"
|
||||||
self.query = ""
|
self.icon_query = ""
|
||||||
|
self.icon_page = 0
|
||||||
|
self.clipboard = ft.Clipboard()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def categories(self) -> list[str]:
|
def categories(self) -> list[str]:
|
||||||
return ["Обзор", *dict.fromkeys(example.category for example in ALL_EXAMPLES)]
|
return ["Обзор", *dict.fromkeys(example.category for example in ALL_EXAMPLES), "Иконки"]
|
||||||
|
|
||||||
def apply_theme(self) -> None:
|
def apply_theme(self) -> None:
|
||||||
self.page.theme_mode = self.settings.theme_mode
|
self.page.theme_mode = self.settings.theme_mode
|
||||||
|
|
@ -136,29 +139,22 @@ class GalleryApp:
|
||||||
examples = ALL_EXAMPLES
|
examples = ALL_EXAMPLES
|
||||||
if self.selected_category != "Обзор":
|
if self.selected_category != "Обзор":
|
||||||
examples = [e for e in examples if e.category == self.selected_category]
|
examples = [e for e in examples if e.category == self.selected_category]
|
||||||
query = self.query.casefold().strip()
|
|
||||||
if query:
|
|
||||||
examples = [
|
|
||||||
e
|
|
||||||
for e in examples
|
|
||||||
if query in f"{e.title} {e.description} {e.category}".casefold()
|
|
||||||
]
|
|
||||||
return examples
|
return examples
|
||||||
|
|
||||||
def header(self) -> ft.Control:
|
def header(self) -> ft.Control:
|
||||||
count = len(self.current_examples())
|
count = len(ICON_NAMES) if self.selected_category == "Иконки" else len(self.current_examples())
|
||||||
return ft.Column(
|
return ft.Column(
|
||||||
controls=[
|
controls=[
|
||||||
ft.Text(self.selected_category, theme_style=ft.TextThemeStyle.DISPLAY_SMALL),
|
ft.Text(self.selected_category, theme_style=ft.TextThemeStyle.DISPLAY_SMALL),
|
||||||
ft.Text(
|
ft.Text(
|
||||||
f"{count} интерактивных примеров · Flet 1.0.1",
|
(f"{count} иконок · нажмите, чтобы скопировать код" if self.selected_category == "Иконки" else f"{count} интерактивных примеров · Flet 1.0.1 · каталог 0.2.0"),
|
||||||
color=ft.Colors.ON_SURFACE_VARIANT,
|
color=ft.Colors.ON_SURFACE_VARIANT,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
spacing=4,
|
spacing=4,
|
||||||
)
|
)
|
||||||
|
|
||||||
def navigation(self) -> ft.Control:
|
def navigation(self) -> ft.NavigationDrawer:
|
||||||
icons = [
|
icons = [
|
||||||
ft.Icons.HOME_OUTLINED,
|
ft.Icons.HOME_OUTLINED,
|
||||||
ft.Icons.TOUCH_APP_OUTLINED,
|
ft.Icons.TOUCH_APP_OUTLINED,
|
||||||
|
|
@ -167,65 +163,140 @@ class GalleryApp:
|
||||||
ft.Icons.GRID_VIEW,
|
ft.Icons.GRID_VIEW,
|
||||||
ft.Icons.NAVIGATION_OUTLINED,
|
ft.Icons.NAVIGATION_OUTLINED,
|
||||||
ft.Icons.DATA_OBJECT,
|
ft.Icons.DATA_OBJECT,
|
||||||
|
ft.Icons.EMOJI_SYMBOLS_OUTLINED,
|
||||||
]
|
]
|
||||||
|
|
||||||
def select(e: ft.Event[ft.NavigationRail]) -> None:
|
async def select(e: ft.Event[ft.NavigationDrawer]) -> None:
|
||||||
self.selected_category = self.categories[e.control.selected_index]
|
self.selected_category = self.categories[e.control.selected_index]
|
||||||
|
await self.page.close_drawer()
|
||||||
self.render()
|
self.render()
|
||||||
|
|
||||||
return ft.NavigationRail(
|
return ft.NavigationDrawer(
|
||||||
selected_index=self.categories.index(self.selected_category),
|
selected_index=self.categories.index(self.selected_category),
|
||||||
destinations=[
|
controls=[
|
||||||
ft.NavigationRailDestination(icon=icon, label=category)
|
ft.Container(
|
||||||
|
content=ft.Text("Flet Material Catalog", theme_style=ft.TextThemeStyle.TITLE_LARGE),
|
||||||
|
padding=20,
|
||||||
|
),
|
||||||
|
ft.Divider(),
|
||||||
|
*[
|
||||||
|
ft.NavigationDrawerDestination(icon=icon, label=category)
|
||||||
for category, icon in zip(self.categories, icons, strict=True)
|
for category, icon in zip(self.categories, icons, strict=True)
|
||||||
|
],
|
||||||
],
|
],
|
||||||
label_type=ft.NavigationRailLabelType.ALL,
|
|
||||||
on_change=select,
|
on_change=select,
|
||||||
min_width=96,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def icon_catalog(self) -> ft.Control:
|
||||||
|
result_count = ft.Text()
|
||||||
|
page_label = ft.Text()
|
||||||
|
icon_grid = ft.GridView(
|
||||||
|
controls=[],
|
||||||
|
max_extent=140,
|
||||||
|
child_aspect_ratio=1.1,
|
||||||
|
spacing=8,
|
||||||
|
run_spacing=8,
|
||||||
|
expand=True,
|
||||||
|
)
|
||||||
|
previous = ft.IconButton(ft.Icons.CHEVRON_LEFT, tooltip="Предыдущая страница")
|
||||||
|
following = ft.IconButton(ft.Icons.CHEVRON_RIGHT, tooltip="Следующая страница")
|
||||||
|
|
||||||
|
async def copy_icon(name: str) -> None:
|
||||||
|
await self.clipboard.set(icon_snippet(name))
|
||||||
|
self.page.show_dialog(ft.SnackBar(f"Скопировано: {icon_snippet(name)}"))
|
||||||
|
|
||||||
|
def refresh(*, initial: bool = False) -> None:
|
||||||
|
matches = matching_icons(self.icon_query)
|
||||||
|
max_page = max(0, (len(matches) - 1) // PAGE_SIZE)
|
||||||
|
self.icon_page = min(self.icon_page, max_page)
|
||||||
|
start = self.icon_page * PAGE_SIZE
|
||||||
|
icon_grid.controls = [
|
||||||
|
ft.Container(
|
||||||
|
content=ft.Column(
|
||||||
|
controls=[
|
||||||
|
ft.Icon(getattr(ft.Icons, name), size=30),
|
||||||
|
ft.Text(name, size=11, text_align=ft.TextAlign.CENTER, max_lines=2),
|
||||||
|
],
|
||||||
|
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||||||
|
alignment=ft.MainAxisAlignment.CENTER,
|
||||||
|
spacing=8,
|
||||||
|
),
|
||||||
|
tooltip=icon_snippet(name),
|
||||||
|
bgcolor=ft.Colors.SURFACE_CONTAINER,
|
||||||
|
border_radius=12,
|
||||||
|
on_click=lambda e, icon_name=name: self.page.run_task(copy_icon, icon_name),
|
||||||
|
)
|
||||||
|
for name in matches[start : start + PAGE_SIZE]
|
||||||
|
]
|
||||||
|
result_count.value = f"Найдено: {len(matches)}"
|
||||||
|
page_label.value = f"{self.icon_page + 1} / {max_page + 1}"
|
||||||
|
previous.disabled = self.icon_page == 0
|
||||||
|
following.disabled = self.icon_page >= max_page
|
||||||
|
if not initial:
|
||||||
|
result_count.update()
|
||||||
|
page_label.update()
|
||||||
|
icon_grid.update()
|
||||||
|
previous.update()
|
||||||
|
following.update()
|
||||||
|
|
||||||
|
def search(e: ft.Event[ft.TextField]) -> None:
|
||||||
|
self.icon_query = e.control.value or ""
|
||||||
|
self.icon_page = 0
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
def turn_page(delta: int) -> None:
|
||||||
|
self.icon_page += delta
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
previous.on_click = lambda e: turn_page(-1)
|
||||||
|
following.on_click = lambda e: turn_page(1)
|
||||||
|
search_field = ft.TextField(
|
||||||
|
value=self.icon_query,
|
||||||
|
hint_text="Найти иконку по имени, например HOME или ARROW",
|
||||||
|
prefix_icon=ft.Icons.SEARCH,
|
||||||
|
on_change=search,
|
||||||
|
)
|
||||||
|
container = ft.Column(
|
||||||
|
controls=[
|
||||||
|
self.header(),
|
||||||
|
search_field,
|
||||||
|
ft.Row([result_count, ft.Container(expand=True), previous, page_label, following]),
|
||||||
|
icon_grid,
|
||||||
|
],
|
||||||
|
expand=True,
|
||||||
|
spacing=12,
|
||||||
|
)
|
||||||
|
refresh(initial=True)
|
||||||
|
return ft.Container(container, padding=ft.Padding.symmetric(horizontal=24, vertical=20), expand=True)
|
||||||
|
|
||||||
def render(self) -> None:
|
def render(self) -> None:
|
||||||
self.page.clean()
|
self.page.clean()
|
||||||
self.page.title = "Flet Component Gallery"
|
self.page.title = "Flet Component Gallery"
|
||||||
self.page.padding = 0
|
self.page.padding = 0
|
||||||
self.apply_theme()
|
self.apply_theme()
|
||||||
|
|
||||||
def search(e: ft.Event[ft.TextField]) -> None:
|
if self.selected_category == "Иконки":
|
||||||
self.query = e.control.value or ""
|
body = self.icon_catalog()
|
||||||
self.render()
|
else:
|
||||||
|
body = ft.ListView(
|
||||||
body = ft.ListView(
|
controls=[self.header(), *[self.example_card(example) for example in self.current_examples()], ft.Container(height=24)],
|
||||||
controls=[
|
expand=True,
|
||||||
self.header(),
|
padding=ft.Padding.symmetric(horizontal=24, vertical=20),
|
||||||
ft.TextField(
|
spacing=16,
|
||||||
value=self.query,
|
)
|
||||||
hint_text="Найти компонент…",
|
self.page.drawer = self.navigation()
|
||||||
prefix_icon=ft.Icons.SEARCH,
|
|
||||||
on_submit=search,
|
|
||||||
suffix=ft.IconButton(ft.Icons.CLOSE, tooltip="Сбросить", on_click=lambda e: self.clear_search()),
|
|
||||||
),
|
|
||||||
*[self.example_card(example) for example in self.current_examples()],
|
|
||||||
ft.Container(height=24),
|
|
||||||
],
|
|
||||||
expand=True,
|
|
||||||
padding=ft.Padding.symmetric(horizontal=24, vertical=20),
|
|
||||||
spacing=16,
|
|
||||||
)
|
|
||||||
self.page.appbar = ft.AppBar(
|
self.page.appbar = ft.AppBar(
|
||||||
leading=ft.Icon(ft.Icons.WIDGETS_OUTLINED),
|
leading=ft.IconButton(ft.Icons.MENU, tooltip="Открыть меню", on_click=lambda e: self.page.run_task(self.page.show_drawer)),
|
||||||
|
automatically_imply_leading=False,
|
||||||
title=ft.Text("Flet Material Catalog"),
|
title=ft.Text("Flet Material Catalog"),
|
||||||
actions=[
|
actions=[
|
||||||
ft.IconButton(ft.Icons.SETTINGS_OUTLINED, tooltip="Настройки", on_click=self.open_settings),
|
ft.IconButton(ft.Icons.SETTINGS_OUTLINED, tooltip="Настройки", on_click=self.open_settings),
|
||||||
ft.IconButton(ft.Icons.OPEN_IN_NEW, tooltip="Репозиторий", url="https://git.snw.su/snw/FletAppDemo"),
|
ft.IconButton(ft.Icons.OPEN_IN_NEW, tooltip="Репозиторий", url="https://git.snw.su/snw/FletAppDemo"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
self.page.add(ft.Row([self.navigation(), ft.VerticalDivider(width=1), body], expand=True, spacing=0))
|
self.page.add(body)
|
||||||
self.page.update()
|
self.page.update()
|
||||||
|
|
||||||
def clear_search(self) -> None:
|
|
||||||
self.query = ""
|
|
||||||
self.render()
|
|
||||||
|
|
||||||
|
|
||||||
def main(page: ft.Page) -> None:
|
def main(page: ft.Page) -> None:
|
||||||
GalleryApp(page).render()
|
GalleryApp(page).render()
|
||||||
|
|
|
||||||
18
src/gallery/icon_catalog.py
Normal file
18
src/gallery/icon_catalog.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
"""Searchable names from the icon set shipped with the pinned Flet version."""
|
||||||
|
|
||||||
|
import flet as ft
|
||||||
|
|
||||||
|
ICON_NAMES = tuple(sorted(name for name in dir(ft.Icons) if name.isupper()))
|
||||||
|
PAGE_SIZE = 80
|
||||||
|
|
||||||
|
|
||||||
|
def matching_icons(query: str) -> tuple[str, ...]:
|
||||||
|
words = query.upper().split()
|
||||||
|
return tuple(name for name in ICON_NAMES if all(word in name for word in words))
|
||||||
|
|
||||||
|
|
||||||
|
def icon_snippet(name: str) -> str:
|
||||||
|
if name not in ICON_NAMES:
|
||||||
|
raise ValueError(f"Unknown Flet icon: {name}")
|
||||||
|
return f"ft.Icon(ft.Icons.{name})"
|
||||||
|
|
||||||
|
|
@ -2,6 +2,7 @@ import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from gallery.examples import ALL_EXAMPLES
|
from gallery.examples import ALL_EXAMPLES
|
||||||
|
from gallery.icon_catalog import ICON_NAMES, PAGE_SIZE, icon_snippet, matching_icons
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
@ -25,3 +26,9 @@ def test_source_manifest_is_complete_and_exact() -> None:
|
||||||
source = (ROOT / anchor["path"]).read_text(encoding="utf-8").splitlines()
|
source = (ROOT / anchor["path"]).read_text(encoding="utf-8").splitlines()
|
||||||
assert source[anchor["line"] - 1].startswith(f"def {anchor['function']}(")
|
assert source[anchor["line"] - 1].startswith(f"def {anchor['function']}(")
|
||||||
|
|
||||||
|
|
||||||
|
def test_icon_catalog_searches_every_icon_and_copies_valid_flet_code() -> None:
|
||||||
|
assert len(ICON_NAMES) > PAGE_SIZE
|
||||||
|
assert "HOME" in matching_icons("home")
|
||||||
|
assert "HOME_OUTLINED" in matching_icons("home outlined")
|
||||||
|
assert icon_snippet("HOME") == "ft.Icon(ft.Icons.HOME)"
|
||||||
|
|
|
||||||
2
uv.lock
generated
2
uv.lock
generated
|
|
@ -50,7 +50,7 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flet-component-gallery"
|
name = "flet-component-gallery"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "flet" },
|
{ name = "flet" },
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue