Add drawer navigation and searchable icon catalog

This commit is contained in:
snw 2026-09-24 00:35:20 +00:00
parent 6e647fed4b
commit b812ed21c5
6 changed files with 147 additions and 47 deletions

View file

@ -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
``` ```

View file

@ -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"

View file

@ -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()

View 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})"

View file

@ -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
View file

@ -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" },