diff --git a/buid_windows.bat b/buid_windows.bat new file mode 100644 index 0000000..60c9399 --- /dev/null +++ b/buid_windows.bat @@ -0,0 +1,173 @@ +@echo off +REM Windows Build Script for SplitNotes +REM Requires PyInstaller: pip install pyinstaller + +echo Building SplitNotes for Windows... +echo. + +REM Check if PyInstaller is installed +python -c "import PyInstaller" 2>nul +if errorlevel 1 ( + echo Error: PyInstaller not found. Installing... + pip install pyinstaller + if errorlevel 1 ( + echo Failed to install PyInstaller. Please install manually: pip install pyinstaller + pause + exit /b 1 + ) +) + +REM Create build directory +if not exist "build" mkdir build +if not exist "dist" mkdir dist + +REM Clean previous builds +if exist "build\*" ( + echo Cleaning previous builds... + rmdir /s /q build + mkdir build +) +if exist "dist\*" ( + rmdir /s /q dist + mkdir dist +) + +REM Check if resources folder exists and has files +if not exist "resources" ( + echo Creating resources folder... + mkdir resources + echo Note: Add icon files to resources folder for better appearance: + echo - app_icon.ico (Windows icon) + echo - green.png (connection active) + echo - red.png (connection inactive) + echo - settings_icon.png (settings window) + set RESOURCES_EXIST=false +) else ( + dir /b "resources\*" >nul 2>&1 + if errorlevel 1 ( + echo Resources folder is empty... + set RESOURCES_EXIST=false + ) else ( + echo Found resources folder with files... + set RESOURCES_EXIST=true + ) +) + +REM Build the application +echo Building executable... + +REM Set icon parameter if icon exists +if exist "resources\app_icon.ico" ( + echo Found app icon, including in build... + set ICON_PARAM=--icon="%CD%\resources\app_icon.ico" +) else ( + echo No app icon found, building without custom icon... + set ICON_PARAM= +) + +REM Build with or without resources depending on availability +if "%RESOURCES_EXIST%"=="true" ( + echo Including resources in build... + pyinstaller ^ + --onedir ^ + --windowed ^ + --name "SplitNotes" ^ + %ICON_PARAM% ^ + --add-data "%CD%\resources;resources" ^ + --hidden-import "tkinter" ^ + --hidden-import "tkinter.ttk" ^ + --hidden-import "tkinter.colorchooser" ^ + --hidden-import "tkinter.filedialog" ^ + --hidden-import "tkinter.messagebox" ^ + --hidden-import "socket" ^ + --hidden-import "threading" ^ + --hidden-import "json" ^ + --hidden-import "time" ^ + --hidden-import "select" ^ + --hidden-import "platform" ^ + --distpath "dist/windows" ^ + --workpath "build/windows" ^ + --specpath "build" ^ + main_window.py +) else ( + echo Building without resources... + pyinstaller ^ + --onedir ^ + --windowed ^ + --name "SplitNotes" ^ + %ICON_PARAM% ^ + --hidden-import "tkinter" ^ + --hidden-import "tkinter.ttk" ^ + --hidden-import "tkinter.colorchooser" ^ + --hidden-import "tkinter.filedialog" ^ + --hidden-import "tkinter.messagebox" ^ + --hidden-import "socket" ^ + --hidden-import "threading" ^ + --hidden-import "json" ^ + --hidden-import "time" ^ + --hidden-import "select" ^ + --hidden-import "platform" ^ + --distpath "dist/windows" ^ + --workpath "build/windows" ^ + --specpath "build" ^ + main_window.py +) + +if errorlevel 1 ( + echo Build failed! + pause + exit /b 1 +) + +REM Copy additional files +echo Copying additional files... +if exist "README.md" copy "README.md" "dist\windows\SplitNotes\" 2>nul +if exist "LICENSE" copy "LICENSE" "dist\windows\SplitNotes\" 2>nul + +REM Create resources directory in dist and copy files if they exist +if not exist "dist\windows\SplitNotes\resources" mkdir "dist\windows\SplitNotes\resources" +if "%RESOURCES_EXIST%"=="true" ( + echo Copying resource files to distribution... + if exist "resources\*.png" copy "resources\*.png" "dist\windows\SplitNotes\resources\" 2>nul + if exist "resources\*.ico" copy "resources\*.ico" "dist\windows\SplitNotes\resources\" 2>nul + if exist "resources\*.cfg" copy "resources\*.cfg" "dist\windows\SplitNotes\resources\" 2>nul +) else ( + echo No resource files to copy... +) + +REM Create batch file for easy launching +echo @echo off > "dist\windows\SplitNotes\SplitNotes.bat" +echo cd /d "%%~dp0" >> "dist\windows\SplitNotes\SplitNotes.bat" +echo start "" "SplitNotes.exe" >> "dist\windows\SplitNotes\SplitNotes.bat" + +REM Create ZIP package +echo Creating ZIP package... +if exist "dist\SplitNotes-Windows.zip" del "dist\SplitNotes-Windows.zip" +powershell -command "Compress-Archive -Path 'dist\windows\SplitNotes\*' -DestinationPath 'dist\SplitNotes-Windows.zip'" + +if errorlevel 1 ( + echo Warning: Failed to create ZIP package. Please create manually. +) + +echo. +echo =============================================== +echo Build completed successfully! +echo. +echo Executable location: dist\windows\SplitNotes\SplitNotes.exe +echo ZIP package: dist\SplitNotes-Windows.zip +echo. +echo To distribute: +echo 1. Copy the entire 'dist\windows\SplitNotes' folder +echo 2. Or use the ZIP package +echo. +echo Requirements for target systems: +echo - Windows 7 or later +echo - No Python installation required +echo =============================================== +echo. + +REM Optional: Open the dist folder +set /p OPEN="Open build folder? (y/n): " +if /i "%OPEN%"=="y" explorer "dist\windows\SplitNotes" + +pause \ No newline at end of file diff --git a/build_linux.sh b/build_linux.sh new file mode 100644 index 0000000..54bdd96 --- /dev/null +++ b/build_linux.sh @@ -0,0 +1,371 @@ +#!/bin/bash +# Linux Build Script for SplitNotes +# Requires PyInstaller: pip install pyinstaller + +echo "Building SplitNotes for Linux..." +echo + +# Detect architecture +ARCH=$(uname -m) +case $ARCH in + x86_64) + ARCH_NAME="x64" + ;; + i386|i686) + ARCH_NAME="x86" + ;; + aarch64|arm64) + ARCH_NAME="arm64" + ;; + armv7l) + ARCH_NAME="armv7" + ;; + *) + ARCH_NAME="$ARCH" + ;; +esac + +echo "Detected architecture: $ARCH ($ARCH_NAME)" + +# Check if we're on Linux +if [[ "$OSTYPE" != "linux-gnu"* ]]; then + echo "Warning: This script is designed for Linux. Continuing anyway..." +fi + +# Check Python version +PYTHON_CMD="python3" +if ! command -v python3 &> /dev/null; then + if command -v python &> /dev/null; then + PYTHON_CMD="python" + else + echo "Error: Python not found. Please install Python 3.6 or later." + exit 1 + fi +fi + +PYTHON_VERSION=$($PYTHON_CMD --version 2>&1 | grep -Po '(?<=Python )[0-9]+\.[0-9]+') +if ! $PYTHON_CMD -c "import sys; sys.exit(0 if sys.version_info >= (3,6) else 1)"; then + echo "Error: Python 3.6 or later is required. Found: $($PYTHON_CMD --version)" + exit 1 +fi + +# Check if PyInstaller is installed +if ! $PYTHON_CMD -c "import PyInstaller" 2>/dev/null; then + echo "PyInstaller not found. Installing..." + $PYTHON_CMD -m pip install pyinstaller --user + if [ $? -ne 0 ]; then + echo "Failed to install PyInstaller. Trying with sudo..." + sudo $PYTHON_CMD -m pip install pyinstaller + if [ $? -ne 0 ]; then + echo "Failed to install PyInstaller. Please install manually:" + echo " pip3 install pyinstaller" + exit 1 + fi + fi +fi + +# Check for required system packages +echo "Checking system dependencies..." +MISSING_DEPS=() + +# Check for tkinter +if ! $PYTHON_CMD -c "import tkinter" 2>/dev/null; then + MISSING_DEPS+=("python3-tk") +fi + +if [ ${#MISSING_DEPS[@]} -gt 0 ]; then + echo "Missing dependencies detected:" + for dep in "${MISSING_DEPS[@]}"; do + echo " - $dep" + done + echo + echo "Install with your package manager:" + echo " Ubuntu/Debian: sudo apt install ${MISSING_DEPS[*]}" + echo " Fedora/RHEL: sudo dnf install python3-tkinter" + echo " Arch: sudo pacman -S tk" + echo " openSUSE: sudo zypper install python3-tk" + exit 1 +fi + +# Create build directories +mkdir -p build dist + +# Clean previous builds +echo "Cleaning previous builds..." +rm -rf build/* +rm -rf dist/* + +# Check if resources folder exists, create if not +if [ ! -d "resources" ]; then + echo "Creating resources folder..." + mkdir -p resources + echo "Note: Add icon files to resources folder for better appearance:" + echo " - app_icon.png (Linux icon, 256x256px recommended)" + echo " - green.png (connection active)" + echo " - red.png (connection inactive)" + echo " - settings_icon.png (settings window)" +fi + +# Build the application +echo "Building Linux executable..." + +# Build with or without resources +if [ -d "resources" ] && [ "$(ls -A resources 2>/dev/null)" ]; then + echo "Including resources folder in build..." + $PYTHON_CMD -m PyInstaller \ + --onedir \ + --console \ +else + echo "Building without resources folder..." + $PYTHON_CMD -m PyInstaller \ + --onedir \ + --console \ + --name "SplitNotes" \ + --hidden-import "tkinter" \ + --hidden-import "tkinter.ttk" \ + --hidden-import "tkinter.colorchooser" \ + --hidden-import "tkinter.filedialog" \ + --hidden-import "tkinter.messagebox" \ + --hidden-import "socket" \ + --hidden-import "threading" \ + --hidden-import "json" \ + --hidden-import "time" \ + --hidden-import "select" \ + --hidden-import "platform" \ + --distpath "dist/linux" \ + --workpath "build/linux" \ + --specpath "build" \ + main_window.py +fi + --add-data "resources:resources" \ + --hidden-import "tkinter" \ + --hidden-import "tkinter.ttk" \ + --hidden-import "tkinter.colorchooser" \ + --hidden-import "tkinter.filedialog" \ + --hidden-import "tkinter.messagebox" \ + --hidden-import "socket" \ + --hidden-import "threading" \ + --hidden-import "json" \ + --hidden-import "time" \ + --hidden-import "select" \ + --hidden-import "platform" \ + --distpath "dist/linux" \ + --workpath "build/linux" \ + --specpath "build" \ + main_window.py +else + echo "Building without resources folder..." + $PYTHON_CMD -m PyInstaller \ + --onedir \ + --console \ + --name "SplitNotes" \ + --hidden-import "tkinter" \ + --hidden-import "tkinter.ttk" \ + --hidden-import "tkinter.colorchooser" \ + --hidden-import "tkinter.filedialog" \ + --hidden-import "tkinter.messagebox" \ + --hidden-import "socket" \ + --hidden-import "threading" \ + --hidden-import "json" \ + --hidden-import "time" \ + --hidden-import "select" \ + --hidden-import "platform" \ + --distpath "dist/linux" \ + --workpath "build/linux" \ + --specpath "build" \ + main_window.py + +if [ $? -ne 0 ]; then + echo "Build failed!" + exit 1 +fi + +# Create proper directory structure +DIST_DIR="dist/linux/SplitNotes-linux-$ARCH_NAME" +mkdir -p "$DIST_DIR" + +# Move the built application +mv "dist/linux/SplitNotes"/* "$DIST_DIR/" +rmdir "dist/linux/SplitNotes" + +# Copy additional files +echo "Copying additional files..." +cp README.md "$DIST_DIR/" 2>/dev/null || true +cp LICENSE "$DIST_DIR/" 2>/dev/null || true + +# Copy resources if they exist +if [ "$RESOURCES_EXIST" = true ]; then + echo "Copying resource files to distribution..." + cp -r resources "$DIST_DIR/" 2>/dev/null || true +else + echo "No resource files to copy..." + # Create empty resources directory for app to use + mkdir -p "$DIST_DIR/resources" +fi + +# Create launch script +cat > "$DIST_DIR/splitnotes.sh" << 'EOF' +#!/bin/bash +# SplitNotes Launch Script + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" + +# Change to the script directory +cd "$SCRIPT_DIR" + +# Set library path for the executable +export LD_LIBRARY_PATH="$SCRIPT_DIR:$LD_LIBRARY_PATH" + +# Launch SplitNotes +if [ -f "./SplitNotes" ]; then + ./SplitNotes "$@" +else + echo "Error: SplitNotes executable not found!" + exit 1 +fi +EOF + +# Make scripts executable +chmod +x "$DIST_DIR/splitnotes.sh" +chmod +x "$DIST_DIR/SplitNotes" + +# Create desktop entry +cat > "$DIST_DIR/SplitNotes.desktop" << EOF +[Desktop Entry] +Type=Application +Name=SplitNotes +Comment=LiveSplit notes synchronization tool +Exec=$DIST_DIR/splitnotes.sh +Icon=$DIST_DIR/resources/app_icon.png +Terminal=false +Categories=Utility;Game; +StartupWMClass=SplitNotes +EOF + +# Create installation script +cat > "$DIST_DIR/install.sh" << 'EOF' +#!/bin/bash +# SplitNotes Installation Script + +INSTALL_DIR="$HOME/.local/share/SplitNotes" +BIN_DIR="$HOME/.local/bin" +DESKTOP_DIR="$HOME/.local/share/applications" + +echo "Installing SplitNotes..." + +# Create directories +mkdir -p "$INSTALL_DIR" +mkdir -p "$BIN_DIR" +mkdir -p "$DESKTOP_DIR" + +# Copy files +cp -r * "$INSTALL_DIR/" + +# Create symlink in bin directory +ln -sf "$INSTALL_DIR/splitnotes.sh" "$BIN_DIR/splitnotes" + +# Install desktop entry +cp "$INSTALL_DIR/SplitNotes.desktop" "$DESKTOP_DIR/" +sed -i "s|$PWD|$INSTALL_DIR|g" "$DESKTOP_DIR/SplitNotes.desktop" + +# Update desktop database +if command -v update-desktop-database &> /dev/null; then + update-desktop-database "$DESKTOP_DIR" +fi + +echo "Installation completed!" +echo "SplitNotes installed to: $INSTALL_DIR" +echo "You can now run 'splitnotes' from terminal or find it in your applications menu." +EOF + +chmod +x "$DIST_DIR/install.sh" + +# Create AppImage (if appimagetool is available) +echo "Checking for AppImage creation tools..." +if command -v appimagetool &> /dev/null; then + echo "Creating AppImage..." + + APPDIR="dist/SplitNotes.AppDir" + mkdir -p "$APPDIR/usr/bin" + mkdir -p "$APPDIR/usr/share/applications" + mkdir -p "$APPDIR/usr/share/icons/hicolor/256x256/apps" + + # Copy files to AppDir + cp -r "$DIST_DIR"/* "$APPDIR/usr/bin/" + + # Create AppRun + cat > "$APPDIR/AppRun" << 'EOF' +#!/bin/bash +SELF=$(readlink -f "$0") +HERE=${SELF%/*} +export PATH="${HERE}/usr/bin/:${PATH}" +export LD_LIBRARY_PATH="${HERE}/usr/bin/:${LD_LIBRARY_PATH}" +cd "${HERE}/usr/bin" +exec ./SplitNotes "$@" +EOF + chmod +x "$APPDIR/AppRun" + + # Copy desktop file and icon + cp "$DIST_DIR/SplitNotes.desktop" "$APPDIR/" + if [ -f "resources/app_icon.png" ]; then + cp "resources/app_icon.png" "$APPDIR/splitnotes.png" + cp "resources/app_icon.png" "$APPDIR/usr/share/icons/hicolor/256x256/apps/" + fi + + # Update desktop file paths + sed -i 's|Exec=.*|Exec=SplitNotes|g' "$APPDIR/SplitNotes.desktop" + sed -i 's|Icon=.*|Icon=splitnotes|g' "$APPDIR/SplitNotes.desktop" + + # Create AppImage + appimagetool "$APPDIR" "dist/SplitNotes-linux-$ARCH_NAME.AppImage" + + if [ $? -eq 0 ]; then + echo "AppImage created successfully!" + chmod +x "dist/SplitNotes-linux-$ARCH_NAME.AppImage" + fi +fi + +# Create tarball +echo "Creating tarball..." +cd dist/linux +tar -czf "../SplitNotes-linux-$ARCH_NAME.tar.gz" "SplitNotes-linux-$ARCH_NAME" +cd ../.. + +echo +echo "===============================================" +echo "Build completed successfully!" +echo +echo "Executable location: $DIST_DIR/SplitNotes" +echo "Launch script: $DIST_DIR/splitnotes.sh" +echo "Tarball: dist/SplitNotes-linux-$ARCH_NAME.tar.gz" +if [ -f "dist/SplitNotes-linux-$ARCH_NAME.AppImage" ]; then + echo "AppImage: dist/SplitNotes-linux-$ARCH_NAME.AppImage" +fi +echo +echo "To install locally:" +echo " cd '$DIST_DIR' && ./install.sh" +echo +echo "To distribute:" +echo "1. Copy the entire '$DIST_DIR' folder" +echo "2. Or use the tarball/AppImage" +echo +echo "Requirements for target systems:" +echo "- Linux with glibc 2.17+ (RHEL 7+, Ubuntu 14.04+)" +echo "- X11 display server" +echo "- No Python installation required" +echo "===============================================" +echo + +# Optional: Open the dist folder +read -p "Open build folder? (y/n): " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + if command -v xdg-open &> /dev/null; then + xdg-open "dist/linux" + elif command -v nautilus &> /dev/null; then + nautilus "dist/linux" + else + echo "Build folder: $(pwd)/dist/linux" + fi +fi \ No newline at end of file diff --git a/build_macos.sh b/build_macos.sh new file mode 100644 index 0000000..4fc8fe3 --- /dev/null +++ b/build_macos.sh @@ -0,0 +1,270 @@ +#!/bin/bash +# macOS Build Script for SplitNotes +# Requires PyInstaller: pip install pyinstaller + +echo "Building SplitNotes for macOS..." +echo + +# Check if we're on macOS +if [[ "$OSTYPE" != "darwin"* ]]; then + echo "Error: This script is for macOS only." + exit 1 +fi + +# Check if PyInstaller is installed +if ! python3 -c "import PyInstaller" 2>/dev/null; then + echo "PyInstaller not found. Installing..." + pip3 install pyinstaller + if [ $? -ne 0 ]; then + echo "Failed to install PyInstaller. Please install manually: pip3 install pyinstaller" + exit 1 + fi +fi + +# Create build directories +mkdir -p build dist + +# Clean previous builds +echo "Cleaning previous builds..." +rm -rf build/* +rm -rf dist/* + +# Check if resources folder exists, create if not +if [ ! -d "resources" ]; then + echo "Creating resources folder..." + mkdir -p resources + echo "Note: Add icon files to resources folder for better appearance:" + echo " - app_icon.icns (macOS icon)" + echo " - green.png (connection active)" + echo " - red.png (connection inactive)" + echo " - settings_icon.png (settings window)" +fi + +# Build the application +echo "Building macOS app bundle..." + +# Set icon parameter if icon exists +if [ -f "resources/app_icon.icns" ]; then + echo "Found app icon, including in build..." + ICON_PARAM="--icon=resources/app_icon.icns" +else + echo "No app icon found, building without custom icon..." + ICON_PARAM="" +fi + +# Build with or without resources +if [ -d "resources" ] && [ "$(ls -A resources 2>/dev/null)" ]; then + echo "Including resources folder in build..." + python3 -m PyInstaller \ + --onedir \ + --windowed \ + --name "SplitNotes" \ +else + echo "Building without resources folder..." + python3 -m PyInstaller \ + --onedir \ + --windowed \ + --name "SplitNotes" \ + $ICON_PARAM \ + --hidden-import "tkinter" \ + --hidden-import "tkinter.ttk" \ + --hidden-import "tkinter.colorchooser" \ + --hidden-import "tkinter.filedialog" \ + --hidden-import "tkinter.messagebox" \ + --hidden-import "socket" \ + --hidden-import "threading" \ + --hidden-import "json" \ + --hidden-import "time" \ + --hidden-import "select" \ + --hidden-import "platform" \ + --distpath "dist/macos" \ + --workpath "build/macos" \ + --specpath "build" \ + main_window.py +fi + --add-data "resources:resources" \ + --hidden-import "tkinter" \ + --hidden-import "tkinter.ttk" \ + --hidden-import "tkinter.colorchooser" \ + --hidden-import "tkinter.filedialog" \ + --hidden-import "tkinter.messagebox" \ + --hidden-import "socket" \ + --hidden-import "threading" \ + --hidden-import "json" \ + --hidden-import "time" \ + --hidden-import "select" \ + --hidden-import "platform" \ + --distpath "dist/macos" \ + --workpath "build/macos" \ + --specpath "build" \ + main_window.py +else + echo "Building without resources folder..." + python3 -m PyInstaller \ + --onedir \ + --windowed \ + --name "SplitNotes" \ + $ICON_PARAM \ + --hidden-import "tkinter" \ + --hidden-import "tkinter.ttk" \ + --hidden-import "tkinter.colorchooser" \ + --hidden-import "tkinter.filedialog" \ + --hidden-import "tkinter.messagebox" \ + --hidden-import "socket" \ + --hidden-import "threading" \ + --hidden-import "json" \ + --hidden-import "time" \ + --hidden-import "select" \ + --hidden-import "platform" \ + --distpath "dist/macos" \ + --workpath "build/macos" \ + --specpath "build" \ + main_window.py + +if [ $? -ne 0 ]; then + echo "Build failed!" + exit 1 +fi + +# Copy additional files to the app bundle +echo "Copying additional files..." +APP_DIR="dist/macos/SplitNotes.app" +CONTENTS_DIR="$APP_DIR/Contents" +RESOURCES_DIR="$CONTENTS_DIR/Resources" + +# Create proper app bundle structure +mkdir -p "$RESOURCES_DIR" + +# Copy resources if they exist +if [ "$RESOURCES_EXIST" = true ]; then + echo "Copying resource files to app bundle..." + cp -r resources/* "$RESOURCES_DIR/" 2>/dev/null || true +else + echo "No resource files to copy..." + # Create empty resources directory for app to use + mkdir -p "$RESOURCES_DIR" +fi + +# Copy documentation +cp README.md "$RESOURCES_DIR/" 2>/dev/null || true +cp LICENSE "$RESOURCES_DIR/" 2>/dev/null || true + +# Create Info.plist if it doesn't exist +INFO_PLIST="$CONTENTS_DIR/Info.plist" +if [ ! -f "$INFO_PLIST" ]; then + cat > "$INFO_PLIST" << EOF + + + + + CFBundleName + SplitNotes + CFBundleDisplayName + SplitNotes + CFBundleIdentifier + com.apfelteesaft.splitnotes + CFBundleVersion + 1.1.0 + CFBundleShortVersionString + 1.1.0 + CFBundleExecutable + SplitNotes + CFBundleIconFile + app_icon.icns + CFBundlePackageType + APPL + CFBundleSignature + ???? + LSMinimumSystemVersion + 10.13 + NSHighResolutionCapable + + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + +EOF +fi + +# Set correct permissions +chmod +x "$APP_DIR/Contents/MacOS/SplitNotes" + +# Code signing (optional, requires Apple Developer account) +echo "Checking for code signing..." +if command -v codesign >/dev/null 2>&1; then + echo "Note: For distribution, you may want to code sign the app:" + echo " codesign --force --sign 'Developer ID Application: Your Name' '$APP_DIR'" + echo " Use --deep flag for embedded frameworks" +fi + +# Create DMG (requires create-dmg tool) +echo "Creating DMG package..." +if command -v create-dmg >/dev/null 2>&1; then + # Use icon if available, otherwise skip + if [ -f "resources/app_icon.icns" ]; then + VOLICON_PARAM="--volicon resources/app_icon.icns" + else + VOLICON_PARAM="" + fi + + create-dmg \ + --volname "SplitNotes" \ + $VOLICON_PARAM \ + --window-pos 200 120 \ + --window-size 600 400 \ + --icon-size 100 \ + --icon "SplitNotes.app" 175 120 \ + --hide-extension "SplitNotes.app" \ + --app-drop-link 425 120 \ + "dist/SplitNotes-macOS.dmg" \ + "dist/macos/" + + if [ $? -eq 0 ]; then + echo "DMG created successfully!" + else + echo "DMG creation failed. Creating ZIP instead..." + cd dist/macos + zip -r "../SplitNotes-macOS.zip" "SplitNotes.app" + cd ../.. + fi +else + echo "create-dmg not found. Creating ZIP package..." + cd dist/macos + zip -r "../SplitNotes-macOS.zip" "SplitNotes.app" + cd ../.. +fi + +echo +echo "===============================================" +echo "Build completed successfully!" +echo +echo "App bundle location: dist/macos/SplitNotes.app" +if [ -f "dist/SplitNotes-macOS.dmg" ]; then + echo "DMG package: dist/SplitNotes-macOS.dmg" +fi +if [ -f "dist/SplitNotes-macOS.zip" ]; then + echo "ZIP package: dist/SplitNotes-macOS.zip" +fi +echo +echo "To distribute:" +echo "1. Copy the SplitNotes.app bundle" +echo "2. Or use the DMG/ZIP package" +echo +echo "Requirements for target systems:" +echo "- macOS 10.13 (High Sierra) or later" +echo "- No Python installation required" +echo +echo "Note: For distribution outside the App Store," +echo "you may need to code sign and notarize the app." +echo "===============================================" +echo + +# Optional: Open the dist folder +read -p "Open build folder? (y/n): " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + open "dist/macos" +fi \ No newline at end of file diff --git a/config.py b/config.py index ce2c9cb..71f8c2e 100644 --- a/config.py +++ b/config.py @@ -1,4 +1,4 @@ -# CONFIG FILE WITH CONSTANTS +# CONFIG FILE WITH CONSTANTS - Clean version with Bridge Server Support import platform import os @@ -6,6 +6,11 @@ import os HOST = "localhost" PORT = 16834 +# Bridge server settings +BRIDGE_HOST = "localhost" +BRIDGE_PORT = 16835 +BRIDGE_ENABLED = False + # In network communication, time out after this time. (in seconds) COM_TIMEOUT = 0.5 @@ -33,21 +38,27 @@ SETTINGS_FILE = "config.cfg" # Default Scrollbar Width SCROLLBAR_WIDTH = 16 -# Popup menu options +# Popup menu options - Enhanced with bridge settings MENU_OPTIONS = { "SINGLE": "Set Single Layout", "DOUBLE": "Set Double Layout", "LOAD": "Load Notes", "BIG": "Big Font", "SMALL": "Small Font", - "SETTINGS": "Settings" + "SETTINGS": "Settings", + "BRIDGE": "Bridge Settings" } -# Error messages -ERRORS = {"NOTES_EMPTY": ("Error!", "Notes empty or can't be loaded!"), - "FONT_SIZE": ("Error!", "Invalid Font Size!"), - "SERVER_PORT": ("Error!", "Invalid server port!"), - "SEPARATOR": ("Error!", "Invalid split separator!")} +# Error messages - Enhanced with bridge server errors +ERRORS = { + "NOTES_EMPTY": ("Error!", "Notes empty or can't be loaded!"), + "FONT_SIZE": ("Error!", "Invalid Font Size!"), + "SERVER_PORT": ("Error!", "Invalid server port!"), + "SEPARATOR": ("Error!", "Invalid split separator!"), + "BRIDGE_PORT": ("Error!", "Invalid bridge server port!"), + "BRIDGE_START": ("Error!", "Failed to start bridge server!"), + "BRIDGE_CONNECTION": ("Error!", "Bridge server connection failed!") +} # Max file size for notes MAX_FILE_SIZE = 1000000000 # 1 Giga-Byte @@ -64,23 +75,37 @@ TEXT_FILES = [ ('All', '*') ] -# Default content of config.cfg file -DEFAULT_CONFIG = "notes=\nfont_size=12\nfont=arial\ntext_color=#000000\nbackground_color=#FFFFFF\ndouble_layout=False\nserver_port=16834\nwidth=400\nheight=300\nseparator=new_line" +# Default content of config.cfg file - Enhanced with bridge settings +DEFAULT_CONFIG = """notes= +font_size=12 +font=arial +text_color=#000000 +background_color=#FFFFFF +double_layout=False +server_port=16834 +width=400 +height=300 +separator=new_line +bridge_enabled=False +bridge_port=16835""" NEWLINE_CONSTANT = "new_line" -# Required settings -REQUIRED_SETTINGS = ("notes", - "font", - "font_size", - "text_color", - "background_color", - "server_port", - "double_layout", - "width", - "height", - "separator" - ) +# Required settings - Enhanced with bridge settings +REQUIRED_SETTINGS = ( + "notes", + "font", + "font_size", + "text_color", + "background_color", + "server_port", + "double_layout", + "width", + "height", + "separator", + "bridge_enabled", + "bridge_port" +) # Settings window options SETTINGS_WINDOW = {"TITLE": "Settings", @@ -89,6 +114,15 @@ SETTINGS_WINDOW = {"TITLE": "Settings", "CANCEL": "Cancel", "SAVE": "Save"} +# Bridge settings window options +BRIDGE_SETTINGS_WINDOW = { + "TITLE": "Bridge Server Settings", + "WIDTH": 500, + "HEIGHT": 400, + "CANCEL": "Cancel", + "APPLY": "Apply" +} + # OPTIONS IN THE SETTINGS WINDOW SETTINGS_OPTIONS = {"FONT": "Font", "FONT_SIZE": "Font Size", @@ -100,6 +134,34 @@ SETTINGS_OPTIONS = {"FONT": "Font", "NEW_LINE_SEPARATOR": "Newline as split separator", "CUSTOM_SEPARATOR": "Custom split separator"} +# Bridge server settings options +BRIDGE_OPTIONS = { + "ENABLE_BRIDGE": "Enable Bridge Server", + "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: + +1. CHROME/CHROMIUM: + • Go to chrome://extensions/ + • Enable Developer Mode + • Click "Load unpacked" + • Select the Chrome extension folder + +2. FIREFOX: + • Go to about:debugging + • Click "This Firefox" → "Load Temporary Add-on" + • Select manifest.json from Firefox extension folder + +3. USAGE: + • Start SplitNotes with bridge enabled + • Open https://one.livesplit.org/ + • Load your splits and start timing + • Notes will automatically sync""" +} + # Platform-specific fonts def get_available_fonts(): """Returns available fonts based on the platform.""" @@ -147,8 +209,71 @@ 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" +} + +# 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 = { + "TIMER_STATE": "timer_state", + "SPLITS_UPDATED": "splits_updated", + "CONNECTION_TEST": "connection_test", + "STATUS_REQUEST": "status_request", + "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.0.0" +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." \ No newline at end of file +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 diff --git a/main_window.py b/main_window.py index 5e40a09..391057a 100644 --- a/main_window.py +++ b/main_window.py @@ -1,6 +1,9 @@ import tkinter -from tkinter import messagebox - +from tkinter import messagebox, ttk +import json +import socket +import threading +import time import os import sys import platform @@ -10,6 +13,7 @@ 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, @@ -18,17 +22,20 @@ runtime_info = { "server_port": 0, "force_reset": False, "double_layout": False, - "settings": {} + "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): - # Running as compiled executable application_path = os.path.dirname(sys.executable) else: - # Running as script application_path = os.path.dirname(os.path.realpath(__file__)) red_path = os.path.join(application_path, config.RESOURCE_FOLDER, config.ICONS["RED"]) @@ -47,74 +54,451 @@ 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() + + 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.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() + + print(f"Bridge server started on {self.host}:{self.port}") + return True + + except Exception as e: + print(f"Failed to start bridge server: {e}") + return False + + def stop(self): + """Stop the bridge server""" + self.running = False + + # Close all client connections + for client in self.clients[:]: + try: + client.close() + except: + pass + self.clients.clear() + + # Close server socket + if self.server_socket: + try: + self.server_socket.close() + except: + pass + + 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}") + + self.clients.append(client) + client_thread = threading.Thread( + target=self._handle_client, + args=(client,), + daemon=True + ) + client_thread.start() + + except Exception as e: + if self.running: + print(f"Error accepting connection: {e}") + + def _handle_client(self, client): + """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}") + + except Exception as e: + print(f"Error handling client: {e}") + finally: + try: + client.close() + except: + pass + if client in self.clients: + self.clients.remove(client) + print("Browser client 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") + 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' # Add newline for better parsing + disconnected_clients = [] + + for client in self.clients: + 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 update(window, com_socket, text1, text2): - """ - Function to loop along tkinter mainloop. - """ + """Enhanced update function with TCP bridge server support""" if runtime_info["force_reset"]: - # Boolean flag to force a connection reset com_socket = reset_connection(com_socket, window, text1, text2) runtime_info["force_reset"] = False elif not runtime_info["ls_connected"]: - # try connecting to ls + # 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 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: - # is_connected + # LiveSplit desktop is connected, use normal logic if runtime_info["notes"]: - # notes loaded - - # get index of current split 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: - # index retrieved successfully if new_index == -1: - # timer not running + # 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 + # Timer is running if not runtime_info["timer_running"]: runtime_info["timer_running"] = True - - # special case to fix scrolling + # Special case to fix scrolling if runtime_info["active_split"] == 0: runtime_info["active_split"] = -1 - if not runtime_info["active_split"] == new_index: - # new split, need to update + 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 + # Notes not yet loaded com_socket = test_connection(com_socket, window, text1, text2) - # self looping + # Continue main loop 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 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. - Sets window title and Text-box content. - Does NOT set window icon. - """ + """Updates all graphics according to current runtime_info""" index = runtime_info["active_split"] if index == -1: index = 0 if runtime_info["timer_running"]: - # Does not test connection if it fails - split_name = con.get_split_name(com_socket) + split_name = con.get_split_name(com_socket) if runtime_info["ls_connected"] else "" else: split_name = False @@ -126,11 +510,7 @@ def update_GUI(window, com_socket, text1, text2): def test_connection(com_socket, window, text1, text2): - """ - Runs a connection test to ls using given socket. - If test is unsuccessful, resets connection. - Returns a socket that should be used for communication with ls. - """ + """Runs a connection test to LiveSplit desktop""" if con.check_connection(com_socket): return com_socket else: @@ -138,11 +518,7 @@ def test_connection(com_socket, window, text1, text2): def reset_connection(com_socket, window, text1, text2): - """ - Resets all variables and closes given socket. - Updates GUI to respond to connection loss. - Returns a fresh socket that can be used to connect to ls. - """ + """Resets LiveSplit desktop connection""" if runtime_info["timer_running"]: runtime_info["timer_running"] = False runtime_info["active_split"] = -1 @@ -152,42 +528,45 @@ def reset_connection(com_socket, window, text1, text2): update_icon(False, window) update_GUI(window, com_socket, text1, text2) - # Close old and return a fresh socket con.close_socket(com_socket) - return con.init_socket() def server_found(window): - """ - Executes correct settings for when - ls connection has been established. - """ + """Executes when LiveSplit desktop connection is established""" runtime_info["ls_connected"] = True update_icon(True, window) def update_icon(active, window): - """Updates icon of window depending on "active" variable""" + """Updates icon with TCP bridge server status consideration""" try: - if active and green_icon: + # 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) + + if (active or bridge_active) and green_icon: window.iconphoto(False, green_icon) - elif not active and red_icon: + elif red_icon: window.iconphoto(False, red_icon) except Exception: - pass # Icon update failed, continue without icon + pass def update_title(name, window): - """Sets the title of given window to name.""" - window.wm_title(name) + """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 size of box1 and box2 according to - layout and size of window. - """ + """Adjusts content layout""" if runtime_info["double_layout"]: set_double_layout(window, box1, box2) else: @@ -195,9 +574,7 @@ def adjust_content(window, box1, box2): def set_double_layout(window, box1, box2): - """ - Configures boxes in the window to fit as in double layout. - """ + """Configures boxes for double layout""" runtime_info["double_layout"] = True w_width = window.winfo_width() @@ -208,9 +585,7 @@ def set_double_layout(window, box1, box2): def set_single_layout(window, box1, box2): - """ - Configures boxes in the window to fit as in single layout. - """ + """Configures boxes for single layout""" runtime_info["double_layout"] = False box2.place_forget() @@ -218,7 +593,7 @@ def set_single_layout(window, box1, box2): def show_popup(event, menu): - """Displays given popup menu at cursor position.""" + """Displays popup menu""" try: menu.post(event.x_root, event.y_root) except Exception: @@ -226,30 +601,26 @@ def show_popup(event, menu): def menu_load_notes(window, text1, text2, com_socket): - """Menu selected load notes option.""" + """Menu load notes option""" load_notes(window, text1, text2, com_socket) def load_notes(window, text1, text2, com_socket): - """ - Prompts user to select notes and then tries to load these into the UI. - """ + """Prompts user to select and load notes""" file = noter.select_file() if file: notes = noter.get_notes(file, runtime_info["settings"]["separator"]) if notes: - # Notes loaded correctly runtime_info["notes"] = notes - # Save notes to settings settings = setting_handler.load_settings() settings["notes"] = file setting_handler.save_settings(settings) split_c = len(notes) show_info(("Notes Loaded", - ("Loaded notes with " + str(split_c) + " splits."))) + f"Loaded notes with {split_c} splits.")) if not runtime_info["timer_running"]: runtime_info["active_split"] = -1 @@ -261,10 +632,7 @@ def load_notes(window, text1, text2, com_socket): def show_info(info, warning=False): - """ - Displays an info popup window. - if warning is True window has a warning triangle. - """ + """Displays info popup""" try: if warning: messagebox.showwarning(info[0], info[1]) @@ -275,15 +643,8 @@ def show_info(info, warning=False): def update_notes(text1, text2, index): - """ - Displays notes with the given index in given text widgets. - If index is lower than 0, displays notes for index 0. - If index is higher than the highest index there are - notes for the text widgets are left empty. - - text2 is always given the notes at index (index + 1) if existing - """ - max_index = (len(runtime_info["notes"]) - 1) + """Displays notes with given index""" + max_index = len(runtime_info["notes"]) - 1 if index < 0: index = 0 @@ -297,7 +658,6 @@ def update_notes(text1, text2, index): if index <= max_index: text1.insert(tkinter.END, runtime_info["notes"][index]) - # can't display notes for index+1 if index < max_index: text2.insert(tkinter.END, runtime_info["notes"][index + 1]) @@ -306,22 +666,19 @@ def update_notes(text1, text2, index): def right_arrow(window, com_socket, text1, text2): - """Event handler for right arrow key.""" + """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.""" + """Event handler for left arrow key""" change_preview(window, com_socket, text1, text2, -1) def change_preview(window, com_socket, text1, text2, move): - """ - Changes notes that are currently displayed. - Move is either 1 for next or -1 for previous. - """ - if runtime_info["notes"] and (not runtime_info["timer_running"]): - max_index = (len(runtime_info["notes"]) - 1) + """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: @@ -335,14 +692,11 @@ def change_preview(window, com_socket, text1, text2, move): 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. - """ + """Set window title to fit with displayed notes""" title = config.DEFAULT_WINDOW["TITLE"] disp_index = str(index + 1) # start at 1 @@ -358,9 +712,7 @@ def set_title_notes(window, index, split_name=False): def menu_open_settings(root_wnd, box1, box2, text1, text2, com_socket): - """ - Opens the settings menu. - """ + """Opens the settings menu""" setting_handler.edit_settings(root_wnd, lambda settings: apply_settings(settings, root_wnd, @@ -369,14 +721,11 @@ def menu_open_settings(root_wnd, box1, box2, text1, text2, com_socket): def apply_settings(settings, window, box1, box2, text1, text2, com_socket): - """ - Applies the given settings to the given components. - Settings must be a correctly formatted dictionary. - """ + """Applies settings to the application""" runtime_info["settings"] = settings # Server port change - if not (runtime_info["server_port"] == int(settings["server_port"])): + if runtime_info["server_port"] != int(settings["server_port"]): runtime_info["server_port"] = int(settings["server_port"]) runtime_info["force_reset"] = True @@ -398,14 +747,13 @@ def apply_settings(settings, window, box1, box2, text1, text2, com_socket): new_notes = noter.get_notes(settings["notes"], settings["separator"]) if new_notes: - # Notes loaded correctly runtime_info["notes"] = new_notes new_note_length = len(new_notes) - if not (new_note_length == old_note_length): + if new_note_length != old_note_length: show_info(("Notes Loaded", - ("Loaded notes with " + str(new_note_length) + " splits."))) + f"Loaded notes with {new_note_length} splits.")) if not runtime_info["timer_running"]: runtime_info["active_split"] = -1 @@ -416,9 +764,7 @@ def apply_settings(settings, window, box1, box2, text1, text2, com_socket): def save_geometry_settings(width, height): - """ - Saves given width and height to settings file. - """ + """Saves geometry settings""" settings = setting_handler.load_settings() settings["width"] = str(width) settings["height"] = str(height) @@ -426,33 +772,45 @@ def save_geometry_settings(width, height): def do_on_close(root_wnd): - """ - Function that is called when the main tk window is closed. - Saves root_wnd's width and height to the settings file and - then closes the window. - """ + """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): - """Draws default UI and creates event bindings.""" - - # Create communication socket + """Initialize UI with TCP bridge server integration""" com_socket = con.init_socket() - # Load Settings + # Load Settings (including bridge 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")) + + # Start TCP bridge server if enabled + if runtime_info["bridge_enabled"]: + 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") + else: + print("Failed to start TCP bridge server") # Graphical components root.geometry(settings["width"] + "x" + settings["height"]) - - # Set minimum window size root.minsize(300, 200) box1 = tkinter.Frame(root) @@ -486,7 +844,7 @@ def init_UI(root): scroll1.config(command=text1.yview) scroll2.config(command=text2.yview) - # Set font and color for text + # Set font and color text_font = (settings["font"], int(settings["font_size"])) text1.config(font=text_font) @@ -499,7 +857,7 @@ def init_UI(root): else: set_single_layout(root, box1, box2) - # create popup menu + # Create popup menu with TCP bridge settings popup = tkinter.Menu(root, tearoff=0) popup.add_command( label=config.MENU_OPTIONS["LOAD"], @@ -509,6 +867,11 @@ def init_UI(root): 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) @@ -538,25 +901,23 @@ def init_UI(root): # Window close bind root.protocol("WM_DELETE_WINDOW", lambda: do_on_close(root)) - # call update loop + # Call update loop update(root, com_socket, text1, text2) def main(): - """Main entry point for the application.""" + """Main entry point with TCP bridge server support""" try: - # Set up the main window root.title(config.DEFAULT_WINDOW["TITLE"]) # Platform-specific optimizations if config.IS_MACOS: - # Use native look on macOS try: root.tk.call('tk', 'scaling', 1.0) except: pass - # Initialize UI + # Initialize UI with TCP bridge server init_UI(root) # Start the main loop @@ -568,6 +929,9 @@ def main(): 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: diff --git a/setting_handler.py b/setting_handler.py index 76115bf..7b5c2cf 100644 --- a/setting_handler.py +++ b/setting_handler.py @@ -6,6 +6,7 @@ import sys import config + # Cross-platform path handling if getattr(sys, 'frozen', False): # Running as compiled executable @@ -173,6 +174,7 @@ def edit_settings(root_wnd, apply_method): y = (settings_wnd.winfo_screenheight() // 2) - (config.SETTINGS_WINDOW['HEIGHT'] // 2) settings_wnd.geometry(f"+{x}+{y}") + # Create labels font_label = tkinter.Label(settings_wnd, text=config.SETTINGS_OPTIONS["FONT"], font=config.GUI_FONT) @@ -333,28 +335,47 @@ def edit_settings(root_wnd, apply_method): text=config.SETTINGS_WINDOW["CANCEL"], font=config.GUI_FONT) - # Place all components - font_label.place(x=15, y=15) - font_size_label.place(x=15, y=55) - text_color_label.place(x=15, y=95) - bg_color_label.place(x=15, y=135) - layout_label.place(x=15, y=175) - newline_label.place(x=15, y=215) - separator_label.place(x=15, y=240) - port_label.place(x=15, y=280) - default_port_label.place(x=15, y=305) + # Place all components with updated positions + y_offset = 15 + font_label.place(x=15, y=y_offset) + y_offset += 40 + font_size_label.place(x=15, y=y_offset) + y_offset += 40 + text_color_label.place(x=15, y=y_offset) + y_offset += 40 + bg_color_label.place(x=15, y=y_offset) + y_offset += 40 + layout_label.place(x=15, y=y_offset) + y_offset += 25 + newline_label.place(x=15, y=y_offset) + y_offset += 25 + separator_label.place(x=15, y=y_offset) + y_offset += 40 + port_label.place(x=15, y=y_offset) + y_offset += 25 + default_port_label.place(x=15, y=y_offset) - font_dropdown.place(x=208, y=15) - font_size_entry.place(x=210, y=55) - text_color.place(x=210, y=95) - bg_color.place(x=210, y=135) - double_layout_btn.place(x=210, y=175) - newline_btn.place(x=210, y=215) - separator_entry.place(x=210, y=240) - port_entry.place(x=210, y=280) + # Place input controls + y_offset = 15 + font_dropdown.place(x=208, y=y_offset) + y_offset += 40 + font_size_entry.place(x=210, y=y_offset) + y_offset += 40 + text_color.place(x=210, y=y_offset) + y_offset += 40 + bg_color.place(x=210, y=y_offset) + y_offset += 40 + double_layout_btn.place(x=210, y=y_offset) + y_offset += 25 + newline_btn.place(x=210, y=y_offset) + y_offset += 25 + separator_entry.place(x=210, y=y_offset) + y_offset += 40 + port_entry.place(x=210, y=y_offset) - save_btn.place(x=110, y=350) - cancel_btn.place(x=190, y=350) + # Place buttons at bottom + save_btn.place(x=110, y=450) + cancel_btn.place(x=190, y=450) # Handle window close def on_closing(): diff --git a/setup_windos.py b/setup_windows.py similarity index 100% rename from setup_windos.py rename to setup_windows.py