""" Settings Dialog for the application Allows users to configure various settings including view modes """ import customtkinter as ctk from gui.styles import COLORS, FONTS from utils.logger import logger import json from pathlib import Path from tkinter import filedialog import os class SettingsDialog(ctk.CTkToplevel): def __init__(self, parent, sidebar_view): super().__init__(parent) self.sidebar_view = sidebar_view self.settings_file = Path.home() / ".claude_project_manager" / "ui_settings.json" # Window setup self.title("Einstellungen") self.minsize(650, 550) self.resizable(True, True) # Make modal self.transient(parent) self.grab_set() # Load current settings self.settings = self.load_settings() # Setup UI self.setup_ui() # Update window size to fit content self.update_idletasks() # Center window self.center_window() # Focus self.focus() def setup_ui(self): """Setup the settings UI""" # Main container main_frame = ctk.CTkFrame(self, fg_color=COLORS['bg_primary']) main_frame.pack(fill="both", expand=True, padx=20, pady=20) # Title title_label = ctk.CTkLabel( main_frame, text="⚙️ Einstellungen", font=FONTS['subtitle'], text_color=COLORS['text_primary'] ) title_label.pack(pady=(0, 20)) # Tab view self.tabview = ctk.CTkTabview( main_frame, fg_color=COLORS['bg_secondary'], segmented_button_fg_color=COLORS['bg_tile'], segmented_button_selected_color=COLORS['accent_primary'], segmented_button_selected_hover_color=COLORS['accent_hover'], segmented_button_unselected_color=COLORS['bg_tile'], segmented_button_unselected_hover_color=COLORS['bg_tile_hover'], text_color=COLORS['text_primary'], text_color_disabled=COLORS['text_dim'] ) self.tabview.pack(fill="both", expand=True) # Add tabs self.tabview.add("Allgemein") self.tabview.add("Gitea") self.tabview.add("Team-Aktivität") # Setup each tab self.setup_general_tab() self.setup_gitea_tab() self.setup_activity_tab() # Buttons (at bottom of main frame) button_frame = ctk.CTkFrame(main_frame, fg_color="transparent") button_frame.pack(fill="x", pady=(20, 0)) # Cancel button (left side) cancel_btn = ctk.CTkButton( button_frame, text="Abbrechen", command=self.destroy, fg_color=COLORS['bg_tile'], hover_color=COLORS['bg_tile_hover'], text_color=COLORS['text_primary'], width=100 ) cancel_btn.pack(side="left", padx=(0, 5)) # Apply button (right side) apply_btn = ctk.CTkButton( button_frame, text="Anwenden", command=self.apply_settings, fg_color=COLORS['accent_primary'], hover_color=COLORS['accent_hover'], width=100 ) apply_btn.pack(side="right", padx=(5, 0)) # Save button (right side, before Apply) save_btn = ctk.CTkButton( button_frame, text="Speichern", command=self.save_settings_only, fg_color=COLORS['accent_secondary'], hover_color=COLORS['accent_hover'], text_color=COLORS['text_primary'], width=100 ) save_btn.pack(side="right", padx=(5, 0)) def setup_general_tab(self): """Setup general settings tab""" tab = self.tabview.tab("Allgemein") # Container with padding container = ctk.CTkFrame(tab, fg_color="transparent") container.pack(fill="both", expand=True, padx=20, pady=20) # Placeholder for future general settings info_label = ctk.CTkLabel( container, text="Allgemeine Einstellungen werden hier angezeigt.\n(Aktuell keine verfügbar)", font=FONTS['body'], text_color=COLORS['text_dim'] ) info_label.pack(pady=50) def setup_gitea_tab(self): """Setup Gitea settings tab""" tab = self.tabview.tab("Gitea") # Scrollable container container = ctk.CTkScrollableFrame(tab, fg_color="transparent") container.pack(fill="both", expand=True, padx=20, pady=20) # Clone Directory Section clone_section = ctk.CTkFrame(container, fg_color=COLORS['bg_tile']) clone_section.pack(fill="x", pady=(0, 15)) clone_header = ctk.CTkLabel( clone_section, text="📁 Clone-Verzeichnis", font=FONTS['body'], text_color=COLORS['text_primary'] ) clone_header.pack(anchor="w", padx=15, pady=(10, 5)) # Clone directory path clone_path_frame = ctk.CTkFrame(clone_section, fg_color="transparent") clone_path_frame.pack(fill="x", padx=30, pady=(5, 10)) default_clone_dir = str(Path.home() / "GiteaRepos") self.clone_dir_var = ctk.StringVar(value=self.settings.get("gitea_clone_directory", default_clone_dir)) self.clone_dir_entry = ctk.CTkEntry( clone_path_frame, textvariable=self.clone_dir_var, width=350, fg_color=COLORS['bg_primary'] ) self.clone_dir_entry.pack(side="left", padx=(0, 10)) browse_btn = ctk.CTkButton( clone_path_frame, text="Durchsuchen...", command=self.browse_clone_directory, fg_color=COLORS['bg_secondary'], hover_color=COLORS['bg_tile_hover'], text_color=COLORS['text_primary'], width=100 ) browse_btn.pack(side="left") # Info label info_label = ctk.CTkLabel( clone_section, text="Standardverzeichnis für geklonte Repositories", font=FONTS['small'], text_color=COLORS['text_dim'] ) info_label.pack(anchor="w", padx=30, pady=(0, 10)) # Server Configuration Section server_section = ctk.CTkFrame(container, fg_color=COLORS['bg_tile']) server_section.pack(fill="x", pady=(0, 15)) server_header = ctk.CTkLabel( server_section, text="🌐 Gitea Server", font=FONTS['body'], text_color=COLORS['text_primary'] ) server_header.pack(anchor="w", padx=15, pady=(10, 5)) # Server URL url_label = ctk.CTkLabel( server_section, text="Server URL:", font=FONTS['small'], text_color=COLORS['text_secondary'] ) url_label.pack(anchor="w", padx=30, pady=(5, 0)) self.gitea_url_var = ctk.StringVar(value=self.settings.get("gitea_server_url", "https://gitea-undso.intelsight.de")) self.gitea_url_entry = ctk.CTkEntry( server_section, textvariable=self.gitea_url_var, width=350, fg_color=COLORS['bg_primary'] ) self.gitea_url_entry.pack(padx=30, pady=(0, 5)) # API Token token_label = ctk.CTkLabel( server_section, text="API Token:", font=FONTS['small'], text_color=COLORS['text_secondary'] ) token_label.pack(anchor="w", padx=30, pady=(5, 0)) self.gitea_token_var = ctk.StringVar(value=self.settings.get("gitea_api_token", "")) self.gitea_token_entry = ctk.CTkEntry( server_section, textvariable=self.gitea_token_var, width=350, fg_color=COLORS['bg_primary'], show="*" ) self.gitea_token_entry.pack(padx=30, pady=(0, 5)) # Username user_label = ctk.CTkLabel( server_section, text="Benutzername:", font=FONTS['small'], text_color=COLORS['text_secondary'] ) user_label.pack(anchor="w", padx=30, pady=(5, 0)) self.gitea_user_var = ctk.StringVar(value=self.settings.get("gitea_username", "")) self.gitea_user_entry = ctk.CTkEntry( server_section, textvariable=self.gitea_user_var, width=350, fg_color=COLORS['bg_primary'] ) self.gitea_user_entry.pack(padx=30, pady=(0, 10)) # Test connection button test_btn = ctk.CTkButton( server_section, text="Verbindung testen", command=self.test_gitea_connection, fg_color=COLORS['bg_secondary'], hover_color=COLORS['bg_tile_hover'], text_color=COLORS['text_primary'], width=150 ) test_btn.pack(pady=(5, 10)) # Status label self.gitea_status_label = ctk.CTkLabel( server_section, text="", font=FONTS['small'], text_color=COLORS['text_secondary'], height=20 ) self.gitea_status_label.pack(pady=(0, 10)) def setup_activity_tab(self): """Setup activity server settings tab""" tab = self.tabview.tab("Team-Aktivität") # Container with padding container = ctk.CTkFrame(tab, fg_color="transparent") container.pack(fill="both", expand=True, padx=20, pady=20) # Activity Server Section activity_section = ctk.CTkFrame(container, fg_color=COLORS['bg_tile']) activity_section.pack(fill="x", pady=(0, 15)) activity_header = ctk.CTkLabel( activity_section, text="👥 Team-Aktivität Server", font=FONTS['body'], text_color=COLORS['text_primary'] ) activity_header.pack(anchor="w", padx=15, pady=(10, 5)) # Server URL url_label = ctk.CTkLabel( activity_section, text="Server URL:", font=FONTS['small'], text_color=COLORS['text_secondary'] ) url_label.pack(anchor="w", padx=30, pady=(5, 0)) self.server_url_var = ctk.StringVar(value=self.settings.get("activity_server_url", "http://91.99.192.14:3001")) self.server_url_entry = ctk.CTkEntry( activity_section, textvariable=self.server_url_var, width=350, fg_color=COLORS['bg_primary'] ) self.server_url_entry.pack(padx=30, pady=(0, 5)) # API Key api_label = ctk.CTkLabel( activity_section, text="API Key:", font=FONTS['small'], text_color=COLORS['text_secondary'] ) api_label.pack(anchor="w", padx=30, pady=(5, 0)) self.api_key_var = ctk.StringVar(value=self.settings.get("activity_api_key", "")) self.api_key_entry = ctk.CTkEntry( activity_section, textvariable=self.api_key_var, width=350, fg_color=COLORS['bg_primary'], show="*" ) self.api_key_entry.pack(padx=30, pady=(0, 5)) # User Name user_label = ctk.CTkLabel( activity_section, text="Benutzername:", font=FONTS['small'], text_color=COLORS['text_secondary'] ) user_label.pack(anchor="w", padx=30, pady=(5, 0)) self.user_name_var = ctk.StringVar(value=self.settings.get("activity_user_name", "")) self.user_name_entry = ctk.CTkEntry( activity_section, textvariable=self.user_name_var, width=350, fg_color=COLORS['bg_primary'] ) self.user_name_entry.pack(padx=30, pady=(0, 5)) # Test connection button test_btn = ctk.CTkButton( activity_section, text="Verbindung testen", command=self.test_activity_connection, fg_color=COLORS['bg_secondary'], hover_color=COLORS['bg_tile_hover'], text_color=COLORS['text_primary'], width=150 ) test_btn.pack(pady=(5, 10)) # Status label self.connection_status_label = ctk.CTkLabel( activity_section, text="", font=FONTS['small'], text_color=COLORS['text_secondary'], height=20 ) self.connection_status_label.pack(pady=(0, 10)) def load_settings(self): """Load settings from file""" try: if self.settings_file.exists(): with open(self.settings_file, 'r') as f: return json.load(f) except Exception as e: logger.error(f"Failed to load UI settings: {e}") return {} # No settings currently def save_settings(self): """Save settings to file""" try: self.settings_file.parent.mkdir(parents=True, exist_ok=True) with open(self.settings_file, 'w') as f: json.dump(self.settings, f, indent=2) logger.info(f"Saved UI settings: {self.settings}") except Exception as e: logger.error(f"Failed to save UI settings: {e}") def browse_clone_directory(self): """Browse for clone directory""" current_dir = self.clone_dir_var.get() if not os.path.exists(current_dir): current_dir = str(Path.home()) directory = filedialog.askdirectory( title="Clone-Verzeichnis auswählen", initialdir=current_dir, parent=self ) if directory: self.clone_dir_var.set(directory) logger.info(f"Selected clone directory: {directory}") def test_gitea_connection(self): """Test connection to Gitea server""" import requests server_url = self.gitea_url_var.get().strip() api_token = self.gitea_token_var.get().strip() if not server_url: self.gitea_status_label.configure( text="⚠️ Bitte Server URL eingeben", text_color=COLORS['accent_warning'] ) return self.gitea_status_label.configure( text="🔄 Teste Verbindung...", text_color=COLORS['text_secondary'] ) self.update() try: # Try to connect to the Gitea API headers = {} if api_token: headers["Authorization"] = f"token {api_token}" response = requests.get( f"{server_url}/api/v1/version", timeout=5, headers=headers ) if response.status_code == 200: version = response.json().get('version', 'Unknown') self.gitea_status_label.configure( text=f"✅ Verbindung erfolgreich! (Gitea {version})", text_color=COLORS['accent_success'] ) logger.info(f"Gitea connection successful: {server_url}, version: {version}") else: self.gitea_status_label.configure( text=f"❌ Server antwortet mit Status {response.status_code}", text_color=COLORS['accent_error'] ) logger.warning(f"Gitea server returned status {response.status_code}") except requests.exceptions.ConnectionError: self.gitea_status_label.configure( text="❌ Server nicht erreichbar", text_color=COLORS['accent_error'] ) logger.error(f"Gitea server not reachable: {server_url}") except requests.exceptions.Timeout: self.gitea_status_label.configure( text="❌ Verbindung Timeout", text_color=COLORS['accent_error'] ) logger.error(f"Gitea server connection timeout: {server_url}") except Exception as e: self.gitea_status_label.configure( text=f"❌ Fehler: {str(e)}", text_color=COLORS['accent_error'] ) logger.error(f"Gitea server connection error: {e}") def save_settings_only(self): """Save settings without applying to service or closing dialog""" # Get activity values server_url = self.server_url_var.get().strip() api_key = self.api_key_var.get().strip() user_name = self.user_name_var.get().strip() # Get Gitea values gitea_url = self.gitea_url_var.get().strip() gitea_token = self.gitea_token_var.get().strip() gitea_user = self.gitea_user_var.get().strip() clone_dir = self.clone_dir_var.get().strip() # Update settings self.settings["activity_server_url"] = server_url self.settings["activity_api_key"] = api_key self.settings["activity_user_name"] = user_name self.settings["gitea_server_url"] = gitea_url self.settings["gitea_api_token"] = gitea_token self.settings["gitea_username"] = gitea_user self.settings["gitea_clone_directory"] = clone_dir self.save_settings() # Show confirmation on the active tab current_tab = self.tabview.get() if current_tab == "Team-Aktivität": self.connection_status_label.configure( text="✅ Einstellungen gespeichert!", text_color=COLORS['accent_success'] ) self.after(2000, lambda: self.connection_status_label.configure(text="")) elif current_tab == "Gitea": self.gitea_status_label.configure( text="✅ Einstellungen gespeichert!", text_color=COLORS['accent_success'] ) self.after(2000, lambda: self.gitea_status_label.configure(text="")) logger.info("Settings saved successfully") def apply_settings(self): """Apply the selected settings""" import uuid from services.activity_sync import activity_service # Save all settings first self.save_settings_only() # Apply activity service settings activity_service.server_url = self.settings.get("activity_server_url", "") activity_service.api_key = self.settings.get("activity_api_key", "") activity_service.user_name = self.settings.get("activity_user_name", "") activity_service.user_id = self.settings.get("activity_user_id", str(uuid.uuid4())) # Save user ID if newly generated if "activity_user_id" not in self.settings: self.settings["activity_user_id"] = activity_service.user_id self.save_settings() # Save activity settings activity_service.save_settings() # Reconnect if settings changed if activity_service.connected: activity_service.disconnect() if activity_service.is_configured(): activity_service.connect() logger.info("Activity server settings applied") # Apply Gitea settings to config self.update_gitea_config() # Close dialog self.destroy() def update_gitea_config(self): """Update Gitea configuration in the application""" try: # Import here to avoid circular imports from src.gitea.gitea_client import gitea_config from src.gitea.git_operations import git_ops # Update Gitea config if hasattr(gitea_config, 'base_url'): gitea_config.base_url = self.settings.get("gitea_server_url", gitea_config.base_url) if hasattr(gitea_config, 'api_token'): gitea_config.api_token = self.settings.get("gitea_api_token", gitea_config.api_token) if hasattr(gitea_config, 'username'): gitea_config.username = self.settings.get("gitea_username", gitea_config.username) # Update clone directory clone_dir = self.settings.get("gitea_clone_directory") if clone_dir: # Re-initialize git_ops to use new settings from src.gitea.git_operations import init_git_ops init_git_ops() # Now update the clone directory if hasattr(git_ops, 'default_clone_dir'): git_ops.default_clone_dir = Path(clone_dir) logger.info(f"Updated git_ops clone directory to: {clone_dir}") # Update all existing RepositoryManager instances # This ensures that any git_ops instances in the application get the new directory try: # Update in main window if it exists parent = self.master if hasattr(parent, 'gitea_handler') and hasattr(parent.gitea_handler, 'repo_manager'): if hasattr(parent.gitea_handler.repo_manager, 'git_ops'): parent.gitea_handler.repo_manager.git_ops.default_clone_dir = Path(clone_dir) logger.info("Updated main window's git_ops clone directory") # Update in sidebar gitea explorer if it exists if hasattr(parent, 'sidebar') and hasattr(parent.sidebar, 'gitea_explorer'): if hasattr(parent.sidebar.gitea_explorer, 'repo_manager'): if hasattr(parent.sidebar.gitea_explorer.repo_manager, 'git_ops'): parent.sidebar.gitea_explorer.repo_manager.git_ops.default_clone_dir = Path(clone_dir) logger.info("Updated sidebar's git_ops clone directory") except Exception as e: logger.warning(f"Could not update all git_ops instances: {e}") logger.info("Gitea configuration updated") except ImportError as e: logger.warning(f"Could not update Gitea config: {e}") except Exception as e: logger.error(f"Error updating Gitea config: {e}") def test_activity_connection(self): """Test connection to activity server""" import requests from tkinter import messagebox server_url = self.server_url_var.get().strip() api_key = self.api_key_var.get().strip() if not server_url: self.connection_status_label.configure( text="⚠️ Bitte Server URL eingeben", text_color=COLORS['accent_warning'] ) return self.connection_status_label.configure( text="🔄 Teste Verbindung...", text_color=COLORS['text_secondary'] ) self.update() try: # Try to connect to the server response = requests.get( f"{server_url}/health", timeout=5, headers={"Authorization": f"Bearer {api_key}"} if api_key else {} ) if response.status_code == 200: self.connection_status_label.configure( text="✅ Verbindung erfolgreich!", text_color=COLORS['accent_success'] ) logger.info(f"Activity server connection successful: {server_url}") else: self.connection_status_label.configure( text=f"❌ Server antwortet mit Status {response.status_code}", text_color=COLORS['accent_error'] ) logger.warning(f"Activity server returned status {response.status_code}") except requests.exceptions.ConnectionError: self.connection_status_label.configure( text="❌ Server nicht erreichbar", text_color=COLORS['accent_error'] ) logger.error(f"Activity server not reachable: {server_url}") except requests.exceptions.Timeout: self.connection_status_label.configure( text="❌ Verbindung Timeout", text_color=COLORS['accent_error'] ) logger.error(f"Activity server connection timeout: {server_url}") except Exception as e: self.connection_status_label.configure( text=f"❌ Fehler: {str(e)}", text_color=COLORS['accent_error'] ) logger.error(f"Activity server connection error: {e}") def center_window(self): """Center the dialog on parent window""" self.update_idletasks() # Get parent window position parent = self.master parent_x = parent.winfo_x() parent_y = parent.winfo_y() parent_width = parent.winfo_width() parent_height = parent.winfo_height() # Get dialog size dialog_width = self.winfo_width() dialog_height = self.winfo_height() # Calculate position x = parent_x + (parent_width - dialog_width) // 2 y = parent_y + (parent_height - dialog_height) // 2 self.geometry(f"+{x}+{y}")