Dieser Commit ist enthalten in:
Claude Project Manager
2025-07-09 22:10:42 +02:00
Commit 4dab418f2f
73 geänderte Dateien mit 16938 neuen und 0 gelöschten Zeilen

113
gui/config.py Normale Datei
Datei anzeigen

@ -0,0 +1,113 @@
"""
Configuration for GUI refactoring
Manages feature flags and refactoring settings
"""
import os
import json
from pathlib import Path
from utils.logger import logger
class RefactoringConfig:
"""Manages refactoring feature flags"""
CONFIG_FILE = "refactoring_config.json"
DEFAULT_FLAGS = {
'USE_GITEA_HANDLER': True, # Enable for Progress Bar support
'USE_PROCESS_HANDLER': True, # Enable for better logging
'USE_PROJECT_HANDLER': False,
'USE_UI_HELPERS': False,
'ENABLE_DEBUG_LOGGING': False,
'FORCE_ORIGINAL_IMPLEMENTATION': False # Emergency override
}
def __init__(self):
self.config_path = Path.home() / ".claude_project_manager" / self.CONFIG_FILE
self.flags = self.DEFAULT_FLAGS.copy()
self._load_config()
self._check_env_overrides()
def _load_config(self):
"""Load configuration from file"""
try:
if self.config_path.exists():
with open(self.config_path, 'r') as f:
loaded_flags = json.load(f)
# Only update known flags
for key, value in loaded_flags.items():
if key in self.flags:
self.flags[key] = value
logger.info(f"Loaded refactoring flag: {key} = {value}")
except Exception as e:
logger.error(f"Failed to load refactoring config: {e}")
def _check_env_overrides(self):
"""Check for environment variable overrides"""
# Global override
if os.getenv('CPM_USE_NEW_HANDLERS'):
logger.info("Enabling all refactored handlers via environment variable")
for key in ['USE_GITEA_HANDLER', 'USE_PROCESS_HANDLER',
'USE_PROJECT_HANDLER', 'USE_UI_HELPERS']:
self.flags[key] = True
# Individual overrides
for flag in self.flags:
env_var = f"CPM_{flag}"
if os.getenv(env_var):
value = os.getenv(env_var).lower() in ('true', '1', 'yes')
self.flags[flag] = value
logger.info(f"Override from env: {flag} = {value}")
def save_config(self):
"""Save current configuration to file"""
try:
self.config_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.config_path, 'w') as f:
json.dump(self.flags, f, indent=2)
logger.info(f"Saved refactoring config to {self.config_path}")
except Exception as e:
logger.error(f"Failed to save refactoring config: {e}")
def get(self, flag_name: str, default=None):
"""Get a flag value"""
return self.flags.get(flag_name, default)
def set(self, flag_name: str, value: bool):
"""Set a flag value"""
if flag_name in self.flags:
self.flags[flag_name] = value
logger.info(f"Set refactoring flag: {flag_name} = {value}")
else:
logger.warning(f"Unknown refactoring flag: {flag_name}")
def enable_handler(self, handler_name: str):
"""Enable a specific handler"""
# Map handler names to flags
handler_map = {
'gitea': 'USE_GITEA_HANDLER',
'process': 'USE_PROCESS_HANDLER',
'project': 'USE_PROJECT_HANDLER',
'ui': 'USE_UI_HELPERS' # Note: not _HANDLER suffix
}
flag_name = handler_map.get(handler_name.lower())
if flag_name and flag_name in self.flags:
self.set(flag_name, True)
else:
logger.warning(f"Unknown handler: {handler_name}")
def disable_all(self):
"""Disable all refactoring features"""
for flag in self.flags:
if flag.startswith('USE_'):
self.flags[flag] = False
logger.info("All refactoring features disabled")
def get_status(self):
"""Get status of all flags"""
return {
"config_file": str(self.config_path),
"flags": self.flags.copy()
}
# Global instance
refactoring_config = RefactoringConfig()

554
gui/gitea_explorer.py Normale Datei
Datei anzeigen

