您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.
当前为
// ==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; } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址