This commit is contained in:
ApfelTeeSaft
2025-08-03 12:27:51 +02:00
parent d33dbd523e
commit 44ff009021
3 changed files with 656 additions and 345 deletions
+54 -84
View File
@@ -1,4 +1,4 @@
# CONFIG FILE WITH CONSTANTS - Clean version with Bridge Server Support
# CONFIG FILE WITH CONSTANTS - Complete Bridge Server Support
import platform
import os
@@ -75,23 +75,23 @@ TEXT_FILES = [
('All', '*')
]
# Default content of config.cfg file - Enhanced with bridge settings
# Default content of config.cfg file - COMPLETE with proper bridge settings
DEFAULT_CONFIG = """notes=
font_size=12
font=arial
text_color=#000000
background_color=#FFFFFF
double_layout=False
double_layout=false
server_port=16834
width=400
height=300
separator=new_line
bridge_enabled=False
bridge_enabled=false
bridge_port=16835"""
NEWLINE_CONSTANT = "new_line"
# Required settings - Enhanced with bridge settings
# Required settings - COMPLETE with bridge settings
REQUIRED_SETTINGS = (
"notes",
"font",
@@ -116,9 +116,9 @@ SETTINGS_WINDOW = {"TITLE": "Settings",
# Bridge settings window options
BRIDGE_SETTINGS_WINDOW = {
"TITLE": "Bridge Server Settings",
"WIDTH": 500,
"HEIGHT": 400,
"TITLE": "TCP Bridge Server Settings",
"WIDTH": 520,
"HEIGHT": 500,
"CANCEL": "Cancel",
"APPLY": "Apply"
}
@@ -136,30 +136,38 @@ SETTINGS_OPTIONS = {"FONT": "Font",
# Bridge server settings options
BRIDGE_OPTIONS = {
"ENABLE_BRIDGE": "Enable Bridge Server",
"ENABLE_BRIDGE": "Enable TCP Bridge Server for Browser Extensions",
"BRIDGE_PORT": "Bridge Server Port",
"DEFAULT_BRIDGE_PORT": "(Default is 16835)",
"BRIDGE_STATUS": "Server Status",
"CONNECTED_BROWSERS": "Connected Browsers",
"LAST_UPDATE": "Last Update",
"HELP_TEXT": """Browser Extension Setup:
"SAVE_SETTINGS": "Save Settings",
"TEST_CONNECTION": "Test Connection",
"HELP_TEXT": """TCP Bridge Server Help:
1. CHROME/CHROMIUM:
• Go to chrome://extensions/
• Enable Developer Mode
• Click "Load unpacked"
• Select the Chrome extension folder
The TCP Bridge Server allows browser extensions to connect to SplitNotes and send timer state information from LiveSplit One.
2. FIREFOX:
• Go to about:debugging
• Click "This Firefox""Load Temporary Add-on"
• Select manifest.json from Firefox extension folder
SETUP:
1. Check 'Enable TCP Bridge Server' checkbox
2. Set the port (default: 16835)
3. Click 'Save Settings'
4. Verify status shows 'RUNNING ✓'
3. USAGE:
• Start SplitNotes with bridge enabled
• Open https://one.livesplit.org/
• Load your splits and start timing
• Notes will automatically sync"""
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"""
}
# Platform-specific fonts
@@ -209,28 +217,14 @@ if IS_MACOS:
elif IS_LINUX:
GUI_FONT = ("Ubuntu", 12)
# Bridge server specific constants
BRIDGE_SERVER_COMMANDS = {
"GET_STATUS": "get_status",
"GET_STATE": "get_state",
"SET_STATE": "set_state",
"BROWSER_CONNECT": "browser_connect",
"BROWSER_DISCONNECT": "browser_disconnect"
}
# Application info for packaging
APP_NAME = "SplitNotes"
APP_VERSION = "1.1.0" # Incremented for bridge server support
APP_AUTHOR = "ApfelTeeSaft"
APP_DESCRIPTION = "Software for syncing notes with LiveSplit using the LiveSplit server component and browser extensions."
# Bridge server status messages
BRIDGE_STATUS = {
"STARTING": "Starting bridge server...",
"RUNNING": "Bridge server running",
"STOPPED": "Bridge server stopped",
"ERROR": "Bridge server error",
"NO_CLIENTS": "No browser clients connected",
"CLIENT_CONNECTED": "Browser client connected",
"CLIENT_DISCONNECTED": "Browser client disconnected"
}
# Browser extension communication protocol
BROWSER_MESSAGE_TYPES = {
# Bridge server message types
BRIDGE_MESSAGE_TYPES = {
"TIMER_STATE": "timer_state",
"SPLITS_UPDATED": "splits_updated",
"CONNECTION_TEST": "connection_test",
@@ -238,42 +232,18 @@ BROWSER_MESSAGE_TYPES = {
"SETTINGS_UPDATE": "settings_update"
}
# Network timeouts for bridge server
BRIDGE_TIMEOUTS = {
"CONNECTION": 5.0, # seconds
"READ": 1.0, # seconds
"KEEPALIVE": 30.0 # seconds
}
# Application info for packaging
APP_NAME = "SplitNotes"
APP_VERSION = "1.1.0" # Incremented for bridge server support
APP_AUTHOR = "ApfelTeeSaft"
APP_DESCRIPTION = "Software for syncing notes with LiveSplit using the LiveSplit server component and browser extensions."
# Bridge server user agent for HTTP requests
BRIDGE_USER_AGENT = f"{APP_NAME}/{APP_VERSION} BridgeServer"
# Default bridge server settings for first run
DEFAULT_BRIDGE_SETTINGS = {
"enabled": False,
"port": BRIDGE_PORT,
"host": BRIDGE_HOST,
"auto_start": True,
"log_connections": True,
"timeout": 30
}
# Maximum number of concurrent browser connections
MAX_BROWSER_CONNECTIONS = 10
# Bridge server logging levels
BRIDGE_LOG_LEVELS = {
"DEBUG": 0,
"INFO": 1,
"WARNING": 2,
"ERROR": 3
}
# Default log level for bridge server
DEFAULT_BRIDGE_LOG_LEVEL = BRIDGE_LOG_LEVELS["INFO"]
# Bridge server status messages
BRIDGE_STATUS_MESSAGES = {
"STARTING": "Starting TCP bridge server...",
"RUNNING": "TCP bridge server running",
"STOPPED": "TCP bridge server stopped",
"ERROR": "TCP bridge server error",
"NO_CLIENTS": "No browser clients connected",
"CLIENT_CONNECTED": "Browser client connected",
"CLIENT_DISCONNECTED": "Browser client disconnected",
"SETTINGS_SAVED": "Settings saved successfully",
"SERVER_STARTED": "TCP Bridge server started successfully",
"SERVER_FAILED": "Failed to start TCP bridge server",
"CONNECTION_TEST_OK": "Connection test successful",
"CONNECTION_TEST_FAIL": "Connection test failed"
}
+494 -241
View File
@@ -64,6 +64,7 @@ class BridgeServer:
self.server_socket = None
self.clients = []
self.state_lock = threading.Lock()
self.server_thread = None
def start(self):
"""Start the TCP bridge server"""
@@ -73,20 +74,28 @@ class BridgeServer:
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
server_thread = threading.Thread(target=self._server_loop, daemon=True)
server_thread.start()
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):
@@ -94,7 +103,7 @@ class BridgeServer:
self.running = False
# Close all client connections
for client in self.clients[:]:
for client in self.clients[:]: # Copy list to avoid modification during iteration
try:
client.close()
except:
@@ -107,6 +116,11 @@ class BridgeServer:
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")
@@ -117,37 +131,55 @@ class BridgeServer:
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,),
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):
def _handle_client(self, client, address):
"""Handle individual client connections"""
try:
while self.running:
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)
except json.JSONDecodeError:
# Handle plain text commands if needed
command = data.decode('utf-8').strip()
print(f"Received plain text command: {command}")
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: {e}")
print(f"Error handling client {address}: {e}")
finally:
try:
client.close()
@@ -155,7 +187,7 @@ class BridgeServer:
pass
if client in self.clients:
self.clients.remove(client)
print("Browser client disconnected")
print(f"Browser client {address} disconnected")
def _process_browser_message(self, message):
"""Process JSON messages from browser extensions"""
@@ -188,6 +220,8 @@ class BridgeServer:
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:
@@ -198,10 +232,10 @@ class BridgeServer:
if not self.clients:
return
message = json.dumps(state) + '\n' # Add newline for better parsing
message = json.dumps(state) + '\n'
disconnected_clients = []
for client in self.clients:
for client in self.clients[:]: # Copy list to avoid modification during iteration
try:
client.send(message.encode('utf-8'))
except:
@@ -222,6 +256,410 @@ class BridgeServer:
}
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"]:
@@ -231,9 +669,8 @@ def update(window, com_socket, text1, text2):
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"):
# Use browser state when LiveSplit is not connected
bridge_state = runtime_info["bridge_state"]
if time.time() - bridge_state.get('timestamp', 0) < 5: # State is recent (5 seconds)
if time.time() - bridge_state.get('timestamp', 0) < 5:
if runtime_info["notes"]:
update_GUI(window, com_socket, text1, text2)
@@ -245,40 +682,28 @@ def update(window, com_socket, text1, text2):
new_index = con.get_split_index(com_socket)
if isinstance(new_index, bool):
# Connection error
com_socket = test_connection(com_socket, window, text1, text2)
else:
if new_index == -1:
# Timer not running
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 of state change
notify_browsers_state_change()
else:
# Timer is running
if not runtime_info["timer_running"]:
runtime_info["timer_running"] = True
# Special case to fix scrolling
if runtime_info["active_split"] == 0:
runtime_info["active_split"] = -1
if runtime_info["active_split"] != new_index:
# New split, need to update
runtime_info["active_split"] = new_index
update_GUI(window, com_socket, text1, text2)
# Notify browsers of state change
notify_browsers_state_change()
else:
# Notes not yet loaded
com_socket = test_connection(com_socket, window, text1, text2)
# Continue main loop
window.after(int(config.POLLING_TIME * 1000),
update, window, com_socket, text1, text2)
window.after(int(config.POLLING_TIME * 1000), update, window, com_socket, text1, text2)
def notify_browsers_state_change():
@@ -298,198 +723,6 @@ def notify_browsers_state_change():
print(f"Error notifying browsers: {e}")
def menu_open_bridge_settings(root_wnd):
"""Open bridge server settings dialog"""
settings_wnd = tkinter.Toplevel(master=root_wnd)
settings_wnd.title("Bridge Server Settings")
settings_wnd.geometry("500x400")
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) - (500 // 2)
y = (settings_wnd.winfo_screenheight() // 2) - (400 // 2)
settings_wnd.geometry(f"+{x}+{y}")
# Create notebook for tabs
notebook = ttk.Notebook(settings_wnd)
notebook.pack(fill='both', expand=True, padx=10, pady=10)
# Bridge Settings Tab
bridge_frame = ttk.Frame(notebook)
notebook.add(bridge_frame, text="Bridge Server")
# Enable bridge checkbox
bridge_enabled_var = tkinter.BooleanVar(value=runtime_info["bridge_enabled"])
bridge_enabled_cb = tkinter.Checkbutton(
bridge_frame,
text="Enable TCP Bridge Server for Browser Extensions",
variable=bridge_enabled_var,
font=config.GUI_FONT
)
bridge_enabled_cb.pack(anchor='w', padx=10, pady=10)
# Port setting
tkinter.Label(bridge_frame, text="Bridge Server Port:", font=config.GUI_FONT).pack(anchor='w', padx=10)
port_var = tkinter.StringVar(value=str(runtime_info["bridge_port"]))
port_entry = tkinter.Entry(bridge_frame, textvariable=port_var, font=config.GUI_FONT, width=10)
port_entry.pack(anchor='w', padx=10, pady=5)
tkinter.Label(
bridge_frame,
text="Browser extensions connect to this TCP port (default: 16835)",
font=('Arial', 9),
fg='gray'
).pack(anchor='w', padx=10)
# Status frame
status_frame = tkinter.LabelFrame(bridge_frame, text="Server Status", font=config.GUI_FONT)
status_frame.pack(fill='x', padx=10, pady=20)
status_text = tkinter.Text(status_frame, height=8, width=60, font=('Courier', 9))
status_text.pack(padx=10, pady=10)
def update_status():
if runtime_info.get("bridge_server"):
status = runtime_info["bridge_server"].get_status()
status_info = f"""TCP Bridge Server Status:
Running: {'Yes' if status['running'] else 'No'}
Port: {status['port']}
Connected Browsers: {status['clients']}
Last State:
Current Split: {status['last_state'].get('currentSplit', 'N/A')}
Timer Running: {status['last_state'].get('timerRunning', 'N/A')}
Last Update: {time.ctime(status['last_state'].get('timestamp', 0)) if status['last_state'].get('timestamp') else 'Never'}
"""
else:
status_info = "TCP Bridge Server: Not Running"
status_text.delete(1.0, tkinter.END)
status_text.insert(1.0, status_info)
# 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()
# Instructions Tab
help_frame = ttk.Frame(notebook)
notebook.add(help_frame, text="Setup Instructions")
help_text = tkinter.Text(help_frame, wrap='word', font=('Arial', 10))
help_text.pack(fill='both', expand=True, padx=10, pady=10)
instructions = """Browser Extension Setup (TCP Bridge):
1. CHROME/CHROMIUM:
• Go to chrome://extensions/
• Enable Developer Mode
• Click "Load unpacked"
• Select the Chrome extension folder
• Click the extension icon and enable the TCP bridge
2. FIREFOX:
• Go to about:debugging
• Click "This Firefox""Load Temporary Add-on"
• Select manifest.json from Firefox extension folder
• Click the extension icon and enable the TCP bridge
3. USAGE:
• Start SplitNotes with TCP bridge enabled
• Open https://one.livesplit.org/
• Load your splits and start timing
• SplitNotes will automatically sync with LiveSplit One
• Notes will advance when you split in the browser
4. TROUBLESHOOTING:
• Check that TCP bridge server is running (see Status tab)
• Verify browser extension is enabled and connected
• Ensure you're on one.livesplit.org
• Check browser console for connection errors (F12)
• Verify port 16835 is not blocked by firewall
TECHNICAL DETAILS:
The TCP bridge server replaces websockets with a simple TCP
connection on port 16835. Browser extensions communicate via
JSON messages over this TCP connection for better reliability
and simpler setup without external dependencies."""
help_text.insert(1.0, instructions)
help_text.config(state='disabled')
# Buttons
button_frame = tkinter.Frame(settings_wnd)
button_frame.pack(fill='x', padx=10, pady=10)
def apply_settings():
try:
# Validate port
port = int(port_var.get())
if not (1024 <= port <= 65535):
raise ValueError("Port must be between 1024 and 65535")
# Stop existing server if running
if runtime_info.get("bridge_server"):
runtime_info["bridge_server"].stop()
runtime_info["bridge_server"] = None
# Update settings
runtime_info["bridge_enabled"] = bridge_enabled_var.get()
runtime_info["bridge_port"] = port
# Start server if enabled
if runtime_info["bridge_enabled"]:
bridge_server = BridgeServer(port)
if bridge_server.start():
runtime_info["bridge_server"] = bridge_server
messagebox.showinfo("Success", "TCP Bridge server started successfully!")
else:
messagebox.showerror("Error", "Failed to start TCP bridge server!")
return
else:
messagebox.showinfo("Info", "TCP Bridge server disabled")
# Save settings
settings = setting_handler.load_settings()
settings["bridge_enabled"] = str(runtime_info["bridge_enabled"])
settings["bridge_port"] = str(runtime_info["bridge_port"])
setting_handler.save_settings(settings)
settings_wnd.destroy()
except ValueError as e:
messagebox.showerror("Error", f"Invalid settings: {e}")
except Exception as e:
messagebox.showerror("Error", f"Failed to apply settings: {e}")
def cancel_settings():
settings_wnd.destroy()
tkinter.Button(
button_frame,
text="Apply",
command=apply_settings,
font=config.GUI_FONT,
width=10
).pack(side='right', padx=5)
tkinter.Button(
button_frame,
text="Cancel",
command=cancel_settings,
font=config.GUI_FONT,
width=10
).pack(side='right')
def update_GUI(window, com_socket, text1, text2):
"""Updates all graphics according to current runtime_info"""
index = runtime_info["active_split"]
@@ -541,7 +774,6 @@ def server_found(window):
def update_icon(active, window):
"""Updates icon with TCP bridge server status consideration"""
try:
# Show green if either LiveSplit connected OR bridge has recent browser data
bridge_active = (runtime_info["bridge_enabled"] and
runtime_info.get("bridge_state") and
time.time() - runtime_info["bridge_state"].get('timestamp', 0) < 10)
@@ -619,14 +851,12 @@ def load_notes(window, text1, text2, com_socket):
setting_handler.save_settings(settings)
split_c = len(notes)
show_info(("Notes Loaded",
f"Loaded notes with {split_c} splits."))
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)
@@ -699,7 +929,7 @@ 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) # start at 1
disp_index = str(index + 1)
title += " - " + disp_index
if split_name:
@@ -786,28 +1016,44 @@ def do_on_close(root_wnd):
def init_UI(root):
"""Initialize UI with TCP bridge server integration"""
"""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
runtime_info["bridge_enabled"] = setting_handler.decode_boolean_setting(
settings.get("bridge_enabled", "False")
)
runtime_info["bridge_port"] = int(settings.get("bridge_port", "16835"))
# 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"
# Start TCP bridge server if enabled
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 for browser extensions")
print("TCP bridge server started successfully for browser extensions")
else:
print("Failed to start TCP bridge server")
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"])
@@ -903,6 +1149,13 @@ def init_UI(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():
+108 -20
View File
@@ -44,14 +44,17 @@ def load_settings():
with open(settings_path, "r", encoding='utf-8') as settings_file:
settings_content = settings_file.readlines()
settings_content = [line.strip() for line in settings_content]
print(f"Settings loaded from: {settings_path}")
except:
# File not found
print("Settings file not found, creating default settings")
settings_content = set_default_settings()
settings = format_settings(settings_content)
# Check so settings file has all settings
if not validate_settings(settings):
print("Settings validation failed, creating default settings")
settings = format_settings(set_default_settings())
return settings
@@ -62,6 +65,7 @@ def set_default_settings():
Creates a config file with default settings.
Returns the default config-file content.
"""
print(f"Creating default settings at: {settings_path}")
set_settings_file_content(config.DEFAULT_CONFIG)
return config.DEFAULT_CONFIG.split("\n")
@@ -79,12 +83,14 @@ def format_settings(file_rows):
for row in file_rows:
row = row.strip()
if '=' in row:
if '=' in row and not row.startswith('#'): # Skip comments
parts = row.split("=", 1)
if len(parts) == SETTING_PART_LENGTH:
# Strip to remove whitespace at end and beginning
settings[parts[0].strip()] = parts[1].strip()
key = parts[0].strip()
value = parts[1].strip()
settings[key] = value
return settings
@@ -92,42 +98,72 @@ def format_settings(file_rows):
def validate_settings(settings):
"""
Checks a settings dictionary so that all the needed settings are present.
Enhanced with comprehensive bridge settings validation.
"""
for req_setting in config.REQUIRED_SETTINGS:
if req_setting not in settings:
print(f"Missing required setting: {req_setting}")
return False
if not validate_font_size(settings["font_size"]):
print(f"Invalid font size: {settings['font_size']}")
return False
if not validate_server_port(settings["server_port"]):
print(f"Invalid server port: {settings['server_port']}")
return False
if not validate_color(settings["text_color"]):
print(f"Invalid text color: {settings['text_color']}")
return False
if not validate_color(settings["background_color"]):
print(f"Invalid background color: {settings['background_color']}")
return False
if settings["font"] not in config.AVAILABLE_FONTS:
print(f"Invalid font: {settings['font']}")
return False
if settings["double_layout"] not in ["True", "False"]:
if settings["double_layout"].lower() not in ["true", "false"]:
print(f"Invalid double_layout: {settings['double_layout']}")
return False
if not validate_pixels(settings["width"]):
print(f"Invalid width: {settings['width']}")
return False
if not validate_pixels(settings["height"]):
print(f"Invalid height: {settings['height']}")
return False
if not validate_separator(settings["separator"]):
print(f"Invalid separator: {settings['separator']}")
return False
# Bridge settings validation
if settings["bridge_enabled"].lower() not in ["true", "false"]:
print(f"Invalid bridge_enabled: {settings['bridge_enabled']}")
return False
if not validate_bridge_port(settings["bridge_port"]):
print(f"Invalid bridge port: {settings['bridge_port']}")
return False
print("All settings validated successfully")
return True
def validate_bridge_port(port):
"""Returns whether or not given port is a valid bridge server port."""
try:
port_num = int(port)
return 1024 <= port_num <= 65535
except:
return False
def set_settings_file_content(content):
"""
Saves given content to the config file, config.cfg, in the resources directory.
@@ -138,8 +174,49 @@ def set_settings_file_content(content):
try:
with open(settings_path, "w", encoding='utf-8') as settings_file:
settings_file.write(content)
print(f"Settings content written to: {settings_path}")
except Exception as e:
print(f"Error saving settings: {e}")
print(f"Error saving settings content: {e}")
def save_settings(settings):
"""
Saves given settings to the settings file.
Enhanced to ensure bridge settings are properly formatted.
"""
print("Saving settings...")
# Ensure bridge settings are present and properly formatted
if "bridge_enabled" not in settings:
settings["bridge_enabled"] = "false"
if "bridge_port" not in settings:
settings["bridge_port"] = "16835"
# Convert boolean values to lowercase strings for consistency
if isinstance(settings.get("bridge_enabled"), bool):
settings["bridge_enabled"] = str(settings["bridge_enabled"]).lower()
file_content = ""
# Write settings in a specific order for better readability
setting_order = [
"notes", "font", "font_size", "text_color", "background_color",
"double_layout", "server_port", "width", "height", "separator",
"bridge_enabled", "bridge_port"
]
# Write ordered settings first
for key in setting_order:
if key in settings:
file_content += f"{key}={settings[key]}\n"
# Write any additional settings not in the order list
for key, value in settings.items():
if key not in setting_order:
file_content += f"{key}={value}\n"
set_settings_file_content(file_content)
print(f"Settings saved: bridge_enabled={settings.get('bridge_enabled')}, bridge_port={settings.get('bridge_port')}")
def edit_settings(root_wnd, apply_method):
@@ -417,31 +494,20 @@ def validate_server_port(port):
return False
def save_settings(settings):
"""
Saves given settings to the settings file.
"""
file_content = ""
for key in settings.keys():
file_content += key + "=" + str(settings[key]) + "\n"
set_settings_file_content(file_content)
def decode_boolean_setting(setting):
"""
Decodes a boolean string of "True" or "False"
to the correct boolean value.
Enhanced to handle multiple formats.
"""
return str(setting) == "True"
return str(setting).lower() in ("true", "1", "yes", "on")
def encode_boolean_setting(value):
"""
Encodes a boolean to the string "True" or "False".
Encodes a boolean to the string "true" or "false".
"""
return "True" if value else "False"
return "true" if value else "false"
def validate_pixels(pixels):
@@ -463,4 +529,26 @@ def validate_separator(separator):
"""
if separator == config.NEWLINE_CONSTANT:
return True
return len(separator.strip()) > 0
return len(separator.strip()) > 0
def debug_settings():
"""Debug function to print current settings"""
print("\n=== Settings Debug ===")
try:
settings = load_settings()
print("Current settings:")
for key, value in settings.items():
print(f" {key}: {value}")
print(f"\nSettings file location: {settings_path}")
print(f"Settings file exists: {os.path.exists(settings_path)}")
if os.path.exists(settings_path):
with open(settings_path, 'r') as f:
content = f.read()
print(f"\nRaw file content:\n{content}")
except Exception as e:
print(f"Error in debug_settings: {e}")
print("=== End Settings Debug ===\n")