stuff
This commit is contained in:
689
main.py
689
main.py
@@ -1,12 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('AppIndicator3', '0.1')
|
||||
from gi.repository import Gtk, Gdk, GLib, Gio, AppIndicator3
|
||||
import threading
|
||||
from gi.repository import Gtk, Gdk, GLib
|
||||
import sys
|
||||
from models import Customer, Location, Host
|
||||
import threading
|
||||
import pystray
|
||||
from PIL import Image, ImageDraw
|
||||
from models import Customer
|
||||
from data_loader import load_customers
|
||||
from widgets import ActiveCustomerCard, InactiveCustomerCard
|
||||
|
||||
|
||||
class VPNManagerWindow:
|
||||
@@ -18,9 +20,10 @@ class VPNManagerWindow:
|
||||
self.window = Gtk.Window()
|
||||
self.window.set_title("VPN Manager")
|
||||
self.window.set_default_size(1200, 750)
|
||||
self.window.connect("delete-event", self.hide_window)
|
||||
self.window.connect("delete-event", self.quit_app_from_close)
|
||||
self.window.connect("window-state-event", self.on_window_state_event)
|
||||
|
||||
# Set up CSS for dark theme
|
||||
# Set up minimal CSS for GNOME-style cards
|
||||
self.setup_css()
|
||||
|
||||
# Create UI
|
||||
@@ -29,184 +32,18 @@ class VPNManagerWindow:
|
||||
|
||||
# Start hidden
|
||||
self.window.hide()
|
||||
|
||||
|
||||
def setup_css(self):
|
||||
"""Minimal CSS for GNOME-style cards"""
|
||||
css_provider = Gtk.CssProvider()
|
||||
css = """
|
||||
window {
|
||||
background-color: #1a1d29;
|
||||
color: #e8eaf6;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #1a1d29 0%, #252836 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #e8eaf6;
|
||||
}
|
||||
|
||||
.search-entry {
|
||||
background-color: #2d3142;
|
||||
color: #e8eaf6;
|
||||
border: 1px solid #5e72e4;
|
||||
.card {
|
||||
background: @theme_base_color;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.customer-card {
|
||||
background-color: #252836;
|
||||
border: 1px solid #3a3f5c;
|
||||
border-radius: 8px;
|
||||
margin: 8px 5px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.location-card {
|
||||
background-color: #2a2e3f;
|
||||
border: 1px solid #3a3f5c;
|
||||
border-radius: 6px;
|
||||
margin: 5px 20px 5px 20px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.host-item {
|
||||
background-color: #1a1d29;
|
||||
border: 1px solid #3a3f5c;
|
||||
border-radius: 4px;
|
||||
margin: 3px 2px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.active-title {
|
||||
color: #2dce89;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.inactive-title {
|
||||
color: #8892b0;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.customer-name {
|
||||
color: #5e72e4;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.inactive-customer-name {
|
||||
color: #8892b0;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.location-name {
|
||||
color: #e8eaf6;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.vpn-type {
|
||||
color: #8892b0;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.connected-status {
|
||||
color: #2dce89;
|
||||
font-weight: bold;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.disconnected-status {
|
||||
color: #f5365c;
|
||||
font-weight: bold;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.connect-button {
|
||||
background-color: #5e72e4;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 5px 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.connect-button:hover {
|
||||
background-color: #3a3f5c;
|
||||
}
|
||||
|
||||
.disconnect-button {
|
||||
background-color: #f5365c;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 5px 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.disconnect-button:hover {
|
||||
background-color: #3a3f5c;
|
||||
}
|
||||
|
||||
.routes-button, .deactivate-button, .activate-button {
|
||||
background-color: #3a3f5c;
|
||||
color: #e8eaf6;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 5px 15px;
|
||||
margin: 0 3px;
|
||||
}
|
||||
|
||||
.deactivate-button {
|
||||
background-color: #fb6340;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.activate-button {
|
||||
background-color: #5e72e4;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
padding: 8px 20px;
|
||||
}
|
||||
|
||||
.launch-button {
|
||||
background-color: #5e72e4;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 12px;
|
||||
font-weight: bold;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.host-name {
|
||||
color: #e8eaf6;
|
||||
font-weight: bold;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.host-address {
|
||||
color: #8892b0;
|
||||
font-size: 8px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.services-header {
|
||||
color: #5e72e4;
|
||||
font-weight: bold;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.service-count {
|
||||
color: #6c757d;
|
||||
font-size: 9px;
|
||||
border: 1px solid @borders;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
padding: 16px;
|
||||
margin: 6px;
|
||||
}
|
||||
"""
|
||||
css_provider.load_from_data(css.encode())
|
||||
@@ -217,115 +54,125 @@ class VPNManagerWindow:
|
||||
style_context.add_provider_for_screen(
|
||||
screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||
)
|
||||
|
||||
|
||||
def setup_ui(self):
|
||||
# Main container
|
||||
main_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
# Use HeaderBar for native GNOME look
|
||||
header_bar = Gtk.HeaderBar()
|
||||
header_bar.set_show_close_button(True)
|
||||
header_bar.set_title("VPN Manager")
|
||||
header_bar.set_subtitle("Connection Manager")
|
||||
self.window.set_titlebar(header_bar)
|
||||
|
||||
# Main container with proper spacing
|
||||
main_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
main_vbox.set_margin_start(12)
|
||||
main_vbox.set_margin_end(12)
|
||||
main_vbox.set_margin_top(12)
|
||||
main_vbox.set_margin_bottom(12)
|
||||
self.window.add(main_vbox)
|
||||
|
||||
# Header
|
||||
header_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
header_box.get_style_context().add_class("header")
|
||||
main_vbox.pack_start(header_box, False, False, 0)
|
||||
|
||||
# Title with icon
|
||||
title_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||
title_box.set_halign(Gtk.Align.CENTER)
|
||||
header_box.pack_start(title_box, False, False, 10)
|
||||
|
||||
icon_label = Gtk.Label(label="🛡️")
|
||||
icon_label.set_markup('<span font="24">🛡️</span>')
|
||||
title_box.pack_start(icon_label, False, False, 0)
|
||||
|
||||
title_label = Gtk.Label(label="VPN Connection Manager")
|
||||
title_label.get_style_context().add_class("title")
|
||||
title_box.pack_start(title_label, False, False, 0)
|
||||
|
||||
# Search bar
|
||||
search_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
|
||||
search_box.set_margin_start(20)
|
||||
search_box.set_margin_end(20)
|
||||
header_box.pack_start(search_box, False, False, 0)
|
||||
|
||||
search_icon = Gtk.Label(label="🔍")
|
||||
search_box.pack_start(search_icon, False, False, 10)
|
||||
|
||||
self.search_entry = Gtk.Entry()
|
||||
# Search bar with SearchEntry
|
||||
self.search_entry = Gtk.SearchEntry()
|
||||
self.search_entry.set_placeholder_text("Search customers, locations, or hosts...")
|
||||
self.search_entry.get_style_context().add_class("search-entry")
|
||||
self.search_entry.connect("changed", self.filter_customers)
|
||||
search_box.pack_start(self.search_entry, True, True, 0)
|
||||
self.search_entry.connect("search-changed", self.filter_customers)
|
||||
main_vbox.pack_start(self.search_entry, False, False, 0)
|
||||
|
||||
# Main content area with two columns
|
||||
columns_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10, homogeneous=True)
|
||||
columns_box.set_margin_start(10)
|
||||
columns_box.set_margin_end(10)
|
||||
columns_box.set_margin_bottom(10)
|
||||
# Clean two-column layout like GNOME Control Center
|
||||
columns_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=24)
|
||||
main_vbox.pack_start(columns_box, True, True, 0)
|
||||
|
||||
# Left column - Active customers
|
||||
left_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
left_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
columns_box.pack_start(left_vbox, True, True, 0)
|
||||
|
||||
active_header = Gtk.Label(label="✅ Active Customers")
|
||||
active_header.get_style_context().add_class("active-title")
|
||||
active_header.set_halign(Gtk.Align.START)
|
||||
left_vbox.pack_start(active_header, False, False, 0)
|
||||
# Simple label header
|
||||
active_label = Gtk.Label()
|
||||
active_label.set_markup("<b>Active Customers</b>")
|
||||
active_label.set_halign(Gtk.Align.START)
|
||||
left_vbox.pack_start(active_label, False, False, 0)
|
||||
|
||||
# Active customers scrolled window
|
||||
# Clean scrolled window without borders
|
||||
active_scrolled = Gtk.ScrolledWindow()
|
||||
active_scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||
active_scrolled.set_shadow_type(Gtk.ShadowType.NONE)
|
||||
left_vbox.pack_start(active_scrolled, True, True, 0)
|
||||
|
||||
self.active_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
self.active_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
active_scrolled.add(self.active_box)
|
||||
|
||||
# Right column - Inactive customers
|
||||
right_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
# Right column - Inactive customers
|
||||
right_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
columns_box.pack_start(right_vbox, True, True, 0)
|
||||
|
||||
inactive_header = Gtk.Label(label="💤 Inactive Customers")
|
||||
inactive_header.get_style_context().add_class("inactive-title")
|
||||
inactive_header.set_halign(Gtk.Align.START)
|
||||
right_vbox.pack_start(inactive_header, False, False, 0)
|
||||
# Simple label header
|
||||
inactive_label = Gtk.Label()
|
||||
inactive_label.set_markup("<b>Inactive Customers</b>")
|
||||
inactive_label.set_halign(Gtk.Align.START)
|
||||
right_vbox.pack_start(inactive_label, False, False, 0)
|
||||
|
||||
# Inactive customers scrolled window
|
||||
# Clean scrolled window without borders
|
||||
inactive_scrolled = Gtk.ScrolledWindow()
|
||||
inactive_scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||
inactive_scrolled.set_shadow_type(Gtk.ShadowType.NONE)
|
||||
right_vbox.pack_start(inactive_scrolled, True, True, 0)
|
||||
|
||||
self.inactive_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
self.inactive_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
inactive_scrolled.add(self.inactive_box)
|
||||
|
||||
# Render initial data
|
||||
self.render_customers()
|
||||
|
||||
def setup_system_tray(self):
|
||||
self.indicator = AppIndicator3.Indicator.new(
|
||||
"vpn-manager",
|
||||
"network-vpn",
|
||||
AppIndicator3.IndicatorCategory.APPLICATION_STATUS
|
||||
# Create a simple icon for the system tray
|
||||
def create_icon():
|
||||
# Create a simple network icon
|
||||
width = height = 64
|
||||
image = Image.new('RGBA', (width, height), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
# Draw a simple network/VPN icon
|
||||
# Outer circle
|
||||
draw.ellipse([8, 8, 56, 56], outline=(50, 150, 50), width=4)
|
||||
# Inner dot
|
||||
draw.ellipse([26, 26, 38, 38], fill=(50, 150, 50))
|
||||
# Connection lines
|
||||
draw.line([32, 16, 32, 24], fill=(50, 150, 50), width=3)
|
||||
draw.line([32, 40, 32, 48], fill=(50, 150, 50), width=3)
|
||||
draw.line([16, 32, 24, 32], fill=(50, 150, 50), width=3)
|
||||
draw.line([40, 32, 48, 32], fill=(50, 150, 50), width=3)
|
||||
|
||||
return image
|
||||
|
||||
# Simple approach: Create tray icon with direct action and minimal menu
|
||||
self.tray_icon = pystray.Icon(
|
||||
"VPN Manager",
|
||||
create_icon(),
|
||||
"VPN Manager - Double-click to open"
|
||||
)
|
||||
self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
|
||||
|
||||
# Create menu
|
||||
menu = Gtk.Menu()
|
||||
# Set direct click action
|
||||
self.tray_icon.default_action = self.show_window_from_tray
|
||||
|
||||
# Open item
|
||||
open_item = Gtk.MenuItem(label="Open VPN Manager")
|
||||
open_item.connect("activate", self.show_window_from_tray)
|
||||
menu.append(open_item)
|
||||
# Also provide a right-click menu
|
||||
menu = pystray.Menu(
|
||||
pystray.MenuItem("Open VPN Manager", self.show_window_from_tray, default=True),
|
||||
pystray.MenuItem("Quit", self.quit_app)
|
||||
)
|
||||
self.tray_icon.menu = menu
|
||||
|
||||
# Separator
|
||||
menu.append(Gtk.SeparatorMenuItem())
|
||||
|
||||
# Quit item
|
||||
quit_item = Gtk.MenuItem(label="Quit")
|
||||
quit_item.connect("activate", self.quit_app)
|
||||
menu.append(quit_item)
|
||||
|
||||
menu.show_all()
|
||||
self.indicator.set_menu(menu)
|
||||
# Start tray icon in separate thread
|
||||
threading.Thread(target=self.tray_icon.run, daemon=True).start()
|
||||
|
||||
def get_callbacks(self):
|
||||
"""Return callback functions for widget interactions"""
|
||||
return {
|
||||
'toggle_connection': self.toggle_connection,
|
||||
'set_location_active': self.set_location_active,
|
||||
'deactivate_location': self.deactivate_location,
|
||||
'open_service': self.open_service,
|
||||
'open_customer_service': self.open_customer_service
|
||||
}
|
||||
|
||||
def render_customers(self):
|
||||
# Clear existing content
|
||||
@@ -343,237 +190,34 @@ class VPNManagerWindow:
|
||||
inactive_locations = customer.get_inactive_locations()
|
||||
|
||||
if active_locations:
|
||||
customer_data = Customer(
|
||||
name=customer.name,
|
||||
locations=active_locations
|
||||
)
|
||||
from models import Customer
|
||||
customer_data = Customer(name=customer.name)
|
||||
customer_data.services = customer.services
|
||||
customer_data.locations = active_locations
|
||||
customers_with_active.append(customer_data)
|
||||
|
||||
if inactive_locations:
|
||||
customer_data = Customer(
|
||||
name=customer.name,
|
||||
locations=inactive_locations
|
||||
)
|
||||
from models import Customer
|
||||
customer_data = Customer(name=customer.name)
|
||||
customer_data.services = customer.services
|
||||
customer_data.locations = inactive_locations
|
||||
customers_with_inactive.append(customer_data)
|
||||
|
||||
# Render active customers
|
||||
for customer in customers_with_active:
|
||||
self.create_customer_with_active_locations(customer)
|
||||
# Get callbacks for widgets
|
||||
callbacks = self.get_callbacks()
|
||||
|
||||
# Render inactive customers
|
||||
# Render active customers using widget classes
|
||||
for customer in customers_with_active:
|
||||
customer_card = ActiveCustomerCard(customer, callbacks)
|
||||
self.active_box.pack_start(customer_card.widget, False, False, 0)
|
||||
|
||||
# Render inactive customers using widget classes
|
||||
for customer in customers_with_inactive:
|
||||
self.create_customer_without_active_locations(customer)
|
||||
customer_card = InactiveCustomerCard(customer, callbacks)
|
||||
self.inactive_box.pack_start(customer_card.widget, False, False, 0)
|
||||
|
||||
self.window.show_all()
|
||||
|
||||
def create_customer_with_active_locations(self, customer):
|
||||
# Customer card
|
||||
customer_frame = Gtk.Frame()
|
||||
customer_frame.get_style_context().add_class("customer-card")
|
||||
self.active_box.pack_start(customer_frame, False, False, 0)
|
||||
|
||||
customer_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
|
||||
customer_frame.add(customer_vbox)
|
||||
|
||||
# Customer header
|
||||
customer_label = Gtk.Label(label=f"🏢 {customer.name}")
|
||||
customer_label.get_style_context().add_class("customer-name")
|
||||
customer_label.set_halign(Gtk.Align.START)
|
||||
customer_vbox.pack_start(customer_label, False, False, 0)
|
||||
|
||||
# Render each location
|
||||
for location in customer.locations:
|
||||
self.create_active_location_card(location, customer_vbox, customer.name)
|
||||
|
||||
def create_active_location_card(self, location, parent_box, customer_name):
|
||||
# Location card
|
||||
location_frame = Gtk.Frame()
|
||||
location_frame.get_style_context().add_class("location-card")
|
||||
parent_box.pack_start(location_frame, False, False, 0)
|
||||
|
||||
location_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
|
||||
location_frame.add(location_vbox)
|
||||
|
||||
# Location header with controls
|
||||
header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||
location_vbox.pack_start(header_box, False, False, 0)
|
||||
|
||||
# Location info
|
||||
info_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
||||
header_box.pack_start(info_vbox, True, True, 0)
|
||||
|
||||
location_label = Gtk.Label(label=f"📍 {location.name}")
|
||||
location_label.get_style_context().add_class("location-name")
|
||||
location_label.set_halign(Gtk.Align.START)
|
||||
info_vbox.pack_start(location_label, False, False, 0)
|
||||
|
||||
# VPN type
|
||||
vpn_icons = {
|
||||
"OpenVPN": "🔒",
|
||||
"WireGuard": "⚡",
|
||||
"IPSec": "🛡️"
|
||||
}
|
||||
vpn_icon = vpn_icons.get(location.vpn_type, "🔑")
|
||||
|
||||
type_label = Gtk.Label(label=f"{vpn_icon} {location.vpn_type} VPN")
|
||||
type_label.get_style_context().add_class("vpn-type")
|
||||
type_label.set_halign(Gtk.Align.START)
|
||||
info_vbox.pack_start(type_label, False, False, 0)
|
||||
|
||||
# Controls
|
||||
controls_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
||||
header_box.pack_end(controls_box, False, False, 0)
|
||||
|
||||
# Status
|
||||
status_text = "● Connected" if location.connected else "○ Disconnected"
|
||||
status_label = Gtk.Label(label=status_text)
|
||||
if location.connected:
|
||||
status_label.get_style_context().add_class("connected-status")
|
||||
else:
|
||||
status_label.get_style_context().add_class("disconnected-status")
|
||||
controls_box.pack_start(status_label, False, False, 0)
|
||||
|
||||
# Connect/Disconnect button
|
||||
btn_text = "Disconnect" if location.connected else "Connect"
|
||||
connect_btn = Gtk.Button(label=btn_text)
|
||||
if location.connected:
|
||||
connect_btn.get_style_context().add_class("disconnect-button")
|
||||
else:
|
||||
connect_btn.get_style_context().add_class("connect-button")
|
||||
connect_btn.connect("clicked", lambda btn, l=location: self.toggle_connection(l))
|
||||
controls_box.pack_start(connect_btn, False, False, 0)
|
||||
|
||||
# Routes button
|
||||
routes_btn = Gtk.Button(label="Routes")
|
||||
routes_btn.get_style_context().add_class("routes-button")
|
||||
routes_btn.connect("clicked", lambda btn, l=location: self.set_route(l))
|
||||
controls_box.pack_start(routes_btn, False, False, 0)
|
||||
|
||||
# Deactivate button
|
||||
deactivate_btn = Gtk.Button(label="Deactivate")
|
||||
deactivate_btn.get_style_context().add_class("deactivate-button")
|
||||
deactivate_btn.connect("clicked", lambda btn, l=location: self.deactivate_location(l, customer_name))
|
||||
controls_box.pack_start(deactivate_btn, False, False, 0)
|
||||
|
||||
# Hosts section
|
||||
if location.hosts:
|
||||
# Separator
|
||||
separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
location_vbox.pack_start(separator, False, False, 5)
|
||||
|
||||
services_label = Gtk.Label(label="💼 Available Services")
|
||||
services_label.get_style_context().add_class("services-header")
|
||||
services_label.set_halign(Gtk.Align.START)
|
||||
location_vbox.pack_start(services_label, False, False, 0)
|
||||
|
||||
for host in location.hosts:
|
||||
self.create_host_item(host, location_vbox)
|
||||
|
||||
def create_host_item(self, host, parent_box):
|
||||
host_frame = Gtk.Frame()
|
||||
host_frame.get_style_context().add_class("host-item")
|
||||
parent_box.pack_start(host_frame, False, False, 0)
|
||||
|
||||
host_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||
host_frame.add(host_box)
|
||||
|
||||
# Icon
|
||||
type_icons = {
|
||||
"SSH": "💻",
|
||||
"Web": "🌐",
|
||||
"SMB": "📂",
|
||||
"PostgreSQL": "🗃️",
|
||||
"Redis": "🗂️"
|
||||
}
|
||||
icon = type_icons.get(host.type, "📡")
|
||||
|
||||
icon_label = Gtk.Label(label=icon)
|
||||
host_box.pack_start(icon_label, False, False, 0)
|
||||
|
||||
# Host details
|
||||
details_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=1)
|
||||
host_box.pack_start(details_vbox, True, True, 0)
|
||||
|
||||
name_label = Gtk.Label(label=host.name)
|
||||
name_label.get_style_context().add_class("host-name")
|
||||
name_label.set_halign(Gtk.Align.START)
|
||||
details_vbox.pack_start(name_label, False, False, 0)
|
||||
|
||||
addr_label = Gtk.Label(label=host.address)
|
||||
addr_label.get_style_context().add_class("host-address")
|
||||
addr_label.set_halign(Gtk.Align.START)
|
||||
details_vbox.pack_start(addr_label, False, False, 0)
|
||||
|
||||
# Launch button for SSH and Web services
|
||||
if host.type in ["SSH", "Web"]:
|
||||
launch_btn = Gtk.Button(label="Launch")
|
||||
launch_btn.get_style_context().add_class("launch-button")
|
||||
launch_btn.connect("clicked", lambda btn, h=host: self.open_service(h))
|
||||
host_box.pack_end(launch_btn, False, False, 0)
|
||||
|
||||
def create_customer_without_active_locations(self, customer):
|
||||
# Customer card
|
||||
customer_frame = Gtk.Frame()
|
||||
customer_frame.get_style_context().add_class("customer-card")
|
||||
self.inactive_box.pack_start(customer_frame, False, False, 0)
|
||||
|
||||
customer_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
|
||||
customer_frame.add(customer_vbox)
|
||||
|
||||
# Customer header
|
||||
customer_label = Gtk.Label(label=f"🏢 {customer.name}")
|
||||
customer_label.get_style_context().add_class("inactive-customer-name")
|
||||
customer_label.set_halign(Gtk.Align.START)
|
||||
customer_vbox.pack_start(customer_label, False, False, 0)
|
||||
|
||||
# Render each location
|
||||
for location in customer.locations:
|
||||
self.create_inactive_location_card(location, customer_vbox, customer.name)
|
||||
|
||||
def create_inactive_location_card(self, location, parent_box, customer_name):
|
||||
# Location card
|
||||
location_frame = Gtk.Frame()
|
||||
location_frame.get_style_context().add_class("location-card")
|
||||
parent_box.pack_start(location_frame, False, False, 0)
|
||||
|
||||
location_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||
location_frame.add(location_hbox)
|
||||
|
||||
# Location info
|
||||
info_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
||||
location_hbox.pack_start(info_vbox, True, True, 0)
|
||||
|
||||
location_label = Gtk.Label(label=f"📍 {location.name}")
|
||||
location_label.get_style_context().add_class("location-name")
|
||||
location_label.set_halign(Gtk.Align.START)
|
||||
info_vbox.pack_start(location_label, False, False, 0)
|
||||
|
||||
# VPN type
|
||||
vpn_icons = {
|
||||
"OpenVPN": "🔒",
|
||||
"WireGuard": "⚡",
|
||||
"IPSec": "🛡️"
|
||||
}
|
||||
vpn_icon = vpn_icons.get(location.vpn_type, "🔑")
|
||||
|
||||
type_label = Gtk.Label(label=f"{vpn_icon} {location.vpn_type} VPN")
|
||||
type_label.get_style_context().add_class("vpn-type")
|
||||
type_label.set_halign(Gtk.Align.START)
|
||||
info_vbox.pack_start(type_label, False, False, 0)
|
||||
|
||||
# Service count
|
||||
service_count = len(location.hosts)
|
||||
count_label = Gtk.Label(label=f"📊 {service_count} services available")
|
||||
count_label.get_style_context().add_class("service-count")
|
||||
count_label.set_halign(Gtk.Align.START)
|
||||
info_vbox.pack_start(count_label, False, False, 0)
|
||||
|
||||
# Activate button
|
||||
activate_btn = Gtk.Button(label="Set Active")
|
||||
activate_btn.get_style_context().add_class("activate-button")
|
||||
activate_btn.connect("clicked", lambda btn, l=location: self.set_location_active(l, customer_name))
|
||||
location_hbox.pack_end(activate_btn, False, False, 0)
|
||||
|
||||
def set_location_active(self, location, customer_name):
|
||||
for customer in self.customers:
|
||||
if customer.name == customer_name:
|
||||
@@ -590,6 +234,7 @@ class VPNManagerWindow:
|
||||
target_location = customer.get_location_by_name(location.name)
|
||||
if target_location:
|
||||
target_location.active = False
|
||||
target_location.connected = False # Disconnect when deactivating
|
||||
print(f"Mock: Deactivating {customer.name} - {target_location.name}")
|
||||
break
|
||||
self.render_customers()
|
||||
@@ -599,24 +244,50 @@ class VPNManagerWindow:
|
||||
if search_term:
|
||||
self.filtered_customers = []
|
||||
for customer in self.customers:
|
||||
# Check if search term matches customer name
|
||||
if search_term in customer.name.lower():
|
||||
self.filtered_customers.append(customer)
|
||||
else:
|
||||
matching_locations = []
|
||||
for location in customer.locations:
|
||||
if (search_term in location.name.lower() or
|
||||
search_term in location.vpn_type.lower() or
|
||||
any(search_term in h.name.lower() or
|
||||
search_term in h.address.lower()
|
||||
for h in location.hosts)):
|
||||
matching_locations.append(location)
|
||||
continue
|
||||
|
||||
# Check customer services
|
||||
if any(search_term in service.name.lower() or
|
||||
search_term in service.url.lower() or
|
||||
search_term in service.service_type.lower()
|
||||
for service in customer.services):
|
||||
self.filtered_customers.append(customer)
|
||||
continue
|
||||
|
||||
# Check locations and their hosts
|
||||
for location in customer.locations:
|
||||
# Check location name
|
||||
if search_term in location.name.lower():
|
||||
self.filtered_customers.append(customer)
|
||||
break
|
||||
|
||||
if matching_locations:
|
||||
filtered_customer = Customer(
|
||||
name=customer.name,
|
||||
locations=matching_locations
|
||||
)
|
||||
self.filtered_customers.append(filtered_customer)
|
||||
# Check hosts and their services in this location
|
||||
def search_hosts(hosts):
|
||||
for host in hosts:
|
||||
if (search_term in host.name.lower() or
|
||||
search_term in host.ip_address.lower() or
|
||||
search_term in host.host_type.value.lower() or
|
||||
search_term in host.description.lower()):
|
||||
return True
|
||||
|
||||
# Check host services
|
||||
if any(search_term in service.name.lower() or
|
||||
search_term in str(service.port).lower() or
|
||||
search_term in service.service_type.value.lower()
|
||||
for service in host.services):
|
||||
return True
|
||||
|
||||
# Check sub-hosts recursively
|
||||
if search_hosts(host.sub_hosts):
|
||||
return True
|
||||
return False
|
||||
|
||||
if search_hosts(location.hosts):
|
||||
self.filtered_customers.append(customer)
|
||||
break
|
||||
else:
|
||||
self.filtered_customers = self.customers.copy()
|
||||
|
||||
@@ -625,24 +296,42 @@ class VPNManagerWindow:
|
||||
def toggle_connection(self, location):
|
||||
location.connected = not location.connected
|
||||
status = "connected to" if location.connected else "disconnected from"
|
||||
print(f"Mock: {status} - {location.name} via {location.vpn_type}")
|
||||
print(f"Mock: {status} {location.name} via {location.vpn_type.value}")
|
||||
self.render_customers()
|
||||
|
||||
def set_route(self, location):
|
||||
print(f"Mock: Setting route for {location.name}")
|
||||
def open_service(self, service):
|
||||
# Get the host IP from context - this would need to be passed properly in a real implementation
|
||||
print(f"Mock: Opening {service.service_type.value} service: {service.name} on port {service.port}")
|
||||
|
||||
def open_service(self, host):
|
||||
print(f"Mock: Opening {host.type} service: {host.name} at {host.address}")
|
||||
def open_customer_service(self, customer_service):
|
||||
print(f"Mock: Opening customer service: {customer_service.name} at {customer_service.url}")
|
||||
|
||||
def show_window_from_tray(self, widget=None):
|
||||
def show_window_from_tray(self, _icon=None, _item=None):
|
||||
# Use GLib.idle_add to safely call GTK functions from the tray thread
|
||||
GLib.idle_add(self._show_window_safe)
|
||||
|
||||
def _show_window_safe(self):
|
||||
"""Safely show window in main GTK thread"""
|
||||
self.window.deiconify()
|
||||
self.window.present()
|
||||
self.window.show_all()
|
||||
return False # Don't repeat the idle call
|
||||
|
||||
def hide_window(self, widget, event):
|
||||
self.window.hide()
|
||||
return True
|
||||
def on_window_state_event(self, _widget, event):
|
||||
"""Handle window state changes - hide to tray when minimized"""
|
||||
if event.new_window_state & Gdk.WindowState.ICONIFIED:
|
||||
self.window.hide()
|
||||
return False
|
||||
|
||||
def quit_app(self, widget=None):
|
||||
def quit_app_from_close(self, _widget=None, _event=None):
|
||||
"""Quit app when close button is pressed"""
|
||||
self.quit_app()
|
||||
return False
|
||||
|
||||
def quit_app(self, _widget=None):
|
||||
# Stop the tray icon
|
||||
if hasattr(self, 'tray_icon'):
|
||||
self.tray_icon.stop()
|
||||
Gtk.main_quit()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user