Update player intro

parent b43952ee
...@@ -142,6 +142,24 @@ Examples: ...@@ -142,6 +142,24 @@ Examples:
action='store_true', action='store_true',
help='Enable debug mode showing only message bus messages' 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( parser.add_argument(
'--no-qt', '--no-qt',
...@@ -200,7 +218,7 @@ Examples: ...@@ -200,7 +218,7 @@ Examples:
'--start-timer', '--start-timer',
type=int, type=int,
default=None, 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( parser.add_argument(
...@@ -252,6 +270,9 @@ def main(): ...@@ -252,6 +270,9 @@ def main():
settings.web_port = args.web_port settings.web_port = args.web_port
settings.debug_mode = args.debug or args.dev_mode settings.debug_mode = args.debug or args.dev_mode
settings.dev_message = args.dev_message 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_qt = not args.no_qt
settings.enable_web = not args.no_web settings.enable_web = not args.no_web
settings.qt.use_native_overlay = args.native_overlay settings.qt.use_native_overlay = args.native_overlay
......
...@@ -369,6 +369,9 @@ class AppSettings: ...@@ -369,6 +369,9 @@ class AppSettings:
version: str = "1.0.0" version: str = "1.0.0"
debug_mode: bool = False debug_mode: bool = False
dev_message: bool = False # Enable debug mode showing only message bus messages 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_web: bool = True
enable_qt: bool = True enable_qt: bool = True
enable_api_client: bool = True enable_api_client: bool = True
...@@ -408,6 +411,10 @@ class AppSettings: ...@@ -408,6 +411,10 @@ class AppSettings:
"timer": self.timer.__dict__, "timer": self.timer.__dict__,
"version": self.version, "version": self.version,
"debug_mode": self.debug_mode, "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_web": self.enable_web,
"enable_qt": self.enable_qt, "enable_qt": self.enable_qt,
"enable_api_client": self.enable_api_client, "enable_api_client": self.enable_api_client,
...@@ -438,7 +445,7 @@ class AppSettings: ...@@ -438,7 +445,7 @@ class AppSettings:
settings.timer = TimerConfig(**data["timer"]) settings.timer = TimerConfig(**data["timer"])
# Update app settings # 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: if key in data:
setattr(settings, key, data[key]) setattr(settings, key, data[key])
......
...@@ -165,7 +165,7 @@ class MbetterClientApplication: ...@@ -165,7 +165,7 @@ class MbetterClientApplication:
def _initialize_message_bus(self) -> bool: def _initialize_message_bus(self) -> bool:
"""Initialize message bus""" """Initialize message bus"""
try: 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 # Register core component
self.message_bus.register_component("core") self.message_bus.register_component("core")
...@@ -322,7 +322,9 @@ class MbetterClientApplication: ...@@ -322,7 +322,9 @@ class MbetterClientApplication:
self.qt_player = QtVideoPlayer( self.qt_player = QtVideoPlayer(
message_bus=self.message_bus, 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 # Don't register with thread manager since QtPlayer no longer inherits from ThreadedComponent
...@@ -760,8 +762,13 @@ class MbetterClientApplication: ...@@ -760,8 +762,13 @@ class MbetterClientApplication:
if self._start_timer_minutes is None: if self._start_timer_minutes is None:
return return
delay_seconds = self._start_timer_minutes * 60 # Special case: --start-timer 0 means 10 seconds delay for system initialization
logger.info(f"Starting command line game timer: {self._start_timer_minutes} minutes ({delay_seconds} seconds)") 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)")
self._game_start_timer = threading.Timer(delay_seconds, self._on_game_timer_expired) self._game_start_timer = threading.Timer(delay_seconds, self._on_game_timer_expired)
self._game_start_timer.daemon = True self._game_start_timer.daemon = True
......
...@@ -684,7 +684,11 @@ class GamesThread(ThreadedComponent): ...@@ -684,7 +684,11 @@ class GamesThread(ThreadedComponent):
# Start match timer # Start match timer
logger.info(f"⏰ Starting match timer for fixture {fixture_id}") logger.info(f"⏰ Starting match timer for fixture {fixture_id}")
self._start_match_timer(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 # Refresh dashboard statuses
self._refresh_dashboard_statuses() self._refresh_dashboard_statuses()
...@@ -897,6 +901,114 @@ class GamesThread(ThreadedComponent): ...@@ -897,6 +901,114 @@ class GamesThread(ThreadedComponent):
except Exception as e: except Exception as e:
logger.error(f"Failed to start match timer: {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): def _cleanup(self):
"""Perform cleanup operations""" """Perform cleanup operations"""
try: try:
......
...@@ -64,7 +64,10 @@ class MessageType(Enum): ...@@ -64,7 +64,10 @@ class MessageType(Enum):
START_GAME = "START_GAME" START_GAME = "START_GAME"
SCHEDULE_GAMES = "SCHEDULE_GAMES" SCHEDULE_GAMES = "SCHEDULE_GAMES"
START_GAME_DELAYED = "START_GAME_DELAYED" START_GAME_DELAYED = "START_GAME_DELAYED"
START_INTRO = "START_INTRO"
MATCH_START = "MATCH_START" MATCH_START = "MATCH_START"
PLAY_VIDEO_MATCH = "PLAY_VIDEO_MATCH"
PLAY_VIDEO_MATCH_DONE = "PLAY_VIDEO_MATCH_DONE"
GAME_STATUS = "GAME_STATUS" GAME_STATUS = "GAME_STATUS"
GAME_UPDATE = "GAME_UPDATE" GAME_UPDATE = "GAME_UPDATE"
...@@ -134,9 +137,10 @@ class Message: ...@@ -134,9 +137,10 @@ class Message:
class MessageBus: class MessageBus:
"""Central message bus for inter-thread communication""" """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.max_queue_size = max_queue_size
self.dev_message = dev_message self.dev_message = dev_message
self.debug_messages = debug_messages
self._queues: Dict[str, Queue] = {} self._queues: Dict[str, Queue] = {}
self._handlers: Dict[str, Dict[MessageType, List[Callable]]] = {} self._handlers: Dict[str, Dict[MessageType, List[Callable]]] = {}
self._global_handlers: Dict[MessageType, List[Callable]] = {} self._global_handlers: Dict[MessageType, List[Callable]] = {}
...@@ -147,6 +151,8 @@ class MessageBus: ...@@ -147,6 +151,8 @@ class MessageBus:
if dev_message: if dev_message:
logger.info("MessageBus initialized with dev_message mode enabled") logger.info("MessageBus initialized with dev_message mode enabled")
elif debug_messages:
logger.info("MessageBus initialized with debug_messages mode enabled")
else: else:
logger.info("MessageBus initialized") logger.info("MessageBus initialized")
...@@ -207,6 +213,16 @@ class MessageBus: ...@@ -207,6 +213,16 @@ class MessageBus:
# Log the message (only in dev_message mode) # Log the message (only in dev_message mode)
if self.dev_message: if self.dev_message:
logger.info(f"📨 MESSAGE_BUS: {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: if broadcast or message.recipient is None:
# Broadcast to all components # Broadcast to all components
...@@ -578,6 +594,18 @@ class MessageBuilder: ...@@ -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 @staticmethod
def match_start(sender: str, fixture_id: str, match_id: int) -> Message: def match_start(sender: str, fixture_id: str, match_id: int) -> Message:
"""Create MATCH_START message""" """Create MATCH_START message"""
...@@ -588,4 +616,30 @@ class MessageBuilder: ...@@ -588,4 +616,30 @@ class MessageBuilder:
"fixture_id": fixture_id, "fixture_id": fixture_id,
"match_id": match_id "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
...@@ -234,23 +234,24 @@ class VideoProcessingWorker(QRunnable): ...@@ -234,23 +234,24 @@ class VideoProcessingWorker(QRunnable):
class OverlayWebView(QWebEngineView): class OverlayWebView(QWebEngineView):
"""Custom QWebEngineView for video overlays with transparent background""" """Custom QWebEngineView for video overlays with transparent background"""
def __init__(self, parent=None): def __init__(self, parent=None, debug_overlay=False):
super().__init__(parent) super().__init__(parent)
self.debug_overlay = debug_overlay
self.web_channel = None self.web_channel = None
self.overlay_channel = None self.overlay_channel = None
self.current_template = "default.html" self.current_template = "default.html"
# Built-in templates directory (bundled with app) # Built-in templates directory (bundled with app)
self.builtin_templates_dir = Path(__file__).parent / "templates" self.builtin_templates_dir = Path(__file__).parent / "templates"
# Persistent uploaded templates directory (user data) # Persistent uploaded templates directory (user data)
self.uploaded_templates_dir = self._get_persistent_templates_dir() self.uploaded_templates_dir = self._get_persistent_templates_dir()
self.uploaded_templates_dir.mkdir(parents=True, exist_ok=True) self.uploaded_templates_dir.mkdir(parents=True, exist_ok=True)
# Primary templates directory for backwards compatibility # Primary templates directory for backwards compatibility
self.templates_dir = self.builtin_templates_dir self.templates_dir = self.builtin_templates_dir
self.setup_web_view() self.setup_web_view()
self._setup_custom_scheme() self._setup_custom_scheme()
logger.info(f"OverlayWebView initialized - builtin: {self.builtin_templates_dir}, uploaded: {self.uploaded_templates_dir}") logger.info(f"OverlayWebView initialized - builtin: {self.builtin_templates_dir}, uploaded: {self.uploaded_templates_dir}")
...@@ -337,9 +338,10 @@ class OverlayWebView(QWebEngineView): ...@@ -337,9 +338,10 @@ class OverlayWebView(QWebEngineView):
def load_template(self, template_name: str): def load_template(self, template_name: str):
"""Load a specific template file, prioritizing uploaded templates""" """Load a specific template file, prioritizing uploaded templates"""
try: try:
logger.debug(f"GREEN SCREEN DEBUG: Starting template load - {template_name}") if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Current page URL before load: {self.url().toString()}") logger.debug(f"GREEN SCREEN DEBUG: Starting template load - {template_name}")
logger.debug(f"GREEN SCREEN DEBUG: WebEngine view visible: {self.isVisible()}") 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()}")
# CRITICAL FIX: Store visibility state before template load # CRITICAL FIX: Store visibility state before template load
was_visible = self.isVisible() was_visible = self.isVisible()
...@@ -376,28 +378,31 @@ class OverlayWebView(QWebEngineView): ...@@ -376,28 +378,31 @@ class OverlayWebView(QWebEngineView):
return return
if template_path and template_path.exists(): if template_path and template_path.exists():
logger.debug(f"GREEN SCREEN DEBUG: About to load template file: {template_path}") if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Template source: {template_source}") logger.debug(f"GREEN SCREEN DEBUG: About to load template file: {template_path}")
logger.debug(f"GREEN SCREEN DEBUG: Template source: {template_source}")
# Check WebEngine state before load
page = self.page() # Check WebEngine state before load
if page: page = self.page()
logger.debug(f"GREEN SCREEN DEBUG: Page background color before load: {page.backgroundColor()}") if page:
logger.debug(f"GREEN SCREEN DEBUG: Page background color before load: {page.backgroundColor()}")
self.load(QUrl.fromLocalFile(str(template_path))) self.load(QUrl.fromLocalFile(str(template_path)))
self.current_template = template_name self.current_template = template_name
# CRITICAL FIX: Force visibility recovery after template load # CRITICAL FIX: Force visibility recovery after template load
if was_visible and not self.isVisible(): if was_visible and not self.isVisible():
logger.debug(f"GREEN SCREEN FIX: Recovering overlay visibility after template load") if self.debug_overlay:
logger.debug(f"GREEN SCREEN FIX: Recovering overlay visibility after template load")
self.show() self.show()
self.raise_() self.raise_()
# CRITICAL FIX: Schedule additional visibility check after page load # CRITICAL FIX: Schedule additional visibility check after page load
from PyQt6.QtCore import QTimer from PyQt6.QtCore import QTimer
QTimer.singleShot(100, lambda: self._ensure_overlay_visibility_post_load(was_visible)) QTimer.singleShot(100, lambda: self._ensure_overlay_visibility_post_load(was_visible))
logger.debug(f"GREEN SCREEN DEBUG: Template load initiated - {template_path}") 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})") logger.info(f"Loaded template: {template_path} (source: {template_source})")
else: else:
logger.error(f"No template found: {template_name}") logger.error(f"No template found: {template_name}")
...@@ -405,7 +410,8 @@ class OverlayWebView(QWebEngineView): ...@@ -405,7 +410,8 @@ class OverlayWebView(QWebEngineView):
self._load_fallback_overlay() self._load_fallback_overlay()
except Exception as e: except Exception as e:
logger.debug(f"GREEN SCREEN DEBUG: Template load failed: {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}") logger.error(f"Failed to load template {template_name}: {e}")
self._load_fallback_overlay() self._load_fallback_overlay()
...@@ -458,10 +464,12 @@ class OverlayWebView(QWebEngineView): ...@@ -458,10 +464,12 @@ class OverlayWebView(QWebEngineView):
def reload_current_template(self): def reload_current_template(self):
"""Reload the current template""" """Reload the current template"""
logger.debug(f"GREEN SCREEN DEBUG: Reloading current template - {self.current_template}") if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: WebEngine state before reload - visible: {self.isVisible()}") 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) self.load_template(self.current_template)
logger.debug(f"GREEN SCREEN DEBUG: Current template reload initiated") if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Current template reload initiated")
def get_available_templates(self) -> List[str]: def get_available_templates(self) -> List[str]:
"""Get list of available template files from both directories""" """Get list of available template files from both directories"""
...@@ -957,9 +965,10 @@ class PlayerWindow(QMainWindow): ...@@ -957,9 +965,10 @@ class PlayerWindow(QMainWindow):
position_changed = pyqtSignal(int, int) position_changed = pyqtSignal(int, int)
video_loaded = pyqtSignal(str) 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__() super().__init__()
self.settings = settings self.settings = settings
self.debug_overlay = debug_overlay
self.mutex = QMutex() self.mutex = QMutex()
self.thread_pool = QThreadPool() self.thread_pool = QThreadPool()
self.thread_pool.setMaxThreadCount(4) self.thread_pool.setMaxThreadCount(4)
...@@ -1067,7 +1076,9 @@ class PlayerWindow(QMainWindow): ...@@ -1067,7 +1076,9 @@ class PlayerWindow(QMainWindow):
self.window_overlay = NativeOverlayWidget(self.overlay_window) self.window_overlay = NativeOverlayWidget(self.overlay_window)
logger.debug("PlayerWindow: Created NativeOverlayWidget overlay as separate window") logger.debug("PlayerWindow: Created NativeOverlayWidget overlay as separate window")
else: 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") logger.debug("PlayerWindow: Created QWebEngineView overlay as separate window")
# Layout for overlay window # Layout for overlay window
...@@ -1142,11 +1153,13 @@ class PlayerWindow(QMainWindow): ...@@ -1142,11 +1153,13 @@ class PlayerWindow(QMainWindow):
self.window_overlay.raise_() self.window_overlay.raise_()
logger.error(f"GREEN SCREEN FIX: WebEngine overlay visibility recovered during sync") logger.error(f"GREEN SCREEN FIX: WebEngine overlay visibility recovered during sync")
logger.debug(f"Overlay window positioned at: {self.overlay_window.geometry()}") if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Overlay window after sync - geometry: {self.overlay_window.geometry()}") 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: except Exception as e:
logger.debug(f"GREEN SCREEN DEBUG: Overlay sync failed: {e}") if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Overlay sync failed: {e}")
logger.error(f"Failed to sync overlay position: {e}") logger.error(f"Failed to sync overlay position: {e}")
def resizeEvent(self, event): def resizeEvent(self, event):
...@@ -1216,7 +1229,19 @@ class PlayerWindow(QMainWindow): ...@@ -1216,7 +1229,19 @@ class PlayerWindow(QMainWindow):
self.loop_count = 0 self.loop_count = 0
self.current_loop_iteration = 0 self.current_loop_iteration = 0
self.current_file_path = None 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 # Set volume
self.audio_output.setVolume(self.settings.volume) self.audio_output.setVolume(self.settings.volume)
self.audio_output.setMuted(self.settings.mute) self.audio_output.setMuted(self.settings.mute)
...@@ -1227,9 +1252,66 @@ class PlayerWindow(QMainWindow): ...@@ -1227,9 +1252,66 @@ class PlayerWindow(QMainWindow):
self.overlay_timer = QTimer() self.overlay_timer = QTimer()
self.overlay_timer.timeout.connect(self.update_overlay_periodically) self.overlay_timer.timeout.connect(self.update_overlay_periodically)
self.overlay_timer.start(1000) # Update every second 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) # Mouse tracking for window interactions (no controls to show/hide)
self.setMouseTracking(True) 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): def open_file_dialog(self):
"""Open file dialog to select video""" """Open file dialog to select video"""
...@@ -1252,26 +1334,37 @@ class PlayerWindow(QMainWindow): ...@@ -1252,26 +1334,37 @@ class PlayerWindow(QMainWindow):
logger.info(f"Loop data: {loop_data}") logger.info(f"Loop data: {loop_data}")
logger.info(f"Media player state before play: {self.media_player.playbackState()}") logger.info(f"Media player state before play: {self.media_player.playbackState()}")
logger.info(f"Media player error state: {self.media_player.error()}") logger.info(f"Media player error state: {self.media_player.error()}")
# Process loop control parameters # Process loop control parameters
if loop_data: if loop_data:
self.loop_enabled = loop_data.get('loop_mode', False) or loop_data.get('infinite_loop', False) or loop_data.get('continuous_playback', False) self.loop_enabled = loop_data.get('loop_mode', False) or loop_data.get('infinite_loop', False) or loop_data.get('continuous_playback', False)
self.infinite_loop = loop_data.get('infinite_loop', False) or loop_data.get('continuous_playback', False) 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) 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: if self.infinite_loop or self.loop_count == -1:
self.infinite_loop = True self.infinite_loop = True
logger.info("INFINITE LOOP MODE ENABLED") logger.info("INFINITE LOOP MODE ENABLED")
elif self.loop_count > 0: elif self.loop_count > 0:
logger.info(f"FINITE LOOP MODE ENABLED - {self.loop_count} iterations") logger.info(f"FINITE LOOP MODE ENABLED - {self.loop_count} iterations")
self.current_loop_iteration = 0 self.current_loop_iteration = 0
else: else:
# No loop data - disable looping # No loop data - disable looping and template rotation
self.loop_enabled = False self.loop_enabled = False
self.infinite_loop = False self.infinite_loop = False
self.loop_count = 0 self.loop_count = 0
self.current_loop_iteration = 0 self.current_loop_iteration = 0
self.template_sequence = []
self.rotating_time = '05:00'
self.current_template_index = 0
with QMutexLocker(self.mutex): with QMutexLocker(self.mutex):
# Handle both absolute and relative file paths # Handle both absolute and relative file paths
...@@ -1359,9 +1452,13 @@ class PlayerWindow(QMainWindow): ...@@ -1359,9 +1452,13 @@ class PlayerWindow(QMainWindow):
# Update overlay safely - handles both native and WebEngine # Update overlay safely - handles both native and WebEngine
QTimer.singleShot(1000, lambda: self._update_overlay_safe(overlay_view, overlay_data)) 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: if self.settings.auto_play:
self.media_player.play() self.media_player.play()
# Ensure proper window focus for video playback # Ensure proper window focus for video playback
app = QApplication.instance() app = QApplication.instance()
if app: if app:
...@@ -1373,7 +1470,7 @@ class PlayerWindow(QMainWindow): ...@@ -1373,7 +1470,7 @@ class PlayerWindow(QMainWindow):
logger.debug(f"Window focus applied for video playback - activeWindow: {app.activeWindow()}") logger.debug(f"Window focus applied for video playback - activeWindow: {app.activeWindow()}")
else: else:
logger.warning("No QApplication instance found for window focus") logger.warning("No QApplication instance found for window focus")
# Start background metadata extraction # Start background metadata extraction
worker = VideoProcessingWorker( worker = VideoProcessingWorker(
"metadata_extraction", "metadata_extraction",
...@@ -1381,7 +1478,7 @@ class PlayerWindow(QMainWindow): ...@@ -1381,7 +1478,7 @@ class PlayerWindow(QMainWindow):
self.on_metadata_extracted self.on_metadata_extracted
) )
self.thread_pool.start(worker) self.thread_pool.start(worker)
logger.info(f"Playing video: {file_path}") logger.info(f"Playing video: {file_path}")
self.video_loaded.emit(file_path) self.video_loaded.emit(file_path)
...@@ -1406,6 +1503,11 @@ class PlayerWindow(QMainWindow): ...@@ -1406,6 +1503,11 @@ class PlayerWindow(QMainWindow):
"""Stop playback (thread-safe)""" """Stop playback (thread-safe)"""
with QMutexLocker(self.mutex): with QMutexLocker(self.mutex):
self.media_player.stop() 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): def seek_to_position(self, percentage: int):
"""Seek to position (percentage) (thread-safe)""" """Seek to position (percentage) (thread-safe)"""
...@@ -1485,7 +1587,7 @@ class PlayerWindow(QMainWindow): ...@@ -1485,7 +1587,7 @@ class PlayerWindow(QMainWindow):
def on_media_status_changed(self, status): def on_media_status_changed(self, status):
"""Handle media status changes and loop control""" """Handle media status changes and loop control"""
logger.debug(f"LOOP DEBUG: Media status changed to: {status} ({status.name if hasattr(status, 'name') else 'unknown'})") logger.debug(f"LOOP DEBUG: Media status changed to: {status} ({status.name if hasattr(status, 'name') else 'unknown'})")
if status == QMediaPlayer.MediaStatus.LoadedMedia: if status == QMediaPlayer.MediaStatus.LoadedMedia:
# Media loaded successfully - use safe update # Media loaded successfully - use safe update
if hasattr(self, 'window_overlay'): if hasattr(self, 'window_overlay'):
...@@ -1493,15 +1595,28 @@ class PlayerWindow(QMainWindow): ...@@ -1493,15 +1595,28 @@ class PlayerWindow(QMainWindow):
status_data = {'subtitle': 'Media loaded successfully'} status_data = {'subtitle': 'Media loaded successfully'}
# Update overlay safely - handles both native and WebEngine # Update overlay safely - handles both native and WebEngine
self._update_overlay_safe(overlay_view, status_data) self._update_overlay_safe(overlay_view, status_data)
elif status == QMediaPlayer.MediaStatus.EndOfMedia: 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: END OF MEDIA DETECTED!")
logger.debug(f"LOOP DEBUG: Loop enabled: {self.loop_enabled}") 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: Infinite loop: {self.infinite_loop}")
logger.debug(f"LOOP DEBUG: Current iteration: {self.current_loop_iteration}") logger.debug(f"LOOP DEBUG: Current iteration: {self.current_loop_iteration}")
logger.debug(f"LOOP DEBUG: Loop count: {self.loop_count}") 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: if self.loop_enabled:
logger.debug(f"LOOP DEBUG: Processing loop restart logic...") logger.debug(f"LOOP DEBUG: Processing loop restart logic...")
if self.infinite_loop: if self.infinite_loop:
...@@ -1621,12 +1736,17 @@ class PlayerWindow(QMainWindow): ...@@ -1621,12 +1736,17 @@ class PlayerWindow(QMainWindow):
with QMutexLocker(self.mutex): with QMutexLocker(self.mutex):
self.media_player.stop() self.media_player.stop()
self.thread_pool.waitForDone(3000) # Wait up to 3 seconds for threads 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 # Close overlay window
if hasattr(self, 'overlay_window') and self.overlay_window: if hasattr(self, 'overlay_window') and self.overlay_window:
self.overlay_window.close() self.overlay_window.close()
logger.debug("Overlay window closed") logger.debug("Overlay window closed")
logger.info("Player window closing - Qt will handle application exit") logger.info("Player window closing - Qt will handle application exit")
event.accept() event.accept()
...@@ -1718,63 +1838,76 @@ class PlayerWindow(QMainWindow): ...@@ -1718,63 +1838,76 @@ class PlayerWindow(QMainWindow):
"""Update overlay data safely, handling both native and WebEngine overlays""" """Update overlay data safely, handling both native and WebEngine overlays"""
try: try:
# Check video state during overlay update # 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() 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 state during overlay update: {video_state}")
logger.debug(f"GREEN SCREEN DEBUG: Video position during overlay update: {self.media_player.position()}") logger.debug(f"GREEN SCREEN DEBUG: Video position during overlay update: {self.media_player.position()}")
# Clean data before sending to prevent null property issues # Clean data before sending to prevent null property issues
cleaned_data = self._clean_overlay_data(data) cleaned_data = self._clean_overlay_data(data)
if not cleaned_data: if not cleaned_data:
logger.debug("No valid data to send to overlay after cleaning") logger.debug("No valid data to send to overlay after cleaning")
return False return False
data_keys = list(cleaned_data.keys()) if isinstance(cleaned_data, dict) else [] if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: About to update overlay with {len(cleaned_data)} data items: {data_keys}") 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): if self._is_native_overlay(overlay_view):
# Native overlay - always ready, update immediately # Native overlay - always ready, update immediately
logger.debug(f"GREEN SCREEN DEBUG: Updating native overlay") if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Updating native overlay")
overlay_view.update_overlay_data(cleaned_data) overlay_view.update_overlay_data(cleaned_data)
logger.debug("Native overlay updated successfully") if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Native overlay update completed") logger.debug("Native overlay updated successfully")
logger.debug(f"GREEN SCREEN DEBUG: Native overlay update completed")
return True return True
elif isinstance(overlay_view, OverlayWebView): elif isinstance(overlay_view, OverlayWebView):
# WebEngine overlay - check readiness first # WebEngine overlay - check readiness first
logger.debug(f"GREEN SCREEN DEBUG: Checking WebEngine overlay readiness") if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: WebEngine URL: {overlay_view.url().toString()}") logger.debug(f"GREEN SCREEN DEBUG: Checking WebEngine overlay readiness")
logger.debug(f"GREEN SCREEN DEBUG: WebEngine visible: {overlay_view.isVisible()}") 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 # CRITICAL FIX: Ensure WebEngine overlay visibility before update
if not overlay_view.isVisible(): if not overlay_view.isVisible():
logger.debug(f"GREEN SCREEN FIX: WebEngine overlay not visible, forcing visibility recovery") if self.debug_overlay:
logger.debug(f"GREEN SCREEN FIX: WebEngine overlay not visible, forcing visibility recovery")
overlay_view.show() overlay_view.show()
overlay_view.raise_() overlay_view.raise_()
logger.debug(f"GREEN SCREEN FIX: WebEngine overlay visibility forced during update") if self.debug_overlay:
logger.debug(f"GREEN SCREEN FIX: WebEngine overlay visibility forced during update")
# Also ensure parent overlay window is visible # Also ensure parent overlay window is visible
if hasattr(self, 'overlay_window') and self.overlay_window and not self.overlay_window.isVisible(): if hasattr(self, 'overlay_window') and self.overlay_window and not self.overlay_window.isVisible():
logger.debug(f"GREEN SCREEN FIX: Parent overlay window not visible, forcing visibility") if self.debug_overlay:
logger.debug(f"GREEN SCREEN FIX: Parent overlay window not visible, forcing visibility")
self.overlay_window.show() self.overlay_window.show()
self.overlay_window.raise_() self.overlay_window.raise_()
logger.debug(f"GREEN SCREEN FIX: Parent overlay window visibility forced") if self.debug_overlay:
logger.debug(f"GREEN SCREEN FIX: Parent overlay window visibility forced")
if self._is_webengine_ready(overlay_view): if self._is_webengine_ready(overlay_view):
logger.debug(f"GREEN SCREEN DEBUG: WebEngine ready, updating overlay") if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: WebEngine ready, updating overlay")
overlay_view.update_overlay_data(cleaned_data) overlay_view.update_overlay_data(cleaned_data)
logger.debug("WebEngine overlay updated successfully") if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: WebEngine overlay update completed") logger.debug("WebEngine overlay updated successfully")
logger.debug(f"GREEN SCREEN DEBUG: WebEngine overlay update completed")
return True return True
else: else:
logger.debug(f"GREEN SCREEN DEBUG: WebEngine not ready, skipping update") if self.debug_overlay:
logger.debug("WebEngine overlay not ready, skipping update") logger.debug(f"GREEN SCREEN DEBUG: WebEngine not ready, skipping update")
logger.debug("WebEngine overlay not ready, skipping update")
return False return False
else: else:
logger.warning(f"Unknown overlay type: {type(overlay_view)}") logger.warning(f"Unknown overlay type: {type(overlay_view)}")
logger.debug(f"GREEN SCREEN DEBUG: Unknown overlay type: {type(overlay_view)}") if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Unknown overlay type: {type(overlay_view)}")
return False return False
except Exception as e: except Exception as e:
logger.debug(f"GREEN SCREEN DEBUG: Overlay update failed: {e}") if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Overlay update failed: {e}")
logger.error(f"Failed to update overlay safely: {e}") logger.error(f"Failed to update overlay safely: {e}")
return False return False
...@@ -1811,27 +1944,32 @@ class PlayerWindow(QMainWindow): ...@@ -1811,27 +1944,32 @@ class PlayerWindow(QMainWindow):
class QtVideoPlayer(QObject): class QtVideoPlayer(QObject):
"""PyQt6 video player component with message bus integration (replaces PyQt5 version)""" """PyQt6 video player component with message bus integration (replaces PyQt5 version)"""
# Signal for cross-thread video playback # Signal for cross-thread video playback
play_video_signal = pyqtSignal(str, dict) 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__() super().__init__()
self.name = "qt_player" self.name = "qt_player"
self.message_bus = message_bus self.message_bus = message_bus
self.settings = settings self.settings = settings
self.debug_player = debug_player
self.debug_overlay = debug_overlay
self.app: Optional[QApplication] = None self.app: Optional[QApplication] = None
self.window: Optional[PlayerWindow] = None self.window: Optional[PlayerWindow] = None
self.mutex = QMutex() self.mutex = QMutex()
# Set web dashboard URL for API calls
self.web_dashboard_url = "http://localhost:5000" # Default web dashboard URL
# Register message queue # Register message queue
logger.info(f"Registering QtVideoPlayer component with message bus - name: '{self.name}'") logger.info(f"Registering QtVideoPlayer component with message bus - name: '{self.name}'")
self.message_queue = self.message_bus.register_component(self.name) self.message_queue = self.message_bus.register_component(self.name)
logger.info(f"QtVideoPlayer component registered successfully - queue: {self.message_queue}") logger.info(f"QtVideoPlayer component registered successfully - queue: {self.message_queue}")
# Message processing timer (runs on Qt main thread) # Message processing timer (runs on Qt main thread)
self.message_timer: Optional[QTimer] = None self.message_timer: Optional[QTimer] = None
logger.info("QtVideoPlayer (PyQt6) initialized") logger.info("QtVideoPlayer (PyQt6) initialized")
def _get_web_server_base_url(self) -> str: def _get_web_server_base_url(self) -> str:
...@@ -1886,8 +2024,8 @@ class QtVideoPlayer(QObject): ...@@ -1886,8 +2024,8 @@ class QtVideoPlayer(QObject):
# Linux-specific application settings # Linux-specific application settings
self._configure_linux_app_settings() self._configure_linux_app_settings()
# Create player window with message bus reference # Create player window with message bus reference and debug settings
self.window = PlayerWindow(self.settings, self.message_bus) self.window = PlayerWindow(self.settings, self.message_bus, debug_overlay=self.debug_overlay)
# CRITICAL: Connect signal to slot for cross-thread video playback # CRITICAL: Connect signal to slot for cross-thread video playback
self.play_video_signal.connect(self.window.play_video, Qt.ConnectionType.QueuedConnection) self.play_video_signal.connect(self.window.play_video, Qt.ConnectionType.QueuedConnection)
...@@ -1938,6 +2076,8 @@ class QtVideoPlayer(QObject): ...@@ -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.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.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.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") logger.info("QtPlayer subscriptions completed successfully")
# Delay loading default overlay to allow JavaScript initialization # Delay loading default overlay to allow JavaScript initialization
...@@ -2225,10 +2365,11 @@ class QtVideoPlayer(QObject): ...@@ -2225,10 +2365,11 @@ class QtVideoPlayer(QObject):
break break
messages_processed += 1 messages_processed += 1
logger.info(f"QtPlayer RECEIVED message: {message.type.value} from {message.sender}") if self.debug_player:
# Don't log full message data to avoid cluttering logs with HTML content logger.info(f"QtPlayer RECEIVED message: {message.type.value} from {message.sender}")
logger.debug(f"Message data keys: {list(message.data.keys()) if isinstance(message.data, dict) else 'non-dict data'}") # 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'}")
# Process message directly on main thread - no threading issues! # Process message directly on main thread - no threading issues!
self._process_message(message) self._process_message(message)
...@@ -2315,39 +2456,58 @@ class QtVideoPlayer(QObject): ...@@ -2315,39 +2456,58 @@ class QtVideoPlayer(QObject):
def _process_message(self, message: Message): def _process_message(self, message: Message):
"""Process received message by routing to appropriate handlers""" """Process received message by routing to appropriate handlers"""
try: try:
logger.info(f"QtPlayer processing message type: {message.type.value}") if self.debug_player:
logger.info(f"QtPlayer processing message type: {message.type.value}")
# Route messages to appropriate handlers # Route messages to appropriate handlers
if message.type == MessageType.VIDEO_PLAY: if message.type == MessageType.VIDEO_PLAY:
logger.info("Calling _handle_video_play handler") if self.debug_player:
logger.info("Calling _handle_video_play handler")
self._handle_video_play(message) self._handle_video_play(message)
elif message.type == MessageType.VIDEO_PAUSE: elif message.type == MessageType.VIDEO_PAUSE:
logger.info("Calling _handle_video_pause handler") if self.debug_player:
logger.info("Calling _handle_video_pause handler")
self._handle_video_pause(message) self._handle_video_pause(message)
elif message.type == MessageType.VIDEO_STOP: elif message.type == MessageType.VIDEO_STOP:
logger.info("Calling _handle_video_stop handler") if self.debug_player:
logger.info("Calling _handle_video_stop handler")
self._handle_video_stop(message) self._handle_video_stop(message)
elif message.type == MessageType.VIDEO_SEEK: elif message.type == MessageType.VIDEO_SEEK:
logger.info("Calling _handle_video_seek handler") if self.debug_player:
logger.info("Calling _handle_video_seek handler")
self._handle_video_seek(message) self._handle_video_seek(message)
elif message.type == MessageType.VIDEO_VOLUME: elif message.type == MessageType.VIDEO_VOLUME:
logger.info("Calling _handle_video_volume handler") if self.debug_player:
logger.info("Calling _handle_video_volume handler")
self._handle_video_volume(message) self._handle_video_volume(message)
elif message.type == MessageType.VIDEO_FULLSCREEN: elif message.type == MessageType.VIDEO_FULLSCREEN:
logger.info("Calling _handle_video_fullscreen handler") if self.debug_player:
logger.info("Calling _handle_video_fullscreen handler")
self._handle_video_fullscreen(message) self._handle_video_fullscreen(message)
elif message.type == MessageType.TEMPLATE_CHANGE: elif message.type == MessageType.TEMPLATE_CHANGE:
logger.info("Calling _handle_template_change handler") if self.debug_player:
logger.info("Calling _handle_template_change handler")
self._handle_template_change(message) self._handle_template_change(message)
elif message.type == MessageType.OVERLAY_UPDATE: elif message.type == MessageType.OVERLAY_UPDATE:
logger.info("Calling _handle_overlay_update handler") if self.debug_player:
logger.info("Calling _handle_overlay_update handler")
self._handle_overlay_update(message) self._handle_overlay_update(message)
elif message.type == MessageType.STATUS_REQUEST: elif message.type == MessageType.STATUS_REQUEST:
logger.info("Calling _handle_status_request handler") if self.debug_player:
logger.info("Calling _handle_status_request handler")
self._handle_status_request(message) 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: else:
logger.warning(f"No handler for message type: {message.type.value}") if self.debug_player:
logger.warning(f"No handler for message type: {message.type.value}")
except Exception as e: except Exception as e:
logger.error(f"Failed to process message: {e}") logger.error(f"Failed to process message: {e}")
import traceback import traceback
...@@ -2519,23 +2679,24 @@ class QtVideoPlayer(QObject): ...@@ -2519,23 +2679,24 @@ class QtVideoPlayer(QObject):
reload_template = template_data.get("reload_template", False) reload_template = template_data.get("reload_template", False)
load_specific_template = template_data.get("load_specific_template", "") load_specific_template = template_data.get("load_specific_template", "")
logger.debug(f"GREEN SCREEN DEBUG: Template change message received") if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Template name: {template_name}") logger.debug(f"GREEN SCREEN DEBUG: Template change message received")
logger.debug(f"GREEN SCREEN DEBUG: Reload template: {reload_template}") logger.debug(f"GREEN SCREEN DEBUG: Template name: {template_name}")
logger.debug(f"GREEN SCREEN DEBUG: Load specific template: {load_specific_template}") logger.debug(f"GREEN SCREEN DEBUG: Reload template: {reload_template}")
logger.debug(f"GREEN SCREEN DEBUG: Load specific template: {load_specific_template}")
if self.window and hasattr(self.window, 'window_overlay'): if self.window and hasattr(self.window, 'window_overlay'):
overlay_view = self.window.window_overlay overlay_view = self.window.window_overlay
# Check video player state before template change # 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() 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 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 position: {self.window.media_player.position()}")
logger.debug(f"GREEN SCREEN DEBUG: Video duration: {self.window.media_player.duration()}") logger.debug(f"GREEN SCREEN DEBUG: Video duration: {self.window.media_player.duration()}")
# Check overlay window transparency state # 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 geometry: {self.window.overlay_window.geometry()}")
logger.debug(f"GREEN SCREEN DEBUG: Overlay window visible: {self.window.overlay_window.isVisible()}") logger.debug(f"GREEN SCREEN DEBUG: Overlay window visible: {self.window.overlay_window.isVisible()}")
...@@ -2543,27 +2704,32 @@ class QtVideoPlayer(QObject): ...@@ -2543,27 +2704,32 @@ class QtVideoPlayer(QObject):
video_widget = None video_widget = None
if hasattr(self.window, 'video_widget') and hasattr(self.window.video_widget, 'get_video_widget'): if hasattr(self.window, 'video_widget') and hasattr(self.window.video_widget, 'get_video_widget'):
video_widget = self.window.video_widget.get_video_widget() video_widget = self.window.video_widget.get_video_widget()
logger.debug(f"GREEN SCREEN FIX: Video widget state before template change - visible: {video_widget.isVisible() if video_widget else 'N/A'}") 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 # Load specific template if requested and using WebEngine overlay
if load_specific_template and isinstance(overlay_view, OverlayWebView): if load_specific_template and isinstance(overlay_view, OverlayWebView):
logger.debug(f"GREEN SCREEN DEBUG: About to load specific template: {load_specific_template}") if self.debug_overlay:
logger.debug(f"GREEN SCREEN FIX: Protecting video rendering during template load") 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) overlay_view.load_template(load_specific_template)
logger.debug(f"GREEN SCREEN DEBUG: Specific template load initiated") if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Specific template load initiated")
# Otherwise reload current template if requested and using WebEngine overlay # Otherwise reload current template if requested and using WebEngine overlay
elif reload_template and isinstance(overlay_view, OverlayWebView): elif reload_template and isinstance(overlay_view, OverlayWebView):
logger.debug(f"GREEN SCREEN DEBUG: About to reload current template") if self.debug_overlay:
logger.debug(f"GREEN SCREEN FIX: Protecting video rendering during template reload") 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() overlay_view.reload_current_template()
logger.debug(f"GREEN SCREEN DEBUG: Current template reload initiated") if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Current template reload initiated")
# CRITICAL FIX: Force video widget refresh after template change # 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") logger.debug(f"GREEN SCREEN FIX: Forcing video widget refresh after template change")
video_widget.repaint() video_widget.repaint()
video_widget.update() video_widget.update()
# Force media player to refresh video output # Force media player to refresh video output
if hasattr(self.window, 'media_player'): if hasattr(self.window, 'media_player'):
logger.debug(f"GREEN SCREEN FIX: Refreshing media player video output") logger.debug(f"GREEN SCREEN FIX: Refreshing media player video output")
...@@ -2573,30 +2739,33 @@ class QtVideoPlayer(QObject): ...@@ -2573,30 +2739,33 @@ class QtVideoPlayer(QObject):
self.window.media_player.setVideoOutput(None) self.window.media_player.setVideoOutput(None)
self.window.media_player.setVideoOutput(current_output) self.window.media_player.setVideoOutput(current_output)
logger.debug(f"GREEN SCREEN FIX: Media player video output refreshed") logger.debug(f"GREEN SCREEN FIX: Media player video output refreshed")
logger.debug(f"GREEN SCREEN FIX: Video widget state after template change - visible: {video_widget.isVisible()}") logger.debug(f"GREEN SCREEN FIX: Video widget state after template change - visible: {video_widget.isVisible()}")
# Update overlay data if provided (excluding template control flags) # Update overlay data if provided (excluding template control flags)
if template_data: if template_data:
# Remove template control flags from data to be sent to overlay # Remove template control flags from data to be sent to overlay
data_to_send = {k: v for k, v in template_data.items() data_to_send = {k: v for k, v in template_data.items()
if k not in ['reload_template', 'load_specific_template']} if k not in ['reload_template', 'load_specific_template']}
if data_to_send: if data_to_send:
# Log data summary instead of full content to avoid cluttering logs with HTML # Log data summary instead of full content to avoid cluttering logs with HTML
data_keys = list(data_to_send.keys()) if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Sending overlay data with keys: {data_keys}") 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 # Validate and clean template_data before sending to overlay
cleaned_data = self._clean_overlay_data(data_to_send) cleaned_data = self._clean_overlay_data(data_to_send)
if cleaned_data: # Only send if we have valid data after cleaning if cleaned_data: # Only send if we have valid data after cleaning
self.window._update_overlay_safe(overlay_view, cleaned_data) self.window._update_overlay_safe(overlay_view, cleaned_data)
else: else:
logger.debug("Template data contained only null/undefined values, skipping update") logger.debug("Template data contained only null/undefined values, skipping update")
logger.debug(f"GREEN SCREEN DEBUG: Template change handler completed") if self.debug_overlay:
logger.debug(f"GREEN SCREEN DEBUG: Template change handler completed")
except Exception as e: except Exception as e:
logger.debug(f"GREEN SCREEN DEBUG: Template change handler failed: {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}") logger.error(f"Failed to handle template change: {e}")
def _handle_overlay_update(self, message: Message): def _handle_overlay_update(self, message: Message):
...@@ -2624,6 +2793,370 @@ class QtVideoPlayer(QObject): ...@@ -2624,6 +2793,370 @@ class QtVideoPlayer(QObject):
self._do_status_request(message) self._do_status_request(message)
except Exception as e: except Exception as e:
logger.error(f"Failed to handle status request: {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): def _do_status_request(self, message: Message):
"""Execute status request on main thread""" """Execute status request on main thread"""
......
<!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
...@@ -345,24 +345,13 @@ class DashboardAPI: ...@@ -345,24 +345,13 @@ class DashboardAPI:
logger.info(f"Found {len(builtin_files)} built-in templates in {builtin_templates_dir}") logger.info(f"Found {len(builtin_files)} built-in templates in {builtin_templates_dir}")
else: else:
logger.warning(f"Built-in templates directory not found: {builtin_templates_dir}") logger.warning(f"Built-in templates directory not found: {builtin_templates_dir}")
# Ensure default template is always available # Sort templates: uploaded first, then built-in
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
template_list.sort(key=lambda t: ( 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 1 if t["source"] == "builtin" else 0, # Uploaded before built-in
t["name"] # Alphabetical within each group t["name"] # Alphabetical within each group
)) ))
return {"templates": template_list} return {"templates": template_list}
except Exception as e: except Exception as e:
...@@ -889,31 +878,382 @@ class DashboardAPI: ...@@ -889,31 +878,382 @@ class DashboardAPI:
try: try:
# Only allow deletion of uploaded templates, not built-in ones # Only allow deletion of uploaded templates, not built-in ones
templates_dir = self._get_persistent_templates_dir() templates_dir = self._get_persistent_templates_dir()
# Add .html extension if not present # Add .html extension if not present
if not template_name.endswith('.html'): if not template_name.endswith('.html'):
template_name += '.html' template_name += '.html'
template_path = templates_dir / template_name template_path = templates_dir / template_name
if not template_path.exists(): if not template_path.exists():
return {"error": "Template not found in uploaded templates"} return {"error": "Template not found in uploaded templates"}
# Delete the file # Delete the file
template_path.unlink() template_path.unlink()
logger.info(f"Template deleted: {template_path}") logger.info(f"Template deleted: {template_path}")
return { return {
"success": True, "success": True,
"message": "Template deleted successfully", "message": "Template deleted successfully",
"template_name": Path(template_name).stem "template_name": Path(template_name).stem
} }
except Exception as e: except Exception as e:
logger.error(f"Template deletion error: {e}") logger.error(f"Template deletion error: {e}")
return {"error": str(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 # Route functions for Flask
def get_config_section(section): def get_config_section(section):
......
...@@ -1754,37 +1754,38 @@ def send_custom_message(): ...@@ -1754,37 +1754,38 @@ def send_custom_message():
def get_intro_templates(): def get_intro_templates():
"""Get intro templates configuration""" """Get intro templates configuration"""
try: try:
from pathlib import Path from ..database.models import GameConfigModel
import json import json
import os
from ..config.settings import get_user_data_dir session = api_bp.db_manager.get_session()
try:
# Get data directory # Get intro templates configuration from database
data_dir = Path(get_user_data_dir()) intro_config = session.query(GameConfigModel).filter_by(config_key='intro_templates_config').first()
config_path = data_dir / 'intro_templates.json'
# Default configuration
# Default configuration default_config = {
default_config = { 'templates': [],
'templates': [], 'default_show_time': '00:30',
'default_show_time': '00:30', 'rotating_time': '05:00'
'rotating_time': '05:00' }
}
if intro_config:
if config_path.exists(): try:
try: config = json.loads(intro_config.config_value)
with open(config_path, 'r') as f:
config = json.load(f)
# Merge with defaults to ensure all fields are present # Merge with defaults to ensure all fields are present
for key, value in default_config.items(): for key, value in default_config.items():
if key not in config: if key not in config:
config[key] = value config[key] = value
return jsonify(config) return jsonify(config)
except (json.JSONDecodeError, FileNotFoundError): except (json.JSONDecodeError, TypeError):
logger.warning("Failed to load intro templates config, using defaults") logger.warning("Failed to parse intro templates config from database, using defaults")
return jsonify(default_config)
else:
return jsonify(default_config) return jsonify(default_config)
else:
return jsonify(default_config) finally:
session.close()
except Exception as e: except Exception as e:
logger.error(f"Error loading intro templates: {str(e)}") logger.error(f"Error loading intro templates: {str(e)}")
return jsonify({'templates': [], 'default_show_time': '00:30', 'rotating_time': '05:00'}) return jsonify({'templates': [], 'default_show_time': '00:30', 'rotating_time': '05:00'})
...@@ -1795,72 +1796,96 @@ def get_intro_templates(): ...@@ -1795,72 +1796,96 @@ def get_intro_templates():
def save_intro_templates(): def save_intro_templates():
"""Save intro templates configuration""" """Save intro templates configuration"""
try: try:
from pathlib import Path from ..database.models import GameConfigModel
import json import json
import os
import re import re
from ..config.settings import get_user_data_dir
data = request.get_json() data = request.get_json()
if not data: if not data:
return jsonify({'error': 'No configuration data provided'}), 400 return jsonify({'error': 'No configuration data provided'}), 400
# Validate data structure # Validate data structure
templates = data.get('templates', []) templates = data.get('templates', [])
default_show_time = data.get('default_show_time', '00:30') default_show_time = data.get('default_show_time', '00:30')
rotating_time = data.get('rotating_time', '05:00') 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) # Validate time format (MM:SS)
time_pattern = re.compile(r'^[0-9]{1,2}:[0-5][0-9]$') time_pattern = re.compile(r'^[0-9]{1,2}:[0-5][0-9]$')
if not time_pattern.match(default_show_time): if not time_pattern.match(default_show_time):
return jsonify({'error': 'Invalid default show time format. Use MM:SS format.'}), 400 return jsonify({'error': 'Invalid default show time format. Use MM:SS format.'}), 400
if not time_pattern.match(rotating_time): if not time_pattern.match(rotating_time):
return jsonify({'error': 'Invalid rotating time format. Use MM:SS format.'}), 400 return jsonify({'error': 'Invalid rotating time format. Use MM:SS format.'}), 400
# Validate templates # Validate templates
if not isinstance(templates, list): if not isinstance(templates, list):
return jsonify({'error': 'Templates must be a list'}), 400 return jsonify({'error': 'Templates must be a list'}), 400
for i, template in enumerate(templates): for i, template in enumerate(templates):
if not isinstance(template, dict): if not isinstance(template, dict):
return jsonify({'error': f'Template {i+1} must be an object'}), 400 return jsonify({'error': f'Template {i+1} must be an object'}), 400
if 'name' not in template or 'show_time' not in template: if 'name' not in template or 'show_time' not in template:
return jsonify({'error': f'Template {i+1} must have name and show_time fields'}), 400 return jsonify({'error': f'Template {i+1} must have name and show_time fields'}), 400
if not time_pattern.match(template['show_time']): 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 return jsonify({'error': f'Template {i+1} has invalid show_time format. Use MM:SS format.'}), 400
# Save configuration # Save configuration to database
config = { config = {
'templates': templates, 'templates': templates,
'default_show_time': default_show_time, 'default_show_time': default_show_time,
'rotating_time': rotating_time, 'rotating_time': rotating_time,
'updated_at': datetime.now().isoformat() 'updated_at': datetime.now().isoformat()
} }
# Get data directory and ensure it exists config_json = json.dumps(config)
data_dir = Path(get_user_data_dir()) logger.debug(f"WebDashboard: Config JSON length: {len(config_json)}")
data_dir.mkdir(parents=True, exist_ok=True)
config_path = data_dir / 'intro_templates.json' session = api_bp.db_manager.get_session()
try:
with open(config_path, 'w') as f: # Check if config already exists
json.dump(config, f, indent=2) 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")
return jsonify({ # Update existing config
'success': True, existing_config.config_value = config_json
'message': f'Intro templates configuration saved successfully with {len(templates)} templates', existing_config.updated_at = datetime.utcnow()
'templates': templates, else:
'default_show_time': default_show_time, logger.debug("WebDashboard: Creating new config")
'rotating_time': rotating_time # 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,
'message': f'Intro templates configuration saved successfully with {len(templates)} templates',
'templates': templates,
'default_show_time': default_show_time,
'rotating_time': rotating_time
})
finally:
session.close()
except Exception as e: 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 return jsonify({'error': 'Internal server error'}), 500
...@@ -4410,9 +4435,9 @@ def get_bet_barcode_data(bet_id): ...@@ -4410,9 +4435,9 @@ def get_bet_barcode_data(bet_id):
from ..utils.barcode_utils import format_bet_id_for_barcode, generate_barcode_image from ..utils.barcode_utils import format_bet_id_for_barcode, generate_barcode_image
import base64 import base64
import io import io
bet_uuid = str(bet_id) bet_uuid = str(bet_id)
# Get barcode configuration # Get barcode configuration
if api_bp.db_manager: if api_bp.db_manager:
enabled = api_bp.db_manager.get_config_value('barcode.enabled', False) enabled = api_bp.db_manager.get_config_value('barcode.enabled', False)
...@@ -4434,11 +4459,11 @@ def get_bet_barcode_data(bet_id): ...@@ -4434,11 +4459,11 @@ def get_bet_barcode_data(bet_id):
# Format bet ID for barcode # Format bet ID for barcode
barcode_data = format_bet_id_for_barcode(bet_uuid, standard) barcode_data = format_bet_id_for_barcode(bet_uuid, standard)
# Generate barcode image and convert to base64 # Generate barcode image and convert to base64
barcode_image_bytes = generate_barcode_image(barcode_data, standard, width, height) barcode_image_bytes = generate_barcode_image(barcode_data, standard, width, height)
barcode_base64 = None barcode_base64 = None
if barcode_image_bytes: if barcode_image_bytes:
# generate_barcode_image() returns PNG bytes directly, so encode to base64 # generate_barcode_image() returns PNG bytes directly, so encode to base64
barcode_base64 = base64.b64encode(barcode_image_bytes).decode('utf-8') barcode_base64 = base64.b64encode(barcode_image_bytes).decode('utf-8')
...@@ -4460,4 +4485,84 @@ def get_bet_barcode_data(bet_id): ...@@ -4460,4 +4485,84 @@ def get_bet_barcode_data(bet_id):
except Exception as e: except Exception as e:
logger.error(f"API get bet barcode data error: {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 return jsonify({"error": str(e)}), 500
\ No newline at end of file
...@@ -115,7 +115,7 @@ ...@@ -115,7 +115,7 @@
<h5>All Fixtures</h5> <h5>All Fixtures</h5>
<span id="filtered-count" class="badge bg-secondary">0 fixtures</span> <span id="filtered-count" class="badge bg-secondary">0 fixtures</span>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0" style="padding-bottom: 100px !important;">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover mb-0" id="fixtures-table"> <table class="table table-hover mb-0" id="fixtures-table">
<thead class="table-light"> <thead class="table-light">
......
...@@ -110,7 +110,12 @@ ...@@ -110,7 +110,12 @@
<i class="fas fa-shield-alt me-2"></i>Administrator Actions <i class="fas fa-shield-alt me-2"></i>Administrator Actions
</h6> </h6>
</div> </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"> <button class="btn btn-outline-danger w-100" id="btn-shutdown-app">
<i class="fas fa-power-off me-2"></i>Shutdown Application <i class="fas fa-power-off me-2"></i>Shutdown Application
</button> </button>
...@@ -364,6 +369,51 @@ ...@@ -364,6 +369,51 @@
</div> </div>
</div> </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 %} {% endblock %}
{% block scripts %} {% block scripts %}
...@@ -395,6 +445,10 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -395,6 +445,10 @@ document.addEventListener('DOMContentLoaded', function() {
window.location.href = '/tokens'; window.location.href = '/tokens';
}); });
document.getElementById('btn-upload-intro-video').addEventListener('click', function() {
new bootstrap.Modal(document.getElementById('uploadIntroVideoModal')).show();
});
// Match interval save button // Match interval save button
document.getElementById('btn-save-interval').addEventListener('click', function() { document.getElementById('btn-save-interval').addEventListener('click', function() {
saveMatchInterval(); saveMatchInterval();
...@@ -509,22 +563,22 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -509,22 +563,22 @@ document.addEventListener('DOMContentLoaded', function() {
const icon = document.getElementById('message-icon').value || '📢'; const icon = document.getElementById('message-icon').value || '📢';
const template = document.getElementById('message-template').value || 'text'; const template = document.getElementById('message-template').value || 'text';
const displayTime = parseInt(document.getElementById('message-display-time').value) || 10; const displayTime = parseInt(document.getElementById('message-display-time').value) || 10;
if (!title.trim()) { if (!title.trim()) {
alert('Please enter a message title'); alert('Please enter a message title');
return; return;
} }
if (!content.trim()) { if (!content.trim()) {
alert('Please enter message content'); alert('Please enter message content');
return; return;
} }
if (displayTime < 1 || displayTime > 300) { if (displayTime < 1 || displayTime > 300) {
alert('Display time must be between 1 and 300 seconds'); alert('Display time must be between 1 and 300 seconds');
return; return;
} }
fetch('/api/send-custom-message', { fetch('/api/send-custom-message', {
method: 'POST', method: 'POST',
headers: { headers: {
...@@ -556,6 +610,71 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -556,6 +610,71 @@ document.addEventListener('DOMContentLoaded', function() {
alert('Error sending message: ' + error.message); alert('Error sending message: ' + error.message);
}); });
}); });
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 // Status update functions
function updateSystemStatus() { function updateSystemStatus() {
......
...@@ -69,13 +69,13 @@ ...@@ -69,13 +69,13 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Default Show Time</label> <label class="form-label">Default Show Time</label>
<input type="text" class="form-control" id="defaultShowTime" <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 class="form-text">Default display duration (MM:SS)</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Rotating Interval</label> <label class="form-label">Rotating Interval</label>
<input type="text" class="form-control" id="rotatingTime" <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 class="form-text">Time between template rotations (MM:SS)</div>
</div> </div>
</div> </div>
...@@ -176,8 +176,8 @@ ...@@ -176,8 +176,8 @@
let availableTemplates = []; let availableTemplates = [];
let outcomeAssignments = {}; let outcomeAssignments = {};
let introTemplates = []; let introTemplates = [];
let defaultShowTime = '00:30'; let defaultShowTime = '00:15';
let rotatingTime = '05:00'; let rotatingTime = '00:15';
// Define all possible outcomes // Define all possible outcomes
const allOutcomes = [ const allOutcomes = [
...@@ -517,15 +517,54 @@ ...@@ -517,15 +517,54 @@
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
introTemplates = data.templates || []; introTemplates = data.templates || [];
defaultShowTime = data.default_show_time || '00:30'; defaultShowTime = data.default_show_time || '00:15';
rotatingTime = data.rotating_time || '05:00'; 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('defaultShowTime').value = defaultShowTime;
document.getElementById('rotatingTime').value = rotatingTime; document.getElementById('rotatingTime').value = rotatingTime;
}) })
.catch(error => { .catch(error => {
console.error('Error loading intro templates:', error); console.error('Error loading intro templates:', error);
introTemplates = []; 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 @@ ...@@ -696,8 +735,8 @@
const data = { const data = {
templates: introTemplates, templates: introTemplates,
default_show_time: defaultTime || '00:30', default_show_time: defaultTime || '00:15',
rotating_time: rotatingInterval || '05:00' rotating_time: rotatingInterval || '00:15'
}; };
fetch('/api/intro-templates', { fetch('/api/intro-templates', {
...@@ -898,10 +937,6 @@ ...@@ -898,10 +937,6 @@
cursor: grabbing; cursor: grabbing;
} }
/* Add margin at bottom for better scrolling */
.container-fluid {
padding-bottom: 100px;
}
</style> </style>
{% endblock %} {% endblock %}
\ No newline at end of file
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