@ -0,0 +1,554 @@
"""
Gitea Repository Explorer for the sidebar
"""
import customtkinter as ctk
from tkinter import ttk, messagebox
import threading
import logging
import os
from pathlib import Path
from typing import Optional, Callable, List, Dict
from gui.styles import COLORS, FONTS
from src.gitea.repository_manager import RepositoryManager
from src.gitea.gitea_client import GiteaClient
import json
logger = logging.getLogger(__name__)
class GiteaExplorer(ctk.CTkFrame):
def __init__(self, parent, on_repo_select: Optional[Callable] = None, **kwargs):
super().__init__(parent, fg_color=COLORS['bg_secondary'], **kwargs)
self.on_repo_select = on_repo_select
self.repo_manager = RepositoryManager()
self.repositories = []
self.selected_repo = None
self.view_mode = "organization" # Always organization mode
self.organization_name = "IntelSight" # Fixed to IntelSight
self.organizations = [] # List of user's organizations
self.main_window = None # Will be set by main window
self.setup_ui()
# Skip load_organizations - we always use IntelSight
# Automatisches Laden der Repositories beim Start nach kurzer Verzögerung
self.after(500, self.refresh_repositories)
def _show_gitea_menu(self, repo: Dict, button: ctk.CTkButton):
"""Show Gitea operations menu for repository"""
import tkinter as tk
# Create menu
menu = tk.Menu(self, tearoff=0)
menu.configure(
bg=COLORS['bg_secondary'],
fg=COLORS['text_primary'],
activebackground=COLORS['bg_tile_hover'],
activeforeground=COLORS['text_primary'],
borderwidth=0,
relief="flat"
)
# Check if repository is already cloned
local_path = Path.home() / "GiteaRepos" / repo['name']
is_cloned = local_path.exists()
if is_cloned:
# Already cloned options
menu.add_command(label="📂 Im Explorer öffnen", command=lambda: self._open_in_explorer(repo))
menu.add_command(label="📊 Git Status", command=lambda: self._git_status(repo))
menu.add_separator()
menu.add_command(label="⬇️ Pull (Aktualisieren)", command=lambda: self._git_pull(repo))
menu.add_command(label="🔄 Fetch", command=lambda: self._git_fetch(repo))
menu.add_separator()
menu.add_command(label="🗑️ Lokale Kopie löschen", command=lambda: self._delete_local_copy(repo))
else:
# Not cloned options
menu.add_command(label="📥 Repository klonen", command=lambda: self._clone_repository(repo))
menu.add_separator()
menu.add_command(label=" Repository Info", command=lambda: self._show_repo_info(repo))
menu.add_command(label="🌐 In Browser öffnen", command=lambda: self._open_in_browser(repo))
# Show menu at button position
try:
x = button.winfo_rootx()
y = button.winfo_rooty() + button.winfo_height()
menu.tk_popup(x, y)
finally:
menu.grab_release()
def setup_ui(self):
"""Setup the explorer UI"""
# Header
header_frame = ctk.CTkFrame(self, fg_color="transparent")
header_frame.pack(fill="x", padx=10, pady=(10, 5))
# Title
self.title_label = ctk.CTkLabel(
header_frame,
text="🔧 Gitea Repositories",
font=FONTS['subtitle'],
text_color=COLORS['text_primary']
)
self.title_label.pack(side="left")
# Refresh button
refresh_btn = ctk.CTkButton(
header_frame,
text="🔄",
command=self.refresh_repositories,
width=30,
height=30,
fg_color=COLORS['accent_primary'],
hover_color=COLORS['accent_hover']
)
refresh_btn.pack(side="right")
# IntelSight label instead of toggle
org_frame = ctk.CTkFrame(self, fg_color=COLORS['bg_secondary'])
org_frame.pack(fill="x", padx=10, pady=5)
org_label = ctk.CTkLabel(
org_frame,
text="🏢 IntelSight Organization",
font=FONTS['body'],
text_color=COLORS['accent_primary']
)
org_label.pack()
# Search box
self.search_var = ctk.StringVar()
self.search_var.trace('w', self._on_search_changed)
search_frame = ctk.CTkFrame(self, fg_color=COLORS['bg_secondary'])
search_frame.pack(fill="x", padx=10, pady=5)
self.search_entry = ctk.CTkEntry(
search_frame,
placeholder_text="Repository suchen...",
textvariable=self.search_var,
fg_color=COLORS['bg_primary'],
text_color=COLORS['text_primary']
)
self.search_entry.pack(fill="x")
# Repository list with scrollbar
list_frame = ctk.CTkFrame(self, fg_color=COLORS['bg_secondary'])
list_frame.pack(fill="both", expand=True, padx=10, pady=(5, 10))
# Scrollable frame for repositories
self.scroll_frame = ctk.CTkScrollableFrame(
list_frame,
fg_color=COLORS['bg_primary'],
corner_radius=6
)
self.scroll_frame.pack(fill="both", expand=True)
# Repository items container
self.repo_container = ctk.CTkFrame(self.scroll_frame, fg_color=COLORS['bg_primary'])
self.repo_container.pack(fill="both", expand=True)
# Status label
self.status_label = ctk.CTkLabel(
self,
text="Lade Repositories...",
font=FONTS['small'],
text_color=COLORS['text_secondary']
)
self.status_label.pack(pady=5)
def _get_clone_directory(self) -> Path:
"""Get clone directory from settings or use default"""
try:
settings_file = Path.home() / ".claude_project_manager" / "ui_settings.json"
if settings_file.exists():
with open(settings_file, 'r') as f:
settings = json.load(f)
clone_dir = settings.get("gitea_clone_directory")
if clone_dir:
return Path(clone_dir)
except Exception as e:
logger.warning(f"Could not load clone directory from settings: {e}")
# Return default if not found in settings
return Path.home() / "GiteaRepos"
def refresh_repositories(self):
"""Refresh repository list from Gitea - only IntelSight"""
self.status_label.configure(text="Lade IntelSight Repositories...")
def fetch_repos():
try:
# Always fetch IntelSight organization repositories
all_repos = []
page = 1
while True:
# Fetch page by page to ensure we get all repositories
repos = self.repo_manager.list_organization_repositories("IntelSight", page=page, per_page=50)
if not repos:
break
all_repos.extend(repos)
page += 1
# Break if we got less than the requested amount (last page)
if len(repos) < 50:
break
self.repositories = all_repos
logger.info(f"Fetched {len(self.repositories)} repositories from IntelSight")
self.after(0, self._update_repository_list)
self.after(0, lambda: self.status_label.configure(
text=f"{len(self.repositories)} IntelSight Repositories"
))
except Exception as e:
logger.error(f"Error fetching repositories: {str(e)}")
self.after(0, lambda: self.status_label.configure(
text=f"Fehler: {str(e)}",
text_color=COLORS['accent_error']
))
threading.Thread(target=fetch_repos, daemon=True).start()
def _update_repository_list(self):
"""Update the displayed repository list"""
# Filter repositories based on search and remove duplicates
search_term = self.search_var.get().lower()
# Use a dict to track unique repos by name (preferring non-forks)
unique_repos = {}
for repo in self.repositories:
repo_name = repo['name']
# If we haven't seen this repo, or this one is not a fork (prefer originals)
if repo_name not in unique_repos or not repo.get('fork', False):
unique_repos[repo_name] = repo
# Filter based on search
filtered_repos = [
repo for repo in unique_repos.values()
if search_term in repo['name'].lower() or
search_term in repo.get('description', '').lower()
]
# Sort by name
filtered_repos.sort(key=lambda r: r['name'].lower())
# Get existing repo widgets
existing_widgets = {}
for widget in self.repo_container.winfo_children():
if hasattr(widget, 'repo_data'):
existing_widgets[widget.repo_data['name']] = widget
# Remove widgets for repos no longer in filtered list
filtered_names = {repo['name'] for repo in filtered_repos}
for name, widget in existing_widgets.items():
if name not in filtered_names:
widget.destroy()
# Update or create repository items
for i, repo in enumerate(filtered_repos):
if repo['name'] in existing_widgets:
# Update existing widget
widget = existing_widgets[repo['name']]
self._update_repo_item(widget, repo)
# Ensure correct order
widget.pack_forget()
widget.pack(fill="x", pady=2)
else:
# Create new widget
self._create_repo_item(repo)
def _create_repo_item(self, repo: Dict):
"""Create a repository item widget"""
# Main frame
item_frame = ctk.CTkFrame(
self.repo_container,
fg_color=COLORS['bg_gitea_tile'],
corner_radius=6,
height=70
)
item_frame.pack(fill="x", pady=2)
item_frame.pack_propagate(False)
# Store repo data
item_frame.repo_data = repo
repo['_widget'] = item_frame
# Content frame
content_frame = ctk.CTkFrame(item_frame, fg_color="transparent")
content_frame.pack(fill="both", expand=True, padx=10, pady=8)
# Top row - name and status
top_row = ctk.CTkFrame(content_frame, fg_color="transparent")
top_row.pack(fill="x")
# Repository name
name_label = ctk.CTkLabel(
top_row,
text=repo['name'],
font=FONTS['body'],
text_color=COLORS['text_primary'],
anchor="w"
)
name_label.pack(side="left", fill="x", expand=True)
# Clone/Status indicator
clone_dir = self._get_clone_directory()
local_path = clone_dir / repo['name']
if local_path.exists():
status_label = ctk.CTkLabel(
top_row,
text="✓ Cloned",
font=FONTS['small'],
text_color=COLORS['accent_success']
)
status_label.pack(side="right", padx=(5, 0))
# Check if it's a CPM project
if self.main_window and hasattr(self.main_window, 'project_manager'):
for project_data in self.main_window.project_manager.projects:
# project_data is a Project object, check if it has gitea_repo attribute
if hasattr(project_data, 'gitea_repo') and project_data.gitea_repo == f"{self.organization_name}/{repo['name']}":
cpm_label = ctk.CTkLabel(
top_row,
text="📁 CPM",
font=FONTS['small'],
text_color=COLORS['accent_primary']
)
cpm_label.pack(side="right", padx=(5, 0))
break
# Description
if repo.get('description'):
desc_label = ctk.CTkLabel(
content_frame,
text=repo['description'][:60] + '...' if len(repo.get('description', '')) > 60 else repo['description'],
font=FONTS['small'],
text_color=COLORS['text_secondary'],
anchor="w"
)
desc_label.pack(fill="x", pady=(2, 0))
# Bottom row - stats
stats_row = ctk.CTkFrame(content_frame, fg_color="transparent")
stats_row.pack(fill="x", pady=(4, 0))
# Private/Public
visibility = "🔒 Private" if repo.get('private', False) else "🌍 Public"
vis_label = ctk.CTkLabel(
stats_row,
text=visibility,
font=FONTS['small'],
text_color=COLORS['text_dim']
)
vis_label.pack(side="left", padx=(0, 10))
# Stars if available
if 'stars_count' in repo:
star_label = ctk.CTkLabel(
stats_row,
text=f"{repo['stars_count']}",
font=FONTS['small'],
text_color=COLORS['text_dim']
)
star_label.pack(side="left", padx=(0, 10))
# Menu button
menu_btn = ctk.CTkButton(
stats_row,
text="",
width=30,
height=24,
fg_color=COLORS['bg_tile'],
hover_color=COLORS['bg_tile_hover'],
font=('Segoe UI', 16)
)
menu_btn.configure(command=lambda: self._show_gitea_menu(repo, menu_btn))
menu_btn.pack(side="right")
# Click to select
def on_click(event):
self._select_repository(repo)
self._bind_click_recursive(item_frame, on_click)
# Hover effect
def on_enter(e):
if self.selected_repo != repo:
item_frame.configure(fg_color=COLORS['bg_gitea_hover'])
def on_leave(e):
if self.selected_repo != repo:
item_frame.configure(fg_color=COLORS['bg_gitea_tile'])
item_frame.bind("<Enter>", on_enter)
item_frame.bind("<Leave>", on_leave)
def _update_repo_item(self, item_frame: ctk.CTkFrame, repo: Dict):
"""Update an existing repository item widget"""
# Update stored data
item_frame.repo_data = repo
repo['_widget'] = item_frame
# Update clone status
local_path = Path.home() / "GiteaRepos" / repo['name']
# Find status label in widget hierarchy
for widget in item_frame.winfo_children():
if isinstance(widget, ctk.CTkFrame):
for child in widget.winfo_children():
if isinstance(child, ctk.CTkFrame):
for subchild in child.winfo_children():
if isinstance(subchild, ctk.CTkLabel) and "✓ Cloned" in subchild.cget("text"):
if not local_path.exists():
subchild.destroy()
break
def _select_repository(self, repo: Dict):
"""Select a repository"""
# Deselect previous
if self.selected_repo and '_widget' in self.selected_repo:
self.selected_repo['_widget'].configure(
fg_color=COLORS['bg_gitea_tile']
)
# Select new
self.selected_repo = repo
if '_widget' in repo:
repo['_widget'].configure(
fg_color=COLORS['bg_selected']
)
# Notify callback
if self.on_repo_select:
self.on_repo_select(repo)
def _on_search_changed(self, *args):
"""Handle search text change"""
self._update_repository_list()
def update_theme(self):
"""Update colors when theme changes"""
# Import updated colors
from gui.styles import get_colors, FONTS
global COLORS
COLORS = get_colors()
# Update main frame
self.configure(fg_color=COLORS['bg_secondary'])
# Update labels
self.title_label.configure(text_color=COLORS['text_primary'])
# Update search entry
self.search_entry.configure(
fg_color=COLORS['bg_primary'],
text_color=COLORS['text_primary'],
border_color=COLORS['border_primary']
)
# Update scroll frame and container
self.scroll_frame.configure(fg_color=COLORS['bg_primary'])
self.repo_container.configure(fg_color=COLORS['bg_primary'])
# Update status label
self.status_label.configure(text_color=COLORS['text_secondary'])
# Refresh the repository list to update item colors
self._update_repository_list()
# Clear any previous selection when theme changes
if self.selected_repo and '_widget' in self.selected_repo:
self.selected_repo['_widget'].configure(fg_color=COLORS['bg_gitea_tile'])
self.selected_repo = None
def _clone_repository(self, repo: Dict):
"""Clone repository and create CPM project"""
if self.main_window and hasattr(self.main_window, 'clone_repository'):
self.main_window.clone_repository(repo)
def _open_in_explorer(self, repo: Dict):
"""Open repository folder in file explorer"""
import platform
import subprocess
local_path = Path.home() / "GiteaRepos" / repo['name']
if local_path.exists():
if platform.system() == 'Windows':
os.startfile(str(local_path))
elif platform.system() == 'Darwin':
subprocess.Popen(['open', str(local_path)])
else:
subprocess.Popen(['xdg-open', str(local_path)])
def _git_status(self, repo: Dict):
"""Show git status for repository"""
if self.main_window and hasattr(self.main_window, 'show_git_status'):
# Create a pseudo-project object for compatibility
from types import SimpleNamespace
project = SimpleNamespace()
project.name = repo['name']
project.path = str(Path.home() / "GiteaRepos" / repo['name'])
self.main_window.show_git_status(project)
def _git_pull(self, repo: Dict):
"""Pull changes from remote"""
if self.main_window and hasattr(self.main_window, 'pull_from_gitea'):
from types import SimpleNamespace
project = SimpleNamespace()
project.name = repo['name']
project.path = str(Path.home() / "GiteaRepos" / repo['name'])
self.main_window.pull_from_gitea(project)
def _git_fetch(self, repo: Dict):
"""Fetch changes from remote"""
if self.main_window and hasattr(self.main_window, 'fetch_from_gitea'):
self.main_window.fetch_from_gitea(repo)
def _delete_local_copy(self, repo: Dict):
"""Delete local repository copy"""
from tkinter import messagebox
local_path = Path.home() / "GiteaRepos" / repo['name']
if messagebox.askyesno("Lokale Kopie löschen",
f"Möchten Sie die lokale Kopie von '{repo['name']}' wirklich löschen?\n\n"
f"Pfad: {local_path}"):
try:
import shutil
shutil.rmtree(local_path)
messagebox.showinfo("Erfolg", f"Lokale Kopie von '{repo['name']}' wurde gelöscht.")
self.refresh_repositories()
except Exception as e:
messagebox.showerror("Fehler", f"Fehler beim Löschen: {str(e)}")
def _show_repo_info(self, repo: Dict):
"""Show repository information"""
from tkinter import messagebox
info = f"Repository: {repo['name']}\n"
info += f"Beschreibung: {repo.get('description', 'Keine Beschreibung')}\n"
info += f"Privat: {'Ja' if repo.get('private', False) else 'Nein'}\n"
info += f"Erstellt: {repo.get('created_at', 'Unbekannt')}\n"
info += f"Aktualisiert: {repo.get('updated_at', 'Unbekannt')}\n"
messagebox.showinfo("Repository Info", info)
def _open_in_browser(self, repo: Dict):
"""Open repository in browser"""
import webbrowser
if 'html_url' in repo:
webbrowser.open(repo['html_url'])
def _bind_click_recursive(self, widget, callback):
"""Recursively bind click event to widget and all children"""
# Don't bind to buttons to avoid interfering with their functionality
if not isinstance(widget, ctk.CTkButton):
widget.bind("<Button-1>", callback)
for child in widget.winfo_children():
# Skip buttons
if not isinstance(child, ctk.CTkButton):
self._bind_click_recursive(child, callback)
def clear_selection(self):
"""Clear the current selection"""
if self.selected_repo and '_widget' in self.selected_repo:
# Remove border from selection
self.selected_repo['_widget'].configure(border_width=0)
self.selected_repo = None

