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-04 提交的版本,查看 最新版本

// ==UserScript==
// @name         Betfred All Games Random + Provider Filter + Random Favorite
// @namespace    http://tampermonkey.net/
// @version      1.0.4
// @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'
  };

  // --- 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);
      }
    }, 500);
  }

  // --- 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 (optionsPanel) optionsPanel.style.display = 'block';
        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';

    // Random Game Button
    randomBtn = document.createElement('button');
    randomBtn.textContent = selectedProvider ? `🎲 Random ${selectedProvider} 🎲` : '🎲 Random Game 🎲';
    randomBtn.style.width = '100%';
    randomBtn.style.marginBottom = '10px';
    styleButton(randomBtn, '#1877f2', '#005bb5');
    randomBtn.onclick = pickRandomGame;
    optionsPanel.appendChild(randomBtn);

    // Random Favorite Button (games marked "yes")
    randomFavoriteBtn = document.createElement('button');
    randomFavoriteBtn.textContent = selectedProvider ? `🎯 Random Favorite ${selectedProvider} 🎯` : '🎯 Random Favorite 🎯';
    randomFavoriteBtn.style.width = '100%';
    randomFavoriteBtn.style.marginBottom = '10px';
    styleButton(randomFavoriteBtn, '#28a745', '#1e7e34');
    randomFavoriteBtn.onclick = pickRandomFavoriteGame;
    optionsPanel.appendChild(randomFavoriteBtn);

    // 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 (totalVisible === 0) {
    // No games visible: full scan needed, reset progress
    scanProgress = { index: 0, completed: false };
    saveData();
    scanProviders();
  } else if (totalVisible !== totalKnown) {
    // Game list changed, update scan (resume or start new)
    scanProviders();
  } else {
    // No changes, nothing to scan
    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);

function showResetOptionsPrompt() {
  if (document.getElementById('resetOptionsPrompt')) return; // Prevent multiple prompts

  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.7)';
  overlay.style.zIndex = '12000';
  overlay.style.display = 'flex';
  overlay.style.alignItems = 'center';
  overlay.style.justifyContent = 'center';

  const panel = document.createElement('div');
  panel.style.backgroundColor = '#0a5bab';
  panel.style.color = '#fff';
  panel.style.padding = '20px 30px';
  panel.style.borderRadius = '10px';
  panel.style.textAlign = 'center';
  panel.style.maxWidth = '400px';
  panel.style.fontFamily = 'Arial, sans-serif';

  const message = document.createElement('p');
  message.textContent = 'What would you like to reset? Remember to always refresh the page after.';
  message.style.marginBottom = '20px';
  panel.appendChild(message);

  const buttonsDiv = document.createElement('div');
  buttonsDiv.style.display = 'flex';
  buttonsDiv.style.justifyContent = 'space-around';

  // Reset Everything button
  const resetAllBtn = document.createElement('button');
  resetAllBtn.textContent = 'Reset Everything';
  styleButton(resetAllBtn, '#dc3545', '#a71d2a');
  resetAllBtn.onclick = () => {
    playedGames = {};
    providerData = {};
    playAgainFeedback = {};
    scanProgress = { index: 0, completed: false };
    saveData();
    updateProviderDropdownCounts();
    filterGamesByProvider();
    closePrompt();
    alert('All data has been reset.');
  };
  buttonsDiv.appendChild(resetAllBtn);

  // Reset Feedback Only button
  const resetFeedbackBtn = document.createElement('button');
  resetFeedbackBtn.textContent = 'Reset Feedback Only';
  styleButton(resetFeedbackBtn, '#ffc107', '#d39e00');
  resetFeedbackBtn.onclick = () => {
    playAgainFeedback = {};
    saveData();
    updateProviderDropdownCounts();
    filterGamesByProvider();
    closePrompt();
    alert('Game feedback has been reset please refresh page for it to update.');
  };
  buttonsDiv.appendChild(resetFeedbackBtn);

  // Cancel button
  const cancelBtn = document.createElement('button');
  cancelBtn.textContent = 'Cancel';
  styleButton(cancelBtn, '#888', '#555');
  cancelBtn.onclick = () => {
    closePrompt();
  };
  buttonsDiv.appendChild(cancelBtn);

  panel.appendChild(buttonsDiv);
  overlay.appendChild(panel);
  document.body.appendChild(overlay);

  function closePrompt() {
    if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
  }
}

    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.userSelect = 'none';
    scanToggleBtn.style.fontWeight = 'bold';
    scanToggleBtn.style.fontSize = '18px';
    scanToggleBtn.style.lineHeight = '1';

    scanToggleBtn.onclick = () => {
      if (scanButtonsContainer.style.display === 'none') {
        scanButtonsContainer.style.display = 'flex';
        scanToggleBtn.textContent = '▲';
      } else {
        scanButtonsContainer.style.display = 'none';
        scanToggleBtn.textContent = '▼';
      }
    };

    scanToggleContainer.appendChild(scanToggleBtn);
    optionsPanel.appendChild(scanToggleContainer);

    document.body.appendChild(optionsPanel);
  }

  // Button styling helper
  function styleButton(button, bgColor, hoverColor) {
    button.style.cursor = 'pointer';
    button.style.fontWeight = 'bold';
    button.style.padding = '8px';
    button.style.border = 'none';
    button.style.borderRadius = '3px';
    button.style.backgroundColor = bgColor;
    button.style.color = '#fff';
    button.style.transition = 'background-color 0.3s ease';
    button.onmouseenter = () => (button.style.backgroundColor = hoverColor);
    button.onmouseleave = () => (button.style.backgroundColor = bgColor);
  }

  // Add the "Random Game Options" button next to "All Games" link
