From b812ed21c5de17f3863a159ec1c196c3a3155444 Mon Sep 17 00:00:00 2001 From: snw Date: Thu, 24 Sep 2026 00:35:20 +0000 Subject: [PATCH] Add drawer navigation and searchable icon catalog --- README.md | 4 + pyproject.toml | 2 +- src/gallery/app.py | 161 ++++++++++++++++++++++++++---------- src/gallery/icon_catalog.py | 18 ++++ tests/test_gallery.py | 7 ++ uv.lock | 2 +- 6 files changed, 147 insertions(+), 47 deletions(-) create mode 100644 src/gallery/icon_catalog.py diff --git a/README.md b/README.md index 4a39a80..124da7f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ типографика, карточки, списки, адаптивные layouts, навигация, таблицы, progress, dialogs, snackbar и bottom sheet. Постоянная кнопка настроек позволяет менять светлую/тёмную/системную тему, 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 \ --project flet_component_gallery \ --product "Flet Material Catalog" \ + --build-version 0.2.0 \ --org su.snw ``` diff --git a/pyproject.toml b/pyproject.toml index 21a8075..b28694d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "flet-component-gallery" -version = "0.1.0" +version = "0.2.0" description = "Composer-inspired interactive gallery of Flet controls" readme = "README.md" requires-python = ">=3.11" diff --git a/src/gallery/app.py b/src/gallery/app.py index ae03f11..c1784f9 100644 --- a/src/gallery/app.py +++ b/src/gallery/app.py @@ -1,6 +1,7 @@ import flet as ft 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.source_links import source_url @@ -18,11 +19,13 @@ class GalleryApp: self.page = page self.settings = GallerySettings() self.selected_category = "Обзор" - self.query = "" + self.icon_query = "" + self.icon_page = 0 + self.clipboard = ft.Clipboard() @property 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: self.page.theme_mode = self.settings.theme_mode @@ -136,29 +139,22 @@ class GalleryApp: examples = ALL_EXAMPLES if 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 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( controls=[ ft.Text(self.selected_category, theme_style=ft.TextThemeStyle.DISPLAY_SMALL), 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, ), ], spacing=4, ) - def navigation(self) -> ft.Control: + def navigation(self) -> ft.NavigationDrawer: icons = [ ft.Icons.HOME_OUTLINED, ft.Icons.TOUCH_APP_OUTLINED, @@ -167,65 +163,140 @@ class GalleryApp: ft.Icons.GRID_VIEW, ft.Icons.NAVIGATION_OUTLINED, 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] + await self.page.close_drawer() self.render() - return ft.NavigationRail( + return ft.NavigationDrawer( selected_index=self.categories.index(self.selected_category), - destinations=[ - ft.NavigationRailDestination(icon=icon, label=category) + controls=[ + 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) + ], ], - label_type=ft.NavigationRailLabelType.ALL, 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: self.page.clean() self.page.title = "Flet Component Gallery" self.page.padding = 0 self.apply_theme() - def search(e: ft.Event[ft.TextField]) -> None: - self.query = e.control.value or "" - self.render() - - body = ft.ListView( - controls=[ - self.header(), - ft.TextField( - value=self.query, - hint_text="Найти компонент…", - 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, - ) + if self.selected_category == "Иконки": + body = self.icon_catalog() + else: + body = ft.ListView( + controls=[self.header(), *[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.drawer = self.navigation() 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"), actions=[ 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"), ], ) - self.page.add(ft.Row([self.navigation(), ft.VerticalDivider(width=1), body], expand=True, spacing=0)) + self.page.add(body) self.page.update() - def clear_search(self) -> None: - self.query = "" - self.render() - def main(page: ft.Page) -> None: GalleryApp(page).render() diff --git a/src/gallery/icon_catalog.py b/src/gallery/icon_catalog.py new file mode 100644 index 0000000..e4634f5 --- /dev/null +++ b/src/gallery/icon_catalog.py @@ -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})" + diff --git a/tests/test_gallery.py b/tests/test_gallery.py index 31de425..d6922e3 100644 --- a/tests/test_gallery.py +++ b/tests/test_gallery.py @@ -2,6 +2,7 @@ import json from pathlib import Path 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] @@ -25,3 +26,9 @@ def test_source_manifest_is_complete_and_exact() -> None: source = (ROOT / anchor["path"]).read_text(encoding="utf-8").splitlines() 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)" diff --git a/uv.lock b/uv.lock index cd3944d..da4f4c2 100644 --- a/uv.lock +++ b/uv.lock @@ -50,7 +50,7 @@ wheels = [ [[package]] name = "flet-component-gallery" -version = "0.1.0" +version = "0.2.0" source = { virtual = "." } dependencies = [ { name = "flet" },