319
gui/gitea_toolbar.py Normale Datei
Datei anzeigen

@ -0,0 +1,319 @@
"""
Gitea Toolbar for context-sensitive Git operations
"""
import customtkinter as ctk
from tkinter import messagebox
import threading
from pathlib import Path
from typing import Optional, Callable, Dict
from gui.styles import COLORS, FONTS
class GiteaToolbar(ctk.CTkFrame):
def __init__(self, parent, **kwargs):
super().__init__(parent, fg_color=COLORS['bg_secondary'], height=60, **kwargs)
self.pack_propagate(False)
# Callbacks
self.callbacks: Dict[str, Optional[Callable]] = {
'commit': None,
'push': None,
'pull': None,
'fetch': None,
'status': None,
'branch': None,
'link': None,
'clone': None,
'create_project': None
}
# Current context
self.current_context = None # 'local_project', 'gitea_repo', or None
self.current_item = None # Project or Repo object
# Animation
self.is_visible = False
self.target_height = 60
self.current_height = 0
# Create buttons container
self.button_container = ctk.CTkFrame(self, fg_color="transparent")
self.button_container.pack(fill="both", expand=True, padx=10, pady=8)
# Status label
self.status_label = ctk.CTkLabel(
self.button_container,
text="",
font=FONTS['small'],
text_color=COLORS['text_secondary']
)
self.status_label.pack(side="left", padx=(0, 20))
# Button groups
self.button_groups = {
'local_project': [],
'gitea_repo': []
}
self.setup_buttons()
def setup_buttons(self):
"""Setup all buttons for different contexts"""
# Buttons for local project context
local_buttons = [
("📊 Status", "status", "Git-Status anzeigen\nZeigt uncommittete Änderungen"),
("💾 Commit", "commit", "Änderungen speichern\nLokale Änderungen committen"),
("📤 Push", "push", "Zu Gitea hochladen\nCommits zum Server pushen"),
("📥 Pull", "pull", "Änderungen abrufen\nNeueste Änderungen vom Server"),
("🔗 Verknüpfen", "link", "Mit Gitea verknüpfen\nLokales Projekt mit Repository verbinden"),
("🌿 Branch", "branch", "Branch-Verwaltung\nBranches anzeigen und wechseln")
]
# Buttons for Gitea repo context
gitea_buttons = [
("📥 Clone", "clone", "Repository klonen\nLokale Kopie erstellen und Projekt anlegen"),
("🔄 Fetch", "fetch", "Updates prüfen\nNeueste Änderungen abrufen"),
("📊 Info", "status", "Repository-Info\nDetails zum Repository anzeigen")
]
# Create button frames
self.local_frame = ctk.CTkFrame(self.button_container, fg_color="transparent")
self.gitea_frame = ctk.CTkFrame(self.button_container, fg_color="transparent")
# Create local project buttons
for text, callback_name, tooltip in local_buttons:
btn = self.create_button(self.local_frame, text, callback_name, tooltip)
self.button_groups['local_project'].append(btn)
# Create Gitea repo buttons
for text, callback_name, tooltip in gitea_buttons:
btn = self.create_button(self.gitea_frame, text, callback_name, tooltip)
self.button_groups['gitea_repo'].append(btn)
def create_button(self, parent, text, callback_name, tooltip):
"""Create a button with tooltip"""
btn = ctk.CTkButton(
parent,
text=text,
command=lambda: self.execute_callback(callback_name),
width=120,
height=36,
fg_color=COLORS['accent_secondary'],
hover_color=COLORS['accent_hover'],
text_color="#FFFFFF",
font=FONTS['body']
)
btn.pack(side="left", padx=4)
# Create tooltip
self.create_tooltip(btn, tooltip)
return btn
def create_tooltip(self, widget, text):
"""Create a tooltip for a widget"""
def on_enter(event):
# Destroy any existing tooltip first
if hasattr(widget, 'tooltip') and widget.tooltip:
try:
widget.tooltip.destroy()
except:
pass
tooltip = ctk.CTkToplevel(widget)
tooltip.wm_overrideredirect(True)
tooltip.wm_geometry(f"+{event.x_root + 10}+{event.y_root + 10}")
label = ctk.CTkLabel(
tooltip,
text=text,
font=FONTS['small'],
fg_color=COLORS['bg_secondary'],
text_color=COLORS['text_primary'],
corner_radius=6,
padx=10,
pady=5
)
label.pack()
widget.tooltip = tooltip
def on_leave(event):
if hasattr(widget, 'tooltip') and widget.tooltip:
try:
widget.tooltip.destroy()
widget.tooltip = None
except:
pass
# Also destroy tooltip when button is clicked
def on_click(event):
if hasattr(widget, 'tooltip') and widget.tooltip:
try:
widget.tooltip.destroy()
widget.tooltip = None
except:
pass
widget.bind("<Enter>", on_enter)
widget.bind("<Leave>", on_leave)
widget.bind("<Button-1>", on_click, add=True)
def set_callback(self, name: str, callback: Callable):
"""Set a callback for a button"""
if name in self.callbacks:
self.callbacks[name] = callback
def execute_callback(self, name: str):
"""Execute a callback if set"""
if name in self.callbacks and self.callbacks[name]:
# Run in thread to avoid blocking UI
threading.Thread(
target=self.callbacks[name],
args=(self.current_item,),
daemon=True
).start()
def show(self):
"""Show the toolbar"""
if not self.is_visible:
self.animate_show()
def show_for_context(self, context: str, item=None, status_text: str = ""):
"""Show toolbar for specific context"""
if context not in ['local_project', 'gitea_repo']:
self.hide()
return
self.current_context = context
self.current_item = item
# Update status
self.status_label.configure(text=status_text)
# Hide all frames
self.local_frame.pack_forget()
self.gitea_frame.pack_forget()
# Show relevant frame
if context == 'local_project':
self.local_frame.pack(side="left", padx=10)
elif context == 'gitea_repo':
self.gitea_frame.pack(side="left", padx=10)
# Animate show
if not self.is_visible:
self.animate_show()
def hide(self):
"""Hide the toolbar"""
if self.is_visible:
self.animate_hide()
def animate_show(self):
"""Animate toolbar appearing"""
print("animate_show called")
self.is_visible = True
# Pack the toolbar
try:
# Simple approach: just pack after any header that exists
parent_children = self.master.winfo_children()
print(f"Parent has {len(parent_children)} children")
# Find the best position (after header, before content)
packed = False
for i, child in enumerate(parent_children):
# Look for a frame that might be the header
if isinstance(child, ctk.CTkFrame):
# Check if it's likely the header (has small height or contains title)
try:
if child.winfo_height() < 100 or any(
isinstance(w, ctk.CTkLabel) and "Claude Project Manager" in str(w.cget("text"))
for w in child.winfo_children()
):
print(f"Packing toolbar after header at position {i}")
self.pack(fill="x", after=child, pady=(0, 10))
packed = True
break
except:
pass
if not packed:
print("Packing toolbar at default position")
self.pack(fill="x", pady=(0, 10))
except Exception as e:
print(f"Error showing toolbar: {e}")
import traceback
traceback.print_exc()
self.pack(fill="x", pady=(0, 10))
# Skip animation for now - just show full height
self.configure(height=self.target_height)
print(f"Toolbar configured with height {self.target_height}")
def animate_hide(self):
"""Animate toolbar disappearing"""
self.is_visible = False
# Destroy all tooltips before hiding
for group in self.button_groups.values():
for btn in group:
if hasattr(btn, 'tooltip') and btn.tooltip:
try:
btn.tooltip.destroy()
btn.tooltip = None
except:
pass
# Skip animation for now - just hide immediately
self.pack_forget()
def _animate_height(self, start, end, duration=200, callback=None):
"""Animate height change"""
steps = 10
step_duration = duration // steps
step_size = (end - start) / steps
def animate_step(current_step):
if current_step <= steps:
new_height = start + (step_size * current_step)
self.configure(height=new_height)
self.after(step_duration, lambda: animate_step(current_step + 1))
elif callback:
callback()
animate_step(0)
def set_selected_repo(self, repo):
"""Set the selected repository and show appropriate buttons"""
self.show_for_context('gitea_repo', repo)
def update_button_states(self, states: Dict[str, bool]):
"""Update button enabled/disabled states"""
for context_buttons in self.button_groups.values():
for btn in context_buttons:
# Extract callback name from button text
btn_text = btn.cget("text")
for callback_name in self.callbacks:
if callback_name in states:
# Simple matching - could be improved
btn.configure(state="normal" if states[callback_name] else "disabled")
def refresh_colors(self):
"""Refresh the toolbar's colors"""
from gui.styles import COLORS
# Update frame color
self.configure(fg_color=COLORS['bg_secondary'])
# Update status label
self.status_label.configure(text_color=COLORS['text_secondary'])
# Update all buttons
for context_buttons in self.button_groups.values():
for btn in context_buttons:
btn.configure(
fg_color=COLORS['accent_primary'],
hover_color=COLORS['accent_hover'],
text_color="#FFFFFF"
)