async function addOptionsButton() {
  const selector = 'a._1rwiby3._mdg8s6x[href="/games/category/all-games"]';
  try {
    const allGamesLink = await waitForElement(selector, 10000); // wait up to 10s
    if (!allGamesLink) return;
    if (container && document.body.contains(container)) return;

    container = document.createElement('div');
    container.style.display = 'inline-block';
    container.style.marginLeft = '10px';

    const btn = document.createElement('button');
    btn.textContent = 'Random Game Options';
    btn.style.cursor = 'pointer';
    btn.style.fontWeight = 'bold';
    btn.style.padding = '6px 12px';
    btn.style.borderRadius = '4px';
    btn.style.border = 'none';
    btn.style.backgroundColor = '#1877f2';
    btn.style.color = '#fff';
    btn.style.transition = 'background-color 0.3s ease';
    btn.onmouseenter = () => (btn.style.backgroundColor = '#005bb5');
    btn.onmouseleave = () => (btn.style.backgroundColor = '#1877f2');

    container.appendChild(btn);
    allGamesLink.parentNode.insertBefore(container, allGamesLink.nextSibling);

    btn.addEventListener('click', () => {
      if (!optionsPanel) return;
      if (optionsPanel.style.display === 'none') {
        const rect = btn.getBoundingClientRect();
        optionsPanel.style.top = rect.bottom + window.scrollY + 5 + 'px';
        optionsPanel.style.left = rect.left + window.scrollX + 'px';
        optionsPanel.style.display = 'block';
        updateProviderDropdownCounts();
        updateRandomButtonText();
        updateScanButtonText();
      } else {
        optionsPanel.style.display = 'none';
      }
    });
  } catch (e) {
    console.warn('Failed to add options button:', e);
  }
}

function observeGameListChanges() {
  const gameListContainer = document.querySelector('div._dmn8hc[data-actionable="GamesCategoryPage.all-games.GameList"]');

  if (!gameListContainer) return;

  if (gameListObserver) {
    gameListObserver.disconnect();
  }

  gameListObserver = new MutationObserver(() => {
    // When games list changes, refresh UI and counts
    filterGamesByProvider();
    updateProviderDropdownCounts();
    updateRandomButtonText();
      updateScanButtonText();
  });

  gameListObserver.observe(gameListContainer, {
    childList: true,
    subtree: true,
  });
}

