Implement keyboard navigation and improve receipt printing

- Add keyboard navigation to /bets and /bets/new pages (admin and cashier)
- Add keyboard navigation to /cashier/verify-bet page with auto-focus
- Implement conditional QR code and barcode printing based on settings
- Remove fist icon and MBETTER title from printed receipts
- Remove FIXTURE, ITEMS, and STATUS lines from receipts
- Add python-barcode dependency for barcode generation
- Update PyInstaller build script with barcode imports
- Apply receipt changes to all bet list and detail pages
parent 35f6eccf
......@@ -197,6 +197,15 @@ def collect_hidden_imports() -> List[str]:
# Other dependencies
'packaging',
'pkg_resources',
# Barcode generation
'barcode',
'barcode.codex',
'barcode.ean',
'barcode.isbn',
'barcode.code39',
'barcode.code128',
'barcode.pzn',
]
# Conditionally add ffmpeg module if available
......
......@@ -316,7 +316,7 @@ class GeneralConfig:
app_name: str = "MbetterClient"
log_level: str = "INFO"
enable_qt: bool = True
match_interval: int = 20 # Default match interval in minutes
match_interval: int = 5 # Default match interval in minutes
@dataclass
......
......@@ -387,7 +387,7 @@ class MatchTimerComponent(ThreadedComponent):
try:
if self.config_manager:
general_config = self.config_manager.get_section_config("general") or {}
return general_config.get('match_interval', 20)
return general_config.get('match_interval', 5)
else:
return 20 # Default fallback
except Exception as e:
......
......@@ -1046,9 +1046,9 @@ def get_match_interval():
try:
if api_bp.config_manager:
general_config = api_bp.config_manager.get_section_config("general") or {}
match_interval = general_config.get('match_interval', 20) # Default 20 minutes
match_interval = general_config.get('match_interval', 5) # Default 5 minutes
else:
match_interval = 20 # Default fallback
match_interval = 5 # Default fallback
return jsonify({
"success": True,
......@@ -1778,9 +1778,12 @@ def get_intro_templates():
# Default configuration
default_config = {
'templates': [],
'default_show_time': '00:30',
'rotating_time': '05:00'
'templates': [
{'name': 'fixtures', 'show_time': '00:15'},
{'name': 'match', 'show_time': '00:15'}
],
'default_show_time': '00:15',
'rotating_time': '00:15'
}
if intro_config:
......@@ -2390,9 +2393,9 @@ def get_match_timer_config():
try:
if api_bp.config_manager:
general_config = api_bp.config_manager.get_section_config("general") or {}
match_interval = general_config.get('match_interval', 20) # Default 20 minutes
match_interval = general_config.get('match_interval', 5) # Default 5 minutes
else:
match_interval = 20 # Default fallback
match_interval = 5 # Default fallback
return jsonify({
"success": True,
......
......@@ -429,13 +429,9 @@ function generateReceiptHtml(betData) {
let receiptHtml = `
<div class="thermal-receipt-content">
<!-- Header with Boxing Glove Icon -->
<!-- Header -->
<div class="receipt-header">
<div class="receipt-logo">
<i class="fas fa-hand-rock boxing-glove"></i>
</div>
<div class="receipt-title">MBETTER</div>
<div class="receipt-subtitle">BETTING SLIP</div>
<div class="receipt-title">BETTING SLIP</div>
</div>
<!-- Separator -->
......@@ -514,16 +510,9 @@ function generateReceiptHtml(betData) {
<!-- Separator -->
<div class="receipt-separator">================================</div>
<!-- QR Code and Barcode -->
<div class="receipt-verification">
<div class="receipt-qr">
<div class="qr-code" id="qr-code-${betData.uuid}"></div>
<div class="qr-text">Scan QR for verification</div>
</div>
<div class="receipt-barcode" id="barcode-container-${betData.uuid}" style="display: none;">
<div class="barcode-image" id="barcode-${betData.uuid}"></div>
<div class="barcode-text">Scan barcode for verification</div>
</div>
<!-- QR Code and Barcode (conditional based on settings) -->
<div class="receipt-verification" id="receipt-verification-${betData.uuid}">
<!-- QR Code and Barcode will be conditionally added here -->
</div>
<!-- Footer -->
......@@ -535,10 +524,9 @@ function generateReceiptHtml(betData) {
</div>
`;
// Generate QR code and barcode after inserting HTML
// Generate QR code and barcode after inserting HTML (conditional)
setTimeout(() => {
generateQRCode(betData.uuid);
generateBarcodeForReceipt(betData.uuid);
generateVerificationCodes(betData.uuid);
}, 100);
return receiptHtml;
......@@ -554,30 +542,88 @@ function generateQRCode(betUuid) {
}
}
function generateBarcodeForReceipt(betUuid) {
// Generate barcode for thermal receipt if enabled
function generateVerificationCodes(betUuid) {
const verificationContainer = document.getElementById(`receipt-verification-${betUuid}`);
if (!verificationContainer) {
return;
}
// Check QR code settings - QR codes should NOT print if disabled
// Default to NOT showing QR codes if API fails
let shouldShowQR = false;
try {
// Try to get QR settings from API
fetch('/api/qrcode-settings')
.then(response => {
if (!response.ok) {
console.warn('QR settings API failed with status:', response.status);
return null;
}
return response.json();
})
.then(qrSettings => {
if (qrSettings && qrSettings.success && qrSettings.settings) {
shouldShowQR = qrSettings.settings.enabled === true && qrSettings.settings.show_on_thermal === true;
console.log('QR settings check result:', shouldShowQR, qrSettings.settings);
} else {
console.warn('Invalid QR settings response:', qrSettings);
shouldShowQR = false; // Default to not showing
}
// Add QR code if enabled
if (shouldShowQR) {
console.log('Adding QR code to receipt');
const qrHtml = `
<div class="receipt-qr">
<div class="qr-code" id="qr-code-${betUuid}"></div>
<div class="qr-text">Scan QR for verification</div>
</div>
`;
verificationContainer.insertAdjacentHTML('beforeend', qrHtml);
// Generate QR code
const qrContainer = document.getElementById(`qr-code-${betUuid}`);
if (qrContainer) {
const qrImageUrl = `https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=${encodeURIComponent(betUuid)}&format=png`;
qrContainer.innerHTML = `<img src="${qrImageUrl}" alt="QR Code" class="qr-image">`;
}
} else {
console.log('QR code disabled - not adding to receipt');
}
})
.catch(error => {
console.warn('Failed to check QR code settings, defaulting to disabled:', error);
shouldShowQR = false; // Default to not showing on error
});
} catch (error) {
console.warn('Error in QR code settings check, defaulting to disabled:', error);
shouldShowQR = false; // Default to not showing on error
}
// Check barcode settings
fetch(`/api/barcode-data/${betUuid}`)
.then(response => response.json())
.then(data => {
if (data.success && data.enabled && data.barcode_data) {
const barcodeData = data.barcode_data;
// Only show barcode if configured for thermal receipts
if (barcodeData.show_on_thermal && barcodeData.image_base64) {
const barcodeContainer = document.getElementById(`barcode-container-${betUuid}`);
const barcodeElement = document.getElementById(`barcode-${betUuid}`);
if (barcodeContainer && barcodeElement) {
// Display the barcode image
barcodeElement.innerHTML = `<img src="data:image/png;base64,${barcodeData.image_base64}" alt="Barcode" class="barcode-img" style="max-width: ${barcodeData.width}px; height: ${barcodeData.height}px;">`;
barcodeContainer.style.display = 'block';
}
if (data.success && data.enabled && data.barcode_data && data.barcode_data.show_on_thermal && data.barcode_data.image_base64) {
// Add barcode to receipt
const barcodeHtml = `
<div class="receipt-barcode" id="barcode-container-${betUuid}">
<div class="barcode-image" id="barcode-${betUuid}"></div>
<div class="barcode-text">Scan barcode for verification</div>
</div>
`;
verificationContainer.insertAdjacentHTML('beforeend', barcodeHtml);
// Display barcode
const barcodeElement = document.getElementById(`barcode-${betUuid}`);
if (barcodeElement) {
barcodeElement.innerHTML = `<img src="data:image/png;base64,${data.barcode_data.image_base64}" alt="Barcode" class="barcode-img" style="width: ${data.barcode_data.width}px; height: ${data.barcode_data.height}px;">`;
}
}
})
.catch(error => {
console.warn('Failed to load barcode data:', error);
// Don't show error to user, just log it - barcodes are optional
console.warn('Failed to check barcode settings:', error);
});
}
......@@ -594,10 +640,7 @@ function printThermalReceipt() {
body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; }
.thermal-receipt-content { width: 100%; }
.receipt-header { text-align: center; margin-bottom: 10px; }
.receipt-logo { font-size: 24px; margin-bottom: 5px; }
.boxing-glove { color: #000; }
.receipt-title { font-size: 18px; font-weight: bold; margin-bottom: 2px; }
.receipt-subtitle { font-size: 12px; margin-bottom: 5px; }
.receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; }
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line {
......@@ -625,16 +668,13 @@ function printThermalReceipt() {
color: #000;
background: #fff;
}
.thermal-receipt-content {
max-width: 300px;
margin: 0 auto;
padding: 10px;
.thermal-receipt-content {
max-width: 300px;
margin: 0 auto;
padding: 10px;
}
.receipt-header { text-align: center; margin-bottom: 15px; }
.receipt-logo { font-size: 28px; margin-bottom: 5px; }
.boxing-glove { color: #000; }
.receipt-title { font-size: 20px; font-weight: bold; margin-bottom: 3px; letter-spacing: 2px; }
.receipt-subtitle { font-size: 14px; margin-bottom: 5px; }
.receipt-separator {
text-align: center;
margin: 12px 0;
......
......@@ -10,6 +10,42 @@
</div>
</div>
<!-- Keyboard Input Form -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-keyboard me-2"></i>Match Selection
</h5>
</div>
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-hashtag"></i>
</span>
<input type="text" class="form-control form-control-lg" id="match-input"
placeholder="Enter match number and press Enter" readonly
style="font-size: 1.2rem; font-weight: bold;">
<span class="input-group-text">
<small class="text-muted">Press digits + Enter to select match</small>
</span>
</div>
</div>
<div class="col-md-4 text-end">
<small class="text-muted">
<i class="fas fa-info-circle me-1"></i>
Use keyboard navigation for faster betting
</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Back Button and Submit Button -->
<div class="row mb-4">
<div class="col-12">
......@@ -171,8 +207,200 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('btn-submit-bet').addEventListener('click', function() {
submitBet();
});
// Initialize keyboard navigation
initializeKeyboardNavigation();
});
// Keyboard navigation state
let keyboardState = {
inputBuffer: '',
currentMatch: null,
currentOutcomeIndex: -1,
outcomes: []
};
function initializeKeyboardNavigation() {
const matchInput = document.getElementById('match-input');
// Global keyboard event listener
document.addEventListener('keydown', function(event) {
// Handle digit input for match selection
if (event.key >= '0' && event.key <= '9' && !isInInputField(event.target)) {
event.preventDefault();
keyboardState.inputBuffer += event.key;
matchInput.value = keyboardState.inputBuffer;
matchInput.classList.add('is-valid');
return;
}
// Handle Enter key for match selection
if (event.key === 'Enter' && keyboardState.inputBuffer && !isInInputField(event.target)) {
event.preventDefault();
const matchNumber = parseInt(keyboardState.inputBuffer);
selectMatchByNumber(matchNumber);
keyboardState.inputBuffer = '';
matchInput.value = '';
matchInput.classList.remove('is-valid');
return;
}
// Handle Tab key for outcome navigation (only when in match)
if (event.key === 'Tab' && keyboardState.currentMatch) {
event.preventDefault();
navigateOutcomes();
return;
}
// Handle digits + Enter for bet amount (when outcome is selected)
if (event.key === 'Enter' && keyboardState.currentOutcomeIndex >= 0 && keyboardState.inputBuffer) {
event.preventDefault();
const amount = parseFloat(keyboardState.inputBuffer);
if (amount > 0) {
enterBetAmount(amount);
}
keyboardState.inputBuffer = '';
matchInput.value = '';
matchInput.classList.remove('is-valid');
return;
}
// Handle Ctrl+Enter for submitting bet
if (event.key === 'Enter' && event.ctrlKey) {
event.preventDefault();
submitBet();
return;
}
// Handle Escape key for canceling
if (event.key === 'Escape') {
event.preventDefault();
if (keyboardState.currentMatch) {
// Close current match and reset
closeCurrentMatch();
} else {
// Go back to bets page
window.location.href = '/bets';
}
return;
}
});
}
function isInInputField(element) {
return element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' || element.tagName === 'SELECT' ||
element.contentEditable === 'true';
}
function selectMatchByNumber(matchNumber) {
// Find match card with this number
const matchCards = document.querySelectorAll('.match-card');
for (const card of matchCards) {
const matchId = card.getAttribute('data-match-id');
const headerText = card.querySelector('h6').textContent;
const matchNumMatch = headerText.match(/Match #(\d+)/);
if (matchNumMatch && parseInt(matchNumMatch[1]) === matchNumber) {
// Open this match
openMatch(card, matchId);
return;
}
}
// Match not found - visual feedback
const matchInput = document.getElementById('match-input');
matchInput.classList.add('is-invalid');
setTimeout(() => matchInput.classList.remove('is-invalid'), 1000);
}
function openMatch(card, matchId) {
// Close any currently open match
closeCurrentMatch();
// Open the selected match
const outcomesDiv = document.getElementById(`outcomes-${matchId}`);
const icon = card.querySelector('.toggle-match i');
if (outcomesDiv && outcomesDiv.style.display === 'none') {
outcomesDiv.style.display = 'block';
icon.className = 'fas fa-chevron-up me-1';
card.querySelector('.toggle-match').innerHTML = '<i class="fas fa-chevron-up me-1"></i>Hide Outcomes';
// Set as current match
keyboardState.currentMatch = matchId;
keyboardState.currentOutcomeIndex = -1;
// Get outcomes for navigation
const outcomeCards = outcomesDiv.querySelectorAll('.card');
keyboardState.outcomes = Array.from(outcomeCards);
// Highlight the match card
card.classList.add('border-primary');
card.style.boxShadow = '0 0 0 0.2rem rgba(13, 110, 253, 0.25)';
}
}
function closeCurrentMatch() {
if (keyboardState.currentMatch) {
const card = document.querySelector(`[data-match-id="${keyboardState.currentMatch}"]`);
if (card) {
const outcomesDiv = document.getElementById(`outcomes-${keyboardState.currentMatch}`);
const icon = card.querySelector('.toggle-match i');
outcomesDiv.style.display = 'none';
icon.className = 'fas fa-chevron-down me-1';
card.querySelector('.toggle-match').innerHTML = '<i class="fas fa-chevron-down me-1"></i>Select Outcomes';
// Remove highlighting
card.classList.remove('border-primary');
card.style.boxShadow = '';
}
keyboardState.currentMatch = null;
keyboardState.currentOutcomeIndex = -1;
keyboardState.outcomes = [];
}
}
function navigateOutcomes() {
if (keyboardState.outcomes.length === 0) return;
// Remove previous highlighting
if (keyboardState.currentOutcomeIndex >= 0) {
keyboardState.outcomes[keyboardState.currentOutcomeIndex].style.border = '';
keyboardState.outcomes[keyboardState.currentOutcomeIndex].style.boxShadow = '';
}
// Move to next outcome
keyboardState.currentOutcomeIndex = (keyboardState.currentOutcomeIndex + 1) % keyboardState.outcomes.length;
// Highlight current outcome
const currentOutcome = keyboardState.outcomes[keyboardState.currentOutcomeIndex];
currentOutcome.style.border = '3px solid #0d6efd';
currentOutcome.style.boxShadow = '0 0 0 0.2rem rgba(13, 110, 253, 0.25)';
// Scroll into view
currentOutcome.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function enterBetAmount(amount) {
if (keyboardState.currentOutcomeIndex < 0 || !keyboardState.outcomes[keyboardState.currentOutcomeIndex]) return;
const outcomeCard = keyboardState.outcomes[keyboardState.currentOutcomeIndex];
const amountInput = outcomeCard.querySelector('.amount-input');
if (amountInput) {
amountInput.value = amount.toFixed(2);
amountInput.dispatchEvent(new Event('input')); // Trigger the input event to update summary
// Visual feedback
amountInput.classList.add('is-valid');
setTimeout(() => amountInput.classList.remove('is-valid'), 500);
// Move to next outcome automatically
setTimeout(() => navigateOutcomes(), 200);
}
}
let selectedOutcomes = new Map(); // matchId -> { outcomes: [], amounts: [] }
// Function to load and display available matches for betting
......
......@@ -164,16 +164,16 @@ let animationFrame = null;
document.addEventListener('DOMContentLoaded', function() {
// Generate QR code for mobile access
generateMobileAccessQR();
// Initialize QR scanner elements
video = document.getElementById('qr-video');
canvas = document.getElementById('qr-canvas');
canvasContext = canvas.getContext('2d');
// Scanner controls
document.getElementById('start-scanner').addEventListener('click', startScanner);
document.getElementById('stop-scanner').addEventListener('click', stopScanner);
// Barcode input handling
const barcodeInput = document.getElementById('barcode-input');
barcodeInput.addEventListener('input', handleBarcodeInput);
......@@ -183,6 +183,11 @@ document.addEventListener('DOMContentLoaded', function() {
processBarcodeInput();
}
});
// Focus on barcode input immediately when page loads
setTimeout(() => {
barcodeInput.focus();
}, 100);
});
function generateMobileAccessQR() {
......
......@@ -10,6 +10,42 @@
</div>
</div>
<!-- Keyboard Input Form -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-keyboard me-2"></i>Match Selection
</h5>
</div>
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-hashtag"></i>
</span>
<input type="text" class="form-control form-control-lg" id="match-input"
placeholder="Enter match number and press Enter" readonly
style="font-size: 1.2rem; font-weight: bold;">
<span class="input-group-text">
<small class="text-muted">Press digits + Enter to select match</small>
</span>
</div>
</div>
<div class="col-md-4 text-end">
<small class="text-muted">
<i class="fas fa-info-circle me-1"></i>
Use keyboard navigation for faster betting
</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Back Button and Submit Button -->
<div class="row mb-4">
<div class="col-12">
......@@ -171,8 +207,200 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('btn-submit-bet').addEventListener('click', function() {
submitBet();
});
// Initialize keyboard navigation
initializeKeyboardNavigation();
});
// Keyboard navigation state
let keyboardState = {
inputBuffer: '',
currentMatch: null,
currentOutcomeIndex: -1,
outcomes: []
};
function initializeKeyboardNavigation() {
const matchInput = document.getElementById('match-input');
// Global keyboard event listener
document.addEventListener('keydown', function(event) {
// Handle digit input for match selection
if (event.key >= '0' && event.key <= '9' && !isInInputField(event.target)) {
event.preventDefault();
keyboardState.inputBuffer += event.key;
matchInput.value = keyboardState.inputBuffer;
matchInput.classList.add('is-valid');
return;
}
// Handle Enter key for match selection
if (event.key === 'Enter' && keyboardState.inputBuffer && !isInInputField(event.target)) {
event.preventDefault();
const matchNumber = parseInt(keyboardState.inputBuffer);
selectMatchByNumber(matchNumber);
keyboardState.inputBuffer = '';
matchInput.value = '';
matchInput.classList.remove('is-valid');
return;
}
// Handle Tab key for outcome navigation (only when in match)
if (event.key === 'Tab' && keyboardState.currentMatch) {
event.preventDefault();
navigateOutcomes();
return;
}
// Handle digits + Enter for bet amount (when outcome is selected)
if (event.key === 'Enter' && keyboardState.currentOutcomeIndex >= 0 && keyboardState.inputBuffer) {
event.preventDefault();
const amount = parseFloat(keyboardState.inputBuffer);
if (amount > 0) {
enterBetAmount(amount);
}
keyboardState.inputBuffer = '';
matchInput.value = '';
matchInput.classList.remove('is-valid');
return;
}
// Handle Ctrl+Enter for submitting bet
if (event.key === 'Enter' && event.ctrlKey) {
event.preventDefault();
submitBet();
return;
}
// Handle Escape key for canceling
if (event.key === 'Escape') {
event.preventDefault();
if (keyboardState.currentMatch) {
// Close current match and reset
closeCurrentMatch();
} else {
// Go back to bets page
window.location.href = '/cashier/bets';
}
return;
}
});
}
function isInInputField(element) {
return element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' || element.tagName === 'SELECT' ||
element.contentEditable === 'true';
}
function selectMatchByNumber(matchNumber) {
// Find match card with this number
const matchCards = document.querySelectorAll('.match-card');
for (const card of matchCards) {
const matchId = card.getAttribute('data-match-id');
const headerText = card.querySelector('h6').textContent;
const matchNumMatch = headerText.match(/Match #(\d+)/);
if (matchNumMatch && parseInt(matchNumMatch[1]) === matchNumber) {
// Open this match
openMatch(card, matchId);
return;
}
}
// Match not found - visual feedback
const matchInput = document.getElementById('match-input');
matchInput.classList.add('is-invalid');
setTimeout(() => matchInput.classList.remove('is-invalid'), 1000);
}
function openMatch(card, matchId) {
// Close any currently open match
closeCurrentMatch();
// Open the selected match
const outcomesDiv = document.getElementById(`outcomes-${matchId}`);
const icon = card.querySelector('.toggle-match i');
if (outcomesDiv && outcomesDiv.style.display === 'none') {
outcomesDiv.style.display = 'block';
icon.className = 'fas fa-chevron-up me-1';
card.querySelector('.toggle-match').innerHTML = '<i class="fas fa-chevron-up me-1"></i>Hide Outcomes';
// Set as current match
keyboardState.currentMatch = matchId;
keyboardState.currentOutcomeIndex = -1;
// Get outcomes for navigation
const outcomeCards = outcomesDiv.querySelectorAll('.card');
keyboardState.outcomes = Array.from(outcomeCards);
// Highlight the match card
card.classList.add('border-primary');
card.style.boxShadow = '0 0 0 0.2rem rgba(13, 110, 253, 0.25)';
}
}
function closeCurrentMatch() {
if (keyboardState.currentMatch) {
const card = document.querySelector(`[data-match-id="${keyboardState.currentMatch}"]`);
if (card) {
const outcomesDiv = document.getElementById(`outcomes-${keyboardState.currentMatch}`);
const icon = card.querySelector('.toggle-match i');
outcomesDiv.style.display = 'none';
icon.className = 'fas fa-chevron-down me-1';
card.querySelector('.toggle-match').innerHTML = '<i class="fas fa-chevron-down me-1"></i>Select Outcomes';
// Remove highlighting
card.classList.remove('border-primary');
card.style.boxShadow = '';
}
keyboardState.currentMatch = null;
keyboardState.currentOutcomeIndex = -1;
keyboardState.outcomes = [];
}
}
function navigateOutcomes() {
if (keyboardState.outcomes.length === 0) return;
// Remove previous highlighting
if (keyboardState.currentOutcomeIndex >= 0) {
keyboardState.outcomes[keyboardState.currentOutcomeIndex].style.border = '';
keyboardState.outcomes[keyboardState.currentOutcomeIndex].style.boxShadow = '';
}
// Move to next outcome
keyboardState.currentOutcomeIndex = (keyboardState.currentOutcomeIndex + 1) % keyboardState.outcomes.length;
// Highlight current outcome
const currentOutcome = keyboardState.outcomes[keyboardState.currentOutcomeIndex];
currentOutcome.style.border = '3px solid #0d6efd';
currentOutcome.style.boxShadow = '0 0 0 0.2rem rgba(13, 110, 253, 0.25)';
// Scroll into view
currentOutcome.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function enterBetAmount(amount) {
if (keyboardState.currentOutcomeIndex < 0 || !keyboardState.outcomes[keyboardState.currentOutcomeIndex]) return;
const outcomeCard = keyboardState.outcomes[keyboardState.currentOutcomeIndex];
const amountInput = outcomeCard.querySelector('.amount-input');
if (amountInput) {
amountInput.value = amount.toFixed(2);
amountInput.dispatchEvent(new Event('input')); // Trigger the input event to update summary
// Visual feedback
amountInput.classList.add('is-valid');
setTimeout(() => amountInput.classList.remove('is-valid'), 500);
// Move to next outcome automatically
setTimeout(() => navigateOutcomes(), 200);
}
}
let selectedOutcomes = new Map(); // matchId -> { outcomes: [], amounts: [] }
// Function to load and display available matches for betting
......
......@@ -660,7 +660,7 @@
document.getElementById('introDropZone').classList.remove('drag-over');
const templateName = e.dataTransfer.getData('text/plain');
const defaultTime = document.getElementById('defaultShowTime').value || '00:30';
const defaultTime = document.getElementById('defaultShowTime').value || '00:15';
// Add template to intro list
const newTemplate = {
......
......@@ -33,6 +33,7 @@ netifaces>=0.11.0
# Video and image processing
opencv-python>=4.5.0
Pillow>=9.0.0
python-barcode[images]>=0.14.0
# Screen capture and streaming (optional dependencies)
ffmpeg-python>=0.2.0
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment