Files
ClaudeProjectManager-main/gui/settings_dialog.py
Claude Project Manager 5f32daf3a4 Status LED
2025-07-08 13:13:46 +02:00

696 Zeilen
25 KiB
Python

"""
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}")