Initial commit
Dieser Commit ist enthalten in:
113
gui/config.py
Normale Datei
113
gui/config.py
Normale Datei
@ -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
554
gui/gitea_explorer.py
Normale Datei
@ -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
319
gui/gitea_toolbar.py
Normale Datei
@ -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
16
gui/handlers/__init__.py
Normale Datei
@ -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
19
gui/handlers/base_handler.py
Normale Datei
@ -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")
|
||||
1891
gui/handlers/gitea_operations.py
Normale Datei
1891
gui/handlers/gitea_operations.py
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
101
gui/handlers/process_manager.py
Normale Datei
101
gui/handlers/process_manager.py
Normale Datei
@ -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)
|
||||
102
gui/handlers/project_manager.py
Normale Datei
102
gui/handlers/project_manager.py
Normale Datei
@ -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
203
gui/handlers/ui_helpers.py
Normale Datei
@ -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
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
272
gui/progress_bar.py
Normale Datei
@ -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
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
696
gui/settings_dialog.py
Normale Datei
@ -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
42
gui/sidebar_view.py
Normale Datei
@ -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
124
gui/styles.py
Normale Datei
@ -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'
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren