import flet as ft import designer as d import api_client as api class NewsFeedView: def __init__(self, page: ft.Page): self.page = page self._articles: list = [] self._offset = 0 self._limit = 20 self._has_more = True self._loading = False self._category: str | None = None self._categories: list = [] self._list_col = ft.Column(spacing=16) self._loader = ft.Container( content=d.loader(36), alignment=ft.alignment.center, padding=20, visible=True, ) self._load_more_btn = ft.ElevatedButton( "Загрузить ещё", icon=ft.icons.EXPAND_MORE, on_click=lambda e: self.page.run_task(self._load_more), visible=False, style=ft.ButtonStyle( bgcolor=d.SURFACE, color=d.PRIMARY, shape=ft.RoundedRectangleBorder(radius=8), ), ) self._cat_row = ft.Row(scroll=ft.ScrollMode.AUTO, spacing=8) def build_page_view(self) -> ft.View: header = ft.Container( content=ft.Row( [ ft.Row([ ft.Icon(ft.icons.NEWSPAPER, color=ft.colors.WHITE, size=28), ft.Text("Новости", size=20, weight=ft.FontWeight.BOLD, color=ft.colors.WHITE), ], spacing=8), ft.Row([ ft.TextButton( "Войти", icon=ft.icons.ADMIN_PANEL_SETTINGS_OUTLINED, style=ft.ButtonStyle(color=ft.colors.WHITE70), on_click=lambda e: self.page.go("/admin/login"), ) ]), ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN, ), bgcolor=d.ADMIN_SIDEBAR, padding=ft.padding.symmetric(horizontal=24, vertical=14), ) cat_section = ft.Container( content=ft.Column([ ft.Container(content=self._cat_row, padding=ft.padding.symmetric(horizontal=24, vertical=8)), ]), bgcolor=d.SURFACE, border=ft.Border(bottom=ft.BorderSide(1, d.DIVIDER)), ) main_content = ft.Container( content=ft.Column( [ self._list_col, self._loader, ft.Container( content=self._load_more_btn, alignment=ft.alignment.center, padding=16, ), ], spacing=0, ), padding=ft.padding.symmetric(horizontal=24, vertical=20), alignment=ft.alignment.top_center, ) return ft.View( route="/", bgcolor=d.BACKGROUND, controls=[ ft.Column( [ header, cat_section, ft.Container( content=main_content, alignment=ft.alignment.top_center, expand=True, ), ], spacing=0, expand=True, scroll=ft.ScrollMode.AUTO, ) ], ) async def load(self): self._categories = await api.get_categories() self._render_categories() await self._load_articles() async def _load_more(self): if self._loading or not self._has_more: return self._loading = True self._load_more_btn.disabled = True self.page.update() self._offset += self._limit await self._load_articles() self._loading = False self._load_more_btn.disabled = False self.page.update() async def _load_articles(self): self._loader.visible = True self.page.update() data = await api.get_news(offset=self._offset, limit=self._limit, category=self._category) self._loader.visible = False if not data: self.page.update() return items = data.get("items", []) self._articles.extend(items) self._has_more = data.get("has_more", False) for a in items: self._list_col.controls.append(self._build_card(a)) self._load_more_btn.visible = self._has_more self.page.update() def _render_categories(self): self._cat_row.controls = [ self._cat_chip(None, "Все") ] + [ self._cat_chip(c["slug"], c["name"]) for c in self._categories ] self.page.update() def _cat_chip(self, slug: str | None, label: str) -> ft.Chip: is_active = self._category == slug return ft.Chip( label=ft.Text(label, size=13), bgcolor=d.PRIMARY if is_active else d.SURFACE, label_style=ft.TextStyle(color=ft.colors.WHITE if is_active else d.TEXT_PRIMARY), on_click=lambda e, s=slug: self.page.run_task(self._filter_category, s), padding=ft.padding.symmetric(horizontal=12, vertical=6), side=ft.BorderSide(1, d.PRIMARY if is_active else d.DIVIDER), ) async def _filter_category(self, slug: str | None): self._category = slug self._articles = [] self._offset = 0 self._has_more = True self._list_col.controls.clear() self._render_categories() await self._load_articles() def _build_card(self, article: dict) -> ft.Container: pub_date = "" if article.get("published_at"): pub_date = article["published_at"][:10] cat = article.get("category") cat_chip = ft.Container( content=ft.Text(cat["name"], size=11, color=ft.colors.WHITE), bgcolor=d.PRIMARY, border_radius=4, padding=ft.padding.symmetric(horizontal=8, vertical=3), visible=bool(cat), ) if cat else ft.Container(visible=False) cover = ft.Image( src=article["cover_url"], width=260, height=160, fit=ft.ImageFit.COVER, border_radius=ft.BorderRadius(0, 8, 8, 0), ) if article.get("cover_url") else ft.Container(visible=False) text_part = ft.Column( [ ft.Row([cat_chip, ft.Text(pub_date, size=11, color=d.TEXT_SECONDARY)], spacing=8), ft.Text( article["title"], size=18, weight=ft.FontWeight.BOLD, color=d.TEXT_PRIMARY, max_lines=3, overflow=ft.TextOverflow.ELLIPSIS, ), ft.Text( article.get("excerpt", ""), size=13, color=d.TEXT_SECONDARY, max_lines=3, overflow=ft.TextOverflow.ELLIPSIS, ), ft.Row( [ ft.Row([ ft.Icon(ft.icons.REMOVE_RED_EYE_OUTLINED, size=14, color=d.TEXT_SECONDARY), ft.Text(str(article.get("view_count", 0)), size=12, color=d.TEXT_SECONDARY), ], spacing=4), ft.TextButton( "Читать →", style=ft.ButtonStyle(color=d.PRIMARY), on_click=lambda e, s=article["slug"]: self.page.go(f"/news/{s}"), ), ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN, ), ], spacing=8, expand=True, ) return ft.Container( content=ft.Row([ft.Container(content=text_part, expand=True, padding=20), cover], spacing=0), bgcolor=d.SURFACE, border_radius=12, border=ft.border.all(1, d.DIVIDER), shadow=ft.BoxShadow(blur_radius=6, color=ft.colors.with_opacity(0.05, ft.colors.BLACK)), clip_behavior=ft.ClipBehavior.HARD_EDGE, on_click=lambda e, s=article["slug"]: self.page.go(f"/news/{s}"), ink=True, )