Initial commit after repository repair and requirements update
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
venv/
|
||||
84
How to use.txt
Normal file
@@ -0,0 +1,84 @@
|
||||
|
||||
Here is the complete content for the `How to use.txt` file, starting from **point 3** and including all the missing information:
|
||||
|
||||
```plaintext
|
||||
### How to Install, Start, and Use the Signage Player Application
|
||||
|
||||
This guide provides step-by-step instructions for installing, starting, and using the signage player application.
|
||||
|
||||
---
|
||||
|
||||
## 3. Using the Application
|
||||
|
||||
### MediaPlayer Screen
|
||||
- **Play/Pause Button**:
|
||||
- Toggles between playing and pausing the media.
|
||||
- Automatically resumes playback after 30 seconds if not toggled again.
|
||||
- **Next/Previous Buttons**:
|
||||
- Navigate to the next or previous media in the playlist.
|
||||
- **Exit App Button**:
|
||||
- Opens a password-protected popup. Enter the `quickconnect_key` from the configuration file to exit the app.
|
||||
|
||||
### Settings Screen
|
||||
- **Configuration Fields**:
|
||||
- Update fields like `screen_orientation`, `screen_name`, `quickconnect_key`, `server_ip`, and `port`.
|
||||
- **Save Button**:
|
||||
- Saves the updated configuration and returns to the MediaPlayer screen.
|
||||
- **Exit App Button**:
|
||||
- Opens a password-protected popup. Enter the `quickconnect_key` to exit the app.
|
||||
|
||||
---
|
||||
|
||||
## 4. Configuration
|
||||
|
||||
### Configuration File
|
||||
The configuration file is located at:
|
||||
```bash
|
||||
app_config.txt
|
||||
```
|
||||
|
||||
### Example Configuration
|
||||
```json
|
||||
{
|
||||
"screen_orientation": "Landscape",
|
||||
"screen_name": "MyScreen",
|
||||
"quickconnect_key": "12345",
|
||||
"server_ip": "192.168.1.1",
|
||||
"port": "8080"
|
||||
}
|
||||
```
|
||||
|
||||
### Updating Configuration
|
||||
1. Navigate to the **Settings Screen** in the app.
|
||||
2. Update the fields and click **Save**.
|
||||
3. The configuration will be saved to `app_config.txt`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Troubleshooting
|
||||
|
||||
### Permission Denied for `run_app.sh`
|
||||
- Ensure the script is executable:
|
||||
```bash
|
||||
chmod +x /home/pi/Desktop/signage-player/run_app.sh
|
||||
```
|
||||
|
||||
### Application Does Not Start on Boot
|
||||
- Verify the `~/.bashrc` or `crontab` entry is correct.
|
||||
- Check the script's permissions and paths.
|
||||
|
||||
### Media Files Not Playing
|
||||
- Ensure media files are downloaded to the correct directory:
|
||||
```bash
|
||||
/home/pi/Desktop/signage-player/src/static/resurse
|
||||
```
|
||||
- Check the playlist configuration and server connection.
|
||||
|
||||
### Password for Exit App
|
||||
- The password is the `quickconnect_key` from the configuration file (`app_config.txt`).
|
||||
|
||||
---
|
||||
|
||||
Let me know if you need further clarification or additional details!
|
||||
```
|
||||
|
||||
150
README.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# 🎥 Kivy Media Player
|
||||
|
||||
A media player application built using **Kivy** that allows users to play video files, display images, and manage settings for quick connect codes and server configurations. The application checks for updates to the playlist every five minutes while running in full-screen mode.
|
||||
|
||||
---
|
||||
|
||||
## 📂 Project Structure
|
||||
|
||||
```
|
||||
signage-player
|
||||
├── src
|
||||
│ ├── media_player.py # Handles media playback
|
||||
│ ├── python_functions.py # Utility functions for downloading files and updating playlists
|
||||
│ ├── kv
|
||||
│ │ └── media_player.kv # Layout for the media player screen
|
||||
│ ├── Resurse
|
||||
│ │ ├── app_config.txt # Configuration file
|
||||
│ │ ├── log.txt # Log file for media events
|
||||
│ │ ├── play.png # Play button icon
|
||||
│ │ ├── pause.png # Pause button icon
|
||||
│ │ └── other icons... # Additional icons for the UI
|
||||
│ └── static
|
||||
│ └── resurse # Directory for media files (images/videos)
|
||||
├── requirements.txt # Project dependencies
|
||||
├── run_app.sh # Script to run the application
|
||||
├── install.sh # Installation script
|
||||
├── How to use.txt # Detailed usage instructions
|
||||
└── README.md # Documentation for the project
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Setup Instructions
|
||||
|
||||
### Prerequisites
|
||||
Before installing the application, ensure the following are installed on your Raspberry Pi:
|
||||
- **Python 3.7 or higher**
|
||||
- **pip3** (Python package manager)
|
||||
- **ffmpeg** (for video conversion)
|
||||
- **Internet connection** for downloading dependencies
|
||||
|
||||
### Installation Steps
|
||||
|
||||
1. **Clone the Repository**
|
||||
```bash
|
||||
git clone https://gitea.moto-adv.com/ske087/signage-player.git
|
||||
cd signage-player
|
||||
```
|
||||
|
||||
2. **Run the Installation Script**
|
||||
```bash
|
||||
./install.sh
|
||||
```
|
||||
|
||||
The installation script will:
|
||||
- Update the system.
|
||||
- Install required Python and system dependencies.
|
||||
- Clone the repository to `/home/pi/Desktop/ds-player`.
|
||||
- Add the run_app.sh script to `~/.bashrc` for autostart.
|
||||
- Make the run_app.sh script executable.
|
||||
|
||||
3. **Reboot the Device**
|
||||
After installation, reboot the Raspberry Pi to start the application automatically:
|
||||
```bash
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎮 How to Use
|
||||
|
||||
### MediaPlayer Screen
|
||||
- **▶️ Play/Pause Button**:
|
||||
- Toggles between playing and pausing the media.
|
||||
- Automatically resumes playback after 30 seconds if not toggled again.
|
||||
- **⏩ Next/⏪ Previous Buttons**:
|
||||
- Navigate to the next or previous media in the playlist.
|
||||
- **❌ Exit App Button**:
|
||||
- Opens a password-protected popup. Enter the `quickconnect_key` from the configuration file to exit the app.
|
||||
|
||||
### Settings Screen
|
||||
- **Configuration Fields**:
|
||||
- Update fields like `screen_orientation`, `screen_name`, `quickconnect_key`, `server_ip`, and `port`.
|
||||
- **💾 Save Button**:
|
||||
- Saves the updated configuration and returns to the MediaPlayer screen.
|
||||
- **❌ Exit App Button**:
|
||||
- Opens a password-protected popup. Enter the `quickconnect_key` to exit the app.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
### Configuration File
|
||||
The configuration file is located at:
|
||||
```bash
|
||||
/home/pi/Desktop/signage-player/src/Resurse/app_config.txt
|
||||
```
|
||||
|
||||
### Example Configuration
|
||||
```json
|
||||
{
|
||||
"screen_orientation": "Landscape",
|
||||
"screen_name": "tv-panou1",
|
||||
"quickconnect_key": "8887779",
|
||||
"server_ip": "172.20.10.9",
|
||||
"port": "80"
|
||||
}
|
||||
```
|
||||
|
||||
### Updating Configuration
|
||||
1. Navigate to the **Settings Screen** in the app.
|
||||
2. Update the fields and click **Save**.
|
||||
3. The configuration will be saved to app_config.txt.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### Permission Denied for run_app.sh
|
||||
- Ensure the script is executable:
|
||||
```bash
|
||||
chmod +x /home/pi/Desktop/signage-player/run_app.sh
|
||||
```
|
||||
|
||||
### Application Does Not Start on Boot
|
||||
- Verify the `~/.bashrc` entry is correct.
|
||||
- Check the script's permissions and paths.
|
||||
|
||||
### Media Files Not Playing
|
||||
- Ensure media files are downloaded to the correct directory:
|
||||
```bash
|
||||
/home/pi/Desktop/signage-player/src/static/resurse
|
||||
```
|
||||
- Check the playlist configuration and server connection.
|
||||
|
||||
### Password for Exit App
|
||||
- The password is the `quickconnect_key` from the configuration file (`app_config.txt`).
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a pull request or open an issue for any suggestions or improvements.
|
||||
|
||||
---
|
||||
|
||||
## 📜 License
|
||||
|
||||
This project is licensed under the **MIT License**. See the `LICENSE` file for more details.
|
||||
````
|
||||
68
install_tkinter.sh
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
# Tkinter Media Player Installation Script
|
||||
|
||||
echo "Installing Tkinter Media Player..."
|
||||
|
||||
# Update system packages
|
||||
echo "Updating system packages..."
|
||||
sudo apt update
|
||||
sudo apt upgrade -y
|
||||
|
||||
# Install system dependencies
|
||||
echo "Installing system dependencies..."
|
||||
sudo apt install -y python3 python3-pip python3-venv python3-tk
|
||||
sudo apt install -y ffmpeg libopencv-dev python3-opencv
|
||||
sudo apt install -y libsdl2-dev libsdl2-mixer-dev libsdl2-image-dev libsdl2-ttf-dev
|
||||
sudo apt install -y libjpeg-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev
|
||||
|
||||
# Create project directory if it doesn't exist
|
||||
PROJECT_DIR="/home/pi/Desktop/signage-player"
|
||||
if [ ! -d "$PROJECT_DIR" ]; then
|
||||
echo "Project directory not found. Please ensure the signage-player directory exists."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# Create virtual environment
|
||||
echo "Creating Python virtual environment..."
|
||||
python3 -m venv venv
|
||||
|
||||
# Activate virtual environment and install requirements
|
||||
echo "Installing Python dependencies..."
|
||||
source venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -r tkinter_requirements.txt
|
||||
deactivate
|
||||
|
||||
# Make launcher script executable
|
||||
chmod +x run_tkinter_app.sh
|
||||
|
||||
# Create systemd service for auto-start
|
||||
echo "Creating systemd service..."
|
||||
sudo tee /etc/systemd/system/tkinter-signage-player.service > /dev/null <<EOF
|
||||
[Unit]
|
||||
Description=Tkinter Signage Player
|
||||
After=graphical-session.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Environment=DISPLAY=:0
|
||||
ExecStart=/home/pi/Desktop/signage-player/run_tkinter_app.sh
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical-session.target
|
||||
EOF
|
||||
|
||||
# Enable the service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable tkinter-signage-player.service
|
||||
|
||||
echo "Installation completed!"
|
||||
echo "The tkinter media player will start automatically on boot."
|
||||
echo "To start manually, run: ./run_tkinter_app.sh"
|
||||
echo "To stop the service: sudo systemctl stop tkinter-signage-player.service"
|
||||
echo "To view logs: sudo journalctl -u tkinter-signage-player.service -f"
|
||||
14
run_tkinter_debug.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Debugging launch script for the tkinter player application
|
||||
|
||||
# Activate the virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Change to the tkinter app src directory
|
||||
cd tkinter_app/src
|
||||
|
||||
# Run the main application with full error output
|
||||
python main.py
|
||||
|
||||
# Deactivate virtual environment when done
|
||||
deactivate
|
||||
111
src/README.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Signage Player - Complete Offline Installation Package
|
||||
|
||||
This directory contains everything needed to install the Signage Player application completely offline on a Raspberry Pi or similar Debian-based system.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── offline_packages/ # Pre-downloaded Python packages (.whl files)
|
||||
│ ├── requests-2.32.4-py3-none-any.whl
|
||||
│ ├── pillow-11.1.0-cp311-cp311-linux_armv7l.whl
|
||||
│ ├── pygame-2.6.1-cp311-cp311-linux_armv7l.whl
|
||||
│ └── ... (dependencies)
|
||||
├── shared_modules/ # Shared Python modules
|
||||
│ ├── logging_config.py
|
||||
│ └── python_functions.py
|
||||
├── system_packages/ # System dependency information
|
||||
│ └── apt_packages.txt # List of required APT packages
|
||||
└── scripts/ # Installation and utility scripts
|
||||
├── install_offline.sh # Main offline installation script
|
||||
└── check_dependencies.sh # Dependency verification script
|
||||
```
|
||||
|
||||
## Quick Installation
|
||||
|
||||
1. **Run the offline installer:**
|
||||
```bash
|
||||
cd /path/to/signage-player
|
||||
chmod +x src/scripts/install_offline.sh
|
||||
./src/scripts/install_offline.sh
|
||||
```
|
||||
|
||||
2. **Verify installation:**
|
||||
```bash
|
||||
chmod +x src/scripts/check_dependencies.sh
|
||||
./src/scripts/check_dependencies.sh
|
||||
```
|
||||
|
||||
3. **Run the application:**
|
||||
```bash
|
||||
./run_tkinter_debug.sh
|
||||
```
|
||||
|
||||
## What Gets Installed
|
||||
|
||||
### System Packages (via APT)
|
||||
- Python 3 development tools
|
||||
- OpenCV libraries and Python bindings
|
||||
- SDL2 libraries for pygame
|
||||
- Image processing libraries (JPEG, PNG, TIFF, WebP)
|
||||
- Audio libraries
|
||||
- Build tools
|
||||
|
||||
### Python Packages (from offline wheels)
|
||||
- **requests** - HTTP library for server communication
|
||||
- **pillow** - Image processing library
|
||||
- **pygame** - Audio and input handling
|
||||
- **certifi, charset_normalizer, idna, urllib3** - Dependencies
|
||||
|
||||
### Application Components
|
||||
- Shared logging and playlist management modules
|
||||
- Modern tkinter-based media player
|
||||
- Configuration management
|
||||
- Resource directories
|
||||
|
||||
## Manual Installation Steps
|
||||
|
||||
If you prefer to install manually:
|
||||
|
||||
1. **Install system packages:**
|
||||
```bash
|
||||
sudo apt update
|
||||
cat src/system_packages/apt_packages.txt | grep -v '^#' | xargs sudo apt install -y
|
||||
```
|
||||
|
||||
2. **Create virtual environment:**
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
3. **Install Python packages:**
|
||||
```bash
|
||||
pip install --no-index --find-links src/offline_packages requests pillow pygame
|
||||
```
|
||||
|
||||
4. **Copy shared modules:**
|
||||
```bash
|
||||
cp src/shared_modules/*.py tkinter_app/src/
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Permission errors:** Make sure scripts are executable with `chmod +x`
|
||||
- **Missing packages:** Run `src/scripts/check_dependencies.sh` to verify installation
|
||||
- **Virtual environment issues:** Delete `venv` folder and re-run installer
|
||||
- **OpenCV errors:** Ensure `python3-opencv` system package is installed
|
||||
|
||||
## Requirements
|
||||
|
||||
- Debian/Ubuntu-based system (Raspberry Pi OS recommended)
|
||||
- Internet connection for system package installation (APT only)
|
||||
- Sudo privileges for system package installation
|
||||
- At least 200MB free space
|
||||
|
||||
## Notes
|
||||
|
||||
- This package includes all Python dependencies as pre-compiled wheels
|
||||
- No internet connection needed for Python packages during installation
|
||||
- Compatible with ARM-based systems (Raspberry Pi)
|
||||
- Includes fallback mechanisms for offline operation
|
||||
BIN
src/offline_packages/certifi-2025.8.3-py3-none-any.whl
Normal file
BIN
src/offline_packages/charset_normalizer-3.4.2-py3-none-any.whl
Normal file
BIN
src/offline_packages/idna-3.10-py3-none-any.whl
Normal file
BIN
src/offline_packages/pillow-11.1.0-cp311-cp311-linux_armv7l.whl
Normal file
BIN
src/offline_packages/pygame-2.6.1-cp311-cp311-linux_armv7l.whl
Normal file
BIN
src/offline_packages/requests-2.32.4-py3-none-any.whl
Normal file
BIN
src/offline_packages/urllib3-2.5.0-py3-none-any.whl
Normal file
77
src/scripts/check_dependencies.sh
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
# Dependency Verification Script
|
||||
# Checks if all required dependencies are properly installed
|
||||
|
||||
echo "=== Signage Player Dependency Check ==="
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
ERRORS=0
|
||||
|
||||
# Function to check system package
|
||||
check_system_package() {
|
||||
if dpkg -l | grep -q "^ii.*$1"; then
|
||||
echo -e "${GREEN}✓${NC} $1 is installed"
|
||||
else
|
||||
echo -e "${RED}✗${NC} $1 is NOT installed"
|
||||
((ERRORS++))
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check Python package
|
||||
check_python_package() {
|
||||
if python3 -c "import $1" 2>/dev/null; then
|
||||
echo -e "${GREEN}✓${NC} Python package '$1' is available"
|
||||
else
|
||||
echo -e "${RED}✗${NC} Python package '$1' is NOT available"
|
||||
((ERRORS++))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Checking system packages..."
|
||||
check_system_package "python3-dev"
|
||||
check_system_package "python3-opencv"
|
||||
check_system_package "libsdl2-dev"
|
||||
check_system_package "libjpeg-dev"
|
||||
|
||||
echo ""
|
||||
echo "Checking Python packages..."
|
||||
check_python_package "cv2"
|
||||
check_python_package "pygame"
|
||||
check_python_package "PIL"
|
||||
check_python_package "requests"
|
||||
|
||||
echo ""
|
||||
echo "Checking application files..."
|
||||
if [ -f "tkinter_app/src/main.py" ]; then
|
||||
echo -e "${GREEN}✓${NC} Main application file exists"
|
||||
else
|
||||
echo -e "${RED}✗${NC} Main application file missing"
|
||||
((ERRORS++))
|
||||
fi
|
||||
|
||||
if [ -f "tkinter_app/src/tkinter_simple_player.py" ]; then
|
||||
echo -e "${GREEN}✓${NC} Player module exists"
|
||||
else
|
||||
echo -e "${RED}✗${NC} Player module missing"
|
||||
((ERRORS++))
|
||||
fi
|
||||
|
||||
if [ -d "venv" ]; then
|
||||
echo -e "${GREEN}✓${NC} Virtual environment exists"
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} Virtual environment not found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [ $ERRORS -eq 0 ]; then
|
||||
echo -e "${GREEN}=== All dependencies are properly installed! ===${NC}"
|
||||
echo "You can run the application with: ./run_tkinter_debug.sh"
|
||||
else
|
||||
echo -e "${RED}=== Found $ERRORS issues ===${NC}"
|
||||
echo "Run the installation script: src/scripts/install_offline.sh"
|
||||
fi
|
||||
109
src/scripts/install_offline.sh
Executable file
@@ -0,0 +1,109 @@
|
||||
#!/bin/bash
|
||||
# Offline Installation Script for Signage Player
|
||||
# This script installs all dependencies and sets up the application completely offline
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
||||
|
||||
echo "=== Signage Player Offline Installation ==="
|
||||
echo "Project root: $PROJECT_ROOT"
|
||||
|
||||
# Check if running as root for system packages
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
echo "ERROR: Please run this script as a regular user, not as root."
|
||||
echo "The script will prompt for sudo when needed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to check if command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Install system packages
|
||||
echo "Step 1: Installing system packages..."
|
||||
if command_exists apt; then
|
||||
echo "Installing APT packages..."
|
||||
sudo apt update
|
||||
|
||||
# Read and install packages from the list
|
||||
while IFS= read -r package; do
|
||||
# Skip comments and empty lines
|
||||
if [[ ! "$package" =~ ^[[:space:]]*# ]] && [[ -n "${package// }" ]]; then
|
||||
echo "Installing: $package"
|
||||
sudo apt install -y "$package" || echo "Warning: Could not install $package"
|
||||
fi
|
||||
done < "$SCRIPT_DIR/../system_packages/apt_packages.txt"
|
||||
else
|
||||
echo "ERROR: apt package manager not found. This script is designed for Debian/Ubuntu systems."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create virtual environment
|
||||
echo "Step 2: Creating virtual environment..."
|
||||
cd "$PROJECT_ROOT"
|
||||
if [ ! -d "venv" ]; then
|
||||
python3 -m venv --system-site-packages venv
|
||||
echo "Virtual environment created with system site packages access."
|
||||
else
|
||||
echo "Removing existing virtual environment and creating new one with system site packages..."
|
||||
rm -rf venv
|
||||
python3 -m venv --system-site-packages venv
|
||||
echo "Virtual environment recreated with system site packages access."
|
||||
fi
|
||||
|
||||
# Activate virtual environment
|
||||
echo "Step 3: Activating virtual environment..."
|
||||
source venv/bin/activate
|
||||
|
||||
# Install Python packages from offline wheels
|
||||
echo "Step 4: Installing Python packages from offline wheels..."
|
||||
OFFLINE_PACKAGES_DIR="$PROJECT_ROOT/src/offline_packages"
|
||||
if [ -d "$OFFLINE_PACKAGES_DIR" ]; then
|
||||
pip install --upgrade pip
|
||||
pip install --no-index --find-links "$OFFLINE_PACKAGES_DIR" \
|
||||
requests pillow pygame certifi charset_normalizer idna urllib3
|
||||
echo "Python packages installed successfully."
|
||||
else
|
||||
echo "ERROR: Offline packages directory not found at: $OFFLINE_PACKAGES_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy shared modules to tkinter app
|
||||
echo "Step 5: Setting up shared modules..."
|
||||
SHARED_MODULES_DIR="$PROJECT_ROOT/src/shared_modules"
|
||||
TKINTER_APP_DIR="$PROJECT_ROOT/tkinter_app/src"
|
||||
if [ -d "$SHARED_MODULES_DIR" ]; then
|
||||
cp "$SHARED_MODULES_DIR"/*.py "$TKINTER_APP_DIR"/ 2>/dev/null || echo "Shared modules already in place."
|
||||
echo "Shared modules configured."
|
||||
else
|
||||
echo "Warning: Shared modules directory not found at: $SHARED_MODULES_DIR"
|
||||
fi
|
||||
|
||||
# Create necessary directories
|
||||
echo "Step 6: Creating application directories..."
|
||||
mkdir -p "$PROJECT_ROOT/tkinter_app/resources/static/resurse"
|
||||
mkdir -p "$PROJECT_ROOT/tkinter_app/src/static/resurse"
|
||||
|
||||
# Set permissions
|
||||
echo "Step 7: Setting permissions..."
|
||||
chmod +x "$PROJECT_ROOT/run_tkinter_debug.sh" 2>/dev/null || true
|
||||
chmod +x "$PROJECT_ROOT/install_tkinter.sh" 2>/dev/null || true
|
||||
chmod +x "$PROJECT_ROOT/src/scripts"/*.sh 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "=== Installation Complete! ==="
|
||||
echo ""
|
||||
echo "To run the signage player:"
|
||||
echo " ./run_tkinter_debug.sh"
|
||||
echo ""
|
||||
echo "To configure settings, run the app and click the Settings button."
|
||||
echo ""
|
||||
echo "The application has been set up with:"
|
||||
echo " - All system dependencies"
|
||||
echo " - Python virtual environment with required packages"
|
||||
echo " - Shared modules properly configured"
|
||||
echo " - Necessary directories created"
|
||||
echo ""
|
||||
27
src/shared_modules/logging_config.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
# Path to the log file
|
||||
# Update the path to point to the new resources directory
|
||||
LOG_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'log.txt')
|
||||
|
||||
# Create a logger instance
|
||||
Logger = logging.getLogger('SignageApp')
|
||||
Logger.setLevel(logging.INFO) # Set the logging level to INFO
|
||||
|
||||
# Create a file handler to write logs to the log.txt file
|
||||
file_handler = logging.FileHandler(LOG_FILE_PATH, mode='a') # Append logs to the file
|
||||
file_handler.setLevel(logging.INFO)
|
||||
|
||||
# Create a formatter for the log messages
|
||||
formatter = logging.Formatter('[%(levelname)s] [%(name)s] %(message)s')
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
# Add the file handler to the logger
|
||||
Logger.addHandler(file_handler)
|
||||
|
||||
# Optionally, add a stream handler to log messages to the console
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setLevel(logging.INFO)
|
||||
stream_handler.setFormatter(formatter)
|
||||
Logger.addHandler(stream_handler)
|
||||
196
src/shared_modules/python_functions.py
Normal file
@@ -0,0 +1,196 @@
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from logging_config import Logger # Import the shared logger
|
||||
import bcrypt
|
||||
import time
|
||||
|
||||
# Update paths to use the new directory structure
|
||||
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'app_config.txt')
|
||||
LOCAL_PLAYLIST_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'local_playlist.json')
|
||||
|
||||
def load_config():
|
||||
"""Load configuration from app_config.txt."""
|
||||
Logger.info("python_functions: Starting load_config function.")
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r') as file:
|
||||
Logger.info("python_functions: Configuration file loaded successfully.")
|
||||
return json.load(file)
|
||||
except json.JSONDecodeError as e:
|
||||
Logger.error(f"python_functions: Failed to parse configuration file. Error: {e}")
|
||||
return {}
|
||||
else:
|
||||
Logger.error(f"python_functions: Configuration file {CONFIG_FILE} not found.")
|
||||
return {}
|
||||
Logger.info("python_functions: Finished load_config function.")
|
||||
|
||||
# Load configuration and initialize variables
|
||||
config_data = load_config()
|
||||
server = config_data.get("server_ip", "")
|
||||
host = config_data.get("screen_name", "")
|
||||
quick = config_data.get("quickconnect_key", "")
|
||||
port = config_data.get("port", "")
|
||||
|
||||
Logger.info(f"python_functions: Configuration loaded: server={server}, host={host}, quick={quick}, port={port}")
|
||||
|
||||
def load_local_playlist():
|
||||
"""Load the playlist and version from local storage."""
|
||||
Logger.info("python_functions: Starting load_local_playlist function.")
|
||||
if os.path.exists(LOCAL_PLAYLIST_FILE):
|
||||
try:
|
||||
with open(LOCAL_PLAYLIST_FILE, 'r') as local_file:
|
||||
local_playlist = json.load(local_file)
|
||||
Logger.info(f"python_functions: Local playlist loaded: {local_playlist}")
|
||||
if isinstance(local_playlist, dict) and 'playlist' in local_playlist and 'version' in local_playlist:
|
||||
Logger.info("python_functions: Finished load_local_playlist function successfully.")
|
||||
return local_playlist # Return the full playlist data
|
||||
else:
|
||||
Logger.error("python_functions: Invalid local playlist structure.")
|
||||
return {'playlist': [], 'version': 0}
|
||||
except json.JSONDecodeError as e:
|
||||
Logger.error(f"python_functions: Failed to parse local playlist file. Error: {e}")
|
||||
return {'playlist': [], 'version': 0}
|
||||
else:
|
||||
Logger.warning("python_functions: Local playlist file not found.")
|
||||
return {'playlist': [], 'version': 0}
|
||||
Logger.info("python_functions: Finished load_local_playlist function.")
|
||||
|
||||
def save_local_playlist(playlist):
|
||||
"""Save the updated playlist locally."""
|
||||
Logger.info("python_functions: Starting save_local_playlist function.")
|
||||
Logger.debug(f"python_functions: Playlist to save: {playlist}")
|
||||
if not playlist or 'playlist' not in playlist:
|
||||
Logger.error("python_functions: Invalid playlist data. Cannot save local playlist.")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(LOCAL_PLAYLIST_FILE, 'w') as local_file:
|
||||
json.dump(playlist, local_file, indent=4) # Ensure proper formatting
|
||||
Logger.info("python_functions: Updated local playlist with server data.")
|
||||
except IOError as e:
|
||||
Logger.error(f"python_functions: Failed to save local playlist: {e}")
|
||||
Logger.info("python_functions: Finished save_local_playlist function.")
|
||||
|
||||
def fetch_server_playlist():
|
||||
"""Fetch the updated playlist from the server."""
|
||||
try:
|
||||
server_ip = f'{server}:{port}' # Construct the server IP with port
|
||||
url = f'http://{server_ip}/api/playlists'
|
||||
params = {
|
||||
'hostname': host,
|
||||
'quickconnect_code': quick
|
||||
}
|
||||
Logger.info(f"Fetching playlist from URL: {url} with params: {params}")
|
||||
response = requests.get(url, params=params)
|
||||
|
||||
if response.status_code == 200:
|
||||
response_data = response.json()
|
||||
Logger.info(f"Server response: {response_data}")
|
||||
playlist = response_data.get('playlist', [])
|
||||
version = response_data.get('playlist_version', None)
|
||||
hashed_quickconnect = response_data.get('hashed_quickconnect', None)
|
||||
|
||||
if version is not None and hashed_quickconnect is not None:
|
||||
if bcrypt.checkpw(quick.encode('utf-8'), hashed_quickconnect.encode('utf-8')):
|
||||
Logger.info("Fetched updated playlist from server.")
|
||||
|
||||
# Update the playlist version in app_config.txt
|
||||
update_config_playlist_version(version)
|
||||
|
||||
return {'playlist': playlist, 'version': version}
|
||||
else:
|
||||
Logger.error("Quickconnect code validation failed.")
|
||||
else:
|
||||
Logger.error("Failed to retrieve playlist or hashed quickconnect from the response.")
|
||||
else:
|
||||
Logger.error(f"Failed to fetch playlist. Status Code: {response.status_code}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
Logger.error(f"Failed to fetch playlist: {e}")
|
||||
|
||||
return {'playlist': [], 'version': 0}
|
||||
|
||||
def download_media_files(playlist, version):
|
||||
"""Download media files from the server and update the local playlist."""
|
||||
Logger.info("python_functions: Starting media file download...")
|
||||
base_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse') # Path to the local folder
|
||||
if not os.path.exists(base_dir):
|
||||
os.makedirs(base_dir)
|
||||
Logger.info(f"python_functions: Created directory {base_dir} for media files.")
|
||||
|
||||
updated_playlist = [] # List to store updated media entries
|
||||
|
||||
for media in playlist:
|
||||
file_name = media.get('file_name', '')
|
||||
file_url = media.get('url', '')
|
||||
duration = media.get('duration', 10) # Default duration if not provided
|
||||
local_path = os.path.join(base_dir, file_name) # Local file path
|
||||
|
||||
Logger.debug(f"python_functions: Preparing to download {file_name} from {file_url}...")
|
||||
|
||||
if os.path.exists(local_path):
|
||||
Logger.info(f"python_functions: File {file_name} already exists. Skipping download.")
|
||||
else:
|
||||
try:
|
||||
response = requests.get(file_url, timeout=10)
|
||||
if response.status_code == 200:
|
||||
with open(local_path, 'wb') as file:
|
||||
file.write(response.content)
|
||||
Logger.info(f"python_functions: Successfully downloaded {file_name} to {local_path}")
|
||||
else:
|
||||
Logger.error(f"python_functions: Failed to download {file_name}. Status Code: {response.status_code}")
|
||||
continue
|
||||
except requests.exceptions.RequestException as e:
|
||||
Logger.error(f"python_functions: Error downloading {file_name}: {e}")
|
||||
continue
|
||||
|
||||
# Update the playlist entry to point to the local file path
|
||||
updated_media = {
|
||||
'file_name': file_name,
|
||||
'url': f"static/resurse/{file_name}", # Update URL to local path
|
||||
'duration': duration
|
||||
}
|
||||
Logger.debug(f"python_functions: Updated media entry: {updated_media}")
|
||||
updated_playlist.append(updated_media)
|
||||
|
||||
# Save the updated playlist locally
|
||||
save_local_playlist({'playlist': updated_playlist, 'version': version})
|
||||
Logger.info("python_functions: Finished media file download and updated local playlist.")
|
||||
|
||||
def clean_unused_files(playlist):
|
||||
"""Remove unused media files from the resource folder."""
|
||||
Logger.info("python_functions: Cleaning unused media files...")
|
||||
base_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse')
|
||||
if not os.path.exists(base_dir):
|
||||
Logger.debug(f"python_functions: Directory {base_dir} does not exist. No files to clean.")
|
||||
return
|
||||
|
||||
playlist_files = {media.get('file_name', '') for media in playlist}
|
||||
all_files = set(os.listdir(base_dir))
|
||||
unused_files = all_files - playlist_files
|
||||
|
||||
for file_name in unused_files:
|
||||
file_path = os.path.join(base_dir, file_name)
|
||||
try:
|
||||
os.remove(file_path)
|
||||
Logger.info(f"python_functions: Deleted unused file: {file_path}")
|
||||
except OSError as e:
|
||||
Logger.error(f"python_functions: Failed to delete {file_path}: {e}")
|
||||
|
||||
def update_config_playlist_version(version):
|
||||
"""Update the playlist version in app_config.txt."""
|
||||
if not os.path.exists(CONFIG_FILE):
|
||||
Logger.error(f"python_functions: Configuration file {CONFIG_FILE} not found.")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r') as file:
|
||||
config_data = json.load(file)
|
||||
|
||||
config_data['playlist_version'] = version # Add or update the playlist version
|
||||
|
||||
with open(CONFIG_FILE, 'w') as file:
|
||||
json.dump(config_data, file, indent=4)
|
||||
Logger.info(f"python_functions: Updated playlist version in app_config.txt to {version}.")
|
||||
except (IOError, json.JSONDecodeError) as e:
|
||||
Logger.error(f"python_functions: Failed to update playlist version in app_config.txt. Error: {e}")
|
||||
43
src/system_packages/apt_packages.txt
Normal file
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
# System Package Dependencies for Signage Player
|
||||
# These packages need to be installed via apt before installing Python packages
|
||||
|
||||
# Core system packages
|
||||
python3-dev
|
||||
python3-pip
|
||||
python3-venv
|
||||
python3-setuptools
|
||||
python3-wheel
|
||||
|
||||
# OpenCV system dependencies
|
||||
libopencv-dev
|
||||
python3-opencv
|
||||
libopencv-core-dev
|
||||
libopencv-imgproc-dev
|
||||
libopencv-imgcodecs-dev
|
||||
libopencv-videoio-dev
|
||||
|
||||
# Audio/Video libraries
|
||||
libasound2-dev
|
||||
libsdl2-dev
|
||||
libsdl2-image-dev
|
||||
libsdl2-mixer-dev
|
||||
libsdl2-ttf-dev
|
||||
libfreetype6-dev
|
||||
libportmidi-dev
|
||||
|
||||
# Image processing libraries
|
||||
libjpeg-dev
|
||||
libpng-dev
|
||||
libtiff-dev
|
||||
libwebp-dev
|
||||
libopenjp2-7-dev
|
||||
|
||||
# Build tools (may be needed for some packages)
|
||||
build-essential
|
||||
cmake
|
||||
pkg-config
|
||||
|
||||
# Networking
|
||||
curl
|
||||
wget
|
||||
143
test_centering.py
Normal file
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify window centering functionality
|
||||
"""
|
||||
import tkinter as tk
|
||||
import sys
|
||||
import os
|
||||
sys.path.append('tkinter_app/src')
|
||||
from tkinter_simple_player import SettingsWindow, SimpleMediaPlayerApp
|
||||
|
||||
def test_settings_centering():
|
||||
"""Test settings window centering"""
|
||||
root = tk.Tk()
|
||||
root.withdraw() # Hide main window
|
||||
|
||||
# Create a mock app object
|
||||
class MockApp:
|
||||
def __init__(self):
|
||||
self.playlist = []
|
||||
self.current_index = 0
|
||||
|
||||
def play_current_media(self):
|
||||
print('play_current_media called')
|
||||
|
||||
app = MockApp()
|
||||
|
||||
# Test settings window centering
|
||||
try:
|
||||
print("Testing settings window centering...")
|
||||
settings = SettingsWindow(root, app)
|
||||
|
||||
# Get screen dimensions
|
||||
screen_width = settings.window.winfo_screenwidth()
|
||||
screen_height = settings.window.winfo_screenheight()
|
||||
|
||||
# Get window position
|
||||
settings.window.update_idletasks()
|
||||
window_x = settings.window.winfo_x()
|
||||
window_y = settings.window.winfo_y()
|
||||
window_width = 900
|
||||
window_height = 700
|
||||
|
||||
# Calculate expected center position
|
||||
expected_x = (screen_width - window_width) // 2
|
||||
expected_y = (screen_height - window_height) // 2
|
||||
|
||||
print(f"Screen size: {screen_width}x{screen_height}")
|
||||
print(f"Window position: {window_x}, {window_y}")
|
||||
print(f"Expected center: {expected_x}, {expected_y}")
|
||||
print(f"Window size: {window_width}x{window_height}")
|
||||
|
||||
# Check if window is roughly centered (allow some margin for window decorations)
|
||||
margin = 50
|
||||
is_centered_x = abs(window_x - expected_x) <= margin
|
||||
is_centered_y = abs(window_y - expected_y) <= margin
|
||||
|
||||
if is_centered_x and is_centered_y:
|
||||
print("✅ Settings window is properly centered!")
|
||||
else:
|
||||
print("❌ Settings window centering needs adjustment")
|
||||
|
||||
# Keep window open for 3 seconds to visually verify
|
||||
root.after(3000, root.quit)
|
||||
root.mainloop()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error testing settings window: {e}")
|
||||
|
||||
def test_exit_dialog_centering():
|
||||
"""Test exit dialog centering"""
|
||||
print("\nTesting exit dialog centering...")
|
||||
|
||||
# Create a simple test for the centering function
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
|
||||
# Create a test dialog
|
||||
dialog = tk.Toplevel(root)
|
||||
dialog.title("Test Exit Dialog")
|
||||
dialog.geometry("400x200")
|
||||
dialog.configure(bg='#2d2d2d')
|
||||
dialog.resizable(False, False)
|
||||
|
||||
# Test the centering logic
|
||||
dialog.update_idletasks()
|
||||
screen_width = dialog.winfo_screenwidth()
|
||||
screen_height = dialog.winfo_screenheight()
|
||||
dialog_width = 400
|
||||
dialog_height = 200
|
||||
|
||||
# Calculate center position
|
||||
center_x = int((screen_width - dialog_width) / 2)
|
||||
center_y = int((screen_height - dialog_height) / 2)
|
||||
|
||||
# Ensure the dialog doesn't go off-screen
|
||||
center_x = max(0, min(center_x, screen_width - dialog_width))
|
||||
center_y = max(0, min(center_y, screen_height - dialog_height))
|
||||
|
||||
dialog.geometry(f"{dialog_width}x{dialog_height}+{center_x}+{center_y}")
|
||||
dialog.lift()
|
||||
|
||||
# Add test content
|
||||
tk.Label(dialog, text="🎬 Test Exit Dialog",
|
||||
font=('Arial', 16, 'bold'),
|
||||
fg='white', bg='#2d2d2d').pack(pady=20)
|
||||
|
||||
tk.Label(dialog, text="This dialog should be centered on screen",
|
||||
font=('Arial', 12),
|
||||
fg='white', bg='#2d2d2d').pack(pady=10)
|
||||
|
||||
# Get actual position
|
||||
dialog.update_idletasks()
|
||||
actual_x = dialog.winfo_x()
|
||||
actual_y = dialog.winfo_y()
|
||||
|
||||
print(f"Screen size: {screen_width}x{screen_height}")
|
||||
print(f"Dialog position: {actual_x}, {actual_y}")
|
||||
print(f"Expected center: {center_x}, {center_y}")
|
||||
|
||||
# Check centering
|
||||
margin = 50
|
||||
is_centered_x = abs(actual_x - center_x) <= margin
|
||||
is_centered_y = abs(actual_y - center_y) <= margin
|
||||
|
||||
if is_centered_x and is_centered_y:
|
||||
print("✅ Exit dialog is properly centered!")
|
||||
else:
|
||||
print("❌ Exit dialog centering needs adjustment")
|
||||
|
||||
# Close dialog after 3 seconds
|
||||
root.after(3000, root.quit)
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🧪 Testing Window Centering Functionality")
|
||||
print("=" * 50)
|
||||
|
||||
test_settings_centering()
|
||||
test_exit_dialog_centering()
|
||||
|
||||
print("\n✅ Centering tests completed!")
|
||||
print("\nThe windows should appear centered on your screen regardless of resolution.")
|
||||
print("This works for any screen size: 1024x768, 1920x1080, 4K, etc.")
|
||||
54
test_image_display.py
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify image display functionality
|
||||
"""
|
||||
import tkinter as tk
|
||||
from PIL import Image, ImageTk
|
||||
import os
|
||||
|
||||
def test_image_display():
|
||||
# Create a simple tkinter window
|
||||
root = tk.Tk()
|
||||
root.title("Image Display Test")
|
||||
root.geometry("800x600")
|
||||
root.configure(bg='black')
|
||||
|
||||
# Create image label
|
||||
image_label = tk.Label(root, bg='black')
|
||||
image_label.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Test image path
|
||||
test_image = "/home/pi/Desktop/signage-player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg"
|
||||
|
||||
try:
|
||||
if os.path.exists(test_image):
|
||||
print(f"Loading image: {test_image}")
|
||||
|
||||
# Load and display image
|
||||
img = Image.open(test_image)
|
||||
img.thumbnail((800, 600), Image.LANCZOS)
|
||||
photo = ImageTk.PhotoImage(img)
|
||||
|
||||
image_label.config(image=photo)
|
||||
image_label.image = photo # Keep reference
|
||||
|
||||
print(f"Image loaded successfully: {img.size}")
|
||||
|
||||
# Close after 3 seconds
|
||||
root.after(3000, root.quit)
|
||||
|
||||
else:
|
||||
print(f"Image file not found: {test_image}")
|
||||
image_label.config(text="Image file not found", fg='white')
|
||||
root.after(2000, root.quit)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading image: {e}")
|
||||
image_label.config(text=f"Error: {e}", fg='red')
|
||||
root.after(2000, root.quit)
|
||||
|
||||
root.mainloop()
|
||||
print("Image display test completed")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_image_display()
|
||||
38
test_imports.py
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
print("Testing imports...")
|
||||
|
||||
try:
|
||||
import tkinter as tk
|
||||
print("✓ tkinter imported successfully")
|
||||
except Exception as e:
|
||||
print(f"✗ tkinter import failed: {e}")
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageTk
|
||||
print("✓ PIL and ImageTk imported successfully")
|
||||
except Exception as e:
|
||||
print(f"✗ PIL import failed: {e}")
|
||||
|
||||
try:
|
||||
from virtual_keyboard import VirtualKeyboard
|
||||
print("✓ Virtual keyboard imported successfully")
|
||||
except Exception as e:
|
||||
print(f"✗ Virtual keyboard import failed: {e}")
|
||||
|
||||
try:
|
||||
from python_functions import load_local_playlist
|
||||
print("✓ Python functions imported successfully")
|
||||
|
||||
# Test loading playlist
|
||||
playlist_data = load_local_playlist()
|
||||
playlist = playlist_data.get('playlist', [])
|
||||
print(f"✓ Local playlist loaded: {len(playlist)} items")
|
||||
|
||||
for i, item in enumerate(playlist):
|
||||
print(f" {i+1}. {item.get('file_name', 'Unknown')}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Python functions import/execution failed: {e}")
|
||||
|
||||
print("Import test completed")
|
||||
97
test_scaling.py
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for full-screen image scaling functionality
|
||||
"""
|
||||
import tkinter as tk
|
||||
from PIL import Image, ImageTk
|
||||
import os
|
||||
|
||||
def test_scaling_modes():
|
||||
"""Test different scaling modes for images"""
|
||||
|
||||
def scale_image_to_screen(img, screen_width, screen_height, mode='fit'):
|
||||
"""Test scaling function"""
|
||||
img_width, img_height = img.size
|
||||
|
||||
if mode == 'stretch':
|
||||
return img.resize((screen_width, screen_height), Image.LANCZOS), (0, 0)
|
||||
|
||||
elif mode == 'fill':
|
||||
screen_ratio = screen_width / screen_height
|
||||
img_ratio = img_width / img_height
|
||||
|
||||
if img_ratio > screen_ratio:
|
||||
new_height = screen_height
|
||||
new_width = int(screen_height * img_ratio)
|
||||
x_offset = (screen_width - new_width) // 2
|
||||
y_offset = 0
|
||||
else:
|
||||
new_width = screen_width
|
||||
new_height = int(screen_width / img_ratio)
|
||||
x_offset = 0
|
||||
y_offset = (screen_height - new_height) // 2
|
||||
|
||||
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
||||
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
|
||||
|
||||
if new_width > screen_width:
|
||||
crop_x = (new_width - screen_width) // 2
|
||||
img_resized = img_resized.crop((crop_x, 0, crop_x + screen_width, new_height))
|
||||
x_offset = 0
|
||||
if new_height > screen_height:
|
||||
crop_y = (new_height - screen_height) // 2
|
||||
img_resized = img_resized.crop((0, crop_y, new_width, crop_y + screen_height))
|
||||
y_offset = 0
|
||||
|
||||
final_img.paste(img_resized, (x_offset, y_offset))
|
||||
return final_img, (x_offset, y_offset)
|
||||
|
||||
else: # fit mode
|
||||
screen_ratio = screen_width / screen_height
|
||||
img_ratio = img_width / img_height
|
||||
|
||||
if img_ratio > screen_ratio:
|
||||
new_width = screen_width
|
||||
new_height = int(screen_width / img_ratio)
|
||||
else:
|
||||
new_height = screen_height
|
||||
new_width = int(screen_height * img_ratio)
|
||||
|
||||
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
||||
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
|
||||
x_offset = (screen_width - new_width) // 2
|
||||
y_offset = (screen_height - new_height) // 2
|
||||
final_img.paste(img_resized, (x_offset, y_offset))
|
||||
|
||||
return final_img, (x_offset, y_offset)
|
||||
|
||||
# Test image path
|
||||
test_image = "/home/pi/Desktop/signage-player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg"
|
||||
|
||||
if not os.path.exists(test_image):
|
||||
print(f"Test image not found: {test_image}")
|
||||
return
|
||||
|
||||
try:
|
||||
# Load test image
|
||||
img = Image.open(test_image)
|
||||
original_size = img.size
|
||||
screen_width, screen_height = 800, 600
|
||||
|
||||
print(f"Testing scaling modes for image: {original_size}")
|
||||
print(f"Target screen size: {screen_width}x{screen_height}")
|
||||
|
||||
# Test each scaling mode
|
||||
modes = ['fit', 'fill', 'stretch']
|
||||
|
||||
for mode in modes:
|
||||
final_img, offset = scale_image_to_screen(img, screen_width, screen_height, mode)
|
||||
print(f"{mode.upper()} mode: Final size: {final_img.size}, Offset: {offset}")
|
||||
|
||||
print("✅ All scaling modes tested successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error testing scaling: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_scaling_modes()
|
||||
230
test_touch.py
Normal file
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Touch Display Test - Test the touch-optimized interface with virtual keyboard
|
||||
"""
|
||||
import tkinter as tk
|
||||
import sys
|
||||
import os
|
||||
sys.path.append('tkinter_app/src')
|
||||
|
||||
def test_touch_interface():
|
||||
"""Test the touch-optimized settings interface"""
|
||||
try:
|
||||
from tkinter_simple_player import SettingsWindow
|
||||
|
||||
# Create main window
|
||||
root = tk.Tk()
|
||||
root.title("🎮 Touch Display Test")
|
||||
root.geometry("1024x768")
|
||||
root.configure(bg='#2c3e50')
|
||||
|
||||
# Create welcome screen
|
||||
welcome_frame = tk.Frame(root, bg='#2c3e50', padx=40, pady=40)
|
||||
welcome_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Title
|
||||
title_label = tk.Label(welcome_frame,
|
||||
text="🎬 Touch Display Digital Signage",
|
||||
font=('Segoe UI', 24, 'bold'),
|
||||
fg='white', bg='#2c3e50')
|
||||
title_label.pack(pady=30)
|
||||
|
||||
# Description
|
||||
desc_text = (
|
||||
"Touch-Optimized Features:\n\n"
|
||||
"📱 Virtual On-Screen Keyboard\n"
|
||||
"🎯 Larger Touch-Friendly Buttons\n"
|
||||
"⌨️ Auto-Show Keyboard on Input Focus\n"
|
||||
"👆 Enhanced Touch Feedback\n"
|
||||
"🎨 Dark Theme Optimized for Displays\n\n"
|
||||
"Click the button below to test the settings interface:"
|
||||
)
|
||||
|
||||
desc_label = tk.Label(welcome_frame, text=desc_text,
|
||||
font=('Segoe UI', 14),
|
||||
fg='#ecf0f1', bg='#2c3e50',
|
||||
justify=tk.CENTER)
|
||||
desc_label.pack(pady=20)
|
||||
|
||||
# Create mock app for testing
|
||||
class MockApp:
|
||||
def __init__(self):
|
||||
self.playlist = []
|
||||
self.current_index = 0
|
||||
|
||||
def play_current_media(self):
|
||||
print("Mock: play_current_media called")
|
||||
|
||||
mock_app = MockApp()
|
||||
|
||||
# Test button to open touch-optimized settings
|
||||
def open_touch_settings():
|
||||
try:
|
||||
settings = SettingsWindow(root, mock_app)
|
||||
print("✅ Touch-optimized settings window opened successfully!")
|
||||
except Exception as e:
|
||||
print(f"❌ Error opening settings: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Large touch-friendly button
|
||||
settings_btn = tk.Button(welcome_frame,
|
||||
text="🔧 Open Touch Settings",
|
||||
command=open_touch_settings,
|
||||
bg='#3498db', fg='white',
|
||||
font=('Segoe UI', 16, 'bold'),
|
||||
relief=tk.FLAT, padx=40, pady=20,
|
||||
cursor='hand2')
|
||||
settings_btn.pack(pady=30)
|
||||
|
||||
# Instructions
|
||||
instructions = (
|
||||
"Touch Instructions:\n"
|
||||
"• Tap input fields to show virtual keyboard\n"
|
||||
"• Use large buttons for easy touch interaction\n"
|
||||
"• Virtual keyboard stays on top for easy access\n"
|
||||
"• Click outside input fields to hide keyboard"
|
||||
)
|
||||
|
||||
instr_label = tk.Label(welcome_frame, text=instructions,
|
||||
font=('Segoe UI', 11),
|
||||
fg='#bdc3c7', bg='#2c3e50',
|
||||
justify=tk.LEFT)
|
||||
instr_label.pack(pady=20)
|
||||
|
||||
# Exit button
|
||||
exit_btn = tk.Button(welcome_frame,
|
||||
text="❌ Exit Test",
|
||||
command=root.quit,
|
||||
bg='#e74c3c', fg='white',
|
||||
font=('Segoe UI', 12, 'bold'),
|
||||
relief=tk.FLAT, padx=30, pady=15,
|
||||
cursor='hand2')
|
||||
exit_btn.pack(pady=20)
|
||||
|
||||
# Add touch feedback to buttons
|
||||
def add_touch_feedback(button):
|
||||
def on_press(e):
|
||||
button.configure(relief=tk.SUNKEN)
|
||||
def on_release(e):
|
||||
button.configure(relief=tk.FLAT)
|
||||
def on_enter(e):
|
||||
button.configure(relief=tk.RAISED)
|
||||
def on_leave(e):
|
||||
button.configure(relief=tk.FLAT)
|
||||
|
||||
button.bind("<Button-1>", on_press)
|
||||
button.bind("<ButtonRelease-1>", on_release)
|
||||
button.bind("<Enter>", on_enter)
|
||||
button.bind("<Leave>", on_leave)
|
||||
|
||||
add_touch_feedback(settings_btn)
|
||||
add_touch_feedback(exit_btn)
|
||||
|
||||
print("🎮 Touch Display Test Started")
|
||||
print("=" * 50)
|
||||
print("Features being tested:")
|
||||
print("- Virtual keyboard integration")
|
||||
print("- Touch-optimized input fields")
|
||||
print("- Large, finger-friendly buttons")
|
||||
print("- Enhanced visual feedback")
|
||||
print("- Dark theme for displays")
|
||||
print("\nClick 'Open Touch Settings' to test the interface!")
|
||||
|
||||
root.mainloop()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error in touch interface test: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def test_virtual_keyboard_standalone():
|
||||
"""Test just the virtual keyboard component"""
|
||||
try:
|
||||
from virtual_keyboard import VirtualKeyboard, TouchOptimizedEntry, TouchOptimizedButton
|
||||
|
||||
root = tk.Tk()
|
||||
root.title("🎹 Virtual Keyboard Test")
|
||||
root.geometry("800x500")
|
||||
root.configure(bg='#2f3136')
|
||||
|
||||
# Create virtual keyboard
|
||||
vk = VirtualKeyboard(root, dark_theme=True)
|
||||
|
||||
# Test interface
|
||||
test_frame = tk.Frame(root, bg='#2f3136', padx=30, pady=30)
|
||||
test_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
tk.Label(test_frame, text="🎹 Virtual Keyboard Test",
|
||||
font=('Segoe UI', 20, 'bold'),
|
||||
bg='#2f3136', fg='white').pack(pady=20)
|
||||
|
||||
tk.Label(test_frame, text="Click on the input fields below to test the virtual keyboard:",
|
||||
font=('Segoe UI', 12),
|
||||
bg='#2f3136', fg='#b9bbbe').pack(pady=10)
|
||||
|
||||
# Test input fields
|
||||
tk.Label(test_frame, text="Server IP:", bg='#2f3136', fg='white',
|
||||
font=('Segoe UI', 11, 'bold')).pack(anchor=tk.W, pady=(20, 5))
|
||||
entry1 = TouchOptimizedEntry(test_frame, vk, width=40, bg='#36393f',
|
||||
fg='white', insertbackground='white')
|
||||
entry1.pack(pady=5, fill=tk.X)
|
||||
|
||||
tk.Label(test_frame, text="Device Name:", bg='#2f3136', fg='white',
|
||||
font=('Segoe UI', 11, 'bold')).pack(anchor=tk.W, pady=(15, 5))
|
||||
entry2 = TouchOptimizedEntry(test_frame, vk, width=40, bg='#36393f',
|
||||
fg='white', insertbackground='white')
|
||||
entry2.pack(pady=5, fill=tk.X)
|
||||
|
||||
tk.Label(test_frame, text="Password:", bg='#2f3136', fg='white',
|
||||
font=('Segoe UI', 11, 'bold')).pack(anchor=tk.W, pady=(15, 5))
|
||||
entry3 = TouchOptimizedEntry(test_frame, vk, width=40, bg='#36393f',
|
||||
fg='white', insertbackground='white', show='*')
|
||||
entry3.pack(pady=5, fill=tk.X)
|
||||
|
||||
# Control buttons
|
||||
btn_frame = tk.Frame(test_frame, bg='#2f3136')
|
||||
btn_frame.pack(pady=30)
|
||||
|
||||
TouchOptimizedButton(btn_frame, text="🎹 Show Keyboard",
|
||||
command=lambda: vk.show_keyboard(entry1),
|
||||
bg='#7289da', fg='white').pack(side=tk.LEFT, padx=10)
|
||||
|
||||
TouchOptimizedButton(btn_frame, text="❌ Hide Keyboard",
|
||||
command=vk.hide_keyboard,
|
||||
bg='#ed4245', fg='white').pack(side=tk.LEFT, padx=10)
|
||||
|
||||
TouchOptimizedButton(btn_frame, text="🔄 Clear All",
|
||||
command=lambda: [e.delete(0, tk.END) for e in [entry1, entry2, entry3]],
|
||||
bg='#faa61a', fg='white').pack(side=tk.LEFT, padx=10)
|
||||
|
||||
print("🎹 Virtual Keyboard Test Started")
|
||||
print("- Click input fields to auto-show keyboard")
|
||||
print("- Type using virtual or physical keyboard")
|
||||
print("- Test touch-friendly interface")
|
||||
|
||||
root.mainloop()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error in virtual keyboard test: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Touch Display Tests")
|
||||
parser.add_argument("--keyboard-only", action="store_true",
|
||||
help="Test only the virtual keyboard component")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("🎮 Touch Display Digital Signage Tests")
|
||||
print("=" * 50)
|
||||
|
||||
if args.keyboard_only:
|
||||
test_virtual_keyboard_standalone()
|
||||
else:
|
||||
test_touch_interface()
|
||||
|
||||
print("\n✅ Touch display tests completed!")
|
||||
10
tkinter_app/resources/app_config.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"screen_orientation": "Landscape",
|
||||
"screen_name": "tv-terasa",
|
||||
"quickconnect_key": "8887779",
|
||||
"server_ip": "digi-signage.moto-adv.com",
|
||||
"port": "8880",
|
||||
"screen_w": "1920",
|
||||
"screen_h": "1080",
|
||||
"playlist_version": 5
|
||||
}
|
||||
BIN
tkinter_app/resources/demo1.jpg
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
tkinter_app/resources/demo2.jpeg
Normal file
|
After Width: | Height: | Size: 537 KiB |
15
tkinter_app/resources/demo_playlist.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"playlist": [
|
||||
{
|
||||
"file_name": "demo1.jpg",
|
||||
"url": "Resurse/demo1.jpg",
|
||||
"duration": 20
|
||||
},
|
||||
{
|
||||
"file_name": "demo2.jpg",
|
||||
"url": "Resurse/demo2.jpg",
|
||||
"duration": 20
|
||||
}
|
||||
],
|
||||
"version": 1
|
||||
}
|
||||
BIN
tkinter_app/resources/home_icon.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
tkinter_app/resources/left-arrow-blue.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
tkinter_app/resources/left-arrow-green.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
20
tkinter_app/resources/local_playlist.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"playlist": [
|
||||
{
|
||||
"file_name": "1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg",
|
||||
"url": "static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg",
|
||||
"duration": 20
|
||||
},
|
||||
{
|
||||
"file_name": "wp2782770-1846651530.jpg",
|
||||
"url": "static/resurse/wp2782770-1846651530.jpg",
|
||||
"duration": 15
|
||||
},
|
||||
{
|
||||
"file_name": "SampleVideo_1280x720_1mb.mp4",
|
||||
"url": "static/resurse/SampleVideo_1280x720_1mb.mp4",
|
||||
"duration": 5
|
||||
}
|
||||
],
|
||||
"version": 5
|
||||
}
|
||||
1186
tkinter_app/resources/log.txt
Normal file
BIN
tkinter_app/resources/pause.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
tkinter_app/resources/play.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
tkinter_app/resources/right-arrow-blue.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
tkinter_app/resources/right-arrow-green.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
tkinter_app/src/__pycache__/logging_config.cpython-311.pyc
Normal file
BIN
tkinter_app/src/__pycache__/python_functions.cpython-311.pyc
Normal file
BIN
tkinter_app/src/__pycache__/virtual_keyboard.cpython-311.pyc
Normal file
27
tkinter_app/src/logging_config.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
# Path to the log file
|
||||
# Update the path to point to the new resources directory
|
||||
LOG_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'log.txt')
|
||||
|
||||
# Create a logger instance
|
||||
Logger = logging.getLogger('SignageApp')
|
||||
Logger.setLevel(logging.INFO) # Set the logging level to INFO
|
||||
|
||||
# Create a file handler to write logs to the log.txt file
|
||||
file_handler = logging.FileHandler(LOG_FILE_PATH, mode='a') # Append logs to the file
|
||||
file_handler.setLevel(logging.INFO)
|
||||
|
||||
# Create a formatter for the log messages
|
||||
formatter = logging.Formatter('[%(levelname)s] [%(name)s] %(message)s')
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
# Add the file handler to the logger
|
||||
Logger.addHandler(file_handler)
|
||||
|
||||
# Optionally, add a stream handler to log messages to the console
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setLevel(logging.INFO)
|
||||
stream_handler.setFormatter(formatter)
|
||||
Logger.addHandler(stream_handler)
|
||||
20
tkinter_app/src/main.py
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Main entry point for the tkinter-based signage player application.
|
||||
This file acts as the main executable for launching the tkinter player.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import cv2 # Import OpenCV to confirm it's available
|
||||
|
||||
# Add the current directory to the path so we can import our modules
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Import the player module
|
||||
from tkinter_simple_player import SimpleMediaPlayerApp
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Using OpenCV version: {cv2.__version__}")
|
||||
# Create and run the player
|
||||
player = SimpleMediaPlayerApp()
|
||||
player.run()
|
||||
196
tkinter_app/src/python_functions.py
Normal file
@@ -0,0 +1,196 @@
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from logging_config import Logger # Import the shared logger
|
||||
import bcrypt
|
||||
import time
|
||||
|
||||
# Update paths to use the new directory structure
|
||||
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'app_config.txt')
|
||||
LOCAL_PLAYLIST_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'local_playlist.json')
|
||||
|
||||
def load_config():
|
||||
"""Load configuration from app_config.txt."""
|
||||
Logger.info("python_functions: Starting load_config function.")
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r') as file:
|
||||
Logger.info("python_functions: Configuration file loaded successfully.")
|
||||
return json.load(file)
|
||||
except json.JSONDecodeError as e:
|
||||
Logger.error(f"python_functions: Failed to parse configuration file. Error: {e}")
|
||||
return {}
|
||||
else:
|
||||
Logger.error(f"python_functions: Configuration file {CONFIG_FILE} not found.")
|
||||
return {}
|
||||
Logger.info("python_functions: Finished load_config function.")
|
||||
|
||||
# Load configuration and initialize variables
|
||||
config_data = load_config()
|
||||
server = config_data.get("server_ip", "")
|
||||
host = config_data.get("screen_name", "")
|
||||
quick = config_data.get("quickconnect_key", "")
|
||||
port = config_data.get("port", "")
|
||||
|
||||
Logger.info(f"python_functions: Configuration loaded: server={server}, host={host}, quick={quick}, port={port}")
|
||||
|
||||
def load_local_playlist():
|
||||
"""Load the playlist and version from local storage."""
|
||||
Logger.info("python_functions: Starting load_local_playlist function.")
|
||||
if os.path.exists(LOCAL_PLAYLIST_FILE):
|
||||
try:
|
||||
with open(LOCAL_PLAYLIST_FILE, 'r') as local_file:
|
||||
local_playlist = json.load(local_file)
|
||||
Logger.info(f"python_functions: Local playlist loaded: {local_playlist}")
|
||||
if isinstance(local_playlist, dict) and 'playlist' in local_playlist and 'version' in local_playlist:
|
||||
Logger.info("python_functions: Finished load_local_playlist function successfully.")
|
||||
return local_playlist # Return the full playlist data
|
||||
else:
|
||||
Logger.error("python_functions: Invalid local playlist structure.")
|
||||
return {'playlist': [], 'version': 0}
|
||||
except json.JSONDecodeError as e:
|
||||
Logger.error(f"python_functions: Failed to parse local playlist file. Error: {e}")
|
||||
return {'playlist': [], 'version': 0}
|
||||
else:
|
||||
Logger.warning("python_functions: Local playlist file not found.")
|
||||
return {'playlist': [], 'version': 0}
|
||||
Logger.info("python_functions: Finished load_local_playlist function.")
|
||||
|
||||
def save_local_playlist(playlist):
|
||||
"""Save the updated playlist locally."""
|
||||
Logger.info("python_functions: Starting save_local_playlist function.")
|
||||
Logger.debug(f"python_functions: Playlist to save: {playlist}")
|
||||
if not playlist or 'playlist' not in playlist:
|
||||
Logger.error("python_functions: Invalid playlist data. Cannot save local playlist.")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(LOCAL_PLAYLIST_FILE, 'w') as local_file:
|
||||
json.dump(playlist, local_file, indent=4) # Ensure proper formatting
|
||||
Logger.info("python_functions: Updated local playlist with server data.")
|
||||
except IOError as e:
|
||||
Logger.error(f"python_functions: Failed to save local playlist: {e}")
|
||||
Logger.info("python_functions: Finished save_local_playlist function.")
|
||||
|
||||
def fetch_server_playlist():
|
||||
"""Fetch the updated playlist from the server."""
|
||||
try:
|
||||
server_ip = f'{server}:{port}' # Construct the server IP with port
|
||||
url = f'http://{server_ip}/api/playlists'
|
||||
params = {
|
||||
'hostname': host,
|
||||
'quickconnect_code': quick
|
||||
}
|
||||
Logger.info(f"Fetching playlist from URL: {url} with params: {params}")
|
||||
response = requests.get(url, params=params)
|
||||
|
||||
if response.status_code == 200:
|
||||
response_data = response.json()
|
||||
Logger.info(f"Server response: {response_data}")
|
||||
playlist = response_data.get('playlist', [])
|
||||
version = response_data.get('playlist_version', None)
|
||||
hashed_quickconnect = response_data.get('hashed_quickconnect', None)
|
||||
|
||||
if version is not None and hashed_quickconnect is not None:
|
||||
if bcrypt.checkpw(quick.encode('utf-8'), hashed_quickconnect.encode('utf-8')):
|
||||
Logger.info("Fetched updated playlist from server.")
|
||||
|
||||
# Update the playlist version in app_config.txt
|
||||
update_config_playlist_version(version)
|
||||
|
||||
return {'playlist': playlist, 'version': version}
|
||||
else:
|
||||
Logger.error("Quickconnect code validation failed.")
|
||||
else:
|
||||
Logger.error("Failed to retrieve playlist or hashed quickconnect from the response.")
|
||||
else:
|
||||
Logger.error(f"Failed to fetch playlist. Status Code: {response.status_code}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
Logger.error(f"Failed to fetch playlist: {e}")
|
||||
|
||||
return {'playlist': [], 'version': 0}
|
||||
|
||||
def download_media_files(playlist, version):
|
||||
"""Download media files from the server and update the local playlist."""
|
||||
Logger.info("python_functions: Starting media file download...")
|
||||
base_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse') # Path to the local folder
|
||||
if not os.path.exists(base_dir):
|
||||
os.makedirs(base_dir)
|
||||
Logger.info(f"python_functions: Created directory {base_dir} for media files.")
|
||||
|
||||
updated_playlist = [] # List to store updated media entries
|
||||
|
||||
for media in playlist:
|
||||
file_name = media.get('file_name', '')
|
||||
file_url = media.get('url', '')
|
||||
duration = media.get('duration', 10) # Default duration if not provided
|
||||
local_path = os.path.join(base_dir, file_name) # Local file path
|
||||
|
||||
Logger.debug(f"python_functions: Preparing to download {file_name} from {file_url}...")
|
||||
|
||||
if os.path.exists(local_path):
|
||||
Logger.info(f"python_functions: File {file_name} already exists. Skipping download.")
|
||||
else:
|
||||
try:
|
||||
response = requests.get(file_url, timeout=10)
|
||||
if response.status_code == 200:
|
||||
with open(local_path, 'wb') as file:
|
||||
file.write(response.content)
|
||||
Logger.info(f"python_functions: Successfully downloaded {file_name} to {local_path}")
|
||||
else:
|
||||
Logger.error(f"python_functions: Failed to download {file_name}. Status Code: {response.status_code}")
|
||||
continue
|
||||
except requests.exceptions.RequestException as e:
|
||||
Logger.error(f"python_functions: Error downloading {file_name}: {e}")
|
||||
continue
|
||||
|
||||
# Update the playlist entry to point to the local file path
|
||||
updated_media = {
|
||||
'file_name': file_name,
|
||||
'url': f"static/resurse/{file_name}", # Update URL to local path
|
||||
'duration': duration
|
||||
}
|
||||
Logger.debug(f"python_functions: Updated media entry: {updated_media}")
|
||||
updated_playlist.append(updated_media)
|
||||
|
||||
# Save the updated playlist locally
|
||||
save_local_playlist({'playlist': updated_playlist, 'version': version})
|
||||
Logger.info("python_functions: Finished media file download and updated local playlist.")
|
||||
|
||||
def clean_unused_files(playlist):
|
||||
"""Remove unused media files from the resource folder."""
|
||||
Logger.info("python_functions: Cleaning unused media files...")
|
||||
base_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse')
|
||||
if not os.path.exists(base_dir):
|
||||
Logger.debug(f"python_functions: Directory {base_dir} does not exist. No files to clean.")
|
||||
return
|
||||
|
||||
playlist_files = {media.get('file_name', '') for media in playlist}
|
||||
all_files = set(os.listdir(base_dir))
|
||||
unused_files = all_files - playlist_files
|
||||
|
||||
for file_name in unused_files:
|
||||
file_path = os.path.join(base_dir, file_name)
|
||||
try:
|
||||
os.remove(file_path)
|
||||
Logger.info(f"python_functions: Deleted unused file: {file_path}")
|
||||
except OSError as e:
|
||||
Logger.error(f"python_functions: Failed to delete {file_path}: {e}")
|
||||
|
||||
def update_config_playlist_version(version):
|
||||
"""Update the playlist version in app_config.txt."""
|
||||
if not os.path.exists(CONFIG_FILE):
|
||||
Logger.error(f"python_functions: Configuration file {CONFIG_FILE} not found.")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r') as file:
|
||||
config_data = json.load(file)
|
||||
|
||||
config_data['playlist_version'] = version # Add or update the playlist version
|
||||
|
||||
with open(CONFIG_FILE, 'w') as file:
|
||||
json.dump(config_data, file, indent=4)
|
||||
Logger.info(f"python_functions: Updated playlist version in app_config.txt to {version}.")
|
||||
except (IOError, json.JSONDecodeError) as e:
|
||||
Logger.error(f"python_functions: Failed to update playlist version in app_config.txt. Error: {e}")
|
||||
|
After Width: | Height: | Size: 361 KiB |
BIN
tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4
Normal file
BIN
tkinter_app/src/static/resurse/har_page_001.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_002.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_003.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_004.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_005.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_006.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_007.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_008.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_009.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/har_page_010.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tkinter_app/src/static/resurse/wp2782770-1846651530.jpg
Normal file
|
After Width: | Height: | Size: 794 KiB |
2107
tkinter_app/src/tkinter_simple_player.py
Normal file
360
tkinter_app/src/virtual_keyboard.py
Normal file
@@ -0,0 +1,360 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Virtual Keyboard Component for Touch Displays
|
||||
Provides an on-screen keyboard for touch-friendly input
|
||||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
class VirtualKeyboard:
|
||||
def __init__(self, parent, target_entry=None, dark_theme=True):
|
||||
self.parent = parent
|
||||
self.target_entry = target_entry
|
||||
self.dark_theme = dark_theme
|
||||
self.keyboard_window = None
|
||||
self.caps_lock = False
|
||||
self.shift_pressed = False
|
||||
|
||||
# Define color schemes
|
||||
if dark_theme:
|
||||
self.colors = {
|
||||
'bg_primary': '#1e2124',
|
||||
'bg_secondary': '#2f3136',
|
||||
'bg_tertiary': '#36393f',
|
||||
'accent': '#7289da',
|
||||
'accent_hover': '#677bc4',
|
||||
'text_primary': '#ffffff',
|
||||
'text_secondary': '#b9bbbe',
|
||||
'key_normal': '#4f545c',
|
||||
'key_hover': '#5865f2',
|
||||
'key_special': '#ed4245',
|
||||
'key_function': '#57f287'
|
||||
}
|
||||
else:
|
||||
self.colors = {
|
||||
'bg_primary': '#ffffff',
|
||||
'bg_secondary': '#f8f9fa',
|
||||
'bg_tertiary': '#e9ecef',
|
||||
'accent': '#0d6efd',
|
||||
'accent_hover': '#0b5ed7',
|
||||
'text_primary': '#000000',
|
||||
'text_secondary': '#6c757d',
|
||||
'key_normal': '#dee2e6',
|
||||
'key_hover': '#0d6efd',
|
||||
'key_special': '#dc3545',
|
||||
'key_function': '#198754'
|
||||
}
|
||||
|
||||
def show_keyboard(self, entry_widget=None):
|
||||
"""Show the virtual keyboard"""
|
||||
if entry_widget:
|
||||
self.target_entry = entry_widget
|
||||
|
||||
if self.keyboard_window and self.keyboard_window.winfo_exists():
|
||||
self.keyboard_window.lift()
|
||||
return
|
||||
|
||||
self.create_keyboard()
|
||||
|
||||
def hide_keyboard(self):
|
||||
"""Hide the virtual keyboard"""
|
||||
if self.keyboard_window and self.keyboard_window.winfo_exists():
|
||||
self.keyboard_window.destroy()
|
||||
self.keyboard_window = None
|
||||
|
||||
def create_keyboard(self):
|
||||
"""Create the virtual keyboard window"""
|
||||
self.keyboard_window = tk.Toplevel(self.parent)
|
||||
self.keyboard_window.title("Virtual Keyboard")
|
||||
self.keyboard_window.configure(bg=self.colors['bg_primary'])
|
||||
self.keyboard_window.resizable(False, False)
|
||||
|
||||
# Make keyboard stay on top
|
||||
self.keyboard_window.attributes('-topmost', True)
|
||||
|
||||
# Position keyboard at bottom of screen
|
||||
self.position_keyboard()
|
||||
|
||||
# Create keyboard layout
|
||||
self.create_keyboard_layout()
|
||||
|
||||
# Bind events
|
||||
self.keyboard_window.protocol("WM_DELETE_WINDOW", self.hide_keyboard)
|
||||
|
||||
def position_keyboard(self):
|
||||
"""Position keyboard at bottom center of screen"""
|
||||
self.keyboard_window.update_idletasks()
|
||||
|
||||
# Get screen dimensions
|
||||
screen_width = self.keyboard_window.winfo_screenwidth()
|
||||
screen_height = self.keyboard_window.winfo_screenheight()
|
||||
|
||||
# Keyboard dimensions
|
||||
kb_width = 800
|
||||
kb_height = 300
|
||||
|
||||
# Position at bottom center
|
||||
x = (screen_width - kb_width) // 2
|
||||
y = screen_height - kb_height - 50 # 50px from bottom
|
||||
|
||||
self.keyboard_window.geometry(f"{kb_width}x{kb_height}+{x}+{y}")
|
||||
|
||||
def create_keyboard_layout(self):
|
||||
"""Create the keyboard layout"""
|
||||
main_frame = tk.Frame(self.keyboard_window, bg=self.colors['bg_primary'], padx=10, pady=10)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Title bar
|
||||
title_frame = tk.Frame(main_frame, bg=self.colors['bg_secondary'], height=40)
|
||||
title_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
title_frame.pack_propagate(False)
|
||||
|
||||
title_label = tk.Label(title_frame, text="⌨️ Virtual Keyboard",
|
||||
font=('Segoe UI', 12, 'bold'),
|
||||
bg=self.colors['bg_secondary'], fg=self.colors['text_primary'])
|
||||
title_label.pack(side=tk.LEFT, padx=10, pady=10)
|
||||
|
||||
# Close button
|
||||
close_btn = tk.Button(title_frame, text="✕", command=self.hide_keyboard,
|
||||
bg=self.colors['key_special'], fg=self.colors['text_primary'],
|
||||
font=('Segoe UI', 12, 'bold'), relief=tk.FLAT, width=3)
|
||||
close_btn.pack(side=tk.RIGHT, padx=10, pady=5)
|
||||
|
||||
# Keyboard rows
|
||||
self.create_keyboard_rows(main_frame)
|
||||
|
||||
def create_keyboard_rows(self, parent):
|
||||
"""Create keyboard rows"""
|
||||
# Define keyboard layout
|
||||
rows = [
|
||||
['`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 'Backspace'],
|
||||
['Tab', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\'],
|
||||
['Caps', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', "'", 'Enter'],
|
||||
['Shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', 'Shift'],
|
||||
['Ctrl', 'Alt', 'Space', 'Alt', 'Ctrl']
|
||||
]
|
||||
|
||||
# Special keys with different sizes
|
||||
special_keys = {
|
||||
'Backspace': 2,
|
||||
'Tab': 1.5,
|
||||
'Enter': 2,
|
||||
'Caps': 1.8,
|
||||
'Shift': 2.3,
|
||||
'Ctrl': 1.2,
|
||||
'Alt': 1.2,
|
||||
'Space': 6
|
||||
}
|
||||
|
||||
for row_index, row in enumerate(rows):
|
||||
row_frame = tk.Frame(parent, bg=self.colors['bg_primary'])
|
||||
row_frame.pack(fill=tk.X, pady=2)
|
||||
|
||||
for key in row:
|
||||
width = special_keys.get(key, 1)
|
||||
self.create_key_button(row_frame, key, width)
|
||||
|
||||
def create_key_button(self, parent, key, width=1):
|
||||
"""Create a keyboard key button"""
|
||||
# Determine key type and color
|
||||
if key in ['Backspace', 'Tab', 'Enter', 'Caps', 'Shift', 'Ctrl', 'Alt']:
|
||||
bg_color = self.colors['key_function']
|
||||
elif key == 'Space':
|
||||
bg_color = self.colors['key_normal']
|
||||
else:
|
||||
bg_color = self.colors['key_normal']
|
||||
|
||||
# Calculate button width
|
||||
base_width = 4
|
||||
button_width = int(base_width * width)
|
||||
|
||||
# Display text for special keys
|
||||
display_text = {
|
||||
'Backspace': '⌫',
|
||||
'Tab': '⇥',
|
||||
'Enter': '⏎',
|
||||
'Caps': '⇪',
|
||||
'Shift': '⇧',
|
||||
'Ctrl': 'Ctrl',
|
||||
'Alt': 'Alt',
|
||||
'Space': '___'
|
||||
}.get(key, key.upper() if self.caps_lock or self.shift_pressed else key)
|
||||
|
||||
button = tk.Button(parent, text=display_text,
|
||||
command=lambda k=key: self.key_pressed(k),
|
||||
bg=bg_color, fg=self.colors['text_primary'],
|
||||
font=('Segoe UI', 10, 'bold'),
|
||||
relief=tk.FLAT, bd=1,
|
||||
width=button_width, height=2)
|
||||
|
||||
# Add hover effects
|
||||
def on_enter(e, btn=button):
|
||||
btn.configure(bg=self.colors['key_hover'])
|
||||
|
||||
def on_leave(e, btn=button):
|
||||
btn.configure(bg=bg_color)
|
||||
|
||||
button.bind("<Enter>", on_enter)
|
||||
button.bind("<Leave>", on_leave)
|
||||
|
||||
button.pack(side=tk.LEFT, padx=1, pady=1)
|
||||
|
||||
def key_pressed(self, key):
|
||||
"""Handle key press"""
|
||||
if not self.target_entry:
|
||||
return
|
||||
|
||||
if key == 'Backspace':
|
||||
current_pos = self.target_entry.index(tk.INSERT)
|
||||
if current_pos > 0:
|
||||
self.target_entry.delete(current_pos - 1)
|
||||
|
||||
elif key == 'Tab':
|
||||
self.target_entry.insert(tk.INSERT, '\t')
|
||||
|
||||
elif key == 'Enter':
|
||||
# Try to trigger any bound return event
|
||||
self.target_entry.event_generate('<Return>')
|
||||
|
||||
elif key == 'Caps':
|
||||
self.caps_lock = not self.caps_lock
|
||||
self.update_key_display()
|
||||
|
||||
elif key == 'Shift':
|
||||
self.shift_pressed = not self.shift_pressed
|
||||
self.update_key_display()
|
||||
|
||||
elif key == 'Space':
|
||||
self.target_entry.insert(tk.INSERT, ' ')
|
||||
|
||||
elif key in ['Ctrl', 'Alt']:
|
||||
# These could be used for key combinations in the future
|
||||
pass
|
||||
|
||||
else:
|
||||
# Regular character
|
||||
char = key.upper() if self.caps_lock or self.shift_pressed else key
|
||||
|
||||
# Handle shifted characters
|
||||
if self.shift_pressed and not self.caps_lock:
|
||||
shift_map = {
|
||||
'1': '!', '2': '@', '3': '#', '4': '$', '5': '%',
|
||||
'6': '^', '7': '&', '8': '*', '9': '(', '0': ')',
|
||||
'-': '_', '=': '+', '[': '{', ']': '}', '\\': '|',
|
||||
';': ':', "'": '"', ',': '<', '.': '>', '/': '?',
|
||||
'`': '~'
|
||||
}
|
||||
char = shift_map.get(key, char)
|
||||
|
||||
self.target_entry.insert(tk.INSERT, char)
|
||||
|
||||
# Reset shift after character input
|
||||
if self.shift_pressed:
|
||||
self.shift_pressed = False
|
||||
self.update_key_display()
|
||||
|
||||
def update_key_display(self):
|
||||
"""Update key display based on caps lock and shift state"""
|
||||
# This would update the display of keys, but for simplicity
|
||||
# we'll just recreate the keyboard when needed
|
||||
pass
|
||||
|
||||
|
||||
class TouchOptimizedEntry(tk.Entry):
|
||||
"""Entry widget optimized for touch displays with virtual keyboard"""
|
||||
|
||||
def __init__(self, parent, virtual_keyboard=None, **kwargs):
|
||||
# Make entry larger for touch
|
||||
kwargs.setdefault('font', ('Segoe UI', 12))
|
||||
kwargs.setdefault('relief', tk.FLAT)
|
||||
kwargs.setdefault('bd', 8)
|
||||
|
||||
super().__init__(parent, **kwargs)
|
||||
|
||||
self.virtual_keyboard = virtual_keyboard
|
||||
|
||||
# Bind focus events to show/hide keyboard
|
||||
self.bind('<FocusIn>', self.on_focus_in)
|
||||
self.bind('<Button-1>', self.on_click)
|
||||
|
||||
def on_focus_in(self, event):
|
||||
"""Show virtual keyboard when entry gets focus"""
|
||||
if self.virtual_keyboard:
|
||||
self.virtual_keyboard.show_keyboard(self)
|
||||
|
||||
def on_click(self, event):
|
||||
"""Show virtual keyboard when entry is clicked"""
|
||||
if self.virtual_keyboard:
|
||||
self.virtual_keyboard.show_keyboard(self)
|
||||
|
||||
|
||||
class TouchOptimizedButton(tk.Button):
|
||||
"""Button widget optimized for touch displays"""
|
||||
|
||||
def __init__(self, parent, **kwargs):
|
||||
# Make buttons larger for touch
|
||||
kwargs.setdefault('font', ('Segoe UI', 11, 'bold'))
|
||||
kwargs.setdefault('relief', tk.FLAT)
|
||||
kwargs.setdefault('padx', 20)
|
||||
kwargs.setdefault('pady', 12)
|
||||
kwargs.setdefault('cursor', 'hand2')
|
||||
|
||||
super().__init__(parent, **kwargs)
|
||||
|
||||
# Add touch feedback
|
||||
self.bind('<Button-1>', self.on_touch_down)
|
||||
self.bind('<ButtonRelease-1>', self.on_touch_up)
|
||||
|
||||
def on_touch_down(self, event):
|
||||
"""Visual feedback when button is touched"""
|
||||
self.configure(relief=tk.SUNKEN)
|
||||
|
||||
def on_touch_up(self, event):
|
||||
"""Reset visual feedback when touch is released"""
|
||||
self.configure(relief=tk.FLAT)
|
||||
|
||||
|
||||
# Test the virtual keyboard
|
||||
if __name__ == "__main__":
|
||||
def test_virtual_keyboard():
|
||||
root = tk.Tk()
|
||||
root.title("Virtual Keyboard Test")
|
||||
root.geometry("600x400")
|
||||
root.configure(bg='#2f3136')
|
||||
|
||||
# Create virtual keyboard instance
|
||||
vk = VirtualKeyboard(root, dark_theme=True)
|
||||
|
||||
# Test frame
|
||||
test_frame = tk.Frame(root, bg='#2f3136', padx=20, pady=20)
|
||||
test_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Title
|
||||
tk.Label(test_frame, text="🎮 Touch Display Test",
|
||||
font=('Segoe UI', 16, 'bold'),
|
||||
bg='#2f3136', fg='white').pack(pady=20)
|
||||
|
||||
# Test entries
|
||||
tk.Label(test_frame, text="Click entries to show virtual keyboard:",
|
||||
bg='#2f3136', fg='white', font=('Segoe UI', 12)).pack(pady=10)
|
||||
|
||||
entry1 = TouchOptimizedEntry(test_frame, vk, width=30, bg='#36393f',
|
||||
fg='white', insertbackground='white')
|
||||
entry1.pack(pady=10)
|
||||
|
||||
entry2 = TouchOptimizedEntry(test_frame, vk, width=30, bg='#36393f',
|
||||
fg='white', insertbackground='white')
|
||||
entry2.pack(pady=10)
|
||||
|
||||
# Test buttons
|
||||
TouchOptimizedButton(test_frame, text="Show Keyboard",
|
||||
command=lambda: vk.show_keyboard(entry1),
|
||||
bg='#7289da', fg='white').pack(pady=10)
|
||||
|
||||
TouchOptimizedButton(test_frame, text="Hide Keyboard",
|
||||
command=vk.hide_keyboard,
|
||||
bg='#ed4245', fg='white').pack(pady=5)
|
||||
|
||||
root.mainloop()
|
||||
|
||||
test_virtual_keyboard()
|
||||
15
tkinter_requirements.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
# Tkinter Media Player Requirements - Raspberry Pi Compatible
|
||||
# Core GUI and media handling (lighter alternatives)
|
||||
opencv-python-headless>=4.8.0
|
||||
Pillow>=9.0.0
|
||||
pygame>=2.1.0
|
||||
|
||||
# Networking and data handling
|
||||
requests>=2.28.0
|
||||
|
||||
# System utilities for Python 3.9+
|
||||
# pathlib2 not needed on modern Python versions
|
||||
|
||||
# Optional: Basic image processing without heavy dependencies
|
||||
# numpy - will be installed with opencv-python-headless
|
||||
bcrypt
|
||||
7
tkinter_requirements_minimal.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
# Minimal Tkinter Media Player Requirements - Raspberry Pi Compatible
|
||||
# Core dependencies only
|
||||
requests>=2.28.0
|
||||
|
||||
# Optional but recommended if available via apt
|
||||
# python3-pil (install via apt instead of pip)
|
||||
# python3-tk (should already be installed with Python)
|
||||