Compare commits

..

11 Commits

Author SHA1 Message Date
a4cc026e38 changes 2025-08-06 02:27:50 +03:00
65843c255a feat: Full-screen scaling system with three display modes
- Implemented comprehensive image/video scaling system
- Added fit, fill, and stretch scaling modes
- Keyboard shortcuts 1,2,3 for real-time mode switching
- Enhanced PIL/Pillow integration for image processing
- OpenCV video playback with full-screen scaling
- Settings integration for scaling preferences
- Fixed touch feedback for control buttons
- Thread-safe video frame processing
- Perfect aspect ratio calculations for any resolution
2025-08-06 02:26:12 +03:00
7e69b12f71 saved 2025-08-05 17:01:19 +03:00
1197077954 Add complete offline installation package
- Added offline Python packages (requests, pillow, pygame, etc.)
- Created comprehensive installation scripts for offline deployment
- Added system package dependency lists
- Included shared modules for the tkinter application
- Added complete documentation for offline installation
- Removed old Kivy-related files to clean up repository
- Package supports complete offline installation on Raspberry Pi systems
2025-08-05 16:59:27 +03:00
8293111e09 updated to tkinter 2025-08-05 16:51:15 +03:00
93c0637bca updated 2025-06-25 16:06:13 +03:00
4e2909718c updated systems 2025-06-25 15:45:01 +03:00
f7d749cc8b fixed the update_config_playlist version 2025-06-25 10:30:46 +03:00
01a6a95645 final task 2025-06-24 16:10:27 +03:00
cca7d75376 updated the playlist and playng state 2025-06-24 15:41:17 +03:00
71eab75d51 updated install sh 2025-06-23 15:36:55 +03:00
72 changed files with 5030 additions and 1136 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
venv/

View File

@@ -1,156 +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."
read -p "Press any key to close the terminal..." -n 1 -s
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 || {
echo "Error: Failed to update system packages."
read -p "Press any key to close the terminal..." -n 1 -s
exit 1
}
echo "Installation directory is set to: $INSTALL_DIR and is starting the installation process."
# Install required system packages
echo "Installing required system packages..."
sudo apt install -y python3 python3-pip git || {
echo "Error: Failed to install required system packages."
read -p "Press any key to close the terminal..." -n 1 -s
exit 1
}
# 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" || {
echo "Error: Failed to clone the repository."
read -p "Press any key to close the terminal..." -n 1 -s
exit 1
}
# Navigate to the cloned repository
cd "$INSTALL_DIR" || {
echo "Error: Failed to navigate to the cloned repository."
read -p "Press any key to close the terminal..." -n 1 -s
exit 1
}
# Install Python dependencies
echo "Installing Python dependencies..."
pip3 install -r requirements.txt --break-system-packages || {
echo "Error: Failed to install Python dependencies."
read -p "Press any key to close the terminal..." -n 1 -s
exit 1
}
# Set permissions for the run_app.sh script
echo "Setting permissions for run_app.sh..."
chmod +x "$INSTALL_DIR/run_app.sh" || {
echo "Error: Failed to set permissions for run_app.sh."
read -p "Press any key to close the terminal..." -n 1 -s
exit 1
}
# 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 "Error: Failed to reload systemd daemon."
read -p "Press any key to close the terminal..." -n 1 -s
exit 1
}
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 "Error: Failed to reload systemd daemon."
read -p "Press any key to close the terminal..." -n 1 -s
exit 1
}
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 "Error: Failed to update working directory in run_app.sh."
read -p "Press any key to close the terminal..." -n 1 -s
exit 1
}
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."
read -p "Press any key to close the terminal..." -n 1 -s
exit 1
fi
# Enable the service
echo "Enabling signage_player.service..."
sudo systemctl enable signage_player.service || {
echo "Error: Failed to enable signage_player.service."
read -p "Press any key to close the terminal..." -n 1 -s
exit 1
}
# Restart the system to check if the service starts
echo "Restarting the system to check if the service starts..."
sudo reboot || {
echo "Error: Failed to restart the system."
read -p "Press any key to close the terminal..." -n 1 -s
exit 1
}

68
install_tkinter.sh Executable file
View 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"

View File

@@ -1,5 +0,0 @@
kivy
requests
watchdog
Pillow
bcrypt

View File

@@ -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
View 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

View File

@@ -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
View 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

View File

@@ -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"}

View File

@@ -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

View File

@@ -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()

View File

@@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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.")

View 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
View 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 ""

View 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)

View File

@@ -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}")

View File

@@ -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}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

View 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
View 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
View 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
View 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
View 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
View 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!")

View 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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

View 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
}

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View 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
}

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View 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
View 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()

View 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}")

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 KiB

File diff suppressed because it is too large Load Diff

View 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
View 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

View 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)