function disconnectGameListObserver() {
  if (gameListObserver) {
    gameListObserver.disconnect();
    gameListObserver = null;
  }
}
function observeAllGamesLink() {
  const parent = document.querySelector('nav') || document.body;
  if (!parent) return;

  if (allGamesLinkObserver) allGamesLinkObserver.disconnect();

  allGamesLinkObserver = new MutationObserver(() => {
    if (!addOptionsButtonScheduled) {
      addOptionsButtonScheduled = true;
      setTimeout(async () => {
        addOptionsButtonScheduled = false;
        if (!container || !document.body.contains(container)) {
          const allGamesLink = document.querySelector('a._1rwiby3._mdg8s6x[href="/games/category/all-games"]');
          if (allGamesLink) {
            await addOptionsButton();
          }
        }
      }, 200);
    }
  });

  allGamesLinkObserver.observe(parent, { childList: true, subtree: true });
}

  // --- Filtering Games by Provider ---
  function filterGamesByProvider() {
    const games = getAllGameElements();
    games.forEach(gameDiv => {
      const gameLink = gameDiv.querySelector(GAME_LINK_SELECTOR);
      if (!gameLink) return;

      const path = getGamePath(gameLink.href);
const provider = (providerData[path] && providerData[path].provider) || '';

      if (selectedProvider === '' || provider === selectedProvider) {
        gameDiv.style.display = '';
      } else {
        gameDiv.style.display = 'none';
      }
    });
  }

  // --- Update provider dropdown counts ---
  function updateProviderDropdownCounts() {
    const games = getAllGameElements();
    const counts = {};

    games.forEach(gameDiv => {
      const gameLink = gameDiv.querySelector(GAME_LINK_SELECTOR);
      if (!gameLink) return;

      const path = getGamePath(gameLink.href);
      const provider = (providerData[path] && providerData[path].provider) || 'Unknown';

      counts[provider] = (counts[provider] || 0) + 1;
    });

    // Update default option with total games count
    if (providerFilterSelect && providerFilterSelect.options.length) {
      providerFilterSelect.options[0].textContent = `Select Provider (All Games: ${games.length})`;
      for (let i = 1; i < providerFilterSelect.options.length; i++) {
        const opt = providerFilterSelect.options[i];
        const provider = opt.value;
        const count = counts[provider] || 0;
        opt.textContent = `${provider} (${count})`;
      }
    }
  }

  // --- Update Random Button Text ---
  function updateRandomButtonText() {
    if (!randomBtn || !randomFavoriteBtn) return;

    const totalGames = getAllGameElements().filter(gameDiv => {
      if (gameDiv.style.display === 'none') return false;
      return true;
    }).length;

    const favoriteGamesCount = Object.values(playAgainFeedback)
      .filter(val => val === 'yes')
      .reduce((acc, val) => acc + 1, 0);

    if (selectedProvider) {
      const providerGamesCount = getAllGameElements().filter(gameDiv => {
        if (gameDiv.style.display === 'none') return false;
        return true;
      }).length;


      // Favorite count for selected provider:
      const favCountForProvider = Object.entries(playAgainFeedback).filter(([gameKey, val]) => {
        if (val !== 'yes') return false;
        if (!providerData[gameKey]) return false;
        return providerData[gameKey] && providerData[gameKey].provider === selectedProvider;
      }).length;

      randomBtn.textContent = `Random Game (${selectedProvider}) (${providerGamesCount})`;
      randomFavoriteBtn.textContent = `Random Favorite (${selectedProvider}) (${favCountForProvider})`;
    } else {
      randomBtn.textContent = `Random Game (All) (${totalGames})`;
      randomFavoriteBtn.textContent = `Random Favorite (All) (${favoriteGamesCount})`;
    }
  }
