deleted old app
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"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": 30
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 537 KiB |
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"playlist": [
|
|
||||||
{
|
|
||||||
"file_name": "demo1.jpg",
|
|
||||||
"url": "Resurse/demo1.jpg",
|
|
||||||
"duration": 20
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file_name": "demo2.jpg",
|
|
||||||
"url": "Resurse/demo2.jpg",
|
|
||||||
"duration": 20
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"version": 1
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"playlist": [
|
|
||||||
{
|
|
||||||
"file_name": "Cindrel_1.jpg",
|
|
||||||
"url": "static/resurse/Cindrel_1.jpg",
|
|
||||||
"duration": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file_name": "trans_cindrel_4.jpg",
|
|
||||||
"url": "static/resurse/trans_cindrel_4.jpg",
|
|
||||||
"duration": 10
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"version": 30
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,27 +0,0 @@
|
|||||||
import logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Path to the log file
|
|
||||||
# Update the path to point to the new resources directory
|
|
||||||
LOG_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'log.txt')
|
|
||||||
|
|
||||||
# Create a logger instance
|
|
||||||
Logger = logging.getLogger('SignageApp')
|
|
||||||
Logger.setLevel(logging.INFO) # Set the logging level to INFO
|
|
||||||
|
|
||||||
# Create a file handler to write logs to the log.txt file
|
|
||||||
file_handler = logging.FileHandler(LOG_FILE_PATH, mode='a') # Append logs to the file
|
|
||||||
file_handler.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
# Create a formatter for the log messages
|
|
||||||
formatter = logging.Formatter('[%(levelname)s] [%(name)s] %(message)s')
|
|
||||||
file_handler.setFormatter(formatter)
|
|
||||||
|
|
||||||
# Add the file handler to the logger
|
|
||||||
Logger.addHandler(file_handler)
|
|
||||||
|
|
||||||
# Optionally, add a stream handler to log messages to the console
|
|
||||||
stream_handler = logging.StreamHandler()
|
|
||||||
stream_handler.setLevel(logging.INFO)
|
|
||||||
stream_handler.setFormatter(formatter)
|
|
||||||
Logger.addHandler(stream_handler)
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
#!/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
|
|
||||||
|
|
||||||
# Add the current directory to the path so we can import our modules
|
|
||||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
|
|
||||||
|
|
||||||
from logging_config import Logger
|
|
||||||
from player_ui import PlayerUI
|
|
||||||
from media_playback_controller import MediaPlaybackController
|
|
||||||
from splash_screen import SplashScreen
|
|
||||||
from playlist_manager import PlaylistManager
|
|
||||||
from threading import Thread
|
|
||||||
from settings_screen import SettingsWindow
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import tkinter as tk
|
|
||||||
root = tk.Tk()
|
|
||||||
playlist_manager = PlaylistManager()
|
|
||||||
playlist_manager.fetch_playlist() # Start fetching playlist in parallel with splash
|
|
||||||
intro_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'intro1.mp4')
|
|
||||||
preload_result = {}
|
|
||||||
def preload_first_media(playlist):
|
|
||||||
if playlist and len(playlist) > 0:
|
|
||||||
first_item = playlist[0]
|
|
||||||
file_path = first_item.get('url', '')
|
|
||||||
if file_path and os.path.exists(file_path):
|
|
||||||
try:
|
|
||||||
from PIL import Image
|
|
||||||
if file_path.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
|
|
||||||
img = Image.open(file_path)
|
|
||||||
img.load() # Force decode
|
|
||||||
preload_result['image'] = img
|
|
||||||
except Exception as e:
|
|
||||||
Logger.warning(f"[MAIN] Preload failed: {e}")
|
|
||||||
Logger.info(f"[MAIN] About to show splash screen: {intro_path}")
|
|
||||||
def start_player():
|
|
||||||
Logger.info("[MAIN] Splash finished, waiting for playlist...")
|
|
||||||
playlist = playlist_manager.wait_for_playlist()
|
|
||||||
Logger.info(f"[MAIN] Playlist loaded: {playlist}")
|
|
||||||
Thread(target=preload_first_media, args=(playlist,), daemon=True).start()
|
|
||||||
# Create playback first so we can reference its methods in callbacks
|
|
||||||
playback = MediaPlaybackController(app=None, ui=None) # UI will be set after
|
|
||||||
def open_settings():
|
|
||||||
SettingsWindow(root, None)
|
|
||||||
def show_exit():
|
|
||||||
root.quit()
|
|
||||||
control_callbacks = {
|
|
||||||
'prev': playback.previous_media,
|
|
||||||
'play_pause': playback.toggle_play_pause,
|
|
||||||
'next': playback.next_media,
|
|
||||||
'settings': open_settings,
|
|
||||||
'exit': show_exit
|
|
||||||
}
|
|
||||||
ui = PlayerUI(root, control_callbacks=control_callbacks)
|
|
||||||
ui.setup_window() # Ensure fullscreen and geometry
|
|
||||||
ui.bind_show_controls_on_activity() # Bind activity to show controls
|
|
||||||
playback.ui = ui # Set UI reference now that it's created
|
|
||||||
playback.set_playlist(playlist or [])
|
|
||||||
playback.play_current_media()
|
|
||||||
Logger.info("[MAIN] Player UI and playback started.")
|
|
||||||
splash = SplashScreen(root, intro_path, duration=10)
|
|
||||||
splash.show(on_finish=start_player)
|
|
||||||
Logger.info("[MAIN] splash.show() called, entering mainloop...")
|
|
||||||
root.mainloop()
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
import threading
|
|
||||||
import os
|
|
||||||
from logging_config import Logger
|
|
||||||
from PIL import Image, ImageTk
|
|
||||||
|
|
||||||
class MediaPlaybackController:
|
|
||||||
def __init__(self, app, ui):
|
|
||||||
self.app = app # Reference to SimpleMediaPlayerApp
|
|
||||||
self.ui = ui # Reference to PlayerUI
|
|
||||||
self.auto_advance_timer = None
|
|
||||||
self.current_index = 0
|
|
||||||
self.playlist = []
|
|
||||||
self.scaling_mode = 'fit'
|
|
||||||
|
|
||||||
def set_playlist(self, playlist):
|
|
||||||
self.playlist = playlist
|
|
||||||
self.current_index = 0
|
|
||||||
|
|
||||||
def play_current_media(self):
|
|
||||||
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)
|
|
||||||
if file_path.startswith('static/resurse/'):
|
|
||||||
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}")
|
|
||||||
self.log_event(file_name, "STARTED")
|
|
||||||
self.cancel_timers()
|
|
||||||
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.ui.status_label.config(text=f"Missing or unsupported media:\n{file_name}")
|
|
||||||
self.auto_advance_timer = self.ui.root.after(5000, self.next_media)
|
|
||||||
|
|
||||||
def play_video(self, file_path):
|
|
||||||
self.ui.status_label.place_forget()
|
|
||||||
def run_vlc_subprocess():
|
|
||||||
import subprocess
|
|
||||||
try:
|
|
||||||
Logger.info(f"Starting system VLC subprocess for video: {file_path}")
|
|
||||||
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.ui.root.after_idle(lambda: setattr(self, 'auto_advance_timer', self.ui.root.after(1000, self.next_media)))
|
|
||||||
threading.Thread(target=run_vlc_subprocess, daemon=True).start()
|
|
||||||
|
|
||||||
def show_image(self, file_path, duration):
|
|
||||||
try:
|
|
||||||
self.ui.status_label.place_forget()
|
|
||||||
self.ui.status_label.config(text="")
|
|
||||||
img = Image.open(file_path)
|
|
||||||
original_size = img.size
|
|
||||||
screen_width = self.ui.root.winfo_width()
|
|
||||||
screen_height = self.ui.root.winfo_height()
|
|
||||||
if screen_width <= 1 or screen_height <= 1:
|
|
||||||
screen_width = 1920
|
|
||||||
screen_height = 1080
|
|
||||||
final_img, offset = self.ui.scale_image_to_screen(img, screen_width, screen_height, self.scaling_mode)
|
|
||||||
photo = ImageTk.PhotoImage(final_img)
|
|
||||||
self.ui.image_label.config(image=photo)
|
|
||||||
self.ui.image_label.image = photo
|
|
||||||
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})")
|
|
||||||
self.auto_advance_timer = self.ui.root.after(
|
|
||||||
int(duration * 1000),
|
|
||||||
self.next_media
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
Logger.error(f"Failed to show image {file_path}: {e}")
|
|
||||||
self.ui.image_label.config(image='')
|
|
||||||
self.ui.status_label.config(text=f"Image Error:\n{os.path.basename(file_path)}\n{str(e)}")
|
|
||||||
self.ui.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
|
||||||
self.auto_advance_timer = self.ui.root.after(5000, self.next_media)
|
|
||||||
|
|
||||||
def show_text_content(self, text, duration):
|
|
||||||
self.ui.image_label.config(image='')
|
|
||||||
self.ui.status_label.config(text=text)
|
|
||||||
self.auto_advance_timer = self.ui.root.after(
|
|
||||||
int(duration * 1000),
|
|
||||||
self.next_media
|
|
||||||
)
|
|
||||||
|
|
||||||
def next_media(self):
|
|
||||||
self.cancel_timers()
|
|
||||||
if not self.playlist:
|
|
||||||
return
|
|
||||||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
|
||||||
self.play_current_media()
|
|
||||||
|
|
||||||
def previous_media(self):
|
|
||||||
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):
|
|
||||||
self.app.is_paused = not self.app.is_paused
|
|
||||||
if self.app.is_paused:
|
|
||||||
self.ui.play_pause_btn.config(text="▶ Play")
|
|
||||||
self.cancel_timers()
|
|
||||||
else:
|
|
||||||
self.ui.play_pause_btn.config(text="⏸ Pause")
|
|
||||||
self.play_current_media()
|
|
||||||
Logger.info(f"Media {'paused' if self.app.is_paused else 'resumed'}")
|
|
||||||
|
|
||||||
def cancel_timers(self):
|
|
||||||
if self.auto_advance_timer:
|
|
||||||
self.ui.root.after_cancel(self.auto_advance_timer)
|
|
||||||
self.auto_advance_timer = None
|
|
||||||
|
|
||||||
def show_no_content_message(self):
|
|
||||||
self.ui.status_label.config(text="No content available.")
|
|
||||||
|
|
||||||
def log_event(self, file_name, event):
|
|
||||||
import datetime
|
|
||||||
try:
|
|
||||||
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
log_message = f"{timestamp} - {event}: {file_name}\n"
|
|
||||||
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}")
|
|
||||||
@@ -1,407 +0,0 @@
|
|||||||
# player_app.py
|
|
||||||
# Main player application logic moved from tkinter_simple_player.py
|
|
||||||
|
|
||||||
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
|
|
||||||
from media_playback_controller import MediaPlaybackController
|
|
||||||
try:
|
|
||||||
import vlc # For video playback with hardware acceleration
|
|
||||||
except Exception as e:
|
|
||||||
vlc = None
|
|
||||||
print(f"WARNING: VLC not available: {e}. Video playback may be limited.")
|
|
||||||
|
|
||||||
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.")
|
|
||||||
|
|
||||||
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
|
|
||||||
from virtual_keyboard import VirtualKeyboard, TouchOptimizedEntry, TouchOptimizedButton
|
|
||||||
from settings_screen import SettingsWindow
|
|
||||||
from splash_screen import SplashScreen
|
|
||||||
from player_ui import PlayerUI
|
|
||||||
|
|
||||||
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'app_config.txt')
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleMediaPlayerApp:
|
|
||||||
def __init__(self, root):
|
|
||||||
self.root = root
|
|
||||||
self.running = True
|
|
||||||
self.is_paused = False
|
|
||||||
self.is_fullscreen = True
|
|
||||||
self.playlist = []
|
|
||||||
self.current_index = 0
|
|
||||||
self.scaling_mode = 'fit'
|
|
||||||
self.auto_advance_timer = None
|
|
||||||
self.hide_controls_timer = None
|
|
||||||
self.settings_window = None
|
|
||||||
# UI extraction
|
|
||||||
self.ui = PlayerUI(root, control_callbacks={
|
|
||||||
'prev': self.previous_media,
|
|
||||||
'play_pause': self.toggle_play_pause,
|
|
||||||
'next': self.next_media,
|
|
||||||
'settings': self.open_settings,
|
|
||||||
'exit': self.show_exit_dialog
|
|
||||||
})
|
|
||||||
self.content_frame = self.ui.content_frame
|
|
||||||
self.image_label = self.ui.image_label
|
|
||||||
self.status_label = self.ui.status_label
|
|
||||||
self.play_pause_btn = self.ui.play_pause_btn
|
|
||||||
self.prev_btn = self.ui.prev_btn
|
|
||||||
self.next_btn = self.ui.next_btn
|
|
||||||
self.exit_btn = self.ui.exit_btn
|
|
||||||
self.settings_btn = self.ui.settings_btn
|
|
||||||
self.playback = MediaPlaybackController(self, self.ui)
|
|
||||||
self.setup_window()
|
|
||||||
self.start_player_logic()
|
|
||||||
|
|
||||||
def start_player_logic(self):
|
|
||||||
# Start playlist/server thread
|
|
||||||
self._playlist_ready = threading.Event()
|
|
||||||
self._playlist_data = None
|
|
||||||
def fetch_playlist_logic():
|
|
||||||
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}")
|
|
||||||
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}")
|
|
||||||
playlist = None
|
|
||||||
# Try to fetch from server
|
|
||||||
if server and host and quick and port:
|
|
||||||
try:
|
|
||||||
Logger.info("Attempting to connect to server...")
|
|
||||||
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}")
|
|
||||||
download_media_files(server_playlist, server_version)
|
|
||||||
update_config_playlist_version(server_version)
|
|
||||||
local_playlist_data = load_local_playlist()
|
|
||||||
playlist = local_playlist_data.get('playlist', [])
|
|
||||||
if playlist:
|
|
||||||
Logger.info(f"Successfully loaded {len(playlist)} items from server")
|
|
||||||
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 Exception as e:
|
|
||||||
Logger.error(f"Failed to fetch playlist from server: {e}, using fallback playlist")
|
|
||||||
if not playlist:
|
|
||||||
playlist = fallback_playlist
|
|
||||||
if not playlist:
|
|
||||||
playlist = None
|
|
||||||
self._playlist_data = playlist
|
|
||||||
self._playlist_ready.set()
|
|
||||||
threading.Thread(target=fetch_playlist_logic, daemon=True).start()
|
|
||||||
self._playlist_ready.wait()
|
|
||||||
playlist = self._playlist_data
|
|
||||||
if playlist and len(playlist) > 0:
|
|
||||||
self.playlist = playlist
|
|
||||||
Logger.info(f"Loaded playlist with {len(self.playlist)} items")
|
|
||||||
self.play_current_media()
|
|
||||||
else:
|
|
||||||
Logger.warning("No playlist available, loading demo content")
|
|
||||||
self.load_demo_or_local_playlist()
|
|
||||||
self.start_periodic_checks()
|
|
||||||
|
|
||||||
def setup_window(self):
|
|
||||||
self.root.title("Simple Signage Player")
|
|
||||||
self.root.configure(bg='black')
|
|
||||||
try:
|
|
||||||
config = load_config()
|
|
||||||
width = int(config.get('screen_w', 1920))
|
|
||||||
height = int(config.get('screen_h', 1080))
|
|
||||||
self.scaling_mode = config.get('scaling_mode', 'fit')
|
|
||||||
except:
|
|
||||||
width, height = 800, 600
|
|
||||||
self.scaling_mode = 'fit'
|
|
||||||
self.root.geometry(f"{width}x{height}")
|
|
||||||
self.root.attributes('-fullscreen', True)
|
|
||||||
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 play_current_media(self):
|
|
||||||
self.playback.play_current_media()
|
|
||||||
|
|
||||||
def play_video(self, file_path):
|
|
||||||
self.playback.play_video(file_path)
|
|
||||||
|
|
||||||
def show_image(self, file_path, duration):
|
|
||||||
self.playback.show_image(file_path, duration)
|
|
||||||
|
|
||||||
def show_text_content(self, text, duration):
|
|
||||||
self.playback.show_text_content(text, duration)
|
|
||||||
|
|
||||||
def next_media(self):
|
|
||||||
self.playback.next_media()
|
|
||||||
|
|
||||||
def previous_media(self):
|
|
||||||
self.playback.previous_media()
|
|
||||||
|
|
||||||
def toggle_play_pause(self):
|
|
||||||
self.playback.toggle_play_pause()
|
|
||||||
|
|
||||||
def cancel_timers(self):
|
|
||||||
self.playback.cancel_timers()
|
|
||||||
|
|
||||||
def show_no_content_message(self):
|
|
||||||
self.playback.show_no_content_message()
|
|
||||||
|
|
||||||
def log_event(self, file_name, event):
|
|
||||||
self.playback.log_event(file_name, event)
|
|
||||||
|
|
||||||
def show_controls(self):
|
|
||||||
if self.ui.control_frame:
|
|
||||||
self.ui.control_frame.place(relx=0.98, rely=0.98, anchor='se')
|
|
||||||
|
|
||||||
def hide_controls(self):
|
|
||||||
if self.ui.control_frame:
|
|
||||||
self.ui.control_frame.place_forget()
|
|
||||||
|
|
||||||
def schedule_hide_controls(self):
|
|
||||||
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):
|
|
||||||
self.show_controls()
|
|
||||||
self.schedule_hide_controls()
|
|
||||||
|
|
||||||
def on_mouse_motion(self, event):
|
|
||||||
self.show_controls()
|
|
||||||
self.schedule_hide_controls()
|
|
||||||
|
|
||||||
def on_key_press(self, event):
|
|
||||||
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:
|
|
||||||
if key == 's':
|
|
||||||
self.open_settings()
|
|
||||||
self.show_controls()
|
|
||||||
self.schedule_hide_controls()
|
|
||||||
|
|
||||||
def set_scaling_mode(self, mode):
|
|
||||||
old_mode = self.scaling_mode
|
|
||||||
self.scaling_mode = mode
|
|
||||||
Logger.info(f"Scaling mode changed from '{old_mode}' to '{mode}'")
|
|
||||||
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')
|
|
||||||
self.root.after(2000, lambda: self.status_label.place_forget())
|
|
||||||
if self.playlist and 0 <= self.current_index < len(self.playlist):
|
|
||||||
self.cancel_timers()
|
|
||||||
self.play_current_media()
|
|
||||||
|
|
||||||
def toggle_fullscreen(self):
|
|
||||||
self.is_fullscreen = not self.is_fullscreen
|
|
||||||
self.root.attributes('-fullscreen', self.is_fullscreen)
|
|
||||||
|
|
||||||
def open_settings(self):
|
|
||||||
if hasattr(self, 'settings_window') and self.settings_window and self.settings_window.winfo_exists():
|
|
||||||
self.settings_window.lift()
|
|
||||||
return
|
|
||||||
if not self.is_paused:
|
|
||||||
self.toggle_play_pause()
|
|
||||||
self.settings_window = SettingsWindow(self.root, self)
|
|
||||||
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):
|
|
||||||
try:
|
|
||||||
config = load_config()
|
|
||||||
quickconnect_key = config.get('quickconnect_key', '')
|
|
||||||
except:
|
|
||||||
quickconnect_key = ''
|
|
||||||
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)
|
|
||||||
self.center_dialog_on_screen(exit_dialog, 400, 200)
|
|
||||||
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 = tk.Frame(exit_dialog, bg='#2d2d2d', padx=20, pady=20)
|
|
||||||
content_frame.pack(fill=tk.BOTH, expand=True)
|
|
||||||
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_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 = 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():
|
|
||||||
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 = tk.Label(content_frame, text="", font=('Arial', 9),
|
|
||||||
bg='#2d2d2d')
|
|
||||||
error_label.pack()
|
|
||||||
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)
|
|
||||||
password_entry.bind('<Return>', lambda e: check_password())
|
|
||||||
exit_dialog.bind('<Escape>', lambda e: cancel_exit())
|
|
||||||
|
|
||||||
def exit_application(self):
|
|
||||||
Logger.info("Application exit requested")
|
|
||||||
self.running = False
|
|
||||||
self.root.quit()
|
|
||||||
self.root.destroy()
|
|
||||||
|
|
||||||
def center_dialog_on_screen(self, dialog, width, height):
|
|
||||||
dialog.update_idletasks()
|
|
||||||
screen_width = dialog.winfo_screenwidth()
|
|
||||||
screen_height = dialog.winfo_screenheight()
|
|
||||||
center_x = int((screen_width - width) / 2)
|
|
||||||
center_y = int((screen_height - height) / 2)
|
|
||||||
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}")
|
|
||||||
dialog.lift()
|
|
||||||
dialog.focus_force()
|
|
||||||
return center_x, center_y
|
|
||||||
|
|
||||||
def check_playlist_updates(self):
|
|
||||||
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}")
|
|
||||||
local_playlist_data = load_local_playlist()
|
|
||||||
clean_unused_files(local_playlist_data.get('playlist', []))
|
|
||||||
download_media_files(
|
|
||||||
server_playlist_data.get('playlist', []),
|
|
||||||
server_version
|
|
||||||
)
|
|
||||||
local_playlist_data = load_local_playlist()
|
|
||||||
self.playlist = local_playlist_data.get('playlist', [])
|
|
||||||
self.current_index = 0
|
|
||||||
Logger.info("Playlist updated successfully")
|
|
||||||
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):
|
|
||||||
try:
|
|
||||||
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
log_message = f"{timestamp} - {event}: {file_name}\n"
|
|
||||||
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):
|
|
||||||
def check_loop():
|
|
||||||
while self.running:
|
|
||||||
try:
|
|
||||||
time.sleep(300)
|
|
||||||
if self.running:
|
|
||||||
self.check_playlist_updates()
|
|
||||||
except Exception as e:
|
|
||||||
Logger.error(f"Error in periodic check: {e}")
|
|
||||||
threading.Thread(target=check_loop, daemon=True).start()
|
|
||||||
|
|
||||||
def next_media(self):
|
|
||||||
self.cancel_timers()
|
|
||||||
if not self.playlist:
|
|
||||||
return
|
|
||||||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
|
||||||
# At end of playlist, check for updates
|
|
||||||
if self.current_index == 0:
|
|
||||||
self.check_playlist_updates()
|
|
||||||
self.play_current_media()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
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}")
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,833 +0,0 @@
|
|||||||
# player_app.py
|
|
||||||
# Main player application logic moved from tkinter_simple_player.py
|
|
||||||
|
|
||||||
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:
|
|
||||||
from PIL import Image, ImageTk
|
|
||||||
PIL_AVAILABLE = True
|
|
||||||
except ImportError:
|
|
||||||
PIL_AVAILABLE = False
|
|
||||||
print("WARNING: PIL not available. Image display functionality will be limited.")
|
|
||||||
|
|
||||||
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
|
|
||||||
from virtual_keyboard import VirtualKeyboard, TouchOptimizedEntry, TouchOptimizedButton
|
|
||||||
from settings_screen import SettingsWindow
|
|
||||||
|
|
||||||
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'app_config.txt')
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleMediaPlayerApp:
|
|
||||||
def __init__(self, root):
|
|
||||||
self.root = root
|
|
||||||
self.running = True
|
|
||||||
self.is_paused = False
|
|
||||||
self.is_fullscreen = True
|
|
||||||
self.playlist = []
|
|
||||||
self.current_index = 0
|
|
||||||
self.scaling_mode = 'fit'
|
|
||||||
self.auto_advance_timer = None
|
|
||||||
self.hide_controls_timer = None
|
|
||||||
self.control_frame = None
|
|
||||||
self.settings_window = None
|
|
||||||
self.content_frame = None
|
|
||||||
self.image_label = None
|
|
||||||
self.status_label = None
|
|
||||||
self.play_pause_btn = None
|
|
||||||
self.prev_btn = None
|
|
||||||
self.next_btn = None
|
|
||||||
self.exit_btn = None
|
|
||||||
self.settings_btn = None
|
|
||||||
self.setup_window()
|
|
||||||
self.play_splash_then_start()
|
|
||||||
|
|
||||||
def play_splash_then_start(self):
|
|
||||||
# Start playlist/server thread
|
|
||||||
self._playlist_ready = threading.Event()
|
|
||||||
self._playlist_data = None
|
|
||||||
def fetch_playlist_logic():
|
|
||||||
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}")
|
|
||||||
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}")
|
|
||||||
playlist = None
|
|
||||||
# Try to fetch from server
|
|
||||||
if server and host and quick and port:
|
|
||||||
try:
|
|
||||||
Logger.info("Attempting to connect to server...")
|
|
||||||
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}")
|
|
||||||
download_media_files(server_playlist, server_version)
|
|
||||||
update_config_playlist_version(server_version)
|
|
||||||
local_playlist_data = load_local_playlist()
|
|
||||||
playlist = local_playlist_data.get('playlist', [])
|
|
||||||
if playlist:
|
|
||||||
Logger.info(f"Successfully loaded {len(playlist)} items from server")
|
|
||||||
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 Exception as e:
|
|
||||||
Logger.error(f"Failed to fetch playlist from server: {e}, using fallback playlist")
|
|
||||||
if not playlist:
|
|
||||||
playlist = fallback_playlist
|
|
||||||
if not playlist:
|
|
||||||
playlist = None
|
|
||||||
self._playlist_data = playlist
|
|
||||||
self._playlist_ready.set()
|
|
||||||
threading.Thread(target=fetch_playlist_logic, daemon=True).start()
|
|
||||||
# Only play intro on first playlist loop
|
|
||||||
self._first_playlist_loop = True
|
|
||||||
self.setup_ui()
|
|
||||||
self._playlist_ready.wait()
|
|
||||||
playlist = self._playlist_data
|
|
||||||
if playlist and len(playlist) > 0:
|
|
||||||
self.playlist = playlist
|
|
||||||
Logger.info(f"Loaded playlist with {len(self.playlist)} items")
|
|
||||||
self.play_current_media()
|
|
||||||
else:
|
|
||||||
Logger.warning("No playlist available, loading demo content")
|
|
||||||
self.load_demo_or_local_playlist()
|
|
||||||
self.start_periodic_checks()
|
|
||||||
|
|
||||||
# _after_splash is now handled inline in play_splash_then_start
|
|
||||||
|
|
||||||
|
|
||||||
def setup_window(self):
|
|
||||||
self.root.title("Simple Signage Player")
|
|
||||||
self.root.configure(bg='black')
|
|
||||||
try:
|
|
||||||
config = load_config()
|
|
||||||
width = int(config.get('screen_w', 1920))
|
|
||||||
height = int(config.get('screen_h', 1080))
|
|
||||||
self.scaling_mode = config.get('scaling_mode', 'fit')
|
|
||||||
except:
|
|
||||||
width, height = 800, 600
|
|
||||||
self.scaling_mode = 'fit'
|
|
||||||
self.root.geometry(f"{width}x{height}")
|
|
||||||
self.root.attributes('-fullscreen', True)
|
|
||||||
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):
|
|
||||||
self.content_frame = tk.Frame(self.root, bg='black')
|
|
||||||
self.content_frame.pack(fill=tk.BOTH, expand=True)
|
|
||||||
self.image_label = tk.Label(self.content_frame, bg='black')
|
|
||||||
self.image_label.pack(fill=tk.BOTH, expand=True)
|
|
||||||
self.status_label = tk.Label(
|
|
||||||
self.content_frame,
|
|
||||||
bg='black',
|
|
||||||
fg='white',
|
|
||||||
font=('Arial', 20),
|
|
||||||
text=""
|
|
||||||
)
|
|
||||||
self.create_control_panel()
|
|
||||||
self.show_controls()
|
|
||||||
self.schedule_hide_controls()
|
|
||||||
|
|
||||||
# --- FULL METHOD BODIES FROM tkinter_simple_player_old.py BELOW ---
|
|
||||||
def create_control_panel(self):
|
|
||||||
"""Create touch-optimized control panel with larger buttons"""
|
|
||||||
self.control_frame = tk.Frame(
|
|
||||||
self.root,
|
|
||||||
bg='#1a1a1a',
|
|
||||||
bd=2,
|
|
||||||
relief=tk.RAISED,
|
|
||||||
padx=15,
|
|
||||||
pady=15
|
|
||||||
)
|
|
||||||
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
|
|
||||||
button_config = {
|
|
||||||
'bg': '#333333',
|
|
||||||
'fg': 'white',
|
|
||||||
'activebackground': '#555555',
|
|
||||||
'activeforeground': 'white',
|
|
||||||
'relief': tk.FLAT,
|
|
||||||
'borderwidth': 0,
|
|
||||||
'width': 10,
|
|
||||||
'height': 3,
|
|
||||||
'font': ('Segoe UI', 10, 'bold'),
|
|
||||||
'cursor': 'hand2'
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
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',
|
|
||||||
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)
|
|
||||||
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)
|
|
||||||
self.settings_btn = tk.Button(
|
|
||||||
self.control_frame,
|
|
||||||
text="⚙️ Settings",
|
|
||||||
command=self.open_settings,
|
|
||||||
bg='#9b59b6',
|
|
||||||
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)
|
|
||||||
self.exit_btn = tk.Button(
|
|
||||||
self.control_frame,
|
|
||||||
text="❌ EXIT",
|
|
||||||
command=self.show_exit_dialog,
|
|
||||||
bg='#e74c3c',
|
|
||||||
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)
|
|
||||||
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'):
|
|
||||||
img_width, img_height = img.size
|
|
||||||
if mode == 'stretch':
|
|
||||||
return img.resize((screen_width, screen_height), Image.LANCZOS), (0, 0)
|
|
||||||
elif mode == 'fill':
|
|
||||||
screen_ratio = screen_width / screen_height
|
|
||||||
img_ratio = img_width / img_height
|
|
||||||
if img_ratio > screen_ratio:
|
|
||||||
new_height = screen_height
|
|
||||||
new_width = int(screen_height * img_ratio)
|
|
||||||
x_offset = (screen_width - new_width) // 2
|
|
||||||
y_offset = 0
|
|
||||||
else:
|
|
||||||
new_width = screen_width
|
|
||||||
new_height = int(screen_width / img_ratio)
|
|
||||||
x_offset = 0
|
|
||||||
y_offset = (screen_height - new_height) // 2
|
|
||||||
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
|
||||||
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
|
|
||||||
if new_width > screen_width:
|
|
||||||
crop_x = (new_width - screen_width) // 2
|
|
||||||
img_resized = img_resized.crop((crop_x, 0, crop_x + screen_width, new_height))
|
|
||||||
x_offset = 0
|
|
||||||
if new_height > screen_height:
|
|
||||||
crop_y = (new_height - screen_height) // 2
|
|
||||||
img_resized = img_resized.crop((0, crop_y, new_width, crop_y + screen_height))
|
|
||||||
y_offset = 0
|
|
||||||
final_img.paste(img_resized, (x_offset, y_offset))
|
|
||||||
return final_img, (x_offset, y_offset)
|
|
||||||
else:
|
|
||||||
screen_ratio = screen_width / screen_height
|
|
||||||
img_ratio = img_width / img_height
|
|
||||||
if img_ratio > screen_ratio:
|
|
||||||
new_width = screen_width
|
|
||||||
new_height = int(screen_width / img_ratio)
|
|
||||||
else:
|
|
||||||
new_height = screen_height
|
|
||||||
new_width = int(screen_height * img_ratio)
|
|
||||||
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
|
||||||
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
|
|
||||||
x_offset = (screen_width - new_width) // 2
|
|
||||||
y_offset = (screen_height - new_height) // 2
|
|
||||||
final_img.paste(img_resized, (x_offset, y_offset))
|
|
||||||
return final_img, (x_offset, y_offset)
|
|
||||||
|
|
||||||
def add_touch_feedback_to_control_button(self, button):
|
|
||||||
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):
|
|
||||||
# No-op: logic now handled in play_splash_then_start
|
|
||||||
pass
|
|
||||||
def _play_intro_splash(self, on_finish=None, duration=5):
|
|
||||||
"""Embed intro1.mp4 as splash video in splash_frame using python-vlc. If video ends early, pause last frame until 'duration' seconds."""
|
|
||||||
intro_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'intro1.mp4')
|
|
||||||
self._splash_ended = False
|
|
||||||
def finish():
|
|
||||||
self._stop_intro_splash()
|
|
||||||
if on_finish:
|
|
||||||
self.root.after(0, on_finish)
|
|
||||||
def on_end(event=None):
|
|
||||||
if not self._splash_ended:
|
|
||||||
self._splash_ended = True
|
|
||||||
# Pause on last frame
|
|
||||||
if hasattr(self, '_splash_player') and self._splash_player:
|
|
||||||
self._splash_player.set_pause(1)
|
|
||||||
if os.path.exists(intro_path) and hasattr(self, 'splash_frame'):
|
|
||||||
try:
|
|
||||||
self._splash_vlc_instance = vlc.Instance('--no-osd', '--no-video-title-show', '--quiet')
|
|
||||||
self._splash_media = self._splash_vlc_instance.media_new(intro_path)
|
|
||||||
self._splash_player = self._splash_vlc_instance.media_player_new()
|
|
||||||
self._splash_player.set_media(self._splash_media)
|
|
||||||
self.splash_frame.update_idletasks()
|
|
||||||
window_id = self.splash_frame.winfo_id()
|
|
||||||
self._splash_player.set_xwindow(window_id)
|
|
||||||
# Attach event for end of video
|
|
||||||
event_manager = self._splash_player.event_manager()
|
|
||||||
event_manager.event_attach(vlc.EventType.MediaPlayerEndReached, on_end)
|
|
||||||
self._splash_player.play()
|
|
||||||
self.root.after(int(duration * 1000), finish)
|
|
||||||
except Exception as e:
|
|
||||||
Logger.warning(f"Could not play splash video with python-vlc: {e}")
|
|
||||||
finish()
|
|
||||||
else:
|
|
||||||
Logger.warning(f"Splash video not found or splash_frame missing: {intro_path}")
|
|
||||||
finish()
|
|
||||||
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._stop_intro_splash()
|
|
||||||
self.load_fallback_playlist(fallback_playlist)
|
|
||||||
return
|
|
||||||
server_connection_successful = False
|
|
||||||
try:
|
|
||||||
Logger.info("Attempting to connect to server...")
|
|
||||||
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(server_playlist, server_version)
|
|
||||||
update_config_playlist_version(server_version)
|
|
||||||
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._stop_intro_splash()
|
|
||||||
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 not server_connection_successful:
|
|
||||||
self._stop_intro_splash()
|
|
||||||
self.load_fallback_playlist(fallback_playlist)
|
|
||||||
def _stop_intro_splash(self):
|
|
||||||
"""Stop the intro splash video if running (python-vlc version)."""
|
|
||||||
if hasattr(self, '_splash_player') and self._splash_player:
|
|
||||||
try:
|
|
||||||
self._splash_player.stop()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self._splash_player = None
|
|
||||||
if hasattr(self, '_splash_vlc_instance'):
|
|
||||||
self._splash_vlc_instance = None
|
|
||||||
|
|
||||||
def load_fallback_playlist(self, fallback_playlist):
|
|
||||||
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):
|
|
||||||
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
|
|
||||||
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):
|
|
||||||
demo_images = []
|
|
||||||
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 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:
|
|
||||||
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):
|
|
||||||
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):
|
|
||||||
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):
|
|
||||||
# On first playlist loop, play intro splash, then continue playlist
|
|
||||||
if getattr(self, '_first_playlist_loop', False):
|
|
||||||
self._first_playlist_loop = False
|
|
||||||
# Create splash_frame for intro video
|
|
||||||
self.splash_frame = tk.Frame(self.root, bg='black')
|
|
||||||
self.splash_frame.pack(fill=tk.BOTH, expand=True)
|
|
||||||
self.root.update_idletasks()
|
|
||||||
def after_intro():
|
|
||||||
if hasattr(self, 'splash_frame') and self.splash_frame:
|
|
||||||
self.splash_frame.destroy()
|
|
||||||
self.splash_frame = None
|
|
||||||
self._play_current_media_after_intro()
|
|
||||||
self._play_intro_splash(on_finish=after_intro, duration=5)
|
|
||||||
return
|
|
||||||
self._play_current_media_after_intro()
|
|
||||||
|
|
||||||
def _play_current_media_after_intro(self):
|
|
||||||
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)
|
|
||||||
if file_path.startswith('static/resurse/'):
|
|
||||||
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}")
|
|
||||||
self.log_event(file_name, "STARTED")
|
|
||||||
self.cancel_timers()
|
|
||||||
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}")
|
|
||||||
self.auto_advance_timer = self.root.after(5000, self.next_media)
|
|
||||||
|
|
||||||
def play_video(self, file_path):
|
|
||||||
self.status_label.place_forget()
|
|
||||||
def run_vlc_subprocess():
|
|
||||||
try:
|
|
||||||
Logger.info(f"Starting system VLC subprocess for video: {file_path}")
|
|
||||||
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):
|
|
||||||
try:
|
|
||||||
self.image_label.config(image=photo)
|
|
||||||
self.image_label.image = photo
|
|
||||||
except Exception as e:
|
|
||||||
Logger.error(f"Error updating video frame: {e}")
|
|
||||||
|
|
||||||
def _show_video_error(self, error_msg):
|
|
||||||
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):
|
|
||||||
self.image_label.config(image='')
|
|
||||||
self.status_label.config(text=text)
|
|
||||||
self.auto_advance_timer = self.root.after(
|
|
||||||
int(duration * 1000),
|
|
||||||
self.next_media
|
|
||||||
)
|
|
||||||
|
|
||||||
def show_image(self, file_path, duration):
|
|
||||||
try:
|
|
||||||
self.status_label.place_forget()
|
|
||||||
self.status_label.config(text="")
|
|
||||||
if PIL_AVAILABLE:
|
|
||||||
img = Image.open(file_path)
|
|
||||||
original_size = img.size
|
|
||||||
screen_width = self.root.winfo_width()
|
|
||||||
screen_height = self.root.winfo_height()
|
|
||||||
if screen_width <= 1 or screen_height <= 1:
|
|
||||||
screen_width = 1920
|
|
||||||
screen_height = 1080
|
|
||||||
final_img, offset = self.scale_image_to_screen(img, screen_width, screen_height, self.scaling_mode)
|
|
||||||
photo = ImageTk.PhotoImage(final_img)
|
|
||||||
self.image_label.config(image=photo)
|
|
||||||
self.image_label.image = photo
|
|
||||||
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:
|
|
||||||
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")
|
|
||||||
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):
|
|
||||||
self.cancel_timers()
|
|
||||||
if not self.playlist:
|
|
||||||
return
|
|
||||||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
|
||||||
if self.current_index == 0:
|
|
||||||
threading.Thread(target=self.check_playlist_updates, daemon=True).start()
|
|
||||||
self.play_current_media()
|
|
||||||
|
|
||||||
def previous_media(self):
|
|
||||||
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):
|
|
||||||
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")
|
|
||||||
self.play_current_media()
|
|
||||||
Logger.info(f"Media {'paused' if self.is_paused else 'resumed'}")
|
|
||||||
|
|
||||||
def cancel_timers(self):
|
|
||||||
if self.auto_advance_timer:
|
|
||||||
self.root.after_cancel(self.auto_advance_timer)
|
|
||||||
self.auto_advance_timer = None
|
|
||||||
|
|
||||||
def show_controls(self):
|
|
||||||
if self.control_frame:
|
|
||||||
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
|
|
||||||
|
|
||||||
def hide_controls(self):
|
|
||||||
if self.control_frame:
|
|
||||||
self.control_frame.place_forget()
|
|
||||||
|
|
||||||
def schedule_hide_controls(self):
|
|
||||||
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):
|
|
||||||
self.show_controls()
|
|
||||||
self.schedule_hide_controls()
|
|
||||||
|
|
||||||
def on_mouse_motion(self, event):
|
|
||||||
self.show_controls()
|
|
||||||
self.schedule_hide_controls()
|
|
||||||
|
|
||||||
def on_key_press(self, event):
|
|
||||||
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:
|
|
||||||
if key == 's':
|
|
||||||
self.open_settings()
|
|
||||||
self.show_controls()
|
|
||||||
self.schedule_hide_controls()
|
|
||||||
|
|
||||||
def set_scaling_mode(self, mode):
|
|
||||||
old_mode = self.scaling_mode
|
|
||||||
self.scaling_mode = mode
|
|
||||||
Logger.info(f"Scaling mode changed from '{old_mode}' to '{mode}'")
|
|
||||||
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')
|
|
||||||
self.root.after(2000, lambda: self.status_label.place_forget())
|
|
||||||
if self.playlist and 0 <= self.current_index < len(self.playlist):
|
|
||||||
self.cancel_timers()
|
|
||||||
self.play_current_media()
|
|
||||||
|
|
||||||
def toggle_fullscreen(self):
|
|
||||||
self.is_fullscreen = not self.is_fullscreen
|
|
||||||
self.root.attributes('-fullscreen', self.is_fullscreen)
|
|
||||||
|
|
||||||
def open_settings(self):
|
|
||||||
if hasattr(self, 'settings_window') and self.settings_window and self.settings_window.winfo_exists():
|
|
||||||
self.settings_window.lift()
|
|
||||||
return
|
|
||||||
if not self.is_paused:
|
|
||||||
self.toggle_play_pause()
|
|
||||||
self.settings_window = SettingsWindow(self.root, self)
|
|
||||||
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):
|
|
||||||
try:
|
|
||||||
config = load_config()
|
|
||||||
quickconnect_key = config.get('quickconnect_key', '')
|
|
||||||
except:
|
|
||||||
quickconnect_key = ''
|
|
||||||
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)
|
|
||||||
self.center_dialog_on_screen(exit_dialog, 400, 200)
|
|
||||||
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 = tk.Frame(exit_dialog, bg='#2d2d2d', padx=20, pady=20)
|
|
||||||
content_frame.pack(fill=tk.BOTH, expand=True)
|
|
||||||
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_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 = 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():
|
|
||||||
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 = tk.Label(content_frame, text="", font=('Arial', 9),
|
|
||||||
bg='#2d2d2d')
|
|
||||||
error_label.pack()
|
|
||||||
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)
|
|
||||||
password_entry.bind('<Return>', lambda e: check_password())
|
|
||||||
exit_dialog.bind('<Escape>', lambda e: cancel_exit())
|
|
||||||
|
|
||||||
def exit_application(self):
|
|
||||||
Logger.info("Application exit requested")
|
|
||||||
self.running = False
|
|
||||||
self.root.quit()
|
|
||||||
self.root.destroy()
|
|
||||||
|
|
||||||
def center_dialog_on_screen(self, dialog, width, height):
|
|
||||||
dialog.update_idletasks()
|
|
||||||
screen_width = dialog.winfo_screenwidth()
|
|
||||||
screen_height = dialog.winfo_screenheight()
|
|
||||||
center_x = int((screen_width - width) / 2)
|
|
||||||
center_y = int((screen_height - height) / 2)
|
|
||||||
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}")
|
|
||||||
dialog.lift()
|
|
||||||
dialog.focus_force()
|
|
||||||
return center_x, center_y
|
|
||||||
|
|
||||||
def check_playlist_updates(self):
|
|
||||||
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}")
|
|
||||||
local_playlist_data = load_local_playlist()
|
|
||||||
clean_unused_files(local_playlist_data.get('playlist', []))
|
|
||||||
download_media_files(
|
|
||||||
server_playlist_data.get('playlist', []),
|
|
||||||
server_version
|
|
||||||
)
|
|
||||||
local_playlist_data = load_local_playlist()
|
|
||||||
self.playlist = local_playlist_data.get('playlist', [])
|
|
||||||
self.current_index = 0
|
|
||||||
Logger.info("Playlist updated successfully")
|
|
||||||
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):
|
|
||||||
try:
|
|
||||||
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
log_message = f"{timestamp} - {event}: {file_name}\n"
|
|
||||||
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):
|
|
||||||
def check_loop():
|
|
||||||
while self.running:
|
|
||||||
try:
|
|
||||||
time.sleep(300)
|
|
||||||
if self.running:
|
|
||||||
self.check_playlist_updates()
|
|
||||||
except Exception as e:
|
|
||||||
Logger.error(f"Error in periodic check: {e}")
|
|
||||||
threading.Thread(target=check_loop, daemon=True).start()
|
|
||||||
|
|
||||||
def next_media(self):
|
|
||||||
self.cancel_timers()
|
|
||||||
if not self.playlist:
|
|
||||||
return
|
|
||||||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
|
||||||
# At end of playlist, check for updates
|
|
||||||
if self.current_index == 0:
|
|
||||||
self.check_playlist_updates()
|
|
||||||
self.play_current_media()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
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}")
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
import tkinter as tk
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
class PlayerUI:
|
|
||||||
def __init__(self, root, control_callbacks=None):
|
|
||||||
self.root = root
|
|
||||||
self.control_frame = None
|
|
||||||
self.content_frame = None
|
|
||||||
self.image_label = None
|
|
||||||
self.status_label = None
|
|
||||||
self.play_pause_btn = None
|
|
||||||
self.prev_btn = None
|
|
||||||
self.next_btn = None
|
|
||||||
self.exit_btn = None
|
|
||||||
self.settings_btn = None
|
|
||||||
self.hide_controls_timer = None
|
|
||||||
# Set fullscreen and geometry before packing widgets
|
|
||||||
self.root.title("Simple Signage Player")
|
|
||||||
self.root.configure(bg='black')
|
|
||||||
try:
|
|
||||||
config = None
|
|
||||||
try:
|
|
||||||
from python_functions import load_config
|
|
||||||
config = load_config()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
width = int(config.get('screen_w', 1920)) if config else 1920
|
|
||||||
height = int(config.get('screen_h', 1080)) if config else 1080
|
|
||||||
self.scaling_mode = config.get('scaling_mode', 'fit') if config else 'fit'
|
|
||||||
except:
|
|
||||||
width, height = 1920, 1080
|
|
||||||
self.scaling_mode = 'fit'
|
|
||||||
self.root.geometry(f"{width}x{height}")
|
|
||||||
self.root.attributes('-fullscreen', True)
|
|
||||||
self.root.focus_set()
|
|
||||||
self.setup_ui(control_callbacks)
|
|
||||||
|
|
||||||
def setup_ui(self, control_callbacks=None):
|
|
||||||
self.content_frame = tk.Frame(self.root, bg='black')
|
|
||||||
self.content_frame.pack(fill=tk.BOTH, expand=True)
|
|
||||||
self.image_label = tk.Label(self.content_frame, bg='black')
|
|
||||||
self.image_label.pack(fill=tk.BOTH, expand=True)
|
|
||||||
self.status_label = tk.Label(
|
|
||||||
self.content_frame,
|
|
||||||
bg='black',
|
|
||||||
fg='white',
|
|
||||||
font=('Arial', 20),
|
|
||||||
text=""
|
|
||||||
)
|
|
||||||
self.create_control_panel(control_callbacks)
|
|
||||||
self.show_controls()
|
|
||||||
self.schedule_hide_controls()
|
|
||||||
|
|
||||||
def create_control_panel(self, control_callbacks=None):
|
|
||||||
self.control_frame = tk.Frame(
|
|
||||||
self.root,
|
|
||||||
bg='#1a1a1a',
|
|
||||||
bd=2,
|
|
||||||
relief=tk.RAISED,
|
|
||||||
padx=15,
|
|
||||||
pady=15
|
|
||||||
)
|
|
||||||
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
|
|
||||||
button_config = {
|
|
||||||
'bg': '#333333',
|
|
||||||
'fg': 'white',
|
|
||||||
'activebackground': '#555555',
|
|
||||||
'activeforeground': 'white',
|
|
||||||
'relief': tk.FLAT,
|
|
||||||
'borderwidth': 0,
|
|
||||||
'width': 10,
|
|
||||||
'height': 3,
|
|
||||||
'font': ('Segoe UI', 10, 'bold'),
|
|
||||||
'cursor': 'hand2'
|
|
||||||
}
|
|
||||||
self.prev_btn = tk.Button(
|
|
||||||
self.control_frame,
|
|
||||||
text="⏮ Prev",
|
|
||||||
command=control_callbacks.get('prev') if control_callbacks else None,
|
|
||||||
**button_config
|
|
||||||
)
|
|
||||||
self.prev_btn.grid(row=0, column=0, padx=5)
|
|
||||||
self.play_pause_btn = tk.Button(
|
|
||||||
self.control_frame,
|
|
||||||
text="⏸ Pause",
|
|
||||||
command=control_callbacks.get('play_pause') if control_callbacks else None,
|
|
||||||
bg='#27ae60',
|
|
||||||
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)
|
|
||||||
self.next_btn = tk.Button(
|
|
||||||
self.control_frame,
|
|
||||||
text="Next ⏭",
|
|
||||||
command=control_callbacks.get('next') if control_callbacks else None,
|
|
||||||
**button_config
|
|
||||||
)
|
|
||||||
self.next_btn.grid(row=0, column=2, padx=5)
|
|
||||||
self.settings_btn = tk.Button(
|
|
||||||
self.control_frame,
|
|
||||||
text="⚙️ Settings",
|
|
||||||
command=control_callbacks.get('settings') if control_callbacks else None,
|
|
||||||
bg='#9b59b6',
|
|
||||||
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)
|
|
||||||
self.exit_btn = tk.Button(
|
|
||||||
self.control_frame,
|
|
||||||
text="❌ EXIT",
|
|
||||||
command=control_callbacks.get('exit') if control_callbacks else None,
|
|
||||||
bg='#e74c3c',
|
|
||||||
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)
|
|
||||||
|
|
||||||
def show_controls(self):
|
|
||||||
if self.control_frame:
|
|
||||||
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
|
|
||||||
|
|
||||||
def hide_controls(self):
|
|
||||||
if self.control_frame:
|
|
||||||
self.control_frame.place_forget()
|
|
||||||
|
|
||||||
def schedule_hide_controls(self):
|
|
||||||
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 scale_image_to_screen(self, img, screen_width, screen_height, mode='fit'):
|
|
||||||
img_width, img_height = img.size
|
|
||||||
if mode == 'stretch':
|
|
||||||
return img.resize((screen_width, screen_height), Image.LANCZOS), (0, 0)
|
|
||||||
elif mode == 'fill':
|
|
||||||
screen_ratio = screen_width / screen_height
|
|
||||||
img_ratio = img_width / img_height
|
|
||||||
if img_ratio > screen_ratio:
|
|
||||||
new_height = screen_height
|
|
||||||
new_width = int(screen_height * img_ratio)
|
|
||||||
x_offset = (screen_width - new_width) // 2
|
|
||||||
y_offset = 0
|
|
||||||
else:
|
|
||||||
new_width = screen_width
|
|
||||||
new_height = int(screen_width / img_ratio)
|
|
||||||
x_offset = 0
|
|
||||||
y_offset = (screen_height - new_height) // 2
|
|
||||||
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
|
||||||
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
|
|
||||||
if new_width > screen_width:
|
|
||||||
crop_x = (new_width - screen_width) // 2
|
|
||||||
img_resized = img_resized.crop((crop_x, 0, crop_x + screen_width, new_height))
|
|
||||||
x_offset = 0
|
|
||||||
if new_height > screen_height:
|
|
||||||
crop_y = (new_height - screen_height) // 2
|
|
||||||
img_resized = img_resized.crop((0, crop_y, new_width, crop_y + screen_height))
|
|
||||||
y_offset = 0
|
|
||||||
final_img.paste(img_resized, (x_offset, y_offset))
|
|
||||||
return final_img, (x_offset, y_offset)
|
|
||||||
else:
|
|
||||||
screen_ratio = screen_width / screen_height
|
|
||||||
img_ratio = img_width / img_height
|
|
||||||
if img_ratio > screen_ratio:
|
|
||||||
new_width = screen_width
|
|
||||||
new_height = int(screen_width / img_ratio)
|
|
||||||
else:
|
|
||||||
new_height = screen_height
|
|
||||||
new_width = int(screen_height * img_ratio)
|
|
||||||
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
|
||||||
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
|
|
||||||
x_offset = (screen_width - new_width) // 2
|
|
||||||
y_offset = (screen_height - new_height) // 2
|
|
||||||
final_img.paste(img_resized, (x_offset, y_offset))
|
|
||||||
return final_img, (x_offset, y_offset)
|
|
||||||
|
|
||||||
def bind_show_controls_on_activity(self):
|
|
||||||
def on_activity(event=None):
|
|
||||||
self.show_controls()
|
|
||||||
self.schedule_hide_controls()
|
|
||||||
self.root.bind('<Button-1>', on_activity)
|
|
||||||
self.root.bind('<Motion>', on_activity)
|
|
||||||
self.root.bind('<Key>', on_activity)
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import threading
|
|
||||||
from logging_config import Logger
|
|
||||||
from python_functions import (
|
|
||||||
load_local_playlist, download_media_files, update_config_playlist_version, fetch_server_playlist, load_config
|
|
||||||
)
|
|
||||||
|
|
||||||
class PlaylistManager:
|
|
||||||
def __init__(self):
|
|
||||||
self._playlist_ready = threading.Event()
|
|
||||||
self._playlist_data = None
|
|
||||||
|
|
||||||
def fetch_playlist(self):
|
|
||||||
def fetch_logic():
|
|
||||||
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}")
|
|
||||||
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}")
|
|
||||||
playlist = None
|
|
||||||
if server and host and quick and port:
|
|
||||||
try:
|
|
||||||
Logger.info("Attempting to connect to server...")
|
|
||||||
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}")
|
|
||||||
download_media_files(server_playlist, server_version)
|
|
||||||
update_config_playlist_version(server_version)
|
|
||||||
local_playlist_data = load_local_playlist()
|
|
||||||
playlist = local_playlist_data.get('playlist', [])
|
|
||||||
if playlist:
|
|
||||||
Logger.info(f"Successfully loaded {len(playlist)} items from server")
|
|
||||||
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 Exception as e:
|
|
||||||
Logger.error(f"Failed to fetch playlist from server: {e}, using fallback playlist")
|
|
||||||
if not playlist:
|
|
||||||
playlist = fallback_playlist
|
|
||||||
if not playlist:
|
|
||||||
playlist = None
|
|
||||||
self._playlist_data = playlist
|
|
||||||
self._playlist_ready.set()
|
|
||||||
threading.Thread(target=fetch_logic, daemon=True).start()
|
|
||||||
|
|
||||||
def wait_for_playlist(self):
|
|
||||||
self._playlist_ready.wait()
|
|
||||||
return self._playlist_data
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
import os
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
from logging_config import Logger # Import the shared logger
|
|
||||||
import bcrypt
|
|
||||||
import time
|
|
||||||
import re
|
|
||||||
|
|
||||||
# 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:
|
|
||||||
# Use port only if server is an IP address
|
|
||||||
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
|
|
||||||
if re.match(ip_pattern, server):
|
|
||||||
server_url = f'http://{server}:{port}/api/playlists' # fixed endpoint
|
|
||||||
else:
|
|
||||||
server_url = f'http://{server}/api/playlists' # fixed endpoint
|
|
||||||
params = {
|
|
||||||
'hostname': host,
|
|
||||||
'quickconnect_code': quick
|
|
||||||
}
|
|
||||||
Logger.info(f"Fetching playlist from URL: {server_url} with params: {params}")
|
|
||||||
response = requests.get(server_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}")
|
|
||||||
|
|
||||||
def check_for_new_server_playlist():
|
|
||||||
"""Check if the server has a new playlist version compared to app_config.txt."""
|
|
||||||
config = load_config()
|
|
||||||
local_version = config.get('playlist_version', 0)
|
|
||||||
server_data = fetch_server_playlist()
|
|
||||||
server_version = server_data.get('version', 0)
|
|
||||||
if server_version > local_version:
|
|
||||||
print(f"A new playlist is available on the server: version {server_version} (local: {local_version})")
|
|
||||||
Logger.info(f"A new playlist is available on the server: version {server_version} (local: {local_version})")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f"No new playlist on the server. Local version: {local_version}, Server version: {server_version}")
|
|
||||||
Logger.info(f"No new playlist on the server. Local version: {local_version}, Server version: {server_version}")
|
|
||||||
return False
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
import tkinter as tk
|
|
||||||
from logging_config import Logger
|
|
||||||
|
|
||||||
class SplashScreen:
|
|
||||||
def __init__(self, root, video_path, duration=5):
|
|
||||||
self.root = root
|
|
||||||
self.video_path = video_path
|
|
||||||
self.duration = duration
|
|
||||||
|
|
||||||
def show(self, on_finish=None):
|
|
||||||
Logger.info(f"[SplashScreen] Running splash as standalone VLC subprocess: {self.video_path}")
|
|
||||||
if os.path.exists(self.video_path):
|
|
||||||
try:
|
|
||||||
vlc_cmd = [
|
|
||||||
'cvlc',
|
|
||||||
'--fullscreen',
|
|
||||||
'--no-osd',
|
|
||||||
'--no-video-title-show',
|
|
||||||
'--play-and-exit',
|
|
||||||
'--quiet',
|
|
||||||
self.video_path
|
|
||||||
]
|
|
||||||
Logger.info(f"[SplashScreen] Launching: {' '.join(vlc_cmd)}")
|
|
||||||
proc = subprocess.Popen(vlc_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
||||||
proc.wait() # Remove timeout for immediate transition
|
|
||||||
Logger.info("[SplashScreen] VLC splash finished.")
|
|
||||||
except Exception as e:
|
|
||||||
Logger.warning(f"[SplashScreen] VLC splash failed: {e}")
|
|
||||||
else:
|
|
||||||
Logger.warning(f"[SplashScreen] Splash video not found: {self.video_path}")
|
|
||||||
if on_finish:
|
|
||||||
self.root.after(0, on_finish)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
root = tk.Tk()
|
|
||||||
# Use the same logic as main.py to resolve the video path
|
|
||||||
base_dir = os.path.dirname(os.path.dirname(__file__))
|
|
||||||
video_path = os.path.join(base_dir, 'resources', 'intro1.mp4')
|
|
||||||
Logger.info(f"[SplashScreen] Standalone test: video_path={video_path}")
|
|
||||||
def on_finish():
|
|
||||||
Logger.info("[SplashScreen] Standalone test: Splash finished, exiting app.")
|
|
||||||
root.destroy()
|
|
||||||
splash = SplashScreen(root, video_path, duration=10)
|
|
||||||
splash.show(on_finish=on_finish)
|
|
||||||
root.mainloop()
|
|
||||||
|
Before Width: | Height: | Size: 361 KiB |
|
Before Width: | Height: | Size: 5.5 MiB |
|
Before Width: | Height: | Size: 7.5 MiB |
|
Before Width: | Height: | Size: 794 KiB |
@@ -1,360 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import vlc
|
|
||||||
import time
|
|
||||||
import os
|
|
||||||
from logging_config import Logger
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
video_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'intro1.mp4')
|
|
||||||
Logger.info(f"[VLC TEST] Attempting to play video: {video_path}")
|
|
||||||
if not os.path.exists(video_path):
|
|
||||||
Logger.error(f"[VLC TEST] File does not exist: {video_path}")
|
|
||||||
exit(1)
|
|
||||||
instance = vlc.Instance('--no-osd', '--no-video-title-show', '--quiet', '--fullscreen')
|
|
||||||
player = instance.media_player_new()
|
|
||||||
media = instance.media_new(video_path)
|
|
||||||
player.set_media(media)
|
|
||||||
player.play()
|
|
||||||
Logger.info("[VLC TEST] player.play() called. Waiting for video to finish...")
|
|
||||||
# Wait for video to finish (max 30 seconds)
|
|
||||||
start = time.time()
|
|
||||||
while True:
|
|
||||||
state = player.get_state()
|
|
||||||
if state in (vlc.State.Ended, vlc.State.Error):
|
|
||||||
Logger.info(f"[VLC TEST] Playback ended with state: {state}")
|
|
||||||
break
|
|
||||||
if time.time() - start > 30:
|
|
||||||
Logger.warning("[VLC TEST] Timeout waiting for video to finish.")
|
|
||||||
break
|
|
||||||
time.sleep(0.2)
|
|
||||||
player.stop()
|
|
||||||
Logger.info("[VLC TEST] Done.")
|
|
||||||