16
gui/handlers/__init__.py Normale Datei
Datei anzeigen

@ -0,0 +1,16 @@
"""
Handler modules for MainWindow refactoring
Separates concerns and reduces God Class anti-pattern
"""
from .gitea_operations import GiteaOperationsHandler
from .process_manager import ProcessManagerHandler
from .project_manager import ProjectManagerHandler
from .ui_helpers import UIHelpersHandler
__all__ = [
'GiteaOperationsHandler',
'ProcessManagerHandler',
'ProjectManagerHandler',
'UIHelpersHandler'
]

19
gui/handlers/base_handler.py Normale Datei
Datei anzeigen

@ -0,0 +1,19 @@
"""
Base Handler for common functionality
"""
from typing import TYPE_CHECKING
from utils.logger import logger
if TYPE_CHECKING:
from gui.main_window import MainWindow
class BaseHandler:
"""Base class for all handlers"""
def __init__(self, main_window: 'MainWindow'):
"""Initialize with reference to main window"""
self.main_window = main_window
self.root = main_window.root
logger.info(f"{self.__class__.__name__} initialized")

Datei-Diff unterdrückt, da er zu groß ist Diff laden

Datei anzeigen

@ -0,0 +1,101 @@
"""
Process Manager Handler
Handles process monitoring and management operations
"""
from typing import Optional, TYPE_CHECKING
from utils.logger import logger
if TYPE_CHECKING:
from gui.main_window import MainWindow
from project_manager import Project
class ProcessManagerHandler:
"""Handles all process management operations for MainWindow"""
def __init__(self, main_window: 'MainWindow'):
"""Initialize with reference to main window"""
self.main_window = main_window
self.root = main_window.root
self.process_manager = main_window.process_manager
self.process_tracker = main_window.process_tracker
self.project_manager = main_window.project_manager
logger.info("ProcessManagerHandler initialized")
def monitor_process(self, project: 'Project', process) -> None:
"""Monitor a process for a project"""
return self.main_window._original_monitor_process(project, process)
def check_process_status(self) -> None:
"""Check status of all processes"""
return self.main_window._original_check_process_status()
def stop_project(self, project: 'Project') -> None:
"""Stop a running project"""
return self.main_window._original_stop_project(project)
def update_status(self, message: str, error: bool = False) -> None:
"""Update status bar message"""
# Direct implementation
from gui.styles import COLORS
if hasattr(self.main_window, 'status_label'):
self.main_window.status_label.configure(
text=message,
text_color=COLORS['accent_error'] if error else COLORS['text_secondary']
)
logger.debug(f"Status updated: {message} (error={error})")
def download_log(self) -> None:
"""Download comprehensive application log with all interactions"""
# Direct implementation
from tkinter import filedialog, messagebox
import os
from datetime import datetime
logger.info(f"Download log clicked - Total entries: {len(logger.log_entries)}")
try:
# Generate default filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
default_filename = f"CPM_FullLog_{timestamp}.log"
# Open file dialog to choose save location
file_path = filedialog.asksaveasfilename(
defaultextension=".log",
filetypes=[("Log files", "*.log"), ("Text files", "*.txt"), ("All files", "*.*")],
initialfile=default_filename,
title="Save Complete Application Log"
)
if file_path:
# Export logs to chosen location with system info
logger.export_logs(file_path, include_system_info=True)
# Add completion entry
logger.info(f"Log export completed - Total entries: {len(logger.log_entries)}, Interactions: {logger.interaction_count}")
# Update status
self.update_status(f"Log saved: {os.path.basename(file_path)} ({len(logger.log_entries)} entries)")
# Show success message with log details
messagebox.showinfo(
"Log Export Successful",
f"Complete application log saved to:\n{file_path}\n\n"
f"Total log entries: {len(logger.log_entries):,}\n"
f"UI interactions logged: {logger.interaction_count:,}\n"
f"File size: {os.path.getsize(file_path) / 1024:.1f} KB"
)
else:
logger.info("Log export cancelled by user")
except Exception as e:
error_msg = f"Error saving log: {str(e)}"
self.update_status(error_msg, error=True)
logger.log_exception(e, "download_log")
messagebox.showerror("Export Error", error_msg)
def _handle_process_ended(self, project: 'Project') -> None:
"""Handle process end event (private method)"""
return self.main_window._original_handle_process_ended(project)

