# markdownconverter/gui/batch_converter.py import os import tkinter as tk from pathlib import Path from tkinter import filedialog, messagebox, StringVar, BooleanVar import ttkbootstrap as tb from tkinter.scrolledtext import ScrolledText from ttkbootstrap.constants import * from ..core.core import ( combine_markdown_files, convert_markdown_to_docx_with_pandoc, convert_docx_to_pdf ) from ..utils.logger import get_logger log = get_logger(__name__) class BatchConverterTab(tb.Frame): """ Tab per la conversione batch di file markdown da una cartella. Trova tutti i file markdown numerati (es. 01_*.md, 02_*.md), li combina in ordine alfabetico e genera DOCX e/o PDF. """ def __init__(self, parent): super().__init__(parent, padding=10) self.folder_path = StringVar() self.output_name = StringVar(value="manuale") self.template_path = StringVar() self.use_template = BooleanVar(value=False) self.generate_pdf = BooleanVar(value=True) self._build_ui() def _build_ui(self): """Costruisce l'interfaccia utente del tab batch.""" # Frame per la selezione della cartella folder_frame = tb.Labelframe(self, text="Cartella Sorgente", padding=10) folder_frame.pack(fill=tk.X, pady=(0, 10)) folder_frame.columnconfigure(1, weight=1) tb.Label(folder_frame, text="Cartella Markdown:").grid( row=0, column=0, padx=5, pady=5, sticky="w" ) tb.Entry(folder_frame, textvariable=self.folder_path).grid( row=0, column=1, padx=5, pady=5, sticky="ew" ) tb.Button( folder_frame, text="Sfoglia...", command=self._choose_folder, bootstyle=PRIMARY ).grid(row=0, column=2, padx=5, pady=5) # Frame per le opzioni options_frame = tb.Labelframe(self, text="Opzioni di Conversione", padding=10) options_frame.pack(fill=tk.X, pady=(0, 10)) options_frame.columnconfigure(1, weight=1) tb.Label(options_frame, text="Nome base output:").grid( row=0, column=0, padx=5, pady=5, sticky="w" ) tb.Entry(options_frame, textvariable=self.output_name).grid( row=0, column=1, padx=5, pady=5, sticky="ew" ) # Checkbox per template tb.Checkbutton( options_frame, text="Usa template DOCX", variable=self.use_template, command=self._toggle_template, bootstyle="primary-round-toggle" ).grid(row=1, column=0, padx=5, pady=5, sticky="w") # Frame per template self.template_entry = tb.Entry( options_frame, textvariable=self.template_path, state="disabled" ) self.template_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew") self.template_button = tb.Button( options_frame, text="Seleziona template", command=self._choose_template, state="disabled" ) self.template_button.grid(row=1, column=2, padx=5, pady=5) # Checkbox per PDF tb.Checkbutton( options_frame, text="Genera anche PDF finale", variable=self.generate_pdf, bootstyle="primary-round-toggle" ).grid(row=2, column=0, columnspan=2, padx=5, pady=5, sticky="w") # Pulsante di conversione tb.Button( self, text="Genera Documento", command=self._generate_output, bootstyle=SUCCESS, width=20 ).pack(pady=10) # Frame che mostra la lista dei file trovati nella cartella selezionata files_frame = tb.Labelframe(self, text="File trovati (in ordine alfabetico)", padding=10) files_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) files_frame.rowconfigure(0, weight=1); files_frame.columnconfigure(0, weight=1) self.files_listbox = tk.Listbox(files_frame, height=8, exportselection=False) self.files_listbox.grid(row=0, column=0, sticky="nsew") files_scroll = tb.Scrollbar(files_frame, orient=tk.VERTICAL, command=self.files_listbox.yview) files_scroll.grid(row=0, column=1, sticky="ns") self.files_listbox.config(yscrollcommand=files_scroll.set) # Log area log_frame = tb.Labelframe(self, text="Log", padding=10) log_frame.pack(fill=tk.BOTH, expand=True) self.log_box = ScrolledText(log_frame, height=15, state="disabled", wrap=tk.WORD) self.log_box.pack(fill=tk.BOTH, expand=True) def _log(self, text): """Aggiunge un messaggio all'area log.""" self.log_box.configure(state="normal") self.log_box.insert(tk.END, text + "\n") self.log_box.configure(state="disabled") self.log_box.see(tk.END) self.update_idletasks() def _choose_folder(self): """Apre il dialogo per selezionare la cartella.""" folder = filedialog.askdirectory(title="Seleziona la cartella Markdown") if folder: self.folder_path.set(folder) self._log(f"Cartella selezionata: {folder}") # Aggiorna immediatamente la lista dei file trovati try: self._scan_and_display_files(folder) except Exception as e: log.error(f"Errore durante la scansione della cartella: {e}", exc_info=True) def _toggle_template(self): """Attiva/disattiva i controlli del template.""" state = "normal" if self.use_template.get() else "disabled" self.template_entry.configure(state=state) self.template_button.configure(state=state) def _choose_template(self): """Apre il dialogo per selezionare il template.""" file = filedialog.askopenfilename( title="Seleziona template DOCX", filetypes=[("Word Documents", "*.docx"), ("Word Templates", "*.dotx")] ) if file: self.template_path.set(file) self._log(f"Template selezionato: {os.path.basename(file)}") def _generate_output(self): """Esegue la conversione batch dei file markdown.""" folder = self.folder_path.get().strip() output_name = self.output_name.get().strip() template = self.template_path.get().strip() use_template = self.use_template.get() make_pdf = self.generate_pdf.get() # Validazione input if not folder: messagebox.showwarning( "Attenzione", "Seleziona una cartella contenente i file Markdown." ) return if not output_name: messagebox.showwarning( "Attenzione", "Inserisci un nome per il file di output." ) return folder_path = Path(folder) if not folder_path.exists(): messagebox.showerror("Errore", "La cartella selezionata non esiste.") return # Trova i file markdown numerati md_files = sorted(folder_path.glob("[0-9][0-9]_*.md")) if not md_files: messagebox.showerror( "Errore", "Nessun file Markdown numerato trovato nella cartella.\n" "I file devono seguire il pattern: 01_nome.md, 02_nome.md, ecc." ) return # Mostra una finestra di conferma con la lista dei file trovati if not self._confirm_file_list(md_files): self._log("Operazione annullata dall'utente dopo la visualizzazione dei file.") return self._log(f"\n{'='*60}") self._log(f"INIZIO CONVERSIONE BATCH") self._log(f"{'='*60}") self._log(f"Trovati {len(md_files)} file Markdown:") for f in md_files: self._log(f" - {f.name}") # Prepara i percorsi di output output_docx = folder_path / f"{output_name}.docx" output_pdf = folder_path / f"{output_name}.pdf" combined_md = folder_path / "_manuale_unico_temp.md" try: # Combina i file markdown self._log(f"\nUnione dei file in {combined_md.name}...") combine_markdown_files(md_files, combined_md) self._log("✅ File unito con successo.") # Genera DOCX self._log("\nGenerazione DOCX...") if use_template: if not Path(template).exists(): messagebox.showerror( "Template non trovato", f"Il file {template} non esiste." ) return convert_markdown_to_docx_with_pandoc( str(combined_md), str(output_docx), template_path=template ) else: convert_markdown_to_docx_with_pandoc( str(combined_md), str(output_docx) ) self._log(f"✅ DOCX generato: {output_docx.name}") # Genera PDF opzionale if make_pdf: self._log("\nGenerazione PDF dal DOCX...") convert_docx_to_pdf(str(output_docx), str(output_pdf)) self._log(f"✅ PDF generato: {output_pdf.name}") messagebox.showinfo( "Completato", f"Conversione completata con successo!\n\n" f"DOCX: {output_docx.name}\n" f"PDF: {output_pdf.name}" ) else: messagebox.showinfo( "Completato", f"Conversione completata con successo!\n\n" f"DOCX: {output_docx.name}" ) self._log(f"\n{'='*60}") self._log("CONVERSIONE COMPLETATA CON SUCCESSO") self._log(f"{'='*60}\n") except Exception as e: log.error(f"Errore durante la conversione batch: {e}", exc_info=True) self._log(f"\n❌ ERRORE: {str(e)}") messagebox.showerror( "Errore", f"Si è verificato un errore durante la conversione:\n\n{str(e)}" ) finally: # Pulisce il file temporaneo if combined_md.exists(): combined_md.unlink() self._log(f"File temporaneo {combined_md.name} rimosso.") def _scan_and_display_files(self, folder: str): """Scansiona la cartella e popola la listbox con i file markdown trovati. Il pattern usato corrisponde ai file numerati: 01_nome.md, 02_nome.md, ... """ folder_path = Path(folder) if not folder_path.exists(): raise FileNotFoundError("La cartella selezionata non esiste.") md_files = sorted(folder_path.glob("[0-9][0-9]_*.md")) # Pulisce la listbox self.files_listbox.delete(0, tk.END) for f in md_files: self.files_listbox.insert(tk.END, f.name) self._log(f"Scansionati {len(md_files)} file markdown nella cartella.") def _confirm_file_list(self, md_files: list) -> bool: """Apre una finestra modale che mostra la lista dei file trovati e chiede conferma all'utente. Restituisce True se l'utente conferma, False se annulla. """ dlg = tb.Toplevel(self, title="Conferma file trovati") dlg.transient(self) dlg.grab_set() dlg.geometry("600x400") tb.Label(dlg, text="I seguenti file sono stati trovati e saranno uniti in questo ordine:", bootstyle=INFO).pack(anchor="w", padx=10, pady=(10, 0)) list_frame = tb.Frame(dlg); list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) listbox = tk.Listbox(list_frame) listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar = tb.Scrollbar(list_frame, orient=tk.VERTICAL, command=listbox.yview) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) listbox.config(yscrollcommand=scrollbar.set) for f in md_files: listbox.insert(tk.END, f.name) btn_frame = tb.Frame(dlg); btn_frame.pack(fill=tk.X, pady=(0, 10), padx=10) confirmed = {"value": False} def _on_confirm(): confirmed["value"] = True dlg.grab_release(); dlg.destroy() def _on_cancel(): dlg.grab_release(); dlg.destroy() tb.Button(btn_frame, text="Conferma e procedi", bootstyle=SUCCESS, command=_on_confirm).pack(side=tk.RIGHT, padx=5) tb.Button(btn_frame, text="Annulla", bootstyle=SECONDARY, command=_on_cancel).pack(side=tk.RIGHT) self.wait_window(dlg) return confirmed["value"]