function updateScanButtonText() {
  if (!scanBtn) return;

  const totalGames = getAllGameElements().filter(div => div.style.display !== 'none').length;
  const savedGamesCount = Object.keys(providerData).length;

  if (savedGamesCount === 0) {
    scanBtn.textContent = 'Full Scan';
  } else if (savedGamesCount < totalGames) {
    scanBtn.textContent = 'Update Required';
  } else {
    scanBtn.textContent = 'Up to Date';
  }
}

  // --- Pick Random Game ---
  function pickRandomGame() {
    const games = getAllGameElements().filter(gameDiv => {
      if (gameDiv.style.display === 'none') return false;
      const link = gameDiv.querySelector(GAME_LINK_SELECTOR);
      if (!link) return false;
      const path = getGamePath(link.href);
      if (playedGames[path]) return false; // Skip played
      return true;
    });

    if (games.length === 0) {
      alert('No available games to pick.');
      return;
    }

    const choice = games[Math.floor(Math.random() * games.length)];
    const gameLink = choice.querySelector(GAME_LINK_SELECTOR);
    if (gameLink) {
      window.open(gameLink.href, '_blank');
      const path = getGamePath(gameLink.href);
      playedGames[path] = true;
      saveData();
      filterGamesByProvider();
      updateRandomButtonText();
      updateScanButtonText();
      const savedTitle = (providerData[path] && providerData[path].title) || '';
      showPlayAgainPrompt(path, prettifyTitle(savedTitle));
    }
  }

  // --- Pick Random Favorite Game (only "yes" feedback) ---
  function pickRandomFavoriteGame() {
    // Filter all games user marked "yes"
    const yesGames = Object.entries(playAgainFeedback)
      .filter(([gameKey, val]) => val === 'yes')
      .filter(([gameKey]) => {
        if (selectedProvider) {
          if (!providerData[gameKey]) return false;
          return providerData[gameKey]?.provider === selectedProvider;
        }
        return true;
      });

    if (yesGames.length === 0) {
      alert('No favorite games available to pick.');
      return;
    }

    const [gameKey] = yesGames[Math.floor(Math.random() * yesGames.length)];

    // Find the link element for this gameKey
    const gameDiv = getAllGameElements().find(gameDiv => {
      const link = gameDiv.querySelector(GAME_LINK_SELECTOR);
      if (!link) return false;
      const path = getGamePath(link.href);
      return path === gameKey;
    });

    if (!gameDiv) {
      alert('Game link not found for the selected favorite game.');
      return;
    }

    const link = gameDiv.querySelector(GAME_LINK_SELECTOR);
    if (link) {
      window.open(link.href, '_blank');
      playedGames[gameKey] = true;
      saveData();
      filterGamesByProvider();
      updateRandomButtonText();
        updateScanButtonText();
      const savedTitle = (providerData[gameKey] && providerData[gameKey].title) || '';

    }
  }

  // --- Show Play Again Prompt ---
