333 lines
13 KiB
Python
333 lines
13 KiB
Python
# 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"]
|