From ffdbba26554af00fb1fff2d74adf80c236357f21 Mon Sep 17 00:00:00 2001 From: allebonvi Date: Thu, 2 Jul 2026 09:47:57 +0200 Subject: [PATCH] Abilita selezione multipla nella coda --- SPECIFICHE.md | 4 ++-- src/bakrest/config_app.py | 46 +++++++++++++++++++++++++++------------ 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/SPECIFICHE.md b/SPECIFICHE.md index 84abbdb..0fb9649 100644 --- a/SPECIFICHE.md +++ b/SPECIFICHE.md @@ -77,8 +77,8 @@ La GUI di configurazione deve permettere di: - aprire i log giornalieri; - eventualmente eseguire un test di raggiungibilita' del server; - visualizzare l'elenco dei file in coda per il backup; -- rimuovere singoli file dalla coda; -- proporre, quando si rimuove un file dalla coda, se rimuovere solo il file o escludere dal backup la cartella che lo contiene. +- rimuovere singoli file o selezioni multiple dalla coda; +- proporre, quando si rimuovono file dalla coda, se rimuovere solo i file selezionati o escludere dal backup le cartelle che li contengono. Se un computer viene spento senza usare `Backup e spegni`, i file gia' registrati come `pending` devono restare nel registro cambiamenti e accumularsi con quelli del giorno successivo, in modo da poter essere sincronizzati al backup successivo. diff --git a/src/bakrest/config_app.py b/src/bakrest/config_app.py index 1f82fcc..7557ea9 100644 --- a/src/bakrest/config_app.py +++ b/src/bakrest/config_app.py @@ -155,7 +155,7 @@ class BakRestConfigApp(ctk.CTk): refresh_button = ctk.CTkButton(toolbar, text="Aggiorna", command=self._refresh_queue) refresh_button.grid(row=0, column=1, padx=4) - remove_button = ctk.CTkButton(toolbar, text="Rimuovi selezionato", command=self._remove_selected_pending) + remove_button = ctk.CTkButton(toolbar, text="Rimuovi selezionati", command=self._remove_selected_pending) remove_button.grid(row=0, column=2, padx=4) list_frame = ctk.CTkFrame(self.queue_tab) @@ -163,7 +163,12 @@ class BakRestConfigApp(ctk.CTk): list_frame.grid_columnconfigure(0, weight=1) list_frame.grid_rowconfigure(0, weight=1) - self.queue_list = tk.Listbox(list_frame, activestyle="dotbox", exportselection=False) + self.queue_list = tk.Listbox( + list_frame, + activestyle="dotbox", + exportselection=False, + selectmode=tk.EXTENDED, + ) self.queue_list.grid(row=0, column=0, sticky="nsew", padx=(8, 0), pady=8) scrollbar = ctk.CTkScrollbar(list_frame, command=self.queue_list.yview) @@ -230,18 +235,21 @@ class BakRestConfigApp(ctk.CTk): def _remove_selected_pending(self) -> None: selection = self.queue_list.curselection() if not selection: - messagebox.showinfo("Bak&Rest", "Seleziona un file dalla coda.") + messagebox.showinfo("Bak&Rest", "Seleziona uno o piu' file dalla coda.") return - event = self.pending_events[selection[0]] - dialog = RemovePendingDialog(self, event.path) + selected_events = [self.pending_events[index] for index in selection] + dialog = RemovePendingDialog(self, [event.path for event in selected_events]) self.wait_window(dialog) if dialog.choice == "file": - self.registry.mark_ignored(event.path) + self.registry.mark_status([event.path for event in selected_events], "ignored") elif dialog.choice == "folder": - folder = str(Path(event.path).parent) - self._add_exclude_dir(folder) - self.registry.mark_status(self._pending_paths_inside(folder), "ignored") + folders = sorted({str(Path(event.path).parent) for event in selected_events}) + paths_to_ignore: set[str] = set() + for folder in folders: + self._add_exclude_dir(folder) + paths_to_ignore.update(self._pending_paths_inside(folder)) + self.registry.mark_status(sorted(paths_to_ignore), "ignored") save_config_data(self.data, self.config_path) self.exclude_dirs.set_items(self.data.setdefault("watch", {}).get("exclude_dirs", [])) else: @@ -268,25 +276,28 @@ class BakRestConfigApp(ctk.CTk): class RemovePendingDialog(ctk.CTkToplevel): - def __init__(self, parent: ctk.CTk, file_path: str) -> None: + def __init__(self, parent: ctk.CTk, file_paths: list[str]) -> None: super().__init__(parent) self.choice: str | None = None self.title("Rimuovi dalla coda") - self.geometry("520x190") + self.geometry("560x240") self.resizable(False, False) self.transient(parent) self.grab_set() self.grid_columnconfigure(0, weight=1) + count = len(file_paths) + subject = "il file selezionato" if count == 1 else f"i {count} file selezionati" label = ctk.CTkLabel( self, - text="Vuoi rimuovere solo il file o isolare dal backup la cartella che lo contiene?", - wraplength=470, + text=f"Vuoi rimuovere solo {subject} o isolare dal backup le cartelle che li contengono?", + wraplength=510, justify="left", ) label.grid(row=0, column=0, sticky="ew", padx=18, pady=(18, 8)) - path_label = ctk.CTkLabel(self, text=file_path, wraplength=470, justify="left") + preview = _selection_preview(file_paths) + path_label = ctk.CTkLabel(self, text=preview, wraplength=510, justify="left") path_label.grid(row=1, column=0, sticky="ew", padx=18, pady=(0, 16)) buttons = ctk.CTkFrame(self, fg_color="transparent") @@ -301,6 +312,13 @@ class RemovePendingDialog(ctk.CTkToplevel): self.destroy() +def _selection_preview(file_paths: list[str]) -> str: + if len(file_paths) <= 3: + return "\n".join(file_paths) + shown = "\n".join(file_paths[:3]) + return f"{shown}\n... altri {len(file_paths) - 3} file" + + class _ListEditor(ctk.CTkFrame): def __init__(self, parent: ctk.CTkBaseClass, title: str, browse_dirs: bool = False) -> None: super().__init__(parent)