Update player intro

parent b43952ee
...@@ -143,6 +143,24 @@ Examples: ...@@ -143,6 +143,24 @@ Examples:
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',
action='store_true', action='store_true',
...@@ -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,6 +762,11 @@ class MbetterClientApplication: ...@@ -760,6 +762,11 @@ class MbetterClientApplication:
if self._start_timer_minutes is None: if self._start_timer_minutes is None:
return return
# Special case: --start-timer 0 means 10 seconds delay for system initialization
if self._start_timer_minutes == 0:
delay_seconds = 10
logger.info(f"Starting command line game timer: --start-timer 0 = 10 seconds delay for system initialization")
else:
delay_seconds = self._start_timer_minutes * 60 delay_seconds = self._start_timer_minutes * 60
logger.info(f"Starting command line game timer: {self._start_timer_minutes} minutes ({delay_seconds} seconds)") logger.info(f"Starting command line game timer: {self._start_timer_minutes} minutes ({delay_seconds} seconds)")
......
...@@ -685,6 +685,10 @@ class GamesThread(ThreadedComponent): ...@@ -685,6 +685,10 @@ class GamesThread(ThreadedComponent):
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")
...@@ -208,6 +214,16 @@ class MessageBus: ...@@ -208,6 +214,16 @@ class MessageBus:
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
success_count = 0 success_count = 0
...@@ -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"""
...@@ -589,3 +617,29 @@ class MessageBuilder: ...@@ -589,3 +617,29 @@ class MessageBuilder:
"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
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -1754,14 +1754,13 @@ def send_custom_message(): ...@@ -1754,14 +1754,13 @@ 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
# Get data directory session = api_bp.db_manager.get_session()
data_dir = Path(get_user_data_dir()) try:
config_path = data_dir / 'intro_templates.json' # Get intro templates configuration from database
intro_config = session.query(GameConfigModel).filter_by(config_key='intro_templates_config').first()
# Default configuration # Default configuration
default_config = { default_config = {
...@@ -1770,21 +1769,23 @@ def get_intro_templates(): ...@@ -1770,21 +1769,23 @@ def get_intro_templates():
'rotating_time': '05:00' 'rotating_time': '05:00'
} }
if config_path.exists(): if intro_config:
try: try:
with open(config_path, 'r') as f: config = json.loads(intro_config.config_value)
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) return jsonify(default_config)
else: else:
return jsonify(default_config) 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,11 +1796,9 @@ def get_intro_templates(): ...@@ -1795,11 +1796,9 @@ 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:
...@@ -1810,6 +1809,10 @@ def save_intro_templates(): ...@@ -1810,6 +1809,10 @@ def save_intro_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]$')
...@@ -1833,7 +1836,7 @@ def save_intro_templates(): ...@@ -1833,7 +1836,7 @@ def save_intro_templates():
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,
...@@ -1841,15 +1844,34 @@ def save_intro_templates(): ...@@ -1841,15 +1844,34 @@ def save_intro_templates():
'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'
with open(config_path, 'w') as f: session = api_bp.db_manager.get_session()
json.dump(config, f, indent=2) try:
# Check if config already exists
existing_config = session.query(GameConfigModel).filter_by(config_key='intro_templates_config').first()
logger.info(f"Intro templates configuration saved with {len(templates)} templates") if existing_config:
logger.debug("WebDashboard: Updating existing config")
# Update existing config
existing_config.config_value = config_json
existing_config.updated_at = datetime.utcnow()
else:
logger.debug("WebDashboard: Creating new config")
# Create new config
new_config = GameConfigModel(
config_key='intro_templates_config',
config_value=config_json,
value_type='json',
description='Intro templates configuration for video player',
is_system=False
)
session.add(new_config)
session.commit()
logger.info(f"WebDashboard: Successfully saved intro templates configuration: {len(templates)} templates")
return jsonify({ return jsonify({
'success': True, 'success': True,
...@@ -1859,8 +1881,11 @@ def save_intro_templates(): ...@@ -1859,8 +1881,11 @@ def save_intro_templates():
'rotating_time': rotating_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
...@@ -4461,3 +4486,83 @@ def get_bet_barcode_data(bet_id): ...@@ -4461,3 +4486,83 @@ 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 return jsonify({"error": str(e)}), 500
@api_bp.route('/templates/<template_name>')
def get_template_preview(template_name):
"""Serve template preview with black background - no authentication required"""
try:
api = g.get('api')
if not api:
return "API not available", 500
# Get template preview HTML
preview_html = api.get_template_preview(template_name)
# Return HTML response
from flask import Response
return Response(preview_html, mimetype='text/html')
except Exception as e:
logger.error(f"Template preview route error: {e}")
return f"Error loading template preview: {str(e)}", 500
@api_bp.route('/upload-intro-video', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def upload_intro_video():
"""Upload default intro video file (admin only)"""
try:
from ..config.settings import get_user_data_dir
from werkzeug.utils import secure_filename
import os
# Check if file was uploaded
if 'video_file' not in request.files:
return jsonify({"error": "No video file provided"}), 400
file = request.files['video_file']
overwrite = request.form.get('overwrite', 'false').lower() == 'true'
if file.filename == '':
return jsonify({"error": "No video file selected"}), 400
# Validate file type
allowed_extensions = {'mp4', 'avi', 'mov', 'mkv', 'webm'}
if not ('.' in file.filename and
file.filename.rsplit('.', 1)[1].lower() in allowed_extensions):
return jsonify({"error": "Invalid file type. Allowed: MP4, AVI, MOV, MKV, WebM"}), 400
# Get persistent storage directory
user_data_dir = get_user_data_dir()
videos_dir = user_data_dir / "videos"
videos_dir.mkdir(parents=True, exist_ok=True)
# Set filename for intro video
filename = secure_filename("intro_video." + file.filename.rsplit('.', 1)[1].lower())
filepath = videos_dir / filename
# Check if file already exists
if filepath.exists() and not overwrite:
return jsonify({"error": "Intro video already exists. Check 'overwrite existing' to replace it."}), 400
# Save the file
file.save(str(filepath))
# Store the path in configuration
if api_bp.db_manager:
api_bp.db_manager.set_config_value('intro_video_path', str(filepath))
logger.info(f"Intro video uploaded successfully: {filepath}")
return jsonify({
"success": True,
"message": "Intro video uploaded successfully",
"filename": filename,
"path": str(filepath)
})
except Exception as e:
logger.error(f"API upload intro video error: {e}")
return jsonify({"error": str(e)}), 500
\ No newline at end of file
...@@ -115,7 +115,7 @@ ...@@ -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();
...@@ -557,6 +611,71 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -557,6 +611,71 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
}); });
document.getElementById('confirm-upload-intro-video').addEventListener('click', function() {
const fileInput = document.getElementById('intro-video-file');
const file = fileInput.files[0];
const overwrite = document.getElementById('overwrite-existing').checked;
if (!file) {
alert('Please select a video file to upload');
return;
}
// Validate file size (500MB limit)
const maxSize = 500 * 1024 * 1024; // 500MB in bytes
if (file.size > maxSize) {
alert('File size must be less than 500MB');
return;
}
// Validate file type
const allowedTypes = ['video/mp4', 'video/avi', 'video/quicktime', 'video/x-matroska', 'video/webm'];
if (!allowedTypes.includes(file.type) && !file.name.match(/\.(mp4|avi|mov|mkv|webm)$/i)) {
alert('Please select a valid video file (MP4, AVI, MOV, MKV, or WebM)');
return;
}
const formData = new FormData();
formData.append('video_file', file);
formData.append('overwrite', overwrite);
const uploadBtn = document.getElementById('confirm-upload-intro-video');
const originalText = uploadBtn.innerHTML;
const progressDiv = document.getElementById('upload-progress');
const progressBar = progressDiv.querySelector('.progress-bar');
// Show progress and disable button
progressDiv.classList.remove('d-none');
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
fetch('/api/upload-intro-video', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('uploadIntroVideoModal')).hide();
// Clear the form
document.getElementById('upload-intro-video-form').reset();
alert('Intro video uploaded successfully!');
} else {
alert('Failed to upload video: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
alert('Error uploading video: ' + error.message);
})
.finally(() => {
// Hide progress and restore button
progressDiv.classList.add('d-none');
progressBar.style.width = '0%';
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalText;
});
});
// Status update functions // Status update functions
function updateSystemStatus() { function updateSystemStatus() {
fetch('/api/status') fetch('/api/status')
......
...@@ -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,8 +517,29 @@ ...@@ -517,8 +517,29 @@
.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;
...@@ -526,6 +547,24 @@ ...@@ -526,6 +547,24 @@
.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 %}
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