Datei anzeigen

@ -0,0 +1,102 @@
"""
Project Manager Handler
Handles project CRUD operations and project-related UI updates
"""
from typing import Optional, TYPE_CHECKING
from utils.logger import logger
if TYPE_CHECKING:
from gui.main_window import MainWindow
from project_manager import Project
class ProjectManagerHandler:
"""Handles all project management operations for MainWindow"""
def __init__(self, main_window: 'MainWindow'):
"""Initialize with reference to main window"""
self.main_window = main_window
self.root = main_window.root
self.project_manager = main_window.project_manager
self.terminal_launcher = main_window.terminal_launcher
self.readme_generator = main_window.readme_generator
self.vps_connection = main_window.vps_connection
logger.info("ProjectManagerHandler initialized")
def add_new_project(self) -> None:
"""Add a new project with logging"""
logger.info("Add new project initiated")
return self.main_window._original_add_new_project()
def open_project(self, project: 'Project') -> None:
"""Open a project with comprehensive logging"""
logger.info(f"Opening project: {project.name} (ID: {project.id}) at {project.path}")
return self.main_window._original_open_project(project)
def delete_project(self, project: 'Project') -> None:
"""Delete a project"""
# Direct implementation
from tkinter import messagebox
logger.info(f"Attempting to delete project: {project.name}")
if messagebox.askyesno("Projekt löschen",
f"Möchten Sie das Projekt '{project.name}' wirklich aus dem Projekt-Manager entfernen?\n\n"
"Hinweis: Die Dateien werden NICHT gelöscht."):
self.project_manager.remove_project(project.id)
self.main_window.refresh_projects()
if hasattr(self.main_window, 'update_status'):
self.main_window.update_status(f"Removed: {project.name}")
logger.info(f"Project deleted: {project.name}")
def rename_project(self, project: 'Project') -> None:
"""Rename a project with logging"""
logger.info(f"Rename project initiated for: {project.name}")
return self.main_window._original_rename_project(project)
def refresh_projects(self) -> None:
"""Refresh the project display with logging"""
logger.debug("Refresh projects called")
logger.info(f"Refreshing projects - Total: {len(self.project_manager.projects)}")
return self.main_window._original_refresh_projects()
def create_project_from_repo(self, repo_data: dict, project_path: str) -> None:
"""Create a project from repository data"""
return self.main_window._original_create_project_from_repo(repo_data, project_path)
def open_vps_connection(self, project: 'Project') -> None:
"""Open VPS connection for project"""
return self.main_window._original_open_vps_connection(project)
def open_admin_panel(self, project: 'Project') -> None:
"""Open admin panel for project"""
return self.main_window._original_open_admin_panel(project)
def open_vps_docker(self, project: 'Project') -> None:
"""Open VPS Docker management"""
return self.main_window._original_open_vps_docker(project)
def open_readme(self, project: 'Project') -> None:
"""Open or generate README for project"""
return self.main_window._original_open_readme(project)
def generate_readme_background(self, project: 'Project') -> None:
"""Generate README in background"""
return self.main_window._original_generate_readme_background(project)
def open_gitea_window(self) -> None:
"""Open Gitea explorer window"""
return self.main_window._original_open_gitea_window()
def on_gitea_repo_select(self, repo_data: dict) -> None:
"""Handle Gitea repository selection"""
return self.main_window._original_on_gitea_repo_select(repo_data)
def clear_project_selection(self) -> None:
"""Clear current project selection"""
return self.main_window._original_clear_project_selection()
def on_project_select(self, project: 'Project', tile) -> None:
"""Handle project selection with logging"""
logger.info(f"Project selected: {project.name} (has Gitea repo: {bool(getattr(project, 'gitea_repo', None))})")
return self.main_window._original_on_project_select(project, tile)

203
gui/handlers/ui_helpers.py Normale Datei
Datei anzeigen

@ -0,0 +1,203 @@
"""
UI Helpers Handler
Handles UI creation, updates, and helper functions
"""
from typing import TYPE_CHECKING
from utils.logger import logger
if TYPE_CHECKING:
from gui.main_window import MainWindow
from project_manager import Project
class UIHelpersHandler:
"""Handles UI helper operations for MainWindow"""
def __init__(self, main_window: 'MainWindow'):
"""Initialize with reference to main window"""
self.main_window = main_window
self.root = main_window.root
logger.info("UIHelpersHandler initialized")
def setup_ui(self) -> None:
"""Setup the main UI"""
return self.main_window._original_setup_ui()
def create_header(self) -> None:
"""Create header section"""
# Direct implementation with proper references
import customtkinter as ctk
from gui.styles import COLORS, FONTS
header_frame = ctk.CTkFrame(self.main_window.main_container, fg_color=COLORS['bg_secondary'], height=80)
header_frame.pack(fill="x", padx=0, pady=0)
header_frame.pack_propagate(False)
# Title
self.main_window.title_label = ctk.CTkLabel(
header_frame,
text="IntelSight - Claude Project Manager",
font=FONTS['heading'],
text_color=COLORS['text_primary']
)
self.main_window.title_label.pack(side="left", padx=30, pady=20)
# Toolbar buttons
toolbar = ctk.CTkFrame(header_frame, fg_color="transparent")
toolbar.pack(side="right", padx=30, pady=20)
# Log download button
self.main_window.log_btn = ctk.CTkButton(
toolbar,
text="📥 Log",
command=self.main_window.download_log,
width=80,
fg_color=COLORS['accent_primary'],
hover_color=COLORS['accent_hover'],
text_color="#FFFFFF",
font=('Segoe UI', 12)
)
self.main_window.log_btn.pack(side="left", padx=(0, 10))
# Refresh button
self.main_window.refresh_btn = ctk.CTkButton(
toolbar,
text="↻ Refresh",
command=self.main_window.refresh_projects,
width=100,
fg_color=COLORS['bg_tile'],
hover_color=COLORS['bg_tile_hover'],
text_color=COLORS['text_primary']
)
self.main_window.refresh_btn.pack(side="left", padx=(0, 10))
logger.debug("Header created")
def create_content_area(self) -> None:
"""Create content area"""
return self.main_window._original_create_content_area()
def create_status_bar(self) -> None:
"""Create status bar"""
# Direct implementation
import customtkinter as ctk
from gui.styles import COLORS, FONTS
self.main_window.status_bar = ctk.CTkFrame(
self.main_window.main_container,
fg_color=COLORS['bg_secondary'],
height=30
)
self.main_window.status_bar.pack(fill="x", side="bottom")
self.main_window.status_bar.pack_propagate(False)
self.main_window.status_label = ctk.CTkLabel(
self.main_window.status_bar,
text="Ready",
font=FONTS['small'],
text_color=COLORS['text_secondary']
)
self.main_window.status_label.pack(side="left", padx=20, pady=5)
# Project count
self.main_window.count_label = ctk.CTkLabel(
self.main_window.status_bar,
text="0 projects",
font=FONTS['small'],
text_color=COLORS['text_secondary']
)
self.main_window.count_label.pack(side="right", padx=20, pady=5)
logger.debug("Status bar created")
def create_project_tile(self, project: 'Project', parent) -> 'ProjectTile':
"""Create a project tile"""
return self.main_window._original_create_project_tile(project, parent)
def create_add_tile(self, parent) -> 'AddProjectTile':
"""Create add project tile"""
return self.main_window._original_create_add_tile(parent)
def create_project_tile_flow(self, project: 'Project', parent) -> 'ProjectTile':
"""Create project tile for flow layout"""
return self.main_window._original_create_project_tile_flow(project, parent)
def create_add_tile_flow(self, parent) -> 'AddProjectTile':
"""Create add tile for flow layout"""
return self.main_window._original_create_add_tile_flow(parent)
def refresh_ui(self) -> None:
"""Refresh the UI"""
return self.main_window._original_refresh_ui()
def load_and_apply_theme(self) -> None:
"""Load and apply theme preference"""
# Direct implementation - very simple method
import customtkinter as ctk
ctk.set_appearance_mode('dark')
logger.info("Theme applied: dark mode")
def on_window_resize(self, event) -> None:
"""Handle window resize event"""
# Direct implementation
# Only process resize events from the main window
if event.widget == self.root:
# Cancel previous timer
if self.main_window.resize_timer:
self.root.after_cancel(self.main_window.resize_timer)
# Set new timer to refresh after resize stops with differential update
self.main_window.resize_timer = self.root.after(
300,
lambda: self.main_window.refresh_projects(differential=True)
)
logger.debug("Window resize event handled")
def setup_interaction_tracking(self) -> None:
"""Setup interaction tracking"""
return self.main_window._original_setup_interaction_tracking()
def _show_scrollable_info(self, title: str, content: str) -> None:
"""Show scrollable information dialog"""
# Direct implementation - standalone UI method
import customtkinter as ctk
dialog = ctk.CTkToplevel(self.root)
dialog.title(title)
dialog.geometry("600x500")
# Center the dialog
dialog.transient(self.root)
dialog.update_idletasks()
x = (dialog.winfo_screenwidth() - 600) // 2
y = (dialog.winfo_screenheight() - 500) // 2
dialog.geometry(f"600x500+{x}+{y}")
# Text widget with scrollbar
text_frame = ctk.CTkFrame(dialog)
text_frame.pack(fill="both", expand=True, padx=10, pady=10)
text_widget = ctk.CTkTextbox(text_frame, width=580, height=450)
text_widget.pack(fill="both", expand=True)
text_widget.insert("1.0", content)
text_widget.configure(state="disabled")
# Close button
close_btn = ctk.CTkButton(
dialog,
text="Schließen",
command=dialog.destroy,
width=100
)
close_btn.pack(pady=(0, 10))
dialog.grab_set()
dialog.focus_set()
logger.debug(f"Scrollable dialog shown: {title}")
def _differential_update(self, current_projects: list, previous_projects: list) -> None:
"""Perform differential update of projects"""
return self.main_window._original_differential_update(current_projects, previous_projects)
def _update_project_tiles_colors(self) -> None:
"""Update project tile colors"""
return self.main_window._original_update_project_tiles_colors()

