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()
|
||||
536
gui/gitea_explorer.py
Normale Datei
536
gui/gitea_explorer.py
Normale Datei
@ -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
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()
|
||||
3120
gui/main_window.py
Normale Datei
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
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(), [])
|
||||
789
gui/project_tile.py
Normale Datei
789
gui/project_tile.py
Normale Datei
@ -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
314
gui/settings_dialog.py
Normale Datei
@ -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
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': 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'
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren