Betfred All Games Random + Provider Filter + Random Favorite

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.

当前为 2025-07-05 提交的版本,查看 最新版本

// ==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或关注我们的公众号极客氢云获取最新地址