SXXXXXXX_RepoSync/reposync/gui/main_window.py
2025-07-11 08:27:29 +02:00

627 lines
31 KiB
Python

# RepoSync/gui/main_window.py
"""
Main window and application logic for the RepoSync GUI.
Organizes the UI into separate tabs for different functionalities and
manages server profiles and settings dynamically.
"""
import logging
import os
import shutil
import threading
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from tkinter.scrolledtext import ScrolledText
from pathlib import Path
# Custom module imports
from ..config.profile_manager import ProfileManager
from .profile_dialog import ProfileDialog
from ..core.gitea_client import GiteaClient
from ..core.git_manager import GitManager
from ..core.sync_manager import SyncManager, SyncState, robust_rmtree,WikiInitializationRequiredError
from ..utility import logger as logger_util
def format_size(size_kb: int) -> str:
"""Formats size in kilobytes to a human-readable string (KB, MB, GB)."""
if not isinstance(size_kb, (int, float)) or size_kb <= 0:
return "N/A"
if size_kb < 1024:
return f"{size_kb} KB"
elif size_kb < 1024 * 1024:
return f"{size_kb / 1024:.2f} MB"
else:
return f"{size_kb / (1024 * 1024):.2f} GB"
def format_bytes(size_bytes: int) -> str:
"""Formats size in bytes to a human-readable string (B, KB, MB, GB)."""
if not isinstance(size_bytes, (int, float)) or size_bytes < 0:
return "0 B"
if size_bytes < 1024:
return f"{size_bytes} B"
return format_size(size_bytes // 1024)
class BaseTab(ttk.Frame):
"""Base class for our application tabs to reduce code duplication."""
def __init__(self, parent, app_instance, tab_name, **kwargs):
super().__init__(parent, **kwargs)
self.app = app_instance
self.logger = logging.getLogger(tab_name)
self.active_client = None
self.selected_profile_name = tk.StringVar()
self.sync_bundle_path = None
def create_profile_selector(self, parent_frame, label_text):
"""Creates the profile selection Combobox and status label."""
selector_frame = ttk.Frame(parent_frame)
ttk.Label(selector_frame, text=label_text).pack(side="left", padx=(0, 5))
self.profile_combo = ttk.Combobox(selector_frame, textvariable=self.selected_profile_name, state="readonly", width=25)
self.profile_combo.pack(side="left")
self.profile_combo.bind("<<ComboboxSelected>>", self._on_profile_select)
self.status_label = ttk.Label(selector_frame, text="Status: Not Connected", foreground="red")
self.status_label.pack(side="left", padx=10)
return selector_frame
def update_profile_list(self):
"""Updates the list of profiles in the Combobox."""
profile_names = self.app.profile_manager.get_profile_names()
self.profile_combo['values'] = sorted(profile_names)
if not profile_names:
self.selected_profile_name.set("")
self._update_status_label(False, "No profiles configured.")
def update_bundle_path(self):
"""Updates the bundle path from the main application's setting."""
new_path_str = self.app.bundle_path_var.get()
self.sync_bundle_path = Path(new_path_str) if new_path_str else None
def _on_profile_select(self, event=None):
"""Handles profile selection and initializes the appropriate client."""
profile_name = self.selected_profile_name.get()
if not profile_name:
self.active_client = None
self._update_status_label(False)
return
profile_data = self.app.profile_manager.get_profile(profile_name)
if not profile_data:
self.active_client = None
self.logger.error(f"Could not load data for profile '{profile_name}'.")
self._update_status_label(False, "Profile data missing.")
return
service_type = profile_data.get("type", "gitea")
if service_type == "gitea":
try:
self.active_client = GiteaClient(api_url=profile_data["url"], token=profile_data["token"], logger=logging.getLogger(f"GiteaClient({profile_name})"))
self._update_status_label(True, f"Selected: {profile_data['url']}")
except Exception as e:
self.logger.error(f"Failed to initialize GiteaClient for '{profile_name}': {e}")
self._update_status_label(False, f"Error initializing client.")
else:
self.logger.error(f"Unsupported service type '{service_type}' for profile '{profile_name}'.")
self._update_status_label(False, f"Unsupported type: {service_type}")
def _update_status_label(self, is_connected, message=""):
"""Updates the status label text and color."""
if is_connected:
self.status_label.config(text=message or "Status: Connected", foreground="green")
else:
self.status_label.config(text=message or "Status: Not Connected", foreground="red")
class ExportTab(BaseTab):
"""Tab for fetching repos and wikis from a server and exporting them."""
def __init__(self, parent, app_instance, **kwargs):
super().__init__(parent, app_instance, "ExportTab", **kwargs)
self.repositories = []
self.repo_data_map = {}
self.selection_count_var = tk.StringVar(value="Selected: 0")
self.selection_size_var = tk.StringVar(value="Total Est. Size: 0 KB")
self.total_export_size = 0
self._create_widgets()
self.update_profile_list()
def _create_widgets(self):
top_frame = ttk.Frame(self, padding="5")
top_frame.pack(side="top", fill="x")
selector_frame = self.create_profile_selector(top_frame, "Export from Server:")
selector_frame.pack(side="left")
self.fetch_button = ttk.Button(top_frame, text="Fetch Items", command=self._start_repo_fetch)
self.fetch_button.pack(side="left", padx=10)
self.fetch_button.state(['disabled'])
self.export_button = ttk.Button(top_frame, text="Export Selected", command=self._start_export)
self.export_button.pack(side="left", padx=5)
self.export_button.state(['disabled'])
tree_frame = ttk.Frame(self)
tree_frame.pack(expand=True, fill="both", pady=5)
columns = ("name", "status", "type", "size", "description")
self.repo_tree = ttk.Treeview(tree_frame, columns=columns, show="headings", selectmode="extended")
self.repo_tree.heading("name", text="Name")
self.repo_tree.heading("status", text="Status")
self.repo_tree.heading("type", text="Type")
self.repo_tree.heading("size", text="Size (est.)")
self.repo_tree.heading("description", text="Description")
self.repo_tree.column("name", width=250, stretch=tk.NO)
self.repo_tree.column("status", width=120, stretch=tk.NO, anchor="center")
self.repo_tree.column("type", width=80, stretch=tk.NO, anchor="center")
self.repo_tree.column("size", width=100, stretch=tk.NO, anchor="e")
self.repo_tree.column("description", width=400)
for state in SyncState:
self.repo_tree.tag_configure(state.name, background=self._get_color_for_state(state))
vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self.repo_tree.yview)
self.repo_tree.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
self.repo_tree.pack(side="left", expand=True, fill="both")
self.repo_tree.bind("<<TreeviewSelect>>", self._update_selection_info)
info_frame = ttk.LabelFrame(self, text="Selection & Legend", padding="5")
info_frame.pack(side="bottom", fill="x", pady=(5, 0))
selection_buttons_frame = ttk.Frame(info_frame)
selection_buttons_frame.pack(side="left", padx=5, anchor="n")
ttk.Button(selection_buttons_frame, text="Select Modified", command=self._select_modified).pack(side="top", fill="x")
ttk.Button(selection_buttons_frame, text="Select All", command=self._select_all).pack(side="top", fill="x", pady=2)
ttk.Button(selection_buttons_frame, text="Select None", command=self._deselect_all).pack(side="top", fill="x")
selection_info_frame = ttk.Frame(info_frame)
selection_info_frame.pack(side="left", padx=10, anchor="n")
ttk.Label(selection_info_frame, textvariable=self.selection_count_var).pack(anchor="w")
ttk.Label(selection_info_frame, textvariable=self.selection_size_var).pack(anchor="w")
# --- MODIFIED PART FOR ALIGNMENT ---
legend_frame = ttk.Frame(info_frame, padding=(10, 0))
legend_frame.pack(side="right", padx=10, anchor="n") # Changed side to "right"
ttk.Label(legend_frame, text="Legend:").pack(anchor="e", pady=1) # Changed anchor to "e" (East)
ttk.Label(legend_frame, text="To Export (New/Modified)", background=self._get_color_for_state(SyncState.AHEAD)).pack(anchor="e", pady=1)
ttk.Label(legend_frame, text="Synchronized", background=self._get_color_for_state(SyncState.IDENTICAL)).pack(anchor="e", pady=1)
ttk.Label(legend_frame, text="Orphaned (Not on Server)", background=self._get_color_for_state(SyncState.ORPHANED_BUNDLE)).pack(anchor="e", pady=1)
def _get_color_for_state(self, state):
return {
SyncState.AHEAD: "lightgreen",
SyncState.NEW_REPO: "lightgreen",
SyncState.IDENTICAL: "white",
SyncState.ORPHANED_BUNDLE: "khaki"
}.get(state, "white")
def _on_profile_select(self, event=None):
super()._on_profile_select(event)
self.fetch_button.state(['!disabled'] if self.active_client else ['disabled'])
self.export_button.state(['disabled'])
self.repo_tree.delete(*self.repo_tree.get_children())
self._update_selection_info()
def _select_all(self):
self.repo_tree.selection_set(self.repo_tree.get_children())
def _deselect_all(self):
self.repo_tree.selection_set([])
def _select_modified(self):
# Correctly clear the previous selection before making a new one
self.repo_tree.selection_set([])
for iid in self.repo_tree.get_children():
repo = self.repo_data_map.get(iid)
if repo and repo.get("state") in [SyncState.AHEAD, SyncState.NEW_REPO]:
self.repo_tree.selection_add(iid)
def _update_selection_info(self, event=None):
selected_iids = self.repo_tree.selection()
count = len(selected_iids)
total_size_kb = sum(self.repo_data_map[iid].get("size_kb", 0) for iid in selected_iids if iid in self.repo_data_map)
self.selection_count_var.set(f"Selected: {count}")
self.selection_size_var.set(f"Total Est. Size: {format_size(total_size_kb)}")
def _start_repo_fetch(self):
if not self.active_client or not self.sync_bundle_path: return
self.logger.info(f"Fetching items list from '{self.selected_profile_name.get()}'...")
self.fetch_button.state(['disabled']); self.export_button.state(['disabled'])
self.repo_tree.delete(*self.repo_tree.get_children())
threading.Thread(target=self._load_repositories_thread, daemon=True).start()
def _load_repositories_thread(self):
try:
# 1. Get all repos from server and filter out non-existent wikis
initial_repos = self.active_client.get_repositories()
self.logger.info(f"Received {len(initial_repos)} items. Verifying wiki existence...")
verified_repos = []
for repo in initial_repos:
if repo.get("is_wiki"):
if repo.get("clone_url") and self.app.git_manager.check_remote_exists(repo["clone_url"], self.active_client.token):
verified_repos.append(repo)
else:
verified_repos.append(repo)
# 2. Compare the verified server list with the local manifest
sync_manager = SyncManager(self.app.git_manager, self.active_client, self.sync_bundle_path, logging.getLogger("SyncManager"))
self.repositories = sync_manager.compare_server_with_manifest(verified_repos)
self.repo_data_map = {repo['name']: repo for repo in self.repositories}
# 3. Update UI
self.app.root.after(10, self._update_repo_treeview)
except Exception as e:
self.logger.error(f"Failed to fetch and compare items: {e}", exc_info=True)
self.app.root.after(10, lambda: messagebox.showerror("Fetch Error", f"Failed to fetch items:\n{e}"))
finally:
self.app.root.after(10, lambda: self.fetch_button.state(['!disabled']))
def _update_repo_treeview(self):
self.repo_tree.delete(*self.repo_tree.get_children())
sorted_repos = sorted(self.repositories, key=lambda x: x['name'].replace(" (Wiki)", " (Wiki)Z"))
for repo in sorted_repos:
repo_type = "Wiki" if repo.get("is_wiki") else "Repository"
state = repo.get("state", SyncState.ERROR)
self.repo_tree.insert("", "end", values=(
repo["name"],
state.name,
repo_type,
format_size(repo.get("size_kb", 0)),
repo.get("description", "")
), iid=repo["name"], tags=(state.name,))
self.logger.info(f"Displayed {len(self.repositories)} items with sync status.")
self.export_button.state(['!disabled'] if self.repositories else ['disabled'])
self._update_selection_info()
def _start_export(self):
selected_iids = self.repo_tree.selection()
if not selected_iids: return
if not self.sync_bundle_path: return
repos_to_export = [self.repo_data_map[iid] for iid in selected_iids if iid in self.repo_data_map]
sync_manager = SyncManager(self.app.git_manager, self.active_client, self.sync_bundle_path, logging.getLogger("SyncManager"))
self.logger.info(f"Starting export for {len(repos_to_export)} selected items...")
self.export_button.state(['disabled']); self.fetch_button.state(['disabled'])
self.total_export_size = 0
self.app.status_bar_progress.config(value=0)
self.app.status_bar_label.config(text="Starting export...")
thread = threading.Thread(target=self._export_thread, args=(sync_manager, repos_to_export), daemon=True)
thread.start()
def _progress_callback(self, current, total, bundle_size_bytes):
progress_percent = (current / total) * 100
self.total_export_size += bundle_size_bytes
self.app.status_bar_progress.config(value=progress_percent)
self.app.status_bar_label.config(text=f"Exporting {current}/{total}...")
self.app.status_bar_size_label.config(text=f"Total Size: {format_bytes(self.total_export_size)}")
def _export_thread(self, sync_manager, repos_to_export):
try:
callback = lambda c, t, s: self.app.root.after(0, self._progress_callback, c, t, s)
sync_manager.export_repositories_to_bundles(repos_to_export, progress_callback=callback)
self.app.root.after(0, lambda: messagebox.showinfo("Success", "Export completed successfully."))
except Exception as e:
self.logger.error(f"Export failed: {e}", exc_info=True)
self.app.root.after(0, lambda: messagebox.showerror("Export Error", f"An error occurred: {e}"))
finally:
self.app.root.after(0, lambda: self.app.status_bar_label.config(text="Export finished."))
self.app.root.after(0, lambda: self.export_button.state(['!disabled']))
self.app.root.after(0, lambda: self.fetch_button.state(['!disabled']))
# Refresh the view to show the new 'IDENTICAL' state
self.app.root.after(100, self._start_repo_fetch)
class ImportTab(BaseTab):
def __init__(self, parent, app_instance, **kwargs):
super().__init__(parent, app_instance, "ImportTab", **kwargs)
self.comparison_results = []
self.repo_data_map = {}
self.selection_count_var = tk.StringVar(value="Selected: 0")
self.selection_size_var = tk.StringVar(value="Total Size: 0 B")
self._create_widgets()
self.update_profile_list()
def _create_widgets(self):
top_frame = ttk.Frame(self, padding="5")
top_frame.pack(side="top", fill="x")
selector_frame = self.create_profile_selector(top_frame, "Import to Server:")
selector_frame.pack(side="left")
self.check_status_button = ttk.Button(top_frame, text="Check Bundle Status", command=self._start_bundle_check)
self.check_status_button.pack(side="left", padx=10)
self.check_status_button.state(['disabled'])
self.import_button = ttk.Button(top_frame, text="Import Selected", command=self._start_import)
self.import_button.pack(side="left", padx=5)
self.import_button.state(['disabled'])
tree_frame = ttk.Frame(self)
tree_frame.pack(expand=True, fill="both", pady=5)
columns = ("name", "status", "size")
self.bundle_tree = ttk.Treeview(tree_frame, columns=columns, show="headings", selectmode="extended")
self.bundle_tree.heading("name", text="Repository Name")
self.bundle_tree.heading("status", text="Sync Status")
self.bundle_tree.heading("size", text="Bundle Size")
self.bundle_tree.column("name", width=300)
self.bundle_tree.column("status", width=150, anchor="center")
self.bundle_tree.column("size", width=100, anchor="e")
for state in SyncState:
self.bundle_tree.tag_configure(state.name, background=self._get_color_for_state(state))
vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self.bundle_tree.yview)
self.bundle_tree.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
self.bundle_tree.pack(side="left", expand=True, fill="both")
self.bundle_tree.bind("<<TreeviewSelect>>", self._update_selection_info)
info_frame = ttk.LabelFrame(self, text="Selection Info", padding="5")
info_frame.pack(side="bottom", fill="x", pady=(5, 0))
selection_buttons_frame = ttk.Frame(info_frame)
selection_buttons_frame.pack(side="left", padx=5)
ttk.Button(selection_buttons_frame, text="Select All", command=self._select_all).pack(side="left")
ttk.Button(selection_buttons_frame, text="Select None", command=self._deselect_all).pack(side="left", padx=5)
ttk.Label(info_frame, textvariable=self.selection_count_var).pack(side="left", padx=10)
ttk.Label(info_frame, textvariable=self.selection_size_var).pack(side="left", padx=10)
def _get_color_for_state(self, state):
return {
SyncState.BEHIND: "lightblue", # BEHIND means bundle is ahead of remote
SyncState.NEW_REPO: "lightgreen",
SyncState.AHEAD: "khaki", # AHEAD means remote is ahead of bundle
SyncState.DIVERGED: "salmon",
SyncState.ERROR: "lightgrey",
SyncState.IDENTICAL: "white"
}.get(state, "white")
def _select_all(self):
self.bundle_tree.selection_set(self.bundle_tree.get_children())
def _deselect_all(self):
self.bundle_tree.selection_set([])
def _update_selection_info(self, event=None):
selected_iids = self.bundle_tree.selection()
count = len(selected_iids)
total_size_bytes = sum(self.repo_data_map[iid]["bundle_info"].get("bundle_size_bytes", 0) for iid in selected_iids if iid in self.repo_data_map)
self.selection_count_var.set(f"Selected: {count}")
self.selection_size_var.set(f"Total Size: {format_bytes(total_size_bytes)}")
def _on_profile_select(self, event=None):
super()._on_profile_select(event)
self.check_status_button.state(['!disabled'] if self.active_client else ['disabled'])
self.import_button.state(['disabled'])
self.bundle_tree.delete(*self.bundle_tree.get_children())
self._update_selection_info()
def _start_bundle_check(self):
if not self.active_client or not self.sync_bundle_path:
return
sync_manager = SyncManager(self.app.git_manager, self.active_client, self.sync_bundle_path, logging.getLogger("SyncManager"))
self.logger.info("Checking bundle status...")
self.check_status_button.state(['disabled'])
threading.Thread(target=self._check_status_thread, args=(sync_manager,), daemon=True).start()
def _check_status_thread(self, sync_manager):
try:
self.comparison_results = sync_manager.compare_bundles_with_remote()
self.repo_data_map = {repo['name']: repo for repo in self.comparison_results}
self.app.root.after(10, self._update_bundle_treeview)
except Exception as e:
self.logger.error(f"Failed to compare bundle status: {e}", exc_info=True)
self.app.root.after(10, lambda: messagebox.showerror("Comparison Error", f"Failed to compare bundle status:\n{e}"))
finally:
self.app.root.after(10, lambda: self.check_status_button.state(['!disabled']))
def _update_bundle_treeview(self):
self.bundle_tree.delete(*self.bundle_tree.get_children())
for result in self.comparison_results:
bundle_size_bytes = result.get("bundle_info", {}).get("bundle_size_bytes", 0)
self.bundle_tree.insert("", "end", values=(
result["name"],
result["state"].name,
format_bytes(bundle_size_bytes)
), tags=(result["state"].name,), iid=result["name"])
self.import_button.state(['!disabled'] if self.comparison_results else ['disabled'])
self._update_selection_info()
def _start_import(self):
selected_iids = self.bundle_tree.selection()
if not selected_iids: return
# For import, we want repos where the bundle is ahead (BEHIND) or new
repos_to_import = [r for r in self.comparison_results if r["name"] in selected_iids and r["state"] in [SyncState.BEHIND, SyncState.NEW_REPO]]
if not repos_to_import:
messagebox.showwarning("Invalid Selection", "Please select repositories that are 'BEHIND' or 'NEW_REPO' to import.")
return
sync_manager = SyncManager(self.app.git_manager, self.active_client, self.sync_bundle_path, logging.getLogger("SyncManager"))
self.logger.info(f"Starting import for {len(repos_to_import)} repositories...")
self.import_button.state(['disabled']); self.check_status_button.state(['disabled'])
self.app.status_bar_progress.config(value=0)
self.app.status_bar_label.config(text="Starting import...")
self.app.status_bar_size_label.config(text="")
thread = threading.Thread(target=self._import_thread, args=(sync_manager, repos_to_import), daemon=True)
thread.start()
def _progress_callback(self, current, total):
progress_percent = (current / total) * 100
self.app.status_bar_progress.config(value=progress_percent)
self.app.status_bar_label.config(text=f"Importing {current}/{total}...")
def _import_thread(self, sync_manager, repos_to_import):
try:
callback = lambda c, t: self.app.root.after(0, self._progress_callback, c, t)
sync_manager.import_repositories_from_bundles(repos_to_import, progress_callback=callback)
self.app.root.after(0, lambda: messagebox.showinfo("Success", "Import process completed."))
except WikiInitializationRequiredError as e:
self.logger.warning(f"Wiki initialization required for {e.repo_name}.")
repo_name = e.repo_name
base_repo_name = repo_name.removesuffix(" (Wiki)").strip()
# Mostra un messaggio informativo specifico
self.app.root.after(0, lambda: messagebox.showinfo(
"Azione Manuale Richiesta",
f"L'importazione della wiki '{repo_name}' richiede un'azione manuale:\n\n"
"1. Vai al tuo server Gitea e apri la pagina del repository principale:\n"
f" '{base_repo_name}'\n\n"
"2. Clicca sulla scheda 'Wiki'.\n\n"
"3. Clicca sul pulsante 'Crea la prima pagina'.\n\n"
"Una volta creata la pagina, torna su RepoSync e riesegui l'importazione per questa wiki."
))
except Exception as e:
self.logger.error(f"Import failed: {e}", exc_info=True)
self.app.root.after(0, lambda: messagebox.showerror("Import Error", f"An error occurred: {e}"))
finally:
self.app.root.after(0, lambda: self.app.status_bar_label.config(text="Import finished."))
self.app.root.after(0, lambda: self.import_button.state(['!disabled']))
self.app.root.after(0, lambda: self.check_status_button.state(['!disabled']))
# Aggiorna la vista per mostrare che lo stato non è cambiato
self.app.root.after(100, self._start_bundle_check)
class Application:
"""The main class for the RepoSync graphical user interface."""
def __init__(self):
self.root = tk.Tk()
self.root.title("RepoSync - Offline Repository Synchronization")
self.root.geometry("1200x800")
self.profile_manager = ProfileManager()
self.git_manager = None
self.bundle_path_var = tk.StringVar()
self._create_menu()
self._create_main_widgets()
self._setup_logging()
self._init_backend()
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
def _create_menu(self):
menubar = tk.Menu(self.root)
self.root.config(menu=menubar)
file_menu = tk.Menu(menubar, tearoff=0)
file_menu.add_command(label="Configure Profiles...", command=self._open_profile_dialog)
file_menu.add_separator()
file_menu.add_command(label="Exit", command=self._on_closing)
menubar.add_cascade(label="File", menu=file_menu)
def _create_main_widgets(self):
top_frame = ttk.Frame(self.root, padding="10")
top_frame.pack(side="top", fill="x", expand=False)
main_frame = ttk.Frame(self.root, padding=(10, 0, 10, 10))
main_frame.pack(expand=True, fill="both")
bundle_path_frame = ttk.LabelFrame(top_frame, text="Bundle Folder Path", padding="5")
bundle_path_frame.pack(side="top", fill="x")
self.bundle_path_entry = ttk.Entry(bundle_path_frame, textvariable=self.bundle_path_var, state="readonly", width=100)
self.bundle_path_entry.pack(side="left", fill="x", expand=True, padx=(0, 5))
browse_button = ttk.Button(bundle_path_frame, text="Browse...", command=self._browse_bundle_path)
browse_button.pack(side="left")
reset_button = ttk.Button(bundle_path_frame, text="Reset", command=self._reset_bundle_folder)
reset_button.pack(side="left", padx=(5, 0))
paned_window = ttk.PanedWindow(main_frame, orient=tk.VERTICAL)
paned_window.pack(expand=True, fill="both")
notebook = ttk.Notebook(paned_window)
paned_window.add(notebook, weight=3)
self.export_tab = ExportTab(notebook, self, padding="10")
self.import_tab = ImportTab(notebook, self, padding="10")
notebook.add(self.export_tab, text="Export from Server")
notebook.add(self.import_tab, text="Import to Server")
log_frame = ttk.LabelFrame(paned_window, text="Logs", padding="5")
paned_window.add(log_frame, weight=1)
self.log_widget = ScrolledText(log_frame, state=tk.DISABLED, wrap=tk.WORD, height=10)
self.log_widget.pack(expand=True, fill="both")
status_bar = ttk.Frame(self.root, padding=(5, 2))
status_bar.pack(side="bottom", fill="x")
self.status_bar_label = ttk.Label(status_bar, text="Ready")
self.status_bar_label.pack(side="left")
self.status_bar_size_label = ttk.Label(status_bar, text="")
self.status_bar_size_label.pack(side="right")
self.status_bar_progress = ttk.Progressbar(status_bar, orient='horizontal', mode='determinate')
self.status_bar_progress.pack(side="right", fill="x", expand=True, padx=10)
def _browse_bundle_path(self):
initial_dir = self.bundle_path_var.get() or str(Path.home())
new_path = filedialog.askdirectory(title="Select Bundle Folder", initialdir=initial_dir)
if new_path:
self.bundle_path_var.set(new_path)
self.profile_manager.set_setting("bundle_path", new_path)
self.logger.info(f"Bundle path updated to: {new_path}")
self.export_tab.update_bundle_path()
self.import_tab.update_bundle_path()
def _reset_bundle_folder(self):
bundle_path_str = self.bundle_path_var.get()
if not bundle_path_str:
messagebox.showwarning("No Folder Selected", "Please select a bundle folder first.", parent=self.root)
return
if not messagebox.askyesno(
"Confirm Reset",
f"Are you sure you want to permanently delete all contents of the folder:\n\n{bundle_path_str}\n\nThis will remove all exported bundles and the manifest file. This action cannot be undone.",
parent=self.root
):
return
try:
bundle_path = Path(bundle_path_str)
if bundle_path.exists():
self.logger.info(f"Resetting bundle folder: {bundle_path}")
# Use robust_rmtree to handle potential permission issues
robust_rmtree(bundle_path)
# Recreate the folder so it's ready for use
os.makedirs(bundle_path, exist_ok=True)
self.logger.info("Bundle folder has been cleared.")
messagebox.showinfo("Reset Complete", "The bundle folder has been successfully cleared.", parent=self.root)
else:
self.logger.warning("Attempted to reset a folder that does not exist.")
# Refresh the UI to reflect the reset state
self.export_tab._on_profile_select()
self.import_tab._on_profile_select()
except Exception as e:
self.logger.error(f"Failed to reset bundle folder: {e}", exc_info=True)
messagebox.showerror("Reset Error", f"An error occurred while clearing the folder:\n{e}", parent=self.root)
def _setup_logging(self):
log_config = {"level": logging.INFO, "enable_console": True}
logger_util.setup_logging(self.log_widget, self.root, log_config)
self.logger = logging.getLogger(__name__)
def _init_backend(self):
self.logger.info("Initializing backend components...")
self.git_manager = GitManager(logger=logging.getLogger("GitManager"))
initial_bundle_path = self.profile_manager.get_setting("bundle_path")
self.bundle_path_var.set(initial_bundle_path)
self.export_tab.update_bundle_path()
self.import_tab.update_bundle_path()
self.logger.info("Backend components initialized.")
def _open_profile_dialog(self):
dialog = ProfileDialog(self.root, self.profile_manager)
self.root.wait_window(dialog)
self.update_all_profile_comboboxes()
def update_all_profile_comboboxes(self):
self.export_tab.update_profile_list()
self.import_tab.update_profile_list()
def _on_closing(self):
self.logger.info("Application is closing.")
#logger_util.shutdown_logging()
self.root.destroy()
def run(self):
self.logger.info("Starting RepoSync application...")
self.root.mainloop()