您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Improved random game picker with provider filter, scan, export, favorites, draggable UI, and more for Betfred All Games page. (No provider search box.)
// ==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(); } } }); })();