272 Zeilen
9.0 KiB
Python
272 Zeilen
9.0 KiB
Python
"""
|
|
Progress Bar Component for Git Operations
|
|
Provides visual feedback for long-running operations
|
|
"""
|
|
|
|
import customtkinter as ctk
|
|
from typing import Optional, Callable
|
|
import threading
|
|
import time
|
|
|
|
class ProgressBar(ctk.CTkToplevel):
|
|
"""Modern progress bar window for Git operations"""
|
|
|
|
def __init__(self, parent, title: str = "Operation in Progress", width: int = 500):
|
|
super().__init__(parent)
|
|
|
|
# Window configuration
|
|
self.title(title)
|
|
self.geometry(f"{width}x200")
|
|
self.resizable(False, False)
|
|
|
|
# Center on parent window
|
|
self.transient(parent)
|
|
self.grab_set()
|
|
|
|
# Colors from styles
|
|
from gui.styles import get_colors
|
|
self.colors = get_colors()
|
|
|
|
# Configure window
|
|
self.configure(fg_color=self.colors['bg_primary'])
|
|
|
|
# Create UI elements
|
|
self._setup_ui()
|
|
|
|
# Variables
|
|
self.is_cancelled = False
|
|
self._update_callback: Optional[Callable] = None
|
|
|
|
# Center window on parent
|
|
self._center_on_parent(parent)
|
|
|
|
def _setup_ui(self):
|
|
"""Create the progress bar UI"""
|
|
# Main container
|
|
self.container = ctk.CTkFrame(
|
|
self,
|
|
fg_color=self.colors['bg_secondary'],
|
|
corner_radius=10
|
|
)
|
|
self.container.pack(fill="both", expand=True, padx=20, pady=20)
|
|
|
|
# Title label
|
|
self.title_label = ctk.CTkLabel(
|
|
self.container,
|
|
text="",
|
|
font=("Segoe UI", 16, "bold"),
|
|
text_color=self.colors['text_primary']
|
|
)
|
|
self.title_label.pack(pady=(10, 5))
|
|
|
|
# Status label
|
|
self.status_label = ctk.CTkLabel(
|
|
self.container,
|
|
text="Initialisierung...",
|
|
font=("Segoe UI", 14),
|
|
text_color=self.colors['text_secondary']
|
|
)
|
|
self.status_label.pack(pady=(0, 15))
|
|
|
|
# Progress bar
|
|
self.progress = ctk.CTkProgressBar(
|
|
self.container,
|
|
width=440,
|
|
height=20,
|
|
corner_radius=10,
|
|
fg_color=self.colors['bg_tile'],
|
|
progress_color=self.colors['accent_success'],
|
|
mode="determinate"
|
|
)
|
|
self.progress.pack(pady=10)
|
|
self.progress.set(0)
|
|
|
|
# Percentage label
|
|
self.percentage_label = ctk.CTkLabel(
|
|
self.container,
|
|
text="0%",
|
|
font=("Segoe UI", 12),
|
|
text_color=self.colors['text_secondary']
|
|
)
|
|
self.percentage_label.pack(pady=(0, 10))
|
|
|
|
# Cancel button
|
|
self.cancel_button = ctk.CTkButton(
|
|
self.container,
|
|
text="Abbrechen",
|
|
width=100,
|
|
height=32,
|
|
corner_radius=6,
|
|
fg_color=self.colors['accent_error'],
|
|
hover_color="#FF5252",
|
|
text_color="#FFFFFF",
|
|
font=("Segoe UI", 13, "bold"),
|
|
command=self._cancel_operation
|
|
)
|
|
self.cancel_button.pack(pady=5)
|
|
|
|
def _center_on_parent(self, parent):
|
|
"""Center the window on parent"""
|
|
self.update_idletasks()
|
|
|
|
# Get parent position and size
|
|
parent_x = parent.winfo_x()
|
|
parent_y = parent.winfo_y()
|
|
parent_width = parent.winfo_width()
|
|
parent_height = parent.winfo_height()
|
|
|
|
# Get this window size
|
|
width = self.winfo_width()
|
|
height = self.winfo_height()
|
|
|
|
# Calculate position
|
|
x = parent_x + (parent_width - width) // 2
|
|
y = parent_y + (parent_height - height) // 2
|
|
|
|
# Set position
|
|
self.geometry(f"+{x}+{y}")
|
|
|
|
def _cancel_operation(self):
|
|
"""Handle cancel button click"""
|
|
self.is_cancelled = True
|
|
self.cancel_button.configure(state="disabled", text="Wird abgebrochen...")
|
|
|
|
def update_progress(self, value: float, status: str, title: str = "", is_error: bool = False):
|
|
"""Update progress bar
|
|
|
|
Args:
|
|
value: Progress value (0.0 to 1.0)
|
|
status: Status message to display
|
|
title: Optional title update
|
|
is_error: If True, colors the progress bar red
|
|
"""
|
|
# Ensure value is in range
|
|
value = max(0.0, min(1.0, value))
|
|
|
|
# Update UI in main thread
|
|
self.after(0, self._update_ui, value, status, title, is_error)
|
|
|
|
def _update_ui(self, value: float, status: str, title: str, is_error: bool = False):
|
|
"""Update UI elements (must be called from main thread)"""
|
|
if title:
|
|
self.title_label.configure(text=title)
|
|
|
|
self.status_label.configure(text=status)
|
|
self.progress.set(value)
|
|
self.percentage_label.configure(text=f"{int(value * 100)}%")
|
|
|
|
# Change color if error
|
|
if is_error:
|
|
self.progress.configure(progress_color=self.colors['accent_error'])
|
|
self.status_label.configure(text_color=self.colors['accent_error'])
|
|
|
|
# If complete, update button
|
|
if value >= 1.0 and not is_error:
|
|
self.cancel_button.configure(
|
|
text="Schließen",
|
|
state="normal",
|
|
command=self.destroy
|
|
)
|
|
# Auto-close after 0.5 seconds if successful
|
|
if not self.is_cancelled:
|
|
self.after(500, self.destroy)
|
|
|
|
def set_update_callback(self, callback: Callable):
|
|
"""Set callback for progress updates"""
|
|
self._update_callback = callback
|
|
|
|
def simulate_progress(self, steps: list):
|
|
"""Simulate progress for testing
|
|
|
|
Args:
|
|
steps: List of (duration, status, progress) tuples
|
|
"""
|
|
def run_simulation():
|
|
for duration, status, progress in steps:
|
|
if self.is_cancelled:
|
|
break
|
|
|
|
self.update_progress(progress, status)
|
|
time.sleep(duration)
|
|
|
|
if self._update_callback:
|
|
self._update_callback(progress, status)
|
|
|
|
thread = threading.Thread(target=run_simulation, daemon=True)
|
|
thread.start()
|
|
|
|
|
|
def set_error(self, error_message: str):
|
|
"""Set error state and message"""
|
|
self.update_progress(1.0, error_message, is_error=True)
|
|
self.cancel_button.configure(
|
|
text="OK",
|
|
state="normal",
|
|
command=self.destroy,
|
|
fg_color=self.colors['accent_error']
|
|
)
|
|
|
|
|
|
class GitOperationProgress:
|
|
"""Helper class to manage progress for Git operations"""
|
|
|
|
# Progress stages for different operations
|
|
CLONE_STAGES = [
|
|
(0.0, "Verbindung zum Server wird hergestellt..."),
|
|
(0.15, "Repository-Informationen werden abgerufen..."),
|
|
(0.30, "Objekte werden gezählt..."),
|
|
(0.50, "Objekte werden heruntergeladen..."),
|
|
(0.80, "Dateien werden entpackt..."),
|
|
(0.95, "Arbeitsverzeichnis wird erstellt..."),
|
|
(1.0, "Repository erfolgreich geklont!")
|
|
]
|
|
|
|
PUSH_STAGES = [
|
|
(0.0, "Verbindung wird hergestellt..."),
|
|
(0.10, "Authentifizierung läuft..."),
|
|
(0.25, "Lokale Änderungen werden analysiert..."),
|
|
(0.40, "Daten werden komprimiert..."),
|
|
(0.60, "Objekte werden übertragen..."),
|
|
(0.85, "Remote-Repository wird aktualisiert..."),
|
|
(1.0, "Push erfolgreich abgeschlossen!")
|
|
]
|
|
|
|
PULL_STAGES = [
|
|
(0.0, "Verbindung zum Server wird hergestellt..."),
|
|
(0.15, "Neue Änderungen werden gesucht..."),
|
|
(0.35, "Änderungen werden heruntergeladen..."),
|
|
(0.60, "Dateien werden entpackt..."),
|
|
(0.80, "Änderungen werden zusammengeführt..."),
|
|
(0.95, "Arbeitsverzeichnis wird aktualisiert..."),
|
|
(1.0, "Pull erfolgreich abgeschlossen!")
|
|
]
|
|
|
|
FETCH_STAGES = [
|
|
(0.0, "Verbindung wird hergestellt..."),
|
|
(0.20, "Remote-Referenzen werden abgerufen..."),
|
|
(0.40, "Neue Objekte werden gesucht..."),
|
|
(0.70, "Objekte werden heruntergeladen..."),
|
|
(0.90, "Lokale Referenzen werden aktualisiert..."),
|
|
(1.0, "Fetch erfolgreich abgeschlossen!")
|
|
]
|
|
|
|
COMMIT_STAGES = [
|
|
(0.0, "Änderungen werden vorbereitet..."),
|
|
(0.20, "Dateistatus wird geprüft..."),
|
|
(0.40, "Änderungen werden indiziert..."),
|
|
(0.60, "Commit-Objekt wird erstellt..."),
|
|
(0.80, "Referenzen werden aktualisiert..."),
|
|
(1.0, "Commit erfolgreich erstellt!")
|
|
]
|
|
|
|
@staticmethod
|
|
def get_stages(operation: str) -> list:
|
|
"""Get progress stages for an operation"""
|
|
stages_map = {
|
|
'clone': GitOperationProgress.CLONE_STAGES,
|
|
'push': GitOperationProgress.PUSH_STAGES,
|
|
'pull': GitOperationProgress.PULL_STAGES,
|
|
'fetch': GitOperationProgress.FETCH_STAGES,
|
|
'commit': GitOperationProgress.COMMIT_STAGES
|
|
}
|
|
return stages_map.get(operation.lower(), []) |