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,
|
||||
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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
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 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)"
|
||||
|
|
|
|||
2
uv.lock
generated
2
uv.lock
generated
|
|
@ -50,7 +50,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "flet-component-gallery"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "flet" },
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue