789 Zeilen
30 KiB
Python
789 Zeilen
30 KiB
Python
"""
|
|
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) |