more stuff
This commit is contained in:
277
main.py
277
main.py
@@ -1,38 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
from views import ActiveView, InactiveView
|
||||
from data_loader import load_customers
|
||||
from models import Customer
|
||||
from PIL import Image, ImageDraw
|
||||
import pystray
|
||||
import threading
|
||||
import sys
|
||||
from gi.repository import Gtk, Gdk, GLib
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gdk, GLib
|
||||
import sys
|
||||
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:
|
||||
def __init__(self):
|
||||
self.customers = load_customers()
|
||||
self.filtered_customers = self.customers.copy()
|
||||
|
||||
self.current_location = None # Track user's current location
|
||||
|
||||
# Create main window
|
||||
self.window = Gtk.Window()
|
||||
self.window.set_title("VPN Manager")
|
||||
self.window.set_default_size(1200, 750)
|
||||
self.window.connect("delete-event", self.quit_app_from_close)
|
||||
self.window.connect("window-state-event", self.on_window_state_event)
|
||||
|
||||
|
||||
# Set up minimal CSS for GNOME-style cards
|
||||
self.setup_css()
|
||||
|
||||
|
||||
# Create UI
|
||||
self.setup_ui()
|
||||
self.setup_system_tray()
|
||||
|
||||
|
||||
# Start hidden
|
||||
self.window.hide()
|
||||
|
||||
|
||||
def setup_css(self):
|
||||
"""Minimal CSS for GNOME-style cards"""
|
||||
css_provider = Gtk.CssProvider()
|
||||
@@ -47,15 +48,14 @@ class VPNManagerWindow:
|
||||
}
|
||||
"""
|
||||
css_provider.load_from_data(css.encode())
|
||||
|
||||
|
||||
# Apply CSS to default screen
|
||||
screen = Gdk.Screen.get_default()
|
||||
style_context = Gtk.StyleContext()
|
||||
style_context.add_provider_for_screen(
|
||||
screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||
)
|
||||
|
||||
|
||||
|
||||
def setup_ui(self):
|
||||
# Use HeaderBar for native GNOME look
|
||||
header_bar = Gtk.HeaderBar()
|
||||
@@ -63,7 +63,7 @@ class VPNManagerWindow:
|
||||
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)
|
||||
@@ -71,58 +71,41 @@ class VPNManagerWindow:
|
||||
main_vbox.set_margin_top(12)
|
||||
main_vbox.set_margin_bottom(12)
|
||||
self.window.add(main_vbox)
|
||||
|
||||
|
||||
# Current location display
|
||||
self.current_location_label = Gtk.Label()
|
||||
self.current_location_label.set_markup("<i>Current location: Not set</i>")
|
||||
self.current_location_label.set_halign(Gtk.Align.CENTER)
|
||||
self.current_location_label.set_margin_bottom(8)
|
||||
main_vbox.pack_start(self.current_location_label, False, False, 0)
|
||||
|
||||
# Search bar with SearchEntry
|
||||
self.search_entry = Gtk.SearchEntry()
|
||||
self.search_entry.set_placeholder_text("Search customers, locations, or hosts...")
|
||||
self.search_entry.set_placeholder_text(
|
||||
"Search customers, locations, or hosts... (* for all)")
|
||||
self.search_entry.connect("search-changed", self.filter_customers)
|
||||
main_vbox.pack_start(self.search_entry, False, False, 0)
|
||||
|
||||
# Create a stack to switch between views
|
||||
self.view_stack = Gtk.Stack()
|
||||
self.view_stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
|
||||
self.view_stack.set_transition_duration(200)
|
||||
main_vbox.pack_start(self.view_stack, True, True, 0)
|
||||
|
||||
# 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)
|
||||
# Get callbacks for views
|
||||
callbacks = self.get_callbacks()
|
||||
|
||||
# Left column - Active customers
|
||||
left_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
columns_box.pack_start(left_vbox, True, True, 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)
|
||||
|
||||
# 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=12)
|
||||
active_scrolled.add(self.active_box)
|
||||
|
||||
# Right column - Inactive customers
|
||||
right_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
columns_box.pack_start(right_vbox, True, True, 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)
|
||||
|
||||
# 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=12)
|
||||
inactive_scrolled.add(self.inactive_box)
|
||||
# Create active view (shown by default)
|
||||
self.active_view = ActiveView(callbacks)
|
||||
self.view_stack.add_named(self.active_view.widget, "active")
|
||||
|
||||
# Create inactive view (shown when searching)
|
||||
self.inactive_view = InactiveView(callbacks)
|
||||
self.view_stack.add_named(self.inactive_view.widget, "inactive")
|
||||
|
||||
# Render initial data
|
||||
self.render_customers()
|
||||
|
||||
|
||||
def setup_system_tray(self):
|
||||
# Create a simple icon for the system tray
|
||||
def create_icon():
|
||||
@@ -130,7 +113,7 @@ class VPNManagerWindow:
|
||||
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)
|
||||
@@ -141,7 +124,7 @@ class VPNManagerWindow:
|
||||
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
|
||||
@@ -150,84 +133,85 @@ class VPNManagerWindow:
|
||||
create_icon(),
|
||||
"VPN Manager - Double-click to open"
|
||||
)
|
||||
|
||||
|
||||
# Set direct click action
|
||||
self.tray_icon.default_action = self.show_window_from_tray
|
||||
|
||||
|
||||
# Also provide a right-click menu
|
||||
menu = pystray.Menu(
|
||||
pystray.MenuItem("Open VPN Manager", self.show_window_from_tray, default=True),
|
||||
pystray.MenuItem("Open VPN Manager",
|
||||
self.show_window_from_tray, default=True),
|
||||
pystray.MenuItem("Quit", self.quit_app)
|
||||
)
|
||||
self.tray_icon.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,
|
||||
'set_current_location': self.set_current_location,
|
||||
'open_service': self.open_service,
|
||||
'open_customer_service': self.open_customer_service
|
||||
}
|
||||
|
||||
|
||||
def render_customers(self):
|
||||
# Clear existing content
|
||||
for child in self.active_box.get_children():
|
||||
child.destroy()
|
||||
for child in self.inactive_box.get_children():
|
||||
child.destroy()
|
||||
|
||||
# Check if we're in search mode
|
||||
search_term = self.search_entry.get_text().strip()
|
||||
is_searching = bool(search_term)
|
||||
|
||||
# Separate customers with active and inactive locations
|
||||
customers_with_active = []
|
||||
customers_with_inactive = []
|
||||
|
||||
|
||||
for customer in self.filtered_customers:
|
||||
active_locations = customer.get_active_locations()
|
||||
inactive_locations = customer.get_inactive_locations()
|
||||
|
||||
|
||||
# Prepare active locations (shown when not searching)
|
||||
if 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)
|
||||
|
||||
|
||||
# Prepare inactive locations (shown when searching)
|
||||
if inactive_locations:
|
||||
from models import Customer
|
||||
customer_data = Customer(name=customer.name)
|
||||
customer_data.services = customer.services
|
||||
customer_data.services = customer.services
|
||||
customer_data.locations = inactive_locations
|
||||
customers_with_inactive.append(customer_data)
|
||||
|
||||
# Get callbacks for widgets
|
||||
callbacks = self.get_callbacks()
|
||||
|
||||
# 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:
|
||||
customer_card = InactiveCustomerCard(customer, callbacks)
|
||||
self.inactive_box.pack_start(customer_card.widget, False, False, 0)
|
||||
|
||||
|
||||
# Update views based on mode
|
||||
if is_searching:
|
||||
# Search mode: Switch to inactive view and update it
|
||||
self.view_stack.set_visible_child_name("inactive")
|
||||
self.inactive_view.update(customers_with_inactive, search_term)
|
||||
else:
|
||||
# Normal mode: Switch to active view and update it
|
||||
self.view_stack.set_visible_child_name("active")
|
||||
self.active_view.update(customers_with_active)
|
||||
|
||||
self.window.show_all()
|
||||
|
||||
|
||||
def set_location_active(self, location, customer_name):
|
||||
for customer in self.customers:
|
||||
if customer.name == customer_name:
|
||||
target_location = customer.get_location_by_name(location.name)
|
||||
if target_location:
|
||||
target_location.active = True
|
||||
print(f"Mock: Setting {customer.name} - {target_location.name} as active")
|
||||
print(
|
||||
f"Mock: Setting {customer.name} - {target_location.name} as active")
|
||||
break
|
||||
|
||||
# Clear search and return to active view
|
||||
self.search_entry.set_text("")
|
||||
self.render_customers()
|
||||
|
||||
|
||||
def deactivate_location(self, location, customer_name):
|
||||
for customer in self.customers:
|
||||
if customer.name == customer_name:
|
||||
@@ -235,106 +219,137 @@ class VPNManagerWindow:
|
||||
if target_location:
|
||||
target_location.active = False
|
||||
target_location.connected = False # Disconnect when deactivating
|
||||
print(f"Mock: Deactivating {customer.name} - {target_location.name}")
|
||||
print(
|
||||
f"Mock: Deactivating {customer.name} - {target_location.name}")
|
||||
break
|
||||
self.render_customers()
|
||||
|
||||
def set_current_location(self, location, customer_name):
|
||||
"""Set the user's current location."""
|
||||
for customer in self.customers:
|
||||
if customer.name == customer_name:
|
||||
target_location = customer.get_location_by_name(location.name)
|
||||
if target_location:
|
||||
self.current_location = (customer.name, target_location.name)
|
||||
print(f"Current location set to: {customer.name} - {target_location.name}")
|
||||
self.update_current_location_display()
|
||||
break
|
||||
|
||||
def update_current_location_display(self):
|
||||
"""Update the current location display label."""
|
||||
if self.current_location:
|
||||
customer_name, location_name = self.current_location
|
||||
self.current_location_label.set_markup(
|
||||
f"<i>📍 Current location: <b>{customer_name} - {location_name}</b></i>"
|
||||
)
|
||||
else:
|
||||
self.current_location_label.set_markup("<i>Current location: Not set</i>")
|
||||
|
||||
def filter_customers(self, entry):
|
||||
search_term = entry.get_text().lower()
|
||||
if search_term:
|
||||
search_term = entry.get_text().strip()
|
||||
|
||||
# Check for wildcard - show all customers
|
||||
if search_term == "*":
|
||||
self.filtered_customers = self.customers.copy()
|
||||
elif search_term:
|
||||
# Normal search logic
|
||||
search_term_lower = search_term.lower()
|
||||
self.filtered_customers = []
|
||||
for customer in self.customers:
|
||||
# Check if search term matches customer name
|
||||
if search_term in customer.name.lower():
|
||||
if search_term_lower in customer.name.lower():
|
||||
self.filtered_customers.append(customer)
|
||||
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()
|
||||
if any(search_term_lower in service.name.lower() or
|
||||
search_term_lower in service.url.lower() or
|
||||
search_term_lower 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():
|
||||
if search_term_lower in location.name.lower():
|
||||
self.filtered_customers.append(customer)
|
||||
break
|
||||
|
||||
|
||||
# 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()):
|
||||
if (search_term_lower in host.name.lower() or
|
||||
search_term_lower in host.ip_address.lower() or
|
||||
search_term_lower in host.host_type.value.lower() or
|
||||
search_term_lower 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()
|
||||
if any(search_term_lower in service.name.lower() or
|
||||
search_term_lower in str(service.port).lower() or
|
||||
search_term_lower 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:
|
||||
# Empty search - show all customers
|
||||
self.filtered_customers = self.customers.copy()
|
||||
|
||||
|
||||
self.render_customers()
|
||||
|
||||
|
||||
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.value}")
|
||||
self.render_customers()
|
||||
|
||||
|
||||
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}")
|
||||
|
||||
print(
|
||||
f"Mock: Opening {service.service_type.value} service: {service.name} on port {service.port}")
|
||||
|
||||
def open_customer_service(self, customer_service):
|
||||
print(f"Mock: Opening customer service: {customer_service.name} at {customer_service.url}")
|
||||
|
||||
print(
|
||||
f"Mock: Opening customer service: {customer_service.name} at {customer_service.url}")
|
||||
|
||||
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 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_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)
|
||||
|
||||
|
||||
def run(self):
|
||||
self.window.show_all()
|
||||
Gtk.main()
|
||||
@@ -346,4 +361,4 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user