This commit is contained in:
ApfelTeeSaft
2025-08-03 11:25:10 +02:00
parent ca2a6d93a6
commit 59f075c247
7 changed files with 1497 additions and 173 deletions
+173
View File
@@ -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
+371
View File
@@ -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
+270
View File
@@ -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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>SplitNotes</string>
<key>CFBundleDisplayName</key>
<string>SplitNotes</string>
<key>CFBundleIdentifier</key>
<string>com.apfelteesaft.splitnotes</string>
<key>CFBundleVersion</key>
<string>1.1.0</string>
<key>CFBundleShortVersionString</key>
<string>1.1.0</string>
<key>CFBundleExecutable</key>
<string>SplitNotes</string>
<key>CFBundleIconFile</key>
<string>app_icon.icns</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>LSMinimumSystemVersion</key>
<string>10.13</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>
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
+149 -24
View File
@@ -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."
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"]
+493 -129
View File
@@ -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:
+41 -20
View File
@@ -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():