2049 lines
87 KiB
Python
2049 lines
87 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Tkinter Simple Media Player - A lightweight version with minimal dependencies
|
||
Features:
|
||
- Image display
|
||
- Basic playlist management
|
||
- Settings configuration
|
||
- Auto-hiding controls
|
||
- Touch display optimization with virtual keyboard
|
||
"""
|
||
|
||
import tkinter as tk
|
||
from tkinter import ttk, messagebox, simpledialog
|
||
import threading
|
||
import time
|
||
import os
|
||
import json
|
||
import datetime
|
||
from pathlib import Path
|
||
import subprocess
|
||
import sys
|
||
import requests # Required for server communication
|
||
import queue
|
||
import vlc # For video playback with hardware acceleration
|
||
|
||
# Try importing PIL but provide fallback
|
||
try:
|
||
from PIL import Image, ImageTk
|
||
PIL_AVAILABLE = True
|
||
except ImportError:
|
||
PIL_AVAILABLE = False
|
||
print("WARNING: PIL not available. Image display functionality will be limited.")
|
||
|
||
# Import existing functions
|
||
from python_functions import (
|
||
load_local_playlist, download_media_files, clean_unused_files,
|
||
save_local_playlist, update_config_playlist_version, fetch_server_playlist,
|
||
load_config
|
||
)
|
||
from logging_config import Logger
|
||
|
||
# Import virtual keyboard components
|
||
from virtual_keyboard import VirtualKeyboard, TouchOptimizedEntry, TouchOptimizedButton
|
||
|
||
# Update the config file path to use the resources directory
|
||
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'app_config.txt')
|
||
|
||
class SimpleMediaPlayerApp:
|
||
def __init__(self):
|
||
self.root = tk.Tk()
|
||
self.setup_window()
|
||
|
||
# Media player state
|
||
self.playlist = []
|
||
self.current_index = 0
|
||
self.is_paused = False
|
||
self.is_fullscreen = True
|
||
self.auto_advance_timer = None
|
||
self.hide_controls_timer = None
|
||
self.video_thread = None
|
||
self.update_queue = queue.Queue()
|
||
|
||
# UI Elements
|
||
self.image_label = None
|
||
self.status_label = None
|
||
self.control_frame = None
|
||
self.settings_window = None
|
||
|
||
# Running state
|
||
self.running = True
|
||
|
||
# Display scaling mode ('fit', 'fill', 'stretch')
|
||
self.scaling_mode = 'fit' # Default to fit (maintain aspect ratio with black bars)
|
||
|
||
# VLC will be used for video/audio playback (no pygame needed)
|
||
|
||
self.setup_ui()
|
||
|
||
# Initialize from server
|
||
self.initialize_playlist_from_server()
|
||
|
||
# Start periodic playlist checks
|
||
self.start_periodic_checks()
|
||
|
||
def setup_window(self):
|
||
"""Configure the main window"""
|
||
self.root.title("Simple Signage Player")
|
||
self.root.configure(bg='black')
|
||
|
||
# Load window size from config
|
||
try:
|
||
config = load_config()
|
||
width = int(config.get('screen_w', 1920))
|
||
height = int(config.get('screen_h', 1080))
|
||
# Load scaling mode preference
|
||
self.scaling_mode = config.get('scaling_mode', 'fit')
|
||
except:
|
||
width, height = 800, 600 # Fallback size if config fails
|
||
self.scaling_mode = 'fit' # Default scaling mode
|
||
|
||
self.root.geometry(f"{width}x{height}")
|
||
self.root.attributes('-fullscreen', True)
|
||
|
||
# Bind events
|
||
self.root.bind('<Key>', self.on_key_press)
|
||
self.root.bind('<Button-1>', self.on_mouse_click)
|
||
self.root.bind('<Motion>', self.on_mouse_motion)
|
||
self.root.focus_set()
|
||
|
||
def setup_ui(self):
|
||
"""Create the user interface"""
|
||
# Main content area - make sure it's black and fill the entire window
|
||
self.content_frame = tk.Frame(self.root, bg='black')
|
||
self.content_frame.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# Image display - expand to fill the entire window
|
||
self.image_label = tk.Label(self.content_frame, bg='black')
|
||
self.image_label.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# Status label - hidden by default
|
||
self.status_label = tk.Label(
|
||
self.content_frame,
|
||
bg='black',
|
||
fg='white',
|
||
font=('Arial', 24),
|
||
text=""
|
||
)
|
||
# Don't place the status label by default to keep it hidden
|
||
|
||
# Control panel
|
||
self.create_control_panel()
|
||
self.show_controls()
|
||
self.schedule_hide_controls()
|
||
|
||
def create_control_panel(self):
|
||
"""Create touch-optimized control panel with larger buttons"""
|
||
# Create control frame with larger size for touch
|
||
self.control_frame = tk.Frame(
|
||
self.root,
|
||
bg='#1a1a1a', # Dark background
|
||
bd=2,
|
||
relief=tk.RAISED,
|
||
padx=15,
|
||
pady=15
|
||
)
|
||
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
|
||
|
||
# Touch-optimized button configuration
|
||
button_config = {
|
||
'bg': '#333333',
|
||
'fg': 'white',
|
||
'activebackground': '#555555',
|
||
'activeforeground': 'white',
|
||
'relief': tk.FLAT,
|
||
'borderwidth': 0,
|
||
'width': 10, # Larger for touch
|
||
'height': 3, # Larger for touch
|
||
'font': ('Segoe UI', 10, 'bold'), # Larger font
|
||
'cursor': 'hand2'
|
||
}
|
||
|
||
# Previous button
|
||
self.prev_btn = tk.Button(
|
||
self.control_frame,
|
||
text="⏮ Prev",
|
||
command=self.previous_media,
|
||
**button_config
|
||
)
|
||
self.prev_btn.grid(row=0, column=0, padx=5)
|
||
|
||
# Play/Pause button
|
||
self.play_pause_btn = tk.Button(
|
||
self.control_frame,
|
||
text="⏸ Pause" if not self.is_paused else "▶ Play",
|
||
command=self.toggle_play_pause,
|
||
bg='#27ae60', # Green for play/pause
|
||
activebackground='#35d974',
|
||
**{k: v for k, v in button_config.items() if k not in ['bg', 'activebackground']}
|
||
)
|
||
self.play_pause_btn.grid(row=0, column=1, padx=5)
|
||
|
||
# Next button
|
||
self.next_btn = tk.Button(
|
||
self.control_frame,
|
||
text="Next ⏭",
|
||
command=self.next_media,
|
||
**button_config
|
||
)
|
||
self.next_btn.grid(row=0, column=2, padx=5)
|
||
|
||
# Settings button
|
||
self.settings_btn = tk.Button(
|
||
self.control_frame,
|
||
text="⚙️ Settings",
|
||
command=self.open_settings,
|
||
bg='#9b59b6', # Purple for settings
|
||
activebackground='#bb8fce',
|
||
**{k: v for k, v in button_config.items() if k not in ['bg', 'activebackground']}
|
||
)
|
||
self.settings_btn.grid(row=0, column=3, padx=5)
|
||
|
||
# Exit button with special styling
|
||
self.exit_btn = tk.Button(
|
||
self.control_frame,
|
||
text="❌ EXIT",
|
||
command=self.show_exit_dialog,
|
||
bg='#e74c3c', # Red background
|
||
fg='white',
|
||
activebackground='#ec7063',
|
||
activeforeground='white',
|
||
relief=tk.FLAT,
|
||
borderwidth=0,
|
||
width=8,
|
||
height=3,
|
||
font=('Segoe UI', 10, 'bold'),
|
||
cursor='hand2'
|
||
)
|
||
self.exit_btn.grid(row=0, column=4, padx=5)
|
||
|
||
# Add touch feedback to all buttons
|
||
for button in [self.prev_btn, self.play_pause_btn, self.next_btn,
|
||
self.settings_btn, self.exit_btn]:
|
||
self.add_touch_feedback_to_control_button(button)
|
||
|
||
def scale_image_to_screen(self, img, screen_width, screen_height, mode='fit'):
|
||
"""
|
||
Scale image to screen with different modes:
|
||
- 'fit': Maintain aspect ratio, add black bars if needed (letterbox/pillarbox)
|
||
- 'fill': Maintain aspect ratio, crop if needed to fill entire screen
|
||
- 'stretch': Ignore aspect ratio, stretch to fill entire screen
|
||
"""
|
||
img_width, img_height = img.size
|
||
|
||
if mode == 'stretch':
|
||
# Stretch to fill entire screen, ignoring aspect ratio
|
||
return img.resize((screen_width, screen_height), Image.LANCZOS), (0, 0)
|
||
|
||
elif mode == 'fill':
|
||
# Maintain aspect ratio, crop to fill entire screen
|
||
screen_ratio = screen_width / screen_height
|
||
img_ratio = img_width / img_height
|
||
|
||
if img_ratio > screen_ratio:
|
||
# Image is wider - scale by height and crop width
|
||
new_height = screen_height
|
||
new_width = int(screen_height * img_ratio)
|
||
x_offset = (screen_width - new_width) // 2
|
||
y_offset = 0
|
||
else:
|
||
# Image is taller - scale by width and crop height
|
||
new_width = screen_width
|
||
new_height = int(screen_width / img_ratio)
|
||
x_offset = 0
|
||
y_offset = (screen_height - new_height) // 2
|
||
|
||
# Resize and crop
|
||
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
||
|
||
# Create final image and paste (this will crop automatically)
|
||
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
|
||
|
||
# Calculate crop area if image is larger than screen
|
||
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: # mode == 'fit' (default)
|
||
# Maintain aspect ratio, add black bars if needed
|
||
screen_ratio = screen_width / screen_height
|
||
img_ratio = img_width / img_height
|
||
|
||
if img_ratio > screen_ratio:
|
||
# Image is wider than screen - fit to width
|
||
new_width = screen_width
|
||
new_height = int(screen_width / img_ratio)
|
||
else:
|
||
# Image is taller than screen - fit to height
|
||
new_height = screen_height
|
||
new_width = int(screen_height * img_ratio)
|
||
|
||
# Resize image
|
||
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
||
|
||
# Create black background and center the image
|
||
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)
|
||
|
||
def add_touch_feedback_to_control_button(self, button):
|
||
"""Add touch feedback effects to control panel buttons"""
|
||
original_bg = button.cget('bg')
|
||
|
||
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)
|
||
|
||
def initialize_playlist_from_server(self):
|
||
"""Initialize the playlist from the server on startup with fallback to local playlist"""
|
||
# First try to load any existing local playlist as fallback
|
||
fallback_playlist = None
|
||
try:
|
||
local_playlist_data = load_local_playlist()
|
||
fallback_playlist = local_playlist_data.get('playlist', [])
|
||
if fallback_playlist:
|
||
Logger.info(f"Found fallback playlist with {len(fallback_playlist)} items")
|
||
except Exception as e:
|
||
Logger.warning(f"No fallback playlist available: {e}")
|
||
|
||
# Show connection status
|
||
self.status_label.config(text="Connecting to server...\nPlease wait")
|
||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||
self.root.update()
|
||
|
||
# Load configuration
|
||
config = load_config()
|
||
server = config.get("server_ip", "")
|
||
host = config.get("screen_name", "")
|
||
quick = config.get("quickconnect_key", "")
|
||
port = config.get("port", "")
|
||
|
||
Logger.info(f"Initializing with settings: server={server}, host={host}, port={port}")
|
||
|
||
if not server or not host or not quick or not port:
|
||
Logger.warning("Missing server configuration, using fallback playlist")
|
||
self.status_label.place_forget()
|
||
self.load_fallback_playlist(fallback_playlist)
|
||
return
|
||
|
||
# Attempt to fetch server playlist with timeout
|
||
server_connection_successful = False
|
||
try:
|
||
# Add connection timeout and retry logic
|
||
Logger.info("Attempting to connect to server...")
|
||
self.status_label.config(text="Connecting to server...\nAttempting connection")
|
||
self.root.update()
|
||
|
||
server_playlist_data = fetch_server_playlist()
|
||
server_playlist = server_playlist_data.get('playlist', [])
|
||
server_version = server_playlist_data.get('version', 0)
|
||
|
||
if server_playlist:
|
||
Logger.info(f"Server playlist found with {len(server_playlist)} items, version {server_version}")
|
||
server_connection_successful = True
|
||
|
||
# Download media files and update local playlist
|
||
self.status_label.config(text="Downloading media files...\nPlease wait")
|
||
self.root.update()
|
||
|
||
download_media_files(server_playlist, server_version)
|
||
update_config_playlist_version(server_version)
|
||
|
||
# Load the updated local playlist
|
||
local_playlist_data = load_local_playlist()
|
||
self.playlist = local_playlist_data.get('playlist', [])
|
||
|
||
if self.playlist:
|
||
Logger.info(f"Successfully loaded {len(self.playlist)} items from server")
|
||
self.status_label.place_forget()
|
||
self.play_current_media()
|
||
return
|
||
else:
|
||
Logger.warning("Server playlist was empty, falling back to local playlist")
|
||
else:
|
||
Logger.warning("Server returned empty playlist, falling back to local playlist")
|
||
|
||
except requests.exceptions.ConnectTimeout:
|
||
Logger.error("Server connection timeout, using fallback playlist")
|
||
except requests.exceptions.ConnectionError:
|
||
Logger.error("Cannot connect to server, using fallback playlist")
|
||
except requests.exceptions.Timeout:
|
||
Logger.error("Server request timeout, using fallback playlist")
|
||
except Exception as e:
|
||
Logger.error(f"Failed to fetch playlist from server: {e}, using fallback playlist")
|
||
|
||
# If we reach here, server connection failed - use fallback
|
||
if not server_connection_successful:
|
||
self.status_label.config(text="Server unavailable\nLoading last playlist...")
|
||
self.root.update()
|
||
time.sleep(1) # Brief pause to show message
|
||
|
||
self.status_label.place_forget()
|
||
self.load_fallback_playlist(fallback_playlist)
|
||
|
||
def load_fallback_playlist(self, fallback_playlist):
|
||
"""Load fallback playlist when server is unavailable"""
|
||
if fallback_playlist and len(fallback_playlist) > 0:
|
||
self.playlist = fallback_playlist
|
||
Logger.info(f"Loaded fallback playlist with {len(self.playlist)} items")
|
||
self.play_current_media()
|
||
else:
|
||
Logger.warning("No fallback playlist available, loading demo content")
|
||
self.load_demo_or_local_playlist()
|
||
|
||
def load_demo_or_local_playlist(self):
|
||
"""Load either the existing local playlist or demo content"""
|
||
# First try to load the local playlist
|
||
local_playlist_data = load_local_playlist()
|
||
self.playlist = local_playlist_data.get('playlist', [])
|
||
|
||
if self.playlist:
|
||
Logger.info(f"Loaded existing local playlist with {len(self.playlist)} items")
|
||
self.play_current_media()
|
||
return
|
||
|
||
# If no local playlist, try loading demo content
|
||
Logger.info("No local playlist found, loading demo content")
|
||
self.create_demo_content()
|
||
|
||
if self.playlist:
|
||
self.play_current_media()
|
||
else:
|
||
self.show_no_content_message()
|
||
|
||
def create_demo_content(self):
|
||
"""Create demo content for testing"""
|
||
demo_images = []
|
||
|
||
# First check static/resurse folder for any media
|
||
static_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse')
|
||
if os.path.exists(static_dir):
|
||
for file in os.listdir(static_dir):
|
||
if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
|
||
full_path = os.path.join(static_dir, file)
|
||
demo_images.append({
|
||
'file_name': file,
|
||
'url': full_path,
|
||
'duration': 5
|
||
})
|
||
|
||
# If no files found in static/resurse, look in Resurse folder
|
||
if not demo_images:
|
||
demo_dir = './Resurse'
|
||
if os.path.exists(demo_dir):
|
||
for file in os.listdir(demo_dir):
|
||
if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
|
||
demo_images.append({
|
||
'file_name': file,
|
||
'url': os.path.join(demo_dir, file),
|
||
'duration': 5
|
||
})
|
||
|
||
if demo_images:
|
||
self.playlist = demo_images
|
||
Logger.info(f"Created demo playlist with {len(demo_images)} images")
|
||
else:
|
||
# Create a text-only demo if no images found
|
||
self.playlist = [{
|
||
'file_name': 'Demo Text',
|
||
'url': 'text://Welcome to Tkinter Media Player!\n\nPlease configure server settings',
|
||
'duration': 5
|
||
}]
|
||
|
||
def show_no_content_message(self):
|
||
"""Show message when no content is available"""
|
||
self.image_label.config(image='')
|
||
self.status_label.config(
|
||
text="No media content available.\nPress Settings to configure server connection."
|
||
)
|
||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||
|
||
def show_error_message(self, message):
|
||
"""Show error message"""
|
||
self.image_label.config(image='')
|
||
self.status_label.config(text=f"Error: {message}")
|
||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||
|
||
def play_current_media(self):
|
||
"""Play the current media item"""
|
||
if not self.playlist or self.current_index >= len(self.playlist):
|
||
self.show_no_content_message()
|
||
return
|
||
|
||
media = self.playlist[self.current_index]
|
||
file_path = media.get('url', '')
|
||
file_name = media.get('file_name', '')
|
||
duration = media.get('duration', 10)
|
||
|
||
# Handle relative paths by converting to absolute paths
|
||
if file_path.startswith('static/resurse/'):
|
||
# Convert relative path to absolute
|
||
absolute_path = os.path.join(os.path.dirname(__file__), file_path)
|
||
file_path = absolute_path
|
||
|
||
Logger.info(f"Playing media: {file_name} from {file_path}")
|
||
|
||
# Log media start
|
||
self.log_event(file_name, "STARTED")
|
||
|
||
# Cancel existing timers
|
||
self.cancel_timers()
|
||
|
||
# Handle different media types
|
||
if file_path.startswith('text://'):
|
||
self.show_text_content(file_path[7:], duration)
|
||
elif file_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')):
|
||
self.play_video(file_path)
|
||
elif os.path.exists(file_path) and file_path.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp')):
|
||
self.show_image(file_path, duration)
|
||
else:
|
||
Logger.error(f"Unsupported or missing media: {file_path}")
|
||
self.status_label.config(text=f"Missing or unsupported media:\n{file_name}")
|
||
# Schedule next media after short delay
|
||
self.auto_advance_timer = self.root.after(5000, self.next_media)
|
||
|
||
def play_video(self, file_path):
|
||
"""Play video file using system VLC as a subprocess for robust hardware acceleration and stability."""
|
||
self.status_label.place_forget()
|
||
def run_vlc_subprocess():
|
||
try:
|
||
Logger.info(f"Starting system VLC subprocess for video: {file_path}")
|
||
# Build VLC command
|
||
vlc_cmd = [
|
||
'cvlc',
|
||
'--fullscreen',
|
||
'--no-osd',
|
||
'--no-video-title-show',
|
||
'--play-and-exit',
|
||
'--quiet',
|
||
file_path
|
||
]
|
||
proc = subprocess.Popen(vlc_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||
proc.wait()
|
||
Logger.info(f"VLC subprocess finished: {file_path}")
|
||
except Exception as e:
|
||
Logger.error(f"VLC subprocess error: {e}")
|
||
finally:
|
||
self.root.after_idle(lambda: setattr(self, 'auto_advance_timer', self.root.after(1000, self.next_media)))
|
||
threading.Thread(target=run_vlc_subprocess, daemon=True).start()
|
||
|
||
def _update_video_frame(self, photo):
|
||
"""Update video frame from main thread"""
|
||
try:
|
||
self.image_label.config(image=photo)
|
||
self.image_label.image = photo # Keep reference
|
||
except Exception as e:
|
||
Logger.error(f"Error updating video frame: {e}")
|
||
|
||
def _show_video_error(self, error_msg):
|
||
"""Show video error from main thread"""
|
||
try:
|
||
self.status_label.config(text=f"Video Error:\n{error_msg}")
|
||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||
self.auto_advance_timer = self.root.after(5000, self.next_media)
|
||
except Exception as e:
|
||
Logger.error(f"Error showing video error: {e}")
|
||
self.auto_advance_timer = self.root.after(5000, self.next_media)
|
||
|
||
def show_text_content(self, text, duration):
|
||
"""Display text content"""
|
||
self.image_label.config(image='')
|
||
self.status_label.config(text=text)
|
||
|
||
# Schedule next media
|
||
self.auto_advance_timer = self.root.after(
|
||
int(duration * 1000),
|
||
self.next_media
|
||
)
|
||
|
||
def show_image(self, file_path, duration):
|
||
"""Display an image in full screen, properly fitted to screen size"""
|
||
try:
|
||
# Hide status label and clear any previous text
|
||
self.status_label.place_forget()
|
||
self.status_label.config(text="")
|
||
|
||
if PIL_AVAILABLE:
|
||
# Use PIL for better image handling
|
||
img = Image.open(file_path)
|
||
original_size = img.size
|
||
|
||
# Get actual screen dimensions
|
||
screen_width = self.root.winfo_width()
|
||
screen_height = self.root.winfo_height()
|
||
|
||
# Ensure we have valid screen dimensions
|
||
if screen_width <= 1 or screen_height <= 1:
|
||
screen_width = 1920 # Default fallback
|
||
screen_height = 1080
|
||
|
||
# Scale image using the scaling helper
|
||
final_img, offset = self.scale_image_to_screen(img, screen_width, screen_height, self.scaling_mode)
|
||
|
||
# Convert to PhotoImage
|
||
photo = ImageTk.PhotoImage(final_img)
|
||
|
||
# Clear previous image and display new one
|
||
self.image_label.config(image=photo)
|
||
self.image_label.image = photo # Keep reference
|
||
|
||
Logger.info(f"Successfully displayed image: {os.path.basename(file_path)} "
|
||
f"(Original: {original_size}, Screen: {screen_width}x{screen_height}, "
|
||
f"Mode: {self.scaling_mode}, Offset: {offset})")
|
||
else:
|
||
# Fall back to basic text display if PIL not available
|
||
self.image_label.config(image='')
|
||
self.status_label.config(text=f"IMAGE: {os.path.basename(file_path)}\n\n(Install PIL for image display)")
|
||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||
Logger.warning("PIL not available - showing text placeholder for image")
|
||
|
||
# Schedule next media
|
||
self.auto_advance_timer = self.root.after(
|
||
int(duration * 1000),
|
||
self.next_media
|
||
)
|
||
|
||
except Exception as e:
|
||
Logger.error(f"Failed to show image {file_path}: {e}")
|
||
self.image_label.config(image='')
|
||
self.status_label.config(text=f"Image Error:\n{os.path.basename(file_path)}\n{str(e)}")
|
||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||
self.auto_advance_timer = self.root.after(5000, self.next_media)
|
||
|
||
def next_media(self):
|
||
"""Move to next media"""
|
||
self.cancel_timers()
|
||
|
||
if not self.playlist:
|
||
return
|
||
|
||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||
|
||
# Check for playlist updates at end of cycle
|
||
if self.current_index == 0:
|
||
threading.Thread(target=self.check_playlist_updates, daemon=True).start()
|
||
|
||
self.play_current_media()
|
||
|
||
def previous_media(self):
|
||
"""Move to previous media"""
|
||
self.cancel_timers()
|
||
|
||
if not self.playlist:
|
||
return
|
||
|
||
self.current_index = (self.current_index - 1) % len(self.playlist)
|
||
self.play_current_media()
|
||
|
||
def toggle_play_pause(self):
|
||
"""Toggle play/pause state"""
|
||
self.is_paused = not self.is_paused
|
||
|
||
if self.is_paused:
|
||
self.play_pause_btn.config(text="▶ Play")
|
||
self.cancel_timers()
|
||
else:
|
||
self.play_pause_btn.config(text="⏸ Pause")
|
||
# Resume current media
|
||
self.play_current_media()
|
||
|
||
Logger.info(f"Media {'paused' if self.is_paused else 'resumed'}")
|
||
|
||
def cancel_timers(self):
|
||
"""Cancel all active timers"""
|
||
if self.auto_advance_timer:
|
||
self.root.after_cancel(self.auto_advance_timer)
|
||
self.auto_advance_timer = None
|
||
|
||
def show_controls(self):
|
||
"""Show control panel"""
|
||
if self.control_frame:
|
||
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
|
||
|
||
def hide_controls(self):
|
||
"""Hide control panel"""
|
||
if self.control_frame:
|
||
self.control_frame.place_forget()
|
||
|
||
def schedule_hide_controls(self):
|
||
"""Schedule hiding controls after delay"""
|
||
if self.hide_controls_timer:
|
||
self.root.after_cancel(self.hide_controls_timer)
|
||
self.hide_controls_timer = self.root.after(10000, self.hide_controls)
|
||
|
||
def on_mouse_click(self, event):
|
||
"""Handle mouse clicks"""
|
||
self.show_controls()
|
||
self.schedule_hide_controls()
|
||
|
||
def on_mouse_motion(self, event):
|
||
"""Handle mouse motion"""
|
||
self.show_controls()
|
||
self.schedule_hide_controls()
|
||
|
||
def on_key_press(self, event):
|
||
"""Handle keyboard events"""
|
||
key = event.keysym.lower()
|
||
|
||
if key == 'f':
|
||
self.toggle_fullscreen()
|
||
elif key == 'space':
|
||
self.toggle_play_pause()
|
||
elif key == 'left':
|
||
self.previous_media()
|
||
elif key == 'right':
|
||
self.next_media()
|
||
elif key == 'escape':
|
||
self.show_exit_dialog()
|
||
elif key == '1':
|
||
self.set_scaling_mode('fit')
|
||
elif key == '2':
|
||
self.set_scaling_mode('fill')
|
||
elif key == '3':
|
||
self.set_scaling_mode('stretch')
|
||
elif event.state & 0x4: # Ctrl key pressed
|
||
if key == 's':
|
||
self.open_settings()
|
||
|
||
self.show_controls()
|
||
self.schedule_hide_controls()
|
||
|
||
def set_scaling_mode(self, mode):
|
||
"""Change the scaling mode and refresh current media"""
|
||
old_mode = self.scaling_mode
|
||
self.scaling_mode = mode
|
||
Logger.info(f"Scaling mode changed from '{old_mode}' to '{mode}'")
|
||
|
||
# Show temporary notification
|
||
self.status_label.config(text=f"Scaling Mode: {mode.title()}\n"
|
||
f"1=Fit 2=Fill 3=Stretch")
|
||
self.status_label.place(relx=0.5, rely=0.05, anchor='center')
|
||
|
||
# Hide notification after 2 seconds
|
||
self.root.after(2000, lambda: self.status_label.place_forget())
|
||
|
||
# Refresh current media with new scaling
|
||
if self.playlist and 0 <= self.current_index < len(self.playlist):
|
||
self.cancel_timers()
|
||
self.play_current_media()
|
||
|
||
def toggle_fullscreen(self):
|
||
"""Toggle fullscreen mode"""
|
||
self.is_fullscreen = not self.is_fullscreen
|
||
self.root.attributes('-fullscreen', self.is_fullscreen)
|
||
|
||
def open_settings(self):
|
||
"""Open settings window"""
|
||
if hasattr(self, 'settings_window') and self.settings_window and self.settings_window.winfo_exists():
|
||
self.settings_window.lift()
|
||
return
|
||
|
||
# Pause media playback when opening settings
|
||
if not self.is_paused:
|
||
self.toggle_play_pause()
|
||
|
||
self.settings_window = SettingsWindow(self.root, self)
|
||
|
||
# Add a callback to resume playback when the settings window is closed
|
||
def on_settings_close():
|
||
if self.is_paused:
|
||
self.toggle_play_pause()
|
||
|
||
self.settings_window.protocol("WM_DELETE_WINDOW", on_settings_close)
|
||
|
||
def show_exit_dialog(self):
|
||
"""Show modern password-protected exit dialog"""
|
||
try:
|
||
config = load_config()
|
||
quickconnect_key = config.get('quickconnect_key', '')
|
||
except:
|
||
quickconnect_key = ''
|
||
|
||
# Create modern exit dialog
|
||
exit_dialog = tk.Toplevel(self.root)
|
||
exit_dialog.title("Exit Application")
|
||
exit_dialog.geometry("400x200")
|
||
exit_dialog.configure(bg='#2d2d2d')
|
||
exit_dialog.transient(self.root)
|
||
exit_dialog.grab_set()
|
||
exit_dialog.resizable(False, False)
|
||
|
||
# Center the dialog using helper method
|
||
self.center_dialog_on_screen(exit_dialog, 400, 200)
|
||
|
||
# Header with icon
|
||
header_frame = tk.Frame(exit_dialog, bg='#cc0000', height=60)
|
||
header_frame.pack(fill=tk.X)
|
||
header_frame.pack_propagate(False)
|
||
|
||
icon_label = tk.Label(header_frame, text="⚠", font=('Arial', 20, 'bold'),
|
||
fg='white', bg='#cc0000')
|
||
icon_label.pack(side=tk.LEFT, padx=15, pady=15)
|
||
|
||
title_label = tk.Label(header_frame, text="Exit Application",
|
||
font=('Arial', 14, 'bold'), fg='white', bg='#cc0000')
|
||
title_label.pack(side=tk.LEFT, pady=15)
|
||
|
||
# Content frame
|
||
content_frame = tk.Frame(exit_dialog, bg='#2d2d2d', padx=20, pady=20)
|
||
content_frame.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# Password prompt
|
||
prompt_label = tk.Label(content_frame, text="Enter password to exit:",
|
||
font=('Arial', 11), fg='white', bg='#2d2d2d')
|
||
prompt_label.pack(pady=(0, 10))
|
||
|
||
# Password entry
|
||
password_var = tk.StringVar()
|
||
password_entry = tk.Entry(content_frame, textvariable=password_var,
|
||
font=('Arial', 11), show='*', width=25,
|
||
bg='#404040', fg='white', insertbackground='white',
|
||
relief=tk.FLAT, bd=5)
|
||
password_entry.pack(pady=(0, 15))
|
||
password_entry.focus_set()
|
||
|
||
# Button frame
|
||
button_frame = tk.Frame(content_frame, bg='#2d2d2d')
|
||
button_frame.pack(fill=tk.X)
|
||
|
||
def check_password():
|
||
if password_var.get() == quickconnect_key:
|
||
exit_dialog.destroy()
|
||
self.exit_application()
|
||
elif password_var.get(): # Only show error if password was entered
|
||
# Show error in red
|
||
error_label.config(text="✗ Incorrect password", fg='#ff4444')
|
||
password_entry.delete(0, tk.END)
|
||
password_entry.focus_set()
|
||
|
||
def cancel_exit():
|
||
exit_dialog.destroy()
|
||
|
||
# Error label (hidden initially)
|
||
error_label = tk.Label(content_frame, text="", font=('Arial', 9),
|
||
bg='#2d2d2d')
|
||
error_label.pack()
|
||
|
||
# Buttons
|
||
cancel_btn = tk.Button(button_frame, text="Cancel", command=cancel_exit,
|
||
bg='#555555', fg='white', font=('Arial', 10, 'bold'),
|
||
relief=tk.FLAT, padx=20, pady=8, width=10)
|
||
cancel_btn.pack(side=tk.RIGHT, padx=(10, 0))
|
||
|
||
exit_btn = tk.Button(button_frame, text="Exit", command=check_password,
|
||
bg='#cc0000', fg='white', font=('Arial', 10, 'bold'),
|
||
relief=tk.FLAT, padx=20, pady=8, width=10)
|
||
exit_btn.pack(side=tk.RIGHT)
|
||
|
||
# Bind Enter key to check password
|
||
password_entry.bind('<Return>', lambda e: check_password())
|
||
exit_dialog.bind('<Escape>', lambda e: cancel_exit())
|
||
|
||
def exit_application(self):
|
||
"""Exit the application"""
|
||
Logger.info("Application exit requested")
|
||
self.running = False
|
||
self.root.quit()
|
||
self.root.destroy()
|
||
|
||
def center_dialog_on_screen(self, dialog, width, height):
|
||
"""Center a dialog window on screen regardless of screen size"""
|
||
dialog.update_idletasks() # Ensure geometry is calculated
|
||
screen_width = dialog.winfo_screenwidth()
|
||
screen_height = dialog.winfo_screenheight()
|
||
|
||
# Calculate center position
|
||
center_x = int((screen_width - width) / 2)
|
||
center_y = int((screen_height - height) / 2)
|
||
|
||
# Ensure the dialog doesn't go off-screen on smaller displays
|
||
center_x = max(0, min(center_x, screen_width - width))
|
||
center_y = max(0, min(center_y, screen_height - height))
|
||
|
||
dialog.geometry(f"{width}x{height}+{center_x}+{center_y}")
|
||
|
||
# Bring to front and focus
|
||
dialog.lift()
|
||
dialog.focus_force()
|
||
|
||
return center_x, center_y
|
||
|
||
def check_playlist_updates(self):
|
||
"""Check for playlist updates from server with fallback protection"""
|
||
try:
|
||
config = load_config()
|
||
local_version = config.get('playlist_version', 0)
|
||
|
||
server_playlist_data = fetch_server_playlist()
|
||
server_version = server_playlist_data.get('version', 0)
|
||
|
||
if server_version > local_version:
|
||
Logger.info(f"Updating playlist: {local_version} -> {server_version}")
|
||
|
||
# Clean old files
|
||
local_playlist_data = load_local_playlist()
|
||
clean_unused_files(local_playlist_data.get('playlist', []))
|
||
|
||
# Download new content
|
||
download_media_files(
|
||
server_playlist_data.get('playlist', []),
|
||
server_version
|
||
)
|
||
|
||
# Update local playlist
|
||
local_playlist_data = load_local_playlist()
|
||
self.playlist = local_playlist_data.get('playlist', [])
|
||
|
||
# Reset to beginning of playlist
|
||
self.current_index = 0
|
||
|
||
Logger.info("Playlist updated successfully")
|
||
|
||
# Continue with current media after update
|
||
self.play_current_media()
|
||
else:
|
||
Logger.info("No playlist updates available")
|
||
|
||
except requests.exceptions.ConnectTimeout:
|
||
Logger.warning("Server connection timeout during update check - continuing with current playlist")
|
||
except requests.exceptions.ConnectionError:
|
||
Logger.warning("Cannot connect to server during update check - continuing with current playlist")
|
||
except requests.exceptions.Timeout:
|
||
Logger.warning("Server request timeout during update check - continuing with current playlist")
|
||
except Exception as e:
|
||
Logger.warning(f"Failed to check playlist updates: {e} - continuing with current playlist")
|
||
|
||
def log_event(self, file_name, event):
|
||
"""Log media events"""
|
||
try:
|
||
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
log_message = f"{timestamp} - {event}: {file_name}\n"
|
||
|
||
# Update the log file path to the resources directory
|
||
log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'log.txt')
|
||
with open(log_file, 'a') as f:
|
||
f.write(log_message)
|
||
|
||
except Exception as e:
|
||
Logger.error(f"Failed to log event: {e}")
|
||
|
||
def start_periodic_checks(self):
|
||
"""Start periodic playlist checks"""
|
||
def check_loop():
|
||
"""Background thread for periodic checks"""
|
||
while self.running:
|
||
try:
|
||
time.sleep(300) # Check every 5 minutes
|
||
if self.running:
|
||
self.check_playlist_updates()
|
||
except Exception as e:
|
||
Logger.error(f"Error in periodic check: {e}")
|
||
|
||
# Start background thread
|
||
threading.Thread(target=check_loop, daemon=True).start()
|
||
|
||
def run(self):
|
||
"""Start the application"""
|
||
Logger.info("Starting Simple Tkinter Media Player")
|
||
try:
|
||
self.root.mainloop()
|
||
except KeyboardInterrupt:
|
||
self.exit_application()
|
||
except Exception as e:
|
||
Logger.error(f"Application error: {e}")
|
||
print(f"Error: {e}")
|
||
|
||
|
||
class SettingsWindow:
|
||
def __init__(self, parent, app):
|
||
self.parent = parent
|
||
self.app = app
|
||
self.window = tk.Toplevel(parent)
|
||
|
||
# Initialize virtual keyboard for touch displays
|
||
self.virtual_keyboard = VirtualKeyboard(self.window, dark_theme=True)
|
||
|
||
self.setup_window()
|
||
self.create_widgets()
|
||
self.load_config()
|
||
|
||
# Setup touch optimization
|
||
self.setup_touch_optimization()
|
||
|
||
def setup_window(self):
|
||
"""Setup settings window with enhanced dark theme"""
|
||
self.window.title("🎬 Signage Player Settings")
|
||
self.window.geometry("900x700")
|
||
|
||
# Enhanced dark theme colors
|
||
self.colors = {
|
||
'bg_primary': '#1e2124', # Very dark background
|
||
'bg_secondary': '#2f3136', # Slightly lighter background
|
||
'bg_tertiary': '#36393f', # Card backgrounds
|
||
'accent': '#7289da', # Discord-like blue accent
|
||
'accent_hover': '#677bc4', # Darker accent for hover
|
||
'success': '#43b581', # Green for success
|
||
'warning': '#faa61a', # Orange for warnings
|
||
'danger': '#f04747', # Red for errors
|
||
'text_primary': '#ffffff', # White text
|
||
'text_secondary': '#b9bbbe', # Gray text
|
||
'text_muted': '#72767d', # Muted text
|
||
'border': '#202225' # Border color
|
||
}
|
||
|
||
self.window.configure(bg=self.colors['bg_primary'])
|
||
self.window.transient(self.parent)
|
||
self.window.grab_set()
|
||
|
||
# Set window properties
|
||
self.window.resizable(True, True)
|
||
self.window.minsize(700, 500)
|
||
|
||
# Set window icon if available
|
||
try:
|
||
# Try to use a simple emoji icon
|
||
self.window.iconname("🎬")
|
||
except:
|
||
pass
|
||
|
||
# Center the window on screen using helper method
|
||
self.center_window_on_screen(self.window, 900, 700)
|
||
|
||
# Add subtle window border effect
|
||
self.window.configure(highlightbackground=self.colors['border'], highlightthickness=1)
|
||
|
||
def create_widgets(self):
|
||
"""Create settings widgets with enhanced dark theme styling"""
|
||
# Configure enhanced custom styles
|
||
style = ttk.Style()
|
||
style.theme_use('clam')
|
||
# Enhanced dark theme styles
|
||
style.configure('Dark.TNotebook',
|
||
background=self.colors['bg_secondary'],
|
||
borderwidth=0,
|
||
tabmargins=[2, 5, 2, 0])
|
||
style.configure('Dark.TNotebook.Tab',
|
||
padding=[20, 12],
|
||
font=('Segoe UI', 11, 'bold'),
|
||
background=self.colors['bg_tertiary'],
|
||
foreground=self.colors['text_secondary'],
|
||
borderwidth=1,
|
||
focuscolor='none')
|
||
style.map('Dark.TNotebook.Tab',
|
||
background=[('selected', self.colors['accent']),
|
||
('active', self.colors['accent_hover'])],
|
||
foreground=[('selected', self.colors['text_primary']),
|
||
('active', self.colors['text_primary'])])
|
||
style.configure('Dark.TFrame', background=self.colors['bg_secondary'])
|
||
style.configure('Dark.TLabel',
|
||
background=self.colors['bg_secondary'],
|
||
foreground=self.colors['text_primary'],
|
||
font=('Segoe UI', 10))
|
||
style.configure('Dark.TEntry',
|
||
fieldbackground=self.colors['bg_tertiary'],
|
||
foreground=self.colors['text_primary'],
|
||
bordercolor=self.colors['border'],
|
||
lightcolor=self.colors['bg_tertiary'],
|
||
darkcolor=self.colors['bg_tertiary'],
|
||
font=('Segoe UI', 10),
|
||
insertcolor=self.colors['text_primary'])
|
||
|
||
# Main container frame
|
||
main_frame = tk.Frame(self.window, bg=self.colors['bg_primary'])
|
||
main_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
|
||
|
||
# Header section with gradient-like effect
|
||
header_frame = tk.Frame(main_frame, bg=self.colors['bg_secondary'], height=80)
|
||
header_frame.pack(fill=tk.X, padx=0, pady=0)
|
||
header_frame.pack_propagate(False)
|
||
|
||
# Title with enhanced styling
|
||
title_container = tk.Frame(header_frame, bg=self.colors['bg_secondary'])
|
||
title_container.pack(expand=True, fill=tk.BOTH)
|
||
|
||
title_label = tk.Label(title_container,
|
||
text="🎬 Digital Signage Control Center",
|
||
font=('Segoe UI', 24, 'bold'),
|
||
fg=self.colors['text_primary'],
|
||
bg=self.colors['bg_secondary'])
|
||
title_label.pack(expand=True)
|
||
|
||
subtitle_label = tk.Label(title_container,
|
||
text="Configure your digital signage display settings",
|
||
font=('Segoe UI', 11),
|
||
fg=self.colors['text_secondary'],
|
||
bg=self.colors['bg_secondary'])
|
||
subtitle_label.pack()
|
||
|
||
# Content area
|
||
content_frame = tk.Frame(main_frame, bg=self.colors['bg_primary'])
|
||
content_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
|
||
|
||
# Create enhanced notebook with dark theme
|
||
notebook = ttk.Notebook(content_frame, style='Dark.TNotebook')
|
||
notebook.pack(fill=tk.BOTH, expand=True, pady=(0, 20))
|
||
|
||
# Create tabs with enhanced styling
|
||
self.create_connection_tab_enhanced(notebook)
|
||
self.create_display_tab_enhanced(notebook)
|
||
self.create_advanced_tab_enhanced(notebook)
|
||
self.create_logs_tab_enhanced(notebook)
|
||
self.create_about_tab_enhanced(notebook)
|
||
|
||
# Enhanced bottom button section
|
||
self.create_bottom_controls(content_frame)
|
||
|
||
# Load initial data
|
||
self.load_logs()
|
||
|
||
def create_connection_tab_enhanced(self, notebook):
|
||
"""Create enhanced connection settings tab"""
|
||
tab_frame = tk.Frame(notebook, bg=self.colors['bg_secondary'])
|
||
notebook.add(tab_frame, text="🌐 Connection")
|
||
|
||
# Create scrollable content
|
||
canvas = tk.Canvas(tab_frame, bg=self.colors['bg_secondary'], highlightthickness=0)
|
||
scrollbar = tk.Scrollbar(tab_frame, orient="vertical", command=canvas.yview,
|
||
bg=self.colors['bg_tertiary'], troughcolor=self.colors['bg_primary'])
|
||
scrollable_frame = tk.Frame(canvas, bg=self.colors['bg_secondary'])
|
||
|
||
scrollable_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
||
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
|
||
canvas.configure(yscrollcommand=scrollbar.set)
|
||
|
||
# Server connection card
|
||
server_card = self.create_settings_card(scrollable_frame, "🖥️ Server Connection",
|
||
"Configure your signage server connection details")
|
||
|
||
# Server IP/Domain
|
||
self.create_input_field(server_card, "Server IP/Domain:", "server_ip_var",
|
||
placeholder="e.g., digi-server.example.com")
|
||
|
||
# Port
|
||
self.create_input_field(server_card, "Port:", "port_var", width=15,
|
||
placeholder="8880")
|
||
|
||
# Device settings card
|
||
device_card = self.create_settings_card(scrollable_frame, "📱 Device Settings",
|
||
"Identify this display device")
|
||
|
||
# Screen/Device name
|
||
self.create_input_field(device_card, "Device Name:", "screen_name_var",
|
||
placeholder="e.g., lobby-display-01")
|
||
|
||
# QuickConnect key
|
||
self.create_input_field(device_card, "QuickConnect Key:", "quickconnect_var",
|
||
password=True, placeholder="Enter your access key")
|
||
|
||
# Connection testing card
|
||
test_card = self.create_settings_card(scrollable_frame, "🔗 Connection Test",
|
||
"Test your server connection")
|
||
|
||
test_btn_frame = tk.Frame(test_card, bg=self.colors['bg_tertiary'])
|
||
test_btn_frame.pack(fill=tk.X, pady=10)
|
||
|
||
test_btn = self.create_action_button(test_btn_frame, "🔗 Test Connection",
|
||
self.test_connection, self.colors['accent'])
|
||
test_btn.pack(side=tk.LEFT, padx=5)
|
||
|
||
canvas.pack(side="left", fill="both", expand=True, padx=20, pady=20)
|
||
scrollbar.pack(side="right", fill="y")
|
||
|
||
def create_display_tab_enhanced(self, notebook):
|
||
"""Create enhanced display settings tab"""
|
||
tab_frame = tk.Frame(notebook, bg=self.colors['bg_secondary'])
|
||
notebook.add(tab_frame, text="🖼️ Display")
|
||
|
||
main_container = tk.Frame(tab_frame, bg=self.colors['bg_secondary'])
|
||
main_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
|
||
|
||
# Resolution settings card
|
||
resolution_card = self.create_settings_card(main_container, "🖥️ Screen Resolution",
|
||
"Configure display resolution settings")
|
||
|
||
# Resolution input fields
|
||
resolution_inputs = tk.Frame(resolution_card, bg=self.colors['bg_tertiary'])
|
||
resolution_inputs.pack(fill=tk.X, pady=10)
|
||
|
||
self.create_input_field(resolution_inputs, "Width (px):", "screen_w_var",
|
||
width=15, placeholder="1920")
|
||
self.create_input_field(resolution_inputs, "Height (px):", "screen_h_var",
|
||
width=15, placeholder="1080")
|
||
|
||
# Preset buttons with enhanced styling
|
||
presets_frame = tk.Frame(resolution_card, bg=self.colors['bg_tertiary'])
|
||
presets_frame.pack(fill=tk.X, pady=15)
|
||
|
||
tk.Label(presets_frame, text="Quick Presets:",
|
||
font=('Segoe UI', 11, 'bold'),
|
||
bg=self.colors['bg_tertiary'],
|
||
fg=self.colors['text_primary']).pack(anchor=tk.W, pady=(0, 10))
|
||
|
||
preset_buttons = tk.Frame(presets_frame, bg=self.colors['bg_tertiary'])
|
||
preset_buttons.pack(anchor=tk.W)
|
||
|
||
presets = [("📱 HD", "1366", "768"), ("💻 Full HD", "1920", "1080"),
|
||
("🖥️ 4K", "3840", "2160"), ("📺 Classic", "1024", "768")]
|
||
|
||
for preset_name, width, height in presets:
|
||
btn = self.create_preset_button(preset_buttons, preset_name, width, height)
|
||
btn.pack(side=tk.LEFT, padx=3, pady=2)
|
||
|
||
# Scaling mode settings card
|
||
scaling_card = self.create_settings_card(main_container, "📐 Image/Video Scaling",
|
||
"Configure how images and videos are displayed on screen")
|
||
|
||
# Scaling mode options
|
||
self.scaling_mode_var = tk.StringVar(value=self.app.scaling_mode if hasattr(self.app, 'scaling_mode') else 'fit')
|
||
|
||
scaling_label = tk.Label(scaling_card, text="Scaling Mode:",
|
||
font=('Segoe UI', 12, 'bold'),
|
||
bg=self.colors['bg_tertiary'],
|
||
fg=self.colors['text_primary'])
|
||
scaling_label.pack(anchor=tk.W, pady=(0, 10))
|
||
|
||
scaling_options = tk.Frame(scaling_card, bg=self.colors['bg_tertiary'])
|
||
scaling_options.pack(fill=tk.X, pady=5)
|
||
|
||
# Radio buttons for scaling modes
|
||
modes = [
|
||
("fit", "🖼️ Fit", "Maintain aspect ratio, add black bars if needed"),
|
||
("fill", "🔍 Fill", "Maintain aspect ratio, crop to fill entire screen"),
|
||
("stretch", "↔️ Stretch", "Ignore aspect ratio, stretch to fill screen")
|
||
]
|
||
|
||
for mode_value, mode_label, mode_desc in modes:
|
||
mode_frame = tk.Frame(scaling_options, bg=self.colors['bg_tertiary'])
|
||
mode_frame.pack(fill=tk.X, pady=2)
|
||
|
||
radio_btn = tk.Radiobutton(mode_frame,
|
||
text=mode_label,
|
||
variable=self.scaling_mode_var,
|
||
value=mode_value,
|
||
bg=self.colors['bg_tertiary'],
|
||
fg=self.colors['text_primary'],
|
||
font=('Segoe UI', 11, 'bold'),
|
||
selectcolor=self.colors['bg_primary'],
|
||
activebackground=self.colors['bg_tertiary'],
|
||
activeforeground=self.colors['text_primary'],
|
||
command=lambda: self.update_scaling_mode())
|
||
radio_btn.pack(side=tk.LEFT, anchor=tk.W)
|
||
|
||
desc_label = tk.Label(mode_frame, text=f" - {mode_desc}",
|
||
font=('Segoe UI', 9),
|
||
bg=self.colors['bg_tertiary'],
|
||
fg=self.colors['text_secondary'])
|
||
desc_label.pack(side=tk.LEFT, anchor=tk.W, padx=(10, 0))
|
||
|
||
# Keyboard shortcuts info
|
||
shortcuts_frame = tk.Frame(scaling_card, bg=self.colors['bg_tertiary'])
|
||
shortcuts_frame.pack(fill=tk.X, pady=(15, 0))
|
||
|
||
shortcuts_label = tk.Label(shortcuts_frame,
|
||
text="💡 Tip: Use keyboard shortcuts 1, 2, 3 to quickly change scaling mode during playback",
|
||
font=('Segoe UI', 9, 'italic'),
|
||
bg=self.colors['bg_tertiary'],
|
||
fg=self.colors['text_muted'],
|
||
wraplength=400)
|
||
shortcuts_label.pack(anchor=tk.W)
|
||
|
||
def update_scaling_mode(self):
|
||
"""Update the scaling mode in the main app"""
|
||
new_mode = self.scaling_mode_var.get()
|
||
if hasattr(self.app, 'set_scaling_mode'):
|
||
self.app.set_scaling_mode(new_mode)
|
||
else:
|
||
self.app.scaling_mode = new_mode
|
||
|
||
def create_advanced_tab_enhanced(self, notebook):
|
||
"""Create enhanced advanced settings tab"""
|
||
tab_frame = tk.Frame(notebook, bg=self.colors['bg_secondary'])
|
||
notebook.add(tab_frame, text="⚙️ Advanced")
|
||
|
||
main_container = tk.Frame(tab_frame, bg=self.colors['bg_secondary'])
|
||
main_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
|
||
|
||
# Sync settings card
|
||
sync_card = self.create_settings_card(main_container, "🔄 Synchronization",
|
||
"Configure automatic content updates")
|
||
|
||
self.create_input_field(sync_card, "Auto-refresh (minutes):", "refresh_interval_var",
|
||
width=15, placeholder="15")
|
||
|
||
# Performance settings card
|
||
perf_card = self.create_settings_card(main_container, "⚡ Performance",
|
||
"Optimize playback performance")
|
||
|
||
self.hardware_accel_var = tk.BooleanVar(value=True)
|
||
self.create_checkbox(perf_card, "Enable hardware acceleration", self.hardware_accel_var)
|
||
self.settings_window = SettingsWindow(self.root, self, dark_theme=True)
|
||
self.cache_media_var = tk.BooleanVar(value=True)
|
||
self.create_checkbox(perf_card, "Cache media files locally", self.cache_media_var)
|
||
|
||
self.auto_retry_var = tk.BooleanVar(value=True)
|
||
self.create_checkbox(perf_card, "Auto-retry failed downloads", self.auto_retry_var)
|
||
|
||
def create_logs_tab_enhanced(self, notebook):
|
||
"""Create enhanced logs tab"""
|
||
tab_frame = tk.Frame(notebook, bg=self.colors['bg_secondary'])
|
||
notebook.add(tab_frame, text="📋 Logs")
|
||
|
||
main_container = tk.Frame(tab_frame, bg=self.colors['bg_secondary'])
|
||
main_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
|
||
|
||
# Log header
|
||
log_header = tk.Frame(main_container, bg=self.colors['bg_secondary'])
|
||
log_header.pack(fill=tk.X, pady=(0, 15))
|
||
|
||
tk.Label(log_header, text="📋 Application Logs",
|
||
font=('Segoe UI', 16, 'bold'),
|
||
bg=self.colors['bg_secondary'],
|
||
fg=self.colors['text_primary']).pack(side=tk.LEFT)
|
||
|
||
refresh_btn = self.create_action_button(log_header, "🔄 Refresh Logs",
|
||
self.load_logs, self.colors['accent'])
|
||
refresh_btn.pack(side=tk.RIGHT)
|
||
|
||
# Log display area
|
||
log_container = tk.Frame(main_container, bg=self.colors['bg_tertiary'],
|
||
relief=tk.FLAT, bd=2)
|
||
log_container.pack(fill=tk.BOTH, expand=True)
|
||
|
||
self.log_text = tk.Text(log_container,
|
||
font=('Consolas', 9),
|
||
bg=self.colors['bg_primary'],
|
||
fg=self.colors['text_primary'],
|
||
insertbackground=self.colors['accent'],
|
||
selectbackground=self.colors['accent'],
|
||
selectforeground=self.colors['text_primary'],
|
||
relief=tk.FLAT,
|
||
bd=10,
|
||
wrap=tk.WORD)
|
||
|
||
log_scrollbar = tk.Scrollbar(log_container, command=self.log_text.yview,
|
||
bg=self.colors['bg_tertiary'])
|
||
self.log_text.configure(yscrollcommand=log_scrollbar.set)
|
||
|
||
self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||
|
||
def create_about_tab_enhanced(self, notebook):
|
||
"""Create enhanced about tab"""
|
||
tab_frame = tk.Frame(notebook, bg=self.colors['bg_secondary'])
|
||
notebook.add(tab_frame, text="ℹ️ About")
|
||
|
||
main_container = tk.Frame(tab_frame, bg=self.colors['bg_secondary'])
|
||
main_container.pack(fill=tk.BOTH, expand=True, padx=40, pady=40)
|
||
|
||
# App branding section
|
||
branding_frame = tk.Frame(main_container, bg=self.colors['bg_secondary'])
|
||
branding_frame.pack(fill=tk.X, pady=(0, 40))
|
||
|
||
# Large app icon
|
||
icon_label = tk.Label(branding_frame, text="🎬",
|
||
font=('Segoe UI Emoji', 64),
|
||
bg=self.colors['bg_secondary'],
|
||
fg=self.colors['accent'])
|
||
icon_label.pack()
|
||
|
||
# App title and version
|
||
title_label = tk.Label(branding_frame, text="Digital Signage Player",
|
||
font=('Segoe UI', 24, 'bold'),
|
||
bg=self.colors['bg_secondary'],
|
||
fg=self.colors['text_primary'])
|
||
title_label.pack(pady=(10, 5))
|
||
|
||
version_label = tk.Label(branding_frame, text="Version 2.1 - Enhanced Dark Edition",
|
||
font=('Segoe UI', 12),
|
||
bg=self.colors['bg_secondary'],
|
||
fg=self.colors['text_secondary'])
|
||
version_label.pack()
|
||
|
||
# Feature highlights
|
||
features_text = (
|
||
"🚀 Features:\n"
|
||
"• Modern dark theme interface\n"
|
||
"• High-resolution media display\n"
|
||
"• Remote playlist management\n"
|
||
"• Real-time content synchronization\n"
|
||
"• Hardware acceleration support\n"
|
||
"• Cross-platform compatibility\n"
|
||
"• Advanced logging and diagnostics\n\n"
|
||
"⌨️ Keyboard Shortcuts:\n"
|
||
"F11 - Toggle fullscreen mode\n"
|
||
"Space - Play/Pause media\n"
|
||
"← → - Navigate between media\n"
|
||
"1 - Set scaling mode to Fit (maintain aspect ratio with black bars)\n"
|
||
"2 - Set scaling mode to Fill (maintain aspect ratio, crop to fill screen)\n"
|
||
"3 - Set scaling mode to Stretch (ignore aspect ratio, stretch to fill)\n"
|
||
"Ctrl+S - Open settings panel\n"
|
||
"Escape - Exit application (password protected)\n\n"
|
||
"Built with ❤️ using Python & Tkinter"
|
||
)
|
||
|
||
features_label = tk.Label(main_container, text=features_text,
|
||
justify=tk.LEFT,
|
||
font=('Segoe UI', 11),
|
||
bg=self.colors['bg_secondary'],
|
||
fg=self.colors['text_primary'],
|
||
wraplength=600)
|
||
features_label.pack(anchor=tk.W)
|
||
|
||
def create_bottom_controls(self, parent):
|
||
"""Create enhanced bottom control buttons"""
|
||
controls_frame = tk.Frame(parent, bg=self.colors['bg_secondary'],
|
||
relief=tk.FLAT, bd=1)
|
||
controls_frame.pack(fill=tk.X, pady=(10, 0))
|
||
|
||
# Left side buttons
|
||
left_buttons = tk.Frame(controls_frame, bg=self.colors['bg_secondary'])
|
||
left_buttons.pack(side=tk.LEFT, padx=10, pady=15)
|
||
|
||
save_btn = self.create_action_button(left_buttons, "💾 Save Configuration",
|
||
self.save_config, self.colors['success'])
|
||
save_btn.pack(side=tk.LEFT, padx=5)
|
||
|
||
test_btn = self.create_action_button(left_buttons, "🔗 Test Connection",
|
||
self.test_connection, self.colors['accent'])
|
||
test_btn.pack(side=tk.LEFT, padx=5)
|
||
|
||
refresh_btn = self.create_action_button(left_buttons, "🔄 Refresh Playlist",
|
||
self.force_playlist_refresh, self.colors['warning'])
|
||
refresh_btn.pack(side=tk.LEFT, padx=5)
|
||
|
||
# Right side buttons
|
||
right_buttons = tk.Frame(controls_frame, bg=self.colors['bg_secondary'])
|
||
right_buttons.pack(side=tk.RIGHT, padx=10, pady=15)
|
||
|
||
cancel_btn = self.create_action_button(right_buttons, "❌ Cancel",
|
||
self.window.destroy, self.colors['danger'])
|
||
cancel_btn.pack(side=tk.RIGHT, padx=5)
|
||
|
||
# Status display
|
||
self.status_frame = tk.Frame(parent, bg=self.colors['bg_primary'])
|
||
self.status_frame.pack(fill=tk.X, pady=(10, 0))
|
||
|
||
self.connection_status = tk.Label(self.status_frame,
|
||
text="Ready to configure your digital signage settings",
|
||
bg=self.colors['bg_primary'],
|
||
fg=self.colors['text_secondary'],
|
||
font=('Segoe UI', 9))
|
||
self.connection_status.pack(anchor=tk.W, padx=10, pady=5)
|
||
|
||
def create_settings_card(self, parent, title, description):
|
||
"""Create a settings card with enhanced styling"""
|
||
card_frame = tk.Frame(parent, bg=self.colors['bg_tertiary'],
|
||
relief=tk.FLAT, bd=2)
|
||
card_frame.pack(fill=tk.X, pady=(0, 20), padx=5)
|
||
|
||
# Card header
|
||
header_frame = tk.Frame(card_frame, bg=self.colors['bg_tertiary'])
|
||
header_frame.pack(fill=tk.X, padx=20, pady=(15, 5))
|
||
|
||
title_label = tk.Label(header_frame, text=title,
|
||
font=('Segoe UI', 14, 'bold'),
|
||
bg=self.colors['bg_tertiary'],
|
||
fg=self.colors['text_primary'])
|
||
title_label.pack(anchor=tk.W)
|
||
|
||
desc_label = tk.Label(header_frame, text=description,
|
||
font=('Segoe UI', 9),
|
||
bg=self.colors['bg_tertiary'],
|
||
fg=self.colors['text_secondary'])
|
||
desc_label.pack(anchor=tk.W, pady=(2, 0))
|
||
|
||
# Card content area
|
||
content_frame = tk.Frame(card_frame, bg=self.colors['bg_tertiary'])
|
||
content_frame.pack(fill=tk.X, padx=20, pady=(10, 15))
|
||
|
||
return content_frame
|
||
|
||
def create_input_field(self, parent, label_text, var_name, width=35, placeholder="", password=False):
|
||
"""Create a touch-optimized input field with virtual keyboard support"""
|
||
field_frame = tk.Frame(parent, bg=self.colors['bg_tertiary'])
|
||
field_frame.pack(fill=tk.X, pady=12) # Increased padding for touch
|
||
|
||
label = tk.Label(field_frame, text=label_text,
|
||
font=('Segoe UI', 12, 'bold'), # Larger font for touch
|
||
bg=self.colors['bg_tertiary'],
|
||
fg=self.colors['text_primary'],
|
||
width=18, anchor='w')
|
||
label.pack(side=tk.LEFT)
|
||
|
||
# Create StringVar if it doesn't exist
|
||
if not hasattr(self, var_name):
|
||
setattr(self, var_name, tk.StringVar())
|
||
|
||
var = getattr(self, var_name)
|
||
|
||
# Use touch-optimized entry with virtual keyboard
|
||
entry = TouchOptimizedEntry(field_frame,
|
||
virtual_keyboard=self.virtual_keyboard,
|
||
textvariable=var,
|
||
width=width,
|
||
font=('Segoe UI', 12), # Larger font
|
||
bg=self.colors['bg_primary'],
|
||
fg=self.colors['text_primary'],
|
||
insertbackground=self.colors['accent'],
|
||
relief=tk.FLAT,
|
||
bd=10) # Larger border for easier touch
|
||
|
||
if password:
|
||
entry.configure(show='*')
|
||
|
||
entry.pack(side=tk.LEFT, padx=(15, 0), pady=5)
|
||
|
||
# Add placeholder text effect
|
||
if placeholder:
|
||
self.add_placeholder(entry, placeholder)
|
||
|
||
return entry
|
||
|
||
def create_checkbox(self, parent, text, variable):
|
||
"""Create a checkbox with dark theme styling"""
|
||
cb_frame = tk.Frame(parent, bg=self.colors['bg_tertiary'])
|
||
cb_frame.pack(fill=tk.X, pady=5)
|
||
|
||
checkbox = tk.Checkbutton(cb_frame, text=text, variable=variable,
|
||
bg=self.colors['bg_tertiary'],
|
||
fg=self.colors['text_primary'],
|
||
font=('Segoe UI', 10),
|
||
selectcolor=self.colors['bg_primary'],
|
||
activebackground=self.colors['bg_tertiary'],
|
||
activeforeground=self.colors['text_primary'])
|
||
checkbox.pack(anchor=tk.W, padx=5)
|
||
|
||
def create_action_button(self, parent, text, command, color):
|
||
"""Create a touch-optimized action button with enhanced styling"""
|
||
button = TouchOptimizedButton(parent,
|
||
text=text,
|
||
command=command,
|
||
bg=color,
|
||
fg=self.colors['text_primary'],
|
||
font=('Segoe UI', 12, 'bold'), # Larger font
|
||
relief=tk.FLAT,
|
||
padx=25, # Larger padding for touch
|
||
pady=15, # Larger padding for touch
|
||
cursor='hand2',
|
||
bd=3)
|
||
|
||
# Add enhanced hover effects for touch feedback
|
||
def on_enter(e):
|
||
button.configure(bg=self.lighten_color(color), relief=tk.RAISED)
|
||
|
||
def on_leave(e):
|
||
button.configure(bg=color, relief=tk.FLAT)
|
||
|
||
def on_press(e):
|
||
button.configure(relief=tk.SUNKEN)
|
||
|
||
def on_release(e):
|
||
button.configure(relief=tk.FLAT)
|
||
|
||
button.bind("<Enter>", on_enter)
|
||
button.bind("<Leave>", on_leave)
|
||
button.bind("<Button-1>", on_press)
|
||
button.bind("<ButtonRelease-1>", on_release)
|
||
|
||
return button
|
||
|
||
def create_preset_button(self, parent, text, width, height):
|
||
"""Create a touch-optimized preset resolution button"""
|
||
button = TouchOptimizedButton(parent,
|
||
text=text,
|
||
command=lambda: self.set_resolution_preset(width, height),
|
||
bg=self.colors['bg_primary'],
|
||
fg=self.colors['text_primary'],
|
||
font=('Segoe UI', 10, 'bold'),
|
||
relief=tk.FLAT,
|
||
padx=20, # Larger for touch
|
||
pady=10, # Larger for touch
|
||
cursor='hand2')
|
||
|
||
def on_enter(e):
|
||
button.configure(bg=self.colors['accent'], relief=tk.RAISED)
|
||
|
||
def on_leave(e):
|
||
button.configure(bg=self.colors['bg_primary'], relief=tk.FLAT)
|
||
|
||
def on_press(e):
|
||
button.configure(relief=tk.SUNKEN)
|
||
|
||
def on_release(e):
|
||
button.configure(relief=tk.FLAT)
|
||
|
||
button.bind("<Enter>", on_enter)
|
||
button.bind("<Leave>", on_leave)
|
||
button.bind("<Button-1>", on_press)
|
||
button.bind("<ButtonRelease-1>", on_release)
|
||
|
||
return button
|
||
|
||
def add_placeholder(self, entry, placeholder_text):
|
||
"""Add placeholder text to entry widget"""
|
||
entry.insert(0, placeholder_text)
|
||
entry.configure(fg=self.colors['text_muted'])
|
||
|
||
def on_focus_in(event):
|
||
if entry.get() == placeholder_text:
|
||
entry.delete(0, tk.END)
|
||
entry.configure(fg=self.colors['text_primary'])
|
||
|
||
def on_focus_out(event):
|
||
if not entry.get():
|
||
entry.insert(0, placeholder_text)
|
||
entry.configure(fg=self.colors['text_muted'])
|
||
|
||
entry.bind('<FocusIn>', on_focus_in)
|
||
entry.bind('<FocusOut>', on_focus_out)
|
||
|
||
def lighten_color(self, color):
|
||
"""Lighten a hex color for hover effects"""
|
||
color = color.lstrip('#')
|
||
rgb = tuple(int(color[i:i+2], 16) for i in (0, 2, 4))
|
||
lighter_rgb = tuple(min(255, int(c * 1.2)) for c in rgb)
|
||
return f"#{lighter_rgb[0]:02x}{lighter_rgb[1]:02x}{lighter_rgb[2]:02x}"
|
||
|
||
def center_window_on_screen(self, window, width, height):
|
||
"""Center a window on screen regardless of screen size"""
|
||
window.update_idletasks() # Ensure geometry is calculated
|
||
screen_width = window.winfo_screenwidth()
|
||
screen_height = window.winfo_screenheight()
|
||
|
||
# Calculate center position
|
||
center_x = int((screen_width - width) / 2)
|
||
center_y = int((screen_height - height) / 2)
|
||
|
||
# Ensure the window doesn't go off-screen on smaller displays
|
||
center_x = max(0, min(center_x, screen_width - width))
|
||
center_y = max(0, min(center_y, screen_height - height))
|
||
|
||
window.geometry(f"{width}x{height}+{center_x}+{center_y}")
|
||
|
||
# Bring to front and focus
|
||
window.lift()
|
||
window.focus_force()
|
||
|
||
return center_x, center_y
|
||
|
||
def setup_touch_optimization(self):
|
||
"""Setup touch-friendly optimizations"""
|
||
# Hide virtual keyboard when clicking outside input fields
|
||
def hide_keyboard_on_click(event):
|
||
# Check if click is not on an entry widget
|
||
if not isinstance(event.widget, (tk.Entry, TouchOptimizedEntry)):
|
||
self.virtual_keyboard.hide_keyboard()
|
||
|
||
self.window.bind("<Button-1>", hide_keyboard_on_click, "+")
|
||
|
||
# Make window touch-friendly by increasing minimum size
|
||
self.window.minsize(800, 600)
|
||
|
||
# Override window close to hide keyboard first
|
||
original_destroy = self.window.destroy
|
||
def enhanced_destroy():
|
||
self.virtual_keyboard.hide_keyboard()
|
||
original_destroy()
|
||
|
||
self.window.destroy = enhanced_destroy
|
||
self.window.protocol("WM_DELETE_WINDOW", enhanced_destroy)
|
||
|
||
def set_resolution_preset(self, width, height):
|
||
"""Set resolution to preset values with visual feedback"""
|
||
self.screen_w_var.set(width)
|
||
self.screen_h_var.set(height)
|
||
self.connection_status.configure(
|
||
text=f"✅ Resolution preset applied: {width}x{height}",
|
||
fg=self.colors['success']
|
||
)
|
||
|
||
# Reset status color after 3 seconds
|
||
self.window.after(3000, lambda: self.connection_status.configure(
|
||
fg=self.colors['text_secondary']
|
||
))
|
||
|
||
def load_config(self):
|
||
"""Load current configuration with enhanced feedback"""
|
||
try:
|
||
Logger.info("Loading configuration in enhanced settings window")
|
||
config = load_config()
|
||
Logger.info(f"Config loaded: {config}")
|
||
|
||
# Set values for connection settings
|
||
if hasattr(self, 'screen_name_var'):
|
||
self.screen_name_var.set(config.get('screen_name', ''))
|
||
if hasattr(self, 'server_ip_var'):
|
||
self.server_ip_var.set(config.get('server_ip', ''))
|
||
if hasattr(self, 'port_var'):
|
||
self.port_var.set(config.get('port', '8880'))
|
||
if hasattr(self, 'quickconnect_var'):
|
||
self.quickconnect_var.set(config.get('quickconnect_key', ''))
|
||
|
||
# Set values for display settings
|
||
if hasattr(self, 'screen_w_var'):
|
||
self.screen_w_var.set(config.get('screen_w', '1920'))
|
||
if hasattr(self, 'screen_h_var'):
|
||
self.screen_h_var.set(config.get('screen_h', '1080'))
|
||
if hasattr(self, 'scaling_mode_var'):
|
||
self.scaling_mode_var.set(config.get('scaling_mode', 'fit'))
|
||
|
||
# Set values for advanced settings
|
||
if hasattr(self, 'refresh_interval_var'):
|
||
self.refresh_interval_var.set(config.get('refresh_interval', '15'))
|
||
if hasattr(self, 'hardware_accel_var'):
|
||
self.hardware_accel_var.set(config.get('hardware_acceleration', True))
|
||
if hasattr(self, 'cache_media_var'):
|
||
self.cache_media_var.set(config.get('cache_media', True))
|
||
if hasattr(self, 'auto_retry_var'):
|
||
self.auto_retry_var.set(config.get('auto_retry', True))
|
||
|
||
# Update status with success message
|
||
if hasattr(self, 'connection_status'):
|
||
self.connection_status.configure(
|
||
text="✅ Configuration loaded successfully",
|
||
fg=self.colors['success']
|
||
)
|
||
# Reset status color after 3 seconds
|
||
self.window.after(3000, lambda: self.connection_status.configure(
|
||
fg=self.colors['text_secondary']
|
||
))
|
||
|
||
Logger.info("Configuration values loaded successfully in enhanced settings")
|
||
|
||
except Exception as e:
|
||
Logger.error(f"Failed to load config in enhanced settings: {e}")
|
||
|
||
# Set default values if loading fails
|
||
if hasattr(self, 'screen_name_var'):
|
||
self.screen_name_var.set('')
|
||
if hasattr(self, 'server_ip_var'):
|
||
self.server_ip_var.set('')
|
||
if hasattr(self, 'port_var'):
|
||
self.port_var.set('8880')
|
||
if hasattr(self, 'quickconnect_var'):
|
||
self.quickconnect_var.set('')
|
||
if hasattr(self, 'screen_w_var'):
|
||
self.screen_w_var.set('1920')
|
||
if hasattr(self, 'screen_h_var'):
|
||
self.screen_h_var.set('1080')
|
||
|
||
# Set advanced defaults
|
||
if hasattr(self, 'refresh_interval_var'):
|
||
self.refresh_interval_var.set('15')
|
||
if hasattr(self, 'hardware_accel_var'):
|
||
self.hardware_accel_var.set(True)
|
||
if hasattr(self, 'cache_media_var'):
|
||
self.cache_media_var.set(True)
|
||
if hasattr(self, 'auto_retry_var'):
|
||
self.auto_retry_var.set(True)
|
||
|
||
# Show error message with enhanced styling
|
||
if hasattr(self, 'connection_status'):
|
||
self.connection_status.configure(
|
||
text=f"⚠️ Warning: Could not load existing config: {str(e)[:50]}...",
|
||
fg=self.colors['warning']
|
||
)
|
||
|
||
def save_config(self):
|
||
"""Save configuration with enhanced feedback"""
|
||
try:
|
||
# Load existing config or create new one
|
||
try:
|
||
config = load_config()
|
||
except:
|
||
config = {}
|
||
|
||
# Update with new values from the enhanced interface
|
||
if hasattr(self, 'screen_name_var'):
|
||
config['screen_name'] = self.screen_name_var.get()
|
||
if hasattr(self, 'server_ip_var'):
|
||
config['server_ip'] = self.server_ip_var.get()
|
||
if hasattr(self, 'port_var'):
|
||
config['port'] = self.port_var.get()
|
||
if hasattr(self, 'quickconnect_var'):
|
||
config['quickconnect_key'] = self.quickconnect_var.get()
|
||
if hasattr(self, 'screen_w_var'):
|
||
config['screen_w'] = self.screen_w_var.get()
|
||
if hasattr(self, 'screen_h_var'):
|
||
config['screen_h'] = self.screen_h_var.get()
|
||
|
||
# Save advanced settings if they exist
|
||
if hasattr(self, 'refresh_interval_var'):
|
||
config['refresh_interval'] = self.refresh_interval_var.get()
|
||
if hasattr(self, 'hardware_accel_var'):
|
||
config['hardware_acceleration'] = self.hardware_accel_var.get()
|
||
if hasattr(self, 'cache_media_var'):
|
||
config['cache_media'] = self.cache_media_var.get()
|
||
if hasattr(self, 'auto_retry_var'):
|
||
config['auto_retry'] = self.auto_retry_var.get()
|
||
|
||
# Save display settings
|
||
if hasattr(self, 'scaling_mode_var'):
|
||
config['scaling_mode'] = self.scaling_mode_var.get()
|
||
# Also update the main app's scaling mode
|
||
if hasattr(self.app, 'scaling_mode'):
|
||
self.app.scaling_mode = self.scaling_mode_var.get()
|
||
|
||
# Ensure directory exists
|
||
config_dir = os.path.dirname(CONFIG_FILE)
|
||
os.makedirs(config_dir, exist_ok=True)
|
||
|
||
with open(CONFIG_FILE, 'w') as f:
|
||
json.dump(config, f, indent=4)
|
||
|
||
Logger.info(f"Enhanced configuration saved to {CONFIG_FILE}")
|
||
|
||
# Show enhanced success message
|
||
self.show_enhanced_success_message("Configuration saved successfully!")
|
||
|
||
# Ask if user wants to refresh playlist now
|
||
if messagebox.askyesno("Refresh Playlist",
|
||
"Configuration saved! Would you like to refresh the playlist from server now?",
|
||
icon='question'):
|
||
self.force_playlist_refresh()
|
||
|
||
self.window.destroy()
|
||
|
||
except Exception as e:
|
||
Logger.error(f"Failed to save enhanced configuration: {e}")
|
||
self.show_enhanced_error_message(f"Failed to save configuration: {e}")
|
||
|
||
def show_enhanced_success_message(self, message):
|
||
"""Show an enhanced success message with dark theme"""
|
||
success_window = tk.Toplevel(self.window)
|
||
success_window.title("Success")
|
||
success_window.geometry("400x150")
|
||
success_window.configure(bg=self.colors['bg_primary'])
|
||
success_window.transient(self.window)
|
||
success_window.grab_set()
|
||
success_window.resizable(False, False)
|
||
|
||
# Center the window using helper method
|
||
self.center_window_on_screen(success_window, 400, 150)
|
||
|
||
# Main frame
|
||
main_frame = tk.Frame(success_window, bg=self.colors['bg_primary'], padx=30, pady=20)
|
||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# Success icon
|
||
icon_label = tk.Label(main_frame, text="✅", font=('Segoe UI Emoji', 32),
|
||
fg=self.colors['success'], bg=self.colors['bg_primary'])
|
||
icon_label.pack(pady=(0, 10))
|
||
|
||
# Success message
|
||
msg_label = tk.Label(main_frame, text=message, font=('Segoe UI', 12, 'bold'),
|
||
fg=self.colors['text_primary'], bg=self.colors['bg_primary'],
|
||
wraplength=340, justify=tk.CENTER)
|
||
msg_label.pack(pady=(0, 20))
|
||
|
||
# OK button
|
||
ok_btn = self.create_action_button(main_frame, "OK", success_window.destroy,
|
||
self.colors['success'])
|
||
ok_btn.pack()
|
||
|
||
# Auto-close after 3 seconds
|
||
success_window.after(3000, success_window.destroy)
|
||
|
||
def show_enhanced_error_message(self, message):
|
||
"""Show an enhanced error message with dark theme"""
|
||
error_window = tk.Toplevel(self.window)
|
||
error_window.title("Error")
|
||
error_window.geometry("400x150")
|
||
error_window.configure(bg=self.colors['bg_primary'])
|
||
error_window.transient(self.window)
|
||
error_window.grab_set()
|
||
error_window.resizable(False, False)
|
||
|
||
# Center the window using helper method
|
||
self.center_window_on_screen(error_window, 400, 150)
|
||
|
||
# Main frame
|
||
main_frame = tk.Frame(error_window, bg=self.colors['bg_primary'], padx=30, pady=20)
|
||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# Error icon
|
||
icon_label = tk.Label(main_frame, text="❌", font=('Segoe UI Emoji', 32),
|
||
fg=self.colors['danger'], bg=self.colors['bg_primary'])
|
||
icon_label.pack(pady=(0, 10))
|
||
|
||
# Error message
|
||
msg_label = tk.Label(main_frame, text=message, font=('Segoe UI', 11),
|
||
fg=self.colors['text_primary'], bg=self.colors['bg_primary'],
|
||
wraplength=340, justify=tk.CENTER)
|
||
msg_label.pack(pady=(0, 20))
|
||
|
||
# OK button
|
||
ok_btn = self.create_action_button(main_frame, "OK", error_window.destroy,
|
||
self.colors['danger'])
|
||
ok_btn.pack()
|
||
|
||
def show_success_message(self, message):
|
||
"""Show a modern success message"""
|
||
success_window = tk.Toplevel(self.window)
|
||
success_window.title("Success")
|
||
success_window.geometry("300x120")
|
||
success_window.configure(bg='#2d5a2d')
|
||
success_window.transient(self.window)
|
||
success_window.grab_set()
|
||
|
||
# Center the window using helper method
|
||
self.center_window_on_screen(success_window, 300, 120)
|
||
|
||
# Success icon and message
|
||
icon_label = tk.Label(success_window, text="✓", font=('Arial', 24, 'bold'),
|
||
fg='white', bg='#2d5a2d')
|
||
icon_label.pack(pady=10)
|
||
|
||
msg_label = tk.Label(success_window, text=message, font=('Arial', 10),
|
||
fg='white', bg='#2d5a2d', wraplength=250)
|
||
msg_label.pack(pady=5)
|
||
|
||
ok_btn = tk.Button(success_window, text="OK", command=success_window.destroy,
|
||
bg='#4d7d4d', fg='white', font=('Arial', 10, 'bold'),
|
||
relief=tk.FLAT, padx=20, pady=5)
|
||
ok_btn.pack(pady=10)
|
||
|
||
def force_playlist_refresh(self):
|
||
"""Force refresh of playlist from server"""
|
||
try:
|
||
# Show connection message
|
||
self.connection_status.configure(text="Refreshing playlist from server...")
|
||
self.window.update()
|
||
|
||
# Fetch 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:
|
||
# Clean old files
|
||
local_playlist_data = load_local_playlist()
|
||
clean_unused_files(local_playlist_data.get('playlist', []))
|
||
|
||
# Download new content
|
||
download_media_files(server_playlist, server_version)
|
||
update_config_playlist_version(server_version)
|
||
|
||
# Let the app know to reload
|
||
self.app.playlist = load_local_playlist().get('playlist', [])
|
||
self.app.current_index = 0 # Reset to beginning
|
||
|
||
self.connection_status.configure(text=f"Playlist refreshed! Downloaded {len(server_playlist)} media files.")
|
||
|
||
# Force the app to play the current media
|
||
if self.app.playlist:
|
||
self.app.play_current_media()
|
||
else:
|
||
self.connection_status.configure(text="Server returned empty playlist. Check server connection settings.")
|
||
|
||
except Exception as e:
|
||
self.connection_status.configure(text=f"Error refreshing playlist: {str(e)[:50]}...")
|
||
Logger.error(f"Failed to refresh playlist: {e}")
|
||
|
||
def test_connection(self):
|
||
"""Test server connection"""
|
||
try:
|
||
self.connection_status.configure(text="Testing connection...")
|
||
self.window.update()
|
||
|
||
server_ip = self.server_ip_var.get()
|
||
port = self.port_var.get()
|
||
screen_name = self.screen_name_var.get()
|
||
quickconnect = self.quickconnect_var.get()
|
||
|
||
if not all([server_ip, port, screen_name, quickconnect]):
|
||
self.connection_status.configure(text="Please fill all connection fields")
|
||
return
|
||
|
||
url = f"http://{server_ip}:{port}/api/playlists"
|
||
params = {
|
||
'hostname': screen_name,
|
||
'quickconnect_code': quickconnect
|
||
}
|
||
|
||
response = requests.get(url, params=params, timeout=10)
|
||
|
||
if response.status_code == 200:
|
||
data = response.json()
|
||
version = data.get('playlist_version', 'Unknown')
|
||
num_items = len(data.get('playlist', []))
|
||
self.connection_status.configure(
|
||
text=f"✓ Connected! Server playlist version: {version}, {num_items} media items available"
|
||
)
|
||
else:
|
||
self.connection_status.configure(
|
||
text=f"✗ Connection failed (Status: {response.status_code})"
|
||
)
|
||
|
||
except Exception as e:
|
||
self.connection_status.configure(text=f"✗ Connection error: {str(e)[:50]}...")
|
||
|
||
def load_logs(self):
|
||
"""Load recent log entries"""
|
||
try:
|
||
# Update to use the resources directory
|
||
log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'log.txt')
|
||
|
||
if os.path.exists(log_file):
|
||
with open(log_file, 'r') as f:
|
||
lines = f.readlines()
|
||
|
||
# Show last 20 lines
|
||
recent_lines = lines[-20:] if len(lines) > 20 else lines
|
||
|
||
self.log_text.delete(1.0, tk.END)
|
||
self.log_text.insert(tk.END, ''.join(recent_lines))
|
||
self.log_text.see(tk.END)
|
||
else:
|
||
self.log_text.delete(1.0, tk.END)
|
||
self.log_text.insert(tk.END, "No log file found")
|
||
|
||
except Exception as e:
|
||
self.log_text.delete(1.0, tk.END)
|
||
self.log_text.insert(tk.END, f"Error loading logs: {e}")
|
||
|
||
def load_playlist_view(self):
|
||
"""Load playlist items into treeview"""
|
||
try:
|
||
# Clear existing items
|
||
for item in self.playlist_view.get_children():
|
||
self.playlist_view.delete(item)
|
||
except Exception as e:
|
||
Logger.error(f"Failed to load playlist view: {e}")
|
||
messagebox.showerror("Error", f"Failed to load playlist: {e}")
|
||
|
||
def show_video_placeholder(self, filename, duration):
|
||
"""Show placeholder for video files"""
|
||
self.image_label.config(image='')
|
||
self.status_label.config(text="") # Clear any status text for cleaner display
|
||
|
||
# Schedule next media
|
||
self.auto_advance_timer = self.root.after(
|
||
int(duration * 1000),
|
||
self.next_media
|
||
)
|