3374
gui/main_window.py Normale Datei

Datei-Diff unterdrückt, da er zu groß ist Diff laden

272
gui/progress_bar.py Normale Datei
Datei anzeigen

@ -0,0 +1,272 @@
"""
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(), [])

1059
gui/project_tile.py Normale Datei

Datei-Diff unterdrückt, da er zu groß ist Diff laden

696
gui/settings_dialog.py Normale Datei
Datei anzeigen

@ -0,0 +1,696 @@
"""
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}")

42
gui/sidebar_view.py Normale Datei
Datei anzeigen

@ -0,0 +1,42 @@
"""
Sidebar View Manager
Simplified version with only Gitea explorer
"""
import customtkinter as ctk
from typing import Optional
from gui.styles import COLORS, FONTS
from gui.gitea_explorer import GiteaExplorer
from utils.logger import logger
class SidebarView(ctk.CTkFrame):
def __init__(self, parent, on_repo_select=None, **kwargs):
super().__init__(parent, fg_color=COLORS['bg_secondary'], **kwargs)
self.on_repo_select = on_repo_select
self.main_window = None # Will be set by main window
# Create Gitea explorer
self.gitea_explorer = GiteaExplorer(
self,
on_repo_select=self.on_repo_select
)
self.gitea_explorer.pack(fill="both", expand=True)
logger.info("Sidebar view initialized with Gitea explorer")
def get_gitea_explorer(self) -> Optional[GiteaExplorer]:
"""Get the Gitea explorer instance"""
return self.gitea_explorer
def refresh_all(self):
"""Refresh all explorers"""
if self.gitea_explorer:
self.gitea_explorer.refresh_repositories()
def set_main_window(self, main_window):
"""Set main window reference"""
self.main_window = main_window
if self.gitea_explorer:
self.gitea_explorer.main_window = main_window

124
gui/styles.py Normale Datei
Datei anzeigen

@ -0,0 +1,124 @@
"""
Style Configuration for GUI
Modern design with dark/light theme support
"""
# Dark Mode color definitions (only mode)
COLORS = {
# Background colors
'bg_primary': '#000000', # Schwarz
'bg_secondary': '#1A1F3A', # Dunkleres Blau
'bg_tile': '#232D53', # Dunkelblau
'bg_tile_hover': '#2A3560', # Helleres Dunkelblau
'bg_vps': '#1A2347', # VPS tile etwas dunkler
'bg_gitea_tile': '#2A3560', # Gitea tiles - deutlich heller für bessere Lesbarkeit
'bg_gitea_hover': '#364170', # Gitea hover - noch heller
'bg_selected': '#364170', # Selected - dunkleres Blau ohne Alpha
# Text colors
'text_primary': '#FFFFFF', # Weiß
'text_secondary': '#B0B0B0', # Grau
'text_dim': '#707070', # Dunkles Grau
# Accent colors
'accent_primary': '#00D4FF', # Cyan/Hellblau
'accent_hover': '#00E5FF', # Helleres Cyan
'accent_secondary': '#6B7AA1', # Sekundäres Blau-Grau
'accent_selected': '#00D4FF', # Auswahl-Farbe
'accent_success': '#4CAF50', # Success green
'accent_warning': '#FF9800', # Warning orange
'accent_error': '#F44336', # Error red
'accent_vps': '#00D4FF', # VPS auch Cyan
# Border colors
'border_primary': '#232D53', # Dunkelblau
'border_secondary': '#2A3560', # Helleres Dunkelblau
}
# Get current colors (always dark mode)
def get_colors():
"""Get colors (always returns dark mode colors)"""
return COLORS
# Font Configuration
FONTS = {
'heading': ('Segoe UI', 28, 'bold'),
'subheading': ('Segoe UI', 22, 'bold'),
'title': ('Segoe UI', 20, 'bold'), # Added for gitea UI
'subtitle': ('Segoe UI', 16, 'bold'), # Added for gitea UI
'tile_title': ('Segoe UI', 18, 'bold'),
'tile_text': ('Segoe UI', 14),
'body': ('Segoe UI', 14), # Added for gitea UI
'button': ('Segoe UI', 13, 'bold'),
'small': ('Segoe UI', 12),
'code': ('Consolas', 12), # Added for gitea UI code display
}
# Tile Dimensions
TILE_SIZE = {
'width': 280,
'height': 180,
'padding': 15,
'margin': 10,
}
# Button Styles
BUTTON_STYLES = {
'primary': {
'fg_color': COLORS['accent_primary'],
'hover_color': COLORS['accent_hover'],
'text_color': '#FFFFFF',
'corner_radius': 6,
'height': 32,
},
'secondary': {
'fg_color': COLORS['bg_secondary'],
'hover_color': COLORS['bg_tile'],
'text_color': COLORS['text_primary'],
'corner_radius': 6,
'height': 32,
'border_width': 1,
'border_color': COLORS['border_primary'],
},
'danger': {
'fg_color': COLORS['accent_error'],
'hover_color': '#FF5252',
'text_color': '#FFFFFF',
'corner_radius': 6,
'height': 32,
},
'vps': {
'fg_color': COLORS['accent_vps'],
'hover_color': COLORS['accent_hover'],
'text_color': '#FFFFFF',
'corner_radius': 6,
'height': 32,
}
}
def get_button_styles():
"""Get button styles (for compatibility)"""
return BUTTON_STYLES
# Window Configuration
WINDOW_CONFIG = {
'title': 'Claude Project Manager',
'width': 1600, # Increased to fit 4 VPS tiles side by side
'height': 1000, # Increased to show Gitea repos without scrolling
'min_width': 800,
'min_height': 600,
}
# Theme functions (kept for compatibility but always return dark)
def set_theme(theme_name):
"""Kept for compatibility - does nothing as only dark mode exists"""
return True
def get_theme():
"""Always returns 'dark'"""
return 'dark'
def toggle_theme():
"""Kept for compatibility - always returns 'dark'"""
return 'dark'