From 44ff0090212592261e7ef64ba54679e1d8e39e29 Mon Sep 17 00:00:00 2001 From: ApfelTeeSaft <91074565+ApfelTeeSaft@users.noreply.github.com> Date: Sun, 3 Aug 2025 12:27:51 +0200 Subject: [PATCH] lol --- config.py | 138 ++++----- main_window.py | 735 ++++++++++++++++++++++++++++++--------------- setting_handler.py | 128 ++++++-- 3 files changed, 656 insertions(+), 345 deletions(-) diff --git a/config.py b/config.py index 71f8c2e..bbb7790 100644 --- a/config.py +++ b/config.py @@ -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"] \ No newline at end of file +# 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" +} \ No newline at end of file diff --git a/main_window.py b/main_window.py index 391057a..f6006a2 100644 --- a/main_window.py +++ b/main_window.py @@ -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(): diff --git a/setting_handler.py b/setting_handler.py index 7b5c2cf..06395dc 100644 --- a/setting_handler.py +++ b/setting_handler.py @@ -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 \ No newline at end of file + 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") \ No newline at end of file