Update player intro

parent b43952ee
......@@ -143,6 +143,24 @@ Examples:
help='Enable debug mode showing only message bus messages'
)
parser.add_argument(
'--debug-messages',
action='store_true',
help='Show all messages passing through the message bus on screen'
)
parser.add_argument(
'--debug-player',
action='store_true',
help='Enable debug mode for Qt player component'
)
parser.add_argument(
'--debug-overlay',
action='store_true',
help='Enable debug mode for overlay rendering'
)
parser.add_argument(
'--no-qt',
action='store_true',
......@@ -200,7 +218,7 @@ Examples:
'--start-timer',
type=int,
default=None,
help='Configure timer delay in minutes for START_GAME_DELAYED message when START_GAME is received (default: 4 minutes)'
help='Configure timer delay in minutes for START_GAME_DELAYED message when START_GAME is received. Use 0 for 10-second delay (default: 4 minutes)'
)
parser.add_argument(
......@@ -252,6 +270,9 @@ def main():
settings.web_port = args.web_port
settings.debug_mode = args.debug or args.dev_mode
settings.dev_message = args.dev_message
settings.debug_messages = args.debug_messages
settings.debug_player = args.debug_player
settings.debug_overlay = args.debug_overlay
settings.enable_qt = not args.no_qt
settings.enable_web = not args.no_web
settings.qt.use_native_overlay = args.native_overlay
......
......@@ -369,6 +369,9 @@ class AppSettings:
version: str = "1.0.0"
debug_mode: bool = False
dev_message: bool = False # Enable debug mode showing only message bus messages
debug_messages: bool = False # Show all messages passing through the message bus on screen
debug_player: bool = False # Enable debug mode for Qt player component
debug_overlay: bool = False # Enable debug mode for overlay rendering
enable_web: bool = True
enable_qt: bool = True
enable_api_client: bool = True
......@@ -408,6 +411,10 @@ class AppSettings:
"timer": self.timer.__dict__,
"version": self.version,
"debug_mode": self.debug_mode,
"dev_message": self.dev_message,
"debug_messages": self.debug_messages,
"debug_player": self.debug_player,
"debug_overlay": self.debug_overlay,
"enable_web": self.enable_web,
"enable_qt": self.enable_qt,
"enable_api_client": self.enable_api_client,
......@@ -438,7 +445,7 @@ class AppSettings:
settings.timer = TimerConfig(**data["timer"])
# Update app settings
for key in ["version", "debug_mode", "enable_web", "enable_qt", "enable_api_client", "enable_screen_cast"]:
for key in ["version", "debug_mode", "dev_message", "debug_messages", "debug_player", "debug_overlay", "enable_web", "enable_qt", "enable_api_client", "enable_screen_cast"]:
if key in data:
setattr(settings, key, data[key])
......
......@@ -165,7 +165,7 @@ class MbetterClientApplication:
def _initialize_message_bus(self) -> bool:
"""Initialize message bus"""
try:
self.message_bus = MessageBus(max_queue_size=1000, dev_message=self.settings.dev_message)
self.message_bus = MessageBus(max_queue_size=1000, dev_message=self.settings.dev_message, debug_messages=self.settings.debug_messages)
# Register core component
self.message_bus.register_component("core")
......@@ -322,7 +322,9 @@ class MbetterClientApplication:
self.qt_player = QtVideoPlayer(
message_bus=self.message_bus,
settings=self.settings.qt
settings=self.settings.qt,
debug_player=self.settings.debug_player,
debug_overlay=self.settings.debug_overlay
)
# Don't register with thread manager since QtPlayer no longer inherits from ThreadedComponent
......@@ -760,6 +762,11 @@ class MbetterClientApplication:
if self._start_timer_minutes is None:
return
# Special case: --start-timer 0 means 10 seconds delay for system initialization
if self._start_timer_minutes == 0:
delay_seconds = 10
logger.info(f"Starting command line game timer: --start-timer 0 = 10 seconds delay for system initialization")
else:
delay_seconds = self._start_timer_minutes * 60
logger.info(f"Starting command line game timer: {self._start_timer_minutes} minutes ({delay_seconds} seconds)")
......
......@@ -685,6 +685,10 @@ class GamesThread(ThreadedComponent):
logger.info(f"⏰ Starting match timer for fixture {fixture_id}")
self._start_match_timer(fixture_id)
# Dispatch START_INTRO message
logger.info(f"🎬 Dispatching START_INTRO message for fixture {fixture_id}")
self._dispatch_start_intro(fixture_id)
# Refresh dashboard statuses
self._refresh_dashboard_statuses()
......@@ -897,6 +901,114 @@ class GamesThread(ThreadedComponent):
except Exception as e:
logger.error(f"Failed to start match timer: {e}")
def _dispatch_start_intro(self, fixture_id: str):
"""Dispatch START_INTRO message to trigger intro content"""
try:
from .message_bus import MessageBuilder
# Find the first match that was set to 'bet' status
first_bet_match_id = self._get_first_bet_match_id(fixture_id)
# Unzip the ZIP file of the first match if it exists
self._unzip_match_zip_file(first_bet_match_id)
# Create and send START_INTRO message
start_intro_message = MessageBuilder.start_intro(
sender=self.name,
fixture_id=fixture_id,
match_id=first_bet_match_id
)
self.message_bus.publish(start_intro_message, broadcast=True)
logger.info(f"START_INTRO message dispatched for fixture {fixture_id}, match {first_bet_match_id}")
except Exception as e:
logger.error(f"Failed to dispatch START_INTRO message: {e}")
def _get_first_bet_match_id(self, fixture_id: str) -> Optional[int]:
"""Get the ID of the first match set to 'bet' status in the fixture"""
try:
session = self.db_manager.get_session()
try:
from ..database.models import MatchModel
# Find the first match with 'bet' status in this fixture
first_bet_match = session.query(MatchModel).filter(
MatchModel.fixture_id == fixture_id,
MatchModel.status == 'bet',
MatchModel.active_status == True
).order_by(MatchModel.match_number.asc()).first()
if first_bet_match:
return first_bet_match.id
else:
logger.warning(f"No match with 'bet' status found in fixture {fixture_id}")
return None
finally:
session.close()
except Exception as e:
logger.error(f"Failed to get first bet match ID for fixture {fixture_id}: {e}")
return None
def _unzip_match_zip_file(self, match_id: int):
"""Unzip the ZIP file associated with a match to a temporary directory"""
try:
import zipfile
import tempfile
import os
from pathlib import Path
session = self.db_manager.get_session()
try:
# Get the match from database
match = session.query(MatchModel).filter_by(id=match_id).first()
if not match:
logger.warning(f"Match {match_id} not found, skipping ZIP extraction")
return
if not match.zip_filename:
logger.info(f"Match {match_id} has no associated ZIP file, skipping extraction")
return
# Determine ZIP file location (ZIP files are stored in the zip_files directory)
from ..config.settings import get_user_data_dir
user_data_dir = get_user_data_dir()
zip_file_path = user_data_dir / "zip_files" / match.zip_filename
if not zip_file_path.exists():
logger.warning(f"ZIP file not found: {zip_file_path}")
return
# Create temporary directory for extraction
temp_dir = Path(tempfile.mkdtemp(prefix=f"match_{match_id}_"))
logger.info(f"Extracting ZIP file {zip_file_path} to temporary directory: {temp_dir}")
# Extract the ZIP file
with zipfile.ZipFile(str(zip_file_path), 'r') as zip_ref:
zip_ref.extractall(str(temp_dir))
# Log extraction results
extracted_files = list(temp_dir.rglob("*"))
logger.info(f"Successfully extracted {len(extracted_files)} files from {match.zip_filename}")
# Store the temporary directory path for potential cleanup
# In a real implementation, you might want to track this for cleanup
match.temp_extract_path = str(temp_dir)
# Update match in database with temp path (optional)
session.commit()
logger.info(f"ZIP extraction completed for match {match_id}")
finally:
session.close()
except Exception as e:
logger.error(f"Failed to unzip ZIP file for match {match_id}: {e}")
def _cleanup(self):
"""Perform cleanup operations"""
try:
......
......@@ -64,7 +64,10 @@ class MessageType(Enum):
START_GAME = "START_GAME"
SCHEDULE_GAMES = "SCHEDULE_GAMES"
START_GAME_DELAYED = "START_GAME_DELAYED"
START_INTRO = "START_INTRO"
MATCH_START = "MATCH_START"
PLAY_VIDEO_MATCH = "PLAY_VIDEO_MATCH"
PLAY_VIDEO_MATCH_DONE = "PLAY_VIDEO_MATCH_DONE"
GAME_STATUS = "GAME_STATUS"
GAME_UPDATE = "GAME_UPDATE"
......@@ -134,9 +137,10 @@ class Message:
class MessageBus:
"""Central message bus for inter-thread communication"""
def __init__(self, max_queue_size: int = 1000, dev_message: bool = False):
def __init__(self, max_queue_size: int = 1000, dev_message: bool = False, debug_messages: bool = False):
self.max_queue_size = max_queue_size
self.dev_message = dev_message
self.debug_messages = debug_messages
self._queues: Dict[str, Queue] = {}
self._handlers: Dict[str, Dict[MessageType, List[Callable]]] = {}
self._global_handlers: Dict[MessageType, List[Callable]] = {}
......@@ -147,6 +151,8 @@ class MessageBus:
if dev_message:
logger.info("MessageBus initialized with dev_message mode enabled")
elif debug_messages:
logger.info("MessageBus initialized with debug_messages mode enabled")
else:
logger.info("MessageBus initialized")
......@@ -208,6 +214,16 @@ class MessageBus:
if self.dev_message:
logger.info(f"📨 MESSAGE_BUS: {message}")
# Display message on screen (debug_messages mode with debug enabled)
if self.debug_messages and self.dev_message:
timestamp_str = datetime.fromtimestamp(message.timestamp).strftime('%H:%M:%S.%f')[:-3]
print(f"[{timestamp_str}] 📨 {message.sender} -> {message.recipient or 'ALL'}: {message.type.value}")
if message.data:
# Show key data fields (truncate long values)
data_str = ", ".join([f"{k}: {str(v)[:50]}{'...' if len(str(v)) > 50 else ''}" for k, v in message.data.items()])
print(f" Data: {{{data_str}}}")
print() # Empty line for readability
if broadcast or message.recipient is None:
# Broadcast to all components
success_count = 0
......@@ -578,6 +594,18 @@ class MessageBuilder:
}
)
@staticmethod
def start_intro(sender: str, fixture_id: Optional[str] = None, match_id: Optional[int] = None) -> Message:
"""Create START_INTRO message"""
return Message(
type=MessageType.START_INTRO,
sender=sender,
data={
"fixture_id": fixture_id,
"match_id": match_id
}
)
@staticmethod
def match_start(sender: str, fixture_id: str, match_id: int) -> Message:
"""Create MATCH_START message"""
......@@ -589,3 +617,29 @@ class MessageBuilder:
"match_id": match_id
}
)
@staticmethod
def play_video_match(sender: str, match_id: int, video_filename: str, fixture_id: Optional[str] = None) -> Message:
"""Create PLAY_VIDEO_MATCH message"""
return Message(
type=MessageType.PLAY_VIDEO_MATCH,
sender=sender,
data={
"match_id": match_id,
"video_filename": video_filename,
"fixture_id": fixture_id
}
)
@staticmethod
def play_video_match_done(sender: str, match_id: int, video_filename: str, fixture_id: Optional[str] = None) -> Message:
"""Create PLAY_VIDEO_MATCH_DONE message"""
return Message(
type=MessageType.PLAY_VIDEO_MATCH_DONE,
sender=sender,
data={
"match_id": match_id,
"video_filename": video_filename,
"fixture_id": fixture_id
}
)
\ No newline at end of file
......@@ -235,8 +235,9 @@ class VideoProcessingWorker(QRunnable):
class OverlayWebView(QWebEngineView):
"""Custom QWebEngineView for video overlays with transparent background"""
def __init__(self, parent=None):
def __init__(self, parent=None, debug_overlay=False):
super().__init__(parent)
self.debug_overlay = debug_overlay
self.web_channel = None
self.overlay_channel = None
self.current_template = "default.html"
......@@ -337,6 +338,7 @@ class OverlayWebView(QWebEngineView):
def load_template(self, template_name: str):
"""Load a specific template file, prioritizing uploaded templates"""
try:
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Starting template load - {template_name}")
logger.debug(f"GREEN SCREEN DEBUG: Current page URL before load: {self.url().toString()}")
logger.debug(f"GREEN SCREEN DEBUG: WebEngine view visible: {self.isVisible()}")
......@@ -376,6 +378,7 @@ class OverlayWebView(QWebEngineView):
return
if template_path and template_path.exists():
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: About to load template file: {template_path}")
logger.debug(f"GREEN SCREEN DEBUG: Template source: {template_source}")
......@@ -389,6 +392,7 @@ class OverlayWebView(QWebEngineView):
# CRITICAL FIX: Force visibility recovery after template load
if was_visible and not self.isVisible():
if self.debug_overlay:
logger.debug(f"GREEN SCREEN FIX: Recovering overlay visibility after template load")
self.show()
self.raise_()
......@@ -397,6 +401,7 @@ class OverlayWebView(QWebEngineView):
from PyQt6.QtCore import QTimer
QTimer.singleShot(100, lambda: self._ensure_overlay_visibility_post_load(was_visible))
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Template load initiated - {template_path}")
logger.info(f"Loaded template: {template_path} (source: {template_source})")
else:
......@@ -405,6 +410,7 @@ class OverlayWebView(QWebEngineView):
self._load_fallback_overlay()
except Exception as e:
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Template load failed: {e}")
logger.error(f"Failed to load template {template_name}: {e}")
self._load_fallback_overlay()
......@@ -458,9 +464,11 @@ class OverlayWebView(QWebEngineView):
def reload_current_template(self):
"""Reload the current template"""
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Reloading current template - {self.current_template}")
logger.debug(f"GREEN SCREEN DEBUG: WebEngine state before reload - visible: {self.isVisible()}")
self.load_template(self.current_template)
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Current template reload initiated")
def get_available_templates(self) -> List[str]:
......@@ -957,9 +965,10 @@ class PlayerWindow(QMainWindow):
position_changed = pyqtSignal(int, int)
video_loaded = pyqtSignal(str)
def __init__(self, settings: QtConfig, message_bus: MessageBus = None):
def __init__(self, settings: QtConfig, message_bus: MessageBus = None, debug_overlay: bool = False):
super().__init__()
self.settings = settings
self.debug_overlay = debug_overlay
self.mutex = QMutex()
self.thread_pool = QThreadPool()
self.thread_pool.setMaxThreadCount(4)
......@@ -1067,7 +1076,9 @@ class PlayerWindow(QMainWindow):
self.window_overlay = NativeOverlayWidget(self.overlay_window)
logger.debug("PlayerWindow: Created NativeOverlayWidget overlay as separate window")
else:
self.window_overlay = OverlayWebView(self.overlay_window)
# Pass debug_overlay setting to OverlayWebView
debug_overlay = getattr(self, 'debug_overlay', False)
self.window_overlay = OverlayWebView(self.overlay_window, debug_overlay=debug_overlay)
logger.debug("PlayerWindow: Created QWebEngineView overlay as separate window")
# Layout for overlay window
......@@ -1142,10 +1153,12 @@ class PlayerWindow(QMainWindow):
self.window_overlay.raise_()
logger.error(f"GREEN SCREEN FIX: WebEngine overlay visibility recovered during sync")
if self.debug_overlay:
logger.debug(f"Overlay window positioned at: {self.overlay_window.geometry()}")
logger.debug(f"GREEN SCREEN DEBUG: Overlay window after sync - geometry: {self.overlay_window.geometry()}")
except Exception as e:
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Overlay sync failed: {e}")
logger.error(f"Failed to sync overlay position: {e}")
......@@ -1217,6 +1230,18 @@ class PlayerWindow(QMainWindow):
self.current_loop_iteration = 0
self.current_file_path = None
# Match video tracking
self.is_playing_match_video = False
self.current_match_id = None
self.current_match_video_filename = None
self.current_fixture_id = None
# Template rotation state
self.template_sequence = []
self.rotating_time = '05:00'
self.current_template_index = 0
self.template_rotation_timer = None
# Set volume
self.audio_output.setVolume(self.settings.volume)
self.audio_output.setMuted(self.settings.mute)
......@@ -1228,9 +1253,66 @@ class PlayerWindow(QMainWindow):
self.overlay_timer.timeout.connect(self.update_overlay_periodically)
self.overlay_timer.start(1000) # Update every second
# Template rotation timer (initially disabled)
self.template_rotation_timer = QTimer()
self.template_rotation_timer.timeout.connect(self._rotate_template)
self.template_rotation_timer.setSingleShot(False) # Repeat timer
# Mouse tracking for window interactions (no controls to show/hide)
self.setMouseTracking(True)
def _start_template_rotation(self):
"""Start the template rotation timer"""
try:
if not self.template_sequence or len(self.template_sequence) <= 1:
logger.debug("Template rotation not needed - insufficient templates")
return
# Parse rotating time (format: MM:SS)
try:
minutes, seconds = map(int, self.rotating_time.split(':'))
rotation_interval_ms = (minutes * 60 + seconds) * 1000 # Convert to milliseconds
except (ValueError, AttributeError):
logger.warning(f"Invalid rotating_time format '{self.rotating_time}', using default 5 minutes")
rotation_interval_ms = 5 * 60 * 1000 # Default 5 minutes
logger.info(f"Starting template rotation every {rotation_interval_ms}ms ({self.rotating_time})")
# Stop any existing timer
if self.template_rotation_timer and self.template_rotation_timer.isActive():
self.template_rotation_timer.stop()
# Start the rotation timer
self.template_rotation_timer.start(rotation_interval_ms)
self.current_template_index = 0 # Reset to first template
except Exception as e:
logger.error(f"Failed to start template rotation: {e}")
def _rotate_template(self):
"""Rotate to the next template in the sequence"""
try:
if not self.template_sequence or len(self.template_sequence) == 0:
logger.debug("No template sequence available for rotation")
return
# Move to next template
self.current_template_index = (self.current_template_index + 1) % len(self.template_sequence)
next_template = self.template_sequence[self.current_template_index]['name']
logger.info(f"Rotating to template: {next_template} (index {self.current_template_index})")
# Load the new template
if hasattr(self, 'window_overlay') and isinstance(self.window_overlay, OverlayWebView):
self.window_overlay.load_template(next_template)
logger.info(f"Template rotated to: {next_template}")
else:
logger.warning("No WebEngine overlay available for template rotation")
except Exception as e:
logger.error(f"Failed to rotate template: {e}")
def open_file_dialog(self):
"""Open file dialog to select video"""
from PyQt6.QtWidgets import QFileDialog
......@@ -1259,6 +1341,14 @@ class PlayerWindow(QMainWindow):
self.infinite_loop = loop_data.get('infinite_loop', False) or loop_data.get('continuous_playback', False)
self.loop_count = loop_data.get('loop_count', 0)
# Handle template rotation for intro videos
self.template_sequence = loop_data.get('template_sequence', [])
self.rotating_time = loop_data.get('rotating_time', '05:00')
self.current_template_index = 0
if self.template_sequence:
logger.info(f"TEMPLATE ROTATION ENABLED: {len(self.template_sequence)} templates, rotating every {self.rotating_time}")
if self.infinite_loop or self.loop_count == -1:
self.infinite_loop = True
logger.info("INFINITE LOOP MODE ENABLED")
......@@ -1267,11 +1357,14 @@ class PlayerWindow(QMainWindow):
self.current_loop_iteration = 0
else:
# No loop data - disable looping
# No loop data - disable looping and template rotation
self.loop_enabled = False
self.infinite_loop = False
self.loop_count = 0
self.current_loop_iteration = 0
self.template_sequence = []
self.rotating_time = '05:00'
self.current_template_index = 0
with QMutexLocker(self.mutex):
# Handle both absolute and relative file paths
......@@ -1359,6 +1452,10 @@ class PlayerWindow(QMainWindow):
# Update overlay safely - handles both native and WebEngine
QTimer.singleShot(1000, lambda: self._update_overlay_safe(overlay_view, overlay_data))
# Start template rotation timer if enabled
if self.template_sequence and len(self.template_sequence) > 1:
self._start_template_rotation()
if self.settings.auto_play:
self.media_player.play()
......@@ -1407,6 +1504,11 @@ class PlayerWindow(QMainWindow):
with QMutexLocker(self.mutex):
self.media_player.stop()
# Stop template rotation timer
if self.template_rotation_timer and self.template_rotation_timer.isActive():
self.template_rotation_timer.stop()
logger.debug("Template rotation timer stopped")
def seek_to_position(self, percentage: int):
"""Seek to position (percentage) (thread-safe)"""
with QMutexLocker(self.mutex):
......@@ -1495,13 +1597,26 @@ class PlayerWindow(QMainWindow):
self._update_overlay_safe(overlay_view, status_data)
elif status == QMediaPlayer.MediaStatus.EndOfMedia:
# Handle end of media for loop functionality
# Handle end of media for loop functionality and match video completion
logger.debug(f"LOOP DEBUG: END OF MEDIA DETECTED!")
logger.debug(f"LOOP DEBUG: Loop enabled: {self.loop_enabled}")
logger.debug(f"LOOP DEBUG: Infinite loop: {self.infinite_loop}")
logger.debug(f"LOOP DEBUG: Current iteration: {self.current_loop_iteration}")
logger.debug(f"LOOP DEBUG: Loop count: {self.loop_count}")
logger.debug(f"MATCH DEBUG: Is playing match video: {self.is_playing_match_video}")
# Check if this is the end of a match video
if self.is_playing_match_video:
logger.info(f"MATCH DEBUG: Match video ended - sending PLAY_VIDEO_MATCH_DONE")
self._send_match_video_done_message()
# Reset match video tracking
self.is_playing_match_video = False
self.current_match_id = None
self.current_match_video_filename = None
self.current_fixture_id = None
return
# Handle loop functionality for intro videos
if self.loop_enabled:
logger.debug(f"LOOP DEBUG: Processing loop restart logic...")
if self.infinite_loop:
......@@ -1622,6 +1737,11 @@ class PlayerWindow(QMainWindow):
self.media_player.stop()
self.thread_pool.waitForDone(3000) # Wait up to 3 seconds for threads
# Stop template rotation timer
if self.template_rotation_timer and self.template_rotation_timer.isActive():
self.template_rotation_timer.stop()
logger.debug("Template rotation timer stopped on window close")
# Close overlay window
if hasattr(self, 'overlay_window') and self.overlay_window:
self.overlay_window.close()
......@@ -1718,7 +1838,7 @@ class PlayerWindow(QMainWindow):
"""Update overlay data safely, handling both native and WebEngine overlays"""
try:
# Check video state during overlay update
if hasattr(self, 'media_player'):
if hasattr(self, 'media_player') and self.debug_overlay:
video_state = self.media_player.playbackState()
logger.debug(f"GREEN SCREEN DEBUG: Video state during overlay update: {video_state}")
logger.debug(f"GREEN SCREEN DEBUG: Video position during overlay update: {self.media_player.position()}")
......@@ -1729,51 +1849,64 @@ class PlayerWindow(QMainWindow):
logger.debug("No valid data to send to overlay after cleaning")
return False
if self.debug_overlay:
data_keys = list(cleaned_data.keys()) if isinstance(cleaned_data, dict) else []
logger.debug(f"GREEN SCREEN DEBUG: About to update overlay with {len(cleaned_data)} data items: {data_keys}")
if self._is_native_overlay(overlay_view):
# Native overlay - always ready, update immediately
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Updating native overlay")
overlay_view.update_overlay_data(cleaned_data)
if self.debug_overlay:
logger.debug("Native overlay updated successfully")
logger.debug(f"GREEN SCREEN DEBUG: Native overlay update completed")
return True
elif isinstance(overlay_view, OverlayWebView):
# WebEngine overlay - check readiness first
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Checking WebEngine overlay readiness")
logger.debug(f"GREEN SCREEN DEBUG: WebEngine URL: {overlay_view.url().toString()}")
logger.debug(f"GREEN SCREEN DEBUG: WebEngine visible: {overlay_view.isVisible()}")
# CRITICAL FIX: Ensure WebEngine overlay visibility before update
if not overlay_view.isVisible():
if self.debug_overlay:
logger.debug(f"GREEN SCREEN FIX: WebEngine overlay not visible, forcing visibility recovery")
overlay_view.show()
overlay_view.raise_()
if self.debug_overlay:
logger.debug(f"GREEN SCREEN FIX: WebEngine overlay visibility forced during update")
# Also ensure parent overlay window is visible
if hasattr(self, 'overlay_window') and self.overlay_window and not self.overlay_window.isVisible():
if self.debug_overlay:
logger.debug(f"GREEN SCREEN FIX: Parent overlay window not visible, forcing visibility")
self.overlay_window.show()
self.overlay_window.raise_()
if self.debug_overlay:
logger.debug(f"GREEN SCREEN FIX: Parent overlay window visibility forced")
if self._is_webengine_ready(overlay_view):
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: WebEngine ready, updating overlay")
overlay_view.update_overlay_data(cleaned_data)
if self.debug_overlay:
logger.debug("WebEngine overlay updated successfully")
logger.debug(f"GREEN SCREEN DEBUG: WebEngine overlay update completed")
return True
else:
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: WebEngine not ready, skipping update")
logger.debug("WebEngine overlay not ready, skipping update")
return False
else:
logger.warning(f"Unknown overlay type: {type(overlay_view)}")
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Unknown overlay type: {type(overlay_view)}")
return False
except Exception as e:
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Overlay update failed: {e}")
logger.error(f"Failed to update overlay safely: {e}")
return False
......@@ -1815,15 +1948,20 @@ class QtVideoPlayer(QObject):
# Signal for cross-thread video playback
play_video_signal = pyqtSignal(str, dict)
def __init__(self, message_bus: MessageBus, settings: QtConfig):
def __init__(self, message_bus: MessageBus, settings: QtConfig, debug_player: bool = False, debug_overlay: bool = False):
super().__init__()
self.name = "qt_player"
self.message_bus = message_bus
self.settings = settings
self.debug_player = debug_player
self.debug_overlay = debug_overlay
self.app: Optional[QApplication] = None
self.window: Optional[PlayerWindow] = None
self.mutex = QMutex()
# Set web dashboard URL for API calls
self.web_dashboard_url = "http://localhost:5000" # Default web dashboard URL
# Register message queue
logger.info(f"Registering QtVideoPlayer component with message bus - name: '{self.name}'")
self.message_queue = self.message_bus.register_component(self.name)
......@@ -1886,8 +2024,8 @@ class QtVideoPlayer(QObject):
# Linux-specific application settings
self._configure_linux_app_settings()
# Create player window with message bus reference
self.window = PlayerWindow(self.settings, self.message_bus)
# Create player window with message bus reference and debug settings
self.window = PlayerWindow(self.settings, self.message_bus, debug_overlay=self.debug_overlay)
# CRITICAL: Connect signal to slot for cross-thread video playback
self.play_video_signal.connect(self.window.play_video, Qt.ConnectionType.QueuedConnection)
......@@ -1938,6 +2076,8 @@ class QtVideoPlayer(QObject):
self.message_bus.subscribe(self.name, MessageType.TEMPLATE_CHANGE, self._handle_template_change)
self.message_bus.subscribe(self.name, MessageType.OVERLAY_UPDATE, self._handle_overlay_update)
self.message_bus.subscribe(self.name, MessageType.STATUS_REQUEST, self._handle_status_request)
self.message_bus.subscribe(self.name, MessageType.START_INTRO, self._handle_start_intro)
self.message_bus.subscribe(self.name, MessageType.PLAY_VIDEO_MATCH, self._handle_play_video_match)
logger.info("QtPlayer subscriptions completed successfully")
# Delay loading default overlay to allow JavaScript initialization
......@@ -2225,6 +2365,7 @@ class QtVideoPlayer(QObject):
break
messages_processed += 1
if self.debug_player:
logger.info(f"QtPlayer RECEIVED message: {message.type.value} from {message.sender}")
# Don't log full message data to avoid cluttering logs with HTML content
logger.debug(f"Message data keys: {list(message.data.keys()) if isinstance(message.data, dict) else 'non-dict data'}")
......@@ -2315,37 +2456,56 @@ class QtVideoPlayer(QObject):
def _process_message(self, message: Message):
"""Process received message by routing to appropriate handlers"""
try:
if self.debug_player:
logger.info(f"QtPlayer processing message type: {message.type.value}")
# Route messages to appropriate handlers
if message.type == MessageType.VIDEO_PLAY:
if self.debug_player:
logger.info("Calling _handle_video_play handler")
self._handle_video_play(message)
elif message.type == MessageType.VIDEO_PAUSE:
if self.debug_player:
logger.info("Calling _handle_video_pause handler")
self._handle_video_pause(message)
elif message.type == MessageType.VIDEO_STOP:
if self.debug_player:
logger.info("Calling _handle_video_stop handler")
self._handle_video_stop(message)
elif message.type == MessageType.VIDEO_SEEK:
if self.debug_player:
logger.info("Calling _handle_video_seek handler")
self._handle_video_seek(message)
elif message.type == MessageType.VIDEO_VOLUME:
if self.debug_player:
logger.info("Calling _handle_video_volume handler")
self._handle_video_volume(message)
elif message.type == MessageType.VIDEO_FULLSCREEN:
if self.debug_player:
logger.info("Calling _handle_video_fullscreen handler")
self._handle_video_fullscreen(message)
elif message.type == MessageType.TEMPLATE_CHANGE:
if self.debug_player:
logger.info("Calling _handle_template_change handler")
self._handle_template_change(message)
elif message.type == MessageType.OVERLAY_UPDATE:
if self.debug_player:
logger.info("Calling _handle_overlay_update handler")
self._handle_overlay_update(message)
elif message.type == MessageType.STATUS_REQUEST:
if self.debug_player:
logger.info("Calling _handle_status_request handler")
self._handle_status_request(message)
elif message.type == MessageType.START_INTRO:
if self.debug_player:
logger.info("Calling _handle_start_intro handler")
self._handle_start_intro(message)
elif message.type == MessageType.PLAY_VIDEO_MATCH:
if self.debug_player:
logger.info("Calling _handle_play_video_match handler")
self._handle_play_video_match(message)
else:
if self.debug_player:
logger.warning(f"No handler for message type: {message.type.value}")
except Exception as e:
......@@ -2519,6 +2679,7 @@ class QtVideoPlayer(QObject):
reload_template = template_data.get("reload_template", False)
load_specific_template = template_data.get("load_specific_template", "")
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Template change message received")
logger.debug(f"GREEN SCREEN DEBUG: Template name: {template_name}")
logger.debug(f"GREEN SCREEN DEBUG: Reload template: {reload_template}")
......@@ -2528,14 +2689,14 @@ class QtVideoPlayer(QObject):
overlay_view = self.window.window_overlay
# Check video player state before template change
if hasattr(self.window, 'media_player'):
if hasattr(self.window, 'media_player') and self.debug_overlay:
video_state = self.window.media_player.playbackState()
logger.debug(f"GREEN SCREEN DEBUG: Video playback state during template change: {video_state}")
logger.debug(f"GREEN SCREEN DEBUG: Video position: {self.window.media_player.position()}")
logger.debug(f"GREEN SCREEN DEBUG: Video duration: {self.window.media_player.duration()}")
# Check overlay window transparency state
if hasattr(self.window, 'overlay_window'):
if hasattr(self.window, 'overlay_window') and self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Overlay window geometry: {self.window.overlay_window.geometry()}")
logger.debug(f"GREEN SCREEN DEBUG: Overlay window visible: {self.window.overlay_window.isVisible()}")
......@@ -2543,23 +2704,28 @@ class QtVideoPlayer(QObject):
video_widget = None
if hasattr(self.window, 'video_widget') and hasattr(self.window.video_widget, 'get_video_widget'):
video_widget = self.window.video_widget.get_video_widget()
if self.debug_overlay:
logger.debug(f"GREEN SCREEN FIX: Video widget state before template change - visible: {video_widget.isVisible() if video_widget else 'N/A'}")
# Load specific template if requested and using WebEngine overlay
if load_specific_template and isinstance(overlay_view, OverlayWebView):
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: About to load specific template: {load_specific_template}")
logger.debug(f"GREEN SCREEN FIX: Protecting video rendering during template load")
overlay_view.load_template(load_specific_template)
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Specific template load initiated")
# Otherwise reload current template if requested and using WebEngine overlay
elif reload_template and isinstance(overlay_view, OverlayWebView):
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: About to reload current template")
logger.debug(f"GREEN SCREEN FIX: Protecting video rendering during template reload")
overlay_view.reload_current_template()
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Current template reload initiated")
# CRITICAL FIX: Force video widget refresh after template change
if video_widget:
if video_widget and self.debug_overlay:
logger.debug(f"GREEN SCREEN FIX: Forcing video widget refresh after template change")
video_widget.repaint()
video_widget.update()
......@@ -2584,6 +2750,7 @@ class QtVideoPlayer(QObject):
if data_to_send:
# Log data summary instead of full content to avoid cluttering logs with HTML
if self.debug_overlay:
data_keys = list(data_to_send.keys())
logger.debug(f"GREEN SCREEN DEBUG: Sending overlay data with keys: {data_keys}")
# Validate and clean template_data before sending to overlay
......@@ -2593,9 +2760,11 @@ class QtVideoPlayer(QObject):
else:
logger.debug("Template data contained only null/undefined values, skipping update")
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Template change handler completed")
except Exception as e:
if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Template change handler failed: {e}")
logger.error(f"Failed to handle template change: {e}")
......@@ -2625,6 +2794,370 @@ class QtVideoPlayer(QObject):
except Exception as e:
logger.error(f"Failed to handle status request: {e}")
def _handle_start_intro(self, message: Message):
"""Handle START_INTRO message - find and play intro video with configured template sequence"""
try:
logger.info("Handling START_INTRO message")
# Get match_id and fixture_id from message
match_id = message.data.get("match_id")
fixture_id = message.data.get("fixture_id")
# Find the correct intro video file
intro_video_path = self._find_intro_video_file(match_id)
if intro_video_path:
logger.info(f"Found intro video: {intro_video_path}")
# Load intro templates configuration
intro_templates = self._load_intro_templates_config()
if intro_templates and intro_templates.get('templates'):
# Use configured template sequence
template_sequence = intro_templates['templates']
logger.info(f"Using configured template sequence: {[t['name'] for t in template_sequence]}")
# Set up loop control for intro video with template rotation
loop_data = {
'infinite_loop': True, # Loop indefinitely until PLAY_VIDEO_MATCH
'continuous_playback': True,
'template_sequence': template_sequence,
'rotating_time': intro_templates.get('rotating_time', '05:00')
}
# Start with first template in sequence
first_template = template_sequence[0]['name'] if template_sequence else 'news_template'
# Play the intro video with template rotation
if self.window:
self.window.play_video(
str(intro_video_path),
template_data={"title": "Intro Video", "subtitle": "Preparing for match..."},
template_name=first_template,
loop_data=loop_data
)
logger.info(f"Intro video started with template rotation: {first_template}")
else:
logger.error("No window available for intro video playback")
else:
# Fallback to default behavior if no templates configured
logger.warning("No intro templates configured, using default news_template")
loop_data = {
'infinite_loop': True,
'continuous_playback': True
}
if self.window:
self.window.play_video(
str(intro_video_path),
template_data={"title": "Intro Video", "subtitle": "Preparing for match..."},
template_name="news_template",
loop_data=loop_data
)
logger.info("Intro video started in loop mode with default template")
else:
logger.error("No window available for intro video playback")
else:
logger.warning("No intro video found, skipping intro playback")
except Exception as e:
logger.error(f"Failed to handle START_INTRO message: {e}")
def _handle_play_video_match(self, message: Message):
"""Handle PLAY_VIDEO_MATCH message - stop intro loop and play match video"""
try:
logger.info("Handling PLAY_VIDEO_MATCH message")
match_id = message.data.get("match_id")
video_filename = message.data.get("video_filename")
fixture_id = message.data.get("fixture_id")
if not match_id or not video_filename:
logger.error("Missing match_id or video_filename in PLAY_VIDEO_MATCH message")
return
# Stop the current intro video loop and template rotation
if self.window and hasattr(self.window, 'media_player'):
logger.info("Stopping intro video loop and template rotation")
self.window.stop_playback()
# Explicitly stop template rotation timer
if hasattr(self.window, 'template_rotation_timer') and self.window.template_rotation_timer and self.window.template_rotation_timer.isActive():
self.window.template_rotation_timer.stop()
logger.info("Template rotation timer stopped for match video")
# Find the match video file from the ZIP
match_video_path = self._find_match_video_file(match_id, video_filename)
if match_video_path:
logger.info(f"Found match video: {match_video_path}")
# Set match video tracking flags
self.is_playing_match_video = True
self.current_match_id = match_id
self.current_match_video_filename = video_filename
self.current_fixture_id = fixture_id
# Reset loop state for match videos (they don't loop)
self.loop_enabled = False
self.infinite_loop = False
self.loop_count = 0
self.current_loop_iteration = 0
# Play the match video (no looping)
if self.window:
self.window.play_video(
str(match_video_path),
template_data={"title": f"Match {match_id}", "subtitle": "Live Action"},
template_name="news_template",
loop_data=None # No looping for match videos
)
logger.info(f"Match video started: {video_filename}")
else:
logger.error("No window available for match video playback")
else:
logger.error(f"Match video not found: {video_filename} for match {match_id}")
except Exception as e:
logger.error(f"Failed to handle PLAY_VIDEO_MATCH message: {e}")
def _load_intro_templates_config(self) -> Optional[Dict[str, Any]]:
"""Load intro templates configuration from database with retry mechanism"""
try:
from ..database.manager import DatabaseManager
from ..database.models import GameConfigModel
import json
import time
logger.info("QtPlayer: Starting to load intro templates configuration")
# Retry mechanism for database access during startup
max_retries = 3
retry_delay = 0.5 # 500ms delay between retries
for attempt in range(max_retries):
try:
logger.debug(f"QtPlayer: Database access attempt {attempt + 1}")
# Get database manager from message bus or create one
db_manager = None
if hasattr(self, 'message_bus') and self.message_bus:
logger.debug("QtPlayer: Trying to get db_manager from message bus")
# Try to get db_manager from web_dashboard component
try:
web_dashboard_queue = self.message_bus._queues.get('web_dashboard')
if web_dashboard_queue and hasattr(web_dashboard_queue, 'component'):
component = web_dashboard_queue.component
if hasattr(component, 'db_manager'):
db_manager = component.db_manager
logger.info(f"QtPlayer: Got db_manager from web_dashboard component (attempt {attempt + 1})")
except Exception as e:
logger.debug(f"QtPlayer: Could not get db_manager from message bus (attempt {attempt + 1}): {e}")
if not db_manager:
logger.debug("QtPlayer: Creating database manager directly")
# Fallback: create database manager directly
from ..config.settings import get_user_data_dir
db_path = get_user_data_dir() / "mbetterclient.db"
logger.debug(f"QtPlayer: Database path: {db_path}")
db_manager = DatabaseManager(str(db_path))
# Initialize the database manager
if not db_manager.initialize():
logger.warning(f"QtPlayer: Failed to initialize database manager in Qt player (attempt {attempt + 1})")
if attempt < max_retries - 1:
time.sleep(retry_delay)
continue
return self._get_default_intro_config()
logger.debug("QtPlayer: Database manager ready, getting session")
session = db_manager.get_session()
try:
logger.debug("QtPlayer: Querying for intro_templates_config")
# Get intro templates configuration from database
intro_config = session.query(GameConfigModel).filter_by(
config_key='intro_templates_config'
).first()
if intro_config:
logger.debug(f"QtPlayer: Found intro config, value length: {len(intro_config.config_value)}")
try:
config = json.loads(intro_config.config_value)
logger.info(f"QtPlayer: Successfully loaded intro templates config from database: {len(config.get('templates', []))} templates")
return config
except (json.JSONDecodeError, TypeError) as e:
logger.warning(f"QtPlayer: Invalid JSON in intro templates config: {e}")
return self._get_default_intro_config()
else:
logger.info("QtPlayer: No intro templates config found in database, using defaults")
return self._get_default_intro_config()
finally:
session.close()
except Exception as e:
logger.warning(f"QtPlayer: Database access attempt {attempt + 1} failed: {str(e)}")
if attempt < max_retries - 1:
time.sleep(retry_delay)
continue
else:
logger.error(f"QtPlayer: All database access attempts failed: {str(e)}")
return self._get_default_intro_config()
# If we get here, all retries failed
logger.error("QtPlayer: All retries exhausted, returning defaults")
return self._get_default_intro_config()
except Exception as e:
logger.error(f"QtPlayer: Error loading intro templates from database: {str(e)}")
return self._get_default_intro_config()
def _get_default_intro_config(self) -> Dict[str, Any]:
"""Get default intro templates configuration"""
return {
'templates': [],
'default_show_time': '00:30',
'rotating_time': '05:00'
}
def _find_intro_video_file(self, match_id: int) -> Optional[Path]:
"""Find the correct intro video file based on priority"""
try:
# Priority 1: Check for INTRO.mp4 in the unzipped ZIP file of the match
if match_id:
from ..database.manager import DatabaseManager
from ..config.settings import get_user_data_dir
# Get database manager (assuming it's available via message bus or settings)
# For now, we'll use a simplified approach
user_data_dir = get_user_data_dir()
temp_dir_pattern = f"match_{match_id}_"
# Look for temp directories created by _unzip_match_zip_file
import tempfile
import os
temp_base = Path(tempfile.gettempdir())
for temp_dir in temp_base.glob(f"{temp_dir_pattern}*"):
if temp_dir.is_dir():
intro_file = temp_dir / "INTRO.mp4"
if intro_file.exists():
logger.info(f"Found INTRO.mp4 in match ZIP: {intro_file}")
return intro_file
# Priority 2: Check for uploaded intro video
# This would need to be implemented based on how uploaded intro videos are stored
# For now, we'll skip this and go to priority 3
# Priority 3: Fallback to INTRO.mp4 in assets directory
assets_dir = Path(__file__).parent.parent / "assets"
assets_intro = assets_dir / "INTRO.mp4"
if assets_intro.exists():
logger.info(f"Using fallback INTRO.mp4 from assets: {assets_intro}")
return assets_intro
logger.warning("No intro video found in any location")
return None
except Exception as e:
logger.error(f"Failed to find intro video file: {e}")
return None
def _find_match_video_file(self, match_id: int, video_filename: str) -> Optional[Path]:
"""Find the match video file from the unzipped ZIP"""
try:
import tempfile
from pathlib import Path
# Look for temp directories created by _unzip_match_zip_file
temp_base = Path(tempfile.gettempdir())
temp_dir_pattern = f"match_{match_id}_"
for temp_dir in temp_base.glob(f"{temp_dir_pattern}*"):
if temp_dir.is_dir():
video_file = temp_dir / video_filename
if video_file.exists():
logger.info(f"Found match video: {video_file}")
return video_file
logger.warning(f"Match video not found: {video_filename} for match {match_id}")
return None
except Exception as e:
logger.error(f"Failed to find match video file: {e}")
return None
def _start_template_rotation(self):
"""Start the template rotation timer"""
try:
if not self.template_sequence or len(self.template_sequence) <= 1:
logger.debug("Template rotation not needed - insufficient templates")
return
# Parse rotating time (format: MM:SS)
try:
minutes, seconds = map(int, self.rotating_time.split(':'))
rotation_interval_ms = (minutes * 60 + seconds) * 1000 # Convert to milliseconds
except (ValueError, AttributeError):
logger.warning(f"Invalid rotating_time format '{self.rotating_time}', using default 5 minutes")
rotation_interval_ms = 5 * 60 * 1000 # Default 5 minutes
logger.info(f"Starting template rotation every {rotation_interval_ms}ms ({self.rotating_time})")
# Stop any existing timer
if self.template_rotation_timer and self.template_rotation_timer.isActive():
self.template_rotation_timer.stop()
# Start the rotation timer
self.template_rotation_timer.start(rotation_interval_ms)
self.current_template_index = 0 # Reset to first template
except Exception as e:
logger.error(f"Failed to start template rotation: {e}")
def _rotate_template(self):
"""Rotate to the next template in the sequence"""
try:
if not self.template_sequence or len(self.template_sequence) == 0:
logger.debug("No template sequence available for rotation")
return
# Move to next template
self.current_template_index = (self.current_template_index + 1) % len(self.template_sequence)
next_template = self.template_sequence[self.current_template_index]['name']
logger.info(f"Rotating to template: {next_template} (index {self.current_template_index})")
# Load the new template
if hasattr(self, 'window_overlay') and isinstance(self.window_overlay, OverlayWebView):
self.window_overlay.load_template(next_template)
logger.info(f"Template rotated to: {next_template}")
else:
logger.warning("No WebEngine overlay available for template rotation")
except Exception as e:
logger.error(f"Failed to rotate template: {e}")
def _send_match_video_done_message(self):
"""Send PLAY_VIDEO_MATCH_DONE message when match video finishes"""
try:
if (self.current_match_id is not None and
self.current_match_video_filename is not None):
from ..core.message_bus import MessageBuilder
done_message = MessageBuilder.play_video_match_done(
sender=self.name,
match_id=self.current_match_id,
video_filename=self.current_match_video_filename,
fixture_id=self.current_fixture_id
)
self.message_bus.publish(done_message, broadcast=True)
logger.info(f"Sent PLAY_VIDEO_MATCH_DONE for match {self.current_match_id}, video {self.current_match_video_filename}")
except Exception as e:
logger.error(f"Failed to send match video done message: {e}")
def _do_status_request(self, message: Message):
"""Execute status request on main thread"""
try:
......
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Text Message Overlay</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: transparent;
overflow: hidden;
width: 100vw;
height: 100vh;
position: relative;
}
/* Debug indicator to verify CSS is loaded */
body::before {
content: 'Text Message Overlay v1.0 loaded';
position: absolute;
top: 5px;
left: 5px;
color: rgba(255,255,255,0.5);
font-size: 10px;
z-index: 9999;
}
.overlay-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.message-panel {
background: rgba(0, 123, 255, 0.40);
border-radius: 20px;
padding: 40px 60px;
min-width: 500px;
max-width: 80%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 2px solid rgba(255, 255, 255, 0.2);
opacity: 0;
transform: translateY(-30px);
animation: slideInDown 1s ease-out forwards;
}
.message-title {
color: #ffffff;
font-size: 32px;
font-weight: bold;
text-align: center;
margin-bottom: 20px;
text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.8);
opacity: 0;
animation: titleFadeIn 1.5s ease-out 0.5s forwards;
}
.message-content {
color: rgba(255, 255, 255, 0.95);
font-size: 20px;
text-align: center;
line-height: 1.6;
max-width: 100%;
word-wrap: break-word;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.6);
opacity: 0;
animation: contentFadeIn 1.5s ease-out 1s forwards;
}
.message-icon {
font-size: 48px;
color: #ffffff;
margin-bottom: 20px;
text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.8);
opacity: 0;
animation: iconBounce 2s ease-out 0.2s forwards;
}
/* Animations */
@keyframes slideInDown {
0% {
opacity: 0;
transform: translateY(-50px) scale(0.8);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes titleFadeIn {
0% {
opacity: 0;
transform: translateY(-10px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes contentFadeIn {
0% {
opacity: 0;
transform: translateY(10px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes iconBounce {
0% {
opacity: 0;
transform: scale(0.5);
}
50% {
transform: scale(1.2);
}
100% {
opacity: 1;
transform: scale(1);
}
}
/* Background effects */
.message-panel::before {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: linear-gradient(45deg,
transparent, rgba(255, 255, 255, 0.2), transparent,
rgba(255, 255, 255, 0.2), transparent);
border-radius: 20px;
animation: shimmer 3s ease-in-out infinite;
z-index: -1;
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(200%);
}
}
/* Responsive Design */
@media (max-width: 1200px) {
.message-panel {
padding: 30px 50px;
min-width: 400px;
}
.message-title {
font-size: 28px;
}
.message-content {
font-size: 18px;
}
.message-icon {
font-size: 40px;
}
}
@media (max-width: 800px) {
.message-panel {
padding: 25px 35px;
min-width: 90%;
max-width: 95%;
}
.message-title {
font-size: 24px;
margin-bottom: 15px;
}
.message-content {
font-size: 16px;
line-height: 1.5;
}
.message-icon {
font-size: 36px;
margin-bottom: 15px;
}
}
</style>
</head>
<body>
<div class="overlay-container">
<div class="message-panel" id="messagePanel">
<div class="message-icon" id="messageIcon">📢</div>
<div class="message-title" id="messageTitle">Announcement</div>
<div class="message-content" id="messageContent">This is a custom message from the system.</div>
</div>
</div>
<script>
// Global variables for overlay data handling
let overlayData = {};
let currentTitle = 'Announcement';
let currentMessage = 'This is a custom message from the system.';
let currentIcon = '📢';
// Function to update overlay data (called by Qt WebChannel)
function updateOverlayData(data) {
console.log('Received text overlay data:', data);
overlayData = data || {};
if (data && data.title) {
currentTitle = data.title;
}
if (data && data.message) {
currentMessage = data.message;
}
if (data && data.icon) {
currentIcon = data.icon;
}
updateMessageDisplay();
}
// Update the message display
function updateMessageDisplay() {
const titleElement = document.getElementById('messageTitle');
const contentElement = document.getElementById('messageContent');
const iconElement = document.getElementById('messageIcon');
// Update content
titleElement.textContent = currentTitle;
contentElement.textContent = currentMessage;
iconElement.textContent = currentIcon;
// Restart animations
restartAnimations();
}
// Restart animations
function restartAnimations() {
const messagePanel = document.getElementById('messagePanel');
// Reset animations by removing and re-adding classes
messagePanel.style.animation = 'none';
messagePanel.offsetHeight; // Trigger reflow
messagePanel.style.animation = null;
}
// Set message for testing/demo
function setMessage(title, message, icon = '📢') {
currentTitle = title;
currentMessage = message;
currentIcon = icon;
updateMessageDisplay();
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('Text message overlay initialized');
updateMessageDisplay();
});
// Qt WebChannel initialization (when available)
if (typeof QWebChannel !== 'undefined') {
new QWebChannel(qt.webChannelTransport, function(channel) {
console.log('WebChannel initialized for text message overlay');
// Connect to overlay object if available
if (channel.objects.overlay) {
channel.objects.overlay.dataChanged.connect(function(data) {
updateOverlayData(data);
});
// Get initial data
if (channel.objects.overlay.getCurrentData) {
channel.objects.overlay.getCurrentData(function(data) {
updateOverlayData(data);
});
}
}
});
}
// Export functions for external use
window.setMessage = setMessage;
window.updateOverlayData = updateOverlayData;
</script>
<!--
IMPORTANT: When creating or editing custom templates, always maintain these two script tags:
1. qrc:///qtwebchannel/qwebchannel.js - Required for Qt WebChannel communication
2. overlay://overlay.js - Required for overlay functionality and data updates
These scripts enable communication between the Qt application and the overlay template.
Without them, the template will not receive data updates or function properly.
NOTE: When editing this template or creating new ones, never remove these script sources!
The overlay:// custom scheme ensures JavaScript files work for both built-in and uploaded templates.
-->
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script src="overlay://overlay.js"></script>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Match Overlay</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: transparent !important;
background-color: transparent !important;
overflow: hidden;
width: 100vw;
height: 100vh;
position: relative;
}
/* Debug indicator to verify CSS is loaded */
body::before {
content: 'Match Overlay v1.0 loaded';
position: absolute;
top: 5px;
left: 5px;
color: rgba(255,255,255,0.5);
font-size: 10px;
z-index: 9999;
}
.overlay-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.fixtures-panel {
background: rgba(0, 123, 255, 0.40);
border-radius: 20px;
padding: 30px;
max-width: 90%;
max-height: 80%;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 2px solid rgba(255, 255, 255, 0.1);
opacity: 0;
animation: fadeInScale 1s ease-out forwards;
}
.fixtures-title {
color: white;
font-size: 28px;
font-weight: bold;
text-align: center;
margin-bottom: 25px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
.fixtures-table {
width: 100%;
border-collapse: collapse;
color: white;
font-size: 16px;
background: transparent;
}
.fixtures-table th {
padding: 15px 10px;
text-align: center;
background: rgba(255, 255, 255, 0.1);
font-weight: bold;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
border-radius: 8px;
margin: 2px;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
.fixtures-table td {
padding: 12px 10px;
text-align: center;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
margin: 1px;
transition: background-color 0.3s ease;
}
.fixtures-table tbody tr:hover td {
background: rgba(255, 255, 255, 0.15);
}
.match-info {
font-weight: bold;
color: #ffffff;
}
.fighter-names {
font-size: 14px;
color: #e6f3ff;
}
.venue-info {
font-size: 13px;
color: #ccddff;
font-style: italic;
}
.odds-value {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
padding: 4px 8px;
font-weight: bold;
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
}
.odds-value:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
.under-over {
background: rgba(40, 167, 69, 0.3);
border-color: rgba(40, 167, 69, 0.5);
}
.loading-message {
text-align: center;
color: white;
font-size: 18px;
padding: 40px;
}
.no-matches {
text-align: center;
color: rgba(255, 255, 255, 0.8);
font-size: 16px;
padding: 30px;
font-style: italic;
}
.fixture-info {
text-align: center;
color: rgba(255, 255, 255, 0.9);
font-size: 14px;
margin-bottom: 20px;
font-style: italic;
}
.venue-display {
font-size: 24px;
color: rgba(255, 255, 255, 0.9);
text-align: center;
margin-bottom: 30px;
font-style: italic;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
/* Animations */
@keyframes fadeInScale {
0% {
opacity: 0;
transform: scale(0.8);
}
100% {
opacity: 1;
transform: scale(1);
}
}
/* Responsive Design */
@media (max-width: 1200px) {
.fixtures-panel {
padding: 20px;
max-width: 95%;
}
.fixtures-title {
font-size: 24px;
}
.fixtures-table {
font-size: 14px;
}
.fixtures-table th,
.fixtures-table td {
padding: 8px 6px;
}
}
@media (max-width: 800px) {
.fixtures-panel {
padding: 15px;
max-width: 98%;
max-height: 90%;
}
.fixtures-title {
font-size: 20px;
margin-bottom: 15px;
}
.fixtures-table {
font-size: 12px;
}
.fixtures-table th,
.fixtures-table td {
padding: 6px 4px;
}
.odds-value {
padding: 2px 4px;
font-size: 11px;
}
}
/* Scrollbar styling */
.fixtures-panel::-webkit-scrollbar {
width: 8px;
}
.fixtures-panel::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.fixtures-panel::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
.fixtures-panel::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
</style>
</head>
<body>
<div class="overlay-container">
<div class="fixtures-panel" id="fixturesPanel">
<div class="fixtures-title" id="matchTitle">Next Match</div>
<div class="venue-display" id="matchVenue">Venue</div>
<div class="loading-message" id="loadingMessage" style="display: none;">Loading match data...</div>
<div id="matchContent" style="display: none;">
<table class="fixtures-table" id="outcomesTable">
<thead>
<tr id="outcomesHeader">
<!-- Headers will be populated by JavaScript -->
</tr>
</thead>
<tbody id="outcomesBody">
<!-- Content will be populated by JavaScript -->
</tbody>
</table>
</div>
<div class="no-matches" id="noMatches" style="display: none;">
No matches available for betting
</div>
</div>
</div>
<script>
// Global variables for overlay data handling
let overlayData = {};
let fixturesData = null;
let outcomesData = null;
// Web server configuration - will be set via WebChannel
let webServerBaseUrl = 'http://127.0.0.1:5001'; // Default fallback
// Function to update overlay data (called by Qt WebChannel)
function updateOverlayData(data) {
console.log('Received overlay data:', data);
overlayData = data || {};
// Update web server base URL if provided
if (data && data.webServerBaseUrl) {
webServerBaseUrl = data.webServerBaseUrl;
console.log('Updated web server base URL:', webServerBaseUrl);
}
// Check if we have fixtures data
if (data && data.fixtures) {
fixturesData = data.fixtures;
renderMatch();
} else {
// Fetch fixtures data from API
fetchFixturesData().then(() => {
renderMatch();
});
}
}
// Fetch fixtures data from the API
async function fetchFixturesData() {
try {
console.log('Fetching fixtures data from API...');
// Try multiple API endpoints with different authentication levels
const apiEndpoints = [
`${webServerBaseUrl}/api/cashier/pending-matches`,
`${webServerBaseUrl}/api/fixtures`,
`${webServerBaseUrl}/api/status` // Fallback to basic status endpoint
];
let apiData = null;
let usedEndpoint = null;
for (const endpoint of apiEndpoints) {
try {
console.log(`Trying API endpoint: ${endpoint}`);
const response = await fetch(endpoint, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include' // Include cookies for authentication
});
if (response.ok) {
const data = await response.json();
console.log(`API Response from ${endpoint}:`, data);
if (data.success) {
apiData = data;
usedEndpoint = endpoint;
break;
}
} else {
console.warn(`API endpoint ${endpoint} returned status ${response.status}`);
}
} catch (endpointError) {
console.warn(`Failed to fetch from ${endpoint}:`, endpointError);
continue;
}
}
if (apiData && apiData.matches && apiData.matches.length > 0) {
console.log(`Found ${apiData.matches.length} matches from ${usedEndpoint}`);
fixturesData = apiData.matches;
renderFixtures();
return Promise.resolve();
} else if (apiData && apiData.fixtures && apiData.fixtures.length > 0) {
// Handle fixtures endpoint format
console.log(`Found ${apiData.fixtures.length} fixtures from ${usedEndpoint}`);
// Convert fixtures to matches format
fixturesData = [];
apiData.fixtures.forEach(fixture => {
if (fixture.matches) {
fixturesData.push(...fixture.matches);
}
});
if (fixturesData.length > 0) {
renderFixtures();
return Promise.resolve();
}
}
// If we reach here, no valid data was found
console.log('No fixture data available from any API endpoint, will show fallback');
return Promise.reject('No API data available');
} catch (error) {
console.error('Error fetching fixtures data:', error);
return Promise.reject(error);
}
}
// Show fallback sample matches when API is not available
function showFallbackMatches() {
console.log('Showing fallback sample matches');
fixturesData = [
{
id: 1,
match_number: 1,
fighter1_township: 'John Doe',
fighter2_township: 'Mike Smith',
venue_kampala_township: 'Sports Arena',
outcomes: [
{ outcome_name: 'WIN1', outcome_value: 1.85 },
{ outcome_name: 'X', outcome_value: 3.20 },
{ outcome_name: 'WIN2', outcome_value: 2.10 },
{ outcome_name: 'UNDER', outcome_value: 1.75 },
{ outcome_name: 'OVER', outcome_value: 2.05 }
]
},
{
id: 2,
match_number: 2,
fighter1_township: 'Alex Johnson',
fighter2_township: 'Chris Brown',
venue_kampala_township: 'Championship Hall',
outcomes: [
{ outcome_name: 'WIN1', outcome_value: 2.20 },
{ outcome_name: 'X', outcome_value: 3.10 },
{ outcome_name: 'WIN2', outcome_value: 1.65 },
{ outcome_name: 'UNDER', outcome_value: 1.90 },
{ outcome_name: 'OVER', outcome_value: 1.95 }
]
}
];
renderFixtures();
}
// Show fallback only when absolutely necessary
function showFallbackWithDefaults(message) {
console.log('API failed, showing no matches message instead of fallback');
showNoMatches('No live matches available - API connection failed');
}
// Enhance matches with outcomes data by fetching match details for each
async function enhanceMatchesWithOutcomes() {
try {
console.log('Enhancing matches with outcomes data...');
// For each match, try to get its outcomes
for (let i = 0; i < fixturesData.length; i++) {
const match = fixturesData[i];
try {
// Try to get match outcomes from fixture details API
const response = await fetch(`${webServerBaseUrl}/api/fixtures/${match.fixture_id}`);
const fixtureData = await response.json();
if (fixtureData.success && fixtureData.matches) {
// Find this specific match in the fixture data
const matchWithOutcomes = fixtureData.matches.find(m => m.id === match.id);
if (matchWithOutcomes && matchWithOutcomes.outcomes) {
console.log(`Found ${matchWithOutcomes.outcomes.length} outcomes for match ${match.id}`);
fixturesData[i].outcomes = matchWithOutcomes.outcomes;
} else {
console.log(`No outcomes found for match ${match.id}, using defaults`);
fixturesData[i].outcomes = getDefaultOutcomes();
}
} else {
console.log(`Failed to get fixture details for match ${match.id}`);
fixturesData[i].outcomes = getDefaultOutcomes();
}
} catch (error) {
console.error(`Error fetching outcomes for match ${match.id}:`, error);
fixturesData[i].outcomes = getDefaultOutcomes();
}
}
console.log('Finished enhancing matches with outcomes');
} catch (error) {
console.error('Error enhancing matches with outcomes:', error);
}
}
// Get default outcomes when API data is not available
function getDefaultOutcomes() {
return [
{ outcome_name: 'WIN1', outcome_value: 1.85 },
{ outcome_name: 'X', outcome_value: 3.20 },
{ outcome_name: 'WIN2', outcome_value: 2.10 },
{ outcome_name: 'UNDER', outcome_value: 1.75 },
{ outcome_name: 'OVER', outcome_value: 2.05 }
];
}
// Render the focused match view (first match in bet status)
function renderMatch() {
const loadingMessage = document.getElementById('loadingMessage');
const matchContent = document.getElementById('matchContent');
const noMatches = document.getElementById('noMatches');
const matchTitle = document.getElementById('matchTitle');
const matchVenue = document.getElementById('matchVenue');
const outcomesHeader = document.getElementById('outcomesHeader');
const outcomesBody = document.getElementById('outcomesBody');
loadingMessage.style.display = 'none';
noMatches.style.display = 'none';
if (!fixturesData || fixturesData.length === 0) {
showNoMatches('No matches available for betting');
return;
}
// Find the first match with status 'bet'
const betMatch = fixturesData.find(match => match.status === 'bet');
if (!betMatch) {
showNoMatches('No matches currently available for betting');
return;
}
console.log('Rendering focused match:', betMatch);
// Update title and venue
const fighter1 = betMatch.fighter1_township || betMatch.fighter1 || 'Fighter 1';
const fighter2 = betMatch.fighter2_township || betMatch.fighter2 || 'Fighter 2';
matchTitle.textContent = `${fighter1} vs ${fighter2}`;
const venue = betMatch.venue_kampala_township || betMatch.venue || 'TBD';
matchVenue.textContent = venue;
// Get outcomes for this match
const outcomes = betMatch.outcomes || [];
if (outcomes.length === 0) {
console.log('No outcomes found for match, using defaults');
// Use default outcomes if none available
outcomes.push(...getDefaultOutcomes());
}
console.log(`Found ${outcomes.length} outcomes for match ${betMatch.id || betMatch.match_number}`);
// Sort outcomes: common ones first, then alphabetically
const sortedOutcomes = outcomes.sort((a, b) => {
// Handle both API formats
const aName = a.outcome_name || a.column_name || '';
const bName = b.outcome_name || b.column_name || '';
// Priority order for common outcomes
const priority = {
'WIN1': 1, 'X': 2, 'WIN2': 3, 'DRAW': 4,
'UNDER': 5, 'OVER': 6, 'KO1': 7, 'KO2': 8,
'PTS1': 9, 'PTS2': 10, 'DKO': 11, 'RET1': 12, 'RET2': 13
};
const aPriority = priority[aName] || 100;
const bPriority = priority[bName] || 100;
if (aPriority !== bPriority) {
return aPriority - bPriority;
}
return aName.localeCompare(bName);
});
// Create table header
outcomesHeader.innerHTML = sortedOutcomes.map(outcome => {
const outcomeName = outcome.outcome_name || outcome.column_name;
return `<th>${outcomeName}</th>`;
}).join('');
// Create table body with odds
outcomesBody.innerHTML = `
<tr>
${sortedOutcomes.map(outcome => {
const outcomeName = outcome.outcome_name || outcome.column_name;
const outcomeValue = outcome.outcome_value || outcome.float_value;
const isUnderOver = outcomeName === 'UNDER' || outcomeName === 'OVER';
const oddsClass = isUnderOver ? 'odds-value under-over' : 'odds-value';
const displayValue = outcomeValue !== undefined && outcomeValue !== null ? parseFloat(outcomeValue).toFixed(2) : '-';
return `<td><span class="${oddsClass}">${displayValue}</span></td>`;
}).join('')}
</tr>
`;
matchContent.style.display = 'block';
}
// Show no matches message
function showNoMatches(message) {
document.getElementById('loadingMessage').style.display = 'none';
document.getElementById('fixturesContent').style.display = 'none';
const noMatches = document.getElementById('noMatches');
noMatches.textContent = message;
noMatches.style.display = 'block';
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('Match overlay initialized - attempting to fetch next match data');
// Show loading message initially
document.getElementById('fixturesContent').style.display = 'none';
document.getElementById('noMatches').style.display = 'none';
document.getElementById('loadingMessage').style.display = 'block';
document.getElementById('loadingMessage').textContent = 'Loading next match data...';
// Start fetching real data immediately
fetchFixturesData().then(() => {
renderMatch();
// If API fails completely, show fallback data after a short delay
setTimeout(() => {
if (!fixturesData || fixturesData.length === 0) {
console.log('No data loaded after API attempts, forcing fallback display');
showFallbackMatches();
renderMatch();
}
}, 2000);
}).catch(() => {
console.log('API fetch failed, showing fallback data');
showFallbackMatches();
renderMatch();
});
// Refresh data every 30 seconds
setInterval(function() {
console.log('Refreshing match data...');
fetchFixturesData().then(() => {
renderMatch();
});
}, 30000);
});
// Qt WebChannel initialization (when available)
if (typeof QWebChannel !== 'undefined') {
new QWebChannel(qt.webChannelTransport, function(channel) {
console.log('WebChannel initialized for match overlay');
// Connect to overlay object if available
if (channel.objects.overlay) {
channel.objects.overlay.dataChanged.connect(function(data) {
updateOverlayData(data);
});
// Get initial data
if (channel.objects.overlay.getCurrentData) {
channel.objects.overlay.getCurrentData(function(data) {
updateOverlayData(data);
});
}
}
});
}
</script>
<!--
IMPORTANT: When creating or editing custom templates, always maintain these two script tags:
1. qrc:///qtwebchannel/qwebchannel.js - Required for Qt WebChannel communication
2. overlay://overlay.js - Required for overlay functionality and data updates
These scripts enable communication between the Qt application and the overlay template.
Without them, the template will not receive data updates or function properly.
NOTE: When editing this template or creating new ones, never remove these script sources!
The overlay:// custom scheme ensures JavaScript files work for both built-in and uploaded templates.
-->
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script src="overlay://overlay.js"></script>
</body>
</html>
\ No newline at end of file
......@@ -346,19 +346,8 @@ class DashboardAPI:
else:
logger.warning(f"Built-in templates directory not found: {builtin_templates_dir}")
# Ensure default template is always available
if not any(t["name"] == "default" for t in template_list):
template_list.insert(0, {
"name": "default",
"filename": "default.html",
"display_name": "Default",
"source": "builtin",
"can_delete": False
})
# Sort templates: uploaded first, then built-in, with default always first
# Sort templates: uploaded first, then built-in
template_list.sort(key=lambda t: (
0 if t["name"] == "default" else 1, # Default first
1 if t["source"] == "builtin" else 0, # Uploaded before built-in
t["name"] # Alphabetical within each group
))
......@@ -914,6 +903,357 @@ class DashboardAPI:
logger.error(f"Template deletion error: {e}")
return {"error": str(e)}
def get_template_preview(self, template_name: str) -> str:
"""Get template HTML content for preview with black background"""
try:
from pathlib import Path
# Get built-in templates directory
builtin_templates_dir = Path(__file__).parent.parent / "qt_player" / "templates"
# Get persistent uploaded templates directory
uploaded_templates_dir = self._get_persistent_templates_dir()
# Add .html extension if not present
if not template_name.endswith('.html'):
template_name += '.html'
template_path = None
# First check uploaded templates (they take priority)
uploaded_path = uploaded_templates_dir / template_name
if uploaded_path.exists():
template_path = uploaded_path
logger.info(f"Found uploaded template for preview: {template_path}")
else:
# Check built-in templates
builtin_path = builtin_templates_dir / template_name
if builtin_path.exists():
template_path = builtin_path
logger.info(f"Found built-in template for preview: {template_path}")
else:
# Try without .html extension
template_name_no_ext = template_name.replace('.html', '')
builtin_path_no_ext = builtin_templates_dir / f"{template_name_no_ext}.html"
if builtin_path_no_ext.exists():
template_path = builtin_path_no_ext
logger.info(f"Found built-in template for preview (added .html): {template_path}")
if not template_path or not template_path.exists():
logger.error(f"Template not found for preview: {template_name}")
return self._get_template_not_found_html(template_name)
# Read template content
try:
with open(template_path, 'r', encoding='utf-8') as f:
template_html = f.read()
except Exception as e:
logger.error(f"Failed to read template file: {e}")
return self._get_template_error_html(template_name, str(e))
# Wrap template in black background container
preview_html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Template Preview: {template_name.replace('.html', '')}</title>
<style>
body {{
margin: 0;
padding: 20px;
background-color: #000000;
color: #ffffff;
font-family: Arial, sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}}
.preview-container {{
width: 100%;
max-width: 1920px;
background-color: #000000;
border-radius: 8px;
padding: 20px;
box-shadow: 0 0 20px rgba(255, 255, 255, 0.1);
}}
.preview-header {{
text-align: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #333;
}}
.preview-title {{
font-size: 24px;
font-weight: bold;
color: #ffffff;
margin: 0;
}}
.preview-subtitle {{
font-size: 16px;
color: #cccccc;
margin: 5px 0 0 0;
}}
.template-content {{
width: 100%;
background-color: #000000 !important;
border-radius: 4px;
overflow: hidden;
}}
/* Force black background on ALL elements */
.template-content,
.template-content *,
.template-content body,
.template-content html,
.template-content div,
.template-content span,
.template-content p,
.template-content h1,
.template-content h2,
.template-content h3,
.template-content h4,
.template-content h5,
.template-content h6 {{
background-color: #000000 !important;
background: #000000 !important;
}}
/* Override any background styles in the template */
.template-content [style*="background-color"],
.template-content [style*="background:"] {{
background-color: #000000 !important;
background: #000000 !important;
}}
/* Specific overrides for common background colors */
.template-content [style*="background-color: white"],
.template-content [style*="background-color: #fff"],
.template-content [style*="background-color: #ffffff"],
.template-content [style*="background: white"],
.template-content [style*="background: #fff"],
.template-content [style*="background: #ffffff"] {{
background-color: #000000 !important;
background: #000000 !important;
}}
/* Make sure text is visible on black background */
.template-content [style*="color: black"],
.template-content [style*="color: #000"],
.template-content [style*="color: #000000"] {{
color: #ffffff !important;
}}
/* Force all text to be white for visibility */
.template-content * {{
color: #ffffff !important;
}}
/* Override any white text that might be invisible on black */
.template-content [style*="color: white"],
.template-content [style*="color: #fff"],
.template-content [style*="color: #ffffff"] {{
color: #ffffff !important;
}}
</style>
</head>
<body>
<div class="preview-container">
<div class="preview-header">
<h1 class="preview-title">Template Preview</h1>
<p class="preview-subtitle">{template_name.replace('.html', '')}</p>
</div>
<div class="template-content" id="template-content">
{template_html}
</div>
</div>
<script>
// Force black background on all elements after page load
document.addEventListener('DOMContentLoaded', function() {{
function forceBlackBackground(element) {{
if (element) {{
element.style.backgroundColor = '#000000';
element.style.background = '#000000';
// Force all child elements to have black background
const allElements = element.querySelectorAll('*');
allElements.forEach(function(el) {{
el.style.backgroundColor = '#000000';
el.style.background = '#000000';
el.style.color = '#ffffff';
}});
}}
}}
// Force black background on template content
const templateContent = document.getElementById('template-content');
forceBlackBackground(templateContent);
// Also force on body and html elements within the template
const templateBody = templateContent.querySelector('body');
const templateHtml = templateContent.querySelector('html');
if (templateBody) {{
templateBody.style.backgroundColor = '#000000';
templateBody.style.background = '#000000';
}}
if (templateHtml) {{
templateHtml.style.backgroundColor = '#000000';
templateHtml.style.background = '#000000';
}}
// Continuous enforcement every 100ms for 2 seconds
let enforcementCount = 0;
const maxEnforcements = 20;
const enforceInterval = setInterval(function() {{
forceBlackBackground(templateContent);
enforcementCount++;
if (enforcementCount >= maxEnforcements) {{
clearInterval(enforceInterval);
}}
}}, 100);
}});
</script>
</body>
</html>
"""
logger.info(f"Generated template preview for: {template_name}")
return preview_html
except Exception as e:
logger.error(f"Failed to generate template preview: {e}")
return self._get_template_error_html(template_name, str(e))
def _get_template_not_found_html(self, template_name: str) -> str:
"""Get HTML for template not found error"""
return f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Template Not Found</title>
<style>
body {{
margin: 0;
padding: 20px;
background-color: #000000;
color: #ffffff;
font-family: Arial, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}}
.error-container {{
text-align: center;
background-color: #1a1a1a;
padding: 40px;
border-radius: 8px;
box-shadow: 0 0 20px rgba(255, 255, 255, 0.1);
max-width: 500px;
}}
.error-title {{
font-size: 24px;
color: #ff6b6b;
margin-bottom: 20px;
}}
.error-message {{
font-size: 16px;
color: #cccccc;
}}
</style>
</head>
<body>
<div class="error-container">
<h1 class="error-title">Template Not Found</h1>
<p class="error-message">The template "{template_name}" could not be found.</p>
</div>
</body>
</html>
"""
def _get_template_error_html(self, template_name: str, error: str) -> str:
"""Get HTML for template error"""
return f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Template Error</title>
<style>
body {{
margin: 0;
padding: 20px;
background-color: #000000;
color: #ffffff;
font-family: Arial, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}}
.error-container {{
text-align: center;
background-color: #1a1a1a;
padding: 40px;
border-radius: 8px;
box-shadow: 0 0 20px rgba(255, 255, 255, 0.1);
max-width: 500px;
}}
.error-title {{
font-size: 24px;
color: #ff6b6b;
margin-bottom: 20px;
}}
.error-message {{
font-size: 16px;
color: #cccccc;
margin-bottom: 10px;
}}
.error-details {{
font-size: 14px;
color: #888;
font-family: monospace;
background-color: #2a2a2a;
padding: 10px;
border-radius: 4px;
text-align: left;
white-space: pre-wrap;
word-break: break-all;
}}
</style>
</head>
<body>
<div class="error-container">
<h1 class="error-title">Template Error</h1>
<p class="error-message">Error loading template "{template_name}"</p>
<div class="error-details">{error}</div>
</div>
</body>
</html>
"""
# Route functions for Flask
def get_config_section(section):
......
......@@ -1754,14 +1754,13 @@ def send_custom_message():
def get_intro_templates():
"""Get intro templates configuration"""
try:
from pathlib import Path
from ..database.models import GameConfigModel
import json
import os
from ..config.settings import get_user_data_dir
# Get data directory
data_dir = Path(get_user_data_dir())
config_path = data_dir / 'intro_templates.json'
session = api_bp.db_manager.get_session()
try:
# Get intro templates configuration from database
intro_config = session.query(GameConfigModel).filter_by(config_key='intro_templates_config').first()
# Default configuration
default_config = {
......@@ -1770,21 +1769,23 @@ def get_intro_templates():
'rotating_time': '05:00'
}
if config_path.exists():
if intro_config:
try:
with open(config_path, 'r') as f:
config = json.load(f)
config = json.loads(intro_config.config_value)
# Merge with defaults to ensure all fields are present
for key, value in default_config.items():
if key not in config:
config[key] = value
return jsonify(config)
except (json.JSONDecodeError, FileNotFoundError):
logger.warning("Failed to load intro templates config, using defaults")
except (json.JSONDecodeError, TypeError):
logger.warning("Failed to parse intro templates config from database, using defaults")
return jsonify(default_config)
else:
return jsonify(default_config)
finally:
session.close()
except Exception as e:
logger.error(f"Error loading intro templates: {str(e)}")
return jsonify({'templates': [], 'default_show_time': '00:30', 'rotating_time': '05:00'})
......@@ -1795,11 +1796,9 @@ def get_intro_templates():
def save_intro_templates():
"""Save intro templates configuration"""
try:
from pathlib import Path
from ..database.models import GameConfigModel
import json
import os
import re
from ..config.settings import get_user_data_dir
data = request.get_json()
if not data:
......@@ -1810,6 +1809,10 @@ def save_intro_templates():
default_show_time = data.get('default_show_time', '00:30')
rotating_time = data.get('rotating_time', '05:00')
logger.info(f"WebDashboard: Saving intro templates - received {len(templates)} templates")
logger.debug(f"WebDashboard: Templates data: {templates}")
logger.debug(f"WebDashboard: Default show time: {default_show_time}, Rotating time: {rotating_time}")
# Validate time format (MM:SS)
time_pattern = re.compile(r'^[0-9]{1,2}:[0-5][0-9]$')
......@@ -1833,7 +1836,7 @@ def save_intro_templates():
if not time_pattern.match(template['show_time']):
return jsonify({'error': f'Template {i+1} has invalid show_time format. Use MM:SS format.'}), 400
# Save configuration
# Save configuration to database
config = {
'templates': templates,
'default_show_time': default_show_time,
......@@ -1841,15 +1844,34 @@ def save_intro_templates():
'updated_at': datetime.now().isoformat()
}
# Get data directory and ensure it exists
data_dir = Path(get_user_data_dir())
data_dir.mkdir(parents=True, exist_ok=True)
config_path = data_dir / 'intro_templates.json'
config_json = json.dumps(config)
logger.debug(f"WebDashboard: Config JSON length: {len(config_json)}")
with open(config_path, 'w') as f:
json.dump(config, f, indent=2)
session = api_bp.db_manager.get_session()
try:
# Check if config already exists
existing_config = session.query(GameConfigModel).filter_by(config_key='intro_templates_config').first()
logger.info(f"Intro templates configuration saved with {len(templates)} templates")
if existing_config:
logger.debug("WebDashboard: Updating existing config")
# Update existing config
existing_config.config_value = config_json
existing_config.updated_at = datetime.utcnow()
else:
logger.debug("WebDashboard: Creating new config")
# Create new config
new_config = GameConfigModel(
config_key='intro_templates_config',
config_value=config_json,
value_type='json',
description='Intro templates configuration for video player',
is_system=False
)
session.add(new_config)
session.commit()
logger.info(f"WebDashboard: Successfully saved intro templates configuration: {len(templates)} templates")
return jsonify({
'success': True,
......@@ -1859,8 +1881,11 @@ def save_intro_templates():
'rotating_time': rotating_time
})
finally:
session.close()
except Exception as e:
logger.error(f"Error saving intro templates: {str(e)}")
logger.error(f"WebDashboard: Error saving intro templates: {str(e)}")
return jsonify({'error': 'Internal server error'}), 500
......@@ -4461,3 +4486,83 @@ def get_bet_barcode_data(bet_id):
except Exception as e:
logger.error(f"API get bet barcode data error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/templates/<template_name>')
def get_template_preview(template_name):
"""Serve template preview with black background - no authentication required"""
try:
api = g.get('api')
if not api:
return "API not available", 500
# Get template preview HTML
preview_html = api.get_template_preview(template_name)
# Return HTML response
from flask import Response
return Response(preview_html, mimetype='text/html')
except Exception as e:
logger.error(f"Template preview route error: {e}")
return f"Error loading template preview: {str(e)}", 500
@api_bp.route('/upload-intro-video', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def upload_intro_video():
"""Upload default intro video file (admin only)"""
try:
from ..config.settings import get_user_data_dir
from werkzeug.utils import secure_filename
import os
# Check if file was uploaded
if 'video_file' not in request.files:
return jsonify({"error": "No video file provided"}), 400
file = request.files['video_file']
overwrite = request.form.get('overwrite', 'false').lower() == 'true'
if file.filename == '':
return jsonify({"error": "No video file selected"}), 400
# Validate file type
allowed_extensions = {'mp4', 'avi', 'mov', 'mkv', 'webm'}
if not ('.' in file.filename and
file.filename.rsplit('.', 1)[1].lower() in allowed_extensions):
return jsonify({"error": "Invalid file type. Allowed: MP4, AVI, MOV, MKV, WebM"}), 400
# Get persistent storage directory
user_data_dir = get_user_data_dir()
videos_dir = user_data_dir / "videos"
videos_dir.mkdir(parents=True, exist_ok=True)
# Set filename for intro video
filename = secure_filename("intro_video." + file.filename.rsplit('.', 1)[1].lower())
filepath = videos_dir / filename
# Check if file already exists
if filepath.exists() and not overwrite:
return jsonify({"error": "Intro video already exists. Check 'overwrite existing' to replace it."}), 400
# Save the file
file.save(str(filepath))
# Store the path in configuration
if api_bp.db_manager:
api_bp.db_manager.set_config_value('intro_video_path', str(filepath))
logger.info(f"Intro video uploaded successfully: {filepath}")
return jsonify({
"success": True,
"message": "Intro video uploaded successfully",
"filename": filename,
"path": str(filepath)
})
except Exception as e:
logger.error(f"API upload intro video error: {e}")
return jsonify({"error": str(e)}), 500
\ No newline at end of file
......@@ -115,7 +115,7 @@
<h5>All Fixtures</h5>
<span id="filtered-count" class="badge bg-secondary">0 fixtures</span>
</div>
<div class="card-body p-0">
<div class="card-body p-0" style="padding-bottom: 100px !important;">
<div class="table-responsive">
<table class="table table-hover mb-0" id="fixtures-table">
<thead class="table-light">
......
......@@ -110,7 +110,12 @@
<i class="fas fa-shield-alt me-2"></i>Administrator Actions
</h6>
</div>
<div class="col-md-6 mb-3">
<div class="col-md-4 mb-3">
<button class="btn btn-outline-primary w-100" id="btn-upload-intro-video">
<i class="fas fa-upload me-2"></i>Upload Intro Video
</button>
</div>
<div class="col-md-4 mb-3">
<button class="btn btn-outline-danger w-100" id="btn-shutdown-app">
<i class="fas fa-power-off me-2"></i>Shutdown Application
</button>
......@@ -364,6 +369,51 @@
</div>
</div>
</div>
<!-- Upload Intro Video Modal -->
<div class="modal fade" id="uploadIntroVideoModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Upload Default Intro Video</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="upload-intro-video-form" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label">Select Video File</label>
<input type="file" class="form-control" id="intro-video-file"
accept="video/*" required>
<div class="form-text">
Supported formats: MP4, AVI, MOV, MKV, WebM (Max 500MB)
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="overwrite-existing">
<label class="form-check-label" for="overwrite-existing">
Overwrite existing intro video
</label>
</div>
</div>
<div id="upload-progress" class="d-none">
<div class="progress mb-2">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
<small class="text-muted">Uploading...</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirm-upload-intro-video">
<i class="fas fa-upload me-1"></i>Upload Video
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
......@@ -395,6 +445,10 @@ document.addEventListener('DOMContentLoaded', function() {
window.location.href = '/tokens';
});
document.getElementById('btn-upload-intro-video').addEventListener('click', function() {
new bootstrap.Modal(document.getElementById('uploadIntroVideoModal')).show();
});
// Match interval save button
document.getElementById('btn-save-interval').addEventListener('click', function() {
saveMatchInterval();
......@@ -557,6 +611,71 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
document.getElementById('confirm-upload-intro-video').addEventListener('click', function() {
const fileInput = document.getElementById('intro-video-file');
const file = fileInput.files[0];
const overwrite = document.getElementById('overwrite-existing').checked;
if (!file) {
alert('Please select a video file to upload');
return;
}
// Validate file size (500MB limit)
const maxSize = 500 * 1024 * 1024; // 500MB in bytes
if (file.size > maxSize) {
alert('File size must be less than 500MB');
return;
}
// Validate file type
const allowedTypes = ['video/mp4', 'video/avi', 'video/quicktime', 'video/x-matroska', 'video/webm'];
if (!allowedTypes.includes(file.type) && !file.name.match(/\.(mp4|avi|mov|mkv|webm)$/i)) {
alert('Please select a valid video file (MP4, AVI, MOV, MKV, or WebM)');
return;
}
const formData = new FormData();
formData.append('video_file', file);
formData.append('overwrite', overwrite);
const uploadBtn = document.getElementById('confirm-upload-intro-video');
const originalText = uploadBtn.innerHTML;
const progressDiv = document.getElementById('upload-progress');
const progressBar = progressDiv.querySelector('.progress-bar');
// Show progress and disable button
progressDiv.classList.remove('d-none');
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
fetch('/api/upload-intro-video', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('uploadIntroVideoModal')).hide();
// Clear the form
document.getElementById('upload-intro-video-form').reset();
alert('Intro video uploaded successfully!');
} else {
alert('Failed to upload video: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
alert('Error uploading video: ' + error.message);
})
.finally(() => {
// Hide progress and restore button
progressDiv.classList.add('d-none');
progressBar.style.width = '0%';
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalText;
});
});
// Status update functions
function updateSystemStatus() {
fetch('/api/status')
......
......@@ -69,13 +69,13 @@
<div class="col-md-6">
<label class="form-label">Default Show Time</label>
<input type="text" class="form-control" id="defaultShowTime"
placeholder="00:30" pattern="[0-9]{1,2}:[0-5][0-9]">
placeholder="00:15" pattern="[0-9]{1,2}:[0-5][0-9]">
<div class="form-text">Default display duration (MM:SS)</div>
</div>
<div class="col-md-6">
<label class="form-label">Rotating Interval</label>
<input type="text" class="form-control" id="rotatingTime"
placeholder="05:00" pattern="[0-9]{1,2}:[0-5][0-9]">
placeholder="00:15" pattern="[0-9]{1,2}:[0-5][0-9]">
<div class="form-text">Time between template rotations (MM:SS)</div>
</div>
</div>
......@@ -176,8 +176,8 @@
let availableTemplates = [];
let outcomeAssignments = {};
let introTemplates = [];
let defaultShowTime = '00:30';
let rotatingTime = '05:00';
let defaultShowTime = '00:15';
let rotatingTime = '00:15';
// Define all possible outcomes
const allOutcomes = [
......@@ -517,8 +517,29 @@
.then(response => response.json())
.then(data => {
introTemplates = data.templates || [];
defaultShowTime = data.default_show_time || '00:30';
rotatingTime = data.rotating_time || '05:00';
defaultShowTime = data.default_show_time || '00:15';
rotatingTime = data.rotating_time || '00:15';
// Set default intro templates if none exist
if (introTemplates.length === 0) {
// Check if fixtures and match templates are available
const fixturesTemplate = availableTemplates.find(t => t.name === 'fixtures');
const matchTemplate = availableTemplates.find(t => t.name === 'match');
if (fixturesTemplate) {
introTemplates.push({
name: 'fixtures',
show_time: '00:15'
});
}
if (matchTemplate) {
introTemplates.push({
name: 'match',
show_time: '00:15'
});
}
}
document.getElementById('defaultShowTime').value = defaultShowTime;
document.getElementById('rotatingTime').value = rotatingTime;
......@@ -526,6 +547,24 @@
.catch(error => {
console.error('Error loading intro templates:', error);
introTemplates = [];
// Set default intro templates on error too
const fixturesTemplate = availableTemplates.find(t => t.name === 'fixtures');
const matchTemplate = availableTemplates.find(t => t.name === 'match');
if (fixturesTemplate) {
introTemplates.push({
name: 'fixtures',
show_time: '00:15'
});
}
if (matchTemplate) {
introTemplates.push({
name: 'match',
show_time: '00:15'
});
}
});
}
......@@ -696,8 +735,8 @@
const data = {
templates: introTemplates,
default_show_time: defaultTime || '00:30',
rotating_time: rotatingInterval || '05:00'
default_show_time: defaultTime || '00:15',
rotating_time: rotatingInterval || '00:15'
};
fetch('/api/intro-templates', {
......@@ -898,10 +937,6 @@
cursor: grabbing;
}
/* Add margin at bottom for better scrolling */
.container-fluid {
padding-bottom: 100px;
}
</style>
{% endblock %}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment