Compare commits
13 Commits
267ebb9251
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a4cc026e38 | |||
| 65843c255a | |||
| 7e69b12f71 | |||
| 1197077954 | |||
| 8293111e09 | |||
| 93c0637bca | |||
| 4e2909718c | |||
| f7d749cc8b | |||
| 01a6a95645 | |||
| cca7d75376 | |||
| 71eab75d51 | |||
| 45c4040fa4 | |||
| 5f2736c15a |
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
venv/
|
||||
110
Install.sh
@@ -1,110 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Define variables
|
||||
REPO_URL="https://gitea.moto-adv.com/ske087/signage-player.git"
|
||||
DEFAULT_INSTALL_DIR="/home/pi/signage-player"
|
||||
SERVICE_FILE="/etc/systemd/system/signage_player.service"
|
||||
RUN_APP_FILE="$DEFAULT_INSTALL_DIR/run_app.sh"
|
||||
|
||||
# Ask the user whether to use the default installation directory
|
||||
echo "The default installation directory is: $DEFAULT_INSTALL_DIR"
|
||||
read -p "Do you want to use the default installation directory? (y/n): " response
|
||||
|
||||
# Handle the user's response
|
||||
if [[ "$response" == "y" || "$response" == "Y" ]]; then
|
||||
INSTALL_DIR="$DEFAULT_INSTALL_DIR"
|
||||
echo "Using default installation directory: $INSTALL_DIR"
|
||||
else
|
||||
read -p "Enter the custom installation directory: " CUSTOM_INSTALL_DIR
|
||||
INSTALL_DIR="$CUSTOM_INSTALL_DIR"
|
||||
if [[ ! -d "$INSTALL_DIR" ]]; then
|
||||
echo "Directory $INSTALL_DIR does not exist. Creating it..."
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "Error: Failed to create directory $INSTALL_DIR."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "Using custom installation directory: $INSTALL_DIR"
|
||||
fi
|
||||
|
||||
# Update system packages
|
||||
echo "Updating system packages..."
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# Install required system packages
|
||||
echo "Installing required system packages..."
|
||||
sudo apt install -y python3 python3-pip git
|
||||
|
||||
# Clone the repository
|
||||
echo "Cloning the repository..."
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
echo "Directory $INSTALL_DIR already exists. Removing it..."
|
||||
sudo rm -rf "$INSTALL_DIR"
|
||||
fi
|
||||
git clone "$REPO_URL" "$INSTALL_DIR"
|
||||
|
||||
# Navigate to the cloned repository
|
||||
cd "$INSTALL_DIR" || exit
|
||||
|
||||
# Install Python dependencies
|
||||
echo "Installing Python dependencies..."
|
||||
pip3 install -r requirements.txt --break-system-packages
|
||||
|
||||
# Set permissions for the run_app.sh script
|
||||
echo "Setting permissions for run_app.sh..."
|
||||
chmod +x "$INSTALL_DIR/run_app.sh"
|
||||
|
||||
# Check and update the signage_player.service file
|
||||
if [[ -f "$SERVICE_FILE" ]]; then
|
||||
echo "Checking signage_player.service file..."
|
||||
WORKING_DIR=$(grep -oP '(?<=^WorkingDirectory=).*' "$SERVICE_FILE")
|
||||
EXEC_START=$(grep -oP '(?<=^ExecStart=).*' "$SERVICE_FILE")
|
||||
|
||||
EXPECTED_WORKING_DIR="$INSTALL_DIR"
|
||||
EXPECTED_EXEC_START="$INSTALL_DIR/run_app.sh"
|
||||
|
||||
if [[ "$WORKING_DIR" != "$EXPECTED_WORKING_DIR" || "$EXEC_START" != "$EXPECTED_EXEC_START" ]]; then
|
||||
echo "Updating signage_player.service file..."
|
||||
sudo cp "$INSTALL_DIR/signage_player.service" "$SERVICE_FILE"
|
||||
sudo sed -i "s|^WorkingDirectory=.*|WorkingDirectory=$INSTALL_DIR|" "$SERVICE_FILE"
|
||||
sudo sed -i "s|^ExecStart=.*|ExecStart=$INSTALL_DIR/run_app.sh|" "$SERVICE_FILE"
|
||||
sudo systemctl daemon-reload
|
||||
echo "signage_player.service file updated successfully."
|
||||
else
|
||||
echo "signage_player.service file is already configured correctly."
|
||||
fi
|
||||
else
|
||||
echo "signage_player.service file not found. Copying and configuring it..."
|
||||
sudo cp "$INSTALL_DIR/signage_player.service" "$SERVICE_FILE"
|
||||
sudo sed -i "s|^WorkingDirectory=.*|WorkingDirectory=$INSTALL_DIR|" "$SERVICE_FILE"
|
||||
sudo sed -i "s|^ExecStart=.*|ExecStart=$INSTALL_DIR/run_app.sh|" "$SERVICE_FILE"
|
||||
sudo systemctl daemon-reload
|
||||
echo "signage_player.service file created and configured successfully."
|
||||
fi
|
||||
|
||||
# Check and update the working directory in run_app.sh
|
||||
echo "Checking run_app.sh for correct working directory..."
|
||||
if [[ -f "$RUN_APP_FILE" ]]; then
|
||||
CURRENT_WORKING_DIR=$(grep -oP '(?<=^cd ).*' "$RUN_APP_FILE" | head -n 1)
|
||||
EXPECTED_WORKING_DIR="$INSTALL_DIR/src"
|
||||
|
||||
if [[ "$CURRENT_WORKING_DIR" != "$EXPECTED_WORKING_DIR" ]]; then
|
||||
echo "Updating working directory in run_app.sh..."
|
||||
sudo sed -i "s|^cd .*|cd $EXPECTED_WORKING_DIR|" "$RUN_APP_FILE"
|
||||
echo "run_app.sh updated successfully."
|
||||
else
|
||||
echo "run_app.sh is already configured correctly."
|
||||
fi
|
||||
else
|
||||
echo "Error: run_app.sh file not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Enable the service
|
||||
echo "Enabling signage_player.service..."
|
||||
sudo systemctl enable signage_player.service
|
||||
|
||||
# Restart the system to check if the service starts
|
||||
echo "Restarting the system to check if the service starts..."
|
||||
sudo reboot
|
||||
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"
|
||||
@@ -1,5 +0,0 @@
|
||||
kivy
|
||||
requests
|
||||
watchdog
|
||||
Pillow
|
||||
bctypt
|
||||
17
run_app.sh
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# filepath: /home/pi/Desktop/signage-player/run_app.sh
|
||||
|
||||
# Navigate to the application directory
|
||||
cd /home/pi/signage-player/src || exit
|
||||
|
||||
# Check for the --verbose flag
|
||||
if [[ "$1" == "--verbose" ]]; then
|
||||
# Run the application with terminal output
|
||||
echo "Starting the application with terminal output..."
|
||||
python3 media_player.py
|
||||
else
|
||||
# Run the application and suppress terminal output
|
||||
echo "Starting the application without terminal output..."
|
||||
python3 media_player.py > /dev/null 2>&1 &
|
||||
fi
|
||||
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
|
||||
@@ -1,15 +0,0 @@
|
||||
[Unit]
|
||||
Description=Signage Player Service
|
||||
After=network.targhet
|
||||
|
||||
[Service]
|
||||
ExecStart=/home/pi/signage-player/run_app.sh
|
||||
WorkingDirectory=/home/pi/signage-player
|
||||
StandardOutput=inherit
|
||||
StandardError=inherit
|
||||
Restart=always
|
||||
User=pi
|
||||
Enviroment=DISPLAY=:0
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.targhet
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
{"screen_orientation": "Landscape", "screen_name": "rpi-tv11", "quickconnect_key": "8887779", "server_ip": "192.168.1.74", "port": "5000", "screen_w": "1920", "screen_h": "1080"}
|
||||
@@ -1,7 +0,0 @@
|
||||
2025-06-20 16:32:40 - STARTED: edit_pencil.png
|
||||
2025-06-20 16:33:00 - STARTED: delete.png
|
||||
2025-06-20 16:35:13 - STARTED: edit_pencil.png
|
||||
2025-06-20 16:35:15 - STARTED: delete.png
|
||||
2025-06-20 16:35:16 - STARTED: edit_pencil.png
|
||||
2025-06-20 16:35:17 - STARTED: delete.png
|
||||
2025-06-20 16:35:18 - STARTED: edit_pencil.png
|
||||
@@ -1,180 +0,0 @@
|
||||
<MediaPlayer>:
|
||||
video_player: video_player
|
||||
image_display: image_display
|
||||
|
||||
Video:
|
||||
id: video_player
|
||||
allow_stretch: True
|
||||
size_hint: (1, 1)
|
||||
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
|
||||
opacity: 0
|
||||
|
||||
Image:
|
||||
id: image_display
|
||||
allow_stretch: True
|
||||
keep_ratio: True
|
||||
size_hint: (1, 1)
|
||||
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
|
||||
opacity: 0
|
||||
|
||||
# Settings (Home) button
|
||||
Button:
|
||||
id: settings_button
|
||||
size_hint: None, None
|
||||
size: 75, 75
|
||||
pos_hint: {'right': 0.98, 'bottom': 0.98}
|
||||
background_normal: './Resurse/home_icon.png'
|
||||
background_down: './Resurse/home_icon.png'
|
||||
background_color: 1, 1, 1, 0.9 # Temporary red background for debugging
|
||||
border: [0, 0, 0, 0] # Remove the default border
|
||||
opacity: 1
|
||||
on_release: app.open_settings()
|
||||
|
||||
# Right Arrow button
|
||||
Button:
|
||||
id: right_arrow_button
|
||||
size_hint: None, None
|
||||
size: 75, 75
|
||||
pos_hint: {'right': 0.93, 'bottom': 0.98}
|
||||
background_normal: './Resurse/left-arrow-blue.png'
|
||||
background_down: './Resurse/left-arrow-green.png'
|
||||
background_color: 1, 1, 1, 0.9 # Temporary green background for debugging
|
||||
border: [0, 0, 0, 0] # Remove the default border
|
||||
opacity: 1
|
||||
on_release: root.next_media(None)
|
||||
|
||||
# Play/Pause button
|
||||
Button:
|
||||
id: play_pause_button
|
||||
size_hint: None, None
|
||||
size: 75, 75
|
||||
pos_hint: {'right': 0.88, 'bottom': 0.98}
|
||||
background_normal: './Resurse/play.png' # Initial state
|
||||
background_down: './Resurse/play.png' # Initial state
|
||||
background_color: 1, 1, 1, 0.9 # White with 90% transparency
|
||||
border: [0, 0, 0, 0]
|
||||
opacity: 1
|
||||
on_press: root.manage_play_pause_state() # Call the new function
|
||||
|
||||
# Left Arrow button
|
||||
Button:
|
||||
id: left_arrow_button
|
||||
size_hint: None, None
|
||||
size: 75, 75
|
||||
pos_hint: {'right': 0.83, 'bottom': 0.98}
|
||||
background_normal: './Resurse/right-arrow-blue.png'
|
||||
background_down: './Resurse/right-arrow-green.png'
|
||||
background_color: 1, 1, 1, 0.9 # Temporary yellow background for debugging
|
||||
border: [0, 0, 0, 0] # Remove the default border
|
||||
opacity: 1
|
||||
on_release: root.previous_media()
|
||||
|
||||
<SettingsScreen>:
|
||||
BoxLayout:
|
||||
orientation: 'vertical'
|
||||
padding: 20
|
||||
spacing: 10
|
||||
|
||||
# Input fields in the upper half of the screen
|
||||
BoxLayout:
|
||||
orientation: 'vertical'
|
||||
size_hint_y: 0.4 # Allocate 85% of the screen height for input fields
|
||||
|
||||
Label:
|
||||
text: "Screen Orientation"
|
||||
size_hint_y: None
|
||||
height: 40
|
||||
|
||||
TextInput:
|
||||
id: orientation_input
|
||||
hint_text: "Enter 'portrait' or 'landscape'"
|
||||
multiline: False
|
||||
size_hint_y: None
|
||||
height: 40
|
||||
|
||||
Label:
|
||||
text: "Screen Name"
|
||||
size_hint_y: None
|
||||
height: 40
|
||||
|
||||
TextInput:
|
||||
id: screen_name_input
|
||||
hint_text: "Enter screen name"
|
||||
multiline: False
|
||||
size_hint_y: None
|
||||
height: 40
|
||||
|
||||
Label:
|
||||
text: "QuickConnect Key"
|
||||
size_hint_y: None
|
||||
height: 40
|
||||
|
||||
TextInput:
|
||||
id: quickconnect_key_input
|
||||
hint_text: "Enter QuickConnect key"
|
||||
multiline: False
|
||||
size_hint_y: None
|
||||
height: 40
|
||||
|
||||
Label:
|
||||
text: "Server IP / Hostname"
|
||||
size_hint_y: None
|
||||
height: 40
|
||||
|
||||
TextInput:
|
||||
id: server_ip_input
|
||||
hint_text: "Enter server IP or hostname"
|
||||
multiline: False
|
||||
size_hint_y: None
|
||||
height: 40
|
||||
|
||||
Label:
|
||||
text: "Set Port"
|
||||
size_hint_y: None
|
||||
height: 40
|
||||
|
||||
TextInput:
|
||||
id: port_input
|
||||
hint_text: "Enter port number"
|
||||
multiline: False
|
||||
size_hint_y: None
|
||||
height: 40
|
||||
Label:
|
||||
text: "Screen Size Width / Height"
|
||||
size_hint_y: None
|
||||
height: 40
|
||||
|
||||
# New row for Screen Size Width / Height
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: None
|
||||
height: 40
|
||||
spacing: 10
|
||||
TextInput:
|
||||
id: screen_width_input
|
||||
hint_text: "Width"
|
||||
multiline: False
|
||||
size_hint_x: 0.3
|
||||
|
||||
TextInput:
|
||||
id: screen_height_input
|
||||
hint_text: "Height"
|
||||
multiline: False
|
||||
size_hint_x: 0.3
|
||||
|
||||
# Buttons in the lower part of the screen
|
||||
BoxLayout:
|
||||
size_hint_y: 0.4 # Allocate 40% of the screen height for buttons
|
||||
spacing: 20
|
||||
|
||||
Button:
|
||||
text: "Save"
|
||||
size_hint_y: None
|
||||
height: 50
|
||||
on_release: root.save_config()
|
||||
|
||||
Button:
|
||||
text: "Exit App"
|
||||
size_hint_y: None
|
||||
height: 50
|
||||
on_release: root.show_exit_popup()
|
||||
@@ -1,486 +0,0 @@
|
||||
from kivy.config import Config
|
||||
Config.set('kivy', 'video', 'ffpyplayer')
|
||||
|
||||
|
||||
# Now import other Kivy modules
|
||||
from kivy.app import App
|
||||
from kivy.uix.screenmanager import ScreenManager, Screen # Import ScreenManager and Screen for managing screens
|
||||
from kivy.clock import Clock # Import Clock for scheduling tasks
|
||||
from kivy.core.window import Window # Import Window for handling window events
|
||||
from kivy.uix.video import Video # Import Video widget for video playback
|
||||
from kivy.uix.image import Image # Import Image widget for displaying images
|
||||
from kivy.logger import Logger # Import Logger for logging messages
|
||||
from kivy.lang import Builder # Import Builder for loading KV files
|
||||
from kivy.animation import Animation # Import Animation for fade effects
|
||||
from kivy.uix.popup import Popup
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.label import Label
|
||||
from kivy.uix.textinput import TextInput
|
||||
from kivy.uix.button import Button
|
||||
import os # Import os for file and directory operations
|
||||
import json # Import json for handling JSON data
|
||||
import datetime # Import datetime for timestamping logs
|
||||
import subprocess
|
||||
|
||||
# Import functions from python_functions.py
|
||||
from python_functions import load_local_playlist, download_media_files, clean_unused_files, fetch_server_playlist
|
||||
|
||||
# Load the KV file for UI layout
|
||||
Builder.load_file('kv/media_player.kv')
|
||||
|
||||
# Path to the configuration file
|
||||
CONFIG_FILE = './Resurse/app_config.txt'
|
||||
|
||||
class MediaPlayer(Screen):
|
||||
# Main screen for media playback.
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(MediaPlayer, self).__init__(**kwargs)
|
||||
self.playlist = [] # Initialize the playlist
|
||||
self.current_index = 0 # Index of the currently playing media
|
||||
self.updated_playlist = None # Store the updated playlist
|
||||
self.is_playlist_update_pending = False # Flag to indicate a pending playlist update
|
||||
self.video_player = self.ids.video_player # Reference to the Video widget
|
||||
self.image_display = self.ids.image_display # Reference to the Image widget
|
||||
self.log_file = os.path.join(os.path.dirname(__file__), 'Resurse', 'log.txt') # Path to the log file
|
||||
self.is_paused = False # Track the state of the play/pause button
|
||||
self.reset_timer = None # Timer to reset the button state after 3 minutes
|
||||
self.image_timer = None # Timer for scheduling the next media for images
|
||||
|
||||
# Load screen size from the configuration file
|
||||
self.load_screen_size()
|
||||
|
||||
# Schedule periodic updates to check for playlist updates
|
||||
Clock.schedule_interval(self.check_playlist_updates, 300) # Every 5 minutes
|
||||
# Bind key events to handle fullscreen toggle
|
||||
Window.bind(on_key_down=self.on_key_down)
|
||||
|
||||
# Start a timer to hide the buttons after 10 seconds
|
||||
self.hide_button_timer = Clock.schedule_once(self.hide_buttons, 10)
|
||||
|
||||
def load_screen_size(self):
|
||||
"""Load screen size from the configuration file and set the window size."""
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
with open(CONFIG_FILE, 'r') as file:
|
||||
config_data = json.load(file)
|
||||
screen_w = config_data.get("screen_w", "1920")
|
||||
screen_h = config_data.get("screen_h", "1080")
|
||||
|
||||
# Set the window size
|
||||
try:
|
||||
Window.size = (int(screen_w), int(screen_h))
|
||||
Logger.info(f"Screen size set to {screen_w}x{screen_h}")
|
||||
except ValueError:
|
||||
Logger.error("Invalid screen size values in configuration file.")
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
# Handle touch events to reset the button visibility.
|
||||
self.show_buttons() # Make all buttons visible
|
||||
if hasattr(self, 'hide_button_timer'):
|
||||
Clock.unschedule(self.hide_button_timer) # Cancel the existing hide timer
|
||||
self.hide_button_timer = Clock.schedule_once(self.hide_buttons, 10) # Restart the hide timer
|
||||
return super(MediaPlayer, self).on_touch_down(touch)
|
||||
|
||||
def hide_buttons(self, *args):
|
||||
# Hide all buttons after inactivity.
|
||||
self.ids.settings_button.opacity = 0 # Hide the Home button
|
||||
self.ids.right_arrow_button.opacity = 0 # Hide the Right Arrow button
|
||||
self.ids.play_pause_button.opacity = 0 # Hide the Play/Pause button
|
||||
self.ids.left_arrow_button.opacity = 0 # Hide the Left Arrow button
|
||||
|
||||
def show_buttons(self):
|
||||
# Show all buttons.
|
||||
self.ids.settings_button.opacity = 1 # Show the Home button
|
||||
self.ids.right_arrow_button.opacity = 1 # Show the Right Arrow button
|
||||
self.ids.play_pause_button.opacity = 1 # Show the Play/Pause button
|
||||
self.ids.left_arrow_button.opacity = 1 # Show the Left Arrow button
|
||||
|
||||
def on_key_down(self, window, key, *args):
|
||||
# Handle key events for toggling fullscreen mode.
|
||||
if key == 102: # 'f' key
|
||||
Window.fullscreen = not Window.fullscreen
|
||||
|
||||
def on_enter(self):
|
||||
"""Called when the screen is entered."""
|
||||
Logger.info("MediaPlayer: Entering screen...")
|
||||
|
||||
# Attempt to load the local playlist
|
||||
self.playlist = load_local_playlist()
|
||||
Logger.info(f"MediaPlayer: Loaded local playlist: {self.playlist}")
|
||||
|
||||
if not self.playlist: # If no local playlist exists
|
||||
Logger.warning("MediaPlayer: No local playlist found. Fetching from server...")
|
||||
|
||||
# Fetch the server playlist
|
||||
server_playlist_data = fetch_server_playlist()
|
||||
server_playlist = server_playlist_data.get('playlist', [])
|
||||
server_version = server_playlist_data.get('version', 0)
|
||||
|
||||
if server_playlist: # If server playlist is valid
|
||||
Logger.info("MediaPlayer: Server playlist fetched successfully.")
|
||||
|
||||
# Download media files and save the playlist locally
|
||||
download_media_files(server_playlist, server_version)
|
||||
self.playlist = load_local_playlist() # Reload the updated local playlist
|
||||
|
||||
if self.playlist:
|
||||
Logger.info("MediaPlayer: Local playlist updated successfully.")
|
||||
else:
|
||||
Logger.error("MediaPlayer: Failed to update local playlist.")
|
||||
else:
|
||||
Logger.error("MediaPlayer: Failed to fetch server playlist. No media to play.")
|
||||
return
|
||||
|
||||
if self.playlist: # If the playlist is loaded successfully
|
||||
self.play_media() # Start playing media
|
||||
self.show_buttons() # Ensure buttons are visible when the screen is entered
|
||||
else:
|
||||
Logger.warning("MediaPlayer: Playlist is empty. No media to play.")
|
||||
|
||||
def log_event(self, file_name, event):
|
||||
# Log the start or stop event of a media file and clean up old logs.
|
||||
# Get the current timestamp
|
||||
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
log_message = f"{timestamp} - {event}: {file_name}\n" # Format the log message
|
||||
|
||||
# Write the log message to the log file
|
||||
with open(self.log_file, 'a') as log:
|
||||
log.write(log_message)
|
||||
Logger.info(f"Logged event: {log_message.strip()}") # Log the event to the console
|
||||
|
||||
# Clean up logs older than 24 hours
|
||||
self.cleanup_old_logs()
|
||||
|
||||
def cleanup_old_logs(self):
|
||||
# Delete log entries older than 24 hours.
|
||||
try:
|
||||
# Read all log entries
|
||||
if os.path.exists(self.log_file):
|
||||
with open(self.log_file, 'r') as log:
|
||||
lines = log.readlines()
|
||||
|
||||
# Get the current time
|
||||
now = datetime.datetime.now()
|
||||
|
||||
# Filter out log entries older than 24 hours
|
||||
filtered_lines = []
|
||||
for line in lines:
|
||||
try:
|
||||
# Extract the timestamp from the log entry
|
||||
timestamp_str = line.split(' - ')[0]
|
||||
log_time = datetime.datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# Keep the log entry if it's within the last 24 hours
|
||||
if (now - log_time).total_seconds() <= 86400: # 24 hours in seconds
|
||||
filtered_lines.append(line)
|
||||
except (ValueError, IndexError):
|
||||
# If the log entry is malformed, skip it
|
||||
Logger.warning(f"Malformed log entry skipped: {line.strip()}")
|
||||
|
||||
# Write the filtered log entries back to the log file
|
||||
with open(self.log_file, 'w') as log:
|
||||
log.writelines(filtered_lines)
|
||||
Logger.info("Old log entries cleaned up successfully.")
|
||||
except Exception as e:
|
||||
Logger.error(f"Failed to clean up old logs: {e}")
|
||||
|
||||
def play_media(self):
|
||||
"""Play the current media in the playlist."""
|
||||
if not self.playlist or not isinstance(self.playlist, list):
|
||||
Logger.error("MediaPlayer: Playlist is invalid or empty. Cannot play media.")
|
||||
return
|
||||
|
||||
media = self.playlist[self.current_index] # Get the current media
|
||||
file_name = media.get('file_name', '') # Get the file name
|
||||
file_extension = os.path.splitext(file_name)[1].lower() # Get the file extension
|
||||
duration = media.get('duration', 10) # Get the duration (default: 10 seconds)
|
||||
|
||||
# Define the base directory for media files
|
||||
base_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse')
|
||||
file_path = os.path.join(base_dir, file_name) # Full path to the media file
|
||||
|
||||
Logger.info(f"Playing media: {file_path}")
|
||||
|
||||
# Check if the file exists
|
||||
if not os.path.exists(file_path):
|
||||
Logger.error(f"Media file not found: {file_path}")
|
||||
return
|
||||
|
||||
# Cancel any existing timers
|
||||
if self.image_timer:
|
||||
Logger.info("Canceling existing image timer.")
|
||||
Clock.unschedule(self.image_timer)
|
||||
|
||||
# Log the start of the media
|
||||
self.log_event(file_name, "STARTED")
|
||||
|
||||
# Determine the type of media and play it
|
||||
if file_extension in ['.mp4', '.avi', '.mov']:
|
||||
self.play_video(file_path) # Play video
|
||||
elif file_extension in ['.jpg', '.jpeg', '.png', '.gif']:
|
||||
self.show_image(file_path, duration) # Show image
|
||||
else:
|
||||
Logger.error(f"Unsupported media type for file: {file_name}")
|
||||
|
||||
def play_video(self, file_path):
|
||||
"""Play a video file without a fade-in effect."""
|
||||
Logger.info(f"Playing video: {file_path}")
|
||||
if not os.path.exists(file_path):
|
||||
Logger.error(f"Video file not found: {file_path}")
|
||||
return
|
||||
|
||||
# Set the video source and start playback
|
||||
self.video_player.source = file_path
|
||||
self.video_player.state = 'play' # Start playing the video
|
||||
self.video_player.audio = True # Enable audio playback
|
||||
self.video_player.opacity = 1 # Ensure the video is fully visible
|
||||
self.image_display.opacity = 0 # Hide the image display
|
||||
|
||||
# Schedule the next media after the video's duration
|
||||
if self.video_player.duration > 0:
|
||||
Clock.schedule_once(self.next_media, self.video_player.duration)
|
||||
else:
|
||||
Logger.warning("Video duration is unknown. Using default duration")
|
||||
|
||||
def show_image(self, file_path, duration):
|
||||
"""Display an image with a fade-in effect."""
|
||||
Logger.info(f"Showing image: {file_path}")
|
||||
if not os.path.exists(file_path):
|
||||
Logger.error(f"Image file not found: {file_path}")
|
||||
return
|
||||
|
||||
# Set the image source
|
||||
self.image_display.source = file_path
|
||||
self.image_display.opacity = 0 # Start with the image hidden
|
||||
self.image_display.reload() # Reload the image to ensure it updates
|
||||
self.video_player.opacity = 0 # Hide the video player
|
||||
|
||||
# Create a fade-in animation
|
||||
fade_in = Animation(opacity=1, duration=1) # Fade in over 1 second
|
||||
fade_in.start(self.image_display) # Start the fade-in animation
|
||||
|
||||
# Schedule the next media after the duration
|
||||
self.image_timer = Clock.schedule_once(self.next_media, duration)
|
||||
|
||||
def next_media(self, dt=None):
|
||||
"""Move to the next media in the playlist."""
|
||||
Logger.info("Navigating to the next media.")
|
||||
|
||||
# Cancel any existing timers
|
||||
if self.image_timer:
|
||||
Logger.info("Canceling image timer.")
|
||||
Clock.unschedule(self.image_timer)
|
||||
|
||||
# Update the current index
|
||||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||
|
||||
# Play the next media
|
||||
self.play_media()
|
||||
|
||||
def previous_media(self):
|
||||
"""Move to the previous media in the playlist."""
|
||||
Logger.info("Navigating to the previous media.")
|
||||
|
||||
# Cancel any existing timers
|
||||
if self.image_timer:
|
||||
Logger.info("Canceling image timer.")
|
||||
Clock.unschedule(self.image_timer)
|
||||
|
||||
# Update the current index
|
||||
self.current_index = (self.current_index - 1) % len(self.playlist)
|
||||
|
||||
# Play the previous media
|
||||
self.play_media()
|
||||
|
||||
def toggle_play_pause(self):
|
||||
#Toggle the play/pause button state and update its appearance.
|
||||
self.manage_play_pause_state()
|
||||
|
||||
def manage_play_pause_state(self):
|
||||
# Manage the state of the play/pause button and media playback.
|
||||
if self.is_paused:
|
||||
Logger.info("Resuming media playback.")
|
||||
self.video_player.state = 'play'
|
||||
|
||||
# Resume the image timer if it exists
|
||||
if self.image_timer:
|
||||
Logger.info("Resuming image timer.")
|
||||
self.image_timer()
|
||||
|
||||
# Update the button to indicate the playing state
|
||||
self.ids.play_pause_button.background_down = './Resurse/play.png'
|
||||
self.ids.play_pause_button.background_normal = './Resurse/play.png'
|
||||
|
||||
# Cancel the reset timer if it exists
|
||||
if self.reset_timer:
|
||||
Clock.unschedule(self.reset_timer)
|
||||
else:
|
||||
Logger.info("Pausing media playback.")
|
||||
self.video_player.state = 'pause'
|
||||
|
||||
# Pause the image timer if it exists
|
||||
if self.image_timer:
|
||||
Logger.info("Pausing image timer.")
|
||||
Clock.unschedule(self.image_timer)
|
||||
|
||||
# Update the button to indicate the paused state
|
||||
self.ids.play_pause_button.background_down = './Resurse/pause.png'
|
||||
self.ids.play_pause_button.background_normal = './Resurse/pause.png'
|
||||
|
||||
# Start a timer to reset the button state after 30 seconds
|
||||
self.reset_timer = Clock.schedule_once(self.reset_play_pause_state, 30)
|
||||
|
||||
# Toggle the state
|
||||
self.is_paused = not self.is_paused
|
||||
|
||||
def reset_play_pause_state(self, dt):
|
||||
# Reset the play/pause button state to 'play' after 30 seconds.
|
||||
Logger.info("Resetting play/pause button state to 'play' after timeout.")
|
||||
self.is_paused = False
|
||||
self.video_player.state = 'play'
|
||||
|
||||
# Resume the image timer if it exists
|
||||
if self.image_timer:
|
||||
Logger.info("Resuming image timer.")
|
||||
self.image_timer()
|
||||
|
||||
# Update the button appearance
|
||||
self.ids.play_pause_button.background_down = './Resurse/play.png'
|
||||
self.ids.play_pause_button.background_normal = './Resurse/play.png'
|
||||
|
||||
def check_playlist_updates(self, dt):
|
||||
"""Check for updates to the playlist."""
|
||||
Logger.info("Checking for playlist updates...")
|
||||
|
||||
# Fetch the server playlist
|
||||
server_playlist_data = fetch_server_playlist() # Fetch the playlist from the server
|
||||
server_playlist = server_playlist_data.get('playlist', [])
|
||||
server_version = server_playlist_data.get('version', 0)
|
||||
|
||||
# Load the local playlist
|
||||
local_playlist_data = load_local_playlist() # Load the local playlist
|
||||
local_version = local_playlist_data.get('version', 0) if local_playlist_data else 0
|
||||
|
||||
# Compare versions
|
||||
if server_version != local_version: # If versions differ
|
||||
Logger.info(f"Playlist version mismatch detected. Local version: {local_version}, Server version: {server_version}")
|
||||
|
||||
# Update the local playlist and download new media files
|
||||
download_media_files(server_playlist) # Download media files from the server
|
||||
self.playlist = load_local_playlist() # Reload the updated local playlist
|
||||
Logger.info("Playlist updated successfully.")
|
||||
else:
|
||||
Logger.info("Playlist versions match. No update needed.")
|
||||
|
||||
class SettingsScreen(Screen):
|
||||
"""Settings screen for configuring the app."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(SettingsScreen, self).__init__(**kwargs)
|
||||
self.config_data = self.load_config() # Load the configuration data
|
||||
|
||||
def load_config(self):
|
||||
"""Load the configuration from the config file."""
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
with open(CONFIG_FILE, 'r') as file:
|
||||
return json.load(file)
|
||||
return {
|
||||
"screen_orientation": "",
|
||||
"screen_name": "",
|
||||
"quickconnect_key": "",
|
||||
"server_ip": "",
|
||||
"port": "",
|
||||
"screen_w": "", # Default width
|
||||
"screen_h": "" # Default height
|
||||
}
|
||||
|
||||
def save_config(self):
|
||||
"""Save the configuration to the config file."""
|
||||
self.config_data["screen_orientation"] = self.ids.orientation_input.text
|
||||
self.config_data["screen_name"] = self.ids.screen_name_input.text
|
||||
self.config_data["quickconnect_key"] = self.ids.quickconnect_key_input.text
|
||||
self.config_data["server_ip"] = self.ids.server_ip_input.text
|
||||
self.config_data["port"] = self.ids.port_input.text
|
||||
self.config_data["screen_w"] = self.ids.screen_width_input.text
|
||||
self.config_data["screen_h"] = self.ids.screen_height_input.text
|
||||
|
||||
with open(CONFIG_FILE, 'w') as file:
|
||||
json.dump(self.config_data, file)
|
||||
Logger.info("SettingsScreen: Configuration saved.")
|
||||
|
||||
# Return to the MediaPlayer screen after saving
|
||||
self.manager.current = 'media_player'
|
||||
|
||||
def on_pre_enter(self):
|
||||
"""Populate input fields with current config data."""
|
||||
self.ids.orientation_input.text = self.config_data.get("screen_orientation", "landscape")
|
||||
self.ids.screen_name_input.text = self.config_data.get("screen_name", "")
|
||||
self.ids.quickconnect_key_input.text = self.config_data.get("quickconnect_key", "")
|
||||
self.ids.server_ip_input.text = self.config_data.get("server_ip", "")
|
||||
self.ids.port_input.text = self.config_data.get("port", "8080")
|
||||
self.ids.screen_width_input.text = self.config_data.get("screen_w", "")
|
||||
self.ids.screen_height_input.text = self.config_data.get("screen_h", "")
|
||||
|
||||
def show_exit_popup(self):
|
||||
# Create the popup layout
|
||||
layout = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||||
|
||||
# Add a label
|
||||
label = Label(text="Enter Password to Exit", size_hint=(1, 0.3))
|
||||
layout.add_widget(label)
|
||||
|
||||
# Add a password input field
|
||||
password_input = TextInput(password=True, multiline=False, size_hint=(1, 0.3))
|
||||
layout.add_widget(password_input)
|
||||
|
||||
# Add buttons for "OK" and "Cancel"
|
||||
button_layout = BoxLayout(size_hint=(1, 0.3), spacing=10)
|
||||
ok_button = Button(text="OK", on_release=lambda *args: self.validate_exit_password(password_input.text, popup))
|
||||
cancel_button = Button(text="Cancel", on_release=lambda *args: popup.dismiss())
|
||||
button_layout.add_widget(ok_button)
|
||||
button_layout.add_widget(cancel_button)
|
||||
layout.add_widget(button_layout)
|
||||
|
||||
# Create the popup
|
||||
popup = Popup(title="Exit App", content=layout, size_hint=(0.8, 0.4))
|
||||
popup.open()
|
||||
|
||||
def validate_exit_password(self, password, popup):
|
||||
# Validate the entered password
|
||||
quickconnect_key = self.config_data.get("quickconnect_key", "")
|
||||
|
||||
if password == quickconnect_key:
|
||||
Logger.info("Password correct. Exiting app.")
|
||||
App.get_running_app().stop() # Exit the app
|
||||
else:
|
||||
Logger.warning("Incorrect password. Returning to SettingsScreen.")
|
||||
popup.dismiss() # Close the popup
|
||||
|
||||
class MediaPlayerApp(App):
|
||||
"""Main application class."""
|
||||
|
||||
def build(self):
|
||||
"""Build the app and initialize screens."""
|
||||
Window.fullscreen = True # Start the app in fullscreen mode
|
||||
sm = ScreenManager() # Create a screen manager
|
||||
sm.add_widget(MediaPlayer(name='media_player')) # Add the MediaPlayer screen
|
||||
sm.add_widget(SettingsScreen(name='settings')) # Add the SettingsScreen
|
||||
return sm
|
||||
|
||||
def open_settings(self):
|
||||
"""Switch to the SettingsScreen."""
|
||||
self.root.current = 'settings'
|
||||
|
||||
def convert_video_to_mp4(input_path, output_path):
|
||||
"""Convert a video to H.264 MP4 format."""
|
||||
try:
|
||||
subprocess.run(
|
||||
['ffmpeg', '-i', input_path, '-vcodec', 'libx264', '-acodec', 'aac', '-strict', 'experimental', output_path],
|
||||
check=True
|
||||
)
|
||||
Logger.info(f"Converted video: {input_path} to {output_path}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
Logger.error(f"Failed to convert video: {input_path}. Error: {e}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
MediaPlayerApp().run() # Run the app
|
||||
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
@@ -1,248 +0,0 @@
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from kivy.logger import Logger
|
||||
import bcrypt
|
||||
import time
|
||||
|
||||
CONFIG_FILE = './Resurse/app_config.txt'
|
||||
LOCAL_PLAYLIST_FILE = './static/local_playlist.json' # Path to the local playlist file
|
||||
def load_config():
|
||||
"""Load configuration from app_config.txt."""
|
||||
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 {
|
||||
"screen_orientation": "Landscape",
|
||||
"screen_name": "",
|
||||
"quickconnect_key": "",
|
||||
"server_ip": "",
|
||||
"port": ""
|
||||
}
|
||||
else:
|
||||
Logger.error(f"python_functions: Configuration file {CONFIG_FILE} not found.")
|
||||
return {
|
||||
"screen_orientation": "Landscape",
|
||||
"screen_name": "",
|
||||
"quickconnect_key": "",
|
||||
"server_ip": "",
|
||||
"port": ""
|
||||
}
|
||||
|
||||
# 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", "")
|
||||
print(server, host, quick, port)
|
||||
# Determine the configuration status
|
||||
if server and host and quick and port:
|
||||
config_status = "ok"
|
||||
else:
|
||||
config_status = "not_ok"
|
||||
|
||||
Logger.info(f"python_functions: Configuration loaded: server={server}, host={host}, quick={quick}, port={port}")
|
||||
Logger.info(f"python_functions: Configuration status: {config_status}")
|
||||
|
||||
|
||||
def load_playlist():
|
||||
"""Load playlist from the server or local storage and periodically check for updates."""
|
||||
local_playlist = load_local_playlist()
|
||||
local_version = local_playlist.get('version', 0) if local_playlist else 0
|
||||
|
||||
while True:
|
||||
try:
|
||||
Logger.info("python_functions: Checking playlist version on the server...")
|
||||
server_ip = f'{server}:{port}' # Construct the server IP with port
|
||||
url = f'http://{server_ip}/api/playlist_version'
|
||||
params = {
|
||||
'hostname': host,
|
||||
'quickconnect_code': quick
|
||||
}
|
||||
response = requests.get(url, params=params)
|
||||
|
||||
if response.status_code == 200:
|
||||
response_data = response.json()
|
||||
server_version = response_data.get('playlist_version', None)
|
||||
hashed_quickconnect = response_data.get('hashed_quickconnect', None)
|
||||
|
||||
if server_version is not None and hashed_quickconnect is not None:
|
||||
# Validate the quickconnect code using bcrypt
|
||||
if bcrypt.checkpw(quick.encode('utf-8'), hashed_quickconnect.encode('utf-8')):
|
||||
Logger.info(f"python_functions: Server playlist version: {server_version}, Local playlist version: {local_version}")
|
||||
|
||||
if server_version != local_version:
|
||||
Logger.info("python_functions: Playlist versions differ. Updating local playlist...")
|
||||
updated_playlist = fetch_server_playlist()
|
||||
if updated_playlist and 'playlist' in updated_playlist:
|
||||
save_local_playlist(updated_playlist) # Update local playlist
|
||||
local_version = server_version # Update local version
|
||||
else:
|
||||
Logger.error("python_functions: Failed to update local playlist. Using existing playlist.")
|
||||
else:
|
||||
Logger.info("python_functions: Playlist versions match. No update needed.")
|
||||
else:
|
||||
Logger.error("python_functions: Quickconnect code validation failed.")
|
||||
else:
|
||||
Logger.error("python_functions: Failed to retrieve playlist version or hashed quickconnect from the response.")
|
||||
else:
|
||||
Logger.error(f"python_functions: Failed to retrieve playlist version. Status Code: {response.status_code}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
Logger.error(f"python_functions: Failed to check playlist version: {e}")
|
||||
|
||||
# Wait for 5 minutes before checking again
|
||||
time.sleep(300)
|
||||
|
||||
def load_local_playlist():
|
||||
"""Load the playlist from local storage."""
|
||||
if os.path.exists(LOCAL_PLAYLIST_FILE):
|
||||
try:
|
||||
with open(LOCAL_PLAYLIST_FILE, 'r') as local_file:
|
||||
local_playlist = json.load(local_file)
|
||||
|
||||
# Validate the structure of the local playlist
|
||||
if isinstance(local_playlist, dict) and 'playlist' in local_playlist and 'version' in local_playlist:
|
||||
Logger.info("python_functions: Loaded and validated local playlist.")
|
||||
return local_playlist
|
||||
else:
|
||||
Logger.error("python_functions: Invalid local playlist structure.")
|
||||
return None
|
||||
except json.JSONDecodeError:
|
||||
Logger.error("python_functions: Failed to parse local playlist file.")
|
||||
return None
|
||||
else:
|
||||
Logger.warning("python_functions: Local playlist file not found.")
|
||||
return None
|
||||
|
||||
def download_media_files(playlist):
|
||||
"""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
|
||||
file_path = os.path.join(base_dir, file_name)
|
||||
|
||||
Logger.debug(f"python_functions: Preparing to download {file_name} from {file_url}...")
|
||||
|
||||
if os.path.exists(file_path):
|
||||
Logger.info(f"python_functions: File {file_name} already exists. Skipping download.")
|
||||
else:
|
||||
try:
|
||||
response = requests.get(file_url, timeout=10) # Add timeout for better error handling
|
||||
if response.status_code == 200:
|
||||
with open(file_path, 'wb') as file:
|
||||
file.write(response.content)
|
||||
Logger.info(f"python_functions: Successfully downloaded {file_name} to {file_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 media entry with the local file path and duration
|
||||
updated_media = {
|
||||
'file_name': file_name,
|
||||
'url': file_path, # Update URL to local path
|
||||
'duration': duration
|
||||
}
|
||||
updated_playlist.append(updated_media)
|
||||
|
||||
# Save the updated playlist locally
|
||||
save_local_playlist({'playlist': updated_playlist, 'version': playlist.get('version', 0)})
|
||||
|
||||
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') # Update this to the correct path
|
||||
if not os.path.exists(base_dir):
|
||||
Logger.debug(f"python_functions: Directory {base_dir} does not exist. No files to clean.")
|
||||
return
|
||||
|
||||
# Get all file names from the playlist
|
||||
playlist_files = {media.get('file_name', '') for media in playlist}
|
||||
|
||||
# Get all files in the directory
|
||||
all_files = set(os.listdir(base_dir))
|
||||
|
||||
# Determine unused files
|
||||
unused_files = all_files - playlist_files
|
||||
|
||||
# Delete unused 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 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
|
||||
}
|
||||
response = requests.get(url, params=params)
|
||||
|
||||
if response.status_code == 200:
|
||||
response_data = response.json()
|
||||
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:
|
||||
# Validate the quickconnect code using bcrypt
|
||||
if bcrypt.checkpw(quick.encode('utf-8'), hashed_quickconnect.encode('utf-8')):
|
||||
Logger.info("python_functions: Fetched updated playlist from server.")
|
||||
return {'playlist': playlist, 'version': version}
|
||||
else:
|
||||
Logger.error("python_functions: Quickconnect code validation failed.")
|
||||
else:
|
||||
Logger.error("python_functions: Failed to retrieve playlist or hashed quickconnect from the response.")
|
||||
else:
|
||||
Logger.error(f"python_functions: Failed to fetch playlist. Status Code: {response.status_code}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
Logger.error(f"python_functions: Failed to fetch playlist: {e}")
|
||||
|
||||
# Return an empty playlist if fetching fails
|
||||
return {'playlist': [], 'version': 0}
|
||||
|
||||
def save_local_playlist(playlist):
|
||||
"""Save the updated playlist locally."""
|
||||
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)
|
||||
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}")
|
||||
|
||||
def check_playlist_updates(self, dt):
|
||||
"""Check for updates to the playlist."""
|
||||
new_playlist = load_playlist() # Load the new playlist
|
||||
if new_playlist != self.playlist: # Compare the new playlist with the current one
|
||||
Logger.info("Playlist updated. Changes detected.")
|
||||
self.updated_playlist = new_playlist # Store the updated playlist
|
||||
self.is_playlist_update_pending = True # Mark the update as pending
|
||||
else:
|
||||
Logger.info("Playlist update skipped. No changes detected.")
|
||||
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)
|
||||
@@ -1,15 +1,17 @@
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from kivy.logger import Logger
|
||||
from logging_config import Logger # Import the shared logger
|
||||
import bcrypt
|
||||
import time
|
||||
|
||||
CONFIG_FILE = './Resurse/app_config.txt'
|
||||
LOCAL_PLAYLIST_FILE = './static/local_playlist.json' # Path to the local playlist file
|
||||
# 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:
|
||||
@@ -21,6 +23,7 @@ def load_config():
|
||||
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()
|
||||
@@ -32,36 +35,42 @@ 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 from local storage."""
|
||||
"""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:
|
||||
return local_playlist.get('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 []
|
||||
return {'playlist': [], 'version': 0}
|
||||
except json.JSONDecodeError as e:
|
||||
Logger.error(f"python_functions: Failed to parse local playlist file. Error: {e}")
|
||||
return []
|
||||
return {'playlist': [], 'version': 0}
|
||||
else:
|
||||
Logger.warning("python_functions: Local playlist file not found.")
|
||||
return []
|
||||
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)
|
||||
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."""
|
||||
@@ -72,26 +81,32 @@ def fetch_server_playlist():
|
||||
'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("python_functions: Fetched updated playlist from server.")
|
||||
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("python_functions: Quickconnect code validation failed.")
|
||||
Logger.error("Quickconnect code validation failed.")
|
||||
else:
|
||||
Logger.error("python_functions: Failed to retrieve playlist or hashed quickconnect from the response.")
|
||||
Logger.error("Failed to retrieve playlist or hashed quickconnect from the response.")
|
||||
else:
|
||||
Logger.error(f"python_functions: Failed to fetch playlist. Status Code: {response.status_code}")
|
||||
Logger.error(f"Failed to fetch playlist. Status Code: {response.status_code}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
Logger.error(f"python_functions: Failed to fetch playlist: {e}")
|
||||
Logger.error(f"Failed to fetch playlist: {e}")
|
||||
|
||||
return {'playlist': [], 'version': 0}
|
||||
|
||||
@@ -109,19 +124,19 @@ def download_media_files(playlist, version):
|
||||
file_name = media.get('file_name', '')
|
||||
file_url = media.get('url', '')
|
||||
duration = media.get('duration', 10) # Default duration if not provided
|
||||
file_path = os.path.join(base_dir, file_name)
|
||||
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(file_path):
|
||||
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(file_path, 'wb') as file:
|
||||
with open(local_path, 'wb') as file:
|
||||
file.write(response.content)
|
||||
Logger.info(f"python_functions: Successfully downloaded {file_name} to {file_path}")
|
||||
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
|
||||
@@ -129,15 +144,18 @@ def download_media_files(playlist, version):
|
||||
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': file_path, # Update URL to local path
|
||||
'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."""
|
||||
@@ -157,4 +175,22 @@ def clean_unused_files(playlist):
|
||||
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}")
|
||||
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}")
|
||||
@@ -1 +0,0 @@
|
||||
{"playlist": [{"file_name": "edit_pencil.png", "url": "/home/pi/Desktop/signage-player/src/static/resurse/edit_pencil.png", "duration": 20}, {"file_name": "delete.png", "url": "/home/pi/Desktop/signage-player/src/static/resurse/delete.png", "duration": 20}], "version": 2}
|
||||
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 77 KiB |
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
|
||||
}
|
||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 17 KiB 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
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 17 KiB 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()
|
||||
14
tkinter_requirements.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
# 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
|
||||
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)
|
||||