import flet as ft import requests from helpers.roles import Roles class Articles: def __init__(self, page: ft.Page, home): self.page = page self.home = home self.base_url = self.page.session.store.get('api_base_url') self.token = self.page.session.store.get('token') self.user = self.page.session.store.get('user') or {} self.all_articles = [] self.selected_article_id = None # UI Elements for Search self.search_bar = ft.TextField( label="Caută articole după titlu...", prefix_icon=ft.Icons.SEARCH, on_change=self.on_search_change, expand=True, hint_text="Introdu titlul sau fragmente din titlu..." ) # UI list container self.articles_container = ft.Column( scroll=ft.ScrollMode.AUTO, spacing=15, expand=True ) # Delete confirmation dialog self.article_id_to_delete = None self.delete_confirm_dialog = ft.AlertDialog( title=ft.Text("Confirmare Ștergere"), content=ft.Text("Sigur doriți să ștergeți acest articol? Această acțiune este ireversibilă."), actions=[ ft.FilledButton("Șterge", on_click=self.confirm_delete_article, bgcolor=ft.Colors.RED), ft.FilledButton("Anulează", on_click=self.close_dialog, bgcolor=ft.Colors.GREY) ], actions_alignment=ft.MainAxisAlignment.END ) # Read Article Modal Dialog self.read_title = ft.Text(size=22, weight=ft.FontWeight.BOLD) self.read_meta = ft.Text(size=12, italic=True, color=ft.Colors.GREY_600) self.read_content = ft.Markdown(selectable=True, expand=True) self.read_article_dialog = ft.AlertDialog( content=ft.Container( content=ft.Column( [ self.read_title, self.read_meta, ft.Divider(height=10), ft.Column( [self.read_content], scroll=ft.ScrollMode.AUTO, expand=True ) ], spacing=10, ), width=650, height=500, ), actions=[ ft.TextButton("Închide", on_click=self.close_dialog) ] ) def load_articles(self): try: response = requests.get( f"{self.base_url}/articles/", headers={"Authorization": f"Bearer {self.token}"} ) if response.status_code == 200: self.all_articles = response.json() else: self.all_articles = [] except Exception as e: print(f"Error fetching articles: {e}") self.all_articles = [] def populate_articles_list(self, query=""): self.articles_container.controls.clear() query = query.strip().lower() filtered = [ art for art in self.all_articles if query in art.get('title', '').lower() ] if not filtered: self.articles_container.controls.append( ft.Container( content=ft.Text("Nu s-au găsit articole publicate.", size=16, color=ft.Colors.GREY_600), alignment=ft.Alignment.CENTER, padding=40 ) ) else: can_publish = self.user.get('can_create_articles') == 1 user_id = self.user.get('id') for art in filtered: # Curățăm formatarea markdown pentru previzualizare preview = self.strip_markdown(art.get('content', '')) if len(preview) > 180: preview = preview[:180] + "..." # Check if logged-in user is the author of this post is_author = art.get('author_id') == user_id # Formatează data raw_date = art.get('created_at', '') formatted_date = raw_date.replace("T", " ").split(".")[0] if raw_date else "Dată necunoscută" # Row of actions actions_row = ft.Row( [ ft.TextButton( "Citește Articolul", icon=ft.Icons.ARROW_FORWARD, on_click=lambda e, a=art: self.open_read_dialog(a) ) ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN ) # Daca utilizatorul este autorul, adaugam butoane de Edit/Delete if can_publish and is_author: actions_row.controls.append( ft.Row( [ ft.IconButton( icon=ft.Icons.EDIT_OUTLINED, tooltip="Editează articolul", icon_color=ft.Colors.BLUE_700, on_click=lambda e, a=art: self.page.run_task(self.open_edit_dialog, a) ), ft.IconButton( icon=ft.Icons.DELETE_OUTLINE, tooltip="Șterge articolul", icon_color=ft.Colors.RED_600, on_click=lambda e, a_id=art['id']: self.open_delete_confirm(a_id) ) ], spacing=5 ) ) self.articles_container.controls.append( ft.Container( content=ft.Column( [ ft.Text(art.get('title', 'Fără Titlu'), weight=ft.FontWeight.BOLD, size=18, color=ft.Colors.BLUE_GREY_900), ft.Row( [ ft.Icon(ft.Icons.PERSON_OUTLINE, size=14, color=ft.Colors.GREY_500), ft.Text(f"Autor: {art.get('author_name', 'Necunoscut')}", size=12, color=ft.Colors.GREY_600), ft.VerticalDivider(width=1, color=ft.Colors.GREY_300), ft.Icon(ft.Icons.ACCESS_TIME, size=14, color=ft.Colors.GREY_500), ft.Text(f"Publicat: {formatted_date}", size=12, color=ft.Colors.GREY_600), ], spacing=10 ), ft.Divider(height=5, color=ft.Colors.TRANSPARENT), ft.Text(preview, size=14, color=ft.Colors.GREY_700), ft.Divider(height=10), actions_row ], spacing=10 ), bgcolor=ft.Colors.WHITE, padding=20, border_radius=12, border=ft.Border.all(1, ft.Colors.GREY_200), shadow=ft.BoxShadow( blur_radius=8, color=ft.Colors.with_opacity(0.04, ft.Colors.BLACK), offset=ft.Offset(0, 3) ) ) ) #self.articles_container.update() def on_search_change(self, e): self.populate_articles_list(self.search_bar.value) def strip_markdown(self, text): if not text: return "" import re # Elimină antetele (ex: # Titlu 1) text = re.sub(r'(?m)^#{1,6}\s+', '', text) # Elimină marcatorii de listă (ex: - element) text = re.sub(r'(?m)^[\-\*\+]\s+', '', text) # Elimină caracterele pentru bold, italic și inline-code text = re.sub(r'\*\*|__|\*|_|`', '', text) # Înlocuiește multiplele spații sau linii noi cu un singur spațiu text = re.sub(r'\s+', ' ', text) return text.strip() # Creation/Editing Dialog (deschise în tab separat) async def open_add_dialog(self, e): editor_url = f"{self.base_url}/articles/editor?token={self.token}" await self.page.launch_url(editor_url) async def open_edit_dialog(self, article): print('edit') editor_url = f"{self.base_url}/articles/editor?token={self.token}&article_id={article['id']}" await self.page.launch_url(editor_url) # Delete Dialog def open_delete_confirm(self, article_id): self.article_id_to_delete = article_id self.page.show_dialog(self.delete_confirm_dialog) self.page.update() def confirm_delete_article(self, e): if not self.article_id_to_delete: return try: response = requests.delete( f"{self.base_url}/articles/delete/{self.article_id_to_delete}", headers={"Authorization": f"Bearer {self.token}"} ) if response.status_code == 200: self.page.pop_dialog() self.load_articles() self.populate_articles_list(self.search_bar.value) self.show_snack_bar("Articolul a fost șters cu succes.", ft.Colors.GREEN) else: self.page.pop_dialog() err = response.json().get('error', 'Eroare la ștergerea articolului.') self.show_snack_bar(err, ft.Colors.RED) except Exception as ex: print(f"Error deleting article: {ex}") self.page.pop_dialog() self.show_snack_bar("Eroare de conexiune la server.", ft.Colors.RED) # Read modal dialog def open_read_dialog(self, article): self.read_title.value = article.get('title', 'Fără Titlu') raw_date = article.get('created_at', '') formatted_date = raw_date.replace("T", " ").split(".")[0] if raw_date else "Dată necunoscută" self.read_meta.value = f"Scris de {article.get('author_name', 'Necunoscut')} la data de {formatted_date}" self.read_content.value = article.get('content', '') self.page.show_dialog(self.read_article_dialog) self.page.update() def close_dialog(self, e): self.page.pop_dialog() def show_snack_bar(self, message, color): self.page.snack_bar = ft.SnackBar( content=ft.Text(message, color=ft.Colors.WHITE), bgcolor=color, duration=3000 ) self.page.snack_bar.open = True self.page.update() def on_refresh_click(self, e): self.load_articles() self.populate_articles_list(self.search_bar.value) self.show_snack_bar("Lista de articole a fost actualizată.", ft.Colors.GREEN) def build(self): self.load_articles() self.populate_articles_list() # Check if the user is an expert with posting privileges can_publish = self.user.get('can_create_articles') == 1 header_row = ft.Row( [ ft.Column( [ ft.Text("Articole și Publicații", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_GREY_900), ft.Text("Răsfoiți noutățile, ghidurile și publicațiile juridice recente.", size=14, color=ft.Colors.GREY_600) ], spacing=2 ) ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN ) # Dacă utilizatorul are bifa de publicare, adăugăm butonul de Adăugare if can_publish: header_row.controls.append( ft.FilledButton( "Adaugă Articol", icon=ft.Icons.ADD, #on_click=lambda e: self.page.run_task(self.open_add_dialog(e)), on_click=self.open_add_dialog, style=ft.ButtonStyle( shape=ft.RoundedRectangleBorder(radius=8) ) ) ) refresh_btn = ft.IconButton( icon=ft.Icons.REFRESH, tooltip="Reîncarcă articolele", icon_color=ft.Colors.BLUE_700, on_click=self.on_refresh_click ) return ft.Container( content=ft.Column( [ header_row, ft.Divider(height=10, color=ft.Colors.TRANSPARENT), ft.Row( [ self.search_bar, refresh_btn ], spacing=10 ), ft.Divider(height=10, color=ft.Colors.GREY_100), self.articles_container ], expand=True ), expand=True, padding=20, bgcolor=ft.Colors.GREY_50 )