import flet as ft import designer as d import api_client as api from views.dashboard import _admin_appbar, _sidebar class MediaLibraryView: def __init__(self, page: ft.Page): self.page = page self._grid = ft.GridView( expand=True, runs_count=4, max_extent=200, child_aspect_ratio=1.0, spacing=12, run_spacing=12, ) self._filter = None self._picker = ft.FilePicker(on_result=self._on_files_picked) def build_page_view(self) -> ft.View: filter_tabs = ft.Tabs( selected_index=0, on_change=self._on_filter, tabs=[ft.Tab(text="Все"), ft.Tab(text="Изображения"), ft.Tab(text="Видео")], ) return ft.View( route="/admin/media", bgcolor=d.BACKGROUND, appbar=_admin_appbar(self.page, "Медиа"), controls=[ ft.Row( [ _sidebar(self.page, active="/admin/media"), ft.Container( content=ft.Column( [ ft.Row( [ d.headline("Медиатека", size=22), d.btn_primary( "Загрузить файлы", icon=ft.icons.UPLOAD, on_click=lambda e: self._picker.pick_files( allow_multiple=True, dialog_title="Выберите изображения или видео", ), ), ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN, ), filter_tabs, self._grid, ], expand=True, spacing=16, ), expand=True, padding=24, ), ], expand=True, spacing=0, ), self._picker, ], ) async def load(self): await self._reload() async def _on_filter(self, e): self._filter = [None, "image", "video"][e.control.selected_index] await self._reload() async def _reload(self): token = self.page.session.get("token") items = await api.admin_list_media(token, limit=200, media_type=self._filter) self._render(items) def _render(self, items: list): if not items: self._grid.controls = [ d.empty_state(ft.icons.PERM_MEDIA_OUTLINED, "Медиафайлов пока нет") ] self.page.update() return self._grid.controls = [self._build_card(m) for m in items] self.page.update() def _build_card(self, m: dict) -> ft.Container: is_image = m.get("media_type") == "image" content = ( ft.Image(src=m["url"], fit=ft.ImageFit.COVER, expand=True, error_content=ft.Icon(ft.icons.BROKEN_IMAGE)) if is_image else ft.Column( [ft.Icon(ft.icons.PLAY_CIRCLE_OUTLINED, size=40, color=d.TEXT_SECONDARY), ft.Text("Видео", size=11, color=d.TEXT_SECONDARY)], alignment=ft.MainAxisAlignment.CENTER, horizontal_alignment=ft.CrossAxisAlignment.CENTER, expand=True, ) ) size_kb = round(m.get("size_bytes", 0) / 1024, 1) name = m.get("filename", "")[:20] return ft.Container( content=ft.Stack( [ ft.Container(content=content, expand=True, bgcolor="#F0F0F0", border_radius=8), ft.Container( content=ft.Row( [ ft.Container( content=ft.Column( [ ft.Text(name, size=10, color=ft.colors.WHITE, max_lines=1, overflow=ft.TextOverflow.ELLIPSIS), ft.Text(f"{size_kb} КБ", size=9, color=ft.colors.WHITE70), ], spacing=2, expand=True, ), expand=True, ), ft.Row( [ ft.IconButton( ft.icons.COPY, icon_size=14, icon_color=ft.colors.WHITE, tooltip="Копировать URL", on_click=lambda e, url=m["url"]: self._copy_url(url), ), ft.IconButton( ft.icons.DELETE_OUTLINE, icon_size=14, icon_color=ft.colors.WHITE, tooltip="Удалить", on_click=lambda e, mid=m["id"]: self._confirm_delete(mid, m.get("filename", "")), ), ], spacing=0, ), ], spacing=0, ), bgcolor=ft.colors.with_opacity(0.6, ft.colors.BLACK), border_radius=ft.BorderRadius(0, 0, 8, 8), padding=ft.padding.symmetric(horizontal=8, vertical=6), bottom=0, left=0, right=0, ), ], expand=True, ), height=180, border_radius=8, border=ft.border.all(1, d.DIVIDER), clip_behavior=ft.ClipBehavior.HARD_EDGE, ) def _copy_url(self, url: str): self.page.set_clipboard(url) d.snack(self.page, "URL скопирован в буфер обмена") def _confirm_delete(self, media_id: str, filename: str): dlg = d.confirm_dialog( self.page, "Удалить файл?", f'"{filename}" будет удалён с сервера.', lambda: self.page.run_task(self._delete, media_id), ) self.page.dialog = dlg dlg.open = True self.page.update() async def _delete(self, media_id: str): token = self.page.session.get("token") ok = await api.admin_delete_media(token, media_id) if ok: d.snack(self.page, "Файл удалён") await self._reload() else: d.snack(self.page, "Ошибка удаления", error=True) async def _on_files_picked(self, e: ft.FilePickerResultEvent): if not e.files: return token = self.page.session.get("token") uploaded = 0 for f in e.files: ext = (f.name or "").rsplit(".", 1)[-1].lower() video_exts = {"mp4", "webm", "ogg", "mov"} kind = "video" if ext in video_exts else "image" ct_map = { "jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "gif": "image/gif", "webp": "image/webp", "mp4": "video/mp4", "webm": "video/webm", "ogg": "video/ogg", "mov": "video/quicktime", } ct = ct_map.get(ext, "application/octet-stream") try: if hasattr(f, "path") and f.path: with open(f.path, "rb") as fh: data = fh.read() result = await api.admin_upload_media(token, data, f.name, ct) if result: uploaded += 1 except Exception: pass d.snack(self.page, f"Загружено файлов: {uploaded}") await self._reload()