Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
M
MBetterc
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Mbetter
MBetterc
Commits
4d37119a
Commit
4d37119a
authored
Sep 27, 2025
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update player intro
parent
b43952ee
Changes
14
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
2545 additions
and
234 deletions
+2545
-234
INTRO.mp4
assets/INTRO.mp4
+0
-0
main.py
main.py
+22
-1
settings.py
mbetterclient/config/settings.py
+8
-1
application.py
mbetterclient/core/application.py
+11
-4
games_thread.py
mbetterclient/core/games_thread.py
+113
-1
message_bus.py
mbetterclient/core/message_bus.py
+55
-1
player.py
mbetterclient/qt_player/player.py
+656
-123
default.html
mbetterclient/qt_player/templates/default.html
+328
-0
match.html
mbetterclient/qt_player/templates/match.html
+650
-0
api.py
mbetterclient/web_dashboard/api.py
+361
-21
routes.py
mbetterclient/web_dashboard/routes.py
+167
-62
fixtures.html
...terclient/web_dashboard/templates/dashboard/fixtures.html
+1
-1
index.html
mbetterclient/web_dashboard/templates/dashboard/index.html
+124
-5
templates.html
...erclient/web_dashboard/templates/dashboard/templates.html
+49
-14
No files found.
dist/MBetterDiscovery.exe
→
assets/INTRO.mp4
View file @
4d37119a
No preview for this file type
main.py
View file @
4d37119a
...
@@ -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
...
...
mbetterclient/config/settings.py
View file @
4d37119a
...
@@ -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
])
...
...
mbetterclient/core/application.py
View file @
4d37119a
...
@@ -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
...
...
mbetterclient/core/games_thread.py
View file @
4d37119a
...
@@ -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
:
...
...
mbetterclient/core/message_bus.py
View file @
4d37119a
...
@@ -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
mbetterclient/qt_player/player.py
View file @
4d37119a
...
@@ -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"""
...
...
mbetterclient/qt_player/templates/default.html
0 → 100644
View file @
4d37119a
<!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
mbetterclient/qt_player/templates/match.html
0 → 100644
View file @
4d37119a
<!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
mbetterclient/web_dashboard/api.py
View file @
4d37119a
...
@@ -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
):
...
...
mbetterclient/web_dashboard/routes.py
View file @
4d37119a
...
@@ -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
mbetterclient/web_dashboard/templates/dashboard/fixtures.html
View file @
4d37119a
...
@@ -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"
>
...
...
mbetterclient/web_dashboard/templates/dashboard/index.html
View file @
4d37119a
...
@@ -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
()
{
...
...
mbetterclient/web_dashboard/templates/dashboard/templates.html
View file @
4d37119a
...
@@ -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=
"0
5:00
"
pattern=
"[0-9]{1,2}:[0-5][0-9]"
>
placeholder=
"0
0: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
=
'0
5:00
'
;
let
rotatingTime
=
'0
0: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
||
'0
5:00
'
rotating_time
:
rotatingInterval
||
'0
0: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
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment