Initial commit after repository repair and requirements update

This commit is contained in:
2025-08-22 06:10:33 +01:00
commit ce4440bf6e
60 changed files with 5405 additions and 0 deletions

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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

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