function showPlayAgainPrompt(gameKey, gameTitle) {
  // If already answered "yes" before, don't show prompt again
  if (playAgainFeedback[gameKey] === '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[gameKey] = 'yes';
    saveData();
    closePrompt();
  };
  buttonsDiv.appendChild(yesBtn);

  // Maybe button
  const maybeBtn = document.createElement('button');
  maybeBtn.textContent = 'Maybe';
  styleButton(maybeBtn, '#ffc107', '#d39e00');
  maybeBtn.onclick = () => {
    playAgainFeedback[gameKey] = 'maybe';
    saveData();
    closePrompt();
  };
  buttonsDiv.appendChild(maybeBtn);

  // No button
  const noBtn = document.createElement('button');
  noBtn.textContent = 'No';
  styleButton(noBtn, '#dc3545', '#a71d2a');
  noBtn.onclick = () => {
    playAgainFeedback[gameKey] = 'no';
    saveData();
    closePrompt();
  };
  buttonsDiv.appendChild(noBtn);

  panel.appendChild(buttonsDiv);
  overlay.appendChild(panel);
  document.body.appendChild(overlay);

function closePrompt() {
  document.body.removeChild(overlay);
  updateRandomButtonText();
  updateScanButtonText();
}
}
function updateScanButtonText() {
  if (!scanBtn) return;

  const visibleGamesCount = getAllGameElements().filter(div => div.style.display !== 'none').length;
  const savedGamesCount = Object.keys(providerData).length;

  if (savedGamesCount === 0) {
    scanBtn.textContent = 'Full Scan';
  } else if (savedGamesCount < visibleGamesCount) {
    scanBtn.textContent = 'Update Required';
  } else {
    scanBtn.textContent = 'Scan (Up to Date)';
  }
}

  // --- Scan Providers ---
  async function scanProviders() {
    if (scanning) return;
    scanning = true;
    scanCancelRequested = false;
    scanBtn.style.display = 'none';
    cancelScanBtn.style.display = '';
    scanProgressText.textContent = 'Starting scan...';

    try {
      const gameDivs = getAllGameElements();
      const total = gameDivs.length;

      for (let i = scanProgress.index || 0; i < total; i++) {
        if (scanCancelRequested) {
          scanProgress.index = i;
          scanProgress.completed = false;
          saveData();
          scanProgressText.textContent = 'Scan cancelled.';
          scanning = false;
          scanBtn.style.display = '';
          cancelScanBtn.style.display = 'none';
          return;
        }

        const gameDiv = gameDivs[i];
        const infoBtn = gameDiv.querySelector(INFO_BTN_SELECTOR);
        if (!infoBtn) {
          // Fallback: skip if no info button
          scanProgressText.textContent = `Skipping game ${i + 1} (no info icon)`;
          await wait(300);
          continue;
        }

        scanProgressText.textContent = `Scanning game ${i + 1} / ${total}...`;
        infoBtn.click();

        // Wait for overlay with title h4._1dujhhk
        try {
          const titleEl = await waitForElement('h4._1dujhhk', 5000);
          await wait(100); // Additional wait for provider to appear

          // Provider usually in a <li> containing "Game Provider - <provider>"
          let providerName = '';
          const lis = [...document.querySelectorAll('li')];
          for (const li of lis) {
            if (li.textContent.trim().startsWith('Game Provider -')) {
              providerName = li.textContent.replace('Game Provider -', '').trim();
              break;
            }
          }

if (providerName) {
  const normalized = normalizeProvider(providerName);
  const gameLink = gameDiv.querySelector(GAME_LINK_SELECTOR);
  const titleEl = document.querySelector('h4._1dujhhk');
  const gameTitle = titleEl ? titleEl.textContent.trim() : '';
  if (gameLink) {
    const path = getGamePath(gameLink.href);
    providerData[path] = { provider: normalized, title: gameTitle };
  }
}

        } catch {
          // Timeout or missing elements, skip
        }

        // Close overlay
        const closeBtn = document.querySelector(CLOSE_OVERLAY_SELECTOR);
        if (closeBtn) closeBtn.click();

        await wait(100);
        scanProgress.index = i + 1;
        saveData();
      }

      scanProgress.completed = true;
      scanProgress.index = 0;
      saveData();
      scanProgressText.textContent = 'Scan completed refresh page.';
      updateProviderDropdownCounts();
      filterGamesByProvider();
      updateRandomButtonText();
      updateScanButtonText();

    } catch (e) {
      scanProgressText.textContent = 'Scan error: ' + e.message;
    } finally {
      scanning = false;
      scanBtn.style.display = '';
      cancelScanBtn.style.display = 'none';
    }
  }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址