// ==UserScript==
// @name Betfred All Games Random + Provider Filter + Random Favorite (No Provider Search)
// @namespace http://tampermonkey.net/
// @version 1.3.0
// @description Improved random game picker with provider filter, scan, export, favorites, draggable UI, and more for Betfred All Games page. (No provider search box.)
// @author The Devil
// @match *://www.betfred.com/*
// @match *://betfred.com/*
// @run-at document-idle
// @license MIT
// @grant none
// ==/UserScript==
(function () {
'use strict';
// --- State ---
const state = {
playedGames: {},
providerData: {},
playAgainFeedback: {},
scanProgress: { index: 0, completed: false },
selectedProvider: '',
initialized: false,
scanning: false,
scanCancelRequested: false,
addOptionsButtonScheduled: false,
container: null,
optionsPanel: null,
randomBtn: null,
randomFavoriteBtn: null,
providerFilterSelect: null,
scanBtn: null,
resetBtn: null,
cancelScanBtn: null,
scanToggleBtn: null,
scanButtonsContainer: null,
scanProgressText: null,
gameListObserver: null,
allGamesLinkObserver: null,
drag: { active: false, offsetX: 0, offsetY: 0 },
panelPosition: { left: null, top: null }
};
// --- 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';
const PANEL_POSITION_KEY = 'betfred_panel_position';
// --- 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'
};
// --- Utility Functions ---
function normalizeProvider(name) {
return providerAliases[name.trim()] || name.trim();
}
function prettifyTitle(title) {
return title
.replace(/[_\-]+/g, ' ')
.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/(\D)(\d)/g, '$1 $2')
.replace(/(\d)([A-Za-z])/g, '$1 $2')
.replace(/\b(\d+)k\b/gi, (_, num) => `${num}K`)
.replace(/\b(\d{4,})\b/g, n => Number(n).toLocaleString())
.replace(/\s+/g, ' ')
.trim()
.replace(/\b\w/g, c => c.toUpperCase());
}
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
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();
});
}
function getAllGameElements() {
return [...document.querySelectorAll(GAME_LINK_SELECTOR)].map(a => a.closest('div')).filter(Boolean);
}
function getGamePath(href) {
try {
return new URL(href, location.origin).pathname;
} catch {
return '';
}
}
function saveData() {
localStorage.setItem(PLAYED_GAMES_KEY, JSON.stringify(state.playedGames));
localStorage.setItem(MAPPING_KEY, JSON.stringify(state.providerData));
localStorage.setItem(YES_NO_MAYBE_KEY, JSON.stringify(state.playAgainFeedback));
localStorage.setItem(SCAN_PROGRESS_KEY, JSON.stringify(state.scanProgress));
localStorage.setItem(PANEL_POSITION_KEY, JSON.stringify(state.panelPosition));
}
function loadData() {
state.playedGames = JSON.parse(localStorage.getItem(PLAYED_GAMES_KEY) || '{}');
state.providerData = JSON.parse(localStorage.getItem(MAPPING_KEY) || '{}');
state.playAgainFeedback = JSON.parse(localStorage.getItem(YES_NO_MAYBE_KEY) || '{}');
state.scanProgress = JSON.parse(localStorage.getItem(SCAN_PROGRESS_KEY) || '{"index":0,"completed":false}');
state.panelPosition = JSON.parse(localStorage.getItem(PANEL_POSITION_KEY) || '{}');
}
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 state.providerData)) {
missed.push(path);
}
});
return missed;
}
// --- 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);
}
// --- UI Creation & Management ---
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; };
btn.setAttribute('tabindex', '0');
btn.setAttribute('role', 'button');
}
function createOptionsPanel() {
// Panel
state.optionsPanel = document.createElement('div');
state.optionsPanel.setAttribute('role', 'dialog');
state.optionsPanel.setAttribute('aria-label', 'Game Options');
state.optionsPanel.style.position = 'absolute';
state.optionsPanel.style.backgroundColor = '#0a5bab';
state.optionsPanel.style.color = '#fff';
state.optionsPanel.style.padding = '10px';
state.optionsPanel.style.borderRadius = '5px';
state.optionsPanel.style.display = 'none';
state.optionsPanel.style.zIndex = '10000';
state.optionsPanel.style.width = '350px';
state.optionsPanel.style.fontFamily = 'Arial, sans-serif';
state.optionsPanel.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
state.optionsPanel.style.userSelect = 'none';
state.optionsPanel.style.cursor = 'move';
// --- Draggable Panel ---
state.optionsPanel.addEventListener('mousedown', function(e) {
if (e.target !== state.optionsPanel) return;
state.drag.active = true;
state.drag.offsetX = e.clientX - state.optionsPanel.offsetLeft;
state.drag.offsetY = e.clientY - state.optionsPanel.offsetTop;
});
document.addEventListener('mousemove', function(e) {
if (!state.drag.active) return;
state.optionsPanel.style.left = (e.clientX - state.drag.offsetX) + 'px';
state.optionsPanel.style.top = (e.clientY - state.drag.offsetY) + 'px';
// Save position
state.panelPosition.left = state.optionsPanel.style.left;
state.panelPosition.top = state.optionsPanel.style.top;
saveData();
});
document.addEventListener('mouseup', function() {
state.drag.active = false;
});
// --- Provider filter dropdown (NO SEARCH) ---
state.providerFilterSelect = document.createElement('select');
state.providerFilterSelect.style.width = '100%';
state.providerFilterSelect.style.margin = '4px 0 15px 0';
state.providerFilterSelect.style.padding = '6px';
state.providerFilterSelect.style.borderRadius = '3px';
state.providerFilterSelect.style.fontSize = '14px';
state.providerFilterSelect.style.cursor = 'pointer';
state.providerFilterSelect.style.color = '#000';
state.providerFilterSelect.setAttribute('aria-label', 'Select Provider');
state.providerFilterSelect.onchange = () => {
state.selectedProvider = state.providerFilterSelect.value;
filterGamesByProvider();
updateRandomButtonText();
updateScanButtonText();
};
state.optionsPanel.appendChild(state.providerFilterSelect);
// --- Random Game Button ---
state.randomBtn = document.createElement('button');
state.randomBtn.style.width = '100%';
state.randomBtn.style.marginBottom = '8px';
styleButton(state.randomBtn, '#1877f2', '#005bb5');
state.randomBtn.onclick = pickRandomGame;
state.optionsPanel.appendChild(state.randomBtn);
// --- Random Favorite Button ---
state.randomFavoriteBtn = document.createElement('button');
state.randomFavoriteBtn.style.width = '100%';
state.randomFavoriteBtn.style.marginBottom = '10px';
styleButton(state.randomFavoriteBtn, '#28a745', '#1e7e34');
state.randomFavoriteBtn.onclick = pickRandomFavoriteGame;
state.optionsPanel.appendChild(state.randomFavoriteBtn);
// --- Export Button ---
const exportBtn = document.createElement('button');
exportBtn.textContent = 'Export Game List (.txt)';
styleButton(exportBtn, '#17a2b8', '#117a8b');
exportBtn.onclick = exportGameList;
exportBtn.style.width = '100%';
exportBtn.style.marginBottom = '10px';
state.optionsPanel.appendChild(exportBtn);
// --- Scan/Reset Buttons Container ---
state.scanButtonsContainer = document.createElement('div');
state.scanButtonsContainer.style.display = 'none';
state.scanButtonsContainer.style.justifyContent = 'space-between';
state.scanButtonsContainer.style.gap = '10px';
state.scanButtonsContainer.style.marginBottom = '10px';
// Scan Button
state.scanBtn = document.createElement('button');
styleButton(state.scanBtn, '#e03e2f', '#b52a1f');
state.scanBtn.style.flexGrow = '1';
state.scanBtn.onclick = () => {
if (state.scanning) return;
state.scanCancelRequested = false;
scanProviders(false);
};
state.scanButtonsContainer.appendChild(state.scanBtn);
// Cancel Scan Button
state.cancelScanBtn = document.createElement('button');
state.cancelScanBtn.textContent = 'Cancel';
styleButton(state.cancelScanBtn, '#555', '#333');
state.cancelScanBtn.style.flexGrow = '1';
state.cancelScanBtn.style.display = 'none';
state.cancelScanBtn.onclick = () => {
state.scanCancelRequested = true;
};
state.scanButtonsContainer.appendChild(state.cancelScanBtn);
// Reset Button
state.resetBtn = document.createElement('button');
state.resetBtn.textContent = 'Reset Data';
state.resetBtn.style.flexGrow = '1';
styleButton(state.resetBtn, '#888', '#555');
state.resetBtn.onclick = () => {
showResetOptionsPrompt();
};
state.scanButtonsContainer.appendChild(state.resetBtn);
state.optionsPanel.appendChild(state.scanButtonsContainer);
// --- Scan Progress Text ---
state.scanProgressText = document.createElement('div');
state.scanProgressText.style.color = '#fff';
state.scanProgressText.style.fontSize = '14px';
state.scanProgressText.style.marginBottom = '8px';
state.scanProgressText.textContent = '';
state.optionsPanel.appendChild(state.scanProgressText);
// --- Toggle Arrow for Scan/Reset ---
const scanToggleContainer = document.createElement('div');
scanToggleContainer.style.width = '100%';
scanToggleContainer.style.display = 'flex';
scanToggleContainer.style.justifyContent = 'center';
scanToggleContainer.style.marginBottom = '10px';
state.scanToggleBtn = document.createElement('span');
state.scanToggleBtn.textContent = '▼';
state.scanToggleBtn.style.cursor = 'pointer';
state.scanToggleBtn.style.color = '#fff';
state.scanToggleBtn.title = 'Show/Hide Scan Options';
state.scanToggleBtn.onclick = () => {
if (state.scanButtonsContainer.style.display === 'none') {
state.scanButtonsContainer.style.display = 'flex';
state.scanToggleBtn.textContent = '▲';
} else {
state.scanButtonsContainer.style.display = 'none';
state.scanToggleBtn.textContent = '▼';
}
};
scanToggleContainer.appendChild(state.scanToggleBtn);
state.optionsPanel.insertBefore(scanToggleContainer, state.scanButtonsContainer);
document.body.appendChild(state.optionsPanel);
// Panel keyboard navigation: ESC to close
state.optionsPanel.addEventListener('keydown', function(e) {
if (e.key === 'Escape') state.optionsPanel.style.display = 'none';
});
positionOptionsPanel();
}
function positionOptionsPanel() {
// Use saved position if available
if (state.panelPosition.left && state.panelPosition.top) {
state.optionsPanel.style.left = state.panelPosition.left;
state.optionsPanel.style.top = state.panelPosition.top;
return;
}
// Otherwise, position relative to All Games link
const allGamesLink = document.querySelector('a._1rwiby3._mdg8s6x[href="/games/category/all-games"]');
if (!allGamesLink || !state.optionsPanel) return;
const rect = allGamesLink.getBoundingClientRect();
state.optionsPanel.style.position = 'absolute';
state.optionsPanel.style.top = `${rect.bottom + window.scrollY + 5}px`;
state.optionsPanel.style.left = `${rect.left + window.scrollX}px`;
}
async function addOptionsButton() {
if (state.addOptionsButtonScheduled) return;
state.addOptionsButtonScheduled = true;
const allGamesLink = await waitForElement('a._1rwiby3._mdg8s6x[href="/games/category/all-games"]', 15000);
if (!allGamesLink) return;
if (state.container) {
state.container.style.display = 'inline-block';
return;
}
state.container = document.createElement('a');
state.container.textContent = 'Options';
state.container.title = 'Open Game Options';
state.container.href = 'javascript:void(0)';
state.container.className = allGamesLink.className;
state.container.style.marginLeft = '8px';
state.container.onclick = (e) => {
e.preventDefault();
if (state.optionsPanel) {
state.optionsPanel.style.display = state.optionsPanel.style.display === 'block' ? 'none' : 'block';
if (state.optionsPanel.style.display === 'block') {
state.optionsPanel.focus();
}
}
positionOptionsPanel();
};
allGamesLink.parentElement.appendChild(state.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>`;
const allVisibleGames = getAllGameElements().filter(div => div.style.display !== 'none');
let filteredGames = allVisibleGames;
if (state.selectedProvider) {
filteredGames = allVisibleGames.filter(div => {
const link = div.querySelector(GAME_LINK_SELECTOR);
if (!link) return false;
const path = getGamePath(link.href);
const prov = state.providerData[path]?.provider;
return prov === state.selectedProvider;
});
}
const count = filteredGames.length;
if (state.randomBtn) state.randomBtn.innerHTML = `${diceSVG} <span style="margin: 0 6px; vertical-align: middle;">Random Game${state.selectedProvider ? ' (' + state.selectedProvider + ')' : ''} (${count})</span> ${diceSVG}`;
let favoriteGames = Object.entries(state.playAgainFeedback)
.filter(([path, feedback]) => feedback === 'yes')
.filter(([path]) => {
if (!state.selectedProvider) return true;
return state.providerData[path]?.provider === state.selectedProvider;
});
if (state.randomFavoriteBtn) state.randomFavoriteBtn.innerHTML = `${starSVG} <span style="margin: 0 6px; vertical-align: middle;">Random Favs${state.selectedProvider ? ' (' + state.selectedProvider + ')' : ''} (${favoriteGames.length})</span> ${starSVG}`;
}
function updateScanButtonText() {
if (!state.scanBtn) return;
const totalVisible = getAllGameElements().filter(div => div.style.display !== 'none').length;
const totalKnown = Object.keys(state.providerData).length;
const mismatchTolerance = 10;
if (state.scanning) {
state.scanBtn.textContent = 'Scanning...';
state.scanBtn.disabled = true;
state.scanBtn.style.opacity = '0.6';
state.scanBtn.style.cursor = 'default';
return;
}
if (!state.scanProgress.completed) {
state.scanBtn.textContent = 'Resume Scan';
state.scanBtn.disabled = false;
state.scanBtn.style.opacity = '1';
state.scanBtn.style.cursor = '';
} else if (totalVisible === 0) {
state.scanBtn.textContent = 'Scan (No Games)';
state.scanBtn.disabled = true;
state.scanBtn.style.opacity = '0.6';
state.scanBtn.style.cursor = 'default';
} else if (totalVisible - totalKnown > mismatchTolerance) {
state.scanBtn.textContent = 'Scan (Update Required)';
state.scanBtn.disabled = false;
state.scanBtn.style.opacity = '1';
state.scanBtn.style.cursor = '';
} else {
state.scanBtn.textContent = 'Up to Date';
state.scanBtn.disabled = true;
state.scanBtn.style.opacity = '0.6';
state.scanBtn.style.cursor = 'default';
}
}
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 (!state.selectedProvider) {
div.style.display = '';
} else {
const provider = state.providerData[path]?.provider;
div.style.display = provider === state.selectedProvider ? '' : 'none';
}
});
updateProviderDropdownCounts();
}
function updateProviderDropdownCounts() {
if (!state.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 = state.providerData[path]?.provider;
if (!provider) return;
counts[provider] = (counts[provider] || 0) + 1;
});
// Update option text with counts
[...state.providerFilterSelect.options].forEach(opt => {
if (!opt.value) {
// default option
const totalCount = allGames.length;
opt.textContent = `All Providers (${totalCount})`;
} else {
opt.textContent = `${opt.value} (${counts[opt.value] || 0})`;
}
});
}
function updateProviderDropdown() {
// Remove all options
while (state.providerFilterSelect.options.length) state.providerFilterSelect.remove(0);
// Default option
const defaultOption = document.createElement('option');
defaultOption.value = '';
defaultOption.textContent = `All Providers (${getAllGameElements().length})`;
state.providerFilterSelect.appendChild(defaultOption);
// All unique providers sorted
const uniqueProviders = [...new Set(Object.values(state.providerData).map(d => d.provider))].sort((a, b) => a.localeCompare(b));
uniqueProviders.forEach(provider => {
const option = document.createElement('option');
option.value = provider;
option.textContent = provider + ' (0)';
state.providerFilterSelect.appendChild(option);
});
updateProviderDropdownCounts();
}
// --- Export ---
function exportGameList() {
const selectedProvider = state.providerFilterSelect?.value || '';
const allGameDivs = getAllGameElements();
const lines = [];
allGameDivs.forEach(div => {
const link = div.querySelector(GAME_LINK_SELECTOR);
if (!link) return;
const path = getGamePath(link.href);
const data = state.providerData[path];
if (selectedProvider && selectedProvider !== 'All Providers') {
if (!data || normalizeProvider(data.provider) !== selectedProvider) return;
}
const title = data?.title || prettifyTitle(div.textContent.trim()) || 'Unknown Title';
const provider = data?.provider || 'Unknown';
lines.push(`${title} (${provider})`);
});
if (lines.length === 0) {
alert('No games found to export.');
return;
}
const blob = new Blob([lines.join('\n')], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = selectedProvider && selectedProvider !== 'All Providers'
? `games-${selectedProvider}.txt`
: 'all-games.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// --- Scan Providers ---
async function scanProviders(fullScan = false) {
if (state.scanning) return;
state.scanning = true;
state.scanCancelRequested = false;
if (fullScan) {
state.scanProgress.index = 0;
state.providerData = {};
state.scanProgress.completed = false;
saveData();
}
if (state.scanBtn) state.scanBtn.style.display = 'none';
if (state.cancelScanBtn) state.cancelScanBtn.style.display = '';
if (state.scanProgressText) state.scanProgressText.textContent = 'Starting scan...';
try {
const gameDivs = getAllGameElements();
const total = gameDivs.length;
for (let i = state.scanProgress.index || 0; i < total; i++) {
const gameDiv = gameDivs[i];
const gameLink = gameDiv.querySelector(GAME_LINK_SELECTOR);
const path = gameLink ? getGamePath(gameLink.href) : null;
if (!fullScan && path && state.providerData[path]) continue;
if (state.scanCancelRequested) {
state.scanProgress.index = i;
state.scanProgress.completed = false;
saveData();
if (state.scanProgressText) state.scanProgressText.textContent = 'Scan cancelled.';
state.scanning = false;
if (state.scanBtn) state.scanBtn.style.display = '';
if (state.cancelScanBtn) state.cancelScanBtn.style.display = 'none';
return;
}
const infoBtn = gameDiv.querySelector(INFO_BTN_SELECTOR);
if (!infoBtn) {
if (state.scanProgressText) state.scanProgressText.textContent = `Skipping game ${i + 1} (no info icon)`;
await wait(200);
continue;
}
if (state.scanProgressText) state.scanProgressText.textContent = `Scanning game ${i + 1} / ${total}...`;
infoBtn.click();
try {
const titleEl = await waitForElement('h4._1dujhhk', 5000);
await wait(80);
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;
}
}
if (!providerName) providerName = 'Unknown';
if (providerName && gameLink && path) {
const normalized = normalizeProvider(providerName);
const gameTitle = titleEl ? titleEl.textContent.trim() : '';
state.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(80);
state.scanProgress.index = i + 1;
saveData();
}
const missedGames = getMissedGames();
if (missedGames.length > 0) {
if (state.scanProgressText) state.scanProgressText.textContent = `Scan completed but missed ${missedGames.length} games. Consider rescanning.`;
state.scanProgress.completed = false;
} else {
state.scanProgress.completed = true;
}
state.scanProgress.index = 0;
saveData();
if (state.scanProgressText) state.scanProgressText.textContent = 'Scan completed. Refresh page.';
updateProviderDropdown();
filterGamesByProvider();
updateRandomButtonText();
updateScanButtonText();
} catch (e) {
if (state.scanProgressText) state.scanProgressText.textContent = 'Scan error: ' + (e.message || e);
} finally {
state.scanning = false;
if (state.scanBtn) state.scanBtn.style.display = '';
if (state.cancelScanBtn) state.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';
overlay.setAttribute('role', 'dialog');
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 = () => {
state.playedGames = {};
state.providerData = {};
state.playAgainFeedback = {};
state.scanProgress = { index: 0, completed: false };
state.panelPosition = {};
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 = () => {
state.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 = () => {
state.providerData = {};
state.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);
overlay.focus();
}
// --- Play Again Prompt ---
function showPlayAgainPrompt(path, gameTitle) {
if (state.playAgainFeedback[path] === 'yes') return;
if (document.getElementById('playAgainPrompt')) return;
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';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-label', 'Play Again Prompt');
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);
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 = () => {
state.playAgainFeedback[path] = 'yes';
saveData();
closePrompt();
};
buttonsDiv.appendChild(yesBtn);
// Maybe button
const maybeBtn = document.createElement('button');
maybeBtn.textContent = 'Maybe';
styleButton(maybeBtn, '#ffc107', '#d39e00');
maybeBtn.onclick = () => {
state.playAgainFeedback[path] = 'maybe';
saveData();
closePrompt();
};
buttonsDiv.appendChild(maybeBtn);
// No button
const noBtn = document.createElement('button');
noBtn.textContent = 'No';
styleButton(noBtn, '#dc3545', '#a71d2a');
noBtn.onclick = () => {
state.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();
}
overlay.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closePrompt();
});
overlay.focus();
}
// --- Game List Observers ---
function observeGameListChanges() {
if (state.gameListObserver) state.gameListObserver.disconnect();
const gameListContainer = document.querySelector('div._1bue0p6');
if (!gameListContainer) return;
state.gameListObserver = new MutationObserver(() => {
updateProviderDropdown();
filterGamesByProvider();
updateRandomButtonText();
updateScanButtonText();
});
state.gameListObserver.observe(gameListContainer, { childList: true, subtree: true });
}
function disconnectGameListObserver() {
if (state.gameListObserver) {
state.gameListObserver.disconnect();
state.gameListObserver = null;
}
}
function observeAllGamesLink() {
if (state.allGamesLinkObserver) state.allGamesLinkObserver.disconnect();
const navContainer = document.querySelector('nav._7r22w2h');
if (!navContainer) return;
state.allGamesLinkObserver = new MutationObserver(() => {
positionOptionsPanel();
});
state.allGamesLinkObserver.observe(navContainer, { childList: true, subtree: true });
}
// --- Game Opening and Prompts ---
window.addEventListener('focus', () => {
const lastGamePath = sessionStorage.getItem('betfred_last_opened_game');
if (!lastGamePath) return;
const gameData = state.providerData[lastGamePath];
const feedbackExists = state.playAgainFeedback[lastGamePath];
if (gameData && !feedbackExists) {
const title = prettifyTitle(gameData.title || 'Unknown Game');
showPlayAgainPrompt(lastGamePath, title);
}
sessionStorage.removeItem('betfred_last_opened_game');
});
function openGameWithPrompt(linkHref) {
sessionStorage.setItem('betfred_last_opened_game', getGamePath(linkHref));
window.open(linkHref, '_blank');
}
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);
state.playedGames[path] = true;
saveData();
openGameWithPrompt(gameLink.href);
updateRandomButtonText();
}
function pickRandomFavoriteGame() {
const yesGames = Object.entries(state.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)];
let 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;
}
state.playedGames[chosenPath] = true;
saveData();
const gameLink = gameDiv.querySelector(GAME_LINK_SELECTOR);
if (gameLink) openGameWithPrompt(gameLink.href);
updateRandomButtonText();
}
// --- Initialization ---
async function initialize() {
if (state.initialized) return;
state.initialized = true;
loadData();
createOptionsPanel();
await addOptionsButton();
observeAllGamesLink();
updateProviderDropdown();
filterGamesByProvider();
updateRandomButtonText();
updateScanButtonText();
}
// --- SPA Navigation Handling ---
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 (!state.initialized) {
loadData();
await initialize();
observeGameListChanges();
} else {
if (state.container) state.container.style.display = 'inline-block';
if (state.optionsPanel) state.optionsPanel.style.display = 'none';
updateProviderDropdown();
filterGamesByProvider();
updateRandomButtonText();
updateScanButtonText();
}
} catch (err) {
state.initialized = false;
if (state.optionsPanel) state.optionsPanel.style.display = 'none';
if (state.container) state.container.style.display = 'none';
disconnectGameListObserver();
console.warn('Game elements did not load on SPA navigation:', err);
}
} else {
if (state.initialized) {
state.initialized = false;
if (state.optionsPanel) state.optionsPanel.style.display = 'none';
if (state.container) state.container.style.display = 'none';
disconnectGameListObserver();
}
}
});
})();