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

554 Zeilen
22 KiB
Python

Diese Datei enthält unsichtbare Unicode-Zeichen

Diese Datei enthält unsichtbare Unicode-Zeichen, die für Menschen nicht unterscheidbar sind, aber von einem Computer unterschiedlich verarbeitet werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.

Diese Datei enthält Unicode-Zeichen, die mit anderen Zeichen verwechselt werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.

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