mirror of
https://github.com/ApfelTeeSaft/SplitNotes.git
synced 2026-08-26 19:33:39 +00:00
yay
This commit is contained in:
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user