// ==UserScript==
// @name Betfred All Games Random + Provider Filter + Random Favorite
// @namespace http://tampermonkey.net/
// @version 1.1.2
// @description Random game picker with provider filter, scan with live count update, yes/maybe/no prompt showing game name, random favorite picker, and UI enhancements on Betfred All Games page with SPA navigation detection.
// @author The Devil
// @match *://www.betfred.com/*
// @match *://betfred.com/*
// @run-at document-idle
// @license MIT
// @grant none
// ==/UserScript==
(function () {
'use strict';
// --- Constants and Selectors ---
const GAME_LINK_SELECTOR = 'a._19pd3t9s[href^="/games/play/"]';
const INFO_BTN_SELECTOR = 'img._zdxht7[alt="More Info"]';
const CLOSE_OVERLAY_SELECTOR = 'span._1ye7m8b[data-actionable][role="button"]';
const PLAYED_GAMES_KEY = 'betfred_played_games';
const MAPPING_KEY = 'betfred_game_to_provider';
const YES_NO_MAYBE_KEY = 'betfred_play_again_feedback';
const SCAN_PROGRESS_KEY = 'betfred_scan_progress';
// --- Provider Aliases ---
const providerAliases = {
'1x2 Gaming': '1x2 Gaming', '4ThePlayer': '4ThePlayer', 'AGS': 'AGS',
'Alchemy Games': 'Alchemy Gaming', 'Alchemy Gaming': 'Alchemy Gaming',
'All For One Studios': 'All For One Studios', 'Area Vegas': 'Area Vegas',
'Aurum Signature Studios': 'Aurum Signature Studios',
'BTG': 'Big Time Gaming', 'Bang Bang': 'Bang Bang', 'Big Time Gaming': 'Big Time Gaming',
'Blue Ring Studios': 'Blue Ring Studios', 'Blueprint': 'Blueprint',
'Boomerang': 'Boomerang', 'Buck Stakes Entertainment': 'Buck Stakes Entertainment',
'BulletProof': 'BulletProof', 'Bulletproof': 'BulletProof',
'Chance Interactive': 'Chance Interactive', 'Circular Arrow': 'Circular Arrow',
'Coin Machine Gaming': 'Coin Machine Gaming',
'Crazy Tooth Studio': 'Crazy Tooth Studios', 'Crazy Tooth Studios': 'Crazy Tooth Studios',
'DWG': 'DWG', 'ELK Studio': 'ELK Studios', 'ELK Studios': 'ELK Studios',
'Fortune Factory': 'Fortune Factory Studios', 'Fortune Factory Studios': 'Fortune Factory Studios',
'Foxium Studios': 'Foxium Studios', 'G Games': 'G Games', 'G Gaming': 'G Games',
'Game Evolution': 'Game Evolution', 'GameBurger Studios': 'Gameburger Studios', 'Gameburger Studios': 'Gameburger Studios',
'Games Global': 'Games Global', 'Gold Coin Studios': 'Gold Coin Studios',
'Golden Rock Studios': 'Golden Rock Studios', 'Hacksaw Gaming': 'Hacksaw Gaming',
'Hammertime Games': 'Hammertime Games', 'High Limit Studio': 'High Limit Studio',
'Hungry Bear Gaming': 'Hungry Bear Gaming', 'IGT': 'IGT', 'INO Games': 'INO Games',
'Infinity Dragon': 'Infinity Dragon Studios', 'Infinity Dragon Studios': 'Infinity Dragon Studios',
'Inspired': 'Inspired', 'Jelly': 'Jelly', 'Just For The Win': 'Just For The Win',
'Light & Wonder': 'Light & Wonder', 'Lightning Box': 'Lightning Box',
'NYX - Pragmatic': 'Pragmatic Play', 'Nailed It Games': 'Nailed It! Games',
'Nailed It! Games': 'Nailed It! Games', 'Nailed it! Games': 'Nailed It! Games',
'Neon Valley Studios': 'Neon Valley Studios', 'NetEnt': 'NetEnt', 'Netent': 'NetEnt',
'NoLimit City': 'NoLimit City', 'Nolimit City': 'NoLimit City',
'Northern Lights': 'Northern Lights Gaming', 'Northern Lights Gaming': 'Northern Lights Gaming',
'Old Skool Studios': 'Old Skool Studios', 'Oros Gaming': 'Oros Gaming',
'Pear Fiction Studios': 'Pear Fiction Studios', 'Peter & Sons': 'Peter & Sons',
"Play'n Go": "Play'n Go", 'Playtech': 'Playtech',
'Pragmatic': 'Pragmatic Play', 'Pragmatic Play': 'Pragmatic Play',
'Prospect Gaming': 'Prospect Gaming', 'Realistic': 'Realistic',
'Red TIger': 'Red Tiger', 'Red Tiger': 'Red Tiger', 'RedTiger': 'Red Tiger',
'Reel Paly': 'Reel Play', 'Reel Play': 'Reel Play', 'ReelPlay': 'Reel Play',
'Reflex Gaming': 'Reflex Gaming', 'Scientific Games': 'Scientific Games',
'Slingshot Studios': 'Slingshot Studios', 'Snowborn Games': 'Snowborn Studios',
'Snowborn Studios': 'Snowborn Studios', 'Spin On': 'Spin Play Games',
'Spin Play Games': 'Spin Play Games', 'SpinPlay Games': 'Spin Play Games',
'Stormcraft Studios': 'Stormcraft Studios', 'Switch Studios': 'Switch Studios',
'Thunderkick': 'Thunderkick', 'Triple Edge Studios': 'Triple Edge Studios',
'Wishbone Games': 'Wishbone Games', 'Wizard Games': 'Wizard Games',
'Yggdrasil': 'Yggdrasil', 'iSoftBet': 'iSoftBet', 'Unknown': 'Unknown'
};
// --- Variables ---
let playedGames = JSON.parse(localStorage.getItem(PLAYED_GAMES_KEY) || '{}');
let providerData = JSON.parse(localStorage.getItem(MAPPING_KEY) || '{}');
let playAgainFeedback = JSON.parse(localStorage.getItem(YES_NO_MAYBE_KEY) || '{}');
let scanProgress = JSON.parse(localStorage.getItem(SCAN_PROGRESS_KEY) || '{"index":0,"completed":false}');
let selectedProvider = '';
let container, optionsPanel;
let randomBtn, randomFavoriteBtn, providerFilterSelect, scanBtn, resetBtn, cancelScanBtn, scanToggleBtn, scanButtonsContainer;
let scanProgressText = null;
let initialized = false;
let scanning = false;
let scanCancelRequested = false;
let gameListObserver = null;
let allGamesLinkObserver = null;
let addOptionsButtonScheduled = false;
// --- Utility ---
function normalizeProvider(name) {
return providerAliases[name.trim()] || name.trim();
}
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Wait for element with timeout
function waitForElement(selector, timeout = 10000) {
return new Promise((resolve, reject) => {
const interval = 100;
let elapsed = 0;
const check = () => {
const el = document.querySelector(selector);
if (el) return resolve(el);
elapsed += interval;
if (elapsed >= timeout) reject(`Element ${selector} not found`);
else setTimeout(check, interval);
};
check();
});
}
// Get all game container divs containing the game links
function getAllGameElements() {
return [...document.querySelectorAll(GAME_LINK_SELECTOR)].map(a => a.closest('div'));
}
// Normalize path from href for mapping keys
function getGamePath(href) {
try {
return new URL(href, location.origin).pathname;
} catch {
return '';
}
}
// Save data to localStorage
function saveData() {
localStorage.setItem(PLAYED_GAMES_KEY, JSON.stringify(playedGames));
localStorage.setItem(MAPPING_KEY, JSON.stringify(providerData));
localStorage.setItem(YES_NO_MAYBE_KEY, JSON.stringify(playAgainFeedback));
localStorage.setItem(SCAN_PROGRESS_KEY, JSON.stringify(scanProgress));
}
// --- SPA URL Change Detection ---
function onSPAUrlChange(callback) {
let lastUrl = location.href;
const pushState = history.pushState;
history.pushState = function () {
pushState.apply(this, arguments);
callback(location.href);
};
const replaceState = history.replaceState;
history.replaceState = function () {
replaceState.apply(this, arguments);
callback(location.href);
};
window.addEventListener('popstate', () => {
callback(location.href);
});
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
callback(location.href);
}
}, 300);
}
// --- Title Prettifier ---
function prettifyTitle(title) {
return title
.replace(/[_\-]+/g, ' ') // Replace underscores/dashes with space
.replace(/([a-z])([A-Z])/g, '$1 $2') // Split camelCase
.replace(/(\D)(\d)/g, '$1 $2') // Space before numbers
.replace(/(\d)([A-Za-z])/g, '$1 $2') // Space after numbers
.replace(/\b(\d+)k\b/gi, (_, num) => `${num}K`) // 4k → 4K
.replace(/\b(\d{4,})\b/g, n => Number(n).toLocaleString()) // 4000 → 4,000
.replace(/([a-z])([A-Z])/g, '$1 $2') // Again handle PascalCase
.replace(/\s+/g, ' ') // Collapse extra spaces
.trim()
.replace(/\b\w/g, c => c.toUpperCase()); // Capitalize first letter of each word
}
async function initialize() {
if (initialized) return;
initialized = true;
// Reload saved data from localStorage
playedGames = JSON.parse(localStorage.getItem(PLAYED_GAMES_KEY) || '{}');
providerData = JSON.parse(localStorage.getItem(MAPPING_KEY) || '{}');
playAgainFeedback = JSON.parse(localStorage.getItem(YES_NO_MAYBE_KEY) || '{}');
scanProgress = JSON.parse(localStorage.getItem(SCAN_PROGRESS_KEY) || '{"index":0,"completed":false}');
createOptionsPanel();
await addOptionsButton();
observeAllGamesLink();
filterGamesByProvider();
updateRandomButtonText();
updateScanButtonText();
}
// Initialize on first load if on All Games page
if (location.pathname === '/games/category/all-games') {
waitForElement(GAME_LINK_SELECTOR, 15000)
.then(() => {
initialize();
observeGameListChanges();
})
.catch(e => {
console.warn('Games did not load on initial page load:', e);
});
}
onSPAUrlChange(async (newUrl) => {
const urlPath = new URL(newUrl).pathname;
if (urlPath === '/games/category/all-games') {
try {
await waitForElement(GAME_LINK_SELECTOR, 15000);
await waitForElement('a._1rwiby3._mdg8s6x[href="/games/category/all-games"]', 10000);
observeAllGamesLink();
if (!initialized) {
playedGames = JSON.parse(localStorage.getItem(PLAYED_GAMES_KEY) || '{}');
providerData = JSON.parse(localStorage.getItem(MAPPING_KEY) || '{}');
playAgainFeedback = JSON.parse(localStorage.getItem(YES_NO_MAYBE_KEY) || '{}');
scanProgress = JSON.parse(localStorage.getItem(SCAN_PROGRESS_KEY) || '{"index":0,"completed":false}');
await initialize();
observeGameListChanges();
} else {
if (container) container.style.display = 'inline-block';
filterGamesByProvider();
updateProviderDropdownCounts();
updateRandomButtonText();
updateScanButtonText();
}
} catch (err) {
initialized = false;
if (optionsPanel) optionsPanel.style.display = 'none';
if (container) container.style.display = 'none';
disconnectGameListObserver();
console.warn('Game elements did not load on SPA navigation:', err);
}
} else {
if (initialized) {
initialized = false;
if (optionsPanel) optionsPanel.style.display = 'none';
if (container) container.style.display = 'none';
disconnectGameListObserver();
}
}
});
// --- UI Creation ---
// Create the floating options panel with controls
function createOptionsPanel() {
optionsPanel = document.createElement('div');
optionsPanel.style.position = 'absolute';
optionsPanel.style.backgroundColor = '#0a5bab';
optionsPanel.style.color = '#fff';
optionsPanel.style.padding = '10px';
optionsPanel.style.borderRadius = '5px';
optionsPanel.style.display = 'none';
optionsPanel.style.zIndex = '10000';
optionsPanel.style.width = '340px';
optionsPanel.style.fontFamily = 'Arial, sans-serif';
optionsPanel.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
optionsPanel.style.userSelect = 'none';
randomBtn = document.createElement('button');
randomBtn.style.width = '100%';
randomBtn.style.marginBottom = '10px';
styleButton(randomBtn, '#1877f2', '#005bb5');
randomBtn.onclick = pickRandomGame;
optionsPanel.appendChild(randomBtn);
randomFavoriteBtn = document.createElement('button');
randomFavoriteBtn.style.width = '100%';
randomFavoriteBtn.style.marginBottom = '10px';
styleButton(randomFavoriteBtn, '#28a745', '#1e7e34');
randomFavoriteBtn.onclick = pickRandomFavoriteGame;
optionsPanel.appendChild(randomFavoriteBtn);
// Then call the update function once to set initial button labels:
updateRandomButtonText();
// Provider filter dropdown
providerFilterSelect = document.createElement('select');
providerFilterSelect.style.width = '100%';
providerFilterSelect.style.margin = '10px 0 15px 0';
providerFilterSelect.style.padding = '6px';
providerFilterSelect.style.borderRadius = '3px';
providerFilterSelect.style.fontSize = '14px';
providerFilterSelect.style.cursor = 'pointer';
providerFilterSelect.style.color = '#000';
// Add default option showing total count of all games
const defaultOption = document.createElement('option');
defaultOption.value = '';
defaultOption.textContent = `Select Provider (All Games: ${getAllGameElements().length})`;
providerFilterSelect.appendChild(defaultOption);
// Add all unique providers sorted alphabetically
const uniqueProviders = [...new Set(Object.values(providerAliases))].sort((a, b) => a.localeCompare(b));
uniqueProviders.forEach(provider => {
const option = document.createElement('option');
option.value = provider;
option.textContent = provider + ' (0)';
providerFilterSelect.appendChild(option);
});
providerFilterSelect.onchange = () => {
selectedProvider = providerFilterSelect.value;
filterGamesByProvider();
updateRandomButtonText();
updateScanButtonText();
};
optionsPanel.appendChild(providerFilterSelect);
// Create container to hold scan and reset buttons, hidden by default
scanButtonsContainer = document.createElement('div');
scanButtonsContainer.style.display = 'none';
scanButtonsContainer.style.justifyContent = 'space-between';
scanButtonsContainer.style.gap = '10px';
scanButtonsContainer.style.marginBottom = '10px';
// Scan Button
scanBtn = document.createElement('button');
updateScanButtonText();
styleButton(scanBtn, '#e03e2f', '#b52a1f');
scanBtn.style.flexGrow = '1';
scanBtn.onclick = () => {
if (scanning) return;
scanCancelRequested = false;
const visibleGames = getAllGameElements().filter(div => div.style.display !== 'none');
const totalVisible = visibleGames.length;
const totalKnown = Object.keys(providerData).length;
if (!scanProgress.completed) {
// Resume incomplete scan
scanProviders(false);
return;
}
if (totalVisible === 0) {
// Nothing to scan
scanProgress = { index: 0, completed: false };
saveData();
scanProviders(false);
} else if (totalVisible !== totalKnown) {
// Partial data: perform incremental scan
scanProviders(false);
} else {
alert('No new games to scan. Provider data is up to date.');
}
};
scanButtonsContainer.appendChild(scanBtn);
// Cancel Scan Button (hidden initially)
cancelScanBtn = document.createElement('button');
cancelScanBtn.textContent = 'Cancel';
styleButton(cancelScanBtn, '#555', '#333');
cancelScanBtn.style.flexGrow = '1';
cancelScanBtn.style.display = 'none';
cancelScanBtn.onclick = () => {
scanCancelRequested = true;
};
scanButtonsContainer.appendChild(cancelScanBtn);
// Reset Button
resetBtn = document.createElement('button');
resetBtn.textContent = 'Reset Data';
resetBtn.style.flexGrow = '1';
styleButton(resetBtn, '#888', '#555');
resetBtn.onclick = () => {
showResetOptionsPrompt();
};
scanButtonsContainer.appendChild(resetBtn);
optionsPanel.appendChild(scanButtonsContainer);
// Scan Progress Text
scanProgressText = document.createElement('div');
scanProgressText.style.color = '#fff';
scanProgressText.style.fontSize = '14px';
scanProgressText.style.marginBottom = '8px';
scanProgressText.textContent = '';
optionsPanel.appendChild(scanProgressText);
// Add toggle arrow to show/hide scan/reset buttons, centered
const scanToggleContainer = document.createElement('div');
scanToggleContainer.style.width = '100%';
scanToggleContainer.style.display = 'flex';
scanToggleContainer.style.justifyContent = 'center';
scanToggleContainer.style.marginBottom = '10px';
scanToggleBtn = document.createElement('span');
scanToggleBtn.textContent = '▼';
scanToggleBtn.style.cursor = 'pointer';
scanToggleBtn.style.color = '#fff';
scanToggleBtn.title = 'Show/Hide Scan Options';
scanToggleBtn.onclick = () => {
if (scanButtonsContainer.style.display === 'none') {
scanButtonsContainer.style.display = 'flex';
scanToggleBtn.textContent = '▲';
} else {
scanButtonsContainer.style.display = 'none';
scanToggleBtn.textContent = '▼';
}
};
scanToggleContainer.appendChild(scanToggleBtn);
optionsPanel.insertBefore(scanToggleContainer, scanButtonsContainer);
document.body.appendChild(optionsPanel);
// Position options panel next to the "All Games" link
positionOptionsPanel();
}
// Position options panel relative to All Games link
function positionOptionsPanel() {
const allGamesLink = document.querySelector('a._1rwiby3._mdg8s6x[href="/games/category/all-games"]');
if (!allGamesLink || !optionsPanel) return;
const rect = allGamesLink.getBoundingClientRect();
optionsPanel.style.position = 'absolute';
optionsPanel.style.top = `${rect.bottom + window.scrollY + 5}px`;
optionsPanel.style.left = `${rect.left + window.scrollX}px`;
}
// Add the “Game Options” button next to “All Games” link
async function addOptionsButton() {
if (addOptionsButtonScheduled) return;
addOptionsButtonScheduled = true;
const allGamesLink = await waitForElement('a._1rwiby3._mdg8s6x[href="/games/category/all-games"]', 15000);
if (!allGamesLink) return;
if (container) {
container.style.display = 'inline-block';
return;
}
container = document.createElement('a');
container.textContent = 'Options';
container.title = 'Open Game Options';
container.href = 'javascript:void(0)';
// Match the CSS classes from Betfred
container.className = allGamesLink.className; // copy classes
container.style.marginLeft = '8px'; // adjust spacing if needed
container.onclick = (e) => {
e.preventDefault();
if (optionsPanel) {
optionsPanel.style.display = optionsPanel.style.display === 'block' ? 'none' : 'block';
}
positionOptionsPanel();
};
allGamesLink.parentElement.appendChild(container);
positionOptionsPanel();
}
function updateRandomButtonText() {
const diceSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style="vertical-align: middle;">
<rect width="16" height="16" rx="3" ry="3" fill="#f2f2f2" stroke="#444" stroke-width="1"/>
<circle cx="4" cy="4" r="1.2" fill="#444"/>
<circle cx="8" cy="8" r="1.2" fill="#444"/>
<circle cx="12" cy="12" r="1.2" fill="#444"/>
</svg>`;
const starSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="gold" viewBox="0 0 16 16" style="vertical-align: middle;">
<path d="M8 12.146l3.717 2.184-1-4.147 3.184-2.767-4.262-.358L8 3.5 6.361 7.058l-4.262.358 3.184 2.767-1 4.147z"/>
</svg>`;
function getRandomGameBtnHTML(provider, count) {
const label = provider
? `Random Game (${provider}) (${count})`
: `Random Game (All Providers) (${count})`;
return `${diceSVG} <span style="margin: 0 6px; vertical-align: middle;">${label}</span> ${diceSVG}`;
}
function getRandomFavBtnHTML(provider, count) {
const label = provider
? `Random Favs (${provider}) (${count})`
: `Random Favs (All Providers) (${count})`;
return `${starSVG} <span style="margin: 0 6px; vertical-align: middle;">${label}</span> ${starSVG}`;
}
const allVisibleGames = getAllGameElements().filter(div => div.style.display !== 'none');
let filteredGames = allVisibleGames;
if (selectedProvider) {
filteredGames = allVisibleGames.filter(div => {
const link = div.querySelector(GAME_LINK_SELECTOR);
if (!link) return false;
const path = getGamePath(link.href);
const prov = providerData[path]?.provider;
return prov === selectedProvider;
});
}
const count = filteredGames.length;
if (randomBtn) randomBtn.innerHTML = getRandomGameBtnHTML(selectedProvider, count);
let favoriteGames = Object.entries(playAgainFeedback)
.filter(([path, feedback]) => feedback === 'yes')
.filter(([path]) => {
if (!selectedProvider) return true;
return providerData[path]?.provider === selectedProvider;
});
if (randomFavoriteBtn) randomFavoriteBtn.innerHTML = getRandomFavBtnHTML(selectedProvider, favoriteGames.length);
}
// Update Scan Button text based on provider data completeness
function updateScanButtonText() {
if (!scanBtn) return;
const totalVisible = getAllGameElements().filter(div => div.style.display !== 'none').length;
const totalKnown = Object.keys(providerData).length;
const mismatchTolerance = 10; // allow up to 10 missing games
if (scanning) {
scanBtn.textContent = 'Scanning...';
scanBtn.disabled = true;
scanBtn.style.opacity = '0.6';
scanBtn.style.cursor = 'default';
return;
}
if (!scanProgress.completed) {
scanBtn.textContent = 'Resume Scan';
scanBtn.disabled = false;
scanBtn.style.opacity = '1';
scanBtn.style.cursor = '';
} else if (totalVisible === 0) {
scanBtn.textContent = 'Scan (No Games)';
scanBtn.disabled = true;
scanBtn.style.opacity = '0.6';
scanBtn.style.cursor = 'default';
} else if (totalVisible - totalKnown > mismatchTolerance) {
scanBtn.textContent = 'Scan (Update Required)';
scanBtn.disabled = false;
scanBtn.style.opacity = '1';
scanBtn.style.cursor = '';
} else {
scanBtn.textContent = 'Up to Date';
scanBtn.disabled = true;
scanBtn.style.opacity = '0.6';
scanBtn.style.cursor = 'default';
}
}
// Filter games by selected provider
function filterGamesByProvider() {
const games = getAllGameElements();
games.forEach(div => {
const link = div.querySelector(GAME_LINK_SELECTOR);
if (!link) {
div.style.display = 'none';
return;
}
const path = getGamePath(link.href);
if (!selectedProvider) {
div.style.display = '';
} else {
const provider = providerData[path]?.provider;
div.style.display = provider === selectedProvider ? '' : 'none';
}
});
updateProviderDropdownCounts();
}
// Update provider dropdown counts in options panel
function updateProviderDropdownCounts() {
if (!providerFilterSelect) return;
const allGames = getAllGameElements();
// Count games per provider
const counts = {};
allGames.forEach(div => {
const link = div.querySelector(GAME_LINK_SELECTOR);
if (!link) return;
const path = getGamePath(link.href);
const provider = providerData[path]?.provider;
if (!provider) return;
counts[provider] = (counts[provider] || 0) + 1;
});
// Update option text with counts
[...providerFilterSelect.options].forEach(opt => {
if (!opt.value) {
// default option
const totalCount = allGames.length;
opt.textContent = `Select Provider (All Games: ${totalCount})`;
} else {
opt.textContent = `${opt.value} (${counts[opt.value] || 0})`;
}
});
}
// Show the "Would you play this game again?" prompt with Yes/Maybe/No buttons
function showPlayAgainPrompt(path, gameTitle) {
// If already answered "yes" before, don't show prompt again
if (playAgainFeedback[path] === 'yes') return;
if (document.getElementById('playAgainPrompt')) return; // Already shown
const overlay = document.createElement('div');
overlay.id = 'playAgainPrompt';
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.right = '0';
overlay.style.bottom = '0';
overlay.style.backgroundColor = 'rgba(0,0,0,0.8)';
overlay.style.zIndex = '20000';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
const panel = document.createElement('div');
panel.style.backgroundColor = '#333';
panel.style.color = '#fff';
panel.style.padding = '20px 30px';
panel.style.borderRadius = '8px';
panel.style.textAlign = 'center';
panel.style.maxWidth = '400px';
panel.style.fontFamily = 'Arial, sans-serif';
const title = document.createElement('h2');
title.textContent = 'Would you play this game again?';
panel.appendChild(title);
const nameEl = document.createElement('p');
nameEl.style.marginTop = '10px';
nameEl.style.fontWeight = 'bold';
nameEl.textContent = gameTitle || 'Unknown Game';
panel.appendChild(nameEl);
// Buttons container
const buttonsDiv = document.createElement('div');
buttonsDiv.style.marginTop = '20px';
buttonsDiv.style.display = 'flex';
buttonsDiv.style.justifyContent = 'space-around';
// Yes button
const yesBtn = document.createElement('button');
yesBtn.textContent = 'Yes';
styleButton(yesBtn, '#28a745', '#1e7e34');
yesBtn.onclick = () => {
playAgainFeedback[path] = 'yes';
saveData();
closePrompt();
};
buttonsDiv.appendChild(yesBtn);
// Maybe button
const maybeBtn = document.createElement('button');
maybeBtn.textContent = 'Maybe';
styleButton(maybeBtn, '#ffc107', '#d39e00');
maybeBtn.onclick = () => {
playAgainFeedback[path] = 'maybe';
saveData();
closePrompt();
};
buttonsDiv.appendChild(maybeBtn);
// No button
const noBtn = document.createElement('button');
noBtn.textContent = 'No';
styleButton(noBtn, '#dc3545', '#a71d2a');
noBtn.onclick = () => {
playAgainFeedback[path] = 'no';
saveData();
closePrompt();
};
buttonsDiv.appendChild(noBtn);
panel.appendChild(buttonsDiv);
overlay.appendChild(panel);
document.body.appendChild(overlay);
function closePrompt() {
document.body.removeChild(overlay);
updateRandomButtonText();
updateScanButtonText();
}
}
// --- Scan Providers ---
async function scanProviders(fullScan = false) {
if (scanning) return;
scanning = true;
scanCancelRequested = false;
if (fullScan) {
scanProgress.index = 0;
providerData = {};
scanProgress.completed = false;
saveData();
}
if (scanBtn) scanBtn.style.display = 'none';
if (cancelScanBtn) cancelScanBtn.style.display = '';
if (scanProgressText) scanProgressText.textContent = 'Starting scan...';
try {
const gameDivs = getAllGameElements();
const total = gameDivs.length;
for (let i = scanProgress.index || 0; i < total; i++) {
const gameDiv = gameDivs[i];
const gameLink = gameDiv.querySelector(GAME_LINK_SELECTOR);
const path = gameLink ? getGamePath(gameLink.href) : null;
// Skip already scanned games unless it's a full scan
if (!fullScan && path && providerData[path]) continue;
if (scanCancelRequested) {
scanProgress.index = i;
scanProgress.completed = false;
saveData();
if (scanProgressText) scanProgressText.textContent = 'Scan cancelled.';
scanning = false;
if (scanBtn) scanBtn.style.display = '';
if (cancelScanBtn) cancelScanBtn.style.display = 'none';
return;
}
const infoBtn = gameDiv.querySelector(INFO_BTN_SELECTOR);
if (!infoBtn) {
if (scanProgressText) scanProgressText.textContent = `Skipping game ${i + 1} (no info icon)`;
console.warn(`Skipped game ${i + 1}: missing info icon`, gameDiv); // Log skipped game
await wait(300);
continue;
}
if (scanProgressText) scanProgressText.textContent = `Scanning game ${i + 1} / ${total}...`;
infoBtn.click();
try {
const titleEl = await waitForElement('h4._1dujhhk', 5000);
await wait(100);
let providerName = '';
const lis = [...document.querySelectorAll('li')];
for (const li of lis) {
const text = li.textContent.trim();
if (text.startsWith('Game Provider -')) {
providerName = text.replace('Game Provider -', '').trim();
break;
} else if (text.startsWith('Provider -')) {
providerName = text.replace('Provider -', '').trim();
break;
} else if (text.startsWith('Games Provider -')) {
providerName = text.replace('Games Provider -', '').trim();
break;
}
}
// Default to 'Unknown' if providerName is still empty
if (!providerName) {
providerName = 'Unknown';
}
if (providerName && gameLink && path) {
const normalized = normalizeProvider(providerName);
const gameTitle = titleEl ? titleEl.textContent.trim() : '';
providerData[path] = { provider: normalized, title: gameTitle };
}
} catch {
// Timeout or missing elements, skip
}
const closeBtn = document.querySelector(CLOSE_OVERLAY_SELECTOR);
if (closeBtn) closeBtn.click();
await wait(100);
scanProgress.index = i + 1;
saveData();
}
const missedGames = getMissedGames();
if (missedGames.length > 0) {
if (scanProgressText) scanProgressText.textContent = `Scan completed but missed ${missedGames.length} games. Consider rescanning.`;
console.warn('Missed games during scan:', missedGames);
scanProgress.completed = false; // mark incomplete so user can rescan
} else {
scanProgress.completed = true;
}
scanProgress.index = 0;
saveData();
if (scanProgressText) scanProgressText.textContent = 'Scan completed. Refresh page.';
updateProviderDropdownCounts();
filterGamesByProvider();
updateRandomButtonText();
updateScanButtonText();
} catch (e) {
if (scanProgressText) scanProgressText.textContent = 'Scan error: ' + e.message;
} finally {
scanning = false;
if (scanBtn) scanBtn.style.display = '';
if (cancelScanBtn) cancelScanBtn.style.display = 'none';
updateScanButtonText();
}
}
// --- Reset Data ---
function showResetOptionsPrompt() {
if (document.getElementById('resetOptionsPrompt')) return;
const overlay = document.createElement('div');
overlay.id = 'resetOptionsPrompt';
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.right = '0';
overlay.style.bottom = '0';
overlay.style.backgroundColor = 'rgba(0,0,0,0.8)';
overlay.style.zIndex = '20000';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
const panel = document.createElement('div');
panel.style.backgroundColor = '#333';
panel.style.color = '#fff';
panel.style.padding = '20px 30px';
panel.style.borderRadius = '8px';
panel.style.textAlign = 'center';
panel.style.maxWidth = '400px';
panel.style.fontFamily = 'Arial, sans-serif';
const title = document.createElement('h2');
title.textContent = 'Reset Data Options';
panel.appendChild(title);
const msg = document.createElement('p');
msg.textContent = 'Choose which data to reset:';
panel.appendChild(msg);
const buttonsDiv = document.createElement('div');
buttonsDiv.style.marginTop = '20px';
buttonsDiv.style.display = 'flex';
buttonsDiv.style.justifyContent = 'space-around';
// Reset All
const resetAllBtn = document.createElement('button');
resetAllBtn.textContent = 'Reset All';
styleButton(resetAllBtn, '#dc3545', '#a71d2a');
resetAllBtn.onclick = () => {
playedGames = {};
providerData = {};
playAgainFeedback = {};
scanProgress = { index: 0, completed: false };
saveData();
location.reload();
};
buttonsDiv.appendChild(resetAllBtn);
// Reset Played Games
const resetPlayedBtn = document.createElement('button');
resetPlayedBtn.textContent = 'Reset Played Games';
styleButton(resetPlayedBtn, '#ffc107', '#d39e00');
resetPlayedBtn.onclick = () => {
playedGames = {};
saveData();
location.reload();
};
buttonsDiv.appendChild(resetPlayedBtn);
// Reset Provider Data
const resetProviderBtn = document.createElement('button');
resetProviderBtn.textContent = 'Reset Provider Data';
styleButton(resetProviderBtn, '#007bff', '#0056b3');
resetProviderBtn.onclick = () => {
providerData = {};
scanProgress = { index: 0, completed: false };
saveData();
location.reload();
};
buttonsDiv.appendChild(resetProviderBtn);
// Cancel Button
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Cancel';
styleButton(cancelBtn, '#555', '#333');
cancelBtn.onclick = () => {
document.body.removeChild(overlay);
};
buttonsDiv.appendChild(cancelBtn);
panel.appendChild(buttonsDiv);
overlay.appendChild(panel);
document.body.appendChild(overlay);
}
// --- Style Button Helper ---
function styleButton(btn, bgColor, hoverColor) {
btn.style.backgroundColor = bgColor;
btn.style.color = '#fff';
btn.style.border = 'none';
btn.style.padding = '8px 14px';
btn.style.borderRadius = '4px';
btn.style.cursor = 'pointer';
btn.style.fontWeight = 'bold';
btn.style.fontSize = '14px';
btn.style.transition = 'background-color 0.3s ease';
btn.onmouseenter = () => {
btn.style.backgroundColor = hoverColor;
};
btn.onmouseleave = () => {
btn.style.backgroundColor = bgColor;
};
}
// --- Observe changes in game list to update counts and UI ---
function observeGameListChanges() {
if (gameListObserver) gameListObserver.disconnect();
const gameListContainer = document.querySelector('div._1bue0p6'); // Main container for game tiles; may need adjustment
if (!gameListContainer) return;
gameListObserver = new MutationObserver(() => {
filterGamesByProvider();
updateProviderDropdownCounts();
updateRandomButtonText();
updateScanButtonText();
});
gameListObserver.observe(gameListContainer, { childList: true, subtree: true });
}
function disconnectGameListObserver() {
if (gameListObserver) {
gameListObserver.disconnect();
gameListObserver = null;
}
}
// Observe All Games link to reposition options panel if page layout changes
function observeAllGamesLink() {
if (allGamesLinkObserver) allGamesLinkObserver.disconnect();
const navContainer = document.querySelector('nav._7r22w2h');
if (!navContainer) return;
allGamesLinkObserver = new MutationObserver(() => {
positionOptionsPanel();
});
allGamesLinkObserver.observe(navContainer, { childList: true, subtree: true });
}
// --- Open a game tab and hook close event to show "play again" prompt ---
// We cannot hook the tab close event directly from the opened window, but
// if the user switches back to main page, we can detect focus and show prompt if needed.
window.addEventListener('focus', () => {
// On focus, check if a game was recently opened via random picker
const lastGamePath = sessionStorage.getItem('betfred_last_opened_game');
if (!lastGamePath) return;
// Only show prompt if it's a known game AND we haven't recorded feedback yet
const gameData = providerData[lastGamePath];
const feedbackExists = playAgainFeedback[lastGamePath];
if (gameData && !feedbackExists) {
const title = prettifyTitle(gameData.title || 'Unknown Game');
showPlayAgainPrompt(lastGamePath, title);
}
// Clear the session marker so it doesn't repeat
sessionStorage.removeItem('betfred_last_opened_game');
});
// Modify pickRandomGame and pickRandomFavoriteGame to store last opened game for prompt
function openGameWithPrompt(linkHref) {
sessionStorage.setItem('betfred_last_opened_game', getGamePath(linkHref));
window.open(linkHref, '_blank');
}
// Updated pick random game to use openGameWithPrompt
function pickRandomGame() {
const games = getAllGameElements().filter(div => div.style.display !== 'none');
if (games.length === 0) {
alert('No games available for the selected provider.');
return;
}
const gameDiv = games[Math.floor(Math.random() * games.length)];
const gameLink = gameDiv.querySelector(GAME_LINK_SELECTOR);
if (!gameLink) return;
const path = getGamePath(gameLink.href);
playedGames[path] = true;
saveData();
openGameWithPrompt(gameLink.href);
updateRandomButtonText();
}
// Updated pick random favorite game to use openGameWithPrompt
function pickRandomFavoriteGame() {
const yesGames = Object.entries(playAgainFeedback).filter(([path, feedback]) => feedback === 'yes');
if (yesGames.length === 0) {
alert('No favourite games found. Please mark some games as "Yes" in the play again prompt.');
return;
}
const [chosenPath] = yesGames[Math.floor(Math.random() * yesGames.length)];
const gameDiv = getAllGameElements().find(gameDiv => {
const link = gameDiv.querySelector(GAME_LINK_SELECTOR);
if (!link) return false;
const linkPath = getGamePath(link.href);
return linkPath === chosenPath;
});
if (!gameDiv) {
alert('Favourite game not found in the current list.');
return;
}
playedGames[chosenPath] = true;
saveData();
const gameLink = gameDiv.querySelector(GAME_LINK_SELECTOR);
if (gameLink) {
openGameWithPrompt(gameLink.href);
}
updateRandomButtonText();
}
function getMissedGames() {
const allVisibleGames = getAllGameElements().filter(div => div.style.display !== 'none');
const missed = [];
allVisibleGames.forEach(div => {
const link = div.querySelector(GAME_LINK_SELECTOR);
if (!link) return;
const path = getGamePath(link.href);
if (!(path in providerData)) {
missed.push(path);
}
});
return missed;
}
})();