Dieser Commit ist enthalten in:
Claude Project Manager
2025-07-07 22:11:38 +02:00
Commit ec92da8a64
73 geänderte Dateien mit 16367 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()

536
gui/gitea_explorer.py Normale Datei
Datei anzeigen

@ -0,0 +1,536 @@
"""
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
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 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
local_path = Path.home() / "GiteaRepos" / 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()

3120
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(), [])

789
gui/project_tile.py Normale Datei
Datei anzeigen

@ -0,0 +1,789 @@
"""
Project Tile Component
Individual tile for each project in the grid
"""
import customtkinter as ctk
import tkinter as tk
from datetime import datetime
import os
import subprocess
import platform
import time
from typing import Callable, Optional
from pathlib import Path
from gui.styles import COLORS, FONTS, TILE_SIZE, BUTTON_STYLES
from utils.logger import logger
class ProjectTile(ctk.CTkFrame):
def __init__(self, parent, project, on_open: Callable, on_readme: Callable,
on_delete: Optional[Callable] = None, is_vps: bool = False,
on_stop: Optional[Callable] = None, is_running: bool = False,
on_rename: Optional[Callable] = None, on_select: Optional[Callable] = None):
super().__init__(
parent,
width=TILE_SIZE['width'],
height=TILE_SIZE['height'],
fg_color=COLORS['bg_vps'] if is_vps else COLORS['bg_tile'],
corner_radius=10
)
self.project = project
self.on_open = on_open
self.on_readme = on_readme
self.on_delete = on_delete
self.on_stop = on_stop
self.on_rename = on_rename
self.on_select = on_select
self.is_vps = is_vps
self.is_running = is_running
self.is_selected = False
self.grid_propagate(False)
self.setup_ui()
# Hover effect
self.bind("<Enter>", self.on_hover_enter)
self.bind("<Leave>", self.on_hover_leave)
# Click to select - only bind to the main frame
self.bind("<Button-1>", self._on_click)
def setup_ui(self):
"""Create tile UI elements"""
# Main container with padding
container = ctk.CTkFrame(self, fg_color="transparent")
container.pack(fill="both", expand=True, padx=TILE_SIZE['padding'], pady=TILE_SIZE['padding'])
# Bind click to container for selection (but not buttons)
container.bind("<Button-1>", self._on_click)
# Title row with edit button
title_row = ctk.CTkFrame(container, fg_color="transparent")
title_row.pack(fill="x", pady=(0, 5))
# Activity indicator (initially hidden)
self.activity_indicator = ctk.CTkLabel(
title_row,
text="🟢",
font=('Segoe UI', 10),
text_color=COLORS['accent_success'],
width=20
)
# Don't pack initially
# Title
title_text = self.project.name
if self.is_vps:
title_text = "🌐 " + title_text
self.title_label = ctk.CTkLabel(
title_row,
text=title_text,
font=FONTS['tile_title'],
text_color=COLORS['text_primary'],
anchor="w"
)
self.title_label.pack(side="left", fill="x", expand=True)
# Edit button (not for VPS)
if not self.is_vps and self.on_rename:
self.edit_button = ctk.CTkButton(
title_row,
text="📝",
command=lambda: self.on_rename(self.project),
width=25,
height=25,
fg_color="transparent",
hover_color=COLORS['bg_secondary'],
text_color=COLORS['text_secondary'],
font=('Segoe UI', 12)
)
self.edit_button.pack(side="right", padx=(5, 0))
# Path/Description
path_text = self.project.path
if len(path_text) > 40:
path_text = "..." + path_text[-37:]
self.path_label = ctk.CTkLabel(
container,
text=path_text,
font=FONTS['small'],
text_color=COLORS['text_secondary'],
anchor="w"
)
self.path_label.pack(fill="x")
# Tags if available
if self.project.tags:
tags_text = "".join(self.project.tags[:3])
self.tags_label = ctk.CTkLabel(
container,
text=tags_text,
font=FONTS['small'],
text_color=COLORS['text_dim'],
anchor="w"
)
self.tags_label.pack(fill="x", pady=(2, 0))
# Gitea repo link if available
if hasattr(self.project, 'gitea_repo') and self.project.gitea_repo:
self.gitea_label = ctk.CTkLabel(
container,
text=f"🔗 {self.project.gitea_repo}",
font=FONTS['small'],
text_color=COLORS['accent_success'],
anchor="w"
)
self.gitea_label.pack(fill="x", pady=(2, 0))
# Spacer
ctk.CTkFrame(container, height=1, fg_color="transparent").pack(expand=True)
# Last accessed
try:
last_accessed = datetime.fromisoformat(self.project.last_accessed)
time_diff = datetime.now() - last_accessed
if time_diff.days > 0:
time_text = f"{time_diff.days} days ago"
elif time_diff.seconds > 3600:
hours = time_diff.seconds // 3600
time_text = f"{hours} hours ago"
else:
minutes = time_diff.seconds // 60
time_text = f"{minutes} minutes ago" if minutes > 0 else "Just now"
except:
time_text = "Never"
self.time_label = ctk.CTkLabel(
container,
text=f"Last opened: {time_text}",
font=FONTS['small'],
text_color=COLORS['text_dim']
)
self.time_label.pack(fill="x", pady=(0, 10))
# Status message (initially hidden)
self.status_message = ctk.CTkLabel(
container,
text="",
font=FONTS['small'],
text_color=COLORS['accent_warning'],
anchor="w"
)
# Don't pack it initially - will be shown when needed
# Buttons - First row
button_frame = ctk.CTkFrame(container, fg_color="transparent")
button_frame.pack(fill="x")
# Open/Start/Stop button
if self.is_vps:
# VPS also uses red when running
btn_style = BUTTON_STYLES['danger'] if self.is_running else BUTTON_STYLES['vps']
# Special text for VPS Docker
if self.project.id == "vps-docker-permanent":
button_text = "Restart"
else:
button_text = "Connect"
else:
# Use danger (red) style when running, primary (cyan) when not
btn_style = BUTTON_STYLES['danger'] if self.is_running else BUTTON_STYLES['primary']
button_text = "Start Claude"
self.open_button = ctk.CTkButton(
button_frame,
text=button_text,
command=self._handle_open_click,
width=60 if (self.is_vps and self.project.id == "vps-permanent") else 95, # Smaller width only for main VPS Server
**btn_style
)
self.open_button.pack(side="left", padx=(0, 5))
# CMD button only for main VPS Server tile (not Admin Panel or Docker)
if self.is_vps and self.project.id == "vps-permanent":
self.cmd_button = ctk.CTkButton(
button_frame,
text="CMD",
command=self._open_cmd_ssh,
width=50,
**BUTTON_STYLES['secondary']
)
self.cmd_button.pack(side="left", padx=(0, 5))
# Gitea button (not for VPS)
if not self.is_vps:
self.gitea_button = ctk.CTkButton(
button_frame,
text="Gitea ▼",
command=self._show_gitea_menu,
width=60,
**BUTTON_STYLES['secondary']
)
self.gitea_button.pack(side="left", padx=(0, 2))
# Activity button - show even if not connected for better UX
from services.activity_sync import activity_service
self.activity_button = ctk.CTkButton(
button_frame,
text="",
command=self._toggle_activity,
width=30,
**BUTTON_STYLES['secondary']
)
self.activity_button.pack(side="left")
# Update button appearance based on connection status
if not activity_service.is_configured() or not activity_service.connected:
self.activity_button.configure(state="normal", text_color=COLORS['text_dim'])
# Delete button (not for VPS) - keep it in first row
if not self.is_vps and self.on_delete:
self.delete_button = ctk.CTkButton(
button_frame,
text="",
command=lambda: self.on_delete(self.project),
width=30,
fg_color=COLORS['bg_secondary'],
hover_color=COLORS['accent_error'],
text_color=COLORS['text_secondary']
)
self.delete_button.pack(side="right")
# Second row - Open Explorer button (only for non-VPS tiles)
if not self.is_vps:
explorer_frame = ctk.CTkFrame(container, fg_color="transparent")
explorer_frame.pack(fill="x", pady=(5, 0))
self.explorer_button = ctk.CTkButton(
explorer_frame,
text="Open Explorer",
command=self._open_explorer,
width=195, # Full width to match the buttons above
**BUTTON_STYLES['secondary']
)
self.explorer_button.pack()
# Don't apply click bindings - they interfere with button functionality
def on_hover_enter(self, event):
"""Handle mouse enter"""
if not self.is_vps:
self.configure(fg_color=COLORS['bg_tile_hover'])
def on_hover_leave(self, event):
"""Handle mouse leave"""
if not self.is_vps and not self.is_selected:
self.configure(fg_color=COLORS['bg_tile'])
def _on_click(self, event):
"""Handle click event"""
# Don't process if click was on a button or functional element
clicked_widget = event.widget
# Check if clicked on any button
if isinstance(clicked_widget, ctk.CTkButton):
return
# Walk up the widget hierarchy to check parent widgets
parent = clicked_widget
while parent:
if isinstance(parent, ctk.CTkButton):
return
# Stop at the tile itself
if parent == self:
break
parent = parent.master if hasattr(parent, 'master') else None
if self.on_select:
self.on_select(self.project)
def set_selected(self, selected: bool):
"""Set the selected state of the tile"""
self.is_selected = selected
if selected:
# Show border for selected tile
self.configure(border_width=2, border_color=COLORS['accent_primary'])
else:
# Remove border for unselected tile
self.configure(border_width=0)
def _handle_open_click(self):
"""Handle open/stop button click"""
if self.is_running and self.on_stop:
self.on_stop(self.project)
else:
# Disable button immediately to prevent multiple clicks
self.open_button.configure(state="disabled")
self.on_open(self.project)
def update_status(self, is_running: bool):
"""Update the running status of the tile"""
self.is_running = is_running
# Update button color and state
if self.is_vps:
btn_style = BUTTON_STYLES['danger'] if is_running else BUTTON_STYLES['vps']
else:
btn_style = BUTTON_STYLES['danger'] if is_running else BUTTON_STYLES['primary']
self.open_button.configure(
fg_color=btn_style['fg_color'],
hover_color=btn_style['hover_color'],
state="normal" # Re-enable button when status updates
)
# Update status message
if is_running:
self.status_message.configure(text="Claude instance is running. Close it to start a new one.")
self.status_message.pack(fill="x", pady=(0, 5), before=self.open_button.master)
else:
self.status_message.pack_forget()
def update_project(self, project):
"""Update tile with new project data"""
self.project = project
# Update last accessed time
self.time_label.configure(text=f"Last opened: Just now")
# Update title
title_text = self.project.name
if self.is_vps:
title_text = "🌐 " + title_text
self.title_label.configure(text=title_text)
# Update Gitea label if exists
if hasattr(self.project, 'gitea_repo') and self.project.gitea_repo:
if hasattr(self, 'gitea_label'):
self.gitea_label.configure(text=f"🔗 {self.project.gitea_repo}")
else:
# Create new Gitea label if it doesn't exist
self.gitea_label = ctk.CTkLabel(
self.winfo_children()[0], # Get container
text=f"🔗 {self.project.gitea_repo}",
font=FONTS['small'],
text_color=COLORS['accent_success'],
anchor="w"
)
# Pack it after tags label or path label
if hasattr(self, 'tags_label'):
self.gitea_label.pack(fill="x", pady=(2, 0), after=self.tags_label)
else:
self.gitea_label.pack(fill="x", pady=(2, 0), after=self.path_label)
def _bind_click_recursive(self, widget):
"""Recursively bind left-click to all children except buttons"""
# Don't bind to buttons - they have their own click handlers
if not isinstance(widget, ctk.CTkButton):
widget.bind("<Button-1>", self._on_click)
# Recursively bind to children only if not a button
for child in widget.winfo_children():
self._bind_click_recursive(child)
def _open_explorer(self):
"""Open the project folder in the file explorer"""
path = self.project.path
if os.path.exists(path):
if platform.system() == 'Windows':
os.startfile(path)
elif platform.system() == 'Darwin': # macOS
subprocess.Popen(['open', path])
else: # Linux and other Unix-like
subprocess.Popen(['xdg-open', path])
def _open_cmd_ssh(self):
"""Open CMD with SSH connection to VPS server with automatic authentication"""
import tempfile
import os
# SSH credentials
ssh_host = "91.99.192.14"
ssh_user = "claude-dev"
ssh_pass = "z0E1Al}q2H?Yqd!O"
if platform.system() == 'Windows':
# Create a temporary batch file for Windows
with tempfile.NamedTemporaryFile(mode='w', suffix='.bat', delete=False) as f:
batch_content = f'''@echo off
echo Connecting to VPS Server...
echo.
echo Please wait while establishing connection...
echo.
REM Try using plink if available (PuTTY command line)
where plink >nul 2>&1
if %ERRORLEVEL% EQU 0 (
echo Using PuTTY plink for connection...
plink -ssh -l {ssh_user} -pw "{ssh_pass}" {ssh_host}
) else (
echo PuTTY plink not found. Trying ssh with manual password entry...
echo.
echo Password: {ssh_pass}
echo.
echo Please copy the password above and paste it when prompted.
echo.
pause
ssh {ssh_user}@{ssh_host}
)
pause
'''
f.write(batch_content)
temp_batch = f.name
# Open CMD with the batch file
cmd = f'start "VPS SSH Connection" cmd /k "{temp_batch}"'
subprocess.Popen(cmd, shell=True)
# Clean up temp file after a delay
def cleanup():
time.sleep(2)
try:
os.unlink(temp_batch)
except:
pass
import threading
threading.Thread(target=cleanup, daemon=True).start()
else:
# For Unix-like systems, use sshpass if available
ssh_command = f'sshpass -p "{ssh_pass}" ssh {ssh_user}@{ssh_host}'
fallback_command = f'echo "Password: {ssh_pass}"; echo "Copy and paste when prompted:"; ssh {ssh_user}@{ssh_host}'
if platform.system() == 'Darwin': # macOS
# Check if sshpass is available
check_cmd = 'which sshpass'
result = subprocess.run(check_cmd, shell=True, capture_output=True)
if result.returncode == 0:
cmd = f'''osascript -e 'tell app "Terminal" to do script "{ssh_command}"' '''
else:
cmd = f'''osascript -e 'tell app "Terminal" to do script "{fallback_command}"' '''
subprocess.Popen(cmd, shell=True)
else: # Linux
# Try common terminal emulators
terminals = ['gnome-terminal', 'konsole', 'xterm', 'terminal']
for term in terminals:
try:
# Check if sshpass is available
check_cmd = 'which sshpass'
result = subprocess.run(check_cmd, shell=True, capture_output=True)
if result.returncode == 0:
if term == 'gnome-terminal':
subprocess.Popen([term, '--', 'bash', '-c', ssh_command])
else:
subprocess.Popen([term, '-e', ssh_command])
else:
if term == 'gnome-terminal':
subprocess.Popen([term, '--', 'bash', '-c', fallback_command])
else:
subprocess.Popen([term, '-e', fallback_command])
break
except FileNotFoundError:
continue
def _show_gitea_menu(self):
"""Show Gitea operations menu"""
# 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 project is a git repository
project_path = Path(self.project.path)
is_git_repo = (project_path / ".git").exists()
# Check if has remote
has_remote = False
if is_git_repo:
try:
# Hide console window on Windows
startupinfo = None
if hasattr(subprocess, 'STARTUPINFO'):
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
result = subprocess.run(
["git", "remote", "-v"],
cwd=project_path,
capture_output=True,
text=True,
startupinfo=startupinfo
)
has_remote = bool(result.stdout.strip())
except:
pass
if is_git_repo:
# Git repository options
menu.add_command(label="📊 Status anzeigen", command=lambda: self._gitea_operation("status"))
menu.add_separator()
menu.add_command(label="🔧 Repository reparieren", command=lambda: self._gitea_operation("fix_repo"))
menu.add_separator()
menu.add_command(label="💾 Commit", command=lambda: self._gitea_operation("commit"))
if has_remote:
menu.add_command(label="⬆️ Push", command=lambda: self._gitea_operation("push"))
menu.add_command(label="⬇️ Pull", command=lambda: self._gitea_operation("pull"))
menu.add_command(label="🔄 Fetch", command=lambda: self._gitea_operation("fetch"))
else:
menu.add_command(label="🔗 Mit Gitea verknüpfen", command=lambda: self._gitea_operation("link"))
menu.add_separator()
menu.add_command(label="🌿 Branch verwalten", command=lambda: self._gitea_operation("branch"))
menu.add_separator()
menu.add_command(label="🗂️ Große Dateien verwalten", command=lambda: self._gitea_operation("manage_large"))
menu.add_command(label="📦 Git LFS einrichten", command=lambda: self._gitea_operation("lfs"))
menu.add_separator()
menu.add_command(label="🔍 Verbindung testen", command=lambda: self._gitea_operation("test"))
menu.add_command(label="🔎 Repository verifizieren", command=lambda: self._gitea_operation("verify"))
else:
# Not a git repository
menu.add_command(label="🚀 Zu Gitea pushen", command=lambda: self._gitea_operation("init_push"))
menu.add_command(label="📥 Git initialisieren", command=lambda: self._gitea_operation("init"))
menu.add_separator()
menu.add_command(label="🔍 Verbindung testen", command=lambda: self._gitea_operation("test"))
# Show menu at button position
try:
# Get button position
x = self.gitea_button.winfo_rootx()
y = self.gitea_button.winfo_rooty() + self.gitea_button.winfo_height()
menu.tk_popup(x, y)
finally:
menu.grab_release()
def _gitea_operation(self, operation: str):
"""Trigger Gitea operation callback"""
# Get the main window reference
main_window = self.winfo_toplevel()
if hasattr(main_window, 'master') and hasattr(main_window.master, 'gitea_operation'):
main_window.master.gitea_operation(self.project, operation)
else:
# Try to find the main window
root = self.winfo_toplevel()
if hasattr(root, 'gitea_operation'):
root.gitea_operation(self.project, operation)
def _start_activity(self):
"""Start activity for this project"""
# Use main_window reference if available
if hasattr(self, 'main_window') and self.main_window:
self.main_window.start_activity(self.project)
return
# Otherwise try to find in widget hierarchy
widget = self
while widget:
if hasattr(widget, 'start_activity'):
widget.start_activity(self.project)
return
widget = widget.master if hasattr(widget, 'master') else None
# If not found, log error
logger.error("Could not find start_activity method in widget hierarchy")
def _stop_activity(self):
"""Stop current activity"""
# Use main_window reference if available
if hasattr(self, 'main_window') and self.main_window:
self.main_window.stop_activity()
return
# Otherwise try to find in widget hierarchy
widget = self
while widget:
if hasattr(widget, 'stop_activity'):
widget.stop_activity()
return
widget = widget.master if hasattr(widget, 'master') else None
# If not found, log error
logger.error("Could not find stop_activity method in widget hierarchy")
def _toggle_activity(self):
"""Toggle activity for this project"""
logger.info(f"Activity button clicked for project: {self.project.name}")
from services.activity_sync import activity_service
if not activity_service.is_configured():
logger.warning("Activity service not configured")
from tkinter import messagebox
messagebox.showinfo(
"Activity Server",
"Bitte konfigurieren Sie den Activity Server in den Einstellungen."
)
return
if not activity_service.connected:
logger.warning("Activity service not connected")
from tkinter import messagebox
messagebox.showwarning(
"Nicht verbunden",
"Keine Verbindung zum Activity Server.\n\nDer Server ist möglicherweise nicht erreichbar."
)
return
current_activity = activity_service.get_current_activity()
is_this_project_active = (current_activity and
current_activity.get('projectName') == self.project.name)
logger.info(f"Current activity status: {is_this_project_active}")
if is_this_project_active:
self._stop_activity()
else:
self._start_activity()
def update_activity_status(self, is_active: bool = False, user_name: str = None):
"""Update activity indicator on tile"""
if is_active:
# Show indicator
self.activity_indicator.pack(side="left", padx=(0, 5))
if user_name:
# Create tooltip with user name
self.activity_indicator.configure(cursor="hand2")
self.activity_indicator.bind("<Enter>", lambda e: self._show_activity_tooltip(user_name))
self.activity_indicator.bind("<Leave>", lambda e: self._hide_activity_tooltip())
# Update activity button if exists
if hasattr(self, 'activity_button'):
self.activity_button.configure(text="")
else:
# Hide indicator
self.activity_indicator.pack_forget()
# Update activity button if exists
if hasattr(self, 'activity_button'):
self.activity_button.configure(text="")
def _show_activity_tooltip(self, user_name: str):
"""Show tooltip with active user name"""
# Create tooltip
self.tooltip = ctk.CTkToplevel(self)
self.tooltip.wm_overrideredirect(True)
self.tooltip.configure(fg_color=COLORS['bg_secondary'])
label = ctk.CTkLabel(
self.tooltip,
text=f"{user_name} arbeitet hieran",
font=FONTS['small'],
text_color=COLORS['text_primary'],
fg_color=COLORS['bg_secondary']
)
label.pack(padx=5, pady=2)
# Position tooltip
x = self.activity_indicator.winfo_rootx()
y = self.activity_indicator.winfo_rooty() + 20
self.tooltip.geometry(f"+{x}+{y}")
def _hide_activity_tooltip(self):
"""Hide activity tooltip"""
if hasattr(self, 'tooltip'):
self.tooltip.destroy()
def check_activity(self):
"""Check if this project has active users"""
from services.activity_sync import activity_service
activity = activity_service.is_project_active(self.project.name)
if activity:
self.update_activity_status(True, activity.get('userName'))
else:
self.update_activity_status(False)
class AddProjectTile(ctk.CTkFrame):
"""Special tile for adding new projects"""
def __init__(self, parent, on_add: Callable):
super().__init__(
parent,
width=TILE_SIZE['width'],
height=TILE_SIZE['height'],
fg_color=COLORS['bg_secondary'],
corner_radius=10,
border_width=2,
border_color=COLORS['border_primary']
)
self.on_add = on_add
self.grid_propagate(False)
self.setup_ui()
# Hover effect
self.bind("<Enter>", self.on_hover_enter)
self.bind("<Leave>", self.on_hover_leave)
# Make entire tile clickable with cursor change
self.configure(cursor="hand2")
self.bind("<Button-1>", lambda e: self.on_add())
for child in self.winfo_children():
child.configure(cursor="hand2")
child.bind("<Button-1>", lambda e: self.on_add())
# Also bind to all nested children
self._bind_children_recursive(child)
def setup_ui(self):
"""Create add tile UI"""
container = ctk.CTkFrame(self, fg_color="transparent")
container.pack(fill="both", expand=True)
# Folder icon instead of plus
folder_icon = ctk.CTkLabel(
container,
text="📁",
font=('Segoe UI', 48),
text_color=COLORS['text_dim']
)
folder_icon.pack(expand=True)
# Text
text_label = ctk.CTkLabel(
container,
text="Add New Project",
font=FONTS['tile_title'],
text_color=COLORS['text_secondary']
)
text_label.pack(pady=(0, 10))
# Instruction text
instruction_label = ctk.CTkLabel(
container,
text="Click to select folder",
font=FONTS['small'],
text_color=COLORS['text_dim']
)
instruction_label.pack(pady=(0, 20))
def on_hover_enter(self, event):
"""Handle mouse enter"""
self.configure(border_color=COLORS['accent_primary'])
def on_hover_leave(self, event):
"""Handle mouse leave"""
self.configure(border_color=COLORS['border_primary'])
def _bind_children_recursive(self, widget):
"""Recursively bind click event to all children"""
for child in widget.winfo_children():
child.configure(cursor="hand2")
child.bind("<Button-1>", lambda e: self.on_add())
self._bind_children_recursive(child)

314
gui/settings_dialog.py Normale Datei
Datei anzeigen

@ -0,0 +1,314 @@
"""
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
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.geometry("450x400")
self.resizable(False, False)
# Make modal
self.transient(parent)
self.grab_set()
# Load current settings
self.settings = self.load_settings()
# Setup UI
self.setup_ui()
# 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))
# Activity Server Section
activity_section = ctk.CTkFrame(main_frame, fg_color=COLORS['bg_secondary'])
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=300,
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=300,
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=300,
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_tile'],
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']
)
self.connection_status_label.pack(pady=(0, 10))
# Buttons
button_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
button_frame.pack(side="bottom", fill="x", pady=(20, 0))
# Apply button
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))
# Cancel button
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="right")
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 apply_settings(self):
"""Apply the selected settings"""
import uuid
from services.activity_sync import activity_service
# Get 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()
# Update settings
self.settings["activity_server_url"] = server_url
self.settings["activity_api_key"] = api_key
self.settings["activity_user_name"] = user_name
self.save_settings()
# Update activity service
activity_service.server_url = server_url
activity_service.api_key = api_key
activity_service.user_name = 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")
# Close dialog
self.destroy()
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': 1200,
'height': 800,
'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'