mirror of
https://github.com/ApfelTeeSaft/SplitNotes.git
synced 2026-08-26 19:33:39 +00:00
1195 lines
34 KiB
Python
1195 lines
34 KiB
Python
import tkinter
|
|
from tkinter import messagebox, ttk
|
|
import json
|
|
import socket
|
|
import threading
|
|
import time
|
|
import os
|
|
import sys
|
|
import platform
|
|
|
|
import config
|
|
import ls_connection as con
|
|
import note_reader as noter
|
|
import setting_handler
|
|
|
|
# Enhanced runtime info with bridge server capabilities
|
|
runtime_info = {
|
|
"ls_connected": False,
|
|
"timer_running": False,
|
|
"active_split": -1,
|
|
"notes": [],
|
|
"server_port": 0,
|
|
"force_reset": False,
|
|
"double_layout": False,
|
|
"settings": {},
|
|
# Bridge server specific (TCP-based, no websockets)
|
|
"bridge_enabled": False,
|
|
"bridge_port": 16835,
|
|
"bridge_server": None,
|
|
"bridge_state": {}
|
|
}
|
|
|
|
root = tkinter.Tk()
|
|
|
|
# Cross-platform path handling
|
|
if getattr(sys, 'frozen', False):
|
|
application_path = os.path.dirname(sys.executable)
|
|
else:
|
|
application_path = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
red_path = os.path.join(application_path, config.RESOURCE_FOLDER, config.ICONS["RED"])
|
|
green_path = os.path.join(application_path, config.RESOURCE_FOLDER, config.ICONS["GREEN"])
|
|
|
|
# Initialize icons
|
|
red_icon = None
|
|
green_icon = None
|
|
|
|
try:
|
|
if os.path.exists(red_path):
|
|
red_icon = tkinter.PhotoImage(file=red_path)
|
|
if os.path.exists(green_path):
|
|
green_icon = tkinter.PhotoImage(file=green_path)
|
|
except Exception as e:
|
|
print(f"Warning: Could not load icons: {e}")
|
|
|
|
|
|
class BridgeServer:
|
|
"""TCP-based bridge server for browser extensions (replaces websockets)"""
|
|
|
|
def __init__(self, port=16835):
|
|
self.port = port
|
|
self.host = 'localhost'
|
|
self.running = False
|
|
self.server_socket = None
|
|
self.clients = []
|
|
self.state_lock = threading.Lock()
|
|
self.server_thread = None
|
|
|
|
def start(self):
|
|
"""Start the TCP bridge server"""
|
|
if self.running:
|
|
return True
|
|
|
|
try:
|
|
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
self.server_socket.settimeout(1.0) # Add timeout for clean shutdown
|
|
self.server_socket.bind((self.host, self.port))
|
|
self.server_socket.listen(5)
|
|
|
|
self.running = True
|
|
|
|
# Start server thread
|
|
self.server_thread = threading.Thread(target=self._server_loop, daemon=True)
|
|
self.server_thread.start()
|
|
|
|
print(f"Bridge server started on {self.host}:{self.port}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"Failed to start bridge server: {e}")
|
|
self.running = False
|
|
if self.server_socket:
|
|
try:
|
|
self.server_socket.close()
|
|
except:
|
|
pass
|
|
self.server_socket = None
|
|
return False
|
|
|
|
def stop(self):
|
|
"""Stop the bridge server"""
|
|
self.running = False
|
|
|
|
# Close all client connections
|
|
for client in self.clients[:]: # Copy list to avoid modification during iteration
|
|
try:
|
|
client.close()
|
|
except:
|
|
pass
|
|
self.clients.clear()
|
|
|
|
# Close server socket
|
|
if self.server_socket:
|
|
try:
|
|
self.server_socket.close()
|
|
except:
|
|
pass
|
|
self.server_socket = None
|
|
|
|
# Wait for server thread to finish
|
|
if self.server_thread and self.server_thread.is_alive():
|
|
self.server_thread.join(timeout=2.0)
|
|
|
|
print("Bridge server stopped")
|
|
|
|
def _server_loop(self):
|
|
"""Main server loop for accepting connections"""
|
|
while self.running:
|
|
try:
|
|
client, address = self.server_socket.accept()
|
|
print(f"Browser client connected from {address}")
|
|
|
|
# Set client timeout
|
|
client.settimeout(30.0)
|
|
|
|
self.clients.append(client)
|
|
client_thread = threading.Thread(
|
|
target=self._handle_client,
|
|
args=(client, address),
|
|
daemon=True
|
|
)
|
|
client_thread.start()
|
|
|
|
except socket.timeout:
|
|
continue # Normal timeout, check if still running
|
|
except Exception as e:
|
|
if self.running:
|
|
print(f"Error accepting connection: {e}")
|
|
break
|
|
|
|
def _handle_client(self, client, address):
|
|
"""Handle individual client connections"""
|
|
try:
|
|
while self.running:
|
|
try:
|
|
data = client.recv(1024)
|
|
if not data:
|
|
break
|
|
|
|
try:
|
|
# Parse JSON message from browser extension
|
|
message = json.loads(data.decode('utf-8'))
|
|
self._process_browser_message(message)
|
|
|
|
# Send acknowledgment
|
|
response = {"status": "ok", "timestamp": time.time()}
|
|
client.send((json.dumps(response) + '\n').encode('utf-8'))
|
|
|
|
except json.JSONDecodeError:
|
|
# Handle plain text commands if needed
|
|
command = data.decode('utf-8').strip()
|
|
print(f"Received plain text command: {command}")
|
|
|
|
except socket.timeout:
|
|
continue
|
|
except Exception as e:
|
|
print(f"Error receiving from client {address}: {e}")
|
|
break
|
|
|
|
except Exception as e:
|
|
print(f"Error handling client {address}: {e}")
|
|
finally:
|
|
try:
|
|
client.close()
|
|
except:
|
|
pass
|
|
if client in self.clients:
|
|
self.clients.remove(client)
|
|
print(f"Browser client {address} disconnected")
|
|
|
|
def _process_browser_message(self, message):
|
|
"""Process JSON messages from browser extensions"""
|
|
with self.state_lock:
|
|
try:
|
|
if message.get('type') == 'timer_state':
|
|
# Update runtime info with browser state
|
|
old_split = runtime_info["active_split"]
|
|
old_running = runtime_info["timer_running"]
|
|
|
|
runtime_info["timer_running"] = message.get('running', False)
|
|
runtime_info["active_split"] = message.get('currentSplit', -1)
|
|
|
|
# Log changes for debugging
|
|
if old_split != runtime_info["active_split"]:
|
|
print(f"Browser sync: Split changed {old_split} -> {runtime_info['active_split']}")
|
|
|
|
if old_running != runtime_info["timer_running"]:
|
|
print(f"Browser sync: Timer {'started' if runtime_info['timer_running'] else 'stopped'}")
|
|
|
|
# Store bridge state
|
|
runtime_info["bridge_state"] = {
|
|
'timestamp': time.time(),
|
|
'currentSplit': runtime_info["active_split"],
|
|
'timerRunning': runtime_info["timer_running"],
|
|
'splitName': message.get('splitName', ''),
|
|
'source': 'browser'
|
|
}
|
|
|
|
elif message.get('type') == 'splits_updated':
|
|
splits = message.get('splits', [])
|
|
print(f"Browser sync: Received {len(splits)} split names")
|
|
if 'bridge_state' not in runtime_info:
|
|
runtime_info["bridge_state"] = {}
|
|
runtime_info["bridge_state"]['splits'] = splits
|
|
|
|
except Exception as e:
|
|
print(f"Error processing browser message: {e}")
|
|
|
|
def send_state_to_browsers(self, state):
|
|
"""Send current state to all connected browser clients"""
|
|
if not self.clients:
|
|
return
|
|
|
|
message = json.dumps(state) + '\n'
|
|
disconnected_clients = []
|
|
|
|
for client in self.clients[:]: # Copy list to avoid modification during iteration
|
|
try:
|
|
client.send(message.encode('utf-8'))
|
|
except:
|
|
disconnected_clients.append(client)
|
|
|
|
# Remove disconnected clients
|
|
for client in disconnected_clients:
|
|
if client in self.clients:
|
|
self.clients.remove(client)
|
|
|
|
def get_status(self):
|
|
"""Get bridge server status"""
|
|
return {
|
|
'running': self.running,
|
|
'port': self.port,
|
|
'clients': len(self.clients),
|
|
'last_state': runtime_info.get("bridge_state", {})
|
|
}
|
|
|
|
|
|
def menu_open_bridge_settings(root_wnd):
|
|
"""Open bridge server settings dialog with proper save button functionality"""
|
|
settings_wnd = tkinter.Toplevel(master=root_wnd)
|
|
settings_wnd.title("TCP Bridge Server Settings")
|
|
settings_wnd.geometry("520x500")
|
|
settings_wnd.resizable(False, False)
|
|
settings_wnd.transient(root_wnd)
|
|
settings_wnd.grab_set()
|
|
|
|
# Center window
|
|
settings_wnd.update_idletasks()
|
|
x = (settings_wnd.winfo_screenwidth() // 2) - (520 // 2)
|
|
y = (settings_wnd.winfo_screenheight() // 2) - (500 // 2)
|
|
settings_wnd.geometry(f"+{x}+{y}")
|
|
|
|
# Main container
|
|
main_frame = tkinter.Frame(settings_wnd)
|
|
main_frame.pack(fill='both', expand=True, padx=10, pady=10)
|
|
|
|
# Settings section
|
|
settings_section = tkinter.LabelFrame(main_frame, text="Bridge Configuration", font=config.GUI_FONT)
|
|
settings_section.pack(fill='x', pady=(0, 10))
|
|
|
|
# Load current settings from config file
|
|
current_settings = setting_handler.load_settings()
|
|
current_enabled = current_settings.get("bridge_enabled", "false").lower() == "true"
|
|
current_port = int(current_settings.get("bridge_port", "16835"))
|
|
|
|
# Enable bridge checkbox
|
|
bridge_enabled_var = tkinter.BooleanVar(value=current_enabled)
|
|
bridge_enabled_cb = tkinter.Checkbutton(
|
|
settings_section,
|
|
text="Enable TCP Bridge Server for Browser Extensions",
|
|
variable=bridge_enabled_var,
|
|
font=config.GUI_FONT
|
|
)
|
|
bridge_enabled_cb.pack(anchor='w', padx=15, pady=10)
|
|
|
|
# Port setting frame
|
|
port_frame = tkinter.Frame(settings_section)
|
|
port_frame.pack(fill='x', padx=15, pady=5)
|
|
|
|
tkinter.Label(port_frame, text="Bridge Server Port:", font=config.GUI_FONT).pack(side='left')
|
|
|
|
port_var = tkinter.StringVar(value=str(current_port))
|
|
port_entry = tkinter.Entry(port_frame, textvariable=port_var, font=config.GUI_FONT, width=8)
|
|
port_entry.pack(side='left', padx=(10, 5))
|
|
|
|
tkinter.Label(
|
|
port_frame,
|
|
text="(default: 16835)",
|
|
font=('Arial', 9),
|
|
fg='gray'
|
|
).pack(side='left')
|
|
|
|
# Help text
|
|
help_label = tkinter.Label(
|
|
settings_section,
|
|
text="Browser extensions connect to this TCP port to sync with LiveSplit One",
|
|
font=('Arial', 9),
|
|
fg='gray',
|
|
wraplength=450
|
|
)
|
|
help_label.pack(anchor='w', padx=15, pady=(0, 10))
|
|
|
|
# Save button and feedback area
|
|
save_frame = tkinter.Frame(settings_section)
|
|
save_frame.pack(fill='x', padx=15, pady=10)
|
|
|
|
# Feedback text area
|
|
feedback_var = tkinter.StringVar(value="")
|
|
feedback_label = tkinter.Label(
|
|
save_frame,
|
|
textvariable=feedback_var,
|
|
font=('Arial', 10),
|
|
fg='blue',
|
|
wraplength=300,
|
|
justify='left'
|
|
)
|
|
feedback_label.pack(side='left', fill='x', expand=True)
|
|
|
|
def save_bridge_settings():
|
|
"""Save bridge settings and start/stop server based on checkbox"""
|
|
try:
|
|
# Clear previous feedback
|
|
feedback_var.set("Saving settings...")
|
|
feedback_label.config(fg='blue')
|
|
settings_wnd.update()
|
|
|
|
# Validate port
|
|
try:
|
|
port = int(port_var.get())
|
|
if not (1024 <= port <= 65535):
|
|
raise ValueError("Port must be between 1024 and 65535")
|
|
except ValueError as e:
|
|
feedback_var.set(f"Error: {e}")
|
|
feedback_label.config(fg='red')
|
|
return
|
|
|
|
# Get checkbox state
|
|
is_enabled = bridge_enabled_var.get()
|
|
|
|
print(f"Saving bridge settings: enabled={is_enabled}, port={port}")
|
|
|
|
# Stop existing server if running
|
|
if runtime_info.get("bridge_server"):
|
|
print("Stopping existing bridge server...")
|
|
runtime_info["bridge_server"].stop()
|
|
runtime_info["bridge_server"] = None
|
|
|
|
# Update runtime settings
|
|
runtime_info["bridge_enabled"] = is_enabled
|
|
runtime_info["bridge_port"] = port
|
|
|
|
# Save to configuration file
|
|
settings = setting_handler.load_settings()
|
|
settings["bridge_enabled"] = "true" if is_enabled else "false"
|
|
settings["bridge_port"] = str(port)
|
|
setting_handler.save_settings(settings)
|
|
|
|
print(f"Settings saved to config file: bridge_enabled={settings['bridge_enabled']}")
|
|
|
|
# Start or stop server based on checkbox state
|
|
if is_enabled:
|
|
print(f"Starting TCP bridge server on port {port}...")
|
|
bridge_server = BridgeServer(port)
|
|
if bridge_server.start():
|
|
runtime_info["bridge_server"] = bridge_server
|
|
feedback_var.set(f"✓ Settings saved! TCP Bridge server started on port {port}")
|
|
feedback_label.config(fg='green')
|
|
print("✓ Bridge server started successfully")
|
|
else:
|
|
# Failed to start server, disable the setting
|
|
runtime_info["bridge_enabled"] = False
|
|
settings["bridge_enabled"] = "false"
|
|
setting_handler.save_settings(settings)
|
|
bridge_enabled_var.set(False) # Update checkbox
|
|
|
|
feedback_var.set(f"✗ Failed to start server on port {port}. Settings disabled.")
|
|
feedback_label.config(fg='red')
|
|
print("✗ Failed to start bridge server")
|
|
else:
|
|
feedback_var.set("✓ Settings saved! TCP Bridge server disabled")
|
|
feedback_label.config(fg='orange')
|
|
print("✓ Bridge server disabled")
|
|
|
|
# Update main window title to reflect bridge status
|
|
try:
|
|
update_title(config.DEFAULT_WINDOW["TITLE"], root_wnd)
|
|
except:
|
|
pass
|
|
|
|
except Exception as e:
|
|
feedback_var.set(f"✗ Error saving settings: {e}")
|
|
feedback_label.config(fg='red')
|
|
print(f"Error in save_bridge_settings: {e}")
|
|
|
|
# Save button
|
|
save_button = tkinter.Button(
|
|
save_frame,
|
|
text="Save Settings",
|
|
command=save_bridge_settings,
|
|
font=config.GUI_FONT,
|
|
width=12,
|
|
bg='lightblue',
|
|
relief='raised'
|
|
)
|
|
save_button.pack(side='right', padx=(10, 0))
|
|
|
|
# Status section
|
|
status_frame = tkinter.LabelFrame(main_frame, text="Server Status", font=config.GUI_FONT)
|
|
status_frame.pack(fill='both', expand=True, pady=(0, 10))
|
|
|
|
# Status text area with scrollbar
|
|
status_text_frame = tkinter.Frame(status_frame)
|
|
status_text_frame.pack(fill='both', expand=True, padx=10, pady=10)
|
|
|
|
status_text = tkinter.Text(
|
|
status_text_frame,
|
|
height=8,
|
|
width=60,
|
|
font=('Courier', 9),
|
|
state='disabled',
|
|
wrap='word'
|
|
)
|
|
status_scrollbar = tkinter.Scrollbar(status_text_frame, command=status_text.yview)
|
|
status_text.config(yscrollcommand=status_scrollbar.set)
|
|
|
|
status_text.pack(side='left', fill='both', expand=True)
|
|
status_scrollbar.pack(side='right', fill='y')
|
|
|
|
def update_status():
|
|
"""Update the status display"""
|
|
status_text.config(state='normal')
|
|
status_text.delete(1.0, tkinter.END)
|
|
|
|
# Get current runtime status
|
|
bridge_server = runtime_info.get("bridge_server")
|
|
bridge_enabled = runtime_info.get("bridge_enabled", False)
|
|
bridge_port = runtime_info.get("bridge_port", 16835)
|
|
|
|
if bridge_server and bridge_enabled:
|
|
status = bridge_server.get_status()
|
|
last_state = status.get('last_state', {})
|
|
|
|
status_info = f"""TCP Bridge Server Status: RUNNING ✓
|
|
Port: {status['port']}
|
|
Connected Browsers: {status['clients']}
|
|
Server Running: {status['running']}
|
|
|
|
Runtime Information:
|
|
Bridge Enabled: {bridge_enabled}
|
|
Current Split: {last_state.get('currentSplit', 'N/A')}
|
|
Timer Running: {last_state.get('timerRunning', 'N/A')}
|
|
Last Update: {time.ctime(last_state.get('timestamp', 0)) if last_state.get('timestamp') else 'Never'}
|
|
|
|
Configuration Status:
|
|
Settings file: resources/config.cfg
|
|
bridge_enabled: {"true" if bridge_enabled else "false"}
|
|
bridge_port: {bridge_port}
|
|
|
|
Connection Info:
|
|
Browser extensions can connect to localhost:{bridge_port}
|
|
Send JSON messages with timer state data"""
|
|
else:
|
|
status_info = f"""TCP Bridge Server Status: STOPPED ✗
|
|
Port: {bridge_port}
|
|
Connected Browsers: 0
|
|
Server Running: False
|
|
|
|
Runtime Information:
|
|
Bridge Enabled: {bridge_enabled}
|
|
Current Split: N/A
|
|
Timer Running: N/A
|
|
Last Update: Never
|
|
|
|
Configuration Status:
|
|
Settings file: resources/config.cfg
|
|
bridge_enabled: {"true" if bridge_enabled else "false"}
|
|
bridge_port: {bridge_port}
|
|
|
|
To enable:
|
|
1. Check 'Enable TCP Bridge Server' checkbox above
|
|
2. Click 'Save Settings' button
|
|
3. Verify status changes to 'RUNNING ✓'"""
|
|
|
|
status_text.insert(1.0, status_info)
|
|
status_text.config(state='disabled')
|
|
|
|
# Schedule next update if window still exists
|
|
try:
|
|
if settings_wnd.winfo_exists():
|
|
settings_wnd.after(2000, update_status)
|
|
except:
|
|
pass
|
|
|
|
# Start status updates
|
|
update_status()
|
|
|
|
# Test connection button
|
|
test_frame = tkinter.Frame(main_frame)
|
|
test_frame.pack(fill='x', pady=(0, 10))
|
|
|
|
test_feedback_var = tkinter.StringVar(value="")
|
|
test_feedback_label = tkinter.Label(
|
|
test_frame,
|
|
textvariable=test_feedback_var,
|
|
font=('Arial', 9),
|
|
fg='gray'
|
|
)
|
|
test_feedback_label.pack(side='left', fill='x', expand=True)
|
|
|
|
def test_connection():
|
|
"""Test the bridge server connection"""
|
|
test_feedback_var.set("Testing connection...")
|
|
test_feedback_label.config(fg='blue')
|
|
settings_wnd.update()
|
|
|
|
try:
|
|
port = int(port_var.get())
|
|
except ValueError:
|
|
test_feedback_var.set("✗ Invalid port number")
|
|
test_feedback_label.config(fg='red')
|
|
return
|
|
|
|
try:
|
|
# Try to connect to the bridge server
|
|
test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
test_socket.settimeout(3.0)
|
|
test_socket.connect(('localhost', port))
|
|
|
|
# Send test message
|
|
test_message = {
|
|
"type": "connection_test",
|
|
"timestamp": time.time(),
|
|
"source": "settings_dialog"
|
|
}
|
|
test_socket.send((json.dumps(test_message) + '\n').encode('utf-8'))
|
|
|
|
# Wait for response
|
|
response = test_socket.recv(1024)
|
|
test_socket.close()
|
|
|
|
if response:
|
|
test_feedback_var.set("✓ Connection test successful! Server is responding.")
|
|
test_feedback_label.config(fg='green')
|
|
else:
|
|
test_feedback_var.set("✗ Connected but no response received")
|
|
test_feedback_label.config(fg='orange')
|
|
|
|
except ConnectionRefusedError:
|
|
test_feedback_var.set("✗ Connection refused. Server not running.")
|
|
test_feedback_label.config(fg='red')
|
|
except socket.timeout:
|
|
test_feedback_var.set("✗ Connection timeout. Check if server is running.")
|
|
test_feedback_label.config(fg='red')
|
|
except Exception as e:
|
|
test_feedback_var.set(f"✗ Connection test failed: {e}")
|
|
test_feedback_label.config(fg='red')
|
|
|
|
test_button = tkinter.Button(
|
|
test_frame,
|
|
text="Test Connection",
|
|
command=test_connection,
|
|
font=config.GUI_FONT,
|
|
width=15
|
|
)
|
|
test_button.pack(side='right')
|
|
|
|
# Bottom buttons frame
|
|
button_frame = tkinter.Frame(main_frame)
|
|
button_frame.pack(fill='x')
|
|
|
|
def close_dialog():
|
|
"""Close the dialog"""
|
|
settings_wnd.destroy()
|
|
|
|
# Close button
|
|
tkinter.Button(
|
|
button_frame,
|
|
text="Close",
|
|
command=close_dialog,
|
|
font=config.GUI_FONT,
|
|
width=10
|
|
).pack(side='right')
|
|
|
|
# Info button for help
|
|
def show_help():
|
|
"""Show help information"""
|
|
help_text = """TCP Bridge Server Help:
|
|
|
|
The TCP Bridge Server allows browser extensions to connect to SplitNotes and send timer state information from LiveSplit One.
|
|
|
|
SETUP:
|
|
1. Check 'Enable TCP Bridge Server' checkbox
|
|
2. Set the port (default: 16835)
|
|
3. Click 'Save Settings'
|
|
4. Verify status shows 'RUNNING ✓'
|
|
|
|
BROWSER EXTENSION:
|
|
Browser extensions should connect to localhost:16835 and send JSON messages like:
|
|
{
|
|
"type": "timer_state",
|
|
"running": true,
|
|
"currentSplit": 2,
|
|
"splitName": "Split Name"
|
|
}
|
|
|
|
TROUBLESHOOTING:
|
|
• Make sure port is not blocked by firewall
|
|
• Check that no other application is using the port
|
|
• Use 'Test Connection' to verify server is working
|
|
• Restart SplitNotes if server fails to start"""
|
|
|
|
messagebox.showinfo("TCP Bridge Help", help_text)
|
|
|
|
tkinter.Button(
|
|
button_frame,
|
|
text="Help",
|
|
command=show_help,
|
|
font=config.GUI_FONT,
|
|
width=10
|
|
).pack(side='right', padx=(0, 10))
|
|
|
|
# Handle window close
|
|
def on_closing():
|
|
settings_wnd.grab_release()
|
|
settings_wnd.destroy()
|
|
|
|
settings_wnd.protocol("WM_DELETE_WINDOW", on_closing)
|
|
|
|
# Initial feedback
|
|
if current_enabled:
|
|
if runtime_info.get("bridge_server"):
|
|
feedback_var.set(f"✓ TCP Bridge server is currently running on port {current_port}")
|
|
feedback_label.config(fg='green')
|
|
else:
|
|
feedback_var.set(f"⚠ Bridge enabled in config but server not running")
|
|
feedback_label.config(fg='orange')
|
|
else:
|
|
feedback_var.set("TCP Bridge server is currently disabled")
|
|
feedback_label.config(fg='gray')
|
|
|
|
|
|
def update(window, com_socket, text1, text2):
|
|
"""Enhanced update function with TCP bridge server support"""
|
|
if runtime_info["force_reset"]:
|
|
com_socket = reset_connection(com_socket, window, text1, text2)
|
|
runtime_info["force_reset"] = False
|
|
|
|
elif not runtime_info["ls_connected"]:
|
|
# Check if we have browser state as fallback
|
|
if runtime_info["bridge_enabled"] and runtime_info.get("bridge_state"):
|
|
bridge_state = runtime_info["bridge_state"]
|
|
if time.time() - bridge_state.get('timestamp', 0) < 5:
|
|
if runtime_info["notes"]:
|
|
update_GUI(window, com_socket, text1, text2)
|
|
|
|
# Still try to connect to LiveSplit desktop
|
|
con.ls_connect(com_socket, server_found, window, runtime_info["server_port"])
|
|
else:
|
|
# LiveSplit desktop is connected, use normal logic
|
|
if runtime_info["notes"]:
|
|
new_index = con.get_split_index(com_socket)
|
|
|
|
if isinstance(new_index, bool):
|
|
com_socket = test_connection(com_socket, window, text1, text2)
|
|
else:
|
|
if new_index == -1:
|
|
if runtime_info["timer_running"]:
|
|
runtime_info["timer_running"] = False
|
|
runtime_info["active_split"] = new_index
|
|
update_GUI(window, com_socket, text1, text2)
|
|
notify_browsers_state_change()
|
|
else:
|
|
if not runtime_info["timer_running"]:
|
|
runtime_info["timer_running"] = True
|
|
if runtime_info["active_split"] == 0:
|
|
runtime_info["active_split"] = -1
|
|
|
|
if runtime_info["active_split"] != new_index:
|
|
runtime_info["active_split"] = new_index
|
|
update_GUI(window, com_socket, text1, text2)
|
|
notify_browsers_state_change()
|
|
else:
|
|
com_socket = test_connection(com_socket, window, text1, text2)
|
|
|
|
window.after(int(config.POLLING_TIME * 1000), update, window, com_socket, text1, text2)
|
|
|
|
|
|
def notify_browsers_state_change():
|
|
"""Notify browser extensions of state changes via TCP"""
|
|
if runtime_info["bridge_enabled"] and runtime_info.get("bridge_server"):
|
|
state = {
|
|
'type': 'state_update',
|
|
'currentSplit': runtime_info["active_split"],
|
|
'timerRunning': runtime_info["timer_running"],
|
|
'totalSplits': len(runtime_info["notes"]),
|
|
'timestamp': time.time()
|
|
}
|
|
|
|
try:
|
|
runtime_info["bridge_server"].send_state_to_browsers(state)
|
|
except Exception as e:
|
|
print(f"Error notifying browsers: {e}")
|
|
|
|
|
|
def update_GUI(window, com_socket, text1, text2):
|
|
"""Updates all graphics according to current runtime_info"""
|
|
index = runtime_info["active_split"]
|
|
|
|
if index == -1:
|
|
index = 0
|
|
|
|
if runtime_info["timer_running"]:
|
|
split_name = con.get_split_name(com_socket) if runtime_info["ls_connected"] else ""
|
|
else:
|
|
split_name = False
|
|
|
|
if runtime_info["notes"]:
|
|
set_title_notes(window, index, split_name)
|
|
update_notes(text1, text2, index)
|
|
else:
|
|
update_title(config.DEFAULT_WINDOW["TITLE"], window)
|
|
|
|
|
|
def test_connection(com_socket, window, text1, text2):
|
|
"""Runs a connection test to LiveSplit desktop"""
|
|
if con.check_connection(com_socket):
|
|
return com_socket
|
|
else:
|
|
return reset_connection(com_socket, window, text1, text2)
|
|
|
|
|
|
def reset_connection(com_socket, window, text1, text2):
|
|
"""Resets LiveSplit desktop connection"""
|
|
if runtime_info["timer_running"]:
|
|
runtime_info["timer_running"] = False
|
|
runtime_info["active_split"] = -1
|
|
|
|
runtime_info["ls_connected"] = False
|
|
|
|
update_icon(False, window)
|
|
update_GUI(window, com_socket, text1, text2)
|
|
|
|
con.close_socket(com_socket)
|
|
return con.init_socket()
|
|
|
|
|
|
def server_found(window):
|
|
"""Executes when LiveSplit desktop connection is established"""
|
|
runtime_info["ls_connected"] = True
|
|
update_icon(True, window)
|
|
|
|
|
|
def update_icon(active, window):
|
|
"""Updates icon with TCP bridge server status consideration"""
|
|
try:
|
|
bridge_active = (runtime_info["bridge_enabled"] and
|
|
runtime_info.get("bridge_state") and
|
|
time.time() - runtime_info["bridge_state"].get('timestamp', 0) < 10)
|
|
|
|
if (active or bridge_active) and green_icon:
|
|
window.iconphoto(False, green_icon)
|
|
elif red_icon:
|
|
window.iconphoto(False, red_icon)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def update_title(name, window):
|
|
"""Sets the title with TCP bridge status if enabled"""
|
|
title = name
|
|
if runtime_info["bridge_enabled"] and runtime_info.get("bridge_server"):
|
|
status = runtime_info["bridge_server"].get_status()
|
|
if status['clients'] > 0:
|
|
title += f" [TCP Bridge: {status['clients']} browser(s)]"
|
|
|
|
window.wm_title(title)
|
|
|
|
|
|
def adjust_content(window, box1, box2):
|
|
"""Adjusts content layout"""
|
|
if runtime_info["double_layout"]:
|
|
set_double_layout(window, box1, box2)
|
|
else:
|
|
set_single_layout(window, box1, box2)
|
|
|
|
|
|
def set_double_layout(window, box1, box2):
|
|
"""Configures boxes for double layout"""
|
|
runtime_info["double_layout"] = True
|
|
|
|
w_width = window.winfo_width()
|
|
w_height = window.winfo_height()
|
|
|
|
box1.place(height=(w_height // 2), width=w_width)
|
|
box2.place(height=(w_height // 2), width=w_width, y=(w_height // 2))
|
|
|
|
|
|
def set_single_layout(window, box1, box2):
|
|
"""Configures boxes for single layout"""
|
|
runtime_info["double_layout"] = False
|
|
|
|
box2.place_forget()
|
|
box1.place(height=window.winfo_height(), width=window.winfo_width())
|
|
|
|
|
|
def show_popup(event, menu):
|
|
"""Displays popup menu"""
|
|
try:
|
|
menu.post(event.x_root, event.y_root)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def menu_load_notes(window, text1, text2, com_socket):
|
|
"""Menu load notes option"""
|
|
load_notes(window, text1, text2, com_socket)
|
|
|
|
|
|
def load_notes(window, text1, text2, com_socket):
|
|
"""Prompts user to select and load notes"""
|
|
file = noter.select_file()
|
|
|
|
if file:
|
|
notes = noter.get_notes(file, runtime_info["settings"]["separator"])
|
|
if notes:
|
|
runtime_info["notes"] = notes
|
|
|
|
settings = setting_handler.load_settings()
|
|
settings["notes"] = file
|
|
setting_handler.save_settings(settings)
|
|
|
|
split_c = len(notes)
|
|
show_info(("Notes Loaded", f"Loaded notes with {split_c} splits."))
|
|
|
|
if not runtime_info["timer_running"]:
|
|
runtime_info["active_split"] = -1
|
|
|
|
update_GUI(window, com_socket, text1, text2)
|
|
else:
|
|
show_info(config.ERRORS["NOTES_EMPTY"], True)
|
|
|
|
|
|
def show_info(info, warning=False):
|
|
"""Displays info popup"""
|
|
try:
|
|
if warning:
|
|
messagebox.showwarning(info[0], info[1])
|
|
else:
|
|
messagebox.showinfo(info[0], info[1])
|
|
except Exception:
|
|
print(f"Info: {info[1]}")
|
|
|
|
|
|
def update_notes(text1, text2, index):
|
|
"""Displays notes with given index"""
|
|
max_index = len(runtime_info["notes"]) - 1
|
|
|
|
if index < 0:
|
|
index = 0
|
|
|
|
text1.config(state=tkinter.NORMAL)
|
|
text2.config(state=tkinter.NORMAL)
|
|
|
|
text1.delete("1.0", tkinter.END)
|
|
text2.delete("1.0", tkinter.END)
|
|
|
|
if index <= max_index:
|
|
text1.insert(tkinter.END, runtime_info["notes"][index])
|
|
|
|
if index < max_index:
|
|
text2.insert(tkinter.END, runtime_info["notes"][index + 1])
|
|
|
|
text1.config(state=tkinter.DISABLED)
|
|
text2.config(state=tkinter.DISABLED)
|
|
|
|
|
|
def right_arrow(window, com_socket, text1, text2):
|
|
"""Event handler for right arrow key"""
|
|
change_preview(window, com_socket, text1, text2, 1)
|
|
|
|
|
|
def left_arrow(window, com_socket, text1, text2):
|
|
"""Event handler for left arrow key"""
|
|
change_preview(window, com_socket, text1, text2, -1)
|
|
|
|
|
|
def change_preview(window, com_socket, text1, text2, move):
|
|
"""Changes displayed notes for preview"""
|
|
if runtime_info["notes"] and not runtime_info["timer_running"]:
|
|
max_index = len(runtime_info["notes"]) - 1
|
|
index = runtime_info["active_split"]
|
|
|
|
if index < 0:
|
|
index = 0
|
|
|
|
index += move
|
|
|
|
if index > max_index:
|
|
index = max_index
|
|
elif index < 0:
|
|
index = 0
|
|
|
|
runtime_info["active_split"] = index
|
|
update_GUI(window, com_socket, text1, text2)
|
|
|
|
|
|
def set_title_notes(window, index, split_name=False):
|
|
"""Set window title to fit with displayed notes"""
|
|
title = config.DEFAULT_WINDOW["TITLE"]
|
|
|
|
disp_index = str(index + 1)
|
|
title += " - " + disp_index
|
|
|
|
if split_name:
|
|
title += " - " + split_name
|
|
|
|
if runtime_info["timer_running"]:
|
|
title += " - " + config.RUNNING_ALERT
|
|
|
|
update_title(title, window)
|
|
|
|
|
|
def menu_open_settings(root_wnd, box1, box2, text1, text2, com_socket):
|
|
"""Opens the settings menu"""
|
|
setting_handler.edit_settings(root_wnd,
|
|
lambda settings: apply_settings(settings,
|
|
root_wnd,
|
|
box1, box2,
|
|
text1, text2, com_socket))
|
|
|
|
|
|
def apply_settings(settings, window, box1, box2, text1, text2, com_socket):
|
|
"""Applies settings to the application"""
|
|
runtime_info["settings"] = settings
|
|
|
|
# Server port change
|
|
if runtime_info["server_port"] != int(settings["server_port"]):
|
|
runtime_info["server_port"] = int(settings["server_port"])
|
|
runtime_info["force_reset"] = True
|
|
|
|
text_font = (settings["font"], int(settings["font_size"]))
|
|
|
|
if setting_handler.decode_boolean_setting(settings["double_layout"]):
|
|
set_double_layout(window, box1, box2)
|
|
else:
|
|
set_single_layout(window, box1, box2)
|
|
|
|
text1.config(font=text_font)
|
|
text2.config(font=text_font)
|
|
text1.config(fg=settings["text_color"], bg=settings["background_color"])
|
|
text2.config(fg=settings["text_color"], bg=settings["background_color"])
|
|
|
|
old_note_length = len(runtime_info["notes"])
|
|
|
|
if settings["notes"] and noter.file_exists(settings["notes"]):
|
|
new_notes = noter.get_notes(settings["notes"], settings["separator"])
|
|
|
|
if new_notes:
|
|
runtime_info["notes"] = new_notes
|
|
|
|
new_note_length = len(new_notes)
|
|
|
|
if new_note_length != old_note_length:
|
|
show_info(("Notes Loaded",
|
|
f"Loaded notes with {new_note_length} splits."))
|
|
|
|
if not runtime_info["timer_running"]:
|
|
runtime_info["active_split"] = -1
|
|
|
|
update_GUI(window, com_socket, text1, text2)
|
|
else:
|
|
show_info(config.ERRORS["NOTES_EMPTY"], True)
|
|
|
|
|
|
def save_geometry_settings(width, height):
|
|
"""Saves geometry settings"""
|
|
settings = setting_handler.load_settings()
|
|
settings["width"] = str(width)
|
|
settings["height"] = str(height)
|
|
setting_handler.save_settings(settings)
|
|
|
|
|
|
def do_on_close(root_wnd):
|
|
"""Function called when main window is closed"""
|
|
try:
|
|
save_geometry_settings(root_wnd.winfo_width(), root_wnd.winfo_height())
|
|
except:
|
|
pass
|
|
|
|
# Stop TCP bridge server
|
|
if runtime_info.get("bridge_server"):
|
|
runtime_info["bridge_server"].stop()
|
|
|
|
root_wnd.destroy()
|
|
|
|
|
|
def init_UI(root):
|
|
"""Initialize UI with proper TCP bridge server integration"""
|
|
com_socket = con.init_socket()
|
|
|
|
# Load Settings (including bridge settings)
|
|
print("Loading SplitNotes settings...")
|
|
settings = setting_handler.load_settings()
|
|
runtime_info["server_port"] = int(settings["server_port"])
|
|
runtime_info["settings"] = settings
|
|
|
|
# Load TCP bridge settings with proper validation
|
|
print("Loading TCP bridge settings...")
|
|
bridge_enabled_str = settings.get("bridge_enabled", "false").lower().strip()
|
|
runtime_info["bridge_enabled"] = bridge_enabled_str == "true"
|
|
|
|
try:
|
|
runtime_info["bridge_port"] = int(settings.get("bridge_port", "16835"))
|
|
except (ValueError, TypeError):
|
|
print("Invalid bridge port in settings, using default 16835")
|
|
runtime_info["bridge_port"] = 16835
|
|
# Update settings with correct port
|
|
settings["bridge_port"] = "16835"
|
|
setting_handler.save_settings(settings)
|
|
|
|
print(f"Bridge settings loaded: enabled={runtime_info['bridge_enabled']}, port={runtime_info['bridge_port']}")
|
|
|
|
# Start TCP bridge server if enabled in settings
|
|
if runtime_info["bridge_enabled"]:
|
|
print(f"Bridge is enabled in config, starting TCP bridge server on port {runtime_info['bridge_port']}...")
|
|
bridge_server = BridgeServer(runtime_info["bridge_port"])
|
|
if bridge_server.start():
|
|
runtime_info["bridge_server"] = bridge_server
|
|
print("✓ TCP bridge server started successfully for browser extensions")
|
|
else:
|
|
print("✗ Failed to start TCP bridge server")
|
|
print(" This might be due to port already in use or firewall settings")
|
|
# Don't disable the setting here - let user handle it in settings dialog
|
|
else:
|
|
print("TCP bridge server is disabled in configuration")
|
|
|
|
# Graphical components
|
|
root.geometry(settings["width"] + "x" + settings["height"])
|
|
root.minsize(300, 200)
|
|
|
|
box1 = tkinter.Frame(root)
|
|
box2 = tkinter.Frame(root)
|
|
|
|
scroll1 = tkinter.Scrollbar(box1, width=config.SCROLLBAR_WIDTH)
|
|
scroll2 = tkinter.Scrollbar(box2, width=config.SCROLLBAR_WIDTH)
|
|
scroll1.pack(side=tkinter.RIGHT, fill=tkinter.Y)
|
|
scroll2.pack(side=tkinter.RIGHT, fill=tkinter.Y)
|
|
|
|
text1 = tkinter.Text(
|
|
box1,
|
|
yscrollcommand=scroll1.set,
|
|
wrap=tkinter.WORD,
|
|
cursor="arrow"
|
|
)
|
|
text1.insert(tkinter.END, config.DEFAULT_MSG)
|
|
text1.config(state=tkinter.DISABLED)
|
|
text1.pack(fill=tkinter.BOTH, expand=True)
|
|
|
|
text2 = tkinter.Text(
|
|
box2,
|
|
yscrollcommand=scroll2.set,
|
|
wrap=tkinter.WORD,
|
|
cursor="arrow"
|
|
)
|
|
text2.insert(tkinter.END, config.DEFAULT_MSG)
|
|
text2.config(state=tkinter.DISABLED)
|
|
text2.pack(fill=tkinter.BOTH, expand=True)
|
|
|
|
scroll1.config(command=text1.yview)
|
|
scroll2.config(command=text2.yview)
|
|
|
|
# Set font and color
|
|
text_font = (settings["font"], int(settings["font_size"]))
|
|
|
|
text1.config(font=text_font)
|
|
text2.config(font=text_font)
|
|
text1.config(fg=settings["text_color"], bg=settings["background_color"])
|
|
text2.config(fg=settings["text_color"], bg=settings["background_color"])
|
|
|
|
if setting_handler.decode_boolean_setting(settings["double_layout"]):
|
|
set_double_layout(root, box1, box2)
|
|
else:
|
|
set_single_layout(root, box1, box2)
|
|
|
|
# Create popup menu with TCP bridge settings
|
|
popup = tkinter.Menu(root, tearoff=0)
|
|
popup.add_command(
|
|
label=config.MENU_OPTIONS["LOAD"],
|
|
command=lambda: menu_load_notes(root, text1, text2, com_socket)
|
|
)
|
|
popup.add_command(
|
|
label=config.MENU_OPTIONS["SETTINGS"],
|
|
command=lambda: menu_open_settings(root, box1, box2, text1, text2, com_socket)
|
|
)
|
|
popup.add_separator()
|
|
popup.add_command(
|
|
label="TCP Bridge Settings",
|
|
command=lambda: menu_open_bridge_settings(root)
|
|
)
|
|
|
|
# Set default window icon and title
|
|
update_icon(False, root)
|
|
update_title(config.DEFAULT_WINDOW["TITLE"], root)
|
|
|
|
# Check if notes can be loaded from settings
|
|
if settings["notes"] and noter.file_exists(settings["notes"]):
|
|
notes = noter.get_notes(settings["notes"], settings["separator"])
|
|
|
|
if notes:
|
|
runtime_info["notes"] = notes
|
|
update_GUI(root, com_socket, text1, text2)
|
|
|
|
# Event binds
|
|
root.bind("<Configure>", lambda e: adjust_content(root, box1, box2) if e.widget == root else None)
|
|
|
|
# Platform-specific right-click handling
|
|
if config.IS_MACOS:
|
|
root.bind("<Button-2>", lambda e: show_popup(e, popup))
|
|
root.bind("<Control-Button-1>", lambda e: show_popup(e, popup))
|
|
else:
|
|
root.bind("<Button-3>", lambda e: show_popup(e, popup))
|
|
|
|
root.bind("<Right>", lambda e: right_arrow(root, com_socket, text1, text2))
|
|
root.bind("<Left>", lambda e: left_arrow(root, com_socket, text1, text2))
|
|
|
|
# Window close bind
|
|
root.protocol("WM_DELETE_WINDOW", lambda: do_on_close(root))
|
|
|
|
# Call update loop
|
|
update(root, com_socket, text1, text2)
|
|
|
|
# Debug: Print final bridge status
|
|
bridge_running = runtime_info.get('bridge_server') is not None
|
|
print(f"Final bridge status: enabled={runtime_info['bridge_enabled']}, server_running={bridge_running}")
|
|
|
|
if runtime_info['bridge_enabled'] and not bridge_running:
|
|
print("Warning: Bridge is enabled but server failed to start. Check TCP Bridge Settings.")
|
|
|
|
|
|
def main():
|
|
"""Main entry point with TCP bridge server support"""
|
|
try:
|
|
root.title(config.DEFAULT_WINDOW["TITLE"])
|
|
|
|
# Platform-specific optimizations
|
|
if config.IS_MACOS:
|
|
try:
|
|
root.tk.call('tk', 'scaling', 1.0)
|
|
except:
|
|
pass
|
|
|
|
# Initialize UI with TCP bridge server
|
|
init_UI(root)
|
|
|
|
# Start the main loop
|
|
root.mainloop()
|
|
|
|
except KeyboardInterrupt:
|
|
print("\nApplication interrupted by user")
|
|
except Exception as e:
|
|
print(f"An error occurred: {e}")
|
|
messagebox.showerror("Error", f"An unexpected error occurred:\n{e}")
|
|
finally:
|
|
# Clean up TCP bridge server
|
|
if runtime_info.get("bridge_server"):
|
|
runtime_info["bridge_server"].stop()
|
|
try:
|
|
root.destroy()
|
|
except:
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |